PSPuppetOrchestrator.psm1

function Set-ServerCertificateValidationCallback {
    <#
    .SYNOPSIS
        A stub for testing purposes.
    .DESCRIPTION
        This cmdlet stubs the call to setting the callback to true so that it
        can tested via pester and it's use can be detected via Assert-MockCalled

    .EXAMPLE
        Set-ServerCertificateValidationCallback

        This is a stub method used internally to make Pester Testing easier. It sets
        the ServerCertificateValidationCallback method in a way that's easier to
        Mock during testing.
    #>


    [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
}

function Set-SkipCertificateCheck {
    <#
    .SYNOPSIS
        Detect Invoke-RestMethod version and ignore server ssl certs.

    .DESCRIPTION
        Detect whether certificate checking should be skipped via the switch
        parameter on Invoke-RestMethod or via setting the ServerCertificateCallback
        setting.

    .PARAMETER SkipCertificateCheck
        Indicate that you would like to skip SSL Certificate validation from
        the Master server and trust the certificate you receive.

    .PARAMETER PARAMS
        The hash table of params you will eventually use with Invoke-RestMethod.
        This cmdlet will add a SkipCertificateCheck key and set it to true if
        the current version of Invoke-RestMethod supports that switch. If it
        does not then it will set the ServerCertificateValidationCallback
        method to always return true and achieve the same effect.

    .EXAMPLE
        $invokeParams = @{
        >> Uri = $uri
        >> Method = 'Post'
        >> Body = $req
        >> ContentType = 'application/json'
        >> }

        PS > $invokeParams = Set-SkipCertificateCheck -SkipCertificateCheck $SkipCertificateCheck -Params $invokeParams

        PS > Invoke-RestMethod @invokeParams | Select-Object -ExpandProperty token

        This example detect if the current version of Invoke-RestMethod implements the -SkipCertificateCheck switch parameter
        If it does then it will add that key to the $invokeParams hash table for use when splatting with Invoke-RestMethod.
        If it does not then it will set the ServerCertificateValidationCallback method in .NET to always return $true
        to acheive the same effect in PowerShell 5.1

    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [bool]
        $SkipCertificateCheck,
        [Parameter(Mandatory)]
        [hashtable]
        $params
    )

    process {
        if ($SkipCertificateCheck) {
            if (Get-Help Invoke-RestMethod -Parameter SkipCertificateCheck -ErrorAction SilentlyContinue) {
                $params.SkipCertificateCheck = $true
            } else {
                Set-ServerCertificateValidationCallback
            }
        }
        $params
    }
}

Function Wait-PuppetNodePCPBroker {
    <#
    .SYNOPSIS
        Returns Hello world
    .DESCRIPTION
        Wait-PuppetNodePCPBroker was originally written in an effort to detect when nodes rebooted by
        evaluating a nodes's PCP Broker connected state. Since the advent of the reboot plan as seen
        in https://github.com/puppetlabs/puppetlabs-reboot/blob/master/plans/init.pp Wait-PuppetNodePCPBroker
        is no longer a viable solution. Never was to begin with really.
 
    .PARAMETER Timeout
        x
    .PARAMETER Token
        x
    .PARAMETER Master
        x
    .PARAMETER Node
        x
    .EXAMPLE
        PS> Get-HelloWorld
 
        Runs the command
    #>


    Param(
        [Parameter(Mandatory)]
        [string]$Token,
        [Parameter(Mandatory)]
        [string]$Master,
        [Parameter(Mandatory)]
        [string]$Node,
        [Parameter()]
        [int]$Timeout = 300
    )

    $detailsSplat = @{
        token = $Token
        master = $master
        node = $node
    }

    # create a timespan
    $timespan = New-TimeSpan -Seconds $timeout
    # start a timer
    $stopwatch = [diagnostics.stopwatch]::StartNew()

    # get the broker status every 5 seconds until our timeout is met
    while ($stopwatch.elapsed -lt $timespan) {
        # get the broker status
        if (($one = Get-PuppetNodePCPBrokerDetails @detailsSplat).connected -eq $false) {
            # broker status is disconnected, sleep 5s and check again to confirm not a blip or false positive
            Write-Verbose "Broker status is $($one.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
            Write-Verbose "Sleping 5 seconds and checking again."
            Start-Sleep -Seconds 5
            if (($two = Get-PuppetNodePCPBrokerDetails @detailsSplat).connected -eq $false) {
                Write-Verbose "Broker status is still $($two.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
                # broker status is disconnected, break out of the loop
                break
            }
        } else {
            Write-Verbose "Broker status is $($one.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
        }
        Start-Sleep -Seconds 5
    }
    if ($stopwatch.elapsed -ge $timespan) {
        Write-Error "Timeout of $Timeout`s has exceeded."
        break
    }

    Write-Verbose "$Node broker status confirmed disconnected."

    # get the broker status every 5 seconds until our timeout is met
    while ($stopwatch.elapsed -lt $timespan) {
        # get the broker status
        if (($three = Get-PuppetNodePCPBrokerDetails @detailsSplat).connected -eq $true) {
            # broker status is connected, sleep 5s and check again to confirm not a blip or false positive
            Write-Verbose "Broker status is $($three.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
            Write-Verbose "Sleping 5 seconds and checking again."
            Start-Sleep -Seconds 5
            if (($four = Get-PuppetNodePCPBrokerDetails @detailsSplat).connected -eq $true) {
                Write-Verbose "Broker status is still $($four.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
                # broker status is connected, break out of the loop
                break
            }
        } else {
            Write-Verbose "Broker status is $($three.connected), (timeout: $($stopwatch.elapsed.TotalSeconds)s of $Timeout`s elapsed)."
        }
        Start-Sleep -Seconds 5
    }
    if ($stopwatch.elapsed -ge $timespan) {
        Write-Error "Timeout of $Timeout`s has exceeded."
        break
    }

    Write-Verbose "$Node broker status confirmed connected."
}

function Get-PuppetAuthToken {
    <#
    .SYNOPSIS
        Get a Puppet Auth Token for use with API calls.
    .DESCRIPTION
        Call the /rbac-api/v1/auth/token end point with a username and password
        to obtain an authorization token that can be used with the other
        cmdlets in this module.

    .PARAMETER Master
        The FQDN of the Puppet Master server that your DNS can resolve. You do
        not need to include the HTTPS portion of the address. Only the server name.

    .PARAMETER Lifetime
        An integer value specifying how long you would like the token to last
        paired with one of the letter [smhdy] (example '2d' for two days) to
        specify the units of time in seconds, minutes, hours, days, or years. If
        you call this cmdlet to request a token within the valid lifetime of
        your current token, you will receive your current token back again.

    .PARAMETER Port
        The port your Puppet Master API end poins are listening on. Be default
        this will be 4433, but it is configurable.

    .PARAMETER Credential
        A PSCredential object that contains the username and password for the user
        you would like to receive a token for. If you do not provide one you will
        be prompted at the commandline for one.

    .PARAMETER SkipCertificateCheck
        Skip certificate validation on the SSL certificate provided by the Puppet
        master server you connect to. This parameter will function as expected
        for both PowerShell 5.1 and 6+.

    .EXAMPLE
        $cred = Get-Credential -Username Admin
        PS > $token = Get-PuppetAuthToken -Master 'pe-master.corp.net' -SkipCertificateCheck -Credential $cred

        Create a credential object and then call this cmdlet to retrieve a token

    .EXAMPLE
        $token = Get-PuppetAuthToken -Master 'pe-master.corp.net' -SkipCertificateCheck

        If you do not pass a credential object, you will be prompted for credentials.

    .EXAMPLE
        $token = Get-PuppetAuthToken -Lifetime '2m' -Master 'pe-master.corp.net' -SkipCertificateCheck

        Create a short lived token so that an automated process can use it but it
        dies quickley thereafter.
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        [String] Authorization Token Value.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Master,
        [Parameter()]
        [String]
        $Lifetime = '1d',
        [Parameter()]
        [Int]$Port = 4433,
        [Parameter(Mandatory)]
        [PSCredential]$Credential,
        [Parameter()]
        [Switch]$SkipCertificateCheck = $false
    )
    process {
        $uri = "https://$Master`:$port/rbac-api/v1/auth/token"

        $req = [PSCustomObject]@{
            login       = $credential.UserName
            password    = $credential.GetNetworkCredential().Password
            lifetime    = $Lifetime
            description = "Token used for authentication to Puppet api requests."
            label       = "Personal Workstation token"
        } | ConvertTo-JSON

        $invokeParams = @{
            Uri                  = $uri
            Method               = 'Post'
            Body                 = $req
            ContentType          = 'application/json'
        }

        $invokeParams = Set-SkipCertificateCheck -SkipCertificateCheck $SkipCertificateCheck -Params $invokeParams

        try {
            Invoke-RestMethod @invokeParams | Select-Object -ExpandProperty token
        }
        catch {
            Write-Error $_.Exception.Message
        }
        finally {
            [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
        }
    }
}

Function Get-PuppetJob {
    <#
    .SYNOPSIS
        Get details on a Puppet job.
    .DESCRIPTION
        Get details on a Puppet job.
    .PARAMETER ID
        The ID of the job.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetJob -Token $token -Master $master -ID 906
 
        description :
        report : @{id=https://puppet:8143/orchestrator/v1/jobs/906/report}
        name : 906
        events : @{id=https://puppet:8143/orchestrator/v1/jobs/906/events}
        command : task
        type : task
        state : failed
        nodes : @{id=https://puppet:8143/orchestrator/v1/jobs/906/nodes}
        status : {@{state=ready; enter_time=2019-09-04T16:50:09Z; exit_time=2019-09-04T16:50:10Z}, @{state=running; enter_time=2019-09-04T16:50:10Z; exit_time=2019-09-04T16:50:43Z}, @{state=failed; enter_time=2019-09-04T16:50:43Z; exit_time=}}
        id : https://puppet:8143/orchestrator/v1/jobs/906
        environment : @{name=production}
        options : @{description=; transport=pxp; noop=False; task=powershell_tasks::getkb; sensitive=System.Object[]; params=; scope=; environment=production}
        timestamp : 2019-09-04T16:50:43Z
        owner : @{email=; is_revoked=False; last_login=2019-09-04T16:48:50.049Z; is_remote=False; login=admin; is_superuser=True; id=42bf351c-f9ec-40af-84ad-e976fec7f4bd; role_ids=System.Object[]; display_name=Administrator; is_group=False}
        node_count : 3
        node_states : @{failed=1; finished=2}
    #>


    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-RestMethod -Uri $hoststr -Method Get -Headers $headers
    $content = $result

    Write-Output $content
}

Function Get-PuppetJobReport {
    <#
    .SYNOPSIS
        Get the report for a given Puppet job.
    .DESCRIPTION
        Get the report for a given Puppet job.
    .PARAMETER ID
        The ID of the job.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetJobReport -Master $master -Token $token -ID 906
 
        node state start_timestamp finish_timestamp timestamp events
        ---- ----- --------------- ---------------- --------- ------
        den3w108r2psv2 failed 2019-09-04T16:50:10Z 2019-09-04T16:50:12Z 2019-09-04T16:50:12Z {}
        den3w108r2psv3 finished 2019-09-04T16:50:10Z 2019-09-04T16:50:42Z 2019-09-04T16:50:42Z {}
        den3w108r2psv4 finished 2019-09-04T16:50:10Z 2019-09-04T16:50:43Z 2019-09-04T16:50:43Z {}
    #>


    Param(
        [Parameter(Mandatory)]
        [int]$ID,
        [Parameter(Mandatory)]
        [string]$Token,
        [Parameter(Mandatory)]
        [string]$Master
    )

    $hoststr = "https://$master`:8143/orchestrator/v1/jobs/$id/report"
    $headers = @{'X-Authentication' = $Token}
    $result  = Invoke-RestMethod -Uri $hoststr -Method Get -Headers $headers
    foreach ($report in $result.report) {
        Write-Output $report
    }
}

Function Get-PuppetJobResults {
    <#
    .SYNOPSIS
        Get the results from a Puppet job.
    .DESCRIPTION
        Get the results from a Puppet job.
    .PARAMETER ID
        The ID of the job.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetJobResults -Master $master -Token $token -ID 930
 
        finish_timestamp : 10/4/19 3:45:26 PM
        transaction_uuid :
        start_timestamp : 10/4/19 3:45:18 PM
        name : den3w108r2psv5
        duration : 7.767
        state : finished
        details :
        result : @{Source=DEN3W108R2PSV5; HotFixID=KB2620704; Description=Security Update; InstalledBy=NT AUTHORITY\SYSTEM; InstalledOn=Thursday, September 06, 2018 12:00:00 AM}
        latest-event-id : 5709
        timestamp : 10/4/19 3:45:26 PM
 
        finish_timestamp : 10/4/19 3:45:34 PM
        transaction_uuid :
        start_timestamp : 10/4/19 3:45:19 PM
        name : den3w108r2psv3
        duration : 15.264
        state : finished
        details :
        result : @{Source=DEN3W108R2PSV3; HotFixID=KB2620704; Description=Security Update; InstalledBy=; InstalledOn=Friday, September 07, 2018 12:00:00 AM}
        latest-event-id : 5712
        timestamp : 10/4/19 3:45:34 PM
 
        finish_timestamp : 10/4/19 3:45:34 PM
        transaction_uuid :
        start_timestamp : 10/4/19 3:45:19 PM
        name : den3w108r2psv4
        duration : 15.505
        state : finished
        details :
        result : @{Source=DEN3W108R2PSV4; HotFixID=KB2620704; Description=Security Update; InstalledBy=; InstalledOn=Friday, September 07, 2018 12:00:00 AM}
        latest-event-id : 5713
        timestamp : 10/4/19 3:45:34 PM
    #>


    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-RestMethod -Uri $hoststr -Method Get -Headers $headers
    foreach ($item in $result.items) {
        Write-Output $item
    }
}

Function Get-PuppetPCPNodeBrokerDetails {
    <#
    .SYNOPSIS
        Get a node's PCP broker details.
    .DESCRIPTION
        Get a node's PCP broker details. This is useful if you want to know the status of PCP before executing a task or plan.
    .PARAMETER Node
        The Puppet node name.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetPCPNodeBrokerDetails -Master $master -Token $token -Node 'den3w108r2psv3'
 
        name : den3w108r2psv3
        connected : True
        broker : pcp://puppet/server
        timestamp : 10/2/19 2:01:53 AM
    #>


    Param(
        [Parameter(Mandatory)]
        [string]$Token,
        [Parameter(Mandatory)]
        [string]$Master,
        [Parameter(Mandatory)]
        [string]$Node
    )

    $hoststr = "https://$master`:8143/orchestrator/v1/inventory/$node"
    $headers = @{'X-Authentication' = $Token}
    $result  = Invoke-RestMethod -Uri $hoststr -Method Get -Headers $headers
    Write-Output $result
}

Function Get-PuppetTask {
    <#
    .SYNOPSIS
        Get details on a Puppet task.
    .DESCRIPTION
        Get details on a Puppet task.
    .PARAMETER Module
        The module of the puppet task, if applicable.
    .PARAMETER Name
        The name of the Puppet task.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetTask -Master $master -Token $token -Name 'reboot'
 
        id : https://puppet:8143/orchestrator/v1/tasks/reboot/init
        name : reboot
        permitted : True
        metadata : @{description=Reboots a machine; implementations=System.Object[]; input_method=stdin; parameters=; supports_noop=False}
        files : {@{filename=init.rb; sha256=fb7e0e0de640b82844be931e59405de73e1e290c9540c204a6c79838a0e39fce; size_bytes=2556; uri=}, @{filename=nix.sh; sha256=dfb2ddfe17056c316d7260bcce853aabc5b18a266888f76b23314d0d4c8daee5; size_bytes=692; uri=}, @{filename=win.ps1; sha256=155f5ab7d63f1913ccf8f4f5563f1b2be2a49130a4787a8c48ff770cfe8e6415; size_bytes=785; uri=}}
        environment : @{name=production; code_id=}
    .EXAMPLE
        PS> Get-PuppetTask -Master $master -Token $token -Module 'powershell_tasks' -Name 'disablesmbv1'
 
        id : https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/disablesmbv1
        name : powershell_tasks::disablesmbv1
        permitted : True
        metadata : @{description=A task to test if SMBv1 is enabled and optionally disable it.; input_method=powershell; parameters=; puppet_task_version=1}
        files : {@{filename=disablesmbv1.ps1; sha256=c10f3ae37a6e2686c419ec955ee51f9894109ed073bf5c3b3280255b3785e0dc; size_bytes=3536; uri=}}
        environment : @{name=production; code_id=}
    #>


    Param(
        [Parameter(Mandatory)]
        [string]$Token,
        [Parameter(Mandatory)]
        [string]$Master,
        [Parameter()]
        [string]$Module,
        [Parameter(Mandatory)]
        [string]$Name
    )

    $hoststr = "https://$master`:8143/orchestrator/v1/tasks/$Module/$Name"
    $headers = @{'X-Authentication' = $Token}

    # try and get the task in it's standard form $moduleName/$taskName
    try {
        $result = Invoke-RestMethod -Uri $hoststr -Method Get -Headers $headers -ErrorAction SilentlyContinue
    } catch {
        # try and get the task again assuming it's built in with a default task name of 'init' (e.g. reboot/init)
        try {
            $hoststr = "https://$master`:8143/orchestrator/v1/tasks/$name/init"
            $result  = Invoke-RestMethod -Uri $hoststr -Method Get -Headers $headers
        } catch {
            Write-Error $_.exception.message
        }
    }

    if ($result) {
        Write-Output $result
    }
}

Function Get-PuppetTasks {
    <#
    .SYNOPSIS
        Get a list of Puppet Tasks.
    .DESCRIPTION
        Get a list of Puppet Tasks.
    .PARAMETER Environment
        The environment to use.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        PS> Get-PuppetTasks -token $token -master $master
 
        id name permitted
        -- ---- ---------
        https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/getkb powershell_tasks::getkb True
        https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/account_audit powershell_tasks::account_audit True
        https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/switch powershell_tasks::switch True
        https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/ps1exec powershell_tasks::ps1exec True
        https://puppet:8143/orchestrator/v1/tasks/powershell_tasks/disablesmbv1 powershell_tasks::disablesmbv1 True
    #>


    Param(
        [Parameter(Mandatory)]
        [string]$token,
        [Parameter(Mandatory)]
        [string]$master,
        [Parameter()]
        [string]$environment='production'
    )
    $uri     = "https://$master`:8143/orchestrator/v1/tasks"
    $headers = @{'X-Authentication' = $Token}
    $body    = @{'environment' = $environment}
    $result  = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -Body $body
    foreach ($item in $result.items) {
        Write-Output $item
    }
}

Function Invoke-PuppetTask {
    <#
    .SYNOPSIS
        Invoke a Puppet task.
    .DESCRIPTION
        Invoke a Puppet task.
    .PARAMETER Task
        The name of the Puppet task to invoke.
    .PARAMETER Environment
        The name of the Puppet task environment.
    .PARAMETER Parameters
        A hash of parameters to supply the Puppet task, e.g. $Parameters = @{tp1 = 'foo';tp2 = 'bar'; tp3 = $true}.
    .PARAMETER Description
        A description to submit along with the task.
    .PARAMETER Nodes
        An array of node names to target, e.g. $Scope = @('DEN3W108R2PSV5','DEN3W108R2PSV4','DEN3W108R2PSV3').
    .PARAMETER Query
        A PuppetDB or PQL query to use to discover nodes. The target is built from the certname values collected at
        the top level of the query, e.g. '["from", "inventory", ["=", "facts.os.name", "windows"]]'.
    .PARAMETER Node_group
        A classifier node group ID. The ID must correspond to a node group that has defined rules. It is not sufficient
        for parent groups of the node group in question to define rules. The user must also have permissions to view the
        node group. Any nodes specified in the scope that the user does not have permissions to run the task on are
        excluded, e.g. 7a692b61-8087-4452-9cf8-58ed2acee2a0.
    .PARAMETER WaitLoopInterval
        An optional time in seconds that the wait feature will re-check the invoked task. DEFAULTS to 5s.
    .PARAMETER Wait
        An optional wait value in seconds that Invoke-PuppetTask will use to wait until
        the invoked task completes. If the wait time is exceeded Invoke-PuppetTask will
        return a warning.
    .PARAMETER Token
        The Puppet API orchestrator token.
    .PARAMETER Master
        The Puppet master.
    .EXAMPLE
        $invokePuppetTaskSplat = @{
            Token = $token
            Master = $master
            Task = 'powershell_tasks::disablesmbv1'
            Environment = 'production'
            Parameters = @{action = 'set'; reboot = $true}
            Description = 'Disable smbv1 on 08r2 nodes.'
            Nodes = @('DEN3W108R2PSV5','DEN3W108R2PSV4','DEN3W108R2PSV3')
        }
        PS> Invoke-PuppetTask @invokePuppetTaskSplat
 
        id name
        -- ----
        https://puppet.contoso.us:8143/orchestrator/v1/jobs/1318 1318
    .EXAMPLE
        $invokePuppetTaskSplat = @{
            Token = $token
            Master = $master
            Task = 'powershell_tasks::disablesmbv1'
            Environment = 'production'
            Parameters = @{action = 'set'; reboot = $true}
            Description = 'Disable smbv1 on 08r2 nodes.'
            Query = '["from", "inventory", ["=", "facts.os.name", "windows"]]'
        }
        PS> Invoke-PuppetTask @invokePuppetTaskSplat
 
        id name
        -- ----
        https://puppet.contoso.us:8143/orchestrator/v1/jobs/1318 1318
    .EXAMPLE
        $invokePuppetTaskSplat = @{
            Token = $token
            Master = $master
            Task = 'powershell_tasks::disablesmbv1'
            Environment = 'production'
            Parameters = @{action = 'set'; reboot = $true}
            Description = 'Disable smbv1 on 08r2 nodes.'
            Node_group = '7a692b61-8087-4452-9cf8-58ed2acee2a0'
        }
        PS> Invoke-PuppetTask @invokePuppetTaskSplat
 
        id name
        -- ----
        https://puppet.contoso.us:8143/orchestrator/v1/jobs/1318 1318
    #>


    Param(
        [Parameter(Mandatory)]
        [string]$Token,
        [Parameter(Mandatory)]
        [string]$Master,
        [Parameter(Mandatory)]
        [string]$Task,
        [Parameter()]
        [string]$Environment = 'production',
        [Parameter()]
        [hashtable]$Parameters = @{},
        [Parameter()]
        [string]$Description = '',
        [Parameter()]
        [int]$Wait,
        [Parameter()]
        [int]$WaitLoopInterval = 5,
        [Parameter(Mandatory, ParameterSetName = "nodes")]
        [string[]]$Nodes,
        [Parameter(Mandatory, ParameterSetName = "query")]
        [string]$Query,
        [Parameter(Mandatory, ParameterSetName = "node_group")]
        [string]$Node_group
    )

    # set the scope type to the name of the oh so cleverly named parameter set
    $scopeType = $PSCmdlet.ParameterSetName
    # set the scope to the value of the single parameter of the choosen parameter set
    switch ($scopeType) {
        'nodes'      {$scope = $Nodes}
        'query'      {$scope = $Query}
        'node_group' {$scope = $Node_group}
    }

    $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-RestMethod -Uri $hoststr -Method Post -Headers $headers -Body $req

    if ($PSBoundParameters.ContainsKey('wait')) {
        # sleep 5s for the job to register
        Start-Sleep -Seconds 5

        $jobSplat = @{
            token  = $Token
            master = $master
            id     = $result.job.name
        }

        # create a timespan
        $timespan = New-TimeSpan -Seconds $Wait
        # start a timer
        $stopwatch = [diagnostics.stopwatch]::StartNew()

        # get the job state every 5 seconds until our timeout is met
        while ($stopwatch.elapsed -lt $timespan) {
            # options are new, ready, running, stopping, stopped, finished, or failed
            $job = Get-PuppetJob @jobSplat
            Write-Verbose $job.node_states
            if (($job.State -eq 'stopped') -or ($job.State -eq 'finished') -or ($job.State -eq 'failed')) {
                Write-Output $job
                break
            }
            Start-Sleep -Seconds $WaitLoopInterval
        }
        if ($stopwatch.elapsed -ge $timespan) {
            Write-Warning "Timeout of $wait`s has exceeded. Job $($job.name) may still be running. Last job status: $($job.State)."
            break
        }
    } else {
        Write-Output $result.job
    }
}