Class: MCollective::Util::Playbook

Inherits:
Object
  • Object
show all
Includes:
TemplateUtil
Defined in:
lib/mcollective/util/playbook.rb,
lib/mcollective/util/playbook/uses.rb,
lib/mcollective/util/playbook/nodes.rb,
lib/mcollective/util/playbook/tasks.rb,
lib/mcollective/util/playbook/inputs.rb,
lib/mcollective/util/playbook/report.rb,
lib/mcollective/util/playbook/tasks/base.rb,
lib/mcollective/util/playbook/data_stores.rb,
lib/mcollective/util/playbook/task_result.rb,
lib/mcollective/util/playbook/puppet_logger.rb,
lib/mcollective/util/playbook/template_util.rb,
lib/mcollective/util/playbook/nodes/pql_nodes.rb,
lib/mcollective/util/playbook/playbook_logger.rb,
lib/mcollective/util/playbook/data_stores/base.rb,
lib/mcollective/util/playbook/nodes/yaml_nodes.rb,
lib/mcollective/util/playbook/tasks/shell_task.rb,
lib/mcollective/util/playbook/tasks/slack_task.rb,
lib/mcollective/util/playbook/nodes/shell_nodes.rb,
lib/mcollective/util/playbook/tasks/webhook_task.rb,
lib/mcollective/util/playbook/nodes/terraform_nodes.rb,
lib/mcollective/util/playbook/tasks/mcollective_task.rb,
lib/mcollective/util/playbook/nodes/mcollective_nodes.rb,
lib/mcollective/util/playbook/tasks/graphite_event_task.rb,
lib/mcollective/util/playbook/data_stores/etcd_data_store.rb,
lib/mcollective/util/playbook/data_stores/file_data_store.rb,
lib/mcollective/util/playbook/data_stores/shell_data_store.rb,
lib/mcollective/util/playbook/data_stores/consul_data_store.rb,
lib/mcollective/util/playbook/data_stores/environment_data_store.rb

Defined Under Namespace

Modules: TemplateUtil Classes: DataStores, Inputs, Nodes, Playbook_Logger, Puppet_Logger, Report, TaskResult, Tasks, Uses

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TemplateUtil

#__template_process_string, #__template_resolve, #t

Constructor Details

#initialize(loglevel = nil) ⇒ Playbook

Returns a new instance of Playbook.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/mcollective/util/playbook.rb', line 19

def initialize(loglevel=nil)
  @loglevel = loglevel

  @report = Report.new(self)

  # @todo dear god this Camel_Snake horror but mcollective requires this
  # Configures the main MCollective logger with our custom logger
  @logger = Log.set_logger(Playbook_Logger.new(self))

  @nodes = Nodes.new(self)
  @tasks = Tasks.new(self)
  @uses = Uses.new(self)
  @inputs = Inputs.new(self)
  @data_stores = DataStores.new(self)
  @playbook = self
  @playbook_data = {}
  @input_data = {}

  @metadata = {
    "name" => nil,
    "version" => nil,
    "author" => nil,
    "description" => nil,
    "tags" => [],
    "on_fail" => "fail",
    "loglevel" => "info",
    "run_as" => "choria=deployer"
  }
end

Instance Attribute Details

#contextObject

Returns the value of attribute context.



16
17
18
# File 'lib/mcollective/util/playbook.rb', line 16

def context
  @context
end

#data_storesObject (readonly)

Returns the value of attribute data_stores.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def data_stores
  @data_stores
end

#input_dataObject

Returns the value of attribute input_data.



16
17
18
# File 'lib/mcollective/util/playbook.rb', line 16

def input_data
  @input_data
end

#loggerObject

Returns the value of attribute logger.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def logger
  @logger
end

#metadataObject (readonly)

Returns the value of attribute metadata.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def 
  @metadata
end

#reportObject (readonly)

Returns the value of attribute report.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def report
  @report
end

#tasksObject (readonly)

Returns the value of attribute tasks.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def tasks
  @tasks
end

#usesObject (readonly)

Returns the value of attribute uses.



17
18
19
# File 'lib/mcollective/util/playbook.rb', line 17

def uses
  @uses
end

Instance Method Details

#add_cli_options(application, set_required = false) ⇒ Object

Adds the CLI options for an application based on the playbook inputs

Parameters:

See Also:



398
399
400
# File 'lib/mcollective/util/playbook.rb', line 398

def add_cli_options(application, set_required=false)
  @inputs.add_cli_options(application, set_required)
end

#discovered_nodes(nodeset) ⇒ Array<String>

Nodes belonging to a specific node set

Parameters:

  • nodeset (String)

    node set name

Returns:

Raises:

  • (StandardError)

    when node set is unknown



311
312
313
# File 'lib/mcollective/util/playbook.rb', line 311

def discovered_nodes(nodeset)
  @nodes[nodeset].clone
end

#dynamic_inputsArray<String>

List of known input names that have dynamic values

Returns:



337
338
339
# File 'lib/mcollective/util/playbook.rb', line 337

def dynamic_inputs
  @inputs.dynamic_keys
end

#from_hash(data) ⇒ Playbook

Loads the playbook data and prepare the runner

Parameters:

  • data (Hash)

    playbook data

Returns:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/mcollective/util/playbook.rb', line 60

def from_hash(data)
  in_context("loading") do
    @playbook_data = data

    @metadata = {
      "name" => data["name"],
      "version" => data["version"],
      "author" => data["author"],
      "description" => data["description"],
      "tags" => data.fetch("tags", []),
      "on_fail" => data.fetch("on_fail", "fail"),
      "loglevel" => data.fetch("loglevel", "info"),
      "run_as" => data["run_as"]
    }
  end

  set_logger_level

  in_context("inputs") do
    @inputs.from_hash(data.fetch("inputs", {}))
  end

  self
end

#in_context(context) ⇒ Object



402
403
404
405
406
407
408
409
# File 'lib/mcollective/util/playbook.rb', line 402

def in_context(context)
  old_context = @context
  @context = context

  yield
ensure
  @context = old_context
end

#input_value(input) ⇒ Object

Retrieves the value for a specific input

Parameters:

  • input (String)

    input name

Returns:

  • (Object)

Raises:

  • (StandardError)

    for unknown inputs



323
324
325
# File 'lib/mcollective/util/playbook.rb', line 323

def input_value(input)
  @inputs[input]
end

#inputsArray<String>

A list of known input keys

Returns:



330
331
332
# File 'lib/mcollective/util/playbook.rb', line 330

def inputs
  @inputs.keys
end

#lock_path(lock) ⇒ String

Derives a playbook lock from a given lock

If a lock is in the normal valid format of source/lock then it’s assumed the user gave a full path and knows what she wants otherwise a path will be constructed using the playbook name

Returns:



167
168
169
# File 'lib/mcollective/util/playbook.rb', line 167

def lock_path(lock)
  lock =~ /^[a-zA-Z0-9\-_]+\/.+$/ ? lock : "%s/choria/locks/playbook/%s" % [lock, name]
end

#loglevelObject



239
240
241
# File 'lib/mcollective/util/playbook.rb', line 239

def loglevel
  @loglevel || ("loglevel") || "info"
end

#metadata_item(item) ⇒ Object

Retrieves an item from the metadata

Parameters:

Returns:

  • (Object)

    the corresponding item from ‘@metadata`

Raises:

  • (StandardError)

    for invalid metadata items



302
303
304
305
306
307
308
# File 'lib/mcollective/util/playbook.rb', line 302

def (item)
  if @metadata.include?(item)
    @metadata[item]
  else
    raise("Unknown playbook metadata %s" % item)
  end
end

#nameString

Playbook name as declared in metadata

Returns:



228
229
230
# File 'lib/mcollective/util/playbook.rb', line 228

def name
  ("name")
end

#nodesArray<String>

A list of known node sets

Returns:



318
319
320
# File 'lib/mcollective/util/playbook.rb', line 318

def nodes
  @nodes.keys
end

#obtain_playbook_locksObject

Obtains the playbook level locks



172
173
174
175
176
177
# File 'lib/mcollective/util/playbook.rb', line 172

def obtain_playbook_locks
  Array(@playbook_data["locks"]).each do |lock|
    Log.info("Obtaining playbook lock %s" % [lock_path(lock)])
    @data_stores.lock(lock_path(lock))
  end
end

#prepareObject



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/mcollective/util/playbook.rb', line 141

def prepare
  prepare_inputs

  prepare_data_stores
  save_input_data

  obtain_playbook_locks

  prepare_uses
  prepare_nodes
  prepare_tasks
end

#prepare_data_storesObject

Prepares the data sources from the plabook



248
249
250
# File 'lib/mcollective/util/playbook.rb', line 248

def prepare_data_stores
  in_context("pre.stores") { @data_stores.from_hash(t(@playbook_data["data_stores"] || {})).prepare }
end

#prepare_inputsObject

TODO:

same pattern as prepare_uses and nodes

Note:

this should be done first, before any uses, nodes or tasks are prepared

Prepares the inputs from the playbook



257
258
259
# File 'lib/mcollective/util/playbook.rb', line 257

def prepare_inputs
  in_context("prep.inputs") { @inputs.prepare(@input_data) }
end

#prepare_nodesObject

Prepares the ode lists from the Playbook



271
272
273
# File 'lib/mcollective/util/playbook.rb', line 271

def prepare_nodes
  in_context("prep.nodes") { @nodes.from_hash(t(@playbook_data["nodes"] || {})).prepare }
end

#prepare_tasksObject

Prepares the tasks lists ‘tasks` and `hooks` from the Playbook data



278
279
280
281
282
283
284
285
286
287
# File 'lib/mcollective/util/playbook.rb', line 278

def prepare_tasks
  # we lazy template parse these so that they might refer to run time
  # state via the template system - like for example in a post task you
  # might want to reference properties of another rpc request
  in_context("prep.tasks") do
    @tasks.from_hash(@playbook_data["tasks"] || [])
    @tasks.from_hash(@playbook_data["hooks"] || {})
    @tasks.prepare
  end
end

#prepare_usesObject

Prepares the uses clauses from the playbook



264
265
266
# File 'lib/mcollective/util/playbook.rb', line 264

def prepare_uses
  in_context("prep.uses") { @uses.from_hash(t(@playbook_data["uses"] || {})).prepare }
end

#previous_task(property) ⇒ Object

Looks up a proeprty of the previous task

Parameters:

  • property (success, msg, message, data, description)

Returns:

  • (Object)


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/mcollective/util/playbook.rb', line 352

def previous_task(property)
  if property == "success"
    return false unless previous_task_result && previous_task_result.ran

    previous_task_result.success
  elsif ["msg", "message"].include?(property)
    return "No previous task were found" unless previous_task_result
    return "Previous task did not run" unless previous_task_result.ran

    previous_task_result.msg
  elsif property == "data"
    return [] unless previous_task_result && previous_task_result.ran

    previous_task_result.data || []
  elsif property == "description"
    return "No previous task were found" unless previous_task_result

    previous_task_result.task[:description]
  elsif property == "runtime"
    return 0 unless previous_task_result && previous_task_result.ran

    previous_task_result.run_time.round(2)
  else
    raise("Cannot retrieve %s for the last task outcome" % property)
  end
end

#previous_task_resultTaskResult?

Find the last result from the tasks ran

Returns:



389
390
391
# File 'lib/mcollective/util/playbook.rb', line 389

def previous_task_result
  task_results.last
end

#release_playbook_locksObject

Obtains the playbook level locks



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/mcollective/util/playbook.rb', line 180

def release_playbook_locks
  Array(@playbook_data["locks"]).each do |lock|
    Log.info("Releasing playbook lock %s" % [lock_path(lock)])

    begin
      @data_stores.release(lock_path(lock))
    rescue
      Log.warn("Lock %s could not be released, ignoring" % lock_path(lock))
    end
  end
end

#run!(inputs) ⇒ Hash

Runs the playbook

Parameters:

  • inputs (Hash)

    input data

Returns:

  • (Hash)

    the playbook report



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/mcollective/util/playbook.rb', line 196

def run!(inputs)
  success = false
  validate_configuration!

  begin
    start_time = @report.start!

    @input_data = inputs

    in_context("pre") { Log.info("Starting playbook %s at %s" % [name, start_time]) }

    prepare

    success = in_context("run") { @tasks.run }
    in_context("post") { Log.info("Done running playbook %s in %s" % [name, seconds_to_human(Integer(@report.elapsed_time))]) }

    release_playbook_locks
  rescue
    msg = "Playbook %s failed: %s: %s" % [name, $!.class, $!.to_s]

    Log.error(msg)
    Log.debug($!.backtrace.join("\n\t"))

    report.finalize(false, msg)
  end

  report.finalize(success)
end

#save_input_dataObject

Saves data from any inputs that requested they be written to data stores



155
156
157
# File 'lib/mcollective/util/playbook.rb', line 155

def save_input_data
  @inputs.save_input_data
end

#seconds_to_human(seconds) ⇒ Object



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/mcollective/util/playbook.rb', line 411

def seconds_to_human(seconds)
  days = seconds / 86400
  seconds -= 86400 * days

  hours = seconds / 3600
  seconds -= 3600 * hours

  minutes = seconds / 60
  seconds -= 60 * minutes

  if days > 1
    "%d days %d hours %d minutes %02d seconds" % [days, hours, minutes, seconds]
  elsif days == 1
    "%d day %d hours %d minutes %02d seconds" % [days, hours, minutes, seconds]
  elsif hours > 0
    "%d hours %d minutes %02d seconds" % [hours, minutes, seconds]
  elsif minutes > 0
    "%d minutes %02d seconds" % [minutes, seconds]
  else
    "%02d seconds" % seconds
  end
end

#set_logger_levelObject



243
244
245
# File 'lib/mcollective/util/playbook.rb', line 243

def set_logger_level
  @logger.set_level(loglevel.intern)
end

#static_inputsArray<String>

List of known input names that have static values

Returns:



344
345
346
# File 'lib/mcollective/util/playbook.rb', line 344

def static_inputs
  @inputs.static_keys
end

#task_resultsArray<TaskResult>

All the task results

Returns:



382
383
384
# File 'lib/mcollective/util/playbook.rb', line 382

def task_results
  @tasks.results
end

#validate_agents(agents) ⇒ Object

Validates agent versions on nodes

Parameters:

  • agents (Hash)

    a hash of agent names and nodes that uses that agent

Raises:

  • (StandardError)

    on failure



293
294
295
# File 'lib/mcollective/util/playbook.rb', line 293

def validate_agents(agents)
  @uses.validate_agents(agents)
end

#validate_configuration!Object

TODO:

use JSON Schema

Validate the playbook structure

This is a pretty grim way to do this, ideally AIO would include some JSON Schema tools but they don’t and I don’t want to carray a dependency on that now

Raises:

  • (StandardError)

    for invalid playbooks



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mcollective/util/playbook.rb', line 92

def validate_configuration!
  in_context("validation") do
    failed = false

    valid_keys = (@metadata.keys + ["uses", "inputs", "locks", "data_stores", "nodes", "tasks", "hooks", "macros", "$schema"])
    invalid_keys = @playbook_data.keys - valid_keys

    unless invalid_keys.empty?
      Log.error("Invalid playbook data items %s found" % invalid_keys.join(", "))
      failed = true
    end

    ["name", "version", "author", "description"].each do |item|
      unless @metadata[item]
        Log.error("A playbook %s is needed" % item)
        failed = true
      end
    end

    unless ["debug", "info", "warn", "error", "fatal"].include?(@metadata["loglevel"])
      Log.error("Invalid log level %s, valid levels are debug, info, warn, error, fatal" % @metadata["loglevel"])
      failed = true
    end

    unless PluginManager["security_plugin"].valid_callerid?(@metadata["run_as"])
      Log.error("Invalid callerid %s" % @metadata["run_as"])
      failed = true
    end

    ["uses", "nodes", "hooks", "data_stores", "inputs"].each do |key|
      next unless @playbook_data.include?(key)
      next if @playbook_data[key].is_a?(Hash)

      Log.error("%s should be a hash" % key)
      failed = true
    end

    ["locks", "tasks"].each do |key|
      next unless @playbook_data.include?(key)
      next if @playbook_data[key].is_a?(Array)

      Log.error("%s should be a array" % key)
      failed = true
    end

    raise("Playbook is not in a valid format") if failed
  end
end

#versionString

Playbook version as declared in metadata

Returns:



235
236
237
# File 'lib/mcollective/util/playbook.rb', line 235

def version
  ("version")
end