PowerShell DSC: Stop On Errors

Hello!

I like my code to stop on errors, not continue past them. For example, if a script fails to enable logs for my app I want to know right away. I don’t want to find out tomorrow when I need logs that aren’t there. The earlier I know about errors, the better. I learned this as the Fail Early Fail Often pattern.

PowerShell has a dedicated type of error that’s non-terminating. Its default is to continue past them. That applies in DSC configurations, too.

We can see this with a simple DSC configuration and the Write-Error command in a Script resource:

Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Node 'localhost' {
        Log Before {
            Message = 'Before error.'
        }
        Script NonTerminatingError {
            GetScript = {@{Result = ''}}
            SetScript = {Write-Error 'Non-terminating error.'}
            TestScript = {Return $false}
        }
        Log After {
            Message = 'After error.'
        }
    }
}

Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path '.\Headless\'

The middle resource generates an error but the last resource still runs:

...
VERBOSE: [VAGRANT]:                            [[Log]Before] Before error.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Before]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Before]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Script]NonTerminatingError]  in 0.0780 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]:                            [[Script]NonTerminatingError] Performing the 
operation "Set-TargetResource" on target "Executing the SetScript with the user supplied 
credential".
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Script]NonTerminatingError]  in 0.0470 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]After]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]After]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]After]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]After]
VERBOSE: [VAGRANT]:                            [[Log]After] After error.
...

We can tell DSC to stop on non-terminating errors by passing the ErrorAction flag when we start our configuration:

Start-DscConfiguration -Wait -Force -Verbose -ErrorAction 'Stop' -Path '.\Headless\'

Now it stops on the Write-Error:

VERBOSE: [VAGRANT]:                            [[Log]Before] Before error.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Before]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Before]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Script]NonTerminatingError]  in 0.0780 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]:                            [[Script]NonTerminatingError] Performing the 
operation "Set-TargetResource" on target "Executing the SetScript with the user supplied 
credential".

Stderr from the command:

powershell.exe : Non-terminating error.

There’s also an $ErrorActionPreference variable, but that didn’t work no matter where I set it. The ErrorAction flag seems to be the way.

This isn’t necessarily a best practice, the PowerShell default is to continue past errors, but I usually have a better life if I switch it to stop.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC: Self Signed SSL Certs

Hello!

First, this isn’t a best practices guide for SSL certificates, it’s a how-to for creating functional ones. As always, only use self-signed certs when you’ve specifically validated that they’re a sufficiently secure solution.

When I do need self-signed certs and I’m working in Windows, I generate them with PowerShell DSC and its Script Resource. It works great for my cases. There are more robust ways that may also be worth looking at, like Custom Resources.

I always need certs with no password to make it easy to start apps unattended, so that’s what these instructions create.

This assumes you’ve already installed OpenSSL. I use Chocolatey.

Pre-requisites out of the way. Now, the code:

Script SelfSignedCert {
    GetScript = {@{Result = ''}}
    SetScript = {
        New-Item -ItemType Directory -Force -Path 'C:\Tmp\Ssl\'

        # Generate PFX with a temporary password
        $Cert = New-SelfSignedCertificate `
            -CertStoreLocation 'cert:\localmachine\my' `
            -DnsName 'localhost'
        $Password = ConvertTo-SecureString `
            -String 'temppass' `
            -Force `
            -AsPlainText
        Export-PfxCertificate `
            -Cert "cert:\localmachine\my\$($Cert.Thumbprint)" `
            -FilePath 'C:\Tmp\Ssl\Cert.pfx' `
            -Password $Password

        # Convert PFX to Key/PEM with no password
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe pkcs12 `
            -in 'C:\Tmp\Ssl\Cert.pfx' `
            -nocerts `
            -nodes `
            -out 'C:\Tmp\Ssl\Pkcs12.pem' `
            -passin 'pass:temppass'
        # 'openssl.exe rsa' sends 'writing RSA key' to the error stream
        # on success. We have to redirect that output or the Script
        # resource errors.
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe rsa `
            -in 'C:\Tmp\Ssl\Pkcs12.pem' `
            -out 'C:\Tmp\Ssl\Rsa.key' `
            2> Out-Null
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe pkcs12 `
            -clcerts `
            -in 'C:\Tmp\Ssl\Cert.pfx' `
            -nokeys `
            -out 'C:\Tmp\Ssl\Cert.pem' `
            -passin 'pass:temppass'

        # Clean up leftovers from the conversion
        Remove-Item 'C:\Tmp\Ssl\Cert.pfx'
        Remove-Item 'C:\Tmp\Ssl\Pkcs12.pem'
    }
    TestScript = {Test-Path 'C:\Tmp\Ssl'}
}

Line 32 (highlighted) is the tricky one. The script resource fails when its code outputs to the error stream. OpenSSL’s rsa command sends the string "writing RSA key" to the error stream when it succeeds. So we get failures like these:

Stderr from the command:

powershell.exe : writing RSA key
    + CategoryInfo          : NotSpecified: (writing RSA key:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
    + CategoryInfo          : NotSpecified: (writing RSA key:) [], CimException
    + FullyQualifiedErrorId : NativeCommandError
    + PSComputerName        : localhost

I bet the reason is this: OpenSSL was designed for Linux. In Linux, it’s common to send informational output to stderr. That keeps it out of stdout and therefor keeps it from passing to other apps via pipes (|). In PowerShell, there are many streams, including a dedicated one for informative output (the “verbose” stream). That makes it an anti-pattern to send informative output to PowerShell’s error stream; you should use the verbose stream. So it makes sense for DSC to assume that anything on the error stream is a real error, unlike in Linux. The person who ported OpenSSL didn’t account for this.

The only workaround I could find was to redirect the error stream to Out-Null. OpenSSL didn’t recognize the ErrorAction flag.

I couldn’t reproduce this behavior in raw PowerShell, only in the DSC script resource. If you know the details on why that is, I’d love to hear from you.

Hope this helps! Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC EXE ProductID And Name

Hello!

PowerShell DSC can install EXEs with its Package resource:

Package MyPackage {
    Name = [string]
    Path = [string]
    ProductId = [string]
}

Path is easy, it’s just the path to the installer. But what goes in Name or ProductID?

Let’s use WinRAR as an example. You shouldn’t actually install this with DSC, you should use Chocolatey, but I needed something to demo.

There are lots of instructions for finding the product ID for MSIs, like Sam Cogan’s, but they don’t work for EXEs. After reading tons of sources that weren’t solid enough to link, I figured out that EXEs don’t have a product ID. Just the empty string: ''.

The name was trickier. For WinRAR it’s WinRAR, right? Nope. DSC throws errors:

powershell.exe : PowerShell DSC resource MSFT_PackageResource  failed to execute Set-TargetResource functionality 
    + CategoryInfo          : NotSpecified: (PowerShell DSC ... functionality :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
with error message: Package from C:\winrar-x64-561.exe was installed, but the specified ProductId 
and/or Name does not match package details 
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : ProviderOperationExecutionFailure
    + PSComputerName        : localhost

Turns out you can find the name DSC needs by doing a temp install of the app by hand (I used a Vagrant box) and then checking its entry in “Add or remove programs” in the system settings:

WinRARName

The Name is the full WinRAR 5.61 (64-bit), including the spaces and special characters.

Here’s the full DSC resource:

Package WinRAR {
    Name = 'WinRAR 5.61 (64-bit)'
    Path = "$Env:SystemDrive\winrar-x64-561.exe"
    ProductId = ''
    Arguments = '/S'
}

(The /S stops the installer from asking for user input)

That’s it! Use empty string for the product ID and look up the name in “Add or remove programs” and you’re good to go.

Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC In Vagrant

I work mostly on Apple Mac OS X. I’ve also been writing a lot of Windows automation, and that means PowerShell DSC. PSDSC doesn’t work on OS X yet, and even once it does I won’t be able to test Windows-only resources. To test my configurations, I use Vagrant boxes. It took a little fiddling to get this set up, so I’m posting the code here in case you’re in the same situation.

This assumes you already have Vagrant working. I use the VirtualBox provider and I install everything with homebrew.

Today, Vagrant doesn’t have a native PowerShell DSC provisioner. I use the shell provisioner to run my Headless PSDSC script, which compiles and runs a configuration:

Param(
    [Parameter(Mandatory=$false)]
    [String[]]
    $InputXmlFile
)
 
if ($InputXmlFile) {
    [XML]$Inputs = Get-Content -Path $InputXmlFile
}
 
$NodeData = @{
    NodeName = "localhost"
    Message = "This was hardcoded in the node data."
    Inputs = $Inputs.inputs
}
$ConfigurationData = @{AllNodes = @($NodeData)}
 
Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
 
    Node 'localhost' {
        Log Message {
            Message = $AllNodes.Message
        }
        if ($AllNodes.Inputs) {
            Log Input {
                Message = $AllNodes.Inputs.Message
            }
        }
    }
}
 
Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path .\Headless\

And here’s an input file to provide the example messages:

<Inputs>
    <Message>This message came from the XML inputs.</Message>
</Inputs>

Now all you need is a Vagrantfile in the same directory:

Vagrant.configure("2") do |config|
  config.vm.box = "StefanScherer/windows_2019"
  config.vm.provision "file" do |file|
    file.source = "input.xml"
    file.destination = "input.xml"
  end
  config.vm.provision "shell" do |shell|
    shell.path = "headless_dsc.ps1"
    shell.args = ["-InputXmlFile", "input.xml"]

    # https://github.com/hashicorp/vagrant/issues/9138#issuecomment-444408251
    shell.privileged = false
  end
end

To create the box and run PSDSC:

vagrant up

If you’ve made changes and you want to recomplie and rerun the configuration:

vagrant provision

Notes:

  • If you need to connect to the instance and run commands, check out vagrant rdp. You’ll need to install Remote Desktop from the App Store. There’s a funky bug: if Remote Desktop isn’t open, the first rdp command will open the app but won’t start a session. Just run it again and you’ll get a session window.
  • The provisioner uploads the script and XML file on every run even though the current directory is mounted as a synced folder on the box by default. I could have used an inline shell script to call the script directly via that sync, but sometimes I disable the sync for testing and it’s convenient for the provisioner to keep working.
  • Without shell.privileged = false, I got errors like this:
        default: (10,8):UserId:
        default: At line:72 char:1
        default: + $folder.RegisterTaskDefinition($task_name, $task, 6, $username, $pass ...
        default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        default:     + CategoryInfo          : OperationStopped: (:) [], ArgumentException
        default:     + FullyQualifiedErrorId : System.ArgumentException
        default: The system cannot find the file specified. (Exception from HRESULT: 0x80070002)
        default: At line:74 char:1
        default: + $registered_task = $folder.GetTask("\$task_name")
        default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        default:     + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
        default:     + FullyQualifiedErrorId : System.IO.FileNotFoundException
        default: You cannot call a method on a null-valued expression.
        default: At line:75 char:1
        default: + $registered_task.Run($null) | Out-Null
        default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        default:     + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        default:     + FullyQualifiedErrorId : InvokeMethodOnNull
    

    The whole process froze and I had to ctrl+c twice to close it. There’s a bug. I left a link in a comment in the Vagrantfile that I recommend you keep in your code so this setting isn’t confusing later.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

Which PowerShell DSC Resources Module To Import

Hello!

These three modules all implement various PowerShell DSC resources:

That gives us three top-level Import-DSCResource lines we could use in configurations (check out this article for an in-context example):

Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xPSDesiredStateConfiguration
Import-DscResource -ModuleName PSDscResources

Here are the differences:

Here are my standard practices for which to import:

  1. Start with PSDesiredStateConfiguration.
  2. Upgrade to PSDscResources if you need a feature the built-in module doesn’t have.
  3. Test with xPSDesiredStateConfiguration if things still aren’t working. Avoid using this module in production.

Arguably, it’s a good practice to upgrade to PSDscResources by default. I don’t do that because using the built-in module reduces the number of things I have to install and I like short dependency chains. So far, my use-cases have been simple and the built-in module has been sufficient. Your situation may be different.

Happy configuring!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

Headless PowerShell DSC Script

Hello!

In clouds, “headless” deployment means instances provision themselves when they start. There’s no external server infrastructure orchestrating their config, everything they need to do they do on their own. This is the most common deployment pattern I’ve seen in DevOps.

It took me some fiddling to get this pattern set up in PowerShell DSC for my situation, so I’m posting the code in case you’re running in to the same thing. It’s a single script that compiles and runs your configuration, optionally reading inputs from an XML file.

First, the code:

Param(
    [Parameter(Mandatory=$false)]
    [String[]]
    $InputXmlFile
)

if ($InputXmlFile) {
    [XML]$Inputs = Get-Content -Path $InputXmlFile
}

$NodeData = @{
    NodeName = "localhost"
    Message = "This was hardcoded in the node data."
    Inputs = $Inputs.inputs
}
$ConfigurationData = @{AllNodes = @($NodeData)}

Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration

    Node 'localhost' {
        Log Message {
            Message = $AllNodes.Message
        }
        if ($AllNodes.Inputs) {
            Log Input {
                Message = $AllNodes.Inputs.Message
            }
        }
    }
}

Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path .\Headless\

A sample XML file:

<Inputs>
    <Message>This message came from the XML inputs.</Message>
</Inputs>

And the script output on a Windows Server 2019 Vagrant box:

PS C:\vagrant> .\headless_dsc.ps1 -InputXmlFile .\input.xml

    Directory: C:\vagrant\Headless

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------       11/11/2019   8:21 AM           2488 localhost.mof
VERBOSE: Perform operation 'Invoke CimMethod' with following parameters, ''methodName' =
SendConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' =
root/Microsoft/Windows/DesiredStateConfiguration'.
VERBOSE: An LCM method call arrived from computer VAGRANT with user sid S-1-5-21-1529561135-2561037041-51718299-1000.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]Message]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]Message]
VERBOSE: [VAGRANT]:                            [[Log]Message] This was hardcoded in the node data.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Message]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]Input]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]Input]
VERBOSE: [VAGRANT]:                            [[Log]Input] This message came from the XML inputs.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Input]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]    in  0.0940 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 0.372 seconds

Some notes:

  • There are several modules that implement DSC resources. I start with PSDesiredStateConfiguration and then upgrade if I need to. Check out this article if you’re getting errors defining resources or their properties.
  • This uses Log resources to demonstrate functionality and to show you how to access the ConfigurationData. You should replace those in your config.
  • You don’t have to use ConfigurationData to pass the inputs. You’ll see other patterns using regular function params, for example. I used ConfigurationData because it’s DSC’s built-in method and I use the native approach unless I have a specific reason to use something else.
  • I used XML for the inputs because PowerShell supports XML natively. It also supports JSON, but JSON makes you escape backslashes \ and since those are common path characters in Windows it makes for ugly input strings. Plus XML supports comments and JSON doesn’t and in DevOps you very often need to add comments to your input choices.
  • This is written to run directly as a PowerShell script because that’s what I needed at the time. If you’re using Azure Resource Manager Template, check out its native integration.
  • This assumes you always want verbose output. DSC is a quiet tool by default and I prefer to make it verbose. You may want to parameterize or consider using CmdletBinding.
  • This assumes you’ll only ever define one node: localhost. If you adjust that you’ll need to update the node references.

Happy configuring!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles: