choria-colt 0.4.0 → 0.5.0

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: 8d2f9c64ee41ebc49a88deff71b470baab7c4e83b921102518969fe2a35b77f1
4
- data.tar.gz: 8f69562d201ff4addc3b44aa693d508ceca1a1623455d2ac57b273e59cd300f1
3
+ metadata.gz: fbb608f3080843c72765bb15bb6b375d1e3e02370c59b00b1d34fb9d5c02e57c
4
+ data.tar.gz: cc292639012cdde4ea87d35522f207e792e2727275d6c331bebfe7332948f905
5
5
  SHA512:
6
- metadata.gz: 8fd9cc5e888843d6c8954fe7d2543b4496fe861354162f499831f4ebe7cc8ccd3def73604c6d44e6f83273f71eb8364fc1b01c99581c16e8b2635adbee071aea
7
- data.tar.gz: 5a912066de4c960ffc85debf3cc53a678ac3312f2e75daec0d999f50af7f24215f1347b6c4984de55db9f3bb357075486310ab11db685e8f866c61a06049940f
6
+ metadata.gz: e3ef3078f635f8223eb3e2fbc9d5d043cdb536f0acc34008f61c80a4efc5af760b23021bb1c023a3aeed402903c1f59e66628e78e7087b398cb42bd8bf62d97a
7
+ data.tar.gz: 5b05929aae9c9414c1af2daa68fe7f17d5c8da758282e0d30ec9e27e69ba919eb9e2ec4ab59469aa94d7c7244e45e2f8f3c5a6ee4a9f35eafcd7c09ba7b0a9ae
data/.rubocop.yml CHANGED
@@ -11,6 +11,7 @@ Metrics/AbcSize:
11
11
  Max: 20
12
12
 
13
13
  Metrics/ClassLength:
14
+ Max: 120
14
15
  Exclude:
15
16
  - 'lib/choria/colt/cli.rb'
16
17
 
data/.rubocop_todo.yml CHANGED
@@ -1,17 +1,17 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2022-04-07 15:43:34 UTC using RuboCop version 1.26.0.
3
+ # on 2022-04-28 09:34:37 UTC using RuboCop version 1.26.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
9
  # Offense count: 1
10
- # Configuration parameters: IgnoredMethods.
11
- Metrics/CyclomaticComplexity:
12
- Max: 9
10
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
11
+ Metrics/AbcSize:
12
+ Max: 22
13
13
 
14
14
  # Offense count: 1
15
15
  # Configuration parameters: IgnoredMethods.
16
- Metrics/PerceivedComplexity:
17
- Max: 9
16
+ Metrics/CyclomaticComplexity:
17
+ Max: 8
@@ -5,6 +5,33 @@ module Choria
5
5
  class Colt
6
6
  class CLI < Thor
7
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
+
8
35
  attr_reader :pastel
9
36
 
10
37
  def initialize(colored:)
@@ -13,27 +40,36 @@ module Choria
13
40
  end
14
41
 
15
42
  def process_result(result)
16
- return process_error(result) unless result.dig(:data, :exitcode)&.zero?
43
+ result.extend Formatter::Result
17
44
 
18
- process_success(result)
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
19
59
  end
20
60
 
21
61
  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
62
+ host = format_host(result, "#{pastel.bright_green '√'} ")
63
+ headline = "#{pastel.on_green ' '} "
31
64
 
32
- output_lines.join("\n")
65
+ [
66
+ host,
67
+ result.output.map { |line| "#{headline}#{line}" },
68
+ ].flatten.join("\n")
33
69
  end
34
70
 
35
71
  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])}"
72
+ host = format_host(result, "#{pastel.bright_red '⨯'} ")
37
73
  output = result.dig(:result, '_output')
38
74
  error_details = JSON.pretty_generate(result.dig(:result, :_error, :details)).split "\n"
39
75
  error_description = [
@@ -61,6 +97,26 @@ module Choria
61
97
  ].flatten.map { |line| "#{headline}#{line}" },
62
98
  ].flatten.join("\n")
63
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
64
120
  end
65
121
  end
66
122
  end
@@ -27,6 +27,7 @@ module Choria
27
27
  desc: 'Select the targets which have the specified Puppet classes.'
28
28
  def run(*args) # rubocop:disable Metrics/AbcSize
29
29
  input = extract_task_parameters_from_args(args)
30
+ targets = extract_targets_from_options
30
31
 
31
32
  raise Thor::Error, 'Task name is required' if args.empty?
32
33
  raise Thor::Error, "Too many arguments: #{args}" unless args.count == 1
@@ -35,8 +36,7 @@ module Choria
35
36
 
36
37
  task_name = args.shift
37
38
 
38
- targets = options['targets']&.split(',')
39
- targets = nil if options['targets'] == 'all'
39
+ logger.debug "Targets: #{targets}"
40
40
 
41
41
  targets_with_classes = options['targets_with_classes']&.split(',')
42
42
 
@@ -45,8 +45,8 @@ module Choria
45
45
  end
46
46
 
47
47
  File.write 'last_run.json', JSON.pretty_generate(results)
48
- rescue Choria::Orchestrator::Error => e
49
- raise Thor::Error, "#{e.class}: #{e}"
48
+ rescue Choria::Orchestrator::Error
49
+ # This error is already logged and displayed.
50
50
  end
51
51
 
52
52
  desc 'show [task name] [options]', 'Show available tasks and task documentation'
@@ -90,8 +90,8 @@ module Choria
90
90
  end
91
91
 
92
92
  File.write 'last_run.json', JSON.pretty_generate(results)
93
- rescue Choria::Orchestrator::Error => e
94
- raise Thor::Error, "#{e.class}: #{e}"
93
+ rescue Choria::Orchestrator::Error
94
+ # This error is already logged and displayed.
95
95
  end
96
96
 
97
97
  no_commands do # rubocop:disable Metrics/BlockLength
@@ -130,6 +130,16 @@ module Choria
130
130
  end.to_h
131
131
  end
132
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
+
133
143
  def show_tasks_summary(tasks)
134
144
  tasks.reject! { |_task, metadata| metadata['metadata']['private'] }
135
145
 
@@ -2,6 +2,8 @@ module Choria
2
2
  class Colt
3
3
  module DataStructurer
4
4
  def self.structure(res) # rubocop:disable Metrics/AbcSize
5
+ return res unless [0, 1].include? res[:statuscode]
6
+
5
7
  # data.stdout seems to always be JSON, so parse it once.
6
8
  res[:result] = JSON.parse res[:data][:stdout]
7
9
  res[:data].delete :stdout
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Choria
4
4
  class Colt
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  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,4 +1,4 @@
1
- require 'choria/colt/data_structurer'
1
+ require_relative 'task/result_set'
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext/hash/indifferent_access'
@@ -7,17 +7,15 @@ module Choria
7
7
  class Orchestrator
8
8
  class Task
9
9
  class Error < Orchestrator::Error; end
10
+ class NoNodesLeftError < Error; end
10
11
 
11
- attr_reader :id, :name, :input, :environment, :rpc_results, :results
12
- attr_accessor :rpc_responses
12
+ attr_reader :id, :name, :input, :environment
13
13
 
14
14
  def initialize(orchestrator:, id: nil, name: nil, input: {}, environment: 'production')
15
15
  @id = id
16
16
  @name = name
17
17
  @environment = environment
18
18
  @orchestrator = orchestrator
19
- @results = []
20
-
21
19
  return if @name.nil?
22
20
 
23
21
  @input = default_input.merge input
@@ -33,71 +31,82 @@ module Choria
33
31
  metadata['files'].to_json
34
32
  end
35
33
 
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
34
+ def results
35
+ result_set.results
36
+ end
42
37
 
43
- task_ids = rpc_responses_ok.map { |res| res[:body][:data][:task_id] }.uniq
38
+ def run
39
+ raise Error, 'Unable to run a task by ID' if name.nil?
44
40
 
45
- raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
41
+ @pending_targets = rpc_client.discover
42
+ _download
43
+ _run_no_wait
44
+ end
46
45
 
47
- @id = task_ids.first
48
- end
46
+ def wait
47
+ raise Error, 'Task ID is required!' if @id.nil?
49
48
 
50
- wait_results
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?
54
+ end
51
55
  end
52
56
 
53
57
  def on_result(&block)
54
- @on_result = lambda { |result|
55
- block.call(result)
56
- }
58
+ @on_result = ->(result) { block.call(result) }
57
59
  end
58
60
 
59
61
  private
60
62
 
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)
63
+ def result_set
64
+ @result_set ||= ResultSet.new(on_result: @on_result)
65
+ end
73
66
 
74
- @results << structured_result
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]
75
76
  end
76
-
77
- @rpc_results = results
78
77
  end
79
78
 
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 = []
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?
87
83
 
88
- loop do
89
- self.rpc_results = @orchestrator.rpc_client.task_status(task_id: @id).map(&:results)
84
+ @pending_targets.delete rpc_response.sender
85
+ result_set.integrate_rpc_error(rpc_response)
86
+ end
90
87
 
91
- break if terminated?
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
92
  end
93
+
94
+ raise NoNodesLeftError, "No nodes left to continue after 'download' action" if @pending_targets.empty?
93
95
  end
94
96
 
95
- def terminated?
96
- @rpc_results.each do |result|
97
- return false if result[:data][:exitcode] == -1
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
98
103
  end
104
+ raise NoNodesLeftError, "No nodes left to continue after 'run_no_wait' action" if @pending_targets.empty?
99
105
 
100
- true
106
+ task_ids.uniq!
107
+ raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
108
+
109
+ @id = task_ids.first
101
110
  end
102
111
 
103
112
  def _metadata
@@ -122,6 +131,10 @@ module Choria
122
131
  def logger
123
132
  @orchestrator.logger
124
133
  end
134
+
135
+ def rpc_client
136
+ @orchestrator.rpc_client
137
+ end
125
138
  end
126
139
  end
127
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
@@ -33,51 +51,13 @@ module Choria
33
51
  end
34
52
 
35
53
  logger.info 'Discovering targets…'
36
- raise DiscoverError, 'No request sent, no node discovered' if rpc_client.discover.size.zero?
54
+ raise DiscoverError, 'No requests sent, no nodes discovered' if rpc_client.discover.size.zero?
37
55
 
38
- logger.info "Downloading task '#{task.name}' on #{rpc_client.discover.size} nodes…"
39
- rpc_client.download(task: task.name, files: task.files, verbose: verbose)
40
-
41
- responses = []
42
- logger.info "Starting task '#{task.name}' on #{rpc_client.discover.size} nodes…"
43
- rpc_client.run_no_wait(task: task.name, files: task.files, input: task.input.to_json, verbose: verbose) do |response|
44
- logger.debug " Response: '#{response}'"
45
- responses << response
46
- end
47
-
48
- # TODO: Include stats in logs when logger will be available (see MCollective::RPC#printrpcstats)
49
-
50
- task.rpc_responses = responses
51
- end
52
-
53
- def validate_rpc_result(result)
54
- raise Error, "The RPC agent returned an error: #{result[:statusmsg]}" unless (result[:statuscode]).zero?
56
+ task.run
55
57
  end
56
58
 
57
59
  def rpc_client
58
- @rpc_client ||= rpcclient('bolt_tasks', options: rpc_options)
59
- end
60
-
61
- private
62
-
63
- def rpc_options
64
- {
65
- verbose: false,
66
- disctimeout: nil,
67
- timeout: 5,
68
- config: '/etc/choria/client.conf',
69
- collective: 'mcollective',
70
- discovery_method: nil,
71
- discovery_options: [],
72
- filter: {
73
- 'fact' => [], 'cf_class' => [], 'agent' => [], 'identity' => [], 'compound' => []
74
- },
75
- progress_bar: false,
76
- mcollective_limit_targets: false,
77
- batch_size: nil,
78
- batch_sleep_time: 1,
79
- output_format: :json,
80
- }
60
+ @rpc_client ||= rpcclient('bolt_tasks', options: {})
81
61
  end
82
62
  end
83
63
  end
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.4.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-04-25 00:00:00.000000000 Z
11
+ date: 2022-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -218,6 +218,7 @@ files:
218
218
  - lib/choria/colt/version.rb
219
219
  - lib/choria/orchestrator.rb
220
220
  - lib/choria/orchestrator/task.rb
221
+ - lib/choria/orchestrator/task/result_set.rb
221
222
  - sig/choria/colt.rbs
222
223
  homepage: https://github.com/opus-codium/choria-colt
223
224
  licenses: []