choria-colt 0.1.1 → 0.4.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: 87713c8d6906919afa9b79624fc72f0f353ecd276f4eac20f5c02466722cd4e6
4
- data.tar.gz: b995ac965550cc9f9e244b26e2cf7d0a64dce8ab11fac81528cb8673658b845f
3
+ metadata.gz: 8d2f9c64ee41ebc49a88deff71b470baab7c4e83b921102518969fe2a35b77f1
4
+ data.tar.gz: 8f69562d201ff4addc3b44aa693d508ceca1a1623455d2ac57b273e59cd300f1
5
5
  SHA512:
6
- metadata.gz: cdaa5c4f6250950ecbc674fd5d6ff03c72075b9e7b4c20dcb4f5a566ca5db3a7f8688e8c13d146e83a232349ddeefb05a2251cbe2603efe8013f35b8bebda885
7
- data.tar.gz: 1c8ea277b331ae9f581cff8056da211b0ed8cb8c72421cd0d12043f096186702b802b661b4fc33228727c6b77a640c2f5686ba6a433295733c8c3b28d92b11a9
6
+ metadata.gz: 8fd9cc5e888843d6c8954fe7d2543b4496fe861354162f499831f4ebe7cc8ccd3def73604c6d44e6f83273f71eb8364fc1b01c99581c16e8b2635adbee071aea
7
+ data.tar.gz: 5a912066de4c960ffc85debf3cc53a678ac3312f2e75daec0d999f50af7f24215f1347b6c4984de55db9f3bb357075486310ab11db685e8f866c61a06049940f
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,12 @@ AllCops:
8
10
  Metrics/AbcSize:
9
11
  Max: 20
10
12
 
13
+ Metrics/ClassLength:
14
+ Exclude:
15
+ - 'lib/choria/colt/cli.rb'
16
+
11
17
  Metrics/MethodLength:
12
- Max: 17
18
+ Max: 20
13
19
 
14
20
  Layout/LineLength:
15
21
  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-07 15:43:34 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.
11
+ Metrics/CyclomaticComplexity:
12
+ Max: 9
13
+
14
+ # Offense count: 1
15
+ # Configuration parameters: IgnoredMethods.
16
+ Metrics/PerceivedComplexity:
17
+ Max: 9
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,67 @@
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
+ attr_reader :pastel
9
+
10
+ def initialize(colored:)
11
+ @pastel = Pastel.new(enabled: colored)
12
+ pastel.alias_color(:host, :cyan)
13
+ end
14
+
15
+ def process_result(result)
16
+ return process_error(result) unless result.dig(:data, :exitcode)&.zero?
17
+
18
+ process_success(result)
19
+ end
20
+
21
+ def process_success(result)
22
+ output_lines = [
23
+ "#{pastel.host(result[:sender]).ljust(60, ' ')}duration: #{pastel.bright_white format('%.2fs', result[:data][:runtime])}",
24
+ ]
25
+
26
+ output_lines += if result.dig(:result, :_output).nil?
27
+ JSON.pretty_generate(result[:result]).split("\n").map { |line| " #{line}" }
28
+ else
29
+ result.dig(:result, :_output).map { |line| " #{line}" }
30
+ end
31
+
32
+ output_lines.join("\n")
33
+ end
34
+
35
+ def process_error(result) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
+ host = "#{pastel.bright_red '⨯'} #{pastel.host(result[:sender]).ljust(60, ' ')}duration: #{pastel.bright_white format('%.2fs', result[:data][:runtime])}"
37
+ output = result.dig(:result, '_output')
38
+ error_details = JSON.pretty_generate(result.dig(:result, :_error, :details)).split "\n"
39
+ error_description = [
40
+ "#{pastel.bright_red result.dig(:result, :_error, :kind)}: #{pastel.bright_white result.dig(:result, :_error, :msg)}",
41
+ " details: #{error_details.shift}",
42
+ error_details.map { |line| " #{line}" },
43
+ ]
44
+ output_description = if output.nil? || output.empty?
45
+ []
46
+ else
47
+ [
48
+ nil,
49
+ pastel.bright_red('output:'),
50
+ output,
51
+ ]
52
+ end
53
+
54
+ headline = "#{pastel.on_red ' '} "
55
+
56
+ [
57
+ host,
58
+ [
59
+ error_description,
60
+ output_description,
61
+ ].flatten.map { |line| "#{headline}#{line}" },
62
+ ].flatten.join("\n")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ 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,21 +21,30 @@ 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)
24
30
 
25
31
  raise Thor::Error, 'Task name is required' if args.empty?
26
32
  raise Thor::Error, "Too many arguments: #{args}" unless args.count == 1
27
33
 
34
+ raise Thor::Error, 'Flag --targets or --targets-with-class is required' if options['targets'].nil? && options['targets_with_classes'].nil?
35
+
28
36
  task_name = args.shift
29
37
 
30
- targets = options['targets'].split ','
38
+ targets = options['targets']&.split(',')
31
39
  targets = nil if options['targets'] == 'all'
32
40
 
33
- results = colt.run_bolt_task task_name, input: input, targets: targets
34
- $stdout.puts JSON.pretty_generate(results)
41
+ targets_with_classes = options['targets_with_classes']&.split(',')
42
+
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
46
+
47
+ File.write 'last_run.json', JSON.pretty_generate(results)
35
48
  rescue Choria::Orchestrator::Error => e
36
49
  raise Thor::Error, "#{e.class}: #{e}"
37
50
  end
@@ -54,21 +67,50 @@ module Choria
54
67
  environment = options['environment']
55
68
  cache_directory = File.expand_path('.cache/colt/tasks')
56
69
  FileUtils.mkdir_p cache_directory
57
- # TODO: Support multiple infrastructure
58
70
  cache = Cache.new(path: File.join(cache_directory, "#{environment}.yaml"))
59
71
 
60
72
  tasks = colt.tasks(environment: environment, cache: cache)
61
73
 
62
- if tasks_names.nil?
63
- show_tasks_summary
74
+ if tasks_names.empty?
75
+ show_tasks_summary(tasks)
64
76
  else
65
77
  tasks_names.each { |task_name| show_task_details(task_name, tasks) }
66
78
  end
67
79
  end
68
80
 
69
- 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 => e
94
+ raise Thor::Error, "#{e.class}: #{e}"
95
+ end
96
+
97
+ no_commands do # rubocop:disable Metrics/BlockLength
70
98
  def colt
71
- @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?)
72
114
  end
73
115
 
74
116
  def extract_task_parameters_from_args(args)
@@ -76,7 +118,14 @@ module Choria
76
118
  args.reject! { |arg| arg =~ /^\w+=/ }
77
119
 
78
120
  parameters.map do |parameter|
79
- 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
+
80
129
  [key, value]
81
130
  end.to_h
82
131
  end
@@ -85,7 +134,7 @@ module Choria
85
134
  tasks.reject! { |_task, metadata| metadata['metadata']['private'] }
86
135
 
87
136
  puts <<~OUTPUT
88
- Tasks
137
+ #{pastel.title 'Tasks'}
89
138
  #{tasks.map { |task, metadata| "#{task}#{' ' * (60 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, ' ')}
90
139
  OUTPUT
91
140
  end
@@ -93,12 +142,36 @@ module Choria
93
142
  def show_task_details(task_name, tasks)
94
143
  metadata = tasks[task_name]
95
144
  puts <<~OUTPUT
96
- Task: '#{task_name}'
145
+ #{pastel.title "Task: #{task_name}"}
97
146
  #{metadata['metadata']['description']}
98
147
 
99
- #{metadata}
148
+ #{pastel.title 'Parameters'}
149
+ #{format_task_parameters(metadata['metadata']['parameters']).gsub(/^/, ' ')}
100
150
  OUTPUT
101
151
  end
152
+
153
+ def format_task_parameters(parameters)
154
+ parameters.map do |parameter, metadata|
155
+ output = <<~OUTPUT
156
+ #{pastel.parameter(parameter)} #{pastel.parameter_type metadata['type']}
157
+ #{metadata['description']}
158
+ OUTPUT
159
+ output += " Default: #{metadata['default']}" unless metadata['default'].nil?
160
+ output
161
+ end.join "\n"
162
+ end
163
+
164
+ def pastel
165
+ @pastel ||= _pastel
166
+ end
167
+
168
+ def _pastel
169
+ pastel = Pastel.new(enabled: $stdout.tty?)
170
+ pastel.alias_color(:title, :cyan)
171
+ pastel.alias_color(:parameter, :yellow)
172
+ pastel.alias_color(:parameter_type, :bright_white)
173
+ pastel
174
+ end
102
175
  end
103
176
  end
104
177
 
@@ -0,0 +1,26 @@
1
+ module Choria
2
+ class Colt
3
+ module DataStructurer
4
+ def self.structure(res) # rubocop:disable Metrics/AbcSize
5
+ # data.stdout seems to always be JSON, so parse it once.
6
+ res[:result] = JSON.parse res[:data][:stdout]
7
+ res[:data].delete :stdout
8
+
9
+ # On one side, data.stderr is filled by the remote execution stderr.
10
+ # On the other side, error description is in JSON (ie. '_error')
11
+ # So merge data.stderr in '_error'.'details'
12
+ unless res[:data][:stderr].empty?
13
+ raise NotImplementedError, 'What to do when res[:data][:stderr] contains something?' if res[:result]['_error'].empty?
14
+
15
+ res[:result]['_error']['details'].merge!({ 'stderr' => res[:data][:stderr].split("\n") })
16
+ end
17
+ res[:data].delete :stderr
18
+
19
+ # Convert '_output' (ie. stdout) lines into array
20
+ res[:result]['_output'] = res[:result]['_output'].split("\n") unless res[:result]['_output'].nil?
21
+
22
+ res
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Choria
4
4
  class Colt
5
- VERSION = '0.1.1'
5
+ VERSION = '0.4.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
@@ -1,17 +1,27 @@
1
+ require 'choria/colt/data_structurer'
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
5
10
 
6
- attr_reader :name, :input, :environment, :rpc_results
11
+ attr_reader :id, :name, :input, :environment, :rpc_results, :results
7
12
  attr_accessor :rpc_responses
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
+ @results = []
14
20
 
21
+ return if @name.nil?
22
+
23
+ @input = default_input.merge input
24
+ logger.debug "Task inputs: #{input}"
15
25
  validate_inputs
16
26
  end
17
27
 
@@ -23,37 +33,95 @@ module Choria
23
33
  metadata['files'].to_json
24
34
  end
25
35
 
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
36
+ def wait # rubocop:disable Metrics/AbcSize
37
+ if @id.nil?
38
+ rpc_responses_ok, rpc_responses_error = rpc_responses.partition { |res| (res[:body][:statuscode]).zero? }
39
+ rpc_responses_error.each do |res|
40
+ logger.error "Task request failed on '#{res[:senderid]}':\n#{pp res}"
41
+ end
29
42
 
30
- @rpc_results = @orchestrator.wait_results task_id: task_ids.first
31
- end
43
+ task_ids = rpc_responses_ok.map { |res| res[:body][:data][:task_id] }.uniq
32
44
 
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?
45
+ raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
36
46
 
37
- res[:result] = JSON.parse res[:data][:stdout]
38
- res[:data].delete :stderr
39
- res[:data].delete :stdout
40
- res
47
+ @id = task_ids.first
41
48
  end
49
+
50
+ wait_results
51
+ end
52
+
53
+ def on_result(&block)
54
+ @on_result = lambda { |result|
55
+ block.call(result)
56
+ }
42
57
  end
43
58
 
44
59
  private
45
60
 
61
+ def rpc_results=(results)
62
+ new_result_hosts = (results.map { |res| res[:sender] }) - (@results.map { |res| res[:sender] })
63
+
64
+ new_result_hosts.each do |host|
65
+ result = results.find { |res| res[:sender] == host }
66
+
67
+ next unless result[:data][:exitcode] != -1
68
+
69
+ logger.debug "New result for task ##{@id}: #{result}"
70
+ structured_result = Choria::Colt::DataStructurer.structure(result).with_indifferent_access
71
+
72
+ @on_result&.call(structured_result)
73
+
74
+ @results << structured_result
75
+ end
76
+
77
+ @rpc_results = results
78
+ end
79
+
80
+ def wait_results
81
+ raise 'Task ID is required!' if @id.nil?
82
+
83
+ logger.info "Waiting task #{@id} results…"
84
+
85
+ @results = []
86
+ @rpc_results = []
87
+
88
+ loop do
89
+ self.rpc_results = @orchestrator.rpc_client.task_status(task_id: @id).map(&:results)
90
+
91
+ break if terminated?
92
+ end
93
+ end
94
+
95
+ def terminated?
96
+ @rpc_results.each do |result|
97
+ return false if result[:data][:exitcode] == -1
98
+ end
99
+
100
+ true
101
+ end
102
+
46
103
  def _metadata
47
- # puts 'Retrieving task metadata for task %s from the Puppet Server' % task if verbose
104
+ logger.info 'Downloading task metadata from the Puppet Server'
48
105
  @orchestrator.tasks_support.task_metadata(@name, @environment)
49
106
  rescue RuntimeError => e
50
107
  raise Error, e.message
51
108
  end
52
109
 
110
+ def default_input
111
+ parameters_with_defaults = metadata['metadata']['parameters'].reject { |_k, v| v['default'].nil? }
112
+ parameters_with_defaults.transform_values do |meta|
113
+ meta['default']
114
+ end
115
+ end
116
+
53
117
  def validate_inputs
54
118
  ok, reason = @orchestrator.tasks_support.validate_task_inputs(@input, metadata)
55
119
  raise Error, reason.sub(/^\n/, '') unless ok
56
120
  end
121
+
122
+ def logger
123
+ @orchestrator.logger
124
+ end
57
125
  end
58
126
  end
59
127
  end
@@ -21,21 +21,25 @@ module Choria
21
21
  @tasks_support ||= MCollective::Util::Choria.new.tasks_support
22
22
  end
23
23
 
24
- def run(task, targets: nil, verbose: false)
25
- logger.debug "Running task: '#{task.name}' (targets: #{targets.nil? ? 'all' : targets})"
24
+ def run(task, targets: nil, targets_with_classes: nil, verbose: false) # rubocop:disable Metrics/AbcSize
26
25
  rpc_client.progress = verbose
27
26
 
27
+ logger.debug "Running task: '#{task.name}' (targets: #{targets.nil? ? 'all' : targets})"
28
28
  targets&.each { |target| rpc_client.identity_filter target }
29
29
 
30
- raise DiscoverError, 'No request sent, no node discovered' if rpc_client.discover.size.zero?
30
+ unless targets_with_classes.nil?
31
+ logger.debug "Filtering targets with classes: #{targets_with_classes}"
32
+ targets_with_classes.each { |klass| rpc_client.class_filter klass }
33
+ end
31
34
 
32
- logger.info "Attempting to download and run task '#{task.name}' on #{rpc_client.discover.size} nodes"
35
+ logger.info 'Discovering targets…'
36
+ raise DiscoverError, 'No request sent, no node discovered' if rpc_client.discover.size.zero?
33
37
 
38
+ logger.info "Downloading task '#{task.name}' on #{rpc_client.discover.size} nodes…"
34
39
  rpc_client.download(task: task.name, files: task.files, verbose: verbose)
35
40
 
36
- # TODO: Extract error from 'rpc' (see MCollective::RPC#printrpc)
37
-
38
41
  responses = []
42
+ logger.info "Starting task '#{task.name}' on #{rpc_client.discover.size} nodes…"
39
43
  rpc_client.run_no_wait(task: task.name, files: task.files, input: task.input.to_json, verbose: verbose) do |response|
40
44
  logger.debug " Response: '#{response}'"
41
45
  responses << response
@@ -46,37 +50,16 @@ module Choria
46
50
  task.rpc_responses = responses
47
51
  end
48
52
 
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
53
  def validate_rpc_result(result)
71
54
  raise Error, "The RPC agent returned an error: #{result[:statusmsg]}" unless (result[:statuscode]).zero?
72
55
  end
73
56
 
74
- private
75
-
76
57
  def rpc_client
77
58
  @rpc_client ||= rpcclient('bolt_tasks', options: rpc_options)
78
59
  end
79
60
 
61
+ private
62
+
80
63
  def rpc_options
81
64
  {
82
65
  verbose: false,
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.1.1
4
+ version: 0.4.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-04-25 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,7 +212,9 @@ 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