choria-colt 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a899af183eb44fcd18e470166a824da35e0ab2ed46ab25c9f001c716442edfe
4
- data.tar.gz: 891b29051dacb1ca6e2fa801ae38f0800e8cdb7decc734590c3354bdb5d12dee
3
+ metadata.gz: fbb608f3080843c72765bb15bb6b375d1e3e02370c59b00b1d34fb9d5c02e57c
4
+ data.tar.gz: cc292639012cdde4ea87d35522f207e792e2727275d6c331bebfe7332948f905
5
5
  SHA512:
6
- metadata.gz: 98a116297f7f1d4f4384042f43d0fc90b07e85ea4546a799391271dc2c2b2b0602a4e400754f795ee1c499c027b0861de3d713fea2dc132d1d8a88d6bab2f9ab
7
- data.tar.gz: 51bd11e393b922ae49113fa6784fd24aeb31a592f093ba95b4de0e5a02973a004030233302ff401d58d01fbef411aa4c87ac98dd1a6e219c6219d61727c0181e
6
+ metadata.gz: e3ef3078f635f8223eb3e2fbc9d5d043cdb536f0acc34008f61c80a4efc5af760b23021bb1c023a3aeed402903c1f59e66628e78e7087b398cb42bd8bf62d97a
7
+ data.tar.gz: 5b05929aae9c9414c1af2daa68fe7f17d5c8da758282e0d30ec9e27e69ba919eb9e2ec4ab59469aa94d7c7244e45e2f8f3c5a6ee4a9f35eafcd7c09ba7b0a9ae
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml CHANGED
@@ -1,3 +1,5 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
1
3
  AllCops:
2
4
  TargetRubyVersion: 2.5
3
5
  Exclude:
@@ -8,8 +10,13 @@ AllCops:
8
10
  Metrics/AbcSize:
9
11
  Max: 20
10
12
 
13
+ Metrics/ClassLength:
14
+ Max: 120
15
+ Exclude:
16
+ - 'lib/choria/colt/cli.rb'
17
+
11
18
  Metrics/MethodLength:
12
- Max: 17
19
+ Max: 20
13
20
 
14
21
  Layout/LineLength:
15
22
  Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,17 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2022-04-28 09:34:37 UTC using RuboCop version 1.26.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
11
+ Metrics/AbcSize:
12
+ Max: 22
13
+
14
+ # Offense count: 1
15
+ # Configuration parameters: IgnoredMethods.
16
+ Metrics/CyclomaticComplexity:
17
+ Max: 8
data/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ ## [v0.4.0](https://github.com/opus-codium/choria-colt/tree/v0.4.0) (2022-04-25)
4
+
5
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.3.0...v0.4.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Spec: Fix tests after duration introduction [\#20](https://github.com/opus-codium/choria-colt/pull/20) ([neomilium](https://github.com/neomilium))
10
+ - CLI: Add `colt task status` subcommand [\#19](https://github.com/opus-codium/choria-colt/pull/19) ([neomilium](https://github.com/neomilium))
11
+
12
+ ## [v0.3.0](https://github.com/opus-codium/choria-colt/tree/v0.3.0) (2022-04-12)
13
+
14
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.2.0...v0.3.0)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - Improve CLI output [\#18](https://github.com/opus-codium/choria-colt/pull/18) ([neomilium](https://github.com/neomilium))
19
+ - CLI: Add an option to choose log level [\#17](https://github.com/opus-codium/choria-colt/pull/17) ([neomilium](https://github.com/neomilium))
20
+ - Improve stability, fix results retrieving [\#16](https://github.com/opus-codium/choria-colt/pull/16) ([neomilium](https://github.com/neomilium))
21
+ - CLI: Add an option to set log level [\#15](https://github.com/opus-codium/choria-colt/pull/15) ([neomilium](https://github.com/neomilium))
22
+ - Setup CI [\#14](https://github.com/opus-codium/choria-colt/pull/14) ([neomilium](https://github.com/neomilium))
23
+ - CLI: Format error [\#13](https://github.com/opus-codium/choria-colt/pull/13) ([neomilium](https://github.com/neomilium))
24
+ - CLI: Display default value if available for parameters [\#12](https://github.com/opus-codium/choria-colt/pull/12) ([neomilium](https://github.com/neomilium))
25
+ - Test CLI formatter using RSpec [\#11](https://github.com/opus-codium/choria-colt/pull/11) ([neomilium](https://github.com/neomilium))
26
+ - Colorize outputs [\#10](https://github.com/opus-codium/choria-colt/pull/10) ([neomilium](https://github.com/neomilium))
27
+ - Fix task input option parsing and task output display [\#9](https://github.com/opus-codium/choria-colt/pull/9) ([neomilium](https://github.com/neomilium))
28
+ - Implement continous result display [\#8](https://github.com/opus-codium/choria-colt/pull/8) ([neomilium](https://github.com/neomilium))
29
+ - Improve robustness on errors [\#7](https://github.com/opus-codium/choria-colt/pull/7) ([neomilium](https://github.com/neomilium))
30
+ - Minor code readability improvements [\#6](https://github.com/opus-codium/choria-colt/pull/6) ([neomilium](https://github.com/neomilium))
31
+ - Improve logging [\#5](https://github.com/opus-codium/choria-colt/pull/5) ([neomilium](https://github.com/neomilium))
32
+ - Force convertion to boolean if parameter is true/false [\#4](https://github.com/opus-codium/choria-colt/pull/4) ([neomilium](https://github.com/neomilium))
33
+ - Tasks: Autofill default values [\#3](https://github.com/opus-codium/choria-colt/pull/3) ([neomilium](https://github.com/neomilium))
34
+ - CLI: Support filter on Puppet classes [\#2](https://github.com/opus-codium/choria-colt/pull/2) ([neomilium](https://github.com/neomilium))
35
+ - Tasks: Structure data before return results [\#1](https://github.com/opus-codium/choria-colt/pull/1) ([neomilium](https://github.com/neomilium))
36
+
37
+ ## [v0.2.0](https://github.com/opus-codium/choria-colt/tree/v0.2.0) (2022-03-03)
38
+
39
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.1.1...v0.2.0)
40
+
41
+ ## [v0.1.1](https://github.com/opus-codium/choria-colt/tree/v0.1.1) (2022-03-03)
42
+
43
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.1.0...v0.1.1)
44
+
45
+
46
+
47
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
data/README.md CHANGED
@@ -47,7 +47,14 @@ Or install it yourself as:
47
47
 
48
48
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
49
49
 
50
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
+ To install this gem onto your local machine, run `bundle exec rake install`.
51
+
52
+
53
+ To release a new version:
54
+ 1. update the version number in `version.rb`
55
+ 1. generate CHANGELOG.md with `bundle exec rake changelog`
56
+ 1. commit changes
57
+ 1. run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
51
58
 
52
59
  ## Contributing
53
60
 
data/Rakefile CHANGED
@@ -3,6 +3,17 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rubocop/rake_task'
5
5
 
6
+ require 'choria/colt/version'
7
+
6
8
  RuboCop::RakeTask.new
7
9
 
10
+ require 'github_changelog_generator/task'
11
+
12
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
13
+ config.user = 'opus-codium'
14
+ config.project = 'choria-colt'
15
+ config.since_tag = 'v0.1.0'
16
+ config.future_release = "v#{Choria::Colt::VERSION}"
17
+ end
18
+
8
19
  task default: :rubocop
data/choria-colt.gemspec CHANGED
@@ -28,15 +28,21 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
+ spec.add_dependency 'activesupport'
31
32
  spec.add_dependency 'choria-mcorpc-support'
32
33
  spec.add_dependency 'deep_merge'
34
+ spec.add_dependency 'pastel'
33
35
  spec.add_dependency 'puppet'
34
36
  spec.add_dependency 'thor'
37
+ spec.add_dependency 'tty-logger'
35
38
 
39
+ # spec.add_development_dependency 'byebug'
40
+ spec.add_development_dependency 'github_changelog_generator'
36
41
  spec.add_development_dependency 'rake'
37
42
  spec.add_development_dependency 'rspec'
38
43
  spec.add_development_dependency 'rubocop'
39
- #spec.add_development_dependency 'byebug'
44
+ spec.add_development_dependency 'rubocop-rake'
45
+ spec.add_development_dependency 'rubocop-rspec'
40
46
 
41
47
  # For more information and examples about making a new gem, check out our
42
48
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,123 @@
1
+ require 'choria/colt/cli'
2
+ require 'choria/colt/cli/thor'
3
+
4
+ module Choria
5
+ class Colt
6
+ class CLI < Thor
7
+ class Formatter
8
+ module Result
9
+ def sender
10
+ self[:sender]
11
+ end
12
+
13
+ def exitcode
14
+ dig(:data, :exitcode)
15
+ end
16
+
17
+ def ok?
18
+ exitcode&.zero?
19
+ end
20
+
21
+ def runtime
22
+ dig(:data, :runtime)
23
+ end
24
+
25
+ # CLI
26
+ def output
27
+ if dig(:result, :_output).nil?
28
+ JSON.pretty_generate(self[:result]).split("\n")
29
+ else
30
+ dig(:result, :_output)
31
+ end
32
+ end
33
+ end
34
+
35
+ attr_reader :pastel
36
+
37
+ def initialize(colored:)
38
+ @pastel = Pastel.new(enabled: colored)
39
+ pastel.alias_color(:host, :cyan)
40
+ end
41
+
42
+ def process_result(result)
43
+ result.extend Formatter::Result
44
+
45
+ case result[:statuscode]
46
+ when 0
47
+ # 0 OK
48
+ process_success(result)
49
+ when 1
50
+ # 1 OK, failed. All the data parsed ok, we have a action matching the request but the requested action could not be completed. RPCAborted
51
+ process_error(result) # unless result.ok?
52
+ else
53
+ # 2 Unknown action UnknownRPCAction
54
+ # 3 Missing data MissingRPCData
55
+ # 4 Invalid data InvalidRPCData
56
+ # 5 Other error
57
+ process_rpc_error(result)
58
+ end
59
+ end
60
+
61
+ def process_success(result)
62
+ host = format_host(result, "#{pastel.bright_green '√'} ")
63
+ headline = "#{pastel.on_green ' '} "
64
+
65
+ [
66
+ host,
67
+ result.output.map { |line| "#{headline}#{line}" },
68
+ ].flatten.join("\n")
69
+ end
70
+
71
+ def process_error(result) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
72
+ host = format_host(result, "#{pastel.bright_red '⨯'} ")
73
+ output = result.dig(:result, '_output')
74
+ error_details = JSON.pretty_generate(result.dig(:result, :_error, :details)).split "\n"
75
+ error_description = [
76
+ "#{pastel.bright_red result.dig(:result, :_error, :kind)}: #{pastel.bright_white result.dig(:result, :_error, :msg)}",
77
+ " details: #{error_details.shift}",
78
+ error_details.map { |line| " #{line}" },
79
+ ]
80
+ output_description = if output.nil? || output.empty?
81
+ []
82
+ else
83
+ [
84
+ nil,
85
+ pastel.bright_red('output:'),
86
+ output,
87
+ ]
88
+ end
89
+
90
+ headline = "#{pastel.on_red ' '} "
91
+
92
+ [
93
+ host,
94
+ [
95
+ error_description,
96
+ output_description,
97
+ ].flatten.map { |line| "#{headline}#{line}" },
98
+ ].flatten.join("\n")
99
+ end
100
+
101
+ def process_rpc_error(result)
102
+ host = "#{pastel.bright_red '⨯'} #{pastel.host(result.sender)}"
103
+ headline = "#{pastel.on_red ' '} "
104
+
105
+ [
106
+ host,
107
+ "#{headline}#{pastel.bright_red "RPC error (#{result[:statuscode]})"}: #{pastel.bright_white result[:statusmsg]}",
108
+ ].join("\n")
109
+ end
110
+
111
+ private
112
+
113
+ def format_duration(result)
114
+ result.runtime.nil? ? '' : "duration: #{pastel.bright_white format('%.2fs', result.runtime)}"
115
+ end
116
+
117
+ def format_host(result, headline)
118
+ "#{headline}#{pastel.host(result.sender).ljust(60, ' ')}#{format_duration(result)}"
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,13 +1,17 @@
1
1
  require 'choria/colt'
2
+ require 'choria/colt/cli/formatter'
2
3
  require 'choria/colt/cli/thor'
3
4
 
4
5
  require 'json'
5
- require 'logger'
6
+ require 'tty/logger'
6
7
 
7
8
  module Choria
8
9
  class Colt
9
10
  class CLI < Thor
10
11
  class Tasks < Thor
12
+ class_option :log_level,
13
+ desc: 'Set log level for CLI',
14
+ default: 'info'
11
15
  # BOLT: desc 'run <task name> [parameters] {--targets TARGETS | --query QUERY | --rerun FILTER} [options]', 'Run a Bolt task'
12
16
  desc 'run <task name> [parameters] --targets TARGETS [options]', 'Run a Bolt task'
13
17
  long_desc <<~DESC
@@ -17,26 +21,32 @@ module Choria
17
21
  DESC
18
22
  option :targets,
19
23
  aliases: ['--target', '-t'],
20
- desc: 'Identifies the targets of the command.',
21
- required: true
22
- def run(*args)
24
+ desc: 'Identifies the targets of the command.'
25
+ option :targets_with_classes,
26
+ aliases: ['--targets-with-class', '-C'],
27
+ desc: 'Select the targets which have the specified Puppet classes.'
28
+ def run(*args) # rubocop:disable Metrics/AbcSize
23
29
  input = extract_task_parameters_from_args(args)
30
+ targets = extract_targets_from_options
24
31
 
25
32
  raise Thor::Error, 'Task name is required' if args.empty?
26
33
  raise Thor::Error, "Too many arguments: #{args}" unless args.count == 1
27
34
 
35
+ raise Thor::Error, 'Flag --targets or --targets-with-class is required' if options['targets'].nil? && options['targets_with_classes'].nil?
36
+
28
37
  task_name = args.shift
29
38
 
30
- targets = options['targets'].split ','
31
- targets = nil if options['targets'] == 'all'
39
+ logger.debug "Targets: #{targets}"
32
40
 
33
- results = colt.run_bolt_task task_name, input: input, targets: targets
41
+ targets_with_classes = options['targets_with_classes']&.split(',')
34
42
 
35
- File.write 'last_run.json', JSON.pretty_generate(results)
43
+ results = colt.run_bolt_task task_name, input: input, targets: targets, targets_with_classes: targets_with_classes do |result|
44
+ $stdout.puts formatter.process_result(result)
45
+ end
36
46
 
37
- show_results(results)
38
- rescue Choria::Orchestrator::Error => e
39
- raise Thor::Error, "#{e.class}: #{e}"
47
+ File.write 'last_run.json', JSON.pretty_generate(results)
48
+ rescue Choria::Orchestrator::Error
49
+ # This error is already logged and displayed.
40
50
  end
41
51
 
42
52
  desc 'show [task name] [options]', 'Show available tasks and task documentation'
@@ -68,9 +78,39 @@ module Choria
68
78
  end
69
79
  end
70
80
 
71
- no_commands do
81
+ desc 'status <task id>', 'Show task results'
82
+ long_desc <<~DESC
83
+ Show results from a previously ran task.
84
+
85
+ A task ID is required to request Choria services and retrieve results.
86
+ DESC
87
+ def status(task_id)
88
+ results = colt.wait_bolt_task task_id do |result|
89
+ $stdout.puts formatter.process_result(result)
90
+ end
91
+
92
+ File.write 'last_run.json', JSON.pretty_generate(results)
93
+ rescue Choria::Orchestrator::Error
94
+ # This error is already logged and displayed.
95
+ end
96
+
97
+ no_commands do # rubocop:disable Metrics/BlockLength
72
98
  def colt
73
- @colt ||= Choria::Colt.new logger: Logger.new($stdout)
99
+ @colt ||= Choria::Colt.new logger: logger
100
+ end
101
+
102
+ def logger
103
+ @logger ||= TTY::Logger.new do |config|
104
+ config.handlers = [
105
+ [:console, { output: $stderr, level: options['log_level'].to_sym }],
106
+ [:stream, { output: File.open('colt-debug.log', 'a'), level: :debug }],
107
+ ]
108
+ config.metadata = %i[date time]
109
+ end
110
+ end
111
+
112
+ def formatter
113
+ @formatter ||= Formatter.new(colored: $stdout.tty?)
74
114
  end
75
115
 
76
116
  def extract_task_parameters_from_args(args)
@@ -78,16 +118,33 @@ module Choria
78
118
  args.reject! { |arg| arg =~ /^\w+=/ }
79
119
 
80
120
  parameters.map do |parameter|
81
- key, value = parameter.split('=')
121
+ key, value = parameter.split('=', 2)
122
+
123
+ # TODO: Convert to boolean only if the expected type of parameter is boolean
124
+ # TODO: Support String to integer convertion
125
+ # TODO: Support @notation from parameter and/or whole input
126
+ value = true if value == 'true'
127
+ value = false if value == 'false'
128
+
82
129
  [key, value]
83
130
  end.to_h
84
131
  end
85
132
 
133
+ def extract_targets_from_options
134
+ return nil if options['targets'] == 'all'
135
+
136
+ targets = options['targets']&.split(',')
137
+ targets&.map do |t|
138
+ t = File.read(t.sub(/^@/, '')).lines.map(&:strip) if t.start_with? '@'
139
+ t
140
+ end&.flatten
141
+ end
142
+
86
143
  def show_tasks_summary(tasks)
87
144
  tasks.reject! { |_task, metadata| metadata['metadata']['private'] }
88
145
 
89
146
  puts <<~OUTPUT
90
- Tasks
147
+ #{pastel.title 'Tasks'}
91
148
  #{tasks.map { |task, metadata| "#{task}#{' ' * (60 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, ' ')}
92
149
  OUTPUT
93
150
  end
@@ -95,30 +152,35 @@ module Choria
95
152
  def show_task_details(task_name, tasks)
96
153
  metadata = tasks[task_name]
97
154
  puts <<~OUTPUT
98
- Task: '#{task_name}'
155
+ #{pastel.title "Task: #{task_name}"}
99
156
  #{metadata['metadata']['description']}
100
157
 
101
- Parameters:
102
- #{JSON.pretty_generate(metadata['metadata']['parameters']).gsub(/^/, ' ')}
158
+ #{pastel.title 'Parameters'}
159
+ #{format_task_parameters(metadata['metadata']['parameters']).gsub(/^/, ' ')}
103
160
  OUTPUT
104
161
  end
105
162
 
106
- def show_results(results)
107
- results.each { |result| show_result(result) }
163
+ def format_task_parameters(parameters)
164
+ parameters.map do |parameter, metadata|
165
+ output = <<~OUTPUT
166
+ #{pastel.parameter(parameter)} #{pastel.parameter_type metadata['type']}
167
+ #{metadata['description']}
168
+ OUTPUT
169
+ output += " Default: #{metadata['default']}" unless metadata['default'].nil?
170
+ output
171
+ end.join "\n"
108
172
  end
109
173
 
110
- def show_result(result)
111
- return show_generic_output(result) unless result.dig(:result, '_output').nil? || (result.dig(:result, 'exit_code') != 0)
112
-
113
- $stdout.puts JSON.pretty_generate(result)
174
+ def pastel
175
+ @pastel ||= _pastel
114
176
  end
115
177
 
116
- def show_generic_output(result)
117
- target = result[:sender]
118
-
119
- output = result.dig(:result, '_output')
120
- $stdout.puts "'#{target}':"
121
- output.split("\n").each { |line| $stdout.puts(" #{line}") }
178
+ def _pastel
179
+ pastel = Pastel.new(enabled: $stdout.tty?)
180
+ pastel.alias_color(:title, :cyan)
181
+ pastel.alias_color(:parameter, :yellow)
182
+ pastel.alias_color(:parameter_type, :bright_white)
183
+ pastel
122
184
  end
123
185
  end
124
186
  end
@@ -0,0 +1,28 @@
1
+ module Choria
2
+ class Colt
3
+ module DataStructurer
4
+ def self.structure(res) # rubocop:disable Metrics/AbcSize
5
+ return res unless [0, 1].include? res[:statuscode]
6
+
7
+ # data.stdout seems to always be JSON, so parse it once.
8
+ res[:result] = JSON.parse res[:data][:stdout]
9
+ res[:data].delete :stdout
10
+
11
+ # On one side, data.stderr is filled by the remote execution stderr.
12
+ # On the other side, error description is in JSON (ie. '_error')
13
+ # So merge data.stderr in '_error'.'details'
14
+ unless res[:data][:stderr].empty?
15
+ raise NotImplementedError, 'What to do when res[:data][:stderr] contains something?' if res[:result]['_error'].empty?
16
+
17
+ res[:result]['_error']['details'].merge!({ 'stderr' => res[:data][:stderr].split("\n") })
18
+ end
19
+ res[:data].delete :stderr
20
+
21
+ # Convert '_output' (ie. stdout) lines into array
22
+ res[:result]['_output'] = res[:result]['_output'].split("\n") unless res[:result]['_output'].nil?
23
+
24
+ res
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Choria
4
4
  class Colt
5
- VERSION = '0.2.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
data/lib/choria/colt.rb CHANGED
@@ -19,11 +19,25 @@ module Choria
19
19
  @orchestrator = Choria::Orchestrator.new logger: @logger
20
20
  end
21
21
 
22
- def run_bolt_task(task_name, input: {}, targets: nil)
22
+ def run_bolt_task(task_name, input: {}, targets: nil, targets_with_classes: nil, &block)
23
23
  logger.debug "Instantiate task '#{task_name}' and validate input"
24
- task = Choria::Orchestrator::Task.new(task_name, input: input, orchestrator: orchestrator)
24
+ task = Choria::Orchestrator::Task.new(name: task_name, input: input, orchestrator: orchestrator)
25
+
26
+ task.on_result(&block) if block_given?
27
+
28
+ orchestrator.run(task, targets: targets, targets_with_classes: targets_with_classes)
29
+ task.wait
30
+ task.results
31
+ rescue Choria::Orchestrator::Error => e
32
+ logger.error e.message
33
+ raise
34
+ end
35
+
36
+ def wait_bolt_task(task_id, &block)
37
+ task = Choria::Orchestrator::Task.new(id: task_id, orchestrator: orchestrator)
38
+
39
+ task.on_result(&block) if block_given?
25
40
 
26
- orchestrator.run(task, targets: targets)
27
41
  task.wait
28
42
  task.results
29
43
  rescue Choria::Orchestrator::Error => e
@@ -36,14 +50,6 @@ module Choria
36
50
  task['name']
37
51
  end
38
52
 
39
- def tasks_metadata(tasks, environment)
40
- tasks.map do |task|
41
- logger.debug "Fetching metadata for task '#{task}' (environment: '#{environment}')"
42
- metadata = orchestrator.tasks_support.task_metadata(task, environment)
43
- [task, metadata]
44
- end.to_h
45
- end
46
-
47
53
  return tasks_metadata(tasks_names, environment) if cache.nil?
48
54
 
49
55
  cached_tasks = cache.load
@@ -54,5 +60,17 @@ module Choria
54
60
 
55
61
  updated_tasks
56
62
  end
63
+
64
+ private
65
+
66
+ def tasks_metadata(tasks, environment)
67
+ logger.info "Fetching metadata for tasks (environment: '#{environment}')"
68
+
69
+ tasks.map do |task|
70
+ logger.debug "Fetching metadata for task '#{task}' (environment: '#{environment}')"
71
+ metadata = orchestrator.tasks_support.task_metadata(task, environment)
72
+ [task, metadata]
73
+ end.to_h
74
+ end
57
75
  end
58
76
  end
@@ -0,0 +1,28 @@
1
+ require 'choria/colt/data_structurer'
2
+
3
+ module Choria
4
+ class Orchestrator
5
+ class Task
6
+ class ResultSet
7
+ attr_reader :results
8
+
9
+ def initialize(on_result:)
10
+ @results = []
11
+ @on_result = on_result
12
+ end
13
+
14
+ def integrate_rpc_error(rpc_error)
15
+ result = rpc_error[:body]
16
+ result[:sender] = rpc_error[:senderid]
17
+ integrate_result(result)
18
+ end
19
+
20
+ def integrate_result(result)
21
+ structured_result = Choria::Colt::DataStructurer.structure(result).with_indifferent_access
22
+ @results << structured_result
23
+ @on_result&.call(structured_result)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,17 +1,25 @@
1
+ require_relative 'task/result_set'
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
1
6
  module Choria
2
7
  class Orchestrator
3
8
  class Task
4
9
  class Error < Orchestrator::Error; end
10
+ class NoNodesLeftError < Error; end
5
11
 
6
- attr_reader :name, :input, :environment, :rpc_results
7
- attr_accessor :rpc_responses
12
+ attr_reader :id, :name, :input, :environment
8
13
 
9
- def initialize(name, orchestrator:, input: {}, environment: 'production')
14
+ def initialize(orchestrator:, id: nil, name: nil, input: {}, environment: 'production')
15
+ @id = id
10
16
  @name = name
11
- @input = input
12
17
  @environment = environment
13
18
  @orchestrator = orchestrator
19
+ return if @name.nil?
14
20
 
21
+ @input = default_input.merge input
22
+ logger.debug "Task inputs: #{input}"
15
23
  validate_inputs
16
24
  end
17
25
 
@@ -23,37 +31,110 @@ module Choria
23
31
  metadata['files'].to_json
24
32
  end
25
33
 
26
- def wait
27
- task_ids = rpc_responses.map { |res| res[:body][:data][:task_id] }.uniq
28
- raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
34
+ def results
35
+ result_set.results
36
+ end
37
+
38
+ def run
39
+ raise Error, 'Unable to run a task by ID' if name.nil?
29
40
 
30
- @rpc_results = @orchestrator.wait_results task_id: task_ids.first
41
+ @pending_targets = rpc_client.discover
42
+ _download
43
+ _run_no_wait
31
44
  end
32
45
 
33
- def results
34
- @rpc_results.map do |res|
35
- raise NotImplementedError, 'What to do when res[:data][:stderr] contains something?' unless res[:data][:stderr].empty?
46
+ def wait
47
+ raise Error, 'Task ID is required!' if @id.nil?
36
48
 
37
- res[:result] = JSON.parse res[:data][:stdout]
38
- res[:data].delete :stderr
39
- res[:data].delete :stdout
40
- res
49
+ logger.info "Waiting task #{@id} results…"
50
+ @rpc_results = []
51
+ loop do
52
+ self.rpc_results = rpc_client.task_status(task_id: @id).map(&:results)
53
+ break if @pending_targets.empty?
41
54
  end
42
55
  end
43
56
 
57
+ def on_result(&block)
58
+ @on_result = ->(result) { block.call(result) }
59
+ end
60
+
44
61
  private
45
62
 
63
+ def result_set
64
+ @result_set ||= ResultSet.new(on_result: @on_result)
65
+ end
66
+
67
+ def rpc_results=(results)
68
+ pending_results, completed_results = results.partition { |res| res[:data][:exitcode] == -1 }
69
+ @pending_targets ||= pending_results.map { |res| res[:sender] }
70
+
71
+ new_results = completed_results.select { |res| @pending_targets.include? res[:sender] }
72
+ new_results.each do |res|
73
+ logger.debug "New result for task ##{@id}: #{res}"
74
+ result_set.integrate_result(res)
75
+ @pending_targets.delete res[:sender]
76
+ end
77
+ end
78
+
79
+ def process_rpc_response(rpc_response)
80
+ rpc_response.extend Orchestrator::RpcResponse
81
+ logger.debug " RPC Response: '#{rpc_response}'"
82
+ return unless rpc_response.rpc_error?
83
+
84
+ @pending_targets.delete rpc_response.sender
85
+ result_set.integrate_rpc_error(rpc_response)
86
+ end
87
+
88
+ def _download
89
+ logger.info "Downloading task '#{name}' on #{rpc_client.discover.size} nodes…"
90
+ rpc_client.download(task: name, files: files, verbose: false) do |rpc_response|
91
+ process_rpc_response(rpc_response)
92
+ end
93
+
94
+ raise NoNodesLeftError, "No nodes left to continue after 'download' action" if @pending_targets.empty?
95
+ end
96
+
97
+ def _run_no_wait # rubocop:disable Metrics/AbcSize
98
+ logger.info "Starting task '#{name}' on #{rpc_client.discover.size} nodes…"
99
+ task_ids = []
100
+ rpc_client.run_no_wait(task: name, files: files, input: input.to_json, verbose: false) do |rpc_response|
101
+ process_rpc_response(rpc_response)
102
+ task_ids << rpc_response.task_id
103
+ end
104
+ raise NoNodesLeftError, "No nodes left to continue after 'run_no_wait' action" if @pending_targets.empty?
105
+
106
+ task_ids.uniq!
107
+ raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
108
+
109
+ @id = task_ids.first
110
+ end
111
+
46
112
  def _metadata
47
- # puts 'Retrieving task metadata for task %s from the Puppet Server' % task if verbose
113
+ logger.info 'Downloading task metadata from the Puppet Server'
48
114
  @orchestrator.tasks_support.task_metadata(@name, @environment)
49
115
  rescue RuntimeError => e
50
116
  raise Error, e.message
51
117
  end
52
118
 
119
+ def default_input
120
+ parameters_with_defaults = metadata['metadata']['parameters'].reject { |_k, v| v['default'].nil? }
121
+ parameters_with_defaults.transform_values do |meta|
122
+ meta['default']
123
+ end
124
+ end
125
+
53
126
  def validate_inputs
54
127
  ok, reason = @orchestrator.tasks_support.validate_task_inputs(@input, metadata)
55
128
  raise Error, reason.sub(/^\n/, '') unless ok
56
129
  end
130
+
131
+ def logger
132
+ @orchestrator.logger
133
+ end
134
+
135
+ def rpc_client
136
+ @orchestrator.rpc_client
137
+ end
57
138
  end
58
139
  end
59
140
  end
@@ -6,6 +6,24 @@ module Choria
6
6
  class Error < StandardError; end
7
7
  class DiscoverError < Error; end
8
8
 
9
+ module RpcResponse
10
+ def sender
11
+ self[:senderid]
12
+ end
13
+
14
+ def rpc_error?
15
+ !rpc_success?
16
+ end
17
+
18
+ def rpc_success?
19
+ [0, 1].include? self[:body][:statuscode]
20
+ end
21
+
22
+ def task_id
23
+ self[:body][:data][:task_id]
24
+ end
25
+ end
26
+
9
27
  include MCollective::RPC
10
28
 
11
29
  attr_reader :logger
@@ -21,80 +39,25 @@ module Choria
21
39
  @tasks_support ||= MCollective::Util::Choria.new.tasks_support
22
40
  end
23
41
 
24
- def run(task, targets: nil, verbose: false)
25
- logger.debug "Running task: '#{task.name}' (targets: #{targets.nil? ? 'all' : targets})"
42
+ def run(task, targets: nil, targets_with_classes: nil, verbose: false) # rubocop:disable Metrics/AbcSize
26
43
  rpc_client.progress = verbose
27
44
 
45
+ logger.debug "Running task: '#{task.name}' (targets: #{targets.nil? ? 'all' : targets})"
28
46
  targets&.each { |target| rpc_client.identity_filter target }
29
47
 
30
- raise DiscoverError, 'No request sent, no node discovered' if rpc_client.discover.size.zero?
31
-
32
- logger.info "Attempting to download and run task '#{task.name}' on #{rpc_client.discover.size} nodes"
33
-
34
- rpc_client.download(task: task.name, files: task.files, verbose: verbose)
35
-
36
- # TODO: Extract error from 'rpc' (see MCollective::RPC#printrpc)
37
-
38
- responses = []
39
- rpc_client.run_no_wait(task: task.name, files: task.files, input: task.input.to_json, verbose: verbose) do |response|
40
- logger.debug " Response: '#{response}'"
41
- responses << response
48
+ unless targets_with_classes.nil?
49
+ logger.debug "Filtering targets with classes: #{targets_with_classes}"
50
+ targets_with_classes.each { |klass| rpc_client.class_filter klass }
42
51
  end
43
52
 
44
- # TODO: Include stats in logs when logger will be available (see MCollective::RPC#printrpcstats)
53
+ logger.info 'Discovering targets…'
54
+ raise DiscoverError, 'No requests sent, no nodes discovered' if rpc_client.discover.size.zero?
45
55
 
46
- task.rpc_responses = responses
56
+ task.run
47
57
  end
48
58
 
49
- def wait_results(task_id:)
50
- raise 'Task ID is required!' if task_id.nil?
51
-
52
- task_status_results = nil
53
- loop do
54
- task_status_results = rpc_client.task_status(task_id: task_id).map(&:results)
55
- logger.debug "Task ##{task_id} status: #{task_status_results}"
56
- break if task_completed? task_status_results
57
- end
58
-
59
- task_status_results
60
- end
61
-
62
- def task_completed?(results)
63
- results.each do |result|
64
- return false unless result[:data][:completed]
65
- end
66
-
67
- true
68
- end
69
-
70
- def validate_rpc_result(result)
71
- raise Error, "The RPC agent returned an error: #{result[:statusmsg]}" unless (result[:statuscode]).zero?
72
- end
73
-
74
- private
75
-
76
59
  def rpc_client
77
- @rpc_client ||= rpcclient('bolt_tasks', options: rpc_options)
78
- end
79
-
80
- def rpc_options
81
- {
82
- verbose: false,
83
- disctimeout: nil,
84
- timeout: 5,
85
- config: '/etc/choria/client.conf',
86
- collective: 'mcollective',
87
- discovery_method: nil,
88
- discovery_options: [],
89
- filter: {
90
- 'fact' => [], 'cf_class' => [], 'agent' => [], 'identity' => [], 'compound' => []
91
- },
92
- progress_bar: false,
93
- mcollective_limit_targets: false,
94
- batch_size: nil,
95
- batch_sleep_time: 1,
96
- output_format: :json,
97
- }
60
+ @rpc_client ||= rpcclient('bolt_tasks', options: {})
98
61
  end
99
62
  end
100
63
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: choria-colt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romuald Conty
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-03 00:00:00.000000000 Z
11
+ date: 2022-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: choria-mcorpc-support
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pastel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: puppet
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +94,34 @@ dependencies:
66
94
  - - ">="
67
95
  - !ruby/object:Gem::Version
68
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: tty-logger
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: github_changelog_generator
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
69
125
  - !ruby/object:Gem::Dependency
70
126
  name: rake
71
127
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +164,34 @@ dependencies:
108
164
  - - ">="
109
165
  - !ruby/object:Gem::Version
110
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-rake
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop-rspec
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
111
195
  description: Colt eases the Bolt tasks run through Choria
112
196
  email:
113
197
  - romuald@opus-codium.fr
@@ -116,7 +200,10 @@ executables:
116
200
  extensions: []
117
201
  extra_rdoc_files: []
118
202
  files:
203
+ - ".rspec"
119
204
  - ".rubocop.yml"
205
+ - ".rubocop_todo.yml"
206
+ - CHANGELOG.md
120
207
  - Gemfile
121
208
  - README.md
122
209
  - Rakefile
@@ -125,10 +212,13 @@ files:
125
212
  - lib/choria/colt.rb
126
213
  - lib/choria/colt/cache.rb
127
214
  - lib/choria/colt/cli.rb
215
+ - lib/choria/colt/cli/formatter.rb
128
216
  - lib/choria/colt/cli/thor.rb
217
+ - lib/choria/colt/data_structurer.rb
129
218
  - lib/choria/colt/version.rb
130
219
  - lib/choria/orchestrator.rb
131
220
  - lib/choria/orchestrator/task.rb
221
+ - lib/choria/orchestrator/task/result_set.rb
132
222
  - sig/choria/colt.rbs
133
223
  homepage: https://github.com/opus-codium/choria-colt
134
224
  licenses: []