choria-colt 0.7.0 → 0.8.1

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: 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: []