Writing plans in Puppet Language
Plans allow you to run more than one task with a single command, compute values for the input to a task, process the results of tasks, or make decisions based on the result of running a task.
Write plans in the Puppet language, giving
them a .pp
extension, and place
them in the module's /plans
directory.
Plans can use any combination of Bolt functions or built in Puppetfunctions.
Naming plans
It is important to name plans correctly according to the module name, file name, and path to ensure easy code readability.
./plans
directory, using these file extensions:-
Puppet plans —
.pp
-
YAML plans —
.yaml
, not.yml
Plan names are composed of two or more name segments, indicating:
-
The name of the module the plan is located in.
-
The name of the plan file, without the extension.
-
The path within the module, if the plan is in a subdirectory of
./plans
.
For example, given a module called mymodule
with a plan defined in ./mymodule/plans/myplan.pp
, the plan name is mymodule::myplan
.
A plan defined in ./mymodule/plans/service/myplan.pp
would be mymodule::service::myplan
. This name is how you refer to the plan when
you run commands.
The plan filename init
is special: the plan it defines is referenced using the module name only. For
example, in a module called mymodule
, the plan defined in init.pp
is the mymodule
plan.
Avoid giving plans the same names as constructs in the Puppet language. Although plans do not share their namespace with other language constructs, giving plans these names makes your code difficult to read.
Each plan name segment must begin with a lowercase letter and:
-
May include lowercase letters.
-
May include digits.
-
May include underscores.
-
Must not be a reserved word.
-
Must not have the same name as any Puppet data types.
-
Namespace segments must match the following regular expression
\A[a-z][a-z0-9_]*\Z
RBAC considerations for writing plans
Take user permissions into consideration when writing plans by understanding how RBAC for plans works.
RBAC for plans is separate from RBAC for individual tasks. This means that a user can be excluded from running a certain task, but still have permission to run a plan that contains that task. Setting one permission does not affect the other.
This structure allows you to write plans with more robust, custom control over task permissions. Instead of allowing a user free rein to run a task that can potentially damage your infrastructure under the right conditions, you can wrap a task in a plan and only allow them to run it under circumstances you can control.
For example, say you are configuring permissions for a new user and allow
them to run the plan plan
infra::upgrade_git
:
plan infra::upgrade_git (
TargetSpec $nodes,
Integer $version,
) {
run_task(‘package’, $nodes, name => ’git’, action => ‘upgrade’, version => $version)
}
Within this plan, they can run the package
task, but can only interact with the git
package. The plan does not allow them to use any other parameters for the
package
task.
Even though they can run this plan, they do not have access to
individually run the package
task outside of
this plan unless you grant them permission to do so. In that case, they would have the option
to add any parameters they want to the task.
Use parameter types to fine-tune access
Writing parameter types into plan code provides even more control. In
the upgrade_git
example above, the plan
only provides access to the git
package,
but the user can choose whatever version of git
they want.
Let's say there are known vulnerabilities in some versions of the
git
package and you are concerned with
your new user having the ability to use the versions you deem unsafe. You can use parameter
types like Enum to restrict the version parameter to versions that are safe enough for
deployment.
$version
parameter to versions 1:2.17.0-1ubuntu1 and
1:2.17.1-1ubuntu0.4 only:
plan infra::upgrade_git (
TargetSpec $nodes,
Enum['1:2.17.0-1ubuntu1', '1:2.17.1-1ubuntu0.4'] $version,
) {
run_task(‘package’, $nodes, name => ‘git’, action => ‘upgrade’, version => $version)
}
Any user attempting to run this plan must choose one of these versions for the plan to run.
infra::upgrade_git
can run on. Use a
PuppetDB query to identify which nodes get selected for the
git upgrade. It should look something like
this:plan infra::upgrade_git (
Enum['1:2.17.0-1ubuntu1', '1:2.17.1-1ubuntu0.4'] $version,
) {
# Use puppetdb to find the nodes from the “other” team's web cluster
$query = [from, nodes, ['=', [fact, cluster], "other_team"]]
$selected_nodes = puppetdb_query($query).map() |$target| {
$target[certname]
}
run_task(‘package’, $selected_nodes, name => ‘git’, action => ‘upgrade’, version => $version)
}
Using these ideas, you can write powerful plans that give users exactly what they need without giving them the keys to the kingdom.
Defining plan parameters
Specify parameters within your plan.
Specify each parameter in your plan with its data type. For example, you might want parameters to specify which nodes to run different parts of your plan on.
The following example shows node parameters specified as data type TargetSpec
. This allows this parameter
to be passed as a single URL, comma-separated URL list, Target data type, or Array of either. For
more information about these data types, see the common data types table in the related metadata
type topic.
This allows the user to pass, for each parameter, either a node name or a URI that describes the protocol to use, the hostname, username, and password.
The plan then calls the run_task
function, specifying which nodes to run the tasks on. The
Target
names are collected and stored in
$webserver_names
by iterating over the list of
Target
objects returned by get_targets
. Task parameters are serialized to JSON format;
therefore, extracting the names into an array of strings ensures that the webservers
parameter is in a format that can be converted to
JSON.
plan mymodule::my_plan(
TargetSpec $load_balancer,
TargetSpec $webservers,
) {
# Extract the Target name from $webservers
$webserver_names = get_targets($webservers).map |$n| { $n.name }
# process webservers
run_task('mymodule::lb_remove', $load_balancer, webservers => $webserver_names)
run_task('mymodule::update_frontend_app', $webservers, version => '1.2.3')
run_task('mymodule::lb_add', $load_balancer, webservers => $webserver_names)
}
To execute this plan from the command line, pass the parameters as parameter=value
. The Targetspec
accepts either an array as json or a
comma separated string of target names.
bolt plan run mymodule::myplan --modulepath ./PATH/TO/MODULES load_balancer=lb.myorg.com webservers='["kermit.myorg.com","gonzo.myorg.com"]'
Parameters that are passed to the run_*
plan functions are serialized to JSON.
plan test::parameter_passing (
TargetSpec $nodes,
Optional[String[1]] $example_nul = undef,
) {
return run_task('test::demo_undef_bash', $nodes, example_nul => $example_nul)
}
$example_nul
is undef
. The plan calls
the test::demo_undef_bash
with the example_nul
parameter. The implementation of the
demo_undef_bash.sh
task
is:#!/bin/bash
example_env=$PT_example_nul
echo "Environment: $PT_example_nul"
echo "Stdin:"
cat -
By default, the task expects parameters passed as a JSON string on stdin to be accessible in prefixed environment variables.
bolt@bolt: bolt plan run test::parameter_passing -n localhost
Starting: plan test::parameter_passing
Starting: task test::demo_undef_bash on localhost
Finished: task test::demo_undef_bash with 0 failures in 0.0 sec
Finished: plan test::parameter_passing in 0.01 sec
Finished on localhost:
Environment: null
Stdin:
{"example_nul":null,"_task":"test::demo_undef_bash"}
{
}
Successful on 1 node: localhost
Ran on 1 node
The parameters example_nul
and _task
metadata are passed to the task as a
JSON string over stdin.
Similarly, parameters are made available to the task as environment
variables where the name of the parameter is converted to an environment variable prefixed with
PT_
. The prefixed environment variable points
to the String
representation in JSON
format of the parameter value. So, the PT_example_nul
environment variable has the value of null
of type String
.
Returning results from plans
Use plans to return results that you can use in other plans or save for use outside of Bolt
Plans, unlike functions, are primarily run for side effects but they can
optionally return a result. To return a result from a plan use the return
function. Any plan that does not call the return
function returns undef
.
plan return_result(
$nodes
) {
return run_task('mytask', $nodes)
}
The result of a plan must match the PlanResult
type alias. This roughly includes JSON types as well as the
Plan language types which have well defined JSON representations in Bolt.
-
Undef
-
String
-
Numeric
-
Boolean
-
Target
-
Result
-
ResultSet
-
Error
-
Array
with onlyPlanResult
- Hash with
String
keys andPlanResult
values
or
Variant[Data, String, Numeric, Boolean, Error, Result, ResultSet, Target, Array[Boltlib::PlanResult], Hash[String, Boltlib::PlanResult]]
Returning errors in plans
To return an error if your plan fails, call the fail_plan
function.
Specify parameters to provide details about the failure.
run_plan('mymodule::myplan')
, this would return an error to the
caller.plan mymodule::myplan {
Error(
message => "Sorry, this plan does not work yet.",
kind => 'mymodule/error',
issue_code => 'NOT_IMPLEMENTED'
)
}
fail_plan("Sorry, this plan does not work yet.", 'mymodule/error')
}
Plan success and failure
There are indicators that a plan has run successfully or failed.
Any plan that completes execution without an error is considered
successful. The bolt
command
exits 0 and any calling plans continue execution. If any calls to run_
functions fail without
_catch_errors
then the plan
halts execution and is considered a failure. Any calling plans also halt until a run_plan
call with _catch_errors
or a catch_errors
block is reached. If one isn't reached,
the bolt
command performs an
exit 2. When writing a plan if you have reason to believe it has failed, you can fail the plan
with the fail_plan
function.
This causes the bolt command to exit 2 and prevents calling plans executing any further,
unless run_plan
was called
with _catch_errors
or in a
catch_errors
block.
Failing plans
upload_file
, run_command
, run_script
, or run_task
are called without the _catch_errors
option and they fail on any nodes, the plan
itself fails. To fail a plan directly call the fail_plan
function. Create a new error with a message and
include the kind, details, or issue code, or pass an existing error to it.
fail_plan('The plan is failing', 'mymodules/pear-shaped', {'failednodes' => $result.error_set.names})
# or
fail_plan($errorobject)
Catching errors in plans
Bolt includes a catch_errors
function that executes a block of code and returns the
error if an error is raised, or returns the result of the block if no errors are raised. You
might get an Error
object returned if you
call run_plan
with _catch_errors
, use a catch_errors
block, or call the Error
method on a result.
The Error
data type includes:
-
msg
: The error message string. -
kind
: A string that defines the kind of error similar to an error class. -
details
: A hash with details about the error from a task or from information about the state of a plan when it fails, for example,exit_code
orstack_trace
. -
issue_code
: A unique code for the message that can be used for translation.
Use the Error
data type in a case expression to match against different kind of errors.
To recover from certain errors, while failing on or ignoring others, set up your plan to
include conditionals based on errors that occur while your plan runs. For example, you can
set up a plan to retry a task when a timeout error occurs, but to fail when there is an
authentication error.
mymodule/not-serious
error. Other errors cause the plan to fail.
plan mymodule::handle_errors {
$result = run_plan('mymodule::myplan', '_catch_errors' => true)
case $result {
Error['mymodule/not-serious'] : {
notice("${result.message}")
}
Error : { fail_plan($result) } }
run_plan('mymodule::plan2')
}
Using the catch errors
function:
plan test (String[1] $role) {
$result_or_error = catch_errors(['bolt/puppetdb-error']) || {
puppetdb_query("inventory[certname] { app_role == ${role} }")
}
$targets = if $result_or_error =~ Error {
# If the PuppetDB query fails
warning("Could not fetch from puppet. Using defaults instead")
# TargetSpec string
"all"
} else {
$result_or_error
}
}
Puppet and Ruby functions in plans
You can define and call Puppet language and Ruby functions in plans.
This is useful for packaging common general logic in your plan. You can also
call the plan functions, such as run_task
or run_plan
,
from within a function.
Not all Puppet language constructs are allowed in plans. The following constructs are not allowed:
-
Defined types.
-
Classes.
-
Resource expressions, such as
file { title: mode => '0777' }
-
Resource default expressions, such as
File { mode => '0666' }
-
Resource overrides, such as
File['/tmp/foo'] { mode => '0444' }
-
Relationship operators:
-> <- ~> <~
-
Functions that operate on a catalog:
include
,require
,contain
,create_resources
. -
Collector expressions, such as
SomeType <| |>
,SomeType <<| |>>
-
ERB templates are not supported. Use EPP instead.
Be aware of a few other Puppet behaviors in plans:
-
The
--strict_variables
option is on, so if you reference a variable that is not set, you get an error. -
--strict=error
is always on, so minor language issues generate errors. For example{ a => 10, a => 20 }
is an error because there is a duplicate key in the hash. -
Most Puppet settings are empty and not-configurable when using Bolt.
-
Logs include "source location" (file, line) instead of resource type or name.
Handling plan function results
Each execution function returns an object type ResultSet
. For each node that the execution takes
place on, this object contains a Result
object. The apply action returns a ResultSet
containing ApplyResult
objects.
A ResultSet
has the following methods:
-
names()
: TheString
names (node URIs) of all nodes in the set as anArray
. -
empty()
: ReturnsBoolean
if the execution result set is empty. -
count()
: Returns anInteger
count of nodes. -
first()
: The firstResult
object, useful to unwrap single results. -
find(String $target_name)
: Look up theResult
for a specific target. -
error_set()
: AResultSet
containing only the results of failed nodes. -
ok_set()
: AResultSet
containing only the successful results. -
filter_set(block)
: Filters aResultSet
with the given block and returns aResultSet
object (where the filter function returns an array or hash). -
targets()
: An array of all theTarget
objects from everyResult
in the set. -
ok():
Boolean
that is the same aserror_nodes.empty
. -
to_data()
: An array of hashes representing eitherResult
orApplyResults
.
A Result
has the following methods:
-
value()
: The hash containing the value of theResult
. -
target()
: TheTarget
object that theResult
is from. -
error()
: AnError
object constructed from the_error
in the value. -
message()
: The_output
key from the value. -
ok()
: Returnstrue
if theResult
was successful. -
[]
: Accesses the value hash directly. -
to_data()
: Hash representation ofResult
. -
action()
: String representation of result type (task, command, etc.).
An ApplyResult
has the following methods:
-
report()
: The hash containing the Puppet report from the application. -
target()
: TheTarget
object that theResult
is from. -
error()
: AnError
object constructed from the_error
in the value. -
ok()
: Returnstrue
if theResult
was successful. -
to_data()
: Hash representation ofApplyResult
. -
action()
: String representation of result type (apply).
An instance of ResultSet
is Iterable
as if it were an Array[Variant[Result, ApplyResult]]
so that iterative functions such as each
, map
, reduce
, or filter
work directly on the ResultSet returning each result.
This example checks if a task ran correctly on all nodes. If it did not, the check fails:
$r = run_task('sometask', ..., '_catch_errors' => true)
unless $r.ok {
fail("Running sometask failed on the nodes ${r.error_nodes.names}")
}
You can do iteration and checking if the result is an Error. This example outputs feedback about the result of a task:
$r = run_task('sometask', ..., '_catch_errors' => true)
$r.each |$result| {
$node = $result.target.name
if $result.ok {
notice("${node} returned a value: ${result.value}")
} else {
notice("${node} errored with a message: ${result.error.message}")
}
}
to_data
on aResultSet
and access hash values. For example:
$r = run_command('whoami', 'localhost,local://0.0.0.0')
$r.to_data.each |$result_hash| { notice($result_hash['result']['stdout']) }
filter_set
to filter a ResultSet
and apply a ResultSet
function such as
targets
to the output:
$filtered = $result.filter_set |$r| {
$r['tag'] == "you're it"
}.targets
Passing sensitive data to tasks
Task parameters defined as sensitive are masked when they appear in plans.
You define a task parameter as sensitive with the metadata property "sensitive": true
. When a task runs, the
values for these sensitive parameters are masked.
run_task('task_with_secrets', ..., password => '$ecret!')
Working with the sensitive function
Sensitive
function to mask data in output logs.
Because plans are written in Puppet DSL, you can use this type
freely. The run_task()
function does not allow parameters of Sensitive
function to be passed. When you need to pass a
sensitive value to a task, you must unwrap it prior to calling run_task()
.$pass = Sensitive('$ecret!')
run_task('task_with_secrets', ..., password => $pass.unwrap)
Target objects
The Target
object represents a node and its specific connection options.
The state of a target is stored in the inventory for the duration of a
plan allowing you to collect facts or set vars for a target and retrieve them later. You can
get a printable representation via the name
function, as well as access components of the target: protocol, host, port, user, password
.
TargetSpec
The execution function takes a parameter with the type alias TargetSpec.
This alias accepts the pattern strings allowed by --nodes
, a single Target object, or an Array of Targets and node
patterns. Generally, use this type for plans that accept a set of targets as a parameter, to
ensure clean interaction with the CLI and other plans. To operate on individual nodes,
resolve it to a list via get_targets
. For example, to loop over each node in a plan accept a TargetSpec
argument, but call
get_targets
on it before
looping.
plan loop(TargetSpec $nodes) {
get_targets($nodes).each |$target| {
run_task('my_task', $target)
}
}
If your plan accepts a single TargetSpec
parameter you can call that parameter nodes
so that it can be specified with the
--nodes
flag from the
command line.
Variables and facts on targets
When Bolt runs, it loads transport config
values, variables, and facts from the inventory. These can be accessed with the $target.facts()
and $target.vars()
functions. During
the course of a plan, you can update the facts or variables for any target. Facts usually
come from running facter
or
another fact collection application on the target or from a fact store like PuppetDB. Variables are computed externally or assigned
directly.
Set variables in a plan using $target.set_var
:
plan vars(String $host) {
$target = get_targets($host)[0]
$target.set_var('newly_provisioned', true)
$targetvars = $target.vars
run_command("echo 'Vars for ${host}: ${$targetvars}'", $host)
}
Or set variables in the inventory file using the vars
key at the group level.
groups:
- name: my_nodes
nodes:
- localhost
vars:
operatingsystem: windows
config:
transport: ssh
Collect facts from the targets
The facts plan connects to the target and discovers facts. It then stores these facts on the targets in the inventory for later use.
The methods used to collect facts:
- On
ssh
targets, it runs a Bash script. - On
winrm
targets, it runs a PowerShell script. - On
pcp
or targets where the Puppet agent is present, it runs Facter.
plan run_with_facts(TargetSpec $nodes) {
# This collects facts on nodes and update the inventory
run_plan(facts, nodes => $nodes)
$centos_nodes = get_targets($nodes).filter |$n| { $n.facts['os']['name'] == 'CentOS' }
$ubuntu_nodes = get_targets($nodes).filter |$n| { $n.facts['os']['name'] == 'Ubuntu' }
run_task(centos_task, $centos_nodes)
run_task(ubuntu_task, $ubuntu_nodes)
}
Collect facts from PuppetDB
When targets are running a Puppet agent
and sending facts to PuppetDB, you can use the puppetdb_fact
plan to collect
facts for them. This example collects facts with the puppetdb_fact
plan, and then uses those facts
to decide which task to run on the targets. You must configure the PuppetDB client before you run it.
plan run_with_facts(TargetSpec $nodes) {
# This collects facts on nodes and update the inventory
run_plan(puppetdb_fact, nodes => $nodes)
$centos_nodes = get_targets($nodes).filter |$n| { $n.facts['os']['name'] == 'CentOS' }
$ubuntu_nodes = get_targets($nodes).filter |$n| { $n.facts['os']['name'] == 'Ubuntu' }
run_task(centos_task, $centos_nodes)
run_task(ubuntu_task, $ubuntu_nodes)
}
Collect general data from PuppetDB
puppetdb_query
function in plans to make direct queries to PuppetDB. For example you can discover nodes from PuppetDB and then run tasks on them. You'll have to configure
the PuppetDB client before running it. You can learn how to structure pql queries here, and find pql reference and examples here
plan pdb_discover {
$result = puppetdb_query("inventory[certname] { app_role == 'web_server' }")
# extract the certnames into an array
$names = $result.map |$r| { $r["certname"] }
# wrap in url. You can skip this if the default transport is pcp
$nodes = $names.map |$n| { "pcp://${n}" }
run_task('my_task', $nodes)
}
Plan logging
Plan run information can be captured in log files or printed to a terminal session using the following methods.
Outputting section to the terminal
Print message strings to STDOUT
using the plan function out::message
. This function always prints messages regardless of the log level
and doesn't log them to the log file.
Puppet log functions
To generate log messages from a plan, use the Puppet log function that
corresponds to the level you want to track: error
, warn
, notice
, info
, or debug
. Configure the
log level for both log files and console logging in bolt.yaml
. The default log level for the console is warn
and for log files is notice
. Use the --debug
flag to
set the console log level to debug
for a
single run.
Default action logging
Bolt logs actions that a plan takes on
targets through the upload_file
, run_command
, run_script
, or run_task
functions. By default it logs a notice level message
when an action starts and another when it completes. If you pass a description to the
function, that is used in place of the generic log message.
run_task(my_task, $targets, "Better description", param1 => "val")
If your plan contains many small actions you may want to suppress these
messages and use explicit calls to the Puppet log functions
instead. This can be accomplished by wrapping actions in a without_default_logging
block which causes the
action messages to be logged at info level instead of notice. For example to
loop over a series of nodes without logging each action.
plan deploy( TargetSpec $nodes) {
without_default_logging() || {
get_targets($nodes).each |$node| {
run_task(deploy, $node)
}
}
}
To avoid complications with parser ambiguity, always call without_default_logging
with ()
and empty block args ||
.
without_default_logging() || { run_command('echo hi', $nodes) }
not
without_default_logging { run_command('echo hi', $nodes) }
Example plans
Check out some example plans for inspiration writing your own.
Resource | Description | Level |
---|---|---|
facts module | Contains tasks and plans to discover facts about target systems. | Getting started |
facts plan | Gathers facts using the facts task and sets the facts in inventory. | Getting started |
facts::info plan | Uses the facts task to discover facts and map relevant fact values to targets. | Getting started |
reboot module | Contains tasks and plans for managing system reboots. | Intermediate |
reboot plan | Restarts a target system and waits for it to become available again. | Intermediate |
Introducing Masterless Puppet with Bolt | Blog post explaining how plans can be used to deploy a load-balanced web server. | Advanced |
profiles::nginx_install plan | Shows an example plan for deploying Nginx and HAProxy. | Advanced |
- Getting started resources show simple use cases such as running a task and manipulating the results.
- Intermediate resources show more advanced features in the plan language.
- Advanced resources show more complex use cases such as applying puppet code blocks and using external modules.