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 +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +6 -6
- data/lib/choria/colt/cli/formatter.rb +69 -13
- data/lib/choria/colt/cli.rb +16 -6
- data/lib/choria/colt/data_structurer.rb +2 -0
- data/lib/choria/colt/version.rb +1 -1
- data/lib/choria/orchestrator/task/result_set.rb +28 -0
- data/lib/choria/orchestrator/task.rb +61 -48
- data/lib/choria/orchestrator.rb +21 -41
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbb608f3080843c72765bb15bb6b375d1e3e02370c59b00b1d34fb9d5c02e57c
|
4
|
+
data.tar.gz: cc292639012cdde4ea87d35522f207e792e2727275d6c331bebfe7332948f905
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3ef3078f635f8223eb3e2fbc9d5d043cdb536f0acc34008f61c80a4efc5af760b23021bb1c023a3aeed402903c1f59e66628e78e7087b398cb42bd8bf62d97a
|
7
|
+
data.tar.gz: 5b05929aae9c9414c1af2daa68fe7f17d5c8da758282e0d30ec9e27e69ba919eb9e2ec4ab59469aa94d7c7244e45e2f8f3c5a6ee4a9f35eafcd7c09ba7b0a9ae
|
data/.rubocop.yml
CHANGED
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-
|
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/
|
12
|
-
Max:
|
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/
|
17
|
-
Max:
|
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
|
-
|
43
|
+
result.extend Formatter::Result
|
17
44
|
|
18
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
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 '⨯'}
|
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
|
data/lib/choria/colt/cli.rb
CHANGED
@@ -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
|
-
|
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
|
49
|
-
|
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
|
94
|
-
|
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
|
data/lib/choria/colt/version.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
37
|
-
|
38
|
-
|
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
|
-
|
38
|
+
def run
|
39
|
+
raise Error, 'Unable to run a task by ID' if name.nil?
|
44
40
|
|
45
|
-
|
41
|
+
@pending_targets = rpc_client.discover
|
42
|
+
_download
|
43
|
+
_run_no_wait
|
44
|
+
end
|
46
45
|
|
47
|
-
|
48
|
-
|
46
|
+
def wait
|
47
|
+
raise Error, 'Task ID is required!' if @id.nil?
|
49
48
|
|
50
|
-
|
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 =
|
55
|
-
block.call(result)
|
56
|
-
}
|
58
|
+
@on_result = ->(result) { block.call(result) }
|
57
59
|
end
|
58
60
|
|
59
61
|
private
|
60
62
|
|
61
|
-
def
|
62
|
-
|
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
|
-
|
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
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
89
|
-
|
84
|
+
@pending_targets.delete rpc_response.sender
|
85
|
+
result_set.integrate_rpc_error(rpc_response)
|
86
|
+
end
|
90
87
|
|
91
|
-
|
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
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
data/lib/choria/orchestrator.rb
CHANGED
@@ -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
|
54
|
+
raise DiscoverError, 'No requests sent, no nodes discovered' if rpc_client.discover.size.zero?
|
37
55
|
|
38
|
-
|
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:
|
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
|
+
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-
|
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: []
|