Tips and Patterns
Module Layout and Naming
Generally the Playbooks behave as you would expect any modern Puppet function or data type.
You place the Playbook mymod::mybook in modules/mymod/plans/mybook.pp and in there you need plan mymod::mybook. Naming of Playbooks are subject to the same naming rules as Puppet, for example you cannot have a - in the Playbook name.
The modules with playbooks all go into a directory and you have to point at that directory with –modulepath. which defaults to ~/.puppetlabs/etc/code/modules, the same location puppet module install will place modules by default.
When running in this mode Puppet has a number of changes in behavior for example strict variable checks are enabled by default and templates are disabled. I cannot find a list of these behavior changes but it’s something to be aware of.
Interacting With Task Results
When a task is run it returns an instance of Choria::TaskResults which contains one of more Choria::TaskResult for every node that was affected by the task.
Each Choria::TaskResult has the following properties you can use:
Property / Method | Type | Description |
---|---|---|
host | Choria::Node | The hostname this result applies to |
result | Data | The data that was returned from the task, this depends on the type of task |
error | Optional[Error] | A Puppet standard Error object if the task failed |
ok | Boolean | If the task was successful, always true when fail_ok is given |
type | String[1] | The type of task that this result represents, like mcollective |
$r[“foo”] | Data | Utility to access the data of the result if it’s a hash |
value | Data | The entire value that the task produced |
Each Choria::TaskResults has the following properties you can use:
Property / Method | Type | Description |
---|---|---|
results | Array[Choria::TaskResult] | The individual results |
count | Integer | How many results are contained in this set |
empty | Boolean | If no results are contained in this set |
error_set | Choria::TaskResults | A new result set with only failing nodes |
ok_set | Choria::TaskResults | A new result set with only passing nodes |
ok | Boolean | If all contained results were ok |
fail_ok | Boolean | If failures are ignored when checking ok |
hosts | Choria::Nodes | List of nodes in this result set |
message | String | A short descriptive message about the outcome |
find(“some.node”) | Optional[Choria::TaskResult] | Finds the Choria::TaskResult for a specific node |
first | Optional[Choria::TaskResult] | Returns just the first Choria::TaskResult |
With these you can do complex error and result handling, display statuses you like, send to Slack etc.
$result = choria::task("action" => "rpcutil.ping", "nodes" => $nodes, "fail_ok" => true)
if $result.error_set.empty {
notice(sprintf("Ping reached %d nodes", $result.count))
notice(sprintf("Outcome: %s", $result.message))
} else {
notice(sprintf("Ping failed on: %s", $result.error_set.hosts.join(", "))
}
If you return these from a plan - or the last statement in your plan is a choria::task you can interact and error handle plans based on these.
Nodes on the CLI
Generally nodes come from node sets but you can also use an input to mimic a node set to some extent.
plan example::cli_nodes (
Array[String] $nodes
) {
# use $nodes
}
You can now supply nodes like this:
$ mco playbook run playbook.yaml --nodes web1.example.net --nodes web2.example.net
You can also load nodes from some file like this:
$ mco playbook run playbook.yaml --input @nodes.json
Your JSON would just have a array of node names in JSON format, you can also use YAML format by naming the file nodes.yaml
Error Handling Strategies
By default when you run a playbook any failure will just fail the playbook, but you might want to handle failures and branch to recovery code after failure, here’s a Playbook that handles failures and success, I’ll show a few possible syntaxes of the same thing:
The key to error handling is to pass _catch_errors => true
to choria::run_playbook which would avoid raising errors instead giving you a chance to handle them.
choria::run_playbook("example::app_upgrade", _catch_errors => true,
"action" => "appmgr.restart",
"nodes" => $nodes)
.choria::on_error |Choria::TaskResults $err| {
notice("Application upgrade failed: ${err.message}, recovering using example::recover")
choria::run_playbook("example::recover",
"cluster" => $cluster
)
fail("deploying cluster ${cluster} failed, recovery run successfully")
}
.choria::on_success |Choria::TaskResults $results| {
notice("deployment successful on cluster ${cluster}")
}
This syntax might be a bit foreign to Puppet users, here’s another approach but now you need temporary variables which can be very annoying:
$result = choria::task("mcollective", _catch_errors => true,
"action" => "appmgr.restart",
"nodes" => $nodes
)
$result.choria::on_error |$results| {
choria::run_playbook("example::recover",
"cluster" => $cluster
)
fail("deploying cluster ${cluster} failed, recovery run successfully")
}
$result.choria::on_success |$results| {
notice("deployment successful on cluster ${cluster}")
}
The Base Example page shows this in action where one Playbook wraps another and provides custom error handling.
These functions are clever enough to only trigger for Choria::TaskResults so if you wish to handle failures AND return non Choria::TaskResults from a Plan that’s totally ok.
plan example::update {
$nodes = choria::discover(
# ....
)
choria::task(
# ...
)
$nodes
}
$nodes = choria::run_playbook("example::update", _catch_errors => true)
.on_error |$err| {
# handle
}
notice($nodes.join(", "))
In this example your $nodes is an array or strings, the example::update returns the nodes and you print it using join which only works with arrays. However if the task failed you’d get a Choria::TaskResults back and the error handling will deal with that failure for you.
Utility Functions and Playbooks
These playbooks tend to be made up of a lot of basic building blocks. For example I find I often need to disable Puppet and wait for them to idle, let’s look how we can make this reusable.
plan example::puppet::disable_and_wait (
String $message = sprintf("Disabled by Choria Playbook $s", $facts["choria"]["playbook"]),
Integer $checks = 20,
Integer $sleep = 10
){
$nodes = choria::discover(
"discovery_method" => "choria",
"test" => true,
"agents" => ["puppet"],
)
choria::task(
"action" => "puppet.disable",
"nodes" => $nodes,
"fail_ok" => true,
"silent" => true,
"properties" => {
"message" => $message
}
)
choria::task(
"action" => "puppet.status",
"nodes" => $nodes,
"assert" => "idling=true",
"tries" => $checks,
"silent" => true,
"try_sleep" => $sleep,
"pre_sleep" => 5,
)
}
You can now use this Playbook wherever you need this functionality or indeed from the CLI. This is the benefit of using a Playbook as the abstraction rather than functions since they can be called from the CLI.
$results = choria::run_playbook("example::puppet::disable_and_wait",
"message" => "Disabled while restarting Puppet Server",
)
mco playbook run example::puppet::disable_and_wait --message "disabled while testing"
For things that clearly have no standalone value functions can be made:
function example::plan_rc(String $key) >> String {
choria::data($key, {
"type" => "file",
"file" => "~/.plans.rc",
"format" => "yaml"
})
}
Here you can just call $something = example::plan_rc("something")
whenever you want to fetch some data from your data source.
Iterating Subsets of Node Sets
There’s a small utility function that can iterate over arrays and call a provided block with a subset of the whole array, this is handy for performing multiple actions on batches.
$nodes.choria::in_groups_of(10) |$n| {
# here $n is 10 nodes or fewer
# see the example in the basics section for this in action
}
Validating Playbook syntax
You can use the standard Puppet CLI to validate your Playbook syntax if you pass –tasks like puppet parser validate --tasks playbook.pp
Documentation using Puppet Strings
You can document your plans using the normal Puppet Strings, when generating documents for a module with Playbooks in it just pass –tags like puppet strings generate --tasks 'example/**/*.pp'