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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mcollective.rb +1 -2
  3. data/lib/mcollective/agent/bolt_tasks.ddl +253 -0
  4. data/lib/mcollective/agent/bolt_tasks.json +365 -0
  5. data/lib/mcollective/agent/bolt_tasks.rb +178 -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 +8 -4
  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 +189 -0
  23. data/lib/mcollective/application/completion.rb +6 -6
  24. data/lib/mcollective/application/facts.rb +11 -11
  25. data/lib/mcollective/application/federation.rb +237 -0
  26. data/lib/mcollective/application/find.rb +4 -4
  27. data/lib/mcollective/application/help.rb +3 -3
  28. data/lib/mcollective/application/inventory.rb +3 -341
  29. data/lib/mcollective/application/ping.rb +5 -51
  30. data/lib/mcollective/application/playbook.rb +207 -0
  31. data/lib/mcollective/application/plugin.rb +106 -106
  32. data/lib/mcollective/application/rpc.rb +3 -108
  33. data/lib/mcollective/application/tasks.rb +425 -0
  34. data/lib/mcollective/applications.rb +11 -10
  35. data/lib/mcollective/audit/choria.rb +33 -0
  36. data/lib/mcollective/cache.rb +2 -4
  37. data/lib/mcollective/client.rb +11 -10
  38. data/lib/mcollective/config.rb +21 -34
  39. data/lib/mcollective/connector/base.rb +2 -1
  40. data/lib/mcollective/connector/nats.ddl +9 -0
  41. data/lib/mcollective/connector/nats.rb +450 -0
  42. data/lib/mcollective/data.rb +8 -3
  43. data/lib/mcollective/data/agent_data.rb +1 -1
  44. data/lib/mcollective/data/base.rb +6 -5
  45. data/lib/mcollective/data/bolt_task_data.ddl +90 -0
  46. data/lib/mcollective/data/bolt_task_data.rb +32 -0
  47. data/lib/mcollective/data/collective_data.rb +1 -1
  48. data/lib/mcollective/data/fact_data.rb +6 -6
  49. data/lib/mcollective/data/fstat_data.rb +2 -4
  50. data/lib/mcollective/data/result.rb +7 -2
  51. data/lib/mcollective/ddl/agentddl.rb +5 -17
  52. data/lib/mcollective/ddl/base.rb +10 -13
  53. data/lib/mcollective/discovery.rb +24 -39
  54. data/lib/mcollective/discovery/choria.ddl +11 -0
  55. data/lib/mcollective/discovery/choria.rb +223 -0
  56. data/lib/mcollective/discovery/flatfile.rb +7 -8
  57. data/lib/mcollective/discovery/mc.rb +2 -2
  58. data/lib/mcollective/discovery/stdin.rb +17 -18
  59. data/lib/mcollective/exceptions.rb +13 -0
  60. data/lib/mcollective/facts/base.rb +9 -9
  61. data/lib/mcollective/facts/yaml_facts.rb +12 -12
  62. data/lib/mcollective/generators.rb +3 -3
  63. data/lib/mcollective/generators/agent_generator.rb +3 -4
  64. data/lib/mcollective/generators/base.rb +14 -15
  65. data/lib/mcollective/generators/data_generator.rb +5 -6
  66. data/lib/mcollective/log.rb +2 -2
  67. data/lib/mcollective/logger/base.rb +3 -2
  68. data/lib/mcollective/logger/console_logger.rb +10 -10
  69. data/lib/mcollective/logger/file_logger.rb +7 -7
  70. data/lib/mcollective/logger/syslog_logger.rb +11 -15
  71. data/lib/mcollective/message.rb +8 -39
  72. data/lib/mcollective/monkey_patches.rb +2 -4
  73. data/lib/mcollective/optionparser.rb +2 -1
  74. data/lib/mcollective/pluginmanager.rb +3 -5
  75. data/lib/mcollective/pluginpackager.rb +1 -3
  76. data/lib/mcollective/pluginpackager/agent_definition.rb +3 -8
  77. data/lib/mcollective/pluginpackager/forge_packager.rb +7 -9
  78. data/lib/mcollective/pluginpackager/standard_definition.rb +1 -2
  79. data/lib/mcollective/registration/base.rb +18 -16
  80. data/lib/mcollective/rpc.rb +2 -4
  81. data/lib/mcollective/rpc/actionrunner.rb +16 -18
  82. data/lib/mcollective/rpc/agent.rb +26 -43
  83. data/lib/mcollective/rpc/audit.rb +1 -0
  84. data/lib/mcollective/rpc/client.rb +67 -85
  85. data/lib/mcollective/rpc/helpers.rb +55 -62
  86. data/lib/mcollective/rpc/progress.rb +2 -2
  87. data/lib/mcollective/rpc/reply.rb +17 -19
  88. data/lib/mcollective/rpc/request.rb +7 -5
  89. data/lib/mcollective/rpc/result.rb +6 -8
  90. data/lib/mcollective/rpc/stats.rb +49 -58
  91. data/lib/mcollective/security/base.rb +13 -56
  92. data/lib/mcollective/security/choria.rb +765 -0
  93. data/lib/mcollective/shell.rb +9 -4
  94. data/lib/mcollective/signer/base.rb +28 -0
  95. data/lib/mcollective/signer/choria.rb +185 -0
  96. data/lib/mcollective/ssl.rb +8 -6
  97. data/lib/mcollective/util.rb +58 -55
  98. data/lib/mcollective/util/bolt_support.rb +176 -0
  99. data/lib/mcollective/util/bolt_support/plan_runner.rb +167 -0
  100. data/lib/mcollective/util/bolt_support/task_result.rb +94 -0
  101. data/lib/mcollective/util/bolt_support/task_results.rb +128 -0
  102. data/lib/mcollective/util/choria.rb +946 -0
  103. data/lib/mcollective/util/indifferent_hash.rb +12 -0
  104. data/lib/mcollective/util/natswrapper.rb +242 -0
  105. data/lib/mcollective/util/playbook.rb +435 -0
  106. data/lib/mcollective/util/playbook/data_stores.rb +201 -0
  107. data/lib/mcollective/util/playbook/data_stores/base.rb +99 -0
  108. data/lib/mcollective/util/playbook/data_stores/consul_data_store.rb +88 -0
  109. data/lib/mcollective/util/playbook/data_stores/environment_data_store.rb +33 -0
  110. data/lib/mcollective/util/playbook/data_stores/etcd_data_store.rb +42 -0
  111. data/lib/mcollective/util/playbook/data_stores/file_data_store.rb +106 -0
  112. data/lib/mcollective/util/playbook/data_stores/shell_data_store.rb +103 -0
  113. data/lib/mcollective/util/playbook/inputs.rb +265 -0
  114. data/lib/mcollective/util/playbook/nodes.rb +207 -0
  115. data/lib/mcollective/util/playbook/nodes/mcollective_nodes.rb +86 -0
  116. data/lib/mcollective/util/playbook/nodes/pql_nodes.rb +40 -0
  117. data/lib/mcollective/util/playbook/nodes/shell_nodes.rb +55 -0
  118. data/lib/mcollective/util/playbook/nodes/terraform_nodes.rb +65 -0
  119. data/lib/mcollective/util/playbook/nodes/yaml_nodes.rb +47 -0
  120. data/lib/mcollective/util/playbook/playbook_logger.rb +47 -0
  121. data/lib/mcollective/util/playbook/puppet_logger.rb +51 -0
  122. data/lib/mcollective/util/playbook/report.rb +152 -0
  123. data/lib/mcollective/util/playbook/task_result.rb +55 -0
  124. data/lib/mcollective/util/playbook/tasks.rb +196 -0
  125. data/lib/mcollective/util/playbook/tasks/base.rb +45 -0
  126. data/lib/mcollective/util/playbook/tasks/graphite_event_task.rb +64 -0
  127. data/lib/mcollective/util/playbook/tasks/mcollective_task.rb +356 -0
  128. data/lib/mcollective/util/playbook/tasks/shell_task.rb +93 -0
  129. data/lib/mcollective/util/playbook/tasks/slack_task.rb +105 -0
  130. data/lib/mcollective/util/playbook/tasks/webhook_task.rb +136 -0
  131. data/lib/mcollective/util/playbook/template_util.rb +98 -0
  132. data/lib/mcollective/util/playbook/uses.rb +169 -0
  133. data/lib/mcollective/util/tasks_support.rb +751 -0
  134. data/lib/mcollective/util/tasks_support/cli.rb +260 -0
  135. data/lib/mcollective/util/tasks_support/default_formatter.rb +138 -0
  136. data/lib/mcollective/util/tasks_support/json_formatter.rb +108 -0
  137. data/lib/mcollective/validator.rb +6 -1
  138. data/lib/mcollective/validator/bolt_task_name_validator.ddl +7 -0
  139. data/lib/mcollective/validator/bolt_task_name_validator.rb +11 -0
  140. data/lib/mcollective/validator/length_validator.rb +1 -3
  141. metadata +65 -6
  142. data/lib/mcollective/application/describe_filter.rb +0 -87
  143. data/lib/mcollective/matcher.rb +0 -220
  144. data/lib/mcollective/matcher/parser.rb +0 -128
  145. data/lib/mcollective/matcher/scanner.rb +0 -241
@@ -0,0 +1,12 @@
1
+ module MCollective
2
+ module Util
3
+ class IndifferentHash < Hash
4
+ def [](key)
5
+ return super if key?(key)
6
+ return self[key.to_s] if key.is_a?(Symbol)
7
+
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,242 @@
1
+ require "nats/io/client"
2
+
3
+ module MCollective
4
+ module Util
5
+ # A wrapper class around the Pure Ruby NATS gem
6
+ #
7
+ # MCollective has some non compatible expectations about how
8
+ # message flow works such as having a blocking receive and publish
9
+ # method it calls when it likes, while typical flow is to pass
10
+ # a block and then callbacks will be called.
11
+ #
12
+ # This wrapper bridges the 2 worlds using ruby Queues to simulate the
13
+ # blocking receive expectation MCollective has thanks to its initial
14
+ # design around the Stomp gem.
15
+ class NatsWrapper
16
+ attr_reader :subscriptions, :received_queue
17
+
18
+ def initialize
19
+ @received_queue = Queue.new
20
+ @subscriptions = {}
21
+ @subscription_mutex = Mutex.new
22
+ @started = false
23
+ @client = NATS::IO::Client.new
24
+ end
25
+
26
+ # Has the NATS connection started
27
+ #
28
+ # @return [Boolean]
29
+ def started?
30
+ @started
31
+ end
32
+
33
+ # Is there a NATS client created
34
+ #
35
+ # @return [Boolean]
36
+ def has_client?
37
+ !!@client
38
+ end
39
+
40
+ # Retrieves the current connected server
41
+ #
42
+ # @return [String,nil]
43
+ def connected_server
44
+ return nil unless connected?
45
+
46
+ @client.connected_server
47
+ end
48
+
49
+ # Connection stats from the NATS gem
50
+ #
51
+ # @return [Hash]
52
+ def stats
53
+ return {} unless has_client?
54
+
55
+ @client.stats
56
+ end
57
+
58
+ # Client library flavour
59
+ #
60
+ # @return [String]
61
+ def client_flavour
62
+ "nats-pure"
63
+ end
64
+
65
+ # Client library version
66
+ #
67
+ # @return [String]
68
+ def client_version
69
+ NATS::IO::VERSION
70
+ end
71
+
72
+ # Connection options from the NATS gem
73
+ #
74
+ # @return [Hash]
75
+ def active_options
76
+ return {} unless has_client?
77
+
78
+ @client.options
79
+ end
80
+
81
+ # Is NATS connected
82
+ #
83
+ # @return [Boolean]
84
+ def connected?
85
+ has_client? && @client.connected?
86
+ end
87
+
88
+ # Does a backoff sleep up to 2 seconds
89
+ #
90
+ # @return [void]
91
+ def backoff_sleep
92
+ @backoffcount ||= 1
93
+
94
+ if @backoffcount >= 50
95
+ sleep(2)
96
+ else
97
+ sleep(0.04 * @backoffcount)
98
+ end
99
+
100
+ @backoffcount += 1
101
+ end
102
+
103
+ # Logs the NATS server pool for nats-pure
104
+ #
105
+ # The current server pool is dynamic as the NATS servers can announce
106
+ # new cluster members as they join the pool, little helper for logging
107
+ # the pool on major events
108
+ #
109
+ # @return [void]
110
+ def log_nats_pool
111
+ return unless has_client?
112
+
113
+ servers = @client.server_pool.map do |server|
114
+ server[:uri].to_s
115
+ end
116
+
117
+ Log.info("Current server pool: %s" % servers.join(", "))
118
+ end
119
+
120
+ # Starts the EM based NATS connection
121
+ #
122
+ # @param options [Hash] Options as per {#NATS::IO::Client#connect}
123
+ def start(options={})
124
+ # Client connects pretty much soon as it's initialized which is very early
125
+ # and some applications like 'request_cert' just doesnt need/want a client
126
+ # since for example there won't be SSL stuff yet, so if a application calls
127
+ # disconnect very early on this should avoid that chicken and egg
128
+ return if @force_Stop
129
+
130
+ @client.on_reconnect do
131
+ Log.warn("Reconnected after connection failure: %s" % connected_server)
132
+ log_nats_pool
133
+ @backoffcount = 1
134
+ end
135
+
136
+ @client.on_disconnect do |error|
137
+ if error
138
+ Log.warn("Disconnected from NATS: %s: %s" % [error.class, error.to_s])
139
+ else
140
+ Log.info("Disconnected from NATS for an unknown reason")
141
+ end
142
+ end
143
+
144
+ @client.on_error do |error|
145
+ Log.error("Error in NATS connection: %s: %s" % [error.class, error.to_s])
146
+ end
147
+
148
+ @client.on_close do
149
+ Log.info("Connection to NATS server closed")
150
+ end
151
+
152
+ begin
153
+ @client.connect(options)
154
+ rescue ClientTimeoutError
155
+ raise
156
+ rescue
157
+ Log.error("Error during initial NATS setup: %s: %s" % [$!.class, $!.message])
158
+ Log.debug($!.backtrace.join("\n\t"))
159
+
160
+ sleep 1
161
+
162
+ Log.error("Retrying NATS initial setup")
163
+
164
+ retry
165
+ end
166
+
167
+ sleep(0.01) until connected?
168
+
169
+ @started = true
170
+
171
+ nil
172
+ end
173
+
174
+ # Stops the NATS connection
175
+ def stop
176
+ @force_stop = true
177
+ @client.close
178
+ end
179
+
180
+ # Receives a message from the receive queue
181
+ #
182
+ # This will block until a message is available
183
+ #
184
+ # @return [String] received message
185
+ def receive
186
+ @received_queue.pop
187
+ end
188
+
189
+ # Public a message
190
+ #
191
+ # @param destination [String] the NATS destination
192
+ # @param payload [String] the string to publish
193
+ # @param reply [String] a reply destination
194
+ def publish(destination, payload, reply=nil)
195
+ server_state = "%s %s" % [connected? ? "connected" : "disconnected", @client.connected_server]
196
+
197
+ if reply
198
+ Log.debug("Publishing to %s reply to %s via %s" % [destination, reply, server_state])
199
+ else
200
+ Log.debug("Publishing to %s via %s" % [destination, server_state])
201
+ end
202
+
203
+ @client.publish(destination, payload, reply)
204
+ end
205
+
206
+ # Subscribes to a message source
207
+ #
208
+ # @param source_name [String]
209
+ # @param options [Hash] options as accepted by {NATS::IO::Client#subscribe}
210
+ def subscribe(source_name, options={})
211
+ @subscription_mutex.synchronize do
212
+ Log.debug("Subscribing to %s" % source_name)
213
+
214
+ unless @subscriptions.include?(source_name)
215
+ @subscriptions[source_name] = @client.subscribe(source_name, options) do |msg, _, _|
216
+ @received_queue << msg
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # Unsubscribes from a message source
223
+ #
224
+ # @param source_name [String]
225
+ def unsubscribe(source_name)
226
+ @subscription_mutex.synchronize do
227
+ if @subscriptions.include?(source_name)
228
+ @client.unsubscribe(@subscriptions[source_name])
229
+ @subscriptions.delete(source_name)
230
+ end
231
+ end
232
+ end
233
+
234
+ # Test helper
235
+ #
236
+ # @private
237
+ def stub_client(client)
238
+ @client = client
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,435 @@
1
+ require_relative "playbook/report"
2
+ require_relative "playbook/playbook_logger"
3
+ require_relative "playbook/puppet_logger"
4
+ require_relative "playbook/template_util"
5
+ require_relative "playbook/inputs"
6
+ require_relative "playbook/uses"
7
+ require_relative "playbook/nodes"
8
+ require_relative "playbook/tasks"
9
+ require_relative "playbook/data_stores"
10
+
11
+ module MCollective
12
+ module Util
13
+ class Playbook
14
+ include TemplateUtil
15
+
16
+ attr_accessor :input_data, :context
17
+ attr_reader :metadata, :report, :data_stores, :uses, :tasks, :logger
18
+
19
+ def initialize(loglevel=nil)
20
+ @loglevel = loglevel
21
+
22
+ @report = Report.new(self)
23
+
24
+ # @todo dear god this Camel_Snake horror but mcollective requires this
25
+ # Configures the main MCollective logger with our custom logger
26
+ @logger = Log.set_logger(Playbook_Logger.new(self))
27
+
28
+ @nodes = Nodes.new(self)
29
+ @tasks = Tasks.new(self)
30
+ @uses = Uses.new(self)
31
+ @inputs = Inputs.new(self)
32
+ @data_stores = DataStores.new(self)
33
+ @playbook = self
34
+ @playbook_data = {}
35
+ @input_data = {}
36
+
37
+ @metadata = {
38
+ "name" => nil,
39
+ "version" => nil,
40
+ "author" => nil,
41
+ "description" => nil,
42
+ "tags" => [],
43
+ "on_fail" => "fail",
44
+ "loglevel" => "info",
45
+ "run_as" => "choria=deployer"
46
+ }
47
+ end
48
+
49
+ # Sets a custom logger
50
+ #
51
+ # @param logger [Class] the logger instance to configure and use
52
+ def logger=(logger)
53
+ @logger = Log.set_logger(logger.new(self))
54
+ end
55
+
56
+ # Loads the playbook data and prepare the runner
57
+ #
58
+ # @param data [Hash] playbook data
59
+ # @return [Playbook]
60
+ def from_hash(data)
61
+ in_context("loading") do
62
+ @playbook_data = data
63
+
64
+ @metadata = {
65
+ "name" => data["name"],
66
+ "version" => data["version"],
67
+ "author" => data["author"],
68
+ "description" => data["description"],
69
+ "tags" => data.fetch("tags", []),
70
+ "on_fail" => data.fetch("on_fail", "fail"),
71
+ "loglevel" => data.fetch("loglevel", "info"),
72
+ "run_as" => data["run_as"]
73
+ }
74
+ end
75
+
76
+ set_logger_level
77
+
78
+ in_context("inputs") do
79
+ @inputs.from_hash(data.fetch("inputs", {}))
80
+ end
81
+
82
+ self
83
+ end
84
+
85
+ # Validate the playbook structure
86
+ #
87
+ # This is a pretty grim way to do this, ideally AIO would include some JSON Schema
88
+ # tools but they don't and I don't want to carray a dependency on that now
89
+ #
90
+ # @todo use JSON Schema
91
+ # @raise [StandardError] for invalid playbooks
92
+ def validate_configuration!
93
+ in_context("validation") do
94
+ failed = false
95
+
96
+ valid_keys = (@metadata.keys + ["uses", "inputs", "locks", "data_stores", "nodes", "tasks", "hooks", "macros", "$schema"])
97
+ invalid_keys = @playbook_data.keys - valid_keys
98
+
99
+ unless invalid_keys.empty?
100
+ Log.error("Invalid playbook data items %s found" % invalid_keys.join(", "))
101
+ failed = true
102
+ end
103
+
104
+ ["name", "version", "author", "description"].each do |item|
105
+ unless @metadata[item]
106
+ Log.error("A playbook %s is needed" % item)
107
+ failed = true
108
+ end
109
+ end
110
+
111
+ unless ["debug", "info", "warn", "error", "fatal"].include?(@metadata["loglevel"])
112
+ Log.error("Invalid log level %s, valid levels are debug, info, warn, error, fatal" % @metadata["loglevel"])
113
+ failed = true
114
+ end
115
+
116
+ unless PluginManager["security_plugin"].valid_callerid?(@metadata["run_as"])
117
+ Log.error("Invalid callerid %s" % @metadata["run_as"])
118
+ failed = true
119
+ end
120
+
121
+ ["uses", "nodes", "hooks", "data_stores", "inputs"].each do |key|
122
+ next unless @playbook_data.include?(key)
123
+ next if @playbook_data[key].is_a?(Hash)
124
+
125
+ Log.error("%s should be a hash" % key)
126
+ failed = true
127
+ end
128
+
129
+ ["locks", "tasks"].each do |key|
130
+ next unless @playbook_data.include?(key)
131
+ next if @playbook_data[key].is_a?(Array)
132
+
133
+ Log.error("%s should be a array" % key)
134
+ failed = true
135
+ end
136
+
137
+ raise("Playbook is not in a valid format") if failed
138
+ end
139
+ end
140
+
141
+ def prepare
142
+ prepare_inputs
143
+
144
+ prepare_data_stores
145
+ save_input_data
146
+
147
+ obtain_playbook_locks
148
+
149
+ prepare_uses
150
+ prepare_nodes
151
+ prepare_tasks
152
+ end
153
+
154
+ # Saves data from any inputs that requested they be written to data stores
155
+ def save_input_data
156
+ @inputs.save_input_data
157
+ end
158
+
159
+ # Derives a playbook lock from a given lock
160
+ #
161
+ # If a lock is in the normal valid format of source/lock
162
+ # then it's assumed the user gave a full path and knows what
163
+ # she wants otherwise a path will be constructed using the
164
+ # playbook name
165
+ #
166
+ # @return [String]
167
+ def lock_path(lock)
168
+ lock =~ /^[a-zA-Z0-9\-_]+\/.+$/ ? lock : "%s/choria/locks/playbook/%s" % [lock, name]
169
+ end
170
+
171
+ # Obtains the playbook level locks
172
+ def obtain_playbook_locks
173
+ Array(@playbook_data["locks"]).each do |lock|
174
+ Log.info("Obtaining playbook lock %s" % [lock_path(lock)])
175
+ @data_stores.lock(lock_path(lock))
176
+ end
177
+ end
178
+
179
+ # Obtains the playbook level locks
180
+ def release_playbook_locks
181
+ Array(@playbook_data["locks"]).each do |lock|
182
+ Log.info("Releasing playbook lock %s" % [lock_path(lock)])
183
+
184
+ begin
185
+ @data_stores.release(lock_path(lock))
186
+ rescue
187
+ Log.warn("Lock %s could not be released, ignoring" % lock_path(lock))
188
+ end
189
+ end
190
+ end
191
+
192
+ # Runs the playbook
193
+ #
194
+ # @param inputs [Hash] input data
195
+ # @return [Hash] the playbook report
196
+ def run!(inputs)
197
+ success = false
198
+ validate_configuration!
199
+
200
+ begin
201
+ start_time = @report.start!
202
+
203
+ @input_data = inputs
204
+
205
+ in_context("pre") { Log.info("Starting playbook %s at %s" % [name, start_time]) }
206
+
207
+ prepare
208
+
209
+ success = in_context("run") { @tasks.run }
210
+ in_context("post") { Log.info("Done running playbook %s in %s" % [name, seconds_to_human(Integer(@report.elapsed_time))]) }
211
+
212
+ release_playbook_locks
213
+ rescue
214
+ msg = "Playbook %s failed: %s: %s" % [name, $!.class, $!.to_s]
215
+
216
+ Log.error(msg)
217
+ Log.debug($!.backtrace.join("\n\t"))
218
+
219
+ report.finalize(false, msg)
220
+ end
221
+
222
+ report.finalize(success)
223
+ end
224
+
225
+ # Playbook name as declared in metadata
226
+ #
227
+ # @return [String]
228
+ def name
229
+ metadata_item("name")
230
+ end
231
+
232
+ # Playbook version as declared in metadata
233
+ #
234
+ # @return [String]
235
+ def version
236
+ metadata_item("version")
237
+ end
238
+
239
+ def loglevel
240
+ @loglevel || metadata_item("loglevel") || "info"
241
+ end
242
+
243
+ def set_logger_level
244
+ @logger.set_level(loglevel.intern)
245
+ end
246
+
247
+ # Prepares the data sources from the plabook
248
+ def prepare_data_stores
249
+ in_context("pre.stores") { @data_stores.from_hash(t(@playbook_data["data_stores"] || {})).prepare }
250
+ end
251
+
252
+ # Prepares the inputs from the playbook
253
+ #
254
+ # @todo same pattern as prepare_uses and nodes
255
+ # @see Inputs#prepare
256
+ # @note this should be done first, before any uses, nodes or tasks are prepared
257
+ def prepare_inputs
258
+ in_context("prep.inputs") { @inputs.prepare(@input_data) }
259
+ end
260
+
261
+ # Prepares the uses clauses from the playbook
262
+ #
263
+ # @see Uses#prepare
264
+ def prepare_uses
265
+ in_context("prep.uses") { @uses.from_hash(t(@playbook_data["uses"] || {})).prepare }
266
+ end
267
+
268
+ # Prepares the ode lists from the Playbook
269
+ #
270
+ # @see Nodes#prepare
271
+ def prepare_nodes
272
+ in_context("prep.nodes") { @nodes.from_hash(t(@playbook_data["nodes"] || {})).prepare }
273
+ end
274
+
275
+ # Prepares the tasks lists `tasks` and `hooks` from the Playbook data
276
+ #
277
+ # @see Tasks#prepare
278
+ def prepare_tasks
279
+ # we lazy template parse these so that they might refer to run time
280
+ # state via the template system - like for example in a post task you
281
+ # might want to reference properties of another rpc request
282
+ in_context("prep.tasks") do
283
+ @tasks.from_hash(@playbook_data["tasks"] || [])
284
+ @tasks.from_hash(@playbook_data["hooks"] || {})
285
+ @tasks.prepare
286
+ end
287
+ end
288
+
289
+ # Validates agent versions on nodes
290
+ #
291
+ # @param agents [Hash] a hash of agent names and nodes that uses that agent
292
+ # @raise [StandardError] on failure
293
+ def validate_agents(agents)
294
+ @uses.validate_agents(agents)
295
+ end
296
+
297
+ # Retrieves an item from the metadata
298
+ #
299
+ # @param item [name, version, author, description, tags, on_fail, loglevel, run_as]
300
+ # @return [Object] the corresponding item from `@metadata`
301
+ # @raise [StandardError] for invalid metadata items
302
+ def metadata_item(item)
303
+ if @metadata.include?(item)
304
+ @metadata[item]
305
+ else
306
+ raise("Unknown playbook metadata %s" % item)
307
+ end
308
+ end
309
+
310
+ # (see Nodes#[])
311
+ def discovered_nodes(nodeset)
312
+ @nodes[nodeset].clone
313
+ end
314
+
315
+ # A list of known node sets
316
+ #
317
+ # @return [Array<String>]
318
+ def nodes
319
+ @nodes.keys
320
+ end
321
+
322
+ # (see Inputs#[])
323
+ def input_value(input)
324
+ @inputs[input]
325
+ end
326
+
327
+ # A list of known input keys
328
+ #
329
+ # @return [Array<String>]
330
+ def inputs
331
+ @inputs.keys
332
+ end
333
+
334
+ # List of known input names that have dynamic values
335
+ #
336
+ # @return [Array<String>]
337
+ def dynamic_inputs
338
+ @inputs.dynamic_keys
339
+ end
340
+
341
+ # List of known input names that have static values
342
+ #
343
+ # @return [Array<String>]
344
+ def static_inputs
345
+ @inputs.static_keys
346
+ end
347
+
348
+ # Looks up a proeprty of the previous task
349
+ #
350
+ # @param property [success, msg, message, data, description]
351
+ # @return [Object]
352
+ def previous_task(property)
353
+ if property == "success"
354
+ return false unless previous_task_result && previous_task_result.ran
355
+
356
+ previous_task_result.success
357
+ elsif ["msg", "message"].include?(property)
358
+ return "No previous task were found" unless previous_task_result
359
+ return "Previous task did not run" unless previous_task_result.ran
360
+
361
+ previous_task_result.msg
362
+ elsif property == "data"
363
+ return [] unless previous_task_result && previous_task_result.ran
364
+
365
+ previous_task_result.data || []
366
+ elsif property == "description"
367
+ return "No previous task were found" unless previous_task_result
368
+
369
+ previous_task_result.task[:description]
370
+ elsif property == "runtime"
371
+ return 0 unless previous_task_result && previous_task_result.ran
372
+
373
+ previous_task_result.run_time.round(2)
374
+ else
375
+ raise("Cannot retrieve %s for the last task outcome" % property)
376
+ end
377
+ end
378
+
379
+ # All the task results
380
+ #
381
+ # @return [Array<TaskResult>]
382
+ def task_results
383
+ @tasks.results
384
+ end
385
+
386
+ # Find the last result from the tasks ran
387
+ #
388
+ # @return [TaskResult,nil]
389
+ def previous_task_result
390
+ task_results.last
391
+ end
392
+
393
+ # Adds the CLI options for an application based on the playbook inputs
394
+ #
395
+ # @see Inputs#add_cli_options
396
+ # @param application [MCollective::Application]
397
+ # @param set_required [Boolean]
398
+ def add_cli_options(application, set_required=false)
399
+ @inputs.add_cli_options(application, set_required)
400
+ end
401
+
402
+ def in_context(context)
403
+ old_context = @context
404
+ @context = context
405
+
406
+ yield
407
+ ensure
408
+ @context = old_context
409
+ end
410
+
411
+ def seconds_to_human(seconds)
412
+ days = seconds / 86400
413
+ seconds -= 86400 * days
414
+
415
+ hours = seconds / 3600
416
+ seconds -= 3600 * hours
417
+
418
+ minutes = seconds / 60
419
+ seconds -= 60 * minutes
420
+
421
+ if days > 1
422
+ "%d days %d hours %d minutes %02d seconds" % [days, hours, minutes, seconds]
423
+ elsif days == 1
424
+ "%d day %d hours %d minutes %02d seconds" % [days, hours, minutes, seconds]
425
+ elsif hours > 0
426
+ "%d hours %d minutes %02d seconds" % [hours, minutes, seconds]
427
+ elsif minutes > 0
428
+ "%d minutes %02d seconds" % [minutes, seconds]
429
+ else
430
+ "%02d seconds" % seconds
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end