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 simple headless PSDSC script, which compiles and runs a configuration. Follow that link and save a copy as headless_dsc.ps1 and its XML file as input.xml.

Next, copy this Vagrantfile into 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 that I recommend you keep in your code so this setting isn’t confusing later.

Happy automating!

Adam

If this was helpful and you want to save time by getting “copy and paste” patterns for PowerShell and Cloud DevOps in your inbox, subscribe here.