Vagrant is an open-source product that focuses on automation to build consistent repeatable machines. I will be demonstrating how you can use Vagrant PowerShell Desired State Configuration (DSC) to provision a Windows Server 2016 and how this method can be useful to your testing.
Latest posts by Graham Beer (see all)

If you have not heard of or used Vagrant before, then it's well worth checking out some of the excellent blogs here on 4sysops by Adam Bertram. What is so good about Vagrant is that there are many ways to provision a machine, Ansible, Chef, Puppet, and shell scripting to name just a few.

The ability to provision with shell scripts allows us to use all the PowerShell language, including PowerShell DSC. PowerShell DSC is a declarative platform we can use to configure and manage systems.

There really are only two parts to this: the DSC configuration and the Vagrantfile to set up and provision the machine. In this example, I've created a DSC configuration to install PowerShell Core and OpenSSH on a Windows Server 2016. I will also be making use of the Vagrant boxes available from the Vagrant site.

PowerShell DSC configuration ^

Before getting to the Vagrant configuration, I shall build my DSC configuration. This straightforward configuration will hopefully show the potential of this method to provision a machine.

The first part of the configuration is very similar to a PowerShell function:

Configuration PS6TestServer {
    param (
        [ValidateNotNullOrEmpty()]
        [string]$Node,    
        [ValidateNotNullOrEmpty()]
        [string]$MOFfolder
    )
    Import-DscResource -ModuleName PSDesiredStateConfiguration

I'm declaring a copy of parameters: one for the name of the machine and one for the destination of the Managed Object Format (MOF) file. These values will come from the Vagrant configuration file—more on that in a bit.

Node $Node {
        
        File DSCLogFolder {
            Type            = 'Directory'
            DestinationPath = 'C:\Tmp\DSClogs'
            Ensure          = "Present"
        }  
        Package PSCore {    
            Ensure    = "Present"
            Name      = "PowerShell 6-x64"
            Path      = "$Env:SystemDrive\tmp\SourceFiles\PowerShell-6.1.0-					preview.1-win-x64.msi"
            ProductId = "A16A59B1-5DB1-44FC-BD40-E684B43B3A8E"
            LogPath   = "C:\Tmp\DSClogs\PS6.log"
        }
        Log AfterPSCoreInstall {
            Message   = "Finished Installing PowerShell Core 6 resource with ID 					PSCore"
            DependsOn = "[Package]PSCore"
        }

The first part is to create a log file directory for the install of PowerShell Core; the next is the install of PowerShell Core. I've downloaded a copy of the MSI install, which I will copy later to the machine through the Vagrantfile.

        Script InstallOpenSSH {
            GetScript  = {
                $folderSize = Get-ChildItem -Path 'C:\Program Files\OpenSSH\OpenSSH-Win64' | 
                    Measure-Object -property Length -sum | 
                    Select-Object Sum     
                    
                # return true or false
                return @{ Result = ($folderSize.Sum -eq '7390211') }                   
            }
            SetScript  = {
                param(
                    [string] $From = "C:\Tmp\SourceFiles\OpenSSH-Win64.zip",
                    [string] $To = "C:\Program Files\OpenSSH\",
                    [string] $Installer = "C:\Program Files\OpenSSH\OpenSSH-Win64\install-sshd.ps1"
                )
                if (Test-Path $From) {
                    # Load assembly name to perform unzip    
                    Add-Type -AssemblyName System.IO.Compression.FileSystem
                    # Unzip file to directory
                    [System.IO.Compression.ZipFile]::ExtractToDirectory($From, $To)
                    # Run install
                    & $Installer
                }
            }
            TestScript = {
                # The Test function needs to return True if the system is already in the desired state
                $testpath = Test-Path "$Env:SystemDrive\Program Files\OpenSSH\OpenSSH-Win64" 
            
                if ($testpath) {
                    $count = (Get-ChildItem -Path "$Env:SystemDrive\Program Files\OpenSSH\OpenSSH-Win64").count
                    if ($count -eq 18) { 
                        return $true 
                    }
                } 
                else { 
                    return $false 
                }
            }
        }
        Log AfterInstallOpenSSH {
            Message   = "Finished Installing Open SSH resource with ID InstallOpenSSH"
            DependsOn = "[Script]InstallOpenSSH"
        }
    }
}

The second part is using the PowerShell DSC script resource to install OpenSSH. We need to define three scriptblocks: GetScript, SetScript, and TestScript.

  • The GetScript scriptblock should return a hash table representing the state of the current node.
  • The TestScript scriptblock should determine if the current node requires modification.
  • The SetScript scriptblock should modify the node. The DSC calls it if the TestScript block return $false.

To read more about the script resource, please refer to the Microsoft documentation.

The main scriptblock SetScript unzips the file and runs the ps1 installer for OpenSSH.

The Vagrantfile ^

This is where everything comes together. The file will set the Vagrant box we will use, configurations to perform, and call and invoke our DSC code.

Let's look at the Vagrantfile first, then explain what is happening:

# -*- mode: ruby -*-
# vi: set ft=ruby :
  # Set variables  
  Node = "DSCPSCore6"
  MOFfolder = "C:\\tmp\\MOF"
  CopySource = "D:\\VagrantDemo\\DSC\\SourceFiles"
  CopyDestination = "C:\\tmp\\SourceFiles"
Vagrant.configure("2") do |config|
  config.vm.box = "gusztavvargadr/w16s"
  config.vm.communicator = "winrm"
  
  # Set vagrant machine name
  config.vm.hostname = Node
  # Configure network
  config.vm.network "forwarded_port", host: 33389, guest: 3389
  config.vm.network "forwarded_port", host: 8080, guest: 80
  config.vm.network "forwarded_port", host: 4443, guest: 443
 
  # Perform file copy from Local machine to Vagrant box
  config.vm.provision "file", 
    source: CopySource,
    destination: CopyDestination
  
  # Create MOF   
  config.vm.provision "shell", 
    path: 'D:\VagrantDemo\DSC\Config\PS6TestServer.ps1',
    args: [Node, MOFfolder]
  
  # Invoke MOF file  
  config.vm.provision "shell",
    inline: "Start-DSCConfiguration -Path $Args[0] -Force -Wait -Verbose", 
    args: [MOFfolder]
  end

As you can see, for all that's going on, there isn't much to this Vagrantfile. The Vagrantfile itself is written in Ruby. You don't need to know Ruby inside and out to set up a Vagrant build, so don't let it put you off!

I've commented the above code to help. The first part sets variables for the rest of the code. This simply makes it easier to read and change it for future builds. The file has a few quirks, like having to use double backslashes (\\) for folder separation. The next two lines state which Vagrant box we are going to use (In this case, I've used a Windows 2016 Server from the Vagrant boxes site) and how to communicate with the box.

I'm using the Node variable I set at the start to name the server, followed by setting up forwarding ports for the network. The provisioning steps come next. The first one File copies files or directories from the source machine to our target Vagrant box. I am copying the MSI install for PowerShell Core and the ZIP file to install OpenSSH from my machine. These files need to be in place for our DSC configuration to work.

The second part is running the PowerShell DSC configuration file from the local machine. At the bottom of my DSC file, I added the following:

# For Vagrant output
Write-Host "MySite DSC Config :: Node=$($args[0]), MOFfolder=$($args[1])"
# Call the configuration to generate MOF file
PS6TestServer -Node $args[0] -OutputPath $args[1]

The args part of the Vagrantfile will pass in our two variables: the name of the server (Vagrant box) and the file location for the MOF our DSC configuration created. The DSC file will run locally on our machine and generate the MOF file on the server. I've used PowerShell's built-in variable $args to pass the Node and MOFfolder variables from the Vagrantfile. Write-Host displays the variables passed. The square brackets in Ruby denote an array.

The last step is running Start-DscConfiguration on the Vagrant box to apply our DSC configuration, using the MOFfolder variable to locate the newly generated MOF file.

Performing the build ^

Now let's put it all together. I used this folder structure:

Folder structure

Folder structure

Don't worry about the .vagrant folder; the Vagrant box you choose and typing vagrant up creates this. I created the DSC folder with Config and SourceFiles subfolders. The Config folder is for my DSC code, and SourceFiles is for my installers.

The next part is to bring up the Vagrant box. From your main folder containing your Vagrantfile, type vagrant up. If all goes well, you will see the setup run through. Here is some of the output from my build:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'gusztavvargadr/w16s' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 3389 (guest) => 33389 (host) (adapter 1)
    default: 80 (guest) => 8080 (host) (adapter 1)
    default: 443 (guest) => 4443 (host) (adapter 1)
    default: 5985 (guest) => 55985 (host) (adapter 1)
    default: 5986 (guest) => 55986 (host) (adapter 1)
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: WinRM address: 127.0.0.1:55985
    default: WinRM username: vagrant
    default: WinRM execution_time_limit: PT2H
    default: WinRM transport: negotiate
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Setting hostname...
==> default: Mounting shared folders...
    default: /vagrant => D:/VagrantDemo
==> default: Running provisioner: file...
==> default: Running provisioner: shell...
    default: Running: D:\VagrantDemo\DSC\Config\PS6TestServer.ps1 as c:\tmp\vagrant-shell.ps1
    default: MySite DSC Config :: Node=DSCPSCore6, MOFfolder=C:\tmp\MOF
    default:     Directory: C:\tmp\MOF
    default: Mode                LastWriteTime         Length Name
    default: ----                -------------         ------ ----
    default: -a----         4/7/2018   2:57 PM           8630 DSCPSCore6.mof
==> default: Running provisioner: shell...
    default: Running: inline PowerShell script
    default: VERBOSE: Perform operation 'Invoke CimMethod' with following parameters, ''methodName' =
    default: SendConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' =
    default: root/Microsoft/Windows/DesiredStateConfiguration'.
    default: VERBOSE: An LCM method call arrived from computer DSCPSCORE6 with user sid
    default: S-1-5-21-333254102-1508906537-909750630-1000.
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ Start  Set      ]
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ Start  Resource ]  [[File]DSCLogFolder]
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ Start  Test     ]  [[File]DSCLogFolder]   
     ………………………….
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ Start  Set      ]  [[Log]AfterInstallOpenSSH]
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ End    Set      ]  [[Log]AfterInstallOpenSSH]  in 0.0000 seconds.
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ End    Resource ]  [[Log]AfterInstallOpenSSH]
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ End    Set      ]
    default: VERBOSE: [DSCPSCORE6]: LCM:  [ End    Set      ]    in  0.4790 seconds.
    default: VERBOSE: Operation 'Invoke CimMethod' complete.
    default: VERBOSE: Time taken for configuration job to complete is 0.606 seconds

It is certainly satisfying to see all the stages complete successfully! By running the vagrant PowerShell command, I can test whether our DSC configuration created the install folders:

Testing the build

Testing the build

Now you can type vagrant rdp or vagrant powershell to access your newly provisioned box.

Should you wish to try this example out for yourself, I've added the code to my GitHub.

Subscribe to 4sysops newsletter!

As you can see, Vagrant is very useful tool for testing. Coupled with PowerShell DSC, you can create your own custom build, and then create and remove it as many times as you like and still have the same results when provisioning your vagrant box.

avataravatar
1 Comment
  1. Shahid Siddique 2 years ago

    Thanks, this helped. I was more specifically looking to copy ps1 files on windows VM and run it there on guest node. My host is Mac.

Leave a reply

Your email address will not be published.

*

© 4sysops 2006 - 2022

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account