Executing Puppet Tasks with PowerShell via the Puppet Orchestrator API
The Hard Way
Lets begin by writing the code out manually so we understand what’s going on with the URI
’s and Invoke-WebRequest
. In this example we’ll use the Puppet task powershell_tasks::disablesmbv1
. If we look at the task’s metadata file we see what it does via the description and what parameters it allows. To execute this task we’ll make a call to the Puppet Orchestrator’s commands
endpoint.
{
"puppet_task_version": 1,
"description": "A task to test if SMBv1 is enabled and optionally disable it.",
"input_method": "powershell",
"parameters": {
"action": {
"description": "Valid actions are get and set.",
"type": "Enum[get, set]"
},
"reboot": {
"description": "Do we want to reboot the system if changes were made?",
"type": "Optional[Boolean]"
},
"forcereboot": {
"description": "Do we want to reboot the system regardless if changes were made?",
"type": "Optional[Boolean]"
}
}
}
Lets begin by calling this task with its action
parameter and the value of get
.
$master = 'puppet.piccola.us'
$token = '***'
$targetNodes = @('den3-node-1.ad.piccola.us','den3-node-3.ad.piccola.us')
$req = [PSCustomObject]@{
environment = 'production'
task = 'powershell_tasks::disablesmbv1'
params = [PSCustomOBject]@{
action = 'get'
}
description = 'get smbv1 status'
scope = [PSCustomObject]@{
nodes = $targetNodes
}
} | ConvertTo-Json
$hoststr = "https://$master`:8143/orchestrator/v1/command/task"
$headers = @{'X-Authentication' = $token}
Invoke-WebRequest -Uri $hoststr -Method Post -Headers $headers -Body $req
The output. From here we can see we successfully executed the task and produced the job ID of 494
.
StatusCode : 202
StatusDescription : Accepted
Content : {
"job" : {
"id" : "https://puppet.piccola.us:8143/orchestrator/v1/jobs/494",
"name" : "494"
}
}
RawContent : HTTP/1.1 202 Accepted
Vary: Accept-Encoding, User-Agent
Content-Length: 108
Content-Type: application/json;charset=utf-8
Date: Sun, 30 Sep 2018 17:16:11 GMT
Server: Jetty(9.4.z-SNAPSHOT)
{
"...
Forms : {}
Headers : {[Vary, Accept-Encoding, User-Agent], [Content-Length, 108], [Content-Type, application/json;charset=utf-8], [Date, Sun, 30 Sep 2018 17:16:11 GMT]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 108
That’s great, but lets get the output results from each of the nodes we just ran the task on. To do this we’ll make a call to the Puppet Orchestrator’s jobs
endpoint. Lets use the 494
job ID from before.
$master = 'puppet.piccola.us'
$token = '***'
$hoststr = "https://$master`:8143/orchestrator/v1/jobs/494/nodes"
$headers = @{'X-Authentication' = $Token}
$result = Invoke-WebRequest -Uri $hoststr -Method Get -Headers $headers
$content = $result.content | ConvertFrom-Json
foreach ($item in $content.items) {
$item | Select-Object name, result
}
The output. Nice, we can see that on our systems we not only have SMBv1 as an enabled protocol but the feature itself is also installed. Lets disable it!
name result
---- ------
den3-node-3.ad.piccola.us @{Enable_SMB1Protocol=True; Installed_SMB1Protocol=True}
den3-node-1.ad.piccola.us @{Enable_SMB1Protocol=True; Installed_SMB1Protocol=True}
From here, lets modify our task parameters to disable SMBv1. We’ve changed our action
param to set
and also provided the reboot
parameter with a value of $true
. We want our systems to reboot so they take the SMBv1 changes.
$master = 'puppet.piccola.us'
$token = '***'
$targetNodes = @('den3-node-1.ad.piccola.us','den3-node-3.ad.piccola.us')
$req = [PSCustomObject]@{
environment = 'production'
task = 'powershell_tasks::disablesmbv1'
params = [PSCustomOBject]@{
action = 'set'
reboot = $true
}
description = 'set smbv1 status'
scope = [PSCustomObject]@{
nodes = $targetNodes
}
} | ConvertTo-Json
$hoststr = "https://$master`:8143/orchestrator/v1/command/task"
$headers = @{'X-Authentication' = $token}
Invoke-WebRequest -Uri $hoststr -Method Post -Headers $headers -Body $req
If we run our task again with get
we can see that SMBv1 is disabled and uninstalled. Awesome.
name result
---- ------
den3-node-3.ad.piccola.us @{Enable_SMB1Protocol=False; Installed_SMB1Protocol=False}
den3-node-1.ad.piccola.us @{Enable_SMB1Protocol=False; Installed_SMB1Protocol=False}
The Easy Way
While this has been fun, lets wrap all this PowerShell up into a few functions. Invoke-PuppetTask
, Get-PuppetJobNodes
, and Get-PuppetJob
. Note we added Get-PuppetJob
, this function is going to simply get job details from a supplied Job ID. Invoke-PuppetTask
will use this function internally to monitor our job’s state
. You’ll see this below with Invoke-PuppetTask
’s -Wait
and -Timeout
parameters.
First, lets get the current SMBv1 configurations.
$master = 'puppet.piccola.us'
$token = '***'
$scope = @('den3-node-1.ad.piccola.us','den3-node-5.ad.piccola.us')
$splat = @{
Token = $Token
Master = $Master
Task = 'powershell_tasks::disablesmbv1'
Environment = 'production'
Parameters = [PSCustomObject]@{
action = 'get'
}
Description = 'Get SMBv1'
Scope = $scope
ScopeType = 'nodes'
}
$taskRunGet = Invoke-PuppetTask @splat -Wait -Timeout 120
Get-PuppetJobNodes -Token $Token -Master $Master -ID $taskRunGet.job.name | select name, result
Output.
name result
---- ------
den3-node-5.ad.piccola.us @{Enable_SMB1Protocol=True; Installed_SMB1Protocol=True}
den3-node-1.ad.piccola.us @{Enable_SMB1Protocol=True; Installed_SMB1Protocol=True}
Second, lets set the SMBv1 configurations.
$master = 'puppet.piccola.us'
$token = '***'
$scope = @('den3-node-1.ad.piccola.us','den3-node-5.ad.piccola.us')
$splat = @{
Token = $Token
Master = $Master
Task = 'powershell_tasks::disablesmbv1'
Environment = 'production'
Parameters = [PSCustomObject]@{
action = 'set'
reboot = $true
}
Description = 'Set SMBv1'
Scope = $scope
ScopeType = 'nodes'
}
Invoke-PuppetTask @splat -Wait -Timeout 120
Third, once the systems come back up from rebooting lets get the SMBv1 configurations again.
$master = 'puppet.piccola.us'
$token = '***'
$scope = @('den3-node-1.ad.piccola.us','den3-node-5.ad.piccola.us')
$splat = @{
Token = $Token
Master = $Master
Task = 'powershell_tasks::disablesmbv1'
Environment = 'production'
Parameters = [PSCustomObject]@{
action = 'get'
}
Description = 'Get SMBv1'
Scope = $scope
ScopeType = 'nodes'
}
$taskRunGet = Invoke-PuppetTask @splat -Wait -Timeout 120
Get-PuppetJobNodes -Token $Token -Master $Master -ID $taskRunGet.job.name | select name, result
Output.
name result
---- ------
den3-node-5.ad.piccola.us @{Enable_SMB1Protocol=False; Installed_SMB1Protocol=False}
den3-node-1.ad.piccola.us @{Enable_SMB1Protocol=False; Installed_SMB1Protocol=False}
Sweet, we’ve successfully executed PowerShell on remote systems via a Puppet Task triggered via the Puppet API.
The Functions
Get-PuppetJobNodes
Function Get-PuppetJobNodes {
Param(
[Parameter(Mandatory)]
[int]$ID,
[Parameter(Mandatory)]
[string]$Token,
[Parameter(Mandatory)]
[string]$Master
)
$hoststr = "https://$master`:8143/orchestrator/v1/jobs/$id/nodes"
$headers = @{'X-Authentication' = $Token}
$result = Invoke-WebRequest -Uri $hoststr -Method Get -Headers $headers
$content = $result.content | ConvertFrom-Json
Write-Output $content.items
}
Invoke-PuppetTask
Function Invoke-PuppetTask {
Param(
[Parameter(Mandatory)]
[string]$Token,
[Parameter(Mandatory)]
[string]$Master,
[Parameter(Mandatory)]
[string]$Task,
[Parameter()]
[string]$Environment = 'production',
[Parameter()]
[PSCustomObject]$Parameters = @{},
[Parameter()]
[string]$Description = '',
[Parameter(Mandatory)]
[PSCustomObject[]]$Scope,
[Parameter(Mandatory)]
[ValidateSet('nodes')]
[string]$ScopeType,
[Parameter()]
[switch]$Wait,
[Parameter()]
[int]$Timeout = 300
)
$req = [PSCustomObject]@{
environment = $Environment
task = $Task
params = $Parameters
description = $Description
scope = [PSCustomObject]@{
$ScopeType = $Scope
}
} | ConvertTo-Json
$hoststr = "https://$master`:8143/orchestrator/v1/command/task"
$headers = @{'X-Authentication' = $Token}
$result = Invoke-WebRequest -Uri $hoststr -Method Post -Headers $headers -Body $req
$content = $result.content | ConvertFrom-Json
if ($wait) {
# sleep 5s for the job to register
Start-Sleep -Seconds 5
$jobSplat = @{
token = $Token
master = $master
id = $content.job.name
}
# create a timespan
$timespan = New-TimeSpan -Seconds $timeout
# start a timer
$stopwatch = [diagnostics.stopwatch]::StartNew()
# get the job state every 5 seconds until our timeout is met
while ($stopwatch.elapsed -lt $timespan) {
# optoins are new, ready, running, stopping, stopped, finished, or failed
$job = Get-PuppetJob @jobSplat
if (($job.State -eq 'stopped') -or ($job.State -eq 'finished') -or ($job.State -eq 'failed')) {
$taskJobContent = [PSCustomObject]@{
task = $content
job = $job
}
Write-Output $taskJobContent
break
}
Start-Sleep -Seconds 5
}
if ($stopwatch.elapsed -ge $timespan) {
Write-Error "Timeout of $Timeout`s has exceeded."
break
}
} else {
Write-Output $content
}
}
Get-PuppetJob
Function Get-PuppetJob {
Param(
[Parameter(Mandatory)]
[int]$ID,
[Parameter(Mandatory)]
[string]$Token,
[Parameter(Mandatory)]
[string]$Master
)
$hoststr = "https://$master`:8143/orchestrator/v1/jobs/$id"
$headers = @{'X-Authentication' = $Token}
$result = Invoke-WebRequest -Uri $hoststr -Method Get -Headers $headers
$content = $result.content | ConvertFrom-Json
Write-Output $content
}