Src/Private/Private.ps1

function Add-DiskImageHotfix {
<#
    .SYMOPSIS
        Adds a Windows update/hotfix package to an image.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Id,

        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk

        ## Disk image partition scheme
        [Parameter(Mandatory)]
        [ValidateSet('MBR','GPT')]
        [System.String] $PartitionStyle,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        if ($PartitionStyle -eq 'MBR') {

            $partitionType = 'IFS';
        }
        elseif ($PartitionStyle -eq 'GPT') {

            $partitionType = 'Basic';
        }
        $vhdDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType $partitionType;

        $resolveLabMediaParams = @{
            Id = $Id;
        }
        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $resolveLabMediaParams['ConfigurationData'] = $ConfigurationData;
        }
        $media = Resolve-LabMedia @resolveLabMediaParams;

        foreach ($hotfix in $media.Hotfixes) {

            if ($hotfix.Id -and $hotfix.Uri) {

                $invokeLabMediaDownloadParams = @{
                    Id  = $hotfix.Id;
                    Uri = $hotfix.Uri;
                }

                if ($null -ne $hotfix.Checksum) {

                    $invokeLabMediaDownloadParams['Checksum'] = $hotfix.Checksum;
                }

                $hotfixFileInfo = Invoke-LabMediaDownload @invokeLabMediaDownloadParams;
                $packageName = [System.IO.Path]::GetFileNameWithoutExtension($hotfixFileInfo.FullName);

                Add-DiskImagePackage -Name $packageName -Path $hotfixFileInfo.FullName -DestinationPath $vhdDriveLetter;
            }
        }

    } #end process
} #end function

function Add-DiskImagePackage {
<#
    .SYNOPSIS
        Adds a Windows package (.cab) to an image. This is implmented primarily to support injection of
        packages into Nano server images.
    .NOTES
        The real difference between a hotfix and package is that a package can either be specified in the
        master VHD(X) image creation OR be injected into VHD(X) differencing disk.
#>

    [CmdletBinding()]
    param (
        ## Package name (used for logging)
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## File path to the package (.cab) file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Path,

        ## Destination operating system path (mounted VHD), i.e. G:\
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $DestinationPath
    )
    begin {

        ## We just want the drive letter
        if ($DestinationPath.Length -gt 1) {

            $DestinationPath = $DestinationPath.Substring(0,1);
        }

    }
    process {

        $logPath = '{0}:\Windows\Logs\{1}' -f $DestinationPath, $labDefaults.ModuleName;
        [ref] $null = New-Directory -Path $logPath -Verbose:$false;

        Write-Verbose -Message ($localized.AddingImagePackage -f $Name, $DestinationPath);
        $addWindowsPackageParams = @{
            PackagePath = $Path;
            Path = '{0}:\' -f $DestinationPath;
            LogPath = '{0}\{1}.log' -f $logPath, $Name;
            LogLevel = 'Errors';
        }
        [ref] $null = Microsoft.Dism.Powershell\Add-WindowsPackage @addWindowsPackageParams -Verbose:$false;

    } #end process
} #end function

function Add-LabImageWindowsOptionalFeature {
<#
    .SYMOPSIS
        Enables Windows optional features to an image.
#>

    [CmdletBinding()]
    param (
        ## Source package file path
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,

        ## Mounted VHD(X) Operating System disk drive
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        ## Windows packages to add to the image after expansion
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String[]] $WindowsOptionalFeature,

        ## DISM log path
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $LogPath = $DestinationPath
    )
    process {

        Write-Verbose -Message ($localized.AddingWindowsFeature -f ($WindowsOptionalFeature -join ','), $DestinationPath);
        $enableWindowsOptionalFeatureParams = @{
            Source = $ImagePath;
            Path = $DestinationPath;
            LogPath = $LogPath;
            FeatureName = $WindowsOptionalFeature;
            LimitAccess = $true;
            All = $true;
            Verbose = $false;
        }
        $dismOutput = Microsoft.Dism.Powershell\Enable-WindowsOptionalFeature @enableWindowsOptionalFeatureParams;
        Write-Debug -Message $dismOutput;

    } #end process
} #end function Add-LabImageWindowsOptionalFeature

function Add-LabImageWindowsPackage {
<#
    .SYNOPSIS
        Adds a Windows package to an image.
#>

    [CmdletBinding()]
    param (
        ## Windows packages (.cab) files to add to the image after expansion
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String[]] $Package,

        ## Path to the .cab files
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $PackagePath,

        ## Mounted VHD(X) Operating System disk drive
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        ## Package localization directory/extension (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $PackageLocale = 'en-US',

        ## DISM log path
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $LogPath = $DestinationPath
    )
    process {

        foreach ($packageName in $Package) {

            Write-Verbose -Message ($localized.AddingWindowsPackage -f $packagename, $DestinationPath);
            $packageFilename = '{0}.cab' -f $packageName;
            $packageFilePath = Join-Path -Path $PackagePath -ChildPath $packageFilename;
            Add-DiskImagePackage -Name $packageName -Path $packageFilePath -DestinationPath $DestinationPath;

            ## Check for language-specific package (Change from Server 2016 TP releases and Server 2016 Nano RTM)
            if ($PSBoundParameters.ContainsKey('PackageLocale')) {

                $localizedPackageName = '{0}_{1}' -f $packageName, $packageLocale;
                $localizedPackageFilename = '{0}.cab' -f $localizedPackageName;
                $localizedPackageDirectoryPath = Join-Path -Path $PackagePath -ChildPath $PackageLocale;
                $localizedPackagePath = Join-Path -Path $localizedPackageDirectoryPath -ChildPath $localizedPackageFilename;
                if (Test-Path -Path $localizedPackagePath -PathType Leaf) {

                    Write-Verbose -Message ($localized.AddingLocalizedWindowsPackage -f $localizedPackageName, $DestinationPath);
                    $addDiskImagePackageParams = @{
                        Name = $localizedPackageName;
                        Path = $localizedPackagePath;
                        DestinationPath = $DestinationPath;
                    }
                    Add-DiskImagePackage @addDiskImagePackageParams;
                }
            }

        } #end foreach package

    } #end process
} #end function Add-LabImageWindowsPackage

function Assert-BitLockerFDV {
<#
    .SYNOPSIS
        Enables BitLocker full disk write protection (if enabled on the host system)
#>

    [CmdletBinding()]
    param ( )
    process {

        if ($fdvDenyWriteAccess) {

            Write-Verbose -Message $localized.EnablingBitlockerWriteProtection
            Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE' -Name 'FDVDenyWriteAccess' -Value 1
        }

    } #end process
} #end function

function Assert-LabConfigurationMof {
<#
    .SYNOPSIS
        Checks for node MOF and meta MOF configuration files.
#>

    [CmdletBinding()]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab vm/node name
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Path to .MOF files created from the DSC configuration
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path = (Get-LabHostDscConfigurationPath),

        ## Ignores missing MOF file
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $SkipMofCheck
    )
    process {

        $Path = Resolve-Path -Path $Path -ErrorAction Stop;
        $node = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -eq $Name };

        $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $node.NodeName);
        Write-Verbose -Message ($localized.CheckingForNodeFile -f $mofPath);
        if (-not (Test-Path -Path $mofPath -PathType Leaf)) {

            if ($SkipMofCheck) {

                Write-Warning -Message ($localized.CannotLocateMofFileError -f $mofPath)
            }
            else {

                throw ($localized.CannotLocateMofFileError -f $mofPath);
            }
        }

        $metaMofPath = Join-Path -Path $Path -ChildPath ('{0}.meta.mof' -f $node.NodeName);
        Write-Verbose -Message ($localized.CheckingForNodeFile -f $metaMofPath);
        if (-not (Test-Path -Path $metaMofPath -PathType Leaf)) {

            Write-Warning -Message ($localized.CannotLocateLCMFileWarning -f $metaMofPath);
        }

    } #end process
} #end function Assert-LabConfigurationMof

function Assert-TimeZone {
<#
    .SYNOPSIS
        Validates a timezone string.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $TimeZone
    )
    process {

        try {

            $TZ = [TimeZoneInfo]::FindSystemTimeZoneById($TimeZone)
            return $TZ.Id;
        }
        catch [System.TimeZoneNotFoundException] {

            throw $_;
        }

    } #end process
} #end function

function Assert-VirtualMachineHardDiskDriveParameter {
<#
    .SYNOPSIS
        Ensures parameters specified in the Lability_HardDiskDrive hashtable are correct.
#>

    [CmdletBinding()]
    param (
        ## Virtual hard disk generation
        [Parameter()]
        [System.String] $Generation,

        ## Vhd size. Minimum 3MB and maximum 2,040GB
        [Parameter()]
        [Alias('Size')]
        [System.UInt64] $MaximumSizeBytes,

        [Parameter()]
        [System.String] $VhdPath,

        ## Virtual hard disk type
        [Parameter()]
        [ValidateSet('Dynamic','Fixed')]
        [System.String] $Type,

        [Parameter()]
        [System.UInt32] $VMGeneration
    )
    process {

        if ($PSBoundParameters.ContainsKey('VhdPath')) {

            if (($PSBoundParameters.Keys -contains 'Generation') -or
                ($PSBoundParameters.Keys -contains 'MaximumSizeBytes')) {

                throw ($localized.CannotResoleVhdParameterError);
            }

            if (-not (Test-Path -Path $VhdPath -PathType Leaf)) {

                throw ($localized.CannotLocateVhdError -f $VhdPath);
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Generation')) {

            ## A Generation 2 virtual machine can only utilize VHDX.
            if (($VMGeneration -eq 2) -and ($Generation -eq 'VHD')) {

                throw ($localized.InvalidVhdTypeError -f 'VHD', 2);
            }

            if (($MaximumSizeBytes -lt 3145728) -or ($MaximumSizeBytes -gt 2190433320960)) {

                throw ($localized.InvalidVhdSizeError -f $MaximumSizeBytes);
            }
        }
        else {

            ## Nothing has been specified
            throw ($localized.CannotProcessCommandError -f '"Generation, MaximumSizeBytes, VhdPath');
        }

    } #end process
} #end function

function Clear-LabVirtualMachine {
<#
    .SYNOPSIS
        Removes the current configuration a virtual machine.
    .DESCRIPTION
        Invokes/sets a virtual machine configuration using the xVMHyperV DSC resource.
    .NOTES
        Should be Remove-LabVirtualMachine but that and Remove-LabVM are already used.
#>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $SwitchName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Media,

        [Parameter(Mandatory)]
        [System.UInt64] $StartupMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MinimumMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MaximumMemory,

        [Parameter(Mandatory)]
        [System.Int32] $ProcessorCount,

        [Parameter()]
        [AllowNull()]
        [System.String[]] $MACAddress,

        [Parameter()]
        [System.Boolean] $SecureBoot,

        [Parameter()]
        [System.Boolean] $GuestIntegrationServices,

        [Parameter()]
        [System.Boolean] $AutomaticCheckpoints,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        if ($PSCmdlet.ShouldProcess($Name)) {

            ## Resolve the xVMHyperV resource parameters
            $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters;
            $vmHyperVParams['Ensure'] = 'Absent';
            Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM;
            Invoke-LabDscResource -ResourceName VM -Parameters $vmHyperVParams -ErrorAction SilentlyContinue;
        }

    } #end process
} #end function

function Close-GitHubZipArchive {
<#
    .SYNOPSIS
        Tidies up and closes Zip Archive and file handles
#>

    [CmdletBinding()]
    param ()
    process {

        Write-Verbose -Message ($localized.ClosingZipArchive -f $Path);

        if ($null -ne $zipArchive) {

            $zipArchive.Dispose();
        }

        if ($null -ne $fileStream) {

            $fileStream.Close();
        }

    } # end process
} #end function

function Close-ZipArchive {
<#
    .SYNOPSIS
        Tidies up and closes Zip Archive and file handles
#>

    [CmdletBinding()]
    param ()
    process {

        Write-Verbose -Message ($localized.ClosingZipArchive -f $Path);

        if ($null -ne $zipArchive) {

            $zipArchive.Dispose();
        }

        if ($null -ne $fileStream) {

            $fileStream.Close();
        }

    } # end process
} #end function

function Convert-PSObjectToHashtable {
<#
    .SYNOPSIS
        Converts a PSCustomObject's properties to a hashtable.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Object to convert to a hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSObject[]] $InputObject,

        ## Do not add empty/null values to the generated hashtable
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $IgnoreNullValues
    )
    process {

        foreach ($object in $InputObject) {

            $hashtable = @{ }
            foreach ($property in $object.PSObject.Properties) {


                if ($IgnoreNullValues -and ($property.TypeNameOfValue -ne 'System.Object[]')) {
                    if ([System.String]::IsNullOrEmpty($property.Value)) {
                        ## Ignore empty strings
                        continue;
                    }
                }

                if ($property.TypeNameOfValue -eq 'System.Management.Automation.PSCustomObject') {

                    ## Convert nested custom objects to hashtables
                    $hashtable[$property.Name] = Convert-PSObjectToHashtable -InputObject $property.Value -IgnoreNullValues:$IgnoreNullValues;
                }
                elseif ($property.TypeNameOfValue -eq 'System.Object[]') {

                    ## Convert nested arrays of objects to an array of hashtables (#262)
                    $nestedCollection = @();
                    foreach ($object in $property.Value) {

                        if ($object -is 'System.Management.Automation.PSCustomObject') {

                            $nestedCollection += Convert-PSObjectToHashtable -InputObject $object -IgnoreNullValues:$IgnoreNullValues;
                        }
                        else {

                            ## We have an array of primitive types, e.g. strings
                            $nestedCollection += $object;
                        }

                    }
                    $hashtable[$property.Name] = $nestedCollection;
                }
                else {

                    $hashtable[$property.Name] = $property.Value;
                }

            } #end foreach property
            Write-Output $hashtable;

        }
    } #end proicess
} #end function

function ConvertTo-ConfigurationData {
<#
     .SYNOPSIS
         Converts a file path string to a hashtable. This mimics the -ConfigurationData parameter of the
         Start-DscConfiguration cmdlet.
 #>

     [CmdletBinding()]
     [OutputType([System.Collections.Hashtable])]
     param (
         [Parameter(Mandatory, ValueFromPipeline)]
         [System.String] $ConfigurationData
     )
     process {

        $configurationDataPath = Resolve-Path -Path $ConfigurationData -ErrorAction Stop;
        if (-not (Test-Path -Path $configurationDataPath -PathType Leaf)) {

            throw ($localized.InvalidConfigurationDataFileError -f $ConfigurationData);
        }
        elseif ([System.IO.Path]::GetExtension($configurationDataPath) -ne '.psd1') {

            throw ($localized.InvalidConfigurationDataFileError -f $ConfigurationData);
        }
        $configurationDataContent = Get-Content -Path $configurationDataPath -Raw;
        $configData = Invoke-Command -ScriptBlock ([System.Management.Automation.ScriptBlock]::Create($configurationDataContent));
        if ($configData -isnot [System.Collections.Hashtable]) {

            throw ($localized.InvalidConfigurationDataType -f $configData.GetType());
        }
        return $configData;

    } #end process
} #end function ConvertTo-ConfigurationData

function CopyDirectory {
    <#
    .SYNOPSIS
        Copies a directory structure with progress.
#>

    [CmdletBinding()]
    param (
        ## Source directory path
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.IO.DirectoryInfo] $SourcePath,

        ## Destination directory path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.IO.DirectoryInfo] $DestinationPath,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        if ((Get-Item $SourcePath) -isnot [System.IO.DirectoryInfo]) {

            throw ($localized.CannotProcessArguentError -f 'CopyDirectory', 'SourcePath', $SourcePath, 'System.IO.DirectoryInfo');
        }
        elseif (Test-Path -Path $SourcePath -PathType Leaf) {

            throw ($localized.InvalidDestinationPathError -f $DestinationPath);
        }

    }
    process {

        $activity = $localized.CopyingResource -f $SourcePath.FullName, $DestinationPath;
        $status = $localized.EnumeratingPath -f $SourcePath;
        Write-Progress -Activity $activity -Status $status -PercentComplete 0;
        $fileList = Get-ChildItem -Path $SourcePath -File -Recurse;
        $currentDestinationPath = $SourcePath;

        for ($i = 0; $i -lt $fileList.Count; $i++) {

            if ($currentDestinationPath -ne $fileList[$i].DirectoryName) {

                ## We have a change of directory
                $destinationDirectoryName = $fileList[$i].DirectoryName.Substring($SourcePath.FullName.Trim('\').Length);
                $destinationDirectoryPath = Join-Path -Path $DestinationPath -ChildPath $destinationDirectoryName;
                [ref] $null = New-Item -Path $destinationDirectoryPath -ItemType Directory -ErrorAction Ignore;
                $currentDestinationPath = $fileList[$i].DirectoryName;
            }

            if (($i % 5) -eq 0) {

                [System.Int16] $percentComplete = (($i + 1) / $fileList.Count) * 100;
                $status = $localized.CopyingResourceStatus -f $i, $fileList.Count, $percentComplete;
                Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete;
            }

            $targetPath = Join-Path -Path $DestinationPath -ChildPath $fileList[$i].FullName.Replace($SourcePath, '');

            # This retry method is needed when AV scanners hold locks on files for a moment too long
            $copyTryCount = 1;
            while ($true) {
                try {
                    Copy-Item -Path $fileList[$i].FullName -Destination $targetPath -Force:$Force;
                    break;
                }
                catch {
                    $copyTryCount += 1;
                    if ($copyTryCount -gt 5) {
                        throw;
                    }
                    Write-Warning -Message ($localized.FileCopyFailedRetryingWarning -f $fileList[$i].FullName, $targetPath);
                    Start-Sleep -Seconds 1;
                }
            }
        } #end for

        Write-Progress -Activity $activity -Completed;

    } #end process
} #end function

function Copy-LabModule {
<#
    .SYNOPSIS
        Copies Lability PowerShell and DSC Resource modules.
#>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $ConfigurationData,

        ## Module type(s) to install
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('Module','DscResource')]
        [System.String[]] $ModuleType,

        ## Install a specific node's modules
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $NodeName,

        ## Destination module path
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath
    )
    begin {

        [System.Collections.Hashtable] $ConfigurationData = ConvertTo-ConfigurationData -ConfigurationData $ConfigurationData;

    }
    process {

        ## Copy PowerShell modules
        if ($ModuleType -contains 'Module') {

            if ($PSBoundParameters.ContainsKey('NodeName')) {

                $resolveLabModuleParams = @{
                    NodeName = $NodeName;
                    ConfigurationData = $ConfigurationData;
                    ModuleType = 'Module';
                }
                $powerShellModules = Resolve-LabModule @resolveLabModuleParams;
            }
            else {

                $powerShellModules = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Module;
            }

            if ($null -ne $powerShellModules) {

                Write-Verbose -Message ($localized.CopyingPowerShellModules -f $DestinationPath);
                if ($PSCmdlet.ShouldProcess($DestinationPath, $localized.InstallModulesConfirmation)) {

                    Expand-LabModuleCache -Module $powerShellModules -DestinationPath $DestinationPath;
                }
            }

        } #end if PowerShell modules

        ## Copy DSC resource modules
        if ($ModuleType -contains 'DscResource') {

            if ($PSBoundParameters.ContainsKey('NodeName')) {

                $resolveLabModuleParams = @{
                    NodeName = $NodeName;
                    ConfigurationData = $ConfigurationData;
                    ModuleType = 'DscResource';
                }
                $dscResourceModules = Resolve-LabModule @resolveLabModuleParams;
            }
            else {

                $dscResourceModules = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).DSCResource;
            }

            if ($null -ne $dscResourceModules) {

                Write-Verbose -Message ($localized.CopyingDscResourceModules -f $DestinationPath);
                if ($PSCmdlet.ShouldProcess($DestinationPath, $localized.InstallDscResourcesConfirmation)) {

                    Expand-LabModuleCache -Module $dscResourceModules -DestinationPath $DestinationPath;
                }
            }

        } #end if DSC resources

    } #end process
} #end function

function Disable-BitLockerFDV {
<#
    .SYNOPSIS
        Disables BitLocker full disk write protection (if enabled on the host system)
#>

    [CmdletBinding()]
    param ( )
    process {

        if ($fdvDenyWriteAccess) {

            Write-Verbose -Message $localized.DisablingBitlockerWriteProtection
            Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE' -Name 'FDVDenyWriteAccess' -Value 0;
        }

    } #end process
} #end function

function Expand-GitHubZipArchive {
<#
    .SYNOPSIS
        Extracts a GitHub Zip archive.
    .NOTES
        This is an internal function and should not be called directly.
    .LINK
        This function is derived from the GitHubRepository (https://github.com/IainBrighton/GitHubRepositoryCompression) module.
    .OUTPUTS
        A System.IO.FileInfo object for each extracted file.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [OutputType([System.IO.FileInfo])]
    param (
        # Source path to the Zip Archive.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)]
        [Alias('PSPath','FullName')]
        [System.String[]] $Path,

        # Destination file path to extract the Zip Archive item to.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)]
        [System.String] $DestinationPath,

        # GitHub repository name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Repository,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $OverrideRepository,

        # Overwrite existing files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        ## Validate destination path
        if (-not (Test-Path -Path $DestinationPath -IsValid)) {

            throw ($localized.InvalidDestinationPathError -f $DestinationPath);
        }

        $DestinationPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath);
        Write-Verbose -Message ($localized.ResolvedDestinationPath -f $DestinationPath);
        [ref] $null = New-Directory -Path $DestinationPath;

        foreach ($pathItem in $Path) {

            foreach ($resolvedPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathItem)) {
                Write-Verbose -Message ($localized.ResolvedSourcePath -f $resolvedPath);
                $LiteralPath += $resolvedPath;
            }
        }

        ## If all tests passed, load the required .NET assemblies
        Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.';
        Add-Type -AssemblyName 'System.IO.Compression';
        Add-Type -AssemblyName 'System.IO.Compression.FileSystem';

    } # end begin
    process {

        foreach ($pathEntry in $LiteralPath) {

            try {

                $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($pathEntry);
                $expandZipArchiveItemParams = @{
                    InputObject = [ref] $zipArchive.Entries;
                    DestinationPath = $DestinationPath;
                    Repository = $Repository;
                    Force = $Force;
                }

                if ($OverrideRepository) {
                    $expandZipArchiveItemParams['OverrideRepository'] = $OverrideRepository;
                }

                Expand-GitHubZipArchiveItem @expandZipArchiveItemParams;

            } # end try
            catch {

                Write-Error $_.Exception;
            }
            finally {

                ## Close the file handle
                Close-GitHubZipArchive;
            }

        } # end foreach

    } # end process
} #end function

function Expand-GitHubZipArchiveItem {
<#
    .SYNOPSIS
        Extracts file(s) from a GitHub Zip archive.
    .NOTES
        This is an internal function and should not be called directly.
    .LINK
        This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module.
    .OUTPUTS
        A System.IO.FileInfo object for each extracted file.
#>

    [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')]
    [OutputType([System.IO.FileInfo])]
    param (
        # Reference to Zip archive item.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'InputObject')]
        [System.IO.Compression.ZipArchiveEntry[]] [ref] $InputObject,

        # Destination file path to extract the Zip Archive item to.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)]
        [System.String] $DestinationPath,

        # GitHub repository name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Repository,

        ## Override repository name
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $OverrideRepository,

        # Overwrite existing physical filesystem files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.';
        Add-Type -AssemblyName 'System.IO.Compression';
        Add-Type -AssemblyName 'System.IO.Compression.FileSystem';

    }
    process {

        try {

            ## Regex for locating the <RepositoryName>-<Branch>\ root directory
            $searchString = '^{0}-\S+?\\' -f $Repository;
            $replacementString = '{0}\' -f $Repository;
            if ($OverrideRepository) {

                $replacementString = '{0}\' -f $OverrideRepository;
            }

            [System.Int32] $fileCount = 0;
            $moduleDestinationPath = Join-Path -Path $DestinationPath -ChildPath $Repository;
            $activity = $localized.DecompressingArchive -f $moduleDestinationPath;
            Write-Progress -Activity $activity -PercentComplete 0;

            foreach ($zipArchiveEntry in $InputObject) {

                $fileCount++;
                if (($fileCount % 5) -eq 0) {

                    [System.Int16] $percentComplete = ($fileCount / $InputObject.Count) * 100
                    $status = $localized.CopyingResourceStatus -f $fileCount, $InputObject.Count, $percentComplete;
                    Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete;
                }

                if ($zipArchiveEntry.FullName.Contains('/')) {

                    ## We need to create the directory path as the ExtractToFile extension method won't do this and will throw an exception
                    $pathSplit = $zipArchiveEntry.FullName.Split('/');
                    $relativeDirectoryPath = New-Object System.Text.StringBuilder;

                    ## Generate the relative directory name
                    for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) {
                        [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]);
                    }
                    ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\
                    $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\');

                    ## Create the destination directory path, joining the relative directory name
                    $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath;
                    [ref] $null = New-Directory -Path $directoryPath;

                    $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name;
                } # end if
                else {

                    ## Just a file in the root so just use the $DestinationPath
                    $fullDestinationFilePath = Join-Path -Path $DestinationPath -ChildPath $zipArchiveEntry.Name;
                } # end else

                if ([System.String]::IsNullOrEmpty($zipArchiveEntry.Name)) {

                    ## This is a folder and we need to create the directory path as the
                    ## ExtractToFile extension method won't do this and will throw an exception
                    $pathSplit = $zipArchiveEntry.FullName.Split('/');
                    $relativeDirectoryPath = New-Object System.Text.StringBuilder;

                    ## Generate the relative directory name
                    for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) {

                        [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]);
                    }

                    ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\
                    $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\');

                    ## Create the destination directory path, joining the relative directory name
                    $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath;
                    [ref] $null = New-Directory -Path $directoryPath;

                    $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name;
                }
                elseif (-not $Force -and (Test-Path -Path $fullDestinationFilePath -PathType Leaf)) {

                    ## Are we overwriting existing files (-Force)?
                    Write-Warning ($localized.TargetFileExistsWarning -f $fullDestinationFilePath);
                }
                else {

                    ## Just overwrite any existing file
                    if ($Force -or $PSCmdlet.ShouldProcess($fullDestinationFilePath, 'Expand')) {

                        Write-Debug ($localized.ExtractingZipArchiveEntry -f $fullDestinationFilePath);
                        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipArchiveEntry, $fullDestinationFilePath, $true);
                        ## Return a FileInfo object to the pipline
                        Write-Output (Get-Item -Path $fullDestinationFilePath);
                    }
                } # end if
            } # end foreach zipArchiveEntry

            Write-Progress -Activity $activity -Completed;

        } # end try
        catch {

            Write-Error $_.Exception;
        }

    } # end process
} #end function

function Expand-LabImage {
<#
    .SYNOPSIS
        Writes a .wim image to a mounted VHD/(X) file.
#>

    [CmdletBinding(DefaultParameterSetName = 'Index')]
    param (
        ## File path to WIM file or ISO file containing the WIM image
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $MediaPath,

        ## WIM image index to apply
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Index')]
        [System.Int32] $WimImageIndex,

        ## WIM image name to apply
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [System.String] $WimImageName,

        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk

        ## Disk image partition scheme
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('MBR','GPT')]
        [System.String] $PartitionStyle,

        ## Optional Windows features to add to the image after expansion (ISO only)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String[]] $WindowsOptionalFeature,

        ## Optional Windows features source path
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $SourcePath = '\sources\sxs',

        ## Relative source WIM file path (only used for ISOs)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $WimPath = '\sources\install.wim',

        ## Optional Windows packages to add to the image after expansion (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String[]] $Package,

        ## Relative packages (.cab) file path (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $PackagePath = '\packages',

        ## Package localization directory/extension (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $PackageLocale = 'en-US'
    )
    process {

        ## Assume the media path is a literal path to a WIM file
        $windowsImagePath = $MediaPath;
        $mediaFileInfo = Get-Item -Path $MediaPath;

        try {

            if ($mediaFileInfo.Extension -eq '.ISO') {

                ## Disable BitLocker fixed drive write protection (if enabled)
                Disable-BitLockerFDV;

                ## Mount ISO
                Write-Verbose -Message ($localized.MountingDiskImage -f $MediaPath);
                $mountDiskImageParams = @{
                    ImagePath = $MediaPath;
                    StorageType = 'ISO';
                    Access = 'ReadOnly';
                    PassThru = $true;
                    Verbose = $false;
                    ErrorAction = 'Stop';
                }
                $iso = Storage\Mount-DiskImage @mountDiskImageParams;
                $iso = Storage\Get-DiskImage -ImagePath $iso.ImagePath;
                $isoDriveLetter = Storage\Get-Volume -DiskImage $iso | Select-Object -ExpandProperty DriveLetter;

                ## Update the media path to point to the mounted ISO
                $windowsImagePath = '{0}:{1}' -f $isoDriveLetter, $WimPath;
            }

            if ($PSCmdlet.ParameterSetName -eq 'Name') {

                ## Locate the image index
                $wimImageIndex = Get-WindowsImageByName -ImagePath $windowsImagePath -ImageName $WimImageName;
            }

            if ($PartitionStyle -eq 'MBR') {

                $partitionType = 'IFS';
            }
            elseif ($PartitionStyle -eq 'GPT') {

                $partitionType = 'Basic';
            }
            $vhdDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType $partitionType;

            $logName = '{0}.log' -f [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path);
            $logPath = Join-Path -Path $env:TEMP -ChildPath $logName;
            Write-Verbose -Message ($localized.ApplyingWindowsImage -f $wimImageIndex, $Vhd.Path);

            $expandWindowsImageParams = @{
                ImagePath = $windowsImagePath;
                ApplyPath = '{0}:\' -f $vhdDriveLetter;
                LogPath = $logPath;
                Index = $wimImageIndex;
                Verbose = $false;
                ErrorAction = 'Stop';
            }
            [ref] $null = Expand-WindowsImage @expandWindowsImageParams;
            [ref] $null = Get-PSDrive;

            ## Add additional packages (.cab) files
            if ($Package) {

                ## Default to relative package folder path
                $addLabImageWindowsPackageParams = @{
                    PackagePath = '{0}:{1}' -f $isoDriveLetter, $PackagePath;
                    DestinationPath = '{0}:\' -f $vhdDriveLetter;
                    LogPath = $logPath;
                    Package = $Package;
                    PackageLocale = $PackageLocale;
                    ErrorAction = 'Stop';
                }
                if (-not $PackagePath.StartsWith('\')) {

                    ## Use the specified/literal path
                    $addLabImageWindowsPackageParams['PackagePath'] = $PackagePath;
                }
                [ref] $null = Add-LabImageWindowsPackage @addLabImageWindowsPackageParams;

            } #end if Package

            ## Add additional features if required
            if ($WindowsOptionalFeature) {

                ## Default to ISO relative source folder path
                $addLabImageWindowsOptionalFeatureParams = @{
                    ImagePath = '{0}:{1}' -f $isoDriveLetter, $SourcePath;
                    DestinationPath = '{0}:\' -f $vhdDriveLetter;
                    LogPath = $logPath;
                    WindowsOptionalFeature = $WindowsOptionalFeature;
                    ErrorAction = 'Stop';
                }
                if ($mediaFileInfo.Extension -eq '.WIM') {

                    ## The Windows optional feature source path for .WIM files is a literal path
                    $addLabImageWindowsOptionalFeatureParams['ImagePath'] = $SourcePath;
                }
                [ref] $null = Add-LabImageWindowsOptionalFeature @addLabImageWindowsOptionalFeatureParams;
            } #end if WindowsOptionalFeature

        }
        catch {

            Write-Error -Message $_;

        } #end catch
        finally {

            if ($mediaFileInfo.Extension -eq '.ISO') {

                ## Always dismount ISO (#166)
                Write-Verbose -Message ($localized.DismountingDiskImage -f $MediaPath);
                $null = Storage\Dismount-DiskImage -ImagePath $MediaPath;
            }

            ## Enable BitLocker (if required)
            Assert-BitLockerFDV

        } #end finally

    } #end process
} #end function Expand-LabImage

function Expand-LabIso {
<#
    .SYNOPSIS
        Expands an ISO disk image resource
#>

    param (
        ## Source ISO file path
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Path,

        ## Destination folder path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $DestinationPath
    )
    process {

        ## Disable BitLocker fixed drive write protection (if enabled)
        Disable-BitLockerFDV;

        Write-Verbose -Message ($localized.MountingDiskImage -f $Path);
        $iso = Storage\Mount-DiskImage -ImagePath $Path -StorageType ISO -Access ReadOnly -PassThru -Verbose:$false;
        ## Refresh drives
        [ref] $null = Get-PSDrive;
        $isoDriveLetter = $iso | Storage\Get-Volume | Select-Object -ExpandProperty DriveLetter;
        $sourcePath = '{0}:\' -f $isoDriveLetter;
        Write-Verbose -Message ($localized.ExpandingIsoResource -f $DestinationPath);
        CopyDirectory -SourcePath $sourcePath -DestinationPath $DestinationPath -Force -Verbose:$false;
        Write-Verbose -Message ($localized.DismountingDiskImage -f $Path);
        $null = Storage\Dismount-DiskImage -ImagePath $Path;

        ## Enable BitLocker (if required)
        Assert-BitLockerFDV;

    } #end process
} #end function

function Expand-LabModuleCache {
<#
    .SYNOPSIS
        Extracts a cached PowerShell module to the specified destination module path.
#>

    [CmdletBinding()]
    [OutputType([System.IO.DirectoryInfo])]
    param (
        ## PowerShell module hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable[]] $Module,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $DestinationPath,

        ## Removes existing module directory if present
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Clean,

        ## Catch all to be able to pass parameter via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    begin {

        [ref] $null = $PSBoundParameters.Remove('RemainingArguments');

    }
    process {

        foreach ($moduleInfo in $Module) {

            $moduleFileInfo = Get-LabModuleCache @moduleInfo;
            $moduleSourcePath = $moduleFileInfo.FullName;
            $moduleDestinationPath = Join-Path -Path $DestinationPath -ChildPath $moduleInfo.Name;

            if ($Clean -and (Test-Path -Path $moduleDestinationPath -PathType Container)) {
                Write-Verbose -Message ($localized.CleaningModuleDirectory -f $moduleDestinationPath);
                Remove-Item -Path $moduleDestinationPath -Recurse -Force -Confirm:$false;
            }

            if ((-not $moduleInfo.ContainsKey('Provider')) -or
                    ($moduleInfo.Provider -in 'PSGallery', 'AzDo')) {

                Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath);
                $expandZipArchiveParams = @{
                    Path = $moduleSourcePath;
                    DestinationPath = $moduleDestinationPath;
                    ExcludeNuSpecFiles = $true;
                    Force = $true;
                    Verbose = $false;
                    WarningAction = 'SilentlyContinue';
                    Confirm = $false;
                }
                [ref] $null = Expand-ZipArchive @expandZipArchiveParams;

            } #end if PSGallery or Azdo
            elseif (($moduleInfo.ContainsKey('Provider')) -and
                    ($moduleInfo.Provider -eq 'GitHub')) {

                Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath);
                $expandGitHubZipArchiveParams = @{
                    Path = $moduleSourcePath;
                    ## GitHub modules include the module directory. Therefore, we need the parent root directory
                    DestinationPath = Split-Path -Path $moduleDestinationPath -Parent;;
                    Repository = $moduleInfo.Name;
                    Force = $true;
                    Verbose = $false;
                    WarningAction = 'SilentlyContinue';
                    Confirm = $false;
                }

                if ($moduleInfo.ContainsKey('OverrideRepository')) {
                    $expandGitHubZipArchiveParams['OverrideRepository'] = $moduleInfo.OverrideRepository;
                }

                [ref] $null = Expand-GitHubZipArchive @expandGitHubZipArchiveParams;

            } #end if GitHub
            elseif (($moduleInfo.ContainsKey('Provider')) -and
                    ($moduleInfo.Provider -eq 'FileSystem')) {
                if ($null -ne $moduleFileInfo) {

                    if ($moduleFileInfo -is [System.IO.FileInfo]) {

                        Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath);
                        $expandZipArchiveParams = @{
                            Path = $moduleSourcePath;
                            DestinationPath = $moduleDestinationPath;
                            ExcludeNuSpecFiles = $true;
                            Force = $true;
                            Verbose = $false;
                            WarningAction = 'SilentlyContinue';
                            Confirm = $false;
                        }
                        [ref] $null = Expand-ZipArchive @expandZipArchiveParams;
                    }
                    elseif ($moduleFileInfo -is [System.IO.DirectoryInfo]) {

                        Write-Verbose -Message ($localized.CopyingModuleDirectory -f $moduleFileInfo.Name, $moduleDestinationPath);
                        ## If the target doesn't exist create it. We may be copying a versioned
                        ## module, i.e. \xJea\0.2.16.6 to \xJea..
                        if (-not (Test-Path -Path $moduleDestinationPath -PathType Container)) {
                            New-Item -Path $moduleDestinationPath -ItemType Directory -Force;
                        }
                        $copyItemParams = @{
                            Path = "$moduleSourcePath\*";
                            Destination = $moduleDestinationPath;
                            Recurse = $true;
                            Force = $true;
                            Verbose = $false;
                            Confirm = $false;
                        }
                        Copy-Item @copyItemParams;
                    }

                }
            } #end if FileSystem

            ## Only output if we found a module during this pass
            if ($null -ne $moduleFileInfo) {
                Write-Output -InputObject (Get-Item -Path $moduleDestinationPath);
            }

        } #end foreach module

    } #end process
} #end function

function Expand-LabResource {
<#
    .SYNOPSIS
        Copies files, e.g. EXEs, ISOs and ZIP file resources into a lab VM's mounted VHDX differencing disk image.
    .NOTES
        VHDX should already be mounted and passed in via the $DestinationPath parameter
        Can expand ISO and ZIP files if the 'Expand' property is set to $true on the resource's properties.
#>

    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab VM name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Destination mounted VHDX path to expand resources into
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        ## Source resource path
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourcePath
    )
    begin {

        if (-not $ResourcePath) {

            $hostDefaults = Get-ConfigurationData -Configuration Host;
            $ResourcePath = $hostDefaults.ResourcePath;
        }

    }
    process {

        ## Create the root destination (\Resources) container
        if (-not (Test-Path -Path $DestinationPath -PathType Container)) {

            [ref] $null = New-Item -Path $DestinationPath -ItemType Directory -Force -Confirm:$false;
        }

        $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -ErrorAction Stop;
        foreach ($resourceId in $node.Resource) {

            Write-Verbose -Message ($localized.AddingResource -f $resourceId);
            $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $resourceId;

            ## Default to resource.Id unless there is a filename property defined!
            $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Id;

            if ($resource.Filename) {

                $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Filename;
                if ($resource.IsLocal) {

                    $resourceSourcePath = Resolve-Path -Path $resource.Filename;
                }
            }

            if (-not (Test-Path -Path $resourceSourcePath) -and (-not $resource.IsLocal)) {

                $invokeLabResourceDownloadParams = @{
                    ConfigurationData = $ConfigurationData;
                    ResourceId = $resourceId;
                }
                [ref] $null = Invoke-LabResourceDownload @invokeLabResourceDownloadParams;
            }

            if (-not (Test-Path -Path $resourceSourcePath)) {

                throw ($localized.CannotResolveResourceIdError -f $resourceId);
            }

            $resourceItem = Get-Item -Path $resourceSourcePath;
            $resourceDestinationPath = $DestinationPath;

            if ($resource.DestinationPath -and (-not [System.String]::IsNullOrEmpty($resource.DestinationPath))) {

                $destinationDrive = Split-Path -Path $DestinationPath -Qualifier;
                $resourceDestinationPath = Join-Path -Path $destinationDrive -ChildPath $resource.DestinationPath;

                ## We can't create a drive-rooted folder!
                if (($resource.DestinationPath -ne '\') -and (-not (Test-Path -Path $resourceDestinationPath))) {

                    [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force  -Confirm:$false;
                }
            }
            elseif ($resource.IsLocal -and ($resource.IsLocal -eq $true)) {

                $relativeLocalPath = ($resource.Filename).TrimStart('.');
                $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $relativeLocalPath;
            }

            if (($resource.Expand) -and ($resource.Expand -eq $true)) {

                if ([System.String]::IsNullOrEmpty($resource.DestinationPath)) {

                    ## No explicit destination path, so expand into the <DestinationPath>\<ResourceId> folder
                    $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $resource.Id;
                }

                if (-not (Test-Path -Path $resourceDestinationPath)) {

                    [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force -Confirm:$false;
                }

                switch ([System.IO.Path]::GetExtension($resourceSourcePath)) {

                    '.iso' {

                        Expand-LabIso -Path $resourceItem.FullName -DestinationPath $resourceDestinationPath;
                    }

                    '.zip' {

                        Write-Verbose -Message ($localized.ExpandingZipResource -f $resourceItem.FullName);
                        $expandZipArchiveParams = @{
                            Path = $resourceItem.FullName;
                            DestinationPath = $resourceDestinationPath;
                            Verbose = $false;
                        }
                        [ref] $null = Expand-ZipArchive @expandZipArchiveParams;
                    }

                    Default {

                        throw ($localized.ExpandNotSupportedError -f $resourceItem.Extension);
                    }

                } #end switch
            }
            else {

                Write-Verbose -Message ($localized.CopyingFileResource -f $resourceDestinationPath);
                $copyItemParams = @{
                    Path = "$($resourceItem.FullName)";
                    Destination = "$resourceDestinationPath";
                    Force = $true;
                    Recurse = $true;
                    Verbose = $false;
                    Confirm = $false;
                }
                Copy-Item @copyItemParams;
            }

        } #end foreach ResourceId

    } #end process
} #end function

function Expand-ZipArchive {
<#
    .SYNOPSIS
        Extracts a Zip archive.
    .NOTES
        This is an internal function and should not be called directly.
    .LINK
        This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module.
    .OUTPUTS
        A System.IO.FileInfo object for each extracted file.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([System.IO.FileInfo])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        # Source path to the Zip Archive.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)]
        [Alias('PSPath','FullName')] [System.String[]] $Path,

        # Destination file path to extract the Zip Archive item to.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)]
        [System.String] $DestinationPath,

        # Excludes NuGet .nuspec specific files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $ExcludeNuSpecFiles,

        # Overwrite existing files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        ## Validate destination path
        if (-not (Test-Path -Path $DestinationPath -IsValid)) {

            throw ($localized.InvalidDestinationPathError -f $DestinationPath);
        }

        $DestinationPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath);
        Write-Verbose -Message ($localized.ResolvedDestinationPath -f $DestinationPath);
        [ref] $null = New-Directory -Path $DestinationPath;

        foreach ($pathItem in $Path) {

            foreach ($resolvedPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathItem)) {

                Write-Verbose -Message ($localized.ResolvedSourcePath -f $resolvedPath);
                $LiteralPath += $resolvedPath;
            }
        }

        ## If all tests passed, load the required .NET assemblies
        Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.';
        Add-Type -AssemblyName 'System.IO.Compression';
        Add-Type -AssemblyName 'System.IO.Compression.FileSystem';

    } # end begin
    process {

        foreach ($pathEntry in $LiteralPath) {

            try {

                $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($pathEntry);
                $expandZipArchiveItemParams = @{
                    InputObject = [ref] $zipArchive.Entries;
                    DestinationPath = $DestinationPath;
                    ExcludeNuSpecFiles = $ExcludeNuSpecFiles;
                    Force = $Force;
                }

                Expand-ZipArchiveItem @expandZipArchiveItemParams;

            } # end try
            catch {

                Write-Error -Message $_.Exception;
            }
            finally {

                ## Close the file handle
                Close-ZipArchive;
            }

        } # end foreach

    } # end process
} #end function

function Expand-ZipArchiveItem {
<#
    .SYNOPSIS
        Extracts file(s) from a Zip archive.
    .NOTES
        This is an internal function and should not be called directly.
    .LINK
        This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module.
    .OUTPUTS
        A System.IO.FileInfo object for each extracted file.
#>

    [CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')]
    [OutputType([System.IO.FileInfo])]
    param (
        # Reference to Zip archive item.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'InputObject')]
        [ValidateNotNullOrEmpty()] [System.IO.Compression.ZipArchiveEntry[]] [ref] $InputObject,

        # Destination file path to extract the Zip Archive item to.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        # Excludes NuGet .nuspec specific files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $ExcludeNuSpecFiles,

        # Overwrite existing physical filesystem files
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.';
        Add-Type -AssemblyName 'System.IO.Compression';
        Add-Type -AssemblyName 'System.IO.Compression.FileSystem';

    }
    process {

        try {

            [System.Int32] $fileCount = 0;
            $activity = $localized.DecompressingArchive -f $DestinationPath;
            Write-Progress -Activity $activity -PercentComplete 0;
            foreach ($zipArchiveEntry in $InputObject) {

                $fileCount++;
                if (($fileCount % 5) -eq 0) {

                    [System.Int16] $percentComplete = ($fileCount / $InputObject.Count) * 100
                    $status = $localized.CopyingResourceStatus -f $fileCount, $InputObject.Count, $percentComplete;
                    Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete;
                }

                ## Exclude the .nuspec specific files
                if ($ExcludeNuSpecFiles -and ($zipArchiveEntry.FullName -match '(_rels\/)|(\[Content_Types\]\.xml)|(\w+\.nuspec)')) {
                    Write-Verbose -Message ($localized.IgnoringNuspecZipArchiveEntry -f $zipArchiveEntry.FullName);
                    continue;
                }

                if ($zipArchiveEntry.FullName.Contains('/')) {

                    ## We need to create the directory path as the ExtractToFile extension method won't do this and will throw an exception
                    $pathSplit = $zipArchiveEntry.FullName.Split('/');
                    $relativeDirectoryPath = New-Object -TypeName System.Text.StringBuilder;

                    ## Generate the relative directory name
                    for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) {

                        [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]);
                    }
                    $relativePath = $relativeDirectoryPath.ToString();

                    ## Create the destination directory path, joining the relative directory name
                    $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath;
                    [ref] $null = New-Directory -Path $directoryPath;

                    $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name;
                } # end if
                else {

                    ## Just a file in the root so just use the $DestinationPath
                    $fullDestinationFilePath = Join-Path -Path $DestinationPath -ChildPath $zipArchiveEntry.Name;
                } # end else

                if ([System.String]::IsNullOrEmpty($zipArchiveEntry.Name)) {

                    ## This is a folder and we need to create the directory path as the
                    ## ExtractToFile extension method won't do this and will throw an exception
                    $pathSplit = $zipArchiveEntry.FullName.Split('/');
                    $relativeDirectoryPath = New-Object -TypeName System.Text.StringBuilder;

                    ## Generate the relative directory name
                    for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) {
                        [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]);
                    }
                    $relativePath = $relativeDirectoryPath.ToString();

                    ## Create the destination directory path, joining the relative directory name
                    $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath;
                    [ref] $null = New-Directory -Path $directoryPath;

                    $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name;
                }
                elseif (-not $Force -and (Test-Path -Path $fullDestinationFilePath -PathType Leaf)) {

                    ## Are we overwriting existing files (-Force)?
                    Write-Warning -Message ($localized.TargetFileExistsWarning -f $fullDestinationFilePath);
                }
                else {

                    ## Just overwrite any existing file
                    if ($Force -or $PSCmdlet.ShouldProcess($fullDestinationFilePath, 'Expand')) {
                        Write-Verbose -Message ($localized.ExtractingZipArchiveEntry -f $fullDestinationFilePath);
                        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipArchiveEntry, $fullDestinationFilePath, $true);
                        ## Return a FileInfo object to the pipline
                        Write-Output -InputObject (Get-Item -Path $fullDestinationFilePath);
                    }
                } # end if

            } # end foreach zipArchiveEntry

            Write-Progress -Activity $activity -Completed;

        } # end try
        catch {

            Write-Error -Message $_.Exception;
        }

    } # end process
} #end function

function Get-ConfigurationData {
<#
    .SYNOPSIS
        Gets lab configuration data.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('Host','VM','Media','CustomMedia')]
        [System.String] $Configuration
    )
    process {

        $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration -IncludeDefaultPath;
        if (Test-Path -Path $configurationPath) {
            $configurationData = Get-Content -Path $configurationPath -Raw | ConvertFrom-Json;

            switch ($Configuration) {

                'VM' {

                    ## This property may not be present in the original VM default file TODO: Could be deprecated in the future
                    if ($configurationData.PSObject.Properties.Name -notcontains 'CustomBootstrapOrder') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'CustomBootstrapOrder' -Value 'MediaFirst';
                    }

                    ## This property may not be present in the original VM default file TODO: Could be deprecated in the future
                    if ($configurationData.PSObject.Properties.Name -notcontains 'SecureBoot') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'SecureBoot' -Value $true;
                    }

                    ## This property may not be present in the original VM default file TODO: Could be deprecated in the future
                    if ($configurationData.PSObject.Properties.Name -notcontains 'GuestIntegrationServices') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'GuestIntegrationServices' -Value $false;
                    }

                    ## This property may not be present in the original VM default file TODO: Could be deprecated in the future
                    if ($configurationData.PSObject.Properties.Name -notcontains 'AutomaticCheckpoints') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'AutomaticCheckpoints' -Value $false;
                    }

                    ## This property may not be present in the original VM default file TODO: Could be deprecated in the future
                    if ($configurationData.PSObject.Properties.Name -notcontains 'MaxEnvelopeSizeKb') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'MaxEnvelopeSizeKb' -Value 1024;
                    }

                    ## This property may not be present in the original VM default file. Defaults to $false
                    if ($configurationData.PSObject.Properties.Name -notcontains 'UseNetBIOSName') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'UseNetBIOSName' -Value $false;
                    }
                }
                'CustomMedia' {

                    foreach ($mediaItem in $configurationData) {

                        ## Add missing OperatingSystem property
                        if ($mediaItem.PSObject.Properties.Name -notcontains 'OperatingSystem') {

                            [ref] $null = Add-Member -InputObject $mediaItem -MemberType NoteProperty -Name 'OperatingSystem' -Value 'Windows';
                        }
                    } #end foreach media item
                }
                'Host' {

                    ## This property may not be present in the original machine configuration file
                    if ($configurationData.PSObject.Properties.Name -notcontains 'DisableLocalFileCaching') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableLocalFileCaching' -Value $false;
                    }

                    ## This property may not be present in the original machine configuration file
                    if ($configurationData.PSObject.Properties.Name -notcontains 'EnableCallStackLogging') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'EnableCallStackLogging' -Value $false;
                    }

                    ## This property may not be present in the original machine configuration file
                    if ($configurationData.PSObject.Properties.Name -notcontains 'ModuleCachePath') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'ModuleCachePath' -Value '%ALLUSERSPROFILE%\Lability\Modules';
                    }

                    if ($configurationData.PSObject.Properties.Name -notcontains 'DismPath') {

                        $dismDllName = 'Microsoft.Dism.PowerShell.dll';
                        $dismDllPath = Join-Path -Path "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules\Dism" -ChildPath $dismDllName -Resolve;
                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DismPath' -Value $dismDllPath;
                    }

                    ## This property may not be present in the original machine configuration file
                    if ($configurationData.PSObject.Properties.Name -notcontains 'RepositoryUri') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'RepositoryUri' -Value $labDefaults.RepositoryUri;
                    }

                    ## This property may not be present in the original machine configuration file. Defaults to $true for existing
                    ## deployments, but is disabled ($false) in the default HostDefaults.json for new installs.
                    if ($configurationData.PSObject.Properties.Name -notcontains 'DisableSwitchEnvironmentName') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableSwitchEnvironmentName' -Value $true;
                    }

                    ## This property may not be present in the original machine configuration file. Defaults to $true for existing
                    ## deployments, but is disabled ($false) in the default HostDefaults.json for new installs.
                    if ($configurationData.PSObject.Properties.Name -notcontains 'DisableVhdEnvironmentName') {

                        [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableVhdEnvironmentName' -Value $true;
                    }

                    ## Remove deprecated UpdatePath, if present (Issue #77)
                    $configurationData.PSObject.Properties.Remove('UpdatePath');
                }
            } #end switch

            # Expand any environment variables in configuration data
            $configurationData.PSObject.Members |
                Where-Object { ($_.MemberType -eq 'NoteProperty') -and ($_.IsSettable) -and ($_.TypeNameOfValue -eq 'System.String') } |
                    ForEach-Object {
                        $_.Value = [System.Environment]::ExpandEnvironmentVariables($_.Value);
                    }

            return $configurationData;
        }

    } #end process
} #end function

function Get-DiskImageDriveLetter {
<#
    .SYNOPSIS
        Return a disk image's associated/mounted drive letter.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Object] $DiskImage,

        [Parameter(Mandatory)]
        [ValidateSet('Basic','System','IFS')]
        [System.String] $PartitionType
    )
    process {

        # Microsoft.Vhd.PowerShell.VirtualHardDisk
        $driveLetter = Storage\Get-Partition -DiskNumber $DiskImage.DiskNumber |
            Where-Object Type -eq $PartitionType |
                Where-Object DriveLetter |
                    Select-Object -Last 1 -ExpandProperty DriveLetter;

        if (-not $driveLetter) {

            throw ($localized.CannotLocateDiskImageLetter -f $DiskImage.Path);
        }
        return $driveLetter;
    }
} #end function

function Get-DscResourceModule {
<#
    .SYNOPSIS
        Enumerates a directory path and returns list of all valid DSC resources.
    .DESCRIPTION
        The Get-DscResourceModule returns all the PowerShell DSC resource modules in the specified path. This is used to
        determine which directories to copy to a VM's VHD(X) file. Only the latest version of module that is installed
        is returned, removing any versioned folders that are introduced in WMF 5.0, but cannot be interpreted by
        down-level WMF versions.
    .NOTES
        THIS METHOD IS DEPRECATED IN FAVOUR OF THE NEW MODULE CACHE FUNCTIONALITY
        More context can be found here https://github.com/VirtualEngine/Lab/issues/25
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock','')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String[]] $Path
    )
    process {

        foreach ($basePath in $Path) {

            Get-ChildItem -Path $basePath -Directory | ForEach-Object {

                $moduleInfo = $PSItem;
                ## Check to see if we have a MOF or class resource in the module
                if (Test-DscResourceModule -Path $moduleInfo.FullName -ModuleName $moduleInfo.Name) {

                    Write-Debug -Message ('Discovered DSC resource ''{0}''.' -f $moduleInfo.FullName);
                    $testModuleManifestPath = '{0}\{1}.psd1' -f $moduleInfo.FullName, $moduleInfo.Name;
                    ## Convert the .psd1 file into a hashtable (Test-ModuleManifest can actually load the module)
                    if (Test-Path -Path $testModuleManifestPath -PathType Leaf) {

                        $module = ConvertTo-ConfigurationData -ConfigurationData $testModuleManifestPath;
                        Write-Output -InputObject ([PSCustomObject] @{
                            ModuleName = $moduleInfo.Name;
                            ModuleVersion = $module.ModuleVersion -as [System.Version];
                            Path = $moduleInfo.FullName;
                        });
                    }
                }
                else {

                    ## Enumerate each module\<number>.<number> subdirectory
                    Get-ChildItem -Path $moduleInfo.FullName -Directory | Where-Object Name -match '^\d+\.\d+' | ForEach-Object {

                        Write-Debug -Message ('Checking module versioned directory ''{0}''.' -f $PSItem.FullName);
                        ## Test to see if it's a DSC resource module
                        if (Test-DscResourceModule -Path $PSItem.FullName -ModuleName $moduleInfo.Name) {

                            try {

                                Write-Debug -Message ('Discovered DSC resource ''{0}''.' -f  $PSItem.FullName);
                                $testModuleManifestPath = '{0}\{1}.psd1' -f  $PSItem.FullName, $moduleInfo.Name;
                                ## Convert the .psd1 file into a hashtable (Test-ModuleManifest can actually load the module)
                                $module = ConvertTo-ConfigurationData -ConfigurationData $testModuleManifestPath;
                                Write-Output -InputObject ([PSCustomObject] @{
                                    ModuleName =  $moduleInfo.Name;
                                    ModuleVersion = [System.Version] $module.ModuleVersion;
                                    Path = $PSItem.FullName;
                                });
                            }
                            catch { }
                        }
                    } | #end foreach module\<number>.<number> sub directory
                        Sort-Object -Property ModuleVersion -Descending | Select-Object -First 1;
                }

            } #end foreach module directory

        } #end foreach path

    } #end process
} #end function

function Get-FormattedMessage {
<#
    .SYNOPSIS
        Generates a formatted output message with timestamp.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Message
    )
    process {

        if (($labDefaults.CallStackLogging) -and ($labDefaults.CallStackLogging -eq $true)) {

            $parentCallStack = (Get-PSCallStack)[1]; # store the parent Call Stack
            $functionName = $parentCallStack.FunctionName;
            $lineNumber = $parentCallStack.ScriptLineNumber;
            $scriptName = ($parentCallStack.Location -split ':')[0];
            $formattedMessage = '[{0}] [Script:{1}] [Function:{2}] [Line:{3}] {4}' -f (Get-Date).ToLongTimeString(), $scriptName, $functionName, $lineNumber, $Message;
        }
        else {

            $formattedMessage = '[{0}] {1}' -f (Get-Date).ToLongTimeString(), $Message;
        }

        return $formattedMessage;

    } #end process
} #end function

function Get-LabDscModule {
<#
    .SYNOPSIS
        Locates the directory path of the ResourceName within the specified DSC ModuleName.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory)]
        [System.String] $ModuleName,

        [Parameter()]
        [System.String] $ResourceName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $MinimumVersion
    )
    process {

        $module = Get-Module -Name $ModuleName -ListAvailable;
        $dscModulePath = Split-Path -Path $module.Path -Parent;

        if ($ResourceName) {

            $ModuleName = '{0}\{1}' -f $ModuleName, $ResourceName;
            $dscModulePath = Join-Path -Path $dscModulePath -ChildPath "DSCResources\$ResourceName";
        }

        if (-not (Test-Path -Path $dscModulePath)) {

            Write-Error -Message ($localized.DscResourceNotFoundError -f $ModuleName);
            return $null;
        }

        if ($MinimumVersion) {

            if ($Module.Version -lt [System.Version]$MinimumVersion) {

                Write-Error -Message ($localized.ResourceVersionMismatchError -f $ModuleName, $module.Version.ToString(), $MinimumVersion);
                return $null;
            }
        }

        return $dscModulePath;

    } #end process
} #end function

function Get-LabDscResource {
<#
    .SYNOPSIS
        Gets the ResourceName DSC resource configuration.
    .DESCRIPTION
        The Get-LabDscResource cmdlet invokes the target $ResourceName\Get-TargetResource function using the supplied
        $Parameters hashtable.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Name of the DSC resource to get
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $ResourceName,

        ## The DSC resource's Get-TargetResource parameter hashtable
        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $Parameters
    )
    process {

        $getTargetResourceCommand = 'Get-{0}TargetResource' -f $ResourceName;
        Write-Debug ($localized.InvokingCommand -f $getTargetResourceCommand);
        # Code to factor in the parameters which can be passed to the Get-<Prefix>TargetResource function.
        $CommandInfo = Get-Command -Name $getTargetResourceCommand;
        $RemoveParameters = $Parameters.Keys |
            Where-Object -FilterScript { $($CommandInfo.Parameters.Keys) -notcontains $PSItem };
        $RemoveParameters |
            ForEach-Object -Process { [ref] $null = $Parameters.Remove($PSItem) };

        try {

            $getDscResourceResult = & $getTargetResourceCommand @Parameters;
        }
        catch {

            Write-Warning -Message ($localized.DscResourceFailedError -f $getTargetResourceCommand, $_);
        }

        return $getDscResourceResult;

    } #end process
} #end function

function Get-LabHostDscConfigurationPath {
<#
    .SYNOPSIS
        Shortcut function to resolve the host's default ConfigurationPath property
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param ( )
    process {

        $labHostDefaults = Get-ConfigurationData -Configuration Host;
        return $labHostDefaults.ConfigurationPath;

    } #end process
} #end function Get-LabHostDscConfigurationPath


function Get-LabHostSetupConfiguration {
<#
    .SYNOPSIS
        Returns an array of hashtables defining the desired host configuration.
    .DESCRIPTION
        The Get-LabHostSetupConfiguration function returns an array of hashtables used to determine whether the
        host is in the desired configuration.
    .NOTES
        The configuration is passed to avoid repeated calls to Get-LabHostDefault and polluting verbose output.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param ( )
    process {
        [System.Boolean] $isDesktop = (Get-CimInstance -ClassName Win32_OperatingSystem -Verbose:$false).ProductType -eq 1;
        ## Due to the differences in client/server deployment for Hyper-V, determine the correct method before creating the host configuration array.
        $labHostSetupConfiguration = @();

        if ($isDesktop) {

            Write-Debug -Message 'Implementing desktop configuration.';
            $labHostSetupConfiguration += @{
                UseDefault = $false;
                Description = 'Hyper-V role';
                ModuleName = 'xPSDesiredStateConfiguration';
                ResourceName = 'DSC_xWindowsOptionalFeature';
                Prefix = 'xWindowsOptionalFeature';
                Parameters = @{
                    Ensure = 'Present';
                    Name = 'Microsoft-Hyper-V-All';
                }
            };
        }
        else {

            Write-Debug -Message 'Implementing server configuration.';
            $labHostSetupConfiguration += @{
                UseDefault = $false;
                Description = 'Hyper-V Role';
                ModuleName = 'xPSDesiredStateConfiguration';
                ResourceName = 'DSC_xWindowsFeature';
                Prefix = 'xWindowsFeature';
                Parameters = @{
                    Ensure = 'Present';
                    Name = 'Hyper-V';
                    IncludeAllSubFeature = $true;
                }
            };
            $labHostSetupConfiguration += @{
                UseDefault = $false;
                Description = 'Hyper-V Tools';
                ModuleName = 'xPSDesiredStateConfiguration';
                ResourceName = 'DSC_xWindowsFeature';
                Prefix = 'xWindowsFeature';
                Parameters = @{
                    Ensure = 'Present';
                    Name = 'RSAT-Hyper-V-Tools';
                    IncludeAllSubFeature = $true;
                }
            };
        } #end Server configuration

        $labHostSetupConfiguration += @{
            ## Check for a reboot before continuing
            UseDefault = $false;
            Description = 'Pending reboot';
            ModuleName = 'xPendingReboot';
            ResourceName = 'MSFT_xPendingReboot';
            Prefix = 'PendingReboot';
            Parameters = @{
                Name = 'TestingForHypervReboot';
                SkipCcmClientSDK = $true;
            }
        };

        return $labHostSetupConfiguration;

    } #end process
} #end function

function Get-LabMediaId
{
<#
    .SYNOPSIS
        Helper method for dynamic media Id parameters, returning all valid media Ids and Aliases.
#>

    [CmdletBinding()]
    param( )
    process
    {
        $availableMedia = Get-LabMedia
        $mediaIds = @{ }
        foreach ($media in $availableMedia)
        {
            $mediaIds[$media.Id] = $media.Id
            if ($null -ne $media.Alias)
            {
                if ($mediaIds.ContainsKey($media.Alias))
                {
                    Write-Warning -Message ($localizedData.DuplicateMediaAliasIgnoredWarning -f $media.Id, $media.Alias)
                }
                else
                {
                    $mediaIds[$media.Alias] = $media.Alias
                }
            }
        }
        return $mediaIds.Keys
    }
}

function Get-LabModule {
<#
    .SYNOPSIS
        Tests whether an exising PowerShell module meets the minimum or required version
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name
    )
    process {

        Write-Verbose -Message ($localized.LocatingModule -f $Name);
        ## Only return modules in the %ProgramFiles%\WindowsPowerShell\Modules location, ignore other $env:PSModulePaths
        $programFiles = [System.Environment]::GetFolderPath('ProgramFiles');
        $modulesPath = ('{0}\WindowsPowerShell\Modules' -f $programFiles).Replace('\','\\');
        $module = Get-Module -Name $Name -ListAvailable -Verbose:$false | Where-Object Path -match $modulesPath;

        if (-not $module) {
            Write-Verbose -Message ($localized.ModuleNotFound -f $Name);
        }
        else {
            Write-Verbose -Message ($localized.ModuleFoundInPath -f $module.Path);
        }
        return $module;

    } #end process
} #end function

function Get-LabModuleCache {
<#
    .SYNOPSIS
        Returns the requested cached PowerShell module zip [System.IO.FileInfo] object.
    .NOTES
        File system modules are not stored in the module cache.
#>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $RequiredVersion,

        ## GitHub repository owner
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Owner,

        ## GitHub repository branch
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Branch,

        ## Source Filesystem module path
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Provider used to download the module
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateSet('PSGallery','GitHub', 'AzDo', 'FileSystem')]
        [System.String] $Provider,

        ## Lability PowerShell module info hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Hashtable] $Module,

        ## Catch all to be able to pass parameter via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)] $RemainingArguments
    )
    begin {

        if ([System.String]::IsNullOrEmpty($Provider)) {
            $Provider = 'PSGallery'
        }

        if ($PSCmdlet.ParameterSetName -eq 'Module') {

            $requiredParameters = 'Name';
            foreach ($parameterName in $requiredParameters) {

                if ([System.String]::IsNullOrEmpty($Module[$parameterName])) {

                    throw ($localized.RequiredModuleParameterError -f $parameterName);
                }
            } #end foreach required parameter

            $validParameters = 'Name','Provider','MinimumVersion','RequiredVersion','Owner','Branch','Path';
            foreach ($parameterName in $Module.Keys) {

                if ($parameterName -notin $validParameters) {

                    throw ($localized.InvalidtModuleParameterError -f $parameterName);
                }
                else {

                    Set-Variable -Name $parameterName -Value $Module[$parameterName];
                }
            } #end foreach parameter

        } #end if Module

        if ($Provider -eq 'GitHub') {

            if ([System.String]::IsNullOrEmpty($Owner)) {
                throw ($localized.RequiredModuleParameterError -f 'Owner');
            }

            ## Default to master branch if none specified
            if ([System.String]::IsNullOrEmpty($Branch)) {
                $Branch = 'master';
            }

            $Branch = $Branch.Replace('/','_') # Fix branch names with slashes (#361)

        } #end if GitHub
        elseif ($Provider -eq 'FileSystem') {

            if ([System.String]::IsNullOrEmpty($Path)) {
                throw ($localized.RequiredModuleParameterError -f 'Path');
            }
            elseif (-not (Test-Path -Path $Path)) {
                throw ($localized.InvalidPathError -f 'Module', $Path);
            }
            else {

                ## If we have a file, ensure it's a .Zip file
                $fileSystemInfo = Get-Item -Path $Path;
                if ($fileSystemInfo -is [System.IO.FileInfo]) {
                    if ($fileSystemInfo.Extension -ne '.zip') {
                        throw ($localized.InvalidModulePathExtensionError -f $Path);
                    }
                }
            }

        } #end if FileSystem

    }
    process {

        $moduleCachePath = (Get-ConfigurationData -Configuration Host).ModuleCachePath;

        ## If no provider specified, default to the PSGallery
        if (([System.String]::IsNullOrEmpty($Provider)) -or ($Provider -eq 'PSGallery')) {

            ## PowerShell Gallery modules are just suffixed with -v<Version>.zip
            $moduleRegex = '^{0}-v.+\.zip$' -f $Name;
        }
        elseif ($Provider -eq 'GitHub') {

            ## GitHub modules are suffixed with -v<Version>_<Owner>_<Branch>.zip
            $moduleRegex = '^{0}(-v.+)?_{1}_{2}\.zip$' -f $Name, $Owner, $Branch;
        }
        Write-Debug -Message ("Searching for files matching pattern '$moduleRegex'.");
        if ($Provider -in 'FileSystem') {

            ## We have a directory or a .zip file, so just return this
            return (Get-Item -Path $Path);
        }
        elseif ($Provider -in 'PSGallery', 'AzDo', 'GitHub') {
            $modules = Get-ChildItem -Path $moduleCachePath -ErrorAction SilentlyContinue |
                Where-Object Name -match $moduleRegex |
                    ForEach-Object {

                        Write-Debug -Message ("Discovered file '$($_.FullName)'.");
                        $trimStart = '{0}-v' -f $Name;
                        $moduleVersionString = $PSItem.Name.TrimStart($trimStart);
                        $moduleVersionString = $moduleVersionString -replace '(_\S+_\S+)?\.zip', '';

                        ## If we have no version number, default to the lowest version
                        if ([System.String]::IsNullOrEmpty($moduleVersionString)) {
                            $moduleVersionString = '0.0';
                        }

                        $discoveredModule = [PSCustomObject] @{
                            Name = $Name;
                            Version = $moduleVersionString -as [System.Version];
                            FileInfo = $PSItem;
                        }
                        Write-Output -InputObject $discoveredModule;
                    }
        }

        if ($null -ne $RequiredVersion) {
            Write-Debug -Message ("Checking for modules that match version '$RequiredVersion'.");
            Write-Output -InputObject (
                $modules | Where-Object { $_.Version -eq $RequiredVersion } |
                    Select-Object -ExpandProperty FileInfo);
        }
        elseif ($null -ne $MinimumVersion) {
            Write-Debug -Message ("Checking for modules with a minimum version of '$MinimumVersion'.");
            Write-Output -InputObject (
                $modules | Where-Object Version -ge $MinimumVersion |
                    Sort-Object -Property Version |
                        Select-Object -Last 1 -ExpandProperty FileInfo);
        }
        else {
            Write-Debug -Message ("Checking for the latest module version.");
            Write-Output -InputObject (
                $modules | Sort-Object -Property Version |
                    Select-Object -Last 1 -ExpandProperty FileInfo);
        }

    } #end process
} #end function Get-ModuleCache

function Get-LabModuleCacheManifest {
<#
    .SYNOPSIS
        Returns a zipped module's manifest.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## File path to the zipped module
        [Parameter(Mandatory)]
        [System.String] $Path,

        [ValidateSet('PSGallery','GitHub', 'AzDo')]
        [System.String] $Provider = 'PSGallery'
    )
    begin {

        if (-not (Test-Path -Path $Path -PathType Leaf)) {
            throw ($localized.InvalidPathError -f 'Module', $Path);
        }

    }
    process {

        Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.';
        [ref] $null = [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression");
        [ref] $null = [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem");

        $moduleFileInfo = Get-Item -Path $Path;

        if ($Provider -in 'PSGallery', 'AzDo') {
            $moduleName = $moduleFileInfo.Name -replace '\.zip', '';
        }elseif ($Provider -eq 'GitHub') {
            ## If we have a GitHub module, trim the _Owner_Branch.zip; if we have a PSGallery module, trim the .zip
            $moduleName = $moduleFileInfo.Name -replace '_\S+_\S+\.zip', '';
        }

        $moduleManifestName = '{0}.psd1' -f $moduleName;
        $temporaryArchivePath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "$moduleName.psd1";

        try {

            ### Open the ZipArchive with read access
            Write-Verbose -Message ($localized.OpeningArchive -f $moduleFileInfo.FullName);
            $archive = New-Object System.IO.Compression.ZipArchive(New-Object System.IO.FileStream($moduleFileInfo.FullName, [System.IO.FileMode]::Open));

            ## Zip archive entries are case-sensitive, therefore, we need to search for a match and can't use ::GetEntry()
            foreach ($archiveEntry in $archive.Entries) {
                if ($archiveEntry.Name -eq $moduleManifestName) {
                    $moduleManifestArchiveEntry = $archiveEntry;
                }
            }

            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($moduleManifestArchiveEntry, $temporaryArchivePath, $true);
            $moduleManifest = ConvertTo-ConfigurationData -ConfigurationData $temporaryArchivePath;
        }

        catch {

            Write-Error ($localized.ReadingArchiveItemError -f $moduleManifestName);
        }
        finally {

            if ($null -ne $archive) {
                Write-Verbose -Message ($localized.ClosingArchive -f $moduleFileInfo.FullName);
                $archive.Dispose();
            }
            Remove-Item -Path $temporaryArchivePath -Force;
        }

        return $moduleManifest;

    } #end process
} #end function

function Get-LabMofModule {
<#
    .SYNOPSIS
        Retrieves a list of DSC resource modules defined in a MOF file.
#>

    [CmdletBinding(DefaultParameterSetName='Path')]
    [OutputType([System.Collections.Hashtable])]
    param (
        # Specifies the export path location.
        [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('PSPath','FullName')]
        [System.String] $Path,

        # Specifies a literal export location path.
        [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $LiteralPath
    )
    begin {

        $definedModules = @{ };
    }
    process {

        if ($PSCmdlet.ParameterSetName -eq 'Path') {

            # Resolve any relative paths
            $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
        }
        else {

            $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath);
        }

        if ($Path -match '\.meta\.mof$') {

            Write-Warning -Message ($localized.SkippingMetaConfigurationWarning -f $Path);
        }
        else {

            Write-Verbose -Message ($localized.ProcessingMofFile -f $Path);
            $currentModule = $null;
            $currentLineNumber = 0;

            foreach ($line in [System.IO.File]::ReadLines($Path)) {

                $currentLineNumber++;
                if ($line -match '^instance of (?!MSFT_Credential|MSFT_xWebBindingInformation)') {
                    ## Ignore MSFT_Credential and MSFT_xWebBindingInformation types. There may be
                    ## other types that need suppressing, but they'll be resource specific

                    if ($null -eq $currentModule) {

                        ## Ignore the very first instance definition!
                    }
                    elseif (($currentModule.ContainsKey('Name')) -and
                            ($currentModule.ContainsKey('RequiredVersion'))) {

                        $definedModules[($currentModule.Name)] = $currentModule;
                    }
                    else {

                        Write-Warning -Message ($localized.CannotResolveMofModuleWarning -f $instanceLineNumber);
                    }

                    $instanceLineNumber = $currentLineNumber;
                    $currentModule = @{ };

                }
                elseif ($line -match '(?<=\s?ModuleName\s?=\s?")\w+(?=";)') {

                    $currentModule['Name'] = $Matches[0];
                }
                elseif ($line -match '(?<=\s?ModuleVersion\s?=\s?")\d+(\.\d+){1,3}(?=";)') {

                    $currentModule['RequiredVersion'] = $Matches[0];
                }

            } #end foreach line
        }


    } #end process
    end {

        foreach ($module in $definedModules.GetEnumerator()) {

            ## Exclude the default/built-in PSDesiredStateConfiguration module
            if ($module.Key -ne 'PSDesiredStateConfiguration') {

                Write-Output -InputObject $module.Value;
            }

        }

    } #end end
} #end function

function Get-LabVirtualMachineProperty {
<#
    .SYNOPSIS
        Gets the properties required by DSC xVMHyperV.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $SwitchName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Media,

        [Parameter(Mandatory)]
        [System.UInt64] $StartupMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MinimumMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MaximumMemory,

        [Parameter(Mandatory)]
        [System.Int32] $ProcessorCount,

        [Parameter()]
        [AllowNull()]
        [System.String[]] $MACAddress,

        [Parameter()]
        [System.Boolean] $SecureBoot,

        [Parameter()]
        [System.Boolean] $GuestIntegrationServices,

        [Parameter()]
        [System.Boolean] $AutomaticCheckpoints,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        ## Resolve the media to determine whether we require a Generation 1 or 2 VM..
        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $labMedia = Resolve-LabMedia -Id $Media -ConfigurationData $ConfigurationData;
            $labImage = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData;
        }
        else {

            $labMedia = Resolve-LabMedia -Id $Media;
            $labImage = Get-LabImage -Id $Media;
        }
        if (-not $labImage) {

            ## Should only trigger during a Reset-VM where parent image is not available?!
            ## It will be downloaded during any New-LabVirtualMachine calls..
            $labImage = @{ Generation = 'VHDX'; }
        }
        $labMediaArchitecture = $labMedia.Architecture;

        if (-not [System.String]::IsNullOrEmpty($labMedia.CustomData.PartitionStyle)) {

            ## The partition style has been overridden so use this
            if ($labMedia.CustomData.PartitionStyle -eq 'MBR') {

                $labMediaArchitecture = 'x86';
            }
            elseif ($labMedia.CustomData.PartitionStyle -eq 'GPT') {

                $labMediaArchitecture = 'x64';
            }
        }

        if ($null -ne $labMedia.CustomData.VmGeneration) {

            ## Use the specified VM generation
            $PSBoundParameters.Add('Generation', $labMedia.CustomData.VmGeneration);
        }
        elseif ($labImage.Generation -eq 'VHD') {

            ## VHD files are only supported in G1 VMs
            $PSBoundParameters.Add('Generation', 1);
        }
        elseif ($labMediaArchitecture -eq 'x86') {

            ## Assume G1 for x86 media
            $PSBoundParameters.Add('Generation', 1);
        }
        elseif ($labMediaArchitecture -eq 'x64') {

            ## Assume G2 for x64 media
            $PSBoundParameters.Add('Generation', 2);
        }

        if ($null -eq $MACAddress) {

            [ref] $null = $PSBoundParameters.Remove('MACAddress');
        }

        if ($PSBoundParameters.ContainsKey('GuestIntegrationServices')) {

            [ref] $null = $PSBoundParameters.Add('EnableGuestService', $GuestIntegrationServices);
            [ref] $null = $PSBoundParameters.Remove('GuestIntegrationServices');
        }

        if ($PSBoundParameters.ContainsKey('AutomaticCheckpoints')) {

            ## Always remove 'AutomaticCheckpoints' property (#294)
            [ref] $null = $PSBoundParameters.Remove('AutomaticCheckpoints');

            ## Automatic checkpoints were only introduced in 1709 (and later) builds.
            if (Test-WindowsBuildNumber -MinimumVersion 16299) {

                [ref] $null = $PSBoundParameters.Add('AutomaticCheckpointsEnabled', $AutomaticCheckpoints);
            }
            else {

                Write-Debug -Message ($localized.AutomaticCheckPointsNotSupported);
            }
        }

        $resolveLabVMDiskPathParams = @{
            Name            = $Name;
            Generation      = $labImage.Generation;
            EnvironmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;
        }
        $vhdPath = Resolve-LabVMDiskPath @resolveLabVMDiskPathParams;

        [ref] $null = $PSBoundParameters.Remove('Media');
        [ref] $null = $PSBoundParameters.Remove('ConfigurationData');
        [ref] $null = $PSBoundParameters.Add('VhdPath', $vhdPath);
        [ref] $null = $PSBoundParameters.Add('RestartIfNeeded', $true);

        return $PSBoundParameters;

    } #end process
} #end function

function Get-LabVMDisk {
<#
    .SYNOPSIS
        Retrieves lab virtual machine disk (VHDX) if present.
    .DESCRIPTION
        Gets a VM disk configuration using the xVHD DSC resource.
#>

    [CmdletBinding()]
    param (
        ## VM/node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;

        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData;
        }
        else {

            $image = Get-LabImage -Id $Media;
        }

        $vhd = @{
            Name = $Name;
            Path = $hostDefaults.DifferencingVhdPath;
            ParentPath = $image.ImagePath;
            Generation = $image.Generation;
        }
        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD;
        Get-LabDscResource -ResourceName VHD -Parameters $vhd;

    } #end process
} #end function

function Get-LabVMSnapshot {
<#
    .SYNOPSIS
        Gets snapshots of all virtual machines with the specified snapshot name.
#>

    [CmdletBinding()]
    param (
        ## VM/node name.
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String[]] $Name,

        ## Snapshot name to restore.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $SnapshotName
    )
    process {

        foreach ($vmName in $Name) {

            $snapshot = Hyper-V\Get-VMSnapshot -VMName $vmName -Name $SnapshotName -ErrorAction SilentlyContinue;
            if (-not $snapshot) {

                Write-Warning -Message ($localized.SnapshotMissingWarning -f $SnapshotName, $vmName);
            }
            else {

                Write-Output -InputObject $snapshot;
            }
        } #end foreach VM

    } #end process
} #end function

function Get-ResourceDownload {
<#
    .SYNOPSIS
        Retrieves a downloaded resource's details.
    .NOTES
        Based upon https://github.com/iainbrighton/cRemoteFile/blob/master/DSCResources/VE_RemoteFile/VE_RemoteFile.ps1
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Checksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt32] $BufferSize = 64KB
        ##TODO: Support Headers and UserAgent
    )
    process {

        $checksumPath = '{0}.checksum' -f $DestinationPath;
        if (-not (Test-Path -Path $DestinationPath)) {

            Write-Verbose -Message ($localized.MissingResourceFile -f $DestinationPath);
        }
        elseif (-not (Test-Path -Path $checksumPath)) {

            [ref] $null = Set-ResourceChecksum -Path $DestinationPath;
        }

        if (Test-Path -Path $checksumPath) {

            Write-Debug -Message ('MD5 checksum file ''{0}'' found.' -f $checksumPath);
            $md5Checksum = (Get-Content -Path $checksumPath -Raw).Trim();
            Write-Debug -Message ('Discovered MD5 checksum ''{0}''.' -f $md5Checksum);
        }
        else {

            Write-Debug -Message ('MD5 checksum file ''{0}'' not found.' -f $checksumPath);
        }

        $resource = @{
            DestinationPath = $DestinationPath;
            Uri = $Uri;
            Checksum = $md5Checksum;
        }
        return $resource;

    } #end process
} #end function

function Get-WindowsImageByIndex {
<#
    .SYNOPSIS
        Locates the specified WIM image name by its index.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        # WIM image path
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,

        # Windows image index
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.Int32] $ImageIndex
    )
    process {

        Write-Verbose -Message ($localized.LocatingWimImageName -f $ImageIndex);
        Get-WindowsImage -ImagePath $ImagePath -Verbose:$false |
            Where-Object ImageIndex -eq $ImageIndex |
                Select-Object -ExpandProperty ImageName;

    } #end process
} #end function Get-WindowsImageByIndex

function Get-WindowsImageByName {
<#
    .SYNOPSIS
        Locates the specified WIM image index by its name, i.e. SERVERSTANDARD or SERVERDATACENTERSTANDARD.
    .OUTPUTS
        The WIM image index.
#>

    [CmdletBinding()]
    [OutputType([System.Int32])]
    param (
        # WIM image path
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,

        # Windows image name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImageName
    )
    process {

        Write-Verbose -Message ($localized.LocatingWimImageIndex -f $ImageName);
        Get-WindowsImage -ImagePath $ImagePath -Verbose:$false |
            Where-Object ImageName -eq $ImageName |
                Select-Object -ExpandProperty ImageIndex;

    } #end process
} #end function Get-WindowsImageByName

function Import-DismModule {
<#
    .SYNOPSIS
        Imports the required DISM dll.
#>

    [CmdletBinding()]
    param ( )
    process {

        $dismPath = (Get-LabHostDefault).DismPath;
        Remove-Module -Name 'Microsoft.Dism.PowerShell' -ErrorAction SilentlyContinue;
        $dismModule = Import-Module -Name $dismPath -Force -Scope Global -PassThru -Verbose:$false;
        $labDefaults.DismVersion = $dismModule.Version;
        Write-Verbose -Message ($localized.LoadedModuleVersion -f 'Dism', $dismModule.Version);

    } #end process
} #end function

function Import-LabDscResource {
<#
    .SYNOPSIS
        Imports a DSC module resource.
    .DESCRIPTION
        Imports a DSC resource as Test-<Prefix>TargetResource and Set-<Prefix>TargetResource etc.
#>

    [CmdletBinding()]
    param (
        ## DSC resource's module name containing the resource
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $ModuleName,

        ## DSC resource's name to import
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $ResourceName,

        ## Local prefix, defaults to the resource name
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Prefix = $ResourceName,

        ## Use the built-in/default DSC resource
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseDefault
    )
    process {

        ## Check whether the resource is already imported/registered
        Write-Debug ($localized.CheckingDscResource -f $ModuleName, $ResourceName);
        $testCommandName = 'Test-{0}TargetResource' -f $Prefix;

        if (-not (Get-Command -Name $testCommandName -ErrorAction SilentlyContinue)) {

            if ($UseDefault) {

                Write-Verbose -Message ($localized.ImportingDscResource -f $ModuleName, $ResourceName);
                $resourcePath = Get-LabDscModule -ModuleName $ModuleName -ResourceName $ResourceName -ErrorAction Stop;
            }
            else {

                Write-Verbose -Message ($localized.ImportingBundledDscResource -f $ModuleName, $ResourceName);
                $dscModuleRootPath = '{0}\{1}\{2}\DSCResources' -f $labDefaults.ModuleRoot, $labDefaults.DscResourceDirectory, $ModuleName;
                $dscResourcePath = '{0}\{0}.psm1' -f $ResourceName;
                $resourcePath = Join-Path -Path $dscModuleRootPath -ChildPath $dscResourcePath;
            }

            if ($resourcePath) {

                ## Import the DSC module into the module's global scope to improve performance
                Import-Module -Name $resourcePath -Prefix $Prefix -Force -Scope Global -Verbose:$false;
            }

        }
        else {

            Write-Debug -Message ($localized.DscResourceAlreadyImported -f $ModuleName, $ResourceName);
        }

    } #end process
} #end function

function Invoke-Executable {
<#
    .SYNOPSIS
        Runs an executable and redirects StdOut and StdErr.
#>

    [CmdletBinding()]
    [OutputType([System.Int32])]
    param (
        # Executable path
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        # Executable arguments
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Array] $Arguments,

        # Redirected StdOut and StdErr log name
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $LogName = ('{0}.log' -f $Path)
    )
    process {

        $processArgs = @{
            FilePath = $Path;
            ArgumentList = $Arguments;
            Wait = $true;
            RedirectStandardOutput = '{0}\{1}-StdOut.log' -f $env:temp, $LogName;
            RedirectStandardError = '{0}\{1}-StdErr.log' -f $env:temp, $LogName;
            NoNewWindow = $true;
            PassThru = $true;
        }
        Write-Debug -Message ($localized.RedirectingOutput -f 'StdOut', $processArgs.RedirectStandardOutput);
        Write-Debug -Message ($localized.RedirectingOutput -f 'StdErr', $processArgs.RedirectStandardError);
        Write-Verbose -Message ($localized.StartingProcess -f $Path, [System.String]::Join(' ', $Arguments));
        $process = Start-Process @processArgs;

        if ($process.ExitCode -ne 0) {

            Write-Warning -Message ($localized.ProcessExitCode -f $Path, $process.ExitCode)
        }
        else {

            Write-Verbose -Message ($localized.ProcessExitCode -f $Path, $process.ExitCode);
        }
        ##TODO: Should this actually return the exit code?!

    } #end process
} #end function

function Invoke-LabDscResource {
<#
    .SYNOPSIS
        Runs the ResourceName DSC resource ensuring it's in the desired state.
    .DESCRIPTION
        The Invoke-LabDscResource cmdlet invokes the target $ResourceName\Test-TargetResource function using the supplied
        $Parameters hastable. If the resource is not in the desired state, the $ResourceName\Set-TargetResource
        function is called with the $Parameters hashtable to attempt to correct the resource.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.String] $ResourceName,

        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $Parameters
    )
    process {

        ## Attempt to expand any paths where parameter name contains 'Path'. Requires
        ## creating another hashtable to avoid modifying the collection.
        $resolvedParameters = @{ }
        foreach ($key in $Parameters.Keys) {

            $resolvedParameters[$key] = $Parameters[$key];

            if ($key -match 'Path') {

                $resolvedParameters[$key] = Resolve-PathEx -Path $Parameters[$key];
                if ($Parameters[$key] -ne $resolvedParameters[$key]) {

                    Write-Debug -Message ("Expanding path '{0}' with value '{1}'." -f $key, $Parameters[$key]);
                    Write-Debug -Message ("Resolved path '{0}' to value '{1}'." -f $key, $resolvedParameters[$key]);
                }
            }
        }
        $PSBoundParameters['Parameters'] = $resolvedParameters;

        if (-not (Test-LabDscResource @PSBoundParameters)) {

            if ($ResourceName -match 'PendingReboot') {

                throw $localized.PendingRebootWarning;
            }
            return (Set-LabDscResource @PSBoundParameters);
        }
        else {

            $setTargetResourceCommand = 'Set-{0}TargetResource' -f $ResourceName;
            Write-Verbose -Message ($localized.SkippingCommand -f $setTargetResourceCommand);
        }

    } #end process
} #end function

function Invoke-LabMediaDownload {
<#
    .SYNOPSIS
        Downloads resources.
    .DESCRIPTION
        Initiates a download of a media resource. If the resource has already been downloaded and the checksum
        is correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch.
    .NOTES
        ISO/WIM media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the
        ParentVhdPath location.
#>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Id,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Checksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $destinationPath = Join-Path -Path $hostDefaults.HotfixPath -ChildPath $Id;
        $invokeResourceDownloadParams = @{
            DestinationPath = $destinationPath;
            Uri = $Uri;
        }
        if ($Checksum) {

            [ref] $null = $invokeResourceDownloadParams.Add('Checksum', $Checksum);
        }

        [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force;
        return (Get-Item -Path $destinationPath);

    } #end process
} #end function

function Invoke-LabMediaImageDownload {
<#
    .SYNOPSIS
        Downloads ISO/WIM/VHDX media resources.
    .DESCRIPTION
        Initiates a download of a media resource. If the resource has already been downloaded and the checksum is
        correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch.
    .NOTES
        ISO media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the
        ParentVhdPath location.
#>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        ## Lab media object
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Object] $Media,

        ## Force (re)download of the resource
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {
        $hostDefaults = Get-ConfigurationData -Configuration Host;

        $invokeResourceDownloadParams = @{
            DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename;
            Uri = $media.Uri;
            Checksum = $media.Checksum;
        }
        if ($media.MediaType -eq 'VHD') {

            $invokeResourceDownloadParams['DestinationPath'] = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $media.Filename;
        }

        $mediaUri = New-Object -TypeName System.Uri -ArgumentList $Media.Uri;
        if ($mediaUri.Scheme -eq 'File') {

            ## Use a bigger buffer for local file copies..
            $invokeResourceDownloadParams['BufferSize'] = 1MB;
        }

        if ($media.MediaType -eq 'VHD') {

            ## Always download VHDXs regardless of Uri type
            [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force;
        }
        elseif (($mediaUri.Scheme -eq 'File') -and ($media.MediaType -eq 'WIM') -and $hostDefaults.DisableLocalFileCaching)
        ## TODO: elseif (($mediaUri.Scheme -eq 'File') -and $hostDefaults.DisableLocalFileCaching)
        {
            ## NOTE: Only WIM media can currently be run from a file share (see https://github.com/VirtualEngine/Lab/issues/28)
            ## Caching is disabled and we have a file resource, so just return the source URI path
            Write-Verbose -Message ($localized.MediaFileCachingDisabled -f $Media.Id);
            $invokeResourceDownloadParams['DestinationPath'] = $mediaUri.LocalPath;
        }
        else {

            ## Caching is enabled or it's a http/https source
            [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force;
        }
        return (Get-Item -Path $invokeResourceDownloadParams.DestinationPath);

    } #end process
} #end function

function Invoke-LabModuleCacheDownload {
<#
    .SYNOPSIS
        Downloads a PowerShell module (DSC resource) into the module cache.
#>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [System.String] $Name,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [System.Version] $RequiredVersion,

        ## GitHub repository owner
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Owner,

        ## The GitHub repository name, normally the DSC module's name
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Repository = $Name,

        ## GitHub repository branch
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Branch,

        ## Source Filesystem module path
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Provider used to download the module
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateSet('PSGallery','GitHub','AzDo','FileSystem')]
        [System.String] $Provider,

        ## Lability PowerShell module info hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')]
        [System.Collections.Hashtable[]] $Module,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath,

        ## Credentials to access the an Azure DevOps private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential,

        ## Force a download of the module(s) even if they already exist in the cache.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

        ## Catch all to be able to pass parameter via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments

    )
    begin {

        ## Remove -RemainingArguments to stop it being passed on.
        [ref] $null = $PSBoundParameters.Remove('RemainingArguments');
        if ($PSCmdlet.ParameterSetName -ne 'Module') {

            ## Create a module hashtable
            $newModule = @{
                Name = $Name;
                Repository = $Repository;
            }
            if ($PSBoundParameters.ContainsKey('MinimumVersion')) {
                $newModule['MinimumVersion'] = $MinimumVersion;
            }
            if ($PSBoundParameters.ContainsKey('RequiredVersion')) {
                $newModule['RequiredVersion'] = $RequiredVersion;
            }
            if ($PSBoundParameters.ContainsKey('Owner')) {
                $newModule['Owner'] = $Owner;
            }
            if ($PSBoundParameters.ContainsKey('Branch')) {
                $newModule['Branch'] = $Branch;
            }
            if ($PSBoundParameters.ContainsKey('Path')) {
                $newModule['Path'] = $Path;
            }
            if ($PSBoundParameters.ContainsKey('Provider')) {
                $newModule['Provider'] = $Provider;
            }
            $Module = $newModule;
        }

    }
    process {

        foreach ($moduleInfo in $Module) {

            if ((-not (Test-LabModuleCache @moduleInfo)) -or ($Force) -or ($moduleInfo.Latest -eq $true)) {

                if ($moduleInfo.ContainsKey('RequiredVersion')) {
                    Write-Verbose -Message ($localized.ModuleVersionNotCached -f $moduleInfo.Name, $moduleInfo.RequiredVersion);
                }
                elseif ($moduleInfo.ContainsKey('MinimumVersion')) {
                    Write-Verbose -Message ($localized.ModuleMinmumVersionNotCached -f $moduleInfo.Name, $moduleInfo.MinimumVersion);
                }
                else {
                    Write-Verbose -Message ($localized.ModuleNotCached -f $moduleInfo.Name);
                }

                if ((-not $moduleInfo.ContainsKey('Provider')) -or ($moduleInfo['Provider'] -eq 'PSGallery')) {
                    Invoke-LabModuleDownloadFromPSGallery @moduleInfo;
                }
                elseif ($moduleInfo['Provider'] -eq 'AzDo') {
                    Invoke-LabModuleDownloadFromAzDo @moduleInfo -FeedCredential $FeedCredential
                }
                elseif ($moduleInfo['Provider'] -eq 'GitHub') {
                    Invoke-LabModuleDownloadFromGitHub @moduleInfo;
                }
                elseif ($moduleInfo['Provider'] -eq 'FileSystem') {
                    ## We should never get here as filesystem modules are not cached.
                    ## If the test doesn't throw, it should return $true.
                }
            }
            else {
                Get-LabModuleCache @moduleInfo;
            }

        } #end foreach module

    } #end process
} #end function

function Invoke-LabModuleDownloadFromAzDo {
    <#
    .SYNOPSIS
        Downloads a PowerShell module/DSC resource from an Azure DevOps Feed to the host's module cache.
#>

    [CmdletBinding(DefaultParameterSetName = 'Latest')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')]
        [System.Version] $RequiredVersion,

        ## Credentials to access the an Azure DevOps private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential,

        ## Catch all, for splatting parameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments

    )
    process {

        $destinationModuleName = '{0}.zip' -f $Name;
        $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName;

        # we need to remove the property to pass all remaing arguments else the credentials are not passed
        $null = $PSBoundParameters.Remove('RemainingArguments');

        $setResourceDownloadParams = @{
            DestinationPath = $moduleCacheDestinationPath;
            Uri             = Resolve-AzDoModuleUri @PSBoundParameters;
            NoCheckSum      = $true;
            FeedCredential  = $FeedCredential;
        }

        $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams;

        $renameLabModuleCacheVersionParams = @{
            Name = $Name;
            Path = $moduleDestinationPath;
        }
        if ($PSBoundParameters.ContainsKey('RequiredVersion')) {

            $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion
        }
        elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) {

            $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion
        }
        return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams);

    } #end process
} #end function

function Invoke-LabModuleDownloadFromGitHub {
    <#
.SYNOPSIS
    Downloads a DSC resource if it has not already been downloaded from Github.
.NOTES
    Uses the GitHubRepository module!
#>

    [CmdletBinding(DefaultParameterSetName = 'Latest')]
    [OutputType([System.IO.DirectoryInfo])]
    param (
        ## PowerShell DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath,

        ## The GitHub repository owner, typically 'PowerShell'
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Owner,

        ## The GitHub repository name, normally the DSC module's name
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Repository = $Name,

        ## The GitHub branch to download, defaults to the 'master' branch
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Branch = 'master',

        ## Override the local directory name. Only used if the repository name does not
        ## match the DSC module name
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $OverrideRepositoryName = $Name,

        ## Force a download, overwriting any existing resources
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')]
        [System.Version] $RequiredVersion,

        ## Catch all, for splatting parameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    begin {

        if (-not $PSBoundParameters.ContainsKey('Owner')) {
            throw ($localized.MissingParameterError -f 'Owner');
        }
        if (-not $PSBoundParameters.ContainsKey('Branch')) {
            Write-Warning -Message ($localized.NoModuleBranchSpecified -f $Name);
        }

        ## Remove -RemainingArguments to stop it being passed on.
        [ref] $null = $PSBoundParameters.Remove('RemainingArguments');
        ## Add Repository and Branch as they might not have been explicitly passed.
        $PSBoundParameters['Repository'] = $Repository;
        $PSBoundParameters['Branch'] = $Branch;

        $Branch = $Branch.Replace('/', '_') # Fix branch names with slashes (#361)
    }
    process {

        ## GitHub modules are suffixed with '_Owner_Branch.zip'
        $destinationModuleName = '{0}_{1}_{2}.zip' -f $Name, $Owner, $Branch;
        $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName;
        $setResourceDownloadParams = @{
            DestinationPath = $moduleCacheDestinationPath;
            Uri             = Resolve-GitHubModuleUri @PSBoundParameters;
            NoCheckSum      = $true;
        }
        $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams;

        $renameLabModuleCacheVersionParams = @{
            Name   = $Name;
            Path   = $moduleDestinationPath;
            Owner  = $Owner;
            Branch = $Branch
        }
        if ($PSBoundParameters.ContainsKey('RequiredVersion')) {

            $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion
        }
        elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) {

            $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion
        }
        return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams);

    } #end process
} #end function

function Invoke-LabModuleDownloadFromPSGallery {
    <#
    .SYNOPSIS
        Downloads a PowerShell module/DSC resource from the PowerShell gallery to the host's module cache.
#>

    [CmdletBinding(DefaultParameterSetName = 'Latest')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')]
        [System.Version] $RequiredVersion,

        ## Catch all, for splatting parameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $destinationModuleName = '{0}.zip' -f $Name;
        $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName;
        $setResourceDownloadParams = @{
            DestinationPath = $moduleCacheDestinationPath;
            Uri             = Resolve-PSGalleryModuleUri @PSBoundParameters;
            NoCheckSum      = $true;
        }
        $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams;

        $renameLabModuleCacheVersionParams = @{
            Name = $Name;
            Path = $moduleDestinationPath;
        }
        if ($PSBoundParameters.ContainsKey('RequiredVersion')) {

            $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion
        }
        elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) {

            $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion
        }
        return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams);

    } #end process
} #end function

function Invoke-ResourceDownload {
<#
    .SYNOPSIS
        Downloads a web resource if it has not already been downloaded or the checksum is incorrect.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Checksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt32] $BufferSize = 64KB,

        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
        ##TODO: Support Headers and UserAgent
    )
    process {

        [ref] $null = $PSBoundParameters.Remove('Force');
        if (-not (Test-ResourceDownload @PSBoundParameters) -or $Force) {

            Set-ResourceDownload @PSBoundParameters -Verbose:$false;
            [ref] $null = Test-ResourceDownload @PSBoundParameters -ThrowOnError;
        }
        $resource = Get-ResourceDownload @PSBoundParameters;
        return [PSCustomObject] $resource;

    } #end process
} #end function

function Invoke-WebClientDownload {
<#
    .SYNOPSIS
        Downloads a (web) resource using System.Net.WebClient.
    .NOTES
        This solves issue #19 when running downloading resources using BITS under alternative credentials.
#>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt32] $BufferSize = 64KB,

        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )
    process {

        try {

            [System.Net.WebClient] $webClient = New-Object -TypeName 'System.Net.WebClient';
            $webClient.Headers.Add('user-agent', $labDefaults.ModuleName);
            $webClient.Proxy = [System.Net.WebRequest]::GetSystemWebProxy();

            if (-not $webClient.Proxy.IsBypassed($Uri)) {

                $proxyInfo = $webClient.Proxy.GetProxy($Uri);
                Write-Verbose -Message ($localized.UsingProxyServer -f $proxyInfo.AbsoluteUri);
            }

            if ($Credential) {

                $webClient.Credentials = $Credential;
                $webClient.Proxy.Credentials = $Credential;
            }
            else {

                $webClient.UseDefaultCredentials = $true;
                $webClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
            }

            [System.IO.Stream] $inputStream = $webClient.OpenRead($Uri);
            [System.UInt64] $contentLength = $webClient.ResponseHeaders['Content-Length'];
            $path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath);
            [System.IO.Stream] $outputStream = [System.IO.File]::Create($path);
            [System.Byte[]] $buffer = New-Object -TypeName System.Byte[] -ArgumentList $BufferSize;
            [System.UInt64] $bytesRead = 0;
            [System.UInt64] $totalBytes = 0;
            $writeProgessActivity = $localized.DownloadingActivity -f $Uri;

            do {

                $iteration ++;
                $bytesRead = $inputStream.Read($buffer, 0, $buffer.Length);
                $totalBytes += $bytesRead;
                $outputStream.Write($buffer, 0, $bytesRead);
                ## Avoid divide by zero
                if ($contentLength -gt 0) {

                    if ($iteration % 30 -eq 0) {

                        [System.Byte] $percentComplete = ($totalBytes / $contentLength) * 100;
                        $writeProgressParams = @{
                            Activity = $writeProgessActivity;
                            PercentComplete = $percentComplete;
                            Status = $localized.DownloadStatus -f $totalBytes, $contentLength, $percentComplete;
                        }
                        Write-Progress @writeProgressParams;
                    }
                }
            }
            while ($bytesRead -ne 0)

            $outputStream.Close();
            return (Get-Item -Path $path);
        }
        catch {

            throw ($localized.WebResourceDownloadFailedError -f $Uri);
        }
        finally {

            if ($null -ne $writeProgressActivity) {

                Write-Progress -Activity $writeProgessActivity -Completed;
            }
            if ($null -ne $outputStream) {

                $outputStream.Close();
            }
            if ($null -ne $inputStream) {

                $inputStream.Close();
            }
            if ($null -ne $webClient) {

                $webClient.Dispose();
            }
        }

    } #end process
} #end function

function New-Directory {
<#
    .SYNOPSIS
       Creates a filesystem directory.
    .DESCRIPTION
       The New-Directory cmdlet will create the target directory if it doesn't already exist. If the target path
       already exists, the cmdlet does nothing.
#>

    [CmdletBinding(DefaultParameterSetName = 'ByString', SupportsShouldProcess)]
    [OutputType([System.IO.DirectoryInfo])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        # Target filesystem directory to create
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByDirectoryInfo')]
        [ValidateNotNullOrEmpty()]
        [System.IO.DirectoryInfo[]] $InputObject,

        # Target filesystem directory to create
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByString')]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath')]
        [System.String[]] $Path
    )
    process {

        Write-Debug -Message ("Using parameter set '{0}'." -f $PSCmdlet.ParameterSetName);
        switch ($PSCmdlet.ParameterSetName) {

            'ByString' {

                foreach ($directory in $Path) {
                    $directory = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($directory);

                    Write-Debug -Message ("Testing target directory '{0}'." -f $directory);
                    if (-not (Test-Path -Path $directory -PathType Container)) {

                        if ($PSCmdlet.ShouldProcess($directory, "Create directory")) {

                            Write-Verbose -Message ($localized.CreatingDirectory -f $directory);
                            New-Item -Path $directory -ItemType Directory;
                        }
                    }
                    else {

                        Write-Debug -Message ($localized.DirectoryExists -f $directory);
                        Get-Item -Path $directory;
                    }
                } #end foreach directory
            } #end byString

            'ByDirectoryInfo' {

                 foreach ($directoryInfo in $InputObject) {

                    Write-Debug -Message ("Testing target directory '{0}'." -f $directoryInfo.FullName);
                    if (-not ($directoryInfo.Exists)) {

                        if ($PSCmdlet.ShouldProcess($directoryInfo.FullName, "Create directory")) {

                            Write-Verbose -Message ($localized.CreatingDirectory -f $directoryInfo.FullName);
                            New-Item -Path $directoryInfo.FullName -ItemType Directory;
                        }
                    }
                    else {

                        Write-Debug -Message ($localized.DirectoryExists -f $directoryInfo.FullName);
                        Write-Output -InputObject $directoryInfo;
                    }
                } #end foreach directoryInfo
            } #end byDirectoryInfo

        } #end switch

    } #end process
} #end function

function New-DiskImage {
<#
    .SYNOPSIS
        Create a new formatted disk image.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## VHD/x file path
        [Parameter(Mandatory)]
        [System.String] $Path,

        ## Disk image partition scheme
        [Parameter(Mandatory)]
        [ValidateSet('MBR','GPT')]
        [System.String] $PartitionStyle,

        ## Disk image size in bytes
        [Parameter()]
        [System.UInt64] $Size = 127GB,

        ## Disk image size in bytes
        [Parameter()]
        [ValidateSet('Dynamic','Fixed')]
        [System.String] $Type = 'Dynamic',

        ## Overwrite/recreate existing disk image
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $Force,

        ## Do not dismount the VHD/x and return a reference
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $Passthru
    )
    begin {

        if ((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)) {

            throw ($localized.ImageAlreadyExistsError -f $Path);
        }
        elseif ((Test-Path -Path $Path -PathType Leaf) -and ($Force)) {

            Hyper-V\Dismount-VHD -Path $Path -ErrorAction Stop;
            Write-Verbose -Message ($localized.RemovingDiskImage -f $Path);
            Remove-Item -Path $Path -Force -ErrorAction Stop;
        }

    } #end begin
    process {

        $newVhdParams = @{
            Path = $Path;
            SizeBytes = $Size;
            $Type = $true;
        }

        Write-Verbose -Message ($localized.CreatingDiskImageType -f $Type.ToLower(), $Path, ($Size/1MB));
        [ref] $null = Hyper-V\New-Vhd @newVhdParams;

        ## Disable BitLocker fixed drive write protection (if enabled)
        Disable-BitLockerFDV;

        Write-Verbose -Message ($localized.MountingDiskImage -f $Path);
        $vhdMount = Hyper-V\Mount-VHD -Path $Path -Passthru;

        Write-Verbose -Message ($localized.InitializingDiskImage -f $Path);
        [ref] $null = Storage\Initialize-Disk -Number $vhdMount.DiskNumber -PartitionStyle $PartitionStyle -PassThru;

        switch ($PartitionStyle) {
            'MBR' {
                New-DiskImageMbr -Vhd $vhdMount;
            }
            'GPT' {
                New-DiskImageGpt -Vhd $vhdMount;
            }
        }

        if ($Passthru) {

            return $vhdMount;
        }
        else {

            Hyper-V\Dismount-VHD -Path $Path;
        }

        ## Enable BitLocker (if required)
        Assert-BitLockerFDV;

    } #end process
} #end function

function New-DiskImageGpt {
<#
    .SYNOPSIS
        Create a new GPT-formatted disk image.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk
    )
    process {

        ## Temporarily disable Windows Explorer popup disk initialization and format notifications
        ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx
        Stop-ShellHWDetectionService;

        Write-Verbose -Message ($localized.CreatingDiskPartition -f 'EFI');
        $efiPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -Size 250MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter;
        Write-Verbose -Message ($localized.FormattingDiskPartition -f 'EFI');
        New-DiskPartFat32Partition -DiskNumber $Vhd.DiskNumber -PartitionNumber $efiPartition.PartitionNumber;

        Write-Verbose -Message ($localized.CreatingDiskPartition -f 'MSR');
        [ref] $null = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -Size 128MB -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}';

        Write-Verbose -Message ($localized.CreatingDiskPartition -f 'Windows');
        $osPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -AssignDriveLetter;
        Write-Verbose -Message ($localized.FormattingDiskPartition -f 'Windows');
        [ref] $null = Storage\Format-Volume -Partition $osPartition -FileSystem NTFS -Force -Confirm:$false;

        Start-ShellHWDetectionService;

    } #end process
} #end function

function New-DiskImageMbr {
<#
    .SYNOPSIS
        Create a new MBR-formatted disk image.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk
    )
    process {

        ## Temporarily disable Windows Explorer popup disk initialization and format notifications
        ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx

        Stop-ShellHWDetectionService;

        Write-Verbose -Message ($localized.CreatingDiskPartition -f 'Windows');
        $osPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -UseMaximumSize -MbrType IFS -IsActive |
            Storage\Add-PartitionAccessPath -AssignDriveLetter -PassThru |
                Storage\Get-Partition;
        Write-Verbose -Message ($localized.FormattingDiskPartition -f 'Windows');
        [ref] $null = Storage\Format-Volume -Partition $osPartition -FileSystem NTFS -Force -Confirm:$false;

        Start-ShellHWDetectionService;

    } #end proces
} #end function

function New-DiskPartFat32Partition {
<#
    .SYNOPSIS
        Uses DISKPART.EXE to create a new FAT32 system partition. This permits mocking of DISKPART calls.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        [Parameter(Mandatory)]
        [System.Int32] $DiskNumber,

        [Parameter(Mandatory)]
        [System.Int32] $PartitionNumber
    )
    process {

        @"
select disk $DiskNumber
select partition $PartitionNumber
format fs=fat32 label="System"
"@
 | & "$env:SystemRoot\System32\DiskPart.exe" | Out-Null;

    } #end process
} #end function

function New-EmptyDiskImage {
<#
    .SYNOPSIS
        Create an empty disk image.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## VHD/x file path
        [Parameter(Mandatory)]
        [System.String] $Path,

        ## Disk image size in bytes
        [Parameter()]
        [System.UInt64] $Size = 127GB,

        ## Disk image size in bytes
        [Parameter()]
        [ValidateSet('Dynamic','Fixed')]
        [System.String] $Type = 'Dynamic',

        ## Overwrite/recreate existing disk image
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        if ((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)) {

            throw ($localized.ImageAlreadyExistsError -f $Path);
        }
        elseif ((Test-Path -Path $Path -PathType Leaf) -and ($Force)) {

            Hyper-V\Dismount-VHD -Path $Path -ErrorAction Stop;
            Write-Verbose -Message ($localized.RemovingDiskImage -f $Path);
            Remove-Item -Path $Path -Force -ErrorAction Stop;
        }

    } #end begin
    process {

        $newVhdParams = @{
            Path = $Path;
            SizeBytes = $Size;
            $Type = $true;
        }

        Write-Verbose -Message ($localized.CreatingDiskImageType -f $Type.ToLower(), $Path, ($Size/1MB));
        [ref] $null = Hyper-V\New-Vhd @newVhdParams;

    } #end process
} #end function

function New-LabBootStrap {
<#
    .SYNOPSIS
        Creates a lab DSC BootStrap script block.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.String])]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CoreCLR,

        ## Custom default shell
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $DefaultShell,

        ## WSMan maximum envelope size
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $MaxEnvelopeSizeKb = 1024
    )
    process {

        $coreCLRScriptBlock = {
## Lability CoreCLR DSC Bootstrap
$VerbosePreference = 'Continue';

Import-Certificate -FilePath "$env:SYSTEMDRIVE\BootStrap\LabRoot.cer" -CertStoreLocation 'Cert:\LocalMachine\Root\' -Verbose;
## Import the .PFX certificate with a blank password
Import-PfxCertificate -FilePath "$env:SYSTEMDRIVE\BootStrap\LabClient.pfx" -CertStoreLocation 'Cert:\LocalMachine\My\' -Verbose;

<#CustomBootStrapInjectionPoint#>

if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.meta.mof") {
    Set-DscLocalConfigurationManager -Path "$env:SystemDrive\BootStrap\" -Verbose;
}

if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.mof") {
    Start-DscConfiguration -Path "$env:SystemDrive\Bootstrap\" -Force -Wait -Verbose -ErrorAction Stop;
} #end if localhost.mof

} #end CoreCLR bootstrap scriptblock

        $scriptBlock = {
## Lability DSC Bootstrap
$VerbosePreference = 'Continue';
$DebugPreference = 'Continue';
$transcriptPath = '{0}\BootStrap\Bootstrap-{1}.log' -f $env:SystemDrive, (Get-Date).ToString('yyyyMMdd-hhmmss');
Start-Transcript -Path $transcriptPath -Force;

certutil.exe -addstore -f "Root" "$env:SYSTEMDRIVE\BootStrap\LabRoot.cer";
## Import the .PFX certificate with a blank password
"" | certutil.exe -f -importpfx "$env:SYSTEMDRIVE\BootStrap\LabClient.pfx";

<#CustomBootStrapInjectionPoint#>

## Account for large configurations being "pushed" and increase the default from 500KB to <#MaxEnvelopeSizeKb#>KB (#306)
Set-ItemProperty -LiteralPath HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client -Name maxEnvelopeSize -Value <#MaxEnvelopeSizeKb#> -Force -Verbose

## Disable network location wizard pop-up
$null = New-Item -Path HKLM:\SYSTEM\CurrentControlSet\Control\Network -Name NewNetworkWindowOff -ItemType Container -Force -Verbose

if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.meta.mof") {
    Set-DscLocalConfigurationManager -Path "$env:SystemDrive\BootStrap\" -Verbose;
}

$localhostMofPath = "$env:SystemDrive\BootStrap\localhost.mof";
if (Test-Path -Path $localhostMofPath) {

    if ($PSVersionTable.PSVersion.Major -eq 4) {

        ## Convert the .mof to v4 compatible - credit to Mike Robbins
        ## http://mikefrobbins.com/2014/10/30/powershell-desired-state-configuration-error-undefined-property-configurationname/
        $mof = Get-Content -Path $localhostMofPath;
        $mof -replace '^\sName=.*;$|^\sConfigurationName\s=.*;$' | Set-Content -Path $localhostMofPath -Encoding Unicode -Force;
    }
    while ($true) {
        ## Replay the configuration until the LCM bloody-well takes it (more of a WMF 4 thing)!
        try {

            if (Test-Path -Path "$env:SystemRoot\System32\Configuration\Pending.mof") {

                Start-DscConfiguration -UseExisting -Force -Wait -Verbose -ErrorAction Stop;
                break;
            }
            else {

                Start-DscConfiguration -Path "$env:SystemDrive\Bootstrap\" -Force -Wait -Verbose -ErrorAction Stop;
                break;
            }
        }
        catch {

            Write-Error -Message $_;
            ## SIGH. Try restarting WMI..
            if (-not ($interation % 10)) {

                ## SIGH. Try removing the configuration and restarting WMI..
                Remove-DscConfigurationDocument -Stage Current,Pending,Previous -Force;
                Restart-Service -Name Winmgmt -Force;
            }
            Start-Sleep -Seconds 5;
            $interation++;
        }
    } #end while
} #end if localhost.mof

Stop-Transcript;
} #end bootstrap scriptblock

        if ($CoreCLR) {

            $bootstrap = $coreCLRScriptBlock.ToString();
        }
        else {

            $bootstrap = $scriptBlock.ToString();
        }

        if ($PSBoundParameters.ContainsKey('DefaultShell')) {

            $shellScriptBlock = {
                Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\' -Name Shell -Value '{0}' -Force;

                <#CustomBootStrapInjectionPoint#>
            }

            $shellScriptBlockString = $shellScriptBlock.ToString() -f $DefaultShell;
            $bootstrap = $bootStrap.Replac('<#CustomBootStrapInjectionPoint#>', $shellScriptBlockString);
        }

        $bootstrap = $bootstrap -replace  '<#MaxEnvelopeSizeKb#>', $MaxEnvelopeSizeKb;
        return $bootstrap;

    } #end process
} #end function

function New-LabMedia {
<#
    .SYNOPSIS
        Creates a new lab media object.
    .DESCRIPTION
        Permits validation of custom NonNodeData\Lability\Media entries.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = $(throw ($localized.MissingParameterError -f 'Id')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Filename = $(throw ($localized.MissingParameterError -f 'Filename')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Description = '',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('x86','x64')]
        [System.String] $Architecture = $(throw ($localized.MissingParameterError -f 'Architecture')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ImageName = '',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('ISO','VHD','WIM','NULL')]
        [System.String] $MediaType = $(throw ($localized.MissingParameterError -f 'MediaType')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Uri = $(throw ($localized.MissingParameterError -f 'Uri')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Checksum = '',

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ProductKey = '',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Windows','Linux')]
        [System.String] $OperatingSystem = 'Windows',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Collections.Hashtable] $CustomData = @{},

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Array] $Hotfixes
    )
    begin {

        ## Confirm we have a valid Uri
        try {

            if ($MediaType -ne 'NULL') {

                $resolvedUri = New-Object -TypeName 'System.Uri' -ArgumentList $Uri;
                if ($resolvedUri.Scheme -notin 'http','https','file') {
                    throw ($localized.UnsupportedUriSchemeError -f $resolvedUri.Scheme);
                }
            }
        }
        catch {

            throw $_;
        }

    }
    process {

        $labMedia = [PSCustomObject] @{
            Id = $Id;
            Filename = $Filename;
            Description = $Description;
            Architecture = $Architecture;
            ImageName = $ImageName;
            MediaType = $MediaType;
            OperatingSystem = $OperatingSystem;
            Uri = [System.Uri] $Uri;
            Checksum = $Checksum;
            CustomData = $CustomData;
            Hotfixes = $Hotfixes;
        }

        ## Ensure any explicit product key overrides the CustomData value
        if ($ProductKey) {

            $CustomData['ProductKey'] = $ProductKey;
        }
        return $labMedia;

    } #end process
} #end function

function New-LabSwitch {
<#
    .SYNOPSIS
        Creates a new Lability network switch object.
    .DESCRIPTION
        Permits validation of custom NonNodeData\Lability\Network entries.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Virtual switch name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Virtual switch type
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('Internal','External','Private')]
        [System.String] $Type,

        ## Physical network adapter name (for external switches)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $NetAdapterName,

        ## Share host access (for external virtual switches)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Boolean] $AllowManagementOS = $false,

        ## Virtual switch availability
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Present','Absent')]
        [System.String] $Ensure = 'Present'
    )
    begin {

        if (($Type -eq 'External') -and (-not $NetAdapterName)) {

            throw ($localized.MissingParameterError -f 'NetAdapterName');
        }

    } #end begin
    process {

        $newLabSwitch = @{
            Name = $Name;
            Type = $Type;
            NetAdapterName = $NetAdapterName;
            AllowManagementOS = $AllowManagementOS;
            Ensure = $Ensure;
        }
        if ($Type -ne 'External') {

            [ref] $null = $newLabSwitch.Remove('NetAdapterName');
            [ref] $null = $newLabSwitch.Remove('AllowManagementOS');
        }
        return $newLabSwitch;

    } #end process
} #end function

function New-LabVirtualMachine {
<#
    .SYNOPSIS
        Creates and configures a new lab virtual machine.
    .DESCRIPTION
        Creates an new VM, creating the switch if required, injecting all
        resources and snapshotting as required.
#>

    [CmdletBinding(DefaultParameterSetName = 'PSCredential')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Specifies the lab virtual machine/node name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Local administrator password of the VM. The username is NOT used.
        [Parameter(ParameterSetName = 'PSCredential', ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential = (& $credentialCheckScriptBlock),

        ## Local administrator password of the VM.
        [Parameter(Mandatory, ParameterSetName = 'Password', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.Security.SecureString] $Password,

        ## Virtual machine DSC .mof and .meta.mof location
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Path = (Get-LabHostDscConfigurationPath),

        ## Skip creating baseline snapshots
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoSnapshot,

        ## Is a quick VM, e.g. created via the New-LabVM cmdlet
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $IsQuickVM,

        ## Credentials to access the a private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
    )
    begin {

        ## If we have only a secure string, create a PSCredential
        if ($PSCmdlet.ParameterSetName -eq 'Password') {
            $Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList 'LocalAdministrator', $Password;
        }
        if (-not $Credential) {throw ($localized.CannotProcessCommandError -f 'Credential'); }
        elseif ($Credential.Password.Length -eq 0) { throw ($localized.CannotBindArgumentError -f 'Password'); }

    }
    process {

        $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -ErrorAction Stop;
        $nodeName = $node.NodeName;
        ## Display name includes any environment prefix/suffix
        $displayName = $node.NodeDisplayName;

        if (-not (Test-ComputerName -ComputerName $node.NodeName.Split('.')[0])) {

            throw ($localized.InvalidComputerNameError -f $node.NodeName);
        }

        ## Don't attempt to check certificates for 'Quick VMs'
        if (-not $IsQuickVM) {

            ## Check for certificate before we (re)create the VM
            if (-not [System.String]::IsNullOrWhitespace($node.ClientCertificatePath)) {

                $expandedClientCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.ClientCertificatePath);
                if (-not (Test-Path -Path $expandedClientCertificatePath -PathType Leaf)) {

                    throw ($localized.CannotFindCertificateError -f 'Client', $node.ClientCertificatePath);
                }
            }
            else {

                Write-Warning -Message ($localized.NoCertificateFoundWarning -f 'Client');
            }

            if (-not [System.String]::IsNullOrWhitespace($node.RootCertificatePath)) {

                $expandedRootCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.RootCertificatePath);
                if (-not (Test-Path -Path $expandedRootCertificatePath -PathType Leaf)) {

                    throw ($localized.CannotFindCertificateError -f 'Root', $node.RootCertificatePath);
                }
            }
            else {

                Write-Warning -Message ($localized.NoCertificateFoundWarning -f 'Root');
            }

        } #end if not quick VM

        $environmentSwitchNames = @();
        foreach ($switchName in $node.SwitchName) {

            ## Retrieve prefixed switch names for VM creation (if necessary)
            $resolveLabSwitchParams = @{
                Name = $switchName;
                ConfigurationData = $ConfigurationData;
                WarningAction = 'SilentlyContinue';
            }
            $networkSwitch = Resolve-LabSwitch @resolveLabSwitchParams;

            Write-Verbose -Message ($localized.SettingVMConfiguration -f 'Virtual Switch', $networkSwitch.Name);
            $environmentSwitchNames += $networkSwitch.Name;

            ## Set-LabSwitch also resolves/prefixes the switch name, so pass the naked name (#251)
            Set-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData;
        }

        if (-not (Test-LabImage -Id $node.Media -ConfigurationData $ConfigurationData)) {

            [ref] $null = New-LabImage -Id $node.Media -ConfigurationData $ConfigurationData;
        }

        Write-Verbose -Message ($localized.ResettingVMConfiguration -f 'VHDX', "$displayName.vhdx");
        $resetLabVMDiskParams = @{
            Name = $displayName;
            NodeName = $nodeName;
            Media = $node.Media;
            ConfigurationData = $ConfigurationData;
        }
        Reset-LabVMDisk @resetLabVMDiskParams -ErrorAction Stop;

        Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM', $displayName);
        $setLabVirtualMachineParams = @{
            Name = $DisplayName;
            SwitchName = $environmentSwitchNames;
            Media = $node.Media;
            StartupMemory = $node.StartupMemory;
            MinimumMemory = $node.MinimumMemory;
            MaximumMemory = $node.MaximumMemory;
            ProcessorCount = $node.ProcessorCount;
            MACAddress = $node.MACAddress;
            SecureBoot = $node.SecureBoot;
            GuestIntegrationServices = $node.GuestIntegrationServices;
            AutomaticCheckPoints = $node.AutomaticCheckpoints;
            ConfigurationData = $ConfigurationData;
        }

        ## Add VMProcessor, Dvd Drive and additional HDD options
        foreach ($additionalProperty in 'DvdDrive','ProcessorOption','HardDiskDrive') {

            if ($node.ContainsKey($additionalProperty)) {

                $setLabVirtualMachineParams[$additionalProperty] = $node[$additionalProperty];
            }
        }

        Set-LabVirtualMachine @setLabVirtualMachineParams;

        $media = Resolve-LabMedia -Id $node.Media -ConfigurationData $ConfigurationData;
        if (($media.OperatingSystem -eq 'Linux') -or
            ($media.MediaType -eq 'NULL')) {
            ## Skip injecting files for Linux VMs..
        }
        else {

            Write-Verbose -Message ($localized.AddingVMCustomization -f 'VM');
            $setLabVMDiskFileParams = @{
                NodeName = $nodeName;
                ConfigurationData = $ConfigurationData;
                Path = $Path;
                Credential = $Credential;
                CoreCLR = $media.CustomData.SetupComplete -eq 'CoreCLR';
                MaxEnvelopeSizeKb = $node.MaxEnvelopeSizeKb;
            }
            if (-not [System.String]::IsNullOrEmpty($media.CustomData.DefaultShell)) {

                $setLabVMDiskFileParams['DefaultShell'] = $media.CustomData.DefaultShell;
            }

            $resolveCustomBootStrapParams = @{
                CustomBootstrapOrder = $node.CustomBootstrapOrder;
                ConfigurationCustomBootstrap = $node.CustomBootstrap;
                MediaCustomBootStrap = $media.CustomData.CustomBootstrap;
            }

            $customBootstrap = Resolve-LabCustomBootStrap @resolveCustomBootStrapParams;
            if ($customBootstrap) {

                $setLabVMDiskFileParams['CustomBootstrap'] = $customBootstrap;
            }

            if (-not [System.String]::IsNullOrEmpty($media.CustomData.ProductKey)) {

                $setLabVMDiskFileParams['ProductKey'] = $media.CustomData.ProductKey;
            }
            Set-LabVMDiskFile @setLabVMDiskFileParams -FeedCredential $feedCredential;

        } #end Windows VMs

        if (-not $NoSnapshot) {

            $snapshotName = $localized.BaselineSnapshotName -f $labDefaults.ModuleName;
            Write-Verbose -Message ($localized.CreatingBaselineSnapshot -f $snapshotName);
            Hyper-V\Checkpoint-VM -Name $displayName -SnapshotName $snapshotName -Confirm:$false;
        }

        if ($node.WarningMessage) {

            if ($node.WarningMessage -is [System.String]) {

                Write-Warning -Message ($localized.NodeCustomMessageWarning -f $nodeName, $node.WarningMessage.Trim("`n"));
            }
            else {

                Write-Warning -Message ($localized.IncorrectPropertyTypeError -f 'WarningMessage', '[System.String]')
            }
        }

        Write-Output -InputObject (Hyper-V\Get-VM -Name $displayName);

    } #end process
} #end function

function New-LabVMSnapshot {
<#
    .SYNOPSIS
        Creates a snapshot of all virtual machines with the specified snapshot name.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String[]] $Name,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $SnapshotName
    )
    process {
        foreach ($vmName in $Name) {

            Write-Verbose -Message ($localized.CreatingVirtualMachineSnapshot -f $vmName, $SnapshotName);
            Hyper-V\Checkpoint-VM -VMName $vmName -SnapshotName $SnapshotName;
        } #end foreach VM

    } #end process
} #end function

function New-UnattendXml {
<#
    .SYNOPSIS
       Creates a Windows unattended installation file.
    .DESCRIPTION
       Creates an unattended Windows 8/2012 installation file that will configure
       an operating system deployed from a WIM file, deploy the operating system
       and ensure that Powershell's desired state configuration (DSC) is configured
       to pull its configuration from the specified pull server.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Xml.XmlDocument])]
    param (
        # Local Administrator Password
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        # Computer name
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ComputerName,

        # Product Key
        [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}$')]
        [System.String] $ProductKey,

        # Input Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $InputLocale = 'en-US',

        # System Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $SystemLocale = 'en-US',

        # User Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $UserLocale = 'en-US',

        # UI Language
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $UILanguage = 'en-US',

        # Timezone
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Timezone, ##TODO: Validate timezones?

        # Registered Owner
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $RegisteredOwner = 'Virtual Engine',

        # Registered Organization
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $RegisteredOrganization = 'Virtual Engine',

        # TODO: Execute synchronous commands during OOBE pass as they only currently run during the Specialize pass
        ## Array of hashtables with Description, Order and Path keys
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable[]] $ExecuteCommand
    )
    begin {
        $templateUnattendXml = [System.Xml.XmlDocument] @'
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component>
        <component name="Microsoft-Windows-Deployment" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
                <SkipUserOOBE>true</SkipUserOOBE>
                <SkipMachineOOBE>true</SkipMachineOOBE>
            </OOBE>
            <ShowWindowsLive>false</ShowWindowsLive>
            <TimeZone>GMT Standard Time</TimeZone>
            <UserAccounts>
                <AdministratorPassword>
                    <Value></Value>
                    <PlainText>false</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <RegisteredOrganization>Virtual Engine</RegisteredOrganization>
            <RegisteredOwner>Virtual Engine</RegisteredOwner>
            <BluetoothTaskbarIconEnabled>true</BluetoothTaskbarIconEnabled>
            <DoNotCleanTaskBar>false</DoNotCleanTaskBar>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
                <SkipUserOOBE>true</SkipUserOOBE>
                <SkipMachineOOBE>true</SkipMachineOOBE>
            </OOBE>
            <ShowWindowsLive>false</ShowWindowsLive>
            <TimeZone>GMT Standard Time</TimeZone>
            <UserAccounts>
                <AdministratorPassword>
                    <Value></Value>
                    <PlainText>false</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <RegisteredOrganization>Virtual Engine</RegisteredOrganization>
            <RegisteredOwner>Virtual Engine</RegisteredOwner>
            <BluetoothTaskbarIconEnabled>true</BluetoothTaskbarIconEnabled>
            <DoNotCleanTaskBar>false</DoNotCleanTaskBar>
        </component>
    </settings>
</unattend>
'@

        [xml] $unattendXml = $templateUnattendXml;
    }
    process {

        foreach ($setting in $unattendXml.Unattend.Settings) {

            foreach($component in $setting.Component) {

                if ($setting.'Pass' -eq 'specialize' -and $component.'Name' -eq 'Microsoft-Windows-Deployment') {

                    if (($null -ne $ExecuteCommand) -or ($ExecuteCommand.Length -gt 0)) {

                        $commandOrder = 1;
                        foreach ($synchronousCommand in $ExecuteCommand) {

                            $runSynchronousElement = $component.AppendChild($unattendXml.CreateElement('RunSynchronous','urn:schemas-microsoft-com:unattend'));
                            $syncCommandElement = $runSynchronousElement.AppendChild($unattendXml.CreateElement('RunSynchronousCommand','urn:schemas-microsoft-com:unattend'));
                            [ref] $null = $syncCommandElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add');
                            $syncCommandDescriptionElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Description','urn:schemas-microsoft-com:unattend'));
                            [ref] $null = $syncCommandDescriptionElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Description']));
                            $syncCommandOrderElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Order','urn:schemas-microsoft-com:unattend'));
                            [ref] $null = $syncCommandOrderElement.AppendChild($unattendXml.CreateTextNode($commandOrder));
                            $syncCommandPathElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Path','urn:schemas-microsoft-com:unattend'));
                            [ref] $null = $syncCommandPathElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Path']));
                            $commandOrder++;
                        }
                    }
                }

                if (($setting.'Pass' -eq 'specialize') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) {

                    if ($ComputerName) {

                        $computerNameElement = $component.AppendChild($unattendXml.CreateElement('ComputerName','urn:schemas-microsoft-com:unattend'));
                        [ref] $null = $computerNameElement.AppendChild($unattendXml.CreateTextNode($ComputerName));
                    }
                    if ($ProductKey) {

                        $productKeyElement = $component.AppendChild($unattendXml.CreateElement('ProductKey','urn:schemas-microsoft-com:unattend'));
                        [ref] $null = $productKeyElement.AppendChild($unattendXml.CreateTextNode($ProductKey.ToUpper()));
                    }
                }

                if ((($setting.'Pass' -eq 'specialize') -or ($setting.'Pass' -eq 'oobeSystem')) -and ($component.'Name' -eq 'Microsoft-Windows-International-Core')) {

                    $component.InputLocale = $InputLocale;
                    $component.SystemLocale = $SystemLocale;
                    $component.UILanguage = $UILanguage;
                    $component.UserLocale = $UserLocale;
                }

                if (($setting.'Pass' -eq 'oobeSystem') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) {

                    $component.TimeZone = $Timezone;
                    $concatenatedPassword = '{0}AdministratorPassword' -f $Credential.GetNetworkCredential().Password;
                    $component.UserAccounts.AdministratorPassword.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($concatenatedPassword));
                    $component.RegisteredOrganization = $RegisteredOrganization;
                    $component.RegisteredOwner = $RegisteredOwner;
                }

            } #end foreach setting.Component

        } #end foreach unattendXml.Unattend.Settings

        Write-Output -InputObject $unattendXml;

    } #end process
} #end function

function Remove-ConfigurationData {
<#
    .SYNOPSIS
        Removes custom lab configuration data file.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('Host','VM','Media','CustomMedia')]
        [System.String] $Configuration
    )
    process {

        $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration;
        if (Test-Path -Path $configurationPath) {
            Write-Verbose -Message ($localized.ResettingConfigurationDefaults -f $Configuration);
            Remove-Item -Path $configurationPath -Force;
        }

    } #end process
} # end function Remove-ConfigurationData

function Remove-LabSwitch {
<#
    .SYNOPSIS
        Removes a virtual network switch configuration.
    .DESCRIPTION
        Deletes a virtual network switch configuration using the xVMSwitch DSC resource.
#>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        ## Switch Id/Name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $networkSwitch = Resolve-LabSwitch @PSBoundParameters;
        if (($null -ne $networkSwitch.IsExisting) -and ($networkSwitch.IsExisting -eq $true)) {

            if ($PSCmdlet.ShouldProcess($Name)) {

                $networkSwitch['Ensure'] = 'Absent';
                [ref] $null = $networkSwitch.Remove('IsExisting');
                Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch;
                [ref] $null = Invoke-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch;
            }
        }

    } #end process
} #end function

function Remove-LabVirtualMachine {
    <#
        .SYNOPSIS
            Deletes a lab virtual machine.
    #>

        [CmdletBinding(SupportsShouldProcess)]
        param (
            ## Specifies the lab virtual machine/node name.
            [Parameter(Mandatory, ValueFromPipeline)]
            [ValidateNotNullOrEmpty()]
            [System.String] $Name,

            ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
            [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
            [System.Collections.Hashtable]
            [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
            $ConfigurationData,

            ## Include removal of virtual switch(es). By default virtual switches are not removed.
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.Management.Automation.SwitchParameter] $RemoveSwitch
        )
        process {

            if (-not (Test-LabNode -Name $Name -ConfigurationData $ConfigurationData)) {

                throw ($localized.CannotLocateNodeError -f $Name);
            }
            $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData;
            $nodeDisplayName = $node.NodeDisplayName;

            # Revert to oldest snapshot prior to VM removal to speed things up
            Hyper-V\Get-VMSnapshot -VMName $nodeDisplayName -ErrorAction SilentlyContinue |
                Sort-Object -Property CreationTime |
                    Select-Object -First 1 |
                        Hyper-V\Restore-VMSnapshot -Confirm:$false;

            Remove-LabVMSnapshot -Name $nodeDisplayName;

            $environmentSwitchNames = @();
            foreach ($switchName in $node.SwitchName) {

                $environmentSwitchNames += Resolve-LabEnvironmentName -Name $switchName -ConfigurationData $ConfigurationData;
            }

            Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'VM', $nodeDisplayName);
            $clearLabVirtualMachineParams = @{
                Name = $nodeDisplayName;
                SwitchName = $environmentSwitchNames;
                Media = $node.Media;
                StartupMemory = $node.StartupMemory;
                MinimumMemory = $node.MinimumMemory;
                MaximumMemory = $node.MaximumMemory;
                MACAddress = $node.MACAddress;
                ProcessorCount = $node.ProcessorCount;
                ConfigurationData = $ConfigurationData;
            }
            Clear-LabVirtualMachine @clearLabVirtualMachineParams;

            ## Remove the OS disk
            Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'VHD/X', "$($nodeDisplayName).vhd/vhdx");
            $removeLabVMDiskParams = @{
                Name = $nodeDisplayName;
                NodeName = $Name;
                Media = $node.Media;
                ConfigurationData = $ConfigurationData;
            }
            Remove-LabVMDisk @removeLabVMDiskParams -ErrorAction Stop;

            if ($RemoveSwitch) {

                foreach ($switchName in $node.SwitchName) {

                    $environmentSwitchName = Resolve-LabEnvironmentName -Name $switchName -ConfigurationData $ConfigurationData;
                    Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'Virtual Switch', $environmentSwitchName);
                    Remove-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData;
                }
            }

        } #end process
    } #end function

function Remove-LabVirtualMachineHardDiskDrive {
<#
    .SYNOPSIS
        Removes a virtual machine's additional hard disk drive(s).
    .DESCRIPTION
        Removes one or more additional hard disks. No need to detach the disks as the VM is deleted.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $NodeName,

        ## Collection of additional hard disk drive configurations
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable[]]
        $HardDiskDrive,

        ## Configuration environment name
        [Parameter()]
        [AllowNull()]
        [System.String] $EnvironmentName
    )
    process {

        $vmHardDiskPath = Resolve-LabVMDiskPath -Name $NodeName -EnvironmentName $EnvironmentName -Parent;

        for ($i = 0; $i -lt $HardDiskDrive.Count; $i++) {

            $diskDrive = $HardDiskDrive[$i];
            $controllerLocation = $i + 1;

            if ($diskDrive.ContainsKey('VhdPath')) {

                ## Do not remove VHD/Xs created externally!
            }
            else {

                ## Remove the VHD file
                $vhdName = '{0}-{1}' -f $NodeName, $controllerLocation;
                $vhdParams = @{
                    Name = $vhdName;
                    Path = $vmHardDiskPath;
                    MaximumSizeBytes = $diskDrive.MaximumSizeBytes;
                    Generation = $diskDrive.Generation;
                    Ensure = 'Absent';
                }

                $vhdFilename = '{0}.{1}' -f $vhdName, $diskDrive.Generation.ToLower();
                $vhdPath = Join-Path -Path $vmHardDiskPath -ChildPath $vhdFilename;
                Write-Verbose -Message ($localized.RemovingVhdFile -f $vhdPath);
                Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVhd -Prefix Vhd;
                Invoke-LabDscResource -ResourceName Vhd -Parameters $vhdParams;

            }

        } #end for

    } #end process
} #end function

function Remove-LabVMDisk {
<#
    .SYNOPSIS
        Removes lab VM disk file (VHDX) configuration.
    .DESCRIPTION
        Configures a VM disk configuration using the xVHD DSC resource.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        ## VM/node display name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## VM/node name
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $NodeName = $Name,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData -ErrorAction Stop;
        }
        else {

            $image = Get-LabImage -Id $Media -ErrorAction Stop;
        }

        $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;

        ## If the parent image isn't there, the differencing VHD won't be either!
        if ($image) {

            ## Ensure we look for the correct file extension (#182)
            $vhdPath = Resolve-LabVMDiskPath -Name $Name -Generation $image.Generation -EnvironmentName $environmentName;

            if (Test-Path -Path $vhdPath) {
                ## Only attempt to remove the differencing disk if it's there (and xVHD will throw)
                $vhd = @{
                    Name = $Name;
                    Path = Split-Path -Path $vhdPath -Parent;
                    ParentPath = $image.ImagePath;
                    Generation = $image.Generation;
                    Type = 'Differencing';
                    Ensure = 'Absent';
                }
                Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD;
                [ref] $null = Invoke-LabDscResource -ResourceName VHD -Parameters $vhd;
            }
        }

        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $resolveNodePropertyValueParams = @{
                NodeName = $NodeName;
                ConfigurationData = $ConfigurationData;
                NoEnumerateWildcardNode = $true;
                ErrorAction = 'Stop';
            }
            $node = Resolve-NodePropertyValue @resolveNodePropertyValueParams;
            if ($node.ContainsKey('HardDiskDrive')) {

                ## Remove additional HDDs
                $removeLabVirtualMachineHardDiskDriveParams = @{
                    NodeName = $node.NodeDisplayName;
                    HardDiskDrive = $node.HardDiskDrive;
                    EnvironmentName = $environmentName;
                }
                $null = Remove-LabVirtualMachineHardDiskDrive @removeLabVirtualMachineHardDiskDriveParams;
            }
        }

    } #end process
} #end function

function Remove-LabVMSnapshot {
<#
    .SYNOPSIS
        Removes a VM snapshot.
#>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String[]] $Name,

        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $SnapshotName = '*'
    )
    process {
       <## TODO: Add the ability to force/wait for the snapshots to be removed. When removing snapshots it take a minute
                 or two before the files are actually removed. This causes issues when performing a lab reset #>

        foreach ($vmName in $Name) {

            # Sort by descending CreationTime to ensure we will not have to commit changes from one snapshot to another
            Hyper-V\Get-VMSnapshot -VMName $vmName -ErrorAction SilentlyContinue |
                Where-Object Name -like $SnapshotName |
                    Sort-Object -Property CreationTime -Descending |
                        ForEach-Object {
                            Write-Verbose -Message ($localized.RemovingSnapshot -f $vmName, $_.Name);
                            Hyper-V\Remove-VMSnapshot -VMName $_.VMName -Name $_.Name -Confirm:$false;
                        }

        } #end foreach VM

    } #end process
} #end function

function Rename-LabModuleCacheVersion {
<#
    .SYNOPSIS
        Renames a cached module zip file with its version number.
#>

    [CmdletBinding(DefaultParameterSetName = 'PSGallery')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## Destination directory path to download the PowerShell module/DSC resource module to
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Path,

        ## GitHub module repository owner
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'GitHub')]
        [System.String] $Owner,

        ## GitHub module branch
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'GitHub')]
        [System.String] $Branch,

        ## The minimum version of the module required
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Version] $RequiredVersion
    )
    process {

        if ($PSCmdlet.ParameterSetName -eq 'GitHub') {
            $moduleManifest = Get-LabModuleCacheManifest -Path $Path -Provider 'GitHub';
            $moduleVersion = $moduleManifest.ModuleVersion;
            $versionedModuleFilename = '{0}-v{1}_{2}_{3}.zip' -f $Name, $moduleVersion, $Owner, $Branch;
        }
        else {
            $moduleManifest = Get-LabModuleCacheManifest -Path $Path;
            $moduleVersion = $moduleManifest.ModuleVersion;
            $versionedModuleFilename = '{0}-v{1}.zip' -f $Name, $moduleVersion;
        }

        if ($PSBoundParameters.ContainsKey('RequiredVersion')) {

            if ($moduleVersion -ne $RequiredVersion) {
                throw ($localized.ModuleVersionMismatchError -f $Name, $moduleVersion, $RequiredVersion);
            }
        }
        elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) {

            if ($moduleVersion -lt $MinimumVersion) {
                throw ($localized.ModuleVersionMismatchError -f $Name, $moduleVersion, $MinimumVersion);
            }
        }

        $versionedModulePath = Join-Path -Path (Split-Path -Path $Path -Parent) -ChildPath $versionedModuleFilename;

        if (Test-Path -Path $versionedModulePath -PathType Leaf) {
            ## Remove existing version module
            Remove-Item -Path $versionedModulePath -Force -Confirm:$false;
        }

        Rename-Item -Path $Path -NewName $versionedModuleFilename;
        return (Get-Item -Path $versionedModulePath);

    } #end process
} #end function

function Reset-LabVMDisk {
<#
    .SYNOPSIS
        Removes and resets lab VM disk file (VHDX) configuration.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## VM/node display name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## VM/node name
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $NodeName = $Name,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $null = $PSBoundParameters.Remove('NodeName');

        Remove-LabVMSnapshot -Name $Name;
        Remove-LabVMDisk -NodeName $NodeName @PSBoundParameters;
        Set-LabVMDisk @PSBoundParameters;

    } #end process
} #end function

function Resolve-AzDoModuleUri {
    <#
       .SYNOPSIS
           Returns the direct download Uri for a PowerShell module hosted
           on the Azure DevOps Artifacts.
   #>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ## PowerShell DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## The minimum version of the DSC module required
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.Version] $MinimumVersion,

        ## The exact version of the DSC module required
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.Version] $RequiredVersion,

        ## Direct download Uri
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential,

        ## Catch all, for splatting $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        if ($PSBoundParameters.ContainsKey('Uri')) {

            $psRepositoryUri = $Uri;
        }
        else {

            $psRepositoryUri = (Get-ConfigurationData -Configuration Host).RepositoryUri;
        }

        if ($PSBoundParameters.ContainsKey('RequiredVersion')) {
            ## Download the specific version
            ## you would expect to be able to use $RequiredVersion as the version
            ## However this fails if a 4 part version is used as Azure Artifcates always uses 3 part versions
            return ('{0}?id={1}&version={2}' -f $psRepositoryUri, $Name.ToLower(), "$($RequiredVersion.Major).$($RequiredVersion.Minor).$($RequiredVersion.Build)")
        }
        else {

            ## Find the latest package versions
            $invokeRestMethodParams = @{
                Uri = "{0}/FindPackagesById()?id='{1}'" -f $psRepositoryUri, $Name.ToLower()
            }
            if ($PSBoundParameters.ContainsKey('FeedCredential')) {
                $invokeRestMethodParams['Credential'] = $FeedCredential
            }
            $azDoPackages = Invoke-RestMethod @invokeRestMethodParams

            ## Find and return the latest version
            $lastestDoPackageVersion = $azDoPackages | ForEach-Object {
                $_.properties.NormalizedVersion -as [System.Version] } |
            Sort-Object | Select-Object -Last 1
            return ('{0}?id={1}&version={2}' -f $psRepositoryUri, $Name.ToLower(), $lastestDoPackageVersion.ToString())
        }

    } #end process
} #end function Resolve-AzDoModuleUri.ps1

function Resolve-ConfigurationDataPath {
<#
    .SYNOPSIS
        Resolves the lab configuration data path.
    .NOTES
        When -IncludeDefaultPath is specified, if the configuration data file is not found, the default
        module configuration path is returned.
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('Host', 'VM', 'Media', 'CustomMedia','LegacyMedia')]
        [System.String] $Configuration,

        [Parameter(ParameterSetName = 'Default')]
        [System.Management.Automation.SwitchParameter] $IncludeDefaultPath,

        [Parameter(Mandatory, ParameterSetName = 'IsDefaultPath')]
        [System.Management.Automation.SwitchParameter] $IsDefaultPath
    )
    process {

        switch ($Configuration) {

            'Host' {

                $configPath = $labDefaults.HostConfigFilename;
            }
            'VM' {

                $configPath = $labDefaults.VMConfigFilename;
            }
            'Media' {

                $configPath = $labDefaults.MediaConfigFilename;
            }
            'CustomMedia' {

                $configPath = $labDefaults.CustomMediaConfigFilename;
            }
            'LegacyMedia' {

                $configPath = $labDefaults.LegacyMediaPath;
            }
        }
        $configPath = Join-Path -Path $labDefaults.ConfigurationData -ChildPath $configPath;
        $resolvedPath = Join-Path -Path "$env:ALLUSERSPROFILE\$($labDefaults.ModuleName)" -ChildPath $configPath;

        if ($IsDefaultPath) {

            $resolvedPath = Join-Path -Path $labDefaults.ModuleRoot -ChildPath $configPath;
        }
        elseif ($IncludeDefaultPath) {

            if (-not (Test-Path -Path $resolvedPath)) {

                $resolvedPath = Join-Path -Path $labDefaults.ModuleRoot -ChildPath $configPath;
            }
        }
        $resolvedPath = Resolve-PathEx -Path $resolvedPath;
        Write-Debug -Message ('Resolved ''{0}'' configuration path to ''{1}''.' -f $Configuration, $resolvedPath);
        return $resolvedPath;

    } #end process
} #end function ReolveConfigurationPath

function Resolve-ConfigurationPath {
<#
    .SYNOPSIS
        Resolves a node's .mof configuration file path.
    .DESCRIPTION
        Searches the current working directory and host configuration data path for a node's .mof
        files, searching an environment name subdirectory if environment name is defined.
#>

    [CmdletBinding()]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab vm/node name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Defined .mof path
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Path,

        ## Do not throw and return default configuration path
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseDefaultPath
    )
    process {

        Write-Verbose -Message ($localized.SearchingConfigurationPaths);
        try {

            ## Do we have an environment name?
            $configurationName = $ConfigurationData.NonNodeData[$labDefaults.moduleName].EnvironmentName;
        }
        catch {

            Write-Debug -Message 'No environment name defined';
        }

        if (-not [System.String]::IsNullOrEmpty($Path)) {

            ## Search the Specified path
            $resolvedPath = Resolve-PathEx -Path $Path;
            if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

                return $resolvedPath;
            }
            elseif ($configurationName) {

                ## Search the Specified\ConfigurationName path
                $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName;
                if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

                    return $resolvedPath;
                }
            }
        }

        ## Search the ConfigurationPath path
        $configurationPath = Get-LabHostDscConfigurationPath;
        $resolvedPath = Resolve-PathEx -Path $configurationPath;
        if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

            return $resolvedPath;
        }
        elseif ($configurationName) {

            ## Search the ConfigurationPath\ConfigurationName path
            $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName;
            if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

                return $resolvedPath;
            }
        }

        ## Search the Current path
        $currentPath = (Get-Location -PSProvider FileSystem).Path;
        $resolvedPath = Resolve-PathEx -Path $currentPath;
        if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

            return $resolvedPath;
        }
        elseif ($configurationName) {

            ## Search the Current\ConfigurationName path
            $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName;
            if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) {

                return $resolvedPath;
            }
        }

        if ($UseDefaultPath) {

            if (-not [System.String]::IsNullOrEmpty($Path)) {

                ## Return the specified path
                return (Resolve-PathEx -Path $Path);
            }
            else {

                ## Return the default configuration path
                return Get-LabHostDscConfigurationPath;
            }
        }
        else {

            ## We cannot resolve/locate the mof files..
            throw ($localized.CannotLocateMofFileError -f $Name);
        }

    } #end process
} #end function Resolve-ConfigurationPath

function Resolve-DismPath {
<#
    .SYNOPSIS
        Resolves the specified path to a path to DISM dll.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Path
    )
    process {

        if (-not (Test-Path -Path $Path)) {

            ## Path doesn't exist
            throw ($localized.InvalidPathError -f 'Directory', $DismPath);
        }
        else {

            $dismItem = Get-Item -Path $Path;
            $dismDllName = 'Microsoft.Dism.Powershell.dll';

            if ($dismItem.Name -ne $dismDllName) {

                if ($dismItem -is [System.IO.DirectoryInfo]) {

                    $dismItemPath = Join-Path -Path $DismPath -ChildPath $dismDllName;

                    if (-not (Test-Path -Path $dismItemPath)) {

                        throw ($localized.CannotLocateDismDllError -f $Path);
                    }
                    else {

                        $dismItem = Get-Item -Path $dismItemPath;
                    }
                }
                else {

                    throw ($localized.InvalidPathError -f 'File', $DismPath);
                }

            }
        }

        return $dismItem.FullName;

    } #end process
} #end function

function Resolve-GitHubModuleUri {
<#
    .SYNOPSIS
        Resolves the correct GitHub URI for the specified Owner, Repository and Branch.
#>

    [CmdletBinding()]
    [OutputType([System.Uri])]
    param (
        ## GitHub repository owner
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Owner,

        ## GitHub repository name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Repository,

        ## GitHub repository branch
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $Branch = 'master',

        ## Catch all to be able to pass parameter via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)] $RemainingArguments
    )
    process {

        $uri = 'https://github.com/{0}/{1}/archive/{2}.zip' -f $Owner, $Repository, $Branch;
        return New-Object -TypeName System.Uri -ArgumentList $uri;

    } #end process
} #end function

function Resolve-LabConfigurationModule {
<#
    .SYNOPSIS
        Resolves a lab module definition by its name from Lability configuration data.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        [Parameter(Mandatory)]
        [ValidateSet('Module','DscResource')]
        [System.String] $ModuleType,

        ## Lab module name/ID
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String[]] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $ThrowIfNotFound
    )
    process {

        $modules = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).$ModuleType;

        if (($PSBoundParameters.ContainsKey('Name')) -and ($Name -notcontains '*')) {

            ## Check we have them all first..
            foreach ($moduleName in $Name) {

                if ($modules.Name -notcontains $moduleName) {

                    if ($ThrowIfNotFound) {

                        throw ($localized.CannotResolveModuleNameError -f $ModuleType, $moduleName);
                    }
                    else {

                        Write-Warning -Message ($localized.CannotResolveModuleNameError -f $ModuleType, $moduleName);
                    }
                }
            }

            $modules = $modules | Where-Object { $_.Name -in $Name };
        }

        return $modules;

    } #end process
} #end function

function Resolve-LabCustomBootStrap {
<#
    .SYNOPSIS
        Resolves the media and node custom bootstrap, using the specified CustomBootstrapOrder
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ## Custom bootstrap order
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateSet('ConfigurationFirst','ConfigurationOnly','Disabled','MediaFirst','MediaOnly')]
        [System.String] $CustomBootstrapOrder,

        ## Node/configuration custom bootstrap script
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $ConfigurationCustomBootStrap,

        ## Media custom bootstrap script
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String[]] $MediaCustomBootStrap
    )
    begin {

        if ([System.String]::IsNullOrWhiteSpace($ConfigurationCustomBootStrap)) {

            $ConfigurationCustomBootStrap = "";
        }
        ## Convert the string[] into a multi-line string
        if ($MediaCustomBootstrap) {

            $mediaBootstrap = [System.String]::Join("`r`n", $MediaCustomBootStrap);
        }
        else {

            $mediaBootstrap = "";
        }
    } #end begin
    process {

        switch ($CustomBootstrapOrder) {

            'ConfigurationFirst' {
                $bootStrap = "{0}`r`n{1}" -f $ConfigurationCustomBootStrap, $mediaBootstrap;
            }
            'ConfigurationOnly' {
                $bootStrap = $ConfigurationCustomBootStrap;
            }
            'MediaFirst' {
                $bootStrap = "{0}`r`n{1}" -f $mediaBootstrap, $ConfigurationCustomBootStrap;
            }
            'MediaOnly' {
                $bootStrap = $mediaBootstrap;
            }
            Default {
                #Disabled
            }
        } #end switch

        return $bootStrap;

    } #end process
} #end function

function Resolve-LabEnvironmentName {
<#
    .SYNOPSIS
        Resolves a name with an environment prefix and suffix.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ## Switch Id/Name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        ## Add a prefix if defined
        if ($ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentPrefix) {

            $Name = '{1}{0}' -f $Name, $ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentPrefix;
        }
        if ($ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentSuffix) {

            $Name = '{0}{1}' -f $Name, $ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentSuffix;
        }

        return $Name;

    } #end process
} #end function

function Resolve-LabImage {
<#
    .SYNOPSIS
        Resolves a Lability image by its path.
    .DESCRIPTION
        When running Remove-LabVM there is not always a configuration document supplied. This
        causes issues removing a VMs VHD/X file. The ResolveLabImage function locates the image
        by its physical path.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Path
    )
    process {

        $vhdParentPaths = Resolve-VhdHierarchy -VhdPath $Path;
        Write-Output (Get-LabImage | Where-Object ImagePath -in $vhdParentPaths);

    } #end process
} #end function Resolve-LabImage

function Resolve-LabMedia {
<#
    .SYNOPSIS
        Resolves the specified media using the registered media and configuration data.
    .DESCRIPTION
        Resolves the specified lab media from the registered media, but permitting the defaults to be overridden by configuration data.

        This also permits specifying of media within Configuration Data and not having to be registered on the lab host.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param
    (
        ## Media ID or alias
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Id,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process
    {
        ## Avoid any $media variable scoping issues
        $media = $null

        ## If we have configuration data specific instance, return that
        if ($PSBoundParameters.ContainsKey('ConfigurationData'))
        {
            $customMedia = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Media.Where({ $_.Id -eq $Id -or $_.Alias -eq $Id })
            if ($customMedia)
            {
                $newLabMediaParams = @{ }
                foreach ($key in $customMedia.Keys)
                {
                    $newLabMediaParams[$key] = $customMedia.$key
                }
                $media = New-LabMedia @newLabMediaParams
            }
        }

        ## If we have custom media, return that
        if (-not $media)
        {
            $media = Get-ConfigurationData -Configuration CustomMedia
            $media = $media | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id }
        }

        ## If we still don't have a media image, return the built-in object
        if (-not $media)
        {
            $media = Get-LabMedia -Id $Id
        }

        ## We don't have any defined, custom or built-in media
        if (-not $media)
        {
            throw ($localized.CannotLocateMediaError -f $Id)
        }

        return $media

    } #end process
} #end function

function Resolve-LabModule {
<#
    .SYNOPSIS
        Returns the Node\DSCResource or Node\Module definitions from the
        NonNodeData\Lability\DSCResource or NonNodeData\Lability\Module node.
    .DESCRIPTION
        Resolves lab modules/DSC resources names defined at the Node\DSCResource or
        Node\Module node, and returns a collection of hashtables where the names
        match the definition in the NonNodeData\Lability\DSCResource or
        \NonNodeData\Lability\Module nodes.

        If resources are defined at the \NonNodeData\Lability\DSCResource or
        NonNodeData\Lability\Module nodes, but there are no VM references, then all
        the associated resources are returned.
    .NOTES
        If no NonNodeData\Lability\DscResource collection is defined, all locally
        installed DSC resource modules are returned.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Module type to enumerate
        [Parameter(Mandatory)]
        [ValidateSet('Module','DscResource')]
        [System.String] $ModuleType
    )
    process {

        $resolveNodePropertyValueParams = @{
            NodeName = $NodeName;
            ConfigurationData = $ConfigurationData;
        }
        $nodeProperties = Resolve-NodePropertyValue @resolveNodePropertyValueParams;

        $resolveModuleParams = @{
            ConfigurationData = $ConfigurationData;
            ModuleType = $ModuleType;
        }
        if ($nodeProperties.ContainsKey($ModuleType)) {
            $resolveModuleParams['Name'] = $nodeProperties[$ModuleType];
        }

        $modules = Resolve-LabConfigurationModule @resolveModuleParams;

        ## Only copy all existing DSC resources if there is no node or configuration DscResource
        ## defined. This allows suppressing local DSC resource module deploys
        if (($ModuleType -eq 'DscResource') -and
            (-not $nodeProperties.ContainsKey('DscResource')) -and
            ($null -eq $ConfigurationData.NonNodeData.($labDefaults.ModuleName).DscResource)) {
            <#
                There is no DSCResource = @() node defined either at the node or the configuration
                level. Therefore, we need
                to copy all the existing DSC resources on from the host by
                returning a load of FileSystem provider resources..
            #>

            Write-Warning -Message ($localized.DscResourcesNotDefinedWarning);

            $modules = Get-DscResourceModule -Path "$env:ProgramFiles\WindowsPowershell\Modules" |
                ForEach-Object {
                    ## Create a new hashtable
                    Write-Output -InputObject @{
                        Name = $_.ModuleName;
                        Version = $_.ModuleVersion;
                        Provider = 'FileSystem';
                        Path = $_.Path;
                    }
                };
        }

        return $modules;
    }
} #end function Resolve-LabModule

function Resolve-LabResource {
<#
    .SYNOPSIS
        Resolves a lab resource by its ID
#>

    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab resource ID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId
    )
    process {

        $resource = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource | Where-Object Id -eq $ResourceId;
        if ($resource) {

            return $resource;
        }
        else {

            throw ($localized.CannotResolveResourceIdError -f $resourceId);
        }

    } #end process
} #end function

function Resolve-LabSwitch {
<#
    .SYNOPSIS
        Resolves the specified switch using configuration data.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Switch Id/Name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $networkSwitch = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Network.Where({ $_.Name -eq $Name });

        if ($hostDefaults.DisableSwitchEnvironmentName -eq $false) {

            ## Prefix/suffix switch name
            $Name = Resolve-LabEnvironmentName -Name $Name -ConfigurationData $ConfigurationData;
        }

        if ($networkSwitch) {

            $networkHashtable = @{};
            foreach ($key in $networkSwitch.Keys) {

                [ref] $null = $networkHashtable.Add($key, $networkSwitch.$Key);
            }
            $networkSwitch = New-LabSwitch @networkHashtable;
        }
        elseif (Hyper-V\Get-VMSwitch -Name $Name -ErrorAction SilentlyContinue) {

            $existingSwitch = Hyper-V\Get-VMSwitch -Name $Name;
            ## Ensure that we only resolve a single switch as Hyper-V will allow (#326)
            if ($existingSwitch -is [System.Array]) {
                throw ($localized.AmbiguousSwitchNameError -f $Name);
            }
            Write-Warning -Message ($localized.UsingExistingSwitchWarning -f $Name);
            ## Use an existing virtual switch with a matching name if one exists
            $networkSwitch = @{
                Name = $existingSwitch.Name;
                Type = $existingSwitch.SwitchType;
                AllowManagementOS = $existingSwitch.AllowManagementOS;
                IsExisting = $true;
            }
            if (($existingSwitch.NetAdapterInterfaceDescription).Name) {

                $existingSwitchAdapter = Get-NetAdapter -InterfaceDescription $existingSwitch.NetAdapterInterfaceDescription;
                $networkSwitch['NetAdapterName'] = $existingSwitchAdapter.Name;
            }
        }
        else {

            ## Create an internal switch
            $networkSwitch = @{ Name = $Name; Type = 'Internal'; }
        }

        return $networkSwitch;

    } #end process
} #end function

function Resolve-LabVMGenerationDiskPath {
<#
    .SYNOPSIS
        Resolves the specified VM name's target VHD/X path.
#>

    [CmdletBinding()]
    param (
        ## VM/node name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData;

        $resolveLabVMDiskPathParams = @{
            Name            = $Name;
            Generation      = $image.Generation;
            EnvironmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;
        }
        $vhdPath = Resolve-LabVMDiskPath @resolveLabVMDiskPathParams

        return $vhdPath;

    } #end process
} #end function

function Resolve-LabVMDiskPath {
<#
    .SYNOPSIS
        Resolves the specified VM name to it's target VHDX path.
#>

    param (
        ## VM/node name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        [Parameter()]
        [ValidateSet('VHD','VHDX')]
        [System.String] $Generation = 'VHDX',

        ## Configuration environment name
        [Parameter()]
        [AllowNull()]
        [System.String] $EnvironmentName,

        ## Return the parent/folder path
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $Parent
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $differencingVhdPath = $hostDefaults.DifferencingVhdPath;

        if ((-not $hostDefaults.DisableVhdEnvironmentName) -and
            (-not [System.String]::IsNullOrEmpty($EnvironmentName))) {

            $differencingVhdPath = Join-Path -Path $differencingVhdPath -ChildPath $EnvironmentName;
        }

        if ($Parent) {

            $vhdPath = $differencingVhdPath;
        }
        else {

            $vhdName = '{0}.{1}' -f $Name, $Generation.ToLower();
            $vhdPath = Join-Path -Path $differencingVhdPath -ChildPath $vhdName;
        }

        return $vhdPath;

    } #end process
} #end function

function Resolve-LabVMImage {
<#
    .SYNOPSIS
        Resolves a virtual machine's Lability image.
#>

    [CmdletBinding()]
    param (
        ## VM name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name
    )
    process {

        Hyper-V\Get-VM -Name $Name |
            Hyper-V\Get-VMHardDiskDrive |
                Select-Object -First 1 -ExpandProperty $Path |
                    Resolve-LabImage;

    } #'end process
} #end function Resolve-LabVMImage

function Resolve-NodePropertyValue {
<#
    .SYNOPSIS
        Resolves a node's properites.
    .DESCRIPTION
        Resolves a lab virtual machine properties from the lab defaults, Node\* node
        and Node\NodeName node.

        Properties defined on the wildcard node override the lab defaults.
        Properties defined at the node override the wildcard node settings.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Do not enumerate the AllNodes.'*' node
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoEnumerateWildcardNode
    )
    process {

        $node = @{ };

        if (-not $NoEnumerateWildcardNode) {

            ## Retrieve the AllNodes.* properties
            $ConfigurationData.AllNodes.Where({ $_.NodeName -eq '*' }) | ForEach-Object {
                foreach ($key in $_.Keys) {

                    $node[$key] = $_.$key;
                }
            }
        }

        ## Retrieve the AllNodes.$NodeName properties
        $ConfigurationData.AllNodes.Where({ $_.NodeName -eq $NodeName }) | ForEach-Object {

            foreach ($key in $_.Keys) {

                $node[$key] = $_.$key;
            }
        }

        ## Check VM defaults
        $labDefaultProperties = Get-ConfigurationData -Configuration VM;
        $properties = Get-Member -InputObject $labDefaultProperties -MemberType NoteProperty;
        foreach ($propertyName in $properties.Name) {

            ## Int32 values of 0 get coerced into $false!
            if (($node.$propertyName -isnot [System.Int32]) -and (-not $node.ContainsKey($propertyName))) {

                $node[$propertyName] = $labDefaultProperties.$propertyName;
            }
        }

        $resolveLabEnvironmentNameParams = @{
            Name              = $NodeName;
            ConfigurationData = $ConfigurationData;
        }
        ## Set the node's friendly/display name with any prefix/suffix
        $node['NodeDisplayName'] = Resolve-LabEnvironmentName @resolveLabEnvironmentNameParams;
        ## Use the prefixed/suffixed NetBIOSName is specified
        if ($node.UseNetBIOSName -eq $true) {
            $node['NodeDisplayName'] = $node['NodeDisplayName'].Split('.')[0];
        }

        $moduleName = $labDefaults.ModuleName;
        ## Rename/overwrite existing parameter values where $moduleName-specific parameters exist
        foreach ($key in @($node.Keys)) {

            if ($key.StartsWith("$($moduleName)_")) {

                $node[($key.Replace("$($moduleName)_",''))] = $node.$key;
                $node.Remove($key);
            }
        }

        return $node;

    } #end process
} #end function ResolveLabVMProperties

function Resolve-PathEx {
<#
    .SYNOPSIS
        Resolves the wildcard characters in a path, and displays the path contents, ignoring non-existent paths.
    .DESCRIPTION
        The Resolve-Path cmdlet interprets the wildcard characters in a path and displays the items and containers at
        the location specified by the path, such as the files and folders or registry keys and subkeys.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory)]
        [System.String] $Path
    )
    process {

        try {

            $expandedPath = [System.Environment]::ExpandEnvironmentVariables($Path);
            $resolvedPath = Resolve-Path -Path $expandedPath -ErrorAction Stop;
            $Path = $resolvedPath.ProviderPath;
        }
        catch [System.Management.Automation.ItemNotFoundException] {

            $Path = [System.Environment]::ExpandEnvironmentVariables($_.TargetObject);
            $Error.Remove($Error[-1]);
        }

        return $Path;

    } #end process
} #end function

function Resolve-ProgramFilesFolder {
<#
    .SYNOPSIS
        Resolves known localized %ProgramFiles% directories.
    .LINK
        https://en.wikipedia.org/wiki/Program_Files
#>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    [OutputType([System.IO.DirectoryInfo])]
    param (
        ## Root path to check
        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Drive letter
        [Parameter(Mandatory, ParameterSetName = 'Drive')]
        [ValidateLength(1,1)]
        [System.String] $Drive
    )
    begin {

        if ($PSCmdlet.ParameterSetName -eq 'Drive') {

            $Path = '{0}:\' -f $Drive;
        }

    }
    process {

        $knownFolderNames = @(
            "Program Files",
            "Programmes",
            "Archivos de programa",
            "Programme",
            "Programf�jlok",
            "Programmi",
            "Programmer",
            "Program",
            "Programfiler",
            "Arquivos de Programas",
            "Programas"
            "???e?a ?fa?�????"
        )

        Get-ChildItem -Path $Path -Directory |
            Where-Object Name -in $knownFolderNames |
                Select-Object -First 1;

    } #end process
} #end function

function Resolve-PSGalleryModuleUri {
    <#
       .SYNOPSIS
           Returns the direct download Uri for a PowerShell module hosted
           on the PowerShell Gallery.
   #>

       [CmdletBinding()]
       [OutputType([System.String])]
       param (
           ## PowerShell DSC resource module name
           [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
           [System.String] $Name,

           ## The minimum version of the DSC module required
           [Parameter(ValueFromPipelineByPropertyName)]
           [ValidateNotNullOrEmpty()]
           [System.Version] $MinimumVersion,

           ## The exact version of the DSC module required
           [Parameter(ValueFromPipelineByPropertyName)]
           [ValidateNotNullOrEmpty()]
           [System.Version] $RequiredVersion,

           ## Direct download Uri
           [Parameter(ValueFromPipelineByPropertyName)]
           [ValidateNotNullOrEmpty()]
           [System.String] $Uri,

           ## Catch all, for splatting $PSBoundParameters
           [Parameter(ValueFromRemainingArguments)]
           $RemainingArguments
       )
       process {

           if ($PSBoundParameters.ContainsKey('Uri')) {

               $psRepositoryUri = $Uri;
           }
           else {

               $psRepositoryUri = (Get-ConfigurationData -Configuration Host).RepositoryUri;
           }

           if ($PSBoundParameters.ContainsKey('RequiredVersion')) {

               ## Download the specific version
               return ('{0}/{1}/{2}' -f $psRepositoryUri, $Name, $RequiredVersion);
           }
           else {

               ## Download the latest version
               return ('{0}/{1}' -f $psRepositoryUri, $Name);
           }

       } #end process
   } #end function Resolve-PSGalleryModuleUri

<#

The MIT License (MIT)

Copyright (c) 2015 Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

#>


function Resolve-VhdHierarchy {
<#
    .SYNOPSIS
        Returns VM VHDs, including snapshots and differencing disks
#>

    [CmdletBinding()]
    param (
        ## Path to current virtual machine VHD/X file
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $VhdPath
    )
    process {

        $vmVhdPath = Hyper-V\Get-VHD -Path $VhdPath;
        Write-Output -InputObject $vmVhdPath.Path;
        while (-not [System.String]::IsNullOrEmpty($vmVhdPath.ParentPath)) {

            $vmVhdPath.ParentPath;
            $vmVhdPath = (Hyper-V\Get-VHD -Path $vmVhdPath.ParentPath);
        }

    } #end process
} #end function Resolve-VhdHierarchy

function Set-ConfigurationData {
<#
    .SYNOPSIS
        Saves lab configuration data.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('Host','VM','Media','CustomMedia')]
        [System.String] $Configuration,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Object] $InputObject
    )
    process {

        $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration;
        [ref] $null = New-Directory -Path (Split-Path -Path $configurationPath -Parent) -Verbose:$false;
        Set-Content -Path $configurationPath -Value (ConvertTo-Json -InputObject $InputObject -Depth 5) -Force -Confirm:$false;

    } #end process
} #end function Set-ConfigurationData

function Set-DiskImageBootVolume {
<#
    .SYNOPSIS
        Sets the boot volume of a mounted disk image.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk

        ## Disk image partition scheme
        [Parameter(Mandatory)]
        [ValidateSet('MBR','GPT')]
        [System.String] $PartitionStyle
    )
    process {

        switch ($PartitionStyle) {

            'MBR' {

                Set-DiskImageBootVolumeMbr -Vhd $Vhd;
                break;
            }
            'GPT' {

                Set-DiskImageBootVolumeGpt -Vhd $Vhd;
                break;
            }
        } #end switch

    } #end process
} #end function

function Set-DiskImageBootVolumeGpt {
<#
    .SYNOPSIS
        Configure/repair MBR boot volume
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk
    )
    process {

        $bcdBootExe = 'bcdboot.exe';
        $imageName = [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path);

        $systemPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'System';
        $osPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'Basic';
        Write-Verbose -Message ($localized.RepairingBootVolume -f $osPartitionDriveLetter);
        $bcdBootArgs = @(
            ('{0}:\Windows' -f $osPartitionDriveLetter),   # Path to source Windows boot files
            ('/s {0}:\' -f $systemPartitionDriveLetter),   # Specifies the volume letter of the drive to create the \BOOT folder on.
            '/v'                                           # Enabled verbose logging.
            '/f UEFI'                                      # Specifies the firmware type of the target system partition
        )
        Invoke-Executable -Path $bcdBootExe -Arguments $bcdBootArgs -LogName ('{0}-BootEdit.log' -f $imageName);
        ## Clean up and remove drive access path
        Remove-PSDrive -Name $osPartitionDriveLetter -PSProvider FileSystem -ErrorAction Ignore;
        [ref] $null = Get-PSDrive;

    } #end process
} #end function

function Set-DiskImageBootVolumeMbr {
<#
    .SYNOPSIS
        Configure/repair MBR boot volume
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory)]
        [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk
    )
    process {

        $bcdBootExe = 'bcdboot.exe';
        $bcdEditExe = 'bcdedit.exe';
        $imageName = [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path);

        $osPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'IFS';
        Write-Verbose -Message ($localized.RepairingBootVolume -f $osPartitionDriveLetter);
        $bcdBootArgs = @(
            ('{0}:\Windows' -f $osPartitionDriveLetter), # Path to source Windows boot files
            ('/s {0}:\' -f $osPartitionDriveLetter),     # Volume to create the \BOOT folder on.
            '/v'                                         # Enable verbose logging.
            '/f BIOS'                                    # Firmware type of the target system partition
        )
        Invoke-Executable -Path $bcdBootExe -Arguments $bcdBootArgs -LogName ('{0}-BootEdit.log' -f $imageName);

        $bootmgrDeviceArgs = @(
            ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter),
            '/set {bootmgr} device locate'
        );
        Invoke-Executable -Path $bcdEditExe -Arguments $bootmgrDeviceArgs -LogName ('{0}-BootmgrDevice.log' -f $imageName);

        $defaultDeviceArgs = @(
            ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter),
            '/set {default} device locate'
        );
        Invoke-Executable -Path $bcdEditExe -Arguments $defaultDeviceArgs -LogName ('{0}-DefaultDevice.log' -f $imageName);

        $defaultOsDeviceArgs = @(
            ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter),
            '/set {default} osdevice locate'
        );
        Invoke-Executable -Path $bcdEditExe -Arguments $defaultOsDeviceArgs -LogName ('{0}-DefaultOsDevice.log' -f $imageName);

    } #end process
} #end function

function Set-LabBootStrap {
<#
    .SYNOPSIS
        Writes the lab BootStrap.ps1 file to the target directory.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Destination Bootstrap directory path.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Path,

        ## Custom bootstrap script
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $CustomBootStrap,

        ## Is a CoreCLR VM. The PowerShell switches are different in the CoreCLR, i.e. Nano Server
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CoreCLR,

        ## Custom shell
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $DefaultShell,

        ## WSMan maximum envelope size
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $MaxEnvelopeSizeKb = 1024
    )
    process {

        $newBootStrapParams = @{
            CoreCLR = $CoreCLR;
            MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb;
        }
        if (-not [System.String]::IsNullOrEmpty($DefaultShell)) {

            $newBootStrapParams['DefaultShell'] = $DefaultShell;
        }
        $bootStrap = New-LabBootStrap @newBootStrapParams;

        if ($CustomBootStrap) {

            $bootStrap = $bootStrap.Replace('<#CustomBootStrapInjectionPoint#>', $CustomBootStrap);
        }

        [ref] $null = New-Directory -Path $Path -Confirm:$false;
        $bootStrapPath = Join-Path -Path $Path -ChildPath 'BootStrap.ps1';
        Set-Content -Path $bootStrapPath -Value $bootStrap -Encoding UTF8 -Force -Confirm:$false;

    } #end process
} #end function

function Set-LabDscResource {
<#
    .SYNOPSIS
        Runs the ResourceName DSC resource ensuring it's in the desired state.
    .DESCRIPTION
        The Set-LabDscResource cmdlet invokes the target $ResourceName\Set-TargetResource function using the supplied
        $Parameters hastable.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Name of the DSC resource to invoke
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $ResourceName,

        ## The DSC resource's Set-TargetResource parameter hashtable
        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $Parameters
    )
    process {

        $setTargetResourceCommand = 'Set-{0}TargetResource' -f $ResourceName;
        Write-Debug ($localized.InvokingCommand -f $setTargetResourceCommand);
        $Parameters.Keys | ForEach-Object {

            Write-Debug -Message ($localized.CommandParameter -f $_, $Parameters.$_);
        }

        try {

            $setDscResourceResult = & $setTargetResourceCommand @Parameters;
        }
        catch {

            Write-Warning -Message ($localized.DscResourceFailedError -f $setTargetResourceCommand, $_);
        }

        return $setDscResourceResult;

    } #end process
} #end function

function Set-LabSetupCompleteCmd {
<#
    .SYNOPSIS
        Creates a lab BootStrap script block.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.ScriptBlock])]
    param (
        ## Destination SetupComplete.cmd directory path.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Is a CoreCLR VM. The bootstrapping via Powershell.exe in the CoreCLR doesn't work in its current format, i.e. with Nano Server
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CoreCLR
    )
    process {

        [ref] $null = New-Directory -Path $Path -Confirm:$false;
        $setupCompletePath = Join-Path -Path $Path -ChildPath 'SetupComplete.cmd';
        if ($CoreCLR) {

            Write-Verbose -Message $localized.UsingCoreCLRSetupComplete;
            $setupCompleteCmd = @"
schtasks /create /tn "BootStrap" /tr "cmd.exe /c """Powershell.exe -Command %SYSTEMDRIVE%\BootStrap\BootStrap.ps1""" > %SYSTEMDRIVE%\BootStrap\BootStrap.log" /sc "Once" /sd "01/01/2099" /st "00:00" /ru "System"
schtasks /run /tn "BootStrap"
"@

        }
        else {

            Write-Verbose -Message $localized.UsingDefaultSetupComplete;
            $setupCompleteCmd = 'Powershell.exe -NoProfile -ExecutionPolicy Bypass -NonInteractive -File "%SYSTEMDRIVE%\BootStrap\BootStrap.ps1"';
        }

        Set-Content -Path $setupCompletePath -Value $setupCompleteCmd -Encoding Ascii -Force -Confirm:$false;

    } #end process
} #end function

function Set-LabSwitch {
<#
    .SYNOPSIS
        Sets/invokes a virtual network switch configuration.
    .DESCRIPTION
        Sets/invokes a virtual network switch configuration using the xVMSwitch DSC resource.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Switch Id/Name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $networkSwitch = Resolve-LabSwitch @PSBoundParameters;
        if (($null -eq $networkSwitch.IsExisting) -or ($networkSwitch.IsExisting -eq $false)) {

            Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch;
            [ref] $null = Invoke-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch;
        }

    } #end process
} #end function

function Set-LabVirtualMachine {
<#
    .SYNOPSIS
        Invokes the current configuration a virtual machine.
    .DESCRIPTION
        Invokes/sets a virtual machine configuration using the xVMHyperV DSC resource.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $SwitchName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Media,

        [Parameter(Mandatory)]
        [System.UInt64] $StartupMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MinimumMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MaximumMemory,

        [Parameter(Mandatory)]
        [System.Int32] $ProcessorCount,

        [Parameter()]
        [AllowNull()]
        [System.String[]] $MACAddress,

        [Parameter()]
        [System.Boolean] $SecureBoot,

        [Parameter()]
        [System.Boolean] $GuestIntegrationServices,

        [Parameter()]
        [System.Boolean] $AutomaticCheckpoints,

        ## xVMProcessor options
        [Parameter()]
        [System.Collections.Hashtable] $ProcessorOption,

        ## xVMHardDiskDrive options
        [Parameter()]
        [System.Collections.Hashtable[]] $HardDiskDrive,

        ## xVMDvdDrive options
        [Parameter()]
        [System.Collections.Hashtable] $DvdDrive,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        ## Store additional options for when we have a VM
        $vmProcessorParams = $PSBoundParameters['ProcessorOption'];
        $vmDvdDriveParams = $PSBoundParameters['DvdDrive'];
        $vmHardDiskDriveParams = $PSBoundParameters['HardDiskDrive'];
        [ref] $null = $PSBoundParameters.Remove('ProcessorOption');
        [ref] $null = $PSBoundParameters.Remove('DvdDrive');
        [ref] $null = $PSBoundParameters.Remove('HardDiskDrive');

        ## Resolve the xVMHyperV resource parameters
        $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters;
        Write-Verbose -Message ($localized.CreatingVMGeneration -f $vmHyperVParams.Generation);
        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM;
        Invoke-LabDscResource -ResourceName VM -Parameters $vmHyperVParams;

        if ($null -ne $vmProcessorParams) {

            ## Ensure we have the node's name
            $vmProcessorParams['VMName'] = $Name;
            Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM processor', $Name);
            Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMProcessor -Prefix VMProcessor;
            Invoke-LabDscResource -ResourceName VMProcessor -Parameters $vmProcessorParams;
        }

        if ($null -ne $vmDvdDriveParams) {

            ## Ensure we have the node's name
            $vmDvdDriveParams['VMName'] = $Name;
            Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM DVD drive', $Name);
            Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMDvdDrive -Prefix VMDvdDrive;
            Invoke-LabDscResource -ResourceName VMDvdDrive -Parameters $vmDvdDriveParams;
        }

        if ($null -ne $vmHardDiskDriveParams) {

            $setLabVirtualMachineHardDiskDriveParams = @{
                NodeName = $Name;
                VMGeneration = $vmHyperVParams.Generation;
                HardDiskDrive = $vmHardDiskDriveParams;
            }
            Set-LabVirtualMachineHardDiskDrive @setLabVirtualMachineHardDiskDriveParams;
        }

    } #end process
} #end function

function Set-LabVirtualMachineHardDiskDrive {
<#
    .SYNOPSIS
        Sets a virtual machine's additional hard disk drive(s).
    .DESCRIPTION
        Adds one or more additional hard disks to a VM.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $NodeName,

        ## Collection of additional hard disk drive configurations
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable[]]
        $HardDiskDrive,

        ## Virtual machine generation
        [Parameter()]
        [System.Int32] $VMGeneration
    )
    process {

        $vmHardDiskPath = (Get-ConfigurationData -Configuration Host).DifferencingVhdPath;

        for ($i = 0; $i -lt $HardDiskDrive.Count; $i++) {

            $diskDrive = $HardDiskDrive[$i];
            $controllerLocation = $i + 1;

            Assert-VirtualMachineHardDiskDriveParameter @diskDrive -VMGeneration $VMGeneration;

            if ($diskDrive.ContainsKey('VhdPath')) {

                $vhdPath = $diskDrive.VhdPath;
            }
            else {

                ## Create the VHD file
                $vhdName = '{0}-{1}' -f $NodeName, $controllerLocation;
                $vhdParams = @{
                    Name = $vhdName;
                    Path = $vmHardDiskPath;
                    MaximumSizeBytes = $diskDrive.MaximumSizeBytes;
                    Generation = $diskDrive.Generation;
                    Ensure = 'Present';
                }

                if ($null -ne $diskDrive.Type) {

                    $vhdParams['Type'] = $diskDrive.Type;
                }

                $vhdFilename = '{0}.{1}' -f $vhdName, $diskDrive.Generation.ToLower();
                $vhdPath = Join-Path -Path $vmHardDiskPath -ChildPath $vhdFilename;
                Write-Verbose -Message ($localized.CreatingAdditionalVhdFile -f $vhdPath);
                Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVhd -Prefix Vhd;
                Invoke-LabDscResource -ResourceName Vhd -Parameters $vhdParams;
            }

            ## Now add the VHD
            Write-Verbose -Message ($localized.AddingAdditionalVhdFile -f $vhdPath, "0:$controllerLocation");
            $vmHardDiskDriveParams = @{
                VMName = $NodeName;
                ControllerLocation = $controllerLocation;
                Path = $VhdPath;
                Ensure = 'Present';
            }
            Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHardDiskDrive -Prefix HardDiskDrive;
            Invoke-LabDscResource -ResourceName HardDiskDrive -Parameters $vmHardDiskDriveParams;

        } #end for

    } #end process
} #end function

function Set-LabVMDisk {
    <#
    .SYNOPSIS
        Sets a lab VM disk file (VHDX) configuration.
    .DESCRIPTION
        Configures a VM disk configuration using the xVHD DSC resource.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param (
        ## VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData -ErrorAction Stop;
        }
        else {

            $image = Get-LabImage -Id $Media -ErrorAction Stop;
        }

        $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;
        $vhd = @{
            Name       = $Name;
            Path       = Resolve-LabVMDiskPath -Name $Name -EnvironmentName $environmentName -Parent;
            ParentPath = $image.ImagePath;
            Generation = $image.Generation;
            Type       = 'Differencing';
        }

        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD;
        [ref] $null = Invoke-LabDscResource -ResourceName VHD -Parameters $vhd;

    } #end process
} #end function

function Set-LabVMDiskFile {
<#
    .SYNOPSIS
        Copies Lability files to a node's VHD(X) file.
    .DESCRIPTION
        Copies the Lability bootstrap file, SetupComplete.cmd, unattend.xml,
        mof files, certificates and PowerShell/DSC resource modules to a
        VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Local administrator password of the VM
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        ## Lab VM/Node DSC .mof and .meta.mof configuration files
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Path,

        ## Custom bootstrap script
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $CustomBootstrap,

        ## CoreCLR
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CoreCLR,

        ## Custom/replacement Shell
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $DefaultShell,

        ## WSMan maximum envelope size
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $MaxEnvelopeSizeKb = 1024,

        ## Media-defined product key
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ProductKey,

        ## Credentials to access the a private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
    )
    process {

        ## Temporarily disable Windows Explorer popup disk initialization and format notifications
        ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx
        Stop-ShellHWDetectionService;

        $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop;

        $resolveLabVMGenerationDiskPathParams = @{
            Name = $node.NodeDisplayName;
            Media = $node.Media;
            ConfigurationData = $ConfigurationData;
        }
        $vhdPath = Resolve-LabVMGenerationDiskPath @resolveLabVMGenerationDiskPathParams;

        ## Disable BitLocker fixed drive write protection (if enabled)
        Disable-BitLockerFDV;

        Write-Verbose -Message ($localized.MountingDiskImage -f $VhdPath);
        $vhd = Hyper-V\Mount-Vhd -Path $vhdPath -Passthru -Confirm:$false;
        $driveList = Get-PSDrive -PSProvider FileSystem
        $vhdDriveLetter = Storage\Get-Partition -DiskNumber $vhd.DiskNumber |
                            Where-Object DriveLetter |
                                Select-Object -Last 1 -ExpandProperty DriveLetter;

        ## If no drive letter is automagically assigned, assign one.
        if ($null -eq $vhdDriveLetter)
        {
            $driveListNames = $driveList.Name
            foreach ($driveLetter in [char[]](67..90))
            {
                if ($driveListNames -notcontains $driveLetter)
                {
                    $vhdDriveLetter = $driveLetter
                    break
                }
            }
            Get-Partition | Where-Object { ($_.DiskNumber -eq $vhd.DiskNumber) -and ($_.Type -eq 'Basic') } |
                Set-Partition -NewDriveLetter $vhdDriveLetter
        }
        Start-ShellHWDetectionService;

        try {

            Set-LabVMDiskFileResource @PSBoundParameters -VhdDriveLetter $vhdDriveLetter;
            Set-LabVMDiskFileBootstrap @PSBoundParameters -VhdDriveLetter $vhdDriveLetter;
            Set-LabVMDiskFileUnattendXml @PSBoundParameters -VhdDriveLetter $vhdDriveLetter;
            Set-LabVMDiskFileMof @PSBoundParameters -VhdDriveLetter $vhdDriveLetter;
            Set-LabVMDiskFileCertificate @PSBoundParameters -VhdDriveLetter $vhdDriveLetter;
            Set-LabVMDiskFileModule @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; # FeedCredential passed in bound parameters
        }
        catch {

            ## Bubble up the error to the caller
            throw $_;
        }
        finally {

            ## Ensure the VHD is dismounted (#185)
            Write-Verbose -Message ($localized.DismountingDiskImage -f $VhdPath);
            $null = Hyper-V\Dismount-Vhd -Path $VhdPath -Confirm:$false;

            ## Enable BitLocker (if required)
            Assert-BitLockerFDV;
        }

    } #end process
} #end function Set-LabVMDiskFile

function Set-LabVMDiskFileBootstrap {
<#
    .SYNOPSIS
        Copies a the Lability bootstrap file to a VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Custom bootstrap script
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $CustomBootstrap,

        ## CoreCLR
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CoreCLR,

        ## Custom/replacement shell
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DefaultShell,

        ## WSMan maximum envelope size
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $MaxEnvelopeSizeKb = 1024,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter;
        Write-Verbose -Message ($localized.AddingBootStrapFile -f $bootStrapPath);
        $setBootStrapParams = @{
            Path = $bootStrapPath;
            CoreCLR = $CoreCLR;
            MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb;
        }
        if ($CustomBootStrap) {

            $setBootStrapParams['CustomBootStrap'] = $CustomBootStrap;
        }
        if ($PSBoundParameters.ContainsKey('DefaultShell')) {

            Write-Verbose -Message ($localized.SettingCustomShell -f $DefaultShell);
            $setBootStrapParams['DefaultShell'] = $DefaultShell;
        }
        Set-LabBootStrap @setBootStrapParams;

        $setupCompleteCmdPath = '{0}:\Windows\Setup\Scripts' -f $vhdDriveLetter;
        Write-Verbose -Message ($localized.AddingSetupCompleteCmdFile -f $setupCompleteCmdPath);
        Set-LabSetupCompleteCmd -Path $setupCompleteCmdPath -CoreCLR:$CoreCLR;

    } #end process
} #end function

function Set-LabVMDiskFileCertificate {
<#
    .SYNOPSIS
        Copies a node's certificate(s) to a VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop;
        $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter;

        if (-not [System.String]::IsNullOrWhitespace($node.ClientCertificatePath)) {

            [ref] $null = New-Item -Path $bootStrapPath -ItemType File -Name 'LabClient.pfx' -Force;
            $destinationCertificatePath = Join-Path -Path $bootStrapPath -ChildPath 'LabClient.pfx';
            $expandedClientCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.ClientCertificatePath);
            Write-Verbose -Message ($localized.AddingCertificate -f 'Client', $destinationCertificatePath);
            Copy-Item -Path $expandedClientCertificatePath -Destination $destinationCertificatePath -Force -Confirm:$false;
        }

        if (-not [System.String]::IsNullOrWhitespace($node.RootCertificatePath)) {

            [ref] $null = New-Item -Path $bootStrapPath -ItemType File -Name 'LabRoot.cer' -Force;
            $destinationCertificatePath = Join-Path -Path $bootStrapPath -ChildPath 'LabRoot.cer';
            $expandedRootCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.RootCertificatePath);
            Write-Verbose -Message ($localized.AddingCertificate -f 'Root', $destinationCertificatePath);
            Copy-Item -Path $expandedRootCertificatePath -Destination $destinationCertificatePath -Force -Confirm:$false;
        }

    } #end process
} #end function

function Set-LabVMDiskFileModule {
<#
    .SYNOPSIS
        Copies a node's PowerShell and DSC resource modules to a VHD(X) file.
#>


    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Node DSC .mof and .meta.mof configuration file path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Path,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments,

        ## Credentials to access the a private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
    )
    process {

        ## Resolve the localized %ProgramFiles% directory
        $programFilesPath = '{0}\WindowsPowershell\Modules' -f (Resolve-ProgramFilesFolder -Drive $VhdDriveLetter).FullName

        ## Add the DSC resource modules
        $resolveLabModuleParams = @{
            ConfigurationData = $ConfigurationData;
            NodeName = $NodeName;
            ModuleType = 'DscResource';
        }
        $setLabVMDiskDscModuleParams = @{
            Module = Resolve-LabModule @resolveLabModuleParams;
            DestinationPath = $programFilesPath;
        }

        if ($null -ne $setLabVMDiskDscModuleParams['Module']) {

            ## Check that we have cache copies of all modules used in the .mof file.
            $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $NodeName);
            if (Test-Path -Path $mofPath -PathType Leaf) {

                $testLabMofModuleParams = @{
                    Module = $setLabVMDiskDscModuleParams.Module;
                    MofModule = Get-LabMofModule -Path $mofPath;
                }

                if ($null -ne $testLabMofModuleParams['MofModule']) {

                    ## TODO: Add automatic download of missing resources from the PSGallery.
                    [ref] $null = Test-LabMofModule @testLabMofModuleParams;
                }
            }

            Write-Verbose -Message ($localized.AddingDSCResourceModules -f $programFilesPath);
            Set-LabVMDiskModule @setLabVMDiskDscModuleParams -FeedCredential $FeedCredential;
        }

        ## Add the PowerShell resource modules
        $resolveLabModuleParams =@{
            ConfigurationData = $ConfigurationData;
            NodeName = $NodeName;
            ModuleType = 'Module';
        }
        $setLabVMDiskPowerShellModuleParams = @{
            Module = Resolve-LabModule @resolveLabModuleParams;
            DestinationPath = $programFilesPath;
        }
        if ($null -ne $setLabVMDiskPowerShellModuleParams['Module']) {

            Write-Verbose -Message ($localized.AddingPowerShellModules -f $programFilesPath);
            Set-LabVMDiskModule @setLabVMDiskPowerShellModuleParams;
        }

    } #end process
} #end function

function Set-LabVMDiskFileMof {
<#
    .SYNOPSIS
        Copies a node's mof files to a VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab VM/Node DSC .mof and .meta.mof configuration files
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Path,

        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter;
        $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $NodeName);

        if (-not (Test-Path -Path $mofPath)) {

            Write-Warning -Message ($localized.CannotLocateMofFileError -f $mofPath);
        }
        else {

            $destinationMofPath = Join-Path -Path $bootStrapPath -ChildPath 'localhost.mof';
            Write-Verbose -Message ($localized.AddingDscConfiguration -f $destinationMofPath);
            Copy-Item -Path $mofPath -Destination $destinationMofPath -Force -ErrorAction Stop -Confirm:$false;
        }

        $metaMofPath = Join-Path -Path $Path -ChildPath ('{0}.meta.mof' -f $NodeName);
        if (Test-Path -Path $metaMofPath -PathType Leaf) {

            $destinationMetaMofPath = Join-Path -Path $bootStrapPath -ChildPath 'localhost.meta.mof';
            Write-Verbose -Message ($localized.AddingDscConfiguration -f $destinationMetaMofPath);
            Copy-Item -Path $metaMofPath -Destination $destinationMetaMofPath -Force -Confirm:$false;
        }

    } #end process
} #end function

function Set-LabVMDiskFileResource {
<#
    .SYNOPSIS
        Copies a node's defined resources to VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $resourceDestinationPath = '{0}:\{1}' -f $vhdDriveLetter, $hostDefaults.ResourceShareName;
        $expandLabResourceParams = @{
            ConfigurationData = $ConfigurationData;
            Name = $NodeName;
            DestinationPath = $resourceDestinationPath;
        }
        Write-Verbose -Message ($localized.AddingVMResource -f 'VM');
        Expand-LabResource @expandLabResourceParams;

    } #end process
} #end function

function Set-LabVMDiskFileUnattendXml {
<#
    .SYNOPSIS
        Copies a node's unattent.xml to a VHD(X) file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lab VM/Node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $NodeName,

        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Mounted VHD path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $VhdDriveLetter,

        ## Local administrator password of the VM
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        ## Media-defined product key
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ProductKey,

        ## Catch all to enable splatting @PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop;

        ## Create Unattend.xml
        $newUnattendXmlParams = @{
            ComputerName = $node.NodeName.Split('.')[0]; # Convert any FQDN to NetBIOS (#335)
            Credential = $Credential;
            InputLocale = $node.InputLocale;
            SystemLocale = $node.SystemLocale;
            UserLocale = $node.UserLocale;
            UILanguage = 'en-US';
            Timezone = $node.Timezone;
            RegisteredOwner = $node.RegisteredOwner;
            RegisteredOrganization = $node.RegisteredOrganization;
        }
        Write-Verbose -Message $localized.SettingAdministratorPassword;

        ## Node defined Product Key takes preference over key defined in the media definition
        if ($node.CustomData.ProductKey) {

            $newUnattendXmlParams['ProductKey'] = $node.CustomData.ProductKey;
        }
        elseif ($PSBoundParameters.ContainsKey('ProductKey')) {

            $newUnattendXmlParams['ProductKey'] = $ProductKey;
        }

        ## TODO: We probably need to be localise the \Windows\ (%ProgramFiles% has been done) directory?
        $unattendXmlPath = '{0}:\Windows\System32\Sysprep\Unattend.xml' -f $VhdDriveLetter;
        Write-Verbose -Message ($localized.AddingUnattendXmlFile -f $unattendXmlPath);
        [ref] $null = Set-UnattendXml @newUnattendXmlParams -Path $unattendXmlPath;

    } #end process
} #end function

function Set-LabVMDiskModule {
<#
    .SYNOPSIS
        Downloads (if required) PowerShell/DSC modules and expands
        them to the destination path specified.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        ## Lability PowerShell modules/DSC resource hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable[]] $Module,

        ## The target VHDX modules path
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        ## Force a download of the module(s) even if they already exist in the cache.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

        ## Removes existing target module directory (if present)
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Clean,

        ## Credentials to access the a private feed
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
    )
    process {

        ## Invokes the module download if not cached, and returns the source
        [ref] $null = Invoke-LabModuleCacheDownload -Module $Module -Force:$Force -FeedCredential $FeedCredential;
        ## Expand the modules into the VHDX file
        [ref] $null = Expand-LabModuleCache -Module $Module -DestinationPath $DestinationPath -Clean:$Clean;

    } #end process
} #end function

function Set-ResourceChecksum {
<#
    .SYNOPSIS
        Creates a resource's checksum file.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms','')]
    param (
        ## Path of file to create the checksum of
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Path
    )
    process {

        $checksumPath = '{0}.checksum' -f $Path;
        ## As it can take a long time to calculate the checksum, write it out to disk for future reference
        Write-Verbose -Message ($localized.CalculatingResourceChecksum -f $checksumPath);
        $fileHash = Get-FileHash -Path $Path -Algorithm MD5 -ErrorAction Stop | Select-Object -ExpandProperty Hash;
        Write-Verbose -Message ($localized.WritingResourceChecksum -f $fileHash, $checksumPath);
        $fileHash | Set-Content -Path $checksumPath -Force;

    } #end process
} #end function

function Set-ResourceDownload {
<#
    .SYNOPSIS
        Downloads a (web) resource and creates a MD5 checksum.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Checksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt32] $BufferSize = 64KB,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoChecksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $FeedCredential
        ##TODO: Support Headers and UserAgent
    )
    begin {

        $parentDestinationPath = Split-Path -Path $DestinationPath -Parent;
        [ref] $null = New-Directory -Path $parentDestinationPath;

    }
    process {

        if (-not $PSBoundParameters.ContainsKey('BufferSize')) {

            $systemUri = New-Object -TypeName System.Uri -ArgumentList @($uri);
            if ($systemUri.IsFile) {

                $BufferSize = 1MB;
            }
        }

        Write-Verbose -Message ($localized.DownloadingResource -f $Uri, $DestinationPath);
        Invoke-WebClientDownload -DestinationPath $DestinationPath -Uri $Uri -BufferSize $BufferSize -Credential $FeedCredential;

        if ($NoChecksum -eq $false) {

            ## Create the checksum file for future reference
            [ref] $null = Set-ResourceChecksum -Path $DestinationPath;
        }

    } #end process
} #end function

function Set-UnattendXml {
    <#
        .SYNOPSIS
           Creates a Windows unattended installation file and saves to disk.
    #>

        [CmdletBinding()]
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
        [OutputType([System.Xml.XmlDocument])]
        param (
            # Filename/path to save the unattend file as
            [Parameter(Mandatory, ValueFromPipeline)]
            [System.String] $Path,

            # Local Administrator Password
            [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.CredentialAttribute()]
            $Credential,

            # Computer name
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.String] $ComputerName,

            # Product Key
            [Parameter(ValueFromPipelineByPropertyName)]
            [ValidatePattern('^[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}$')]
            [System.String] $ProductKey,

            # Input Locale
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.String] $InputLocale = 'en-US',

            # System Locale
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.String] $SystemLocale = 'en-US',

            # User Locale
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.String] $UserLocale = 'en-US',

            # UI Language
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.String] $UILanguage = 'en-US',

            # Timezone
            [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
            [System.String] $Timezone, ##TODO: Validate timezones?

            # Registered Owner
            [Parameter(ValueFromPipelineByPropertyName)]
            [ValidateNotNull()]
            [System.String] $RegisteredOwner = 'Virtual Engine',

            # Registered Organization
            [Parameter(ValueFromPipelineByPropertyName)]
            [ValidateNotNull()]
            [System.String] $RegisteredOrganization = 'Virtual Engine',

            # TODO: Execute synchronous commands during OOBE pass as they only currently run during the Specialize pass
            ## Array of hashtables with Description, Order and Path keys
            [Parameter(ValueFromPipelineByPropertyName)]
            [System.Collections.Hashtable[]] $ExecuteCommand
        )
        process {

            [ref] $null = $PSBoundParameters.Remove('Path');
            $unattendXml = New-UnattendXml @PSBoundParameters;
            ## Ensure the parent Sysprep directory exists (#232)
            [ref] $null = New-Directory -Path (Split-Path -Path $Path -Parent);
            $resolvedPath = Resolve-PathEx -Path $Path;
            return $unattendXml.Save($resolvedPath);

        } #end process
    } #end function

function Start-DscConfigurationCompilation {
<#
    .SYNOPSIS
        Compiles an individual DSC configuration as a job.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')]
    [OutputType([System.IO.FileInfo])]
    param (
        ## DSC configuration file path, e.g. CONTROLLER.ps1
        [Parameter(Mandatory)]
        [System.String] $Configuration,

        ## Parameters to pass into the DSC configuration
        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $ConfigurationParameters,

        [Parameter()]
        [System.Management.Automation.SwitchParameter] $AsJob
    )
    begin {

        ## Compilation scriptblock started as a job
        $compileConfigurationScriptBlock = {
            param (
                ## DSC configuration path, e.g. CONTROLLER.ps1
                [Parameter(Mandatory)]
                [System.String] $ConfigurationPath,

                ## Parameters to pass into the configuration
                [Parameter()]
                [System.Collections.Hashtable] [ref] $ConfigurationParameters,

                [Parameter()]
                [System.String] $VerbosePreference
            )
            process {

                $ConfigurationPath = Resolve-Path -Path $ConfigurationPath;
                ## TODO Configuration name must currently match configuration file name
                $configurationName = (Get-Item -Path $ConfigurationPath).BaseName;

                ## Internal functions and localization strings are in a different PowerShell.exe
                ## process so we have to resort to hard coding (for the time being) :(
                Write-Verbose -Message ("Loading configuration '{0}'." -f $ConfigurationPath);
                $existingVerbosePreference = $VerbosePreference;

                ## Hide verbose output
                $VerbosePreference = 'SilentlyContinue';

                ## Import the configuration
                . $ConfigurationPath;

                $VerbosePreference = $existingVerbosePreference;
                Write-Verbose -Message ("Compiling configuration '{0}'." -f $ConfigurationPath);
                $VerbosePreference = 'SilentlyContinue';

                if ($ConfigurationParameters) {

                    ## Call the configuration (complile it) with supplied parameters
                    & $configurationName @ConfigurationParameters;
                }
                else {

                    ## Just call the configuration (compile it)
                    & $configurationName;
                }

                ## Restore verbose preference
                $VerbosePreference = $existingVerbosePreference;

            } #end process
        } #end $compileConfigurationScriptBlock

    }
    process {

        $configurationPath = Resolve-Path -Path $Configuration;

        $startJobParams = @{
            Name = $configurationPath;
            ScriptBlock = $compileConfigurationScriptBlock;
        }

        if ($PSBoundParameters.ContainsKey('ConfigurationParameters')) {

            $startJobParams['ArgumentList'] = @($configurationPath, $ConfigurationParameters, $VerbosePreference);
        }

        $job = Start-Job @startJobParams;
        $activity = $localized.CompilingConfiguration;

        if (-not $AsJob) {

            while ($Job.HasMoreData -or $Job.State -eq 'Running') {

                $percentComplete++;
                if ($percentComplete -gt 100) {
                    $percentComplete = 0;
                }
                Write-Progress -Id $job.Id -Activity $activity -Status $configurationPath -PercentComplete $percentComplete;
                Receive-Job -Job $Job
                Start-Sleep -Milliseconds 500;
            }

            Write-Progress -Id $job.Id -Activity $activity -Completed;
            $job | Receive-Job;

        }
        else {

            return $job;
        }

    } #end process
} #end function

function Start-ShellHWDetectionService {
<#
    .SYNOPSIS
        Starts the ShellHWDetectionService - if present!
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param ( )
    process {

        if (Get-Service -Name 'ShellHWDetection' -ErrorAction SilentlyContinue) {

            Start-Service -Name 'ShellHWDetection' -ErrorAction Ignore -Confirm:$false;
        }

    } #end process
} #end function Start-ShellHWDetectionService

function Stop-ShellHWDetectionService {
<#
    .SYNOPSIS
        Stops the ShellHWDetectionService - if present!
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param ( )
    process {

        if (Get-Service -Name 'ShellHWDetection' -ErrorAction SilentlyContinue) {

            Stop-Service -Name 'ShellHWDetection' -Force -ErrorAction Ignore -Confirm:$false;
        }

    } #end process
} #end function Stop-ShellHWDetectionService

function Test-ComputerName {
<#
    .SYNOPSIS
        Validates a computer name is valid.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateLength(1, 15)]
        [System.String] $ComputerName
    )
    process {

        $invalidMatch = '[~!@#\$%\^&\*\(\)=\+_\[\]{}\\\|;:.''",<>\/\?\s]';
        return ($ComputerName -inotmatch $invalidMatch);

    }
} #end function

function Test-ConfigurationPath  {
<#
    .SYNOPSIS
        Tests the specified path for a computer's .mof file.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Lab vm/node name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Defined .mof path
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [AllowEmptyString()]
        [System.String] $Path
    )
    process {

        $searchString = '{0}.*mof' -f $Name;
        $searchPath = Join-Path -Path $Path -ChildPath $searchString;
        Write-Debug -Message ("Searching configuration path '{0}'." -f $searchPath);

        if (Get-ChildItem -Path $searchPath -ErrorAction SilentlyContinue) {

            return $true;
        }

        return $false;

    } #end process
} #end function Test-ConfigurationPath

function Test-DscResourceModule {
<#
    .SYNOPSIS
        Tests whether the specified PowerShell module directory is a DSC resource.
    .DESCRIPTION
        The Test-DscResourceModule determines whether the specified path is a PowerShell DSC resource module. This is
        used to only copy DSC resources to a VM's VHD(X) file - not ALL modules!
    .NOTES
        THIS METHOD IS DEPRECATED IN FAVOUR OF THE NEW MODULE CACHE FUNCTIONALITY
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Path,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $ModuleName
    )
    process {

        ## This module contains a \DSCResources folder, but we don't want to enumerate this!
        if ($Path -notmatch "\\$($labDefaults.ModuleName)$") {

            Write-Debug -Message ('Testing for MOF-based DSC Resource ''{0}'' directory.' -f "$Path\DSCResources");
            if (Test-Path -Path "$Path\DSCResources" -PathType Container) {

                ## We have a WMF 4.0/MOF DSC resource module
                Write-Debug -Message ('Found MOF-based DSC resource ''{0}''.' -f $Path);
                return $true;
            }

            Write-Debug -Message ('Testing for Class-based DSC resource definition ''{0}''.' -f "$Path\$ModuleName.psm1");
            if (Test-Path -Path "$Path\$ModuleName.psm1") {

                $psm1Content = Get-Content -Path "$Path\$ModuleName.psm1";
                ## If there's a .psm1 file, check if it's a class-based DSC resource
                if ($psm1Content -imatch '^(\s*)\[DscResource\(\)\](\s*)$') {

                    ## File has a [DscResource()] declaration
                    Write-Debug -Message ('Found Class-based DSC resource ''{0}''.' -f $Path);
                    return $true;
                }
            }
        } #end if this module

        return $false;

    } #end process
} #end function

function Test-LabDscModule {
<#
    .SYNOPSIS
        Tests whether the ResourceName of the specified ModuleName can be located on the system.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory)]
        [System.String] $ModuleName,

        [Parameter()]
        [System.String] $ResourceName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $MinimumVersion
    )
    process {

        if (Get-LabDscModule @PSBoundParameters -ErrorAction SilentlyContinue) {

            return $true;
        }
        else {

            return $false;
        }

    } #end process
} #end function

function Test-LabDscResource {
<#
    .SYNOPSIS
        Tests the ResourceName DSC resource to determine if it's in the desired state.
    .DESCRIPTION
        The Test-LabDscResource cmdlet invokes the target $ResourceName\Test-TargetResource function using the supplied
        $Parameters hastable.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Name of the DSC resource to test
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $ResourceName,

        ## The DSC resource's Test-TargetResource parameter hashtable
        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $Parameters
    )
    process {

        $testTargetResourceCommand = 'Test-{0}TargetResource' -f $ResourceName;
        Write-Debug ($localized.InvokingCommand -f $testTargetResourceCommand);
        $Parameters.Keys | ForEach-Object {

            Write-Debug -Message ($localized.CommandParameter -f $_, $Parameters.$_);
        }

        try {

            $testDscResourceResult = & $testTargetResourceCommand @Parameters;
        }
        catch {

            ## No point writing warnings as failures will occur, i.e. "VHD not found"
            ## when a VM does not yet exist.
            Write-Warning -Message ($localized.DscResourceFailedError -f $testTargetResourceCommand, $_);
            $testDscResourceResult = $false;
        }

        if (-not $testDscResourceResult) {

            Write-Verbose -Message ($localized.TestFailed -f $testTargetResourceCommand);
        }

        return $testDscResourceResult;

    } #end process
} #end function

function TestModule {
<#
    .SYNOPSIS
        Tests whether an exising PowerShell module meets the minimum or required version
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $RequiredVersion,

        ## Catch all to be able to pass parameters via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)]
        $RemainingArguments
    )
    process {

        $module = Get-LabModule -Name $Name;
        if ($module) {

            $testLabModuleVersionParams = @{
                ModulePath = $module.Path;
            }

            if ($MinimumVersion) {
                $testLabModuleVersionParams['MinimumVersion'] = $MinimumVersion;
            }

            if ($RequiredVersion) {
                $testLabModuleVersionParams['RequiredVersion'] = $RequiredVersion;
            }

            return (Test-LabModuleVersion @testLabModuleVersionParams);
        }
        else {
            return $false;
        }

    } #end process
} #end function

function Test-LabModuleCache {
<#
    .SYNOPSIS
            Tests whether the requested PowerShell module is cached.
#>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    [OutputType([System.Boolean])]
    param (
        ## PowerShell module/DSC resource module name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $RequiredVersion,

        ## GitHub repository owner
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Owner,

        ## GitHub repository branch
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Branch,

        ## Source Filesystem module path
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Provider used to download the module
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')]
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')]
        [ValidateSet('PSGallery','GitHub','AzDo', 'FileSystem')]
        [System.String] $Provider,

        ## Lability PowerShell module info hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Hashtable] $Module,

        ## Catch all to be able to pass parameter via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)] $RemainingArguments
    )
    begin {

        ## Remove -RemainingArguments to stop it being passed on.
        [ref] $null = $PSBoundParameters.Remove('RemainingArguments');

    }
    process {

        $moduleFileInfo = Get-LabModuleCache @PSBoundParameters;
        return ($null -ne $moduleFileInfo);

    } #end process
} #end function

function Test-LabModuleVersion {
<#
    .SYNOPSIS
        Tests whether an exising PowerShell module meets the minimum or required version
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Path to the module's manifest file
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ModulePath,

        ## The minimum version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [ValidateNotNullOrEmpty()]
        [ValidateNotNullOrEmpty()]
        [System.Version] $MinimumVersion,

        ## The exact version of the module required
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')]
        [ValidateNotNullOrEmpty()]
        [System.Version] $RequiredVersion,

        ## Catch all to be able to pass parameters via $PSBoundParameters
        [Parameter(ValueFromRemainingArguments)] $RemainingArguments
    )
    process {

        try {

            Write-Verbose -Message ($localized.QueryingModuleVersion -f [System.IO.Path]::GetFileNameWithoutExtension($ModulePath));
            $moduleManifest = ConvertTo-ConfigurationData -ConfigurationData $ModulePath;
            Write-Verbose -Message ($localized.ExistingModuleVersion -f $moduleManifest.ModuleVersion);
        }
        catch {

            Write-Error "Oops $ModulePath"
        }

        if ($PSCmdlet.ParameterSetName -eq 'MinimumVersion') {

            return (($moduleManifest.ModuleVersion -as [System.Version]) -ge $MinimumVersion);
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'RequiredVersion') {

            return (($moduleManifest.ModuleVersion -as [System.Version]) -eq $RequiredVersion);
        }

    } #end process
} #end function Test-LabModuleVersion

function Test-LabMofModule {
<#
    .SYNOPSIS
        Tests whether the modules defined in a .mof file match the modules defined in the Lability
        configuration document.
#>

    [CmdletBinding()]
    param (
        ## List of resource modules defined in the Lability configuration file
        [Parameter(Mandatory)]
        [System.Collections.Hashtable[]] $Module,

        ## List of resource modules defined in the MOF file
        [Parameter(Mandatory)]
        [System.Collections.Hashtable[]] $MofModule
    )
    process {

        $isCompliant = $true;

        foreach ($mof in $mofModule) {

            foreach ($labModule in $Module) {

                if ($labModule.Name -eq $mof.Name) {

                    $isModuleDefined = $true;
                    $isVersionMismatch = $false;

                    if ($labModule.MinimumVersion) {

                        Write-Warning -Message ($localized.ModuleUsingMinimumVersionWarning -f $labModule.Name);
                        if ($labModule.MinimumVersion -ne $mof.RequiredVersion) {

                            $isVersionMismatch = $true;
                            $version = $labModule.MinimumVersion;

                        }
                    }
                    elseif ($labModule.RequiredVersion) {

                        if ($labModule.RequiredVersion -ne $mof.RequiredVersion) {

                            $isVersionMismatch = $true;
                            $version = $labModule.RequiredVersion;

                        }
                    }
                    else {

                        ## We have no way of knowing whether we have the right version :()
                        Write-Warning -Message ($locaized.ModuleMissingRequiredVerWarning -f $labModule.Name);
                    }

                    if ($isVersionMismatch) {

                        $isCompliant = $false;
                        Write-Warning -Message ($localized.MofModuleVersionMismatchWarning -f $labModule.Name, $mof.RequiredVersion, $version);
                    }
                }

            } #end foreach configuration module

            if (-not $isModuleDefined) {

                $isCompliant = $false;
                Write-Warning -Message ($localized.ModuleMissingDefinitionWarning -f $mof.Name);
            }

        } #end foreach Mof module

        return $isCompliant;

    } #end process
} #end function

function Test-LabNode {
<#
    .SYNOPSIS
        Tests whether the node name is defined in a configuration document.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## PowerShell module hashtable
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -NoEnumerateWildcardNode;
        return ($null -ne $node.NodeName);

    } #end process
} #end function

function Test-LabResourceIsLocal {
<#
    .SYNOPSIS
        Test whether a lab resource is available locally
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab resource Id to test.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId,

        ## Node's target resource folder
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $LocalResourcePath
    )
    process {

        $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId;

        if (($resource.Expand) -and ($resource.Expand -eq $true)) {

            ## Check the ResourceId folder is present
            $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resourceId;
            $resourceExtension = [System.IO.Path]::GetExtension($resource.Filename);

            switch ($resourceExtension) {
                '.iso' {
                    $isPresent = Test-Path -Path $resourcePath -PathType Container;
                }
                '.zip' {
                    $isPresent = Test-Path -Path $resourcePath -PathType Container;
                }
                default {
                    throw ($localized.ExpandNotSupportedError -f $resourceExtension);
                }
            }
        }
        else {

            $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resource.Filename;
            $isPresent = Test-Path -Path $resourcePath -PathType Leaf;
        }

        if ($isPresent) {

            Write-Verbose -Message ($localized.ResourceFound -f $resourcePath);
            return $true;
        }
        else {

            Write-Verbose -Message ($localized.ResourceNotFound -f $resourcePath);
            return $false;
        }

    } #end process
} #end function

function Test-LabSwitch {
<#
    .SYNOPSIS
        Tests the current configuration a virtual network switch.
    .DESCRIPTION
        Tests a virtual network switch configuration using the xVMSwitch DSC resource.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Switch Id/Name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $networkSwitch = Resolve-LabSwitch @PSBoundParameters;
        if (($null -ne $networkSwitch.IsExisting) -and ($networkSwitch.IsExisting -eq $true)) {

            ## The existing virtual switch may be of a type not supported by the DSC resource.
            return $true;
        }
        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch;
        return Test-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch;

    } #end process
} #end function

function Test-LabVirtualMachine {
<#
    .SYNOPSIS
        Tests the current configuration a virtual machine.
    .DESCRIPTION
        Tests the current configuration a virtual machine using the xVMHyperV DSC resource.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $SwitchName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Media,

        [Parameter(Mandatory)]
        [System.UInt64] $StartupMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MinimumMemory,

        [Parameter(Mandatory)]
        [System.UInt64] $MaximumMemory,

        [Parameter(Mandatory)]
        [System.Int32] $ProcessorCount,

        [Parameter()]
        [AllowNull()]
        [System.String[]] $MACAddress,

        [Parameter()]
        [System.Boolean] $SecureBoot,

        [Parameter()]
        [System.Boolean] $GuestIntegrationServices,

        [Parameter()]
        [System.Boolean] $AutomaticCheckpoints,

        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters;
        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM;

        try {

            ## xVMHyperV\Test-TargetResource throws if the VHD doesn't exist?
            return (Test-LabDscResource -ResourceName VM -Parameters $vmHyperVParams -ErrorAction SilentlyContinue);
        }
        catch {

            return $false;
        }

    } #end process
} #end function

function Test-LabVMDisk {
    <#
    .SYNOPSIS
        Checks whether the lab virtual machine disk (VHDX) is present.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## VM/node name
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Name,

        ## Media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Media,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String] $Ensure = 'Present'
    )
    process {

        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData;
        }
        else {

            $image = Get-LabImage -Id $Media;
        }

        $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;
        $vhd = @{
            Name       = $Name;
            Path       = Resolve-LabVMDiskPath -Name $Name -EnvironmentName $environmentName -Parent;
            ParentPath = $image.ImagePath;
            Generation = $image.Generation;
            Type       = 'Differencing';
        }

        if (-not $image) {

            ## This only occurs when a parent image is not available (#104).
            $vhd['MaximumSize'] = 136365211648; #127GB
            $vhd['Generation'] = 'VHDX';
        }

        Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD;
        $testDscResourceResult = Test-LabDscResource -ResourceName VHD -Parameters $vhd;

        if ($Ensure -eq 'Absent') {

            return -not $testDscResourceResult;
        }
        else {

            return $testDscResourceResult;
        }

    } #end process
} #end function

function Test-ResourceDownload {
<#
    .SYNOPSIS
        Tests if a web resource has been downloaded and whether the MD5 checksum is correct.
    .NOTES
        Based upon https://github.com/iainbrighton/cRemoteFile/blob/master/DSCResources/VE_RemoteFile/VE_RemoteFile.ps1
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Checksum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt32] $BufferSize = 64KB,

        ## Enables mocking terminating calls from Pester
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $ThrowOnError

        ##TODO: Support Headers and UserAgent
    )
    process {

        [ref] $null = $PSBoundParameters.Remove('ThrowOnError');
        $resource = Get-ResourceDownload @PSBoundParameters;
        $isCompliant = $true;

        if (-not (Test-Path -Path $DestinationPath -PathType Leaf)) {

            ## If the actual file doesn't exist return a failure! (#205)
            $isCompliant = $false;
        }
        elseif ([System.String]::IsNullOrEmpty($Checksum)) {

            Write-Verbose -Message ($localized.ResourceChecksumNotSpecified -f $DestinationPath);
            $isCompliant = $true;
        }
        elseif ($Checksum -eq $resource.Checksum) {

            Write-Verbose -Message ($localized.ResourceChecksumMatch -f $DestinationPath, $Checksum);
            $isCompliant = $true;
        }
        else {

            Write-Verbose -Message ($localized.ResourceChecksumMismatch  -f $DestinationPath, $Checksum);
            $isCompliant = $false;
        }

        if ($ThrowOnError -and (-not $isCompliant)) {

            throw ($localized.ResourceChecksumMismatchError -f $DestinationPath, $Checksum);
        }
        else {

            return $isCompliant;
        }

    } #end process
} #end function

function Test-WindowsBuildNumber {
    <#
        .SYNOPSIS
            Validates the host build meets the specified requirements.
    #>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Minimum Windows build number required
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Int32] $MinimumVersion
    )
    begin {

        if (($null -ne $PSVersion.PSEdition) -and ($PSVersion.PSEdition -eq 'Core')) {

            # New-NotSupportedException
        }
    }
    process {

        $buildNumber = $PSVersionTable.BuildVersion.Build;
        return $buildNumber -ge $MinimumVersion;

    } #end process
} #end function

function Write-Verbose {
<#
    .SYNOPSIS
        Proxy function for Write-Verbose that adds a timestamp and/or call stack information to the output.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets','')]
    param
    (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Message
    )
    process
    {
        if (-not [System.String]::IsNullOrEmpty($Message))
        {
            $verboseMessage = Get-FormattedMessage -Message $Message
            Microsoft.PowerShell.Utility\Write-Verbose -Message $verboseMessage
        }
    }
}

function Write-Warning {
<#
    .SYNOPSIS
        Proxy function for Write-Warning that adds a timestamp and/or call stack information to the output.
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets','')]
    param
    (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Message
    )
    process
    {
        if (-not [System.String]::IsNullOrEmpty($Message))
        {
            $warningMessage = Get-FormattedMessage -Message $Message
            Microsoft.PowerShell.Utility\Write-Warning -Message $warningMessage
        }
    }
}


# SIG # Begin signature block
# MIIuugYJKoZIhvcNAQcCoIIuqzCCLqcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAUsv/rU8jslgsr
# fQ2F5dLDvVidQp2wM4bh3FoImvzfVKCCE6QwggWQMIIDeKADAgECAhAFmxtXno4h
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggdYMIIFQKADAgECAhAIfHT3o/FeY5ksO94AUhTmMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjMxMDE4MDAwMDAwWhcNMjYxMjE2MjM1OTU5WjBgMQsw
# CQYDVQQGEwJHQjEPMA0GA1UEBxMGTG9uZG9uMR8wHQYDVQQKExZWaXJ0dWFsIEVu
# Z2luZSBMaW1pdGVkMR8wHQYDVQQDExZWaXJ0dWFsIEVuZ2luZSBMaW1pdGVkMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtyhrsCMi6pgLcX5sWY7I09dO
# WKweRHfDwW5AN6ffgLCYO9dqWWxvqu95FqnNVRyt1VNzEl3TevKVhRE0GGdirei3
# VqnFFjLDwD2jHhGY8qoSYyfffj/WYq2DkvNI62C3gUwSeP3FeqKRalc2c3V2v4jh
# yEYhrgG3nfnWQ/Oq2xzuiCqHy1E4U+IKKDtrXls4JX2Z4J/uAHZIAyKfrcTRQOhZ
# R4ZS1cQkeSBU9Urx578rOmxL0si0GAoaYQC49W7OimRelbahxZw/R+f5ch+C1ycU
# CpeXLg+bFhpa0+EXnkGidlILZbJiZJn7qvMQTZgipQKZ8nhX3rtJLqTeodPWzcCk
# tXQUy0q5fxhR3e6Ls7XQesq/G2yMcCMTCd6eltze0OgabvL6Xkhir5lATHaJtnmw
# FlcKzRr1YXK1k1D84hWfKSAdUg8T1O72ztimbqFLg6WoC8M2qqsHcm2DOc9hM3i2
# CWRpegikitRvZ9i1wkA9arGh7+a7UD+nLh2hnGmO06wONLNqABOEn4JOVnNrQ1gY
# eDeH9FDx7IYuAvMsfXG9Bo+I97TR2VfwDAx+ccR+UQLON3aQyFZ3BefYnvUu0gUR
# ikEAnAS4Jnc3BHizgb0voz0iWRDjFoTTmCmrInCVDGc+5KMy0xyoUwdQvYvRGAWB
# 61OCWnXBXbAEPniTZ80CAwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hf
# EYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRuAv58K4EDYLmb7WNcxt5+r4NfnzA+BgNV
# HSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj
# ZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMD
# MIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEu
# Y3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCBlAYIKwYB
# BQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv
# bTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQw
# CQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnXMg6efkBrwLIvd1Xmuh0dam
# 9FhUtDEj+P5SIqdP/U4veOv66NEQhBHLbW2Dvrdm6ec0HMj9b4e8pt4ylKFzHIPj
# fpuRffHVR9JQSx8qpryN6pP49DfCkAYeZGqjY3pGRzd/xQ0cfwcuYbUF+vwVk7tj
# q8c93VHCM0rb5M4N2hD1Ze1pvZxtaf9QnFKFzgXZjr02K6bswQc2+n5jFCp7zV1f
# KTstyb68rhSJBWKK1tMeFk6a6HXr5buTD3skluC0oyPmD7yAd97r2owjDMEveEso
# kADP/z7XQk7wqbwbpi4W6Uju2qHK/9UUsVRF5KTVEAIzVw2V1Aq/Jh3JuSV7b7C1
# 4CghNekltBb+w7YVp8/IFcj7axqnpNQ/+f7RVc3A5hyjV+MkoSwn8Sg7a7hn6SzX
# jec/TfRVvWCmG94MQHko+6206uIXrZnmQ6UQYFyOHRlyKDEozzkZhIcVlsZloUjL
# 3FZ5V/l8TIIzbc3bkEnu4iByksNvRxI6c5264OLauYlWv50ZUPwXmZ9gX8bs3BqZ
# avbGUrOW2PIjtWhtvs4zHhMBCoU1Z0OMvXcF9rUDqefmVCZK46xz3DGKVkDQnxY6
# UWQ3GL60/lEzju4YS99LJVQks2UGmP6LAzlEZ1dnGqi1aQ51OidCYEs39B75PsvO
# By2iAR8pBVi/byWBypExghpsMIIaaAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBH
# NCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAh8dPej8V5j
# mSw73gBSFOYwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgJ1MnsiUml0MYeKU2x4f1LCah
# 2RlhmnMHik5x3n21ujIwDQYJKoZIhvcNAQEBBQAEggIATWv5r5Bh8q9HQh5Hropm
# WOCu2axcd2COCCMdoXeRPnStk51E/xroNXWWkxBQR/KmIOg/Ags9ks0EVyPB4Cav
# q46j7qI8VwHRdoK6xNvwgWd5CdmKwhf05GPNNqymr4Wx/5bhSf70F3yjBSJQ/axS
# EmwMPglFtDvIZmHXKPT/krW4/09+9P1++2JA2CSEn36JjJ9vYh/CPFz2eScJ/ifJ
# G2mUiH/6WGm4zFSCc0yD9Bl9DP6blUM5wmfBvtO7hlq0hi7I0H5SUVmXYgGq1ysw
# fJKrUTmxQ1d4iXKnsu740NDlWhvY4KSBk+uXMt2RsK+O4oQ2vryhaf4eeFN7Th6L
# 86gfbclqhWzLrHOYVpoyho5yKPM4N94eU9qAdmLSvjApob573bVXS1W15orERlrz
# jsJ2p/QXoeqE45Qo6TKs3FOP66T+KjxCo7XvG0SFvpHVpz8Z2BIXP/yQbbyDk/po
# 5KBXEVcjm3MuSwnnggKCRW+5HZwwUNR+yeSAzBEy0W9htJ7l9Ht6A7loKcsnaDWC
# A4Jtv/sfd5iyjh3IUFBKlRl0t7aHbc5nhtRbqcJ2v/lLGQj+7i7fEx6RvHjD4fSq
# BYSNKiTVECfDjsz0QNo1zoYArU7dUbrXhUYm2LYPsiI1bZmbrLcrsUqj5AIDNj3N
# XQqaGd+cDT9eydaGsJszGDehghc5MIIXNQYKKwYBBAGCNwMDATGCFyUwghchBgkq
# hkiG9w0BBwKgghcSMIIXDgIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ
# EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQg9dWq1U0G
# XYz1QR2AxCp3pjTeAaZsKEiVrrY8igg7Y5oCEBYMj68Nkyg2lXtW6GX+wyQYDzIw
# MjUwNDA4MTYyNjEyWqCCEwMwgga8MIIEpKADAgECAhALrma8Wrp/lYfG+ekE4zME
# MA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy
# dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI
# QTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjQwOTI2MDAwMDAwWhcNMzUxMTI1MjM1
# OTU5WjBCMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxIDAeBgNVBAMT
# F0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEAvmpzn/aVIauWMLpbbeZZo7Xo/ZEfGMSIO2qZ46XB/QowIEMSvgjE
# dEZ3v4vrrTHleW1JWGErrjOL0J4L0HqVR1czSzvUQ5xF7z4IQmn7dHY7yijvoQ7u
# jm0u6yXF2v1CrzZopykD07/9fpAT4BxpT9vJoJqAsP8YuhRvflJ9YeHjes4fduks
# THulntq9WelRWY++TFPxzZrbILRYynyEy7rS1lHQKFpXvo2GePfsMRhNf1F41nyE
# g5h7iOXv+vjX0K8RhUisfqw3TTLHj1uhS66YX2LZPxS4oaf33rp9HlfqSBePejlY
# eEdU740GKQM7SaVSH3TbBL8R6HwX9QVpGnXPlKdE4fBIn5BBFnV+KwPxRNUNK6lY
# k2y1WSKour4hJN0SMkoaNV8hyyADiX1xuTxKaXN12HgR+8WulU2d6zhzXomJ2Ple
# I9V2yfmfXSPGYanGgxzqI+ShoOGLomMd3mJt92nm7Mheng/TBeSA2z4I78JpwGpT
# RHiT7yHqBiV2ngUIyCtd0pZ8zg3S7bk4QC4RrcnKJ3FbjyPAGogmoiZ33c1HG93V
# p6lJ415ERcC7bFQMRbxqrMVANiav1k425zYyFMyLNyE1QulQSgDpW9rtvVcIH7Wv
# G9sqYup9j8z9J1XqbBZPJ5XLln8mS8wWmdDLnBHXgYly/p1DhoQo5fkCAwEAAaOC
# AYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM
# MAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAf
# BgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQUn1csA3cO
# KBWQZqVjXu5Pkh92oFswWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFt
# cGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVT
# dGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAPa0eH3aZW+M4hBJH2UOR
# 9hHbm04IHdEoT8/T3HuBSyZeq3jSi5GXeWP7xCKhVireKCnCs+8GZl2uVYFvQe+p
# PTScVJeCZSsMo1JCoZN2mMew/L4tpqVNbSpWO9QGFwfMEy60HofN6V51sMLMXNTL
# fhVqs+e8haupWiArSozyAmGH/6oMQAh078qRh6wvJNU6gnh5OruCP1QUAvVSu4kq
# VOcJVozZR5RRb/zPd++PGE3qF1P3xWvYViUJLsxtvge/mzA75oBfFZSbdakHJe2B
# VDGIGVNVjOp8sNt70+kEoMF+T6tptMUNlehSR7vM+C13v9+9ZOUKzfRUAYSyyEmY
# tsnpltD/GWX8eM70ls1V6QG/ZOB6b6Yum1HvIiulqJ1Elesj5TMHq8CWT/xrW7tw
# ipXTJ5/i5pkU5E16RSBAdOp12aw8IQhhA/vEbFkEiF2abhuFixUDobZaA0VhqAsM
# HOmaT3XThZDNi5U2zHKhUs5uHHdG6BoQau75KiNbh0c+hatSF+02kULkftARjsyE
# pHKsF7u5zKRbt5oK5YGwFvgc4pEVUNytmB3BpIiowOIIuDgP5M9WArHYSAR16gc0
# dP2XdkMEP5eBsX7bf/MGN4K3HP50v/01ZHo/Z5lGLvNwQ7XHBx1yomzLP8lx4Q1z
# ZKDyHcp4VQJLu2kWTsKsOqQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5b
# MA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5
# NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG
# A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh
# bXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPB
# PXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/
# nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLc
# Z47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mf
# XazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3N
# Ng1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yem
# j052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g
# 3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD
# 4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDS
# LFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwM
# O1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU
# 7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPO
# vxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQ
# TGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWae
# LJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPBy
# oyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfB
# wWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8l
# Y5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/
# O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbb
# bxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3
# OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBl
# dkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt
# 1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwF
# ADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
# ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElE
# IFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYD
# VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln
# aWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKn
# JS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/W
# BTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHi
# LQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhm
# V1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHE
# tWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6
# MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mX
# aXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZ
# xd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfh
# vbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvl
# EFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn1
# 5GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
# HQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4Ix
# LVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAA
# MA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzs
# hV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre
# +i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8v
# C6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38
# dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNr
# Iv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEw
# dzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNV
# BAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1w
# aW5nIENBAhALrma8Wrp/lYfG+ekE4zMEMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqG
# SIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwNDA4MTYy
# NjEyWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZb
# RTAvBgkqhkiG9w0BCQQxIgQgbheeXUcGZKNCwxehXtAFYgHG0YOTNNAGgcYfOK5O
# wxswNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13Pb
# BdZluQWTmEOPmtswDQYJKoZIhvcNAQEBBQAEggIAqkuEmgOeTRUgswqjBDkMyLx5
# h5Qa3fyYaxLFE1tXnMCcKEvjyNzmZq4PTEI7nMpSaIeKv/8ZVFOJhlh+51bbueNK
# VEP4wHlyHQzvt0oY32iRlUT7yxisZ1cvWMNymTj5pqibAYEo1XpcHEvCR8aU95YU
# yvdVFaMb6KePaZxFtU0sCzKMhCzlQagSKUiN554J8oqBAZ5PVAeQzzu9OwOuksCc
# +x7DQNcsTVHJMUFpAQFknHVo7QunI/QUKmTU3c5uochBHyPj3iXBCo5m/V5kpZMo
# FyIE4IzvqTUVCV1daf54dlldhwJxZfF+GN663YiQo0gu1biQAnQd/ylqjxnWlCEv
# MR84DJSdH7EfySereS56z9OMXa7/YM1zdt9agPgXVvA9tANSH0pzwTJQcIXdb98g
# NfTlezRpoWlFbguRrLzDR5D2/9Q060vdEutpsn5l8EH3mEBsFkNgKOmAulds89wJ
# rFgaWUHexhCXyh7LK73nbF0X2vByXDYdErjlskXDiX4rp/54yiCe3Y92tN3aXkz4
# 7/rid6KRnnwUE5EdnJEbnyL8e5le1qnkQdhzyV4enaFcBAJyA0FJ3/RrgMJ86yLN
# 3IH8nhPycRX4+IUldaDFufh44v7VmknAjvhoB02QYr+NVzb/urBKkhCtq0hIPUGI
# TVJd5C2B0WcNGxbftuE=
# SIG # End signature block