choria-mcorpc-support 2.22.1 → 2.23.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mcollective.rb +1 -1
  3. data/lib/mcollective/agent/bolt_tasks.ddl +235 -0
  4. data/lib/mcollective/agent/bolt_tasks.json +347 -0
  5. data/lib/mcollective/agent/bolt_tasks.rb +176 -0
  6. data/lib/mcollective/agent/choria_util.ddl +152 -0
  7. data/lib/mcollective/agent/choria_util.json +244 -0
  8. data/lib/mcollective/agent/rpcutil.ddl +7 -3
  9. data/lib/mcollective/agent/rpcutil.json +333 -0
  10. data/lib/mcollective/agent/scout.ddl +169 -0
  11. data/lib/mcollective/agent/scout.json +224 -0
  12. data/lib/mcollective/agents.rb +7 -6
  13. data/lib/mcollective/aggregate.rb +4 -4
  14. data/lib/mcollective/aggregate/average.rb +2 -2
  15. data/lib/mcollective/aggregate/base.rb +2 -2
  16. data/lib/mcollective/aggregate/result.rb +3 -3
  17. data/lib/mcollective/aggregate/result/collection_result.rb +2 -2
  18. data/lib/mcollective/aggregate/result/numeric_result.rb +2 -2
  19. data/lib/mcollective/aggregate/sum.rb +2 -2
  20. data/lib/mcollective/aggregate/summary.rb +3 -4
  21. data/lib/mcollective/application.rb +57 -21
  22. data/lib/mcollective/application/choria.rb +249 -0
  23. data/lib/mcollective/application/completion.rb +6 -6
  24. data/lib/mcollective/application/describe_filter.rb +20 -20
  25. data/lib/mcollective/application/facts.rb +11 -11
  26. data/lib/mcollective/application/federation.rb +239 -0
  27. data/lib/mcollective/application/find.rb +4 -4
  28. data/lib/mcollective/application/help.rb +3 -3
  29. data/lib/mcollective/application/inventory.rb +3 -341
  30. data/lib/mcollective/application/ping.rb +3 -77
  31. data/lib/mcollective/application/playbook.rb +207 -0
  32. data/lib/mcollective/application/plugin.rb +106 -106
  33. data/lib/mcollective/application/rpc.rb +3 -108
  34. data/lib/mcollective/application/tasks.rb +416 -0
  35. data/lib/mcollective/applications.rb +11 -10
  36. data/lib/mcollective/audit/choria.rb +33 -0
  37. data/lib/mcollective/cache.rb +2 -4
  38. data/lib/mcollective/client.rb +11 -10
  39. data/lib/mcollective/config.rb +21 -26
  40. data/lib/mcollective/connector/base.rb +2 -1
  41. data/lib/mcollective/connector/nats.ddl +9 -0
  42. data/lib/mcollective/connector/nats.rb +450 -0
  43. data/lib/mcollective/data.rb +8 -3
  44. data/lib/mcollective/data/agent_data.rb +1 -1
  45. data/lib/mcollective/data/base.rb +6 -5
  46. data/lib/mcollective/data/bolt_task_data.ddl +90 -0
  47. data/lib/mcollective/data/bolt_task_data.rb +32 -0
  48. data/lib/mcollective/data/collective_data.rb +1 -1
  49. data/lib/mcollective/data/fact_data.rb +6 -6
  50. data/lib/mcollective/data/fstat_data.rb +2 -4
  51. data/lib/mcollective/data/result.rb +7 -2
  52. data/lib/mcollective/ddl/agentddl.rb +5 -17
  53. data/lib/mcollective/ddl/base.rb +10 -13
  54. data/lib/mcollective/discovery.rb +12 -26
  55. data/lib/mcollective/discovery/choria.ddl +11 -0
  56. data/lib/mcollective/discovery/choria.rb +223 -0
  57. data/lib/mcollective/discovery/flatfile.rb +7 -8
  58. data/lib/mcollective/discovery/mc.rb +2 -2
  59. data/lib/mcollective/discovery/stdin.rb +17 -18
  60. data/lib/mcollective/exceptions.rb +13 -0
  61. data/lib/mcollective/facts/base.rb +9 -9
  62. data/lib/mcollective/facts/yaml_facts.rb +12 -12
  63. data/lib/mcollective/generators.rb +3 -3
  64. data/lib/mcollective/generators/agent_generator.rb +3 -4
  65. data/lib/mcollective/generators/base.rb +14 -15
  66. data/lib/mcollective/generators/data_generator.rb +5 -6
  67. data/lib/mcollective/log.rb +2 -2
  68. data/lib/mcollective/logger/base.rb +3 -2
  69. data/lib/mcollective/logger/console_logger.rb +10 -10
  70. data/lib/mcollective/logger/file_logger.rb +7 -7
  71. data/lib/mcollective/logger/syslog_logger.rb +11 -15
  72. data/lib/mcollective/matcher.rb +14 -14
  73. data/lib/mcollective/matcher/parser.rb +31 -41
  74. data/lib/mcollective/matcher/scanner.rb +69 -74
  75. data/lib/mcollective/message.rb +10 -17
  76. data/lib/mcollective/monkey_patches.rb +2 -4
  77. data/lib/mcollective/optionparser.rb +1 -0
  78. data/lib/mcollective/pluginmanager.rb +3 -5
  79. data/lib/mcollective/pluginpackager.rb +1 -3
  80. data/lib/mcollective/pluginpackager/agent_definition.rb +3 -8
  81. data/lib/mcollective/pluginpackager/forge_packager.rb +7 -9
  82. data/lib/mcollective/pluginpackager/standard_definition.rb +1 -2
  83. data/lib/mcollective/registration/base.rb +18 -16
  84. data/lib/mcollective/rpc.rb +2 -4
  85. data/lib/mcollective/rpc/actionrunner.rb +16 -18
  86. data/lib/mcollective/rpc/agent.rb +26 -43
  87. data/lib/mcollective/rpc/audit.rb +1 -0
  88. data/lib/mcollective/rpc/client.rb +67 -85
  89. data/lib/mcollective/rpc/helpers.rb +55 -62
  90. data/lib/mcollective/rpc/progress.rb +2 -2
  91. data/lib/mcollective/rpc/reply.rb +17 -19
  92. data/lib/mcollective/rpc/request.rb +7 -5
  93. data/lib/mcollective/rpc/result.rb +6 -8
  94. data/lib/mcollective/rpc/stats.rb +49 -58
  95. data/lib/mcollective/security/base.rb +29 -36
  96. data/lib/mcollective/security/choria.rb +765 -0
  97. data/lib/mcollective/shell.rb +9 -4
  98. data/lib/mcollective/signer/base.rb +28 -0
  99. data/lib/mcollective/signer/choria.rb +185 -0
  100. data/lib/mcollective/ssl.rb +8 -6
  101. data/lib/mcollective/util.rb +52 -53
  102. data/lib/mcollective/util/bolt_support.rb +176 -0
  103. data/lib/mcollective/util/bolt_support/plan_runner.rb +167 -0
  104. data/lib/mcollective/util/bolt_support/task_result.rb +94 -0
  105. data/lib/mcollective/util/bolt_support/task_results.rb +128 -0
  106. data/lib/mcollective/util/choria.rb +1103 -0
  107. data/lib/mcollective/util/indifferent_hash.rb +12 -0
  108. data/lib/mcollective/util/natswrapper.rb +242 -0
  109. data/lib/mcollective/util/playbook.rb +435 -0
  110. data/lib/mcollective/util/playbook/data_stores.rb +201 -0
  111. data/lib/mcollective/util/playbook/data_stores/base.rb +99 -0
  112. data/lib/mcollective/util/playbook/data_stores/consul_data_store.rb +88 -0
  113. data/lib/mcollective/util/playbook/data_stores/environment_data_store.rb +33 -0
  114. data/lib/mcollective/util/playbook/data_stores/etcd_data_store.rb +42 -0
  115. data/lib/mcollective/util/playbook/data_stores/file_data_store.rb +106 -0
  116. data/lib/mcollective/util/playbook/data_stores/shell_data_store.rb +103 -0
  117. data/lib/mcollective/util/playbook/inputs.rb +265 -0
  118. data/lib/mcollective/util/playbook/nodes.rb +207 -0
  119. data/lib/mcollective/util/playbook/nodes/mcollective_nodes.rb +86 -0
  120. data/lib/mcollective/util/playbook/nodes/pql_nodes.rb +40 -0
  121. data/lib/mcollective/util/playbook/nodes/shell_nodes.rb +55 -0
  122. data/lib/mcollective/util/playbook/nodes/terraform_nodes.rb +65 -0
  123. data/lib/mcollective/util/playbook/nodes/yaml_nodes.rb +47 -0
  124. data/lib/mcollective/util/playbook/playbook_logger.rb +47 -0
  125. data/lib/mcollective/util/playbook/puppet_logger.rb +51 -0
  126. data/lib/mcollective/util/playbook/report.rb +152 -0
  127. data/lib/mcollective/util/playbook/task_result.rb +55 -0
  128. data/lib/mcollective/util/playbook/tasks.rb +196 -0
  129. data/lib/mcollective/util/playbook/tasks/base.rb +45 -0
  130. data/lib/mcollective/util/playbook/tasks/graphite_event_task.rb +64 -0
  131. data/lib/mcollective/util/playbook/tasks/mcollective_task.rb +356 -0
  132. data/lib/mcollective/util/playbook/tasks/shell_task.rb +93 -0
  133. data/lib/mcollective/util/playbook/tasks/slack_task.rb +105 -0
  134. data/lib/mcollective/util/playbook/tasks/webhook_task.rb +136 -0
  135. data/lib/mcollective/util/playbook/template_util.rb +98 -0
  136. data/lib/mcollective/util/playbook/uses.rb +169 -0
  137. data/lib/mcollective/util/tasks_support.rb +733 -0
  138. data/lib/mcollective/util/tasks_support/cli.rb +260 -0
  139. data/lib/mcollective/util/tasks_support/default_formatter.rb +138 -0
  140. data/lib/mcollective/util/tasks_support/json_formatter.rb +108 -0
  141. data/lib/mcollective/validator.rb +6 -1
  142. data/lib/mcollective/validator/bolt_task_name_validator.ddl +7 -0
  143. data/lib/mcollective/validator/bolt_task_name_validator.rb +11 -0
  144. data/lib/mcollective/validator/length_validator.rb +1 -3
  145. metadata +67 -4
@@ -0,0 +1,93 @@
1
+ module MCollective
2
+ module Util
3
+ class Playbook
4
+ class Tasks
5
+ class ShellTask < Base
6
+ def validate_configuration!
7
+ raise("A command was not given") unless @command
8
+ raise("Nodes were given but is not an array") if @nodes && !@nodes.is_a?(Array)
9
+ end
10
+
11
+ def from_hash(data)
12
+ @cwd = data["cwd"] || Dir.pwd
13
+ @timeout = data["timeout"]
14
+ @nodes = data["nodes"]
15
+ @environment = data["environment"]
16
+
17
+ if @nodes.is_a?(Array)
18
+ @command = "%s --nodes %s" % [data["command"], @nodes.join(",")]
19
+ else
20
+ @command = data["command"]
21
+ end
22
+
23
+ self
24
+ end
25
+
26
+ def shell_options
27
+ options = {}
28
+ options["cwd"] = @cwd if @cwd
29
+ options["timeout"] = Integer(@timeout) if @timeout
30
+ options["environment"] = @environment if @environment
31
+ options["stdout"] = []
32
+ options["stderr"] = options["stdout"]
33
+ options
34
+ end
35
+
36
+ def to_execution_result(results)
37
+ result = {
38
+ "value" => results[2].join("\r\n").chomp,
39
+ "type" => "shell",
40
+ "fail_ok" => @fail_ok,
41
+ "error" => {
42
+ "msg" => results[1],
43
+ "kind" => "choria.playbook/taskerror",
44
+ "details" => {
45
+ "command" => @command
46
+ }
47
+ }
48
+ }
49
+
50
+ result.delete("error") if results[0]
51
+
52
+ {"localhost" => result}
53
+ end
54
+
55
+ def run
56
+ if @nodes
57
+ Log.info("Starting command %s against %d nodes" % [@command, @nodes.size])
58
+ else
59
+ Log.info("Starting command %s" % [@command])
60
+ end
61
+
62
+ begin
63
+ options = shell_options
64
+
65
+ shell = Shell.new(@command, options)
66
+ shell.runcommand
67
+
68
+ options["stdout"].each do |output|
69
+ output.lines.each do |line|
70
+ Log.info(line.chomp)
71
+ end
72
+ end
73
+
74
+ if shell.status.exitstatus == 0
75
+ Log.info("Successfully ran command %s" % [@command])
76
+ [true, "Command completed successfully", options["stdout"]]
77
+ else
78
+ Log.warn("Failed to run command %s with exit code %s" % [@command, shell.status.exitstatus])
79
+ [false, "Command failed with code %d" % [shell.status.exitstatus], options["stdout"]]
80
+ end
81
+ rescue
82
+ msg = "Could not run command %s: %s: %s" % [@command, $!.class, $!.to_s]
83
+ Log.debug(msg)
84
+ Log.debug($!.backtrace.join("\t\n"))
85
+
86
+ [false, msg, []]
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,105 @@
1
+ require "cgi"
2
+ require "json"
3
+
4
+ module MCollective
5
+ module Util
6
+ class Playbook
7
+ class Tasks
8
+ class SlackTask < Base
9
+ def validate_configuration!
10
+ raise("A channel is required") unless @channel
11
+ raise("Message text is required") unless @text
12
+ raise("A bot token is required") unless @token
13
+ end
14
+
15
+ def from_hash(data)
16
+ @channel = data["channel"]
17
+ @text = data["text"]
18
+ @token = data["token"]
19
+ @color = data.fetch("color", "#ffa449")
20
+ @username = data.fetch("username", "Choria")
21
+ @icon = "https://choria.io/img/slack-48x48.png"
22
+
23
+ self
24
+ end
25
+
26
+ def choria
27
+ @_choria ||= Util::Choria.new(false)
28
+ end
29
+
30
+ def attachments
31
+ [
32
+ "fallback" => @text,
33
+ "color" => @color,
34
+ "text" => @text,
35
+ "pretext" => "Task: %s" % @description,
36
+ "mrkdwn_in" => ["text"],
37
+ "footer" => "Choria Playbooks",
38
+ "fields" => [
39
+ {
40
+ "title" => "user",
41
+ "value" => PluginManager["security_plugin"].callerid,
42
+ "short" => true
43
+ },
44
+ {
45
+ "title" => "playbook",
46
+ "value" => @playbook.name,
47
+ "short" => true
48
+ }
49
+ ]
50
+ ]
51
+ end
52
+
53
+ def to_execution_result(results)
54
+ result = {
55
+ "value" => results[1],
56
+ "type" => "slack",
57
+ "fail_ok" => @fail_ok
58
+ }
59
+
60
+ unless results[0]
61
+ result["error"] = {
62
+ "msg" => results[1],
63
+ "kind" => "choria.playbook/taskerror",
64
+ "details" => {
65
+ "channel" => @channel
66
+ }
67
+ }
68
+ result["value"] = results[2].first
69
+ end
70
+
71
+ {"slack.com" => result}
72
+ end
73
+
74
+ def run
75
+ https = choria.https(:target => "slack.com", :port => 443)
76
+ path = "/api/chat.postMessage?token=%s&username=%s&channel=%s&icon_url=%s&attachments=%s" % [
77
+ CGI.escape(@token),
78
+ CGI.escape(@username),
79
+ CGI.escape(@channel),
80
+ CGI.escape(@icon),
81
+ CGI.escape(attachments.to_json)
82
+ ]
83
+
84
+ resp, data = https.request(choria.http_get(path))
85
+ data = JSON.parse(data || resp.body)
86
+
87
+ if resp.code == "200" && data["ok"]
88
+ Log.debug("Successfully sent message to slack channel %s" % [@channel])
89
+ [true, "Message submitted to slack channel %s" % [@channel], [data]]
90
+ else
91
+ Log.warn("Failed to send message to slack channel %s: %s" % [@channel, data["error"]])
92
+ [false, "Failed to send message to slack channel %s: %s" % [@channel, data["error"]], [data]]
93
+ end
94
+ rescue
95
+ msg = "Could not publish slack message to channel %s: %s: %s" % [@channel, $!.class, $!.to_s]
96
+ Log.debug(msg)
97
+ Log.debug($!.backtrace.join("\t\n"))
98
+
99
+ [false, msg, []]
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,136 @@
1
+ require "uri"
2
+ require "cgi"
3
+ require "json"
4
+
5
+ module MCollective
6
+ module Util
7
+ class Playbook
8
+ class Tasks
9
+ class WebhookTask < Base
10
+ USER_AGENT = "Choria Playbooks http://choria.io".freeze
11
+
12
+ attr_reader :verify_ssl
13
+
14
+ def validate_configuration!
15
+ raise("A uri is required") unless @uri
16
+ raise("Only GET and POST is supported as methods") unless ["GET", "POST"].include?(@method)
17
+ end
18
+
19
+ def from_hash(data)
20
+ @headers = data.fetch("headers", {})
21
+ @data = data.fetch("data", {})
22
+ @uri = data["uri"]
23
+ @method = data.fetch("method", "POST").upcase
24
+ @request_id = SSL.uuid
25
+ @verify_ssl = Util.str_to_bool(data.fetch("verify_ssl", true))
26
+
27
+ self
28
+ end
29
+
30
+ def create_uri
31
+ uri = URI.parse(@uri)
32
+
33
+ if @method == "GET"
34
+ query = Array(uri.query)
35
+
36
+ @data.each do |k, v|
37
+ query << "%s=%s" % [CGI.escape(k), CGI.escape(v.to_s)]
38
+ end
39
+
40
+ uri.query = query.join("&") unless query.empty?
41
+ end
42
+
43
+ uri
44
+ end
45
+
46
+ def http_get_request(uri)
47
+ headers = {
48
+ "User-Agent" => USER_AGENT,
49
+ "X-Choria-Request-ID" => @request_id
50
+ }.merge(@headers)
51
+
52
+ Net::HTTP::Get.new(uri.request_uri, headers)
53
+ end
54
+
55
+ def http_post_request(uri)
56
+ headers = {
57
+ "Content-Type" => "application/json",
58
+ "User-Agent" => USER_AGENT,
59
+ "X-Choria-Request-ID" => @request_id
60
+ }.merge(@headers)
61
+
62
+ req = Net::HTTP::Post.new(uri.request_uri, headers)
63
+ req.body = @data.to_json
64
+ req
65
+ end
66
+
67
+ def http_request(uri)
68
+ return http_get_request(uri) if @method == "GET"
69
+ return http_post_request(uri) if @method == "POST"
70
+
71
+ raise("Unknown request method %s" % @method)
72
+ end
73
+
74
+ def choria
75
+ @_choria ||= Util::Choria.new(false)
76
+ end
77
+
78
+ def http(uri)
79
+ http = choria.https({:target => uri.host, :port => uri.port}, false)
80
+
81
+ if verify_ssl
82
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
83
+ else
84
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
85
+ end
86
+
87
+ http.use_ssl = false if uri.scheme == "http"
88
+
89
+ http
90
+ end
91
+
92
+ def to_execution_result(results)
93
+ result = {
94
+ "value" => results[2].first,
95
+ "type" => "webhook",
96
+ "fail_ok" => @fail_ok,
97
+ "error" => {
98
+ "msg" => results[1],
99
+ "kind" => "choria.playbook/taskerror",
100
+ "details" => {
101
+ "task" => "webhook",
102
+ "uri" => @uri,
103
+ "method" => @method,
104
+ "request_id" => @request_id
105
+ }
106
+ }
107
+ }
108
+
109
+ result.delete("error") if results[0]
110
+
111
+ {create_uri.host => result}
112
+ end
113
+
114
+ def run
115
+ uri = create_uri
116
+ resp = http(uri).request(http_request(uri))
117
+
118
+ Log.debug("%s request to %s returned code %s with body: %s" % [@method, uri.to_s, resp.code, resp.body])
119
+
120
+ if ["200", "201"].include?(resp.code)
121
+ [true, "Successfully sent %s request to webhook %s with id %s" % [@method, @uri, @request_id], [resp.body]]
122
+ else
123
+ [false, "Failed to send %s request to webhook %s with id %s: %s: %s" % [@method, @uri, @request_id, resp.code, resp.body], [resp.body]]
124
+ end
125
+ rescue
126
+ msg = "Could not send %s to webhook %s: %s: %s" % [@method, @uri, $!.class, $!.to_s]
127
+ Log.debug(msg)
128
+ Log.debug($!.backtrace.join("\t\n"))
129
+
130
+ [false, msg, []]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,98 @@
1
+ module MCollective
2
+ module Util
3
+ class Playbook
4
+ module TemplateUtil
5
+ # Recursively parse a data structure seeking strings that might contain templates
6
+ #
7
+ # Template strings look like `{{{scope.key}}}` where scope is one of `input`, `metadata`,
8
+ # `nodes` and the key is some item contained in those scopes like a named node list.
9
+ #
10
+ # You'll generally mix this into a class you wish to use it in, that class should have
11
+ # a `@playbook` variable set which is an instance of `Playbook`
12
+ #
13
+ # @param data [Object] data structure to traverse
14
+ # @return [Object] deep cloned copy of the structure with strings parsed
15
+ def t(data)
16
+ data = Marshal.load(Marshal.dump(data))
17
+
18
+ case data
19
+ when String
20
+ __template_process_string(data)
21
+ when Hash
22
+ data.each do |k, v|
23
+ data[k] = t(v)
24
+ end
25
+
26
+ data
27
+ when Array
28
+ data.map do |v|
29
+ t(v)
30
+ end
31
+ else
32
+ data
33
+ end
34
+ end
35
+
36
+ def __template_resolve(type, item)
37
+ Log.debug("Resolving template data for %s.%s" % [type, item])
38
+
39
+ case type
40
+ when "input", "inputs"
41
+ @playbook.input_value(item)
42
+ when "nodes"
43
+ @playbook.discovered_nodes(item)
44
+ when "metadata"
45
+ @playbook.metadata_item(item)
46
+ when "previous_task"
47
+ @playbook.previous_task(item)
48
+ when "date"
49
+ Time.now.strftime(item)
50
+ when "utc_date"
51
+ Time.now.utc.strftime(item)
52
+ when "elapsed_time"
53
+ @playbook.report.elapsed_time
54
+ when "uuid"
55
+ SSL.uuid
56
+ else
57
+ raise("Do not know how to process data of type %s" % type)
58
+ end
59
+ end
60
+
61
+ def __template_process_string(string)
62
+ raise("Playbook is not accessible") unless @playbook
63
+
64
+ front = '{{2,3}\s*'
65
+ back = '\s*}{2,3}'
66
+
67
+ data_regex = Regexp.new("%s%s%s" % [front, '(?<type>input(s*)|metadata|nodes)\.(?<item>[a-zA-Z0-9\_\-]+)', back])
68
+ date_regex = Regexp.new("%s%s%s" % [front, '(?<type>date|utc_date)\(\s*["\']*(?<format>.+?)["\']*\s*\)', back])
69
+ task_regex = Regexp.new("%s%s%s" % [front, '(?<type>previous_task)\.(?<item>(success|description|msg|message|data|runtime))', back])
70
+ singles_regex = Regexp.new("%s%s%s" % [front, "(?<type>uuid|elapsed_time)", back])
71
+
72
+ combined_regex = Regexp.union(data_regex, date_regex, task_regex, singles_regex)
73
+
74
+ if req = (string.match(/^#{data_regex}$/) || string.match(/^#{task_regex}$/))
75
+ __template_resolve(req["type"], req["item"])
76
+ elsif req = string.match(/^#{date_regex}$/)
77
+ __template_resolve(req["type"], req["format"])
78
+ elsif req = string.match(/^#{singles_regex}$/)
79
+ __template_resolve(req["type"], "")
80
+ else
81
+ string.gsub(/#{combined_regex}/) do |part|
82
+ value = __template_process_string(part)
83
+
84
+ case value
85
+ when Array
86
+ value.join(", ")
87
+ when Hash
88
+ value.to_json
89
+ else
90
+ value
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,169 @@
1
+ module MCollective
2
+ module Util
3
+ class Playbook
4
+ class Uses
5
+ def initialize(playbook)
6
+ @playbook = playbook
7
+ @uses = {}
8
+ end
9
+
10
+ def [](agent)
11
+ @uses[agent]
12
+ end
13
+
14
+ def keys
15
+ @uses.keys
16
+ end
17
+
18
+ # Retrieves the agent inventory for a set of nodes
19
+ #
20
+ # @param nodes [Array<String>] list of nodes to retrieve it for
21
+ # @return [Boolean, String, Array] success, message and inventory results
22
+ def agent_inventory(nodes)
23
+ rpc = Tasks::McollectiveTask.new(@playbook)
24
+
25
+ rpc.from_hash(
26
+ "nodes" => nodes,
27
+ "action" => "rpcutil.agent_inventory",
28
+ "silent" => true
29
+ )
30
+
31
+ success, msg, inventory = rpc.run
32
+
33
+ # rpcutil#agent_inventory is a hash in a hash not managed by the DDL
34
+ # this is not handled by the JSON encoding magic that does DDL based
35
+ # symbol and string conversion so we normalise the data always to symbol
36
+ # based structures
37
+ inventory.each do |node|
38
+ node["data"][:agents].each do |agent|
39
+ agent.keys.each do |key| # rubocop:disable Style/HashEachMethods
40
+ agent[key.intern] = agent.delete(key) if key.is_a?(String)
41
+ end
42
+ end
43
+ end
44
+
45
+ [success, msg, inventory]
46
+ end
47
+
48
+ # Validates agent versions on nodes
49
+ #
50
+ # @param agents [Hash] a hash of agent names and nodes that uses that agent
51
+ # @raise [StandardError] on failure
52
+ def validate_agents(agents)
53
+ nodes = agents.map {|_, agent_nodes| agent_nodes}.flatten.uniq
54
+
55
+ Log.info("Validating agent inventory on %d nodes" % nodes.size)
56
+
57
+ validation_fail = false
58
+
59
+ success, msg, inventory = agent_inventory(nodes)
60
+
61
+ raise("Could not determine agent inventory: %s" % msg) unless success
62
+
63
+ agents.each do |agent, agent_nodes|
64
+ unless @uses.include?(agent)
65
+ Log.warn("Agent %s is mentioned in node sets but not declared in the uses list" % agent)
66
+ validation_fail = true
67
+ next
68
+ end
69
+
70
+ agent_nodes.each do |node|
71
+ unless node_inventory = inventory.find {|i| i["sender"] == node}
72
+ Log.warn("Did not receive an inventory for node %s" % node)
73
+ validation_fail = true
74
+ next
75
+ end
76
+
77
+ unless metadata = node_inventory["data"][:agents].find {|i| i[:agent] == agent}
78
+ Log.warn("Node %s does not have the agent %s" % [node, agent])
79
+ validation_fail = true
80
+ next
81
+ end
82
+
83
+ if valid_version?(metadata[:version], @uses[agent])
84
+ Log.debug("Agent %s on %s version %s matches desired version %s" % [agent, node, metadata[:version], @uses[agent]])
85
+ else
86
+ Log.warn("Agent %s on %s version %s does not match desired version %s" % [agent, node, metadata[:version], @uses[agent]])
87
+ validation_fail = true
88
+ end
89
+ end
90
+ end
91
+
92
+ raise("Network agents did not match specified SemVer specifications in the playbook") if validation_fail
93
+
94
+ Log.info("Agent inventory on %d nodes validated" % nodes.size)
95
+ end
96
+
97
+ # Determines if a semver version is within a stated range
98
+ #
99
+ # @note mcollective never suggested semver, so versions like "1.1" becomes "1.1.0" for the compare
100
+ # @param have [String] SemVer of what you have
101
+ # @param want [String] SemVer range of what you need
102
+ # @return [Boolean]
103
+ # @raise [StandardError] on invalid version strings
104
+ def valid_version?(have, want)
105
+ have = "%s.0" % have if have.split(".").size == 2
106
+
107
+ require "semantic_puppet" unless defined?(SemanticPuppet)
108
+
109
+ semver_have = SemanticPuppet::Version.parse(have)
110
+ semver_want = SemanticPuppet::VersionRange.parse(want)
111
+ semver_want.include?(semver_have)
112
+ end
113
+
114
+ # Checks that all the declared agent DDLs exist
115
+ #
116
+ # @raise [StandardError] on invalid DDLs
117
+ def prepare
118
+ invalid = @uses.map do |agent, want|
119
+ begin
120
+ have = ddl_version(agent)
121
+
122
+ if valid_version?(have, want)
123
+ Log.debug("Agent %s DDL version %s matches desired %s" % [agent, have, want])
124
+ nil
125
+ else
126
+ Log.warn("Agent %s DDL version %s does not match desired %s" % [agent, have, want])
127
+ agent
128
+ end
129
+ rescue
130
+ Log.warn("Could not process DDL for agent %s: %s: %s" % [agent, $!.class, $!.to_s])
131
+ agent
132
+ end
133
+ end.compact
134
+
135
+ raise("DDLs for agent(s) %s did not match desired versions" % invalid.join(", ")) unless invalid.empty?
136
+ end
137
+
138
+ # Fetches the DDL version for an agent
139
+ #
140
+ # If the agent DDL has versions like 1.0 it will be
141
+ # turned into 1.0.0 as old mco stuff didnt do semver
142
+ #
143
+ # @param agent [String]
144
+ def ddl_version(agent)
145
+ ddl = agent_ddl(agent)
146
+ ddl.meta[:version]
147
+ end
148
+
149
+ # Returns the DDL for a specific agent
150
+ #
151
+ # @param agent [String]
152
+ # @return [DDL::AgentDDL]
153
+ # @raise [StandardError] should the DDL not exist
154
+ def agent_ddl(agent)
155
+ DDL::AgentDDL.new(agent)
156
+ end
157
+
158
+ def from_hash(data)
159
+ data.each do |agent, version|
160
+ Log.debug("Loading usage of %s version %s" % [agent, version])
161
+ @uses[agent] = version
162
+ end
163
+
164
+ self
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end