choria-mcorpc-support 2.21.1 → 2.23.1
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/lib/mcollective.rb +1 -2
- data/lib/mcollective/agent/bolt_tasks.ddl +253 -0
- data/lib/mcollective/agent/bolt_tasks.json +365 -0
- data/lib/mcollective/agent/bolt_tasks.rb +178 -0
- data/lib/mcollective/agent/choria_util.ddl +152 -0
- data/lib/mcollective/agent/choria_util.json +244 -0
- data/lib/mcollective/agent/rpcutil.ddl +8 -4
- data/lib/mcollective/agent/rpcutil.json +333 -0
- data/lib/mcollective/agent/scout.ddl +169 -0
- data/lib/mcollective/agent/scout.json +224 -0
- data/lib/mcollective/agents.rb +7 -6
- data/lib/mcollective/aggregate.rb +4 -4
- data/lib/mcollective/aggregate/average.rb +2 -2
- data/lib/mcollective/aggregate/base.rb +2 -2
- data/lib/mcollective/aggregate/result.rb +3 -3
- data/lib/mcollective/aggregate/result/collection_result.rb +2 -2
- data/lib/mcollective/aggregate/result/numeric_result.rb +2 -2
- data/lib/mcollective/aggregate/sum.rb +2 -2
- data/lib/mcollective/aggregate/summary.rb +3 -4
- data/lib/mcollective/application.rb +57 -21
- data/lib/mcollective/application/choria.rb +189 -0
- data/lib/mcollective/application/completion.rb +6 -6
- data/lib/mcollective/application/facts.rb +11 -11
- data/lib/mcollective/application/federation.rb +237 -0
- data/lib/mcollective/application/find.rb +4 -4
- data/lib/mcollective/application/help.rb +3 -3
- data/lib/mcollective/application/inventory.rb +3 -341
- data/lib/mcollective/application/ping.rb +5 -51
- data/lib/mcollective/application/playbook.rb +207 -0
- data/lib/mcollective/application/plugin.rb +106 -106
- data/lib/mcollective/application/rpc.rb +3 -108
- data/lib/mcollective/application/tasks.rb +425 -0
- data/lib/mcollective/applications.rb +11 -10
- data/lib/mcollective/audit/choria.rb +33 -0
- data/lib/mcollective/cache.rb +2 -4
- data/lib/mcollective/client.rb +11 -10
- data/lib/mcollective/config.rb +21 -34
- data/lib/mcollective/connector/base.rb +2 -1
- data/lib/mcollective/connector/nats.ddl +9 -0
- data/lib/mcollective/connector/nats.rb +450 -0
- data/lib/mcollective/data.rb +8 -3
- data/lib/mcollective/data/agent_data.rb +1 -1
- data/lib/mcollective/data/base.rb +6 -5
- data/lib/mcollective/data/bolt_task_data.ddl +90 -0
- data/lib/mcollective/data/bolt_task_data.rb +32 -0
- data/lib/mcollective/data/collective_data.rb +1 -1
- data/lib/mcollective/data/fact_data.rb +6 -6
- data/lib/mcollective/data/fstat_data.rb +2 -4
- data/lib/mcollective/data/result.rb +7 -2
- data/lib/mcollective/ddl/agentddl.rb +5 -17
- data/lib/mcollective/ddl/base.rb +10 -13
- data/lib/mcollective/discovery.rb +24 -39
- data/lib/mcollective/discovery/choria.ddl +11 -0
- data/lib/mcollective/discovery/choria.rb +223 -0
- data/lib/mcollective/discovery/flatfile.rb +7 -8
- data/lib/mcollective/discovery/mc.rb +2 -2
- data/lib/mcollective/discovery/stdin.rb +17 -18
- data/lib/mcollective/exceptions.rb +13 -0
- data/lib/mcollective/facts/base.rb +9 -9
- data/lib/mcollective/facts/yaml_facts.rb +12 -12
- data/lib/mcollective/generators.rb +3 -3
- data/lib/mcollective/generators/agent_generator.rb +3 -4
- data/lib/mcollective/generators/base.rb +14 -15
- data/lib/mcollective/generators/data_generator.rb +5 -6
- data/lib/mcollective/log.rb +2 -2
- data/lib/mcollective/logger/base.rb +3 -2
- data/lib/mcollective/logger/console_logger.rb +10 -10
- data/lib/mcollective/logger/file_logger.rb +7 -7
- data/lib/mcollective/logger/syslog_logger.rb +11 -15
- data/lib/mcollective/message.rb +8 -39
- data/lib/mcollective/monkey_patches.rb +2 -4
- data/lib/mcollective/optionparser.rb +2 -1
- data/lib/mcollective/pluginmanager.rb +3 -5
- data/lib/mcollective/pluginpackager.rb +1 -3
- data/lib/mcollective/pluginpackager/agent_definition.rb +3 -8
- data/lib/mcollective/pluginpackager/forge_packager.rb +7 -9
- data/lib/mcollective/pluginpackager/standard_definition.rb +1 -2
- data/lib/mcollective/registration/base.rb +18 -16
- data/lib/mcollective/rpc.rb +2 -4
- data/lib/mcollective/rpc/actionrunner.rb +16 -18
- data/lib/mcollective/rpc/agent.rb +26 -43
- data/lib/mcollective/rpc/audit.rb +1 -0
- data/lib/mcollective/rpc/client.rb +67 -85
- data/lib/mcollective/rpc/helpers.rb +55 -62
- data/lib/mcollective/rpc/progress.rb +2 -2
- data/lib/mcollective/rpc/reply.rb +17 -19
- data/lib/mcollective/rpc/request.rb +7 -5
- data/lib/mcollective/rpc/result.rb +6 -8
- data/lib/mcollective/rpc/stats.rb +49 -58
- data/lib/mcollective/security/base.rb +13 -56
- data/lib/mcollective/security/choria.rb +765 -0
- data/lib/mcollective/shell.rb +9 -4
- data/lib/mcollective/signer/base.rb +28 -0
- data/lib/mcollective/signer/choria.rb +185 -0
- data/lib/mcollective/ssl.rb +8 -6
- data/lib/mcollective/util.rb +58 -55
- data/lib/mcollective/util/bolt_support.rb +176 -0
- data/lib/mcollective/util/bolt_support/plan_runner.rb +167 -0
- data/lib/mcollective/util/bolt_support/task_result.rb +94 -0
- data/lib/mcollective/util/bolt_support/task_results.rb +128 -0
- data/lib/mcollective/util/choria.rb +946 -0
- data/lib/mcollective/util/indifferent_hash.rb +12 -0
- data/lib/mcollective/util/natswrapper.rb +242 -0
- data/lib/mcollective/util/playbook.rb +435 -0
- data/lib/mcollective/util/playbook/data_stores.rb +201 -0
- data/lib/mcollective/util/playbook/data_stores/base.rb +99 -0
- data/lib/mcollective/util/playbook/data_stores/consul_data_store.rb +88 -0
- data/lib/mcollective/util/playbook/data_stores/environment_data_store.rb +33 -0
- data/lib/mcollective/util/playbook/data_stores/etcd_data_store.rb +42 -0
- data/lib/mcollective/util/playbook/data_stores/file_data_store.rb +106 -0
- data/lib/mcollective/util/playbook/data_stores/shell_data_store.rb +103 -0
- data/lib/mcollective/util/playbook/inputs.rb +265 -0
- data/lib/mcollective/util/playbook/nodes.rb +207 -0
- data/lib/mcollective/util/playbook/nodes/mcollective_nodes.rb +86 -0
- data/lib/mcollective/util/playbook/nodes/pql_nodes.rb +40 -0
- data/lib/mcollective/util/playbook/nodes/shell_nodes.rb +55 -0
- data/lib/mcollective/util/playbook/nodes/terraform_nodes.rb +65 -0
- data/lib/mcollective/util/playbook/nodes/yaml_nodes.rb +47 -0
- data/lib/mcollective/util/playbook/playbook_logger.rb +47 -0
- data/lib/mcollective/util/playbook/puppet_logger.rb +51 -0
- data/lib/mcollective/util/playbook/report.rb +152 -0
- data/lib/mcollective/util/playbook/task_result.rb +55 -0
- data/lib/mcollective/util/playbook/tasks.rb +196 -0
- data/lib/mcollective/util/playbook/tasks/base.rb +45 -0
- data/lib/mcollective/util/playbook/tasks/graphite_event_task.rb +64 -0
- data/lib/mcollective/util/playbook/tasks/mcollective_task.rb +356 -0
- data/lib/mcollective/util/playbook/tasks/shell_task.rb +93 -0
- data/lib/mcollective/util/playbook/tasks/slack_task.rb +105 -0
- data/lib/mcollective/util/playbook/tasks/webhook_task.rb +136 -0
- data/lib/mcollective/util/playbook/template_util.rb +98 -0
- data/lib/mcollective/util/playbook/uses.rb +169 -0
- data/lib/mcollective/util/tasks_support.rb +751 -0
- data/lib/mcollective/util/tasks_support/cli.rb +260 -0
- data/lib/mcollective/util/tasks_support/default_formatter.rb +138 -0
- data/lib/mcollective/util/tasks_support/json_formatter.rb +108 -0
- data/lib/mcollective/validator.rb +6 -1
- data/lib/mcollective/validator/bolt_task_name_validator.ddl +7 -0
- data/lib/mcollective/validator/bolt_task_name_validator.rb +11 -0
- data/lib/mcollective/validator/length_validator.rb +1 -3
- metadata +65 -6
- data/lib/mcollective/application/describe_filter.rb +0 -87
- data/lib/mcollective/matcher.rb +0 -220
- data/lib/mcollective/matcher/parser.rb +0 -128
- data/lib/mcollective/matcher/scanner.rb +0 -241
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module MCollective
|
|
6
|
+
module Util
|
|
7
|
+
class TasksSupport
|
|
8
|
+
attr_reader :cache_dir, :choria
|
|
9
|
+
|
|
10
|
+
def initialize(choria, cache_dir=nil)
|
|
11
|
+
@choria = choria
|
|
12
|
+
@cache_dir = cache_dir || @choria.get_option("choria.tasks_cache")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Creates an instance of the CLI helpers
|
|
16
|
+
#
|
|
17
|
+
# @param format [:json, :default] the output format to use
|
|
18
|
+
# @return [CLI]
|
|
19
|
+
def cli(format, verbose)
|
|
20
|
+
require_relative "tasks_support/cli"
|
|
21
|
+
CLI.new(self, format, verbose)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Converts a Puppet type into something mcollective understands
|
|
25
|
+
#
|
|
26
|
+
# This is inevitably hacky by its nature, there is no way for me to
|
|
27
|
+
# parse the types. PAL might get some helpers for this but till then
|
|
28
|
+
# this is going to have to be best efforts.
|
|
29
|
+
#
|
|
30
|
+
# When there is a too complex situation users can always put in --input
|
|
31
|
+
# and some JSON to work around it until something better comes around
|
|
32
|
+
#
|
|
33
|
+
# @param type [String] a puppet type
|
|
34
|
+
# @return [Class, Boolean, Boolean] The data type, if its an array input or not and if its required
|
|
35
|
+
def puppet_type_to_ruby(type)
|
|
36
|
+
array = false
|
|
37
|
+
required = true
|
|
38
|
+
|
|
39
|
+
if type =~ /Optional\[(.+)/
|
|
40
|
+
type = $1
|
|
41
|
+
required = false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if type =~ /Array\[(.+)/
|
|
45
|
+
type = $1
|
|
46
|
+
array = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return [Numeric, array, required] if type =~ /Integer/
|
|
50
|
+
return [Numeric, array, required] if type =~ /Float/
|
|
51
|
+
return [Hash, array, required] if type =~ /Hash/
|
|
52
|
+
return [:boolean, array, required] if type =~ /Boolean/
|
|
53
|
+
|
|
54
|
+
[String, array, required]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Determines if a machine is compatible with running bolt
|
|
58
|
+
#
|
|
59
|
+
# @note this should check for a compatible version of Puppet more
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def tasks_compatible?
|
|
62
|
+
File.exist?(wrapper_path) && File.executable?(wrapper_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# AIO path to binaries like wrappers etc
|
|
66
|
+
def aio_bin_path
|
|
67
|
+
if Util.windows?
|
|
68
|
+
'C:\Program Files\Puppet Labs\Puppet\bin'
|
|
69
|
+
else
|
|
70
|
+
"/opt/puppetlabs/puppet/bin"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Path to the AIO task wrapper executable
|
|
75
|
+
#
|
|
76
|
+
# @return [String]
|
|
77
|
+
def aio_wrapper_path
|
|
78
|
+
if Util.windows?
|
|
79
|
+
legacy = File.join(aio_bin_path, "task_wrapper.exe")
|
|
80
|
+
return legacy if File.exist?(legacy)
|
|
81
|
+
|
|
82
|
+
File.join(aio_bin_path, "execution_wrapper.exe")
|
|
83
|
+
else
|
|
84
|
+
legacy = File.join(aio_bin_path, "task_wrapper")
|
|
85
|
+
return legacy if File.exist?(legacy)
|
|
86
|
+
|
|
87
|
+
File.join(aio_bin_path, "execution_wrapper")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Path to the task wrapper executable
|
|
92
|
+
#
|
|
93
|
+
# @return [String]
|
|
94
|
+
def wrapper_path
|
|
95
|
+
@choria.get_option("choria.tasks.wrapper_path", aio_wrapper_path)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Path to the powershell shim for powershell input method
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
def ps_shim_path
|
|
102
|
+
File.join(aio_bin_path, "PowershellShim.ps1")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Expands the path into a platform specific version
|
|
106
|
+
#
|
|
107
|
+
# @see https://github.com/puppetlabs/puppet-specifications/tree/730a2aa23e58b93387d194dbac64af508bdeab01/tasks#task-execution
|
|
108
|
+
# @param path [Array<String>] the path to the executable and any arguments
|
|
109
|
+
# @raise [StandardError] when execution of a specific file is not supported
|
|
110
|
+
def platform_specific_command(path)
|
|
111
|
+
return [path] unless Util.windows?
|
|
112
|
+
|
|
113
|
+
extension = File.extname(path)
|
|
114
|
+
|
|
115
|
+
# https://github.com/puppetlabs/pxp-agent/blob/3e7cada3cedf7f78703781d44e70010d0c5ad209/lib/src/modules/task.cc#L98-L107
|
|
116
|
+
case extension
|
|
117
|
+
when ".rb"
|
|
118
|
+
["ruby", path]
|
|
119
|
+
when ".pp"
|
|
120
|
+
["puppet", "apply", path]
|
|
121
|
+
when ".ps1"
|
|
122
|
+
["powershell", "-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-File", path]
|
|
123
|
+
else
|
|
124
|
+
[path]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Given a task description checks all files are correctly cached
|
|
129
|
+
#
|
|
130
|
+
# @note this checks all files, though for now there's only ever one file
|
|
131
|
+
# @see #task_file?
|
|
132
|
+
# @param files [Array] files list
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def cached?(files)
|
|
135
|
+
files.map {|f| task_file?(f)}.all?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Given a task spec figures out the input method
|
|
139
|
+
#
|
|
140
|
+
# @param task [Hash] task specification
|
|
141
|
+
# @return ["powershell", "both", "stdin", "environment"]
|
|
142
|
+
def task_input_method(task)
|
|
143
|
+
# the spec says only 1 executable, no idea what the point of the 'files' is
|
|
144
|
+
file_extension = File.extname(task["files"][0]["filename"])
|
|
145
|
+
|
|
146
|
+
input_method = task["input_method"]
|
|
147
|
+
input_method = "powershell" if input_method.nil? && file_extension == ".ps1"
|
|
148
|
+
input_method ||= "both"
|
|
149
|
+
|
|
150
|
+
input_method
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Given a task spec figures out the command to run using the wrapper
|
|
154
|
+
#
|
|
155
|
+
# @param spooldir [String] path to the spool for this specific request
|
|
156
|
+
# @param task [Hash] task specification
|
|
157
|
+
# @return [String] path to the command
|
|
158
|
+
def task_command(spooldir, task)
|
|
159
|
+
file_spec = task["files"][0]
|
|
160
|
+
file_name = File.join(spooldir, "files", file_spec["filename"])
|
|
161
|
+
|
|
162
|
+
command = platform_specific_command(file_name)
|
|
163
|
+
|
|
164
|
+
command.unshift(ps_shim_path) if task_input_method(task) == "powershell"
|
|
165
|
+
|
|
166
|
+
command
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Given a task spec calculates the correct environment hash
|
|
170
|
+
#
|
|
171
|
+
# @param task [Hash] task specification
|
|
172
|
+
# @param task_id [String] task id - usually the mcollective request id
|
|
173
|
+
# @param task_caller [String] the caller invoking the task
|
|
174
|
+
# @return [Hash]
|
|
175
|
+
def task_environment(task, task_id, task_caller)
|
|
176
|
+
environment = {
|
|
177
|
+
"_task" => task["task"],
|
|
178
|
+
"_choria_task_id" => task_id,
|
|
179
|
+
"_choria_task_caller" => task_caller
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return environment unless task["input"]
|
|
183
|
+
return environment unless ["both", "environment"].include?(task_input_method(task))
|
|
184
|
+
|
|
185
|
+
JSON.parse(task["input"]).each do |k, v|
|
|
186
|
+
environment["PT_%s" % k] = v.to_s
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
environment["PT__installdir"] = File.join(request_spooldir(task_id), "files")
|
|
190
|
+
|
|
191
|
+
environment
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Generate the path to the spool for a specific request
|
|
195
|
+
#
|
|
196
|
+
# @param requestid [String] task id
|
|
197
|
+
# @return [String] directory
|
|
198
|
+
def request_spooldir(requestid)
|
|
199
|
+
File.join(choria.tasks_spool_dir, requestid)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Generates the spool path and create it
|
|
203
|
+
#
|
|
204
|
+
# @param requestid [String] unique mco request id
|
|
205
|
+
# @param task [Hash] task specification
|
|
206
|
+
# @return [String] path to the spool dir
|
|
207
|
+
# @raise [StandardError] should it not be able to make the directory
|
|
208
|
+
def create_request_spooldir(requestid, task)
|
|
209
|
+
dir = request_spooldir(requestid)
|
|
210
|
+
|
|
211
|
+
FileUtils.mkdir_p(dir, :mode => 0o0750)
|
|
212
|
+
|
|
213
|
+
populate_spooldir(dir, task)
|
|
214
|
+
|
|
215
|
+
dir
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Copy task files to the spool directory
|
|
219
|
+
# @param spooldir [String] path to the spool dir
|
|
220
|
+
# @param task [Hash] task specification
|
|
221
|
+
def populate_spooldir(spooldir, task)
|
|
222
|
+
task["files"].each do |file|
|
|
223
|
+
spool_filename = File.join(spooldir, "files", file["filename"])
|
|
224
|
+
|
|
225
|
+
FileUtils.mkdir_p(File.dirname(spool_filename), :mode => 0o0750)
|
|
226
|
+
FileUtils.cp(task_file_name(file), spool_filename)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Given a task spec, creates the standard input
|
|
231
|
+
#
|
|
232
|
+
# @param task [Hash] task specification
|
|
233
|
+
# @return [Hash, nil]
|
|
234
|
+
def task_input(task)
|
|
235
|
+
task["input"] if ["both", "powershell", "stdin"].include?(task_input_method(task))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Runs the wrapper command detached from mcollective
|
|
239
|
+
#
|
|
240
|
+
# We always detach we have no idea how long these tasks will run
|
|
241
|
+
# since people can do whatever they like, we'll then check them
|
|
242
|
+
# till the agent timeout but if timeout happens they keep running
|
|
243
|
+
#
|
|
244
|
+
# The idea is that UI will in that case present the user with a request
|
|
245
|
+
# id - which is also the spool name - and the user can later come and
|
|
246
|
+
# act on these tasks either by asking for their status or perhaps killing
|
|
247
|
+
# them?
|
|
248
|
+
#
|
|
249
|
+
# @param command [Array<String>] command to run
|
|
250
|
+
# @param environment [Hash] environment to run with
|
|
251
|
+
# @param stdin [String] stdin to send to the command
|
|
252
|
+
# @param spooldir [String] path to the spool for this specific request
|
|
253
|
+
# @param run_as [String] name of the user who will run the command
|
|
254
|
+
# @return [Integer] the pid that was spawned
|
|
255
|
+
def spawn_command(command, environment, stdin, spooldir, run_as)
|
|
256
|
+
wrapper_input = File.join(spooldir, "wrapper_stdin")
|
|
257
|
+
wrapper_stdout = File.join(spooldir, "wrapper_stdout")
|
|
258
|
+
wrapper_stderr = File.join(spooldir, "wrapper_stderr")
|
|
259
|
+
wrapper_pid = File.join(spooldir, "wrapper_pid")
|
|
260
|
+
|
|
261
|
+
options = {
|
|
262
|
+
:chdir => "/",
|
|
263
|
+
:in => :close,
|
|
264
|
+
:out => wrapper_stdout,
|
|
265
|
+
:err => wrapper_stderr
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if stdin
|
|
269
|
+
File.open(wrapper_input, "w") {|i| i.print(stdin) }
|
|
270
|
+
options[:in] = wrapper_input
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if run_as
|
|
274
|
+
raise("System does not allow forking. run_as not usable.") unless Process.respond_to?(:fork)
|
|
275
|
+
|
|
276
|
+
require "etc"
|
|
277
|
+
|
|
278
|
+
u = Etc.getpwnam(run_as)
|
|
279
|
+
|
|
280
|
+
FileUtils.chown_R(u.uid, u.gid, spooldir)
|
|
281
|
+
|
|
282
|
+
pid = Process.fork
|
|
283
|
+
if pid.nil?
|
|
284
|
+
Process.gid = Process.egid = u.gid
|
|
285
|
+
Process.uid = Process.euid = u.uid
|
|
286
|
+
Process.exec(environment, command, options)
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
pid = Process.spawn(environment, command, options)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
sleep 0.1 until File.exist?(wrapper_stdout)
|
|
293
|
+
|
|
294
|
+
File.open(wrapper_pid, "w") {|p| p.write(pid)}
|
|
295
|
+
|
|
296
|
+
Process.detach(pid)
|
|
297
|
+
|
|
298
|
+
pid
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Determines if a task already ran by checkinf if its spool exist
|
|
302
|
+
#
|
|
303
|
+
# @param requestid [String] request id for the task
|
|
304
|
+
# @return [Boolean]
|
|
305
|
+
def task_ran?(requestid)
|
|
306
|
+
File.directory?(request_spooldir(requestid))
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Determines if a task is completed
|
|
310
|
+
#
|
|
311
|
+
# Tasks are run under the wrapper which will write the existcode
|
|
312
|
+
# to a file only after the command have exited, so this will wait
|
|
313
|
+
# for that to appear
|
|
314
|
+
#
|
|
315
|
+
# @param requestid [String] request id for the task
|
|
316
|
+
# @return [Boolean]
|
|
317
|
+
def task_complete?(requestid)
|
|
318
|
+
exitcode = File.join(request_spooldir(requestid), "exitcode")
|
|
319
|
+
wrapper_stderr = File.join(request_spooldir(requestid), "wrapper_stderr")
|
|
320
|
+
|
|
321
|
+
File.exist?(wrapper_stderr) && file_size(wrapper_stderr) > 0 || File.exist?(exitcode) && file_size(exitcode) > 0
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Waits for a task to complete
|
|
325
|
+
#
|
|
326
|
+
# @param requestid [String] request id for the task
|
|
327
|
+
def wait_for_task_completion(requestid)
|
|
328
|
+
sleep 0.1 until task_complete?(requestid)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Given a task spec runs it via the Puppet wrappers
|
|
332
|
+
#
|
|
333
|
+
# The task is run in the background and this method waits for it to
|
|
334
|
+
# finish, but should the thread this method runs in be killed the process
|
|
335
|
+
# will continue and one can later check again using the request id
|
|
336
|
+
#
|
|
337
|
+
# @note before this should be run be sure to download the tasks first
|
|
338
|
+
# @param requestid [String] the task requestid
|
|
339
|
+
# @param task [Hash] task specification
|
|
340
|
+
# @param wait [Boolean] should the we wait for the task to complete
|
|
341
|
+
# @param callerid [String] the mcollective callerid who is running the task
|
|
342
|
+
# @return [Hash] the task result as per {#task_result}
|
|
343
|
+
# @raise [StandardError] when calling the wrapper fails etc
|
|
344
|
+
def run_task_command(requestid, task, wait=true, callerid="local")
|
|
345
|
+
raise("The task wrapper %s does not exist, please upgrade Puppet" % wrapper_path) unless File.exist?(wrapper_path)
|
|
346
|
+
raise("Task %s is not available or does not match the specification, please download it" % task["task"]) unless cached?(task["files"])
|
|
347
|
+
raise("Task spool for request %s already exist, cannot rerun", requestid) if task_ran?(requestid)
|
|
348
|
+
|
|
349
|
+
spool = create_request_spooldir(requestid, task)
|
|
350
|
+
command = task_command(spool, task)
|
|
351
|
+
|
|
352
|
+
Log.debug("Trying to spawn task %s in spool %s using command %s" % [task["task"], spool, command])
|
|
353
|
+
|
|
354
|
+
wrapper_input = {
|
|
355
|
+
"executable" => command[0],
|
|
356
|
+
"arguments" => command[1..-1],
|
|
357
|
+
"input" => task_input(task),
|
|
358
|
+
"stdout" => File.join(spool, "stdout"),
|
|
359
|
+
"stderr" => File.join(spool, "stderr"),
|
|
360
|
+
"exitcode" => File.join(spool, "exitcode")
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
File.open(File.join(spool, "choria.json"), "w") do |meta|
|
|
364
|
+
data = {
|
|
365
|
+
"start_time" => Time.now.utc.to_i,
|
|
366
|
+
"caller" => callerid,
|
|
367
|
+
"task" => task["task"],
|
|
368
|
+
"request" => wrapper_input
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
meta.print(data.to_json)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
pid = spawn_command(wrapper_path, task_environment(task, requestid, callerid), wrapper_input.to_json, spool, task["run_as"])
|
|
375
|
+
|
|
376
|
+
Log.info("Spawned task %s in spool %s with pid %s" % [task["task"], spool, pid])
|
|
377
|
+
|
|
378
|
+
wait_for_task_completion(requestid) if wait
|
|
379
|
+
|
|
380
|
+
task_status(requestid)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Determines how long a task ran for
|
|
384
|
+
#
|
|
385
|
+
# Tasks that had wrapper failures will have a 0 run time, still
|
|
386
|
+
# running tasks will calculate runtime till now and so increase on
|
|
387
|
+
# each invocation
|
|
388
|
+
#
|
|
389
|
+
# @param requestid [String] the request if for the task
|
|
390
|
+
# @return [Float]
|
|
391
|
+
def task_runtime(requestid)
|
|
392
|
+
spool = request_spooldir(requestid)
|
|
393
|
+
wrapper_stderr = File.join(spool, "wrapper_stderr")
|
|
394
|
+
wrapper_pid = File.join(spool, "wrapper_pid")
|
|
395
|
+
exitcode = File.join(spool, "exitcode")
|
|
396
|
+
|
|
397
|
+
if task_complete?(requestid) && File.exist?(exitcode)
|
|
398
|
+
Float(File::Stat.new(exitcode).mtime - File::Stat.new(wrapper_pid).mtime)
|
|
399
|
+
elsif task_complete?(requestid) && file_size(wrapper_stderr) > 0
|
|
400
|
+
0.0
|
|
401
|
+
else
|
|
402
|
+
Float(Time.now - File::Stat.new(wrapper_pid).mtime)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Parses the stdout and turns it into a JSON object
|
|
407
|
+
#
|
|
408
|
+
# If the output is JSON parsable the output is returned else
|
|
409
|
+
# it's wrapped in _output as per the Tasks spec version 1
|
|
410
|
+
#
|
|
411
|
+
# @note https://github.com/puppetlabs/puppet-specifications/blob/730a2aa23e58b93387d194dbac64af508bdeab01/tasks/README.md#output-handling
|
|
412
|
+
# @param stdout [String] the stdout from the script
|
|
413
|
+
# @param completed [Boolean] if the task is done running
|
|
414
|
+
# @param exitcode [Integer] the exitcode from the script
|
|
415
|
+
# @param wrapper_output [String] the wrapper output
|
|
416
|
+
# @return [Object] the new stdout according to spec and the stdout object, not JSON encoded
|
|
417
|
+
def create_task_stdout(stdout, completed, exitcode, wrapper_output)
|
|
418
|
+
result = {}
|
|
419
|
+
|
|
420
|
+
unless wrapper_output.empty?
|
|
421
|
+
result["_error"] = {
|
|
422
|
+
"kind" => "choria.tasks/wrapper-error",
|
|
423
|
+
"msg" => "The task wrapper failed to run",
|
|
424
|
+
"details" => {
|
|
425
|
+
"wrapper_output" => wrapper_output
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return result.to_json
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
begin
|
|
433
|
+
data = JSON.parse(stdout)
|
|
434
|
+
|
|
435
|
+
if data.is_a?(Hash)
|
|
436
|
+
result = data
|
|
437
|
+
else
|
|
438
|
+
result["_output"] = stdout
|
|
439
|
+
end
|
|
440
|
+
rescue
|
|
441
|
+
result["_output"] = stdout
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
if exitcode != 0 && completed && !result["_error"]
|
|
445
|
+
result["_error"] = {
|
|
446
|
+
"kind" => "choria.tasks/task-error",
|
|
447
|
+
"msg" => "The task errored with a code %d" % exitcode,
|
|
448
|
+
"details" => {
|
|
449
|
+
"exitcode" => exitcode
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
result
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Determines if a task failed based on its status
|
|
458
|
+
#
|
|
459
|
+
# @param status [Hash] the status as produced by {#task_status}
|
|
460
|
+
# @return [Boolean]
|
|
461
|
+
def task_failed?(status)
|
|
462
|
+
return true unless status["wrapper_spawned"]
|
|
463
|
+
return true unless status["wrapper_pid"]
|
|
464
|
+
return true unless status["wrapper_error"].empty?
|
|
465
|
+
return true if status["exitcode"] != 0 && status["completed"]
|
|
466
|
+
return true if status["stdout"].include?("_error")
|
|
467
|
+
|
|
468
|
+
false
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Determines the task status for given request
|
|
472
|
+
#
|
|
473
|
+
# @param requestid [String] request id for the task
|
|
474
|
+
# @return [Hash] the task status
|
|
475
|
+
def task_status(requestid)
|
|
476
|
+
raise("Task %s have not been requested" % requestid) unless task_ran?(requestid)
|
|
477
|
+
|
|
478
|
+
spool = request_spooldir(requestid)
|
|
479
|
+
stdout = File.join(spool, "stdout")
|
|
480
|
+
stderr = File.join(spool, "stderr")
|
|
481
|
+
exitcode = File.join(spool, "exitcode")
|
|
482
|
+
wrapper_stderr = File.join(spool, "wrapper_stderr")
|
|
483
|
+
wrapper_pid = File.join(spool, "wrapper_pid")
|
|
484
|
+
meta = File.join(spool, "choria.json")
|
|
485
|
+
|
|
486
|
+
result = {
|
|
487
|
+
"spool" => spool,
|
|
488
|
+
"task" => nil,
|
|
489
|
+
"caller" => nil,
|
|
490
|
+
"stdout" => "",
|
|
491
|
+
"stderr" => "",
|
|
492
|
+
"exitcode" => -1,
|
|
493
|
+
"runtime" => task_runtime(requestid),
|
|
494
|
+
"start_time" => Time.at(0).utc,
|
|
495
|
+
"wrapper_spawned" => false,
|
|
496
|
+
"wrapper_error" => "",
|
|
497
|
+
"wrapper_pid" => nil,
|
|
498
|
+
"completed" => task_complete?(requestid)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
result["exitcode"] = Integer(File.read(exitcode)) if File.exist?(exitcode)
|
|
502
|
+
|
|
503
|
+
if task_ran?(requestid)
|
|
504
|
+
result["stdout"] = File.read(stdout) if File.exist?(stdout)
|
|
505
|
+
result["stderr"] = File.read(stderr) if File.exist?(stderr)
|
|
506
|
+
result["wrapper_spawned"] = File.exist?(wrapper_stderr) && file_size(wrapper_stderr) == 0
|
|
507
|
+
|
|
508
|
+
result["wrapper_error"] = File.read(wrapper_stderr) if File.exist?(wrapper_stderr) && file_size(wrapper_stderr) > 0
|
|
509
|
+
|
|
510
|
+
if File.exist?(wrapper_pid) && file_size(wrapper_pid) > 0
|
|
511
|
+
result["start_time"] = File::Stat.new(wrapper_pid).mtime.utc
|
|
512
|
+
result["wrapper_pid"] = Integer(File.read(wrapper_pid))
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
if File.exist?(meta)
|
|
517
|
+
choria_metadata = JSON.parse(File.read(meta))
|
|
518
|
+
|
|
519
|
+
result["start_time"] = Time.at(choria_metadata["start_time"]).utc
|
|
520
|
+
result["caller"] = choria_metadata["caller"]
|
|
521
|
+
result["task"] = choria_metadata["task"]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
result["stdout"] = create_task_stdout(
|
|
525
|
+
result["stdout"],
|
|
526
|
+
result["completed"],
|
|
527
|
+
result["exitcode"],
|
|
528
|
+
result["wrapper_error"]
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
result
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Retrieves the list of known tasks in an environment
|
|
535
|
+
#
|
|
536
|
+
# @param environment [String] the environment to query
|
|
537
|
+
# @return [Hash] the v3 task list
|
|
538
|
+
# @raise [StandardError] on http failure
|
|
539
|
+
def tasks(environment)
|
|
540
|
+
resp = http_get("/puppet/v3/tasks?environment=%s" % [environment])
|
|
541
|
+
|
|
542
|
+
raise("Failed to retrieve task list: %s: %s" % [$!.class, $!.to_s]) unless resp.code == "200"
|
|
543
|
+
|
|
544
|
+
tasks = JSON.parse(resp.body)
|
|
545
|
+
|
|
546
|
+
tasks.sort_by {|t| t["name"]}
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Retrieves the list of known task names
|
|
550
|
+
#
|
|
551
|
+
# @param environment [String] the environment to query
|
|
552
|
+
# @return [Array<String>] list of task names
|
|
553
|
+
# @raise [StandardError] on http failure
|
|
554
|
+
def task_names(environment)
|
|
555
|
+
tasks(environment).map {|t| t["name"]}
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Parse a task name like module::task into it's 2 pieces
|
|
559
|
+
#
|
|
560
|
+
# @param task [String]
|
|
561
|
+
# @return [Array<String>] 2 part array, first the module name then the task name
|
|
562
|
+
# @raise [StandardError] for invalid task names
|
|
563
|
+
def parse_task(task)
|
|
564
|
+
parts = task.split("::")
|
|
565
|
+
|
|
566
|
+
parts << "init" if parts.size == 1
|
|
567
|
+
|
|
568
|
+
parts
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Determines the cache path for a task file
|
|
572
|
+
#
|
|
573
|
+
# @param file [Hash] a file hash as per the task metadata
|
|
574
|
+
# @return [String] the directory the file would go into
|
|
575
|
+
def task_file_name(file)
|
|
576
|
+
File.join(cache_dir, file["sha256"])
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Does a HTTP GET against the Puppet Server
|
|
580
|
+
#
|
|
581
|
+
# @param path [String] the path to get
|
|
582
|
+
# @param headers [Hash] headers to passs
|
|
583
|
+
# @return [Net::HTTPRequest]
|
|
584
|
+
def http_get(path, headers={}, &blk)
|
|
585
|
+
transport = choria.https(choria.puppet_server, true)
|
|
586
|
+
request = choria.http_get(path)
|
|
587
|
+
|
|
588
|
+
headers.each do |k, v|
|
|
589
|
+
request[k] = v
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
transport.request(request, &blk)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Requests a task metadata from Puppet Server
|
|
596
|
+
#
|
|
597
|
+
# @param task [String] a task name like module::task
|
|
598
|
+
# @param environment [String] the puppet environmnet like production
|
|
599
|
+
# @return [Hash] the metadata according to the v3 spec
|
|
600
|
+
# @raise [StandardError] when the request failed
|
|
601
|
+
def task_metadata(task, environment)
|
|
602
|
+
parsed = parse_task(task)
|
|
603
|
+
path = "/puppet/v3/tasks/%s/%s?environment=%s" % [parsed[0], parsed[1], environment]
|
|
604
|
+
|
|
605
|
+
resp = http_get(path)
|
|
606
|
+
|
|
607
|
+
raise("Failed to request task metadata: %s: %s" % [resp.code, resp.body]) unless resp.code == "200"
|
|
608
|
+
|
|
609
|
+
result = JSON.parse(resp.body)
|
|
610
|
+
|
|
611
|
+
result["metadata"] ||= {}
|
|
612
|
+
result["metadata"]["parameters"] ||= {}
|
|
613
|
+
result["files"] ||= []
|
|
614
|
+
|
|
615
|
+
result
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Validates that the inputs would be acceptable to the task
|
|
619
|
+
#
|
|
620
|
+
# @note Copied from PAL TaskSignature#runnable_with?
|
|
621
|
+
# @param inputs [Hash] of keys and values
|
|
622
|
+
# @param task [Hash] task metadata
|
|
623
|
+
# @return [Array[Boolean, Error]]
|
|
624
|
+
def validate_task_inputs(inputs, task)
|
|
625
|
+
return [true, ""] unless task["metadata"]["parameters"]
|
|
626
|
+
return [true, ""] if task["metadata"]["parameters"].empty? && inputs.empty?
|
|
627
|
+
|
|
628
|
+
require "puppet"
|
|
629
|
+
|
|
630
|
+
signature = {}
|
|
631
|
+
|
|
632
|
+
task["metadata"]["parameters"].each do |k, v|
|
|
633
|
+
signature[k] = Puppet::Pops::Types::TypeParser.singleton.parse(v["type"])
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
signature_type = Puppet::Pops::Types::TypeFactory.struct(signature)
|
|
637
|
+
|
|
638
|
+
return [true, ""] if signature_type.instance?(inputs)
|
|
639
|
+
|
|
640
|
+
tm = Puppet::Pops::Types::TypeMismatchDescriber.singleton
|
|
641
|
+
reason = tm.describe_struct_signature(signature_type, inputs).flatten.map(&:format).join("\n")
|
|
642
|
+
reason = "\nInvalid input: \n\t%s" % [reason]
|
|
643
|
+
|
|
644
|
+
[false, reason]
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Calculates a hex digest SHA256 for a specific file
|
|
648
|
+
#
|
|
649
|
+
# @param file_path [String] a full path to the file to check
|
|
650
|
+
# @return [String]
|
|
651
|
+
# @raise [StandardError] when the file does not exist
|
|
652
|
+
def file_sha256(file_path)
|
|
653
|
+
Digest::SHA256.file(file_path).hexdigest
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Determines the file size of a specific file
|
|
657
|
+
#
|
|
658
|
+
# @param file_path [String] a full path to the file to check
|
|
659
|
+
# @return [Integer] bytes, -1 when the file does not exist
|
|
660
|
+
def file_size(file_path)
|
|
661
|
+
File.stat(file_path).size
|
|
662
|
+
rescue
|
|
663
|
+
-1
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Validates a task cache file
|
|
667
|
+
#
|
|
668
|
+
# @param file [Hash] a file hash as per the task metadata
|
|
669
|
+
# @return [Boolean]
|
|
670
|
+
def task_file?(file)
|
|
671
|
+
file_name = task_file_name(file)
|
|
672
|
+
|
|
673
|
+
Log.debug("Checking if file %s is cached using %s" % [file_name, file.pretty_inspect])
|
|
674
|
+
|
|
675
|
+
return false unless File.exist?(file_name)
|
|
676
|
+
return false unless file_size(file_name) == file["size_bytes"]
|
|
677
|
+
return false unless file_sha256(file_name) == file["sha256"]
|
|
678
|
+
|
|
679
|
+
true
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Attempts to download and cache the file
|
|
683
|
+
#
|
|
684
|
+
# @note Does not first check if the cache is ok, unconditionally downloads
|
|
685
|
+
# @see #task_file?
|
|
686
|
+
# @param file [Hash] a file hash as per the task metadata
|
|
687
|
+
# @raise [StandardError] when downloading fails
|
|
688
|
+
def cache_task_file(file)
|
|
689
|
+
path = [file["uri"]["path"], URI.encode_www_form(file["uri"]["params"])].join("?")
|
|
690
|
+
|
|
691
|
+
file_name = task_file_name(file)
|
|
692
|
+
|
|
693
|
+
Log.debug("Caching task to %s" % file_name)
|
|
694
|
+
|
|
695
|
+
http_get(path, "Accept" => "application/octet-stream") do |resp|
|
|
696
|
+
raise("Failed to request task content %s: %s: %s" % [path, resp.code, resp.body]) unless resp.code == "200"
|
|
697
|
+
|
|
698
|
+
FileUtils.mkdir_p(cache_dir, :mode => 0o0750)
|
|
699
|
+
FileUtils.rm_rf(file_name) if File.directory?(file_name)
|
|
700
|
+
|
|
701
|
+
task_file = Tempfile.new("tasks_%s" % file["filename"])
|
|
702
|
+
task_file.binmode
|
|
703
|
+
|
|
704
|
+
resp.read_body do |segment|
|
|
705
|
+
task_file.write(segment)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
task_file.close
|
|
709
|
+
|
|
710
|
+
FileUtils.chmod(0o0750, task_file.path)
|
|
711
|
+
FileUtils.mv(task_file.path, file_name)
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Downloads and caches a file set
|
|
716
|
+
#
|
|
717
|
+
# @param files [Array] the files description
|
|
718
|
+
# @return [Boolean] indicating download success
|
|
719
|
+
# @raise [StandardError] on download failures
|
|
720
|
+
def download_files(files)
|
|
721
|
+
Log.info("Downloading %d task files" % files.size)
|
|
722
|
+
|
|
723
|
+
files.each do |file|
|
|
724
|
+
next if task_file?(file)
|
|
725
|
+
|
|
726
|
+
try = 0
|
|
727
|
+
|
|
728
|
+
begin
|
|
729
|
+
return false if try == 2
|
|
730
|
+
|
|
731
|
+
try += 1
|
|
732
|
+
|
|
733
|
+
Log.debug("Downloading task file %s (try %s/2)" % [file["filename"], try])
|
|
734
|
+
|
|
735
|
+
cache_task_file(file)
|
|
736
|
+
rescue
|
|
737
|
+
Log.error(msg = "Could not download task file: %s: %s" % [$!.class, $!.to_s])
|
|
738
|
+
|
|
739
|
+
sleep 0.1
|
|
740
|
+
|
|
741
|
+
retry if try < 2
|
|
742
|
+
|
|
743
|
+
raise(msg)
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
true
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
end
|