homeblogcombining powershell bolt and puppet tasks part 1

Combining PowerShell, Bolt and Puppet Tasks – Part 1

Editor's note: We invite you to join us at Puppetize PDX this 9-10 October 2019. There will be several sessions on Bolt, plus a Bolt workshop the day before the conference.

Welcome to part one of a three-part blog series about combining PowerShell, Bolt and Puppet Tasks. In part one, I’ll walk you through adding a PowerShell task to an existing module — the popular WSUS Client Puppet module. I’ll cover what you need to know to get started, from installing Bolt and configuring WinRM, to writing a PowerShell Task and running it on a remote computer.

Setting up

Install Bolt

There are a few ways we could do this on Windows, all of which are described in the Bolt documentation; Installing as an MSI, installing with Chocolatey or installing as a ruby gem

WinRM Configuration

Bolt can use SSH or WinRM to communicate with nodes, but with Windows the natural choice is WinRM. While it is outside the scope of this blog to go over how to configure your WinRM Service, for these examples I am using the HTTP Listener (not recommended for production use), with only the Kerberos and Negotiate authentication methods enabled. This is a typical configuration when using the winrm quickconfig command.

What are commands, scripts, tasks and plans?

Bolt uses the terms commands, scripts, tasks and plans, so it best to define what they mean first.


A Bolt command is a single line of text which can be executed on a computer, for example in PowerShell Write-Host "Hello World!"


From the Bolt documentation

You can execute scripts on remote machines with Bolt.

Bolt copies the script from the local system to the remote node, executes it on that remote node, and then deletes the script from the remote node.

A Bolt script is a single file containing many commands. In this instance it will be a PowerShell file, for example update_history.ps1.


From the Bolt documentation

Puppet tasks are single, ad hoc actions that you can run on target machines in your infrastructure, allowing you to make as-needed changes to remote systems

Tasks use Scripts to execute them on remote machines.


From the Bolt documentation

Plans are sets of tasks that can be combined with other logic. This allows you to do more complex task operations, such as running multiple tasks with one command, computing values for the input for a task, or running certain tasks based on the results of another task.

Running a single command

Now that we have Bolt installed, we can run a test command to make sure it's working. Let's list all the running processes:

Great! This just listed all of the processes on my machine. Let's breakdown the command line:

bolt command run : In this case we simply want to run a command. Bolt can also run script files, tasks and plans

Get-Process : This is the PowerShell command that we will run on the remote computer

--nodes winrm://localhost : We want to run the command against our local computer, so we specify the transport as winrm and the name localhost. The Bolt documentation describes the ways you can specify multiple nodes, or use an inventory file.

--no-ssl : We then specify we want Bolt to use the HTTP listener (as opposed to HTTPS)

--user Administrator --password : We then specify the username as Administrator, and prompt for the password.

While you can add the password on the command line, it's not very secure as it may appear in your console history or in a tool like Process Explorer, which can see the command line for a process.

It's also a good idea to put --password at the end of the command, so that Bolt doesn't misinterpret the password. For example, don't write ... --password detailed=true, as Bolt will try to authenticate with the password detailed=true, instead of prompting for the password and then passing the script parameter called detailed.

Let's move on to writing more than just a one line command with Puppet Tasks.

Writing PowerShell tasks

Tasks are similar to PowerShell script files, but they are kept in Puppet modules and can have metadata. This allows you to reuse and share them more easily. The first thing you need when writing a PowerShell task is a Puppet module. Tasks reside in the tasks directory, for example, here in the Windows Reboot module or here in the MySQL module.

You can create a new task using the Puppet Development Kit (PDK) with the pdk new task command, or by creating a PS1 file in the tasks directory.

Creating a PowerShell task

Source Code Link

Let's start with a task in the WSUS Client module to return the update history of a computer.

We create the file tasks/update_history.ps1 with the content in this link (It's a little long). You may ask, why don't we use the popular PSWindowsUpdate PowerShell module? As we'll be running this on remote computers, we don't know if that module is installed, which means we shouldn't use it. This is important to remember if you later publish your module for other people to use.

One of the great things about having a script file is that we can use our normal PowerShell tools (VS Code, ISE etc.) to write and test the script, and then use Bolt to execute it.~

To run the task manually, we can use normal PowerShell commands:

Running a PowerShell task

Now we can use Bolt to run the task remotely. But first let's make sure the task exists:

Let's breakdown the command line:

bolt task show : This instructs Bolt to list all of the tasks it knows about

--modulepath C:\modules : As tasks are located in Puppet modules, we need to tell Bolt where the modules are located. In this case, my modules are located in C:\modules, and the WSUS Client module is at C:\modules\wsus_client.

The output shows lots of task names with our new task down the bottom of the list.

  • All of the other tasks come as part of Bolt itself. In this instance, we're using Bolt v0.20.5.

  • Tasks are uniquely named by the name of the module (wsus_client), a double colon (::) and then the task filename (update_history)

So now we know Bolt can see our new task, let's run it;

Comparing the output of the manual process versus the Bolt process, they look almost the same. There's additional data added at the end of the Bolt output, which can be ignored.

Why use ConvertTo-JSON?

You may have noticed that the output from the script is not pure text, but is JSON encoded text. This comes from the last line in the PowerShell script

Bolt tasks return text, but if we want the output of the task to be used by other tools or processes, the output should be structured text. In particular, the output can be used by Bolt plans which can orchestrate multiple Bolt tasks. Bolt uses JSON structured text for it's structured output format, which is great as PowerShell has native support for JSON, through the ConvertTo-JSON function in PowerShell 3.0 and above.

So what about PowerShell 2.0? Right now you would need to output the equivalent text by yourself in the PowerShell script, for example:

The string is now in a JSON format.

For small, simple PowerShell scripts this method is adequate, but for complex data, like the update_history.ps1 file we used earlier, it's quite difficult to do. A quick search in your favourite search engine for "convertto-json powershell 2" can provide some good workarounds.

There is a Bolt feature request to add JSON support for PowerShell 2.0, but it is not currently available.

Adding script parameters

Source Code Link

The output from the script is quite verbose. What we really want is for it to only return the information we need, but to still have the ability to get everything. So we need to add a Detailed script parameter:

For brief information

And for detailed information

Bolt supports passing parameters to PowerShell scripts through named parameters. So we need to add cmdlet binding to the top of our script and specify the Detailed parameter.

And then change our output to add the additional settings. I've left this out of this blog, but you can see them on the WSUS Client GitHub repository

Let's try this locally in PowerShell:

Great! We can now change how much information we return. How does Bolt use this? Bolt uses a metadata file to store information about the task, including the available parameters and their type.

Adding task metadata

Source Code Link

Task metadata files are JSON formatted files with the same name as their script. This means with our script called tasks\update_history.ps1, the metadata file will be called tasks\update_history.json. So let's create that file with information about our task:

Let's break this down:

"description": "Returns a history of installed Windows Updates.", : This is a short description of the task. When we previously ran the bolt task show command, there was a description column, and some tasks had information there. This is where that information comes from.

"parameters": { : This is where we define the new Detailed parameter

"detailed": { : The is the name of the new parameter. Note that it is in lowercase, compared to the scripts which are mixed case

"description": "Return detailed update ..., : This is a short description of the parameter and is useful for people to understand how your task works

"type": "Optional[Boolean]" : This defines the type of data we expect from the user when running the task and whether it is mandatory or optional. We will go into more detail about Bolt types below.

"input_method": "powershell" : This tells Bolt that it should use the PowerShell method when sending script parameters. Normally this is not required, as PowerShell script files (.PS1) will automatically use this method. However at the time of writing there is a bug in Bolt, and as a workaround the input method has to be defined. Update: That bug has been fixed, but adding this setting is still a best practice.

The bolt documentation lists all of the available settings in the metadata file.

Choosing a Bolt parameter type

In our PowerShell script, the Detailed parameter is defined as;

This is a Boolean parameter which is not mandatory. The equivalent definition in a Bolt type is;

This reads as a Boolean type which is optional; that is, not mandatory.

The Bolt parameter types come from the Puppet type system and can, mostly, be directly translated into PowerShell types and PowerShell parameter attributes:

Bolt TypePowerShell Parameter
String[Parameter(Mandatory = $True)] [String] $Param
Optional[String][String] $Param
String[5][Parameter(Mandatory = $True)] [ValidateLength(5)] [String] $Param
Pattern[/\A\w+\Z/][Parameter(Mandatory = $True)] [ValidatePattern({\A\w+\Z})] [String] $Param
Integer[Parameter(Mandatory = $True)] [Int] $Param
Integer[1, 20][Parameter(Mandatory = $True)] [ValidateRange(1, 20)] [Int] $Param
Optional[Integer][Int] $Param
Boolean[Parameter(Mandatory = $True)] [Switch] $Param
Boolean[Parameter(Mandatory = $True)] [Bool] $Param
Optional[Boolean][Switch] $Param
Optional[Boolean][Bool] $Param
  • This is not a complete list, but commonly used script parameters

  • In Bolt, all parameters are mandatory unless the Optional[] type is used, whereas in PowerShell, parameters are optional unless Mandatory = $True is set

  • The default values of a task parameter need to be set in the PowerShell script, but are generally documented in the task matadata file

  • While you can create complex Bolt types and PowerShell parameters, it is best to keep them as simple as possible (String, Int, Boolean), as the translation between both types is not always exact. For example, PowerShell parameters can use Position, ParameterSetName and ValidateScript, but they have no comparable Bolt type.

The full list of available parameter types is located in the Bolt documentation, and more detailed Puppet type information in the Puppet documentation.

Viewing task metadata

Now that we have some task metadata, let's display that information in Bolt:

We can now see the description of task in the output. Now let's get more more information about our task:

By adding the task name to the show command (... show show wsus_client::update_history ), the output shows the complete information about the task, including all available parameters.

Running a task with parameters

The task show Bolt command gives us an example of how to use task parameters ... [detailed=<value>]. So let's run the task with detailed output:

We added detailed=true to the command line, which passes the parameter to the PowerShell script. We can also use detailed=false to return only the basic information, which is the same as the default behavior.

What if we pass in something other than true or false? Such as abc123?

Bolt will validate the input, but as we'll see later, it's really useful when used with Puppet Enterprise.

Adding more parameters

Source Code Link

Instead of returning all of the updates, it would be great to add some filtering, for example, by name, a unique identification number (UpdateID) and by total number returned. Let's add three more parameters using the same development process.

The parameters we'll add are:

title : Return updates which match the specified regular expression. Default is to all updates

updateid : Return updates which the specified Update ID. Default is to all update

maximumupdates : Limit the size of the history returned. Default is to return a maximum of 300 items

And the development process:

  1. Add the parameters to the PowerShell file
  2. Make changes to the PowerShell file and test locally
  3. Add the parameters to the task metadata
  4. Test the metadata changes can be seen by Bolt
  5. Run the task with the new parameters using Bolt

1. Add the parameters to the PowerShell file

Source Code Link

2. Make changes to the PowerShell file and test locally

Source Code Link

3. Add the parameters to the task metadata

Source Code Link

Note: I should not have used Optional[String] for the maximumupdates parameter. It really should have been Optional[Integer[0]] as it’s a number, not text. This will be fixed later.

4. Test that the metadata changes can be seen by Bolt

5. Run the task with the new parameters using Bolt

Packaging the module

Now that we have our task working we can share the module, and the task, on the Puppet Forge, or an internal repository. How to package and publish your module is described in the PDK and Puppet documentation.

The Puppet Forge also includes a helpful search filter to find modules that have tasks.

Wrapping up

We created and tested a PowerShell script on our local computer, and then turned that into a Bolt Task. We then packaged the module so others could use the task.

But that's just the beginning ... The task used in this blog was only retrieving information, but tasks can also make changes to the computer. We could start downloading updates or applying updates, combine this task with other tasks to create more complex workflows, for example, patching Active Directory Domain Controllers, or multi-computer Remote Desktop Services farms.

Tasks and plans are very powerful tools to orchestrate your already powerful PowerShell scripts!

In Part 2, we'll look at how to test our newly created PowerShell Task.

Learn more