Src/Public/Public.ps1

function Checkpoint-Lab {
<#
    .SYNOPSIS
        Snapshots all lab VMs in their current configuration.
    .DESCRIPTION
        The Checkpoint-Lab creates a VM checkpoint of all the nodes defined in a PowerShell DSC configuration document.
        When creating the snapshots, they will be created using the snapshot name specified.

        All virtual machines should be powered off when the snapshot is taken to ensure that the machine is in a
        consistent state. If VMs are powered on, an error will be generated. You can override this behaviour by
        specifying the -Force parameter.

        WARNING: If the -Force parameter is used, the virtual machine snapshot(s) may be in an inconsistent state.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document.
    .PARAMETER SnapshotName
        Specifies the virtual machine snapshot name that applied to each VM in the PowerShell DSC configuration
        document. This name is used to restore a lab configuration. It can contain spaces, but is not recommended.
    .PARAMETER Force
        Forces virtual machine snapshots to be taken - even if there are any running virtual machines.
    .LINK
        Restore-Lab
        Reset-Lab
#>

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

        ## Snapshot name
        [Parameter(Mandatory)]
        [Alias('Name')]
        [System.String] $SnapshotName,

        ## Force snapshots if virtual machines are on
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {

        $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object {
             Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData;
        };

        $runningNodes = Hyper-V\Get-VM -Name $nodes.NodeDisplayName | Where-Object { $_.State -ne 'Off' }

        if ($runningNodes -and $Force) {

            New-LabVMSnapshot -Name $nodes.NodeDisplayName -SnapshotName $SnapshotName;
        }
        elseif ($runningNodes) {

            foreach ($runningNode in $runningNodes) {

                Write-Error -Message ($localized.CannotSnapshotNodeError -f $runningNode.Name);
            }
        }
        else {

            $nodesDisplayString = [System.String]::Join(', ', $nodes.NodeDisplayName);
            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Checkpoint-Lab', $node.Name;
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVirtualMachineSnapshot -f $nodesDisplayString, $SnapshotName);
            if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                New-LabVMSnapshot -Name $nodes.NodeDisplayName -SnapshotName $SnapshotName;
            }
        }

    } #end process
} #end function Checkpoint-Lab

function Clear-LabModuleCache {
 <#
    .SYNOPSIS
        Removes all cached modules from the Lability module cache.
    .DESCRIPTION
        The Clear-LabModuleCache removes all cached PowerShell module and DSC resource modules stored in Lability's
        internal cache.
    .PARAMETER Force
        Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or
        read-only aliases or variables.
    .EXAMPLE
        Clear-LabModuleCache -Force

        Removes all previously downloaded/cached PowerShell modules and DSC resources.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param (
        [Parameter(ValueFromPipeline)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {

        $moduleCachePath = (Get-ConfigurationData -Configuration 'Host').ModuleCachePath;
        $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Clear-LabModuleCache', $moduleCachePath;
        $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingDirectory -f $moduleCachePath);
        $shouldProcessWarning = $localized.ShouldProcessWarning;
        if (($Force) -or
            ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $shouldProcessWarning))) {

                Remove-Item -Path $moduleCachePath -Recurse -Force:$Force;
                $moduleCachePathParent = Split-Path -Path $moduleCachePath -Parent;
                $moduleCachePathName = Split-Path -Path $moduleCachePath -Leaf;

                $newItemParams = @{
                    Path = $moduleCachePathParent;
                    Name = $moduleCachePathName;
                    ItemType = 'Directory';
                    Force = $true;
                    Confirm = $false;
                }
                [ref] $null = New-Item @newItemParams;
            }

    } #end process
} #end function

function Clear-ModulePath {
<#
    .SYNOPSIS
        Removes all PowerShell modules installed in a given scope.
    .DESCRIPTION
        The Clear-ModulePath removes all existing PowerShell module and DSC resources from either the current user's
        or the local machine's module path.
    .PARAMETER Scope
        Specifies the scope to install module(s) in to. The default value is 'CurrentUser'.
    .PARAMETER Force
        Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or
        read-only aliases or variables.
    .EXAMPLE
        Clear-ModulePath -Scope CurrentUser

        Removes all PowerShell modules and DSC resources from the current user's module path.
    .EXAMPLE
        Clear-ModulePath -Scope AllUsers -Force

        Removes all PowerShell modules and DSC resources from the local machine's module path.
    .NOTES
        USE WITH CAUTION! Not sure thsi should even be in this module?
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('AllUsers','CurrentUser')]
        [System.String] $Scope = 'CurrentUser',

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

        if ($Scope -eq 'AllUsers') {

            $localizedProgramFiles = Resolve-ProgramFilesFolder -Path $env:SystemRoot;
            $modulePath = Join-Path -Path $localizedProgramFiles -ChildPath 'WindowsPowerShell\Modules';
        }
        elseif ($Scope -eq 'CurrentUser') {

            $userDocuments = [System.Environment]::GetFolderPath('MyDocuments');
            $modulePath = Join-Path -Path $userDocuments -ChildPath 'WindowsPowerShell\Modules';
        }

        if (Test-Path -Path $modulePath) {

            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Clear-ModulePath', $moduleCachePath;
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingDirectory -f $moduleCachePath);
            $shouldProcessWarning = $localized.ShouldProcessWarning;
            if (($Force) -or
                ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $shouldProcessWarning))) {

                ## The -Force on Remove-Item supresses the confirmation :()
                Remove-Item -Path $modulePath -Recurse -Force:$Force;
            }
        }
        else {

            Write-Verbose -Message ($localized.PathDoesNotExist -f $modulePath);
        }

    } #end process
} #end function

function Export-LabHostConfiguration {
<#
    .SYNOPSIS
        Backs up the current lab host configuration.
    .LINK
        Import-LabHostConfiguration
#>

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

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

        ## Do not overwrite an existing file
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoClobber
    )
    process {

        $now = [System.DateTime]::UtcNow;
        $configuration = [PSCustomObject] @{
            Author = $env:USERNAME;
            GenerationHost = $env:COMPUTERNAME;
            GenerationDate = '{0} {1}' -f $now.ToShortDateString(), $now.ToString('hh:mm:ss');
            ModuleVersion = (Get-Module -Name $labDefaults.ModuleName).Version.ToString();
            HostDefaults = [PSCustomObject] (Get-ConfigurationData -Configuration Host);
            VMDefaults = [PSCustomObject] (Get-ConfigurationData -Configuration VM);
            CustomMedia = @([PSCustomObject] (Get-ConfigurationData -Configuration CustomMedia));
        }

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

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

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

        if ($NoClobber -and (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) {

            $errorMessage = $localized.FileAlreadyExistsError -f $Path;
            $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage;
            $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists;
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileExists', $errorCategory, $Path;
            $PSCmdlet.WriteError($errorRecord);
        }
        else {

            $verboseMessage = Get-FormattedMessage -Message ($localized.ExportingConfiguration -f $labDefaults.ModuleName, $Path);
            $operationMessage = $localized.ShouldProcessOperation -f 'Export', $Path;
            $setContentParams = @{
                Path = $Path;
                Value = ConvertTo-Json -InputObject $configuration -Depth 5;
                Force = $true;
                Confirm = $false;
            }
            if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) {

                try {

                    ## Set-Content won't actually throw a terminating error?!
                    Set-Content @setContentParams -ErrorAction Stop;
                    Write-Output -InputObject (Get-Item -Path $Path);
                }
                catch {

                    throw $_;
                }
            }

        }

    } #end process
} #end function


function Export-LabImage {
<#
    .SYNOPSIS
        Exports a lab image (.vhdx file) and creates Lability custom media registration document (.json file).
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms','')]
    param (
        ## Lab media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Id,

        # Specifies the export path location.
        [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias("PSPath")]
        [System.String] $Path,

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

        ## Force the re(creation) of the master/parent image
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

        ## Do not calculate media checksum.
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $NoChecksum,

        ## Do not create Lability custom media regstration (json) file.
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $NoRegistrationDocument
    )
    begin {

        try {

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

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

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

            $pathItem = Get-Item -Path $Path
            if ($pathItem -isnot [System.IO.DirectoryInfo]) {
                throw "not a directory path"
            }
        }
        catch {

            Write-Error -ErrorRecord $_
        }

    }
    process {

        foreach ($mediaId in $Id) {

            try {

                $media = Resolve-LabMedia -Id $mediaId -ErrorAction Stop
                $image = Get-LabImage -Id $mediaId -ErrorAction Stop

                ## Copy vhd/x file to destination path
                $imageItem = Get-Item -Path $image.ImagePath
                $destinationVhdxPath = Join-Path -Path $Path -ChildPath $imageItem.Name
                if (-not (Test-Path -Path $destinationVhdxPath -PathType Leaf) -or $Force) {

                    Write-Verbose -Message ($localized.ExportingImage -f $image.ImagePath, $destinationVhdxPath)
                    Copy-Item -Path $image.ImagePath -Destination $destinationVhdxPath -Force

                    if (-not $NoRegistrationDocument) {

                        ## Create media registration json
                        $registerLabMedia = [ordered] @{
                            Id              = $media.Id
                            Filename        = $imageItem.Name
                            Description     = $media.Description
                            OperatingSystem = $media.OperatingSystem
                            Architecture    = $media.Architecture
                            MediaType       = 'VHD'
                            Uri             = $destinationVhdxPath -as [System.Uri]
                            CustomData      = $media.CustomData
                        }

                        if (-not $NoChecksum) {
                            Write-Verbose ($localized.CalculatingResourceChecksum -f $image.ImagePath)
                            $checksum = (Get-FileHash -Path $image.ImagePath -Algorithm MD5).Hash
                            $registerLabMedia['Checksum'] = $checksum
                        }

                        $registerLabMediaFilename = '{0}.json' -f $media.Id
                        $registerLabMediaPath = Join-Path -Path $Path -ChildPath $registerLabMediaFilename
                        Write-Verbose -Message ($localized.ExportingImageRegistrationFile -f $registerLabMediaPath)
                        ConvertTo-Json -InputObject $registerLabMedia |
                            Out-File -FilePath $registerLabMediaPath -Encoding ASCII -Force
                    }
                }
                else {

                    $errorMessage = $localized.FileAlreadyExistsError -f $destinationVhdxPath;
                    $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage;
                    $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists;
                    $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileExists', $errorCategory, $destinationVhdxPath;
                    $PSCmdlet.WriteError($errorRecord);
                }
            }
            catch {

                Write-Error -ErrorRecord $_
            }
        }
    }
}

function Get-LabHostConfiguration {
<#
    .SYNOPSIS
        Retrieves the current lab host's configuration default values.
    .LINK
        Test-LabHostConfiguration
        Start-LabHostConfiguration
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param ( )
    process {

        $labHostSetupConfiguation = Get-LabHostSetupConfiguration;
        foreach ($configuration in $labHostSetupConfiguation) {

            $importDscResourceParams = @{
                ModuleName = $configuration.ModuleName;
                ResourceName = $configuration.ResourceName;
                Prefix = $configuration.Prefix;
                UseDefault  = $configuration.UseDefault;
            }
            Import-LabDscResource @importDscResourceParams;
            $resource = Get-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters;
            $resource['Resource'] = $configuration.ResourceName;
            Write-Output -InputObject ([PSCustomObject] $resource);

        }

    } #end process
} #end function

function Get-LabHostDefault {
<#
    .SYNOPSIS
        Gets the lab host's default settings.
    .DESCRIPTION
        The Get-LabHostDefault cmdlet returns the lab host's current settings.
    .LINK
        Set-LabHostDefault
        Reset-LabHostDefault
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param ( )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;

        ## Create/update Lability environment variables
        $env:LabilityConfigurationPath = $hostDefaults.ConfigurationPath;
        $env:LabilityDifferencingVhdPath = $hostDefaults.DifferencingVhdPath;
        $env:LabilityHotfixPath = $hostDefaults.HotfixPath;
        $env:LabilityIsoPath = $hostDefaults.IsoPath;
        $env:LabilityModuleCachePath = $hostDefaults.ModuleCachePath;
        $env:LabilityResourcePath = $hostDefaults.ResourcePath;
        $env:LabilityDismPath = $hostDefaults.DismPath;
        $env:LabilityRepositoryUri = $hostDefaults.RepositoryUri;
        $env:LabilityParentVhdPath = $hostDefaults.ParentVhdPath;

        return $hostDefaults;

    } #end process
} #end function

function Get-LabImage {
<#
    .SYNOPSIS
        Gets master/parent disk image.
    .DESCRIPTION
        The Get-LabImage cmdlet returns current master/parent disk image properties.
    .PARAMETER Id
        Specifies the media Id of the image to return. If this parameter is not specified, all images are returned.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document that contains the required media definition.
    .EXAMPLE
        Get-LabImage

        Returns all current lab images on the host.
    .EXAMPLE
        Get-LabImage -Id 2016_x64_Datacenter_EN_Eval

        Returns the '2016_x64_Datacenter_EN_Eval' lab image properties, if available.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id,

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

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $parentVhdPath = Resolve-PathEx -Path $hostDefaults.ParentVhdPath;

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

            ## We have an Id. so resolve that
            try {

                $labMedia = Resolve-LabMedia @PSBoundParameters;
            }
            catch {

                $labMedia = $null;
            }
        }
        else {
            ## Otherwise return all media
            $labMedia = Get-LabMedia;
        }

        foreach ($media in $labMedia) {

            $differencingVhdPath = '{0}.vhdx' -f $media.Id;
            if ($media.MediaType -eq 'VHD') {

                $differencingVhdPath = $media.Filename;
            }

            $imagePath = Join-Path -Path $parentVhdPath -ChildPath $differencingVhdPath;
            if (Test-Path -Path $imagePath -PathType Leaf) {

                $imageFileInfo = Get-Item -Path $imagePath;
                $diskImage = Storage\Get-DiskImage -ImagePath $imageFileInfo.FullName;
                $labImage = [PSCustomObject] @{
                    Id = $media.Id;
                    Attached = $diskImage.Attached;
                    ImagePath = $diskImage.ImagePath;
                    LogicalSectorSize = $diskImage.LogicalSectorSize;
                    BlockSize = $diskImage.BlockSize;
                    FileSize = $diskImage.FileSize;
                    Size = $diskImage.Size;
                    Generation = ($imagePath.Split('.')[-1]).ToUpper();
                }

                $labImage.PSObject.TypeNames.Insert(0, 'VirtualEngine.Lability.Image');
                Write-Output -InputObject $labImage;
            }

        } #end foreach media

    } #end process
} #end function Get-LabImage

function Get-LabMedia {
<#
    .SYNOPSIS
        Gets registered lab media.
    .DESCRIPTION
        The Get-LabMedia cmdlet retrieves all built-in and registered custom media.
    .PARAMETER Id
        Specifies the specific media Id to return.
    .PARAMETER CustomOnly
        Specifies that only registered custom media are returned.
    .PARAMETER Legacy
        Specifies that legacy evaluation media definition(s) are returned. These deprecated media definitions can be
        reregistered using the Register-LabMedia cmdlet with the -Legacy switch.
    .EXAMPLE
        Get-LabMedia -Legacy

        Id Arch Media Description
        -- ---- ----- -----------
        WIN10_x64_Enterprise_1709_EN_Eval x64 ISO Windows 10 64bit Enterprise 1709 English Evaluation
        WIN10_x64_Enterprise_1803_EN_Eval x64 ISO Windows 10 64bit Enterprise 1804 English Evaluation
        WIN10_x64_Enterprise_1809_EN_Eval x64 ISO Windows 10 64bit Enterprise 1809 English Evaluation
        WIN10_x64_Enterprise_1903_EN_Eval x64 ISO Windows 10 64bit Enterprise 1903 English Evaluation
        WIN10_x64_Enterprise_LTSB_2016_EN_Eval x64 ISO Windows 10 64bit Enterprise LTSB 2016 English Evaluation
        WIN10_x86_Enterprise_1709_EN_Eval x86 ISO Windows 10 32bit Enterprise 1709 English Evaluation
        WIN10_x86_Enterprise_1803_EN_Eval x86 ISO Windows 10 32bit Enterprise 1804 English Evaluation
        WIN10_x86_Enterprise_1809_EN_Eval x86 ISO Windows 10 32bit Enterprise 1809 English Evaluation
        WIN10_x86_Enterprise_1903_EN_Eval x86 ISO Windows 10 32bit Enterprise 1903 English Evaluation
        WIN10_x86_Enterprise_LTSB_2016_EN_Eval x86 ISO Windows 10 32bit Enterprise LTSB 2016 English Evaluation

        Returns deprecated/previous media definitions.
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param
    (
        ## Media ID or alias to return
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id,

        ## Only return custom media
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Default')]
        [System.Management.Automation.SwitchParameter] $CustomOnly,

        ## Only return legacy media
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Legacy')]
        [System.Management.Automation.SwitchParameter] $Legacy
    )
    process
    {
        if ($PSBoundParameters.ContainsKey('Legacy'))
        {
            $legacyMediaPath = Resolve-ConfigurationDataPath -Configuration LegacyMedia -IsDefaultPath
            $media = Get-ChildItem -Path $legacyMediaPath | ForEach-Object {
                        Get-Content -Path $_.FullName | ConvertFrom-Json
                    }

            if ($Id)
            {
                $media = $media | Where-Object { $_.Id -eq $Id };
            }
        }
        else
        {
            ## Retrieve built-in media
            if (-not $CustomOnly)
            {
                $defaultMedia = Get-ConfigurationData -Configuration Media;
            }
            ## Retrieve custom media
            $customMedia = @(Get-ConfigurationData -Configuration CustomMedia);
            if (-not $customMedia)
            {
                $customMedia = @();
            }

            ## Are we looking for a specific media Id
            if ($Id)
            {
                ## Return the custom media definition first (if it exists)
                $media = $customMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id };
                if ((-not $media) -and (-not $CustomOnly))
                {
                    ## We didn't find a custom media entry, return a default entry (if it exists)
                    $media = $defaultMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id };
                }
            }
            else
            {
                ## Return all media
                $media = $customMedia;
                if (-not $CustomOnly)
                {
                    foreach ($mediaEntry in $defaultMedia)
                    {
                        ## Determine whether the media is present in the custom media, i.e. make sure
                        ## we don't override a custom entry with the default one.
                        $defaultMediaEntry = $customMedia | Where-Object { $_.Id -eq $mediaEntry.Id }
                        ## If not, add it to the media array to return
                        if (-not $defaultMediaEntry)
                        {
                            $media += $mediaEntry;
                        }
                    } #end foreach default media
                } #end if not custom only
            }
        }

        foreach ($mediaObject in $media)
        {
            $mediaObject.PSObject.TypeNames.Insert(0, 'VirtualEngine.Lability.Media');
            Write-Output -InputObject $mediaObject;
        }

    } #end process
} #end function

function Get-LabStatus {
<#
    .SYNOPSIS
        Queries computers' LCM state to determine whether an existing DSC configuration has applied.
    .EXAMPLE
        Get-LabStatus -ComputerName CONTROLLER, XENAPP

        Queries the CONTROLLER and XENAPP computers' LCM state using the current user credential.
    .EXAMPLE
        Get-LabStatus -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential)

        Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query the LCM state.
    .EXAMPLE
        Get-LabStatus -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential)

        Prompts for credentials to connect to the computers defined in the DSC configuration document (.psd1) and query
        the LCM state.
    .EXAMPLE
        Get-LabStatus -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential)

        Prompts for credentials to connect to the computers by their IPAddress node property as defined in the DSC
        configuration document (.psd1) and query the LCM state.
#>

    [CmdletBinding()]
    param (
        ## Connect to the computer name(s) specified.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')]
        [System.String[]]
        $ComputerName,

        ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's
        ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [System.String]
        $PreferNodeProperty,

        ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN.
        ## The complete identifier for the remote endpoint is in the following format:
        ##
        ## <transport>://<server>:<port>/<ApplicationName>
        ##
        ## For example: `http://server01:8080/WSMAN`
        ##
        ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the
        ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed
        ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell.
        ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ApplicationName,

        ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are:
        ##
        ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy.
        ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. -
        ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. -
        ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. -
        ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the
        ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine
        ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP)
        ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one
        ## remote computer but collect data from or run additional commands on other remote computers.
        ##
        ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice
        ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials
        ## are passed to it, the credentials can be used to control the network session.
        ##
        ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote
        ## computer anonymously, without using authentication. If the request is made anonymously, it returns no
        ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for
        ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')]
        [System.String] $Authentication = 'Default',

        ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this
        ## action. Enter the certificate thumbprint of the certificate.
        ##
        ## Certificates are used in client certificate-based authentication. They can be mapped only to local user
        ## accounts; they do not work with domain accounts.
        ##
        ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert:
        ## drive.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $CertificateThumbprint,

        ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the
        ## default port is 80. When the transport is HTTPS, the default port is 443.
        ##
        ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's
        ## certificate common name (CN).
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Port,

        ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote
        ## computer. By default, SSL is not used.
        ##
        ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL
        ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the
        ## port that is used for the connection, and you specify this parameter, the command fails.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseSSL,

        ## Credential used to connect to the remote computer.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )
    begin {

        ## Authentication might not be explicitly passed, add it so it gets splatted
        $PSBoundParameters['Authentication'] = $Authentication;

    }
    process {

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

            $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' };

            foreach ($node in $nodes) {

                $nodeName = $node.NodeName;
                if (($PSBoundParameters.ContainsKey('PreferNodeProperty')) -and
                    (-not [System.String]::IsNullOrEmpty($node[$PreferNodeProperty]))) {

                    $nodeName = $node[$PreferNodeProperty];
                }

                $ComputerName += $nodeName;
            }
        }

        $sessions = Get-PSSession;
        $activeSessions = @();
        $inactiveSessions = @();

        ## Remove parameters that aren't supported by Get-PSSession, Test-WSMan and New-PSSession
        [ref] $null = $PSBoundParameters.Remove('ComputerName');
        [ref] $null = $PSBoundParameters.Remove('ConfigurationData');
        [ref] $null = $PSBoundParameters.Remove('PreferNodeProperty');
        [ref] $null = $PSBoundParameters.Remove('ErrorAction');

        foreach ($computer in $ComputerName) {

            $session = $sessions |
                            Where-Object { $_.ComputerName -eq $computer -and $_.State -eq 'Opened' } |
                                Select-Object -First 1;

            if (-not $session) {

                Write-Verbose -Message ($localized.TestingWinRMConnection -f $computer);
                try {

                   if (Test-WSMan -ComputerName $computer -ErrorAction Stop @PSBoundParameters) {

                        ## WSMan is up so we should be able to connect, if not throw..
                        Write-Verbose -Message ($localized.ConnectingRemoteSession -f $computer);
                        $activeSessions += New-PSSession -ComputerName $computer -ErrorAction Stop @PSBoundParameters;
                    }
                }
                catch {

                    $inactiveSessions += $computer;
                    Write-Error $_;
                }

            }
            else {

                Write-Verbose -Message ($localized.ReusingExistingRemoteSession -f $computer);
                $activeSessions += $session
            }

        } #end foreach computer

        if ($activeSessions.Count -gt 0) {

            Write-Verbose -Message ($localized.QueryingActiveSessions -f ($activeSessions.ComputerName -join "','"));
            $results = Invoke-Command -Session $activeSessions -ScriptBlock {
                            Get-DscLocalConfigurationManager |
                                Select-Object -Property LCMVersion, LCMState;
                        };
        }

        foreach ($computer in $ComputerName) {

            if ($computer -in $inactiveSessions) {

                $labState = [PSCustomObject] @{
                    ComputerName = $computer;
                    LCMVersion = '';
                    LCMState = 'Unknown';
                    Completed = $false;
                }
                Write-Output -InputObject $labState;
            }
            else {

                $result = $results | Where-Object { $_.PSComputerName -eq $computer };
                $labState = [PSCustomObject] @{
                    ComputerName = $result.PSComputerName;
                    LCMVersion = $result.LCMVersion;
                    LCMState = $result.LCMState;
                    Completed = $result.LCMState -eq 'Idle';
                }
                Write-Output -InputObject $labState;
            }

        } #end foreach computer

    } #end process
} #end function

function Get-LabVM {
    <#
    .SYNOPSIS
        Retrieves the current configuration of a VM.
    .DESCRIPTION
        Gets a virtual machine's configuration using the xVMHyperV DSC resource.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Specifies the lab virtual machine/node name.
        [Parameter(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
    )
    process {

        if (-not $Name) {

            # Return all nodes defined in the configuration
            $Name = $ConfigurationData.AllNodes | Where-Object NodeName -ne '*' | ForEach-Object { $_.NodeName; }
        }

        $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName;

        foreach ($nodeName in $Name) {

            $node = Resolve-NodePropertyValue -NodeName $nodeName -ConfigurationData $ConfigurationData;
            $xVMParams = @{
                Name    = $node.NodeDisplayName;
                VhdPath = Resolve-LabVMDiskPath -Name $node.NodeDisplayName -EnvironmentName $environmentName;
            }

            try {

                Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM;
                $vm = Get-LabDscResource -ResourceName VM -Parameters $xVMParams;
                Write-Output -InputObject ([PSCustomObject] $vm);
            }
            catch {

                Write-Error -Message ($localized.CannotLocateNodeError -f $nodeName);
            }

        } #end foreach node

    } #end process
} #end function Get-LabVM

function Get-LabVMDefault {
<#
    .SYNOPSIS
        Gets the current lab virtual machine default settings.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param ( )
    process {

        $vmDefaults = Get-ConfigurationData -Configuration VM;

        ## BootOrder property should not be exposed via the Get-LabVMDefault/Set-LabVMDefault
        $vmDefaults.PSObject.Properties.Remove('BootOrder');
        return $vmDefaults;

    }
} #end function

function Import-LabHostConfiguration {
<#
    .SYNOPSIS
        Restores the lab host configuration from a backup.
    .LINK
        Export-LabHostConfiguration
#>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        # Specifies the export path location.
        [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias("PSPath")]
        [System.String] $Path,

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

        ## Restores only the lab host default settings
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $HostSetting,

        ## Restores only the lab VM default settings
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $VM,

        ## Restores only the lab custom media default settings
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Media
    )
    process {

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

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

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

        if (-not (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) {

            $errorMessage = $localized.InvalidPathError -f 'Import', $Path;
            $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage;
            $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceUnavailable;
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileNotFound', $errorCategory, $Path;
            $PSCmdlet.WriteError($errorRecord);
            return;
        }

        Write-Verbose -Message ($localized.ImportingConfiguration -f $labDefaults.ModuleName, $Path);
        $configurationDocument = Get-Content -Path $Path -Raw -ErrorAction Stop;
        try {

            $configuration = ConvertFrom-Json -InputObject $configurationDocument -ErrorAction Stop;
        }
        catch {

            $errorMessage = $localized.InvalidConfigurationError -f $Path;
            throw $errorMessage;
        }

        if ((-not $PSBoundParameters.ContainsKey('HostSetting')) -and
                (-not $PSBoundParameters.ContainsKey('VM')) -and
                    (-not $PSBoundParameters.ContainsKey('Media'))) {

            ## Nothing specified to load 'em all!
            $VM = $true;
            $HostSetting = $true;
            $Media = $true;
        }

        Write-Verbose -Message ($localized.ImportingConfigurationSettings -f $configuration.GenerationDate, $configuration.GenerationHost);

        if ($HostSetting) {

            $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Host');
            $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Host';
            if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) {

                [ref] $null = Reset-LabHostDefault -Confirm:$false;
                $hostSettingDefaultObject = $configuration.HostDefaults;
                $hostSettingDefaults = Convert-PSObjectToHashtable -InputObject $hostSettingDefaultObject;
                Set-LabHostDefault @hostSettingDefaults -Confirm:$false;
                Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'Host');
            }
        } #end if restore host defaults

        if ($Media) {

            ## Restore media before VM defaults as VM defaults may reference custom media!
            $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Media');
            $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Media';
            if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) {

                [ref] $null = Reset-LabMedia -Confirm:$false;
                foreach ($mediaObject in $configuration.CustomMedia) {

                    $customMedia = Convert-PSObjectToHashtable -InputObject $mediaObject -IgnoreNullValues;
                    Write-Output (Register-LabMedia @customMedia -Force);
                }
                Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'Media');
            }
        } #end if restore custom media

        if ($VM) {

            $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'VM');
            $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'VM';
            if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) {

                [ref] $null = Reset-LabVMDefault -Confirm:$false;
                $vmDefaultObject = $configuration.VMDefaults;
                $vmDefaults = Convert-PSObjectToHashtable -InputObject $vmDefaultObject;
                ## Boot order is exposed externally
                $vmDefaults.Remove('BootOrder');
                Set-LabVMDefault @vmDefaults -Confirm:$false;
                Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'VM');
            }
        } #end if restore VM defaults

    } #end process
} #end function

function Install-LabModule {
<#
    .SYNOPSIS
        Installs Lability PowerShell and DSC resource modules.
    .DESCRIPTION
        The Install-LabModule cmdlet installs PowerShell modules and/or DSC resource modules from Lability's
        module cache in to the local system. The DSC resources and/or PowerShell module can be installed into
        either the current user's or the local machine's module path.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document that contains ability'srequired media definition.
    .PARAMETER ModuleType
        Specifies the module type(s) defined in a PowerShell DSC configuration (.psd1) document to install.
    .PARAMETER NodeName
        Specifies only modules that target the node(s) specified are installed.
    .PARAMETER Scope
        Specifies the scope to install module(s) in to. The default value is 'CurrentUser'.
    .EXAMPLE
        Install-LabModule -ConfigurationData .\Config.psd1 -ModuleType -DscResource

        Installs all DSC resource modules defined in the 'Config.psd1' document into the user's module scope.
    .EXAMPLE
        Install-LabModule -ConfigurationData .\Config.psd1 -ModuleType -Module -Scope AllUsers

        Installs all PowerShell modules defined in the 'Config.psd1' document into the local machine's module scope.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $ConfigurationData,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('Module','DscResource')]
        [System.String[]] $ModuleType,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $NodeName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('AllUsers','CurrentUser')]
        [System.String] $Scope = 'CurrentUser'
    )
    process {

        $moduleRelativePath = 'WindowsPowerShell\Modules';

        if ($Scope -eq 'AllUsers') {

            $systemDrive = (Resolve-Path -Path $env:SystemDrive).Drive;
            $localizedProgramFiles = Resolve-ProgramFilesFolder -Drive $systemDrive;
            $DestinationPath = Join-Path -Path $localizedProgramFiles.FullName -ChildPath $moduleRelativePath;
        }
        elseif ($Scope -eq 'CurrentUser') {

            $userDocuments = [System.Environment]::GetFolderPath('MyDocuments');
            $DestinationPath = Join-Path -Path $userDocuments -ChildPath $moduleRelativePath;
        }

        $copyLabModuleParams = @{
            ConfigurationData = $ConfigurationData;
            ModuleType = $ModuleType;
            DestinationPath = $DestinationPath;
        }
        Copy-LabModule @copyLabModuleParams;

    } #end process
} #end function

function Invoke-LabResourceDownload {
<#
    .SYNOPSIS
        Starts a download of all required lab resources.
    .DESCRIPTION
        When a lab configuration is started, Lability will attempt to download all the required media and resources.

        In some scenarios you many need to download lab resources in advance, e.g. where internet access is not
        readily available or permitted. The `Invoke-LabResourceDownload` cmdlet can be used to manually download
        all required resources or specific media/resources as needed.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
    .PARAMETER All
        Specifies all media, custom and DSC resources should be downloaded.
    .PARAMETER MediaId
        Specifies the specific media IDs to download.
    .PARAMETER ResourceId
        Specifies the specific custom resource IDs to download.
    .PARAMETER Media
        Specifies all media IDs should be downloaded.
    .PARAMETER Resources
        Specifies all custom resource IDs should be downloaded.
    .PARAMETER DSCResources
        Specifies all defined DSC resources should be downloaded.
    .PARAMETER Moduless
        Specifies all defined PowerShell modules should be downloaded.
    .PARAMETER Force
        Forces a download of all resources, overwriting any existing resources.
    .PARAMETER DestinationPath
        Specifies the target destination path of downloaded custom resources (not media or DSC resources).
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -All

        Downloads all required lab media, any custom resources and DSC resources defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -MediaId 'WIN10_x64_Enterprise_EN_Eval'

        Downloads only the 'WIN10_x64_Enterprise_EN_Eval' media.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -ResourceId 'MyCustomResource'

        Downloads only the 'MyCustomResource' resource defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Media

        Downloads only the media defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Resources -DSCResources

        Downloads only the custom file resources and DSC resources defined in the 'MyLab.psd1' configuration.
#>

    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
         $ConfigurationData = @{ },

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [System.Management.Automation.SwitchParameter] $All,

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

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

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Media')]
        [System.Management.Automation.SwitchParameter] $Media,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')]
        [System.Management.Automation.SwitchParameter] $Resources,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'DSCResources')]
        [System.Management.Automation.SwitchParameter] $DSCResources,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Modules')]
        [System.Management.Automation.SwitchParameter] $Modules,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ResourceId')]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

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

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        if (-not $DestinationPath) {
            $DestinationPath = $hostDefaults.ResourcePath;
        }

    }
    process {

        if ($PSCmdlet.ParameterSetName -in 'MediaId','Media','All') {

            if (-not $MediaId) {

                Write-Verbose -Message ($Localized.DownloadingAllRequiredMedia);
                $uniqueMediaIds = @();
                $ConfigurationData.AllNodes.Where({ $_.NodeName -ne '*' }) | ForEach-Object {
                    $id = (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData).Media;
                    if ($uniqueMediaIds -notcontains $id) { $uniqueMediaIds += $id; }
                }
                $MediaId = $uniqueMediaIds;
            }

            if ($MediaId) {

                foreach ($id in $MediaId) {

                    $labMedia = Resolve-LabMedia -ConfigurationData $ConfigurationData -Id $id;
                    Invoke-LabMediaImageDownload -Media $labMedia -Force:$Force;

                    Write-Verbose -Message $Localized.DownloadingAllRequiredHotfixes;
                    if ($labMedia.Hotfixes.Count -gt 0) {
                        foreach ($hotfix in $labMedia.Hotfixes) {
                            Invoke-LabMediaDownload -Id $hotfix.Id -Uri $hotfix.Uri;
                        }
                    }
                    else {
                        Write-Verbose -Message ($localized.NoHotfixesSpecified);
                    }
                }

            }
            else {
                Write-Verbose -Message ($localized.NoMediaDefined);
            }

        } #end if MediaId or MediaOnly

        if ($PSCmdlet.ParameterSetName -in 'ResourceId','Resources','All') {

            if (-not $ResourceId) {

                Write-Verbose -Message ($Localized.DownloadingAllDefinedResources);
                $ResourceId = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Resource.Id;
            }

            if (($ResourceId.Count -gt 0) -and (-not $MediaOnly)) {

                foreach ($id in $ResourceId) {

                    $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $id;
                    if (($null -eq $resource.IsLocal) -or ($resource.IsLocal -eq $false)) {

                        $fileName = $resource.Id;
                        if ($resource.Filename) { $fileName = $resource.Filename; }
                        $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $fileName;
                        $invokeResourceDownloadParams = @{
                            DestinationPath = $resourceDestinationPath;
                            Uri = $resource.Uri;
                            Checksum = $resource.Checksum;
                            Force = $Force;
                        }
                        [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams;
                        Write-Output (Get-Item -Path $resourceDestinationPath);
                    }
                }
            }
            else {

                Write-Verbose -Message ($localized.NoResourcesDefined);
            }

        } #end if ResourceId or ResourceOnly

        if ($PSCmdlet.ParameterSetName -in 'DSCResources','All') {

            $dscResourceDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).DSCResource;
            if (($null -ne $dscResourceDefinitions) -and ($dscResourceDefinitions.Count -gt 0)) {

                ## Invokes download of DSC resource modules into the module cache
                Write-Verbose -Message ($Localized.DownloadingAllDSCResources);
                Invoke-LabModuleCacheDownload -Module $dscResourceDefinitions -Force:$Force;
            }
            else {
                Write-Verbose -Message ($localized.NoDSCResourcesDefined);
            }
        } #end if DSC resource

        if ($PSCmdlet.ParameterSetName -in 'Modules','All') {

            $moduleDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Module;
            if (($null -ne $moduleDefinitions) -and ($moduleDefinitions.Count -gt 0)) {

                ## Invokes download of PowerShell modules into the module cache
                Write-Verbose -Message ($Localized.DownloadingAllPowerShellModules);
                Invoke-LabModuleCacheDownload -Module $moduleDefinitions -Force:$Force;
            }
            else {
                Write-Verbose -Message ($localized.NoPowerShellModulesDefined);
            }

        } #end PowerShell module

    } #end process
} #end function

function New-LabImage {
<#
    .SYNOPSIS
        Creates a new master/parent lab image.
    .DESCRIPTION
        The New-LabImage cmdlet starts the creation of a lab VHD(X) master image from the specified media Id.

        Lability will automatically create lab images as required. If there is a need to manally recreate an image,
        then the New-LabImage cmdlet can be used.
    .PARAMETER Id
        Specifies the media Id of the image to create.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document that contains the required media definition.
    .PARAMETER Force
        Specifies that any existing image should be overwritten.
    .EXAMPLE
        New-LabImage -Id 2016_x64_Datacenter_EN_Eval

        Creates the VHD(X) image from the '2016_x64_Datacenter_EN_Eval' media Id.
    .EXAMPLE
        New-LabImage -Id 2016_x64_Datacenter_EN_Eval -Force

        Creates the VHD(X) image from the '2016_x64_Datacenter_EN_Eval' media Id, overwriting an existing image with the same name.
    .LINK
        Get-LabMedia
        Get-LabImage
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## Lab media Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id,

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

        ## Force the re(creation) of the master/parent image
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process
    {
        ## Download media if required..
        [ref] $null = $PSBoundParameters.Remove('Force')
        [ref] $null = $PSBoundParameters.Remove('WhatIf')
        [ref] $null = $PSBoundParameters.Remove('Confirm')

        $media = Resolve-LabMedia @PSBoundParameters
        ## Update media Id if alias was/is used
        $Id = $media.Id

        if ((Test-LabImage @PSBoundParameters) -and $Force)
        {
            $image = Get-LabImage @PSBoundParameters
            Write-Verbose -Message ($localized.RemovingDiskImage -f $image.ImagePath)
            [ref] $null = Remove-Item -Path $image.ImagePath -Force -ErrorAction Stop
        }
        elseif (Test-LabImage @PSBoundParameters)
        {
            throw ($localized.ImageAlreadyExistsError -f $Id)
        }

        ## Check Dism requirement (if present) #167
        if ($null -ne $media.CustomData.MinimumDismVersion)
        {
            $minimumDismVersion = $media.CustomData.MinimumDismVersion
            if ($labDefaults.DismVersion -lt $minimumDismVersion)
            {
                throw ($localized.DismVersionMismatchError -f $Id, $minimumDismVersion.ToString())
            }
        }

        $hostDefaults = Get-ConfigurationData -Configuration Host

        if ($media.MediaType -eq 'VHD')
        {
            $mediaFileInfo = Invoke-LabMediaImageDownload -Media $media
            Write-Verbose -Message ($localized.ImportingExistingDiskImage -f $media.Description)
            $imageName = $media.Filename
            $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName
        } #end if VHD
        elseif ($media.MediaType -eq 'NULL')
        {
            Write-Verbose -Message ($localized.CreatingDiskImage -f $media.Description)
            $imageName = '{0}.vhdx' -f $Id
            $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName

            ## Create disk image and refresh PSDrives
            $newEmptyDiskImageParams = @{
                Path = $imagePath
                Force = $true
                ErrorAction = 'Stop'
            }

            if ($media.CustomData.DiskType)
            {
                $newEmptyDiskImageParams['Type'] = $media.CustomData.DiskType
            }

            if ($media.CustomData.DiskSize)
            {
                $newEmptyDiskImageParams['Size'] = $media.CustomData.DiskSize
            }

            $image = New-EmptyDiskImage @newEmptyDiskImageParams
        }
        else
        {
            ## Create VHDX
            $mediaFileInfo = Invoke-LabMediaImageDownload -Media $media

            if ($media.CustomData.PartitionStyle)
            {
                ## Custom partition style has been defined so use that
                $partitionStyle = $media.CustomData.PartitionStyle
            }
            elseif ($media.Architecture -eq 'x86')
            {
                ## Otherwise default to MBR for x86 media
                $partitionStyle = 'MBR'
            }
            else
            {
                $partitionStyle = 'GPT'
            }

            Write-Verbose -Message ($localized.CreatingDiskImage -f $media.Description)

            $imageName = '{0}.vhdx' -f $Id
            $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName

            ## Apply WIM (Expand-LabImage) and add specified features
            $expandLabImageParams = @{
                MediaPath = $mediaFileInfo.FullName
                PartitionStyle = $partitionStyle
            }

            ## Determine whether we're using the WIM image index or image name. This permits
            ## specifying an integer image index in a media's 'ImageName' property.
            [System.Int32] $wimImageIndex = $null
            if ([System.Int32]::TryParse($media.ImageName, [ref] $wimImageIndex))
            {
                $expandLabImageParams['WimImageIndex'] = $wimImageIndex
            }
            else
            {
                if ([System.String]::IsNullOrEmpty($media.ImageName))
                {
                    throw ($localized.ImageNameRequiredError -f 'ImageName')
                }

                $expandLabImageParams['WimImageName'] = $media.ImageName
            }

            $imageCreationFailed = $false

            try
            {
                ## Create disk image and refresh PSDrives
                $newDiskImageParams = @{
                    Path = $imagePath
                    PartitionStyle = $partitionStyle
                    Passthru = $true
                    Force = $true
                    ErrorAction = 'Stop'
                }

                if ($media.CustomData.DiskType)
                {
                    $newDiskImageParams['Type'] = $media.CustomData.DiskType
                }

                if ($media.CustomData.DiskSize)
                {
                    $newDiskImageParams['Size'] = $media.CustomData.DiskSize
                }

                $image = New-DiskImage @newDiskImageParams
                [ref] $null = Get-PSDrive

                $expandLabImageParams['Vhd'] = $image

                if ($media.CustomData.SourcePath)
                {
                    $expandLabImageParams['SourcePath'] = $media.CustomData.SourcePath
                }
                if ($media.CustomData.WimPath)
                {
                    $expandLabImageParams['WimPath'] = $media.CustomData.WimPath
                }
                if ($media.CustomData.WindowsOptionalFeature)
                {
                    $expandLabImageParams['WindowsOptionalFeature'] = $media.CustomData.WindowsOptionalFeature
                }
                if ($media.CustomData.PackagePath)
                {
                    $expandLabImageParams['PackagePath'] = $media.CustomData.PackagePath
                }
                if ($media.CustomData.Package)
                {
                    $expandLabImageParams['Package'] = $media.CustomData.Package
                }
                if ($media.CustomData.PackageLocale)
                {
                    $expandLabImageParams['PackageLocale'] = $media.CustomData.PackageLocale
                }

                Expand-LabImage @expandLabImageParams

                ## Apply hotfixes (Add-DiskImageHotfix)
                $addDiskImageHotfixParams = @{
                    Id = $Id
                    Vhd = $image
                    PartitionStyle = $partitionStyle
                }
                if ($PSBoundParameters.ContainsKey('ConfigurationData'))
                {
                    $addDiskImageHotfixParams['ConfigurationData'] = $ConfigurationData
                }
                Add-DiskImageHotfix @addDiskImageHotfixParams

                ## Configure boot volume (Set-DiskImageBootVolume)
                Set-DiskImageBootVolume -Vhd $image -PartitionStyle $partitionStyle
            }
            catch
            {
                ## Have to ensure VHDX is dismounted before we can delete!
                $imageCreationFailed = $true
                Write-Error -Message $_
            }
            finally
            {
                ## Dismount VHDX
                Hyper-V\Dismount-VHD -Path $imagePath
            }

            if ($imageCreationFailed -eq $true)
            {
                Write-Warning -Message ($localized.RemovingIncompleteImageWarning -f $imagePath)
                Remove-Item -Path $imagePath -Force
            }
        } #end if ISO/WIM

        return (Get-LabImage $PSBoundParameters)

    } #end process
} #end function New-LabImage

function New-LabVM {
<#
    .SYNOPSIS
        Creates a simple bare-metal virtual machine.
    .DESCRIPTION
        The New-LabVM cmdlet creates a bare virtual machine using the specified media. No bootstrap or DSC configuration is applied.

        NOTE: The mandatory -MediaId parameter is dynamic and is not displayed in the help syntax output.

        If optional values are not specified, the virtual machine default settings are applied. To list the current default settings run the `Get-LabVMDefault` command.

        NOTE: If a specified virtual switch cannot be found, an Internal virtual switch will automatically be created. To use any other virtual switch configuration, ensure the virtual switch is created in advance.
    .LINK
        Register-LabMedia
        Unregister-LabMedia
        Get-LabVMDefault
        Set-LabVMDefault
#>

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

        ## Default virtual machine startup memory (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $StartupMemory,

        ## Default virtual machine miniumum dynamic memory allocation (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $MinimumMemory,

        ## Default virtual machine maximum dynamic memory allocation (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $MaximumMemory,

        ## Default virtual machine processor count.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(1, 4)]
        [System.Int32] $ProcessorCount,

        # Input Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^([a-z]{2,2}-[a-z]{2,2})|(\d{4,4}:\d{8,8})$')]
        [System.String] $InputLocale,

        # System Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $SystemLocale,

        # User Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $UserLocale,

        # UI Language
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $UILanguage,

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

        # Registered Owner
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $RegisteredOwner,

        # Registered Organization
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('RegisteredOrganisation')]
        [ValidateNotNullOrEmpty()]
        [System.String] $RegisteredOrganization,

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

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

        ## Virtual machine switch name(s).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $SwitchName,

        ## Virtual machine MAC address(es).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $MACAddress,

        ## Enable Secure boot status
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Boolean] $SecureBoot,

        ## Enable Guest Integration Services
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Boolean] $GuestIntegrationServices,

        ## Custom data
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Collections.Hashtable] $CustomData,

        ## Skip creating baseline snapshots
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoSnapshot
    )
    DynamicParam
    {
        ## Adds a dynamic -MediaId parameter that returns the available media Ids
        $parameterAttribute = New-Object -TypeName 'System.Management.Automation.ParameterAttribute'
        $parameterAttribute.ParameterSetName = '__AllParameterSets'
        $parameterAttribute.Mandatory = $false
        $attributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]'
        $attributeCollection.Add($parameterAttribute)
        $mediaIds = Get-LabMediaId
        $validateSetAttribute = New-Object -TypeName 'System.Management.Automation.ValidateSetAttribute' -ArgumentList $mediaIds
        $attributeCollection.Add($validateSetAttribute)
        $runtimeParameter = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameter' -ArgumentList @('MediaId', [System.String], $attributeCollection)
        $runtimeParameterDictionary = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameterDictionary'
        $runtimeParameterDictionary.Add('MediaId', $runtimeParameter)
        return $runtimeParameterDictionary
    }
    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
    {
        ## Skeleton configuration node
        $configurationNode = @{ }

        if ($CustomData)
        {
            ## Add all -CustomData keys/values to the skeleton configuration
            foreach ($key in $CustomData.Keys)
            {
                $configurationNode[$key] = $CustomData.$key
            }
        }

        ## Explicitly defined parameters override any -CustomData
        $parameterNames = @('StartupMemory','MinimumMemory','MaximumMemory','SwitchName','Timezone','UILanguage','MACAddress',
            'ProcessorCount','InputLocale','SystemLocale','UserLocale','RegisteredOwner','RegisteredOrganization','SecureBoot')
        foreach ($key in $parameterNames)
        {
            if ($PSBoundParameters.ContainsKey($key))
            {
                $configurationNode[$key] = $PSBoundParameters.$key
            }
        }

        ## Ensure the specified MediaId is applied after any CustomData media entry!
        if ($PSBoundParameters.ContainsKey('MediaId'))
        {
            $configurationNode['Media'] = $PSBoundParameters.MediaId
        }
        else
        {
            $configurationNode['Media'] = (Get-LabVMDefault).Media
        }

        $currentNodeCount = 0
        foreach ($vmName in $Name)
        {
            ## Update the node name before creating the VM
            $configurationNode['NodeName'] = $vmName
            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'New-LabVM', $vmName
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingQuickVM -f $vmName, $PSBoundParameters.MediaId)
            if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning))
            {
                $currentNodeCount++
                [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1
                $activity = $localized.ConfiguringNode -f $vmName
                Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete

                $configurationData = @{ AllNodes = @( $configurationNode ) }
                New-LabVirtualMachine -Name $vmName -ConfigurationData $configurationData -Credential $Credential -NoSnapshot:$NoSnapshot -IsQuickVM
            }
        } #end foreach name

        if (-not [System.String]::IsNullOrEmpty($activity))
        {
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete
        }

    } #end process
} #end function New-LabVM

function Register-LabMedia {
<#
    .SYNOPSIS
        Registers a custom media entry.
    .DESCRIPTION
        The Register-LabMedia cmdlet allows adding custom media to the host's configuration. This circumvents the requirement of having to define custom media entries in the DSC configuration document (.psd1).

        You can use the Register-LabMedia cmdlet to override the default media entries, e.g. you have the media hosted internally or you wish to replace the built-in media with your own implementation.

        To override a built-in media entry, specify the same media Id with the -Force switch.
    .PARAMETER Legacy
        Specifies registering a legacy Windows 10 media as custom media.
    .EXAMPLE
        Register-LabMedia -Legacy WIN10_x64_Enterprise_1809_EN_Eval

        Reregisters the deprecated Windows 10 Enterprise x64 English evaluation media.
    .LINK
        Get-LabMedia
        Unregister-LabMedia
#>

    [CmdletBinding(DefaultParameterSetName = 'ID')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param
    (
        ## Specifies the media Id to register. You can override the built-in media if required.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [System.String] $Id,

        ## Specifies the media's type.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateSet('VHD','ISO','WIM','NULL')]
        [System.String] $MediaType,

        ## Specifies the source Uri (http/https/file) of the media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [System.Uri] $Uri,

        ## Specifies the architecture of the media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateSet('x64','x86')]
        [System.String] $Architecture,

        ## Specifies a description of the media.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Description,

        ## Specifies the image name containing the target WIM image. You can specify integer values.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImageName,

        ## Specifies the local filename of the locally cached resource file.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Filename,

        ## Specifies the MD5 checksum of the resource file.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Checksum,

        ## Specifies custom data for the media.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNull()]
        [System.Collections.Hashtable] $CustomData,

        ## Specifies additional Windows hotfixes to install post deployment.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]] $Hotfixes,

        ## Specifies the media type. Linux VHD(X)s do not inject resources.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [ValidateSet('Windows','Linux')]
        [System.String] $OperatingSystem = 'Windows',

        ## Specifies the media alias (Id).
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')]
        [System.String] $Alias,

        ## Registers media via a JSON file hosted externally.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'FromUri')]
        [System.String] $FromUri,

        ## Registers media using a custom Id.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'FromUri')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Legacy')]
        [System.String] $CustomId,

        ## Specifies that an exiting media entry should be overwritten.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    DynamicParam
    {
        ## Adds a dynamic -Legacy parameter that returns the available legacy Windows 10 media Ids
        $parameterAttribute = New-Object -TypeName 'System.Management.Automation.ParameterAttribute'
        $parameterAttribute.ParameterSetName = 'Legacy'
        $parameterAttribute.Mandatory = $true
        $attributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]'
        $attributeCollection.Add($parameterAttribute)
        $mediaIds = (Get-LabMedia -Legacy).Id
        $validateSetAttribute = New-Object -TypeName 'System.Management.Automation.ValidateSetAttribute' -ArgumentList $mediaIds
        $attributeCollection.Add($validateSetAttribute)
        $runtimeParameter = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameter' -ArgumentList @('Legacy', [System.String], $attributeCollection)
        $runtimeParameterDictionary = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameterDictionary'
        $runtimeParameterDictionary.Add('Legacy', $runtimeParameter)
        return $runtimeParameterDictionary
    }
    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            Legacy {

                ## Download the json content and convert into a hashtable
                $legacyMedia = Get-LabMedia -Id $PSBoundParameters.Legacy -Legacy | Convert-PSObjectToHashtable

                if ($PSBoundParameters.ContainsKey('CustomId'))
                {
                    $legacyMedia['Id'] = $CustomId
                }

                ## Recursively call Register-LabMedia and splat the properties
                return (Register-LabMedia @legacyMedia -Force:$Force)
            }

            FromUri {

                ## Download the json content and convert into a hashtable
                $customMedia = Invoke-RestMethod -Uri $FromUri | Convert-PSObjectToHashtable

                if ($PSBoundParameters.ContainsKey('CustomId'))
                {
                    $customMedia['Id'] = $CustomId
                }

                ## Recursively call Register-LabMedia and splat the properties
                return (Register-LabMedia @customMedia -Force:$Force)
            }

            default {

                ## Validate Linux VM media type is VHD or NULL
                if (($OperatingSystem -eq 'Linux') -and ($MediaType -notin 'VHD','NULL'))
                {
                    throw ($localized.InvalidOSMediaTypeError -f $MediaType, $OperatingSystem)
                }

                ## Validate ImageName when media type is ISO/WIM
                if (($MediaType -eq 'ISO') -or ($MediaType -eq 'WIM'))
                {
                    if (-not $PSBoundParameters.ContainsKey('ImageName'))
                    {
                        throw ($localized.ImageNameRequiredError -f '-ImageName')
                    }
                }

                ## Resolve the media Id to see if it's already been used
                try
                {
                    $media = Resolve-LabMedia -Id $Id -ErrorAction SilentlyContinue
                }
                catch
                {
                    Write-Debug -Message ($localized.CannotLocateMediaError -f $Id)
                }

                if ($media -and (-not $Force))
                {
                    throw ($localized.MediaAlreadyRegisteredError -f $Id, '-Force')
                }

                if ($PSBoundParameters.ContainsKey('Alias'))
                {
                    ## Check to see whether the alias is already registered
                    $existingMediaIds = Get-LabMediaId
                    if ($existingMediaIds.Contains($Alias))
                    {
                        throw ($localized.MediaAliasAlreadyRegisteredError -f $Alias)
                    }
                }

                ## Get the custom media list (not the built in media)
                $existingCustomMedia = @(Get-ConfigurationData -Configuration CustomMedia)
                if (-not $existingCustomMedia)
                {
                    $existingCustomMedia = @()
                }

                $customMedia = [PSCustomObject] @{
                    Id              = $Id
                    Alias           = $Alias
                    Filename        = $Filename
                    Description     = $Description
                    Architecture    = $Architecture
                    ImageName       = $ImageName
                    MediaType       = $MediaType
                    OperatingSystem = $OperatingSystem
                    Uri             = $Uri
                    Checksum        = $Checksum
                    CustomData      = $CustomData
                    Hotfixes        = $Hotfixes
                }

                $hasExistingMediaEntry = $false
                for ($i = 0; $i -lt $existingCustomMedia.Count; $i++)
                {
                    if ($existingCustomMedia[$i].Id -eq $Id)
                    {
                        Write-Verbose -Message ($localized.OverwritingCustomMediaEntry -f $Id)
                        $hasExistingMediaEntry = $true
                        $existingCustomMedia[$i] = $customMedia
                    }
                }

                if (-not $hasExistingMediaEntry)
                {
                    ## Add it to the array
                    Write-Verbose -Message ($localized.AddingCustomMediaEntry -f $Id)
                    $existingCustomMedia += $customMedia
                }

                Write-Verbose -Message ($localized.SavingConfiguration -f $Id)
                Set-ConfigurationData -Configuration CustomMedia -InputObject @($existingCustomMedia)
                return $customMedia

            } #end default
        } #end switch

    } #end process
} #end function

function Remove-LabConfiguration {
<#
    .SYNOPSIS
        Removes all VMs and associated snapshots of all nodes defined in a PowerShell DSC configuration document.
    .DESCRIPTION
        The Remove-LabConfiguration removes all virtual machines that have a corresponding NodeName defined in the
        AllNode array of the PowerShell DSC configuration document.

        WARNING: ALL EXISTING VIRTUAL MACHINE DATA WILL BE LOST WHEN VIRTUAL MACHINES ARE REMOVED.

        By default, associated virtual machine switches are not removed as they may be used by other virtual
        machines or lab configurations. If you wish to remove any virtual switche defined in the PowerShell DSC
        configuration document, specify the -RemoveSwitch parameter.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document used to remove existing virtual machines. One virtual machine is removed per node
        defined in the AllNodes array.
    .PARAMETER RemoveSwitch
        Specifies that any connected virtual switch should also be removed when the virtual machine is removed.
    .LINK
        about_ConfigurationData
        Start-LabConfiguration
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [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 {

        Write-Verbose -Message $localized.StartedLabConfiguration;
        $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' };
        $currentNodeCount = 0;
        foreach ($node in $nodes) {

            $currentNodeCount++;
            $nodeProperties = Resolve-NodePropertyValue -NodeName $node.NodeName -ConfigurationData $ConfigurationData;
            [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1;
            $activity = $localized.ConfiguringNode -f $nodeProperties.NodeDisplayName;
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;

            ##TODO: Should this not ensure that VMs are powered off
            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Remove-Lab', $nodeProperties.NodeDisplayName;
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingVM -f $nodeProperties.NodeDisplayName);
            if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                Remove-LabVirtualMachine -Name $node.NodeName -ConfigurationData $ConfigurationData -RemoveSwitch:$RemoveSwitch -Confirm:$false;
            }

        } #end foreach node

        ## TODO: Remove empty (environment name) differencing disk folder(s)

        Write-Progress -Id 42 -Activity $activity -Completed;
        Write-Verbose -Message $localized.FinishedLabConfiguration;

    } #end process
} #end function Remove-LabConfiguration

function Remove-LabVM {
<#
    .SYNOPSIS
        Removes a bare-metal virtual machine and differencing VHD(X).
    .DESCRIPTION
        The Remove-LabVM cmdlet removes a virtual machine and it's VHD(X) file.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        ## Virtual machine name
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Name,

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

        $currentNodeCount = 0;
        foreach ($vmName in $Name) {

            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Remove-LabVM', $vmName;
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingVM -f $vmName);
            if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                $currentNodeCount++;
                [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1;
                $activity = $localized.ConfiguringNode -f $vmName;
                Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;

                ## Create a skeleton config data if one wasn't supplied
                if (-not $PSBoundParameters.ContainsKey('ConfigurationData')) {

                    try {

                        <# If we don't have configuration document, we need to locate
                        the lab image id so that the VM's VHD/X can be removed #182 #>

                        $mediaId = (Resolve-LabVMImage -Name $vmName).Id;
                    }
                    catch {

                        throw ($localized.CannotResolveMediaIdError -f $vmName);
                    }

                    $configurationData = @{
                        AllNodes = @(
                            @{
                                NodeName = $vmName;
                                Media = $mediaId;
                            }
                        )
                    };
                }

                Remove-LabVirtualMachine -Name $vmName -ConfigurationData $configurationData;

            } #end if should process
        } #end foreach VM

        if (-not [System.String]::IsNullOrEmpty($activity)) {

            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
        }

    } #end process
} #end function Remove-LabVM

function Reset-Lab {
<#
     .SYNOPSIS
        Reverts all VMs in a lab back to their initial configuration.
    .DESCRIPTION
        The Reset-Lab cmdlet will reset all the nodes defined in a PowerShell DSC configuration document, back to their
        initial state. If virtual machines are powered on, they will automatically be powered off when restoring the
        snapshot.

        When virtual machines are created - before they are powered on - a baseline snapshot is created. This snapshot
        is taken before the Sysprep process has been run and/or any PowerShell DSC configuration has been applied.

        WARNING: You will lose all changes to all virtual machines that have not been committed via another snapshot.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document.
    .LINK
        Checkpoint-Lab
    .NOTES
        This cmdlet uses the baseline snapshot snapshot created by the Start-LabConfiguration cmdlet. If the baseline
        was not created or the baseline snapshot does not exist, the lab VMs can be recreated with the
        Start-LabConfiguration -Force.
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        ## Revert to Base/Lab snapshots...
        $snapshotName = $localized.BaselineSnapshotName -f $labDefaults.ModuleName;
        Restore-Lab -ConfigurationData $ConfigurationData -SnapshotName $snapshotName -Force;

    } #end process
} #end function Reset-Lab

function Reset-LabHostDefault {
<#
    .SYNOPSIS
        Resets lab host default settings to default.
    .DESCRIPTION
        The Reset-LabHostDefault cmdlet resets the lab host's settings to default values.
    .LINK
        Get-LabHostDefault
        Set-LabHostDefault
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param ( )
    process {

        Remove-ConfigurationData -Configuration Host;
        Get-LabHostDefault;

    } #end process
} #end function Reset-LabHostDefault

function Reset-LabMedia {
<#
    .SYNOPSIS
        Reset the lab media entries to default settings.
    .DESCRIPTION
        The Reset-LabMedia removes all custom media entries, reverting them to default values.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param ( )
    process
    {
        Remove-ConfigurationData -Configuration CustomMedia
        Get-Labmedia
    }
}

function Reset-LabVM {
<#
    .SYNOPSIS
        Recreates a lab virtual machine.
    .DESCRIPTION
        The Reset-LabVM cmdlet deletes and recreates a lab virtual machine, reapplying the MOF.

        To revert a single VM to a previous state, use the Restore-VMSnapshot cmdlet. To revert an entire lab environment, use the Restore-Lab cmdlet.
#>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'PSCredential')]
    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 virtual machine. 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 virtual machine.
        [Parameter(Mandatory, ParameterSetName = 'Password', ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.Security.SecureString] $Password,

        ## Directory path containing the virtual machines' .mof file(s).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path,

        ## Skip creation of the initial baseline snapshot.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoSnapshot,

        ## Ignores missing MOF file
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $SkipMofCheck
    )
    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 {

        ## There is an assumption here is all .mofs are in the same folder
        $resolveConfigurationPathParams = @{
            ConfigurationData = $ConfigurationData;
            Name = $Name | Select-Object -First 1;
            Path = $Path;
            UseDefaultPath = $SkipMofCheck;
        }
        $Path = Resolve-ConfigurationPath @resolveConfigurationPathParams;

        $currentNodeCount = 0;
        foreach ($vmName in $Name) {

            $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Reset-LabVM', $vmName;
            $verboseProcessMessage = Get-FormattedMessage -Message ($localized.ResettingVM -f $vmName);
            if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                $currentNodeCount++;
                [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1;
                $activity = $localized.ConfiguringNode -f $vmName;
                Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;

                [ref] $null = Remove-LabVirtualMachine -Name $vmName -ConfigurationData $ConfigurationData;

                $newLabVirtualMachineParams = @{
                    Name = $vmName;
                    ConfigurationData = $ConfigurationData;
                    Path = $Path;
                    NoSnapshot = $NoSnapshot;
                    Credential = $Credential;
                }
                New-LabVirtualMachine @newLabVirtualMachineParams;

            } #end if should process
        } #end foreach VMd

        if (-not [System.String]::IsNullOrEmpty($activity)) {

            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
        }

    } #end process
} #end function Reset-LabVM

function Reset-LabVMDefault {
<#
    .SYNOPSIS
        Reset the current lab virtual machine default settings back to defaults.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param ( )
    process {

        Remove-ConfigurationData -Configuration VM;
        Get-LabVMDefault;

    }
} #end function

function Restore-Lab {
<#
    .SYNOPSIS
        Restores all lab VMs to a previous configuration.
    .DESCRIPTION
        The Restore-Lab reverts all the nodes defined in a PowerShell DSC configuration document, back to a
        previously captured configuration.

        When creating the snapshots, they are created using a snapshot name. To restore a lab to a previous
        configuration, you must supply the same snapshot name.

        All virtual machines should be powered off when the snapshots are restored. If VMs are powered on,
        an error will be generated. You can override this behaviour by specifying the -Force parameter.

        WARNING: If the -Force parameter is used, running virtual machines will be powered off automatically.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document.
    .PARAMETER SnapshotName
        Specifies the virtual machine snapshot name to be restored. You must use the same snapshot name used when
        creating the snapshot with the Checkpoint-Lab cmdlet.
    .PARAMETER Force
        Forces virtual machine snapshots to be restored - even if there are any running virtual machines.
    .LINK
        Checkpoint-Lab
        Reset-Lab
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Snapshot name
        [Parameter(Mandatory)]
        [Alias('Name')]
        [System.String] $SnapshotName,

        ## Force snapshots if virtual machines are on
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {

        $nodes = @();
        $ConfigurationData.AllNodes |
            Where-Object { $_.NodeName -ne '*' } |
                ForEach-Object {

                    $nodes += Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData;
                };

        $runningNodes = $nodes |
            ForEach-Object { Hyper-V\Get-VM -Name $_.NodeDisplayName } |
                Where-Object { $_.State -ne 'Off' }

        $currentNodeCount = 0;
        if ($runningNodes -and $Force) {

            $nodes |
                Sort-Object { $_.BootOrder } |
                    ForEach-Object {
                        $currentNodeCount++;
                        [System.Int32] $percentComplete = ($currentNodeCount / $nodes.Count) * 100;
                        $activity = $localized.ConfiguringNode -f $_.NodeDisplayName;
                        Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
                        Write-Verbose -Message ($localized.RestoringVirtualMachineSnapshot -f $_.NodeDisplayName, $SnapshotName);

                        Get-LabVMSnapshot -Name $_.NodeDisplayName -SnapshotName $SnapshotName | Hyper-V\Restore-VMSnapshot;
                    }
        }
        elseif ($runningNodes) {

            foreach ($runningNode in $runningNodes) {

                ## The running nodes are Get-VM objects!
                Write-Error -Message ($localized.CannotSnapshotNodeError -f $runningNode.Name);
            }
        }
        else {

            $nodes |
                Sort-Object { $_.BootOrder } |
                    ForEach-Object {

                        $currentNodeCount++;
                        [System.Int32] $percentComplete = ($currentNodeCount / $nodes.Count) * 100;
                        $activity = $localized.ConfiguringNode -f $_.NodeDisplayName;
                        Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
                        Write-Verbose -Message ($localized.RestoringVirtualMachineSnapshot -f $_.NodeDisplayName,  $SnapshotName);

                        Get-LabVMSnapshot -Name $_.NodeDisplayName -SnapshotName $SnapshotName | Hyper-V\Restore-VMSnapshot;
                    }

            Write-Progress -Id 42 -Activity $activity -Completed;
        }


    } #end process
} #end function Restore-Lab

function Set-LabHostDefault {
<#
    .SYNOPSIS
        Sets the lab host's default settings.
    .DESCRIPTION
        The Set-LabHostDefault cmdlet sets one or more lab host default settings.
    .LINK
        Get-LabHostDefault
        Reset-LabHostDefault
#>

    [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param (
        ## Lab host .mof configuration document search path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ConfigurationPath,

        ## Lab host Media/ISO storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $IsoPath,

        ## Lab host parent/master VHD(X) storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ParentVhdPath,

        ## Lab host virtual machine differencing VHD(X) storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DifferencingVhdPath,

        ## Lab module storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('ModulePath')]
        [System.String] $ModuleCachePath,

        ## Lab custom resource storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourcePath,

        ## Lab host DSC resource share name (for SMB Pull Server).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceShareName,

        ## Lab host media hotfix storage location/path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $HotfixPath,

        ## Disable local caching of file-based ISO and WIM files.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $DisableLocalFileCaching,

        ## Enable call stack logging in verbose output
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $EnableCallStackLogging,

        ## Custom DISM/ADK path.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $DismPath,

        ## Specifies an custom/internal PS repository Uri. Use the full Uri without any package
        ## name(s). '/{PackageName}/{PackageVersion}' will automatically be appended as needed.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $RepositoryUri,

        ## Specifies whether environment name prefixes/suffixes are applied to virtual switches.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $DisableSwitchEnvironmentName,

        ## Specifies whether VM differencing disks are placed into a subdirectory when an
        ## environment name is defined.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $DisableVhdEnvironmentName
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;

        $resolvablePaths = @(
            'IsoPath',
            'ParentVhdPath',
            'DifferencingVhdPath',
            'ResourcePath',
            'HotfixPath',
            'UpdatePath',
            'ConfigurationPath',
            'ModuleCachePath'
        )
        foreach ($path in $resolvablePaths) {

            if ($PSBoundParameters.ContainsKey($path)) {

                $resolvedPath = Resolve-PathEx -Path $PSBoundParameters[$path];
                if (-not ((Test-Path -Path $resolvedPath -PathType Container -IsValid) -and (Test-Path -Path (Split-Path -Path $resolvedPath -Qualifier))) ) {

                    throw ($localized.InvalidPathError -f $resolvedPath, $PSBoundParameters[$path]);
                }
                else {

                    $hostDefaults.$path = $resolvedPath.Trim('\');
                }
            }
        }

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

            $hostDefaults.ResourceShareName = $ResourceShareName;
        }
        if ($PSBoundParameters.ContainsKey('DisableLocalFileCaching')) {

            $hostDefaults.DisableLocalFileCaching = $DisableLocalFileCaching.ToBool();
        }
        if ($PSBoundParameters.ContainsKey('EnableCallStackLogging')) {

            ## Set the global script variable read by Write-Verbose
            $script:labDefaults.CallStackLogging = $EnableCallStackLogging;
            $hostDefaults.EnableCallStackLogging = $EnableCallStackLogging.ToBool();
        }
        if ($PSBoundParameters.ContainsKey('DismPath')) {

            $hostDefaults.DismPath = Resolve-DismPath -Path $DismPath;
            Write-Warning -Message ($localized.DismSessionRestartWarning);
        }
        if ($PSBoundParameters.ContainsKey('RepositoryUri')) {

            $hostDefaults.RepositoryUri = $RepositoryUri.TrimEnd('/');
        }
        if ($PSBoundParameters.ContainsKey('DisableSwitchEnvironmentName')) {

            $hostDefaults.DisableSwitchEnvironmentName = $DisableSwitchEnvironmentName.ToBool();
        }
        if ($PSBoundParameters.ContainsKey('DisableVhdEnvironmentName')) {

            $hostDefaults.DisableVhdEnvironmentName = $DisableVhdEnvironmentName.ToBool();
        }

        Set-ConfigurationData -Configuration Host -InputObject $hostDefaults;
        Import-DismModule;

        ## Refresh the defaults to ensure environment variables are updated
        $hostDefaults = Get-ConfigurationData -Configuration Host;
        return $hostDefaults;

    } #end process
} #end function

function Set-LabVMDefault {
<#
    .SYNOPSIS
        Sets the lab virtual machine default settings.
#>

    [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        ## Default virtual machine startup memory (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $StartupMemory,

        ## Default virtual machine miniumum dynamic memory allocation (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $MinimumMemory,

        ## Default virtual machine maximum dynamic memory allocation (bytes).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(536870912, 1099511627776)]
        [System.Int64] $MaximumMemory,

        ## Default virtual machine processor count.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(1, 4)]
        [System.Int32] $ProcessorCount,

        ## Default virtual machine media Id.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Media,

        ## Lab host internal switch name.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $SwitchName,

        # Input Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^([a-z]{2,2}-[a-z]{2,2})|(\d{4,4}:\d{8,8})$')]
        [System.String] $InputLocale,

        # System Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $SystemLocale,

        # User Locale
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $UserLocale,

        # UI Language
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')]
        [System.String] $UILanguage,

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

        # Registered Owner
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $RegisteredOwner,

        # Registered Organization
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] [Alias('RegisteredOrganisation')] $RegisteredOrganization,

        ## Client PFX certificate bundle used to encrypt DSC credentials
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $ClientCertificatePath,

        ## Client certificate's issuing Root Certificate Authority (CA) certificate
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $RootCertificatePath,

        ## Boot delay/pause between VM operations
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt16] $BootDelay,

        ## Secure boot status. Could be a SwitchParameter but boolean is more explicit?
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Boolean] $SecureBoot,

        ## Custom bootstrap order
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('ConfigurationFirst','ConfigurationOnly','Disabled','MediaFirst','MediaOnly')]
        [System.String] $CustomBootstrapOrder = 'MediaFirst',

        ## Enable Guest Integration Services
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Boolean] $GuestIntegrationServices,

        ## Enable Automatic Checkpoints (Windows 10 build 1709 and later)
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Boolean] $AutomaticCheckpoints,

        ## WSMan maximum envelope size
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $MaxEnvelopeSizeKb,

        ## By default, all virtual machines and their associated disks are created using the node name as it is
        ## defined in the .psd1 configuration file. This name can be either a FQDN or a NetBIOS name format. Enabling
        ## the 'UseNetBIOSName' forces all VMs and disk files to be created using the NetBIOS (short) name format.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseNetBIOSName
    )
    process {

        $vmDefaults = Get-ConfigurationData -Configuration VM;

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

            $vmDefaults.StartupMemory = $StartupMemory;
        }

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

            $vmDefaults.MinimumMemory = $MinimumMemory;
        }

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

            $vmDefaults.MaximumMemory = $MaximumMemory;
        }

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

            $vmDefaults.ProcessorCount = $ProcessorCount;
        }

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

            ## Resolve-LabMedia will throw if media cannot be resolved
            $labMedia = Resolve-LabMedia -Id $Media;
            $vmDefaults.Media = $labMedia.Id;
        }

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

            $vmDefaults.SwitchName = $SwitchName;
        }

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

            $vmDefaults.Timezone = Assert-TimeZone -TimeZone $Timezone;
        }

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

            $vmDefaults.UILanguage = $UILanguage;
        }

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

            $vmDefaults.InputLocale = $InputLocale;
        }

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

            $vmDefaults.SystemLocale = $SystemLocale;
        }

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

            $vmDefaults.UserLocale = $UserLocale;
        }

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

            $vmDefaults.RegisteredOwner = $RegisteredOwner;
        }

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

            $vmDefaults.RegisteredOrganization = $RegisteredOrganization;
        }

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

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

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

                    throw ($localized.CannotFindCertificateError -f 'Client', $ClientCertificatePath);
                }
            }
            $vmDefaults.ClientCertificatePath = $ClientCertificatePath;
        }

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

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

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

                    throw ($localized.CannotFindCertificateError -f 'Root', $RootCertificatePath);
                }
            }
            $vmDefaults.RootCertificatePath = $RootCertificatePath;
        }

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

            $vmDefaults.BootDelay = $BootDelay;
        }

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

            $vmDefaults.CustomBootstrapOrder = $CustomBootstrapOrder;
        }

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

            $vmDefaults.SecureBoot = $SecureBoot;
        }

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

            $vmDefaults.GuestIntegrationServices = $GuestIntegrationServices;
        }

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

            $vmDefaults.AutomaticCheckpoints = $AutomaticCheckpoints;
        }

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

            $vmDefaults.MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb;
        }

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

            $vmDefaults.UseNetBIOSName = $UseNetBIOSName.ToBool();
        }

        if ($vmDefaults.StartupMemory -lt $vmDefaults.MinimumMemory) {

            throw ($localized.StartMemLessThanMinMemError -f $vmDefaults.StartupMemory, $vmDefaults.MinimumMemory);
        }
        elseif ($vmDefaults.StartupMemory -gt $vmDefaults.MaximumMemory) {

            throw ($localized.StartMemGreaterThanMaxMemError -f $vmDefaults.StartupMemory, $vmDefaults.MaximumMemory);
        }

        $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Set-LabVMDefault', $vmName;
        $verboseProcessMessage = $localized.SettingVMDefaults;
        if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

            Set-ConfigurationData -Configuration VM -InputObject $vmDefaults;
        }

        ## BootOrder property should not be exposed via the Get-LabVMDefault/Set-LabVMDefault
        $vmDefaults.PSObject.Properties.Remove('BootOrder');
        return $vmDefaults;

    }
} #end function

function Start-DscCompilation {
<#
    .SYNOPSIS
        Starts compilation of one or more DSC configurations.
    .DESCRIPTION
        The Start-DscCompilation cmdlet can compile multiple PowerShell DSC configurations (.ps1 files), in parallel
        using jobs. Each PowerShell DSC configuration (.ps1) is loaded into a separate PowerShell.exe process, and
        called using the supplied configuration parameter hashtable to each each instance.
    .NOTES
        The Start-DscCompilation cmdlet assumes/requires that each node has its own PowerShell DSC configuration
        (.psd1) document.
    .PARAMETER Configuration
        Specifies the file path(s) to PowerShell DSC configuration (.ps1) files to compile.
    .PARAMETER InoutObject
        Specifies the file references to PowerShell DSC configuration (.ps1) files to complile.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document used to create the virtual machines. One virtual machine is created per node defined
        in the AllNodes array.
    .PARAMETER Path
        Specifies the directory path containing the PowerShell DSC configuration files. If this parameter is not
        specified, it defaults to the current working directory.
    .PARAMETER NodeName
        Specifies one or more node names contained in the PowerShell DSC configuration (.psd1) document to compile.
        If no node names are specified, all nodes defined within the configuration are compiled.
    .PARAMETER OutputPath
        Specifies the output path of the compiled DSC .mof file(s). If this parameter is not specified, it defaults
        to the current working directory.
    .PARAMETER AsJob
        Specifies that the cmdlet return a PowerShell job for each PowerShell DSC compilation instance. By default,
        the cmdlet will block the console until it finishes all comilation tasks.
    .EXAMPLE
        Start-DscCompilation -ConfigurationData .\config.psd1 -ConfigurationParameters @{ Credential = $credential; }

        Initiates compilation of all nodes defined in the .\config.psd1 file. For each node, the matching
        <NodeName.ps1> PowerShell DSC configuration file is loaded in to a separate PowerShell.exe process, and is
        subsequently called.

        The 'Credential' parameter is passed to each PowerShell.exe instance. The resulting PowerShell DSC .mof files
        are written out to the current working directory.
#>

    [CmdletBinding(DefaultParameterSetName = 'Configuration')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.IO.FileInfo], [System.Management.Automation.Job])]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Configuration')]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Configuration,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileInfo[]] $InputObject,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [ValidateNotNullOrEmpty()]
        [System.String] $ConfigurationData,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path = (Get-Location -PSProvider Filesystem).Path,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $NodeName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable] $ConfigurationParameters,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $OutputPath = (Get-Location -PSProvider Filesystem).Path,

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

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

            $ConfigurationData = Resolve-Path -Path $ConfigurationData -ErrorAction Stop;
        }

        if (-not ($PSBoundParameters.ContainsKey('ConfigurationParameters'))) {

            ## Set the output path
            $ConfigurationParameters = @{ OutputPath = $OutputPath };

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

                $ConfigurationParameters['ConfigurationData'] = $ConfigurationData;
            }

        }
        else {

            if (($PSBoundParameters.ContainsKey('OutputPath')) -and
                ($ConfigurationParameters.ContainsKey('OutputPath'))) {

                ## OutputPath was explicitly passed and is also defined in ConfigurationParameters
                Write-Warning -Message ($localized.ExplicitOutputPathWarning -f $OutputPath);
                $ConfigurationParameters['OutputPath'] = $OutputPath;
            }
            elseif (-not ($ConfigurationParameters.ContainsKey('OutputPath'))) {

                $ConfigurationParameters['OutputPath'] = $OutputPath;
            }

            if (($ConfigurationParameters.ContainsKey('ConfigurationData')) -and
                ($PSBoundParameters.ContainsKey('ConfigurationData'))) {

                ## ConfigurationData was explicitly passed and is also defined in ConfigurationParameters
                Write-Warning -Message ($localized.ExplicitConfigurationDataWarning -f $ConfigurationData);
                $ConfigurationParameters['ConfigurationData'] = $ConfigurationData;
            }
            elseif (-not ($ConfigurationParameters.ContainsKey('ConfigurationData')) -and
                         ($PSBoundParameters.ContainsKey('ConfigurationData'))) {

                $ConfigurationParameters['ConfigurationData'] = $ConfigurationData;
            }

        }

        $filePaths = @();

    } #end begin

    process {

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

            foreach ($object in $InputObject) {

                Write-Verbose -Message ($localized.AddingConfiguration -f $object.FullName);
                if (Test-Path -Path $object.FullName) {

                    $filePaths += $object.FullName;
                }
                else {

                    Write-Error -Message ($localized.InvalidPathError -f 'File', $nodePath);
                }
            } #end foreach configuration fileinfo

        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ConfigurationData') {

            <# Can't pass a hashtable to Start-Job due to Array/ArrayList serialization/deserialization issue. Need
               to pass -ConfigurationData by file path.

                ConfigurationData parameter property AllNodes needs to be a collection.
                    + CategoryInfo : InvalidOperation: (:) [Write-Error], InvalidOperationException
                    + FullyQualifiedErrorId : ConfiguratonDataAllNodesNeedHashtable,ValidateUpdate-ConfigurationData
                    + PSComputerName : localhost
            #>

            $configData = ConvertTo-ConfigurationData -ConfigurationData $ConfigurationData;

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

                $nodeName = $configData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { $_.NodeName };
            }

            foreach ($node in $nodeName) {

                $nodePath = Join-Path -Path $Path -ChildPath "$node.ps1";
                Write-Verbose -Message ($localized.AddingConfiguration -f $nodePath);
                if (Test-Path -Path $nodePath) {

                    $filePaths += $nodePath;
                }
                else {

                    Write-Error -Message ($localized.InvalidPathError -f 'File', $nodePath);
                }
            } #end foreach node in configuration data

        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Configuration') {

            foreach ($filePath in $Configuration) {

                try {
                    $resolvedFilePath = Resolve-Path -Path $filePath -ErrorAction Stop;
                    if (Test-Path -Path $resolvedFilePath) {

                        Write-Verbose -Message ($localized.AddingConfiguration -f $resolvedFilePath);
                        $filePaths += $resolvedFilePath;
                    }
                    else {

                        Write-Error -Message ($localized.InvalidPathError -f 'File', $resolvedFilePath);
                    }

                }
                catch {

                    Write-Error -Message ($localized.InvalidPathError -f 'File', $filePath);
                }

            } #end foreach configuration file path
        }

    } #end process

    end {

        if ($filePaths.Count -eq 0) {

            throw ($localized.NoConfigurationToCompileError);
        }

        $jobs = @();

        ## Start the jobs
        foreach ($filePath in $filePaths) {

            $invokeDscConfigurationCompilationParams = @{
                Configuration = $filePath;
                AsJob = $true;
            }
            if ($ConfigurationParameters.Keys.Count -gt 0) {

                $invokeDscConfigurationCompilationParams['ConfigurationParameters'] = $ConfigurationParameters;
            }
            Write-Verbose -Message ($localized.AddingCompilationJob -f $filePath);
            $jobs += Start-DscConfigurationCompilation @invokeDscConfigurationCompilationParams;

        }

        if ($AsJob) {

            return $jobs;
        }
        else {

            ## Wait for compilation to finish
            $isJobsComplete = $false;
            $completedJobs = @();

            $activity = $localized.CompilingConfigurationActivity;
            $totalPercentComplete = 0;
            $stopwatch = [System.Diagnostics.Stopwatch]::StartNew();

            while ($isJobsComplete -eq $false) {

                $isJobsComplete = $true;
                $jobPercentComplete++;

                if ($jobPercentComplete -gt 100) {

                    ## Loop progress
                    $jobPercentComplete = 1;
                }

                if ($jobPercentComplete % 2 -eq 0) {

                    ## Ensure total progresses at a different speed
                    $totalPercentComplete++;
                    if ($totalPercentComplete -gt 100) {

                        $totalPercentComplete = 1;
                    }
                }

                $elapsedTime =  $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff');
                Write-Progress -Id $pid -Activity $activity -Status $elapsedTime -PercentComplete $totalPercentComplete;

                foreach ($job in $jobs) {

                    if ($job.HasMoreData -or $job.State -eq 'Running') {

                        Write-Progress -Id $job.Id -ParentId $pid -Activity $job.Name -PercentComplete $jobPercentComplete;
                        $isJobsComplete = $false;
                        $job | Receive-Job;
                    }
                    elseif ($job.State -ne 'NotStarted') {

                        if ($job -notin $completedJobs) {

                            $elapsedTime = $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff');
                            $compilationStatus = $localized.ProcessedComilationStatus;
                            Write-Verbose -Message ("{0} '{1}' in '{2}'." -f $compilationStatus, $job.Name, $elapsedTime);
                            Write-Progress -Id $job.Id -ParentId $pid -Activity $job.Name -Completed;
                            $completedJobs += $job;
                        }
                    }

                } #end foreach job

                Start-Sleep -Milliseconds 750;

            } #end while active job(s)

            $elapsedTime = $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff');
            Write-Verbose -Message ($localized.CompletedCompilationProcessing -f $elapsedTime);
            Write-Progress -Id $pid -Activity $activity -Completed;
            $stopwatch = $null;

        } #end not job

    } #end end

} #end function

function Start-Lab {
<#
    .SYNOPSIS
        Starts all VMs in a lab in a predefined order.
    .DESCRIPTION
        The Start-Lab cmdlet starts all nodes defined in a PowerShell DSC configuration document, in a preconfigured
        order.

        Unlike the standard Start-VM cmdlet, the Start-Lab cmdlet will read the specified PowerShell DSC configuration
        document and infer the required start up order.

        The PowerShell DSC configuration document can define the start/stop order of the virtual machines and the boot
        delay between each VM power operation. This is defined with the BootOrder and BootDelay properties. The lower
        the virtual machine's BootOrder index, the earlier it is started (in relation to the other VMs).

        For example, a VM with a BootOrder index of 10 will be started before a VM with a BootOrder index of 11. All
        virtual machines receive a BootOrder value of 99 unless specified otherwise.

        The delay between each power operation is defined with the BootDelay property. This value is specified in
        seconds and is enforced between starting or stopping a virtual machine.

        For example, a VM with a BootDelay of 30 will enforce a 30 second delay after being powered on or after the
        power off command is issued. All VMs receive a BootDelay value of 0 (no delay) unless specified otherwise.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document.
    .LINK
        about_ConfigurationData
        Stop-Lab
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $nodes = @();
        $ConfigurationData.AllNodes |
            Where-Object { $_.NodeName -ne '*' } |
                ForEach-Object {
                    $nodes += [PSCustomObject] (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData);
                };

        $currentGroupCount = 0;
        $bootGroups = $nodes | Sort-Object -Property BootOrder | Group-Object -Property BootOrder;
        $bootGroups | ForEach-Object {

            $nodeDisplayNames = $_.Group.NodeDisplayName;
            $nodeDisplayNamesString = $nodeDisplayNames -join ', ';
            $currentGroupCount++;
            [System.Int32] $percentComplete = ($currentGroupCount / $bootGroups.Count) * 100;
            $activity = $localized.ConfiguringNode -f $nodeDisplayNamesString;
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
            Write-Verbose -Message ($localized.StartingVirtualMachine -f $nodeDisplayNamesString);
            Hyper-V\Start-VM -Name $nodeDisplayNames;

            $maxGroupBootDelay = $_.Group.BootDelay | Sort-Object -Descending | Select-Object -First 1;
            if (($maxGroupBootDelay -gt 0) -and ($currentGroupCount -lt $bootGroups.Count)) {

                Write-Verbose -Message ($localized.WaitingForVirtualMachine -f $maxGroupBootDelay, $nodeDisplayNamesString);
                for ($i = 1; $i -le $maxGroupBootDelay; $i++) {

                    [System.Int32] $waitPercentComplete = ($i / $maxGroupBootDelay) * 100;
                    $waitActivity = $localized.WaitingForVirtualMachine -f $maxGroupBootDelay, $nodeDisplayNamesString;
                    Write-Progress -ParentId 42 -Activity $waitActivity -PercentComplete $waitPercentComplete;
                    Start-Sleep -Seconds 1;
                }

                Write-Progress -Activity $waitActivity -Completed;

            } #end if boot delay
        } #end foreach boot group

        Write-Progress -Id 42 -Activity $activity -Completed;

    } #end process
} #end function Start-Lab

function Start-LabConfiguration {
<#
    .SYNOPSIS
        Invokes the deployment and configuration of a VM for each node defined in a PowerShell DSC configuration
        document.
    .DESCRIPTION
        The Start-LabConfiguration initiates the configuration of all nodes defined in a PowerShell DSC configuration
        document. The AllNodes array is enumerated and a virtual machine is created per node, using the NodeName
        property.

        If any existing virtual machine exists with the same name as the NodeName declaration of the AllNodes array,
        the cmdlet will throw an error. If this behaviour is not desired, you can forcibly remove the existing VM
        with the -Force parameter. NOTE: THE VIRTUAL MACHINE'S EXISTING DATA WILL BE LOST.

        The virtual machines' local administrator password must be specified when creating the lab VMs. The local
        administrator password can be specified as a [PSCredential] or a [SecureString] type. If a [PSCredential] is
        used then the username is not used.

        It is possible to override the module's virtual machine default settings by specifying the required property
        on the node hashtable in the PowerShell DSC configuration document. Default settings include the Operating
        System image to use, the amount of memory assigned to the virtual machine and/or the virtual switch to
        connect the virtual machine to. If the settings are not overridden, the module's defaults are used. Use the
        Get-LabVMDefault cmdlet to view the module's default values.

        Each virtual machine created by the Start-LabConfiguration cmdlet, has its PowerShell DSC configuraion (.mof)
        file injected into the VHD file as it is created. This configuration is then applied during the first boot
        process to ensure the virtual machine is configured as required. If the path to the VM's .mof files is not
        specified, the module's default Configuration directory is used. Use the Get-LabHostDefault cmdlet to view
        the module's default Configuration directory path.

        The virtual machine .mof files must be created before creating the lab. If any .mof files are missing, the
        Start-LabConfiguration cmdlet will generate an error. You can choose to ignore this error by specifying
        the -SkipMofCheck parameter. If you skip the .mof file check - and no .mof file is found - no configuration
        will be applied to the virtual machine's Operating System settings.

        When deploying a lab, the module will create a default baseline snapshot of all virtual machines. This
        snapshot can be used to revert all VMs back to their default configuration. If this snapshot is not
        required, it can be surpressed with the -NoSnapshot parameter.
    .NOTES
        The same local administrator password is used for all virtual machines created in the same lab configuration.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document used to create the virtual machines. One virtual machine is created per node defined
        in the AllNodes array.
    .PARAMETER Credential
        Specifies the local administrator password of all virtual machines in the lab configuration. The same
        password is used for all virtual machines in the same lab configuration. The username is not used.
    .PARAMETER Password
        Specifies the local administrator password of all virtual machines in the lab configuration. The same
        password is used for all virtual machines in the same lab configuration.
    .PARAMETER Path
        Specifies the directory path containing the individual PowerShell DSC .mof files. If not specified, the
        module's default location is used.
    .PARAMETER NoSnapshot
        Specifies that no default snapshot will be taken when creating the virtual machine.

        NOTE: If no default snapshot is not created, the lab cannot be restored to its initial configuration
        with the Reset-Lab cmdlet.
    .PARAMETER SkipMofCheck
        Specifies that the module will configure a virtual machines that do not have a corresponding .mof file
        located in the -Path specfified. By default, if any .mof file cannot be located then the cmdlet will
        generate an error.

        NOTE: If no .mof file is found and the -SkipMofCheck parameter is specified, no configuration will be
        applied to the virtual machine's Operating System configuration.
    .PARAMETER IgnorePendingReboot
        The host's configuration is checked before invoking a lab configuration, including checking for pending
        reboots. The -IgnorePendingReboot specifies that a pending reboot should be ignored and the lab
        configuration applied.
    .PARAMETER Force
        Specifies that any existing virtual machine with a matching name, will be removed and recreated. By
        default, if a virtual machine already exists with the same name, the cmdlet will generate an error.

        NOTE: If the -Force parameter is specified - and a virtual machine with the same name already exists -
        ALL EXISTING DATA WITHIN THE VM WILL BE LOST.
    .PARAMETER FeedCredential
        A [PSCredential] object containing the credentials to use when accessing a private Azure DevOps feed.
    .LINK
        about_ConfigurationData
        about_Bootstrap
        Get-LabHostDefault
        Set-LabHostDefault
        Get-LabVMDefault
        Set-LabVMDefault
        Reset-Lab
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'PSCredential')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [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,

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

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

        ## Forces a reconfiguration/redeployment of all nodes.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force,

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

        ## Skips pending reboot check
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $IgnorePendingReboot,

        ## 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'); }

        if (-not (Test-LabHostConfiguration -IgnorePendingReboot:$IgnorePendingReboot) -and (-not $Force)) {

            throw $localized.HostConfigurationTestError;
        }
    }
    process {

        Write-Verbose -Message $localized.StartedLabConfiguration;
        $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' };

        ## There is an assumption here is all .mofs are in the same folder
        $resolveConfigurationPathParams = @{
            ConfigurationData = $ConfigurationData;
            Name = $nodes | Select-Object -First 1 | ForEach-Object { $_.NodeName };
            Path = $Path;
            UseDefaultPath = $SkipMofCheck;
        }
        $Path = Resolve-ConfigurationPath @resolveConfigurationPathParams;

        foreach ($node in $nodes) {

            $assertLabConfigurationMofParams = @{
                ConfigurationData = $ConfigurationData;
                Name = $node.NodeName;
                Path = $Path;
            }
            Assert-LabConfigurationMof @assertLabConfigurationMofParams -SkipMofCheck:$SkipMofCheck;

        } #end foreach node

        $currentNodeCount = 0;
        foreach ($node in (Test-LabConfiguration -ConfigurationData $ConfigurationData -WarningAction SilentlyContinue)) {
            ## Ignore Lability warnings during the test phase, e.g. existing switches and .mof files

            $currentNodeCount++;
            [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1;
            $activity = $localized.ConfiguringNode -f $node.Name;
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
            if ($node.IsConfigured -and $Force) {

                Write-Verbose -Message ($localized.NodeForcedConfiguration -f $node.Name);

                $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'New-VM', $node.Name;
                $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVM -f $node.Name);
                if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                    $newLabVirtualMachineParams = @{
                        Name = $node.Name;
                        ConfigurationData = $ConfigurationData;
                        Path = $Path;
                        NoSnapshot = $NoSnapshot;
                        Credential = $Credential;
                    }
                    New-LabVirtualMachine @newLabVirtualMachineParams;
                }
            }
            elseif ($node.IsConfigured) {

                Write-Verbose -Message ($localized.NodeAlreadyConfigured -f $node.Name);
            }
            else {

                Write-Verbose -Message ($localized.NodeMissingOrMisconfigured -f $node.Name);

                $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Start-LabConfiguration', $node.Name;
                $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVM -f $node.Name);
                if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

                    $newLabVirtualMachineParams = @{
                        Name = $node.Name;
                        ConfigurationData = $ConfigurationData;
                        Path = $Path;
                        NoSnapshot = $NoSnapshot;
                        Credential = $Credential;
                        FeedCredential = $FeedCredential;
                    }
                    [ref] $null = New-LabVirtualMachine @newLabVirtualMachineParams;
                }
            }

        } #end foreach node

        Write-Progress -Id 42 -Activity $activity -Completed;
        Write-Verbose -Message $localized.FinishedLabConfiguration;

    } #end process
} #end function Start-LabConfiguration

function Start-LabHostConfiguration {
<#
    .SYNOPSIS
        Invokes the configuration of the lab host.
    .DESCRIPTION
        The Start-LabHostConfiguration cmdlet invokes the configuration of the local host computer.
    .PARAMETER IgnorePendingReboot
        Specifies a pending reboot does not fail the test.
    .LINK
        Test-LabHostConfiguration
        Get-LabHostConfiguration
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Boolean])]
    param (
        ## Ignores pending reboot check
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $IgnorePendingReboot
    )
    process {

        Write-Verbose -Message $localized.StartedHostConfiguration;
        ## Create required directory structure
        $hostDefaults = Get-ConfigurationData -Configuration Host;
        foreach ($property in $hostDefaults.PSObject.Properties) {

            if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) {

                ## DismPath is not a folder and should be ignored (#159)
                if ($property.Name -ne 'DismPath') {

                    [ref] $null = New-Directory -Path $(Resolve-PathEx -Path $Property.Value) -ErrorAction Stop;
                }
            }
        }

        # Once all the path are created, check if the hostdefaults.Json file in the $env:ALLUSERSPROFILE is doesn't have entries with %SYSTEMDRIVE% in it
        # Many subsequent call are failing to Get-LabImage, Test-LabHostConfiguration which do not resolve the "%SYSTEMDRIVE%" in the path for Host defaults
        foreach ($property in $($hostDefaults.PSObject.Properties | Where-Object -Property TypeNameOfValue -eq 'System.String')) {

            if ($property.Value.Contains('%')) {

                # if the Path for host defaults contains a '%' character then resolve it
                $resolvedPath = Resolve-PathEx -Path $Property.Value;
                # update the hostdefaults Object
                $hostDefaults.($property.Name)  = $resolvedPath;
                $hostDefaultsUpdated = $true;
            }
        }
        if ($hostDefaultsUpdated) {

            # Write the changes back to the json file in the $env:ALLUSERSPROFILE
            $hostDefaults | ConvertTo-Json | Out-File -FilePath $(Resolve-ConfigurationDataPath -Configuration Host);
        }

        $labHostSetupConfiguation = Get-LabHostSetupConfiguration;
        foreach ($configuration in $labHostSetupConfiguation) {

            if ($IgnorePendingReboot -and ($configuration.ModuleName -eq 'xPendingReboot')) {

                Write-Warning -Message ($localized.IgnorePendingRebootWarning -f $configuration.Prefix);
                continue;
            }

            Import-LabDscResource -ModuleName $configuration.ModuleName -ResourceName $configuration.ResourceName -Prefix $configuration.Prefix -UseDefault:$configuration.UseDefault;
            Write-Verbose -Message ($localized.TestingNodeConfiguration -f $Configuration.Description);
            [ref] $null = Invoke-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters;
            ## TODO: Need to check for pending reboots..
        }
        Write-Verbose -Message $localized.FinishedHostConfiguration;

    } #end process
} #end function

function Stop-Lab {
<#
    .SYNOPSIS
        Stops all VMs in a lab in a predefined order.
    .DESCRIPTION
        The Stop-Lab cmdlet stops all nodes defined in a PowerShell DSC configuration document, in a preconfigured
        order.

        Unlike the standard Stop-VM cmdlet, the Stop-Lab cmdlet will read the specified PowerShell DSC configuration
        document and infer the required shutdown order.

        The PowerShell DSC configuration document can define the start/stop order of the virtual machines and the boot
        delay between each VM power operation. This is defined with the BootOrder and BootDelay properties. The higher
        the virtual machine's BootOrder index, the earlier it is stopped (in relation to the other VMs).

        For example, a VM with a BootOrder index of 11 will be stopped before a VM with a BootOrder index of 10. All
        virtual machines receive a BootOrder value of 99 unless specified otherwise.

        The delay between each power operation is defined with the BootDelay property. This value is specified in
        seconds and is enforced between starting or stopping a virtual machine.

        For example, a VM with a BootDelay of 30 will enforce a 30 second delay after being powered on or after the
        power off command is issued. All VMs receive a BootDelay value of 0 (no delay) unless specified otherwise.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document.
    .LINK
        about_ConfigurationData
        Start-Lab
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')]
    param (
        ## Lab DSC configuration data
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        $nodes = @();
        $ConfigurationData.AllNodes |
            Where-Object { $_.NodeName -ne '*' } |
                ForEach-Object {
                    $nodes += [PSCustomObject] (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData);
                };

        $currentGroupCount = 0;
        $bootGroups = $nodes | Sort-Object -Property BootOrder -Descending | Group-Object -Property BootOrder;
        $bootGroups | ForEach-Object {

            $nodeDisplayNames = $_.Group.NodeDisplayName;
            $nodeDisplayNamesString = $nodeDisplayNames -join ', ';
            $currentGroupCount++;
            [System.Int32] $percentComplete = ($currentGroupCount / $bootGroups.Count) * 100;
            $activity = $localized.ConfiguringNode -f $nodeDisplayNamesString;
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
            Write-Verbose -Message ($localized.StoppingVirtualMachine -f $nodeDisplayNamesString);
            Hyper-V\Stop-VM -Name $nodeDisplayNames -Force;
        } #end foreach boot group

        Write-Progress -Id 42 -Activity $activity -Completed;

    } #end process
} #end function Stop-Lab

function Test-LabConfiguration {
<#
    .SYNOPSIS
        Tests the configuration of all VMs in a lab.
    .DESCRIPTION
        The Test-LabConfiguration determines whether all nodes defined in a PowerShell DSC configuration document
        are configured correctly and returns the results.

        WANRING: Only the virtual machine configuration is checked, not in the internal VM configuration. For example,
        the virtual machine's memory configuraiton, virtual switch configuration and processor count are tested. The
        VM's operating system configuration is not checked.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document used to create the virtual machines. Each node defined in the AllNodes array is
        tested.
    .LINK
        about_ConfigurationData
        Start-LabConfiguration
#>

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

        Write-Verbose -Message $localized.StartedLabConfigurationTest;
        $currentNodeCount = 0;
        $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' };
        foreach ($node in $nodes) {

            $currentNodeCount++;
            $nodeProperties = Resolve-NodePropertyValue -NodeName $node.NodeName -ConfigurationData $ConfigurationData;
            [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1;
            $activity = $localized.ConfiguringNode -f $nodeProperties.NodeDisplayName;
            Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete;
            $nodeResult = [PSCustomObject] @{
                Name = $nodeProperties.NodeName;
                IsConfigured = Test-LabVM -Name $node.NodeName -ConfigurationData $ConfigurationData;
                DisplayName = $nodeProperties.NodeDisplayName;
            }
            Write-Output -InputObject $nodeResult;
        }

        Write-Verbose -Message $localized.FinishedLabConfigurationTest;

    } #end process
} #end function Test-LabConfiguration

function Test-LabHostConfiguration {
<#
    .SYNOPSIS
        Tests the lab host's configuration.
    .DESCRIPTION
        The Test-LabHostConfiguration tests the current configuration of the lab host.
    .PARAMETER IgnorePendingReboot
        Specifies a pending reboot does not fail the test.
    .LINK
        Get-LabHostConfiguration
        Test-LabHostConfiguration
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Skips pending reboot check
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $IgnorePendingReboot
    )
    process {

        Write-Verbose -Message $localized.StartedHostConfigurationTest;
        ## Test folders/directories
        $hostDefaults = Get-ConfigurationData -Configuration Host;
        foreach ($property in $hostDefaults.PSObject.Properties) {

            if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) {

                ## DismPath is not a folder and should be ignored (#159)
                if ($property.Name -ne 'DismPath') {

                    Write-Verbose -Message ($localized.TestingPathExists -f $property.Value);
                    $resolvedPath = Resolve-PathEx -Path $property.Value;
                    if (-not (Test-Path -Path $resolvedPath -PathType Container)) {

                        Write-Verbose -Message ($localized.PathDoesNotExist -f $resolvedPath);
                        return $false;
                    }
                }
            }
        }

        $labHostSetupConfiguration = Get-LabHostSetupConfiguration;
        foreach ($configuration in $labHostSetupConfiguration) {

            $importDscResourceParams = @{
                ModuleName = $configuration.ModuleName;
                ResourceName = $configuration.ResourceName;
                Prefix = $configuration.Prefix;
                UseDefault = $configuration.UseDefault;
            }
            Import-LabDscResource @importDscResourceParams;
            Write-Verbose -Message ($localized.TestingNodeConfiguration -f $Configuration.Description);

            if (-not (Test-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters)) {

                if ($configuration.Prefix -eq 'PendingReboot') {

                    Write-Warning -Message $localized.PendingRebootWarning;
                    if (-not $IgnorePendingReboot) {

                        return $false;
                    }
                }
                else {

                    return $false;
                }
            }
        } #end foreach labHostSetupConfiguration

        Write-Verbose -Message $localized.FinishedHostConfigurationTest;
        return $true;

    } #end process
} #end function

function Test-LabImage {
<#
    .SYNOPSIS
        Tests whether a master/parent lab image is present.
    .DESCRIPTION
        The Test-LabImage cmdlet returns whether a specified disk image is present.
    .PARAMETER Id
        Specifies the media Id of the image to test.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1
        configuration document that contains the required media definition.
    .EXAMPLE
        Test-LabImage -Id 2016_x64_Datacenter_EN_Eval

        Tests whether the '-Id 2016_x64_Datacenter_EN_Eval' lab image is present.
    .LINK
        Get-LabImage
#>

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

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

        if (Get-LabImage @PSBoundParameters) {

            return $true;
        }
        else {

            return $false;
        }

    } #end process
} #end function Test-LabImage

function Test-LabMedia {
<#
    .SYNOPSIS
        Tests whether lab media has already been successfully downloaded.
    .DESCRIPTION
        The Test-LabMedia cmdlet will check whether the specified media Id has been downloaded and its checksum is correct.
    .PARAMETER Id
        Specifies the media Id (or alias) to test.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Id
    )
    process
    {
        $hostDefaults = Get-ConfigurationData -Configuration Host
        $media = Get-LabMedia -Id $Id
        if ($media)
        {
            if (-not $hostDefaults.DisableLocalFileCaching)
            {
                $testResourceDownloadParams = @{
                    DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename
                    Uri = $media.Uri
                    Checksum = $media.Checksum
                }
                return Test-ResourceDownload @testResourceDownloadParams
            }
            else
            {
                ## Local file resource caching is disabled
                return $true
            }
        }
        else
        {
            return $false
        }

    } #end process
} #end function

function Test-LabResource {
<#
    .SYNOPSIS
        Tests whether a lab's resources are present.
#>

    [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(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId,

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

        if (-not $ResourcePath) {
            $hostDefaults = Get-ConfigurationData -Configuration Host;
            $ResourcePath = $hostDefaults.ResourcePath;
        }

    }
    process {

        if ($resourceId) {

            $resources = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId;
        }
        else {

            $resources = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource;
        }

        foreach ($resource in $resources) {

            $fileName = $resource.Id;
            if ($resource.Filename) { $fileName = $resource.Filename; }

            $testResourceDownloadParams = @{
                DestinationPath = Join-Path -Path $ResourcePath -ChildPath $fileName;;
                Uri = $resource.Uri;
            }

            if ($resource.Checksum) {

                $testResourceDownloadParams['Checksum'] = $resource.Checksum;
            }

            if (-not (Test-ResourceDownload @testResourceDownloadParams)) {

                return $false;
            }
        } #end foreach resource

        return $true;

    } #end process
} #end function

function Test-LabStatus {
<#
    .SYNOPSIS
        Queries computers' LCM state to determine whether an existing DSC configuration has applied successfully.
    .EXAMPLE
        Test-LabStatus -ComputerName CONTROLLER, XENAPP

        Tests the CONTROLLER and XENAPP computers' LCM state, using the current user credentials, returning $true if
        both have finished.
    .EXAMPLE
        Test-LabStatus -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential)

        Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query the LCM state, returning
        $true if both have finished.
    .EXAMPLE
        Test-LabStatus -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential)

        Prompts for credentials to connect to all the computers defined in the DSC configuration document (.psd1) and
        query the LCM state, returning $true if all have finished.
    .EXAMPLE
        Test-LabStatus -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential)

        Prompts for credentials to connect to the computers by their IPAddress node property as defined in the DSC
        configuration document (.psd1) and query the LCM state.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Connect to the computer name(s) specified.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')]
        [System.String[]]
        $ComputerName,

        ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's
        ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [System.String]
        $PreferNodeProperty,

        ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN.
        ## The complete identifier for the remote endpoint is in the following format:
        ##
        ## <transport>://<server>:<port>/<ApplicationName>
        ##
        ## For example: `http://server01:8080/WSMAN`
        ##
        ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the
        ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed
        ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell.
        ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ApplicationName,

        ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are:
        ##
        ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy.
        ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. -
        ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. -
        ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. -
        ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the
        ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine
        ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP)
        ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one
        ## remote computer but collect data from or run additional commands on other remote computers.
        ##
        ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice
        ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials
        ## are passed to it, the credentials can be used to control the network session.
        ##
        ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote
        ## computer anonymously, without using authentication. If the request is made anonymously, it returns no
        ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for
        ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')]
        [System.String] $Authentication = 'Default',

        ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this
        ## action. Enter the certificate thumbprint of the certificate.
        ##
        ## Certificates are used in client certificate-based authentication. They can be mapped only to local user
        ## accounts; they do not work with domain accounts.
        ##
        ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert:
        ## drive.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $CertificateThumbprint,

        ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the
        ## default port is 80. When the transport is HTTPS, the default port is 443.
        ##
        ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's
        ## certificate common name (CN).
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Port,

        ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote
        ## computer. By default, SSL is not used.
        ##
        ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL
        ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the
        ## port that is used for the connection, and you specify this parameter, the command fails.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseSSL,

        ## Credential used to connect to the remote computer.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )
    begin {

        ## Authentication might not be explicitly passed, add it so it gets splatted
        $PSBoundParameters['Authentication'] = $Authentication;

    }
    process {

        $labStatuses = @(Get-LabStatus @PSBoundParameters);
        $isDeploymentCompleted = $true;

        foreach ($labStatus in $labStatuses) {

            Write-Verbose -Message ($localized.TestingNodeStatus -f $labStatus.ComputerName, $labStatus.LCMState);
            if ($labStatus.Completed -eq $false) {

                ## VM still applying (or has failed)
                $isDeploymentCompleted = $false;
            }
        }

        return $isDeploymentCompleted;

    } #end process
} #end function

function Test-LabVM {
<#
    .SYNOPSIS
        Checks whether the (external) lab virtual machine is configured as required.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Specifies the lab virtual machine/node name.
        [Parameter(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
    )
    process {

        if (-not $Name) {

            $Name = $ConfigurationData.AllNodes | Where-Object NodeName -ne '*' | ForEach-Object { $_.NodeName }
        }

        foreach ($vmName in $Name) {

            $isNodeCompliant = $true;
            $node = Resolve-NodePropertyValue -NodeName $vmName -ConfigurationData $ConfigurationData;
            Write-Verbose -Message ($localized.TestingNodeConfiguration -f $node.NodeDisplayName);

            Write-Verbose -Message ($localized.TestingVMConfiguration -f 'Image', $node.Media);
            if (-not (Test-LabImage -Id $node.Media -ConfigurationData $ConfigurationData)) {

                $isNodeCompliant = $false;
            }
            else {

                ## No point testing switch, vhdx and VM if the image isn't available
                Write-Verbose -Message ($localized.TestingVMConfiguration -f 'Virtual Switch', $node.SwitchName);
                foreach ($switchName in $node.SwitchName) {

                    if (-not (Test-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData)) {

                        $isNodeCompliant = $false;
                    }
                }

                Write-Verbose -Message ($localized.TestingVMConfiguration -f 'VHDX', $node.Media);
                $testLabVMDiskParams = @{
                    Name = $node.NodeDisplayName;
                    Media = $node.Media;
                    ConfigurationData = $ConfigurationData;
                }
                if (-not (Test-LabVMDisk @testLabVMDiskParams -ErrorAction SilentlyContinue)) {

                    $isNodeCompliant = $false;
                }
                else {

                    ## No point testing VM if the VHDX isn't available
                    Write-Verbose -Message ($localized.TestingVMConfiguration -f 'VM', $vmName);
                    $testLabVirtualMachineParams = @{
                        Name = $node.NodeDisplayName;
                        SwitchName = $node.SwitchName;
                        Media = $node.Media;
                        StartupMemory = $node.StartupMemory;
                        MinimumMemory = $node.MinimumMemory;
                        MaximumMemory = $node.MaximumMemory;
                        ProcessorCount = $node.ProcessorCount;
                        MACAddress = $node.MACAddress;
                        SecureBoot = $node.SecureBoot;
                        GuestIntegrationServices = $node.GuestIntegrationServices;
                        ConfigurationData = $ConfigurationData;
                    }
                    if (-not (Test-LabVirtualMachine @testLabVirtualMachineParams)) {

                        $isNodeCompliant = $false;
                    }
                }
            }

            Write-Output -InputObject $isNodeCompliant;

        } #end foreach vm

    } #end process
} #end function Test-LabVM

function Unregister-LabMedia {
<#
    .SYNOPSIS
        Unregisters a custom media entry.
    .DESCRIPTION
        The Unregister-LabMedia cmdlet allows removing custom media entries from the host's configuration.
    .LINK
        Get-LabMedia
        Register-LabMedia
#>

    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSProvideDefaultParameterValue', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param
    (
        ## Specifies the custom media Id (or alias) to unregister. You cannot unregister the built-in media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Id
    )
    process
    {
        ## Get the custom media list
        $customMedia = Get-ConfigurationData -Configuration CustomMedia
        if (-not $customMedia)
        {
            ## We don't have anything defined
            Write-Warning -Message ($localized.NoCustomMediaFoundWarning -f $Id)
            return
        }
        else
        {
            ## Check if we have a matching Id
            $media = $customMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id }
            if (-not $media)
            {
                ## We don't have a custom matching Id registered
                Write-Warning -Message ($localized.NoCustomMediaFoundWarning -f $Id)
                return
            }
        }

        $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Unregister-LabMedia', $Id
        $verboseProcessMessage = $localized.RemovingCustomMediaEntry -f $Id
        if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning))
        {
            $customMedia = $customMedia | Where-Object { $_.Id -ne $Id }
            Write-Verbose -Message ($localized.SavingConfiguration -f $Id)
            Set-ConfigurationData -Configuration CustomMedia -InputObject @($customMedia)
            return $media
        }

    } #end process
} #end function

function Wait-Lab {
<#
    .SYNOPSIS
        Queries computers' LCM state, using current user credentials, waiting for DSC configurations to be applied.
    .EXAMPLE
        Wait-Lab -ComputerName CONTROLLER, XENAPP

        Connects to the CONTROLLER and XENAPP machine to query and Wait for the LCM to finish applying the current
        configuration(s).
    .EXAMPLE
        Wait-Lab -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential)

        Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query and wait for the LCM
        to finish applying the current configuration(s).
    .EXAMPLE
        Wait-Lab -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential)

        Prompts for credentials to connect to all the computers defined in the DSC configuration document (.psd1) to
        query and wait for the LCM to finish applying the current configuration(s).
    .EXAMPLE
        Wait-Lab -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential)

        Prompts for credentials to connect to all the computers by their IPAddress node property as defined in the
        DSC configuration document (.psd1) to query and wait for the LCM to finish applying the current
        configuration(s).
    .NOTES
        The default timeout is 15 minutes.
#>

#>
    [CmdletBinding()]
    param (
        ## Connect to the computer name(s) specified.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')]
        [System.String[]]
        $ComputerName,

        ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's
        ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')]
        [System.String]
        $PreferNodeProperty,

        ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN.
        ## The complete identifier for the remote endpoint is in the following format:
        ##
        ## <transport>://<server>:<port>/<ApplicationName>
        ##
        ## For example: `http://server01:8080/WSMAN`
        ##
        ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the
        ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed
        ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell.
        ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ApplicationName,

        ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are:
        ##
        ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy.
        ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. -
        ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. -
        ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. -
        ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the
        ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine
        ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP)
        ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one
        ## remote computer but collect data from or run additional commands on other remote computers.
        ##
        ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice
        ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials
        ## are passed to it, the credentials can be used to control the network session.
        ##
        ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote
        ## computer anonymously, without using authentication. If the request is made anonymously, it returns no
        ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for
        ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0).
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')]
        [System.String] $Authentication = 'Default',

        ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this
        ## action. Enter the certificate thumbprint of the certificate.
        ##
        ## Certificates are used in client certificate-based authentication. They can be mapped only to local user
        ## accounts; they do not work with domain accounts.
        ##
        ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert:
        ## drive.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $CertificateThumbprint,

        ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the
        ## default port is 80. When the transport is HTTPS, the default port is 443.
        ##
        ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's
        ## certificate common name (CN).
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Port,

        ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote
        ## computer. By default, SSL is not used.
        ##
        ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL
        ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the
        ## port that is used for the connection, and you specify this parameter, the command fails.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $UseSSL,

        ## Credential used to connect to the remote computer.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        ## Specifies the interval between connection reties. Defaults to 60 seconds intervals.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $RetryIntervalSeconds = 60,

        ## Specifies the number of connection attempts before failing. Defaults to 15 retries.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $RetryCount = 15
    )
    begin {

        ## Authentication might not be explicitly passed, add it so it gets splatted
        $PSBoundParameters['Authentication'] = $Authentication;

        ## Remove parameters that can't be splatted again Test-LabStatus
        [ref] $null = $PSBoundParameters.Remove('RetryIntervalSeconds');
        [ref] $null = $PSBoundParameters.Remove('RetryCount');
        [ref] $null = $PSBoundParameters.Remove('AsJob');
    }
    process {

        $timer = New-Object System.Diagnostics.Stopwatch;
        $timer.Start();

        Write-Verbose -Message ($localized.StartingWaitForLabDeployment);
        for ($iteration = 0; $iteration -le $RetryCount; $iteration++) {

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

                if ($iteration -lt $RetryCount) {

                    ## Only sleep if this isn't the last iteration
                    Write-Verbose -Message ($localized.SleepingWaitingForLabDeployment -f $RetryIntervalSeconds)
                    Start-Sleep -Seconds $RetryIntervalSeconds;
                }
            }
            else {

                $elapsedTime = $timer.Elapsed.ToString('hh\:mm\:ss\.ff');
                Write-Verbose -Message ($localized.WaitForLabDeploymentComplete -f $elapsedTime);
                $timer = $null;
                return;
            }

        } #end for

        ## We have timed out..
        $elapsedTime = $timer.Elapsed.ToString('hh\:mm\:ss\.ff');
        Write-Error -Message ($localized.WaitLabDeploymentTimeoutError -f $elapsedTime);
        $timer = $null;

    } #end process
} #end function


# SIG # Begin signature block
# MIIuugYJKoZIhvcNAQcCoIIuqzCCLqcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAqDPjaT8xDwZu7
# d+mYeoQEbTOVjOzuHCj3FRoz3Hskx6CCE6QwggWQMIIDeKADAgECAhAFmxtXno4h
# 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
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgqY/woHF+WpuaErBW3zboWF7T
# icTf+z7IvxrvkNfbutYwDQYJKoZIhvcNAQEBBQAEggIASvQozS+eGQtu+DxihscR
# AGQa+VlzB1ud1Pw1NlYHSbOHH8ZtUfI6W+FxzjBdIWWxYAzuLTyIbsoAGg17pSYh
# bjDh5DogjsBfFg8WbXnE0mNSNeF7eR59ZTWQTzprWxYFPUed593wo+WOIlGBeUvN
# UDtKU8WD50a/a7d7l+zWnPVYlHQPx13DQOgGJAkdWSMVVznMlTfHXPX59wbOwm9V
# UZcVlr3ZBRBenMIpIv/AwOtVCyJO+Tdsk/mHw+wm2ZnqI9WQVUYe+mM2UtNu1Prx
# IC5bo4vAarNpMGOhF0vnZzPv9KZoAZ20Y5xxHbbuOA5qJLIrXxAU/+Pm8oBUaExu
# r6MBucMeo5lZGZ2IKVq5Bttzw55RMTz8xUvdmhlz++H1ENhHxG+aBJ2S+jaXu1Z6
# gW4pEq2HQaEwI8toaD0bsMDUR3dvWflz5Y5qIJ17SKNGidDN2/2qmFD7ulODm0Wn
# OILCLRb7KgDanJVxg7LCBwL4RbnYCCiSVITKOQMKf2ifR0bStW1DNzIJUzW1J14X
# pPOShaC/RBDwVANjpjxk1EtrgzdLw1wA72A39RV/5y166wtlo4xNjN+V1FMZqiiX
# zgM8DkK1FdNYn6TK+yv6wpCC0y1P/+65Fno/N+NnF82ErK5YNC6jnZ7ukZS9s4MK
# EIRQeP5eoUT7CblxI0TbGjChghc5MIIXNQYKKwYBBAGCNwMDATGCFyUwghchBgkq
# hkiG9w0BBwKgghcSMIIXDgIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ
# EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgztAkn3Hg
# egs3hK8yjWL5KQRfFbRais3wyP0uB0EZEsMCEFDGeydtSv9kZHshgqnyn1gYDzIw
# MjUwNDA4MTYyNjE1WqCCEwMwgga8MIIEpKADAgECAhALrma8Wrp/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
# NjE1WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZb
# RTAvBgkqhkiG9w0BCQQxIgQgi01fvStPn/A8V9z9pJ+3wKjik+Xl5uL8YLRV5tZk
# fmcwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13Pb
# BdZluQWTmEOPmtswDQYJKoZIhvcNAQEBBQAEggIAEDB5uHPSFU00zX8uVuoBJL8U
# b9c2QRrPmrLoz7MJV8tLv0ya1rXw3h0e0Ro9+fHfbyrC+V5RMTKu0KQ48UMSZigf
# YNr75oeohp3eeZyiyWNXCHR5mroeEalSAf6ghUoJc34LcsS7BAOb5HE6DaJ0KowU
# j8ZI6vl51wiGMcD58IxVsBt80NXtK3uiC6ShmzMMOGvhJiM4eGSleZi+adwXMwNm
# vlkT4LWuwV38mFTU5o/kWxoGAMDBOUSyTrTltMus2qHTLb/bl+T+na9bBEDyCYtg
# 6vw6SkXVtbqUkHx2T8d3HvUR29Vf/Mft2b8nqlrmN2wcOAug0SH15PGzShD9wUr7
# u6lnHG+4INVrzpo6UcNLhHwJlz2bgEubSeksdtO9RMv6UcMi6l9gFXuFVyEgCmdz
# IvmRTlvpU3p+zCmHxFmCHVapJzzd6doCJPrmwpdaQNV2G06A1bfQhcXMKGu70obe
# g161rI5yvlAhxb/5ng27T0Tk7mV5lx6F5Qn0XSIRMi8DgariXmn/ymGSUOtb5Lop
# XbuDL4ag5gTu/RTyKisB+caXuzxO03TBpFodwmyhp9jtFbKY2DParXda3PpOr7/E
# WO0XbbusCgIZi28WeO2oYRtvtRC/LidCEHFQjrxgvSFA3W0K6Ke4Tf6V6J2nsgx0
# Z3unlUI8lpMxW874m/M=
# SIG # End signature block