choria-colt 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e86ae8b9274433cf146d571d2a7466a2c7c131e826ec5e4a8df9540d37f7994d
4
- data.tar.gz: 9e4717f11b4f1f3f93c555818b9281e0118ac59a4bbf7b2573d6a85ed6e74fd9
3
+ metadata.gz: '09cf08b6443654efe090590c045225192ca703516fdea84794072e4b6a8e719c'
4
+ data.tar.gz: a163ba98c06e5ea4d49f6e89fd23d466984fa8bfe8a1e8e713c87710ae943b6d
5
5
  SHA512:
6
- metadata.gz: 3754982bc70a01d44fc4115c76693f67201d5a618d2f61b229d9b3408bea3ff2c2ac187ea5c0bd55fdbf8ca391c668f09086339ab366a85f93100c0ac1bd8973
7
- data.tar.gz: c1a1dd4eaf6287d28d09baa7f45ff7ab297ffff5a8bd78d519d9a814dfcab74512d6f300fe6696789d1687e1a9f04aa021f90531a98143e263fae4d3a8d53688
6
+ metadata.gz: 634b6d2839fe60f98e80180321cf5e6466485c4acee04438306c23c4619dd886a88217e4f4dddcd0fd4ccaa7cf0c883db10d88c8bf128f364409253e62f065c4
7
+ data.tar.gz: 8741a7293a4d5b80a74ac0a5900d9744d3a2b7f8fdd92978a70bed026d9b139607f3d9ed9f9f6a8229477b8e6fa83da6a60dc36d0786f627fba2ee5b5158fd56
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.8.1](https://github.com/opus-codium/choria-colt/tree/v0.8.1) (2024-10-29)
4
+
5
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.8.0...v0.8.1)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - CLI: Fix `colt tasks show` when using it with long task names [\#34](https://github.com/opus-codium/choria-colt/pull/34) ([neomilium](https://github.com/neomilium))
10
+
11
+ ## [v0.8.0](https://github.com/opus-codium/choria-colt/tree/v0.8.0) (2022-11-25)
12
+
13
+ [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.7.0...v0.8.0)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - CLI: Summarize task status results by default [\#33](https://github.com/opus-codium/choria-colt/pull/33) ([neomilium](https://github.com/neomilium))
18
+ - CLI: Always display stderr if available [\#32](https://github.com/opus-codium/choria-colt/pull/32) ([neomilium](https://github.com/neomilium))
19
+
3
20
  ## [v0.7.0](https://github.com/opus-codium/choria-colt/tree/v0.7.0) (2022-09-26)
4
21
 
5
22
  [Full Changelog](https://github.com/opus-codium/choria-colt/compare/v0.6.0...v0.7.0)
data/choria-colt.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency 'puppet'
36
36
  spec.add_dependency 'thor'
37
37
  spec.add_dependency 'tty-logger'
38
+ spec.add_dependency 'tty-progressbar'
38
39
 
39
40
  # spec.add_development_dependency 'byebug'
40
41
  spec.add_development_dependency 'github_changelog_generator'
@@ -22,6 +22,10 @@ module Choria
22
22
  dig(:data, :runtime)
23
23
  end
24
24
 
25
+ def statuscode
26
+ self[:statuscode]
27
+ end
28
+
25
29
  # CLI
26
30
  def output
27
31
  if dig(:result, :_output).nil?
@@ -30,92 +34,131 @@ module Choria
30
34
  dig(:result, :_output)
31
35
  end
32
36
  end
37
+
38
+ def stderr
39
+ dig(:result, :_stderr)
40
+ end
33
41
  end
34
42
 
35
- attr_reader :pastel
43
+ module FormattedResult
44
+ attr_accessor :pastel
36
45
 
37
- def initialize(colored:)
38
- @pastel = Pastel.new(enabled: colored)
39
- pastel.alias_color(:host, :cyan)
40
- end
46
+ def host
47
+ if statuscode.zero?
48
+ format_host pastel.bright_green('√ ').to_s
49
+ else
50
+ format_host pastel.bright_red('⨯ ').to_s
51
+ end
52
+ end
41
53
 
42
- def process_result(result)
43
- result.extend Formatter::Result
54
+ def content
55
+ case statuscode
56
+ when 0
57
+ # 0 OK
58
+ format_success
59
+ when 1
60
+ # 1 OK, failed. All the data parsed ok, we have a action matching the request but the requested action could not be completed. RPCAborted
61
+ format_error
62
+ else
63
+ # 2 Unknown action UnknownRPCAction
64
+ # 3 Missing data MissingRPCData
65
+ # 4 Invalid data InvalidRPCData
66
+ # 5 Other error
67
+ format_rpc_error
68
+ end
69
+ end
44
70
 
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)
71
+ def to_s
72
+ [
73
+ host,
74
+ content,
75
+ ].join("\n")
58
76
  end
59
- end
60
77
 
61
- def process_success(result)
62
- host = format_host(result, "#{pastel.bright_green '√'} ")
63
- headline = "#{pastel.on_green ' '} "
78
+ private
64
79
 
65
- [
66
- host,
67
- result.output.map { |line| "#{headline}#{line}" },
68
- ].flatten.join("\n")
69
- end
80
+ def stderr_description
81
+ if stderr.nil? || stderr.empty?
82
+ []
83
+ else
84
+ [
85
+ nil,
86
+ pastel.bright_red('stderr:'),
87
+ stderr,
88
+ ]
89
+ end
90
+ end
91
+
92
+ def output_description
93
+ if output.nil? || output.empty?
94
+ []
95
+ else
96
+ [
97
+ nil,
98
+ pastel.bright_red('output:'),
99
+ output,
100
+ ]
101
+ end
102
+ end
103
+
104
+ def format_duration
105
+ runtime.nil? ? '' : "duration: #{pastel.bright_white format('%.2fs', runtime)}"
106
+ end
107
+
108
+ def format_host(headline)
109
+ "#{headline}#{pastel.host(sender).ljust(60, ' ')}#{format_duration}".strip
110
+ end
111
+
112
+ def format_success
113
+ headline = "#{pastel.on_green ' '} "
114
+ warning_headline = "#{pastel.on_yellow ' '} "
70
115
 
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
116
  [
95
- error_description,
96
- output_description,
97
- ].flatten.map { |line| "#{headline}#{line}" },
98
- ].flatten.join("\n")
99
- end
117
+ output.map { |line| "#{headline}#{line}" },
118
+ stderr_description.flatten.map { |line| "#{warning_headline}#{line}" },
119
+ ].flatten.join("\n")
120
+ end
121
+
122
+ def format_error
123
+ error_details = JSON.pretty_generate(dig(:result, :_error, :details)).split "\n"
124
+ error_description = [
125
+ "#{pastel.bright_red dig(:result, :_error, :kind)}: #{pastel.bright_white dig(:result, :_error, :msg)}",
126
+ " details: #{error_details.shift}",
127
+ error_details.map { |line| " #{line}" },
128
+ ]
100
129
 
101
- def process_rpc_error(result)
102
- host = "#{pastel.bright_red '⨯'} #{pastel.host(result.sender)}"
103
- headline = "#{pastel.on_red ' '} "
130
+ headline = "#{pastel.on_red ' '} "
104
131
 
105
- [
106
- host,
107
- "#{headline}#{pastel.bright_red "RPC error (#{result[:statuscode]})"}: #{pastel.bright_white result[:statusmsg]}",
108
- ].join("\n")
132
+ [
133
+ [
134
+ error_description,
135
+ output_description,
136
+ stderr_description,
137
+ ].flatten.map { |line| "#{headline}#{line}" },
138
+ ].flatten.join("\n")
139
+ end
140
+
141
+ def format_rpc_error
142
+ headline = "#{pastel.on_red ' '} "
143
+
144
+ [
145
+ "#{headline}#{pastel.bright_red "RPC error (#{statuscode})"}: #{pastel.bright_white self[:statusmsg]}",
146
+ ].join("\n")
147
+ end
109
148
  end
110
149
 
111
- private
150
+ attr_reader :pastel
112
151
 
113
- def format_duration(result)
114
- result.runtime.nil? ? '' : "duration: #{pastel.bright_white format('%.2fs', result.runtime)}"
152
+ def initialize(colored:)
153
+ @pastel = Pastel.new(enabled: colored)
154
+ pastel.alias_color(:host, :cyan)
115
155
  end
116
156
 
117
- def format_host(result, headline)
118
- "#{headline}#{pastel.host(result.sender).ljust(60, ' ')}#{format_duration(result)}"
157
+ def format(result)
158
+ result.extend Formatter::Result
159
+ result.extend Formatter::FormattedResult
160
+ result.pastel = pastel
161
+ result
119
162
  end
120
163
  end
121
164
  end
@@ -44,7 +44,7 @@ module Choria
44
44
 
45
45
  environment = options['environment']
46
46
  results = colt.run_bolt_task task_name, input: input, targets: targets, targets_with_classes: targets_with_classes, environment: environment do |result|
47
- $stdout.puts formatter.process_result(result)
47
+ $stdout.puts formatter.format(result)
48
48
  end
49
49
 
50
50
  File.write 'last_run.json', JSON.pretty_generate(results)
@@ -86,12 +86,23 @@ module Choria
86
86
 
87
87
  A task ID is required to request Choria services and retrieve results.
88
88
  DESC
89
+ option :style,
90
+ aliases: ['-S'],
91
+ desc: "Output style; can be 'continuous' or 'summary'",
92
+ default: 'summary'
89
93
  define_targets_and_filters_options
90
94
  def status(task_id)
95
+ validate_style_option
96
+
91
97
  targets, targets_with_classes = extract_targets_and_filters_from_options
92
98
 
93
- results = colt.wait_bolt_task(task_id, targets: targets, targets_with_classes: targets_with_classes) do |result|
94
- $stdout.puts formatter.process_result(result)
99
+ case options['style']
100
+ when 'continous'
101
+ results = colt.wait_bolt_task(task_id, targets: targets, targets_with_classes: targets_with_classes) do |result|
102
+ $stdout.puts formatter.format(result)
103
+ end
104
+ when 'summary'
105
+ show_summarized_status(task_id, targets: targets, targets_with_classes: targets_with_classes)
95
106
  end
96
107
 
97
108
  File.write 'last_run.json', JSON.pretty_generate(results)
@@ -111,6 +122,7 @@ module Choria
111
122
  [:stream, { output: File.open('colt-debug.log', 'a'), level: :debug }],
112
123
  ]
113
124
  config.metadata = %i[date time]
125
+ Choria::Colt::Debugger.enabled = true
114
126
  end
115
127
  end
116
128
 
@@ -118,6 +130,11 @@ module Choria
118
130
  @formatter ||= Formatter.new(colored: $stdout.tty?)
119
131
  end
120
132
 
133
+ def validate_style_option
134
+ supported_styles = %i[summary continous]
135
+ raise Thor::Error, "Invalid style: '#{options['style']}' (available: #{supported_styles.map(&:to_s)})" unless supported_styles.include? options['style'].to_sym
136
+ end
137
+
121
138
  def extract_task_parameters_from_args(args)
122
139
  parameters = args.grep(/^\w+=/)
123
140
  args.reject! { |arg| arg =~ /^\w+=/ }
@@ -157,9 +174,12 @@ module Choria
157
174
  def show_tasks_summary(tasks)
158
175
  tasks.reject! { |_task, metadata| metadata['metadata']['private'] }
159
176
 
177
+ task_name_max_size = 0
178
+ tasks.each { |task, _metadata| task_name_max_size = [task_name_max_size, task.size].max }
179
+
160
180
  puts <<~OUTPUT
161
181
  #{pastel.title 'Tasks'}
162
- #{tasks.map { |task, metadata| "#{task}#{' ' * (60 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, ' ')}
182
+ #{tasks.map { |task, metadata| "#{task}#{' ' * (task_name_max_size + 4 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, ' ')}
163
183
  OUTPUT
164
184
  end
165
185
 
@@ -185,6 +205,33 @@ module Choria
185
205
  end.join "\n"
186
206
  end
187
207
 
208
+ def summarize(results)
209
+ results_grouped_by_result_content = results.group_by { |result| result[:result] }
210
+ results_grouped_by_result_content.each do |_content, grouped_results|
211
+ # Display each host
212
+ grouped_results.each do |result|
213
+ $stdout.puts formatter.format(result).host
214
+ end
215
+ # Display the result content
216
+ content = formatter.format(grouped_results.first).content
217
+ content = pastel.bright_white '(no output)' if content.nil? || content.empty?
218
+ $stdout.puts content
219
+ end
220
+ end
221
+
222
+ def show_summarized_status(task_id, targets:, targets_with_classes:)
223
+ require 'tty-progressbar'
224
+
225
+ bar = TTY::ProgressBar.new('[:bar] :current/:total ET::elapsed ETA::eta :rate/s')
226
+ results = colt.wait_bolt_task(task_id, targets: targets, targets_with_classes: targets_with_classes) do |_result, count, total_count|
227
+ bar.update total: total_count
228
+ bar.current = count
229
+ end
230
+
231
+ puts "\nSummary:"
232
+ summarize(results)
233
+ end
234
+
188
235
  def pastel
189
236
  @pastel ||= _pastel
190
237
  end
@@ -22,11 +22,7 @@ module Choria
22
22
  # On one side, data.stderr is filled by the remote execution stderr.
23
23
  # On the other side, error description is in JSON (ie. '_error')
24
24
  # So merge data.stderr in '_error'.'details'
25
- unless res.dig(:data, :stderr).nil? || res[:data][:stderr].empty?
26
- raise NotImplementedError, 'What to do when res[:data][:stderr] contains something?' if res[:result]['_error'].empty?
27
-
28
- res[:result]['_error']['details'].merge!({ 'stderr' => res[:data][:stderr].split("\n") })
29
- end
25
+ res[:result]['_stderr'] = res[:data][:stderr].split("\n") unless res.dig(:data, :stderr).nil? || res[:data][:stderr].empty?
30
26
  res[:data].delete :stderr
31
27
 
32
28
  # Convert '_output' (ie. stdout) lines into array
@@ -0,0 +1,28 @@
1
+ module Choria
2
+ class Colt
3
+ module Debugger
4
+ class << self
5
+ attr_writer :enabled
6
+
7
+ def enabled
8
+ @enabled ||= false
9
+ end
10
+
11
+ def root_directory
12
+ 'colt-debug'
13
+ end
14
+
15
+ # This method is helpful to grab raw content to be used as test fixture
16
+ # To do so, copy the generated result set (ie. directory) in relevant fixture directory (e.g. `spec/fixtures/orchestrator/task/result_sets`)
17
+ def save_file(result_set:, filename:, content:)
18
+ directory = File.join Colt::Debugger.root_directory, result_set
19
+ FileUtils.mkdir_p directory
20
+ path = File.join directory, filename
21
+ File.write(path, content)
22
+
23
+ path
24
+ end
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.7.0'
5
+ VERSION = '0.8.1'
6
6
  end
7
7
  end
@@ -6,9 +6,12 @@ module Choria
6
6
  class ResultSet
7
7
  attr_reader :results
8
8
 
9
+ attr_accessor :pending_count
10
+
9
11
  def initialize(on_result:)
10
12
  @results = []
11
13
  @on_result = on_result
14
+ @pending_count = 0
12
15
  end
13
16
 
14
17
  def integrate_rpc_error(rpc_error)
@@ -20,7 +23,7 @@ module Choria
20
23
  def integrate_result(result)
21
24
  structured_result = Choria::Colt::DataStructurer.structure(result).with_indifferent_access
22
25
  @results << structured_result
23
- @on_result&.call(structured_result)
26
+ @on_result&.call(structured_result, @results.count, pending_count + @results.count)
24
27
  end
25
28
  end
26
29
  end
@@ -3,6 +3,8 @@ require_relative 'task/result_set'
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext/hash/indifferent_access'
5
5
 
6
+ require 'choria/colt/debugger'
7
+
6
8
  module Choria
7
9
  class Orchestrator
8
10
  class Task
@@ -55,7 +57,7 @@ module Choria
55
57
  end
56
58
 
57
59
  def on_result(&block)
58
- @on_result = ->(result) { block.call(result) }
60
+ @on_result = ->(result, count, total_count) { block.call(result, count, total_count) }
59
61
  end
60
62
 
61
63
  private
@@ -64,15 +66,26 @@ module Choria
64
66
  @result_set ||= ResultSet.new(on_result: @on_result)
65
67
  end
66
68
 
69
+ def log_new_result(res)
70
+ if Colt::Debugger.enabled
71
+ debug_file = Colt::Debugger.save_file(result_set: @id, filename: "#{Time.now.iso8601}-#{res[:sender]}.json", content: JSON.pretty_generate(res))
72
+ logger.debug "New result for task ##{@id} saved in '#{debug_file}'"
73
+ else
74
+ logger.debug "New result for task ##{@id} from '#{res[:sender]}'"
75
+ end
76
+ end
77
+
67
78
  def rpc_results=(results)
68
79
  completed_results = results.reject { |res| res[:data][:exitcode] == -1 }
69
80
  @pending_targets ||= results.map { |res| res[:sender] }
70
81
 
71
82
  new_results = completed_results.select { |res| @pending_targets.include? res[:sender] }
72
83
  new_results.each do |res|
73
- logger.debug "New result for task ##{@id}: #{res}"
74
- result_set.integrate_result(res)
84
+ log_new_result res
85
+
75
86
  @pending_targets.delete res[:sender]
87
+ result_set.pending_count = @pending_targets.count
88
+ result_set.integrate_result res
76
89
  end
77
90
  end
78
91
 
@@ -42,13 +42,13 @@ module Choria
42
42
  def run(task, targets: nil, targets_with_classes: nil, verbose: false)
43
43
  rpc_client.progress = verbose
44
44
  discover(targets: targets, targets_with_classes: targets_with_classes)
45
- raise DiscoverError, 'No requests sent, no nodes discovered' if rpc_client.discover.size.zero?
45
+ raise DiscoverError, 'No requests sent, no nodes discovered' if rpc_client.discover.empty?
46
46
 
47
47
  task.run
48
48
  end
49
49
 
50
50
  def discover(targets: nil, targets_with_classes: nil)
51
- logger.debug "Targets: #{targets.nil? ? 'all' : targets})"
51
+ logger.debug "Targets: #{targets.nil? ? 'all' : targets}"
52
52
  targets&.each { |target| rpc_client.identity_filter target }
53
53
 
54
54
  unless targets_with_classes.nil?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: choria-colt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romuald Conty
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-26 00:00:00.000000000 Z
11
+ date: 2024-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: tty-progressbar
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: github_changelog_generator
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -214,6 +228,7 @@ files:
214
228
  - lib/choria/colt/cli/formatter.rb
215
229
  - lib/choria/colt/cli/thor.rb
216
230
  - lib/choria/colt/data_structurer.rb
231
+ - lib/choria/colt/debugger.rb
217
232
  - lib/choria/colt/version.rb
218
233
  - lib/choria/orchestrator.rb
219
234
  - lib/choria/orchestrator/task.rb
@@ -226,7 +241,7 @@ metadata:
226
241
  source_code_uri: https://github.com/opus-codium/choria-colt
227
242
  changelog_uri: https://github.com/opus-codium/choria-colt/CHANGELOG.md
228
243
  rubygems_mfa_required: 'true'
229
- post_install_message:
244
+ post_install_message:
230
245
  rdoc_options: []
231
246
  require_paths:
232
247
  - lib
@@ -241,8 +256,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
256
  - !ruby/object:Gem::Version
242
257
  version: '0'
243
258
  requirements: []
244
- rubygems_version: 3.1.2
245
- signing_key:
259
+ rubygems_version: 3.3.15
260
+ signing_key:
246
261
  specification_version: 4
247
262
  summary: Bolt-like CLI to run Bolt tasks, through Choria
248
263
  test_files: []