choria-colt 0.4.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: 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: []