AutomatedLabCore.psm1

function Add-LabDomainAdmin
{
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [System.Security.SecureString]$Password,

        [string]$ComputerName
    )

    $cmd = {
        param(
            [Parameter(Mandatory)]
            [string]$Name,

            [Parameter(Mandatory)]
            [System.Security.SecureString]$Password
        )

        $server = 'localhost'

        $user = New-ADUser -Name $Name -AccountPassword $Password -Enabled $true -PassThru

        Add-ADGroupMember -Identity 'Domain Admins' -Members $user -Server $server

        try
        {
            Add-ADGroupMember -Identity 'Enterprise Admins' -Members $user -Server $server
            Add-ADGroupMember -Identity 'Schema Admins' -Members $user -Server $server
        }
        catch
        {
            #if adding the groups failed, this is executed propably in a child domain
        }
    }

    Invoke-LabCommand -ComputerName $ComputerName -ActivityName AddDomainAdmin -ScriptBlock $cmd -ArgumentList $Name, $Password
}


function Connect-AzureLab
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SourceLab,
        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationLab
    )

    Write-LogFunctionEntry
    Import-Lab $SourceLab -NoValidation
    $lab = Get-Lab
    $sourceResourceGroupName = (Get-LabAzureDefaultResourceGroup).ResourceGroupName
    $sourceLocation = Get-LabAzureDefaultLocation
    $sourceVnet = Initialize-GatewayNetwork -Lab $lab

    Import-Lab $DestinationLab -NoValidation
    $lab = Get-Lab
    $destinationResourceGroupName = (Get-LabAzureDefaultResourceGroup).ResourceGroupName
    $destinationLocation = Get-LabAzureDefaultLocation
    $destinationVnet = Initialize-GatewayNetwork -Lab $lab

    $sourcePublicIpParameters = @{
        ResourceGroupName = $sourceResourceGroupName
        Location          = $sourceLocation
        Name              = 's2sip'
        AllocationMethod  = 'Dynamic'
        IpAddressVersion  = 'IPv4'
        DomainNameLabel   = "$((1..10 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')"
        Force             = $true
    }

    $destinationPublicIpParameters = @{
        ResourceGroupName = $destinationResourceGroupName
        Location          = $destinationLocation
        Name              = 's2sip'
        AllocationMethod  = 'Dynamic'
        IpAddressVersion  = 'IPv4'
        DomainNameLabel   = "$((1..10 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')"
        Force             = $true
    }

    $sourceGatewaySubnet = Get-AzVirtualNetworkSubnetConfig -Name GatewaySubnet -VirtualNetwork $sourceVnet -ErrorAction SilentlyContinue
    $sourcePublicIp = New-AzPublicIpAddress @sourcePublicIpParameters
    $sourceGatewayIpConfiguration = New-AzVirtualNetworkGatewayIpConfig -Name gwipconfig -SubnetId $sourceGatewaySubnet.Id -PublicIpAddressId $sourcePublicIp.Id

    $sourceGatewayParameters = @{
        ResourceGroupName = $sourceResourceGroupName
        Location          = $sourceLocation
        Name              = 's2sgw'
        GatewayType       = 'Vpn'
        VpnType           = 'RouteBased'
        GatewaySku        = 'VpnGw1'
        IpConfigurations  = $sourceGatewayIpConfiguration
    }

    $destinationGatewaySubnet = Get-AzVirtualNetworkSubnetConfig -Name GatewaySubnet -VirtualNetwork $destinationVnet -ErrorAction SilentlyContinue
    $destinationPublicIp = New-AzPublicIpAddress @destinationPublicIpParameters
    $destinationGatewayIpConfiguration = New-AzVirtualNetworkGatewayIpConfig -Name gwipconfig -SubnetId $destinationGatewaySubnet.Id -PublicIpAddressId $destinationPublicIp.Id

    $destinationGatewayParameters = @{
        ResourceGroupName = $destinationResourceGroupName
        Location          = $destinationLocation
        Name              = 's2sgw'
        GatewayType       = 'Vpn'
        VpnType           = 'RouteBased'
        GatewaySku        = 'VpnGw1'
        IpConfigurations  = $destinationGatewayIpConfiguration
    }


    # Gateway creation
    $sourceGateway = Get-AzVirtualNetworkGateway -Name s2sgw -ResourceGroupName $sourceResourceGroupName -ErrorAction SilentlyContinue
    if (-not $sourceGateway)
    {
        Write-ScreenInfo -TaskStart -Message 'Creating Azure Virtual Network Gateway - this will take some time.'
        $sourceGateway = New-AzVirtualNetworkGateway @sourceGatewayParameters
        Write-ScreenInfo -TaskEnd -Message 'Source gateway created'
    }

    $destinationGateway = Get-AzVirtualNetworkGateway -Name s2sgw -ResourceGroupName $destinationResourceGroupName -ErrorAction SilentlyContinue
    if (-not $destinationGateway)
    {
        Write-ScreenInfo -TaskStart -Message 'Creating Azure Virtual Network Gateway - this will take some time.'
        $destinationGateway = New-AzVirtualNetworkGateway @destinationGatewayParameters
        Write-ScreenInfo -TaskEnd -Message 'Destination gateway created'
    }

    $sourceConnection = @{
        ResourceGroupName      = $sourceResourceGroupName
        Location               = $sourceLocation
        Name                   = 's2sconnection'
        ConnectionType         = 'Vnet2Vnet'
        SharedKey              = 'Somepass1'
        Force                  = $true
        VirtualNetworkGateway1 = $sourceGateway
        VirtualNetworkGateway2 = $destinationGateway
    }

    $destinationConnection = @{
        ResourceGroupName      = $destinationResourceGroupName
        Location               = $destinationLocation
        Name                   = 's2sconnection'
        ConnectionType         = 'Vnet2Vnet'
        SharedKey              = 'Somepass1'
        Force                  = $true
        VirtualNetworkGateway1 = $destinationGateway
        VirtualNetworkGateway2 = $sourceGateway
    }

    [void] (New-AzVirtualNetworkGatewayConnection @sourceConnection)
    [void] (New-AzVirtualNetworkGatewayConnection @destinationConnection)

    Write-PSFMessage -Message 'Connection created - please allow some time for initial connection.'

    Set-VpnDnsForwarders -SourceLab $SourceLab -DestinationLab $DestinationLab

    Write-LogFunctionExit
}


function Connect-OnPremisesWithAzure
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SourceLab,
        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationLab,
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $AzureAddressSpaces,
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $OnPremAddressSpaces
    )

    Write-LogFunctionEntry
    Import-Lab $SourceLab -NoValidation
    $lab = Get-Lab
    $sourceResourceGroupName = (Get-LabAzureDefaultResourceGroup).ResourceGroupName
    $sourceLocation = Get-LabAzureDefaultLocation
    $sourceDcs = Get-LabVM -Role DC, RootDC, FirstChildDC

    $vnet = Initialize-GatewayNetwork -Lab $lab

    $labPublicIp = Get-PublicIpAddress

    if (-not $labPublicIp)
    {
        throw 'No public IP for hypervisor found. Make sure you are connected to the internet.'
    }

    Write-PSFMessage -Message "Found Hypervisor host public IP of $labPublicIp"

    $genericParameters = @{
        ResourceGroupName = $sourceResourceGroupName
        Location          = $sourceLocation
    }

    $publicIpParameters = $genericParameters.Clone()
    $publicIpParameters.Add('Name', 's2sip')
    $publicIpParameters.Add('AllocationMethod', 'Dynamic')
    $publicIpParameters.Add('IpAddressVersion', 'IPv4')
    $publicIpParameters.Add('DomainNameLabel', "$((1..10 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')".ToLower())
    $publicIpParameters.Add('Force', $true)

    $gatewaySubnet = Get-AzVirtualNetworkSubnetConfig -Name GatewaySubnet -VirtualNetwork $vnet -ErrorAction SilentlyContinue
    $gatewayPublicIp = New-AzPublicIpAddress @publicIpParameters
    $gatewayIpConfiguration = New-AzVirtualNetworkGatewayIpConfig -Name gwipconfig -SubnetId $gatewaySubnet.Id -PublicIpAddressId $gatewayPublicIp.Id

    $remoteGatewayParameters = $genericParameters.Clone()
    $remoteGatewayParameters.Add('Name', 's2sgw')
    $remoteGatewayParameters.Add('GatewayType', 'Vpn')
    $remoteGatewayParameters.Add('VpnType', 'RouteBased')
    $remoteGatewayParameters.Add('GatewaySku', 'VpnGw1')
    $remoteGatewayParameters.Add('IpConfigurations', $gatewayIpConfiguration)
    $remoteGatewayParameters.Add('Force', $true)

    $onPremGatewayParameters = $genericParameters.Clone()
    $onPremGatewayParameters.Add('Name', 'onpremgw')
    $onPremGatewayParameters.Add('GatewayIpAddress', $labPublicIp)
    $onPremGatewayParameters.Add('AddressPrefix', $onPremAddressSpaces)
    $onPremGatewayParameters.Add('Force', $true)

    # Gateway creation
    $gw = Get-AzVirtualNetworkGateway -Name s2sgw -ResourceGroupName $sourceResourceGroupName -ErrorAction SilentlyContinue
    if (-not $gw)
    {
        Write-ScreenInfo -TaskStart -Message 'Creating Azure Virtual Network Gateway - this will take some time.'
        $gw = New-AzVirtualNetworkGateway @remoteGatewayParameters
        Write-ScreenInfo -TaskEnd -Message 'Virtual Network Gateway created.'
    }

    $onPremisesGw = Get-AzLocalNetworkGateway -Name onpremgw -ResourceGroupName $sourceResourceGroupName -ErrorAction SilentlyContinue
    if (-not $onPremisesGw -or $onPremisesGw.GatewayIpAddress -ne $labPublicIp)
    {
        $onPremisesGw = New-AzLocalNetworkGateway @onPremGatewayParameters
    }

    # Connection creation
    $connectionParameters = $genericParameters.Clone()
    $connectionParameters.Add('Name', 's2sconnection')
    $connectionParameters.Add('ConnectionType', 'IPsec')
    $connectionParameters.Add('SharedKey', 'Somepass1')
    $connectionParameters.Add('EnableBgp', $false)
    $connectionParameters.Add('Force', $true)
    $connectionParameters.Add('VirtualNetworkGateway1', $gw)
    $connectionParameters.Add('LocalNetworkGateway2', $onPremisesGw)

    $conn = New-AzVirtualNetworkGatewayConnection @connectionParameters

    # Step 3: Import the HyperV lab and install a Router if not already present
    Import-Lab $DestinationLab -NoValidation

    $lab = Get-Lab
    $router = Get-LabVm -Role Routing -ErrorAction SilentlyContinue
    $destinationDcs = Get-LabVM -Role DC, RootDC, FirstChildDC
    $gatewayPublicIp = Get-AzPublicIpAddress -Name s2sip -ResourceGroupName $sourceResourceGroupName -ErrorAction SilentlyContinue

    if (-not $gatewayPublicIp -or $gatewayPublicIp.IpAddress -notmatch '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
    {
        throw 'Public IP has either not been created or is currently unassigned.'
    }

    if (-not $router)
    {
        throw @'
        No router in your lab. Please redeploy your lab after adding e.g. the following lines:
        Add-LabVirtualNetworkDefinition -Name External -HyperVProperties @{ SwitchType = 'External'; AdapterName = 'Wi-Fi' }
        $netAdapter = @()
        $netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch $labName
        $netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch External -UseDhcp
        $machineName = "ALS2SVPN$((1..7 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')"
        Add-LabMachineDefinition -Name $machineName -Roles Routing -NetworkAdapter $netAdapter -OperatingSystem 'Windows Server 2016 Datacenter (Desktop Experience)'
'@

    }

    # Step 4: Configure S2S VPN Connection on Router
    $externalAdapters = $router.NetworkAdapters | Where-Object { $_.VirtualSwitch.SwitchType -eq 'External' }

    if ($externalAdapters.Count -ne 1)
    {
        throw "Automatic configuration of VPN gateway can only be done if there is exactly 1 network adapter connected to an external network switch. The machine '$machine' knows about $($externalAdapters.Count) externally connected adapters"
    }

    if ($externalAdapters)
    {
        $mac = $externalAdapters | Select-Object -ExpandProperty MacAddress
        $mac = ($mac | Get-StringSection -SectionSize 2) -join ':'

        if (-not $mac)
        {
            throw ('Get-LabVm returned an empty MAC address for {0}. Cannot continue' -f $router.Name)
        }
    }

    $scriptBlock = {
        param
        (
            $AzureDnsEntry,
            $RemoteAddressSpaces,
            $MacAddress
        )

        if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
        {
            $externalAdapter = Get-CimInstance -Class Win32_NetworkAdapter -Filter ('MACAddress = "{0}"' -f $MacAddress) |
                Select-Object -ExpandProperty NetConnectionID
        }
        else
        {
            $externalAdapter = Get-WmiObject -Class Win32_NetworkAdapter -Filter ('MACAddress = "{0}"' -f $MacAddress) |
                Select-Object -ExpandProperty NetConnectionID
        }

        Set-Service -Name RemoteAccess -StartupType Automatic
        Start-Service -Name RemoteAccess -ErrorAction SilentlyContinue

        $null = netsh.exe routing ip nat install
        $null = netsh.exe routing ip nat add interface $externalAdapter
        $null = netsh.exe routing ip nat set interface $externalAdapter mode=full

        $status = Get-RemoteAccess -ErrorAction SilentlyContinue
        if (($status.VpnStatus -ne 'Uninstalled') -or ($status.DAStatus -ne 'Uninstalled') -or ($status.SstpProxyStatus -ne 'Uninstalled'))
        {
            Uninstall-RemoteAccess -Force
        }

        if ($status.VpnS2SStatus -ne 'Installed' -or $status.RoutingStatus -ne 'Installed')
        {
            Install-RemoteAccess -VpnType VPNS2S -ErrorAction Stop
        }

        try
        {
            # Try/Catch to catch exception while we have to wait for Install-RemoteAccess to finish up
            Start-Service RemoteAccess -ErrorAction SilentlyContinue
            $azureConnection = Get-VpnS2SInterface -Name AzureS2S -ErrorAction SilentlyContinue
        }
        catch
        {
            # If Get-VpnS2SInterface throws HRESULT 800703bc, wait even longer
            Start-Sleep -Seconds 120
            Start-Service RemoteAccess -ErrorAction SilentlyContinue
            $azureConnection = Get-VpnS2SInterface -Name AzureS2S -ErrorAction SilentlyContinue
        }


        if (-not $azureConnection)
        {
            $parameters = @{
                Name                 = 'AzureS2S'
                Protocol             = 'IKEv2'
                Destination          = $AzureDnsEntry
                AuthenticationMethod = 'PskOnly'
                SharedSecret         = 'Somepass1'
                NumberOfTries        = 0
                Persistent           = $true
                PassThru             = $true
            }
            $azureConnection = Add-VpnS2SInterface @parameters
        }

        $count = 1

        while ($count -le 3)
        {
            try
            {
                $azureConnection | Connect-VpnS2SInterface -ErrorAction Stop
                $connectionEstablished = $true
            }
            catch
            {
                Write-ScreenInfo -Message "Could not connect to $AzureDnsEntry ($count/3)" -Type Warning
                $connectionEstablished = $false
            }

            $count++
        }

        if (-not $connectionEstablished)
        {
            throw "Error establishing connection to $AzureDnsEntry after 3 tries. Check your NAT settings, internet connectivity and Azure resource group"
        }

        $null = netsh.exe ras set conf confstate = enabled
        $null = netsh.exe routing ip dnsproxy install


        $dialupInterfaceIndex = (Get-NetIPInterface -AddressFamily IPv4 | Where-Object -Property InterfaceAlias -eq 'AzureS2S').ifIndex

        if (-not $dialupInterfaceIndex)
        {
            throw "Connection to $AzureDnsEntry has not been established. Cannot add routes to $($addressSpace -join ',')."
        }

        foreach ($addressSpace in $RemoteAddressSpaces)
        {
            $null = New-NetRoute -DestinationPrefix $addressSpace -InterfaceIndex $dialupInterfaceIndex -AddressFamily IPv4 -NextHop 0.0.0.0 -RouteMetric 1
        }
    }

    Invoke-LabCommand -ActivityName 'Enabling S2S VPN functionality and configuring S2S VPN connection' `
        -ComputerName $router `
        -ScriptBlock $scriptBlock `
        -ArgumentList @($gatewayPublicIp.IpAddress, $AzureAddressSpaces, $mac) `
        -Retries 3 -RetryIntervalInSeconds 10

    # Configure DNS forwarding
    Set-VpnDnsForwarders -SourceLab $SourceLab -DestinationLab $DestinationLab

    Write-LogFunctionExit
}


function Connect-OnPremisesWithEndpoint
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $LabName,
        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationHost,
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $AddressSpace,
        [Parameter(Mandatory = $true)]
        [System.String]
        $Psk
    )

    Write-LogFunctionEntry
    Import-Lab $LabName -NoValidation

    $lab = Get-Lab
    $router = Get-LabVm -Role Routing -ErrorAction SilentlyContinue

    if (-not $router)
    {
        throw @'
        No router in your lab. Please redeploy your lab after adding e.g. the following lines:
        Add-LabVirtualNetworkDefinition -Name External -HyperVProperties @{ SwitchType = 'External'; AdapterName = 'Wi-Fi' }
        $netAdapter = @()
        $netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch $labName
        $netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch External -UseDhcp
        $machineName = "ALS2SVPN$((1..7 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')"
        Add-LabMachineDefinition -Name $machineName -Roles Routing -NetworkAdapter $netAdapter -OperatingSystem 'Windows Server 2016 Datacenter (Desktop Experience)'
'@

    }

    $externalAdapters = $router.NetworkAdapters | Where-Object { $_.VirtualSwitch.SwitchType -eq 'External' }

    if ($externalAdapters.Count -ne 1)
    {
        throw "Automatic configuration of VPN gateway can only be done if there is exactly 1 network adapter connected to an external network switch. The machine '$machine' knows about $($externalAdapters.Count) externally connected adapters"
    }

    $externalAdapter = $externalAdapters[0]
    $mac = $externalAdapter.MacAddress
    $mac = ($mac | Get-StringSection -SectionSize 2) -join '-'

    $scriptBlock = {
        param
        (
            $DestinationHost,
            $RemoteAddressSpaces
        )

        $status = Get-RemoteAccess -ErrorAction SilentlyContinue
        if ($status.VpnS2SStatus -ne 'Installed' -or $status.RoutingStatus -ne 'Installed')
        {
            Install-RemoteAccess -VpnType VPNS2S -ErrorAction Stop
        }

        Restart-Service -Name RemoteAccess

        $remoteConnection = Get-VpnS2SInterface -Name AzureS2S -ErrorAction SilentlyContinue

        if (-not $remoteConnection)
        {
            $parameters = @{
                Name                 = 'ALS2S'
                Protocol             = 'IKEv2'
                Destination          = $DestinationHost
                AuthenticationMethod = 'PskOnly'
                SharedSecret         = 'Somepass1'
                NumberOfTries        = 0
                Persistent           = $true
                PassThru             = $true
            }
            $remoteConnection = Add-VpnS2SInterface @parameters
        }

        $remoteConnection | Connect-VpnS2SInterface -ErrorAction Stop

        $dialupInterfaceIndex = (Get-NetIPInterface | Where-Object -Property InterfaceAlias -eq 'ALS2S').ifIndex

        foreach ($addressSpace in $RemoteAddressSpaces)
        {
            New-NetRoute -DestinationPrefix $addressSpace -InterfaceIndex $dialupInterfaceIndex -AddressFamily IPv4 -NextHop 0.0.0.0 -RouteMetric 1
        }
    }

    Invoke-LabCommand -ActivityName 'Enabling S2S VPN functionality and configuring S2S VPN connection' `
        -ComputerName $router `
        -ScriptBlock $scriptBlock `
        -ArgumentList @($DestinationHost, $AddressSpace) `
        -Retries 3 -RetryIntervalInSeconds 10

    Write-LogFunctionExit
}


function Dismount-LabDiskImage
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ImagePath
    )

    if (Get-Command -Name Dismount-DiskImage -ErrorAction SilentlyContinue)
    {
        Dismount-DiskImage -ImagePath $ImagePath
    }
    elseif ($IsLinux)
    {
        $image = Get-Item -Path $ImagePath
        $null = umount /mnt/automatedlab/$($image.BaseName)
    }
    else
    {
        throw 'Neither Dismount-DiskImage exists, nor is this a Linux system.'
    }
}


function Get-LabAzureDefaultSubscription
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Update-LabAzureSettings

    $script:lab.AzureSettings.DefaultSubscription

    Write-LogFunctionExit
}


function Get-LabAzureLabSourcesContentRecursive
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [object]$StorageContext,

        # Path relative to labsources file share
        [string]
        $Path
    )

    Test-LabHostConnected -Throw -Quiet

    $content = @()

    $temporaryContent = if ($Path)
    {
        $StorageContext | Get-AzStorageFile -Path $Path -ErrorAction SilentlyContinue
    }
    else
    {
        $StorageContext | Get-AzStorageFile
    }

    foreach ($item in $temporaryContent)
    {
        if ($item.CloudFileDirectory)
        {
            $content += $item.CloudFileDirectory
            $content += Get-LabAzureLabSourcesContentRecursive -StorageContext $item
        }
        elseif ($item.CloudFile)
        {
            $content += $item.CloudFile
        }
        else
        {
            continue
        }
    }

    return $content
}


function Get-LabCAInstallCertificates
{

    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [AutomatedLab.Machine[]]$Machines
    )

    begin
    {
        Write-LogFunctionEntry

        if (-not (Test-Path -Path "$((Get-Lab).LabPath)\Certificates"))
        {
            New-Item -Path "$((Get-Lab).LabPath)\Certificates" -ItemType Directory | Out-Null
        }
    }

    process
    {
        #Get all certificates from CA servers and place temporalily on host machine
        foreach ($machine in $machines)
        {
            $sourceFile = Invoke-LabCommand -ComputerName $machine -ScriptBlock {
                (Get-Item -Path 'C:\Windows\System32\CertSrv\CertEnroll\*.crt' |
                    Sort-Object -Property LastWritten -Descending |
                Select-Object -First 1).FullName
            } -PassThru -NoDisplay

            $tempDestination = "$((Get-Lab).LabPath)\Certificates\$($Machine).crt"

            $caSession = New-LabPSSession -ComputerName $machine.Name
            Receive-File -Source $sourceFile -Destination $tempDestination -Session $caSession
        }
    }

    end
    {
        Write-LogFunctionExit
    }

}


function Get-LabImageOnLinux
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $MountPoint,

        [IO.FileInfo]
        $IsoFile
    )

    $dismPattern = 'Index:\s+(?<Index>\d{1,2})(\r)?\nName:\s+(?<Name>.+)'
    $standardImagePath = Join-Path -Path $MountPoint -ChildPath /sources/install.wim
    $doNotSkipNonNonEnglishIso = Get-LabConfigurationItem -Name DoNotSkipNonNonEnglishIso
    
    if (-not (Get-Command wiminfo))
    {
        throw 'wiminfo is not installed. Please use your package manager to install wimtools'
    }

    if (Test-Path -Path $standardImagePath)
    {
        $dismOutput = wiminfo $standardImagePath | Select-Object -Skip 15
        $dismOutput = $dismOutput -join "`n"
        $splitOutput = $dismoutput -split ([Environment]::NewLine + [Environment]::NewLine)
        Write-PSFMessage "The Windows Image list contains $($split.Count) items"

        foreach ($dismImage in $splitOutput)
        {
            Write-ProgressIndicator
            $imageInfo = $dismImage -replace ':', '=' | ConvertFrom-StringData

            if (($imageInfo.Languages -notlike '*en-us*') -and -not $doNotSkipNonNonEnglishIso)
            {
                Write-ScreenInfo "The windows image '$($imageInfo.Name)' in the ISO '$($IsoFile.Name)' has the language(s) '$($imageInfo.Languages -join ', ')'. AutomatedLab does only support images with the language 'en-us' hence this image will be skipped." -Type Warning
            }

            $os = New-Object -TypeName AutomatedLab.OperatingSystem($imageInfo.Name, $IsoFile.FullName)
            $os.OperatingSystemImageName = $imageInfo.Name
            $os.OperatingSystemName = $imageInfo.Name
            $os.Size = $imageInfo['Total Bytes']
            $os.Version = '{0}.{1}.{2}.{3}' -f $imageInfo['Major Version'], $imageInfo['Minor Version'], $imageInfo['Build'], $imageInfo['Service Pack Build']
            $os.PublishedDate = $imageInfo['Creation Time'] -replace '=', ':'
            $os.Edition = $imageInfo['Edition ID']
            $os.Installation = $imageInfo['Installation Type']
            $os.ImageIndex = $imageInfo.Index

            $os
        }
    }

    # SuSE, openSuSE et al
    $susePath = Join-Path -Path $MountPoint -ChildPath content
    if (Test-Path -Path $susePath -PathType Leaf)
    {
        $content = Get-Content -Path $susePath -Raw
        [void] ($content -match 'DISTRO\s+.+,(?<Distro>[a-zA-Z 0-9.]+)\n.*LINGUAS\s+(?<Lang>.*)\n(?:REGISTERPRODUCT.+\n){0,1}REPOID\s+.+((?<CreationTime>\d{8})|(?<Version>\d{2}\.\d{1}))\/(?<Edition>\w+)\/.*\nVENDOR\s+(?<Vendor>[a-zA-z ]+)')

        $os = New-Object -TypeName AutomatedLab.OperatingSystem($Matches.Distro, $IsoFile.FullName)
        $os.OperatingSystemImageName = $Matches.Distro
        $os.OperatingSystemName = $Matches.Distro
        $os.Size = $IsoFile.Length
        if ($Matches.Version -like '*.*')
        {
            $os.Version = $Matches.Version
        }
        elseif ($Matches.Version)
        {
            $os.Version = [AutomatedLab.Version]::new($Matches.Version, 0)
        }
        else
        {
            $os.Version = [AutomatedLab.Version]::new(0, 0)
        }

        $os.PublishedDate = if ($Matches.CreationTime)
        {
            [datetime]::ParseExact($Matches.CreationTime, 'yyyyMMdd', ([cultureinfo]'en-us'))
        }
        else
        {
            (Get-Item -Path $susePath).CreationTime
        }
        $os.Edition = $Matches.Edition

        $packages = Get-ChildItem -Path (Join-Path -Path $MountPoint -ChildPath suse) -Filter pattern*.rpm -File -Recurse | ForEach-Object {
            if ( $_.Name -match '.*patterns-(openSUSE|SLE|sles)-(?<name>.*(32bit)?)-\d*-\d*\.\d*\.x86')
            {
                $Matches.name
            }
        }

        $os.LinuxPackageGroup = $packages

        $os
    }

    # RHEL, CentOS, Fedora et al
    $rhelPath = Join-Path -Path $MountPoint -ChildPath .treeinfo # TreeInfo Syntax https://release-engineering.github.io/productmd/treeinfo-1.0.html
    $rhelDiscinfo = Join-Path -Path $MountPoint -ChildPath .discinfo
    $rhelPackageInfo = Join-Path -Path $MountPoint -ChildPath repodata
    if ((Test-Path -Path $rhelPath -PathType Leaf) -and (Test-Path -Path $rhelDiscinfo -PathType Leaf))
    {
        $generalInfo = (Get-Content -Path $rhelPath | Select-String '\[general\]' -Context 7).Context.PostContext| Where-Object -FilterScript { $_ -match '^\w+\s*=\s*\w+' }  | ConvertFrom-StringData -ErrorAction SilentlyContinue
        $versionInfo = if ($generalInfo.version.Contains('.')) { $generalInfo.version -as [Version] } else {[Version]::new($generalInfo.Version, 0)}

        $os = New-Object -TypeName AutomatedLab.OperatingSystem(('{0} {1}' -f $content.Family, $os.Version), $IsoFile.FullName)
        $os.OperatingSystemImageName = $content.Name
        $os.Size = $IsoFile.Length

        $packageXml = (Get-ChildItem -Path $rhelPackageInfo -Filter *comps*.xml | Select-Object -First 1).FullName
        if (-not $packageXml)
        {
            # CentOS ISO for some reason contained only GUIDs
            $packageXml = Get-ChildItem -Path $rhelPackageInfo -PipelineVariable file -File |
            Get-Content -TotalCount 10 |
            Where-Object { $_ -like "*<comps>*" } |
            ForEach-Object { $file.FullName } |
            Select-Object -First 1
        }

        if ($null -ne $packageXml)
        {
            [xml]$packageInfo = Get-Content -Path $packageXml -Raw
            $os.LinuxPackageGroup = (Select-Xml -XPath "/comps/group/id" -Xml $packageInfo).Node.InnerText
        }

        if ($generalInfo.version.Contains('.'))
        {
            $os.Version = $generalInfo.version
        }
        else
        {
            $os.Version = [AutomatedLab.Version]::new($generalInfo.version, 0)
        }

        $os.OperatingSystemName = $generalInfo.name

        # Unix time stamp...
        $os.PublishedDate = (Get-Item -Path $rhelPath).CreationTime
        $os.Edition = if ($generalInfo.Variant)
        {
            $content.Variant
        }
        else
        {
            'Server'
        }

        $os
    }
}


function Get-LabImageOnWindows
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [char]
        $DriveLetter,

        [IO.FileInfo]
        $IsoFile
    )

    $dismPattern = 'Index : (?<Index>\d{1,2})(\r)?\nName : (?<Name>.+)'
    $standardImagePath = Get-Item -Path "$DriveLetter`:\Sources\install.*" -ErrorAction SilentlyContinue | Where-Object Name -Match '.*\.(esd|wim)'
    $doNotSkipNonNonEnglishIso = Get-LabConfigurationItem -Name DoNotSkipNonNonEnglishIso
    
    if ($standardImagePath -and (Test-Path -Path $standardImagePath))
    {
        $dismOutput = Dism.exe /English /Get-WimInfo /WimFile:$standardImagePath
        $dismOutput = $dismOutput -join "`n"
        $dismMatches = $dismOutput | Select-String -Pattern $dismPattern -AllMatches
        Write-PSFMessage "The Windows Image list contains $($dismMatches.Matches.Count) items"

        foreach ($dismMatch in $dismMatches.Matches)
        {
            Write-ProgressIndicator
            $index = $dismMatch.Groups['Index'].Value
            $imageInfo = Get-WindowsImage -ImagePath $standardImagePath -Index $index

            if (($imageInfo.Languages -notlike '*en-us*') -and -not $doNotSkipNonNonEnglishIso)
            {
                Write-ScreenInfo "The windows image '$($imageInfo.ImageName)' in the ISO '$($IsoFile.Name)' has the language(s) '$($imageInfo.Languages -join ', ')'. AutomatedLab does only support images with the language 'en-us' hence this image will be skipped." -Type Warning
                continue
            }

            $os = New-Object -TypeName AutomatedLab.OperatingSystem($imageInfo.ImageName, $IsoFile.FullName)
            $os.OperatingSystemImageName = $dismMatch.Groups['Name'].Value
            $os.OperatingSystemName = $dismMatch.Groups['Name'].Value
            $os.Size = $imageInfo.Imagesize
            $os.Version = $imageInfo.Version
            $os.PublishedDate = $imageInfo.CreatedTime
            $os.Edition = $imageInfo.EditionId
            $os.Installation = $imageInfo.InstallationType
            $os.ImageIndex = $imageInfo.ImageIndex
            try 
            {
                $os.Architecture = $imageInfo.Architecture
            }
            catch
            {
                $os.Architecture = 'Unknown'
            }

            $os
        }
    }

    # SuSE, openSuSE et al
    $susePath = "$DriveLetter`:\content"
    if (Test-Path -Path $susePath -PathType Leaf)
    {
        $content = Get-Content -Path $susePath -Raw
        [void] ($content -match 'DISTRO\s+.+,(?<Distro>[a-zA-Z 0-9.]+)\n.*LINGUAS\s+(?<Lang>.*)\n(?:REGISTERPRODUCT.+\n){0,1}REPOID\s+.+((?<CreationTime>\d{8})|(?<Version>\d{2}\.\d{1}))\/(?<Edition>\w+)\/.*\nVENDOR\s+(?<Vendor>[a-zA-z ]+)')

        $os = New-Object -TypeName AutomatedLab.OperatingSystem($Matches.Distro, $IsoFile.FullName)
        $os.OperatingSystemImageName = $Matches.Distro
        $os.OperatingSystemName = $Matches.Distro
        $os.Size = $IsoFile.Length
        if ($Matches.Version -like '*.*')
        {
            $os.Version = $Matches.Version
        }
        elseif ($Matches.Version)
        {
            $os.Version = [AutomatedLab.Version]::new($Matches.Version, 0)
        }
        else
        {
            $os.Version = [AutomatedLab.Version]::new(0, 0)
        }

        $os.PublishedDate = if ($Matches.CreationTime)
        {
            [datetime]::ParseExact($Matches.CreationTime, 'yyyyMMdd', ([cultureinfo]'en-us'))
        }
        else
        {
            (Get-Item -Path $susePath).CreationTime
        }
        $os.Edition = $Matches.Edition

        $packages = Get-ChildItem "$DriveLetter`:\suse" -Filter pattern*.rpm -File -Recurse | ForEach-Object {
            if ( $_.Name -match '.*patterns-(openSUSE|SLE|sles)-(?<name>.*(32bit)?)-\d*-\d*\.\d*\.x86')
            {
                $Matches.name
            }
        }

        $os.LinuxPackageGroup = $packages

        $os
    }

    # RHEL, CentOS, Fedora, OpenSuse Tumbleweed et al
    $rhelPath = "$DriveLetter`:\.treeinfo" # TreeInfo Syntax https://release-engineering.github.io/productmd/treeinfo-1.0.html
    $rhelPackageInfo = "$DriveLetter`:{0}\*\repodata"
    if (Test-Path -Path $rhelPath -PathType Leaf)
    {
        $contentMatch = (Get-Content -Path $rhelPath -Raw) -match '(?s)(?<=\[general\]).*?(?=\[)'
        if (-not $contentMatch)
        {
            throw "Unknown structure of $rhelPath. Cannot add ISO"
        }

        $generalInfo = $Matches.0 -replace ';.*' -split "`n" | ConvertFrom-String -Delimiter '=' -PropertyNames Name, Value
        $version = ([string]$generalInfo.Where({ $_.Name.Trim() -eq 'version' }).Value).Trim()
        $name = ([string]$generalInfo.Where({ $_.Name.Trim() -eq 'name' }).Value).Trim()
        $variant = ([string]$generalInfo.Where({ $_.Name.Trim() -eq 'variant' }).Value).Trim()
        $versionInfo = if (-not $version) { [Version]::new(1, 0, 0, 0) } elseif ($version.Contains('.')) { $version -as [Version] } else { [Version]::new($Version, 0) }
        $arch = if (([string]$generalInfo.Where({ $_.Name.Trim() -eq 'arch' }).Value).Trim() -eq 'x86_64') { 'x64' } else { 'x86' }

        if ($variant -and $versionInfo -ge '8.0')
        {
            $rhelPackageInfo = $rhelPackageInfo -f "\$variant"
        }
        else
        {
            $rhelPackageInfo = $rhelPackageInfo -f $null
        }

        $os = New-Object -TypeName AutomatedLab.OperatingSystem($name, $IsoFile.FullName)
        $os.OperatingSystemImageName = $name
        $os.Size = $IsoFile.Length
        $os.Architecture = $arch

        $packageXml = (Get-ChildItem -Path $rhelPackageInfo -Filter *comps*.xml -ErrorAction SilentlyContinue | Select-Object -First 1).FullName
        if (-not $packageXml)
        {
            # CentOS ISO for some reason contained only GUIDs
            $packageXml = Get-ChildItem -Path $rhelPackageInfo -ErrorAction SilentlyContinue -PipelineVariable file -File |
            Get-Content -TotalCount 10 |
            Where-Object { $_ -like "*<comps>*" } |
            ForEach-Object { $file.FullName } |
            Select-Object -First 1
        }

        if ($packageXml)
        {
            [xml]$packageInfo = Get-Content -Path $packageXml -Raw
            $os.LinuxPackageGroup.AddRange([string[]]((Select-Xml -XPath "/comps/group/id" -Xml $packageInfo).Node.InnerText | ForEach-Object { "@$_" }) )
            $os.LinuxPackageGroup.AddRange([string[]]((Select-Xml -XPath "/comps/environment/id" -Xml $packageInfo).Node.InnerText | ForEach-Object { "@^$_" }) )
        }

        $os.Version = $versionInfo
        $os.OperatingSystemName = $name

        # Unix time stamp...
        $os.PublishedDate = (Get-Item -Path $rhelPath).CreationTime
        $os.Edition = if ($variant)
        {
            $variant
        }
        else
        {
            'Server'
        }

        $os
    }

    # Ubuntu 2004+, Kali
    $ubuntuPath = "$DriveLetter`:\.disk\info"
    $ubuntuPackageInfo = "$DriveLetter`:\pool\main"
    if (Test-Path -Path $ubuntuPath -PathType Leaf)
    {
        $infoContent = Get-Content -Path $ubuntuPath -TotalCount 1
        if ($infoContent -like 'Kali*')
        {
            $null = $infoContent -match '(?:Kali GNU\/Linux)?\s+(?<Version>\d\d\d\d\.\d).*\s+"(?<Name>[\w-]+)".*Official\s(?<Arch>i386|amd64).*(?<ReleaseDate>\d{8})'
            $osversion = $Matches.Version
            $name = 'Kali Linux {0}' -f $osversion
        }
        else
        {
            $null = $infoContent -match '(?:Ubuntu)(?:-Server)?\s+(?<Version>\d\d\.\d\d).*Release\s(?<Arch>i386|amd64)\s\((?<ReleaseDate>\d{8})'
            $osversion = $Matches.Version
            $name = ($infoContent -split '\s-\s')[0]
            if (([version]$osversion) -lt '20.4')
            {
                Write-ScreenInfo -Type Error -Message "Skipping $IsoFile, AutomatedLab was only tested with 20.04 and newer."
            }
        }

        $osDate = $Matches.ReleaseDate

        $os = New-Object -TypeName AutomatedLab.OperatingSystem($name, $IsoFile.FullName)
        if ($Matches.Arch -eq 'i386')
        {
            $os.Architecture = 'x86'
        }
        else
        {
            $os.Architecture = 'x64'
        }
        $os.OperatingSystemImageName = $name
        $os.Size = $IsoFile.Length
        $os.Version = $osversion
        $os.PublishedDate = [datetime]::ParseExact($osDate, 'yyyyMMdd', [cultureinfo]::CurrentCulture)
        $os.Edition = if ($infoContent -match '-Server') { 'Server' } else { 'Desktop' }

        foreach ($package in (Get-ChildItem -Directory -Recurse -Path $ubuntuPackageInfo))
        {
            if ($package.Parent.Name -eq 'main') { continue }

            $null = $os.LinuxPackageGroup.Add($package.Name)
        }

        $os
    }
}


function Get-Type
{
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $GenericType,

        [Parameter(Position = 1, Mandatory = $true)]
        [string[]] $T
    )

    $T = $T -as [type[]]

    try
    {
        $generic = [type]($GenericType + '`' + $T.Count)
        $generic.MakeGenericType($T)
    }
    catch
    {
        throw New-Object -TypeName System.Exception -ArgumentList ('Cannot create generic type', $_.Exception)
    }
}


function Get-VMUacStatus
{
    [CmdletBinding()]
    param(
        [string]$ComputerName = $env:COMPUTERNAME
    )

    $registryPath = 'Software\Microsoft\Windows\CurrentVersion\Policies\System'
    $uacStatus = $false

    $openRegistry = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, 'Default')
    $subkey = $openRegistry.OpenSubKey($registryPath, $false)

    $uacStatus = $subkey.GetValue('EnableLUA')
    $consentPromptBehaviorUser = $subkey.GetValue('ConsentPromptBehaviorUser')
    $consentPromptBehaviorAdmin = $subkey.GetValue('ConsentPromptBehaviorAdmin')

    New-Object -TypeName PSObject -Property @{
        ComputerName = $ComputerName
        EnableLUA = $uacStatus
        PromptBehaviorUser = $consentPromptBehaviorUser
        PromptBehaviorAdmin = $consentPromptBehaviorAdmin
    }
}


function Import-CMModule
{
    Param(
        [String]$ComputerName,
        [String]$SiteCode
    )
    if (-not(Get-Module ConfigurationManager))
    {
        try
        {
            Import-Module ("{0}\..\ConfigurationManager.psd1" -f $ENV:SMS_ADMIN_UI_PATH) -ErrorAction "Stop" -ErrorVariable "ImportModuleError"
        }
        catch
        {
            throw ("Failed to import ConfigMgr module: {0}" -f $ImportModuleError.ErrorRecord.Exception.Message)
        }
    }
    try
    {
        if (-not(Get-PSDrive -Name $SiteCode -PSProvider "CMSite" -ErrorAction "SilentlyContinue"))
        {
            New-PSDrive -Name $SiteCode -PSProvider "CMSite" -Root $ComputerName -Scope "Script" -ErrorAction "Stop" | Out-Null
        }
        Set-Location ("{0}:\" -f $SiteCode) -ErrorAction "Stop"    
    } 
    catch
    {
        if (Get-PSDrive -Name $SiteCode -PSProvider "CMSite" -ErrorAction "SilentlyContinue")
        {
            Remove-PSDrive -Name $SiteCode -Force
        }
        throw ("Failed to create New-PSDrive with site code `"{0}`" and server `"{1}`"" -f $SiteCode, $ComputerName)
    }
}


function Initialize-GatewayNetwork
{
    param
    (
        [Parameter(Mandatory = $true)]
        [AutomatedLab.Lab]
        $Lab
    )

    Write-LogFunctionEntry
    Write-PSFMessage -Message ('Creating gateway subnet for lab {0}' -f $Lab.Name)

    $targetNetwork = $Lab.VirtualNetworks | Select-Object -First 1
    $sourceMask = $targetNetwork.AddressSpace.Cidr
    $sourceMaskIp = $targetNetwork.AddressSpace.NetMask
    $superNetMask = $sourceMask - 1
    $superNetIp = $targetNetwork.AddressSpace.IpAddress.AddressAsString

    $gatewayNetworkAddressFound = $false
    $incrementedIp = $targetNetwork.AddressSpace.IPAddress.Increment()
    $decrementedIp = $targetNetwork.AddressSpace.IPAddress.Decrement()
    $isDecrementing = $false

    while (-not $gatewayNetworkAddressFound)
    {
        if (-not $isDecrementing)
        {
            $incrementedIp = $incrementedIp.Increment()
            $tempNetworkAdress = Get-NetworkAddress -IPAddress $incrementedIp.AddressAsString -SubnetMask $sourceMaskIp.AddressAsString

            if ($tempNetworkAdress -eq $targetNetwork.AddressSpace.Network.AddressAsString)
            {
                continue
            }

            $gatewayNetworkAddress = $tempNetworkAdress

            if ($gatewayNetworkAddress -in (Get-NetworkRange -IPAddress $targetnetwork.AddressSpace.Network.AddressAsString -SubnetMask $superNetMask))
            {
                $gatewayNetworkAddressFound = $true
            }
            else
            {
                $isDecrementing = $true
            }
        }

        $decrementedIp = $decrementedIp.Decrement()
        $tempNetworkAdress = Get-NetworkAddress -IPAddress $decrementedIp.AddressAsString -SubnetMask $sourceMaskIp.AddressAsString

        if ($tempNetworkAdress -eq $targetNetwork.AddressSpace.Network.AddressAsString)
        {
            continue
        }

        $gatewayNetworkAddress = $tempNetworkAdress

        if (([AutomatedLab.IPAddress]$gatewayNetworkAddress).Increment().AddressAsString -in (Get-NetworkRange -IPAddress $targetnetwork.AddressSpace.Network.AddressAsString -SubnetMask $superNetMask))
        {
            $gatewayNetworkAddressFound = $true
        }
    }

    Write-PSFMessage -Message ('Calculated supernet: {0}, extending Azure VNet and creating gateway subnet {1}' -f "$($superNetIp)/$($superNetMask)", "$($gatewayNetworkAddress)/$($sourceMask)")
    $vNet = Get-LWAzureNetworkSwitch -virtualNetwork $targetNetwork
    $vnet.AddressSpace.AddressPrefixes[0] = "$($superNetIp)/$($superNetMask)"
    $gatewaySubnet = Get-AzVirtualNetworkSubnetConfig -Name GatewaySubnet -VirtualNetwork $vnet -ErrorAction SilentlyContinue

    if (-not $gatewaySubnet)
    {
        $vnet | Add-AzVirtualNetworkSubnetConfig -Name GatewaySubnet -AddressPrefix "$($gatewayNetworkAddress)/$($sourceMask)"
        $vnet = $vnet | Set-AzVirtualNetwork -ErrorAction Stop
    }

    $vnet = (Get-LWAzureNetworkSwitch -VirtualNetwork $targetNetwork | Where-Object -Property ID)[0]
    Write-LogFunctionExit

    return $vnet
}


function Install-CMSite
{
    Param (
        [Parameter(Mandatory)]
        [String]$CMServerName,

        [Parameter(Mandatory)]
        [String]$CMBinariesDirectory,

        [Parameter(Mandatory)]
        [String]$Branch,

        [Parameter(Mandatory)]
        [String]$CMPreReqsDirectory,

        [Parameter(Mandatory)]
        [String]$CMSiteCode,

        [Parameter(Mandatory)]
        [String]$CMSiteName,

        [Parameter()]
        [String]$CMProductId = 'EVAL',

        [Parameter(Mandatory)]
        [String[]]$CMRoles,

        [Parameter(Mandatory)]
        [string]
        $SqlServerName,

        [Parameter()]
        [string] $DatabaseName = 'ALCMDB',

        [Parameter()]
        [string] $WsusContentPath = 'C:\WsusContent',

        [Parameter()]
        [string] $AdminUser
    )

    #region Initialise
    $CMServer = Get-LabVM -ComputerName $CMServerName
    $CMServerFqdn = $CMServer.FQDN
    $DCServerName = Get-LabVM -Role RootDC | Where-Object { $_.DomainName -eq $CMServer.DomainName } | Select-Object -ExpandProperty Name
    $downloadTargetDirectory = "{0}\SoftwarePackages" -f $(Get-LabSourcesLocation -Local)
    $VMInstallDirectory = "C:\Install"
    $LabVirtualNetwork = (Get-Lab).VirtualNetworks.Where( { $_.SwitchType -ne 'External' -and $_.ResourceName -in $CMServer.Network }, 'First', 1).AddressSpace
    $CMBoundaryIPRange = "{0}-{1}" -f $LabVirtualNetwork.FirstUsable.AddressAsString, $LabVirtualNetwork.LastUsable.AddressAsString
    $VMCMBinariesDirectory = "C:\Install\CM"
    $VMCMPreReqsDirectory = "C:\Install\CM-Prereqs"
    $CMComputerAccount = '{0}\{1}$' -f $CMServer.DomainName.Substring(0, $CMServer.DomainName.IndexOf('.')), $CMServerName
    $CMSetupConfig = $configurationManagerContent.Clone()
    $labCred = $CMServer.GetCredential((Get-Lab))
    if (-not $AdminUser)
    {
        $AdminUser = $labCred.UserName.Split('\')[1]
    }
    $AdminPass = $labCred.GetNetworkCredential().Password

    Invoke-LabCommand -ComputerName $DCServerName -Variable (Get-Variable labCred, AdminUser) -ScriptBlock {
        try
        {
            $usr = Get-ADUser -Identity $AdminUser -ErrorAction Stop
        }
        catch { }

        if ($usr) { return }

        New-ADUser -SamAccountName $AdminUser -Name $AdminUser -AccountPassword $labCred.Password -PasswordNeverExpires $true -ChangePasswordAtLogon $false -Enabled $true
    }

    $CMSetupConfig['[Options]'].SDKServer = $CMServer.FQDN
    $CMSetupConfig['[CloudConnectorOptions]'].CloudConnectorServer = $CMServer.FQDN
    $CMSetupConfig['[SQLConfigOptions]'].SQLServerName = $SqlServerName
    $CMSetupConfig['[SQLConfigOptions]'].DatabaseName = $DatabaseName

    if ($CMRoles -contains "Management Point")
    {
        $CMSetupConfig["[Options]"].ManagementPoint = $CMServerFqdn
        $CMSetupConfig["[Options]"].ManagementPointProtocol = "HTTP"
    }

    if ($CMRoles -contains "Distribution Point")
    {
        $CMSetupConfig["[Options]"]["DistributionPoint"] = $CMServerFqdn
        $CMSetupConfig["[Options]"]["DistributionPointProtocol"] = "HTTP"
        $CMSetupConfig["[Options]"]["DistributionPointInstallIIS"] = "1"
    }

    # The "Preview" key can not exist in the .ini at all if installing CB
    if ($Branch -eq "TP")
    {
        $CMSetupConfig["[Identification]"]["Preview"] = 1
    }

    $CMSetupConfigIni = "{0}\ConfigurationFile-CM-$CMServer.ini" -f $downloadTargetDirectory
    $null = New-Item -ItemType File -Path $CMSetupConfigIni -Force
    
    foreach ($kvp in $CMSetupConfig.GetEnumerator())
    {
        $kvp.Key | Add-Content -Path $CMSetupConfigIni -Encoding ASCII
        foreach ($configKvp in $kvp.Value.GetEnumerator())
        {
            "$($configKvp.Key) = $($configKvp.Value)" | Add-Content -Path $CMSetupConfigIni -Encoding ASCII
        }
    }

    # Put CM ini file in same location as SQL ini, just for consistency. Placement of SQL ini from SQL role isn't configurable.
    try
    {
        Copy-LabFileItem -Path $("{0}\ConfigurationFile-CM-$CMServer.ini" -f $downloadTargetDirectory) -DestinationFolderPath 'C:\Install' -ComputerName $CMServer
    }
    catch
    {
        $Message = "Failed to copy '{0}' to '{1}' on server '{2}' ({2})" -f $Path, $TargetDir, $CMServerName, $CopyLabFileItem.Exception.Message
        Write-LogFunctionExitWithError -Message $Message
    }
    #endregion
    
    #region Pre-req checks
    Write-ScreenInfo -Message "Checking if site is already installed" -TaskStart
    $cim = New-LabCimSession -ComputerName $CMServer
    $Query = "SELECT * FROM SMS_Site WHERE SiteCode='{0}'" -f $CMSiteCode
    $Namespace = "ROOT/SMS/site_{0}" -f $CMSiteCode

    try
    {
        $InstalledSite = Get-CimInstance -Namespace $Namespace -Query $Query -ErrorAction "Stop" -CimSession $cim -ErrorVariable ReceiveJobErr
    }
    catch
    {
        if ($ReceiveJobErr.ErrorRecord.CategoryInfo.Category -eq 'ObjectNotFound')
        {
            Write-ScreenInfo -Message "No site found, continuing"
        }
        else
        {
            Write-ScreenInfo -Message ("Could not query SMS_Site to check if site is already installed ({0})" -f $ReceiveJobErr.ErrorRecord.Exception.Message) -TaskEnd -Type "Error"
            throw $ReceiveJobErr
        }
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd

    if ($InstalledSite.SiteCode -eq $CMSiteCode)
    {
        Write-ScreenInfo -Message ("Site '{0}' already installed on '{1}', skipping installation" -f $CMSiteCode, $CMServerName) -Type "Warning" -TaskEnd
        return
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Add Windows Defender exclusions
    # https://docs.microsoft.com/en-us/troubleshoot/mem/configmgr/recommended-antivirus-exclusions
    # https://docs.microsoft.com/en-us/powershell/module/defender/add-mppreference?view=win10-ps
    # https://docs.microsoft.com/en-us/powershell/module/defender/set-mppreference?view=win10-ps
    [char]$root = [IO.Path]::GetPathRoot($WsusContentPath).Substring(0, 1)
    $paths = @(
        '{0}:\SMS_DP$' -f $root
        '{0}:\SMSPKGG$' -f $root
        '{0}:\SMSPKG' -f $root
        '{0}:\SMSPKGSIG' -f $root
        '{0}:\SMSSIG$' -f $root
        '{0}:\RemoteInstall' -f $root
        '{0}:\WSUS' -f $root
    )
    foreach ($p in $paths) { $configurationManagerAVExcludedPaths += $p }
    Write-ScreenInfo -Message "Adding Windows Defender exclusions" -TaskStart

    try
    {
        $result = Invoke-LabCommand -ComputerName $CMServer -ActivityName "Adding Windows Defender exclusions" -Variable (Get-Variable "AVExcludedPaths", "AVExcludedProcesses") -ScriptBlock {
            Add-MpPreference -ExclusionPath $configurationManagerAVExcludedPaths -ExclusionProcess $configurationManagerAVExcludedProcesses -ErrorAction "Stop"
            Set-MpPreference -RealTimeScanDirection "Incoming" -ErrorAction "Stop"
        } -ErrorAction Stop
    }
    catch
    {
        Write-ScreenInfo -Message ("Failed to add Windows Defender exclusions ({0})" -f $_.Exception.Message) -Type "Error" -TaskEnd
        throw $_
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Saving NO_SMS_ON_DRIVE.SMS file on C: and F:
    Invoke-LabCommand -ComputerName $CMServer -Variable (Get-Variable WsusContentPath) -ActivityName "Place NO_SMS_ON_DRIVE.SMS file" -ScriptBlock {
        [char]$root = [IO.Path]::GetPathRoot($WsusContentPath).Substring(0, 1)
        foreach ($volume in (Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.DriveLetter -and $_.DriveLetter -ne $root }))
        {
            $Path = "{0}:\NO_SMS_ON_DRIVE.SMS" -f $volume.DriveLetter
            New-Item -Path $Path -ItemType "File" -ErrorAction "Stop" -Force
        }
    }
    #endregion

    #region Create directory for WSUS
    Write-ScreenInfo -Message "Creating directory for WSUS" -TaskStart
    if ($CMRoles -contains "Software Update Point")
    {
        $job = Invoke-LabCommand -ComputerName $CMServer -ActivityName "Creating directory for WSUS" -Variable (Get-Variable -Name "CMComputerAccount", WsusContentPath) -ScriptBlock {
            $null = New-Item -Path $WsusContentPath -Force -ItemType Directory
        }
    }
    else
    {
        Write-ScreenInfo -Message "Software Update Point not included in Roles, skipping" -TaskEnd
    }
    #endregion
    
    

    #region Restart computer
    Write-ScreenInfo -Message "Restarting server" -TaskStart
    Restart-LabVM -ComputerName $CMServerName -Wait -ErrorAction "Stop"
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Extend the AD Schema
    Write-ScreenInfo -Message "Extending the AD Schema" -TaskStart
    Install-LabSoftwarePackage -LocalPath "$VMCMBinariesDirectory\SMSSETUP\BIN\X64\extadsch.exe" -ComputerName $CMServerName
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Configure CM Systems Management Container
    #Need to execute this command on the Domain Controller, since it has the AD Powershell cmdlets available
    #Create the Necessary OU and permissions for the CM container in AD
    Write-ScreenInfo -Message "Configuring CM Systems Management Container" -TaskStart
    Invoke-LabCommand -ComputerName $DCServerName -ActivityName "Configuring CM Systems Management Container" -ArgumentList $CMServerName -ScriptBlock {
        Param (
            [Parameter(Mandatory)]
            [String]$CMServerName
        )

        Import-Module ActiveDirectory
        # Figure out our domain
        $rootDomainNc = (Get-ADRootDSE).defaultNamingContext

        # Get or create the System Management container
        $ou = $null
        try
        {
            $ou = Get-ADObject "CN=System Management,CN=System,$rootDomainNc"
        }
        catch
        {   
            Write-Verbose "System Management container does not currently exist."
            $ou = New-ADObject -Type Container -name "System Management" -Path "CN=System,$rootDomainNc" -Passthru
        }

        # Get the current ACL for the OU
        $acl = Get-ACL -Path "ad:CN=System Management,CN=System,$rootDomainNc"

        # Get the computer's SID (we need to get the computer object, which is in the form <ServerName>$)
        $CMComputer = Get-ADComputer "$CMServerName$"
        $CMServerSId = [System.Security.Principal.SecurityIdentifier] $CMComputer.SID

        $ActiveDirectoryRights = "GenericAll"
        $AccessControlType = "Allow"
        $Inherit = "SelfAndChildren"
        $nullGUID = [guid]'00000000-0000-0000-0000-000000000000'

        # Create a new access control entry to allow access to the OU
        $ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $CMServerSId, $ActiveDirectoryRights, $AccessControlType, $Inherit, $nullGUID
        
        # Add the ACE to the ACL, then set the ACL to save the changes
        $acl.AddAccessRule($ace)
        Set-ACL -AclObject $acl "ad:CN=System Management,CN=System,$rootDomainNc"
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Install WSUS
    Write-ScreenInfo -Message "Installing WSUS" -TaskStart
    if ($CMRoles -contains "Software Update Point")
    {
        Install-LabWindowsFeature -FeatureName "UpdateServices-Services,UpdateServices-DB" -IncludeManagementTools -ComputerName $CMServer
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Software Update Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Run WSUS post configuration tasks
    Write-ScreenInfo -Message "Running WSUS post configuration tasks" -TaskStart
    if ($CMRoles -contains "Software Update Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Running WSUS post configuration tasks" -Variable (Get-Variable "SqlServerName", WsusContentPath) -ScriptBlock {
            Start-Process -FilePath "C:\Program Files\Update Services\Tools\wsusutil.exe" -ArgumentList "postinstall", "SQL_INSTANCE_NAME=`"$SqlServerName`"", "CONTENT_DIR=`"$WsusContentPath`"" -Wait -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Software Update Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Install additional features
    Write-ScreenInfo -Message "Installing additional features (1/2)" -TaskStart
    Install-LabWindowsFeature -ComputerName $CMServer -FeatureName "FS-FileServer,Web-Mgmt-Tools,Web-Mgmt-Console,Web-Mgmt-Compat,Web-Metabase,Web-WMI,Web-WebServer,Web-Common-Http,Web-Default-Doc,Web-Dir-Browsing,Web-Http-Errors,Web-Static-Content,Web-Http-Redirect,Web-Health,Web-Http-Logging,Web-Log-Libraries,Web-Request-Monitor,Web-Http-Tracing,Web-Performance,Web-Stat-Compression,Web-Dyn-Compression,Web-Security,Web-Filtering,Web-Windows-Auth,Web-App-Dev,Web-Net-Ext,Web-Net-Ext45,Web-Asp-Net,Web-Asp-Net45,Web-ISAPI-Ext,Web-ISAPI-Filter"
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    
    Write-ScreenInfo -Message "Installing additional features (2/2)" -TaskStart
    Install-LabWindowsFeature -ComputerName $CMServer -FeatureName "NET-HTTP-Activation,NET-Non-HTTP-Activ,NET-Framework-45-ASPNET,NET-WCF-HTTP-Activation45,BITS,RDC"
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Restart
    Write-ScreenInfo -Message "Restarting server" -TaskStart
    Restart-LabVM -ComputerName $CMServerName -Wait -ErrorAction "Stop"
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion
    
    #region Install Configuration Manager
    Write-ScreenInfo "Installing Configuration Manager" -TaskStart
    $exePath = "{0}\SMSSETUP\BIN\X64\setup.exe" -f $VMCMBinariesDirectory
    $iniPath = "C:\Install\ConfigurationFile-CM-$CMServer.ini"
    $cmd = "/Script `"{0}`" /NoUserInput" -f $iniPath
    $timeout = Get-LabConfigurationItem -Name Timeout_ConfigurationManagerInstallation -Default 60
    if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { $timeout = $timeout + 30 }
    Install-LabSoftwarePackage -LocalPath $exePath -CommandLine $cmd -ProgressIndicator 10 -ExpectedReturnCodes 0 -ComputerName $CMServer -Timeout $timeout
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Restart
    Write-ScreenInfo -Message "Restarting server" -TaskStart
    Restart-LabVM -ComputerName $CMServerName -Wait -ErrorAction "Stop"
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Validating install
    Write-ScreenInfo -Message "Validating install" -TaskStart
    $cim = New-LabCimSession -ComputerName $CMServer
    $Query = "SELECT * FROM SMS_Site WHERE SiteCode='{0}'" -f $CMSiteCode
    $Namespace = "ROOT/SMS/site_{0}" -f $CMSiteCode

    try
    {
        $result = Get-CimInstance -Namespace $Namespace -Query $Query -ErrorAction "Stop" -CimSession $cim -ErrorVariable ReceiveJobErr
    }
    catch
    {
        $Message = "Failed to validate install, could not find site code '{0}' in SMS_Site class ({1})" -f $CMSiteCode, $ReceiveJobErr.ErrorRecord.Exception.Message
        Write-ScreenInfo -Message $Message -Type "Error" -TaskEnd
        Write-PSFMessage -Message "====ConfMgrSetup log content===="
        Invoke-LabCommand -ComputerName $CMServer -PassThru -ScriptBlock { Get-Content -Path C:\ConfigMgrSetup.log } | Write-PSFMessage
        return
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Install PXE Responder
    Write-ScreenInfo -Message "Installing PXE Responder" -TaskStart
    if ($CMRoles -contains "Distribution Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Installing PXE Responder" -Variable (Get-Variable CMSiteCode, CMServerFqdn, CMServerName) -Function (Get-Command "Import-CMModule") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            Set-CMDistributionPoint -SiteSystemServerName $CMServerFqdn -AllowPxeResponse $true -EnablePxe $true -EnableNonWdsPxe $true -ErrorAction "Stop"
            $start = Get-Date
            do
            {
                Start-Sleep -Seconds 5
            } while (-not (Get-Service -Name SccmPxe -ErrorAction SilentlyContinue) -and ((Get-Date) - $start) -lt '00:10:00')
        }
    }
    else
    {
        Write-ScreenInfo -Message "Distribution Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Configuring Distribution Point group
    Write-ScreenInfo -Message "Configuring Distribution Point group" -TaskStart
    if ($CMRoles -contains "Distribution Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Configuring boundary and boundary group" -Variable (Get-Variable "CMServerFqdn", "CMServerName", "CMSiteCode") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            $DPGroup = New-CMDistributionPointGroup -Name "All DPs" -ErrorAction "Stop"
            Add-CMDistributionPointToGroup -DistributionPointGroupId $DPGroup.GroupId -DistributionPointName $CMServerFqdn -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Distribution Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Install Sofware Update Point
    Write-ScreenInfo -Message "Installing Software Update Point" -TaskStart
    if ($CMRoles -contains "Software Update Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Installing Software Update Point" -Variable (Get-Variable "CMServerFqdn", "CMServerName", "CMSiteCode") -Function (Get-Command "Import-CMModule") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            Add-CMSoftwareUpdatePoint -WsusIisPort 8530 -WsusIisSslPort 8531 -SiteSystemServerName $CMServerFqdn -SiteCode $CMSiteCode -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Software Update Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Add CM account to use for Reporting Service Point
    Write-ScreenInfo -Message ("Adding new CM account '{0}' to use for Reporting Service Point" -f $AdminUser) -TaskStart
    if ($CMRoles -contains "Reporting Services Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName ("Adding new CM account '{0}' to use for Reporting Service Point" -f $AdminUser) -Variable (Get-Variable "CMServerName", "CMSiteCode", "AdminUser", "AdminPass") -Function (Get-Command "Import-CMModule") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            $Account = "{0}\{1}" -f $env:USERDOMAIN, $AdminUser
            $Secure = ConvertTo-SecureString -String $AdminPass -AsPlainText -Force
            New-CMAccount -Name $Account -Password $Secure -SiteCode $CMSiteCode -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Reporting Services Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Install Reporting Service Point
    Write-ScreenInfo -Message "Installing Reporting Service Point" -TaskStart
    if ($CMRoles -contains "Reporting Services Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Installing Reporting Service Point" -Variable (Get-Variable "CMServerFqdn", "CMServerName", "CMSiteCode", "AdminUser") -Function (Get-Command "Import-CMModule") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            $Account = "{0}\{1}" -f $env:USERDOMAIN, $AdminUser
            Add-CMReportingServicePoint -SiteCode $CMSiteCode -SiteSystemServerName $CMServerFqdn -ReportServerInstance "SSRS" -UserName $Account -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Reporting Services Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Install Endpoint Protection Point
    Write-ScreenInfo -Message "Installing Endpoint Protection Point" -TaskStart
    if ($CMRoles -contains "Endpoint Protection Point")
    {
        Invoke-LabCommand -ComputerName $CMServer -ActivityName "Installing Endpoint Protection Point" -Variable (Get-Variable "CMServerFqdn", "CMServerName", "CMSiteCode") -ScriptBlock {
            Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
            Add-CMEndpointProtectionPoint -ProtectionService "DoNotJoinMaps" -SiteCode $CMSiteCode -SiteSystemServerName $CMServerFqdn -ErrorAction "Stop"
        }
        Write-ScreenInfo -Message "Activity done" -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Endpoint Protection Point not included in Roles, skipping" -TaskEnd
    }
    #endregion

    #region Configure boundary and boundary group
    Write-ScreenInfo -Message "Configuring boundary and boundary group" -TaskStart
    Invoke-LabCommand -ComputerName $CMServer -ActivityName "Configuring boundary and boundary group" -Variable (Get-Variable "CMServerFqdn", "CMServerName", "CMSiteCode", "CMSiteName", "CMBoundaryIPRange") -ScriptBlock {
        Import-CMModule -ComputerName $CMServerName -SiteCode $CMSiteCode -ErrorAction "Stop"
        $Boundary = New-CMBoundary -DisplayName $CMSiteName -Type "IPRange" -Value $CMBoundaryIPRange -ErrorAction "Stop"
        $BoundaryGroup = New-CMBoundaryGroup -Name $CMSiteName -AddSiteSystemServerName $CMServerFqdn -ErrorAction "Stop"
        Add-CMBoundaryToGroup -BoundaryGroupId $BoundaryGroup.GroupId -BoundaryId $Boundary.BoundaryId -ErrorAction "Stop"
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion
}


function Install-LabCA
{

    [cmdletBinding()]
    param ([switch]$CreateCheckPoints)

    Write-LogFunctionEntry

    $roles = [AutomatedLab.Roles]::CaRoot -bor [AutomatedLab.Roles]::CaSubordinate

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role CaRoot, CaSubordinate
    if (-not $machines)
    {
        Write-ScreenInfo -Message 'There is no machine(s) with CA role' -Type Warning
        return
    }

    if (-not (Get-LabVM -Role CaRoot))
    {
        Write-ScreenInfo -Message 'Subordinate CA(s) defined but lab has no Root CA(s) defined. Skipping installation of CA(s).' -Type Error
        return
    }

    if ((Get-LabVM -Role CaRoot).Name)
    {
        Write-ScreenInfo -Message "Machines with Root CA role to be installed: '$((Get-LabVM -Role CaRoot).Name -join ', ')'" -TaskStart
    }

    #Bring the RootCA server online and start installing
    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline

    Start-LabVM -RoleName CaRoot, CaSubordinate -Wait -ProgressIndicator 15

    $caRootMachines = Get-LabVM -Role CaRoot -IsRunning
    if ($caRootMachines.Count -ne (Get-LabVM -Role CaRoot).Count)
    {
        Write-Error 'Not all machines of type Root CA could be started, aborting the installation'
        return
    }

    $installSequence = 0
    $jobs = @()
    foreach ($caRootMachine in $caRootMachines)
    {
        $caFeature = Invoke-LabCommand -ComputerName $caRootMachine -ActivityName "Check if CA is already installed on '$caRootMachine'" -ScriptBlock { (Get-WindowsFeature -Name 'ADCS-Cert-Authority') } -PassThru -NoDisplay
        if ($caFeature.Installed)
        {
            Write-ScreenInfo -Message "Root CA '$caRootMachine' is already installed" -Type Warning
        }
        else
        {
            $jobs += Install-LabCAMachine -Machine $caRootMachine -PassThru -PreDelaySeconds ($installSequence++*30)
        }
    }

    if ($jobs)
    {
        Write-ScreenInfo -Message 'Waiting for Root CA(s) to complete installation' -NoNewline

        Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -NoDisplay

        Write-PSFMessage -Message "Getting certificates from Root CA servers and placing them in '<labfolder>\Certs' on host machine"
        Get-LabVM -Role CaRoot | Get-LabCAInstallCertificates

        Write-ScreenInfo -Message 'Publishing certificates from CA servers to all online machines' -NoNewLine
        $jobs = Publish-LabCAInstallCertificates -PassThru
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -Timeout 30 -NoNewLine -NoDisplay

        Write-PSFMessage -Message 'Waiting for all running machines to be contactable'
        Wait-LabVM -ComputerName (Get-LabVM -All -IsRunning) -ProgressIndicator 20 -NoNewLine

        Write-PSFMessage -Message 'Invoking a GPUpdate on all running machines'
        $jobs = Invoke-LabCommand -ActivityName 'GPUpdate after Root CA install' -ComputerName (Get-LabVM -All -IsRunning) -ScriptBlock {
            gpupdate.exe /force
        } -AsJob -PassThru -NoDisplay
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -Timeout 30 -NoDisplay
    }

    Write-ScreenInfo -Message 'Finished installation of Root CAs' -TaskEnd

    #If any Subordinate CA servers to install, bring these online and start installing
    if ($machines | Where-Object { $_.Roles.Name -eq ([AutomatedLab.Roles]::CaSubordinate) })
    {
        $caSubordinateMachines = Get-LabVM -Role CaSubordinate -IsRunning
        if ($caSubordinateMachines.Count -ne (Get-LabVM -Role CaSubordinate).Count)
        {
            Write-Error 'Not all machines of type CaSubordinate could be started, aborting the installation'
            return
        }

        Write-ScreenInfo -Message "Machines with Subordinate CA role to be installed: '$($caSubordinateMachines -join ', ')'" -TaskStart


        Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
        Wait-LabVM -ComputerName (Get-LabVM -Role CaSubordinate).Name -ProgressIndicator 10

        $installSequence = 0
        $jobs = @()
        foreach ($caSubordinateMachine in $caSubordinateMachines)
        {
            $caFeature = Invoke-LabCommand -ComputerName $caSubordinateMachine -ActivityName "Check if CA is already installed on '$caSubordinateMachine'" -ScriptBlock { (Get-WindowsFeature -Name 'ADCS-Cert-Authority') } -PassThru -NoDisplay
            if ($caFeature.Installed)
            {
                Write-ScreenInfo -Message "Subordinate CA '$caSubordinateMachine' is already installed" -Type Warning
            }
            else
            {
                $jobs += Install-LabCAMachine -Machine $caSubordinateMachine -PassThru -PreDelaySeconds ($installSequence++ * 30)
            }
        }

        if ($Jobs)
        {
            Write-ScreenInfo -Message 'Waiting for Subordinate CA(s) to complete installation' -NoNewline

            Start-LabVM -StartNextMachines 1

            Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoNewLine -NoDisplay

            Write-PSFMessage -Message "- Getting certificates from CA servers and placing them in '<labfolder>\Certs' on host machine"
            Get-LabVM -Role CaRoot, CaSubordinate | Get-LabCAInstallCertificates

            Write-PSFMessage -Message '- Publishing certificates from Subordinate CA servers to all online machines'
            $jobs = Publish-LabCAInstallCertificates -PassThru
            Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -Timeout 30 -NoNewLine -NoDisplay

            Write-PSFMessage -Message 'Invoking a GPUpdate on all machines that are online'
            $jobs = Invoke-LabCommand -ComputerName (Get-LabVM -All -IsRunning) -ActivityName 'GPUpdate after Root CA install' -NoDisplay -ScriptBlock { gpupdate.exe /force } -AsJob -PassThru
            Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -Timeout 30 -NoDisplay
        }

        Invoke-LabCommand -ComputerName $caRootMachines -NoDisplay -ScriptBlock {
            certutil.exe -setreg ca\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\RequestDisposition 101
            Restart-Service -Name CertSvc
        }

        Write-ScreenInfo -Message 'Finished installation of Subordinate CAs' -TaskEnd
    }


    Write-LogFunctionExit
}


function Install-LabCAMachine
{

    [CmdletBinding()]

    param (
        [Parameter(Mandatory)]
        [AutomatedLab.Machine]$Machine,

        [int]$PreDelaySeconds,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    Write-PSFMessage -Message '****************************************************'
    Write-PSFMessage -Message "Starting installation of machine: $($machine.name)"
    Write-PSFMessage -Message '****************************************************'

    $role = $machine.Roles | Where-Object { $_.Name -eq ([AutomatedLab.Roles]::CaRoot) -or $_.Name -eq ([AutomatedLab.Roles]::CaSubordinate) }

    $param = [ordered]@{ }

    #region - Locate admin username and password for machine
    if ($machine.IsDomainJoined)
    {
        $domain = $lab.Domains | Where-Object { $_.Name -eq $machine.DomainName }

        $param.Add('UserName', ('{0}\{1}' -f $domain.Name, $domain.Administrator.UserName))
        $param.Add('Password', $domain.Administrator.Password)

        $rootDc = Get-LabVM -Role RootDC | Where-Object DomainName -eq $machine.DomainName
        if ($rootDc) #if there is a root domain controller in the same domain as the machine
        {
            $rootDomain = (Get-Lab).Domains | Where-Object Name -eq $rootDc.DomainName
            $rootDomainNetBIOSName = ($rootDomain.Name -split '\.')[0]
        }
        else #else the machine is in a child domain and the parent domain need to be used for the query
        {
            $rootDomain = $lab.GetParentDomain($machine.DomainName)
            $rootDomainNetBIOSName = ($rootDomain.Name -split '\.')[0]
            $rootDc = Get-LabVM -Role RootDC | Where-Object DomainName -eq $rootDomain
        }

        $rdcProperties = $rootDc.Roles | Where-Object Name -eq 'RootDc'
        if ($rdcProperties -and $rdcProperties.Properties.ContainsKey('NetBiosDomainName'))
        {
            $rootDomainNetBIOSName = $rdcProperties.Properties['NetBiosDomainName']
        }

        $param.Add('ForestAdminUserName', ('{0}\{1}' -f $rootDomainNetBIOSName, $rootDomain.Administrator.UserName))
        $param.Add('ForestAdminPassword', $rootDomain.Administrator.Password)

        Write-Debug -Message "Machine : $($machine.name)"
        Write-Debug -Message "Machine Domain : $($machine.DomainName)"
        Write-Debug -Message "Username for job : $($param.username)"
        Write-Debug -Message "Password for job : $($param.Password)"
        Write-Debug -Message "ForestAdmin Username : $($param.ForestAdminUserName)"
        Write-Debug -Message "ForestAdmin Password : $($param.ForestAdminPassword)"
    }
    else
    {
        $param.Add('UserName', ('{0}\{1}' -f $machine.Name, $machine.InstallationUser.UserName))
        $param.Add('Password', $machine.InstallationUser.Password)
    }
    $param.Add('ComputerName', $Machine.Name)
    #endregion



    #region - Determine DNS name for machine. This is used when installing Enterprise CAs
    $caDNSName = $Machine.Name
    if ($Machine.DomainName) { $caDNSName += ('.' + $Machine.DomainName) }

    if ($Machine.DomainName)
    {
        $param.Add('DomainName', $Machine.DomainName)
    }
    else
    {
        $param.Add('DomainName', '')
    }


    if ($role.Name -eq 'CaSubordinate')
    {
        if (!($role.Properties.ContainsKey('ParentCA'))) { $param.Add('ParentCA', '<auto>') }
        else { $param.Add('ParentCA', $role.Properties.ParentCA) }
        if (!($role.Properties.ContainsKey('ParentCALogicalName'))) { $param.Add('ParentCALogicalName', '<auto>') }
        else { $param.Add('ParentCALogicalName', $role.Properties.ParentCALogicalName) }
    }

    if (!($role.Properties.ContainsKey('CACommonName'))) { $param.Add('CACommonName', '<auto>') }
    else { $param.Add('CACommonName', $role.Properties.CACommonName) }
    if (!($role.Properties.ContainsKey('CAType'))) { $param.Add('CAType', '<auto>') }
    else { $param.Add('CAType', $role.Properties.CAType) }
    if (!($role.Properties.ContainsKey('KeyLength'))) { $param.Add('KeyLength', '4096') }
    else { $param.Add('KeyLength', $role.Properties.KeyLength) }

    if (!($role.Properties.ContainsKey('CryptoProviderName'))) { $param.Add('CryptoProviderName', 'RSA#Microsoft Software Key Storage Provider') }
    else { $param.Add('CryptoProviderName', $role.Properties.CryptoProviderName) }
    if (!($role.Properties.ContainsKey('HashAlgorithmName'))) { $param.Add('HashAlgorithmName', 'SHA256') }
    else { $param.Add('HashAlgorithmName', $role.Properties.HashAlgorithmName) }


    if (!($role.Properties.ContainsKey('DatabaseDirectory'))) { $param.Add('DatabaseDirectory', '<auto>') }
    else { $param.Add('DatabaseDirectory', $role.Properties.DatabaseDirectory) }
    if (!($role.Properties.ContainsKey('LogDirectory'))) { $param.Add('LogDirectory', '<auto>') }
    else { $param.Add('LogDirectory', $role.Properties.LogDirectory) }

    if (!($role.Properties.ContainsKey('ValidityPeriod'))) { $param.Add('ValidityPeriod', '<auto>') }
    else { $param.Add('ValidityPeriod', $role.Properties.ValidityPeriod) }
    if (!($role.Properties.ContainsKey('ValidityPeriodUnits'))) { $param.Add('ValidityPeriodUnits', '<auto>') }
    else { $param.Add('ValidityPeriodUnits', $role.Properties.ValidityPeriodUnits) }

    if (!($role.Properties.ContainsKey('CertsValidityPeriod'))) { $param.Add('CertsValidityPeriod', '<auto>') }
    else { $param.Add('CertsValidityPeriod', $role.Properties.CertsValidityPeriod) }
    if (!($role.Properties.ContainsKey('CertsValidityPeriodUnits'))) { $param.Add('CertsValidityPeriodUnits', '<auto>') }
    else { $param.Add('CertsValidityPeriodUnits', $role.Properties.CertsValidityPeriodUnits) }
    if (!($role.Properties.ContainsKey('CRLPeriod'))) { $param.Add('CRLPeriod', '<auto>') }
    else { $param.Add('CRLPeriod', $role.Properties.CRLPeriod) }
    if (!($role.Properties.ContainsKey('CRLPeriodUnits'))) { $param.Add('CRLPeriodUnits', '<auto>') }
    else { $param.Add('CRLPeriodUnits', $role.Properties.CRLPeriodUnits) }
    if (!($role.Properties.ContainsKey('CRLOverlapPeriod'))) { $param.Add('CRLOverlapPeriod', '<auto>') }
    else { $param.Add('CRLOverlapPeriod', $role.Properties.CRLOverlapPeriod) }
    if (!($role.Properties.ContainsKey('CRLOverlapUnits'))) { $param.Add('CRLOverlapUnits', '<auto>') }
    else { $param.Add('CRLOverlapUnits', $role.Properties.CRLOverlapUnits) }
    if (!($role.Properties.ContainsKey('CRLDeltaPeriod'))) { $param.Add('CRLDeltaPeriod', '<auto>') }
    else { $param.Add('CRLDeltaPeriod', $role.Properties.CRLDeltaPeriod) }
    if (!($role.Properties.ContainsKey('CRLDeltaPeriodUnits'))) { $param.Add('CRLDeltaPeriodUnits', '<auto>') }
    else { $param.Add('CRLDeltaPeriodUnits', $role.Properties.CRLDeltaPeriodUnits) }

    if (!($role.Properties.ContainsKey('UseLDAPAIA'))) { $param.Add('UseLDAPAIA', '<auto>') }
    else { $param.Add('UseLDAPAIA', $role.Properties.UseLDAPAIA) }
    if (!($role.Properties.ContainsKey('UseHTTPAIA'))) { $param.Add('UseHTTPAIA', '<auto>') }
    else { $param.Add('UseHTTPAIA', $role.Properties.UseHTTPAIA) }
    if (!($role.Properties.ContainsKey('AIAHTTPURL01'))) { $param.Add('AIAHTTPURL01', '<auto>') }
    else { $param.Add('AIAHTTPURL01', $role.Properties.AIAHTTPURL01) }
    if (!($role.Properties.ContainsKey('AIAHTTPURL02'))) { $param.Add('AIAHTTPURL02', '<auto>') }
    else { $param.Add('AIAHTTPURL02', $role.Properties.AIAHTTPURL02) }
    if (!($role.Properties.ContainsKey('AIAHTTPURL01UploadLocation'))) { $param.Add('AIAHTTPURL01UploadLocation', '') }
    else { $param.Add('AIAHTTPURL01UploadLocation', $role.Properties.AIAHTTPURL01UploadLocation) }
    if (!($role.Properties.ContainsKey('AIAHTTPURL02UploadLocation'))) { $param.Add('AIAHTTPURL02UploadLocation', '') }
    else { $param.Add('AIAHTTPURL02UploadLocation', $role.Properties.AIAHTTPURL02UploadLocation) }

    if (!($role.Properties.ContainsKey('UseLDAPCRL'))) { $param.Add('UseLDAPCRL', '<auto>') }
    else { $param.Add('UseLDAPCRL', $role.Properties.UseLDAPCRL) }
    if (!($role.Properties.ContainsKey('UseHTTPCRL'))) { $param.Add('UseHTTPCRL', '<auto>') }
    else { $param.Add('UseHTTPCRL', $role.Properties.UseHTTPCRL) }
    if (!($role.Properties.ContainsKey('CDPHTTPURL01'))) { $param.Add('CDPHTTPURL01', '<auto>') }
    else { $param.Add('CDPHTTPURL01', $role.Properties.CDPHTTPURL01) }
    if (!($role.Properties.ContainsKey('CDPHTTPURL02'))) { $param.Add('CDPHTTPURL02', '<auto>') }
    else { $param.Add('CDPHTTPURL02', $role.Properties.CDPHTTPURL02) }
    if (!($role.Properties.ContainsKey('CDPHTTPURL01UploadLocation'))) { $param.Add('CDPHTTPURL01UploadLocation', '') }
    else { $param.Add('CDPHTTPURL01UploadLocation', $role.Properties.CDPHTTPURL01UploadLocation) }
    if (!($role.Properties.ContainsKey('CDPHTTPURL02UploadLocation'))) { $param.Add('CDPHTTPURL02UploadLocation', '') }
    else { $param.Add('CDPHTTPURL02UploadLocation', $role.Properties.CDPHTTPURL02UploadLocation) }

    if (!($role.Properties.ContainsKey('InstallWebEnrollment'))) { $param.Add('InstallWebEnrollment', '<auto>') }
    else { $param.Add('InstallWebEnrollment', $role.Properties.InstallWebEnrollment) }
    if (!($role.Properties.ContainsKey('InstallWebRole'))) { $param.Add('InstallWebRole', '<auto>') }
    else { $param.Add('InstallWebRole', $role.Properties.InstallWebRole) }

    if (!($role.Properties.ContainsKey('CPSURL'))) { $param.Add('CPSURL', 'http://' + $caDNSName + '/cps/cps.html') }
    else { $param.Add('CPSURL', $role.Properties.CPSURL) }
    if (!($role.Properties.ContainsKey('CPSText'))) { $param.Add('CPSText', 'Certification Practice Statement') }
    else { $param.Add('CPSText', $($role.Properties.CPSText)) }

    if (!($role.Properties.ContainsKey('InstallOCSP'))) { $param.Add('InstallOCSP', '<auto>') }
    else { $param.Add('InstallOCSP', ($role.Properties.InstallOCSP -like '*Y*')) }
    if (!($role.Properties.ContainsKey('OCSPHTTPURL01'))) { $param.Add('OCSPHTTPURL01', '<auto>') }
    else { $param.Add('OCSPHTTPURL01', $role.Properties.OCSPHTTPURL01) }
    if (!($role.Properties.ContainsKey('OCSPHTTPURL02'))) { $param.Add('OCSPHTTPURL02', '<auto>') }
    else { $param.Add('OCSPHTTPURL02', $role.Properties.OCSPHTTPURL02) }

    if (-not $role.Properties.ContainsKey('DoNotLoadDefaultTemplates'))
    {
        $param.Add('DoNotLoadDefaultTemplates', '<auto>')
    }
    else
    {
        $value = if ($role.Properties.DoNotLoadDefaultTemplates -eq 'Yes') { $true } else { $false }
        $param.Add('DoNotLoadDefaultTemplates', $value)
    }

    #region - Check if any unknown parameter name was passed
    $knownParameters = @()
    $knownParameters += 'ParentCA' #(only valid for Subordinate CA. Ignored for Root CAs)
    $knownParameters += 'ParentCALogicalName' #(only valid for Subordinate CAs. Ignored for Root CAs)
    $knownParameters += 'CACommonName'
    $knownParameters += 'CAType'
    $knownParameters += 'KeyLength'
    $knownParameters += 'CryptoProviderName'
    $knownParameters += 'HashAlgorithmName'
    $knownParameters += 'DatabaseDirectory'
    $knownParameters += 'LogDirectory'
    $knownParameters += 'ValidityPeriod'
    $knownParameters += 'ValidityPeriodUnits'
    $knownParameters += 'CertsValidityPeriod'
    $knownParameters += 'CertsValidityPeriodUnits'
    $knownParameters += 'CRLPeriod'
    $knownParameters += 'CRLPeriodUnits'
    $knownParameters += 'CRLOverlapPeriod'
    $knownParameters += 'CRLOverlapUnits'
    $knownParameters += 'CRLDeltaPeriod'
    $knownParameters += 'CRLDeltaPeriodUnits'
    $knownParameters += 'UseLDAPAIA'
    $knownParameters += 'UseHTTPAIA'
    $knownParameters += 'AIAHTTPURL01'
    $knownParameters += 'AIAHTTPURL02'
    $knownParameters += 'AIAHTTPURL01UploadLocation'
    $knownParameters += 'AIAHTTPURL02UploadLocation'
    $knownParameters += 'UseLDAPCRL'
    $knownParameters += 'UseHTTPCRL'
    $knownParameters += 'CDPHTTPURL01'
    $knownParameters += 'CDPHTTPURL02'
    $knownParameters += 'CDPHTTPURL01UploadLocation'
    $knownParameters += 'CDPHTTPURL02UploadLocation'
    $knownParameters += 'InstallWebEnrollment'
    $knownParameters += 'InstallWebRole'
    $knownParameters += 'CPSURL'
    $knownParameters += 'CPSText'
    $knownParameters += 'InstallOCSP'
    $knownParameters += 'OCSPHTTPURL01'
    $knownParameters += 'OCSPHTTPURL02'
    $knownParameters += 'DoNotLoadDefaultTemplates'
    $knownParameters += 'PreDelaySeconds'
    $unkownParFound = $false
    foreach ($keySet in $role.Properties.GetEnumerator())
    {
        if ($keySet.Key -cnotin $knownParameters)
        {
            Write-ScreenInfo -Message "Parameter name '$($keySet.Key)' is unknown/ignored)" -Type Warning
            $unkownParFound = $true
        }
    }
    if ($unkownParFound)
    {
        Write-ScreenInfo -Message 'Valid parameter names are:' -Type Warning
        Foreach ($name in ($knownParameters.GetEnumerator()))
        {
            Write-ScreenInfo -Message " $($name)" -Type Warning
        }
        Write-ScreenInfo -Message 'NOTE that all parameter names are CASE SENSITIVE!' -Type Warning
    }
    #endregion - Check if any unknown parameter names was passed

    #endregion - Parameters


    #region - Parameters debug
    Write-Debug -Message '---------------------------------------------------------------------------------------'
    Write-Debug -Message "Parameters for $($machine.name)"
    Write-Debug -Message '---------------------------------------------------------------------------------------'
    if ($machine.Roles.Properties.GetEnumerator().Count)
    {
        foreach ($r in $machine.Roles)
        {
            if (([AutomatedLab.Roles]$r.Name -band $roles) -ne 0) #if this is a CA role
            {
                foreach ($key in ($r.Properties.GetEnumerator() | Sort-Object -Property Key))
                {
                    Write-Debug -Message " $($key.Key.PadRight(27)) $($key.Value)"
                }
            }
        }
    }
    else
    {
        Write-Debug -message ' No parameters specified'
    }
    Write-Debug -Message '---------------------------------------------------------------------------------------'
    #endregion - Parameters debug


    #region ----- Input validation (raw values) -----
    if ($role.Properties.ContainsKey('CACommonName') -and ($param.CACommonName.Length -gt 37))
    {
        Write-Error -Message "CACommonName cannot be longer than 37 characters. Specified value is: '$($param.CACommonName)'"; return
    }

    if ($role.Properties.ContainsKey('CACommonName') -and ($param.CACommonName.Length -lt 1))
    {
        Write-Error -Message "CACommonName cannot be blank. Specified value is: '$($param.CACommonName)'"; return
    }

    if ($role.Name -eq 'CaRoot')
    {
        if (-not ($param.CAType -in 'EnterpriseRootCA', 'StandAloneRootCA', '<auto>'))
        {
            Write-Error -Message "CAType needs to be 'EnterpriseRootCA' or 'StandAloneRootCA' when role is CaRoot. Specified value is: '$param.CAType'"; return
        }
    }

    if ($role.Name -eq 'CaSubordinate')
    {
        if (-not ($param.CAType -in 'EnterpriseSubordinateCA', 'StandAloneSubordinateCA', '<auto>'))
        {
            Write-Error -Message "CAType needs to be 'EnterpriseSubordinateCA' or 'StandAloneSubordinateCA' when role is CaSubordinate. Specified value is: '$param.CAType'"; return
        }
    }


    $availableCombinations = @()
    $availableCombinations += @{CryptoProviderName='Microsoft Base SMart Card Crypto Provider';           HashAlgorithmName='sha1','md2','md4','md5';                           KeyLength='1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='Microsoft Enhanced Cryptographic Provider 1.0';       HashAlgorithmName='sha1','md2','md4','md5';                           KeyLength='512','1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='ECDSA_P256#Microsoft Smart Card Key Storage Provider';HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='256'}
    $availableCombinations += @{CryptoProviderName='ECDSA_P521#Microsoft Smart Card Key Storage Provider';HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='521'}
    $availableCombinations += @{CryptoProviderName='RSA#Microsoft Software Key Storage Provider';         HashAlgorithmName='sha256','sha384','sha512','sha1','md5','md4','md2';KeyLength='512','1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='Microsoft Base Cryptographic Provider v1.0';          HashAlgorithmName='sha1','md2','md4','md5';                           KeyLength='512','1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='ECDSA_P521#Microsoft Software Key Storage Provider';  HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='521'}
    $availableCombinations += @{CryptoProviderName='ECDSA_P256#Microsoft Software Key Storage Provider';  HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='256';}
    $availableCombinations += @{CryptoProviderName='Microsoft Strong Cryptographic Provider';             HashAlgorithmName='sha1','md2','md4','md5';                           KeyLength='512','1024','2048','4096';}
    $availableCombinations += @{CryptoProviderName='ECDSA_P384#Microsoft Software Key Storage Provider';  HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='384'}
    $availableCombinations += @{CryptoProviderName='Microsoft Base DSS Cryptographic Provider';           HashAlgorithmName='sha1';                                             KeyLength='512','1024'}
    $availableCombinations += @{CryptoProviderName='RSA#Microsoft Smart Card Key Storage Provider';       HashAlgorithmName='sha256','sha384','sha512','sha1','md5','md4','md2';KeyLength='1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='DSA#Microsoft Software Key Storage Provider';         HashAlgorithmName='sha1';                                             KeyLength='512','1024','2048','4096'}
    $availableCombinations += @{CryptoProviderName='ECDSA_P384#Microsoft Smart Card Key Storage Provider';HashAlgorithmName='sha256','sha384','sha512','sha1';                  KeyLength='384'}

    $combination = $availableCombinations | Where-Object {$_.CryptoProviderName -eq $param.CryptoProviderName}

    if (-not ($param.CryptoProviderName -in $combination.CryptoProviderName))
    {
        Write-Error -Message "CryptoProviderName '$($param.CryptoProviderName)' is unknown. `nList of valid options for CryptoProviderName:`n $($availableCombinations.CryptoProviderName -join "`n ")"; return
    }
    elseif (-not ($param.HashAlgorithmName -in $combination.HashAlgorithmName))
    {
        Write-Error -Message "HashAlgorithmName '$($param.HashAlgorithmName)' is not valid for CryptoProviderName '$($param.CryptoProviderName)'. The Crypto Provider selected supports the following Hash Algorithms:`n $($combination.HashAlgorithmName -join "`n ")"; return
    }
    elseif (-not ($param.KeyLength -in $combination.KeyLength))
    {
        Write-Error -Message "Keylength '$($param.KeyLength)' is not valid for CryptoProviderName '$($param.CryptoProviderName)'. The Crypto Provider selected supports the following keylengths:`n $($combination.KeyLength -join "`n ")"; return
    }



    if ($role.Properties.ContainsKey('DatabaseDirectory') -and -not ($param.DatabaseDirectory -match '^[C-Z]:\\'))
    {
        Write-Error -Message 'DatabaseDirectory needs to be located on a local drive (drive letter C-Z)'; return
    }

    if ($role.Properties.ContainsKey('LogDirectory') -and -not ($param.LogDirectory -match '^[C-Z]:\\'))
    {
        Write-Error -Message 'LogDirectory needs to be located on a local drive (drive letter C-Z)'; return
    }

    if (($param.UseLDAPAIA -ne '<auto>') -and ($param.UseLDAPAIA -notin ('Yes', 'No')))
    {
        Write-Error -Message "UseLDAPAIA needs to be 'Yes' or 'no'. Specified value is: '$($param.UseLDAPAIA)'"; return
    }

    if (($param.UseHTTPAIA -ne '<auto>') -and ($param.UseHTTPAIA -notin ('Yes', 'No')))
    {
        Write-Error -Message "UseHTTPAIA needs to be 'Yes' or 'no'. Specified value is: '$($param.UseHTTPAIA)'"; return
    }

    if (($param.UseLDAPCRL -ne '<auto>') -and ($param.UseLDAPCRL -notin ('Yes', 'No')))
    {
        Write-Error -Message "UseLDAPCRL needs to be 'Yes' or 'no'. Specified value is: '$($param.UseLDAPCRL)'"; return
    }

    if (($param.UseHTTPCRL -ne '<auto>') -and ($param.UseHTTPCRL -notin ('Yes', 'No')))
    {
        Write-Error -Message "UseHTTPCRL needs to be 'Yes' or 'no'. Specified value is: '$($param.UseHTTPCRL)'"; return
    }

    if (($param.InstallWebEnrollment -ne '<auto>') -and ($param.InstallWebEnrollment -notin ('Yes', 'No')))
    {
        Write-Error -Message "InstallWebEnrollment needs to be 'Yes' or 'no'. Specified value is: '$($param.InstallWebEnrollment)'"; return
    }

    if (($param.InstallWebRole -ne '<auto>') -and ($param.InstallWebRole -notin ('Yes', 'No')))
    {
        Write-Error -Message "InstallWebRole needs to be 'Yes' or 'no'. Specified value is: '$($param.InstallWebRole)'"; return
    }

    if (($param.AIAHTTPURL01 -ne '<auto>') -and ($param.AIAHTTPURL01 -notlike 'http://*'))
    {
        Write-Error -Message "AIAHTTPURL01 needs to start with 'http://' (https is not supported). Specified value is: '$($param.AIAHTTPURL01)'"; return
    }

    if (($param.AIAHTTPURL02 -ne '<auto>') -and ($param.AIAHTTPURL02 -notlike 'http://*'))
    {
        Write-Error -Message "AIAHTTPURL02 needs to start with 'http://' (https is not supported). Specified value is: '$($param.AIAHTTPURL02)'"; return
    }

    if (($param.CDPHTTPURL01 -ne '<auto>') -and ($param.CDPHTTPURL01 -notlike 'http://*'))
    {
        Write-Error -Message "CDPHTTPURL01 needs to start with 'http://' (https is not supported). Specified value is: '$($param.CDPHTTPURL01)'"; return
    }

    if (($param.CDPHTTPURL02 -ne '<auto>') -and ($param.CDPHTTPURL02 -notlike 'http://*'))
    {
        Write-Error -Message "CDPHTTPURL02 needs to start with 'http://' (https is not supported). Specified value is: '$($param.CDPHTTPURL02)'"; return
    }

    if (($role.Name -eq 'CaRoot') -and ($param.DoNotLoadDefaultTemplates -ne '<auto>') -and ($param.DoNotLoadDefaultTemplates -notin ('Yes', 'No')))
    {
        Write-Error -Message "DoNotLoadDefaultTemplates needs to be 'Yes' or 'No'. Specified value is: '$($param.DoNotLoadDefaultTemplates)'"; return
    }



    #ValidityPeriod and ValidityPeriodUnits
    if ($param.ValidityPeriodUnits -ne '<auto>')
    {
        try { $dummy = [int]$param.ValidityPeriodUnits }
        catch { Write-Error -Message 'ValidityPeriodUnits is not convertable to an integer. Please specify (enclosed as a string) a number between 1 and 2147483647'; return }
    }

    if (($param.ValidityPeriodUnits -ne '<auto>') -and ([int]$param.ValidityPeriodUnits) -lt 1)
    {
        Write-Error -Message 'ValidityPeriodUnits cannot be less than 1. Please specify (enclosed as a string) a number between 1 and 2147483647'; return
    }

    if (($param.ValidityPeriodUnits) -ne '<auto>' -and (!($role.Properties.ContainsKey('ValidityPeriod'))))
    {
        Write-Error -Message 'ValidityPeriodUnits specified (ok) while ValidityPeriod is not specified. ValidityPeriod needs to be one of "Years", "Months", "Weeks", "Days", "Hours".'; return
    }

    if ($param.ValidityPeriod -ne '<auto>' -and ($param.ValidityPeriod -notin ('Years', 'Months', 'Weeks', 'Days', 'Hours')))
    {
        Write-Error -Message "ValidityPeriod need to be one of 'Years', 'Months', 'Weeks', 'Days', 'Hours'. Specified value is: '$($param.ValidityPeriod)'"; return
    }


    #CertsValidityPeriod and CertsValidityPeriodUnits
    if ($param.CertsValidityPeriodUnits -ne '<auto>')
    {
        try { $dummy = [int]$param.CertsValidityPeriodUnits }
        catch { Write-Error -Message 'CertsValidityPeriodUnits is not convertable to an integer. Please specify (enclosed as a string) a number between 1 and 2147483647'; return }
    }

    if (($param.CertsValidityPeriodUnits) -ne '<auto>' -and (!($role.Properties.ContainsKey('CertsValidityPeriod'))))
    {
        Write-Error -Message 'CertsValidityPeriodUnits specified (ok) while CertsValidityPeriod is not specified. CertsValidityPeriod needs to be one of "Years", "Months", "Weeks", "Days", "Hours" .'; return
    }

    if ($param.CertsValidityPeriod -ne '<auto>' -and ($param.CertsValidityPeriod -notin ('Years', 'Months', 'Weeks', 'Days', 'Hours')))
    {
        Write-Error -Message "CertsValidityPeriod need to be one of 'Years', 'Months', 'Weeks', 'Days', 'Hours'. Specified value is: '$($param.CertsValidityPeriod)'"; return
    }


    #CRLPeriodUnits and CRLPeriodUnitsUnits
    if ($param.CRLPeriodUnits -ne '<auto>')
    {
        try { $dummy = [int]$param.CRLPeriodUnits }
        catch { Write-Error -Message 'CRLPeriodUnits is not convertable to an integer. Please specify (enclosed as a string) a number between 1 and 2147483647'; return }
    }

    if (($param.CRLPeriodUnits) -ne '<auto>' -and (!($role.Properties.ContainsKey('CRLPeriod'))))
    {
        Write-Error -Message 'CRLPeriodUnits specified (ok) while CRLPeriod is not specified. CRLPeriod needs to be one of "Years", "Months", "Weeks", "Days", "Hours" .'; return
    }

    if ($param.CRLPeriod -ne '<auto>' -and ($param.CRLPeriod -notin ('Years', 'Months', 'Weeks', 'Days', 'Hours')))
    {
        Write-Error -Message "CRLPeriod need to be one of 'Years', 'Months', 'Weeks', 'Days', 'Hours'. Specified value is: '$($param.CRLPeriod)'"; return
    }


    #CRLOverlapPeriod and CRLOverlapUnits
    if ($param.CRLOverlapUnits -ne '<auto>')
    {
        try { $dummy = [int]$param.CRLOverlapUnits }
        catch { Write-Error -Message 'CRLOverlapUnits is not convertable to an integer. Please specify (enclosed as a string) a number between 1 and 2147483647'; return }
    }

    if (($param.CRLOverlapUnits) -ne '<auto>' -and (!($role.Properties.ContainsKey('CRLOverlapPeriod'))))
    {
        Write-Error -Message 'CRLOverlapUnits specified (ok) while CRLOverlapPeriod is not specified. CRLOverlapPeriod needs to be one of "Years", "Months", "Weeks", "Days", "Hours" .'; return
    }

    if ($param.CRLOverlapPeriod -ne '<auto>' -and ($param.CRLOverlapPeriod -notin ('Years', 'Months', 'Weeks', 'Days', 'Hours')))
    {
        Write-Error -Message "CRLOverlapPeriod need to be one of 'Years', 'Months', 'Weeks', 'Days', 'Hours'. Specified value is: '$($param.CRLOverlapPeriod)'"; return
    }


    #CRLDeltaPeriod and CRLDeltaPeriodUnits
    if ($param.CRLDeltaPeriodUnits -ne '<auto>')
    {
        try { $dummy = [int]$param.CRLDeltaPeriodUnits }
        catch { Write-Error -Message 'CRLDeltaPeriodUnits is not convertable to an integer. Please specify (enclosed as a string) a number between 1 and 2147483647'; return }
    }

    if (($param.CRLDeltaPeriodUnits) -ne '<auto>' -and (!($role.Properties.ContainsKey('CRLDeltaPeriod'))))
    {
        Write-Error -Message 'CRLDeltaPeriodUnits specified (ok) while CRLDeltaPeriod is not specified. CRLDeltaPeriod needs to be one of "Years", "Months", "Weeks", "Days", "Hours" .'; return
    }

    if ($param.CRLDeltaPeriod -ne '<auto>' -and ($param.CRLDeltaPeriod -notin ('Years', 'Months', 'Weeks', 'Days', 'Hours')))
    {
        Write-Error -Message "CRLDeltaPeriod need to be one of 'Years', 'Months', 'Weeks', 'Days', 'Hours'. Specified value is: '$($param.CRLDeltaPeriod)'"; return
    }

    #endregion ----- Input validation (raw values) -----



    #region ----- Input validation (content analysis) -----
    if (($param.CAType -like 'Enterprise*') -and (!($machine.isDomainJoined)))
    {
        Write-Error -Message "CA Type specified is '$($param.CAType)' while machine is not domain joined. This is not possible"; return
    }

    if (($param.CAType -like 'StandAlone*') -and ($role.Properties.ContainsKey('UseLDAPAIA')) -and ($param.UseLDAPAIA))
    {
        Write-Error -Message "UseLDAPAIA is set to 'Yes' while 'CAType' is set to '$($param.CAType)'. It is not possible to use LDAP based AIA for a $($param.CAType)"; return
    }

    if (($param.CAType -like 'StandAlone*') -and ($role.Properties.ContainsKey('UseLDAPCRL')) -and ($param.UseLDAPCRL))
    {
        Write-Error -Message "UseLDAPCRL is set to 'Yes' while 'CAType' is set to '$($param.CAType)'. It is not possible to use LDAP based CRL for a $($param.CAType)"; return
    }

    if (($param.CAType -like 'StandAlone*') -and ($role.Properties.ContainsKey('InstallWebRole')) -and (!($param.InstallWebRole)))
    {
        Write-Error -Message "InstallWebRole is set to No while CAType is StandAloneCA. $($param.CAType) needs web role for hosting a CDP"
        return
    }

    if (($role.Properties.ContainsKey('OCSPHTTPURL01')) -or ($role.Properties.ContainsKey('OCSPHTTPURL02')) -or ($role.Properties.ContainsKey('InstallOCSP')))
    {
        Write-ScreenInfo -Message 'OCSP is not yet supported. OCSP parameters will be ignored and OCSP will not be installed!' -Type Warning
    }


    #if any validity parameter was defined, get these now and convert them all to hours (temporary variables)
    if ($param.ValidityPeriodUnits -ne '<auto>')
    {
        switch ($param.ValidityPeriod)
        {
            'Years'  { $validityPeriodUnitsHours = [int]$param.ValidityPeriodUnits * 365 * 24 }
            'Months' { $validityPeriodUnitsHours = [int]$param.ValidityPeriodUnits * (365/12) * 24 }
            'Weeks'  { $validityPeriodUnitsHours = [int]$param.ValidityPeriodUnits * 7 * 24 }
            'Days'   { $validityPeriodUnitsHours = [int]$param.ValidityPeriodUnits * 24 }
            'Hours'  { $validityPeriodUnitsHours = [int]$param.ValidityPeriodUnits }
        }
    }
    if ($param.CertsValidityPeriodUnits -ne '<auto>')
    {
        switch ($param.CertsValidityPeriod)
        {
            'Years'  { $certsvalidityPeriodUnitsHours = [int]$param.CertsValidityPeriodUnits * 365 * 24 }
            'Months' { $certsvalidityPeriodUnitsHours = [int]$param.CertsValidityPeriodUnits * (365/12) * 24 }
            'Weeks'  { $certsvalidityPeriodUnitsHours = [int]$param.CertsValidityPeriodUnits * 7 * 24 }
            'Days'   { $certsvalidityPeriodUnitsHours = [int]$param.CertsValidityPeriodUnits * 24 }
            'Hours'  { $certsvalidityPeriodUnitsHours = [int]$param.CertsValidityPeriodUnits }
        }
    }
    if ($param.CRLPeriodUnits -ne '<auto>')
    {
        switch ($param.CRLPeriod)
        {
            'Years'  { $cRLPeriodUnitsHours = [int]([int]$param.CRLPeriodUnits * 365 * 24) }
            'Months' { $cRLPeriodUnitsHours = [int]([int]$param.CRLPeriodUnit * (365/12) * 24) }
            'Weeks'  { $cRLPeriodUnitsHours = [int]([int]$param.CRLPeriodUnits * 7 * 24) }
            'Days'   { $cRLPeriodUnitsHours = [int]([int]$param.CRLPeriodUnits * 24) }
            'Hours'  { $cRLPeriodUnitsHours = [int]([int]$param.CRLPeriodUnits) }
        }
    }
    if ($param.CRLDeltaPeriodUnits -ne '<auto>')
    {
        switch ($param.CRLDeltaPeriod)
        {
            'Years'  { $cRLDeltaPeriodUnitsHours = [int]([int]$param.CRLDeltaPeriodUnits * 365 * 24) }
            'Months' { $cRLDeltaPeriodUnitsHours = [int]([int]$param.CRLDeltaPeriodUnits * (365/12) * 24) }
            'Weeks'  { $cRLDeltaPeriodUnitsHours = [int]([int]$param.CRLDeltaPeriodUnits * 7 * 24) }
            'Days'   { $cRLDeltaPeriodUnitsHours = [int]([int]$param.CRLDeltaPeriodUnits * 24) }
            'Hours'  { $cRLDeltaPeriodUnitsHours = [int]([int]$param.CRLDeltaPeriodUnits) }
        }
    }
    if ($param.CRLOverlapUnits -ne '<auto>')
    {
        switch ($param.CRLOverlapPeriod)
        {
            'Years'  { $CRLOverlapUnitsHours = [int]([int]$param.CRLOverlapUnits * 365 * 24) }
            'Months' { $CRLOverlapUnitsHours = [int]([int]$param.CRLOverlapUnits * (365/12) * 24) }
            'Weeks'  { $CRLOverlapUnitsHours = [int]([int]$param.CRLOverlapUnits * 7 * 24) }
            'Days'   { $CRLOverlapUnitsHours = [int]([int]$param.CRLOverlapUnits * 24) }
            'Hours'  { $CRLOverlapUnitsHours = [int]([int]$param.CRLOverlapUnits) }
        }
    }

    if ($role.Properties.ContainsKey('CRLPeriodUnits') -and ($cRLPeriodUnitsHours) -and ($validityPeriodUnitsHours) -and ($cRLPeriodUnitsHours -ge $validityPeriodUnitsHours))
    {
        Write-Error -Message "CRLPeriodUnits is longer than ValidityPeriodUnits. This is not possible. `
            Specified value for CRLPeriodUnits is: '$($param.CRLPeriodUnits) $($param.CRLPeriod)'`
        Specified value for ValidityPeriodUnits is: '$($param.ValidityPeriodUnits) $($param.ValidityPeriod)'"

        return
    }
    if ($role.Properties.ContainsKey('CertsValidityPeriodUnits') -and ($certsvalidityPeriodUnitsHours) -and ($validityPeriodUnitsHours) -and ($certsvalidityPeriodUnitsHours -ge $validityPeriodUnitsHours))
    {
        Write-Error -Message "CertsValidityPeriodUnits is longer than ValidityPeriodUnits. This is not possible. `
            Specified value for certsValidityPeriodUnits is: '$($param.CertsValidityPeriodUnits) $($param.CertsValidityPeriod)'`
        Specified value for ValidityPeriodUnits is: '$($param.ValidityPeriodUnits) $($param.ValidityPeriod)'"

        return
    }
    if ($role.Properties.ContainsKey('CRLDeltaPeriodUnits') -and ($CRLDeltaPeriodUnitsHours) -and ($cRLPeriodUnitsHours) -and ($cRLDeltaPeriodUnitsHours -ge $cRLPeriodUnitsHours))
    {
        Write-Error -Message "CRLDeltaPeriodUnits is longer than CRLPeriodUnits. This is not possible. `
            Specified value for CRLDeltaPeriodUnits is: '$($param.CRLDeltaPeriodUnits) $($param.CRLDeltaPeriod)'`
        Specified value for ValidityPeriodUnits is: '$($param.CRLPeriodUnits) $($param.CRLPeriod)'"

        return
    }
    if ($role.Properties.ContainsKey('CRLOverlapUnits') -and ($CRLOverlapUnitsHours) -and ($validityPeriodUnitsHours) -and ($CRLOverlapUnitsHours -ge $validityPeriodUnitsHours))
    {
        Write-Error -Message "CRLOverlapUnits is longer than ValidityPeriodUnits. This is not possible. `
            Specified value for CRLOverlapUnits is: '$($param.CRLOverlapUnits) $($param.CRLOverlapPeriod)'`
        Specified value for ValidityPeriodUnits is: '$($param.ValidityPeriodUnits) $($param.ValidityPeriod)'"

        return
    }
    if ($role.Properties.ContainsKey('CRLOverlapUnits') -and ($CRLOverlapUnitsHours) -and ($cRLPeriodUnitsHours) -and ($CRLOverlapUnitsHours -ge $cRLPeriodUnitsHours))
    {
        Write-Error -Message "CRLOverlapUnits is longer than CRLPeriodUnits. This is not possible. `
            Specified value for CRLOverlapUnits is: '$($param.CRLOverlapUnits) $($param.CRLOverlapPeriod)'`
        Specified value for CRLPeriodUnits is: '$($param.CRLPeriodUnits) $($param.CRLPeriod)'"

        return
    }
    if (($param.CAType -like '*root*') -and ($role.Properties.ContainsKey('ValidityPeriod')) -and ($validityPeriodUnitsHours) -and ($validityPeriodUnitsHours -gt (10 * 365 * 24)))
    {
        Write-ScreenInfo -Message "ValidityPeriod is more than 10 years. Overall validity of all issued certificates by Enterprise Root CAs will be set to specified value. `
            However, the default validity (specified by 2012/2012R2 Active Directory) of issued by Enterprise Root CAs to Subordinate CAs, is 5 years. `
        If more than 5 years is needed, a custom certificate template is needed wherein the validity can be changed."
 -Type Warning
    }


    #region - If DatabaseDirectory or LogDirectory is specified, Check for drive existence in the VM
    if (($param.DatabaseDirectory -ne '<auto>') -or ($param.LogDirectory -ne '<auto>'))
    {
        $caSession = New-LabPSSession -ComputerName $Machine

        if ($param.DatabaseDirectory -ne '<auto>')
        {
            $DatabaseDirectoryDrive = ($param.DatabaseDirectory.split(':')[0]) + ':'

            $disk = Invoke-LabCommand -ComputerName $Machine -ScriptBlock {
                if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                {
                    Get-CimInstance -Namespace Root\CIMV2 -Class Win32_LogicalDisk -Filter "DeviceID = ""$DatabaseDirectoryDrive"""
                }
                else
                {
                    Get-WmiObject -Namespace Root\CIMV2 -Class Win32_LogicalDisk -Filter "DeviceID = ""$DatabaseDirectoryDrive"""
                }
            } -Variable (Get-Variable -Name DatabaseDirectoryDrive) -PassThru

            if (-not $disk -or -not $disk.DriveType -eq 3)
            {
                Write-Error -Message "Drive for Database Directory does not exist or is not a hard disk drive. Specified value is: $DatabaseDirectory"
                return
            }
        }

        if ($param.LogDirectory -ne '<auto>')
        {
            $LogDirectoryDrive = ($param.LogDirectory.split(':')[0]) + ':'
            $disk = Invoke-LabCommand -ComputerName $Machine -ScriptBlock {
                if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                {
                    Get-CimInstance -Namespace Root\CIMV2 -Class Win32_LogicalDisk -Filter "DeviceID = ""$LogDirectoryDrive"""
                }
                else
                {
                    Get-WmiObject -Namespace Root\CIMV2 -Class Win32_LogicalDisk -Filter "DeviceID = ""$LogDirectoryDrive"""
                }
            } -Variable (Get-Variable -Name LogDirectoryDrive) -PassThru
            if (-not $disk -or -not $disk.DriveType -eq 3)
            {
                Write-Error -Message "Drive for Log Directory does not exist or is not a hard disk drive. Specified value is: $LogDirectory"
                return
            }
        }
    }
    #endregion - If DatabaseDirectory or LogDirectory is specified, Check for drive existence in the VM

    #endregion ----- Input validation (content analysis) -----


    #region ----- Calculations -----

    #If ValidityPeriodUnits is not defined, define it now and Update machine property "ValidityPeriod"
    if ($param.ValidityPeriodUnits -eq '<auto>')
    {
        $param.ValidityPeriod = 'Years'
        $param.ValidityPeriodUnits = '10'
        if (!($validityPeriodUnitsHours)) { $validityPeriodUnitsHours = [int]($param.ValidityPeriodUnits) * 365 * 24 }
    }


    #If CAType is not defined, define it now
    if ($param.CAType -eq '<auto>')
    {
        if ($machine.IsDomainJoined)
        {
            if ($role.Name -eq 'CaRoot')
            {
                $param.CAType = 'EnterpriseRootCA'
                if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message 'Parameter "CAType" is not specified. Automatically setting CAtype to "EnterpriseRootCA" since machine is domain joined and Root CA role is specified' -Type Warning }
            }
            else
            {
                $param.CAType = 'EnterpriseSubordinateCA'
                if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message 'Parameter "CAType" is not specified. Automatically setting CAtype to "EnterpriseSubordinateCA" since machine is domain joined and Subordinate CA role is specified' -Type Warning }
            }
        }
        else
        {
            if ($role.Name -eq 'CaRoot')
            {
                $param.CAType = 'StandAloneRootCA'
                if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message 'Parameter "CAType" is not specified. Automatically setting CAtype to "StandAloneRootCA" since machine is not domain joined and Root CA role is specified' -Type Warning }
            }
            else
            {
                $param.CAType = 'StandAloneSubordinateCA'
                if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message 'Parameter "CAType" is not specified. Automatically setting CAtype to "StandAloneSubordinateCA" since machine is not domain joined and Subordinate CA role is specified' -Type Warning }
            }
        }
    }


    #If ParentCA is not defined, try to find it automatically
    if ($param.ParentCA -eq '<auto>')
    {
        if ($param.CAType -like '*Subordinate*') #CA is a Subordinate CA
        {
            if ($param.CAType -like 'Enterprise*')
            {
                $rootCA = [array](Get-LabVM -Role CaRoot | Where-Object DomainName -eq $machine.DomainName | Sort-Object -Property DomainName) | Select-Object -First 1

                if (-not $rootCA)
                {
                    $rootCA = [array](Get-LabVM -Role CaRoot | Where-Object { -not $_.IsDomainJoined }) | Select-Object -First 1
                }

            }
            else
            {
                $rootCA = [array](Get-LabVM -Role CaRoot | Where-Object { -not $_.IsDomainJoined }) | Select-Object -First 1
            }

            if ($rootCA)
            {
                $param.ParentCALogicalName = ($rootCA.Roles | Where-Object Name -eq CaRoot).Properties.CACommonName
                $param.ParentCA = $rootCA.Name
                Write-PSFMessage "Root CA '$($param.ParentCALogicalName)' ($($param.ParentCA)) automatically selected as parent CA"
                $ValidityPeriod = $rootCA.roles.Properties.CertsValidityPeriod
                $ValidityPeriodUnits = $rootCA.roles.Properties.CertsValidityPeriodUnits
            }
            else
            {
                Write-Error -Message 'No name for Parent CA specified and no Root CA can be located automatically. Please install a Root CA in the lab before installing a Subordinate CA'
                return
            }

            #Check if Parent CA is valid
            $caSession = New-LabPSSession -ComputerName $param.ComputerName

            Write-Debug -Message "Testing ParentCA with command: 'certutil -ping $($param.ParentCA)\$($param.ParentCALogicalName)'"


            $totalretries = 20
            $retries = 0

            Write-PSFMessage -Message "Testing Root CA availability: certutil -ping $($param.ParentCA)\$($param.ParentCALogicalName)"
            do
            {
                $result = Invoke-LabCommand -ComputerName $param.ComputerName -ScriptBlock {
                    param(
                        [string]$ParentCA,
                        [string]$ParentCALogicalName
                    )
                    Invoke-Expression -Command "certutil -ping $ParentCA\$ParentCALogicalName"
                } -ArgumentList $param.ParentCA, $param.ParentCALogicalName -PassThru -NoDisplay

                if (-not ($result | Where-Object { $_ -like '*interface is alive*' }))
                {
                    $result | ForEach-Object { Write-Debug -Message $_ }
                    $retries++
                    Write-PSFMessage -Message "Could not contact ParentCA. (Computername=$($param.ParentCA), LogicalCAName=$($param.ParentCALogicalName)). (Check $retries of $totalretries)"
                    if ($retries -lt $totalretries) { Start-Sleep -Seconds 5 }
                }
            }
            until (($result | Where-Object { $_ -like '*interface is alive*' }) -or ($retries -ge $totalretries))

            if ($result | Where-Object { $_ -like '*interface is alive*' })
            {
                Write-PSFMessage -Message "Parent CA ($($param.ParentCA)) is contactable"
            }
            else
            {
                Write-Error -Message "Parent CA ($($param.ParentCA)) is not contactable. Please install a Root CA in the lab before installing a Subordinate CA"
                return
            }
        }
        else #CA is a Root CA
        {
            $param.ParentCALogicalName = ''
            $param.ParentCA = ''
        }
    }

    #Calculate and update machine property "CACommonName" if this was not specified. Note: the first instance of a name of a Root CA server, will be used by install code for Sub CAs.
    if ($param.CACommonName -eq '<auto>')
    {
        if ($role.Name -eq 'CaRoot')        { $caBaseName = 'LabRootCA' }
        if ($role.Name -eq 'CaSubordinate') { $caBaseName = 'LabSubCA'  }

        [array]$caNamesAlreadyInUse = Invoke-LabCommand -ComputerName (Get-LabVM -Role $role.Name) -ScriptBlock {
            $name = certutil.exe -getreg CA\CommonName | Where-Object { $_ -match 'CommonName REG' }
            if ($name)
            {
                $name.Split('=')[1].Trim()
            }
        } -NoDisplay -PassThru
        $num = 0
        do
        {
            $num++
        }
        until (($caBaseName + [string]($num)) -notin ((Get-LabVM).Roles.Properties.CACommonName) -and ($caBaseName + [string]($num)) -notin $caNamesAlreadyInUse)

        $param.CACommonName = $caBaseName + ([string]$num)
        ($machine.Roles | Where-Object Name -like Ca*).Properties.Add('CACommonName', $param.CACommonName)
    }

    #Converting to correct types for some parameters
    if ($param.InstallWebEnrollment -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.InstallWebEnrollment = $False
        }
        else
        {
            $param.InstallWebEnrollment = $True
        }
    }
    else
    {
        $param.InstallWebEnrollment = ($param.InstallWebEnrollment -like '*Y*')
    }

    if ($param.InstallWebRole -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.InstallWebRole = $False
        }
        else
        {
            $param.InstallWebRole = $True
        }
    }
    else
    {
        $param.InstallWebRole = ($param.InstallWebRole -like '*Y*')
    }

    if ($param.UseLDAPAIA -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.UseLDAPAIA = $True
        }
        else
        {
            $param.UseLDAPAIA = $False
        }
    }
    else
    {
        $param.UseLDAPAIA = ($param.UseLDAPAIA -like '*Y*')
    }

    if ($param.UseHTTPAIA -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.UseHTTPAIA = $False
        }
        else
        {
            $param.UseHTTPAIA = $True
        }
    }
    else
    {
        $param.UseHTTPAIA = ($param.UseHTTPAIA -like '*Y*')
    }

    if ($param.UseLDAPCRL -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.UseLDAPCRL = $True
        }
        else
        {
            $param.UseLDAPCRL = $False
        }
    }
    else
    {
        $param.UseLDAPCRL = ($param.UseLDAPCRL -like '*Y*')
    }

    if ($param.UseHTTPCRL -eq '<auto>')
    {
        if ($param.CAType -like 'Enterprise*')
        {
            $param.UseHTTPCRL = $False
        }
        else
        {
            $param.UseHTTPCRL = $True
        }
    }
    else
    {
        $param.UseHTTPCRL = ($param.UseHTTPCRL -like '*Y*')
    }

    $param.InstallOCSP = $False
    $param.OCSPHTTPURL01 = ''
    $param.OCSPHTTPURL02 = ''


    $param.AIAHTTPURL01UploadLocation = ''
    $param.AIAHTTPURL02UploadLocation = ''
    $param.CDPHTTPURL01UploadLocation = ''
    $param.CDPHTTPURL02UploadLocation = ''




    if (($param.CaType -like 'StandAlone*') -and $role.Properties.ContainsKey('UseLDAPAIA') -and $param.UseLDAPAIA)
    {
        Write-Error -Message "Parameter 'UseLDAPAIA' is set to 'Yes' while 'CAType' is set to '$($param.CaType)'. It is not possible to use LDAP based AIA for a $($param.CaType)"
        return
    }
    elseif (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('UseLDAPAIA'))))
    {
        $param.UseLDAPAIA = $False
    }

    if (($param.CaType -like 'StandAlone*') -and $role.Properties.ContainsKey('UseHTTPAIA') -and (-not $param.UseHTTPAIA))
    {
        Write-Error -Message "Parameter 'UseHTTPAIA' is set to 'No' while 'CAType' is set to '$($param.CaType)'. Only AIA possible for a $($param.CaType), is Http based AIA."
        return
    }
    elseif (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('UseHTTPAIA'))))
    {
        $param.UseHTTPAIA = $True
    }


    if (($param.CaType -like 'StandAlone*') -and $role.Properties.ContainsKey('UseLDAPCRL') -and $param.UseLDAPCRL)
    {
        Write-Error -Message "Parameter 'UseLDAPCRL' is set to 'Yes' while 'CAType' is set to '$($param.CaType)'. It is not possible to use LDAP based CRL for a $($param.CaType)"
        return
    }
    elseif (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('UseLDAPCRL'))))
    {
        $param.UseLDAPCRL = $False
    }

    if (($param.CaType -like 'StandAlone*') -and $role.Properties.ContainsKey('UseHTTPCRL') -and (-not $param.UseHTTPCRL))
    {
        Write-Error -Message "Parameter 'UseHTTPCRL' is set to 'No' while 'CAType' is set to '$($param.CaType)'. Only CRL possible for a $($param.CaType), is Http based CRL."
        return
    }
    elseif (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('UseHTTPCRL'))))
    {
        $param.UseHTTPCRL = $True
    }


    #If AIAHTTPURL01 or CDPHTTPURL01 was not specified but is needed, populate these now
    if (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('AIAHTTPURL01')) -and $param.UseHTTPAIA))
    {
        $param.AIAHTTPURL01 = ('http://' + $caDNSName + '/aia')
        $param.AIAHTTPURL02 = ''
    }

    if (($param.CaType -like 'StandAlone*') -and (!($role.Properties.ContainsKey('CDPHTTPURL01')) -and $param.UseHTTPCRL))
    {
        $param.CDPHTTPURL01 = ('http://' + $caDNSName + '/cdp')
        $param.CDPHTTPURL02 = ''
    }






    #If Enterprise CA, and UseLDAPAia is "Yes" or not specified, set UseLDAPAIA to True
    if (($param.CaType -like 'Enterprise*') -and (!($role.Properties.ContainsKey('UseLDAPAIA'))))
    {
        $param.UseLDAPAIA = $True
    }


    #If Enterprise CA, and UseLDAPCrl is "Yes" or not specified, set UseLDAPCrl to True
    if (($param.CaType -like 'Enterprise*') -and (!($role.Properties.ContainsKey('UseLDAPCRL'))))
    {
        $param.UseLDAPCRL = $True
    }

    #If AIAHTTPURL01 or CDPHTTPURL01 was not specified but is needed, populate these now (with empty strings)
    if (($param.CaType -like 'Enterprise*') -and (!($role.Properties.ContainsKey('AIAHTTPURL01'))))
    {
        if ($param.UseHTTPAIA)
        {
            $param.AIAHTTPURL01 = 'http://' + $caDNSName + '/aia'
            $param.AIAHTTPURL02 = ''
        }
        else
        {
            $param.AIAHTTPURL01 = ''
            $param.AIAHTTPURL02 = ''
        }
    }

    if (($param.CaType -like 'Enterprise*') -and (!($role.Properties.ContainsKey('CDPHTTPURL01'))))
    {
        if ($param.UseHTTPCRL)
        {
            $param.CDPHTTPURL01 = 'http://' + $caDNSName + '/cdp'
            $param.CDPHTTPURL02 = ''
        }
        else
        {
            $param.CDPHTTPURL01 = ''
            $param.CDPHTTPURL02 = ''
        }
    }


    function Scale-Parameters
    {
        param ([int]$hours)

        $factorYears = 24 * 365
        $factorMonths = 24 * (365/12)
        $factorWeeks = 24 * 7
        $factorDays = 24
        switch ($hours)
        {
            { $_ -ge $factorYears }
            {
                if (($hours / $factorYears) * 100%100 -le 10) { return ([string][int]($hours / $factorYears)), 'Years' }
            }
            { $_ -ge $factorMonths }
            {
                if (($hours / $factorMonths) * 100%100 -le 10) { return ([string][int]($hours / $factorMonths)), 'Months' }
            }
            { $_ -ge $factorWeeks }
            {
                if (($hours / $factorWeeks) * 100%100 -le 50) { return ([string][int]($hours / $factorWeeks)), 'Weeks' }
            }
            { $_ -ge $factorDays }
            {
                if (($hours / $factorDays) * 100%100 -le 75) { return ([string][int]($hours / $factorDays)), 'Days' }
            }
        }
        $returnHours = [int]($hours)
        if ($returnHours -lt 1) { $returnHours = 1 }
        return ([string]$returnHours), 'Hours'
    }

    #if any validity parameter was not defined, calculate these now
    if ($param.CRLPeriodUnits -eq '<auto>') { $param.CRLPeriodUnits, $param.CRLPeriod = Scale-Parameters ($validityPeriodUnitsHours/8) }
    if ($param.CRLDeltaPeriodUnits -eq '<auto>') { $param.CRLDeltaPeriodUnits, $param.CRLDeltaPeriod = Scale-Parameters ($validityPeriodUnitsHours/16) }
    if ($param.CRLOverlapUnits -eq '<auto>') { $param.CRLOverlapUnits, $param.CRLOverlapPeriod = Scale-Parameters ($validityPeriodUnitsHours/32) }
    if ($param.CertsValidityPeriodUnits -eq '<auto>')
    {
        $param.CertsValidityPeriodUnits, $param.CertsValidityPeriod = Scale-Parameters ($validityPeriodUnitsHours/2)
    }

    $role = $machine.Roles | Where-Object { ([AutomatedLab.Roles]$_.Name -band $roles) -ne 0 }
    if (($param.CAType -like '*root*') -and !($role.Properties.ContainsKey('CertsValidityPeriodUnits')))
    {
        if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message "Adding parameter 'CertsValidityPeriodUnits' with value of '$($param.CertsValidityPeriodUnits)' to machine roles properties of machine $($machine.Name)" -Type Warning }
        $role.Properties.Add('CertsValidityPeriodUnits', $param.CertsValidityPeriodUnits)
    }
    if (($param.CAType -like '*root*') -and !($role.Properties.ContainsKey('CertsValidityPeriod')))
    {
        if ($VerbosePreference -ne 'SilentlyContinue') { Write-ScreenInfo -Message "Adding parameter 'CertsValidityPeriod' with value of '$($param.CertsValidityPeriod)' to machine roles properties of machine $($machine.Name)" -Type Warning }
        $role.Properties.Add('CertsValidityPeriod', $param.CertsValidityPeriod)
    }

    #If any HTTP parameter is specified and any of the DNS names in these parameters points to this CA server, install Web Role to host this
    if (!($param.InstallWebRole))
    {
        if (($param.UseHTTPAIA -or $param.UseHTTPCRL) -and `
        $param.AIAHTTPURL01 -or $param.AIAHTTPURL02 -or $param.CDPHTTPURL01 -or $param.CDPHTTPURL02)
        {
            $URLs = @()
            $ErrorActionPreferenceBackup = $ErrorActionPreference
            $ErrorActionPreference = 'SilentlyContinue'
            if ($param.AIAHTTPURL01.IndexOf('/', 2)) { $URLs += ($param.AIAHTTPURL01).Split('/')[2].Split('/')[0] }
            if ($param.AIAHTTPURL02.IndexOf('/', 2)) { $URLs += ($param.AIAHTTPURL02).Split('/')[2].Split('/')[0] }
            if ($param.CDPHTTPURL01.IndexOf('/', 2)) { $URLs += ($param.CDPHTTPURL01).Split('/')[2].Split('/')[0] }
            if ($param.CDPHTTPURL02.IndexOf('/', 2)) { $URLs += ($param.CDPHTTPURL02).Split('/')[2].Split('/')[0] }
            $ErrorActionPreference = $ErrorActionPreferenceBackup

            #$param.InstallWebRole = (($machine.Name + "." + $machine.domainname) -in $URLs)
            if (($machine.Name + '.' + $machine.domainname) -notin $URLs)
            {
                Write-ScreenInfo -Message 'Http based AIA or CDP specified but is NOT pointing to this server. Make sure to MANUALLY establish this web server and DNS name as well as copy AIA and CRL(s) to this web server' -Type Warning
            }
        }
    }


    #Setting DatabaseDirectoryh and LogDirectory to blank if automatic is selected. Hence, default locations will be used (%WINDIR%\System32\CertLog)
    if ($param.DatabaseDirectory -eq '<auto>') { $param.DatabaseDirectory = '' }
    if ($param.LogDirectory -eq '<auto>') { $param.LogDirectory = '' }


    #Test for existence of AIA location
    if (!($param.UseLDAPAia) -and !($param.UseHTTPAia)) { Write-ScreenInfo -Message 'AIA information will not be included in issued certificates because both LDAP and HTTP based AIA has been disabled' -Type Warning }

    #Test for existence of CDP location
    if (!($param.UseLDAPCrl) -and !($param.UseHTTPCrl)) { Write-ScreenInfo -Message 'CRL information will not be included in issued certificates because both LDAP and HTTP based CRLs has been disabled' -Type Warning }


    if (!($param.InstallWebRole) -and ($param.InstallWebEnrollment))
    {
        Write-Error -Message "InstallWebRole is set to No while InstallWebEnrollment is set to Yes. This is not possible. `
            Specified value for InstallWebRole is: $($param.InstallWebRole) `
        Specified value for InstallWebEnrollment is: $($param.InstallWebEnrollment)"

        return
    }



    if ('<auto>' -eq $param.DoNotLoadDefaultTemplates)
    {
        #Only for Root CA server
        if ($param.CaType -like '*Root*')
        {
            if (Get-LabVM -Role CaSubordinate -ErrorAction SilentlyContinue)
            {
                Write-ScreenInfo -Message 'Default templates will be removed (not published) except "SubCA" template, since this is an Enterprise Root CA and Subordinate CA(s) is present in the lab' -Type Verbose
                $param.DoNotLoadDefaultTemplates = $True
            }
            else
            {
                $param.DoNotLoadDefaultTemplates = $False
            }
        }
        else
        {
            $param.DoNotLoadDefaultTemplates = $False
        }
    }
    #endregion ----- Calculations -----


    $job = @()
    $targets = (Get-LabVM -Role FirstChildDC).Name
    foreach ($target in $targets)
    {
        $job += Sync-LabActiveDirectory -ComputerName $target -AsJob -PassThru
    }
    Wait-LWLabJob -Job $job -Timeout 15 -NoDisplay
    $targets = (Get-LabVM -Role DC).Name
    foreach ($target in $targets)
    {
        $job += Sync-LabActiveDirectory -ComputerName $target -AsJob -PassThru
    }
    Wait-LWLabJob -Job $job -Timeout 15 -NoDisplay

    $param.PreDelaySeconds = $PreDelaySeconds

    Write-PSFMessage -Message "Starting install of $($param.CaType) role on machine '$($machine.Name)'"
    $job = Install-LWLabCAServers @param
    if ($PassThru)
    {
        $job
    }

    Write-LogFunctionExit
}


function Install-LabFailoverStorage
{
    [CmdletBinding()]
    param
    ( )

    $storageNodes = Get-LabVM -Role FailoverStorage -ErrorAction SilentlyContinue
    $failoverNodes = Get-LabVM -Role FailoverNode -ErrorAction SilentlyContinue
    if ($storageNodes.Count -gt 1)
    {
        foreach ($failoverNode in $failoverNodes)
        {
            $role = $failoverNode.Roles | Where-Object Name -eq 'FailoverNode'
            if (-not $role.Properties.ContainsKey('StorageTarget'))
            {
                Write-Error "There are $($storageNodes.Count) VMs with the 'FailoverStorage' role and the storage target is not defined for '$failoverNode'. Please define the property 'StorageTarget' with the 'FailoverStorage' role." -ErrorAction Stop
            }
        }
    }
    Start-LabVM -ComputerName (Get-LabVM -Role FailoverStorage, FailoverNode) -Wait
    
    $clusters = @{}
    $storageMapping = @{}
    
    foreach ($failoverNode in $failoverNodes) {
    
        $role = $failoverNode.Roles | Where-Object Name -eq 'FailoverNode'
        $name = $role.Properties['ClusterName']
        $storageMapping."$($failoverNode.Name)" = if ($role.Properties.ContainsKey('StorageTarget'))
        {
            $role.Properties['StorageTarget']
        }
        else
        {
            $storageNodes.Name
        }

        if (-not $name)
        {
            $name = 'ALCluster'
        }
    
        if (-not $clusters.ContainsKey($name))
        {
            $clusters[$name] = @()
        }
        $clusters[$name] += $failoverNode.Name
    }
    
    foreach ($cluster in $clusters.Clone().GetEnumerator())
    {
        $machines = $cluster.Value
        $clusterName = $cluster.Key
        $initiatorIds = Invoke-LabCommand -ActivityName 'Retrieving IQNs' -ComputerName $machines -ScriptBlock {
            Set-Service -Name MSiSCSI -StartupType Automatic
            Start-Service -Name MSiSCSI
            if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
            {
                "IQN:$((Get-CimInstance -Namespace root\wmi -Class MSiSCSIInitiator_MethodClass).iSCSINodeName)"
            }
            else
            {
                "IQN:$((Get-WmiObject -Namespace root\wmi -Class MSiSCSIInitiator_MethodClass).iSCSINodeName)"
            }
        } -PassThru -ErrorAction Stop
    
        $clusters[$clusterName] = $initiatorIds
    }
    
    Install-LabWindowsFeature -ComputerName $storageNodes -FeatureName FS-iSCSITarget-Server

    foreach ($storageNode in $storageNodes)
    {
        foreach ($disk in $storageNode.Disks)
        {
            Write-ScreenInfo "Working on $($disk.name)"
            #$lunDrive = $role.Properties['LunDrive'][0] # Select drive letter only
            $driveLetter = $disk.DriveLetter

            Invoke-LabCommand -ActivityName "Creating iSCSI target for $($disk.name) on '$storageNode'" -ComputerName $storageNode -ScriptBlock {
                # assign drive letter if not provided
                if (-not $driveLetter)
                {
                    # http://vcloud-lab.com/entries/windows-2016-server-r2/find-next-available-free-drive-letter-using-powershell-
                    #$driveLetter = (68..90 | % {$L = [char]$_; if ((gdr).Name -notContains $L) {$L}})[0]
                    $driveLetter = $env:SystemDrive[0]
                }

                $driveInfo = [System.IO.DriveInfo] [string] $driveLetter

                if (-not (Test-Path $driveInfo))
                {
                    $offlineDisk = Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Select-Object -First 1
                    if ($offlineDisk)
                    {
                        $offlineDisk | Set-Disk -IsOffline $false
                        $offlineDisk | Set-Disk -IsReadOnly $false
                    }

                    if (-not ($offlineDisk | Get-Partition | Get-Volume))
                    {
                        $offlineDisk | New-Volume -FriendlyName $disk -FileSystem ReFS -DriveLetter $driveLetter
                    }
                }

                $folderPath = Join-Path -Path $driveInfo -ChildPath $disk.Name
                $folder = New-Item -ItemType Directory -Path $folderPath -ErrorAction SilentlyContinue
                $folder = Get-Item -Path $folderPath -ErrorAction Stop

                foreach ($clu in $clusters.GetEnumerator())
                {
                    if (-not (Get-IscsiServerTarget -TargetName $clu.Key -ErrorAction SilentlyContinue))
                    {
                        New-IscsiServerTarget -TargetName $clu.Key -InitiatorIds $clu.Value
                    }
                    $diskTarget = (Join-Path -Path $folder.FullName -ChildPath "$($disk.name).vhdx")
                    $diskSize = [uint64]$disk.DiskSize*1GB
                    if (-not (Get-IscsiVirtualDisk -Path $diskTarget -ErrorAction SilentlyContinue))
                    {
                        New-IscsiVirtualDisk -Path $diskTarget -Size $diskSize
                    }
                    Add-IscsiVirtualDiskTargetMapping -TargetName $clu.Key -Path $diskTarget
                }
            } -Variable (Get-Variable -Name clusters, disk, driveletter) -ErrorAction Stop

            Invoke-LabCommand -ActivityName "Connecting iSCSI target - storage node '$storageNode' - disk '$disk'" -ComputerName (Get-LabVM -Role FailoverNode) -ScriptBlock {
                $targetAddress = $storageMapping[$env:COMPUTERNAME]
                if (-not (Get-Command New-IscsiTargetPortal -ErrorAction SilentlyContinue))
                {
                    iscsicli.exe QAddTargetPortal $targetAddress
                    $target = ((iscsicli.exe ListTargets) -match 'iqn.+target')[0].Trim()
                    iscsicli.exe QLoginTarget $target
                }
                else
                {
                    New-IscsiTargetPortal -TargetPortalAddress $targetAddress
                    Get-IscsiTarget | Where-Object {-not $_.IsConnected} | Connect-IscsiTarget -IsPersistent $true
                }
            } -Variable (Get-Variable storageMapping) -ErrorAction Stop
        }
    }
}


function Install-LabFileServers
{
    
    [cmdletBinding()]
    param ([switch]$CreateCheckPoints)

    Write-LogFunctionEntry

    $roleName = [AutomatedLab.Roles]::FileServer

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM | Where-Object { $roleName -in $_.Roles.Name }
    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no machine with the role '$roleName'" -Type Warning
        Write-LogFunctionExit
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 30

    Write-ScreenInfo -Message 'Waiting for File Server role to complete installation' -NoNewLine

    $windowsFeatures = 'FileAndStorage-Services', 'File-Services ', 'FS-FileServer', 'FS-DFS-Namespace', 'FS-Resource-Manager', 'Print-Services', 'NET-Framework-Features', 'NET-Framework-45-Core'
    $remainingMachines = $machines | Where-Object {
        Get-LabWindowsFeature -ComputerName $_ -FeatureName $windowsFeatures -NoDisplay | Where-Object -Property Installed -eq $false
    }

    if ($remainingMachines.Count -eq 0)
    {
        Write-ScreenInfo -Message "...done."
        Write-ScreenInfo -Message "All file servers are already installed."
        return
    }
    
    $jobs = @()
    $jobs += Install-LabWindowsFeature -ComputerName $remainingMachines -FeatureName $windowsFeatures -IncludeManagementTools -AsJob -PassThru -NoDisplay

    Start-LabVM -StartNextMachines 1 -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 30 -NoDisplay
    
    Write-ScreenInfo -Message "Restarting $roleName machines..." -NoNewLine
    Restart-LabVM -ComputerName $remainingMachines -Wait -NoNewLine
    Write-ScreenInfo -Message done.

    if ($CreateCheckPoints)
    {
        Checkpoint-LabVM -ComputerName $remainingMachines -SnapshotName "Post '$roleName' Installation"
    }

    Write-LogFunctionExit
}


function Install-LabOrchestrator2012
{
    [cmdletBinding()]
    param ()

    Write-LogFunctionEntry

    #region prepare setup script
    function Install-LabPrivateOrchestratorRole
    {
        param (
            [Parameter(Mandatory)]
            [string]$OrchServiceUser,

            [Parameter(Mandatory)]
            [string]$OrchServiceUserPassword,

            [Parameter(Mandatory)]
            [string]$SqlServer,

            [Parameter(Mandatory)]
            [string]$SqlDbName
        )

        Write-Verbose -Message 'Installing Orchestrator'

        $start = Get-Date

        if (-not ((Get-WindowsFeature -Name NET-Framework-Features).Installed))
        {
            Write-Error "The WindowsFeature 'NET-Framework-Features' must be installed prior of installing Orchestrator. Use the cmdlet 'Install-LabWindowsFeature' to install the missing feature."
            return
        }

        $TimeoutInMinutes = 15
        $productName = 'Orchestrator 2012'
        $installProcessName = 'Setup'
        $installProcessDescription = 'Orchestrator Setup'
        $drive = (Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType = 5').DeviceID
        $computerDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name
        $cmd = "$drive\Setup\Setup.exe /Silent /ServiceUserName:$computerDomain\$OrchServiceUser /ServicePassword:$OrchServiceUserPassword /Components:All /DbServer:$SqlServer /DbNameNew:$SqlDbName /WebServicePort:81 /WebConsolePort:82 /OrchestratorRemote /SendCEIPReports:0 /EnableErrorReporting:never /UseMicrosoftUpdate:0"

        Write-Verbose 'Logs can be found here: C:\Users\<UserName>\AppData\Local\Microsoft System Center 2012\Orchestrator\Logs'

        #--------------------------------------------------------------------------------------

        Write-Verbose "Starting setup of '$productName' with the following command"
        Write-Verbose "`t$cmd"
        Write-Verbose "The timeout is $timeoutInMinutes minutes"

        Invoke-Expression -Command $cmd
        Start-Sleep -Milliseconds 500

        $timeout = Get-Date

        $queryExpression = "`$_.Name -eq '$installProcessName'"
        if ($installProcessDescription)
        {
            $queryExpression += "-and `$_.Description -eq '$installProcessDescription'"
        }
        $queryExpression = [scriptblock]::Create($queryExpression)

        Write-Verbose 'Query expression for looking for the setup process:'
        Write-Verbose "`t$queryExpression"

        if (-not (Get-Process | Where-Object $queryExpression))
        {
            Write-Error "Installation of '$productName' did not start"
            return
        }
        else
        {
            $p = Get-Process | Where-Object $queryExpression
            Write-Verbose "Installation process is '$($p.Name)' with ID $($p.Id)"
        }

        while (Get-Process | Where-Object $queryExpression)
        {
            if ((Get-Date).AddMinutes(- $TimeoutInMinutes) -gt $start)
            {
                Write-Error "Installation of '$productName' hit the timeout of 30 minutes. Killing the setup process"

                if ($installProcessDescription)
                {
                    Get-Process |
                    Where-Object  { $_.Name -eq $installProcessName -and $_.Description -eq 'Orchestrator Setup' } |
                    Stop-Process -Force
                }
                else
                {
                    Get-Process -Name $installProcessName | Stop-Process -Force
                }

                Write-Error "Installation of $productName was not successfull"
                return
            }

            Start-Sleep -Seconds 10
        }

        $end = Get-Date
        Write-Verbose "Installation finished in $($end - $start)"
    }
    #endregion

    $roleName = [AutomatedLab.Roles]::Orchestrator2012

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role $roleName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message "There is no machine with the role $roleName"
        return
    }

    $isoImage = $Script:data.Sources.ISOs | Where-Object { $_.Name -eq $roleName }
    if (-not $isoImage)
    {
        Write-LogFunctionExitWithError -Message "There is no ISO image available to install the role '$roleName'. Please add the required ISO to the lab and name it '$roleName'"
        return
    }

    Start-LabVM -RoleName $roleName -Wait

    Install-LabWindowsFeature -ComputerName $machines -FeatureName RSAT, NET-Framework-Core -Verbose:$false

    Mount-LabIsoImage -ComputerName $machines -IsoPath $isoImage.Path -SupressOutput

    foreach ($machine in $machines)
    {
        $role = $machine.Roles | Where-Object { $_.Name -eq $roleName }

        $createUserScript = "
            `$user = New-ADUser -Name $($role.Properties.ServiceAccount) -AccountPassword ('$($role.Properties.ServiceAccountPassword)' | ConvertTo-SecureString -AsPlainText -Force) -Description 'Orchestrator Service Account' -Enabled `$true -PassThru
            Get-ADGroup -Identity 'Domain Admins' | Add-ADGroupMember -Members `$user
        Get-ADGroup -Identity 'Administrators' | Add-ADGroupMember -Members `$user"


        $dc = Get-LabVM -All | Where-Object {
            $_.DomainName -eq $machine.DomainName -and
            $_.Roles.Name -in @([AutomatedLab.Roles]::DC, [AutomatedLab.Roles]::FirstChildDC, [AutomatedLab.Roles]::RootDC)
        } | Get-Random

        Write-PSFMessage "Domain controller for installation is '$($dc.Name)'"

        Invoke-LabCommand -ComputerName $dc -ScriptBlock ([scriptblock]::Create($createUserScript)) -ActivityName CreateOrchestratorServiceAccount -NoDisplay

        Invoke-LabCommand -ComputerName $machine -ActivityName Orchestrator2012Installation -NoDisplay -ScriptBlock (Get-Command Install-LabPrivateOrchestratorRole).ScriptBlock `
        -ArgumentList $Role.Properties.ServiceAccount, $Role.Properties.ServiceAccountPassword, $Role.Properties.DatabaseServer, $Role.Properties.DatabaseName
    }

    Dismount-LabIsoImage -ComputerName $machines -SupressOutput

    Write-LogFunctionExit
}


function Install-LabSharePoint
{
    [CmdletBinding()]
    param
    (
        [switch]
        $CreateCheckPoints
    )

    Write-LogFunctionEntry

    $lab = Get-Lab

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role SharePoint2013, SharePoint2016, SharePoint2019
    $versionGroups = $machines | Group-Object { $_.Roles.Name | Where-Object { $_ -match 'SharePoint\d{4}' } }

    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no SharePoint server in the lab" -Type Warning
        Write-LogFunctionExit
        return
    }

    foreach ($group in $versionGroups)
    {
        if ($null -eq ($lab.Sources.ISOs | Where-Object { $_.Name -eq $group.Name }))
        {
            Write-ScreenInfo -Message "No ISO was added for $($Group.Name). Please use Add-LabIsoImageDefinition to add it before installing a lab."
            return
        }
    }

    Write-ScreenInfo -Message 'Waiting for machines with SharePoint role to start up' -NoNewline
    Start-LabVM -ComputerName $machines -Wait -ProgressIndicator 15

    # Mount OS ISO for Windows Feature Installation
    Write-ScreenInfo -Message 'Installing required features'
    Install-LabWindowsFeature -ComputerName $machines -FeatureName Net-Framework-Features, Web-Server, Web-WebServer, Web-Common-Http, Web-Static-Content, Web-Default-Doc, Web-Dir-Browsing, Web-Http-Errors, Web-App-Dev, Web-Asp-Net, Web-Net-Ext, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Health, Web-Http-Logging, Web-Log-Libraries, Web-Request-Monitor, Web-Http-Tracing, Web-Security, Web-Basic-Auth, Web-Windows-Auth, Web-Filtering, Web-Digest-Auth, Web-Performance, Web-Stat-Compression, Web-Dyn-Compression, Web-Mgmt-Tools, Web-Mgmt-Console, Web-Mgmt-Compat, Web-Metabase, WAS, WAS-Process-Model, WAS-NET-Environment, WAS-Config-APIs, Web-Lgcy-Scripting, Windows-Identity-Foundation, Server-Media-Foundation, Xps-Viewer -IncludeAllSubFeature -IncludeManagementTools -NoDisplay

    $oldMachines = $machines | Where-Object { $_.OperatingSystem.Version -lt 10 }
    if ($Null -ne $oldMachines)
    {
        # Application Server is deprecated in 2016+, despite the SharePoint documentation stating otherwise
        Install-LabWindowsFeature -ComputerName $oldMachines -FeatureName Application-Server, AS-Web-Support, AS-TCP-Port-Sharing, AS-WAS-Support, AS-HTTP-Activation, AS-TCP-Activation, AS-Named-Pipes, AS-Net-Framework -IncludeManagementTools -IncludeAllSubFeature -NoDisplay
    }

    Restart-LabVM -ComputerName $machines -Wait

    # Mount SharePoint ISO
    Dismount-LabIsoImage -ComputerName $machines -SupressOutput

    $jobs = foreach ($group in $versionGroups)
    {
        foreach ($machine in $group.Group)
        {
            $spImage = Mount-LabIsoImage -ComputerName $machine -IsoPath ($lab.Sources.ISOs | Where-Object { $_.Name -eq $group.Name }).Path -PassThru
            Invoke-LabCommand -ComputerName $machine -ActivityName "Copy SharePoint Installation Files" -ScriptBlock {
                Copy-Item -Path "$($spImage.DriveLetter)\" -Destination "C:\SPInstall\" -Recurse
                if ((Test-Path -Path 'C:\SPInstall\prerequisiteinstallerfiles') -eq $false)
                {
                    $null = New-Item -Path 'C:\SPInstall\prerequisiteinstallerfiles' -ItemType Directory
                }
            } -Variable (Get-Variable -Name spImage) -AsJob -PassThru
        }
    }

    Wait-LWLabJob -Job $jobs -NoDisplay

    foreach ($thing in @('cppredist32_2012', 'cppredist64_2012', 'cppredist32_2015', 'cppredist64_2015', 'cppredist32_2017', 'cppredist64_2017'))
    {
        $fName = $thing -replace '(cppredist)(\d\d)_(\d{4})', 'vcredist_$2_$3.exe'
        Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name $thing) -Path $labsources\SoftwarePackages -FileName $fName -NoDisplay
    }

    Copy-LabFileItem -Path $labsources\SoftwarePackages\vcredist_64_2012.exe, $labsources\SoftwarePackages\vcredist_64_2015.exe, $labsources\SoftwarePackages\vcredist_64_2017.exe -ComputerName $machines.Name  -DestinationFolderPath "C:\SPInstall\prerequisiteinstallerfiles"

    # Download and copy Prerequisite Files to server
    Write-ScreenInfo -Message "Downloading and copying prerequisite files to servers"
    foreach ($group in $versionGroups)
    {
        if ($lab.DefaultVirtualizationEngine -eq 'HyperV' -and -not (Test-Path -Path $labsources\SoftwarePackages\$($group.Name)))
        {
            $null = New-Item -ItemType Directory -Path $labsources\SoftwarePackages\$($group.Name)
        }

        foreach ($prereqUri in (Get-LabConfigurationItem -Name "$($group.Name)Prerequisites"))
        {
            $internalUri = New-Object System.Uri($prereqUri)
            $fileName = $internalUri.Segments[$internalUri.Segments.Count - 1]

            $params = @{
                Uri      = $prereqUri
                Path     = "$labsources\SoftwarePackages\$($group.Name)\$($fileName)"
                PassThru = $true
            }

            if ($prereqUri -match '1CAA41C7' -and $group.Name -eq 'SharePoint2013')
            {
                # This little snowflake would like both packages, pretty please
                $params.FileName = 'WcfDataServices56.exe'
            }

            $download = Get-LabInternetFile @params
        }

        Copy-LabFileItem -ComputerName $group.Group -Path $labsources\SoftwarePackages\$($group.Name)\* -DestinationFolderPath "C:\SPInstall\prerequisiteinstallerfiles"

        # Installing Prereqs
        Write-ScreenInfo -Message "Installing prerequisite files for $($group.Name) on server" -Type Verbose
        Invoke-LabCommand -ComputerName $group.Group -NoDisplay -ScriptBlock {
            param ([string] $Script )
            if (-not (Test-Path -Path C:\DeployDebug))
            {
                $null = New-Item -ItemType Directory -Path C:\DeployDebug
            }
            Set-Content C:\DeployDebug\SPPrereq.ps1 -Value $Script

        } -ArgumentList (Get-Variable -Name "$($Group.Name)InstallScript").Value.ToString()
    }

    $instResult = Invoke-LabCommand -PassThru -ComputerName $machines -ActivityName "Install SharePoint (all) Prerequisites" -ScriptBlock { & C:\DeployDebug\SPPrereq.ps1 -Mode '/unattended' }
    $failed = $instResult | Where-Object { $_.ExitCode -notin 0, 3010 }
    if ($failed)
    {
        Write-ScreenInfo -Type Error -Message "The following SharePoint servers failed installing prerequisites $($failed.PSComputerName)"
        return
    }

    $rebootRequired = $instResult | Where-Object { $_.ExitCode -eq 3010 }
    while ($rebootRequired)
    {
        Write-ScreenInfo -Type Verbose -Message "Some machines require a second pass at installing prerequisites: $($rebootRequired.HostName -join ',')"
        Restart-LabVM -ComputerName $rebootRequired.HostName -Wait
        $instResult = Invoke-LabCommand -PassThru -ComputerName $rebootRequired.HostName -ActivityName "Install $($group.Name) Prerequisites" -ScriptBlock { & C:\DeployDebug\SPPrereq.ps1 -Mode '/unattended /continue' } | Where-Object { $_ -eq 3010 }
        $failed = $instResult | Where-Object { $_.ExitCode -notin 0, 3010 }
        if ($failed)
        {
            Write-ScreenInfo -Type Error -Message "The following SharePoint servers failed installing prerequisites $($failed.HostName)"
        }

        $rebootRequired = $instResult | Where-Object { $_.ExitCode -eq 3010 }
    }

    # Install SharePoint binaries
    Write-ScreenInfo -Message "Installing SharePoint binaries on server"
    Restart-LabVM -ComputerName $machines -Wait

    $jobs = foreach ($group in $versionGroups)
    {
        $productKey = Get-LabConfigurationItem -Name "$($group.Name)Key"
        $configFile = $spsetupConfigFileContent -f $productKey
        Invoke-LabCommand -ComputerName $group.Group -ActivityName "Install SharePoint $($group.Name)" -ScriptBlock {
            Set-Content -Force -Path C:\SPInstall\files\al-config.xml -Value $configFile
            $null = Start-Process -Wait "C:\SPInstall\setup.exe" -ArgumentList "/config C:\SPInstall\files\al-config.xml"
            Set-Content C:\DeployDebug\SPInst.cmd -Value 'C:\SPInstall\setup.exe /config C:\SPInstall\files\al-config.xml'
            Get-ChildItem -Path (Join-Path ([IO.Path]::GetTempPath()) 'SharePoint Server Setup*') | Get-Content
        } -Variable (Get-Variable -Name configFile) -AsJob -PassThru
    }

    Write-ScreenInfo -Message "Waiting for SharePoint role to complete installation" -NoNewLine
    Wait-LWLabJob -Job $jobs -NoDisplay

    foreach ($job in $jobs)
    {
        $jobResult = (Receive-Job -Job $job -Wait -AutoRemoveJob)
        Write-ScreenInfo -Type Verbose -Message "Installation result $jobResult"
    }
}


function Install-LabTeamFoundationServer
{
    [CmdletBinding()]
    param
    ( )

    $tfsMachines = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object SkipDeployment -eq $false | Sort-Object { ($_.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps').Name } -Descending
    if (-not $tfsMachines) { return }

    # Assign unassigned build workers to our most current TFS machine
    Get-LabVM -Role TfsBuildWorker | Where-Object {
        -not ($_.Roles | Where-Object Name -eq TfsBuildWorker).Properties.ContainsKey('TfsServer')
    } | ForEach-Object {
        ($_.Roles | Where-Object Name -eq TfsBuildWorker).Properties.Add('TfsServer', $tfsMachines[0].Name)
    }

    $jobs = Install-LabWindowsFeature -ComputerName $tfsMachines -FeatureName Web-Mgmt-Tools -AsJob
    Write-ScreenInfo -Message 'Waiting for installation of IIS web admin tools to complete' -NoNewline
    Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -Timeout $InstallationTimeout -NoDisplay

    $installationJobs = @()
    $count = 0
    foreach ($machine in $tfsMachines)
    {
        if (Get-LabIssuingCA)
        {
            Write-ScreenInfo -Type Verbose -Message "Found CA in lab, requesting certificate"
            $cert = Request-LabCertificate -Subject "CN=$machine" -TemplateName WebServer -SAN $machine.AzureConnectionInfo.DnsName, $machine.FQDN, $machine.Name -ComputerName $machine -PassThru -ErrorAction Stop
            $machine.InternalNotes.Add('CertificateThumbprint', $cert.Thumbprint)
            Export-Lab
        }

        $role = $machine.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps'
        [string]$sqlServer = switch -Regex ($role.Name)
        {
            'Tfs2015' { Get-LabVM -Role SQLServer2014 | Select-Object -First 1 }
            'Tfs2017' { Get-LabVM -Role SQLServer2014, SQLServer2016 | Select-Object -First 1 }
            'Tfs2018|AzDevOps' { Get-LabVM -Role SQLServer2017, SQLServer2019 | Select-Object -First 1 }
            default { throw 'No fitting SQL Server found in lab!' }
        }
        
        if (-not $sqlServer)
        {
            Write-Error 'No fitting SQL Server found in lab for TFS / Azure DevOps role.' -ErrorAction Stop
        }

        $initialCollection = 'AutomatedLab'
        $tfsPort = 8080
        $databaseLabel = "TFS$count" # Increment database label in case we deploy multiple TFS
        [string]$machineName = $machine
        $count++

        if ($role.Properties.ContainsKey('InitialCollection'))
        {
            $initialCollection = $role.Properties['InitialCollection']
        }

        if ($role.Properties.ContainsKey('Port'))
        {
            $tfsPort = $role.Properties['Port']
        }

        if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
        {
            if (-not (Get-LabAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $machine))
            {
                (Get-Lab).AzureSettings.LoadBalancerPortCounter++
                $remotePort = (Get-Lab).AzureSettings.LoadBalancerPortCounter
                Add-LWAzureLoadBalancedPort -ComputerName $machine -DestinationPort $tfsPort -Port $remotePort
            }

            if ($role.Properties.ContainsKey('Port'))
            {
                $machine.Roles.Where( { $_.Name -match 'Tfs\d{4}|AzDevOps' }).ForEach( { $_.Properties['Port'] = $tfsPort })
            }
            else
            {
                $machine.Roles.Where( { $_.Name -match 'Tfs\d{4}|AzDevOps' }).ForEach( { $_.Properties.Add('Port', $tfsPort) })
            }

            Export-Lab # Export lab again since we changed role properties
        }

        if ($role.Properties.ContainsKey('DbServer'))
        {
            [string]$sqlServer = Get-LabVM -ComputerName $role.Properties['DbServer'] -ErrorAction SilentlyContinue

            if (-not $sqlServer)
            {
                Write-ScreenInfo -Message "No SQL server called $($role.Properties['DbServer']) found in lab." -NoNewLine -Type Warning
                [string]$sqlServer = Get-LabVM -Role SQLServer2016, SQLServer2017, SQLServer2019 | Select-Object -First 1
                Write-ScreenInfo -Message " Selecting $sqlServer instead." -Type Warning
            }
        }

        if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
        {
            # For good luck, disable the firewall again - in case Invoke-AzVmRunCommand failed to do its job.
            Invoke-LabCommand -ComputerName $machine, $sqlServer -NoDisplay -ScriptBlock { Set-NetFirewallProfile -All -Enabled False -PolicyStore PersistentStore }
        }

        Restart-LabVM -ComputerName $machine -Wait -NoDisplay

        $installationJobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock {
            $tfsConfigPath = (Get-ChildItem -Path $env:ProgramFiles -Filter tfsconfig.exe -Recurse | Select-Object -First 1).FullName
            if (-not $tfsConfigPath) { throw 'tfsconfig.exe could not be found.' }

            if (-not (Test-Path C:\DeployDebug))
            {
                [void] (New-Item -Path C:\DeployDebug -ItemType Directory)
            }

            # Create unattend file with fitting parameters and replace all we can find
            [void] (Start-Process -FilePath $tfsConfigPath -ArgumentList 'unattend /create /type:Standard /unattendfile:C:\DeployDebug\TfsConfig.ini' -NoNewWindow -Wait)

            $config = (Get-Item -Path C:\DeployDebug\TfsConfig.ini -ErrorAction Stop).FullName
            $content = [System.IO.File]::ReadAllText($config)

            $content = $content -replace 'SqlInstance=.+', ('SqlInstance={0}' -f $sqlServer)
            $content = $content -replace 'DatabaseLabel=.+', ('DatabaseLabel={0}' -f $databaseLabel)
            $content = $content -replace 'UrlHostNameAlias=.+', ('UrlHostNameAlias={0}' -f $machineName)

            if ($cert.Thumbprint)
            {
                $content = $content -replace 'SiteBindings=.+', ('SiteBindings=https:*:{0}::My:{1}' -f $tfsPort, $cert.Thumbprint)
                $content = $content -replace 'PublicUrl=.+', ('PublicUrl=https://{0}:{1}' -f $machineName, $tfsPort)
            }
            else
            {
                $content = $content -replace 'SiteBindings=.+', ('SiteBindings=http:*:{0}:' -f $tfsPort)
                $content = $content -replace 'PublicUrl=.+', ('PublicUrl=http://{0}:{1}' -f $machineName, $tfsPort)
            }

            if ($cert.ThumbPrint -and $tfsConfigPath -match '14\.0')
            {
                Get-WebBinding -Name 'Team Foundation Server' | Remove-WebBinding
                New-WebBinding -Protocol https -Port $tfsPort -IPAddress * -Name 'Team Foundation Server'
                $binding = Get-Website -Name 'Team Foundation Server' | Get-WebBinding
                $binding.AddSslCertificate($cert.Thumbprint, "my")
            }

            $content = $content -replace 'webSiteVDirName=.+', 'webSiteVDirName='
            $content = $content -replace 'CollectionName=.+', ('CollectionName={0}' -f $initialCollection)
            $content = $content -replace 'CollectionDescription=.+', 'CollectionDescription=Built by AutomatedLab, your friendly lab automation solution'
            $content = $content -replace 'WebSitePort=.+', ('WebSitePort={0}' -f $tfsPort) # Plain TFS 2015
            $content = $content -replace 'UrlHostNameAlias=.+', ('UrlHostNameAlias={0}' -f $machineName) # Plain TFS 2015

            [System.IO.File]::WriteAllText($config, $content)

            $command = "unattend /unattendfile:`"$config`" /continue"
            "`"$tfsConfigPath`" $command" | Set-Content C:\DeployDebug\SetupTfsServer.cmd
            $configurationProcess = Start-Process -FilePath $tfsConfigPath -ArgumentList $command -PassThru -NoNewWindow -Wait

            # Locate log files and cat them
            $log = Get-ChildItem -Path "$env:LOCALAPPDATA\Temp" -Filter dd_*_server_??????????????.log | Sort-Object -Property CreationTime | Select-Object -Last 1
            $log | Get-Content

            if ($configurationProcess.ExitCode -ne 0)
            {
                throw ('Something went wrong while applying the unattended configuration {0}. Try {1} {2} manually. Read the log at {3}.' -f $config, $tfsConfigPath, $command, $log.FullName )
            }
        } -Variable (Get-Variable sqlServer, machineName, InitialCollection, tfsPort, databaseLabel, cert -ErrorAction SilentlyContinue) -AsJob -ActivityName "TFS_Setup_$machine" -PassThru -NoDisplay
    }

    Write-ScreenInfo -Type Verbose -Message "Waiting for the installation of TFS on $tfsMachines to finish."

    Wait-LWLabJob -Job $installationJobs

    foreach ($job in $installationJobs)
    {
        $name = $job.Name.Replace('TFS_Setup_','')
        $type = if ($job.State -eq 'Completed') { 'Verbose' } else { 'Error' }
        $resultVariable = New-Variable -Name ("AL_TFSServer_$($name)_$([guid]::NewGuid().Guid)") -Scope Global -PassThru
        Write-ScreenInfo -Type $type -Message "TFS Deployment $($job.State.ToLower()) on '$($name)'. The job output of $job can be retrieved with `${$($resultVariable.Name)}"
        $resultVariable.Value = $job | Receive-Job -AutoRemoveJob -Wait
    }
}


function Install-LabWebServers
{
    [cmdletBinding()]
    param ([switch]$CreateCheckPoints)

    Write-LogFunctionEntry

    $roleName = [AutomatedLab.Roles]::WebServer

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM | Where-Object { $roleName -in $_.Roles.Name }
    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no machine with the role '$roleName'" -Type Warning
        Write-LogFunctionExit
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 30

    Write-ScreenInfo -Message 'Waiting for Web Server role to complete installation' -NoNewLine

    $coreMachines    = $machines | Where-Object { $_.OperatingSystem.Installation -match 'Core' }
    $nonCoreMachines = $machines | Where-Object { $_.OperatingSystem.Installation -notmatch 'Core' }

    $jobs = @()
    if ($coreMachines)    { $jobs += Install-LabWindowsFeature -ComputerName $coreMachines    -AsJob -PassThru -NoDisplay -IncludeAllSubFeature -FeatureName Web-WebServer, Web-Application-Proxy, Web-Health, Web-Performance, Web-Security, Web-App-Dev, Web-Ftp-Server, Web-Metabase, Web-Lgcy-Scripting, Web-WMI, Web-Scripting-Tools, Web-Mgmt-Service, Web-WHC }
    if ($nonCoreMachines) { $jobs += Install-LabWindowsFeature -ComputerName $nonCoreMachines -AsJob -PassThru -NoDisplay -IncludeAllSubFeature -FeatureName Web-Server }

    Start-LabVm -StartNextMachines 1 -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 30 -NoDisplay

    if ($CreateCheckPoints)
    {
        Checkpoint-LabVM -ComputerName $machines -SnapshotName 'Post Web Installation'
    }

    Write-LogFunctionExit
}


function Install-ScvmmConsole
{
    [CmdletBinding()]
    param
    (
        [AutomatedLab.Machine[]]
        $Computer
    )

    foreach ($vm in $Computer)
    {
        $iniConsole = $iniContentConsoleScvmm.Clone()
        $role = $vm.Roles | Where-Object Name -in Scvmm2016, Scvmm2019, Scvmm2022
        if ($role.Properties -and [Convert]::ToBoolean($role.Properties['SkipServer']))
        {
            foreach ($property in $role.Properties.GetEnumerator())
            {
                if (-not $iniConsole.ContainsKey($property.Key)) { continue }
                $iniConsole[$property.Key] = $property.Value
            }
            $iniConsole.ProgramFiles = $iniConsole.ProgramFiles -f $role.Name.ToString().Substring(5)

            $scvmmIso = Mount-LabIsoImage -ComputerName $vm -IsoPath ($lab.Sources.ISOs | Where-Object { $_.Name -eq $role.Name }).Path -SupressOutput -PassThru

            Invoke-LabCommand -ComputerName $vm -Variable (Get-Variable iniConsole, scvmmIso) -ActivityName 'Extracting SCVMM Console' -ScriptBlock {
                $setup = Get-ChildItem -Path $scvmmIso.DriveLetter -Filter *.exe | Select-Object -First 1
                Start-Process -FilePath $setup.FullName -ArgumentList '/VERYSILENT', '/DIR=C:\SCVMM' -Wait
                '[OPTIONS]' | Set-Content C:\Console.ini
                $iniConsole.GetEnumerator() | ForEach-Object { "$($_.Key) = $($_.Value)" | Add-Content C:\Console.ini }
                "cd C:\SCVMM; C:\SCVMM\setup.exe /client /i /f C:\Console.ini /IACCEPTSCEULA" | Set-Content C:\DeployDebug\VmmSetup.cmd
                Set-Location -Path C:\SCVMM
            }

            Install-LabSoftwarePackage -ComputerName $vm -WorkingDirectory C:\SCVMM -LocalPath C:\SCVMM\setup.exe -CommandLine '/client /i /f C:\Console.ini /IACCEPTSCEULA' -AsJob -PassThru -UseShellExecute -Timeout 20
            Dismount-LabIsoImage -ComputerName $vm -SupressOutput
        }
    }
}


function Install-ScvmmServer
{
    [CmdletBinding()]
    param
    (
        [AutomatedLab.Machine[]]
        $Computer
    )

    $sqlcmd = Get-LabConfigurationItem -Name SqlCommandLineUtils
    $adk = Get-LabConfigurationItem -Name WindowsAdk
    $adkpe = Get-LabConfigurationItem -Name WindowsAdkPe
    $odbc = Get-LabConfigurationItem -Name SqlOdbc13
    $cpp64 = Get-LabConfigurationItem -Name cppredist64_2012
    $cpp32 = Get-LabConfigurationItem -Name cppredist32_2012
    $cpp1464 = Get-LabConfigurationItem -Name cppredist64_2015
    $cpp1432 = Get-LabConfigurationItem -Name cppredist32_2015
    $sqlFile = Get-LabInternetFile -Uri $sqlcmd -Path $labsources\SoftwarePackages -FileName sqlcmd.msi -PassThru
    $odbcFile = Get-LabInternetFile -Uri $odbc -Path $labsources\SoftwarePackages -FileName odbc.msi -PassThru
    $adkFile = Get-LabInternetFile -Uri $adk -Path $labsources\SoftwarePackages -FileName adk.exe -PassThru
    $adkpeFile = Get-LabInternetFile -Uri $adkpe -Path $labsources\SoftwarePackages -FileName adkpe.exe -PassThru
    $cpp64File = Get-LabInternetFile -uri $cpp64 -Path $labsources\SoftwarePackages -FileName vcredist_64_2012.exe -PassThru
    $cpp32File = Get-LabInternetFile -uri $cpp32 -Path $labsources\SoftwarePackages -FileName vcredist_32_2012.exe -PassThru
    $cpp1464File = Get-LabInternetFile -uri $cpp1464 -Path $labsources\SoftwarePackages -FileName vcredist_64_2015.exe -PassThru
    $cpp1432File = Get-LabInternetFile -uri $cpp1432 -Path $labsources\SoftwarePackages -FileName vcredist_32_2015.exe -PassThru
    Install-LabSoftwarePackage -Path $odbcFile.FullName -ComputerName $Computer -CommandLine '/QN ADDLOCAL=ALL IACCEPTMSODBCSQLLICENSETERMS=YES /L*v C:\odbc.log'
    Install-LabSoftwarePackage -Path $sqlFile.FullName -ComputerName $Computer -CommandLine '/QN IACCEPTMSSQLCMDLNUTILSLICENSETERMS=YES /L*v C:\sqlcmd.log'
    Install-LabSoftwarePackage -path $cpp64File.FullName -ComputerName $Computer -CommandLine '/quiet /norestart /log C:\DeployDebug\cpp64_2012.log'
    Install-LabSoftwarePackage -path $cpp32File.FullName -ComputerName $Computer -CommandLine '/quiet /norestart /log C:\DeployDebug\cpp32_2012.log'
    Install-LabSoftwarePackage -path $cpp1464File.FullName -ComputerName $Computer -CommandLine '/quiet /norestart /log C:\DeployDebug\cpp64_2015.log'
    Install-LabSoftwarePackage -path $cpp1432File.FullName -ComputerName $Computer -CommandLine '/quiet /norestart /log C:\DeployDebug\cpp32_2015.log'

    if ($(Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -or (Test-LabMachineInternetConnectivity -ComputerName $Computer[0]))
    {
        Install-LabSoftwarePackage -Path $adkFile.FullName -ComputerName $Computer -CommandLine '/quiet /layout c:\ADKoffline'
        Install-LabSoftwarePackage -Path $adkpeFile.FullName -ComputerName $Computer -CommandLine '/quiet /layout c:\ADKPEoffline'
    }
    else
    {
        Start-Process -FilePath $adkFile.FullName -ArgumentList "/quiet /layout $(Join-Path (Get-LabSourcesLocation -Local) SoftwarePackages/ADKoffline)" -Wait -NoNewWindow
        Start-Process -FilePath $adkpeFile.FullName -ArgumentList " /quiet /layout $(Join-Path (Get-LabSourcesLocation -Local) SoftwarePackages/ADKPEoffline)" -Wait -NoNewWindow
        Copy-LabFileItem -Path (Join-Path (Get-LabSourcesLocation -Local) SoftwarePackages/ADKoffline) -ComputerName $Computer
        Copy-LabFileItem -Path (Join-Path (Get-LabSourcesLocation -Local) SoftwarePackages/ADKPEoffline) -ComputerName $Computer
    }

    Install-LabSoftwarePackage -LocalPath C:\ADKOffline\adksetup.exe -ComputerName $Computer -CommandLine '/quiet /installpath C:\ADK'
    Install-LabSoftwarePackage -LocalPath C:\ADKPEOffline\adkwinpesetup.exe -ComputerName $Computer -CommandLine '/quiet /installpath C:\ADK'
    Install-LabWindowsFeature -ComputerName $Computer -FeatureName RSAT-Clustering -IncludeAllSubFeature
    Restart-LabVM -ComputerName $Computer -Wait

    # Server, includes console
    $jobs = foreach ($vm in $Computer)
    {
        $iniServer = $iniContentServerScvmm.Clone()
        $role = $vm.Roles | Where-Object Name -in Scvmm2016, Scvmm2019, Scvmm2022

        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniServer.ContainsKey($property.Key)) { continue }
            $iniServer[$property.Key] = $property.Value
        }

        if ($role.Properties.ContainsKey('ProductKey'))
        {
            $iniServer['ProductKey'] = $role.Properties['ProductKey']
        }

        $iniServer['ProgramFiles'] = $iniServer['ProgramFiles'] -f $role.Name.ToString().Substring(5)
        if ($iniServer['SqlMachineName'] -eq 'REPLACE' -and $role.Name -eq 'Scvmm2016')
        {
            $iniServer['SqlMachineName'] = Get-LabVM -Role SQLServer2012, SQLServer2014, SQLServer2016 | Select-Object -First 1 -ExpandProperty Fqdn
        }

        if ($iniServer['SqlMachineName'] -eq 'REPLACE' -and $role.Name -eq 'Scvmm2019')
        {
            $iniServer['SqlMachineName'] = Get-LabVM -Role SQLServer2016, SQLServer2017 | Select-Object -First 1 -ExpandProperty Fqdn
        }

        if ($iniServer['SqlMachineName'] -eq 'REPLACE' -and $role.Name -eq 'Scvmm2022')
        {
            $iniServer['SqlMachineName'] = Get-LabVM -Role SQLServer2016, SQLServer2017, SQLServer2019, SQLServer2022 | Select-Object -First 1 -ExpandProperty Fqdn
        }

        Invoke-LabCommand -ComputerName (Get-LabVM -Role ADDS | Select-Object -First 1) -ScriptBlock {
            param ($OUName)
            if ($OUName -match 'CN=')
            {
                $path = ($OUName -split ',')[1..999] -join ','
                $name = ($OUName -split ',')[0] -replace 'CN='
            }
            else
            {
                $path = (Get-ADDomain).SystemsContainer
                $name = $OUName
            }

            try
            {
                $ouExists = Get-ADObject -Identity "CN=$($name),$path" -ErrorAction Stop
            }
            catch { }
            if (-not $ouExists) { New-ADObject -Name $name -Path $path -Type Container -ProtectedFromAccidentalDeletion $true }
        } -ArgumentList $iniServer.TopContainerName

        $scvmmIso = Mount-LabIsoImage -ComputerName $vm -IsoPath ($lab.Sources.ISOs | Where-Object { $_.Name -eq $role.Name }).Path -SupressOutput -PassThru
        $domainCredential = $vm.GetCredential((Get-Lab))
        $commandLine = $setupCommandLineServerScvmm -f $vm.DomainName, $domainCredential.UserName.Replace("$($vm.DomainName)\", ''), $domainCredential.GetNetworkCredential().Password

        Invoke-LabCommand -ComputerName $vm -Variable (Get-Variable iniServer, scvmmIso, commandLine) -ActivityName 'Extracting SCVMM Server' -ScriptBlock {
            $setup = Get-ChildItem -Path $scvmmIso.DriveLetter -Filter *.exe | Select-Object -First 1
            Start-Process -FilePath $setup.FullName -ArgumentList '/VERYSILENT', '/DIR=C:\SCVMM' -Wait
            '[OPTIONS]' | Set-Content C:\Server.ini
            $iniServer.GetEnumerator() | ForEach-Object { "$($_.Key) = $($_.Value)" | Add-Content C:\Server.ini }
            "cd C:\SCVMM; C:\SCVMM\setup.exe $commandline" | Set-Content C:\DeployDebug\VmmSetup.cmd
            Set-Location -Path C:\SCVMM
        }
        Install-LabSoftwarePackage -ComputerName $vm -WorkingDirectory C:\SCVMM -LocalPath C:\SCVMM\setup.exe -CommandLine $commandLine -AsJob -PassThru -UseShellExecute -Timeout 20
     }

    if ($jobs) { Wait-LWLabJob -Job $jobs }

    # Jobs seem to end prematurely...
    Remove-LabPSSession
    Dismount-LabIsoImage -ComputerName (Get-LabVm -Role SCVMM) -SupressOutput
    Invoke-LabCommand -ComputerName (Get-LabVm -Role SCVMM) -ScriptBlock {        
        $installer = Get-Process -Name Setup,SetupVM -ErrorAction SilentlyContinue
        if ($installer)
        {
            $installer.WaitForExit((New-TimeSpan -Minutes 20).TotalMilliseconds)
        }

        robocopy (Join-Path -Path $env:ProgramData VMMLogs) "C:\DeployDebug\VMMLogs" /S /E
    }

    # Onboard Hyper-V servers
    foreach ($vm in $Computer)
    {
        $role = $vm.Roles | Where-Object Name -in Scvmm2016, Scvmm2019, Scvmm2022

        if ($role.Properties.ContainsKey('ConnectHyperVRoleVms') -or $role.Properties.ContainsKey('ConnectClusters'))
        {
            $vmNames = $role.Properties['ConnectHyperVRoleVms'] -split '\s*(?:,|;)\s*'
            $clusterNames = $role.Properties['ConnectClusters'] -split '\s*(?:,|;)\s*'
            $hyperVisors = (Get-LabVm -Role HyperV -Filter { $_.Name -in $vmNames }).FQDN
            $clusters = Get-LabVm | foreach { $_.Roles | Where Name -eq FailoverNode }
            [string[]] $clusterNameProperties = $clusters.Foreach({ $_.Properties['ClusterName'] }) | Select-Object -Unique
            if ($clusters.Where({ -not $_.Properties.ContainsKey('ClusterName') }))
            {
                $clusterNameProperties += 'ALCluster'
            }

            $clusterNameProperties = $clusterNameProperties.Where({ $_ -in $clusterNames })

            $joinCred = $vm.GetCredential((Get-Lab))
            Invoke-LabCommand -ComputerName $vm -ActivityName "Registering Hypervisors with $vm" -ScriptBlock {
                $module = Get-Item "C:\Program Files\Microsoft System Center\Virtual Machine Manager *\bin\psModules\virtualmachinemanager\virtualmachinemanager.psd1"
                Import-Module -Name $module.FullName
                
                foreach ($vmHost in $hyperVisors)
                {
                    $null = Add-SCVMHost -ComputerName $vmHost -Credential $joinCred -VmmServer $vm.FQDN -ErrorAction SilentlyContinue
                }

                foreach ($cluster in $clusterNameProperties)
                {
                    Add-SCVMHostCluster -Name $cluster -Credential $joinCred -VmmServer $vm.FQDN -ErrorAction SilentlyContinue
                }
            } -Variable (Get-Variable hyperVisors, joinCred, vm, clusterNameProperties)
        }
    }
}


function Install-VisualStudio2013
{
    [cmdletBinding()]
    param (
        [int]$InstallationTimeout = (Get-LabConfigurationItem -Name Timeout_VisualStudio2013Installation)
    )

    Write-LogFunctionEntry

    $roleName = [AutomatedLab.Roles]::VisualStudio2013

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first' -Type Warning
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role $roleName | Where-Object HostType -eq 'HyperV'

    if (-not $machines)
    {
        return
    }

    $isoImage = $Script:data.Sources.ISOs | Where-Object Name -eq $roleName
    if (-not $isoImage)
    {
        Write-LogFunctionExitWithError -Message "There is no ISO image available to install the role '$roleName'. Please add the required ISO to the lab and name it '$roleName'"
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    $jobs = @()

    Mount-LabIsoImage -ComputerName $machines -IsoPath $isoImage.Path -SupressOutput

    foreach ($machine in $machines)
    {
        $parameters = @{ }
        $parameters.Add('ComputerName', $machine.Name)
        $parameters.Add('ActivityName', 'InstallationVisualStudio2013')
        $parameters.Add('Verbose', $VerbosePreference)
        $parameters.Add('Scriptblock', {
                Write-Verbose 'Installing Visual Studio 2013'

                Push-Location
                Set-Location -Path (Get-WmiObject -Class Win32_CDRomDrive).Drive
                $exe = Get-ChildItem -Filter *.exe
                if ($exe.Count -gt 1)
                {
                    Write-Error 'More than one executable found, cannot proceed. Make sure you have defined the correct ISO image'
                    return
                }
                Write-Verbose "Calling '$($exe.FullName) /quiet /norestart /noweb /Log c:\VsInstall.log'"
                Invoke-Expression -Command "$($exe.FullName) /quiet /norestart /noweb /Log c:\VsInstall.log"
                Pop-Location

                Write-Verbose 'Waiting 120 seconds'
                Start-Sleep -Seconds 120

                $installationStart = Get-Date
                $installationTimeoutInMinutes = 120
                $installationFinished = $false

                Write-Verbose "Looping until '*Exit code: 0x<digits>, restarting: No' is detected in the VsInstall.log..."
                while (-not $installationFinished)
                {
                    if ((Get-Content -Path C:\VsInstall.log | Select-Object -Last 1) -match '(?<Text1>Exit code: 0x)(?<ReturnCode>\w*)(?<Text2>, restarting: No$)')
                    {
                        $installationFinished = $true
                        Write-Verbose 'Visual Studio installation finished'
                    }
                    else
                    {
                        Write-Verbose 'Waiting for the Visual Studio installation...'
                    }

                    if ($installationStart.AddMinutes($installationTimeoutInMinutes) -lt (Get-Date))
                    {
                        Write-Error "The installation of Visual Studio did not finish within the timeout of $installationTimeoutInMinutes minutes"
                        break
                    }

                    Start-Sleep -Seconds 5
                }
                $matches.ReturnCode
                Write-Verbose '...Installation seems to be done'
            }
        )

        $jobs += Invoke-LabCommand @parameters -AsJob -PassThru -NoDisplay
    }

    Write-ScreenInfo -Message 'Waiting for Visual Studio 2013 to complete installation' -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 60 -Timeout $InstallationTimeout -NoDisplay

    foreach ($job in $jobs)
    {
        $result = Receive-Job -Job $job
        if ($result -ne 0)
        {
            $ipAddress = (Get-Job -Id $job.id).Location
            $machineName = (Get-LabVM | Where-Object {$_.IpV4Address -eq $ipAddress}).Name
            Write-ScreenInfo -Type Warning "Installation generated error or warning for machine '$machineName'. Return code is: $result"
        }
    }

    Dismount-LabIsoImage -ComputerName $machines -SupressOutput

    Write-LogFunctionExit
}


function Install-VisualStudio2015
{
    [cmdletBinding()]
    param (
        [int]$InstallationTimeout = (Get-LabConfigurationItem -Name Timeout_VisualStudio2015Installation)
    )

    Write-LogFunctionEntry

    $roleName = [AutomatedLab.Roles]::VisualStudio2015

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first' -Type Warning
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role $roleName | Where-Object HostType -eq 'HyperV'

    if (-not $machines)
    {
        return
    }

    $isoImage = $Script:data.Sources.ISOs | Where-Object Name -eq $roleName
    if (-not $isoImage)
    {
        Write-LogFunctionExitWithError -Message "There is no ISO image available to install the role '$roleName'. Please add the required ISO to the lab and name it '$roleName'"
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    $jobs = @()

    Mount-LabIsoImage -ComputerName $machines -IsoPath $isoImage.Path -SupressOutput

    foreach ($machine in $machines)
    {
        $parameters = @{ }
        $parameters.Add('ComputerName', $machine.Name)
        $parameters.Add('ActivityName', 'InstallationVisualStudio2015')
        $parameters.Add('Verbose', $VerbosePreference)
        $parameters.Add('Scriptblock', {
                Write-Verbose 'Installing Visual Studio 2015'

                Push-Location
                Set-Location -Path (Get-WmiObject -Class Win32_CDRomDrive).Drive
                $exe = Get-ChildItem -Filter *.exe
                if ($exe.Count -gt 1)
                {
                    Write-Error 'More than one executable found, cannot proceed. Make sure you have defined the correct ISO image'
                    return
                }
                Write-Verbose "Calling '$($exe.FullName) /quiet /norestart /noweb /Log c:\VsInstall.log'"
                $cmd = [scriptblock]::Create("$($exe.FullName) /quiet /norestart /noweb /Log c:\VsInstall.log")
                #there is something that does not work when invoked remotely. Hence a scheduled task is used to work around that.
                Register-ScheduledJob -ScriptBlock $cmd -Name VS2015Installation -RunNow | Out-Null

                Pop-Location

                Write-Verbose 'Waiting 120 seconds'
                Start-Sleep -Seconds 120

                $installationStart = Get-Date
                $installationTimeoutInMinutes = 120
                $installationFinished = $false

                Write-Verbose "Looping until '*Exit code: 0x<hex code>, restarting: No' is detected in the VsInstall.log..."
                while (-not $installationFinished)
                {
                    if ((Get-Content -Path C:\VsInstall.log | Select-Object -Last 1) -match '(?<Text1>Exit code: 0x)(?<ReturnCode>\w*)(?<Text2>, restarting: No$)')
                    {
                        $installationFinished = $true
                        Write-Verbose 'Visual Studio installation finished'
                    }
                    else
                    {
                        Write-Verbose 'Waiting for the Visual Studio installation...'
                    }

                    if ($installationStart.AddMinutes($installationTimeoutInMinutes) -lt (Get-Date))
                    {
                        Write-Error "The installation of Visual Studio did not finish within the timeout of $installationTimeoutInMinutes minutes"
                        break
                    }

                    Start-Sleep -Seconds 5
                }
                $matches.ReturnCode
                Write-Verbose '...Installation seems to be done'
            }
        )

        $jobs += Invoke-LabCommand @parameters -AsJob -PassThru -NoDisplay
    }

    Write-ScreenInfo -Message 'Waiting for Visual Studio 2015 to complete installation' -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 60 -Timeout $InstallationTimeout -NoDisplay

    foreach ($job in $jobs)
    {
        $result = Receive-Job -Job $job -Keep
        if ($result -notin '0', 'bc2') #0 == success, 0xbc2 == sucess but required reboot
        {
            $ipAddress = (Get-Job -Id $job.id).Location
            $machineName = (Get-LabVM | Where-Object {$_.IpV4Address -eq $ipAddress}).Name
            Write-ScreenInfo -Type Warning "Installation generated error or warning for machine '$machineName'. Return code is: $result"
        }
    }

    Dismount-LabIsoImage -ComputerName $machines -SupressOutput

    Restart-LabVM -ComputerName $machines

    Write-LogFunctionExit
}


function Mount-LabDiskImage
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ImagePath,

        [ValidateSet('ISO','VHD','VHDSet','VHDx','Unknown')]
        $StorageType,

        [switch]
        $PassThru
    )

    if (Get-Command -Name Mount-DiskImage -ErrorAction SilentlyContinue)
    {
        $diskImage = Mount-DiskImage -ImagePath $ImagePath -StorageType $StorageType -PassThru

        if ($PassThru.IsPresent)
        {
            $diskImage | Add-Member -MemberType NoteProperty -Name DriveLetter -Value ($diskImage | Get-Volume).DriveLetter -PassThru
        }
    }
    elseif ($IsLinux)
    {
        if (-not (Test-Path -Path /mnt/automatedlab))
        {
            $null = New-Item -Path /mnt/automatedlab -Force -ItemType Directory
        }

        $image = Get-Item -Path $ImagePath
        $null = mount -o loop $ImagePath /mnt/automatedlab/$($image.BaseName)
        [PSCustomObject]@{
            ImagePath   = $ImagePath
            FileSize    = $image.Length
            Size        = $image.Length
            DriveLetter = "/mnt/automatedlab/$($image.BaseName)"
        }
    }
    else
    {
        throw 'Neither Mount-DiskImage exists, nor is this a Linux system.'
    }
}


function Move-LabDomainController
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$ComputerName,

        [Parameter(Mandatory)]
        [string]$SiteName
    )

    Write-LogFunctionEntry


    $dcRole = (Get-LabVM -ComputerName $ComputerName).Roles | Where-Object Name -like '*DC'

    if (-not $dcRole)
    {
        Write-PSFMessage "No Domain Controller roles found on computer '$ComputerName'"
        return
    }

    $machine = Get-LabVM -ComputerName $ComputerName
    $lab = Get-Lab

    $forest = if ($lab.IsRootDomain($machine.DomainName))
    {
        $machine.DomainName
    }
    else
    {
        $lab.GetParentDomain($machine.DomainName)
    }

    Write-PSFMessage -Message "Try to find domain root machine for '$ComputerName'"
    $domainRootMachine = Get-LabVM -Role RootDC | Where-Object DomainName -eq $forest
    if (-not $domainRootMachine)
    {
        Write-PSFMessage -Message "No RootDC found in same domain as '$ComputerName'. Looking for FirstChildDC instead"

        $domainRootMachine = Get-LabVM -Role FirstChildDC | Where-Object DomainName -eq $machine.DomainName
    }
    
    $null = Invoke-LabCommand -ComputerName $domainRootMachine -NoDisplay -PassThru -ScriptBlock `
    {
        param
        (
            $ComputerName, $SiteName
        )

        $searchBase = (Get-ADRootDSE).ConfigurationNamingContext

        Write-Verbose -Message "Moving computer '$ComputerName' to AD site '$SiteName'"
        $targetSite = Get-ADObject -Filter 'ObjectClass -eq "site" -and CN -eq $SiteName' -SearchBase $searchBase
        Write-Verbose -Message "Target site: '$targetSite'"
        $dc =  Get-ADObject -Filter "ObjectClass -eq 'server' -and Name -eq '$ComputerName'" -SearchBase $searchBase
        Write-Verbose -Message "DC distinguished name: '$dc'"
        Move-ADObject -Identity $dc -TargetPath "CN=Servers,$($TargetSite.DistinguishedName)"

    } -ArgumentList $ComputerName, $SiteName

    Write-LogFunctionExit
}


function New-LabADSite
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$ComputerName,

        [Parameter(Mandatory)]
        [string]$SiteName,

        [Parameter(Mandatory)]
        [string]$SiteSubnet
    )

    Write-LogFunctionEntry

    $lab = Get-Lab
    $machine = Get-LabVM -ComputerName $ComputerName
    $dcRole = $machine.Roles | Where-Object Name -like '*DC'

    if (-not $dcRole)
    {
        Write-PSFMessage "No Domain Controller roles found on computer '$Computer'"
        return
    }

    Write-PSFMessage -Message "Try to find domain root machine for '$ComputerName'"
    $rootDc = Get-LabVM -Role RootDC | Where-Object DomainName -eq $machine.DomainName
    if (-not $rootDc)
    {
        Write-PSFMessage -Message "No RootDC found in same domain as '$ComputerName'. Looking for FirstChildDC instead"

        $domain = $lab.Domains | Where-Object Name -eq $machine.DomainName
        if (-not $lab.IsRootDomain($domain))
        {
            $parentDomain = $lab.GetParentDomain($domain)
            $rootDc = Get-LabVM -Role RootDC | Where-Object DomainName -eq $parentDomain
        }
    }

    $createSiteCmd = {
        param
        (
            $ComputerName, $SiteName, $SiteSubnet
        )

        $PSDefaultParameterValues = @{
            '*-AD*:Server' = $env:COMPUTERNAME
        }

        Write-Verbose -Message "For computer '$ComputerName', create AD site '$SiteName' in subnet '$SiteSubnet'"

        if (-not (Get-ADReplicationSite -Filter "Name -eq '$SiteName'"))
        {
            Write-Verbose -Message "SiteName '$SiteName' does not exist. Attempting to create it now"
            New-ADReplicationSite -Name $SiteName
        }
        else
        {
            Write-Verbose -Message "SiteName '$SiteName' already exists"
        }

        $SiteSubnet = $SiteSubnet -split ',|;'
        foreach ($sn in $SiteSubnet)
        {
            $sn = $sn.Trim()
            if (-not (Get-ADReplicationSubNet -Filter "Name -eq '$sn'"))
            {
                Write-Verbose -Message "SiteSubnet does not exist. Attempting to create it now and associate it with site '$SiteName'"
                New-ADReplicationSubnet -Name $sn -Site $SiteName -Location $SiteName
            }
            else
            {
                Write-Verbose -Message "SiteSubnet '$sn' already exists"
            }
        }

        $sites = (Get-ADReplicationSite -Filter 'Name -ne "Default-First-Site-Name"').Name
        foreach ($site in $sites)
        {
            $otherSites = $sites | Where-Object { $_ -ne $site }
            foreach ($otherSite in $otherSites)
            {
                if (-not (Get-ADReplicationSiteLink -Filter "(name -eq '[$site]-[$otherSite]')") -and -not
                (Get-ADReplicationSiteLink -Filter "(name -eq '[$otherSite]-[$Site]')"))
                {
                    Write-Verbose -Message "Site link '[$site]-[$otherSite]' does not exist. Creating it now"
                    New-ADReplicationSiteLink -Name "[$site]-[$otherSite]" `
                    -SitesIncluded $site, $otherSite `
                    -Cost 100 `
                    -ReplicationFrequencyInMinutes 15 `
                    -InterSiteTransportProtocol IP `
                    -OtherAttributes @{ 'options' = 5 }
                }
            }
        }
    }

    try
    {
        $null = Invoke-LabCommand -ComputerName $rootDc -NoDisplay -PassThru -ScriptBlock $createSiteCmd `
        -ArgumentList $ComputerName, $SiteName, $SiteSubnet -ErrorAction Stop
    }
    catch {
        Restart-LabVM -ComputerName $ComputerName -Wait
        Wait-LabADReady -ComputerName $ComputerName
        
        Invoke-LabCommand -ComputerName $rootDc -NoDisplay -PassThru -ScriptBlock $createSiteCmd `
        -ArgumentList $ComputerName, $SiteName, $SiteSubnet -ErrorAction Stop
    }

    Write-LogFunctionExit
}


function New-LabAzureCertificate
{
    [CmdletBinding()]
    param ()
    throw New-Object System.NotImplementedException
    Write-LogFunctionEntry

    Update-LabAzureSettings

    $certSubject = "CN=$($Script:lab.Name).cloudapp.net"
    $service = Get-LabAzureDefaultResourceGroup
    $cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object Subject -eq $certSubject -ErrorAction SilentlyContinue

    if (-not $cert)
    {
        $temp = [System.IO.Path]::GetTempFileName()

        #not required as SSL is not used yet
        #& 'C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\makecert.exe' -r -pe -n $certSubject -b 01/01/2000 -e 01/01/2036 -eku 1.3.6.1.5.5.7.3.1, 1.3.6.1.5.5.7.3.2 -ss my -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 $temp

        certutil.exe -addstore -f Root $temp | Out-Null

        Remove-Item -Path $temp

        $cert = Get-ChildItem Cert:\LocalMachine\Root | Where-Object Subject -eq $certSubject
    }

    #not required as SSL is not used yet
    #$service | Add-AzureCertificate -CertToDeploy (Get-Item -Path "Cert:\LocalMachine\Root\$($cert.Thumbprint)")
}


function New-LabNetworkSwitches
{
    [cmdletBinding()]
    param ()

    Write-LogFunctionEntry

    $Script:data = Get-Lab
    if (-not $Script:data)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $vmwareNetworks = $data.VirtualNetworks | Where-Object HostType -eq VMWare
    if ($vmwareNetworks)
    {
        foreach ($vmwareNetwork in $vmwareNetworks)
        {
            $network = Get-LWVMWareNetworkSwitch -VirtualNetwork $vmwareNetwork
            if (-not $vmwareNetworks)
            {
                throw "The networks '$($vmwareNetwork.Name)' does not exist and must be created before."
            }
            else
            {
                Write-PSFMessage "Network '$($vmwareNetwork.Name)' found"
            }
        }
    }

    Write-PSFMessage "Creating network switch '$($virtualNetwork.ResourceName)'..."

    $hypervNetworks = $data.VirtualNetworks | Where-Object HostType -eq HyperV
    if ($hypervNetworks)
    {
        New-LWHypervNetworkSwitch -VirtualNetwork $hypervNetworks
    }

    Write-PSFMessage 'done'

    Write-LogFunctionExit
}


function New-LabSourcesPath
{
    [CmdletBinding()]
    param
    (
        [string]
        $RelativePath,

        [Microsoft.Azure.Storage.File.CloudFileShare]
        $Share
    )

    $container = Split-Path -Path $RelativePath
    if (-not $container)
    {
        New-AzStorageDirectory -Share $Share -Path $RelativePath -ErrorAction SilentlyContinue
        return
    }

    if (-not (Get-AzStorageFile -Share $Share -Path $container -ErrorAction SilentlyContinue))
    {
        New-LabSourcesPath -RelativePath $container -Share $Share
        New-AzStorageDirectory -Share $Share -Path $container -ErrorAction SilentlyContinue
    }
}


function New-LabSqlAccount
{
    param
    (
        [Parameter(Mandatory = $true)]
        [AutomatedLab.Machine]
        $Machine,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $RoleProperties
    )

    $usersAndPasswords = @{}
    $groups = @()
    if ($RoleProperties.ContainsKey('SQLSvcAccount') -and $RoleProperties.ContainsKey('SQLSvcPassword'))
    {
        $usersAndPasswords[$RoleProperties['SQLSvcAccount']] = $RoleProperties['SQLSvcPassword']
    }

    if ($RoleProperties.ContainsKey('AgtSvcAccount') -and $RoleProperties.ContainsKey('AgtSvcPassword'))
    {
        $usersAndPasswords[$RoleProperties['AgtSvcAccount']] = $RoleProperties['AgtSvcPassword']
    }

    if ($RoleProperties.ContainsKey('RsSvcAccount') -and $RoleProperties.ContainsKey('RsSvcPassword'))
    {
        $usersAndPasswords[$RoleProperties['RsSvcAccount']] = $RoleProperties['RsSvcPassword']
    }

    if ($RoleProperties.ContainsKey('AsSvcAccount') -and $RoleProperties.ContainsKey('AsSvcPassword'))
    {
        $usersAndPasswords[$RoleProperties['AsSvcAccount']] = $RoleProperties['AsSvcPassword']
    }

    if ($RoleProperties.ContainsKey('IsSvcAccount') -and $RoleProperties.ContainsKey('IsSvcPassword'))
    {
        $usersAndPasswords[$RoleProperties['IsSvcAccount']] = $RoleProperties['IsSvcPassword']
    }

    if ($RoleProperties.ContainsKey('SqlSysAdminAccounts'))
    {
        $groups += $RoleProperties['SqlSysAdminAccounts']
    }

    if ($RoleProperties.ContainsKey('ConfigurationFile'))
    {        
        $confPath = if ($lab.DefaultVirtualizationEngine -eq 'Azure' -and (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $RoleProperties.ConfigurationFile))
        {
            $blob = Get-LabAzureLabSourcesContent -Path $RoleProperties.ConfigurationFile.Replace($labSources,'')
            $null = Get-AzStorageFileContent -File $blob -Destination (Join-Path $env:TEMP azsql.ini) -Force
            Join-Path $env:TEMP azsql.ini
        }
        elseif ($lab.DefaultVirtualizationEngine -ne 'Azure' -or ($lab.DefaultVirtualizationEngine -eq 'Azure' -and -not (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $RoleProperties.ConfigurationFile)))
        {
            $RoleProperties.ConfigurationFile
        }

        $config = (Get-Content -Path $confPath) -replace '\\', '\\' | ConvertFrom-String -Delimiter = -PropertyNames Key, Value

        if (($config | Where-Object Key -eq SQLSvcAccount) -and ($config | Where-Object Key -eq SQLSvcPassword))
        {
            $user = ($config | Where-Object Key -eq SQLSvcAccount).Value
            $password = ($config | Where-Object Key -eq SQLSvcPassword).Value
            $user = $user.Substring(1, $user.Length - 2)
            $password = $password.Substring(1, $password.Length - 2)
            $usersAndPasswords[$user] = $password
        }

        if (($config | Where-Object Key -eq AgtSvcAccount) -and ($config | Where-Object Key -eq AgtSvcPassword))
        {
            $user = ($config | Where-Object Key -eq AgtSvcAccount).Value
            $password = ($config | Where-Object Key -eq AgtSvcPassword).Value
            $user = $user.Substring(1, $user.Length - 2)
            $password = $password.Substring(1, $password.Length - 2)
            $usersAndPasswords[$user] = $password
        }

        if (($config | Where-Object Key -eq RsSvcAccount) -and ($config | Where-Object Key -eq RsSvcPassword))
        {
            $user = ($config | Where-Object Key -eq RsSvcAccount).Value
            $password = ($config | Where-Object Key -eq RsSvcPassword).Value
            $user = $user.Substring(1, $user.Length - 2)
            $password = $password.Substring(1, $password.Length - 2)
            $usersAndPasswords[$user] = $password
        }

        if (($config | Where-Object Key -eq AsSvcAccount) -and ($config | Where-Object Key -eq AsSvcPassword))
        {
            $user = ($config | Where-Object Key -eq AsSvcAccount).Value
            $password = ($config | Where-Object Key -eq AsSvcPassword).Value
            $user = $user.Substring(1, $user.Length - 2)
            $password = $password.Substring(1, $password.Length - 2)
            $usersAndPasswords[$user] = $password
        }

        if (($config | Where-Object Key -eq IsSvcAccount) -and ($config | Where-Object Key -eq IsSvcPassword))
        {
            $user = ($config | Where-Object Key -eq IsSvcAccount).Value
            $password = ($config | Where-Object Key -eq IsSvcPassword).Value
            $user = $user.Substring(1, $user.Length - 2)
            $password = $password.Substring(1, $password.Length - 2)
            $usersAndPasswords[$user] = $password
        }

        if (($config | Where-Object Key -eq SqlSysAdminAccounts))
        {
            $group = ($config | Where-Object Key -eq SqlSysAdminAccounts).Value
            $groups += $group.Substring(1, $group.Length - 2)
        }
    }

    foreach ($kvp in $usersAndPasswords.GetEnumerator())
    {
        $user = $kvp.Key

        if ($kvp.Key.Contains("\"))
        {
            $domain = ($kvp.Key -split "\\")[0]
            $user = ($kvp.Key -split "\\")[1]
        }

        if ($kvp.Key.Contains("@"))
        {
            $domain = ($kvp.Key -split "@")[1]
            $user = ($kvp.Key -split "@")[0]
        }

        $password = $kvp.Value

        if ($domain -match 'NT Authority|BUILTIN')
        {
            continue
        }

        if ($domain)
        {
            $dc = Get-LabVm -Role RootDC, FirstChildDC | Where-Object { $_.DomainName -eq $domain -or ($_.DomainName -split "\.")[0] -eq $domain }

            if (-not $dc)
            {
                Write-ScreenInfo -Message ('User {0} will not be created. No domain controller found for {1}' -f $user,$domain) -Type Warning
            }

            Invoke-LabCommand -ComputerName $dc -ActivityName ("Creating user '$user' in domain '$domain'") -ScriptBlock {
                $existingUser = $null #required as the session is not removed
                try
                {
                    $existingUser = Get-ADUser -Identity $user -Server localhost
                }
                catch { }

                if (-not ($existingUser))
                {
                    New-ADUser -SamAccountName $user -AccountPassword ($password | ConvertTo-SecureString -AsPlainText -Force) -Name $user -PasswordNeverExpires $true -CannotChangePassword $true -Enabled $true -Server localhost
                }
            } -Variable (Get-Variable -Name user, password)
        }
        else
        {
            Invoke-LabCommand -ComputerName $Machine -ActivityName ("Creating local user '$user'") -ScriptBlock {
                if (-not (Get-LocalUser $user -ErrorAction SilentlyContinue))
                {
                    New-LocalUser -Name $user -AccountNeverExpires -PasswordNeverExpires -UserMayNotChangePassword -Password ($password | ConvertTo-SecureString -AsPlainText -Force)
                }
            } -Variable (Get-Variable -Name user, password)
        }
    }

    foreach ($group in $groups)
    {
        if ($group.Contains("\"))
        {
            $domain = ($group -split "\\")[0]
            $groupName = ($group -split "\\")[1]
        }
        elseif ($group.Contains("@"))
        {
            $domain = ($group -split "@")[1]
            $groupName = ($group -split "@")[0]
        }
        else
        {
            $groupName = $group
        }

        if ($domain -match 'NT Authority|BUILTIN')
        {
            continue
        }

        if ($domain)
        {
            $dc = Get-LabVM -Role RootDC, FirstChildDC | Where-Object { $_.DomainName -eq $domain -or ($_.DomainName -split "\.")[0] -eq $domain }

            if (-not $dc)
            {
                Write-ScreenInfo -Message ('User {0} will not be created. No domain controller found for {1}' -f $user, $domain) -Type Warning
            }

            Invoke-LabCommand -ComputerName $dc -ActivityName ("Creating group '$groupName' in domain '$domain'") -ScriptBlock {
                $existingGroup = $null #required as the session is not removed
                try
                {
                    $existingGroup = Get-ADGroup -Identity $groupName -Server localhost
                }
                catch { }

                if (-not ($existingGroup))
                {
                    $newGroup = New-ADGroup -Name $groupName -GroupScope Global -Server localhost -PassThru
                    #adding the account the script is running under to the SQL admin group
                    $newGroup | Add-ADGroupMember -Members ([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)

                }
            } -Variable (Get-Variable -Name groupName)
        }
        else
        {
            Invoke-LabCommand $Machine -ActivityName "Creating local group '$groupName'" -ScriptBlock {
                if (-not (Get-LocalGroup -Name $groupName -ErrorAction SilentlyContinue))
                {
                    New-LocalGroup -Name $groupName -ErrorAction SilentlyContinue
                }
            } -Variable (Get-Variable -Name groupName)
        }
    }
}


function Publish-LabCAInstallCertificates
{

    param (
        [switch]$PassThru
    )

    #Install the certificates to all machines in lab

    Write-LogFunctionEntry

    $targetMachines = @()

    #Publish to all Root DC machines (only one DC from each Root domain)
    $targetMachines += Get-LabVM -All -IsRunning | Where-Object { ($_.Roles.Name -eq 'RootDC') -or ($_.Roles.Name -eq 'FirstChildDC') }

    #Also publish to any machines not domain joined
    $targetMachines += Get-LabVM -All -IsRunning | Where-Object { -not $_.IsDomainJoined }
    Write-PSFMessage -Message "Target machines for publishing: '$($targetMachines -join ', ')'"

    $machinesNotTargeted = Get-LabVM -All | Where-Object { $_.Roles.Name -notcontains 'RootDC' -and $_.Name -notin $targetMachines.Name -and -not $_.IsDomainJoined }

    if ($machinesNotTargeted)
    {
        Write-ScreenInfo -Message 'The following machines are not updated with Root and Subordinate certificates from the newly installed Root and Subordinate certificate servers. Please update these manually.' -Type Warning
        $machinesNotTargeted | ForEach-Object { Write-ScreenInfo -Message " $_" -Type Warning }
    }

    foreach ($machine in $targetMachines)
    {
        $machineSession = New-LabPSSession -ComputerName $machine
        foreach ($certfile in (Get-ChildItem -Path "$((Get-Lab).LabPath)\Certificates"))
        {
            Write-PSFMessage -Message "Send file '$($certfile.FullName)' to 'C:\Windows\$($certfile.BaseName).crt'"
            Send-File -SourceFilePath $certfile.FullName -DestinationFolderPath /Windows -Session $machineSession
        }

        $scriptBlock = {
            foreach ($certfile in (Get-ChildItem -Path 'C:\Windows\*.crt'))
            {
                Write-Verbose -Message "Install certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) on machine $(hostname)"
                #If workgroup, publish to local store
                $domJoined = if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                {
                    (Get-CimInstance -Namespace root\cimv2 -Class Win32_ComputerSystem).DomainRole -eq 2
                }
                else
                {
                    (Get-WmiObject -Namespace root\cimv2 -Class Win32_ComputerSystem).DomainRole -eq 2
                }

                if ($domJoined)
                {
                    Write-Verbose -Message ' Machine is not domain joined. Publishing certificate to local store'

                    $Cert = Get-PfxCertificate $certfile.FullName
                    if ($Cert.GetNameInfo('SimpleName', $false) -eq $Cert.GetNameInfo('SimpleName', $true))
                    {
                        $targetStore = 'Root'
                    }
                    else
                    {
                        $targetStore = 'CA'
                    }

                    if (-not (Get-ChildItem -Path "Cert:\LocalMachine\$targetStore" | Where-Object { $_.ThumbPrint -eq (Get-PfxCertificate $($certfile.FullName)).ThumbPrint }))
                    {
                        $result = Invoke-Expression -Command "certutil -addstore -f $targetStore c:\Windows\$($certfile.BaseName).crt"

                        if ($result | Where-Object { $_ -like '*already in store*' })
                        {
                            Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) is already in local store on $(hostname)"
                        }
                        elseif ($result | Where-Object { $_ -like '*added to store.' })
                        {
                            Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) added to local store on $(hostname)"
                        }
                        else
                        {
                            Write-Error -Message "Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) was not added to local store on $(hostname)"
                        }
                    }
                    else
                    {
                        Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) is already in local store on $(hostname)"
                    }
                }
                else #If domain joined, publish to AD Enterprise store
                {
                    Write-Verbose -Message ' Machine is domain controller. Publishing certificate to AD Enterprise store'

                    if (((Get-PfxCertificate $($certfile.FullName)).Subject) -like '*root*')
                    {
                        $dsPublishStoreName = 'RootCA'
                        $readStoreName = 'Root'
                    }
                    else
                    {
                        $dsPublishStoreName = 'SubCA'
                        $readStoreName = 'CA'
                    }


                    if (-not (Get-ChildItem "Cert:\LocalMachine\$readStoreName" | Where-Object { $_.ThumbPrint -eq (Get-PfxCertificate $($certfile.FullName)).ThumbPrint }))
                    {
                        $result = Invoke-Expression -Command "certutil -f -dspublish c:\Windows\$($certfile.BaseName).crt $dsPublishStoreName"

                        if ($result | Where-Object { $_ -like '*Certificate added to DS store*' })
                        {
                            Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) added to DS store on $(hostname)"
                        }
                        elseif ($result | Where-Object { $_ -like '*Certificate already in DS store*' })
                        {
                            Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) is already in DS store on $(hostname)"
                        }
                        else
                        {
                            Write-Error -Message "Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) was not added to DS store on $(hostname)"
                        }
                    }
                    else
                    {
                        Write-Verbose -Message " Certificate ($((Get-PfxCertificate $certfile.FullName).Subject)) is already in DS store on $(hostname)"
                    }
                }
            }
        }

        $job = Invoke-LabCommand -ActivityName 'Publish Lab CA(s) and install certificates' -ComputerName $machine -ScriptBlock $scriptBlock -NoDisplay -AsJob -PassThru
        if ($PassThru) { $job }
    }

    Write-LogFunctionExit
}


function Remove-LabAzureAppServicePlan
{
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroup
    )

    begin
    {
        Write-LogFunctionEntry

        $script:lab = Get-Lab
    }

    process
    {
        $servicePlan = $lab.AzureResources.ServicePlans | Where-Object { $_.Name -eq $Name -and $_.ResourceGroup -eq $ResourceGroup }

        if (-not $servicePlan)
        {
            Write-Error "The Azure App Service Plan '$Name' does not exist."
        }
        else
        {
            $sp = Get-AzAppServicePlan -Name $servicePlan.Name -ResourceGroupName $servicePlan.ResourceGroup -ErrorAction SilentlyContinue

            if ($sp)
            {
                $sp | Remove-AzAppServicePlan -Force
            }
            $lab.AzureResources.ServicePlans.Remove($servicePlan)
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function Remove-LabAzureWebApp
{
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroup
    )

    begin
    {
        Write-LogFunctionEntry

        $script:lab = Get-Lab
    }

    process
    {
        $service = $lab.AzureResources.Services | Where-Object { $_.Name -eq $Name -and $_.ResourceGroup -eq $ResourceGroup }

        if (-not $service)
        {
            Write-Error "The Azure App Service '$Name' does not exist in the lab."
        }
        else
        {
            $s = Get-AzWebApp -Name $service.Name -ResourceGroupName $service.ResourceGroup -ErrorAction SilentlyContinue

            if ($s)
            {
                $s | Remove-AzWebApp -Force
            }

            $lab.AzureResources.Services.Remove($service)
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function Remove-LabNetworkSwitches
{
    [cmdletBinding()]
    param (
        [switch]$RemoveExternalSwitches
    )

    $Script:data = Get-Lab
    if (-not $Script:data)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    Write-LogFunctionEntry

    $virtualNetworks = $Script:data.VirtualNetworks | Where-Object HostType -eq VMWare
    foreach ($virtualNetwork in $virtualNetworks)
    {
        Write-Error "Cannot remove network '$virtualNetwork'. Managing networks is not yet supported for VMWare"
        continue
    }

    $virtualNetworks = $Script:data.VirtualNetworks | Where-Object { $_.HostType -eq 'HyperV' -and $_.Name -ne 'Default Switch' }
    foreach ($virtualNetwork in $virtualNetworks)
    {
        Write-PSFMessage "Removing Hyper-V network switch '$($virtualNetwork.ResourceName)'..."

        if ($virtualNetwork.SwitchType -eq 'External' -and -not $RemoveExternalSwitches)
        {
            Write-ScreenInfo "The virtual switch '$($virtualNetwork.ResourceName)' is of type external and will not be removed as it may also be used by other labs"
            continue
        }
        else
        {
            Remove-LWNetworkSwitch -Name $virtualNetwork.ResourceName
        }
        Write-PSFMessage '...done'
    }

    Write-PSFMessage 'done'

    Write-LogFunctionExit
}


function Reset-DNSConfiguration
{
    [CmdletBinding()]
    param
    (
        [string[]]$ComputerName,

        [int]$ProgressIndicator,

        [switch]$NoNewLine
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName

    $jobs = @()
    foreach ($machine in $machines)
    {
        $jobs += Invoke-LabCommand -ComputerName $machine -ActivityName 'Reset DNS client configuration to match specified DNS configuration' -ScriptBlock `
        {
            param
            (
                $DnsServers
            )
            $AdapterNames = if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
            {
                (Get-CimInstance -Namespace Root\CIMv2 -Class Win32_NetworkAdapter | Where-Object {$_.PhysicalAdapter}).NetConnectionID
            }
            else
            {
                (Get-WmiObject -Namespace Root\CIMv2 -Class Win32_NetworkAdapter | Where-Object {$_.PhysicalAdapter}).NetConnectionID
            }
            foreach ($AdapterName in $AdapterNames)
            {
                netsh.exe interface ipv4 set dnsservers "$AdapterName" static $DnsServers primary

                "netsh interface ipv6 delete dnsservers '$AdapterName' all"
                netsh.exe interface ipv6 delete dnsservers "$AdapterName" all
            }
        } -AsJob -PassThru -NoDisplay -ArgumentList $machine.DNSServers
    }

    Wait-LWLabJob -Job $jobs -NoDisplay -Timeout 30 -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewLine

    Write-LogFunctionExit
}


function Reset-LabAdPassword
{
    param(
        [Parameter(Mandatory)]
        [string]$DomainName
    )
    
    $lab = Get-Lab
    $domain = $lab.Domains | Where-Object Name -eq $DomainName
    $vm = Get-LabVM -Role RootDC, FirstChildDC | Where-Object DomainName -eq $DomainName
    
    Invoke-LabCommand -ActivityName 'Reset Administrator password in AD' -ScriptBlock {
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        $ctx = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain')
        $i = 0
        while (-not $u -and $i -lt 25)
        {
            try
            {
                $u = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($ctx, $args[0])
                $u.SetPassword($args[1])
            }
            catch
            {
                Start-Sleep -Seconds 10
                $i++
            }
        }
        
    } -ComputerName $vm -ArgumentList $domain.Administrator.UserName, $domain.Administrator.Password -NoDisplay
}


function Send-FtpFolder
{
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [string]$DestinationPath,

        [Parameter(Mandatory)]
        [string]$HostUrl,

        [Parameter(Mandatory)]
        [System.Net.NetworkCredential]$Credential,

        [switch]$Recure
    )

    Add-Type -Path (Join-Path -Path (Get-Module AutomatedLabCore).ModuleBase -ChildPath 'Tools\FluentFTP.dll')
    $fileCount = 0

    if (-not (Test-Path -Path $Path -PathType Container))
    {
        Write-Error "The folder '$Path' does not exist or is not a directory."
        return
    }

    $client = New-Object FluentFTP.FtpClient("ftp://$HostUrl", $Credential)
    try
    {
        $client.DataConnectionType = [FluentFTP.FtpDataConnectionType]::PASV
        $client.Connect()
    }
    catch
    {
        Write-Error -Message "Could not connect to FTP server: $($_.Exception.Message)" -Exception $_.Exception
        return
    }

    if ($DestinationPath.Contains('\'))
    {
        Write-Error "The destination path cannot contain backslashes. Please use forward slashes to separate folder names."
        return
    }

    if (-not $DestinationPath.EndsWith('/'))
    {
        $DestinationPath += '/'
    }

    $files = Get-ChildItem -Path $Path -File -Recurse:$Recure
    Write-PSFMessage "Sending folder '$Path' with $($files.Count) files"

    foreach ($file in $files)
    {
        $fileCount++
        Write-PSFMessage "Sending file $($file.FullName) ($fileCount)"
        Write-Progress -Activity "Uploading file '$($file.FullName)'" -Status x -PercentComplete ($fileCount / $files.Count * 100)
        $relativeFullName = $file.FullName.Replace($path, '').Replace('\', '/')
        if ($relativeFullName.StartsWith('/')) { $relativeFullName = $relativeFullName.Substring(1) }
        $newDestinationPath = $DestinationPath + $relativeFullName

        try
        {
            $result = $client.UploadFile($file.FullName, $newDestinationPath, 'Overwrite', $true, 'Retry')
        }
        catch
        {
            Write-Error -Exception $_.Exception
            $client.Disconnect()
            return
        }
        if (-not $result)
        {
            Write-Error "There was an error uploading file '$($file.FullName)'. Canelling the upload process."
            $client.Disconnect()
            return
        }
    }

    Write-PSFMessage "Finsihed sending folder '$Path'"

    $client.Disconnect()
}


function Send-LabAzureWebAppContent
{
    [OutputType([string])]
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName)]
        [string]$Name,

        [Parameter(Position = 1, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName)]
        [string]$ResourceGroup
    )

    begin
    {
        Write-LogFunctionEntry
        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        foreach ($n in $name)
        {
            $webApp = Get-LabAzureWebApp -Name $n | Where-Object ResourceGroup -eq $ResourceGroup

        }
    }

    end
    {
        Export-Lab
        if ($result.Count -eq 1 -and -not $AsHashTable)
        {
            $result[$result.Keys[0]]
        }
        else
        {
            $result
        }
        Write-LogFunctionExit
    }
}


function Set-LabADDNSServerForwarder
{
    [CmdletBinding()]
    param ( )

    Write-PSFMessage 'Setting DNS fowarder on all domain controllers in root domains'

    $rootDcs = Get-LabVM -Role RootDC

    $rootDomains = $rootDcs.DomainName

    $dcs = Get-LabVM -Role RootDC, DC | Where-Object DomainName -in $rootDomains
    $router = Get-LabVM -Role Routing
    Write-PSFMessage "Root DCs are '$dcs'"

    foreach ($dc in $dcs)
    {
        $gateway = if ($dc -eq $router)
        {
            Invoke-LabCommand -ActivityName 'Get default gateway' -ComputerName $dc -ScriptBlock {

                Get-CimInstance -Class Win32_NetworkAdapterConfiguration | Where-Object { $_.DefaultIPGateway } | Select-Object -ExpandProperty DefaultIPGateway | Select-Object -First 1

            } -PassThru -NoDisplay
        }
        else
        {
            $netAdapter = $dc.NetworkAdapters | Where-Object Ipv4Gateway
            $netAdapter.Ipv4Gateway.AddressAsString
        }

        Write-PSFMessage "Read gateway '$gateway' from interface '$($netAdapter.InterfaceName)' on machine '$dc'"

        $defaultDnsForwarder1 = Get-LabConfigurationItem -Name DefaultDnsForwarder1
        $defaultDnsForwarder2 = Get-LabConfigurationItem -Name DefaultDnsForwarder2
        Invoke-LabCommand -ActivityName ResetDnsForwarder -ComputerName $dc -ScriptBlock {
            dnscmd /resetforwarders $args[0] $args[1]
        } -ArgumentList $defaultDnsForwarder1, $defaultDnsForwarder2 -AsJob -NoDisplay
    }
}


function Set-LabBuildWorkerCapability
{
    [CmdletBinding()]
    param
    ( )

    $buildWorkers = Get-LabVM -Role TfsBuildWorker
    if (-not $buildWorkers)
    {
        return
    }

    foreach ($machine in $buildWorkers)
    {
        $role = $machine.Roles | Where-Object Name -eq TfsBuildWorker
        $agentPool = if ($role.Properties.ContainsKey('AgentPool'))
        {
            $role.Properties['AgentPool']
        }
        else
        {
            'default'
        }

        [int]$numberOfBuildWorkers = $role.Properties.NumberOfBuildWorkers
        
        if ((Get-Command -Name Add-TfsAgentUserCapability -ErrorAction SilentlyContinue) -and $role.Properties.ContainsKey('Capabilities'))
        {
            $bwParam = Get-LabTfsParameter -ComputerName $machine
            if ($numberOfBuildWorkers)
            {
                $range = 1..$numberOfBuildWorkers
            }
            else
            {
                $range = 1
            }

            foreach ($numberOfBuildWorker in $range)
            {
                $agt = Get-TfsAgent @bwParam -PoolName $agentPool -Filter ([scriptblock]::Create("`$_.name -eq '$($machine.Name)-$numberOfBuildWorker'"))
                $caps = @{}
                foreach ($prop in ($role.Properties['Capabilities'] | ConvertFrom-Json).PSObject.Properties)
                {
                    $caps[$prop.Name] = $prop.Value
                }

                $null = Add-TfsAgentUserCapability @bwParam -Capability $caps -Agent $agt -PoolName $agentPool
            }
        }
    }
}


function Set-LabDefaultToolsPath
{
    [Cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $Global:labToolsPath = $Path
}


function Set-LabVMDescription
{

    [CmdletBinding()]
    param (
        [hashtable]$Hashtable,

        [string]$ComputerName
    )

    Write-LogFunctionEntry

    $t = Get-Type -GenericType AutomatedLab.SerializableDictionary -T String, String
    $d = New-Object $t

    foreach ($kvp in $Hashtable.GetEnumerator())
    {
        $d.Add($kvp.Key, $kvp.Value)
    }

    $sb = New-Object System.Text.StringBuilder
    $xmlWriterSettings = New-Object System.Xml.XmlWriterSettings
    $xmlWriterSettings.ConformanceLevel = 'Auto'
    $xmlWriter = [System.Xml.XmlWriter]::Create($sb, $xmlWriterSettings)

    $d.WriteXml($xmlWriter)

    Get-LWHypervVm -Name $ComputerName -ErrorAction SilentlyContinue | Set-VM -Notes $sb.ToString()

    Write-LogFunctionExit
}


function Set-VMUacStatus
{
    [CmdletBinding()]
    param(
        [bool]$EnableLUA,

        [int]$ConsentPromptBehaviorAdmin,

        [int]$ConsentPromptBehaviorUser
    )

    $currentSettings = Get-VMUacStatus -ComputerName $ComputerName
    $uacStatusChanged = $false

    $registryPath = 'Software\Microsoft\Windows\CurrentVersion\Policies\System'
    $openRegistry = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, 'Default')

    $subkey = $openRegistry.OpenSubKey($registryPath,$true)

    if ($currentSettings.EnableLUA -ne $EnableLUA -and $PSBoundParameters.ContainsKey('EnableLUA'))
    {
        $subkey.SetValue('EnableLUA', [int]$EnableLUA)
        $uacStatusChanged = $true
    }

    if ($currentSettings.PromptBehaviorAdmin -ne $ConsentPromptBehaviorAdmin -and $PSBoundParameters.ContainsKey('ConsentPromptBehaviorAdmin'))
    {
        $subkey.SetValue('ConsentPromptBehaviorAdmin', $ConsentPromptBehaviorAdmin)
        $uacStatusChanged = $true
    }

    if ($currentSettings.PromptBehaviorUser -ne $ConsentPromptBehaviorUser -and $PSBoundParameters.ContainsKey('ConsentPromptBehaviorUser'))
    {
        $subkey.SetValue('ConsentPromptBehaviorUser', $ConsentPromptBehaviorUser)
        $uacStatusChanged = $true
    }

    return (New-Object psobject -Property @{ UacStatusChanged = $uacStatusChanged } )
}


function Set-VpnDnsForwarders
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SourceLab,
        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationLab
    )

    Import-Lab $SourceLab -NoValidation
    $sourceDcs = Get-LabVM -Role DC, RootDC, FirstChildDC

    Import-Lab $DestinationLab -NoValidation
    $destinationDcs = Get-LabVM -Role DC, RootDC, FirstChildDC

    $forestNames = @($sourceDcs) + @($destinationDcs) | Where-Object { $_.Roles.Name -Contains 'RootDC'} | Select-Object -ExpandProperty DomainName
    $forwarders = Get-FullMesh -List $forestNames

    foreach ($forwarder in $forwarders)
    {
        $targetMachine = @($sourceDcs) + @($destinationDcs) | Where-Object { $_.Roles.Name -contains 'RootDC' -and $_.DomainName -eq $forwarder.Source }
        $machineExists = Get-LabVM | Where-Object {$_.Name -eq $targetMachine.Name -and $_.IpV4Address -eq $targetMachine.IpV4Address}

        if (-not $machineExists)
        {
            if ((Get-Lab).Name -eq $SourceLab)
            {
                Import-Lab -Name $DestinationLab -NoValidation
            }
            else
            {
                Import-Lab -Name $SourceLab -NoValidation
            }
        }

        $masterServers = @($sourceDcs) + @($destinationDcs) | Where-Object {
            ($_.Roles.Name -contains 'RootDC' -or $_.Roles.Name -contains 'FirstChildDC' -or $_.Roles.Name -contains 'DC') -and $_.DomainName -eq $forwarder.Destination
        }

        $cmd = @"
            Write-PSFMessage "Creating a DNS forwarder on server '$env:COMPUTERNAME'. Forwarder name is '$($forwarder.Destination)' and target DNS server is '$($masterServers.IpV4Address)'..."
            dnscmd localhost /ZoneAdd $($forwarder.Destination) /Forwarder $($masterServers.IpV4Address)
            Write-PSFMessage '...done'
"@


        Invoke-LabCommand -ComputerName $targetMachine -ScriptBlock ([scriptblock]::Create($cmd)) -NoDisplay
    }
}


function Start-ShellHWDetectionService
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [CmdletBinding()]
    param ( )

    Write-LogFunctionEntry

    $service = Get-Service -Name ShellHWDetection -ErrorAction SilentlyContinue
    if (-not $service)
    {
        Write-PSFMessage -Message "The service 'ShellHWDetection' is not installed, exiting."
        Write-LogFunctionExit
        return
    }

    if ((Get-Service -Name ShellHWDetection).Status -eq 'Running')
    {
        Write-PSFMessage -Message "'ShellHWDetection' Service is already running."
        Write-LogFunctionExit
        return
    }

    Write-PSFMessage -Message 'Starting the ShellHWDetection service (Shell Hardware Detection) again.'

    $retries = 5
    while ($retries -gt 0 -and ((Get-Service -Name ShellHWDetection).Status -ne 'Running'))
    {
        Write-Debug -Message 'Trying to start ShellHWDetection'
        Start-Service -Name ShellHWDetection -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 1
        if ((Get-Service -Name ShellHWDetection).Status -ne 'Running')
        {
            Write-Debug -Message 'Could not start service ShellHWDetection. Retrying.'
            Start-Sleep -Seconds 5
        }
        $retries--
    }

    Write-LogFunctionExit
}


function Stop-LabVM2
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0)]
        [string[]]$ComputerName,

        [int]$ShutdownTimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_StopLabMachine_Shutdown)
    )

    $scriptBlock = {
        $sessions = quser.exe
        $sessionNames = $sessions |
        Select-Object -Skip 1 |
        ForEach-Object -Process {
            ($_.Trim() -split ' +')[2]
        }

        Write-Verbose -Message "There are $($sessionNames.Count) open sessions"
        foreach ($sessionName in $sessionNames)
        {
            Write-Verbose -Message "Closing session '$sessionName'"
            logoff.exe $sessionName
        }

        Start-Sleep -Seconds 2

        Write-Verbose -Message 'Stopping machine forcefully'
        Stop-Computer -Force
    }

    $jobs = Invoke-LabCommand -ComputerName $ComputerName -ActivityName Shutdown -NoDisplay -ScriptBlock $scriptBlock -AsJob -PassThru
    $jobs | Wait-Job -Timeout ($ShutdownTimeoutInMinutes * 60) | Out-Null

    if ($jobs.Count -ne ($jobs | Where-Object State -eq Completed).Count)
    {
        Write-ScreenInfo "Not all machines stopped in the timeout of $ShutdownTimeoutInMinutes" -Type Warning
    }
}


function Stop-ShellHWDetectionService
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [CmdletBinding()]
    param ( )

    Write-LogFunctionEntry

    $service = Get-Service -Name ShellHWDetection -ErrorAction SilentlyContinue
    if (-not $service)
    {
        Write-PSFMessage -Message "The service 'ShellHWDetection' is not installed, exiting."
        Write-LogFunctionExit
        return
    }

    Write-PSFMessage -Message 'Stopping the ShellHWDetection service (Shell Hardware Detection) to prevent the OS from responding to the new disks.'

    $retries = 5
    while ($retries -gt 0 -and ((Get-Service -Name ShellHWDetection).Status -ne 'Stopped'))
    {
        Write-Debug -Message 'Trying to stop ShellHWDetection'

        Stop-Service -Name ShellHWDetection | Out-Null
        Start-Sleep -Seconds 1
        if ((Get-Service -Name ShellHWDetection).Status -eq 'Running')
        {
            Write-Debug -Message "Could not stop service ShellHWDetection. Retrying."
            Start-Sleep -Seconds 5
        }
        $retries--
    }

    Write-LogFunctionExit
}


function Test-FileName
{
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $fi = $null
    try
    {
        $fi = New-Object System.IO.FileInfo($Path)
    }
    catch [ArgumentException] { }
    catch [System.IO.PathTooLongException] { }
    catch [NotSupportedException] { }
    if ([object]::ReferenceEquals($fi, $null) -or $fi.Name -eq '')
    {
        return $false
    }
    else
    {
        return $true
    }
}


function Test-LabAzureSubscription
{
    [CmdletBinding()]
    param ( )

    Test-LabHostConnected -Throw -Quiet

    try
    {
        $ctx = Get-AzContext
    }
    catch
    {
        throw "No Azure Context found, Please run 'Connect-AzAccount' first"
    }
}


function Update-CMSite
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [String]$CMSiteCode,

        [Parameter(Mandatory)]
        [String]$CMServerName
    )

    #region Initialise
    $CMServer = Get-LabVM -ComputerName $CMServerName
    $CMServerFqdn = $CMServer.FQDN

    Write-ScreenInfo -Message "Validating install" -TaskStart
    $cim = New-LabCimSession -ComputerName $CMServer
    $Query = "SELECT * FROM SMS_Site WHERE SiteCode='{0}'" -f $CMSiteCode
    $Namespace = "ROOT/SMS/site_{0}" -f $CMSiteCode

    try
    {
        $result = Get-CimInstance -Namespace $Namespace -Query $Query -ErrorAction "Stop" -CimSession $cim -ErrorVariable ReceiveJobErr
    }
    catch
    {
        $Message = "Failed to validate install, could not find site code '{0}' in SMS_Site class ({1})" -f $CMSiteCode, $ReceiveJobErr.ErrorRecord.Exception.Message
        Write-ScreenInfo -Message $Message -Type "Error" -TaskEnd
        return
    }
    #endregion

    #region Define enums
    enum SMS_CM_UpdatePackages_State
    {
        AvailableToDownload = 327682
        ReadyToInstall = 262146
        Downloading = 262145
        Installed = 196612
        Failed = 262143
    }
    #endregion

    #region Ensuring CONFIGURATION_MANAGER_UPDATE service is running
    Write-ScreenInfo -Message "Ensuring CONFIGURATION_MANAGER_UPDATE service is running" -TaskStart
    Invoke-LabCommand -ComputerName $CMServerName -ActivityName "Ensuring CONFIGURATION_MANAGER_UPDATE service is running" -ScriptBlock {
        $service = "CONFIGURATION_MANAGER_UPDATE"
        if ((Get-Service $service | Select-Object -ExpandProperty Status) -ne "Running")
        {
            Start-Service "CONFIGURATION_MANAGER_UPDATE" -ErrorAction "Stop"
        }
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Finding update for target version
    Write-ScreenInfo -Message "Waiting for updates to appear in console" -TaskStart
    $cim = New-LabCimSession -ComputerName $CMServerName
    $Query = "SELECT * FROM SMS_CM_UpdatePackages WHERE Impact = '31'"
    $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim | Sort-object -Property FullVersion -Descending
    $start = Get-Date
    while (-not $Update -and ((Get-Date) - $start) -lt '00:30:00')
    {
        $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim | Sort-object -Property FullVersion -Descending
    }

    # https://github.com/PowerShell/PowerShell/issues/9185
    $Update = $Update[0]
    
    # On some occasions, the update was already "ready to install"
    if ($Update.State -eq [SMS_CM_UpdatePackages_State]::ReadyToInstall)
    {
        $null = Invoke-CimMethod -InputObject $Update -MethodName "InitiateUpgrade" -Arguments @{PrereqFlag = 2 }
    }

    if ($Update.State -eq [SMS_CM_UpdatePackages_State]::AvailableToDownload)
    {
        $null = Invoke-CimMethod -InputObject $Update -MethodName "SetPackageToBeDownloaded"

        $Query = "SELECT * FROM SMS_CM_UpdatePackages WHERE PACKAGEGUID = '{0}'" -f $Update.PackageGuid
        $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim

        while ($Update.State -eq [SMS_CM_UpdatePackages_State]::Downloading)
        {
            Start-Sleep -Seconds 5
            $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim
        }

        $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim

        while (-not ($Update.State -eq [SMS_CM_UpdatePackages_State]::ReadyToInstall))
        {
            Start-Sleep -Seconds 5
            $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim
        }

        $null = Invoke-CimMethod -InputObject $Update -MethodName "InitiateUpgrade" -Arguments @{PrereqFlag = 2 }
    }

    # Wait for installation to finish
    $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim
    $start = Get-Date
    while ($Update.State -ne [SMS_CM_UpdatePackages_State]::Installed -and ((Get-Date) - $start) -lt '00:30:00')
    {
        Start-Sleep -Seconds 10
        $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim
    }

    #region Validate update
    Write-ScreenInfo -Message "Validating update" -TaskStart
    $cim = New-LabCimSession -ComputerName $CMServerName
    $Query = "SELECT * FROM SMS_CM_UpdatePackages WHERE PACKAGEGUID = '{0}'" -f $Update.PackageGuid
    $Update = Get-CimInstance -Namespace "ROOT/SMS/site_$CMSiteCode" -Query $Query -ErrorAction SilentlyContinue -CimSession $cim

    try
    {
        $InstalledSite = Get-CimInstance -Namespace "ROOT/SMS/site_$($CMSiteCode)" -ClassName "SMS_Site" -ErrorAction "Stop" -CimSession $cim
    }
    catch
    {
        Write-ScreenInfo -Message ("Could not query SMS_Site to validate update install ({0})" -f $_.ErrorRecord.Exception.Message) -TaskEnd -Type "Error"
        throw $_
    }
    if ($InstalledSite.Version -ne $Update.FullVersion)
    {
        $Message = "Update validation failed, installed version is '{0}' and the expected version is '{1}'. Try running Install-LabConfigurationManager a second time" -f $InstalledSite.Version, $Update.FullVersion
        Write-ScreenInfo -Message $Message -Type "Error" -TaskEnd
        throw $Message
    }
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion

    #region Update console
    Write-ScreenInfo -Message "Updating console" -TaskStart
    $cmd = "/q TargetDir=`"C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole`" DefaultSiteServerName={0}" -f $CMServerFqdn
    $job = Install-LabSoftwarePackage -ComputerName $CMServerName -LocalPath "C:\Program Files\Microsoft Configuration Manager\tools\ConsoleSetup\ConsoleSetup.exe" -CommandLine $cmd
    Write-ScreenInfo -Message "Activity done" -TaskEnd
    #endregion
}


function Update-LabMemorySettings
{
    # Cmdlet is not called on Linux systems
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [Cmdletbinding()]
    Param ()

    Write-LogFunctionEntry

    $machines = Get-LabVM -All -IncludeLinux | Where-Object SkipDeployment -eq $false
    $lab = Get-LabDefinition

    if ($machines | Where-Object Memory -lt 32)
    {
        $totalMemoryAlreadyReservedAndClaimed = ((Get-LWHypervVM -Name $machines.ResourceName -ErrorAction SilentlyContinue) | Measure-Object -Sum -Property MemoryStartup).Sum
        $machinesNotCreated = $machines | Where-Object { (-not (Get-LWHypervVM -Name $_.ResourceName -ErrorAction SilentlyContinue)) }

        $totalMemoryAlreadyReserved = ($machines | Where-Object { $_.Memory -ge 128 -and $_.Name -notin $machinesNotCreated.Name } | Measure-Object -Property Memory -Sum).Sum

        $totalMemory = (Get-CimInstance -Namespace Root\Cimv2 -Class win32_operatingsystem).FreePhysicalMemory * 1KB * 0.8 - $totalMemoryAlreadyReserved + $totalMemoryAlreadyReservedAndClaimed

        if ($lab.MaxMemory -ne 0 -and $lab.MaxMemory -le $totalMemory)
        {
            $totalMemory = $lab.MaxMemory
            Write-Debug -Message "Memory in lab is manually limited to: $totalmemory MB"
        }
        else
        {
            Write-Debug -Message "80% of total available (free) physical memory minus memory already reserved by machines where memory is defined: $totalmemory bytes"
        }


        $totalMemoryUnits = ($machines | Where-Object Memory -lt 32 | Measure-Object -Property Memory -Sum).Sum

        ForEach ($machine in $machines | Where-Object Memory -ge 128)
        {
            Write-Debug -Message "$($machine.Name.PadRight(20)) $($machine.Memory / 1GB)GB (set manually)"
        }

        #Test if necessary to limit memory at all
        $memoryUsagePrediction = $totalMemoryAlreadyReserved
        foreach ($machine in $machines | Where-Object Memory -lt 32)
        {
            switch ($machine.Memory)
            {
                1 { if ($lab.UseStaticMemory)
                    {
                        $memoryUsagePrediction += 768
                    }
                    else
                    {
                        $memoryUsagePrediction += 512
                    }
                }
                2 { if ($lab.UseStaticMemory)
                    {
                        $memoryUsagePrediction += 1024
                    }
                    else
                    {
                        $memoryUsagePrediction += 512
                    }
                }
                3 { if ($lab.UseStaticMemory)
                    {
                        $memoryUsagePrediction += 2048
                    }
                    else
                    {
                        $memoryUsagePrediction += 1024
                    }
                }
                4 { if ($lab.UseStaticMemory)
                    {
                        $memoryUsagePrediction += 4096
                    }
                    else
                    {
                        $memoryUsagePrediction += 1024
                    }
                }
            }
        }

        ForEach ($machine in $machines | Where-Object { $_.Memory -lt 32 -and -not (Get-LWHypervVM -Name $_.ResourceName -ErrorAction SilentlyContinue) })
        {
            $memoryCalculated = ($totalMemory / $totalMemoryUnits * $machine.Memory / 64) * 64
            if ($memoryUsagePrediction -gt $totalMemory)
            {
                $machine.Memory = $memoryCalculated
                if (-not $lab.UseStaticMemory)
                {
                    $machine.MaxMemory = $memoryCalculated * 4
                }
            }
            else
            {
                if ($lab.MaxMemory -eq 4TB)
                {
                    #If parameter UseAllMemory was used for New-LabDefinition
                    $machine.Memory = $memoryCalculated
                }
                else
                {
                    switch ($machine.Memory)
                    {
                        1 { if ($lab.UseStaticMemory)
                            {
                                $machine.Memory = 768MB
                            }
                            else
                            {
                                $machine.MinMemory = 384MB
                                $machine.Memory    = 512MB
                                $machine.MaxMemory = 1.25GB
                            }
                        }
                        2 { if ($lab.UseStaticMemory)
                            {
                                $machine.Memory = 1GB
                            }
                            else
                            {
                                $machine.MinMemory = 384MB
                                $machine.Memory    = 512MB
                                $machine.MaxMemory = 2GB
                            }
                        }
                        3 { if ($lab.UseStaticMemory)
                            {
                                $machine.Memory = 2GB
                            }
                            else
                            {
                                $machine.MinMemory = 384MB
                                $machine.Memory    = 1GB
                                $machine.MaxMemory = 4GB
                            }
                        }
                        4 { if ($lab.UseStaticMemory)
                            {
                                $machine.Memory = 4GB
                            }
                            else
                            {
                                $machine.MinMemory = 384MB
                                $machine.Memory    = 1GB
                                $machine.MaxMemory = 8GB
                            }
                        }
                    }
                }
            }
            Write-Debug -Message "$("Memory in $($machine)".PadRight(30)) $($machine.Memory / 1GB)GB (calculated)"
            if ($machine.MaxMemory)
            {
                Write-Debug -Message "$("MaxMemory in $($machine)".PadRight(30)) $($machine.MaxMemory / 1GB)GB (calculated)"
            }

            if ($memoryCalculated -lt 256)
            {
                Write-ScreenInfo -Message "Machine '$($machine.Name)' is now auto-configured with $($memoryCalculated / 1GB)GB of memory. This might give unsatisfactory performance. Consider adding memory to the host, raising the available memory for this lab or use fewer machines in this lab" -Type Warning
            }
        }
    }

    Write-LogFunctionExit
}


function Update-LabVMWareSettings
{
    if ((Get-PSCallStack).Command -contains 'Import-Lab')
    {
        $Script:lab = Get-Lab
    }
    elseif ((Get-PSCallStack).Command -contains 'Add-LabVMWareSettings')
    {
        $Script:lab = Get-LabDefinition
    }
}


function ValidateUpdate-ConfigurationData
{
    param (
        [Parameter(Mandatory)]
        [hashtable]$ConfigurationData
    )

    if( -not $ConfigurationData.ContainsKey('AllNodes'))
    {
        $errorMessage = 'ConfigurationData parameter need to have property AllNodes.'
        $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
        Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataNeedAllNodes
        return $false
    }

    if($ConfigurationData.AllNodes -isnot [array])
    {
        $errorMessage = 'ConfigurationData parameter property AllNodes needs to be a collection.'
        $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
        Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataAllNodesNeedHashtable
        return $false
    }

    $nodeNames = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
    foreach($Node in $ConfigurationData.AllNodes)
    {
        if($Node -isnot [hashtable] -or -not $Node.NodeName)
        {
            $errorMessage = "all elements of AllNodes need to be hashtable and has a property 'NodeName'."
            $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
            Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataAllNodesNeedHashtable
            return $false
        }

        if($nodeNames.Contains($Node.NodeName))
        {
            $errorMessage = "There are duplicated NodeNames '{0}' in the configurationData passed in." -f $Node.NodeName
            $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
            Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId DuplicatedNodeInConfigurationData
            return $false
        }

        if($Node.NodeName -eq '*')
        {
            $AllNodeSettings = $Node
        }
        [void] $nodeNames.Add($Node.NodeName)
    }

    if($AllNodeSettings)
    {
        foreach($Node in $ConfigurationData.AllNodes)
        {
            if($Node.NodeName -ne '*')
            {
                foreach($nodeKey in $AllNodeSettings.Keys)
                {
                    if(-not $Node.ContainsKey($nodeKey))
                    {
                        $Node.Add($nodeKey, $AllNodeSettings[$nodeKey])
                    }
                }
            }
        }

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

    return $true
}


function Add-LabCertificate
{

    [cmdletBinding(DefaultParameterSetName = 'ByteArray')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')]
        [string]$Path,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByteArray')]
        [byte[]]$RawContentBytes,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Security.Cryptography.X509Certificates.StoreName]$Store,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Security.Cryptography.X509Certificates.CertStoreLocation]$Location,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$ServiceName,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('CER', 'PFX')]
        [string]$CertificateType = 'CER',

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$Password = 'AL',

        [Parameter(Mandatory, ValueFromPipelineByPropertyName = $true)]
        [string[]]$ComputerName
    )

    begin
    {
        Write-LogFunctionEntry
    }

    process
    {
        $variables = Get-Variable -Name PSBoundParameters

        if ($Path)
        {
            $RawContentBytes = [System.IO.File]::ReadAllBytes($Path)
            $PSBoundParameters.Remove('Path')
            $PSBoundParameters.Add('RawContentBytes', $RawContentBytes)
        }

        Invoke-LabCommand -ActivityName 'Importing Cert file' -ComputerName $ComputerName -ScriptBlock {

            Sync-Parameter -Command (Get-Command -Name Add-Certificate2)
            Add-Certificate2 @ALBoundParameters | Out-Null

        } -Variable $variables -PassThru -NoDisplay

    }

    end
    {
        Write-LogFunctionExit
    }
}


function Enable-LabCertificateAutoenrollment
{

    [cmdletBinding()]

    param
    (
        [switch]$Computer,

        [switch]$User,

        [switch]$CodeSigning,

        [string]$CodeSigningTemplateName = 'LabCodeSigning'
    )

    Write-LogFunctionEntry

    $issuingCAs = Get-LabIssuingCA

    Write-PSFMessage -Message "All issuing CAs: '$($issuingCAs -join ', ')'"

    if (-not $issuingCAs)
    {
        Write-ScreenInfo -Message 'No issuing CA(s) found. Skipping operation.'
        return
    }

    Write-ScreenInfo -Message 'Configuring certificate auto enrollment' -TaskStart

    $domainsToProcess = (Get-LabVM -Role RootDC, FirstChildDC, DC | Where-Object DomainName -in $issuingCAs.DomainName | Group-Object DomainName).Name | Sort-Object -Unique
    Write-PSFMessage -Message "Domains to process: '$($domainsToProcess -join ', ')'"

    $issuingCAsToProcess = ($issuingCAs | Where-Object DomainName -in $domainsToProcess).Name
    Write-PSFMessage -Message "Issuing CAs to process: '$($issuingCAsToProcess -join ', ')'"

    $dcsToProcess = @()
    foreach ($domain in $issuingCAs.DomainName)
    {
        $dcsToProcess += Get-LabVM -Role RootDC | Where-Object { $domain -like "*$($_.DomainName)"}
    }
    $dcsToProcess = $dcsToProcess.Name | Sort-Object -Unique

    Write-PSFMessage -Message "DCs to process: '$($dcsToProcess -join ', ')'"


    if ($Computer)
    {
        Write-ScreenInfo -Message 'Configuring permissions for computer certificates' -NoNewLine
        $job = Invoke-LabCommand -ComputerName $dcsToProcess -ActivityName 'Configure permissions on workstation authentication template on CAs' -NoDisplay -AsJob -PassThru -ScriptBlock `
        {
            $domainName = ([adsi]'LDAP://RootDSE').DefaultNamingContext

            dsacls "CN=Workstation,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Computers:GR'
            dsacls "CN=Workstation,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Computers:CA;Enroll'
            dsacls "CN=Workstation,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Computers:CA;AutoEnrollment'
        }
        Wait-LWLabJob -Job $job -ProgressIndicator 20 -Timeout 30 -NoDisplay -NoNewLine


        $job = Invoke-LabCommand -ComputerName $issuingCAsToProcess -ActivityName 'Publish workstation authentication certificate template on CAs' -NoDisplay -AsJob -PassThru -ScriptBlock {
            certutil.exe -SetCAtemplates +Workstation
            #Add-CATemplate -Name 'Workstation' -Confirm:$false
        }
        Wait-LWLabJob -Job $job -ProgressIndicator 20 -Timeout 30 -NoDisplay
    }

    if ($CodeSigning)
    {
        Write-ScreenInfo -Message "Enabling code signing certificate and enabling auto enrollment of these. Code signing certificate template name: '$CodeSigningTemplateName'" -NoNewLine
        $job = Invoke-LabCommand -ComputerName $dcsToProcess -ActivityName 'Create certificate template for Code Signing' -AsJob -PassThru -NoDisplay -ScriptBlock {
            param ($NewCodeSigningTemplateName)

            $ConfigContext = ([adsi]'LDAP://RootDSE').ConfigurationNamingContext
            $adsi = [adsi]"LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"

            if (-not ($adsi.Children | Where-Object {$_.distinguishedName -like "CN=$NewCodeSigningTemplateName,*"}))
            {
                Write-Verbose -Message "Creating certificate template with name: $NewCodeSigningTemplateName"

                $codeSigningOrgiginalTemplate = $adsi.Children | Where-Object {$_.distinguishedName -like 'CN=CodeSigning,*'}


                $newCertTemplate = $adsi.Create('pKICertificateTemplate', "CN=$NewCodeSigningTemplateName")
                $newCertTemplate.put('distinguishedName',"CN=$NewCodeSigningTemplateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext")

                $newCertTemplate.put('flags','32')
                $newCertTemplate.put('displayName',$NewCodeSigningTemplateName)
                $newCertTemplate.put('revision','100')
                $newCertTemplate.put('pKIDefaultKeySpec','2')
                $newCertTemplate.SetInfo()


                $newCertTemplate.put('pKIMaxIssuingDepth','0')
                $newCertTemplate.put('pKICriticalExtensions','2.5.29.15')
                $newCertTemplate.put('pKIExtendedKeyUsage','1.3.6.1.5.5.7.3.3')
                $newCertTemplate.put('pKIDefaultCSPs','2,Microsoft Base Cryptographic Provider v1.0, 1,Microsoft Enhanced Cryptographic Provider v1.0')
                $newCertTemplate.put('msPKI-RA-Signature','0')
                $newCertTemplate.put('msPKI-Enrollment-Flag','32')
                $newCertTemplate.put('msPKI-Private-Key-Flag','16842752')
                $newCertTemplate.put('msPKI-Certificate-Name-Flag','-2113929216')
                $newCertTemplate.put('msPKI-Minimal-Key-Size','2048')
                $newCertTemplate.put('msPKI-Template-Schema-Version','2')
                $newCertTemplate.put('msPKI-Template-Minor-Revision','2')

                $LastTemplateNumber = $adsi.Children | Select-Object @{n='OIDNumber';e={[int]($_.'msPKI-Cert-Template-OID'.split('.')[-1])}} | Sort-Object -Property OIDNumber | Select-Object -ExpandProperty OIDNumber -Last 1
                $LastTemplateNumber++
                $OID = ((($adsi.Children | Select-Object -First 1).'msPKI-Cert-Template-OID'.replace('.', '\') | Split-Path -Parent) + "\$LastTemplateNumber").replace('\', '.')

                $newCertTemplate.put('msPKI-Cert-Template-OID',$OID)
                $newCertTemplate.put('msPKI-Certificate-Application-Policy','1.3.6.1.5.5.7.3.3')

                $newCertTemplate.SetInfo()


                $newCertTemplate.pKIKeyUsage = $codeSigningOrgiginalTemplate.pKIKeyUsage
                #$NewCertTemplate.pKIKeyUsage = "176" (special DSC Template)

                $newCertTemplate.pKIExpirationPeriod = $codeSigningOrgiginalTemplate.pKIExpirationPeriod
                $newCertTemplate.pKIOverlapPeriod = $codeSigningOrgiginalTemplate.pKIOverlapPeriod
                $newCertTemplate.SetInfo()

                $domainName = ([ADSI]'LDAP://RootDSE').DefaultNamingContext


                dsacls "CN=$NewCodeSigningTemplateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Users:GR'
                dsacls "CN=$NewCodeSigningTemplateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Users:CA;Enroll'
                dsacls "CN=$NewCodeSigningTemplateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,$domainName" /G 'Domain Users:CA;AutoEnrollment'
            }
            else
            {
                Write-Verbose -Message "Certificate template with name '$NewCodeSigningTemplateName' already exists"
            }
        } -ArgumentList $CodeSigningTemplateName
        Wait-LWLabJob -Job $job -ProgressIndicator 20 -Timeout 30 -NoDisplay


        Write-ScreenInfo -Message 'Publishing Code Signing certificate template on all issuing CAs' -NoNewLine
        $job = Invoke-LabCommand -ComputerName $issuingCAsToProcess -ActivityName 'Publishing code signing certificate template' -NoDisplay -AsJob -PassThru -ScriptBlock {
            param ($NewCodeSigningTemplateName)

            $ConfigContext = ([ADSI]'LDAP://RootDSE').ConfigurationNamingContext
            $adsi = [ADSI]"LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"
            while (-not ($adsi.Children | Where-Object {$_.distinguishedName -like "CN=$NewCodeSigningTemplateName,*"}))
            {
                gpupdate.exe /force
                certutil.exe -pulse

                $adsi = [ADSI]"LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"
                #Start-Sleep -Seconds 2
            }
            Start-Sleep -Seconds 2

            $start = (Get-Date)
            $done = $false
            do
            {
                Write-Verbose -Message "Trying to publish '$NewCodeSigningTemplateName'"
                $result = certutil.exe -SetCAtemplates "+$NewCodeSigningTemplateName"
                if ($result -like '*successfully*')
                {
                    $done = $True
                }
                else
                {
                    gpupdate.exe /force
                    certutil.exe -pulse
                }
            }
            until ($done -or (((Get-Date)-$start)).TotalMinutes -ge 30)
            Write-Verbose -Message 'DONE'


            if (((Get-Date)-$start).TotalMinutes -ge 10)
            {
                Write-Error -Message "Could not publish certificate template '$NewCodeSigningTemplateName' as it was not found after 10 minutes"
            }
        } -ArgumentList $CodeSigningTemplateName
        Wait-LWLabJob -Job $job -ProgressIndicator 20 -Timeout 15 -NoDisplay
    }


    $machines = Get-LabVM | Where-Object {$_.DomainName -in $domainsToProcess}
    if ($Computer -and ($User -or $CodeSigning))
    {
        $out = 'computer and user'
    }
    elseif ($Computer)
    {
        $out = 'computer'
    }
    else
    {
        $out = 'user'
    }

    Write-ScreenInfo -Message "Enabling auto enrollment of $out certificates" -NoNewLine
    $job = Invoke-LabCommand -ComputerName $machines -ActivityName 'Configuring machines for auto enrollment and performing auto enrollment of certificates' -NoDisplay -AsJob -PassThru -ScriptBlock `
    {
        Add-Type -TypeDefinition $gpoType
        Set-Item WSMan:\localhost\Client\TrustedHosts '*' -Force
        Enable-WSManCredSSP -Role Client -DelegateComputer * -Force

        $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1')
        if ($value -ne '*' -and $value -ne 'WSMAN/*')
        {
            [GPO.Helper]::SetGroupPolicy($true, 'Software\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowFreshCredentials', 1) | Out-Null
            [GPO.Helper]::SetGroupPolicy($true, 'Software\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowFresh', 1) | Out-Null
            [GPO.Helper]::SetGroupPolicy($true, 'Software\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1', 'WSMAN/*') | Out-Null
        }

        Enable-AutoEnrollment -Computer:$Computer -UserOrCodeSigning:($User -or $CodeSigning)

    } -Variable (Get-Variable gpoType, Computer, User, CodeSigning) -Function (Get-Command Enable-AutoEnrollment)
    Wait-LWLabJob -Job $job -ProgressIndicator 20 -Timeout 30 -NoDisplay


    Write-ScreenInfo -Message 'Finished configuring certificate auto enrollment' -TaskEnd

    Write-LogFunctionExit
}


function Get-LabCertificate
{

    [cmdletBinding(DefaultParameterSetName = 'FindCer')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'FindCer')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FindPfx')]
        [string]$SearchString,

        [Parameter(Mandatory = $true, ParameterSetName = 'FindCer')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FindPfx')]
        [System.Security.Cryptography.X509Certificates.X509FindType]$FindType,

        [Parameter(ParameterSetName = 'AllCer')]
        [Parameter(ParameterSetName = 'AllPfx')]
        [Parameter(ParameterSetName = 'FindCer')]
        [Parameter(ParameterSetName = 'FindPfx')]
        [System.Security.Cryptography.X509Certificates.CertStoreLocation]$Location,

        [Parameter(ParameterSetName = 'AllCer')]
        [Parameter(ParameterSetName = 'AllPfx')]
        [Parameter(ParameterSetName = 'FindCer')]
        [Parameter(ParameterSetName = 'FindPfx')]
        [System.Security.Cryptography.X509Certificates.StoreName]$Store,

        [Parameter(ParameterSetName = 'AllCer')]
        [Parameter(ParameterSetName = 'AllPfx')]
        [Parameter(ParameterSetName = 'FindCer')]
        [Parameter(ParameterSetName = 'FindPfx')]
        [string]$ServiceName,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllCer')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllPfx')]
        [switch]$All,

        [Parameter(ParameterSetName = 'AllCer')]
        [Parameter(ParameterSetName = 'AllPfx')]
        [switch]$IncludeServices,

        [Parameter(Mandatory = $true, ParameterSetName = 'FindPfx')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllPfx')]
        [securestring]$Password = ('AL' | ConvertTo-SecureString -AsPlainText -Force),

        [Parameter(ParameterSetName = 'FindPfx')]
        [Parameter(ParameterSetName = 'AllPfx')]
        [switch]$ExportPrivateKey,

        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-LogFunctionEntry

    $variables = Get-Variable -Name PSBoundParameters

    foreach ($computer in $ComputerName)
    {
        Invoke-LabCommand -ActivityName 'Exporting certificates' -ComputerName $ComputerName -ScriptBlock {
            Sync-Parameter -Command (Get-Command -Name Get-Certificate2)
            Get-Certificate2 @ALBoundParameters

        } -Variable $variables -PassThru -NoDisplay
    }

    Write-LogFunctionExit
}


function Get-LabIssuingCA
{

    [OutputType([AutomatedLab.Machine])]
    [cmdletBinding()]

    param(
        [string]$DomainName
    )

    $lab = Get-Lab

    if ($DomainName)
    {
        if ($DomainName -notin $lab.Domains.Name)
        {
            Write-Error "The domain '$DomainName' is not defined in the lab."
            return
        }

        $machines = (Get-LabVM -Role CaRoot, CaSubordinate) | Where-Object DomainName -eq $DomainName
    }
    else
    {
        $machines = (Get-LabVM -Role CaRoot, CaSubordinate)
    }

    if (-not $machines)
    {
        Write-Warning 'There is no Certificate Authority deployed in the lab. Cannot get an Issuing Certificate Authority.'
        return
    }

    $issuingCAs = Invoke-LabCommand -ComputerName $machines -ScriptBlock {
        Start-Service -Name CertSvc -ErrorAction SilentlyContinue
        $templates = certutil.exe -CATemplates
        if ($templates -like '*Machine*')
        {
            $env:COMPUTERNAME
        }
    } -PassThru -NoDisplay

    if (-not $issuingCAs)
    {
        Write-Error 'There was no issuing CA found'
        return
    }

    Get-LabVM -ComputerName $issuingCAs | ForEach-Object {
        $caName = Invoke-LabCommand -ComputerName $_ -ScriptBlock { ((certutil -config $args[0] -ping)[1] -split '"')[1] } -ArgumentList $_.Name -PassThru -NoDisplay

        $_ | Add-Member -Name CaName -MemberType NoteProperty -Value $caName -Force
        $_ | Add-Member -Name CaPath -MemberType ScriptProperty -Value { $this.FQDN + '\' + $this.CaName } -Force
        $_
    }
}


function New-LabCATemplate
{

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TemplateName,

        [string]$DisplayName,

        [Parameter(Mandatory)]
        [string]$SourceTemplateName,

        [ValidateSet('EFS_RECOVERY', 'Auto Update CA Revocation', 'No OCSP Failover to CRL', 'OEM_WHQL_CRYPTO', 'Windows TCB Component', 'DNS Server Trust', 'Windows Third Party Application Component', 'ANY_APPLICATION_POLICY', 'KP_LIFETIME_SIGNING', 'Disallowed List', 'DS_EMAIL_REPLICATION', 'LICENSE_SERVER', 'KP_KEY_RECOVERY', 'Windows Kits Component', 'AUTO_ENROLL_CTL_USAGE', 'PKIX_KP_TIMESTAMP_SIGNING', 'Windows Update', 'Document Encryption', 'KP_CTL_USAGE_SIGNING', 'IPSEC_KP_IKE_INTERMEDIATE', 'PKIX_KP_IPSEC_TUNNEL', 'Code Signing', 'KP_KEY_RECOVERY_AGENT', 'KP_QUALIFIED_SUBORDINATION', 'Early Launch Antimalware Driver', 'Remote Desktop', 'WHQL_CRYPTO', 'EMBEDDED_NT_CRYPTO', 'System Health Authentication', 'DRM', 'PKIX_KP_EMAIL_PROTECTION', 'KP_TIME_STAMP_SIGNING', 'Protected Process Light Verification', 'Endorsement Key Certificate', 'KP_IPSEC_USER', 'PKIX_KP_IPSEC_END_SYSTEM', 'LICENSES', 'Protected Process Verification', 'IdMsKpScLogon', 'HAL Extension', 'KP_OCSP_SIGNING', 'Server Authentication', 'Auto Update End Revocation', 'KP_EFS', 'KP_DOCUMENT_SIGNING', 'Windows Store', 'Kernel Mode Code Signing', 'ENROLLMENT_AGENT', 'ROOT_LIST_SIGNER', 'Windows RT Verification', 'NT5_CRYPTO', 'Revoked List Signer', 'Microsoft Publisher', 'Platform Certificate', ' Windows Software Extension Verification', 'KP_CA_EXCHANGE', 'PKIX_KP_IPSEC_USER', 'Dynamic Code Generator', 'Client Authentication', 'DRM_INDIVIDUALIZATION')]
        [string[]]$ApplicationPolicy,

        [Pki.CATemplate.EnrollmentFlags]$EnrollmentFlags,

        [Pki.CATemplate.PrivateKeyFlags]$PrivateKeyFlags = 0,

        [Pki.CATemplate.KeyUsage]$KeyUsage = 0,

        [int]$Version = 2,

        [timespan]$ValidityPeriod,

        [timespan]$RenewalPeriod,

        [Parameter(Mandatory)]
        [string[]]$SamAccountName,

        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-LogFunctionEntry

    $computer = Get-LabVM -ComputerName $ComputerName
    if (-not $computer)
    {
        Write-Error "The given computer '$ComputerName' could not be found in the lab" -TargetObject $ComputerName
        return
    }

    $variables = Get-Variable -Name KeyUsage, ExtendedKeyUsages, ApplicationPolicies, pkiInternalsTypes, PSBoundParameters
    $functions = Get-Command -Name New-CATemplate, Add-CATemplateStandardPermission, Publish-CATemplate, Get-NextOid, Sync-Parameter, Find-CertificateAuthority

    Invoke-LabCommand -ActivityName "Duplicating CA template $SourceTemplateName -> $TemplateName" -ComputerName $computerName -ScriptBlock {
        Add-Type -TypeDefinition $pkiInternalsTypes

        $p = Sync-Parameter -Command (Get-Command -Name New-CATemplate) -Parameters $ALBoundParameters
        New-CATemplate @p -ErrorVariable e

        if (-not $e)
        {
            $p = Sync-Parameter -Command (Get-Command -Name Add-CATemplateStandardPermission) -Parameters $ALBoundParameters
            Add-CATemplateStandardPermission @p | Out-Null
        }
    } -Variable $variables -Function $functions -PassThru

    Sync-LabActiveDirectory -ComputerName (Get-LabVM -Role RootDC)

    Invoke-LabCommand -ActivityName "Publishing CA template $TemplateName" -ComputerName $ComputerName -ScriptBlock {

        $p = Sync-Parameter -Command (Get-Command -Name Publish-CATemplate, Find-CertificateAuthority) -Parameters $ALBoundParameters
        Publish-CATemplate @p

    } -Function $functions -Variable $variables
}


function Request-LabCertificate
{

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, HelpMessage = 'Please enter the subject beginning with CN=')]
        [ValidatePattern('CN=')]
        [string]$Subject,

        [Parameter(HelpMessage = 'Please enter the SAN domains as a comma separated list')]
        [string[]]$SAN,

        [Parameter(HelpMessage = 'Please enter the Online Certificate Authority')]
        [string]$OnlineCA,

        [Parameter(Mandatory, HelpMessage = 'Please enter the Online Certificate Authority')]
        [string]$TemplateName,

        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    if ($OnlineCA -and -not (Get-LabVM -ComputerName $OnlineCA))
    {
        Write-ScreenInfo -Type Error -Message "Lab does not contain a VM called $OnlineCA, unable to request certificates from it"
        return
    }

    $computer = Get-LabVM -ComputerName $ComputerName

    $caGroups = $computer | Group-Object -Property DomainName

    foreach ($group in $caGroups)
    {
        # Empty group contains workgroup VMs
        if ([string]::IsNullOrWhiteSpace($group.Name) -and -not $OnlineCA)
        {
            Write-ScreenInfo -Type Error "Requesting a certificate from non-domain joined machines $($group.Group -join ',') requires the parameter OnlineCA to be used"
            return
        }

        if ($OnlineCA)
        {
            $onlineCAVM = Get-LabIssuingCA | Where-Object Name -eq $OnlineCA
        }
        else
        {
            $onlineCAVM = Get-LabIssuingCA -DomainName $group.Name
        }

        if (-not $onlineCAVM)
        {
            Write-ScreenInfo -Type Error -Message "No Certificate Authority was found in your lab for domain '$($group.Name)'. Unable to issue certificates for $($group.Group)"
            continue
        }

        # Especially on Azure, the CertSrv was sometimes stopped for no apparent reason
        Invoke-LabCommand -ComputerName $onlineCAVM -ScriptBlock { Start-Service CertSvc } -NoDisplay
        
        $PSBoundParameters.OnlineCA = $onlineCAVM.CaPath
        $variables = Get-Variable -Name PSBoundParameters
        $functions = Get-Command -Name Get-CATemplate, Request-Certificate, Find-CertificateAuthority, Sync-Parameter

        Invoke-LabCommand -ActivityName "Requesting certificate for template '$TemplateName'" -ComputerName $($group.Group) -ScriptBlock {

            Sync-Parameter -Command (Get-Command -Name Request-Certificate)
            Request-Certificate @ALBoundParameters

        } -Variable $variables -Function $functions -PassThru:$PassThru
    }

    Write-LogFunctionExit
}


function Test-LabCATemplate
{

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TemplateName,

        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-LogFunctionEntry

    $computer = Get-LabVM -ComputerName $ComputerName
    if (-not $computer)
    {
        Write-Error "The given computer '$ComputerName' could not be found in the lab" -TargetObject $ComputerName
        return
    }

    $variables = Get-Variable -Name PSBoundParameters
    $functions = Get-Command -Name Test-CATemplate, Sync-Parameter

    Invoke-LabCommand -ActivityName "Testing template $TemplateName" -ComputerName $ComputerName -ScriptBlock {

        $p = Sync-Parameter -Command (Get-Command -Name Test-CATemplate) -Parameters $ALBoundParameters
        Test-CATemplate @p

    } -Function $functions -Variable $variables -PassThru -NoDisplay
}


function Install-LabADDSTrust
{
    $forestNames = (Get-LabVM -Role RootDC).DomainName
    if (-not $forestNames)
    {
        Write-Error 'Could not get forest names from the lab'
        return
    }

    $forwarders = Get-FullMesh -List $forestNames

    foreach ($forwarder in $forwarders)
    {
        $targetMachine = Get-LabVM -Role RootDC | Where-Object { $_.DomainName -eq $forwarder.Source }
        $masterServers = Get-LabVM -Role DC,RootDC,FirstChildDC | Where-Object { $_.DomainName -eq $forwarder.Destination }

        $cmd = @"
            `$hostname = hostname.exe
            Write-Verbose "Creating a DNS forwarder on server '$hostname'. Forwarder name is '$($forwarder.Destination)' and target DNS server is '$($masterServers.IpV4Address)'..."
            #Add-DnsServerConditionalForwarderZone -ReplicationScope Forest -Name $($forwarder.Destination) -MasterServers $($masterServers.IpV4Address)
            dnscmd . /zoneadd $($forwarder.Destination) /dsforwarder $($masterServers.IpV4Address)
            Write-Verbose '...done'
"@


        Invoke-LabCommand -ComputerName $targetMachine -ScriptBlock ([scriptblock]::Create($cmd)) -NoDisplay
    }

    Get-LabVM -Role RootDC | ForEach-Object {
        Invoke-LabCommand -ComputerName $_ -NoDisplay -ScriptBlock {
            Write-Verbose -Message "Replicating forest `$(`$env:USERDNSDOMAIN)..."

            Write-Verbose -Message 'Getting list of DCs'
            $dcs = repadmin.exe /viewlist *
            Write-Verbose -Message "List: '$($dcs -join ', ')'"

            foreach ($dc in $dcs)
            {
                if ($dc)
                {
                    $dcName = $dc.Split()[2]
                    Write-Verbose -Message "Executing 'repadmin.exe /SyncAll /Ae $dcname'"
                    $null = repadmin.exe /SyncAll /AeP $dcName
                }
            }
            Write-Verbose '...done'
        }
    }

    $rootDcs = Get-LabVM -Role RootDC
    $trustMesh = Get-FullMesh -List $forestNames -OneWay

    foreach ($rootDc in $rootDcs)
    {
        $trusts = $trustMesh | Where-Object { $_.Source -eq $rootDc.DomainName }

        Write-PSFMessage "Creating trusts on machine $($rootDc.Name)"
        foreach ($trust in $trusts)
        {
            $domainAdministrator = ((Get-Lab).Domains | Where-Object { $_.Name -eq ($rootDcs | Where-Object { $_.DomainName -eq $trust.Destination }).DomainName }).Administrator

            $cmd = @"
                `$thisForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()

                `$otherForestCtx = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext(
                    [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest,
                    '$($trust.Destination)',
                    '$($domainAdministrator.UserName)',
                    '$($domainAdministrator.Password -replace "'","''" )')
                `$otherForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest(`$otherForestCtx)

                Write-Verbose "Creating forest trust between forests '`$(`$thisForest.Name)' and '`$(`$otherForest.Name)'"

                `$thisForest.CreateTrustRelationship(
                    `$otherForest,
                    [System.DirectoryServices.ActiveDirectory.TrustDirection]::Bidirectional
                )

                Write-Verbose 'Forest trust created'
"@


            Invoke-LabCommand -ComputerName $rootDc -ScriptBlock ([scriptblock]::Create($cmd)) -NoDisplay
        }
    }
}


function Install-LabDcs
{
    [CmdletBinding()]
    param (
        [int]$DcPromotionRestartTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionRestartAfterDcpromo),

        [int]$AdwsReadyTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionAdwsReady),

        [switch]$CreateCheckPoints,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator)
    )

    Write-LogFunctionEntry

    if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role DC | Where-Object { -not $_.SkipDeployment }

    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no machine with the role 'DC'" -Type Warning
        Write-LogFunctionExit
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
    Start-LabVM -RoleName DC -Wait -DoNotUseCredSsp -ProgressIndicator 15 -PostDelaySeconds 5

    #Determine if any machines are already installed as Domain Controllers and exclude these
    $machinesAlreadyInstalled = foreach ($machine in $machines)
    {
        if (Test-LabADReady -ComputerName $machine)
        {
            $machine.Name
        }
    }

    $machines = $machines | Where-Object Name -notin $machinesAlreadyInstalled
    foreach ($m in $machinesAlreadyInstalled)
    {
        Write-ScreenInfo -Message "Machine '$m' is already a Domain Controller. Skipping this machine." -Type Warning
    }

    if ($machines)
    {
        Invoke-LabCommand -ComputerName $machines -ActivityName "Create folder 'C:\DeployDebug' for debug info" -NoDisplay -ScriptBlock {
            New-Item -ItemType Directory -Path 'c:\DeployDebug' -ErrorAction SilentlyContinue | Out-Null

            $acl = Get-Acl -Path C:\DeployDebug
            $rule = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'Read', 'ObjectInherit', 'None', 'Allow')
            $acl.AddAccessRule($rule)
            Set-Acl -Path C:\DeployDebug -AclObject $acl
        } -DoNotUseCredSsp -UseLocalCredential

        $rootDcs = Get-LabVM -Role RootDC
        $childDcs = Get-LabVM -Role FirstChildDC

        $jobs = @()

        foreach ($machine in $machines)
        {
            $dcRole = $machine.Roles | Where-Object Name -like '*DC'

            $isReadOnly = $dcRole.Properties['IsReadOnly']
            if ($isReadOnly -eq 'true')
            {
                $isReadOnly = $true
            }
            else
            {
                $isReadOnly = $false
            }

            #get the root domain to build the root domain credentials
            $parentDc = Get-LabVM -Role RootDC | Where-Object DomainName -eq $lab.GetParentDomain($machine.DomainName).Name
            $parentCredential = $parentDc.GetCredential((Get-Lab))

            Write-PSFMessage -Message 'Invoking script block for DC installation and promotion'
            if ($machine.OperatingSystem.Version -lt 6.2)
            {
                $scriptblock = $adInstallDcPre2012
            }
            else
            {
                $scriptblock = $adInstallDc2012
            }

            $siteName = 'Default-First-Site-Name'

            if ($dcRole.Properties.SiteName)
            {
                $siteName = $dcRole.Properties.SiteName
                New-LabADSite -ComputerName $machine -SiteName $siteName -SiteSubnet $dcRole.Properties.SiteSubnet
            }

            $databasePath = if ($dcRole.Properties.ContainsKey('DatabasePath'))
            {
                $dcRole.Properties.DatabasePath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $logPath = if ($dcRole.Properties.ContainsKey('LogPath'))
            {
                $dcRole.Properties.LogPath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $sysvolPath = if ($dcRole.Properties.ContainsKey('SysvolPath'))
            {
                $dcRole.Properties.SysvolPath
            }
            else
            {
                'C:\Windows\Sysvol'
            }

            $dsrmPassword = if ($dcRole.Properties.ContainsKey('DsrmPassword'))
            {
                $dcRole.Properties.DsrmPassword
            }
            else
            {
                $machine.InstallationUser.Password
            }

            #only print out warnings if verbose logging is enabled
            $WarningPreference = $VerbosePreference

            $jobs += Invoke-LabCommand -ComputerName $machine `
            -ActivityName "Install DC ($($machine.name))" `
            -AsJob `
            -PassThru `
            -UseLocalCredential `
            -NoDisplay `
            -ScriptBlock $scriptblock `
            -ArgumentList $machine.DomainName,
            $parentCredential,
            $isReadOnly,
            7,
            120,
            $siteName,
            $DatabasePath,
            $LogPath,
            $SysvolPath,
            $DsrmPassword
        }

        Write-ScreenInfo -Message 'Waiting for additional Domain Controllers to complete installation of Active Directory and restart' -NoNewLine

        $domains = (Get-LabVM -Role DC).DomainName

        $machinesToStart = @()
        #starting machines in a multi net environment may not work
        if (-not (Get-LabVM -Role Routing))
        {
            $machinesToStart += Get-LabVM | Where-Object { -not $_.IsDomainJoined }
            $machinesToStart += Get-LabVM | Where-Object DomainName -notin $domains
        }

        # Creating sessions from a Linux host requires the correct user name.
        # By setting HasDomainJoined to $true we ensure that not the local, but the domain admin cred is returned
        foreach ($machine in $machines)
        {
            $machine.HasDomainJoined = $true
        }

        if ($lab.DefaultVirtualizationEngine -ne 'Azure')
        {
            Wait-LabVMRestart -ComputerName $machines -StartMachinesWhileWaiting $machinesToStart -TimeoutInMinutes $DcPromotionRestartTimeout -MonitorJob $jobs -ProgressIndicator 60 -NoNewLine -ErrorAction Stop
            Write-ScreenInfo -Message done

            Write-ScreenInfo -Message 'Additional Domain Controllers have now restarted. Waiting for Active Directory to start up' -NoNewLine

            #Wait a little to be able to connect in first attempt
            Wait-LWLabJob -Job (Start-Job -Name 'Delay waiting for machines to be reachable' -ScriptBlock {
                    Start-Sleep -Seconds 60
            }) -ProgressIndicator 20 -NoDisplay -NoNewLine

            Wait-LabVM -ComputerName $machines -TimeoutInMinutes 30 -ProgressIndicator 20 -NoNewLine
        }

        Wait-LabADReady -ComputerName $machines -TimeoutInMinutes $AdwsReadyTimeout -ErrorAction Stop -ProgressIndicator 20 -NoNewLine

        #Restart the Network Location Awareness service to ensure that Windows Firewall Profile is 'Domain'
        Restart-ServiceResilient -ComputerName $machines -ServiceName nlasvc -NoNewLine

        Enable-LabVMRemoting -ComputerName $machines
        Enable-LabAutoLogon -ComputerName $machines

        #DNS client configuration is change by DCpromo process. Change this back
        Reset-DNSConfiguration -ComputerName (Get-LabVM -Role DC) -ProgressIndicator 20 -NoNewLine

        Write-PSFMessage -Message 'Restarting DNS and Netlogon services on all Domain Controllers and triggering replication'
        $jobs = @()
        foreach ($dc in (Get-LabVM -Role RootDC))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 20 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoDisplay -NoNewLine
        $jobs = @()
        foreach ($dc in (Get-LabVM -Role FirstChildDC))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 20 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoDisplay -NoNewLine
        $jobs = @()
        foreach ($dc in (Get-LabVM -Role DC))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 20 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoDisplay -NoNewLine
        Write-ProgressIndicatorEnd

        if ($CreateCheckPoints)
        {
            foreach ($machine in ($machines | Where-Object HostType -eq 'HyperV'))
            {
                Checkpoint-LWVM -ComputerName $machine -SnapshotName 'Post DC Promotion'
            }
        }
    }
    else
    {
        Write-ScreenInfo -Message 'All additional Domain Controllers are already installed' -Type Warning -TaskEnd
        return
    }

    Get-PSSession | Where-Object { $_.Name -ne 'WinPSCompatSession' -and $_.State -ne 'Disconnected'} | Remove-PSSession

    Write-LogFunctionExit
}


function Install-LabDnsForwarder
{
    $forestNames = (Get-LabVM -Role RootDC).DomainName
    if (-not $forestNames)
    {
        Write-Error 'Could not get forest names from the lab'
        return
    }

    $forwarders = Get-FullMesh -List $forestNames

    foreach ($forwarder in $forwarders)
    {
        $targetMachine = Get-LabVM -Role RootDC | Where-Object { $_.DomainName -eq $forwarder.Source }
        $masterServers = Get-LabVM -Role DC,RootDC,FirstChildDC | Where-Object { $_.DomainName -eq $forwarder.Destination }

        $cmd = @"
            `$hostname = hostname.exe
            Write-Verbose "Creating a DNS forwarder on server '$hostname'. Forwarder name is '$($forwarder.Destination)' and target DNS server is '$($masterServers.IpV4Address)'..."
            #Add-DnsServerConditionalForwarderZone -ReplicationScope Forest -Name $($forwarder.Destination) -MasterServers $($masterServers.IpV4Address)
            dnscmd . /zoneadd $($forwarder.Destination) /dsforwarder $($masterServers.IpV4Address)
            Write-Verbose '...done'
"@


        Invoke-LabCommand -ComputerName $targetMachine -ScriptBlock ([scriptblock]::Create($cmd)) -NoDisplay
    }

    $azureRootDCs = Get-LabVM -Role RootDC | Where-Object HostType -eq Azure
    if ($azureRootDCs)
    {
        Invoke-LabCommand -ActivityName 'Configuring DNS Forwarders on Azure Root DCs' -ComputerName $azureRootDCs -ScriptBlock {
            dnscmd /ResetForwarders 168.63.129.16
        } -NoDisplay
    }
}


function Install-LabFirstChildDcs
{
    [CmdletBinding()]
    param (
        [int]$DcPromotionRestartTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionRestartAfterDcpromo),

        [int]$AdwsReadyTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionAdwsReady),

        [switch]$CreateCheckPoints,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator)
    )

    Write-LogFunctionEntry

    if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role FirstChildDC | Where-Object { -not $_.SkipDeployment }
    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no machine with the role 'FirstChildDC'" -Type Warning
        Write-LogFunctionExit
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
    Start-LabVM -RoleName FirstChildDC -Wait -DoNotUseCredSsp -ProgressIndicator 15 -PostDelaySeconds 5

    #Determine if any machines are already installed as Domain Controllers and exclude these
    $machinesAlreadyInstalled = foreach ($machine in $machines)
    {
        if (Test-LabADReady -ComputerName $machine)
        {
            $machine.Name
        }
    }

    $machines = $machines | Where-Object Name -notin $machinesAlreadyInstalled
    foreach ($m in $machinesAlreadyInstalled)
    {
        Write-ScreenInfo -Message "Machine '$m' is already a Domain Controller. Skipping this machine." -Type Warning
    }

    if ($machines)
    {
        Invoke-LabCommand -ComputerName $machines -ActivityName "Create folder 'C:\DeployDebug' for debug info" -NoDisplay -ScriptBlock {
            New-Item -ItemType Directory -Path 'c:\DeployDebug' -ErrorAction SilentlyContinue | Out-Null

            $acl = Get-Acl -Path C:\DeployDebug
            $rule = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'Read', 'ObjectInherit', 'None', 'Allow')
            $acl.AddAccessRule($rule)
            Set-Acl -Path C:\DeployDebug -AclObject $acl
        } -DoNotUseCredSsp -UseLocalCredential

        $jobs = @()
        foreach ($machine in $machines)
        {
            $dcRole = $machine.Roles | Where-Object Name -eq 'FirstChildDc'

            $parentDomainName = $dcRole.Properties['ParentDomain']
            $newDomainName = $dcRole.Properties['NewDomain']
            $domainFunctionalLevel = $dcRole.Properties['DomainFunctionalLevel']
            $parentDomain = $lab.Domains | Where-Object Name -eq $parentDomainName

            #get the root domain to build the root domain credentials
            if (-not $parentDomain)
            {
                throw "New domain '$newDomainName' could not be installed. The root domain ($parentDomainName) could not be found in the lab"
            }
            $rootCredential = $parentDomain.GetCredential()

            #if there is a '.' inside the domain name, it is a new domain tree, otherwise a child domain hence we need to
            #create a DNS zone for the child domain in the parent domain
            if ($NewDomainName.Contains('.'))
            {
                $parentDc = Get-LabVM -Role RootDC, FirstChildDC | Where-Object DomainName -eq $ParentDomainName
                Write-PSFMessage -Message "Setting up a new domain tree hence creating a stub zone on Domain Controller '$($parentDc.Name)'"

                $cmd = "dnscmd . /zoneadd $NewDomainName /dsstub $((Get-LabVM -Role RootDC,FirstChildDC,DC | Where-Object DomainName -eq $NewDomainName).IpV4Address -join ', ') /dp /forest"

                Invoke-LabCommand -ActivityName 'Add DNS zones' -ComputerName $parentDc -ScriptBlock ([scriptblock]::Create($cmd)) -NoDisplay
                Invoke-LabCommand -ActivityName 'Restart DNS' -ComputerName $parentDc -ScriptBlock { Restart-Service -Name Dns } -NoDisplay
            }

            Write-PSFMessage -Message 'Invoking script block for DC installation and promotion'
            if ($machine.OperatingSystem.Version -lt 6.2)
            {
                $scriptBlock = $adInstallFirstChildDcPre2012
                $domainFunctionalLevel = [int][AutomatedLab.ActiveDirectoryFunctionalLevel]$domainFunctionalLevel
            }
            else
            {
                $scriptBlock = $adInstallFirstChildDc2012
            }


            $siteName = 'Default-First-Site-Name'

            if ($dcRole.Properties.SiteName)
            {
                $siteName = $dcRole.Properties.SiteName
                New-LabADSite -ComputerName $machine -SiteName $siteName -SiteSubnet $dcRole.Properties.SiteSubnet
            }

            $databasePath = if ($dcRole.Properties.ContainsKey('DatabasePath'))
            {
                $dcRole.Properties.DatabasePath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $logPath = if ($dcRole.Properties.ContainsKey('LogPath'))
            {
                $dcRole.Properties.LogPath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $sysvolPath = if ($dcRole.Properties.ContainsKey('SysvolPath'))
            {
                $dcRole.Properties.SysvolPath
            }
            else
            {
                'C:\Windows\Sysvol'
            }

            $dsrmPassword = if ($dcRole.Properties.ContainsKey('DsrmPassword'))
            {
                $dcRole.Properties.DsrmPassword
            }
            else
            {
                $machine.InstallationUser.Password
            }

            #only print out warnings if verbose logging is enabled
            $WarningPreference = $VerbosePreference

            $jobs += Invoke-LabCommand -ComputerName $machine.Name `
            -ActivityName "Install FirstChildDC ($($machine.Name))" `
            -AsJob `
            -PassThru `
            -UseLocalCredential `
            -NoDisplay `
            -ScriptBlock $scriptBlock `
            -ArgumentList $newDomainName,
            $parentDomainName,
            $rootCredential,
            $domainFunctionalLevel,
            7,
            120,
            $siteName,
            $dcRole.Properties.NetBiosDomainName,
            $DatabasePath,
            $LogPath,
            $SysvolPath,
            $DsrmPassword
        }


        Write-ScreenInfo -Message 'Waiting for First Child Domain Controllers to complete installation of Active Directory and restart' -NoNewline

        $domains = @((Get-LabVM -Role RootDC).DomainName)
        foreach ($domain in $domains)
        {
            if (Get-LabVM -Role DC | Where-Object DomainName -eq $domain)
            {
                $domains = $domain | Where-Object { $_ -ne $domain }
            }
        }

        $machinesToStart = @()
        $machinesToStart += Get-LabVM -Role DC
        #starting machines in a multi net environment may not work at this point of the deployment
        if (-not (Get-LabVM -Role Routing))
        {
            $machinesToStart += Get-LabVM | Where-Object { -not $_.IsDomainJoined }
            $machinesToStart += Get-LabVM | Where-Object DomainName -in $domains
        }

        # Creating sessions from a Linux host requires the correct user name.
        # By setting HasDomainJoined to $true we ensure that not the local, but the domain admin cred is returned
        foreach ($machine in $machines)
        {
            $machine.HasDomainJoined = $true
        }

        if ($lab.DefaultVirtualizationEngine -ne 'Azure')
        {
            Wait-LabVMRestart -ComputerName $machines.name -StartMachinesWhileWaiting $machinesToStart -ProgressIndicator 45 -TimeoutInMinutes $DcPromotionRestartTimeout -ErrorAction Stop -MonitorJob $jobs -NoNewLine
            Write-ScreenInfo done

            Write-ScreenInfo -Message 'First Child Domain Controllers have now restarted. Waiting for Active Directory to start up' -NoNewLine

            #Wait a little to be able to connect in first attempt
            Wait-LWLabJob -Job (Start-Job -Name 'Delay waiting for machines to be reachable' -ScriptBlock { Start-Sleep -Seconds 60 }) -ProgressIndicator 20 -NoDisplay -NoNewLine

            Wait-LabVM -ComputerName $machines -TimeoutInMinutes 30 -ProgressIndicator 20 -NoNewLine
        }
        Wait-LabADReady -ComputerName $machines -TimeoutInMinutes $AdwsReadyTimeout -ErrorAction Stop -ProgressIndicator 20 -NoNewLine

        #Make sure the specified installation user will be domain admin
        Invoke-LabCommand -ActivityName 'Make installation user Domain Admin' -ComputerName $machines -ScriptBlock {
            $PSDefaultParameterValues = @{
                '*-AD*:Server' = $env:COMPUTERNAME
            }

            $user = Get-ADUser -Identity ([System.Security.Principal.WindowsIdentity]::GetCurrent().User)

            Add-ADGroupMember -Identity 'Domain Admins' -Members $user
        } -NoDisplay

        Invoke-LabCommand -ActivityName 'Add flat domain name DNS record to speed up start of gpsvc in 2016' -ComputerName $machines -ScriptBlock {
            $machine = $args[0] | Where-Object { $_.Name -eq $env:COMPUTERNAME }
            dnscmd localhost /recordadd $env:USERDNSDOMAIN $env:USERDOMAIN A $machine.IpV4Address
        } -ArgumentList $machines -NoDisplay

        Restart-LabVM -ComputerName $machines -Wait -NoDisplay -NoNewLine
        Wait-LabADReady -ComputerName $machines -NoNewLine

        Enable-LabVMRemoting -ComputerName $machines

        #Restart the Network Location Awareness service to ensure that Windows Firewall Profile is 'Domain'
        Restart-ServiceResilient -ComputerName $machines -ServiceName nlasvc -NoNewLine

        #DNS client configuration is change by DCpromo process. Change this back
        Reset-DNSConfiguration -ComputerName (Get-LabVM -Role FirstChildDC) -ProgressIndicator 20 -NoNewLine

        Write-PSFMessage -Message 'Restarting DNS and Netlogon services on Root and Child Domain Controllers and triggering replication'
        $jobs = @()
        foreach ($dc in (@(Get-LabVM -Role RootDC)))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 20 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoDisplay -NoNewLine
        $jobs = @()
        foreach ($dc in (@(Get-LabVM -Role FirstChildDC)))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 20 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 20 -NoDisplay -NoNewLine
        
        foreach ($machine in $machines)
        {
            Reset-LabAdPassword -DomainName $machine.DomainName
            Remove-LabPSSession -ComputerName $machine
            Enable-LabAutoLogon -ComputerName $machine
        }

        if ($CreateCheckPoints)
        {
            foreach ($machine in ($machines | Where-Object HostType -eq 'HyperV'))
            {
                Checkpoint-LWVM -ComputerName $machine -SnapshotName 'Post DC Promotion'
            }
        }
    }
    else
    {
        Write-ScreenInfo -Message 'All First Child Domain Controllers are already installed' -Type Warning -TaskEnd
        return
    }

    Get-PSSession | Where-Object { $_.Name -ne 'WinPSCompatSession' -and $_.State -ne 'Disconnected'} | Remove-PSSession

    #this sections is required to join all machines to the domain. This is happening when starting the machines, that's why all machines are started.
    $domains = $machines.DomainName
    $filterScript = {-not $_.SkipDeployment -and 'RootDC' -notin $_.Roles.Name -and 'FirstChildDC' -notin $_.Roles.Name -and 'DC' -notin $_.Roles.Name -and
    -not $_.HasDomainJoined -and $_.DomainName -in $domains -and $_.HostType -eq 'Azure' }
    $retries = 3

    while ((Get-LabVM | Where-Object -FilterScript $filterScript) -or $retries -le 0 )
    {
        $machinesToJoin = Get-LabVM | Where-Object -FilterScript $filterScript

        Write-ScreenInfo "Restarting the $($machinesToJoin.Count) machines to complete the domain join of ($($machinesToJoin.Name -join ', ')). Retries remaining = $retries"
        Restart-LabVM -ComputerName $machinesToJoin -Wait
        $retries--
    }

    Write-ProgressIndicatorEnd
    Write-LogFunctionExit
}


function Install-LabRootDcs
{
    [CmdletBinding()]
    param (
        [int]$DcPromotionRestartTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionRestartAfterDcpromo),

        [int]$AdwsReadyTimeout = (Get-LabConfigurationItem -Name Timeout_DcPromotionAdwsReady),

        [switch]$CreateCheckPoints,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator)
    )

    Write-LogFunctionEntry

    if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role RootDC | Where-Object { -not $_.SkipDeployment }

    if (-not $machines)
    {
        Write-ScreenInfo -Message "There is no machine with the role 'RootDC'" -Type Warning
        Write-LogFunctionExit
        return
    }


    Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewline
    Start-LabVM -RoleName RootDC -Wait -DoNotUseCredSsp -ProgressIndicator 10 -PostDelaySeconds 5

    #Determine if any machines are already installed as Domain Controllers and exclude these
    $machinesAlreadyInstalled = foreach ($machine in $machines)
    {
        if (Test-LabADReady -ComputerName $machine)
        {
            $machine.Name
        }
    }

    $machines = $machines | Where-Object Name -notin $machinesAlreadyInstalled
    foreach ($m in $machinesAlreadyInstalled)
    {
        Write-ScreenInfo -Message "Machine '$m' is already a Domain Controller. Skipping this machine." -Type Warning
    }

    $jobs = @()
    if ($machines)
    {
        Invoke-LabCommand -ComputerName $machines -ActivityName "Create folder 'C:\DeployDebug' for debug info" -NoDisplay -ScriptBlock {
            New-Item -ItemType Directory -Path 'c:\DeployDebug' -ErrorAction SilentlyContinue | Out-Null

            $acl = Get-Acl -Path C:\DeployDebug
            $rule = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'Read', 'ObjectInherit', 'None', 'Allow')
            $acl.AddAccessRule($rule)
            Set-Acl -Path C:\DeployDebug -AclObject $acl
        } -DoNotUseCredSsp -UseLocalCredential

        foreach ($machine in $machines)
        {
            $dcRole = $machine.Roles | Where-Object Name -eq 'RootDC'

            if ($machine.OperatingSystem.Version -lt 6.2)
            {
                #Pre 2012
                $scriptblock = $adInstallRootDcScriptPre2012
                $forestFunctionalLevel = [int][AutomatedLab.ActiveDirectoryFunctionalLevel]$dcRole.Properties.ForestFunctionalLevel
                $domainFunctionalLevel = [int][AutomatedLab.ActiveDirectoryFunctionalLevel]$dcRole.Properties.DomainFunctionalLevel
            }
            else
            {
                $scriptblock = $adInstallRootDcScript2012
                $forestFunctionalLevel = $dcRole.Properties.ForestFunctionalLevel
                $domainFunctionalLevel = $dcRole.Properties.DomainFunctionalLevel
            }

            $netBiosDomainName = if ($dcRole.Properties.ContainsKey('NetBiosDomainName'))
            {
                $dcRole.Properties.NetBiosDomainName
            }
            else
            {
                $machine.DomainName.Substring(0, $machine.DomainName.IndexOf('.'))
            }

            $databasePath = if ($dcRole.Properties.ContainsKey('DatabasePath'))
            {
                $dcRole.Properties.DatabasePath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $logPath = if ($dcRole.Properties.ContainsKey('LogPath'))
            {
                $dcRole.Properties.LogPath
            }
            else
            {
                'C:\Windows\NTDS'
            }

            $sysvolPath = if ($dcRole.Properties.ContainsKey('SysvolPath'))
            {
                $dcRole.Properties.SysvolPath
            }
            else
            {
                'C:\Windows\Sysvol'
            }

            $dsrmPassword = if ($dcRole.Properties.ContainsKey('DsrmPassword'))
            {
                $dcRole.Properties.DsrmPassword
            }
            else
            {
                $machine.InstallationUser.Password
            }

            #only print out warnings if verbose logging is enabled
            $WarningPreference = $VerbosePreference

            $jobs += Invoke-LabCommand -ComputerName $machine.Name `
            -ActivityName "Install Root DC ($($machine.name))" `
            -AsJob `
            -UseLocalCredential `
            -DoNotUseCredSsp `
            -PassThru `
            -NoDisplay `
            -ScriptBlock $scriptblock `
            -ArgumentList $machine.DomainName,
            $machine.InstallationUser.Password,
            $forestFunctionalLevel,
            $domainFunctionalLevel,
            $netBiosDomainName,
            $DatabasePath,
            $LogPath,
            $SysvolPath,
            $DsrmPassword
        }


        Write-ScreenInfo -Message 'Waiting for Root Domain Controllers to complete installation of Active Directory and restart' -NoNewLine

        $machinesToStart = @()
        $machinesToStart += Get-LabVM -Role FirstChildDC, DC
        #starting machines in a multi net environment may not work
        if (-not (Get-LabVM -Role Routing))
        {
            $machinesToStart += Get-LabVM | Where-Object { -not $_.IsDomainJoined }
        }

        # Creating sessions from a Linux host requires the correct user name.
        # By setting HasDomainJoined to $true we ensure that not the local, but the domain admin cred is returned
        foreach ($machine in $machines)
        {
            $machine.HasDomainJoined = $true
        }

        if ($lab.DefaultVirtualizationEngine -ne 'Azure')
        {
            Wait-LabVMRestart -ComputerName $machines.Name -StartMachinesWhileWaiting $machinesToStart -DoNotUseCredSsp -ProgressIndicator 30 -TimeoutInMinutes $DcPromotionRestartTimeout -ErrorAction Stop -MonitorJob $jobs -NoNewLine
            Write-ScreenInfo -Message done

            Write-ScreenInfo -Message 'Root Domain Controllers have now restarted. Waiting for Active Directory to start up' -NoNewLine

            Wait-LabVM -ComputerName $machines -DoNotUseCredSsp -TimeoutInMinutes 30 -ProgressIndicator 30 -NoNewLine
        }
        Wait-LabADReady -ComputerName $machines -TimeoutInMinutes $AdwsReadyTimeout -ErrorAction Stop -ProgressIndicator 30 -NoNewLine

        #Create reverse lookup zone (forest scope)
        foreach ($network in ((Get-LabVirtualNetworkDefinition).AddressSpace.IpAddress.AddressAsString))
        {
            Invoke-LabCommand -ActivityName 'Create reverse lookup zone' -ComputerName $machines[0] -ScriptBlock {
                param
                (
                    [string]$ip
                )

                $zoneName = "$($ip.split('.')[2]).$($ip.split('.')[1]).$($ip.split('.')[0]).in-addr.arpa"
                dnscmd . /ZoneAdd "$zoneName" /DsPrimary /DP /forest
                dnscmd . /Config "$zoneName" /AllowUpdate 2
                ipconfig.exe -registerdns
            } -ArgumentList $network -NoDisplay
        }


        #Make sure the specified installation user will be forest admin
        Invoke-LabCommand -ActivityName 'Make installation user Domain Admin' -ComputerName $machines -ScriptBlock {
            $PSDefaultParameterValues = @{
                '*-AD*:Server' = $env:COMPUTERNAME
            }

            $user = Get-ADUser -Identity ([System.Security.Principal.WindowsIdentity]::GetCurrent().User) -Server localhost

            Add-ADGroupMember -Identity 'Domain Admins' -Members $user -Server localhost
            Add-ADGroupMember -Identity 'Enterprise Admins' -Members $user -Server localhost
            Add-ADGroupMember -Identity 'Schema Admins' -Members $user -Server localhost
        } -NoDisplay -ErrorAction SilentlyContinue

        #Non-domain-joined machine are not registered in DNS hence cannot be found from inside the lab.
        #creating an A record for each non-domain-joined machine in the first forst solves that.
        #Every non-domain-joined machine get the first forest's name as the primary DNS domain.
        $dnsCmd = Get-LabVM -All -IncludeLinux | Where-Object { -not $_.IsDomainJoined -and $_.IpV4Address } | ForEach-Object {
            "dnscmd /recordadd $(@($rootDomains)[0]) $_ A $($_.IpV4Address)`n"
        }
        $dnsCmd += "Restart-Service -Name DNS -WarningAction SilentlyContinue`n"
        Invoke-LabCommand -ActivityName 'Register non domain joined machines in DNS' -ComputerName $machines[0]`
        -ScriptBlock ([scriptblock]::Create($dnsCmd)) -NoDisplay

        Invoke-LabCommand -ActivityName 'Add flat domain name DNS record to speed up start of gpsvc in 2016' -ComputerName $machines -ScriptBlock {
            $machine = $args[0] | Where-Object { $_.Name -eq $env:COMPUTERNAME }
            dnscmd localhost /recordadd $env:USERDNSDOMAIN $env:USERDOMAIN A $machine.IpV4Address
        } -ArgumentList $machines -NoDisplay

        # Configure DNS forwarders for Azure machines to be able to mount LabSoures
        Install-LabDnsForwarder

        $linuxMachines = Get-LabVM -All -IncludeLinux | Where-Object -Property OperatingSystemType -eq 'Linux'

        if ($linuxMachines)
        {
            $rootDomains = $machines | Group-Object -Property DomainName
            foreach($root in $rootDomains)
            {
                $domainJoinedMachines = ($linuxMachines | Where-Object DomainName -eq $root.Name).Name
                if (-not $domainJoinedMachines) { continue }
                $oneTimePassword = ($root.Group)[0].InstallationUser.Password
                Invoke-LabCommand -ActivityName 'Add computer objects for domain-joined Linux machines' -ComputerName ($root.Group)[0] -ScriptBlock {
                    foreach ($m in $domainJoinedMachines) { New-ADComputer -Name $m -AccountPassword ($oneTimePassword | ConvertTo-SecureString -AsPlaintext -Force)}
                } -Variable (Get-Variable -Name domainJoinedMachines,oneTimePassword) -NoDisplay
            }
        }

        Enable-LabVMRemoting -ComputerName $machines

        #Restart the Network Location Awareness service to ensure that Windows Firewall Profile is 'Domain'
        Restart-ServiceResilient -ComputerName $machines -ServiceName nlasvc -NoNewLine

        #DNS client configuration is change by DCpromo process. Change this back
        Reset-DNSConfiguration -ComputerName (Get-LabVM -Role RootDC) -ProgressIndicator 30 -NoNewLine

        #Need to make sure that A records for domain is registered
        Write-PSFMessage -Message 'Restarting DNS and Netlogon service on Root Domain Controllers'
        $jobs = @()
        foreach ($dc in (@(Get-LabVM -Role RootDC)))
        {
            $jobs += Sync-LabActiveDirectory -ComputerName $dc -ProgressIndicator 5 -AsJob -Passthru
        }
        Wait-LWLabJob -Job $jobs -ProgressIndicator 5 -NoDisplay -NoNewLine

        foreach ($machine in $machines)
        {
            $dcRole = $machine.Roles | Where-Object Name -like '*DC'

            if ($dcRole.Properties.SiteName)
            {
                New-LabADSite -ComputerName $machine -SiteName $dcRole.Properties.SiteName -SiteSubnet $dcRole.Properties.SiteSubnet
                Move-LabDomainController -ComputerName $machine -SiteName $dcRole.Properties.SiteName
            }

            Reset-LabAdPassword -DomainName $machine.DomainName
            Remove-LabPSSession -ComputerName $machine
            Enable-LabAutoLogon -ComputerName $machine
        }

        if ($CreateCheckPoints)
        {
            foreach ($machine in ($machines | Where-Object HostType -eq 'HyperV'))
            {
                Checkpoint-LWVM -ComputerName $machine -SnapshotName 'Post DC Promotion'
            }
        }
    }
    else
    {
        Write-ScreenInfo -Message 'All Root Domain Controllers are already installed' -Type Warning -TaskEnd
        return
    }
    Get-PSSession | Where-Object { $_.Name -ne 'WinPSCompatSession' -and $_.State -ne 'Disconnected'} | Remove-PSSession

    #this sections is required to join all machines to the domain. This is happening when starting the machines, that's why all machines are started.
    $domains = $machines.DomainName
    $filterScript = {-not $_.SkipDeployment -and 'RootDC' -notin $_.Roles.Name -and 'FirstChildDC' -notin $_.Roles.Name -and 'DC' -notin $_.Roles.Name -and
    -not $_.HasDomainJoined -and $_.DomainName -in $domains -and $_.HostType -eq 'Azure' }
    $retries = 3

    while ((Get-LabVM | Where-Object -FilterScript $filterScript) -and $retries -ge 0 )
    {
        $machinesToJoin = Get-LabVM | Where-Object -FilterScript $filterScript

        Write-ScreenInfo -Message ''
        Write-ScreenInfo "Restarting the $($machinesToJoin.Count) machines to complete the domain join of ($($machinesToJoin.Name -join ', ')). Retries remaining = $retries"
        Restart-LabVM -ComputerName $machinesToJoin -Wait -NoNewLine
        $retries--
    }
    Write-ProgressIndicatorEnd

    Write-LogFunctionExit
}


function New-LabADSubnet
{
    [CmdletBinding()]
    param(
        [switch]$PassThru
    )

    Write-LogFunctionEntry

    $createSubnetScript = {
        param(
            $NetworkInfo
        )

        $PSDefaultParameterValues = @{
            '*-AD*:Server' = $env:COMPUTERNAME
        }

        #$defaultSite = Get-ADReplicationSite -Identity Default-First-Site-Name -Server localhost
        $ctx = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest)
        $defaultSite = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::FindByName($ctx, 'Default-First-Site-Name')
        $subnetName = "$($NetworkInfo.Network)/$($NetworkInfo.MaskLength)"

        try
        {
            $subnet = Get-ADReplicationSubnet -Identity $subnetName -Server localhost
        }
        catch { }

        if (-not $subnet)
        {
            #New-ADReplicationSubnet seems to have a bug and reports Access Denied.
            #New-ADReplicationSubnet -Name $subnetName -Site $defaultSite -PassThru -Server localhost
            $subnet = New-Object System.DirectoryServices.ActiveDirectory.ActiveDirectorySubnet($ctx, $subnetName)
            $subnet.Site = $defaultSite
            $subnet.Save()
        }
    }

    $machines = Get-LabVM -Role RootDC, FirstChildDC
    $lab = Get-Lab

    foreach ($machine in $machines)
    {
        $ipAddress = ($machine.IpAddress -split '/')[0]
        
        if ($ipAddress -eq '0.0.0.0') {
            $ipAddress = Get-NetIPAddress -AddressFamily IPv4 | Where-Object InterfaceAlias -eq "vEthernet ($($machine.Network))"
        }
        $ipPrefix = ($machine.IpAddress -split '/')[1]
        $subnetMask = if ([int]$ipPrefix) {        
            $ipPrefix | ConvertTo-Mask
        }
        else {
            $ipAddress.PrefixLength | ConvertTo-Mask
            $ipAddress = $ipAddress.IPAddress
        }

        $networkInfo = Get-NetworkSummary -IPAddress $ipAddress -SubnetMask $subnetMask
        Write-PSFMessage -Message "Creating subnet '$($networkInfo.Network)' with mask '$($networkInfo.MaskLength)' on machine '$($machine.Name)'"

        #if the machine is not a Root Domain Controller
        if (-not ($machine.Roles | Where-Object { $_.Name -eq 'RootDC'}))
        {
            $rootDc = $machines | Where-Object { $_.Roles.Name -eq 'RootDC' -and $_.DomainName -eq $lab.GetParentDomain($machine.DomainName) }
        }
        else
        {
            $rootDc = $machine
        }

        if ($rootDc)
        {
            Invoke-LabCommand -ComputerName $rootDc -ActivityName 'Create AD Subnet' -NoDisplay `
            -ScriptBlock $createSubnetScript -AsJob -ArgumentList $networkInfo
        }
        else
        {
            Write-ScreenInfo -Message 'Root domain controller could not be found, cannot Create AD Subnet automatically.' -Type Warning
        }
    }

    Write-LogFunctionExit
}


function Sync-LabActiveDirectory
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [int]$ProgressIndicator,

        [switch]$AsJob,

        [switch]$Passthru
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName
    $lab = Get-Lab

    if (-not $machines)
    {
        Write-Error "The machine '$ComputerName' could not be found in the current lab"
        return
    }

    foreach ($machine in $machines)
    {
        if (-not $machine.DomainName)
        {
            Write-PSFMessage -Message 'The machine is not domain joined hence AD replication cannot be triggered'
            return
        }

        #region Force Replication Scriptblock
        $adForceReplication = {
            $VerbosePreference = $using:VerbosePreference

            ipconfig.exe -flushdns

            if (-not -(Test-Path -Path C:\DeployDebug))
            {
                New-Item C:\DeployDebug -Force -ItemType Directory | Out-Null
            }

            Write-Verbose -Message 'Getting list of DCs'
            $dcs = repadmin.exe /viewlist *
            Write-Verbose -Message "List: '$($dcs -join ', ')'"
            (Get-Date -Format 'yyyy-MM-dd hh:mm:ss') | Add-Content -Path c:\DeployDebug\DCList.log -Force
            $dcs | Add-Content -Path c:\DeployDebug\DCList.log

            foreach ($dc in $dcs)
            {
                if ($dc)
                {
                    $dcName = $dc.Split()[2]
                    Write-Verbose -Message "Executing 'repadmin.exe /SyncAll /Ae $dcname'"
                    $result = repadmin.exe /SyncAll /Ae $dcName
                    (Get-Date -Format 'yyyy-MM-dd hh:mm:ss') | Add-Content -Path "c:\DeployDebug\Syncs-$($dcName).log" -Force
                    $result | Add-Content -Path "c:\DeployDebug\Syncs-$($dcName).log"
                }
            }
            Write-Verbose -Message "Executing 'repadmin.exe /ReplSum'"
            $result = repadmin.exe /ReplSum
            $result | Add-Content -Path c:\DeployDebug\repadmin.exeResult.log

            Restart-Service -Name DNS -WarningAction SilentlyContinue

            ipconfig.exe /registerdns

            Write-Verbose -Message 'Getting list of DCs'
            $dcs = repadmin.exe /viewlist *
            Write-Verbose -Message "List: '$($dcs -join ', ')'"
            (Get-Date -Format 'yyyy-MM-dd hh:mm:ss') | Add-Content -Path c:\DeployDebug\DCList.log -Force
            $dcs | Add-Content -Path c:\DeployDebug\DCList.log
            foreach ($dc in $dcs)
            {
                if ($dc)
                {
                    $dcName = $dc.Split()[2]
                    Write-Verbose -Message "Executing 'repadmin.exe /SyncAll /Ae $dcname'"
                    $result = repadmin.exe /SyncAll /Ae $dcName
                    (Get-Date -Format 'yyyy-MM-dd hh:mm:ss') | Add-Content -Path "c:\DeployDebug\Syncs-$($dcName).log" -Force
                    $result | Add-Content -Path "c:\DeployDebug\Syncs-$($dcName).log"
                }
            }
            Write-Verbose -Message "Executing 'repadmin.exe /ReplSum'"
            $result = repadmin.exe /ReplSum
            $result | Add-Content -Path c:\DeployDebug\repadmin.exeResult.log

            ipconfig.exe /registerdns

            Restart-Service -Name DNS -WarningAction SilentlyContinue

            #for debugging
            #dnscmd /zoneexport $env:USERDNSDOMAIN "c:\DeployDebug\$($env:USERDNSDOMAIN).txt"
        }
        #endregion Force Replication Scriptblock

        Invoke-LabCommand -ActivityName "Performing ipconfig /registerdns on '$ComputerName'" `
        -ComputerName $ComputerName -ScriptBlock { ipconfig.exe /registerdns } -NoDisplay

        if ($AsJob)
        {
            $job = Invoke-LabCommand -ActivityName "Triggering replication on '$ComputerName'" -ComputerName $ComputerName -ScriptBlock $adForceReplication -AsJob -Passthru -NoDisplay

            if ($PassThru)
            {
                $job
            }
        }
        else
        {
            $result = Invoke-LabCommand -ActivityName "Triggering replication on '$ComputerName'" -ComputerName $ComputerName -ScriptBlock $adForceReplication -Passthru -NoDisplay

            if ($PassThru)
            {
                $result
            }
        }
    }

    Write-LogFunctionExit
}


function Test-LabADReady
{
    param (
        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-LogFunctionEntry

    $machine = Get-LabVM -ComputerName $ComputerName
    if (-not $machine)
    {
        Write-Error "The machine '$ComputerName' could not be found in the lab"
        return
    }

    $adReady = Invoke-LabCommand -ComputerName $machine -ActivityName GetAdwsServiceStatus -ScriptBlock {

        if ((Get-Service -Name ADWS -ErrorAction SilentlyContinue).Status -eq 'Running')
        {
            try
            {
                $env:ADPS_LoadDefaultDrive = 0
                $WarningPreference = 'SilentlyContinue'
                Import-Module -Name ActiveDirectory -ErrorAction Stop
                [bool](Get-ADDomainController -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue)
            }
            catch
            {
                $false
            }
        }

    } -DoNotUseCredSsp -PassThru -NoDisplay  -ErrorAction SilentlyContinue

    [bool]$adReady

    Write-LogFunctionExit
}


function Wait-LabADReady
{
    param (
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [int]$TimeoutInMinutes = 15,

        [int]$ProgressIndicator,

        [switch]$NoNewLine
    )

    Write-LogFunctionEntry

    $start = Get-Date

    $machines = Get-LabVM -ComputerName $ComputerName
    $machines | Add-Member -Name AdRetries -MemberType NoteProperty -Value 2 -Force

    $ProgressIndicatorTimer = (Get-Date)
    do
    {
        foreach ($machine in $machines)
        {
            if ($machine.AdRetries)
            {
                $adReady = Test-LabADReady -ComputerName $machine

                if ($DebugPreference)
                {
                    Write-Debug -Message "Return '$adReady' from '$($machine)'"
                }

                if ($adReady)
                {
                    $machine.AdRetries--
                }
            }

            if (-not $machine.AdRetries)
            {
                Write-PSFMessage -Message "Active Directory is now ready on Domain Controller '$machine'"
            }
            else
            {
                Write-Debug "Active Directory is NOT ready yet on Domain Controller: '$machine'"
            }
        }

        if (((Get-Date) - $ProgressIndicatorTimer).TotalSeconds -ge $ProgressIndicator)
        {
            if ($ProgressIndicator)
            {
                Write-ProgressIndicator
            }
            $ProgressIndicatorTimer = (Get-Date)
        }

        if ($DebugPreference)
        {
            $machines | ForEach-Object {
                Write-Debug -Message "$($_.Name.PadRight(18)) $($_.AdRetries)"
            }
        }

        if ($machines | Where-Object { $_.AdRetries })
        {
            Start-Sleep -Seconds 3
        }
    }
    until (($machines.AdRetries | Measure-Object -Maximum).Maximum -le 0 -or (Get-Date).AddMinutes(-$TimeoutInMinutes) -gt $start)

    if ($ProgressIndicator -and -not $NoNewLine)
    {
        Write-ProgressIndicatorEnd
    }

    if (($machines.AdRetries | Measure-Object -Maximum).Maximum -le 0)
    {
        Write-PSFMessage -Message 'Domain Controllers specified are now ready:'
        Write-PSFMessage -Message ($machines.Name -join ', ')
    }
    else
    {
        $machines | Where-Object { $_.AdRetries -gt 0 } | ForEach-Object {
            Write-Error -Message "Timeout occured waiting for Active Directory to be ready on Domain Controller: $_. Retry count is $($_.AdRetries)" -TargetObject $_
        }
    }

    Write-LogFunctionExit
}


function Install-LabAdfs
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Write-ScreenInfo -Message 'Configuring ADFS roles...'

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first' -Type Warning
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role ADFS

    if (-not $machines)
    {
        return
    }

    if ($machines | Where-Object  { -not $_.DomainName })
    {
        Write-Error "There are ADFS Server defined in the lab that are not domain joined. ADFS must be joined to a domain."
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -ComputerName $machines -Wait -ProgressIndicator 15

    $labAdfsServers = $machines | Group-Object -Property DomainName

    foreach ($domainGroup in $labAdfsServers)
    {
        $domainName = $domainGroup.Name
        $adfsServers = $domainGroup.Group | Where-Object { $_.Roles.Name -eq 'ADFS' }
        Write-ScreenInfo "Installing the ADFS Servers '$($adfsServers -join ',')'" -Type Info

        $ca = Get-LabIssuingCA -DomainName $domainName
        Write-PSFMessage "The CA that will be used is '$ca'"
        $adfsDc = Get-LabVM -Role RootDC, FirstChildDC, DC | Where-Object DomainName -eq $domainName
        Write-PSFMessage "The DC that will be used is '$adfsDc'"

        $1stAdfsServer = $adfsServers | Select-Object -First 1
        $1stAdfsServerAdfsRole = $1stAdfsServer.Roles | Where-Object Name -eq ADFS
        $otherAdfsServers = $adfsServers | Select-Object -Skip 1

        #use the display name as defined in the role. If it is not defined, construct one with the domain name (Adfs<FlatDomainName>)
        $adfsDisplayName = $1stAdfsServerAdfsRole.Properties.DisplayName
        if (-not $adfsDisplayName)
        {
            $adfsDisplayName = "Adfs$($1stAdfsServer.DomainName.Split('.')[0])"
        }

        $adfsServiceName = $1stAdfsServerAdfsRole.Properties.ServiceName
        if (-not $adfsServiceName) { $adfsServiceName = 'AdfsService'}
        $adfsServicePassword = $1stAdfsServerAdfsRole.Properties.ServicePassword
        if (-not $adfsServicePassword) { $adfsServicePassword = 'Somepass1'}

        Write-PSFMessage "The ADFS Farm display name in domain '$domainName' is '$adfsDisplayName'"
        $adfsCertificateSubject = "CN=adfs.$($domainGroup.Name)"
        Write-PSFMessage "The subject used to obtain an SSL certificate is '$adfsCertificateSubject'"
        $adfsCertificateSAN = "adfs.$domainName" , "enterpriseregistration.$domainName"

        $adfsFlatName = $adfsCertificateSubject.Substring(3).Split('.')[0]
        Write-PSFMessage "The ADFS flat name is '$adfsFlatName'"
        $adfsFullName = $adfsCertificateSubject.Substring(3)
        Write-PSFMessage "The ADFS full name is '$adfsFullName'"

        if (-not (Test-LabCATemplate -TemplateName AdfsSsl -ComputerName $ca))
        {
            New-LabCATemplate -TemplateName AdfsSsl -DisplayName 'ADFS SSL' -SourceTemplateName WebServer -ApplicationPolicy "Server Authentication" `
            -EnrollmentFlags Autoenrollment -PrivateKeyFlags AllowKeyExport -Version 2 -SamAccountName 'Domain Computers' -ComputerName $ca -ErrorAction Stop
        }

        Write-PSFMessage "Requesting SSL certificate on the '$1stAdfsServer'"
        $cert = Request-LabCertificate -Subject $adfsCertificateSubject -SAN $adfsCertificateSAN -TemplateName AdfsSsl -ComputerName $1stAdfsServer -PassThru
        $certThumbprint = $cert.Thumbprint
        Write-PSFMessage "Certificate thumbprint is '$certThumbprint'"

        Invoke-LabCommand -ActivityName 'Add ADFS Service User and DNS record' -ComputerName $adfsDc -ScriptBlock {
            Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10) #not required if not used GMSA
            New-ADUser -Name $adfsServiceName -AccountPassword ($adfsServicePassword | ConvertTo-SecureString -AsPlainText -Force) -Enabled $true -PasswordNeverExpires $true

            foreach ($entry in $adfsServers)
            {
                $ip = (Get-DnsServerResourceRecord -Name $entry -ZoneName $domainName).RecordData.IPv4Address.IPAddressToString
                Add-DnsServerResourceRecord -Name $adfsFlatName -ZoneName $domainName -IPv4Address $ip -A
            }
        } -Variable (Get-Variable -Name adfsServers, domainName, adfsFlatName, adfsServiceName, adfsServicePassword)

        Install-LabWindowsFeature -ComputerName $adfsServers -FeatureName ADFS-Federation

        $result = Invoke-LabCommand -ActivityName 'Installing ADFS Farm' -ComputerName $1stAdfsServer -ScriptBlock {
            $cred = New-Object pscredential("$($env:USERDNSDOMAIN)\$adfsServiceName", ($adfsServicePassword | ConvertTo-SecureString -AsPlainText -Force))

            $certificate = Get-Item -Path "Cert:\LocalMachine\My\$certThumbprint"
            Install-AdfsFarm -CertificateThumbprint $certificate.Thumbprint -FederationServiceDisplayName $adfsDisplayName -FederationServiceName $certificate.SubjectName.Name.Substring(3) -ServiceAccountCredential $cred
        } -Variable (Get-Variable -Name certThumbprint, adfsDisplayName, adfsServiceName, adfsServicePassword) -PassThru

        if ($result.Status -ne 'Success')
        {
            Write-Error "ADFS could not be configured. The error message was: '$($result.Message -join ', ')'" -TargetObject $result
            return
        }

        $result = if ($otherAdfsServers)
        {
            # Copy Service-communication/SSL Certificate to all ADFS Servers
            Write-PSFMessage "Copying SSL Certificate to secondary AD FS nodes"
            Invoke-LabCommand -ActivityName 'Exporting SSL Cert' -ComputerName $1stAdfsServer -Variable (Get-Variable -Name certThumbprint) -ScriptBlock {
                Get-Item "Cert:\LocalMachine\My\$certThumbprint" | Export-PfxCertificate -FilePath "C:\sslcert.pfx" -ProtectTo "Domain Users"
            }
            Invoke-LabCommand -ActivityName 'Importing SSL Cert' -ComputerName $otherAdfsServers -Variable (Get-Variable -Name certThumbprint,1stAdfsServer) -ScriptBlock {
                Get-Item "\\$1stAdfsServer\C$\sslcert.pfx" | Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\My -Exportable
            }
            Invoke-LabCommand -ActivityName 'Removing PFX file' -ComputerName $1stAdfsServer -ScriptBlock {
                Remove-Item -Path "C:\sslcert.pfx"
            }

            Invoke-LabCommand -ActivityName 'Installing ADFS Farm' -ComputerName $otherAdfsServers -ScriptBlock {
                $cred = New-Object pscredential("$($env:USERDNSDOMAIN)\$adfsServiceName", ($adfsServicePassword | ConvertTo-SecureString -AsPlainText -Force))

                Add-AdfsFarmNode -CertificateThumbprint $certThumbprint -PrimaryComputerName $1stAdfsServer.Name -ServiceAccountCredential $cred -OverwriteConfiguration
            } -Variable (Get-Variable -Name certThumbprint, 1stAdfsServer, adfsServiceName, adfsServicePassword) -PassThru

            if ($result.Status -ne 'Success')
            {
                Write-Error "ADFS could not be configured. The error message was: '$($result.Message -join ', ')'" -TargetObject $result
                return
            }
        }
    }

    Write-LogFunctionExit
}


function Install-LabAdfsProxy
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Write-ScreenInfo -Message 'Configuring ADFS roles...'

    $lab = Get-Lab

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first' -Type Warning
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role ADFSProxy

    if (-not $machines)
    {
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName ADFSProxy -Wait -ProgressIndicator 15

    $labAdfsProxies = Get-LabVM -Role ADFSProxy
    $job = Install-LabWindowsFeature -ComputerName $labAdfsProxies -FeatureName Web-Application-Proxy -AsJob -PassThru
    Wait-LWLabJob -Job $job

    Write-ScreenInfo "Installing the ADFS Proxy Servers '$($labAdfsProxies -join ',')'" -Type Info
    foreach ($labAdfsProxy in $labAdfsProxies)
    {
        Write-PSFMessage "Installing ADFS Proxy on '$labAdfsProxy'"
        $adfsProxyRole = $labAdfsProxy.Roles | Where-Object Name -eq ADFSProxy
        $adfsFullName = $adfsProxyRole.Properties.AdfsFullName
        $adfsDomainName = $adfsProxyRole.Properties.AdfsDomainName
        Write-PSFMessage "ADFS Full Name is '$adfsFullName'"

        $someAdfsServer = Get-LabVM -Role ADFS | Where-Object DomainName -eq $adfsDomainName | Get-Random
        Write-PSFMessage "Getting certificate from some ADFS server '$someAdfsServer'"
        $cert = Get-LabCertificate -ComputerName $someAdfsServer -DnsName $adfsFullName
        if (-not $cert)
        {
            Write-Error "Could not get certificate from '$someAdfsServer'. Cannot continue with ADFS Proxy setup."
            return
        }
        Write-PSFMessage "Got certificate with thumbprint '$($cert.Thumbprint)'"

        Write-PSFMessage "Adding certificate to '$labAdfsProxy'"
        $cert | Add-LabCertificate -ComputerName $labAdfsProxy

        $certThumbprint = $cert.Thumbprint
        $cred = ($lab.Domains | Where-Object Name -eq $adfsDomainName).GetCredential()

        $null = Invoke-LabCommand -ActivityName 'Configuring ADFS Proxy Servers' -ComputerName $labAdfsProxy -ScriptBlock {
            Install-WebApplicationProxy -FederationServiceTrustCredential $cred -CertificateThumbprint $certThumbprint -FederationServiceName $adfsFullName
        } -Variable (Get-Variable -Name certThumbprint, cred, adfsFullName) -PassThru

    }

    Write-LogFunctionExit
}


function Add-LabAzureSubscription
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(ParameterSetName = 'ByName')]
        [string]$SubscriptionName,

        [Parameter(ParameterSetName = 'ById')]
        [guid]$SubscriptionId,

        [string]
        $Environment,

        [string]$DefaultLocationName,

        [ObsoleteAttribute()]
        [string]$DefaultStorageAccountName,

        [string]$DefaultResourceGroupName,

        [timespan]
        $AutoShutdownTime,

        [string]
        $AutoShutdownTimeZone,

        [switch]$PassThru,

        [switch]
        $AllowBastionHost,

        [switch]
        $AzureStack
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry
    Update-LabAzureSettings

    if (-not $script:lab)
    {
        throw 'No lab defined. Please call New-LabDefinition first before calling Set-LabDefaultOperatingSystem.'
    }

    $null = Test-LabAzureModuleAvailability -AzureStack:$($AzureStack.IsPresent) -ErrorAction Stop

    Write-ScreenInfo -Message 'Adding Azure subscription data' -Type Info -TaskStart

    if ($Environment -and -not (Get-AzEnvironment -Name $Environment -ErrorAction SilentlyContinue))
    {
        throw "Azure environment $Environment cannot be found. Cannot continue. Please use Add-AzEnvironment before trying that again."
    }

    # Try to access Azure RM cmdlets. If credentials are expired, an exception will be raised
    if (-not (Get-AzContext) -or ($Environment -and (Get-AzContext).Environment.Name -ne $Environment))
    {
        Write-ScreenInfo -Message "No Azure context available or environment mismatch. Please login to your Azure account in the next step."
        $param = @{
            UseDeviceAuthentication = $true
            ErrorAction = 'SilentlyContinue' 
            WarningAction = 'Continue'
        }

        if ($Environment)
        {
            $param.Environment = $Environment
        }

        $null = Connect-AzAccount @param
    }

    # Select the proper subscription before saving the profile
    if ($SubscriptionName)
    {
        [void](Set-AzContext -Subscription $SubscriptionName -ErrorAction SilentlyContinue)
    }
    elseif ($SubscriptionId)
    {
        [void](Set-AzContext -Subscription $SubscriptionId -ErrorAction SilentlyContinue)
    }

    $azProfile = Get-AzContext
    if (-not $azProfile)
    {
        throw 'Cannot continue without a valid Azure connection.'
    }

    Update-LabAzureSettings
    if (-not $script:lab.AzureSettings)
    {
        $script:lab.AzureSettings = New-Object AutomatedLab.AzureSettings
    }

    if ($Environment)
    {
        $script:lab.AzureSettings.Environment = $Environment
    }

    $script:lab.AzureSettings.DefaultRoleSize = Get-LabConfigurationItem -Name DefaultAzureRoleSize
    $script:lab.AzureSettings.AllowBastionHost = $AllowBastionHost.IsPresent
    $script:lab.AzureSettings.IsAzureStack = $AzureStack.IsPresent

    if ($AutoShutdownTime -and -not $AzureStack.IsPresent)
    {
        if (-not $AutoShutdownTimeZone)
        {
            $AutoShutdownTimeZone = Get-TimeZone
        }

        $script:lab.AzureSettings.AutoShutdownTime = $AutoShutdownTime
        $script:lab.AzureSettings.AutoShutdownTimeZone = $AutoShutdownTimeZone.Id
    }
    
    # Select the subscription which is associated with this AzureRmProfile
    $subscriptions = Get-AzSubscription
    $script:lab.AzureSettings.Subscriptions = [AutomatedLab.Azure.AzureSubscription]::Create($Subscriptions)
    Write-PSFMessage "Added $($script:lab.AzureSettings.Subscriptions.Count) subscriptions"

    if ($SubscriptionName -and -not ($script:lab.AzureSettings.Subscriptions | Where-Object Name -eq $SubscriptionName))
    {
        throw "A subscription named '$SubscriptionName' cannot be found. Make sure you specify the right subscription name or let AutomatedLab choose on by not defining a subscription name"
    }
    if ($SubscriptionId -and -not ($script:lab.AzureSettings.Subscriptions | Where-Object Id -eq $SubscriptionId))
    {
        throw "A subscription with the ID '$SubscriptionId' cannot be found. Make sure you specify the right subscription name or let AutomatedLab choose on by not defining a subscription ID"
    }

    #select default subscription subscription
    $selectedSubscription = if (-not $SubscriptionName -and -not $SubscriptionId)
    {
        $azProfile.Subscription
    }
    elseif ($SubscriptionName)
    {
        $Subscriptions | Where-Object Name -eq $SubscriptionName
    }
    elseif ($SubscriptionId)
    {
        $Subscriptions | Where-Object Id -eq $SubscriptionId
    }

    if ($selectedSubscription.Count -gt 1)
    {
        throw "There is more than one subscription with the name '$SubscriptionName'. Please use the subscription Id to select a specific subscription."
    }

    Write-ScreenInfo -Message "Using Azure Subscription '$($selectedSubscription.Name)' ($($selectedSubscription.Id))" -Type Info

    Register-LabAzureRequiredResourceProvider -SubscriptionName $selectedSubscription.Name

    try
    {
        [void](Set-AzContext -Subscription $selectedSubscription -ErrorAction SilentlyContinue)
    }
    catch
    {
        throw "Error selecting subscription $SubscriptionName. $($_.Exception.Message). The local Azure profile might have expired. Please try Connect-AzAccount."
    }

    $script:lab.AzureSettings.DefaultSubscription = [AutomatedLab.Azure.AzureSubscription]::Create($selectedSubscription)
    Write-PSFMessage "Azure subscription '$SubscriptionName' selected as default"

    if ($AllowBastionHost.IsPresent -and -not $AzureStack.IsPresent -and (Get-AzProviderFeature -FeatureName AllowBastionHost -ProviderNamespace Microsoft.Network).RegistrationState -eq 'NotRegistered')
    {
        # Check if resource provider allows BastionHost deployment
        $null = Register-AzProviderFeature -FeatureName AllowBastionHost -ProviderNamespace Microsoft.Network
        $null = Register-AzProviderFeature -FeatureName bastionShareableLink -ProviderNamespace Microsoft.Network
    }

    $locations = Get-AzLocation
    $script:lab.AzureSettings.Locations = [AutomatedLab.Azure.AzureLocation]::Create($locations)
    Write-PSFMessage "Added $($script:lab.AzureSettings.Locations.Count) locations"

    if (-not $DefaultLocationName)
    {
        $DefaultLocationName = Get-LabAzureLocation
    }

    try
    {
        Set-LabAzureDefaultLocation -Name $DefaultLocationName -ErrorAction Stop
        Write-ScreenInfo -Message "Using Azure Location '$DefaultLocationName'" -Type Info
    }
    catch
    {
        throw 'Cannot proceed without a valid location specified'
    }

    Write-ScreenInfo -Message "Trying to locate or create default resource group"

    #Create new lab resource group as default
    if (-not $DefaultResourceGroupName)
    {
        $DefaultResourceGroupName = $script:lab.Name
    }

    #Create if no default given or default set and not existing as RG
    $rg = Get-AzResourceGroup -Name $DefaultResourceGroupName -ErrorAction SilentlyContinue
    if (-not $rg)
    {
        $rgParams = @{
            Name     = $DefaultResourceGroupName
            Location = $DefaultLocationName
            Tag      = @{
                AutomatedLab = $script:lab.Name
                CreationTime = Get-Date
            }
        }

        $defaultResourceGroup = New-AzResourceGroup @rgParams -ErrorAction Stop
        $script:lab.AzureSettings.DefaultResourceGroup = [AutomatedLab.Azure.AzureRmResourceGroup]::Create($defaultResourceGroup)
    }
    else
    {
        $script:lab.AzureSettings.DefaultResourceGroup = [AutomatedLab.Azure.AzureRmResourceGroup]::Create((Get-AzResourceGroup -Name $DefaultResourceGroupName))
    }
    Write-PSFMessage "Selected $DefaultResourceGroupName as default resource group"

    $resourceGroups = Get-AzResourceGroup
    $script:lab.AzureSettings.ResourceGroups = [AutomatedLab.Azure.AzureRmResourceGroup]::Create($resourceGroups)
    Write-PSFMessage "Added $($script:lab.AzureSettings.ResourceGroups.Count) resource groups"

    $storageAccounts = Get-AzStorageAccount -ResourceGroupName $DefaultResourceGroupName
    foreach ($storageAccount in $storageAccounts)
    {
        $alStorageAccount = [AutomatedLab.Azure.AzureRmStorageAccount]::Create($storageAccount)
        $alStorageAccount.StorageAccountKey = ($storageAccount | Get-AzStorageAccountKey)[0].Value
        $script:lab.AzureSettings.StorageAccounts.Add($alStorageAccount)
    }

    Write-PSFMessage "Added $($script:lab.AzureSettings.StorageAccounts.Count) storage accounts"

    if ($global:cacheAzureRoleSizes)
    {
        Write-ScreenInfo -Message "Querying available vm sizes for Azure location '$DefaultLocationName' (using cache)" -Type Info
        $defaultSizes = (Get-LabAzureDefaultLocation).VirtualMachineRoleSizes
        $roleSizes = $global:cacheAzureRoleSizes | Where-Object { $_.InstanceSize -in $defaultSizes }
    }
    else
    {
        Write-ScreenInfo -Message "Querying available vm sizes for Azure location '$DefaultLocationName'" -Type Info
        $roleSizes = Get-LabAzureAvailableRoleSize -Location $DefaultLocationName
        $global:cacheAzureRoleSizes = $roleSizes
    }

    if ($roleSizes.Count -eq 0)
    {
        throw "No available role sizes in region '$DefaultLocationName'! Cannot continue."
    }

    $script:lab.AzureSettings.RoleSizes = $rolesizes

    # Add LabSources storage
    if ( -not $AzureStack.IsPresent)
    {
        New-LabAzureLabSourcesStorage
    }

    # Add ISOs
    $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, DateTime

    try
    {
        Write-PSFMessage -Message 'Get last ISO update time'
        if ($IsLinux -or $IsMacOs)
        {
            $timestamps = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $timestamps = $type::ImportFromRegistry('Cache', 'Timestamps')
        }
        $lastChecked = $timestamps.AzureIsosLastChecked
        Write-PSFMessage -Message "Last check was '$lastChecked'."
    }
    catch
    {
        Write-PSFMessage -Message 'Last check time could not be retrieved. Azure ISOs never updated'
        $lastChecked = Get-Date -Year 1601
        $timestamps = New-Object $type
    }

    if ($lastChecked -lt [datetime]::Now.AddDays(-7) -and -not $AzureStack.IsPresent)
    {
        Write-PSFMessage -Message 'ISO cache outdated. Updating ISO files.'
        try
        {
            Write-ScreenInfo -Message 'Auto-adding ISO files from Azure labsources share' -TaskStart
            Add-LabIsoImageDefinition -Path "$labSources\ISOs" -ErrorAction Stop
        }
        catch
        {
            Write-ScreenInfo -Message 'No ISO files have been found in your Azure labsources share. Please make sure that they are present when you try mounting them.' -Type Warning
        }
        finally
        {
            $timestamps['AzureIsosLastChecked'] = Get-Date
            if ($IsLinux -or $IsMacOs)
            {
                $timestamps.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
            }
            else
            {
                $timestamps.ExportToRegistry('Cache', 'Timestamps')
            }

            Write-ScreenInfo -Message 'Done' -TaskEnd
        }
    }

    # Check last LabSources sync timestamp
    if ($IsLinux -or $IsMacOs)
    {
        $timestamps = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
    }
    else
    {
        $timestamps = $type::ImportFromRegistry('Cache', 'Timestamps')
    }

    $lastchecked = $timestamps.LabSourcesSynced
    $syncMaxSize = Get-LabConfigurationItem -Name LabSourcesMaxFileSizeMb
    $syncIntervalDays = Get-LabConfigurationItem -Name LabSourcesSyncIntervalDays

    if (-not (Get-LabConfigurationItem -Name DoNotPrompt -Default $false) -and -not $lastchecked -and -not $AzureStack.IsPresent)
    {
        $lastchecked = [datetime]0
        $syncText = @"
Do you want to sync the content of $(Get-LabSourcesLocationInternal -Local) to your Azure file share $($global:labsources) every $syncIntervalDays days?

By default, all files smaller than $syncMaxSize MB will be synced. Should you require more control,
execute Sync-LabAzureLabSources manually. The maximum file size for the automatic sync can also
be set in your settings with the setting LabSourcesMaxFileSizeMb.
Have a look at Get-Command -Syntax Sync-LabAzureLabSources for additional information.

To configure later:
Get/Set/Register/Unregister-PSFConfig -Module AutomatedLab -Name LabSourcesMaxFileSizeMb
Get/Set/Register/Unregister-PSFConfig -Module AutomatedLab -Name LabSourcesSyncIntervalDays
Get/Set/Register/Unregister-PSFConfig -Module AutomatedLab -Name AutoSyncLabSources
"@

        # Detecting Interactivity this way only works in .NET Full - .NET Core always defaults to $true
        # Last Resort is checking the CommandLine Args
        $choice = if (($PSVersionTable.PSEdition -eq 'Desktop' -and [Environment]::UserInteractive) -or ($PSVersionTable.PSEdition -eq 'Core' -and [string][Environment]::GetCommandLineArgs() -notmatch "-Non"))
        {
            Read-Choice -ChoiceList '&Yes', '&No, do not ask me again', 'N&o, not this time' -Caption 'Sync lab sources to Azure?' -Message $syncText -Default 0
        }
        else
        {
            2
        }

        if ($choice -eq 0)
        {
            Set-PSFConfig -Module AutomatedLab -Name AutoSyncLabSources -Value $true -PassThru | Register-PSFConfig            
        }
        elseif ($choice -eq 1)
        {
            Set-PSFConfig -Module AutomatedLab -Name AutoSyncLabSources -Value $false -PassThru | Register-PSFConfig
        }

        $timestamps.LabSourcesSynced = Get-Date
        if ($IsLinux -or $IsMacOs)
        {
            $timestamps.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $timestamps.ExportToRegistry('Cache', 'Timestamps')
        }
    }

    if ((Get-LabConfigurationItem -Name AutoSyncLabSources) -and $lastchecked -lt [datetime]::Now.AddDays(-$syncIntervalDays) -and -not $AzureStack.IsPresent)
    {
        Sync-LabAzureLabSources -MaxFileSizeInMb $syncMaxSize
        $timestamps.LabSourcesSynced = Get-Date
        if ($IsLinux -or $IsMacOs)
        {
            $timestamps.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $timestamps.ExportToRegistry('Cache', 'Timestamps')
        }
    }

    $script:lab.AzureSettings.VNetConfig = (Get-AzVirtualNetwork) | ConvertTo-Json
    Write-PSFMessage 'Added virtual network configuration'

    # Read cache
    $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.Azure.AzureOSImage

    try
    {
        if ($IsLinux -or $IsMacOs) 
        {
            $global:cacheVmImages = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/AzureOperatingSystems.xml'))
        }
        else
        {
            $global:cacheVmImages = $type::ImportFromRegistry('Cache', 'AzureOperatingSystems')
        }

        Write-PSFMessage "Read $($global:cacheVmImages.Count) OS images from the cache"

        if ($global:cacheVmImages -and $global:cacheVmImages.TimeStamp -gt (Get-Date).AddDays(-7))
        {
            Write-PSFMessage ("Azure OS Cache was older than {0:yyyy-MM-dd HH:mm:ss}. Cache date was {1:yyyy-MM-dd HH:mm:ss}" -f (Get-Date).AddDays(-7) , $global:cacheVmImages.TimeStamp)
            Write-ScreenInfo 'Querying available operating system images (using cache)'
            $vmImages = $global:cacheVmImages
        }
        else
        {
            Write-ScreenInfo 'Could not read OS image info from the cache'
            throw 'Cache outdated or empty'
        }
    }
    catch
    {
        Write-ScreenInfo 'Querying available operating system images from Azure'
        $global:cacheVmImages = Get-LabAzureAvailableSku -Location $DefaultLocationName
        $vmImages = $global:cacheVmImages
    }

    $osImageListType = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.Azure.AzureOSImage
    $script:lab.AzureSettings.VmImages = New-Object $osImageListType

    # Cache all images
    if ($vmImages)
    {
        $osImageList = New-Object $osImageListType


        foreach ($vmImage in $vmImages)
        {
            $osImageList.Add([AutomatedLab.Azure.AzureOSImage]::Create($vmImage))
            $script:lab.AzureSettings.VmImages.Add([AutomatedLab.Azure.AzureOSImage]::Create($vmImage))
        }

        $osImageList.Timestamp = Get-Date
        if ($IsLinux -or $IsMacOS)
        {
            $osImageList.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/AzureOperatingSystems.xml'))
        }
        else
        {
            $osImageList.ExportToRegistry('Cache', 'AzureOperatingSystems')
        }
    }

    Write-PSFMessage "Added $($script:lab.AzureSettings.VmImages.Count) virtual machine images"

    $vms = Get-AzVM
    $script:lab.AzureSettings.VirtualMachines = [AutomatedLab.Azure.AzureVirtualMachine]::Create($vms)
    Write-PSFMessage "Added $($script:lab.AzureSettings.VirtualMachines.Count) virtual machines"

    Write-ScreenInfo -Message "Azure default resource group name will be '$($script:lab.Name)'"
    Write-ScreenInfo -Message "Azure data center location will be '$DefaultLocationName'" -Type Info
    Write-ScreenInfo -Message 'Finished adding Azure subscription data' -Type Info -TaskEnd

    if ($PassThru)
    {
        $script:lab.AzureSettings.Subscription
    }

    Write-LogFunctionExit
}


function Enable-LabAzureJitAccess
{
    [CmdletBinding()]
    param 
    (
        [timespan]
        $MaximumAccessRequestDuration = '05:00:00',

        [switch]
        $PassThru
    )

    $vms = Get-LWAzureVm
    $lab = Get-Lab

    if ($lab.AzureSettings.IsAzureStack)
    {
        Write-Error -Message "$($lab.Name) is running on Azure Stack and thus does not support JIT access."
        return
    }
    
    $parameters = @{
        Location          = $lab.AzureSettings.DefaultLocation.Location
        Name              = 'AutomatedLabJIT'
        ResourceGroupName = $lab.AzureSettings.DefaultResourceGroup.ResourceGroupName
    }

    if (Get-AzJitNetworkAccessPolicy @parameters -ErrorAction SilentlyContinue)
    {
        Write-ScreenInfo -Type Verbose -Message 'JIT policy already configured'
        return
    }

    $weirdTimestampFormat = [System.Xml.XmlConvert]::ToString($MaximumAccessRequestDuration)
    $pip = Get-PublicIpAddress

    $vmPolicies = foreach ($vm in $vms)
    {
        @{
            id    = $vm.Id
            ports = @{
                number                     = 22
                protocol                   = "*"
                allowedSourceAddressPrefix = @($pip)
                maxRequestAccessDuration   = $weirdTimestampFormat
            },
            @{
                number                     = 3389
                protocol                   = "*"
                allowedSourceAddressPrefix = @($pip)
                maxRequestAccessDuration   = $weirdTimestampFormat
            },
            @{
                number                     = 5985
                protocol                   = "*"
                allowedSourceAddressPrefix = @($pip)
                maxRequestAccessDuration   = $weirdTimestampFormat
            }
        }
    }

    $policy = Set-AzJitNetworkAccessPolicy -Kind "Basic" @parameters -VirtualMachine $vmPolicies
    while ($policy.ProvisioningState -ne 'Succeeded')
    {
        $policy = Get-AzJitNetworkAccessPolicy @parameters
    }

    if ($PassThru) { $policy }
}


function Get-LabAzureAvailableRoleSize
{
    [CmdletBinding(DefaultParameterSetName = 'DisplayName')]
    param
    (
        [Parameter(Mandatory, ParameterSetName = 'DisplayName')]
        [Alias('Location')]
        [string]
        $DisplayName,

        [Parameter(Mandatory, ParameterSetName = 'Name')]
        [string]
        $LocationName
    )

    Test-LabHostConnected -Throw -Quiet

    if (-not (Get-AzContext -ErrorAction SilentlyContinue))
    {
        $param = @{
            UseDeviceAuthentication = $true
            ErrorAction             = 'SilentlyContinue' 
            WarningAction           = 'Continue'            
        }

        if ($script:lab.AzureSettings.Environment)
        {
            $param.Environment = $script:Lab.AzureSettings.Environment
        }

        $null = Connect-AzAccount @param
    }

    $azLocation = Get-AzLocation | Where-Object { $_.DisplayName -eq $DisplayName -or $_.Location -eq $LocationName }
    if (-not $azLocation)
    {
        Write-ScreenInfo -Type Error -Message "No location found matching DisplayName '$DisplayName' or Name '$LocationName'"
    }

    $availableRoleSizes = if ((Get-Command Get-AzComputeResourceSku).Parameters.ContainsKey('Location'))
    {
        Get-AzComputeResourceSku -Location $azLocation.Location | Where-Object {
            $_.ResourceType -eq 'virtualMachines' -and $_.Restrictions.ReasonCode -notcontains 'NotAvailableForSubscription' -and ($_.Capabilities | Where-Object Name -eq CpuArchitectureType).Value -notlike '*arm*'
        }
    }
    else
    {
        Get-AzComputeResourceSku | Where-Object {
            $_.Locations -contains $azLocation.Location -and $_.ResourceType -eq 'virtualMachines' -and $_.Restrictions.ReasonCode -notcontains 'NotAvailableForSubscription' -and ($_.Capabilities | Where-Object Name -eq CpuArchitectureType).Value -notlike '*arm*'
        }
    }
    

    foreach ($vms in (Get-AzVMSize -Location $azLocation.Location | Where-Object -Property Name -in $availableRoleSizes.Name))
    {
        $rsInfo = $availableRoleSizes | Where-Object Name -eq $vms.Name

            [AutomatedLab.Azure.AzureRmVmSize]@{
                NumberOfCores = $vms.NumberOfCores
                MemoryInMB = $vms.MemoryInMB
                Name = $vms.Name
                MaxDataDiskCount = $vms.MaxDataDiskCount
                ResourceDiskSizeInMB = $vms.ResourceDiskSizeInMB
                OSDiskSizeInMB = $vms.OSDiskSizeInMB
                Gen1Supported = ($rsInfo.Capabilities | Where-Object Name -eq HyperVGenerations).Value -like '*v1*'
                Gen2Supported = ($rsInfo.Capabilities | Where-Object Name -eq HyperVGenerations).Value -like '*v2*'
            }
    }
}


function Get-LabAzureAvailableSku
{
    [CmdletBinding(DefaultParameterSetName = 'DisplayName')]
    param
    (
        [Parameter(Mandatory, ParameterSetName = 'DisplayName')]
        [Alias('Location')]
        [string]
        $DisplayName,

        [Parameter(Mandatory, ParameterSetName = 'Name')]
        [string]
        $LocationName
    )

    Test-LabHostConnected -Throw -Quiet

    # Server
    $azLocation = Get-AzLocation | Where-Object { $_.DisplayName -eq $DisplayName -or $_.Location -eq $LocationName }
    if (-not $azLocation)
    {
        Write-ScreenInfo -Type Error -Message "No location found matching DisplayName '$DisplayName' or Name '$LocationName'"
    }
    $publishers = Get-AzVMImagePublisher -Location $azLocation.Location
    
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftWindowsServer' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # Linux
    # Ubuntu - official
    $publishers |
    Where-Object PublisherName -eq 'Canonical' |
    Get-AzVMImageOffer |
    Where-Object Offer -match '0001-com-ubuntu-server-\w+$' |
    Get-AzVMImageSku |
    Where-Object Skus -notmatch 'arm64' |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }
    # RedHat - official
    $publishers |
    Where-Object PublisherName -eq 'RedHat' |
    Get-AzVMImageOffer |
    Where-Object Offer -eq 'RHEL' |
    Get-AzVMImageSku |
    Where-Object Skus -notmatch '(RAW|LVM|CI)' |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }
    # CentOS - Roguewave, sounds slightly suspicious
    $publishers |
    Where-Object PublisherName -eq 'OpenLogic' |
    Get-AzVMImageOffer |
    Where-Object Offer -eq CentOS |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }
    # Kali
    $publishers |
    Where-Object PublisherName -eq 'Kali-Linux' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # Desktop
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftWindowsDesktop' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # SQL
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftSQLServer' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Where-Object Skus -in 'Standard','Enterprise' |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # VisualStudio
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftVisualStudio' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Where-Object Offer -eq 'VisualStudio' |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # Client OS
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftVisualStudio' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Where-Object Offer -eq 'Windows' |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }

    # Sharepoint 2013 and 2016
    $publishers |
    Where-Object PublisherName -eq 'MicrosoftSharePoint' |
    Get-AzVMImageOffer |
    Get-AzVMImageSku |
    Get-AzVMImage |
    Where-Object Offer -eq 'MicrosoftSharePointServer' |
    Group-Object -Property Skus, Offer |
    ForEach-Object { $_.Group | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1 }
}


function Get-LabAzureCertificate
{
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    [CmdletBinding()]
    param ()

    throw New-Object System.NotImplementedException
    Write-LogFunctionEntry

    Update-LabAzureSettings

    $certSubject = "CN=$($Script:lab.Name).cloudapp.net"

    $cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object Subject -eq $certSubject -ErrorAction SilentlyContinue

    if (-not $cert)
    {
        #just returning nothing is more convenient
        #Write-LogFunctionExitWithError -Message "The required certificate does not exist"
    }
    else
    {
        $cert
    }

    Write-LogFunctionExit
}


function Get-LabAzureDefaultLocation
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Update-LabAzureSettings

    if (-not $Script:lab.AzureSettings.DefaultLocation)
    {
        Write-Error 'The default location is not defined. Use Set-LabAzureDefaultLocation to define it.'
        return
    }

    $Script:lab.AzureSettings.DefaultLocation

    Write-LogFunctionExit
}


function Get-LabAzureDefaultResourceGroup
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Update-LabAzureSettings

    $script:lab.AzureSettings.ResourceGroups | Where-Object ResourceGroupName -eq $script:lab.Name

    Write-LogFunctionExit
}


function Get-LabAzureLabSourcesContent
{
    [CmdletBinding()]
    param
    (
        [string]
        $RegexFilter,

        # Path relative to labsources file share
        [string]
        $Path,

        [switch]
        $File,

        [switch]
        $Directory
    )

    Test-LabHostConnected -Throw -Quiet

    $azureShare = Get-AzStorageShare -Name labsources -Context (Get-LabAzureLabSourcesStorage).Context

    $params = @{
        StorageContext = $azureShare
    }
    if ($Path) { $params.Path = $Path }

    $content = Get-LabAzureLabSourcesContentRecursive @params

    if (-not [string]::IsNullOrWhiteSpace($RegexFilter))
    {
        $content = $content | Where-Object -FilterScript { $PSItem.Name -match $RegexFilter }
    }

    if ($File)
    {
        $content = $content | Where-Object -FilterScript { $PSItem.GetType().FullName -eq 'Microsoft.Azure.Storage.File.CloudFile' }
    }

    if ($Directory)
    {
        $content = $content | Where-Object -FilterScript { $PSItem.GetType().FullName -eq 'Microsoft.Azure.Storage.File.CloudFileDirectory' }
    }

    $content = $content |
    Add-Member -MemberType ScriptProperty -Name FullName -Value { $this.Uri.AbsoluteUri } -Force -PassThru |
    Add-Member -MemberType ScriptProperty -Name Length -Force -Value { $this.Properties.Length } -PassThru

    return $content
}


function Get-LabAzureLabSourcesStorage
{
    [CmdletBinding()]
    param
    ()

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry

    Test-LabAzureSubscription
    $azureLabSourcesResourceGroupName = 'AutomatedLabSources'

    $currentSubscription = (Get-AzContext).Subscription

    $storageAccount = Get-AzStorageAccount -ResourceGroupName automatedlabsources -ErrorAction SilentlyContinue | Where-Object StorageAccountName -like automatedlabsources?????

    if (-not $storageAccount)
    {
        Write-Error "The AutomatedLabSources share on Azure does not exist"
        return
    }

    $storageAccount | Add-Member -MemberType NoteProperty -Name StorageAccountKey -Value ($storageAccount | Get-AzStorageAccountKey)[0].Value -Force
    $storageAccount | Add-Member -MemberType NoteProperty -Name Path -Value "\\$($storageAccount.StorageAccountName).file.core.windows.net\labsources" -Force
    $storageAccount | Add-Member -MemberType NoteProperty -Name SubscriptionName -Value (Get-AzContext).Subscription.Name -Force

    $storageAccount
}


function Get-LabAzureLocation
{
    [CmdletBinding()]
    param (
        [string]$LocationName,

        [switch]$List
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry

    $azureLocations = Get-AzLocation

    if ($LocationName)
    {
        if ($LocationName -notin ($azureLocations.DisplayName))
        {
            Write-Error "Invalid location. Please specify one of the following locations: ""'$($azureLocations.DisplayName -join ''', ''')"
            return
        }

        $azureLocations | Where-Object DisplayName -eq $LocationName
    }
    else
    {
        if ((Get-Lab -ErrorAction SilentlyContinue) -and (-not $list))
        {
            #if lab already exists, use the location used when this was deployed to create lab stickyness
            return (Get-Lab).AzureSettings.DefaultLocation.Name
        }

        $locationUrls = Get-LabConfigurationItem -Name AzureLocationsUrls

        foreach ($location in $azureLocations)
        {
            if ($locationUrls."$($location.DisplayName)")
            {
                $location | Add-Member -MemberType NoteProperty -Name 'Url' -Value ($locationUrls."$($location.DisplayName)" + '.blob.core.windows.net')
            }
            $location | Add-Member -MemberType NoteProperty -Name 'Latency' -Value 9999
        }

        $jobs = @()
        foreach ($location in ($azureLocations | Where-Object { $_.Url }))
        {
            $url = $location.Url
            $jobs += Start-Job -Name $location.DisplayName -ScriptBlock {
                $testUrl = $using:url

                try
                {
                    (Test-Port -ComputerName $testUrl -Port 443 -Count 4 -ErrorAction Stop | Measure-Object -Property ResponseTime -Average).Average
                }
                catch
                {
                    9999
                    #Write-PSFMessage -Level Warning "$testUrl $($_.Exception.Message)"
                }
            }
        }

        Wait-LWLabJob -Job $jobs -NoDisplay
        foreach ($job in $jobs)
        {
            $result = Receive-Job -Keep -Job $job
            ($azureLocations | Where-Object { $_.DisplayName -eq $job.Name }).Latency = $result
        }
        $jobs | Remove-Job

        Write-PSFMessage -Message 'DisplayName Latency'
        foreach ($location in $azureLocations)
        {
            Write-PSFMessage -Message "$($location.DisplayName.PadRight(20)): $($location.Latency)"
        }

        if ($List)
        {
            $azureLocations | Sort-Object -Property Latency | Format-Table DisplayName, Latency
        }
        else
        {
            $azureLocations | Sort-Object -Property Latency | Select-Object -First 1 | Select-Object -ExpandProperty DisplayName
        }
    }

    Write-LogFunctionExit
}


function Get-LabAzureResourceGroup
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName')]
        [string[]]$ResourceGroupName,

        [Parameter(Position = 0, ParameterSetName = 'ByLab')]
        [switch]$CurrentLab
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry

    Update-LabAzureSettings

    $resourceGroups = $script:lab.AzureSettings.ResourceGroups

    if ($ResourceGroupName)
    {
        Write-PSFMessage "Getting the resource groups '$($ResourceGroupName -join ', ')'"
        $resourceGroups | Where-Object ResourceGroupName -in $ResourceGroupName
    }
    elseif ($CurrentLab)
    {
        $result = $resourceGroups | Where-Object { $_.Tags.AutomatedLab -eq $script:lab.Name }

        if (-not $result)
        {
            $result = $script:lab.AzureSettings.DefaultResourceGroup
        }
        $result
    }
    else
    {
        Write-PSFMessage 'Getting all resource groups'
        $resourceGroups
    }

    Write-LogFunctionExit
}


function Get-LabAzureSubscription
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    Update-LabAzureSettings

    $script:lab.AzureSettings.Subscriptions

    Write-LogFunctionExit
}


function Import-LabAzureCertificate
{
    [CmdletBinding()]
    param ()

    Test-LabHostConnected -Throw -Quiet

    throw New-Object System.NotImplementedException
    Write-LogFunctionEntry

    Update-LabAzureSettings

    $resourceGroup = Get-AzResourceGroup -name (Get-LabAzureDefaultResourceGroup)
    $keyVault = Get-AzKeyVault -VaultName (Get-LabAzureDefaultKeyVault) -ResourceGroupName $resourceGroup
    $temp = [System.IO.Path]::GetTempFileName()

    $cert = ($keyVault | Get-AzKeyVaultCertificate).Data

    if ($cert)
    {
        $cert | Out-File -FilePath $temp
        certutil -addstore -f Root $temp | Out-Null

        Remove-Item -Path $temp
        Write-LogFunctionExit
    }
    else
    {
        Write-LogFunctionExitWithError -Message "Could not receive certificate for resource group '$resourceGroup'"
    }
}


function Install-LabAzureRequiredModule
{
    [CmdletBinding()]
    param
    (
        [string]
        $Repository = 'PSGallery',

        [ValidateSet('CurrentUser', 'AllUsers')]
        [string]
        $Scope = 'CurrentUser',

        [switch]
        $AzureStack
    )

    [hashtable[]] $modules = if ($AzureStack.IsPresent) { Get-LabConfigurationItem -Name RequiredAzStackModules } else { Get-LabConfigurationItem -Name RequiredAzModules }
    foreach ($module in $modules)
    {
        $isPresent = if ($module.MinimumVersion)
        {
            Get-Module -ListAvailable -Name $module.Name | Where-Object Version -ge $module.MinimumVersion
        }
        elseif ($module.RequiredVersion)
        {
            Get-Module -ListAvailable -Name $module.Name | Where-Object Version -eq $module.RequiredVersion
        }
        
        if ($isPresent)
        {
            Write-PSFMessage -Message "$($module.Name) already present"
            continue
        }

        Install-Module @module -Repository $Repository -Scope $Scope -Force
    }
}


function New-LabAzureLabSourcesStorage
{
    [CmdletBinding()]
    param
    (
        [string]$LocationName,

        [switch]$NoDisplay
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry

    Test-LabAzureSubscription
    $azureLabSourcesResourceGroupName = 'AutomatedLabSources'

    if (-not $LocationName)
    {
        $LocationName = (Get-LabAzureDefaultLocation -ErrorAction SilentlyContinue).DisplayName
    }
    if (-not $LocationName)
    {
        Write-Error "LocationName was not provided and could not be retrieved from a present lab. Please specify a location name or import a lab"
        return
    }
    if ($LocationName -notin (Get-AzLocation).DisplayName)
    {
        Write-Error "The location name '$LocationName' is not valid. Please invoke 'Get-AzLocation' to get a list of possible locations"
    }

    $currentSubscription = (Get-AzContext).Subscription
    Write-ScreenInfo "Looking for Azure LabSources inside subscription '$($currentSubscription.Name)'" -TaskStart

    $resourceGroup = Get-AzResourceGroup -Name $azureLabSourcesResourceGroupName -ErrorAction SilentlyContinue
    if (-not $resourceGroup)
    {
        Write-ScreenInfo "Resoure Group '$azureLabSourcesResourceGroupName' could not be found, creating it"
        $resourceGroup = New-AzResourceGroup -Name $azureLabSourcesResourceGroupName -Location $LocationName | Out-Null
    }

    $storageAccount = Get-AzStorageAccount -ResourceGroupName $azureLabSourcesResourceGroupName -ErrorAction SilentlyContinue | Where-Object StorageAccountName -like automatedlabsources?????
    if (-not $storageAccount)
    {
        Write-ScreenInfo "No storage account for AutomatedLabSources could not be found, creating it"
        $storageAccountName = "automatedlabsources$((1..5 | ForEach-Object { [char[]](97..122) | Get-Random }) -join '')"
        New-AzStorageAccount -ResourceGroupName $azureLabSourcesResourceGroupName -Name $storageAccountName -Location $LocationName -Kind Storage -SkuName Standard_LRS | Out-Null
        $storageAccount = Get-AzStorageAccount -ResourceGroupName $azureLabSourcesResourceGroupName | Where-Object StorageAccountName -like automatedlabsources?????
    }

    $share = Get-AzStorageShare -Context $StorageAccount.Context -Name labsources -ErrorAction SilentlyContinue
    if (-not $share)
    {
        Write-ScreenInfo "The share 'labsources' could not be found, creating it"
        New-AzStorageShare -Name 'labsources' -Context $storageAccount.Context | Out-Null
    }

    Write-ScreenInfo "Azure LabSources verified / created" -TaskEnd

    Write-LogFunctionExit
}


function New-LabAzureRmResourceGroup
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string[]]$ResourceGroupNames,

        [Parameter(Mandatory, Position = 1)]
        [string]$LocationName,

        [switch]$PassThru
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionEntry

    Update-LabAzureSettings

    Write-PSFMessage "Creating the resource groups '$($ResourceGroupNames -join ', ')' for location '$LocationName'"

    $resourceGroups = Get-AzResourceGroup

    foreach ($name in $ResourceGroupNames)
    {
        if ($resourceGroups | Where-Object ResourceGroupName -eq $name)
        {
            if (-not $script:lab.AzureSettings.ResourceGroups.ResourceGroupName.Contains($name))
            {
                $script:lab.AzureSettings.ResourceGroups.Add([AutomatedLab.Azure.AzureRmResourceGroup]::Create((Get-AzResourceGroup -ResourceGroupName $name)))
                Write-PSFMessage "The resource group '$name' does already exist"
            }
            continue
        }

        $result = New-AzResourceGroup -Name $name -Location $LocationName -Tag @{
            AutomatedLab = $script:lab.Name
            CreationTime = Get-Date
        }

        $script:lab.AzureSettings.ResourceGroups.Add([AutomatedLab.Azure.AzureRmResourceGroup]::Create((Get-AzResourceGroup -ResourceGroupName $name)))
        if ($PassThru)
        {
            $result
        }

        Write-PSFMessage "Resource group '$name' created"
    }

    Write-LogFunctionExit
}


function Register-LabAzureRequiredResourceProvider
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $SubscriptionName,

        [Parameter()]
        [int]
        $ProgressIndicator = 5,

        [Parameter()]
        [switch]
        $NoDisplay
    )

    Write-LogFunctionEntry

    $null = Set-AzContext -Subscription $SubscriptionName

    $providers = @(
        'Microsoft.Network'
        'Microsoft.Compute'
        'Microsoft.Storage'
    )

    $providerObjects = Get-AzResourceProvider -ProviderNamespace $providers | Where-Object RegistrationState -ne 'Registered'
    if ($providerObjects)
    {
        Write-ScreenInfo -Message "Registering required Azure Resource Providers"
        $providerRegistrations = $providerObjects | Register-AzResourceProvider -ConsentToPermissions $true
        while ($providerRegistrations.RegistrationState -contains 'Registering')
        {
            $providerRegistrations = $providerRegistrations | Get-AzResourceProvider | Where-Object RegistrationState -ne 'Registered'
            Start-Sleep -Seconds 10
            Write-ProgressIndicator
        }
    }

    $providersAndFeatures = @{
        'Microsoft.Network' = @(
            'AllowBastionHost'
        )
    }

    $featureState = foreach ($paf in $providersAndFeatures.GetEnumerator())
    {
        foreach ($featureName in $paf.Value)
        {
            $feature = Get-AzProviderFeature -FeatureName $featureName -ProviderNamespace $paf.Key
            if ($feature.RegistrationState -eq 'NotRegistered')
            {
                Register-AzProviderFeature -FeatureName $featureName -ProviderNamespace $paf.Key
            }
        }
    }

    if (-not $featureState) { Write-LogFunctionExit; return }

    Write-ScreenInfo -Message "Waiting for $($featureState.Count) provider features to register"
    while ($featureState.RegistrationState -contains 'Registering')
    {
        $featureState = $featureState | ForEach-Object {
            Get-AzProviderFeature -FeatureName $_.FeatureName -ProviderNamespace $_.ProviderName
        }
        Start-Sleep -Seconds 10
        Write-ProgressIndicator
    }

    Write-LogFunctionExit
}

function Remove-LabAzureLabSourcesStorage
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param
    ()

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionExit
    Test-LabAzureSubscription

    if (Test-LabAzureLabSourcesStorage)
    {
        $azureLabStorage = Get-LabAzureLabSourcesStorage

        if ($PSCmdlet.ShouldProcess($azureLabStorage.ResourceGroupName, 'Remove Resource Group'))
        {
            Remove-AzResourceGroup -Name $azureLabStorage.ResourceGroupName -Force | Out-Null
            Write-ScreenInfo "Azure Resource Group '$($azureLabStorage.ResourceGroupName)' was removed" -Type Warning
        }
    }

    Write-LogFunctionExit
}


function Remove-LabAzureResourceGroup
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroupName,

        [switch]$Force
    )

    begin
    {
        Test-LabHostConnected -Throw -Quiet

        Write-LogFunctionEntry

        Update-LabAzureSettings

        $resourceGroups = Get-LabAzureResourceGroup -CurrentLab
    }

    process
    {
        foreach ($name in $ResourceGroupName)
        {
            Write-ScreenInfo -Message "Removing the Resource Group '$name'" -Type Warning
            if ($resourceGroups.ResourceGroupName -contains $name)
            {
                Remove-AzResourceGroup -Name $name -Force:$Force | Out-Null
                Write-PSFMessage "Resource Group '$($name)' removed"

                $resourceGroup = $script:lab.AzureSettings.ResourceGroups | Where-Object ResourceGroupName -eq $name
                $script:lab.AzureSettings.ResourceGroups.Remove($resourceGroup) | Out-Null
            }
            else
            {
                Write-ScreenInfo -Message "RG '$name' could not be found" -Type Error
            }
        }
    }

    end
    {
        Write-LogFunctionExit
    }
}


function Request-LabAzureJitAccess
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $ComputerName,

        # Local end time, will be converted to UTC for request
        [timespan]
        $Duration = '04:45:00'
    )

    $lab = Get-Lab

    if ($lab.AzureSettings.IsAzureStack)
    {
        Write-Error -Message "$($lab.Name) is running on Azure Stack and thus does not support JIT access."
        return
    }

    $parameters = @{
        Location          = $lab.AzureSettings.DefaultLocation.Location
        Name              = 'AutomatedLabJIT'
        ResourceGroupName = $lab.AzureSettings.DefaultResourceGroup.ResourceGroupName
    }

    $policy = Get-AzJitNetworkAccessPolicy @parameters -ErrorAction SilentlyContinue
    if (-not $policy) { $policy = Enable-LabAzureJitAccess -MaximumAccessRequestDuration $Duration.Add('00:05:00') -PassThru }
    $nodes = if ($ComputerName.Count -eq 0) { Get-LabVm } else { Get-LabVm -ComputerName $ComputerName }
    $vms = Get-LWAzureVm -ComputerName $nodes.ResourceName
    $end = (Get-Date).Add($Duration)
    $utcEnd = $end.ToUniversalTime().ToString('u')
    $pip = Get-PublicIpAddress

    $jitRequests = foreach ($vm in $vms)
    {
        @{
            id    = $vm.Id
            ports = @{
                number                     = 22
                endTimeUtc                 = $utcEnd
                allowedSourceAddressPrefix = @($pip)
            }, @{
                number                     = 3389
                endTimeUtc                 = $utcEnd
                allowedSourceAddressPrefix = @($pip)
            }, @{
                number                     = 5985
                endTimeUtc                 = $utcEnd
                allowedSourceAddressPrefix = @($pip)
            }
        }
    }

    Set-PSFConfig -Module AutomatedLab -Name AzureJitTimestamp -Value $end -Validation datetime -Hidden
    $null = Start-AzJitNetworkAccessPolicy -ResourceId $policy.Id -VirtualMachine $jitRequests
}


function Set-LabAzureDefaultLocation
{

    param (
        [Parameter(Mandatory)]
        [string]$Name
    )

    Write-LogFunctionEntry

    Update-LabAzureSettings

    if (-not ($Name -in $script:lab.AzureSettings.Locations.DisplayName -or $Name -in $script:lab.AzureSettings.Locations.Location))
    {
        Microsoft.PowerShell.Utility\Write-Error "Invalid location. Please specify one of the following locations: $($script:lab.AzureSettings.Locations.DisplayName -join ', ')"
        return
    }

    $script:lab.AzureSettings.DefaultLocation = $script:lab.AzureSettings.Locations | Where-Object { $_.DisplayName -eq $Name -or $_.Location -eq $Name }

    Write-LogFunctionExit
}


function Sync-LabAzureLabSources
{
    [CmdletBinding()]
    param
    (
        [switch]
        $SkipIsos,

        [switch]
        $DoNotSkipOsIsos,

        [int]
        $MaxFileSizeInMb,

        [string]
        $Filter,

        [switch]
        $NoDisplay
    )

    Test-LabHostConnected -Throw -Quiet

    Write-LogFunctionExit
    Test-LabAzureSubscription

    if (-not (Test-LabAzureLabSourcesStorage))
    {
        Write-Error "There is no LabSources share available in the current subscription '$((Get-AzContext).Subscription.Name)'. To create one, please call 'New-LabAzureLabSourcesStorage'."
        return
    }

    $currentSubscription = (Get-AzContext).Subscription
    Write-ScreenInfo -Message "Syncing LabSources in subscription '$($currentSubscription.Name)'" -TaskStart

    # Retrieve storage context
    $storageAccount = Get-AzStorageAccount -ResourceGroupName automatedlabsources | Where-Object StorageAccountName -like automatedlabsources?????

    $localLabsources = Get-LabSourcesLocationInternal -Local
    Unblock-LabSources -Path $localLabsources

    # Sync the lab sources
    $fileParams = @{
        Recurse     = $true
        Path        = $localLabsources
        File        = $true
        Filter      = if ($Filter) { $Filter } else { "*" }
        ErrorAction = 'SilentlyContinue'
    }

    $files = Get-ChildItem @fileParams
    $share = (Get-AzStorageShare -Name labsources -Context $storageAccount.Context).CloudFileShare

    foreach ($file in $files)
    {
        Write-ProgressIndicator
        if ($SkipIsos -and $file.Directory.Name -eq 'Isos')
        {
            Write-PSFMessage "SkipIsos is true, skipping $($file.Name)"
            continue
        }

        if ($MaxFileSizeInMb -and $file.Length / 1MB -ge $MaxFileSizeInMb)
        {
            Write-PSFMessage "MaxFileSize is $MaxFileSizeInMb MB, skipping '$($file.Name)'"
            continue
        }

        # Check if file is an OS ISO and skip
        if ($file.Extension -eq '.iso')
        {
            $isOs = [bool](Get-LabAvailableOperatingSystem -Path $file.FullName)

            if ($isOs -and -not $DoNotSkipOsIsos)
            {
                Write-PSFMessage "Skipping OS ISO $($file.FullName)"
                continue
            }
        }

        $fileName = $file.FullName.Replace("$($localLabSources)\", '')

        $azureFile = Get-AzStorageFile -Share $share -Path $fileName -ErrorAction SilentlyContinue
        if ($azureFile)
        {
            $sBuilder = [System.Text.StringBuilder]::new()
            foreach ($byte in $azureFile.FileProperties.ContentHash)
            {
                $null = $sBuilder.Append($byte.ToString("x2"))
            }
            $azureHash = $sBuilder.ToString()

            $sBuilder = [System.Text.StringBuilder]::new()
            $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
            $data = $md5.ComputeHash([System.IO.File]::ReadAllBytes($file.Fullname))
            foreach ($byte in $data)
            {
                $null = $sBuilder.Append($byte.ToString("x2"))
            }
            $localHash = $sBuilder.ToString()
            $fileHash = [System.Convert]::ToBase64String($data)
            
            # Azure expects base64 MD5 in the request, returns MD5 :)
            Write-PSFMessage "$fileName already exists in Azure. Source hash is $localHash and Azure hash is $azureHash"
        }

        if ($azureFile -and $localHash -eq $azureHash)
        {
            continue
        }

        if (-not $azureFile -or ($azureFile -and $localHash -ne $azureHash))
        {
            $null = New-LabSourcesPath -RelativePath $fileName -Share $share
            $null = Set-AzStorageFileContent -Share $share -Source $file.FullName -Path $fileName -ErrorAction SilentlyContinue -Force
            Write-PSFMessage "Azure file $fileName successfully uploaded. Updating file hash..."
        }

        # Try to set the file hash
        $uploadedFile = Get-AzStorageFile -Share $share -Path $fileName -ErrorAction SilentlyContinue
        try
        {
            $uploadedFile.CloudFile.Properties.ContentMD5 = $fileHash
            $apiResponse = $uploadedFile.CloudFile.SetProperties()
        }
        catch
        {
            Write-ScreenInfo "Could not update MD5 hash for file $fileName." -Type Warning
        }

        Write-PSFMessage "Azure file $fileName successfully uploaded and hash generated"
    }

    Write-ScreenInfo "LabSources Sync complete" -TaskEnd

    Write-LogFunctionExit
}


function Test-LabAzureLabSourcesStorage
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param ( )

    Test-LabHostConnected -Throw -Quiet

    if ((Get-LabDefinition -ErrorAction SilentlyContinue).AzureSettings.IsAzureStack -or (Get-Lab -ErrorAction SilentlyContinue).AzureSettings.IsAzureStack) { return $false }

    $azureLabSources = Get-LabAzureLabSourcesStorage -ErrorAction SilentlyContinue

    if (-not $azureLabSources)
    {
        return $false
    }

    $azureStorageShare = Get-AzStorageShare -Context $azureLabSources.Context -ErrorAction SilentlyContinue

    [bool]$azureStorageShare
}


function Test-LabAzureModuleAvailability
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param 
    (
        [switch]
        $AzureStack
    )

    [hashtable[]] $modules = if ($AzureStack.IsPresent) { Get-LabConfigurationItem -Name RequiredAzStackModules } else { Get-LabConfigurationItem -Name RequiredAzModules }
    [hashtable[]] $modulesMissing = @()

    foreach ($module in $modules)
    {
        $param = @{
            Name  = $module.Name
            Force = $true
        }

        $isPresent = if ($module.MinimumVersion)
        {
            Get-Module -ListAvailable -Name $module.Name | Where-Object Version -ge $module.MinimumVersion
            $param.MinimumVersion = $module.MinimumVersion
        }
        elseif ($module.RequiredVersion)
        {
            Get-Module -ListAvailable -Name $module.Name | Where-Object Version -eq $module.RequiredVersion
            $param.RequiredVersion = $module.RequiredVersion
        }
        
        if ($isPresent)
        {
            Write-PSFMessage -Message "$($module.Name) found"
            Import-Module @param
            continue
        }

        Write-PSFMessage -Message "$($module.Name) missing"
        $modulesMissing += $module
    }
    
    if ($modulesMissing.Count -gt 0)
    {
        $missingString = $modulesMissing.ForEach({ "$($_.Name), Minimum: $($_.MinimumVersion) or required: $($_.RequiredVersion)" })
        Write-PSFMessage -Level Error -Message "Missing Az modules: $missingString"
    }

    return ($modulesMissing.Count -eq 0)
}


function Test-LabPathIsOnLabAzureLabSourcesStorage
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$Path
    )

    if (-not (Test-LabHostConnected)) { return $false }

    try
    {
        if (Test-LabAzureLabSourcesStorage)
        {
            $azureLabSources = Get-LabAzureLabSourcesStorage

            return $Path -like "$($azureLabSources.Path)*"
        }
        else
        {
            return $false
        }
    }
    catch
    {
        return $false
    }
}


function Update-LabAzureSettings
{
    [CmdletBinding()]
    param ( )
    if ((Get-PSCallStack).Command -contains 'Import-Lab')
    {
        $Script:lab = Get-Lab
    }
    elseif ((Get-PSCallStack).Command -contains 'Add-LabAzureSubscription')
    {
        $Script:lab = Get-LabDefinition
        if (-not $Script:lab)
        {
            $Script:lab = Get-Lab
        }
    }
    else
    {
        $Script:lab = Get-Lab -ErrorAction SilentlyContinue
    }

    if (-not $Script:lab)
    {
        $Script:lab = Get-LabDefinition
    }

    if (-not $Script:lab)
    {
        throw 'No Lab or Lab Definition available'
    }
}


function Get-LabAzureAppServicePlan
{
    [CmdletBinding(DefaultParameterSetName = 'All')]
    [OutputType([AutomatedLab.Azure.AzureRmServerFarmWithRichSku])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name
    )

    begin
    {
        Write-LogFunctionEntry

        $lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            break
        }

        $script:lab = & $MyInvocation.MyCommand.Module { $script:lab }
    }

    process
    {
        if (-not $Name) { return }

        $sp = $lab.AzureResources.ServicePlans | Where-Object Name -eq $Name

        if (-not $sp)
        {
            Write-Error "The Azure App Service Plan '$Name' does not exist."
        }
        else
        {
            $sp
        }
    }

    end
    {
        if ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $lab.AzureResources.ServicePlans
        }

        Write-LogFunctionExit
    }
}


function Get-LabAzureWebApp
{
    [CmdletBinding(DefaultParameterSetName = 'All')]
    [OutputType([AutomatedLab.Azure.AzureRmService])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name
    )

    begin
    {
        Write-LogFunctionEntry

        $script:lab = Get-Lab
    }

    process
    {
        if (-not $Name) { return }

        $sa = $lab.AzureResources.Services | Where-Object Name -eq $Name

        if (-not $sa)
        {
            Write-Error "The Azure App Service '$Name' does not exist."
        }
        else
        {
            $sa
        }
    }

    end
    {
        if ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $lab.AzureResources.Services
        }

        Write-LogFunctionExit
    }
}


function Get-LabAzureWebAppStatus
{
    [CmdletBinding(DefaultParameterSetName = 'All')]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Position = 1, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroup,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All = $true,

        [switch]$AsHashTable
    )

    begin
    {
        Write-LogFunctionEntry
        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
        $allAzureWebApps = Get-AzWebApp
        if ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $Name = $lab.AzureResources.Services.Name
            $ResourceGroup = $lab.AzureResources.Services.Name.ResourceGroup
        }
        $result = [ordered]@{}
    }

    process
    {
        $services = foreach ($n in $name)
        {
            if (-not $n -and -not $PSCmdlet.ParameterSetName -eq 'All') { return }

            $service = if ($ResourceGroup)
            {
                $lab.AzureResources.Services | Where-Object { $_.Name -eq $n -and $_.ResourceGroup -eq $ResourceGroup }
            }
            else
            {
                $lab.AzureResources.Services | Where-Object { $_.Name -eq $n }
            }

            if (-not $service)
            {
                Write-Error "The Azure App Service '$n' does not exist."
            }
            else
            {
                $service
            }
        }

        foreach ($service in $services)
        {
            $s = $allAzureWebApps | Where-Object { $_.Name -eq $service.Name -and $_.ResourceGroup -eq $service.ResourceGroup }
            if ($s)
            {
                $service.Merge($s, 'PublishProfiles')
                $result.Add($service, $s.State)
            }
            else
            {
                Write-Error "The Web App '$($service.Name)' does not exist in the Azure Resource Group $($service.ResourceGroup)."
            }
        }

    }

    end
    {
        Export-Lab
        if ($result.Count -eq 1 -and -not $AsHashTable)
        {
            $result[$result.Keys[0]]
        }
        else
        {
            $result
        }
        Write-LogFunctionExit
    }
}


function Install-LabAzureServices
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry
    $lab = Get-Lab

    if (-not $lab)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    Write-ScreenInfo -Message "Starting Azure Services Deplyment"

    $services = Get-LabAzureWebApp
    $servicePlans = Get-LabAzureAppServicePlan

    if (-not $services)
    {
        Write-ScreenInfo "No Azure service defined, exiting."
        Write-LogFunctionExit
        return
    }

    Write-ScreenInfo "There are $($servicePlans.Count) Azure App Services Plans defined. Starting deployment." -TaskStart
    $servicePlans | New-LabAzureAppServicePlan
    Write-ScreenInfo 'Finished creating Azure App Services Plans.' -TaskEnd

    Write-ScreenInfo "There are $($services.Count) Azure Web Apps defined. Starting deployment." -TaskStart
    $services | New-LabAzureWebApp
    Write-ScreenInfo 'Finished creating Azure Web Apps.' -TaskEnd

    Write-LogFunctionExit
}


function New-LabAzureAppServicePlan
{
    [OutputType([AutomatedLab.Azure.AzureRmServerFarmWithRichSku])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [switch]$PassThru
    )

    begin
    {
        Write-LogFunctionEntry
        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        foreach ($planName in $Name)
        {
            $plan = Get-LabAzureAppServicePlan -Name $planName

            if (-not (Get-LabAzureResourceGroup -ResourceGroupName $plan.ResourceGroup))
            {
                New-LabAzureRmResourceGroup -ResourceGroupNames $plan.ResourceGroup -LocationName $plan.Location
            }

            if ((Get-AzAppServicePlan -Name $plan.Name -ResourceGroupName $plan.ResourceGroup -ErrorAction SilentlyContinue))
            {
                Write-Error "The Azure Application Service Plan '$planName' does already exist in $($plan.ResourceGroup)"
                return
            }

            $plan = New-AzAppServicePlan -Name $plan.Name -Location $plan.Location -ResourceGroupName $plan.ResourceGroup -Tier $plan.Tier -NumberofWorkers $plan.NumberofWorkers -WorkerSize $plan.WorkerSize

            if ($plan)
            {
                $plan = [AutomatedLab.Azure.AzureRmServerFarmWithRichSku]::Create($plan)
                $existingPlan = Get-LabAzureAppServicePlan -Name $plan.Name
                $existingPlan.Merge($plan)

                if ($PassThru)
                {
                    $plan
                }
            }
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function New-LabAzureWebApp
{
    [OutputType([AutomatedLab.Azure.AzureRmService])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [switch]$PassThru
    )

    begin
    {
        Write-LogFunctionEntry

        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        foreach ($serviceName in $Name)
        {
            $app = Get-LabAzureWebApp -Name $serviceName

            if (-not (Get-LabAzureResourceGroup -ResourceGroupName $app.ResourceGroup))
            {
                New-LabAzureRmResourceGroup -ResourceGroupNames $app.ResourceGroup -LocationName $app.Location
            }

            if (-not (Get-LabAzureAppServicePlan -Name $app.ApplicationServicePlan))
            {
                New-LabAzureAppServicePlan -Name $app.ApplicationServicePlan
            }

            $webApp = New-AzWebApp -Name $app.Name -Location $app.Location -AppServicePlan $app.ApplicationServicePlan -ResourceGroupName $app.ResourceGroup

            if ($webApp)
            {
                $webApp = [AutomatedLab.Azure.AzureRmService]::Create($webApp)

                #Get app-level deployment credentials
                $xml = [xml](Get-AzWebAppPublishingProfile -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup -OutputFile null)

                $publishProfile = [AutomatedLab.Azure.PublishProfile]::Create($xml.publishData.publishProfile)
                $webApp.PublishProfiles = $publishProfile

                $existingWebApp = Get-LabAzureWebApp -Name $webApp.Name
                $existingWebApp.Merge($webApp)

                $existingWebApp | Set-LabAzureWebAppContent -LocalContentPath "$(Get-LabSourcesLocationInternal -Local)\PostInstallationActivities\WebSiteDefaultContent"

                if ($PassThru)
                {
                    $webApp
                }
            }
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function Set-LabAzureWebAppContent
{
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Mandatory, Position = 1)]
        [string]$LocalContentPath
    )

    begin
    {
        Write-LogFunctionEntry

        if (-not (Test-Path -Path $LocalContentPath))
        {
            Write-LogFunctionExitWithError -Message "The path '$LocalContentPath' does not exist"
            continue
        }

        $script:lab = Get-Lab
    }

    process
    {
        if (-not $Name) { return }

        $webApp = $lab.AzureResources.Services | Where-Object Name -eq $Name

        if (-not $webApp)
        {
            Write-Error "The Azure App Service '$Name' does not exist."
            return
        }

        $publishingProfile = $webApp.PublishProfiles | Where-Object PublishMethod -eq 'FTP'
        $cred = New-Object System.Net.NetworkCredential($publishingProfile.UserName, $publishingProfile.UserPWD)
        $publishingProfile.PublishUrl -match '(ftp:\/\/)(?<url>[\w-\.]+)(\/)' | Out-Null
        $hostUrl = $Matches.url

        Send-FtpFolder -Path $LocalContentPath -DestinationPath site/wwwroot/ -HostUrl $hostUrl -Credential $cred -Recure
    }

    end
    {
        Write-LogFunctionExit
    }
}


function Start-LabAzureWebApp
{
    [OutputType([AutomatedLab.Azure.AzureRmService])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroup,

        [switch]$PassThru
    )

    begin
    {
        Write-LogFunctionEntry
        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        if (-not $Name) { return }

        $service = $lab.AzureResources.Services | Where-Object { $_.Name -eq $Name -and $_.ResourceGroup -eq $ResourceGroup }

        if (-not $service)
        {
            Write-Error "The Azure App Service '$Name' does not exist."
        }
        else
        {
            try
            {
                $s = Start-AzWebApp -Name $service.Name -ResourceGroupName $service.ResourceGroup -ErrorAction Stop
                $service.Merge($s, 'PublishProfiles')

                if ($PassThru)
                {
                    $service
                }
            }
            catch
            {
                Write-Error "The Azure Web App '$($service.Name)' in resource group '$($service.ResourceGroup)' could not be started"
            }
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function Stop-LabAzureWebApp
{
    [OutputType([AutomatedLab.Azure.AzureRmService])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ResourceGroup,

        [switch]$PassThru
    )

    begin
    {
        Write-LogFunctionEntry
        $script:lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        if (-not $Name) { return }

        $service = $lab.AzureResources.Services | Where-Object { $_.Name -eq $Name -and $_.ResourceGroup -eq $ResourceGroup }

        if (-not $service)
        {
            Write-Error "The Azure App Service '$Name' does not exist in Resource Group '$ResourceGroup'."
        }
        else
        {
            try
            {
                $s = Stop-AzWebApp -Name $service.Name -ResourceGroupName $service.ResourceGroup -ErrorAction Stop
                $service.Merge($s, 'PublishProfiles')

                if ($PassThru)
                {
                    $service
                }
            }
            catch
            {
                Write-Error "The Azure Web App '$($service.Name)' in resource group '$($service.ResourceGroup)' could not be stopped"
            }
        }
    }

    end
    {
        Export-Lab
        Write-LogFunctionExit
    }
}


function Install-LabConfigurationManager
{
    [CmdletBinding()]
    param ()

    $vms = Get-LabVm -Role ConfigurationManager
    Start-LabVm -Role ConfigurationManager -Wait

    #region Prereq: ADK, CM binaries, stuff
    Write-ScreenInfo -Message "Installing Prerequisites on $($vms.Count) machines"
    $adkUrl = Get-LabConfigurationItem -Name WindowsAdk
    $adkPeUrl = Get-LabConfigurationItem -Name WindowsAdkPe
    $adkFile = Get-LabInternetFile -Uri $adkUrl -Path $labsources\SoftwarePackages -FileName adk.exe -PassThru -NoDisplay
    $adkpeFile = Get-LabInternetFile -Uri $adkPeUrl -Path $labsources\SoftwarePackages -FileName adkpe.exe -PassThru -NoDisplay
    
    if ($(Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        Install-LabSoftwarePackage -Path $adkFile.FullName -ComputerName $vms -CommandLine '/quiet /layout c:\ADKoffline' -NoDisplay
        Install-LabSoftwarePackage -Path $adkpeFile.FullName -ComputerName $vms -CommandLine '/quiet /layout c:\ADKPEoffline' -NoDisplay
    }
    else
    {
        Start-Process -FilePath $adkFile.FullName -ArgumentList "/quiet /layout $(Join-Path $labSources SoftwarePackages/ADKoffline)" -Wait -NoNewWindow
        Start-Process -FilePath $adkpeFile.FullName -ArgumentList " /quiet /layout $(Join-Path $labSources SoftwarePackages/ADKPEoffline)" -Wait -NoNewWindow
        Copy-LabFileItem -Path (Join-Path $labSources SoftwarePackages/ADKoffline) -ComputerName $vms
        Copy-LabFileItem -Path (Join-Path $labSources SoftwarePackages/ADKPEoffline) -ComputerName $vms
    }
    
    Install-LabSoftwarePackage -LocalPath C:\ADKOffline\adksetup.exe -ComputerName $vms -CommandLine '/norestart /q /ceip off /features OptionId.DeploymentTools OptionId.UserStateMigrationTool OptionId.ImagingAndConfigurationDesigner' -NoDisplay
    Install-LabSoftwarePackage -LocalPath C:\ADKPEOffline\adkwinpesetup.exe -ComputerName $vms -CommandLine '/norestart /q /ceip off /features OptionId.WindowsPreinstallationEnvironment' -NoDisplay

    $ncliUrl = Get-LabConfigurationItem -Name SqlServerNativeClient2012
    try
    {
        $ncli = Get-LabInternetFile -Uri $ncliUrl -Path "$labSources/SoftwarePackages" -FileName sqlncli.msi -ErrorAction "Stop" -ErrorVariable "GetLabInternetFileErr" -PassThru
    }
    catch
    {
        $Message = "Failed to download SQL Native Client from '{0}' ({1})" -f $ncliUrl, $GetLabInternetFileErr.ErrorRecord.Exception.Message
        Write-LogFunctionExitWithError -Message $Message
    }

    Install-LabSoftwarePackage -Path $ncli.FullName -ComputerName $vms -CommandLine "/qn /norestart IAcceptSqlncliLicenseTerms=Yes" -ExpectedReturnCodes 0

    $WMIv2Zip = "{0}\WmiExplorer.zip" -f (Get-LabSourcesLocation -Local)
    $WMIv2Exe = "{0}\WmiExplorer.exe" -f (Get-LabSourcesLocation -Local)
    $wmiExpUrl = Get-LabConfigurationItem -Name ConfigurationManagerWmiExplorer

    try
    {
        Get-LabInternetFile -Uri $wmiExpUrl -Path (Split-Path -Path $WMIv2Zip -Parent) -FileName (Split-Path -Path $WMIv2Zip -Leaf) -ErrorAction "Stop" -ErrorVariable "GetLabInternetFileErr"
    }
    catch
    {
        Write-ScreenInfo -Message ("Could not download from '{0}' ({1})" -f $wmiExpUrl, $GetLabInternetFileErr.ErrorRecord.Exception.Message) -Type "Warning"
    }

    Expand-Archive -Path $WMIv2Zip -DestinationPath "$(Get-LabSourcesLocation -Local)/Tools" -ErrorAction "Stop" -Force
    try
    {
        Remove-Item -Path $WMIv2Zip -Force -ErrorAction "Stop" -ErrorVariable "RemoveItemErr"
    }
    catch
    {
        Write-ScreenInfo -Message ("Failed to delete '{0}' ({1})" -f $WMIZip, $RemoveItemErr.ErrorRecord.Exception.Message) -Type "Warning"
    }

    if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { Sync-LabAzureLabSources -Filter WmiExplorer.exe }

    # ConfigurationManager
    foreach ($vm in $vms)
    {
        $role = $vm.Roles.Where( { $_.Name -eq 'ConfigurationManager' })
        $cmVersion = if ($role.Properties.ContainsKey('Version')) { $role.Properties.Version } else { '2103' }
        $cmBranch = if ($role.Properties.ContainsKey('Branch')) { $role.Properties.Branch } else { 'CB' }

        $VMInstallDirectory = 'C:\Install'
        $CMBinariesDirectory = "$labSources\SoftwarePackages\CM-$($cmVersion)-$cmBranch"
        $CMPreReqsDirectory = "$labSources\SoftwarePackages\CM-Prereqs-$($cmVersion)-$cmBranch"
        $VMCMBinariesDirectory = "{0}\CM" -f $VMInstallDirectory
        $VMCMPreReqsDirectory = "{0}\CM-PreReqs" -f $VMInstallDirectory

        $cmDownloadUrl = Get-LabConfigurationItem -Name "ConfigurationManagerUrl$($cmVersion)$($cmBranch)"

        if (-not $cmDownloadUrl)
        {
            Write-LogFunctionExitWithError -Message "No URI configuration for CM version $cmVersion, branch $cmBranch."
        }

        #region CM binaries
        $CMZipPath = "{0}\SoftwarePackages\{1}" -f $labsources, ((Split-Path $CMDownloadURL -Leaf) -replace "\.exe$", ".zip")

        try
        {
            $CMZipObj = Get-LabInternetFile -Uri $CMDownloadURL -Path (Split-Path -Path $CMZipPath -Parent) -FileName (Split-Path -Path $CMZipPath -Leaf) -PassThru -ErrorAction "Stop" -ErrorVariable "GetLabInternetFileErr"
        }
        catch
        {
            $Message = "Failed to download from '{0}' ({1})" -f $CMDownloadURL, $GetLabInternetFileErr.ErrorRecord.Exception.Message
            Write-LogFunctionExitWithError -Message $Message
        }
        #endregion

        #region Extract CM binaries
        try
        {
            if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
            {
                Invoke-LabCommand -Computer $vm -ScriptBlock {    
                    $null = mkdir -Force $VMCMBinariesDirectory        
                    Expand-Archive -Path $CMZipObj.FullName -DestinationPath $VMCMBinariesDirectory -Force
                } -Variable (Get-Variable VMCMBinariesDirectory, CMZipObj)
            }
            else
            {
                Expand-Archive -Path $CMZipObj.FullName -DestinationPath $CMBinariesDirectory -Force -ErrorAction "Stop" -ErrorVariable "ExpandArchiveErr"
                Copy-LabFileItem -Path $CMBinariesDirectory/* -Destination $VMCMBinariesDirectory -ComputerName $vm -Recurse
            }
        
        }
        catch
        {
            $Message = "Failed to initiate extraction to '{0}' ({1})" -f $CMBinariesDirectory, $ExpandArchiveErr.ErrorRecord.Exception.Message
            Write-LogFunctionExitWithError -Message $Message
        }
        #endregion

        #region Download CM prerequisites
        switch ($cmBranch)
        {
            "CB"
            {
                if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
                {
                    Install-LabSoftwarePackage -ComputerName $vm -LocalPath $VMCMBinariesDirectory\SMSSETUP\BIN\X64\setupdl.exe -CommandLine "/NOUI $VMCMPreReqsDirectory" -UseShellExecute -AsScheduledJob
                    break       
                }
                
                try
                {
                    $p = Start-Process -FilePath $CMBinariesDirectory\SMSSETUP\BIN\X64\setupdl.exe -ArgumentList "/NOUI", $CMPreReqsDirectory -PassThru -ErrorAction "Stop" -ErrorVariable "StartProcessErr" -Wait
                    Copy-LabFileItem -Path $CMPreReqsDirectory/* -Destination $VMCMPreReqsDirectory -Recurse -ComputerName $vm
                }
                catch
                {
                    $Message = "Failed to initiate download of CM pre-req files to '{0}' ({1})" -f $CMPreReqsDirectory, $StartProcessErr.ErrorRecord.Exception.Message
                    Write-LogFunctionExitWithError -Message $Message
                }
            }
            "TP"
            {
                $Messages = @(
                    "Directory '{0}' is intentionally empty." -f $CMPreReqsDirectory
                    "The prerequisites will be downloaded by the installer within the VM."
                    "This is a workaround due to a known issue with TP 2002 baseline: https://twitter.com/codaamok/status/1268588138437509120"
                )

                try
                {
                    $CMPreReqsDirectory = "$(Get-LabSourcesLocation -Local)\SoftwarePackages\CM-Prereqs-$($cmVersion)-$cmBranch"
                    $PreReqDirObj = New-Item -Path $CMPreReqsDirectory -ItemType "Directory" -Force -ErrorAction "Stop" -ErrorVariable "CreateCMPreReqDir"
                    Set-Content -Path ("{0}\readme.txt" -f $PreReqDirObj.FullName) -Value $Messages -ErrorAction "SilentlyContinue"
                }
                catch
                {
                    $Message = "Failed to create CM prerequisite directory '{0}' ({1})" -f $CMPreReqsDirectory, $CreateCMPreReqDir.ErrorRecord.Exception.Message
                    Write-LogFunctionExitWithError -Message $Message
                }
            }
        }

        $siteParameter = @{
            CMServerName        = $vm
            CMBinariesDirectory = $CMBinariesDirectory
            Branch              = $cmBranch
            CMPreReqsDirectory  = $CMPreReqsDirectory
            CMSiteCode          = 'AL1'
            CMSiteName          = 'AutomatedLab-01'
            CMRoles             = 'Management Point', 'Distribution Point'
            DatabaseName        = 'ALCMDB'
        }

        if ($role.Properties.ContainsKey('SiteCode'))
        {
            $siteParameter.CMSiteCode = $role.Properties.SiteCode
        }

        if ($role.Properties.ContainsKey('SiteName'))
        {
            $siteParameter.CMSiteName = $role.Properties.SiteName
        }

        if ($role.Properties.ContainsKey('ProductId'))
        {
            $siteParameter.CMProductId = $role.Properties.ProductId
        }

        $validRoles = @(
            "None",
            "Management Point", 
            "Distribution Point", 
            "Software Update Point", 
            "Reporting Services Point", 
            "Endpoint Protection Point"
        )
        if ($role.Properties.ContainsKey('Roles'))
        {
            $siteParameter.CMRoles = if ($role.Properties.Roles.Split(',') -contains 'None')
            {
                'None'
            }
            else
            {
                $role.Properties.Roles.Split(',') | Where-Object { $_ -in $validRoles } | Sort-Object -Unique
            }
        }

        if ($role.Properties.ContainsKey('SqlServerName'))
        {
            $sql = $role.Properties.SqlServerName

            if (-not (Get-LabVm -ComputerName $sql.Split('.')[0]))
            {
                Write-ScreenInfo -Type Warning -Message "No SQL server called $sql found in lab. If you wanted to use an existing instance, don't forget to add it with the -SkipDeployment parameter"
            }

            $siteParameter.SqlServerName = $sql
        }
        else
        {
            $sql = (Get-LabVM -Role SQLServer2014, SQLServer2016, SQLServer2017, SQLServer2019 | Select-Object -First 1).Fqdn

            if (-not $sql)
            {
                Write-LogFunctionExitWithError -Message "No SQL server found in lab. Cannot install SCCM"
            }

            $siteParameter.SqlServerName = $sql
        }

        Invoke-LabCommand -ComputerName $sql.Split('.')[0] -ActivityName 'Add computer account as local admin (why...)' -ScriptBlock {
            Add-LocalGroupMember -Group Administrators -Member "$($vm.DomainName)\$($vm.Name)`$"
        } -Variable (Get-Variable vm)

        if ($role.Properties.ContainsKey('DatabaseName'))
        {
            $siteParameter.DatabaseName = $role.Properties.DatabaseName
        }

        if ($role.Properties.ContainsKey('AdminUser'))
        {
            $siteParameter.AdminUser = $role.Properties.AdminUser
        }

        if ($role.Properties.ContainsKey('WsusContentPath'))
        {
            $siteParameter.WsusContentPath = $role.Properties.WsusContentPath
        }
        Install-CMSite @siteParameter

        Restart-LabVM -ComputerName $vm

        if (Test-LabMachineInternetConnectivity -ComputerName $vm)
        {
            Write-ScreenInfo -Type Verbose -Message "$vm is connected, beginning update process"
            $updateParameter = Sync-Parameter -Command (Get-Command Update-CMSite) -Parameters $siteParameter
            Update-CMSite @updateParameter
        }
    }
    #endregion
}


function Add-LabVMUserRight
{
    param
    (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByMachine')]
        [String[]]$ComputerName,
        [string[]]$UserName,
        [validateSet('SeNetworkLogonRight',
                'SeRemoteInteractiveLogonRight',
                'SeBatchLogonRight',
                'SeInteractiveLogonRight',
                'SeServiceLogonRight',
                'SeDenyNetworkLogonRight',
                'SeDenyInteractiveLogonRight',
                'SeDenyBatchLogonRight',
                'SeDenyServiceLogonRight',
                'SeDenyRemoteInteractiveLogonRight',
                'SeTcbPrivilege',
                'SeMachineAccountPrivilege',
                'SeIncreaseQuotaPrivilege',
                'SeBackupPrivilege',
                'SeChangeNotifyPrivilege',
                'SeSystemTimePrivilege',
                'SeCreateTokenPrivilege',
                'SeCreatePagefilePrivilege',
                'SeCreateGlobalPrivilege',
                'SeDebugPrivilege',
                'SeEnableDelegationPrivilege',
                'SeRemoteShutdownPrivilege',
                'SeAuditPrivilege',
                'SeImpersonatePrivilege',
                'SeIncreaseBasePriorityPrivilege',
                'SeLoadDriverPrivilege',
                'SeLockMemoryPrivilege',
                'SeSecurityPrivilege',
                'SeSystemEnvironmentPrivilege',
                'SeManageVolumePrivilege',
                'SeProfileSingleProcessPrivilege',
                'SeSystemProfilePrivilege',
                'SeUndockPrivilege',
                'SeAssignPrimaryTokenPrivilege',
                'SeRestorePrivilege',
                'SeShutdownPrivilege',
                'SeSynchAgentPrivilege',
                'SeTakeOwnershipPrivilege'
        )]
        [Alias('Priveleges')]
        [string[]]$Privilege
    )

    $Job = @()

    foreach ($Computer in $ComputerName)
    {
        $param = @{}
        $param.add('UserName', $UserName)
        $param.add('Right', $Right)
        $param.add('ComputerName', $Computer)

        $Job += Invoke-LabCommand -ComputerName $Computer -ActivityName "Configure user rights '$($Privilege -join ', ')' for user accounts: '$($UserName -join ', ')'" -NoDisplay -AsJob -PassThru -ScriptBlock {
            Add-AccountPrivilege -UserName $UserName -Privilege $Privilege
        } -Variable (Get-Variable UserName, Privilege) -Function (Get-Command Add-AccountPrivilege)
    }
    Wait-LWLabJob -Job $Job -NoDisplay
}


function Clear-Lab
{
    [cmdletBinding()]

    param ()

    Write-LogFunctionEntry

    $Script:data = $null
    foreach ($module in $MyInvocation.MyCommand.Module.NestedModules | Where-Object ModuleType -eq 'Script')
    {
        & $module { $Script:data = $null }
    }

    Write-LogFunctionExit
}


function Clear-LabCache
{
    [cmdletBinding()]

    param()

    Write-LogFunctionEntry

    if ($IsLinux -or $IsMacOs)
    {
        $storePath = Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores'
        Get-ChildItem -Path $storePath -Filter *.xml | Remove-Item -Force -ErrorAction SilentlyContinue
    }
    else
    {
        Remove-Item -Path Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\AutomatedLab\Cache -Force -ErrorAction SilentlyContinue
    }

    Remove-Variable -Name AL_*,
        cacheAzureRoleSizes,
        cacheVmImages,
        cacheVMs,
        taskStart,
        PSLog_*,
        labDeploymentNoNewLine,
        labExported,
        indent,
        firstAzureVMCreated,
        existingAzureNetworks -ErrorAction SilentlyContinue

    Write-PSFMessage 'AutomatedLab cache removed'

    Write-LogFunctionExit
}


function Copy-LabFileItem
{
    param (
        [Parameter(Mandatory)]
        [string[]]$Path,

        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [string]$DestinationFolderPath,

        [switch]$Recurse,

        [bool]$FallbackToPSSession = $true,

        [bool]$UseAzureLabSourcesOnAzureVm = $true,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machine(s) $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }

    $connectedMachines = @{ }

    foreach ($machine in $machines)
    {
        $cred = $machine.GetCredential((Get-Lab))

        if ($machine.HostType -eq 'HyperV' -or
            (-not $UseAzureLabSourcesOnAzureVm -and $machine.HostType -eq 'Azure') -or
            ($path -notlike "$labSources*" -and $machine.HostType -eq 'Azure')
        )
        {
            try
            {
                if ($DestinationFolderPath -match ':')
                {
                    $letter = ($DestinationFolderPath -split ':')[0]
                    $drive = New-PSDrive -Name "$($letter)_on_$machine" -PSProvider FileSystem -Root "\\$machine\$($letter)`$" -Credential $cred -ErrorAction Stop
                }
                else
                {
                    $drive = New-PSDrive -Name "C_on_$machine" -PSProvider FileSystem -Root "\\$machine\c$" -Credential $cred -ErrorAction Stop
                }
                Write-Debug -Message "Drive '$($drive.Name)' created"
                $connectedMachines.Add($machine.Name, $drive)
            }
            catch
            {
                if (-not $FallbackToPSSession)
                {
                    Microsoft.PowerShell.Utility\Write-Error -Message "Could not create a SMB connection to '$machine' ('\\$machine\c$'). Files could not be copied." -TargetObject $machine -Exception $_.Exception
                    continue
                }

                $session = New-LabPSSession -ComputerName $machine -IgnoreAzureLabSources
                foreach ($p in $Path)
                {

                    $destination = if (-not $DestinationFolderPath)
                    {
                        '/'
                    }
                    else
                    {
                        $DestinationFolderPath
                    }
                    try
                    {
                        Send-Directory -SourceFolderPath $p -Session $session -DestinationFolderPath $destination
                        if ($PassThru)
                        {
                            $destination
                        }
                    }
                    catch
                    {
                        Write-Error -ErrorRecord $_
                    }
                }
            }
        }
        else
        {
            foreach ($p in $Path)
            {
                $session = New-LabPSSession -ComputerName $machine
                $folderName = Split-Path -Path $p -Leaf
                $targetFolder = if ($folderName -eq "*")
                {
                    "\"
                }
                else
                {
                    $folderName
                }
                $destination = if (-not $DestinationFolderPath)
                {
                    Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath $targetFolder
                }
                else
                {
                    Join-Path -Path $DestinationFolderPath -ChildPath $targetFolder
                }

                Invoke-LabCommand -ComputerName $machine -ActivityName Copy-LabFileItem -ScriptBlock {

                    Copy-Item -Path $p -Destination $destination -Recurse -Force

                } -NoDisplay -Variable (Get-Variable -Name p, destination)
            }

        }
    }

    Write-Verbose -Message "Copying the items '$($Path -join ', ')' to machines '$($connectedMachines.Keys -join ', ')'"

    foreach ($machine in $connectedMachines.GetEnumerator())
    {
        Write-Debug -Message "Starting copy job for machine '$($machine.Name)'..."

        if ($DestinationFolderPath)
        {
            $drive = "$($machine.Value):"
            $newDestinationFolderPath = Split-Path -Path $DestinationFolderPath -NoQualifier
            $newDestinationFolderPath = Join-Path -Path $drive -ChildPath $newDestinationFolderPath

            if (-not (Test-Path -Path $newDestinationFolderPath))
            {
                New-Item -ItemType Directory -Path $newDestinationFolderPath | Out-Null
            }
        }
        else
        {
            $newDestinationFolderPath = "$($machine.Value):\"
        }

        foreach ($p in $Path)
        {
            try
            {
                Copy-Item -Path $p -Destination $newDestinationFolderPath -Recurse -Force -ErrorAction Stop
                Write-Debug -Message '...finished'
                if ($PassThru)
                {
                    Join-Path -Path $DestinationFolderPath -ChildPath (Split-Path -Path $p -Leaf)
                }
            }
            catch
            {
                Write-Error -ErrorRecord $_
            }
        }

        $machine.Value | Remove-PSDrive
        Write-Debug -Message "Drive '$($drive.Name)' removed"
        Write-Verbose -Message "Files copied on to machine '$($machine.Name)'"
    }

    Write-LogFunctionExit
}


function Disable-LabTelemetry
{
    if ($IsLinux -or $IsMacOs)
    {
        $null = New-Item -ItemType File -Path "$((Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot))/telemetry.disabled" -Force
    }
    else
    {
        [Environment]::SetEnvironmentVariable('AUTOMATEDLAB_TELEMETRY_OPTIN', 'false', 'Machine')
        $env:AUTOMATEDLAB_TELEMETRY_OPTIN = 'false'
    }
}


function Enable-LabHostRemoting
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    param(
        [switch]$Force,

        [switch]$NoDisplay
    )

    if ($IsLinux) { return }

    Write-LogFunctionEntry

    if (-not (Test-IsAdministrator))
    {
        throw 'This function needs to be called in an elevated PowerShell session.'
    }
    $message = "AutomatedLab needs to enable / relax some PowerShell Remoting features.`nYou will be asked before each individual change. Are you OK to proceed?"
    if (-not $Force)
    {
        $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption 'Enabling WinRM and CredSsp' -Message $message -Default 1
        if ($choice -eq 0 -and -not $Force)
        {
            throw "Changes to PowerShell remoting on the host machine are mandatory to use AutomatedLab. You can make the changes later by calling 'Enable-LabHostRemoting'"
        }
    }

    if ((Get-Service -Name WinRM).Status -ne 'Running')
    {
        Write-ScreenInfo 'Starting the WinRM service. This is required in order to read the WinRM configuration...' -NoNewLine
        Start-Service -Name WinRM
        Start-Sleep -Seconds 2
        Write-ScreenInfo done
    }

    if ((Get-Service -Name smphost).StartType -eq 'Disabled')
    {
        Write-ScreenInfo "The StartupType of the service 'smphost' is set to disabled. Setting it to 'manual'. This is required in order to read use the cmdlets in the 'Storage' module..." -NoNewLine
        Set-Service -Name smphost -StartupType Manual
        Write-ScreenInfo done
    }

    #1067

    # force English language output for Get-WSManCredSSP call
    [Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'; $WSManCredSSP = Get-WSManCredSSP
    if ((-not $WSManCredSSP[0].Contains('The machine is configured to') -and -not $WSManCredSSP[0].Contains('WSMAN/*')) -or (Get-Item -Path WSMan:/localhost/Client/Auth/CredSSP).Value -eq $false)
    {
        $message = "AutomatedLab needs to enable CredSsp on the host in order to delegate credentials to the lab VMs.`nAre you OK with enabling CredSsp?"
        if (-not $Force)
        {
            $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption 'Enabling WinRM and CredSsp' -Message $message -Default 1
            if ($choice -eq 0 -and -not $Force)
            {
                throw "CredSsp is required in order to deploy VMs with AutomatedLab. You can make the changes later by calling 'Enable-LabHostRemoting'"
            }
        }

        Write-ScreenInfo "Enabling CredSSP on the host machine for role 'Client'. Delegated computers = '*'..." -NoNewLine
        Enable-WSManCredSSP -Role Client -DelegateComputer * -Force | Out-Null
        Write-ScreenInfo done
    }
    else
    {
        Write-PSFMessage 'Remoting is enabled on the host machine'
    }

    $trustedHostsList = @((Get-Item -Path Microsoft.WSMan.Management\WSMan::localhost\Client\TrustedHosts).Value -split ',' |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ }
    )

    if (-not ($trustedHostsList -contains '*'))
    {
        Write-ScreenInfo -Message "TrustedHosts does not include '*'. Replacing the current value '$($trustedHostsList -join ', ')' with '*'" -Type Warning

        if (-not $Force)
        {
            $message = "AutomatedLab needs to connect to machines using NTLM which does not support mutual authentication. Hence all possible machine names must be put into trusted hosts.`n`nAre you ok with putting '*' into TrustedHosts to allow the host connect to any possible lab VM?"
            $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption "Setting TrustedHosts to '*'" -Message $message -Default 1
            if ($choice -eq 0 -and -not $Force)
            {
                throw "AutomatedLab requires the host to connect to any possible lab machine using NTLM. You can make the changes later by calling 'Enable-LabHostRemoting'"
            }
        }

        Set-Item -Path Microsoft.WSMan.Management\WSMan::localhost\Client\TrustedHosts -Value '*' -Force
    }
    else
    {
        Write-PSFMessage "'*' added to TrustedHosts"
    }

    $allowFreshCredentials = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1')
    $allowFreshCredentialsWhenNTLMOnly = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly', '1')
    $allowSavedCredentials = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentials', '1')
    $allowSavedCredentialsWhenNTLMOnly = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentialsWhenNTLMOnly', '1')

    if (
        ($allowFreshCredentials -ne '*' -and $allowFreshCredentials -ne 'WSMAN/*') -or
        ($allowFreshCredentialsWhenNTLMOnly -ne '*' -and $allowFreshCredentialsWhenNTLMOnly -ne 'WSMAN/*') -or
        ($allowSavedCredentials -ne '*' -and $allowSavedCredentials -ne 'TERMSRV/*') -or
        ($allowSavedCredentialsWhenNTLMOnly -ne '*' -and $allowSavedCredentialsWhenNTLMOnly -ne 'TERMSRV/*')
    )
    {
        $message = @'
The following local policies will be configured if not already done.

Computer Configuration -> Administrative Templates -> System -> Credentials Delegation ->
Allow Delegating Fresh Credentials WSMAN/*
Allow Delegating Fresh Credentials when NTLM only WSMAN/*
Allow Delegating Saved Credentials TERMSRV/*
Allow Delegating Saved Credentials when NTLM only TERMSRV/*

This is required to allow the host computer / AutomatedLab to delegate lab credentials to the lab VMs.

Are you OK with that?
'@

        if (-not $Force)
        {
            $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption "Setting TrustedHosts to '*'" -Message $message -Default 1
            if ($choice -eq 0 -and -not $Force)
            {
                throw "AutomatedLab requires the the previously mentioned policies to be set. You can make the changes later by calling 'Enable-LabHostRemoting'"
            }
        }
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1')
    if ($value -ne '*' -and $value -ne 'WSMAN/*')
    {
        Write-ScreenInfo 'Configuring the local policy for allowing credentials to be delegated to all machines (*). You can find the modified policy using gpedit.msc by navigating to: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials' -Type Warning
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowFreshCredentials', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowFresh', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1', 'WSMAN/*') | Out-Null
    }
    else
    {
        Write-PSFMessage "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials' configured correctly"
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly', '1')
    if ($value -ne '*' -and $value -ne 'WSMAN/*')
    {
        Write-ScreenInfo 'Configuring the local policy for allowing credentials to be delegated to all machines (*). You can find the modified policy using gpedit.msc by navigating to: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication' -Type Warning
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowFreshCredentialsWhenNTLMOnly', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowFreshNTLMOnly', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly', '1', 'WSMAN/*') | Out-Null
    }
    else
    {
        Write-PSFMessage "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials when NTLM only' configured correctly"
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentials', '1')
    if ($value -ne '*' -and $value -ne 'TERMSRV/*')
    {
        Write-ScreenInfo 'Configuring the local policy for allowing credentials to be delegated to all machines (*). You can find the modified policy using gpedit.msc by navigating to: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials' -Type Warning
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowSavedCredentials', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowSaved', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentials', '1', 'TERMSRV/*') | Out-Null
    }
    else
    {
        Write-PSFMessage "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Saved Credentials' configured correctly"
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentialsWhenNTLMOnly', '1')
    if ($value -ne '*' -and $value -ne 'TERMSRV/*')
    {
        Write-ScreenInfo 'Configuring the local policy for allowing credentials to be delegated to all machines (*). You can find the modified policy using gpedit.msc by navigating to: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication' -Type Warning
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowSavedCredentialsWhenNTLMOnly', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowSavedNTLMOnly', 1) | Out-Null
        [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentialsWhenNTLMOnly', '1', 'TERMSRV/*') | Out-Null
    }
    else
    {
        Write-PSFMessage "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Saved Credentials when NTLM only' configured correctly"
    }

    $allowEncryptionOracle = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -ErrorAction SilentlyContinue).AllowEncryptionOracle
    if ($allowEncryptionOracle -ne 2)
    {
        $message = @"
A CredSSP vulnerability has been addressed with`n`n
CVE-2018-0886`n
https://support.microsoft.com/en-us/help/4093492/credssp-updates-for-cve-2018-0886-march-13-2018`n`n
The security setting must be relexed in order to connect to machines using CredSSP that do not have the security patch installed. Are you fine setting the value 'AllowEncryptionOracle' to '2'?
"@

        if (-not $Force)
        {
            $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption "Setting AllowEncryptionOracle to '2'" -Message $message -Default 1
            if ($choice -eq 0 -and -not $Force)
            {
                throw "AutomatedLab requires the the AllowEncryptionOracle setting to be 2. You can make the changes later by calling 'Enable-LabHostRemoting'"
            }
        }

        Write-ScreenInfo "Setting registry value 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters\AllowEncryptionOracle' to '2'."
        New-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Force | Out-Null
        Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Name AllowEncryptionOracle -Value 2 -Force
    }


    Write-LogFunctionExit
}


function Enable-LabTelemetry
{
    if ($IsLinux -or $IsMacOs)
    {
        $null = New-Item -ItemType File -Path "$((Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot))/telemetry.enabled" -Force
    }
    else
    {
        [Environment]::SetEnvironmentVariable('AUTOMATEDLAB_TELEMETRY_OPTIN', 'true', 'Machine')
        $env:AUTOMATEDLAB_TELEMETRY_OPTIN = 'true'
    }
}


function Enable-LabVMRemoting
{
    [cmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    if (-not (Get-LabVM))
    {
        Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    if ($ComputerName)
    {
        $machines = Get-LabVM -All | Where-Object { $_.Name -in $ComputerName }
    }
    else
    {
        $machines = Get-LabVM -All
    }

    $hypervVMs = $machines | Where-Object HostType -eq 'HyperV'
    if ($hypervVMs)
    {
        Enable-LWHypervVMRemoting -ComputerName $hypervVMs
    }

    $azureVms = $machines | Where-Object HostType -eq 'Azure'
    if ($azureVms)
    {
        Enable-LWAzureVMRemoting -ComputerName $azureVms
    }

    $vmwareVms = $machines | Where-Object HostType -eq 'VmWare'
    if ($vmwareVms)
    {
        Enable-LWVMWareVMRemoting -ComputerName $vmwareVms
    }

    Write-LogFunctionExit
}


function Export-Lab
{
    [cmdletBinding()]

    param ()

    Write-LogFunctionEntry

    $lab = Get-Lab

    Remove-Item -Path $lab.LabFilePath

    Remove-Item -Path $lab.MachineDefinitionFiles[0].Path
    Remove-Item -Path $lab.DiskDefinitionFiles[0].Path

    $lab.Machines.Export($lab.MachineDefinitionFiles[0].Path)
    try
    {
        $lab.Disks.Export($lab.DiskDefinitionFiles[0].Path)
    }
    catch
    {
        $tmpList = [AutomatedLab.ListXmlStore[AutomatedLab.Disk]]::new()
        foreach ($d in $lab.Disks)
        {
            $tmpList.Add($d)
        }
        $tmpList.Export($lab.DiskDefinitionFiles[0].Path)
    }
    $lab.Machines.Clear()
    if ($lab.Disks)
    {
        $lab.Disks.Clear()
    }

    $lab.Export($lab.LabFilePath)

    Import-Lab -Name $lab.Name -NoValidation -NoDisplay -DoNotRemoveExistingLabPSSessions

    Write-LogFunctionExit
}


function Get-Lab
{
    [CmdletBinding()]
    [OutputType([AutomatedLab.Lab])]

    param (
        [switch]$List
    )

    if ($List)
    {
        $labsPath = "$((Get-LabConfigurationItem -Name LabAppDataRoot))/Labs"

        foreach ($path in Get-ChildItem -Path $labsPath -Directory -ErrorAction SilentlyContinue)
        {
            $labXmlPath = Join-Path -Path $path.FullName -ChildPath Lab.xml
            if (Test-Path -Path $labXmlPath)
            {
                Split-Path -Path $path -Leaf
            }
        }
    }
    else
    {
        if ($Script:data)
        {
            $Script:data
        }
        else
        {
            Write-Error 'Lab data not available. Use Import-Lab and reference a Lab.xml to import one.'
        }
    }
}


function Get-LabAvailableOperatingSystem
{
    [cmdletBinding(DefaultParameterSetName='Local')]
    [OutputType([AutomatedLab.OperatingSystem])]
    param
    (
        [Parameter(ParameterSetName='Local')]
        [string[]]$Path,

        [switch]$UseOnlyCache,

        [switch]$NoDisplay,

        [Parameter(ParameterSetName = 'Azure')]
        [switch]$Azure,

        [Parameter(Mandatory, ParameterSetName = 'Azure')]
        $Location
    )

    Write-LogFunctionEntry

    if (-not $Path)
    {
        $Path = "$(Get-LabSourcesLocationInternal -Local)/ISOs"
    }

    $labData = if (Get-LabDefinition -ErrorAction SilentlyContinue) {Get-LabDefinition} elseif (Get-Lab -ErrorAction SilentlyContinue) {Get-Lab}
    if ($labData -and $labData.DefaultVirtualizationEngine -eq 'Azure') { $Azure = $true }
    $storeLocationName = if ($Azure.IsPresent) { 'Azure' } else { 'Local' }

    if ($Azure)
    {
        if (-not (Get-AzContext -ErrorAction SilentlyContinue).Subscription)
        {
            throw 'Please login to Azure before trying to list Azure image SKUs'
        }

        if (-not $Location -and $labData.AzureSettings.DefaultLocation.Location)
        {
            $Location = $labData.AzureSettings.DefaultLocation.DisplayName
        }

        if (-not $Location)
        {
            throw 'Please add your subscription using Add-LabAzureSubscription before viewing available operating systems, or use the parameters -Azure and -Location'
        }

        $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.Azure.AzureOSImage
        if ($IsLinux -or $IsMacOS)
        {
            $cachedSkus = try { $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Stores/$($storeLocationName)OperatingSystems.xml")) } catch { }
        }
        else
        {
            $cachedSkus = try { $type::ImportFromRegistry('Cache', "$($storeLocationName)OperatingSystems") } catch { }
        }

        $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.OperatingSystem
        $cachedOsList = New-Object $type
        foreach ($os in $cachedSkus)
        {
            # Converting ToLower() as Azure Stack Hub images seem to mix case
            # building longer SKU to take care of bad naming conventions with the linux images
            $osname = '{0}_{1}' -f $os.Skus, $os.PublisherName
            $cachedOs = [AutomatedLab.OperatingSystem]::new($osname.ToLower(), $true)
            if ($cachedOs.OperatingSystemName) {$cachedOsList.Add($cachedOs)}
        }

        if ($UseOnlyCache)
        {
            return $cachedOsList
        }
        
        $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.OperatingSystem
        $osList = New-Object $type
        $skus = (Get-LabAzureAvailableSku -Location $Location)

        foreach ($sku in $skus)
        {
            # Converting ToLower() as Azure Stack Hub images seem to mix case
            $osname = '{0}_{1}' -f $sku.Skus, $sku.PublisherName
            $azureOs = [AutomatedLab.OperatingSystem]::new($osname.ToLower(), $true)
            if (-not $azureOs.OperatingSystemName) { continue }

            $osList.Add($azureOs )
        }

        $osList.Timestamp = Get-Date
    
        if ($IsLinux -or $IsMacOS)
        {
            $osList.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Stores/$($storeLocationName)OperatingSystems.xml"))
        }
        else
        {
            $osList.ExportToRegistry('Cache', "$($storeLocationName)OperatingSystems")
        }

        return $osList.ToArray()
    }

    if (-not (Test-IsAdministrator))
    {
        throw 'This function needs to be called in an elevated PowerShell session.'
    }

    $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.OperatingSystem
    $isoFiles = Get-ChildItem -Path $Path -Filter *.iso -Recurse
    Write-PSFMessage "Found $($isoFiles.Count) ISO files"

    #read the cache
    try
    {
        if ($IsLinux -or $IsMacOS)
        {
            $cachedOsList = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Stores/$($storeLocationName)OperatingSystems.xml"))
        }
        else
        {
            $cachedOsList = $type::ImportFromRegistry('Cache', "$($storeLocationName)OperatingSystems")
        }

        Write-ScreenInfo -Type Verbose -Message "found $($cachedOsList.Count) OS images in the cache"
    }
    catch
    {
        Write-PSFMessage 'Could not read OS image info from the cache'
    }

    $present, $absent = $cachedOsList.Where({$_.IsoPath -and (Test-Path $_.IsoPath)}, 'Split')
    foreach ($cachedOs in $absent)
    {
        Write-ScreenInfo -Type Verbose -Message "Evicting $cachedOs from cache"
        if ($global:AL_OperatingSystems) { $null = $global:AL_OperatingSystems.Remove($cachedOs) }
        $null = $cachedOsList.Remove($cachedOs)
    }

    if (($UseOnlyCache -and $present))
    {
        Write-ScreenInfo -Type Verbose -Message 'Returning all present ISO files - cache may not be up to date'
        return $present
    }

    $presentFiles = $present.IsoPath | Select-Object -Unique
    $allFiles = ($isoFiles | Where FullName -notin $cachedOsList.MetaData).FullName
    if ($presentFiles -and $allFiles -and -not (Compare-Object -Reference $presentFiles -Difference $allFiles -ErrorAction SilentlyContinue | Where-Object SideIndicator -eq '=>'))
    {
        Write-ScreenInfo -Type Verbose -Message 'ISO cache seems to be up to date'
        if (Test-Path -Path $Path -PathType Leaf)
        {
            return ($present | Where-Object IsoPath -eq $Path)
        }
        else
        {
            return $present
        }
    }

    if ($UseOnlyCache -and -not $present)
    {
        Write-Error -Message "Get-LabAvailableOperatingSystems is used with the switch 'UseOnlyCache', however the cache is empty. Please run 'Get-LabAvailableOperatingSystems' first by pointing to your LabSources\ISOs folder" -ErrorAction Stop
    }

    if (-not $cachedOsList)
    {
        $cachedOsList = New-Object $type
    }

    Write-ScreenInfo -Message "Scanning $($isoFiles.Count) files for operating systems" -NoNewLine

    foreach ($isoFile in $isoFiles)
    {
        if ($cachedOsList.IsoPath -contains $isoFile.FullName) { continue }
        Write-ProgressIndicator
        Write-PSFMessage "Mounting ISO image '$($isoFile.FullName)'"
        $drive = Mount-LabDiskImage -ImagePath $isoFile.FullName -StorageType ISO -PassThru

        Get-PSDrive | Out-Null #This is just to refresh the drives. Somehow if this cmdlet is not called, PowerShell does not see the new drives.

        $opSystems = if ($IsLinux)
        {
            Get-LabImageOnLinux -MountPoint $drive.DriveLetter -IsoFile $isoFile
        }
        else
        {
            Get-LabImageOnWindows -DriveLetter $drive.DriveLetter -IsoFile $isoFile
        }

        if (-not $opSystems)
        {
            $null = $cachedOsList.MetaData.Add($isoFile.FullName)
        }

        foreach ($os in $opSystems)
        {
            $cachedOsList.Add($os)
        }

        Write-PSFMessage 'Dismounting ISO'
        [void] (Dismount-LabDiskImage -ImagePath $isoFile.FullName)
        Write-ProgressIndicator
    }

    $cachedOsList.Timestamp = Get-Date

    if ($IsLinux -or $IsMacOS)
    {
        $cachedOsList.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Stores/$($storeLocationName)OperatingSystems.xml"))
    }
    else
    {
        $cachedOsList.ExportToRegistry('Cache', "$($storeLocationName)OperatingSystems")
    }

    if (Test-Path -Path $Path -PathType Leaf)
    {
        $cachedOsList.ToArray() | Where-Object IsoPath -eq $Path
    }
    else
    {
        $cachedOsList.ToArray()
    }

    Write-ProgressIndicatorEnd
    Write-ScreenInfo "Found $($cachedOsList.Count) OS images."
    Write-LogFunctionExit
}


function Get-LabCache
{
    [CmdletBinding()]
    param
    ( )

    $regKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey('CurrentUser', 'Default')
    try
    {
        $key = $regKey.OpenSubKey('Software\AutomatedLab\Cache')
        foreach ($value in $key.GetValueNames())
        {
            $content = [xml]$key.GetValue($value)
            $timestamp = $content.SelectSingleNode('//Timestamp')
            [pscustomobject]@{
                Store     = $value
                Timestamp = $timestamp.datetime -as [datetime]
                Content   = $content
            }
        }
    }
    catch { Write-PSFMessage -Message "Cache not yet created" }
}


function Get-LabConfigurationItem
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        $Default
    )

    if ($Name)
    {
        $setting = (Get-PSFConfig -Module AutomatedLab -Name $Name -Force).Value
        if (-not $setting -and $Default)
        {
            return $Default
        }

        return $setting
    }

    Get-PSFConfig -Module AutomatedLab
}


function Get-LabSoftwarePackage
{
    param (
        [Parameter(Mandatory)]
        [ValidateScript({
                    Test-Path -Path $_
                }
        )]
        [string]$Path,

        [string]$CommandLine,

        [int]$Timeout = 10
    )

    Write-LogFunctionEntry

    $pack = New-Object -TypeName AutomatedLab.SoftwarePackage
    $pack.CommandLine = $CommandLine
    $pack.CopyFolder = $CopyFolder
    $pack.Path = $Path
    $pack.Timeout = $timeout

    $pack

    Write-LogFunctionExit
}


function Get-LabSourcesLocation
{
    param
    (
        [switch]$Local
    )

    Get-LabSourcesLocationInternal -Local:$Local
}


function Get-LabVariable
{
    $pattern = 'AL_([a-zA-Z0-9]{8})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{12})'
    Get-Variable -Scope Global | Where-Object Name -Match $pattern
}


function Get-LabWindowsFeature
{
    [cmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [ValidateNotNullOrEmpty()]
        [string[]]$FeatureName = '*',

        [switch]$UseLocalCredential,

        [int]$ProgressIndicator = 5,

        [switch]$NoDisplay,

        [switch]$AsJob
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName

    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machines $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }

    Write-ScreenInfo -Message "Getting Windows Feature(s) '$($FeatureName -join ', ')' on computer(s) '$($ComputerName -join ', ')'" -TaskStart

    if ($AsJob)
    {
        Write-ScreenInfo -Message 'Getting Windows Feature(s) in the background' -TaskEnd
    }

    $stoppedMachines = (Get-LabVMStatus -ComputerName $ComputerName -AsHashTable).GetEnumerator() | Where-Object Value -eq Stopped
    if ($stoppedMachines)
    {
        Start-LabVM -ComputerName $stoppedMachines.Name -Wait
    }

    $hyperVMachines = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'HyperV'}
    $azureMachines = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'Azure'}

    if ($hyperVMachines)
    {
        $params = @{
            Machine            = $hyperVMachines
            FeatureName        = $FeatureName
            UseLocalCredential = $UseLocalCredential
            AsJob              = $AsJob
        }

        $result = Get-LWHypervWindowsFeature @params
    }
    elseif ($azureMachines)
    {
        $params = @{
            Machine            = $azureMachines
            FeatureName        = $FeatureName
            UseLocalCredential = $UseLocalCredential
            AsJob              = $AsJob
        }

        $result = Get-LWAzureWindowsFeature @params
    }

    $result

    if (-not $AsJob)
    {
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }
    Write-LogFunctionExit
}


function Import-Lab
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByPath', Position = 1)]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 1)]
        [string]$Name,

        [Parameter(Mandatory, ParameterSetName = 'ByValue', Position = 1)]
        [byte[]]$LabBytes,

        [switch]$DoNotRemoveExistingLabPSSessions,

        [switch]$PassThru,

        [switch]$NoValidation,

        [switch]$NoDisplay
    )

    Write-LogFunctionEntry

    Clear-Lab

    if ($PSCmdlet.ParameterSetName -in 'ByPath', 'ByName')
    {
        if ($Name)
        {
            $Path = "$((Get-LabConfigurationItem -Name LabAppDataRoot))/Labs/$Name"
        }

        if (Test-Path -Path $Path -PathType Container)
        {
            $newPath = Join-Path -Path $Path -ChildPath Lab.xml
            if (-not (Test-Path -Path $newPath -PathType Leaf))
            {
                throw "The file '$newPath' is missing. Please point to an existing lab file / folder."
            }
            else
            {
                $Path = $newPath
            }
        }
        elseif (Test-Path -Path $Path -PathType Leaf)
        {
            #file is there, do nothing
        }
        else
        {
            throw "The file '$Path' is missing. Please point to an existing lab file / folder."
        }

        if ((Get-PSsession) -and -not $DoNotRemoveExistingLabPSSessions)
        {
            Get-PSSession | Where-Object Name -ne WinPSCompatSession | Remove-PSSession -ErrorAction SilentlyContinue
        }

        if (-not (Test-LabHostRemoting))
        {
            Enable-LabHostRemoting -Force:$(Get-LabConfigurationItem -Name DoNotPrompt -Default $false)
        }

        if (-not ($IsLinux -or $IsMacOs) -and -not (Test-IsAdministrator))
        {
            throw 'Import-Lab needs to be called in an elevated PowerShell session.'
        }

        if (-not ($IsLinux -or $IsMacOs))
        {
            if ((Get-Item -Path Microsoft.WSMan.Management\WSMan::localhost\Client\TrustedHosts -Force).Value -ne '*')
            {
                Write-ScreenInfo 'The host system is not prepared yet. Call the cmdlet Set-LabHost to set the requirements' -Type Warning
                Write-ScreenInfo 'After installing the lab you should undo the changes for security reasons' -Type Warning
                throw "TrustedHosts need to be set to '*' in order to be able to connect to the new VMs. Please run the cmdlet 'Set-LabHostRemoting' to make the required changes."
            }

            $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1')
            if ($value -ne '*' -and $value -ne 'WSMAN/*')
            {
                throw "Please configure the local policy for allowing credentials to be delegated. Use gpedit.msc and look at the following policy: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials. Just add '*' to the server list to be able to delegate credentials to all machines."
            }
        }

        if (-not $NoValidation)
        {
            Write-ScreenInfo -Message 'Validating lab definition' -TaskStart

            $validation = Test-LabDefinition -Path $Path -Quiet

            if ($validation)
            {
                Write-ScreenInfo -Message 'Success' -TaskEnd -Type Info
            }
            else
            {
                break
            }
        }

        if (Test-Path -Path $Path)
        {
            $Script:data = [AutomatedLab.Lab]::Import((Resolve-Path -Path $Path))

            $Script:data | Add-Member -MemberType ScriptMethod -Name GetMachineTargetPath -Value {
                param (
                    [string]$MachineName
                )

                (Join-Path -Path $this.Target.Path -ChildPath $MachineName)
            }
        }
        else
        {
            throw 'Lab Definition File not found'
        }

        #import all the machine files referenced in the lab.xml
        $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.Machine
        $importMethodInfo = $type.GetMethod('Import',[System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static, [System.Type]::DefaultBinder, [Type[]]@([string]), $null)

        try
        {
            $Script:data.Machines = $importMethodInfo.Invoke($null, $Script:data.MachineDefinitionFiles[0].Path)

            if ($Script:data.MachineDefinitionFiles.Count -gt 1)
            {
                foreach ($machineDefinitionFile in $Script:data.MachineDefinitionFiles[1..($Script:data.MachineDefinitionFiles.Count - 1)])
                {
                    $Script:data.Machines.AddFromFile($machineDefinitionFile.Path)
                }
            }

            if ($Script:data.Machines)
            {
                $Script:data.Machines | Add-Member -MemberType ScriptProperty -Name UnattendedXmlContent -Value {
                    if ($this.OperatingSystem.Version -lt '6.2')
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath 'Unattended2008.xml'
                    }
                    else
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath 'Unattended2012.xml'
                    }
                    if ($this.OperatingSystemType -eq 'Linux' -and $this.LinuxType -eq 'RedHat' -and $this.OperatingSystem.Version -lt 8.0)
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath ks_defaultLegacy.cfg
                    }
                    if ($this.OperatingSystemType -eq 'Linux' -and $this.LinuxType -eq 'RedHat' -and $this.OperatingSystem.Version -ge 8.0)
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath ks_default.cfg
                    }
                    if ($this.OperatingSystemType -eq 'Linux' -and $this.LinuxType -eq 'Suse')
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath autoinst_default.xml
                    }
                    if ($this.OperatingSystemType -eq 'Linux' -and $this.LinuxType -eq 'Ubuntu')
                    {
                        $Path = Join-Path -Path (Get-Lab).Sources.UnattendedXml.Value -ChildPath cloudinit_default.yml
                    }
                    return (Get-Content -Path $Path)
                }
            }
        }
        catch
        {
            Write-Error -Message "No machines imported from file $machineDefinitionFile" -Exception $_.Exception -ErrorAction Stop
        }

        $minimumAzureModuleVersion = Get-LabConfigurationItem -Name MinimumAzureModuleVersion
        if (($Script:data.Machines | Where-Object HostType -eq Azure) -and -not (Test-LabAzureModuleAvailability -AzureStack:$($script:data.AzureSettings.IsAzureStack)))
        {
            throw "The Azure PowerShell modules required to run AutomatedLab are not available. Please install them using the command 'Install-LabAzureRequiredModule'"
        }

        if (($Script:data.Machines | Where-Object HostType -eq VMWare) -and ((Get-PSSnapin -Name VMware.VimAutomation.*).Count -ne 1))
        {
            throw 'The VMWare snapin was not loaded. Maybe it is missing'
        }

        #import all the disk files referenced in the lab.xml
        $type = Get-Type -GenericType AutomatedLab.ListXmlStore -T AutomatedLab.Disk
        $importMethodInfo = $type.GetMethod('Import',[System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static, [System.Type]::DefaultBinder, [Type[]]@([string]), $null)

        try
        {
            $Script:data.Disks = $importMethodInfo.Invoke($null, $Script:data.DiskDefinitionFiles[0].Path)
            if ($script:lab.DefaultVirtualizationEngine -eq 'HyperV') { $Script:data.Disks = Get-LabVHDX -All }

            if ($Script:data.DiskDefinitionFiles.Count -gt 1)
            {
                foreach ($diskDefinitionFile in $Script:data.DiskDefinitionFiles[1..($Script:data.DiskDefinitionFiles.Count - 1)])
                {
                    $Script:data.Disks.AddFromFile($diskDefinitionFile.Path)
                }
            }
        }
        catch
        {
            Write-ScreenInfo "No disks imported from file '$diskDefinitionFile': $($_.Exception.Message)" -Type Warning
        }

        if ($Script:data.VMWareSettings.DataCenterName)
        {
            Add-LabVMWareSettings -DataCenterName $Script:data.VMWareSettings.DataCenterName `
            -DataStoreName $Script:data.VMWareSettings.DataStoreName `
            -ResourcePoolName $Script:data.VMWareSettings.ResourcePoolName `
            -VCenterServerName $Script:data.VMWareSettings.VCenterServerName `
            -Credential ([System.Management.Automation.PSSerializer]::Deserialize($Script:data.VMWareSettings.Credential))
        }

        if (-not ($IsLinux -or $IsMacOs) -and (Get-LabConfigurationItem -Name OverridePowerPlan))
        {
            $powerSchemeBackup = (powercfg.exe -GETACTIVESCHEME).Split(':')[1].Trim().Split()[0]
            powercfg.exe -setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
        }
    }
    elseif($PSCmdlet.ParameterSetName -eq 'ByValue')
    {
        $Script:data = [AutomatedLab.Lab]::Import($LabBytes)
    }

    if ($PassThru)
    {
        $Script:data
    }

    $global:AL_CurrentLab = $Script:data

    Write-ScreenInfo ("Lab '{0}' hosted on '{1}' imported with {2} machines" -f $Script:data.Name, $Script:data.DefaultVirtualizationEngine ,$Script:data.Machines.Count) -Type Info

    Register-LabArgumentCompleters

    Write-LogFunctionExit -ReturnValue $true
}


function Install-Lab
{
    [cmdletBinding()]
    param (
        [switch]$NetworkSwitches,
        [switch]$BaseImages,
        [switch]$VMs,
        [switch]$Domains,
        [switch]$AdTrusts,
        [switch]$DHCP,
        [switch]$Routing,
        [switch]$PostInstallations,
        [switch]$SQLServers,
        [switch]$Orchestrator2012,
        [switch]$WebServers,
        [Alias('Sharepoint2013')]
        [switch]$SharepointServer,
        [switch]$CA,
        [switch]$ADFS,
        [switch]$DSCPullServer,
        [switch]$VisualStudio,
        [switch]$Office2013,
        [switch]$Office2016,
        [switch]$AzureServices,
        [switch]$TeamFoundation,
        [switch]$FailoverStorage,
        [switch]$FailoverCluster,
        [switch]$FileServer,
        [switch]$HyperV,
        [switch]$WindowsAdminCenter,
        [switch]$Scvmm,
        [switch]$Scom,
        [switch]$Dynamics,
        [switch]$RemoteDesktop,
        [switch]$ConfigurationManager,
        [switch]$StartRemainingMachines,
        [switch]$CreateCheckPoints,
        [switch]$InstallRdsCertificates,
        [switch]$InstallSshKnownHosts,
        [switch]$PostDeploymentTests,
        [switch]$NoValidation,
        [int]$DelayBetweenComputers
    )

    Write-LogFunctionEntry
    $global:PSLog_Indent = 0

    $labDiskDeploymentInProgressPath = Get-LabConfigurationItem -Name DiskDeploymentInProgressPath

    #perform full install if no role specific installation is requested
    $performAll = -not ($PSBoundParameters.Keys | Where-Object { $_ -notin ('NoValidation', 'DelayBetweenComputers' + [System.Management.Automation.Internal.CommonParameters].GetProperties().Name)}).Count

    if (-not $Global:labExported -and -not (Get-Lab -ErrorAction SilentlyContinue))
    {
        Export-LabDefinition -Force -ExportDefaultUnattendedXml

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }
    if ($Global:labExported -and -not (Get-Lab -ErrorAction SilentlyContinue))
    {
        if ($NoValidation)
        {
            Import-Lab -Path (Get-LabDefinition).LabFilePath -NoValidation
        }
        else
        {
            Import-Lab -Path (Get-LabDefinition).LabFilePath
        }
    }

    if (-not $Script:data)
    {
        Write-Error 'No definitions imported, so there is nothing to test. Please use Import-Lab against the xml file'
        return
    }

    try
    {
        [AutomatedLab.LabTelemetry]::Instance.LabStarted((Get-Lab).Export(), (Get-Module AutomatedLabCore)[-1].Version, $PSVersionTable.BuildVersion, $PSVersionTable.PSVersion)
    }
    catch
    {
        # Nothing to catch - if an error occurs, we simply do not get telemetry.
        Write-PSFMessage -Message ('Error sending telemetry: {0}' -f $_.Exception)
    }

    Unblock-LabSources

    Send-ALNotification -Activity 'Lab started' -Message ('Lab deployment started with {0} machines' -f (Get-LabVM).Count) -Provider (Get-LabConfigurationItem -Name Notifications.SubscribedProviders)
    $engine = $Script:data.DefaultVirtualizationEngine

    if (Get-LabVM -All -IncludeLinux | Where-Object HostType -eq 'HyperV')
    {
        Update-LabMemorySettings
    }

    if ($engine -ne 'Azure' -and ($NetworkSwitches -or $performAll))
    {
        Write-ScreenInfo -Message 'Creating virtual networks' -TaskStart

        New-LabNetworkSwitches

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($BaseImages -or $performAll) -and (Get-LabVM -All | Where-Object HostType -eq 'HyperV'))
    {
        try
        {
            if (Test-Path -Path $labDiskDeploymentInProgressPath)
            {
                Write-ScreenInfo "Another lab disk deployment seems to be in progress. If this is not correct, please delete the file '$labDiskDeploymentInProgressPath'." -Type Warning
                Write-ScreenInfo 'Waiting until other disk deployment is finished.' -NoNewLine
                do
                {
                    Write-ScreenInfo -Message . -NoNewLine
                    Start-Sleep -Seconds 15
                } while (Test-Path -Path $labDiskDeploymentInProgressPath)
            }
            Write-ScreenInfo 'done'

            Write-ScreenInfo -Message 'Creating base images' -TaskStart

            New-Item -Path $labDiskDeploymentInProgressPath -ItemType File -Value ($Script:data).Name | Out-Null

            New-LabBaseImages

            Write-ScreenInfo -Message 'Done' -TaskEnd
        }
        finally
        {
            Remove-Item -Path $labDiskDeploymentInProgressPath -Force
        }
    }

    if ($VMs -or $performAll)
    {
        try
        {
            if ((Test-Path -Path $labDiskDeploymentInProgressPath) -and (Get-LabVM -All -IncludeLinux | Where-Object HostType -eq 'HyperV'))
            {
                Write-ScreenInfo "Another lab disk deployment seems to be in progress. If this is not correct, please delete the file '$labDiskDeploymentInProgressPath'." -Type Warning
                Write-ScreenInfo 'Waiting until other disk deployment is finished.' -NoNewLine
                do
                {
                    Write-ScreenInfo -Message . -NoNewLine
                    Start-Sleep -Seconds 15
                } while (Test-Path -Path $labDiskDeploymentInProgressPath)
            }
            Write-ScreenInfo 'done'

            if (Get-LabVM -All -IncludeLinux | Where-Object HostType -eq 'HyperV')
            {
                Write-ScreenInfo -Message 'Creating Additional Disks' -TaskStart
                New-Item -Path $labDiskDeploymentInProgressPath -ItemType File -Value ($Script:data).Name | Out-Null
                New-LabVHDX
                Write-ScreenInfo -Message 'Done' -TaskEnd
            }

            Write-ScreenInfo -Message 'Creating VMs' -TaskStart
            #add a hosts entry for each lab machine
            $hostFileAddedEntries = 0
            foreach ($machine in ($Script:data.Machines | Where-Object { [string]::IsNullOrEmpty($_.FriendlyName) }))
            {
                if ($machine.HostType -ne 'HyperV' -or (Get-LabConfigurationItem -Name SkipHostFileModification))
                {
                    continue
                }
                $defaultNic = $machine.NetworkAdapters | Where-Object Default
                $addresses = if ($defaultNic)
                {
                    ($defaultNic | Select-Object -First 1).Ipv4Address.IpAddress.AddressAsString
                }
                if (-not $addresses)
                {
                    $addresses = @($machine.NetworkAdapters[0].Ipv4Address.IpAddress.AddressAsString)
                }

                if (-not $addresses)
                {
                    continue
                }

                #only the first addredd of a machine is added as for local connectivity the other addresses don't make a difference
                $hostFileAddedEntries += Add-HostEntry -HostName $machine.Name -IpAddress $addresses[0] -Section $Script:data.Name
                $hostFileAddedEntries += Add-HostEntry -HostName $machine.FQDN -IpAddress $addresses[0] -Section $Script:data.Name
            }

            if ($hostFileAddedEntries)
            {
                Write-ScreenInfo -Message "The hosts file has been added $hostFileAddedEntries records. Clean them up using 'Remove-Lab' or manually if needed" -Type Warning
            }

            if ($script:data.Machines | Where-Object SkipDeployment -eq $false)
            {
                New-LabVM -Name ($script:data.Machines | Where-Object SkipDeployment -eq $false) -CreateCheckPoints:$CreateCheckPoints
            }

            #VMs created, export lab definition again to update MAC addresses
            Set-LabDefinition -Machines $Script:data.Machines
            Export-LabDefinition -Force -ExportDefaultUnattendedXml -Silent

            Write-ScreenInfo -Message 'Done' -TaskEnd
        }
        finally
        {
            Remove-Item -Path $labDiskDeploymentInProgressPath -Force -ErrorAction SilentlyContinue
        }
    }

    #Root DCs are installed first, then the Routing role is installed in order to allow domain joined routers in the root domains
    if (($Domains -or $performAll) -and (Get-LabVM -Role RootDC | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Root Domain Controllers' -TaskStart
        foreach ($azVm in (Get-LabVM -IncludeLinux -Filter {$_.HostType -eq 'Azure'}))
        {
            $nicCount = 0
            foreach ($azNic in $azVm.NetworkAdapters)
            {
                $dns = ($Lab.VirtualNetworks | Where-Object ResourceName -eq $azNic.VirtualSwitch).DnsServers.AddressAsString
                if ($nic.Ipv4DnsServers.AddressAsString) {$dns = $nic.Ipv4DnsServers.AddressAsString}
                if ($dns.Count -eq 0) { continue }
                # Set NIC configured DNS
                [string]$vmNicId = (Get-LWAzureVm -ComputerName $azVm.ResourceName).NetworkProfile.NetworkInterfaces.Id.Where({$_.EndsWith("nic$nicCount")})
                $vmNic = Get-AzNetworkInterface -ResourceId $vmNicId
                if ($dns -and $vmNic.DnsSettings.DnsServers -and -not (Compare-Object -ReferenceObject $dns -DifferenceObject $vmNic.DnsSettings.DnsServers)) { continue }

                $vmNic.DnsSettings.DnsServers = [Collections.Generic.List[string]]$dns
                $null = $vmNic | Set-AzNetworkInterface
                $nicCount++
            }
        }

        foreach ($azNet in ((Get-Lab).VirtualNetworks | Where HostType -eq 'Azure'))
        {
            # Set VNET DNS
            if ($null -eq $aznet.DnsServers.AddressAsString) { continue }

            $net = Get-AzVirtualNetwork -Name $aznet.ResourceName
            if (-not $net.DhcpOptions)
            {
                $net.DhcpOptions = @{}
            }

            $net.DhcpOptions.DnsServers = [Collections.Generic.List[string]]$aznet.DnsServers.AddressAsString
            $null = $net | Set-AzVirtualNetwork
        }

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role RootDC | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message "Machines with RootDC role to be installed: '$((Get-LabVM -Role RootDC).Name -join ', ')'"
        Install-LabRootDcs -CreateCheckPoints:$CreateCheckPoints
        
        New-LabADSubnet

        # Set account expiration for builtin account and lab domain account
        foreach ($machine in (Get-LabVM -Role RootDC -ErrorAction SilentlyContinue))
        {
            $userName = (Get-Lab).Domains.Where({ $_.Name -eq $machine.DomainName }).Administrator.UserName
            Invoke-LabCommand -ActivityName 'Setting PasswordNeverExpires for deployment accounts in AD' -ComputerName $machine -ScriptBlock {
                Set-ADUser -Identity $userName -PasswordNeverExpires $true -Confirm:$false
            } -Variable (Get-Variable userName) -NoDisplay
        }

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Routing -or $performAll) -and (Get-LabVM -Role Routing | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Configuring routing' -TaskStart

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Routing | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabRouting

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($DHCP -or $performAll) -and (Get-LabVM -Role DHCP | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Configuring DHCP servers' -TaskStart

        #Install-DHCP
        Write-Error 'The DHCP role is not implemented yet'

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Domains -or $performAll) -and (Get-LabVM -Role FirstChildDC | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Child Domain Controllers' -TaskStart

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role FirstChildDC | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Write-ScreenInfo -Message "Machines with FirstChildDC role to be installed: '$((Get-LabVM -Role FirstChildDC).Name -join ', ')'"
        Install-LabFirstChildDcs -CreateCheckPoints:$CreateCheckPoints

        New-LabADSubnet

        $allDcVMs = Get-LabVM -Role RootDC, FirstChildDC | Where-Object { -not $_.SkipDeployment }

        if ($allDcVMs)
        {
            if ($CreateCheckPoints)
            {
                Write-ScreenInfo -Message 'Creating a snapshot of all domain controllers'
                Checkpoint-LabVM -ComputerName $allDcVMs -SnapshotName 'Post Forest Setup'
            }
        }
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Domains -or $performAll) -and (Get-LabVM -Role DC | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Additional Domain Controllers' -TaskStart

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role DC | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Write-ScreenInfo -Message "Machines with DC role to be installed: '$((Get-LabVM -Role DC).Name -join ', ')'"
        Install-LabDcs -CreateCheckPoints:$CreateCheckPoints

        New-LabADSubnet

        $allDcVMs = Get-LabVM -Role RootDC, FirstChildDC, DC | Where-Object { -not $_.SkipDeployment }

        if ($allDcVMs)
        {
            if ($CreateCheckPoints)
            {
                Write-ScreenInfo -Message 'Creating a snapshot of all domain controllers'
                Checkpoint-LabVM -ComputerName $allDcVMs -SnapshotName 'Post Forest Setup'
            }
        }

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($AdTrusts -or $performAll) -and ((Get-LabVM -Role RootDC | Measure-Object).Count -gt 1))
    {
        Write-ScreenInfo -Message 'Configuring AD trusts' -TaskStart
        Install-LabADDSTrust
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if ((Get-LabVm -Filter {-not $_.SkipDeployment -and $_.Roles.Count -eq 0}))
    {
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName (Get-LabVm -Filter {-not $_.SkipDeployment -and $_.Roles.Count -eq 0}) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
    }
    
    if (($FileServer -or $performAll) -and (Get-LabVM -Role FileServer))
    {
        Write-ScreenInfo -Message 'Installing File Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role FileServer | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabFileServers -CreateCheckPoints:$CreateCheckPoints

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($CA -or $performAll) -and (Get-LabVM -Role CaRoot, CaSubordinate))
    {
        Write-ScreenInfo -Message 'Installing Certificate Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role CaRoot,CaSubordinate | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabCA -CreateCheckPoints:$CreateCheckPoints

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if(($HyperV -or $performAll) -and (Get-LabVm -Role HyperV | Where-Object {-not $_.SkipDeployment}))
    {
        Write-ScreenInfo -Message 'Installing HyperV servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role HyperV | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Install-LabHyperV

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }
    
    if (($FailoverStorage -or $performAll) -and (Get-LabVM -Role FailoverStorage | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Failover Storage' -TaskStart

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role FailoverStorage | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Start-LabVM -RoleName FailoverStorage -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        Install-LabFailoverStorage

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($FailoverCluster -or $performAll) -and (Get-LabVM -Role FailoverNode, FailoverStorage | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Failover Cluster' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role FailoverNode, FailoverStorage | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Start-LabVM -RoleName FailoverNode,FailoverStorage -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        Install-LabFailoverCluster

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($SQLServers -or $performAll) -and (Get-LabVM -Role SQLServer | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing SQL Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role SQLServer | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        if (Get-LabVM -Role SQLServer2008)   { Write-ScreenInfo -Message "Machines to have SQL Server 2008 installed: '$((Get-LabVM -Role SQLServer2008).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2008R2) { Write-ScreenInfo -Message "Machines to have SQL Server 2008 R2 installed: '$((Get-LabVM -Role SQLServer2008R2).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2012)   { Write-ScreenInfo -Message "Machines to have SQL Server 2012 installed: '$((Get-LabVM -Role SQLServer2012).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2014)   { Write-ScreenInfo -Message "Machines to have SQL Server 2014 installed: '$((Get-LabVM -Role SQLServer2014).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2016)   { Write-ScreenInfo -Message "Machines to have SQL Server 2016 installed: '$((Get-LabVM -Role SQLServer2016).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2017)   { Write-ScreenInfo -Message "Machines to have SQL Server 2017 installed: '$((Get-LabVM -Role SQLServer2017).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2019)   { Write-ScreenInfo -Message "Machines to have SQL Server 2019 installed: '$((Get-LabVM -Role SQLServer2019).Name -join ', ')'" }
        if (Get-LabVM -Role SQLServer2022)   { Write-ScreenInfo -Message "Machines to have SQL Server 2022 installed: '$((Get-LabVM -Role SQLServer2022).Name -join ', ')'" }
        Install-LabSqlServers -CreateCheckPoints:$CreateCheckPoints

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($ConfigurationManager -or $performAll) -and (Get-LabVm -Role ConfigurationManager -Filter {-not $_.SkipDeployment}))
    {
        Write-ScreenInfo -Message 'Deploying System Center Configuration Manager' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role ConfigurationManager | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabConfigurationManager
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($RemoteDesktop -or $performAll) -and (Get-LabVm -Role RDS -Filter {-not $_.SkipDeployment}))
    {
        Write-ScreenInfo -Message 'Deploying Remote Desktop Services' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role RDS | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabRemoteDesktopServices
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Dynamics -or $performAll) -and (Get-LabVm -Role Dynamics | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Dynamics' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Dynamics | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabDynamics -CreateCheckPoints:$CreateCheckPoints
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($DSCPullServer -or $performAll) -and (Get-LabVM -Role DSCPullServer | Where-Object { -not $_.SkipDeployment }))
    {
        Start-LabVM -RoleName DSCPullServer -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role DSCPullServer | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message 'Installing DSC Pull Servers' -TaskStart
        Install-LabDscPullServer

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($ADFS -or $performAll) -and (Get-LabVM -Role ADFS))
    {
        Write-ScreenInfo -Message 'Configuring ADFS' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role ADFS | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Install-LabAdfs

        Write-ScreenInfo -Message 'Done' -TaskEnd

        Write-ScreenInfo -Message 'Configuring ADFS Proxies' -TaskStart

        Install-LabAdfsProxy

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($WebServers -or $performAll) -and (Get-LabVM -Role WebServer | Where-Object { -not $_.SkipDeployment }))
    {
        Write-ScreenInfo -Message 'Installing Web Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role WebServer | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Write-ScreenInfo -Message "Machines to have Web Server role installed: '$((Get-LabVM -Role WebServer | Where-Object { -not $_.SkipDeployment }).Name -join ', ')'"
        Install-LabWebServers -CreateCheckPoints:$CreateCheckPoints

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($WindowsAdminCenter -or $performAll) -and (Get-LabVm -Role WindowsAdminCenter))
    {
        Write-ScreenInfo -Message 'Installing Windows Admin Center Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role WindowsAdminCenter | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Write-ScreenInfo -Message "Machines to have Windows Admin Center installed: '$((Get-LabVM -Role WindowsAdminCenter | Where-Object { -not $_.SkipDeployment }).Name -join ', ')'"
        Install-LabWindowsAdminCenter

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Orchestrator2012 -or $performAll) -and (Get-LabVM -Role Orchestrator2012))
    {
        Write-ScreenInfo -Message 'Installing Orchestrator Servers' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Orchestrator2012 | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabOrchestrator2012

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($SharepointServer -or $performAll) -and (Get-LabVM -Role SharePoint))
    {
        Write-ScreenInfo -Message 'Installing SharePoint Servers' -TaskStart

        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role SharePoint | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Install-LabSharePoint

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($VisualStudio -or $performAll) -and (Get-LabVM -Role VisualStudio2013))
    {
        Write-ScreenInfo -Message 'Installing Visual Studio 2013' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role VisualStudio2013 | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message "Machines to have Visual Studio 2013 installed: '$((Get-LabVM -Role VisualStudio2013).Name -join ', ')'"
        Install-VisualStudio2013

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($VisualStudio -or $performAll) -and (Get-LabVM -Role VisualStudio2015))
    {
        Write-ScreenInfo -Message 'Installing Visual Studio 2015' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role VisualStudio2015 | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message "Machines to have Visual Studio 2015 installed: '$((Get-LabVM -Role VisualStudio2015).Name -join ', ')'"
        Install-VisualStudio2015

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Office2013 -or $performAll) -and (Get-LabVM -Role Office2013))
    {
        Write-ScreenInfo -Message 'Installing Office 2013' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Office2013 | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message "Machines to have Office 2013 installed: '$((Get-LabVM -Role Office2013).Name -join ', ')'"
        Install-LabOffice2013

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($Office2016 -or $performAll) -and (Get-LabVM -Role Office2016))
    {
        Write-ScreenInfo -Message 'Installing Office 2016' -TaskStart
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Office2016 | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        Write-ScreenInfo -Message "Machines to have Office 2016 installed: '$((Get-LabVM -Role Office2016).Name -join ', ')'"
        Install-LabOffice2016

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($TeamFoundation -or $performAll) -and (Get-LabVM -Role Tfs2015,Tfs2017,Tfs2018,TfsBuildWorker,AzDevOps))
    {
        Write-ScreenInfo -Message 'Installing Team Foundation Server environment'
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role Tfs2015,Tfs2017,Tfs2018,TfsBuildWorker,AzDevOps | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
        Write-ScreenInfo -Message "Machines to have TFS or the build agent installed: '$((Get-LabVM -Role Tfs2015,Tfs2017,Tfs2018,TfsBuildWorker,AzDevOps).Name -join ', ')'"

        $machinesToStart = Get-LabVM -Role Tfs2015,Tfs2017,Tfs2018,TfsBuildWorker,AzDevOps | Where-Object -Property SkipDeployment -eq $false
        if ($machinesToStart)
        {
            Start-LabVm -ComputerName $machinesToStart -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        }

        Install-LabTeamFoundationEnvironment
        Write-ScreenInfo -Message 'Team Foundation Server environment deployed'
    }

    if (($Scvmm -or $performAll) -and (Get-LabVM -Role SCVMM))
    {
        Write-ScreenInfo -Message 'Installing SCVMM'
        Write-ScreenInfo -Message "Machines to have SCVMM Management or Console installed: '$((Get-LabVM -Role SCVMM).Name -join ', ')'"
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role SCVMM | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        $machinesToStart = Get-LabVM -Role SCVMM | Where-Object -Property SkipDeployment -eq $false
        if ($machinesToStart)
        {
            Start-LabVm -ComputerName $machinesToStart -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        }

        Install-LabScvmm
        Write-ScreenInfo -Message 'SCVMM environment deployed'
    }

    if (($Scom -or $performAll) -and (Get-LabVM -Role SCOM))
    {
        Write-ScreenInfo -Message 'Installing SCOM'
        Write-ScreenInfo -Message "Machines to have SCOM components installed: '$((Get-LabVM -Role SCOM).Name -join ', ')'"
        $jobs = Invoke-LabCommand -PreInstallationActivity -ActivityName 'Pre-installation' -ComputerName $(Get-LabVM -Role SCOM | Where-Object { -not $_.SkipDeployment }) -PassThru -NoDisplay
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null

        $machinesToStart = Get-LabVM -Role SCOM | Where-Object -Property SkipDeployment -eq $false
        if ($machinesToStart)
        {
            Start-LabVm -ComputerName $machinesToStart -ProgressIndicator 15 -PostDelaySeconds 5 -Wait
        }

        Install-LabScom
        Write-ScreenInfo -Message 'SCOM environment deployed'
    }

    if (($StartRemainingMachines -or $performAll) -and (Get-LabVM -IncludeLinux | Where-Object -Property SkipDeployment -eq $false))
    {
        $linuxHosts = (Get-LabVM -IncludeLinux | Where-Object OperatingSystemType -eq 'Linux').Count
        Write-ScreenInfo -Message 'Starting remaining machines' -TaskStart
        $timeoutRemaining = 60
        if ($linuxHosts -and -not (Get-LabConfigurationItem -Name DoNotWaitForLinux -Default $false))
        {
            $timeoutRemaining = 15
            Write-ScreenInfo -Type Warning -Message "There are $linuxHosts Linux hosts in the lab.
                On Windows, those are installed from scratch and do not use differencing disks.
        
                If you did not connect them to an external switch or deploy a router in your lab,
                AutomatedLab will not be able to reach your VMs, as PowerShell will not be installed.

                The timeout to wait for VMs to be accessible via PowerShell was reduced from 60 to 15
            minutes."

        }

        if ($null -eq $DelayBetweenComputers)
        {
            $hypervMachineCount = (Get-LabVM -IncludeLinux | Where-Object HostType -eq HyperV).Count
            if ($hypervMachineCount)
            {
                $DelayBetweenComputers = [System.Math]::Log($hypervMachineCount, 5) * 30
                Write-ScreenInfo -Message "DelayBetweenComputers not defined, value calculated is $DelayBetweenComputers seconds"
            }
            else
            {
                $DelayBetweenComputers = 0
            }            
        }

        Write-ScreenInfo -Message 'Waiting for machines to start up...' -NoNewLine

        $toStart = Get-LabVM -IncludeLinux:$(-not (Get-LabConfigurationItem -Name DoNotWaitForLinux -Default $false)) | Where-Object SkipDeployment -eq $false
        Start-LabVM -ComputerName $toStart -DelayBetweenComputers $DelayBetweenComputers -ProgressIndicator 30 -TimeoutInMinutes $timeoutRemaining -Wait

        $userName = (Get-Lab).DefaultInstallationCredential.UserName
        $nonDomainControllers = Get-LabVM -Filter { $_.Roles.Name -notcontains 'RootDc' -and $_.Roles.Name -notcontains 'DC' -and $_.Roles.Name -notcontains 'FirstChildDc' -and -not $_.SkipDeployment }
        if ($nonDomainControllers) {
            Invoke-LabCommand -ActivityName 'Setting PasswordNeverExpires for local deployment accounts' -ComputerName $nonDomainControllers -ScriptBlock {
                # Still supporting ANCIENT server 2008 R2 with it's lack of CIM cmdlets :'(
                    if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                    {
                        Get-CimInstance -Query "Select * from Win32_UserAccount where name = '$userName' and localaccount='true'" | Set-CimInstance -Property @{ PasswordExpires = $false}
                    }
                    else
                    {
                        Get-WmiObject -Query "Select * from Win32_UserAccount where name = '$userName' and localaccount='true'" | Set-WmiInstance -Arguments @{ PasswordExpires = $false}
                    }
            } -Variable (Get-Variable userName) -NoDisplay
        }

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    # A new bug surfaced where on some occasion, Azure IaaS workloads were not connected to the internet
    # until a restart was done
    if ($lab.DefaultVirtualizationEngine -eq 'Azure')
    {
        $azvms = Get-LabVm | Where-Object SkipDeployment -eq $false
        $disconnectedVms = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $azvms -ScriptBlock { $null -eq (Get-NetConnectionProfile -IPv4Connectivity Internet -ErrorAction SilentlyContinue) } | Where-Object { $_}
        if ($disconnectedVms) { Restart-LabVm $disconnectedVms.PSComputerName -Wait -NoDisplay -NoNewLine }
    }

    if (($PostInstallations -or $performAll) -and (Get-LabVM | Where-Object -Property SkipDeployment -eq $false))
    {
        $machines = Get-LabVM | Where-Object { -not $_.SkipDeployment }
        $jobs = Invoke-LabCommand -PostInstallationActivity -ActivityName 'Post-installation' -ComputerName $machines -PassThru -NoDisplay
        #PostInstallations can be installed as jobs or as direct calls. If there are jobs returned, wait until they are finished
        $jobs | Where-Object { $_ -is [System.Management.Automation.Job] } | Wait-Job | Out-Null
    }

    if (($AzureServices -or $performAll) -and (Get-LabAzureWebApp))
    {
        Write-ScreenInfo -Message 'Starting deployment of Azure services' -TaskStart

        Install-LabAzureServices

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if ($InstallRdsCertificates -or $performAll)
    {
        Write-ScreenInfo -Message 'Installing RDS certificates of lab machines' -TaskStart
        
        Install-LabRdsCertificate
        
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if (($InstallSshKnownHosts -and (Get-LabVm).SshPublicKey) -or ($performAll-and (Get-LabVm).SshPublicKey))
    {
        Write-ScreenInfo -Message "Adding lab machines to $home/.ssh/known_hosts" -TaskStart
        
        Install-LabSshKnownHost
        
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    try
    {
        [AutomatedLab.LabTelemetry]::Instance.LabFinished((Get-Lab).Export())
    }
    catch
    {
        # Nothing to catch - if an error occurs, we simply do not get telemetry.
        Write-PSFMessage -Message ('Error sending telemetry: {0}' -f $_.Exception)
    }

    Initialize-LabWindowsActivation -ErrorAction SilentlyContinue

    if (-not $NoValidation -and ($performAll -or $PostDeploymentTests))
    {
        if ((Get-Module -ListAvailable -Name Pester -ErrorAction SilentlyContinue).Version -ge [version]'5.0')
        {
            if ($m = Get-Module -Name Pester | Where-Object Version -lt ([version]'5.0'))
            {
                Write-PSFMessage "The loaded version of Pester $($m.Version) is not compatible with AutomatedLab. Unloading it." -Level Verbose
                $m | Remove-Module
            }
            
            Write-ScreenInfo -Type Verbose -Message "Testing deployment with Pester"
            $result = Invoke-LabPester -Lab (Get-Lab) -Show Normal -PassThru
            if ($result.Result -eq 'Failed')
            {
                Write-ScreenInfo -Type Error -Message "Lab deployment seems to have failed. The following tests were not passed:"
            }

            foreach ($fail in $result.Failed)
            {
                Write-ScreenInfo -Type Error -Message "$($fail.Name)"
            }
        }
        else
        {
            Write-Warning "Cannot run post-deployment Pester test as there is no Pester version 5.0+ installed. Please run 'Install-Module -Name Pester -Force' if you want the post-deployment script to work. You can start the post-deployment tests separately with the command 'Install-Lab -PostDeploymentTests'"
        }
    }

    Send-ALNotification -Activity 'Lab finished' -Message 'Lab deployment successfully finished.' -Provider (Get-LabConfigurationItem -Name Notifications.SubscribedProviders)

    Write-LogFunctionExit
}


function Install-LabSoftwarePackage
{
    param (
        [Parameter(Mandatory, ParameterSetName = 'SinglePackage')]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'SingleLocalPackage')]
        [ValidateNotNullOrEmpty()]
        [string]$LocalPath,

        [Parameter(ParameterSetName = 'SinglePackage')]
        [Parameter(ParameterSetName = 'SingleLocalPackage')]
        [ValidateNotNullOrEmpty()]
        [string]$CommandLine,

        [int]$Timeout = 10,

        [Parameter(ParameterSetName = 'SinglePackage')]
        [Parameter(ParameterSetName = 'SingleLocalPackage')]
        [bool]$CopyFolder,

        [Parameter(Mandatory, ParameterSetName = 'SinglePackage')]
        [Parameter(Mandatory, ParameterSetName = 'SingleLocalPackage')]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'MulitPackage')]
        [AutomatedLab.Machine[]]$Machine,

        [Parameter(Mandatory, ParameterSetName = 'MulitPackage')]
        [AutomatedLab.SoftwarePackage]$SoftwarePackage,

        [string]$WorkingDirectory,

        [switch]$DoNotUseCredSsp,

        [switch]$AsJob,

        [switch]$AsScheduledJob,

        [switch]$UseExplicitCredentialsForScheduledJob,

        [switch]$UseShellExecute,

        [int[]]$ExpectedReturnCodes,

        [switch]$PassThru,

        [switch]$NoDisplay,

        [int]$ProgressIndicator = 5
    )

    Write-LogFunctionEntry
    $parameterSetName = $PSCmdlet.ParameterSetName

    if ($Path -and (Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        if (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $Path)
        {
            $parameterSetName = 'SingleLocalPackage'
            $LocalPath = $Path
        }
    }

    if ($parameterSetName -eq 'SinglePackage')
    {
        if (-not (Test-Path -Path $Path))
        {
            Write-Error "The file '$Path' cannot be found. Software cannot be installed"
            return
        }

        if (Get-Command -Name Unblock-File -ErrorAction SilentlyContinue)
        {
            Unblock-File -Path $Path
        }
    }

    if ($parameterSetName -like 'Single*')
    {
        $Machine = Get-LabVM -ComputerName $ComputerName

        if (-not $Machine)
        {
            Write-Error "The machine '$ComputerName' could not be found."
            return
        }

        $unknownMachines = (Compare-Object -ReferenceObject $ComputerName -DifferenceObject $Machine.Name).InputObject
        if ($unknownMachines)
        {
            Write-ScreenInfo "The machine(s) '$($unknownMachines -join ', ')' could not be found." -Type Warning
        }

        if ($AsScheduledJob -and $UseExplicitCredentialsForScheduledJob -and
        ($Machine | Group-Object -Property DomainName).Count -gt 1)
        {
            Write-Error "If you install software in a background job and require the scheduled job to run with explicit credentials, this task can only be performed on VMs being member of the same domain."
            return
        }
    }

    if ($Path)
    {
        Write-ScreenInfo -Message "Installing software package '$Path' on machines '$($ComputerName -join ', ')' " -TaskStart
    }
    else
    {
        Write-ScreenInfo -Message "Installing software package on VM '$LocalPath' on machines '$($ComputerName -join ', ')' " -TaskStart
    }

    if ('Stopped' -in (Get-LabVMStatus $ComputerName -AsHashTable).Values)
    {
        Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewLine
        Start-LabVM -ComputerName $ComputerName -Wait -ProgressIndicator 30 -NoNewline
    }

    $jobs = @()

    $parameters = @{ }
    $parameters.Add('ComputerName', $ComputerName)
    $parameters.Add('DoNotUseCredSsp', $DoNotUseCredSsp)
    $parameters.Add('PassThru', $True)
    $parameters.Add('AsJob', $True)
    $parameters.Add('ScriptBlock', (Get-Command -Name Install-SoftwarePackage).ScriptBlock)

    if ($parameterSetName -eq 'SinglePackage')
    {
        if ($CopyFolder)
        {
            $parameters.Add('DependencyFolderPath', [System.IO.Path]::GetDirectoryName($Path))
            $dependency = Split-Path -Path ([System.IO.Path]::GetDirectoryName($Path)) -Leaf
            $installPath = Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath "$($dependency)/$(Split-Path -Path $Path -Leaf)"
        }
        else
        {
            $parameters.Add('DependencyFolderPath', $Path)
            $installPath = Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath (Split-Path -Path $Path -Leaf)
        }        
    }
    elseif ($parameterSetName -eq 'SingleLocalPackage')
    {
        $installPath = $LocalPath
        if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and $CopyFolder)
        {
            $parameters.Add('DependencyFolderPath', [System.IO.Path]::GetDirectoryName($Path))
        }
    }
    else
    {
        if ($SoftwarePackage.CopyFolder)
        {
            $parameters.Add('DependencyFolderPath', [System.IO.Path]::GetDirectoryName($SoftwarePackage.Path))
            $dependency = Split-Path -Path ([System.IO.Path]::GetDirectoryName($SoftwarePackage.Path)) -Leaf
            $installPath = Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath "$($dependency)/$(Split-Path -Path $SoftwarePackage.Path -Leaf)"
        }
        else
        {
            $parameters.Add('DependencyFolderPath', $SoftwarePackage.Path)
            $installPath = Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath $(Split-Path -Path $SoftwarePackage.Path -Leaf)
        }        
    }

    $installParams = @{
        Path = $installPath
        CommandLine = $CommandLine
    }
    if ($AsScheduledJob) { $installParams.AsScheduledJob = $true }
    if ($UseShellExecute) { $installParams.UseShellExecute = $true }
    if ($AsScheduledJob -and $UseExplicitCredentialsForScheduledJob) { $installParams.Credential = $Machine[0].GetCredential((Get-Lab)) }
    if ($ExpectedReturnCodes) { $installParams.ExpectedReturnCodes = $ExpectedReturnCodes }
    if ($WorkingDirectory) { $installParams.WorkingDirectory = $WorkingDirectory }
    if ($CopyFolder -and (Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        $child = Split-Path -Leaf -Path $parameters.DependencyFolderPath
        $installParams.DestinationPath = Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath $child
    }

    $parameters.Add('ActivityName', "Installation of '$installPath'")

    Write-PSFMessage -Message "Starting background job for '$($parameters.ActivityName)'"

    $parameters.ScriptBlock = {
        Import-Module -Name AutomatedLab.Common -ErrorAction SilentlyContinue
        if ($installParams.Path.StartsWith('\\') -and (Test-Path /ALAzure))
        {
            # Often issues with Zone Mapping
            if ($installParams.DestinationPath)
            {
                $newPath = (New-Item -ItemType Directory -Path $installParams.DestinationPath -Force).FullName
            }
            else
            {
                $newPath = if ($IsLinux) { "/$(Split-Path -Path $installParams.Path -Leaf)" } else { "C:\$(Split-Path -Path $installParams.Path -Leaf)"}
            }

            $installParams.Remove('DestinationPath')
            Copy-Item -Path $installParams.Path -Destination $newPath -Force

            if (-not (Test-Path -Path $newPath -PathType Leaf))
            {
                $newPath = Join-Path -Path $newPath -ChildPath (Split-Path -Path $installParams.Path -Leaf)
            }
            $installParams.Path = $newPath
        }

        if ($PSEdition -eq 'core' -and $installParams.Contains('AsScheduledJob'))
        {
            # Core cannot work with PSScheduledJob module
            $xmlParameters = ([System.Management.Automation.PSSerializer]::Serialize($installParams, 2)) -replace "`r`n"
            $b64str = [Convert]::ToBase64String(([Text.Encoding]::Unicode.GetBytes("`$installParams = [System.Management.Automation.PSSerializer]::Deserialize('$xmlParameters'); Install-SoftwarePackage @installParams")))
            powershell.exe -EncodedCommand $b64str
        }
        else
        {
            Install-SoftwarePackage @installParams
        }
    }

    $parameters.Add('NoDisplay', $True)

    if (-not $AsJob)
    {
        Write-ScreenInfo -Message "Copying files and initiating setup on '$($ComputerName -join ', ')' and waiting for completion" -NoNewLine
    }

    $job = Invoke-LabCommand @parameters -Variable (Get-Variable -Name installParams) -Function (Get-Command Install-SoftwarePackage)

    if (-not $AsJob)
    {
        Write-PSFMessage "Waiting on job ID '$($job.ID -join ', ')' with name '$($job.Name -join ', ')'"
        $results = Wait-LWLabJob -Job $job -Timeout $Timeout -ProgressIndicator 15 -NoDisplay -PassThru #-ErrorAction SilentlyContinue

        Write-PSFMessage "Job ID '$($job.ID -join ', ')' with name '$($job.Name -join ', ')' finished"
    }

    if ($AsJob)
    {
        Write-ScreenInfo -Message 'Installation started in background' -TaskEnd
        if ($PassThru) { $job }
    }
    else
    {
        Write-ScreenInfo -Message 'Installation done' -TaskEnd
        if ($PassThru) { $results }
    }

    Write-LogFunctionExit
}


function Install-LabSoftwarePackages
{
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [AutomatedLab.Machine[]]$Machine,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [AutomatedLab.SoftwarePackage[]]$SoftwarePackage,

        [switch]$WaitForInstallation,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    $start = Get-Date
    $jobs = @()

    foreach ($m in $Machine)
    {
        Write-PSFMessage -Message "Install-LabSoftwarePackages: Working on machine '$m'"
        foreach ($p in $SoftwarePackage)
        {
            Write-PSFMessage -Message "Install-LabSoftwarePackages: Building installation package for '$p'"

            $param = @{ }
            $param.Add('Path', $p.Path)
            if ($p.CommandLine)
            {
                $param.Add('CommandLine', $p.CommandLine)
            }
            $param.Add('Timeout', $p.Timeout)
            $param.Add('ComputerName', $m.Name)
            $param.Add('PassThru', $true)

            Write-PSFMessage -Message "Install-LabSoftwarePackages: Calling installation package '$p'"

            $jobs += Install-LabSoftwarePackage @param

            Write-PSFMessage -Message "Install-LabSoftwarePackages: Installation for package '$p' finished"
        }
    }

    Write-PSFMessage 'Waiting for installation jobs to finish'

    if ($WaitForInstallation)
    {
        Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -NoDisplay
    }

    $end = Get-Date

    Write-PSFMessage "Installation of all software packages took '$($end - $start)'"

    if ($PassThru)
    {
        $jobs
    }

    Write-LogFunctionExit
}


function Install-LabWindowsFeature
{
    [cmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$FeatureName,

        [switch]$IncludeAllSubFeature,

        [switch]$IncludeManagementTools,

        [switch]$UseLocalCredential,

        [int]$ProgressIndicator = 5,

        [switch]$NoDisplay,

        [switch]$PassThru,

        [switch]$AsJob
    )

    Write-LogFunctionEntry

    $results = @()

    $machines = Get-LabVM -ComputerName $ComputerName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machines $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }

    Write-ScreenInfo -Message "Installing Windows Feature(s) '$($FeatureName -join ', ')' on computer(s) '$($ComputerName -join ', ')'" -TaskStart

    if ($AsJob)
    {
        Write-ScreenInfo -Message 'Windows Feature(s) is being installed in the background' -TaskEnd
    }

    $stoppedMachines = (Get-LabVMStatus -ComputerName $ComputerName -AsHashTable).GetEnumerator() | Where-Object Value -eq Stopped
    if ($stoppedMachines)
    {
        Start-LabVM -ComputerName $stoppedMachines.Name -Wait
    }

    $hyperVMachines = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'HyperV'}
    $azureMachines  = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'Azure'}

    if ($hyperVMachines)
    {
        foreach ($machine in $hyperVMachines)
        {
            $isoImagePath = $machine.OperatingSystem.IsoPath
            Mount-LabIsoImage -ComputerName $machine -IsoPath $isoImagePath -SupressOutput
        }
        $jobs = Install-LWHypervWindowsFeature -Machine $hyperVMachines -FeatureName $FeatureName -UseLocalCredential:$UseLocalCredential -IncludeAllSubFeature:$IncludeAllSubFeature -IncludeManagementTools:$IncludeManagementTools -AsJob:$AsJob -PassThru:$PassThru
    }
    elseif ($azureMachines)
    {
        $jobs = Install-LWAzureWindowsFeature -Machine $azureMachines -FeatureName $FeatureName -UseLocalCredential:$UseLocalCredential -IncludeAllSubFeature:$IncludeAllSubFeature -IncludeManagementTools:$IncludeManagementTools -AsJob:$AsJob -PassThru:$PassThru
    }

    if (-not $AsJob)
    {
        if ($hyperVMachines)
        {
            Dismount-LabIsoImage -ComputerName $hyperVMachines -SupressOutput
        }
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if ($PassThru)
    {
        $jobs
    }

    Write-LogFunctionExit
}


function Invoke-Ternary ([scriptblock]$decider, [scriptblock]$ifTrue, [scriptblock]$ifFalse)
{
    if (&$decider)
    {
        &$ifTrue
    }
    else
    {
        &$ifFalse
    }
}

function New-LabSourcesFolder
{
    [CmdletBinding(
            SupportsShouldProcess = $true,
    ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(Mandatory = $false)]
        [System.String]
        $DriveLetter,

        [switch]
        $Force,

        [switch]
        $FolderStructureOnly,

        [ValidateSet('master','develop')]
        [string]
        $Branch = 'master'
    )

    $path = Get-LabSourcesLocation -Local
    if (-not $path -and (Get-LabConfigurationItem -Name LabSourcesLocation))
    {
        $path = Get-LabConfigurationItem -Name LabSourcesLocation
    }
    elseif (-not $path)
    {
        $path = (Join-Path -Path (Get-LabConfigurationItem -Name OsRoot) -ChildPath LabSources)
    }

    if ($DriveLetter)
    {
        try
        {
            $drive = [System.IO.DriveInfo]$DriveLetter
        }
        catch
        {
            throw "$DriveLetter is not a valid drive letter. Exception was ($_.Exception.Message)"
        }

        if (-not $drive.IsReady)
        {
            throw "LabSource cannot be placed on $DriveLetter. The drive is not ready."
        }

        $Path = Join-Path -Path $drive.RootDirectory -ChildPath LabSources
    }

    if ((Test-Path -Path $Path) -and -not $Force)
    {
        return $Path
    }

    if (-not $Force.IsPresent)
    {
        Write-ScreenInfo -Message 'Downloading LabSources from GitHub. This only happens once if no LabSources folder can be found.' -Type Warning
    }

    if ($PSCmdlet.ShouldProcess('Downloading module and creating new LabSources', $Path))
    {
        if ($FolderStructureOnly.IsPresent)
        {
            $null = New-Item -Path (Join-Path -Path $Path -ChildPath ISOs\readme.md) -Force
            $null = New-Item -Path (Join-Path -Path $Path -ChildPath SoftwarePackages\readme.md) -Force
            $null = New-Item -Path (Join-Path -Path $Path -ChildPath PostInstallationActivities\readme.md) -Force
            $null = New-Item -Path (Join-Path -Path $Path -ChildPath Tools\readme.md) -Force
            $null = New-Item -Path (Join-Path -Path $Path -ChildPath CustomRoles\readme.md) -Force
            'ISO files go here' | Set-Content -Force -Path (Join-Path -Path $Path -ChildPath ISOs\readme.md)
            'Software packages (for example installers) go here. To prepare offline setups, visit https://automatedlab.org/en/latest/Wiki/Basic/fullyoffline' | Set-Content -Force -Path (Join-Path -Path $Path -ChildPath SoftwarePackages\readme.md)
            'Pre- and Post-Installation activities go here. For more information, visit https://automatedlab.org/en/latest/AutomatedLabDefinition/en-us/Get-LabInstallationActivity' | Set-Content -Force -Path (Join-Path -Path $Path -ChildPath PostInstallationActivities\readme.md)
            'Tools to copy to all lab VMs (if parameter ToolsPath is used) go here' | Set-Content -Force -Path (Join-Path -Path $Path -ChildPath Tools\readme.md)
            'Custom roles go here. For more information, visit https://automatedlab.org/en/latest/Wiki/Advanced/customroles' | Set-Content -Force -Path (Join-Path -Path $Path -ChildPath CustomRoles\readme.md)
            return $Path
        }

        $temporaryPath = [System.IO.Path]::GetTempFileName().Replace('.tmp', '')
        [void] (New-Item -ItemType Directory -Path $temporaryPath -Force)
        $archivePath = (Join-Path -Path $temporaryPath -ChildPath "$Branch.zip")

        try
        {
            Get-LabInternetFile -Uri ('https://github.com/AutomatedLab/AutomatedLab/archive/{0}.zip' -f $Branch) -Path $archivePath -ErrorAction Stop
        }
        catch
        {
            Write-Error "Could not download the LabSources folder due to connection issues. Please try again." -ErrorAction Stop
        }

        Microsoft.PowerShell.Archive\Expand-Archive -Path $archivePath -DestinationPath $temporaryPath

        if (-not (Test-Path -Path $Path))
        {
            $Path = (New-Item -ItemType Directory -Path $Path).FullName
        }

        Copy-Item -Path (Join-Path -Path $temporaryPath -ChildPath AutomatedLab-*/LabSources/*) -Destination $Path -Recurse -Force:$Force

        Remove-Item -Path $temporaryPath -Recurse -Force -ErrorAction SilentlyContinue

        $Path
    }
}


function Remove-Lab
{
    [CmdletBinding(DefaultParameterSetName = 'ByName', ConfirmImpact = 'High', SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByPath', ValueFromPipeline)]
        [string]$Path,

        [Parameter(ParameterSetName = 'ByName', ValueFromPipelineByPropertyName)]
        [string]$Name,
        
        [switch]$RemoveExternalSwitches
    )

    begin
    {
        Write-LogFunctionEntry
        $global:PSLog_Indent = 0
    }

    process
    {
        if ($Name)
        {
            Import-Lab -Name $Name -NoValidation -NoDisplay
            $labName = $Name
        }
        elseif ($Path)
        {
            Import-Lab -Path $Path -NoValidation -NoDisplay
            
        }

        if (-not $Script:data)
        {
            Write-Error 'No definitions imported, so there is nothing to remove. Please use Import-Lab against the xml file'
            return
        }

        $labName = (Get-Lab).Name

        if($pscmdlet.ShouldProcess($labName, 'Remove the lab completely'))
        {
            Write-ScreenInfo -Message "Removing lab '$labName'" -Type Warning -TaskStart
            if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and -not (Get-AzContext))
            {
                Write-ScreenInfo -Type Info -Message "Your Azure session is expired. Please log in to remove your resource group"
                $param = @{
                    UseDeviceAuthentication = $true
                    ErrorAction             = 'SilentlyContinue' 
                    WarningAction           = 'Continue'
                    Environment             = $(Get-Lab).AzureSettings.Environment
                }

                $null = Connect-AzAccount @param
            }

            try
            {
                [AutomatedLab.LabTelemetry]::Instance.LabRemoved((Get-Lab).Export())
            }
            catch
            {
                Write-PSFMessage -Message ('Error sending telemetry: {0}' -f $_.Exception)
            }

            Write-ScreenInfo -Message 'Removing lab sessions'
            Remove-LabPSSession -All
            Write-PSFMessage '...done'

            Write-ScreenInfo -Message 'Removing imported RDS certificates'
            Uninstall-LabRdsCertificate
            Write-PsfMessage '...done'

            Write-ScreenInfo -Message 'Removing lab background jobs'
            $jobs = Get-Job
            Write-PSFMessage "Removing remaining $($jobs.Count) jobs..."
            $jobs | Remove-Job -Force -ErrorAction SilentlyContinue
            Write-PSFMessage '...done'

            if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
            {
                Write-ScreenInfo -Message "Removing Resource Group '$labName' and all resources in this group"
                #without cloning the collection, a Runtime Exceptionis thrown: An error occurred while enumerating through a collection: Collection was modified; enumeration operation may not execute
                # If RG contains Recovery Vault, remove vault properly
                Remove-LWAzureRecoveryServicesVault
                @(Get-LabAzureResourceGroup -CurrentLab).Clone() | Remove-LabAzureResourceGroup -Force
            }

            $labMachines = Get-LabVM -IncludeLinux | Where-Object HostType -eq 'HyperV' | Where-Object { -not $_.SkipDeployment }
            if ($labMachines)
            {
                $labName = (Get-Lab).Name

                $removeMachines = foreach ($machine in $labMachines)
                {
                    $machineMetadata = Get-LWHypervVMDescription -ComputerName $machine.ResourceName -ErrorAction SilentlyContinue
                    $vm = Get-LWHypervVM -Name $machine.ResourceName -ErrorAction SilentlyContinue
                    if (-not $machineMetadata)
                    {
                        Write-Error -Message "Cannot remove machine '$machine' because lab meta data could not be retrieved"
                    }
                    elseif ($machineMetadata.LabName -ne $labName -and $vm)
                    {
                        Write-Error -Message "Cannot remove machine '$machine' because it does not belong to this lab"
                    }
                    else
                    {
                        $machine
                    }
                }

                if ($removeMachines)
                {
                    Remove-LabVM -Name $removeMachines

                    $disks = Get-LabVHDX -All
                    Write-PSFMessage "Lab knows about $($disks.Count) disks"

                    if ($disks)
                    {
                        Write-ScreenInfo -Message 'Removing additionally defined disks'

                        Write-PSFMessage 'Removing disks...'
                        foreach ($disk in $disks)
                        {
                            Write-PSFMessage "Removing disk '$($disk.Name)'"

                            if (Test-Path -Path $disk.Path)
                            {
                                Remove-Item -Path $disk.Path
                            }
                            else
                            {
                                Write-ScreenInfo "Disk '$($disk.Path)' does not exist" -Type Verbose
                            }
                        }
                    }

                    if ($Script:data.Target.Path)
                    {
                        $diskPath = (Join-Path -Path $Script:data.Target.Path -ChildPath Disks)
                        #Only remove disks folder if empty
                        if ((Test-Path -Path $diskPath) -and (-not (Get-ChildItem -Path $diskPath)) )
                        {
                            Remove-Item -Path $diskPath
                        }
                    }
                }

                #Only remove folder for VMs if folder is empty
                if ($Script:data.Target.Path -and (-not (Get-ChildItem -Path $Script:data.Target.Path)))
                {
                    Remove-Item -Path $Script:data.Target.Path -Recurse -Force -Confirm:$false
                }

                Write-ScreenInfo -Message 'Removing entries in the hosts file'
                Clear-HostFile -Section $Script:data.Name -ErrorAction SilentlyContinue

                if ($labMachines.SshPublicKey)
                {
                    Write-ScreenInfo -Message 'Removing SSH known hosts'
                    UnInstall-LabSshKnownHost
                }
            }

            Write-ScreenInfo -Message 'Removing virtual networks'
            Remove-LabNetworkSwitches -RemoveExternalSwitches:$RemoveExternalSwitches

            if ($Script:data.LabPath)
            {
                Write-ScreenInfo -Message 'Removing Lab XML files'
                if (Test-Path "$($Script:data.LabPath)/$(Get-LabConfigurationItem -Name LabFileName)") { Remove-Item -Path "$($Script:data.LabPath)/Lab.xml" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/$(Get-LabConfigurationItem -Name DiskFileName)") { Remove-Item -Path "$($Script:data.LabPath)/Disks.xml" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/$(Get-LabConfigurationItem -Name MachineFileName)") { Remove-Item -Path "$($Script:data.LabPath)/Machines.xml" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/Unattended*.xml") { Remove-Item -Path "$($Script:data.LabPath)/Unattended*.xml" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/armtemplate.json") { Remove-Item -Path "$($Script:data.LabPath)/armtemplate.json" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/ks*.cfg") { Remove-Item -Path "$($Script:data.LabPath)/ks*.cfg" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/*.bash") { Remove-Item -Path "$($Script:data.LabPath)/*.bash" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/autoinst*.xml") { Remove-Item -Path "$($Script:data.LabPath)/autoinst*.xml" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/cloudinit*") { Remove-Item -Path "$($Script:data.LabPath)/cloudinit*" -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/AzureNetworkConfig.Xml") { Remove-Item -Path "$($Script:data.LabPath)/AzureNetworkConfig.Xml" -Recurse -Force -Confirm:$false }
                if (Test-Path "$($Script:data.LabPath)/Certificates") { Remove-Item -Path "$($Script:data.LabPath)/Certificates" -Recurse -Force -Confirm:$false }

                #Only remove lab path folder if empty
                if ((Test-Path "$($Script:data.LabPath)") -and (-not (Get-ChildItem -Path $Script:data.LabPath)))
                {
                    Remove-Item -Path $Script:data.LabPath
                }
            }

            $Script:data = $null

            Write-ScreenInfo -Message "Done removing lab '$labName'" -TaskEnd
        }
    }

    end
    {
        Write-LogFunctionExit
    }
}


function Remove-LabVariable
{
    $pattern = 'AL_([a-zA-Z0-9]{8})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{4})+[-.]+([a-zA-Z0-9]{12})'
    Get-LabVariable | Remove-Variable -Scope Global
}


function Set-LabDefaultOperatingSystem
{
    [Cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [Alias('Name')]
        [string]
        $OperatingSystem,

        [string]
        $Version
    )

    $labDefinition = Get-LabDefinition -ErrorAction SilentlyContinue

    if (-not $labDefinition) { throw 'No lab defined. Please call New-LabDefinition first before calling Set-LabDefaultOperatingSystem.' }

    if ($labDefinition.DefaultVirtualizationEngine -eq 'Azure' -and -not $labDefinition.AzureSettings)
    {
        try
        {
            Add-LabAzureSubscription -ErrorAction Stop
        }
        catch
        {
            throw "No Azure subscription added yet. Please run 'Add-LabAzureSubscription' first."
        }
        $labDefinition = Get-LabDefinition -ErrorAction Stop
    }

    $additionalParameter = @{}
    if ($labDefinition.DefaultVirtualizationEngine -eq 'Azure')
    {
        $additionalParameter['Location'] = $labDefinition.AzureSettings.DefaultLocation.DisplayName
        $additionalParameter['Azure'] = $true
    }
   
    if ($Version)
    {
        $os = Get-LabAvailableOperatingSystem @additionalParameter | Where-Object { $_.OperatingSystemName -eq $OperatingSystem -and $_.Version -eq $OperatingSystemVersion }
    }
    else
    {
        $os = Get-LabAvailableOperatingSystem @additionalParameter | Where-Object { $_.OperatingSystemName -eq $OperatingSystem }
        if ($os.Count -gt 1)
        {
            $os = $os | Sort-Object Version -Descending | Select-Object -First 1
            Write-ScreenInfo "The operating system '$OperatingSystem' is available multiple times. Choosing the one with the highest version ($($os.Version)) as default operating system" -Type Warning
        }
    }

    if (-not $os)
    {
        throw "The operating system '$OperatingSystem' could not be found in the available operating systems. Call 'Get-LabAvailableOperatingSystem' to get a list of operating systems available to the lab."
    }
    $labDefinition.DefaultOperatingSystem = $os
}


function Set-LabDefaultVirtualizationEngine
{
    [Cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [ValidateSet('Azure', 'HyperV', 'VMware')]
        [string]$VirtualizationEngine
    )

    if (Get-LabDefinition)
    {
        (Get-LabDefinition).DefaultVirtualizationEngine = $VirtualizationEngine
    }
    else
    {
        throw 'No lab defined. Please call New-LabDefinition first before calling Set-LabDefaultOperatingSystem.'
    }
}


function Set-LabGlobalNamePrefix
{
    [Cmdletbinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [ValidatePattern("^([\'\""a-zA-Z0-9]){1,4}$|()")]
        [string]$Name
    )

    $Global:labNamePrefix = $Name
}


function Set-LabInstallationCredential
{
    [OutputType([System.Int32])]
    [CmdletBinding(DefaultParameterSetName = 'All')]
    Param (
        [Parameter(Mandatory, ParameterSetName = 'All')]
        [Parameter(Mandatory=$false, ParameterSetName = 'Prompt')]
        [ValidatePattern('^([\w\.-]){2,15}$')]
        [string]$Username,

        [Parameter(Mandatory, ParameterSetName = 'All')]
        [Parameter(Mandatory=$false, ParameterSetName = 'Prompt')]
        [string]$Password,

        [Parameter(Mandatory, ParameterSetName = 'Prompt')]
        [switch]$Prompt
    )

    # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/faq#what-are-the-password-requirements-when-creating-a-vm
    $azurePasswordBlacklist = @(
        'abc@123'
        'iloveyou!'
        'P@$$w0rd'
        'P@ssw0rd'
        'P@ssword123'
        'Pa$$word'
        'pass@word1'
        'Password!'
        'Password1'
        'Password22'
    )

    if (-not (Get-LabDefinition))
    {
        throw 'No lab defined. Please call New-LabDefinition first before calling Set-LabInstallationCredential.'
    }

    if ((Get-LabDefinition).DefaultVirtualizationEngine -eq 'Azure')
    {
        if ($Password -and $azurePasswordBlacklist -contains $Password)
        {
            throw "Password '$Password' is in the list of forbidden passwords for Azure VMs: $($azurePasswordBlacklist -join ', ')"
        }

        if ($Username -eq 'Administrator')
        {
            throw 'Username may not be Administrator for Azure VMs.'
        }

        $checks = @(
            $Password -match '[A-Z]'
            $Password -match '[a-z]'
            $Password -match '\d'
            $Password.Length -ge 8
        )

        if ($Password -and $checks -contains $false)
        {
            throw "Passwords for Azure VM administrator have to:
                Be at least 8 characters long
                Have lower characters
                Have upper characters
                Have a digit
            "

        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'All')
    {
        $user = New-Object AutomatedLab.User($Username, $Password)
        (Get-LabDefinition).DefaultInstallationCredential = $user
    }
    else
    {
        $promptUser = Read-Host "Type desired username for admin user (or leave blank for 'Install'. Username cannot be 'Administrator' if deploying in Azure)"

        if (-not $promptUser)
        {
            $promptUser = 'Install'
        }
        do
        {
            $promptPassword = Read-Host "Type password for admin user (leave blank for 'Somepass1' or type 'x' to cancel )"

            if (-not $promptPassword)
            {
                $promptPassword = 'Somepass1'
                $checks = 5
                break
            }

            [int]$minLength  = 8
            [int]$numUpper   = 1
            [int]$numLower   = 1
            [int]$numNumbers = 1
            [int]$numSpecial = 1

            $upper   = [regex]'[A-Z]'
            $lower   = [regex]'[a-z]'
            $number  = [regex]'[0-9]'
            $special = [regex]'[^a-zA-Z0-9]'

            $checks = 0

            if ($promptPassword.length -ge 8)                            { $checks++ }
            if ($upper.Matches($promptPassword).Count -ge $numUpper )    { $checks++ }
            if ($lower.Matches($promptPassword).Count -ge $numLower )    { $checks++ }
            if ($number.Matches($promptPassword).Count -ge $numNumbers ) { $checks++ }

            if ($checks -lt 4)
            {
                if ($special.Matches($promptPassword).Count -ge $numSpecial )  { $checks }
            }

            if ($checks -lt 4)
            {
                Write-PSFMessage -Level Host 'Password must be have minimum length of 8'
                Write-PSFMessage -Level Host 'Password must contain minimum one upper case character'
                Write-PSFMessage -Level Host 'Password must contain minimum one lower case character'
                Write-PSFMessage -Level Host 'Password must contain minimum one special character'
            }
        }
        until ($checks -ge 4 -or (-not $promptUser) -or (-not $promptPassword) -or $promptPassword -eq 'x')

        if ($checks -ge 4 -and $promptPassword -ne 'x')
        {
            $user = New-Object AutomatedLab.User($promptUser, $promptPassword)
        }
    }
}


function Show-LabDeploymentSummary
{
    [OutputType([System.TimeSpan])]
    [Cmdletbinding()]
    param (
        [switch]$Detailed
    )

    if (-not (Get-Lab -ErrorAction SilentlyContinue))
    {
        Write-ScreenInfo "There is no lab information available in the current PowerShell session. Deploy a lab with AutomatedLab or import an already deployed lab with the 'Import-Lab' cmdlet."
        return
    }

    $lab = Get-Lab
    $ts = New-TimeSpan -Start $Global:AL_DeploymentStart -End (Get-Date)
    $hoursPlural = ''
    $minutesPlural = ''
    $secondsPlural = ''

    if ($ts.Hours   -gt 1) { $hoursPlural   = 's' }
    if ($ts.minutes -gt 1) { $minutesPlural = 's' }
    if ($ts.Seconds -gt 1) { $secondsPlural = 's' }

    $machines = Get-LabVM -IncludeLinux

    Write-ScreenInfo -Message '---------------------------------------------------------------------------'
    Write-ScreenInfo -Message ("Setting up the lab took {0} hour$hoursPlural, {1} minute$minutesPlural and {2} second$secondsPlural" -f $ts.hours, $ts.minutes, $ts.seconds)
    Write-ScreenInfo -Message "Lab name is '$($lab.Name)' and is hosted on '$($lab.DefaultVirtualizationEngine)'. There are $($machines.Count) machine(s) and $($lab.VirtualNetworks.Count) network(s) defined."

    if (-not $Detailed)
    {
        Write-ScreenInfo -Message '---------------------------------------------------------------------------'
    }
    else
    {
        Write-ScreenInfo -Message '----------------------------- Network Summary -----------------------------'
        $networkInfo = $lab.VirtualNetworks | Format-Table -Property Name, AddressSpace, SwitchType, AdapterName, @{ Name = 'IssuedIpAddresses'; Expression = { $_.IssuedIpAddresses.Count } } | Out-String
        $networkInfo -split "`n" | ForEach-Object {
            if ($_) { Write-ScreenInfo -Message $_ }
        }

        Write-ScreenInfo -Message '----------------------------- Domain Summary ------------------------------'
        $domainInfo = $lab.Domains | Format-Table -Property Name,
        @{ Name = 'Administrator'; Expression = { $_.Administrator.UserName } },
        @{ Name = 'Password'; Expression = { $_.Administrator.Password } },
        @{ Name = 'RootDomain'; Expression = { if ($lab.GetParentDomain($_.Name).Name -ne $_.Name) { $lab.GetParentDomain($_.Name) } } } |
        Out-String

        $domainInfo -split "`n" | ForEach-Object {
            if ($_) { Write-ScreenInfo -Message $_ }
        }

        Write-ScreenInfo -Message '------------------------- Virtual Machine Summary -------------------------'
        $vmInfo = Get-LabVM -IncludeLinux | Format-Table -Property Name, DomainName, IpV4Address, Roles, OperatingSystem,
        @{ Name = 'Local Admin'; Expression = { $_.InstallationUser.UserName } },
        @{ Name = 'Password'; Expression = { $_.InstallationUser.Password } } -AutoSize |
        Out-String

        $vmInfo -split "`n" | ForEach-Object {
            if ($_) { Write-ScreenInfo -Message $_ }
        }

        Write-ScreenInfo -Message '---------------------------------------------------------------------------'
        Write-ScreenInfo -Message 'Please use the following cmdlets to interact with the machines:'
        Write-ScreenInfo -Message '- Get-LabVMStatus, Get, Start, Restart, Stop, Wait, Connect, Save-LabVM and Wait-LabVMRestart (some of them provide a Wait switch)'
        Write-ScreenInfo -Message '- Invoke-LabCommand, Enter-LabPSSession, Install-LabSoftwarePackage and Install-LabWindowsFeature (do not require credentials and'
        Write-ScreenInfo -Message ' work the same way with Hyper-V and Azure)'
        Write-ScreenInfo -Message '- Checkpoint-LabVM, Restore-LabVMSnapshot and Get-LabVMSnapshot (only for Hyper-V)'
        Write-ScreenInfo -Message '- Get-LabInternetFile downloads files from the internet and places them on LabSources (locally or on Azure)'
        Write-ScreenInfo -Message '---------------------------------------------------------------------------'
    }
}


function Test-LabHostConnected
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingComputerNameHardcoded", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("ALSimpleNullComparison", "", Justification="We want a boolean")]
    [CmdletBinding()]
    param
    (
        [switch]
        $Throw,

        [switch]
        $Quiet
    )

    if (Get-LabConfigurationItem -Name DisableConnectivityCheck)
    {
        $script:connected = $true
    }

    if (-not $script:connected)
    {
        $script:connected = if (Get-Command Get-NetConnectionProfile -ErrorAction SilentlyContinue)
        {
            $null -ne (Get-NetConnectionProfile | Where-Object {$_.IPv4Connectivity -eq 'Internet' -or $_.IPv6Connectivity -eq 'Internet'})
        }
        elseif ((Get-ChildItem -Path env:\ACC_OID,env:\ACC_VERSION,env:\ACC_TID -ErrorAction SilentlyContinue).Count -eq 3)
        {
            # Assuming that we are in Azure Cloud Console aka Cloud Shell which is connected but cannot send ICMP packages
            $true
        }
        elseif ($IsLinux)
        {
            # Due to an unadressed issue with Test-Connection on Linux
            $portOpen = Test-Port -ComputerName automatedlab.org -Port 443
            if (-not $portOpen.Open)
            {
                [System.Net.NetworkInformation.Ping]::new().Send('automatedlab.org').Status -eq 'Success'
            }
            else
            {
                $portOpen.Open
            }
        }
        else
        {
            Test-Connection -ComputerName automatedlab.org -Count 4 -Quiet -ErrorAction SilentlyContinue -InformationAction Ignore
        }
    }

    if ($Throw.IsPresent -and -not $script:connected)
    {
        throw "$env:COMPUTERNAME does not seem to be connected to the internet. All internet-related tasks will fail."
    }

    if ($Quiet.IsPresent)
    {
        return
    }

    $script:connected
}


function Test-LabHostRemoting
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param()

    if ($IsLinux) { return }
    Write-LogFunctionEntry

    $configOk = $true

    if ($IsLinux -or $IsMacOs)
    {
        return $configOk
    }

    if ((Get-Service -Name WinRM).Status -ne 'Running')
    {
        Write-ScreenInfo 'Starting the WinRM service. This is required in order to read the WinRM configuration...' -NoNewLine
        Start-Service -Name WinRM
        Start-Sleep -Seconds 5
        Write-ScreenInfo done
    }

    # force English language output for Get-WSManCredSSP call
    [Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'; $WSManCredSSP = Get-WSManCredSSP
    if ((-not $WSManCredSSP[0].Contains('The machine is configured to') -and -not $WSManCredSSP[0].Contains('WSMAN/*')) -or (Get-Item -Path WSMan:\localhost\Client\Auth\CredSSP).Value -eq $false)
    {
        Write-ScreenInfo "'Get-WSManCredSSP' returned that CredSSP is not enabled on the host machine for role 'Client' and being able to delegate to '*'..." -Type Verbose
        $configOk = $false
    }

    $trustedHostsList = @((Get-Item -Path Microsoft.WSMan.Management\WSMan::localhost\Client\TrustedHosts).Value -split ',' |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ }
    )

    if (-not ($trustedHostsList -contains '*'))
    {
        Write-ScreenInfo -Message "TrustedHosts does not include '*'." -Type Verbose
        $configOk = $false
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1')
    if ($value -ne '*' -and $value -ne 'WSMAN/*')
    {
        Write-ScreenInfo "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials' is not configured as required" -Type Verbose
        $configOk = $false
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly', '1')
    if ($value -ne '*' -and $value -ne 'WSMAN/*')
    {
        Write-ScreenInfo "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication' is not configured as required" -Type Verbose
        $configOk = $false
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentials', '1')
    if ($value -ne '*' -and $value -ne 'TERMSRV/*')
    {
        Write-ScreenInfo "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials' is not configured as required" -Type Verbose
        $configOk = $false
    }

    $value = [GPO.Helper]::GetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentialsWhenNTLMOnly', '1')
    if ($value -ne '*' -and $value -ne 'TERMSRV/*')
    {
        Write-ScreenInfo "Local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication' is not configured as required" -Type Verbose
        $configOk = $false
    }

    $allowEncryptionOracle = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -ErrorAction SilentlyContinue).AllowEncryptionOracle
    if ($allowEncryptionOracle -ne 2)
    {
        Write-ScreenInfo "AllowEncryptionOracle is set to '$allowEncryptionOracle'. The value should be '2'" -Type Verbose
        $configOk = $false
    }

    $configOk

    Write-LogFunctionExit
}


function Undo-LabHostRemoting
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    param(
        [switch]$Force,

        [switch]$NoDisplay
    )

    if ($IsLinux) { return }
    Write-LogFunctionEntry

    if (-not (Test-IsAdministrator))
    {
        throw 'This function needs to be called in an elevated PowerShell session.'
    }
    $message = "All settings altered by 'Enable-LabHostRemoting' will be set back to Windows defaults. Are you OK to proceed?"
    if (-not $Force)
    {
        $choice = Read-Choice -ChoiceList '&No','&Yes' -Caption 'Enabling WinRM and CredSsp' -Message $message -Default 1
        if ($choice -eq 0)
        {
            throw "'Undo-LabHostRemoting' cancelled. You can make the changes later by calling 'Undo-LabHostRemoting'"
        }
    }

    if ((Get-Service -Name WinRM).Status -ne 'Running')
    {
        Write-ScreenInfo 'Starting the WinRM service. This is required in order to read the WinRM configuration...' -NoNewLine
        Start-Service -Name WinRM
        Start-Sleep -Seconds 5
        Write-ScreenInfo done
    }

    Write-ScreenInfo "Calling 'Disable-WSManCredSSP -Role Client'..." -NoNewline
    Disable-WSManCredSSP -Role Client
    Write-ScreenInfo done

    Write-ScreenInfo -Message "Setting 'TrustedHosts' to an empty string"
    Set-Item -Path Microsoft.WSMan.Management\WSMan::localhost\Client\TrustedHosts -Value '' -Force

    Write-ScreenInfo "Resetting local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials'"
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowFreshCredentials', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowFresh', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials', '1', $null) | Out-Null

    Write-ScreenInfo "Resetting local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication'"
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowFreshCredentialsWhenNTLMOnly', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowFreshNTLMOnly', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly', '1', $null) | Out-Null

    Write-ScreenInfo "Resetting local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials'"
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowSavedCredentials', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowSaved', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentials', '1', $null) | Out-Null

    Write-ScreenInfo "Resetting local policy 'Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> Allow Delegating Fresh Credentials with NTLM-only server authentication'"
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'AllowSavedCredentialsWhenNTLMOnly', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation', 'ConcatenateDefaults_AllowSavedNTLMOnly', $null) | Out-Null
    [GPO.Helper]::SetGroupPolicy($true, 'SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowSavedCredentialsWhenNTLMOnly', '1', $null) | Out-Null

    Write-ScreenInfo "removing 'AllowEncryptionOracle' registry setting"
    if (Test-Path -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP)
    {
        Remove-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP -Recurse -Force
    }

    Write-ScreenInfo "All settings changed by the cmdlet Enable-LabHostRemoting of AutomatedLab are back to Windows defaults."

    Write-LogFunctionExit
}


function Uninstall-LabWindowsFeature
{
    [cmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$FeatureName,

        [switch]$IncludeManagementTools,

        [switch]$UseLocalCredential,

        [int]$ProgressIndicator = 5,

        [switch]$NoDisplay,

        [switch]$PassThru,

        [switch]$AsJob
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machines $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }

    Write-ScreenInfo -Message "Uninstalling Windows Feature(s) '$($FeatureName -join ', ')' on computer(s) '$($ComputerName -join ', ')'" -TaskStart

    if ($AsJob)
    {
        Write-ScreenInfo -Message 'Windows Feature(s) is being uninstalled in the background' -TaskEnd
    }

    $stoppedMachines = (Get-LabVMStatus -ComputerName $ComputerName -AsHashTable).GetEnumerator() | Where-Object Value -eq Stopped
    if ($stoppedMachines)
    {
        Start-LabVM -ComputerName $stoppedMachines.Name -Wait
    }

    $hyperVMachines = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'HyperV'}
    $azureMachines = Get-LabVM -ComputerName $ComputerName | Where-Object {$_.HostType -eq 'Azure'}

    if ($hyperVMachines)
    {
        $jobs = Uninstall-LWHypervWindowsFeature -Machine $hyperVMachines -FeatureName $FeatureName -UseLocalCredential:$UseLocalCredential -IncludeManagementTools:$IncludeManagementTools -AsJob:$AsJob -PassThru:$PassThru
    }
    elseif ($azureMachines)
    {
        $jobs = Uninstall-LWAzureWindowsFeature -Machine $azureMachines -FeatureName $FeatureName -UseLocalCredential:$UseLocalCredential -IncludeManagementTools:$IncludeManagementTools -AsJob:$AsJob -PassThru:$PassThru
    }

    if (-not $AsJob)
    {
        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    if ($PassThru)
    {
        $jobs
    }

    Write-LogFunctionExit
}


function Get-LabVHDX
{
    [OutputType([AutomatedLab.Disk])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'ByName')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    $lab = Get-Lab

    if ($lab.DefaultVirtualizationEngine -ne 'HyperV') # We should not even be here!
    {
        return
    }

    if (-not $lab)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        $disks = $lab.Disks | Where-Object Name -In $Name
    }

    if ($PSCmdlet.ParameterSetName -eq 'All')
    {
        $disks = $lab.Disks
    }

    if (-not (Get-LabMachineDefinition -ErrorAction SilentlyContinue))
    {
        Import-LabDefinition -Name $lab.Name
        Import-Lab -Name $lab.Name -NoDisplay -NoValidation -DoNotRemoveExistingLabPSSessions
    }

    if ($disks)
    {
        foreach ($disk in $disks)
        {
            if ($vm = Get-LabMachineDefinition | Where-Object { $_.Disks.Name -contains $disk.Name })
            {
                $disk.Path = Join-Path -Path $lab.Target.Path -ChildPath $vm.ResourceName
            }
            else
            {
                $disk.Path = Join-Path -Path $lab.Target.Path -ChildPath Disks
            }
            $disk.Path = Join-Path -Path $disk.Path -ChildPath ($disk.Name + '.vhdx')
        }

        Write-LogFunctionExit -ReturnValue $disks.ToString()

        return $disks
    }
    else
    {
        return
    }
}


function New-LabBaseImages
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    $lab = Get-Lab
    if (-not $lab)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $oses = (Get-LabVm -All | Where-Object {[string]::IsNullOrWhiteSpace($_.ReferenceDiskPath)}).OperatingSystem

    if (-not $lab.Sources.AvailableOperatingSystems)
    {
        throw "There isn't a single operating system ISO available in the lab. Please call 'Get-LabAvailableOperatingSystem' to see what AutomatedLab has found and check the LabSources folder location by calling 'Get-LabSourcesLocation'."
    }

    $osesProcessed = @()
    $BaseImagesCreated = 0

    foreach ($os in $oses)
    {
        if (-not $os.ProductKey)
        {
            $message = "The product key is unknown for the OS '$($os.OperatingSystemName)' in ISO image '$($os.OSName)'. Cannot install lab until this problem is solved."
            Write-LogFunctionExitWithError -Message $message
            throw $message
        }

        $archString = if ($os.Architecture -eq 'x86') { "_$($os.Architecture)"} else { '' }
        $legacyDiskPath = Join-Path -Path $lab.Target.Path -ChildPath "BASE_$($os.OperatingSystemName.Replace(' ', ''))$($archString)_$($os.Version).vhdx"
        if (Test-Path $legacyDiskPath)
        {
            [int]$legacySize = (Get-Vhd -Path $legacyDiskPath).Size / 1GB
            $newName = Join-Path -Path $lab.Target.Path -ChildPath "BASE_$($os.OperatingSystemName.Replace(' ', ''))$($archString)_$($os.Version)_$($legacySize).vhdx"
            $affectedDisks = @()
            $affectedDisks += Get-LWHypervVM | Get-VMHardDiskDrive | Get-VHD | Where-Object ParentPath -eq $legacyDiskPath
            $affectedDisks += Get-LWHypervVM | Get-VMSnapshot | Get-VMHardDiskDrive | Get-VHD | Where-Object ParentPath -eq $legacyDiskPath
            
            if ($affectedDisks)
            {
                $affectedVms = Get-LWHypervVM | Where-Object {
                    ($_ | Get-VMHardDiskDrive | Get-VHD | Where-Object { $_.ParentPath -eq $legacyDiskPath -and $_.Attached }) -or
                    ($_ | Get-VMSnapshot | Get-VMHardDiskDrive | Get-VHD | Where-Object { $_.ParentPath -eq $legacyDiskPath -and $_.Attached })                
                }
            }

            if ($affectedVms)
            {
                Write-ScreenInfo -Type Warning -Message "Unable to rename $(Split-Path -Leaf -Path $legacyDiskPath) to $(Split-Path -Leaf -Path $newName), disk is currently in use by VMs: $($affectedVms.Name -join ',').
                You will need to clean up the disk manually, while a new reference disk is being created. To cancel, press CTRL-C and shut down the affected VMs manually."

                $count = 5
                do
                {
                    Write-ScreenInfo -Type Warning -NoNewLine:$($count -ne 1) -Message "$($count) "
                    Start-Sleep -Seconds 1
                    $count--
                }
                until ($count -eq 0)
                Write-ScreenInfo -Type Warning -Message "A new reference disk will be created."
            }
            elseif (-not (Test-Path -Path $newName))
            {
                Write-ScreenInfo -Message "Renaming $(Split-Path -Leaf -Path $legacyDiskPath) to $(Split-Path -Leaf -Path $newName) and updating VHD parent paths"                
                Rename-Item -Path $legacyDiskPath -NewName $newName
                $affectedDisks | Set-VHD -ParentPath $newName
            }
            else
            {
                # This is the critical scenario: If both files exist (i.e. a VM was running and the disk could not be renamed)
                # changing the parent of the VHD to the newly created VHD would not work. Renaming the old VHD to the new format
                # would also not work, as there would again be ID conflicts. All in all, the worst situtation
                Write-ScreenInfo -Type Warning -Message "Unable to rename $(Split-Path -Leaf -Path $legacyDiskPath) to $(Split-Path -Leaf -Path $newName) since both files exist and would cause issues with the Parent Disk ID for existing differencing disks"
            }
        }

        $baseDiskPath = Join-Path -Path $lab.Target.Path -ChildPath "BASE_$($os.OperatingSystemName.Replace(' ', ''))$($archString)_$($os.Version)_$($lab.Target.ReferenceDiskSizeInGB).vhdx"
        $os.BaseDiskPath = $baseDiskPath


        $hostOsVersion = [System.Environment]::OSVersion.Version

        if ($hostOsVersion -ge [System.Version]'6.3' -and $os.Version -ge [System.Version]'6.2')
        {
            Write-PSFMessage -Message "Host OS version is '$($hostOsVersion)' and OS to create disk for is version '$($os.Version)'. So, setting partition style to GPT."
            $partitionStyle = 'GPT'
        }
        else
        {
            Write-PSFMessage -Message "Host OS version is '$($hostOsVersion)' and OS to create disk for is version '$($os.Version)'. So, KEEPING partition style as MBR."
            $partitionStyle = 'MBR'
        }

        if ($osesProcessed -notcontains $os)
        {
            $osesProcessed += $os

            if (-not (Test-Path $baseDiskPath))
            {
                Stop-ShellHWDetectionService

                New-LWReferenceVHDX -IsoOsPath $os.IsoPath `
                    -ReferenceVhdxPath $baseDiskPath `
                    -OsName $os.OperatingSystemName `
                    -ImageName $os.OperatingSystemImageName `
                    -SizeInGb $lab.Target.ReferenceDiskSizeInGB `
                    -PartitionStyle $partitionStyle

                $BaseImagesCreated++
            }
            else
            {
                Write-PSFMessage -Message "The base image $baseDiskPath already exists"
            }
        }
        else
        {
            Write-PSFMessage -Message "Base disk for operating system '$os' already created previously"
        }
    }

    if (-not $BaseImagesCreated)
    {
        Write-ScreenInfo -Message 'All base images were created previously'
    }

    Start-ShellHWDetectionService

    Write-LogFunctionExit
}


function New-LabVHDX
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByName')]
        [string[]]$Name,

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    $lab = Get-Lab
    if (-not $lab)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    Write-PSFMessage -Message 'Stopping the ShellHWDetection service (Shell Hardware Detection) to prevent the OS from responding to the new disks.'
    Stop-ShellHWDetectionService

    if ($Name)
    {
        $disks = Get-LabVHDX -Name $Name
    }
    else
    {
        $disks = Get-LabVHDX -All
    }

    if (-not $disks)
    {
        Write-PSFMessage -Message 'No disks found to create. Either the given name is wrong or there is no disk defined yet'
        Write-LogFunctionExit
        return
    }

    $createOnlyReferencedDisks = Get-LabConfigurationItem -Name CreateOnlyReferencedDisks
    
    $param = @{
        ReferenceObject  = $disks
        DifferenceObject = (Get-LabVM | Where-Object { -not $_.SkipDeployment }).Disks
        ExcludeDifferent = $true
        IncludeEqual     = $true
    }
    $referencedDisks = (Compare-Object @param).InputObject
    if ($createOnlyReferencedDisks -and $($disks.Count - $referencedDisks.Count) -gt 0)
    {
        Write-ScreenInfo "There are $($disks.Count - $referencedDisks.Count) disks defined that are not referenced by any machine. These disks won't be created." -Type Warning
        $disks = $referencedDisks
    }

    foreach ($disk in $disks)
    {
        Write-ScreenInfo -Message "Creating disk '$($disk.Name)'" -TaskStart -NoNewLine
        
        if (-not (Test-Path -Path $disk.Path))
        {
            $params = @{
                VhdxPath = $disk.Path
                SizeInGB = $disk.DiskSize
                SkipInitialize = $disk.SkipInitialization
                Label = $disk.Label
                UseLargeFRS = $disk.UseLargeFRS
                AllocationUnitSize = $disk.AllocationUnitSize
                PartitionStyle = $disk.PartitionStyle
            }
            if ($disk.DriveLetter)
            {
                $params.DriveLetter = $disk.DriveLetter
            }
            New-LWVHDX @params
            Write-ScreenInfo -Message 'Done' -TaskEnd
        }
        else
        {
            Write-ScreenInfo "The disk '$($disk.Path)' does already exist, no new disk is created." -Type Warning -TaskEnd
        }
    }

    Write-PSFMessage -Message 'Starting the ShellHWDetection service (Shell Hardware Detection) again.'
    Start-ShellHWDetectionService

    Write-LogFunctionExit
}


function Update-LabBaseImage
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(Mandatory)]
        [string]$BaseImagePath,

        [Parameter(Mandatory)]
        [string]$UpdateFolderPath
    )

    if ($IsLinux)
    {
        throw 'Sorry - not implemented on Linux yet.'
    }

    if (-not (Test-Path -Path $BaseImagePath -PathType Leaf))
    {
        Write-Error "The specified image '$BaseImagePath' could not be found"
        return
    }

    if ([System.IO.Path]::GetExtension($BaseImagePath) -ne '.vhdx')
    {
        Write-Error "The specified image must have the extension '.vhdx'"
        return
    }

    $patchesCab = Get-ChildItem -Path $UpdateFolderPath\* -Include *.cab -ErrorAction SilentlyContinue
    $patchesMsu = Get-ChildItem -Path $UpdateFolderPath\* -Include *.msu -ErrorAction SilentlyContinue

    if (($null -eq $patchesCab) -and ($null -eq $patchesMsu))
    {
        Write-Error "No .cab and .msu files found in '$UpdateFolderPath'"
        return
    }

    Write-PSFMessage -Level Host -Message 'Updating base image'
    Write-PSFMessage -Level Host -Message $BaseImagePath
    Write-PSFMessage -Level Host -Message "with $($patchesCab.Count + $patchesMsu.Count) updates from"
    Write-PSFMessage -Level Host -Message $UpdateFolderPath
    Write-PSFMessage -Level Host -Message 'This process can take a long time, depending on the number of updates'

    $start = Get-Date
    Write-PSFMessage -Level Host -Message "Start time: $start"

    Write-PSFMessage -Level Host -Message 'Creating temp folder (mount point)'
    $mountTempFolder = New-Item -ItemType Directory -Path $labSources -Name ([guid]::NewGuid())

    Write-PSFMessage -Level Host -Message "Mounting Windows Image '$BaseImagePath'"
    Write-PSFMessage -Level Host -Message "to folder '$mountTempFolder'"
    Mount-WindowsImage -Path $mountTempFolder -ImagePath $BaseImagePath -Index 1

    Write-PSFMessage -Level Host -Message 'Adding patches to the mounted Windows Image.'
    $patchesCab | ForEach-Object {

        $UpdateReady = Get-WindowsPackage -PackagePath $_ -Path $mountTempFolder | Select-Object -Property PackageState, PackageName, Applicable

        if ($UpdateReady.PackageState -eq 'Installed')
        {
            Write-PSFMessage -Level Host -Message "$($UpdateReady.PackageName) is already installed"
        }
        elseif ($UpdateReady.Applicable -eq $true)
        {
            Add-WindowsPackage -PackagePath $_.FullName -Path $mountTempFolder
        }
    }
    $patchesMsu | ForEach-Object {

        Add-WindowsPackage -PackagePath $_.FullName -Path $mountTempFolder
    }

    Write-PSFMessage -Level Host -Message "Dismounting Windows Image from path '$mountTempFolder' and saving the changes. This can take quite some time again..."
    Dismount-WindowsImage -Path $mountTempFolder -Save
    Write-PSFMessage -Level Host -Message 'finished'

    Write-PSFMessage -Level Host -Message "Deleting temp folder '$mountTempFolder'"
    Remove-Item -Path $mountTempFolder -Recurse -Force

    $end = Get-Date
    Write-PSFMessage -Level Host -Message "finished at $end. Runtime: $($end - $start)"
}


function Update-LabIsoImage
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "")]
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(Mandatory)]
        [string]$SourceIsoImagePath,

        [Parameter(Mandatory)]
        [string]$TargetIsoImagePath,

        [Parameter(Mandatory)]
        [string]$UpdateFolderPath,

        [Parameter(Mandatory)]
        [int]$SourceImageIndex,

        [Parameter(Mandatory=$false)]
        [Switch]$SkipSuperseededCleanup
    )

    if ($IsLinux)
    {
        throw 'Sorry - not implemented on Linux yet.'
    }

    #region Expand-IsoImage
    function Expand-IsoImage
    {
        param(
            [Parameter(Mandatory)]
            [string]$SourceIsoImagePath,

            [Parameter(Mandatory)]
            [string]$OutputPath,

            [switch]$Force
        )

        if (-not (Test-Path -Path $SourceIsoImagePath -PathType Leaf))
        {
            Write-Error "The specified ISO image '$SourceIsoImagePath' could not be found"
            return
        }

        if ((Test-Path -Path $OutputPath) -and -not $Force)
        {
            Write-Error "The output folder does already exist" -TargetObject $OutputPath
            return
        }
        else
        {
            Remove-Item -Path $OutputPath -Force -Recurse -ErrorAction Ignore
        }

        New-Item -ItemType Directory -Path $OutputPath | Out-Null


        $image = Mount-LabDiskImage -ImagePath $SourceIsoImagePath -StorageType ISO -PassThru
        Get-PSDrive | Out-Null #This is just to refresh the drives. Somehow if this cmdlet is not called, PowerShell does not see the new drives.

        if($image)
        {
            $source = Join-Path -Path ([IO.DriveInfo][string]$image.DriveLetter).Name -ChildPath '*'

            Write-PSFMessage -Message "Extracting ISO image '$source' to '$OutputPath'"
            Copy-Item -Path $source -Destination $OutputPath -Recurse -Force
            [void] (Dismount-LabDiskImage -ImagePath $SourceIsoImagePath)
            Write-PSFMessage -Message 'Copy complete'
        }
        else
        {
            Write-Error "Could not mount ISO image '$SourceIsoImagePath'" -TargetObject $SourceIsoImagePath
            return
        }
    }
    #endregion Expand-IsoImage

    #region Get-IsoImageName
    function Get-IsoImageName
    {
        param(
            [Parameter(Mandatory)]
            [string]$IsoImagePath
        )

        if (-not (Test-Path -Path $IsoImagePath -PathType Leaf))
        {
            Write-Error "The specified ISO image '$IsoImagePath' could not be found"
            return
        }

        $image = Mount-DiskImage $IsoImagePath -StorageType ISO -PassThru
        $image | Get-Volume | Select-Object -ExpandProperty FileSystemLabel
        [void] ($image | Dismount-DiskImage)
    }
    #endregion Get-IsoImageName

    if (-not (Test-Path -Path $SourceIsoImagePath -PathType Leaf))
    {
        Write-Error "The specified ISO image '$SourceIsoImagePath' could not be found"
        return
    }

    if (Test-Path -Path $TargetIsoImagePath -PathType Leaf)
    {
        Write-Error "The specified target ISO image '$TargetIsoImagePath' does already exist"
        return
    }

    if ([System.IO.Path]::GetExtension($TargetIsoImagePath) -ne '.iso')
    {
        Write-Error "The specified target ISO image path must have the extension '.iso'"
        return
    }

    Write-PSFMessage -Level Host -Message 'Creating an updated ISO from'
    Write-PSFMessage -Level Host -Message "Target path $TargetIsoImagePath"
    Write-PSFMessage -Level Host -Message "Source path $SourceIsoImagePath"
    Write-PSFMessage -Level Host -Message "with updates from path $UpdateFolderPath"
    Write-PSFMessage -Level Host -Message "This process can take a long time, depending on the number of updates"
    $start = Get-Date
    Write-PSFMessage -Level Host -Message "Start time: $start"

    $extractTempFolder = New-Item -ItemType Directory -Path $labSources -Name ([guid]::NewGuid())
    $mountTempFolder = New-Item -ItemType Directory -Path $labSources -Name ([guid]::NewGuid())

    $isoImageName = Get-IsoImageName -IsoImagePath $SourceIsoImagePath

    Write-PSFMessage -Level Host -Message "Extracting ISO image '$SourceIsoImagePath' to '$extractTempFolder'"
    Expand-IsoImage -SourceIsoImagePath $SourceIsoImagePath -OutputPath $extractTempFolder -Force

    $installWim = Get-ChildItem -Path $extractTempFolder -Filter install.wim -Recurse
    $windowsImage = Get-WindowsImage -ImagePath $installWim.FullName -Index $SourceImageIndex
    Write-PSFMessage -Level Host -Message "The Windows Image targeted is named '$($windowsImage.ImageName)'"

    Write-PSFMessage -Level Host -Message "Mounting Windows Image '$($windowsImage.ImagePath)' to folder '$mountTempFolder'"
    Set-ItemProperty $installWim.FullName -Name IsReadOnly -Value $false
    Mount-WindowsImage -Path $mountTempFolder -ImagePath $installWim.FullName -Index $SourceImageIndex

    $patches = Get-ChildItem -Path $UpdateFolderPath\* -Include *.msu, *.cab
    Write-PSFMessage -Level Host -Message "Found $($patches.Count) patches in the UpdateFolderPath '$UpdateFolderPath'"

    Write-PSFMessage -Level Host -Message "Adding patches to the mounted Windows Image. This can take quite some time..."
    foreach ($patch in $patches)
    {
        Write-PSFMessage -Level Host -Message "Adding patch '$($patch.Name)'..."
        Add-WindowsPackage -PackagePath $patch.FullName -Path $mountTempFolder | Out-Null
        Write-PSFMessage -Level Host -Message 'finished'
    }

    if (! $SkipSuperseededCleanup) {
        Write-PSFMessage -Level Host -Message "Cleaning up superseeded updates. This can take quite some time..."
        $cmd = "dism.exe /image:$mountTempFolder /Cleanup-Image /StartComponentCleanup /ResetBase"
        Write-PSFMessage -Message $cmd
        $global:dismResult = Invoke-Expression -Command $cmd 2>&1
        Write-PSFMessage -Level Host -Message 'finished'
    }

    Write-PSFMessage -Level Host -Message "Dismounting Windows Image from path '$mountTempFolder' and saving the changes. This can take quite some time again..."
    Dismount-WindowsImage -Path $mountTempFolder -Save
    Set-ItemProperty $installWim.FullName -Name IsReadOnly -Value $true
    Write-PSFMessage -Level Host -Message 'finished'

    Write-PSFMessage -Level Host -Message "Calling oscdimg.exe to create a new bootable ISO image '$TargetIsoImagePath'..."
    $cmd = "$labSources\Tools\oscdimg.exe -m -o -u2 -l$isoImageName -udfver102 -bootdata:2#p0,e,b$extractTempFolder\boot\etfsboot.com#pEF,e,b$extractTempFolder\efi\microsoft\boot\efisys.bin $extractTempFolder $TargetIsoImagePath"
    Write-PSFMessage -Message $cmd
    $global:oscdimgResult = Invoke-Expression -Command $cmd 2>&1
    Write-PSFMessage -Level Host -Message 'finished'

    Write-PSFMessage -Level Host -Message "Deleting temp folder '$extractTempFolder'"
    Remove-Item -Path $extractTempFolder -Recurse -Force

    Write-PSFMessage -Level Host -Message "Deleting temp folder '$mountTempFolder'"
    Remove-Item -Path $mountTempFolder -Recurse -Force

    $end = Get-Date
    Write-PSFMessage -Level Host -Message "finished at $end. Runtime: $($end - $start)"
}


function Install-LabDscClient
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string[]]$ComputerName,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All,

        [string[]]$PullServer
    )

    if ($All)
    {
        $machines = Get-LabVM | Where-Object { $_.Roles.Name -notin 'DC', 'RootDC', 'FirstChildDC', 'DSCPullServer' }
    }
    else
    {
        $machines = Get-LabVM -ComputerName $ComputerName
    }

    if (-not $machines)
    {
        Write-Error 'Machines to configure DSC Pull not defined or not found in the lab.'
        return
    }

    Start-LabVM -ComputerName $machines -Wait

    if ($PullServer)
    {
        if (-not (Get-LabVM -ComputerName $PullServer | Where-Object { $_.Roles.Name -contains 'DSCPullServer' }))
        {
            Write-Error "The given DSC Pull Server '$PullServer' could not be found in the lab."
            return
        }
        else
        {
            $pullServerMachines = Get-LabVM -ComputerName $PullServer
        }
    }
    else
    {
        $pullServerMachines = Get-LabVM -Role DSCPullServer
    }

    Copy-LabFileItem -Path $labSources\PostInstallationActivities\SetupDscClients\SetupDscClients.ps1 -ComputerName $machines

    [bool] $useSsl = Get-LabIssuingCA -WarningAction SilentlyContinue

    foreach ($machine in $machines)
    {
        Invoke-LabCommand -ActivityName 'Setup DSC Pull Clients' -ComputerName $machine -ScriptBlock {
            param
            (
                [Parameter(Mandatory)]
                [string[]]$PullServer,

                [Parameter(Mandatory)]
                [string[]]$RegistrationKey,
                [bool] $UseSsl
            )

            C:\SetupDscClients.ps1 -PullServer $PullServer -RegistrationKey $RegistrationKey -UseSsl $UseSsl
        } -ArgumentList $pullServerMachines.FQDN, $pullServerMachines.InternalNotes.DscRegistrationKey, $useSsl -PassThru
    }
}


function Install-LabDscPullServer
{
    [cmdletBinding()]
    param (
        [int]$InstallationTimeout = 15
    )

    Write-LogFunctionEntry

    $online = $true
    $lab = Get-Lab
    $roleName = [AutomatedLab.Roles]::DSCPullServer
    $requiredModules = 'xPSDesiredStateConfiguration', 'xDscDiagnostics', 'xWebAdministration'

    Write-ScreenInfo "Starting Pull Servers and waiting until they are ready" -NoNewLine
    Start-LabVM -RoleName DSCPullServer -ProgressIndicator 15 -Wait

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role $roleName
    if (-not $machines)
    {
        Write-ScreenInfo -Message 'No DSC Pull Server defined in this lab, so there is nothing to do'
        Write-LogFunctionExit
        return
    }

    if (-not (Get-LabVM -Role Routing) -and $lab.DefaultVirtualizationEngine -eq 'HyperV')
    {
        Write-ScreenInfo 'Routing Role not detected, installing DSC in offline mode.'
        $online = $false
    }
    else
    {
        Write-ScreenInfo 'Routing Role detected, installing DSC in online mode.'
    }

    if ($online)
    {
        $machinesOnline = $machines | ForEach-Object {
            Test-LabMachineInternetConnectivity -ComputerName $_ -AsJob
        } |
        Receive-Job -Wait -AutoRemoveJob |
        Where-Object { $_.TcpTestSucceeded } |
        ForEach-Object { $_.NetAdapter.SystemName }

        #if there are machines online, get the ones that are offline
        if ($machinesOnline)
        {
            $machinesOffline = (Compare-Object -ReferenceObject $machines.FQDN -DifferenceObject $machinesOnline).InputObject
        }

        #if there are machines offline or all machines are offline
        if ($machinesOffline -or -not $machinesOnline)
        {
            Write-Error "The machines $($machinesOffline -join ', ') are not connected to the internet. Switching to offline mode."
            $online = $false
        }
        else
        {
            Write-ScreenInfo 'All DSC Pull Servers can reach the internet.'
        }
    }

    $wrongPsVersion = Invoke-LabCommand -ComputerName $machines -ScriptBlock {
        $PSVersionTable | Add-Member -Name ComputerName -MemberType NoteProperty -Value $env:COMPUTERNAME -PassThru -Force
    } -PassThru -NoDisplay |
    Where-Object { $_.PSVersion.Major -lt 5 } |
    Select-Object -ExpandProperty ComputerName

    if ($wrongPsVersion)
    {
        Write-Error "The following machines have an unsupported PowerShell version. At least PowerShell 5.0 is required. $($wrongPsVersion -join ', ')"
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    $ca = Get-LabIssuingCA -WarningAction SilentlyContinue
    if ($ca)
    {
        if (-not (Test-LabCATemplate -TemplateName DscPullSsl -ComputerName $ca))
        {
            New-LabCATemplate -TemplateName DscPullSsl -DisplayName 'Dsc Pull Sever SSL' -SourceTemplateName WebServer -ApplicationPolicy 'Server Authentication' `
            -EnrollmentFlags Autoenrollment -PrivateKeyFlags AllowKeyExport -Version 2 -SamAccountName 'Domain Computers' -ComputerName $ca -ErrorAction Stop
        }

        if (-not (Test-LabCATemplate -TemplateName DscMofFileEncryption  -ComputerName $ca))
        {
            New-LabCATemplate -TemplateName DscMofFileEncryption -DisplayName 'Dsc Mof File Encryption' -SourceTemplateName CEPEncryption -ApplicationPolicy 'Document Encryption' `
            -KeyUsage KEY_ENCIPHERMENT, DATA_ENCIPHERMENT -EnrollmentFlags Autoenrollment -PrivateKeyFlags AllowKeyExport -Version 2 -SamAccountName 'Domain Computers' -ComputerName $ca
        }
    }

    if ($Online)
    {
        Invoke-LabCommand -ActivityName 'Setup Dsc Pull Server 1' -ComputerName $machines -ScriptBlock {
            # Due to changes in the gallery: Accept TLS12
            try
            {
                #https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=netcore-2.0#System_Net_SecurityProtocolType_SystemDefault
                if ($PSVersionTable.PSVersion.Major -lt 6 -and [Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12')
                {
                    Write-Verbose -Message 'Adding support for TLS 1.2'
                    [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
                }
            }
            catch
            {
                Write-Warning -Message 'Adding TLS 1.2 to supported security protocols was unsuccessful.'
            }

            Install-WindowsFeature -Name DSC-Service
            Install-PackageProvider -Name NuGet -Force
            Install-Module -Name $requiredModules -Force
        } -Variable (Get-Variable -Name requiredModules) -AsJob -PassThru | Wait-Job | Receive-Job -Keep | Out-Null #only interested in errors
    }
    else
    {
        if ((Get-Module -ListAvailable -Name $requiredModules).Count -eq $requiredModules.Count)
        {
            Write-ScreenInfo "The required modules to install DSC ($($requiredModules -join ', ')) are found in PSModulePath"
        }
        else
        {
            Write-ScreenInfo "Downloading the modules '$($requiredModules -join ', ')' locally and copying them to the DSC Pull Servers."
            try
            {
                #https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=netcore-2.0#System_Net_SecurityProtocolType_SystemDefault
                if ($PSVersionTable.PSVersion.Major -lt 6 -and [Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12')
                {
                    Write-Verbose -Message 'Adding support for TLS 1.2'
                    [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
                }
            }
            catch
            {
                Write-Warning -Message 'Adding TLS 1.2 to supported security protocols was unsuccessful.'
            }

            Install-PackageProvider -Name NuGet -Force | Out-Null
            Install-Module -Name $requiredModules -Force
        }

        foreach ($module in $requiredModules)
        {
            $moduleBase = Get-Module -Name $module -ListAvailable |
            Sort-Object -Property Version -Descending |
            Select-Object -First 1 -ExpandProperty ModuleBase
            $moduleDestination = Split-Path -Path $moduleBase -Parent

            Copy-LabFileItem -Path $moduleBase -ComputerName $machines -DestinationFolderPath $moduleDestination -Recurse
        }
    }

    Copy-LabFileItem -Path $labSources\PostInstallationActivities\SetupDscPullServer\SetupDscPullServerEdb.ps1,
    $labSources\PostInstallationActivities\SetupDscPullServer\SetupDscPullServerMdb.ps1,
    $labSources\PostInstallationActivities\SetupDscPullServer\SetupDscPullServerSql.ps1,
    $labSources\PostInstallationActivities\SetupDscPullServer\DscTestConfig.ps1 -ComputerName $machines

    foreach ($machine in $machines)
    {
        $role = $machine.Roles | Where-Object Name -eq $roleName
        $doNotPushLocalModules = [bool]$role.Properties.DoNotPushLocalModules

        if (-not $doNotPushLocalModules)
        {
            $moduleNames = (Get-Module -ListAvailable | Where-Object { $_.Tags -contains 'DSCResource' -and $_.Name -notin $requiredModules }).Name
            Write-ScreenInfo "Publishing local DSC resources: $($moduleNames -join ', ')..." -NoNewLine

            foreach ($module in $moduleNames)
            {
                $moduleBase = Get-Module -Name $module -ListAvailable |
                Sort-Object -Property Version -Descending |
                Select-Object -First 1 -ExpandProperty ModuleBase
                $moduleDestination = Split-Path -Path $moduleBase -Parent

                Copy-LabFileItem -Path $moduleBase -ComputerName $machines -DestinationFolderPath $moduleDestination -Recurse
            }

            Write-ScreenInfo 'finished'
        }
    }


    $accessDbEngine = Get-LabInternetFile -Uri $(Get-LabConfigurationItem -Name AccessDatabaseEngine2016x86) -Path $labsources\SoftwarePackages -PassThru
    $jobs = @()

    foreach ($machine in $machines)
    {
        $role = $machine.Roles | Where-Object Name -eq $roleName
        $databaseEngine = if ($role.Properties.DatabaseEngine)
        {
            $role.Properties.DatabaseEngine
        }
        else
        {
            'edb'
        }

        if ($databaseEngine -eq 'sql' -and $role.Properties.SqlServer)
        {
            $sqledition = ((Get-LabVm -ComputerName $role.Properties.SqlServer).Roles | Where-Object Name -like SQLServer*).Name -replace 'SQLServer'
            $isNew = $sqledition -ge 2019
            Invoke-LabCommand -ActivityName 'Creating DSC SQL Database' -FilePath $labSources\PostInstallationActivities\SetupDscPullServer\CreateDscSqlDatabase.ps1 -ComputerName $role.Properties.SqlServer -ArgumentList $machine.DomainAccountName,$isNew
        }

        if ($databaseEngine -eq 'mdb')
        {
            #Install the missing database driver for access mbd that is no longer available on Windows Server 2016+
            if ((Get-LabVM -ComputerName $machine).OperatingSystem.Version -gt '6.3.0.0')
            {
                Install-LabSoftwarePackage -Path $accessDbEngine.FullName -CommandLine '/passive /quiet' -ComputerName $machines
            }
        }

        if ($machine.DefaultVirtualizationEngine -eq 'Azure')
        {
            Write-PSFMessage -Message ('Adding external port 8080 to Azure load balancer')
            (Get-Lab).AzureSettings.LoadBalancerPortCounter++
            $remotePort = (Get-Lab).AzureSettings.LoadBalancerPortCounter
            Add-LWAzureLoadBalancedPort -Port $remotePort -DestinationPort 8080 -ComputerName $machine -ErrorAction SilentlyContinue
        }

        if (Get-LabIssuingCA -WarningAction SilentlyContinue)
        {
            Request-LabCertificate -Subject "CN=$machine" -TemplateName DscMofFileEncryption -ComputerName $machine -PassThru | Out-Null

            $cert = Request-LabCertificate -Subject "CN=$($machine.Name)" -SAN $machine.Name, $machine.FQDN -TemplateName DscPullSsl -ComputerName $machine -PassThru -ErrorAction Stop
        }
        else
        {
            $cert = @{Thumbprint = 'AllowUnencryptedTraffic'}
        }

        $setupParams = @{
            ComputerName = $machine
            CertificateThumbPrint = $cert.Thumbprint
            RegistrationKey = Get-LabConfigurationItem -Name DscPullServerRegistrationKey
            DatabaseEngine  = $databaseEngine
        }
        if ($role.Properties.DatabaseName) { $setupParams.DatabaseName = $role.Properties.DatabaseName }
        if ($role.Properties.SqlServer) { $setupParams.SqlServer = $role.Properties.SqlServer }

        $jobs += Invoke-LabCommand -ActivityName "Setting up DSC Pull Server on '$machine'" -ComputerName $machine -ScriptBlock {
            if ($setupParams.DatabaseEngine -eq 'edb')
            {
                C:\SetupDscPullServerEdb.ps1 -ComputerName $setupParams.ComputerName -CertificateThumbPrint $setupParams.CertificateThumbPrint -RegistrationKey $setupParams.RegistrationKey
            }
            elseif ($setupParams.DatabaseEngine -eq 'mdb')
            {
                C:\SetupDscPullServerMdb.ps1 -ComputerName $setupParams.ComputerName -CertificateThumbPrint $setupParams.CertificateThumbPrint -RegistrationKey $setupParams.RegistrationKey
                Copy-Item -Path C:\Windows\System32\WindowsPowerShell\v1.0\Modules\PSDesiredStateConfiguration\PullServer\Devices.mdb -Destination 'C:\Program Files\WindowsPowerShell\DscService\Devices.mdb'
            }
            elseif ($setupParams.DatabaseEngine -eq 'sql')
            {
                C:\SetupDscPullServerSql.ps1 -ComputerName $setupParams.ComputerName -CertificateThumbPrint $setupParams.CertificateThumbPrint -RegistrationKey $setupParams.RegistrationKey -SqlServer $setupParams.SqlServer -DatabaseName $setupParams.DatabaseName
            }
            else
            {
                Write-Error "The database engine is unknown"
                return
            }

            C:\DscTestConfig.ps1
            Start-Job -ScriptBlock { Publish-DSCModuleAndMof -Source C:\DscTestConfig } | Wait-Job | Out-Null

        } -Variable (Get-Variable -Name setupParams) -AsJob -PassThru
    }

    Write-ScreenInfo -Message 'Waiting for configuration of DSC Pull Server to complete' -NoNewline
    Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -Timeout $InstallationTimeout -NoDisplay

    if ($jobs | Where-Object -Property State -eq 'Failed')
    {
        throw ('Setting up the DSC pull server failed. Please review the output of the following jobs: {0}' -f ($jobs.Id -join ','))
    }

    $jobs = Install-LabWindowsFeature -ComputerName $machines -FeatureName Web-Mgmt-Tools -AsJob -NoDisplay
    Write-ScreenInfo -Message 'Waiting for installation of IIS web admin tools to complete'
    Wait-LWLabJob -Job $jobs -ProgressIndicator 0 -Timeout $InstallationTimeout -NoDisplay

    foreach ($machine in $machines)
    {
        $registrationKey = Invoke-LabCommand -ActivityName 'Get Registration Key created on the Pull Server' -ComputerName $machine -ScriptBlock {
            Get-Content 'C:\Program Files\WindowsPowerShell\DscService\RegistrationKeys.txt'
        } -PassThru -NoDisplay

        $machine.InternalNotes.DscRegistrationKey = $registrationKey
    }

    Export-Lab

    Write-LogFunctionExit
}


function Invoke-LabDscConfiguration
{
    [CmdletBinding(DefaultParameterSetName = 'New')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'New')]
        [System.Management.Automation.ConfigurationInfo]$Configuration,

        [Parameter(Mandatory)]
        [string[]]$ComputerName,
        
        [Parameter()]
        [hashtable]$Parameter,

        [Parameter(ParameterSetName = 'New')]
        [hashtable]$ConfigurationData,

        [Parameter(ParameterSetName = 'UseExisting')]
        [switch]$UseExisting,

        [switch]$Wait,

        [switch]$Force
    )

    Write-LogFunctionEntry

    $lab = Get-Lab
    $localLabSoures = Get-LabSourcesLocation -Local
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -ComputerName $ComputerName
    if ($machines.Count -ne $ComputerName.Count)
    {
        Write-Error -Message 'Not all machines specified could be found in the lab.'
        Write-LogFunctionExit
        return
    }

    if ($PSCmdlet.ParameterSetName -eq 'New')
    {
        $outputPath = "$localLabSoures\$(Get-LabConfigurationItem -Name DscMofPath)\$(New-Guid)"

        if (Test-Path -Path $outputPath)
        {
            Remove-Item -Path $outputPath -Recurse -Force
        }
        New-Item -ItemType Directory -Path $outputPath -Force | Out-Null

        if ($ConfigurationData)
        {
            $result = ValidateUpdate-ConfigurationData -ConfigurationData $ConfigurationData
            if (-not $result)
            {
                return
            }
        }

        $tempPath = [System.IO.Path]::GetTempFileName()
        Remove-Item -Path $tempPath
        New-Item -ItemType Directory -Path $tempPath | Out-Null

        $dscModules = @()

        $null = foreach ($c in $ComputerName)
        {
            if ($ConfigurationData)
            {
                $adaptedConfig = $ConfigurationData.Clone()
            }

            Push-Location -Path Function:
            if ($configuration | Get-Item -ErrorAction SilentlyContinue)
            {
                $configuration | Remove-Item
            }
            $configuration | New-Item -Force
            Pop-Location

            Write-Information -MessageData "Creating Configuration MOF '$($Configuration.Name)' for node '$c'" -Tags DSC
            
            $param = @{
                OutputPath = $tempPath
                WarningAction = 'SilentlyContinue'
            }
            if ($Configuration.Parameters.ContainsKey('ComputerName'))
            {
                $param.ComputerName = $c
            }
            if ($adaptedConfig)
            {
                $param.ConfigurationData = $adaptedConfig
            }
            
            if ($Parameter)
            {
                $param += $Parameter
            }
            
            $mof = & $Configuration.Name @param
            
            if ($mof.Count -gt 1)
            {
                $mof = $mof | Where-Object { $_.Name -like "*$c*" }
            }
            $mof = $mof | Rename-Item -NewName "$($Configuration.Name)_$c.mof" -Force -PassThru
            $mof | Move-Item -Destination $outputPath -Force

            Remove-Item -Path $tempPath -Force -Recurse
        }

        $mofFiles = Get-ChildItem -Path $outputPath -Filter *.mof | Where-Object Name -Match '(?<ConfigurationName>\w+)_(?<ComputerName>[\w-_]+)\.mof'

        foreach ($c in $ComputerName)
        {
            foreach ($mofFile in $mofFiles)
            {
                if ($mofFile.Name -match "(?<ConfigurationName>$($Configuration.Name))_(?<ComputerName>$c)\.mof")
                {
                    Send-File -Source $mofFile.FullName -Session (New-LabPSSession -ComputerName $Matches.ComputerName) -Destination "C:\AL Dsc\$($Configuration.Name)" -Force
                }
            }
        }

        #Get-DscConfigurationImportedResource now needs to walk over all the resources used in the composite resource
        #to find out all the reuqired modules we need to upload in total
        $requiredDscModules = Get-DscConfigurationImportedResource -Configuration $Configuration -ErrorAction Stop
        foreach ($requiredDscModule in $requiredDscModules)
        {
            Send-ModuleToPSSession -Module (Get-Module -Name $requiredDscModule -ListAvailable) -Session (New-LabPSSession -ComputerName $ComputerName) -Scope AllUsers -IncludeDependencies
        }

        Invoke-LabCommand -ComputerName $ComputerName -ActivityName 'Applying new DSC configuration' -ScriptBlock {

            $path = "C:\AL Dsc\$($Configuration.Name)"

            Remove-Item -Path "$path\localhost.mof" -ErrorAction SilentlyContinue

            $mofFiles = Get-ChildItem -Path $path -Filter *.mof
            if ($mofFiles.Count -gt 1)
            {
                throw "There is more than one MOF file in the folder '$path'. Expected is only one file."
            }

            $mofFiles | Rename-Item -NewName localhost.mof

            Start-DscConfiguration -Path $path -Wait:$Wait -Force:$Force

        } -Variable (Get-Variable -Name Configuration, Wait, Force)
    }
    else
    {
        Invoke-LabCommand -ComputerName $ComputerName -ActivityName 'Applying existing DSC configuration' -ScriptBlock {

            Start-DscConfiguration -UseExisting -Wait:$Wait -Force:$Force

        } -Variable (Get-Variable -Name Wait, Force)
    }

    Remove-Item -Path $outputPath -Recurse -Force

    Write-LogFunctionExit
}


function Remove-LabDscLocalConfigurationManagerConfiguration
{
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-LogFunctionEntry

    function Remove-DscLocalConfigurationManagerConfiguration
    {
        param(
            [string[]]$ComputerName = 'localhost'
        )

        $configurationScript = @'
        [DSCLocalConfigurationManager()]
        configuration LcmDefaultConfiguration
        {
            param(
                [string[]]$ComputerName = 'localhost'
            )

            Node $ComputerName
            {
                Settings
                {
                    RefreshMode = 'Push'
                    ConfigurationModeFrequencyMins = 15
                    ConfigurationMode = 'ApplyAndMonitor'
                    RebootNodeIfNeeded = $true
                }
            }
        }
'@


        [scriptblock]::Create($configurationScript).Invoke()
        $path = New-Item -ItemType Directory -Path "$([System.IO.Path]::GetTempPath())\$(New-Guid)"

        Remove-DscConfigurationDocument -Stage Current, Pending -Force
        LcmDefaultConfiguration -OutputPath $path.FullName | Out-Null
        Set-DscLocalConfigurationManager -Path $path.FullName -Force

        Remove-Item -Path $path.FullName -Recurse -Force

        try
        {
            Test-DscConfiguration -ErrorAction Stop
            Write-Error 'There was a problem resetting the Local Configuration Manger configuration'
        }
        catch
        {
            Write-Host 'DSC Local Configuration Manger was reset to default values'
        }
    }

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -ComputerName $ComputerName
    if ($machines.Count -ne $ComputerName.Count)
    {
        Write-Error -Message 'Not all machines specified could be found in the lab.'
        Write-LogFunctionExit
        return
    }

    Invoke-LabCommand -ActivityName 'Removing DSC LCM configuration' -ComputerName $ComputerName -ScriptBlock (Get-Command -Name Remove-DscLocalConfigurationManagerConfiguration).ScriptBlock

    Write-LogFunctionExit
}


function Set-LabDscLocalConfigurationManagerConfiguration
{
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [ValidateSet('ContinueConfiguration', 'StopConfiguration')]
        [string]$ActionAfterReboot,

        [string]$CertificateID,

        [string]$ConfigurationID,

        [int]$RefreshFrequencyMins,

        [bool]$AllowModuleOverwrite,

        [ValidateSet('ForceModuleImport','All', 'None')]
        [string]$DebugMode,

        [string[]]$ConfigurationNames,

        [int]$StatusRetentionTimeInDays,

        [ValidateSet('Push', 'Pull')]
        [string]$RefreshMode,

        [int]$ConfigurationModeFrequencyMins,

        [ValidateSet('ApplyAndAutoCorrect', 'ApplyOnly', 'ApplyAndMonitor')]
        [string]$ConfigurationMode,

        [bool]$RebootNodeIfNeeded,

        [hashtable[]]$ConfigurationRepositoryWeb,

        [hashtable[]]$ReportServerWeb,

        [hashtable[]]$PartialConfiguration
    )

    Write-LogFunctionEntry

    function Set-DscLocalConfigurationManagerConfiguration
    {
        param(
            [string[]]$ComputerName = 'localhost',

            [ValidateSet('ContinueConfiguration', 'StopConfiguration')]
            [string]$ActionAfterReboot,

            [string]$CertificateID,

            [string]$ConfigurationID,

            [int]$RefreshFrequencyMins,

            [bool]$AllowModuleOverwrite,

            [ValidateSet('ForceModuleImport','All', 'None')]
            [string]$DebugMode,

            [string[]]$ConfigurationNames,

            [int]$StatusRetentionTimeInDays,

            [ValidateSet('Push', 'Pull')]
            [string]$RefreshMode,

            [int]$ConfigurationModeFrequencyMins,

            [ValidateSet('ApplyAndAutoCorrect', 'ApplyOnly', 'ApplyAndMonitor')]
            [string]$ConfigurationMode,

            [bool]$RebootNodeIfNeeded,

            [hashtable[]]$ConfigurationRepositoryWeb,

            [hashtable[]]$ReportServerWeb,

            [hashtable[]]$PartialConfiguration
        )

        if ($PartialConfiguration)
        {
            throw (New-Object System.NotImplementedException)
        }

        if ($ConfigurationRepositoryWeb)
        {
            $validKeys = 'Name', 'ServerURL', 'RegistrationKey', 'ConfigurationNames', 'AllowUnsecureConnection'
            foreach ($hashtable in $ConfigurationRepositoryWeb)
            {

                if (-not (Test-HashtableKeys -Hashtable $hashtable -ValidKeys $validKeys))
                {
                    Write-Error 'The parameter hashtable contains invalid keys. Check the previous error to see details'
                    return
                }
            }
        }

        if ($ReportServerWeb)
        {
            $validKeys = 'Name', 'ServerURL', 'RegistrationKey', 'AllowUnsecureConnection'
            foreach ($hashtable in $ReportServerWeb)
            {

                if (-not (Test-HashtableKeys -Hashtable $hashtable -ValidKeys $validKeys))
                {
                    Write-Error 'The parameter hashtable contains invalid keys. Check the previous error to see details'
                    return
                }
            }
        }

        $sb = New-Object System.Text.StringBuilder

        [void]$sb.AppendLine('[DSCLocalConfigurationManager()]')
        [void]$sb.AppendLine('configuration LcmConfiguration')
        [void]$sb.AppendLine('{')
        [void]$sb.AppendLine('param([string[]]$ComputerName = "localhost")')
        [void]$sb.AppendLine('Node $ComputerName')
        [void]$sb.AppendLine('{')
        [void]$sb.AppendLine('Settings')
        [void]$sb.AppendLine('{')
        if ($PSBoundParameters.ContainsKey('ActionAfterReboot')) { [void]$sb.AppendLine("ActionAfterReboot = '$ActionAfterReboot'") }
        if ($PSBoundParameters.ContainsKey('RefreshMode')) { [void]$sb.AppendLine("RefreshMode = '$RefreshMode'") }
        if ($PSBoundParameters.ContainsKey('ConfigurationModeFrequencyMins')) { [void]$sb.AppendLine("ConfigurationModeFrequencyMins = $ConfigurationModeFrequencyMins") }
        if ($PSBoundParameters.ContainsKey('CertificateID')) { [void]$sb.AppendLine("CertificateID = $CertificateID") }
        if ($PSBoundParameters.ContainsKey('ConfigurationID')) { [void]$sb.AppendLine("ConfigurationID = $ConfigurationID") }
        if ($PSBoundParameters.ContainsKey('AllowModuleOverwrite')) { [void]$sb.AppendLine("AllowModuleOverwrite = `$$AllowModuleOverwrite") }
        if ($PSBoundParameters.ContainsKey('RebootNodeIfNeeded')) { [void]$sb.AppendLine("RebootNodeIfNeeded = `$$RebootNodeIfNeeded") }
        if ($PSBoundParameters.ContainsKey('DebugMode')) { [void]$sb.AppendLine("DebugMode = '$DebugMode'") }
        if ($PSBoundParameters.ContainsKey('ConfigurationNames')) { [void]$sb.AppendLine("ConfigurationNames = @('$($ConfigurationNames -join "', '")')") }
        if ($PSBoundParameters.ContainsKey('StatusRetentionTimeInDays')) { [void]$sb.AppendLine("StatusRetentionTimeInDays = $StatusRetentionTimeInDays") }
        if ($PSBoundParameters.ContainsKey('ConfigurationMode')) { [void]$sb.AppendLine("ConfigurationMode = '$ConfigurationMode'") }
        if ($PSBoundParameters.ContainsKey('RefreshFrequencyMins')) { [void]$sb.AppendLine("RefreshFrequencyMins = $RefreshFrequencyMins") }

        [void]$sb.AppendLine('}')
        foreach ($web in $ConfigurationRepositoryWeb)
        {
            [void]$sb.AppendLine("ConfigurationRepositoryWeb '$($web.Name)'")
            [void]$sb.AppendLine('{')
            [void]$sb.AppendLine("ServerURL = 'https://$($web.ServerURL):$($web.Port)/PSDSCPullServer.svc'")
            [void]$sb.AppendLine("RegistrationKey = '$($Web.RegistrationKey)'")
            [void]$sb.AppendLine("ConfigurationNames = @('$($Web.ConfigurationNames)')")
            [void]$sb.AppendLine("AllowUnsecureConnection = `$$($web.AllowUnsecureConnection)")
            [void]$sb.AppendLine('}')
        }
        [void]$sb.AppendLine('}')

        [void]$sb.AppendLine('{')
        foreach ($web in $ConfigurationRepositoryWeb)
        {
            [void]$sb.AppendLine("ReportServerWeb '$($web.Name)'")
            [void]$sb.AppendLine('{')
            [void]$sb.AppendLine("ServerURL = 'https://$($web.ServerURL):$($web.Port)/PSDSCPullServer.svc'")
            [void]$sb.AppendLine("RegistrationKey = '$($Web.RegistrationKey)'")
            [void]$sb.AppendLine("AllowUnsecureConnection = `$$($web.AllowUnsecureConnection)")
            [void]$sb.AppendLine('}')
        }
        [void]$sb.AppendLine('}')

        [void]$sb.AppendLine('}')

        Invoke-Expression $sb.ToString()
        $sb.ToString() | Out-File -FilePath c:\AL_DscLcm_Debug.txt

        $path = New-Item -ItemType Directory -Path "$([System.IO.Path]::GetTempPath())\$(New-Guid)"

        LcmConfiguration -OutputPath $path.FullName | Out-Null
        Set-DscLocalConfigurationManager -Path $path.FullName

        Remove-Item -Path $path.FullName -Recurse -Force

        try
        {
            Test-DscConfiguration -ErrorAction Stop | Out-Null
            Write-Host 'DSC Local Configuration Manger was set to the new values'
        }
        catch
        {
            Write-Error 'There was a problem resetting the Local Configuration Manger configuration'
        }
    }

    $lab = Get-Lab
    if (-not $lab.Machines)
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -ComputerName $ComputerName
    if ($machines.Count -ne $ComputerName.Count)
    {
        Write-Error -Message 'Not all machines specified could be found in the lab.'
        Write-LogFunctionExit
        return
    }

    $params = ([hashtable]$PSBoundParameters).Clone()
    Invoke-LabCommand -ActivityName 'Setting DSC LCM configuration' -ComputerName $ComputerName -ScriptBlock {
        Set-DscLocalConfigurationManagerConfiguration @params
    } -Function (Get-Command -Name Set-DscLocalConfigurationManagerConfiguration) -Variable (Get-Variable -Name params)

    Write-LogFunctionExit
}


function Install-LabDynamics
{
    [CmdletBinding()]
    param
    (
        [switch]
        $CreateCheckPoints
    )

    Write-LogFunctionEntry

    $lab = Get-Lab -ErrorAction Stop
    $vms = Get-LabVm -Role Dynamics
    $sql = Get-LabVm -Role SQLServer2016, SQLServer2017 | Sort-Object { $_.Roles.Name } | Select-Object -Last 1
    Start-LabVM -ComputerName $vms -Wait

    Invoke-LabCommand -ComputerName $vms -ScriptBlock {
        if (-not (Test-Path C:\DeployDebug))
        {
            $null = New-Item -ItemType Directory -Path C:\DeployDebug
        }
        if (-not (Test-Path C:\DynamicsSetup))
        {
            $null = New-Item -ItemType Directory -Path C:\DynamicsSetup
        }
    } -NoDisplay
    $someDc = Get-LabVm -Role RootDc | Select -First 1
    $defaultDomain = Invoke-LabCommand -ComputerName $someDc -ScriptBlock { Get-ADDomain } -PassThru -NoDisplay

    # Download prerequisites (which are surprisingly old...)
    Write-ScreenInfo -Message "Downloading and installing prerequisites on $($vms.Count) machines"
    $downloadTargetFolder = "$labSources\SoftwarePackages"
    $dynamicsUri = Get-LabConfigurationItem -Name Dynamics365Uri
    $cppRedist64_2013 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist64_2013) -Path $downloadTargetFolder -FileName vcredist_x64_2013.exe -PassThru -NoDisplay
    $cppRedist64_2010 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist64_2010) -Path $downloadTargetFolder -FileName vcredist_x64_2010.exe -PassThru -NoDisplay
    $odbc = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name SqlOdbc13) -Path $downloadTargetFolder -FileName odbc2013.msi -PassThru -NoDisplay
    $sqlServerNativeClient2012 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name SqlServerNativeClient2012) -Path $downloadTargetFolder -FileName sqlncli2012.msi -PassThru -NoDisplay
    $sqlClrType = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name SqlClrType2016) -Path $downloadTargetFolder -FileName sqlclrtype2016.msi -PassThru -NoDisplay
    $sqlSmo = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name SqlSmo2016) -Path $downloadTargetFolder -FileName sqlsmo2016.msi -PassThru -NoDisplay
    $installer = Get-LabInternetFile -Uri $dynamicsUri -Path $labSources/SoftwarePackages -PassThru -NoDisplay
    Install-LabSoftwarePackage -ComputerName $vms -Path $installer.FullName -CommandLine '/extract:C:\DynamicsSetup /quiet' -NoDisplay
    Install-LabSoftwarePackage -Path  $cppRedist64_2010.FullName -Computer $vms -CommandLine '/quiet' -NoDisplay
    Install-LabSoftwarePackage -Path  $cppRedist64_2013.FullName -Computer $vms -CommandLine '/s' -NoDisplay
    Install-LabSoftwarePackage -Path $odbc.FullName -ComputerName $vms -CommandLine '/QN ADDLOCAL=ALL IACCEPTMSODBCSQLLICENSETERMS=YES /L*v C:\odbc.log' -NoDisplay
    Install-LabSoftwarePackage -Path $sqlServerNativeClient2012.FullName -ComputerName $vms -CommandLine '/QN IACCEPTSQLNCLILICENSETERMS=YES' -NoDisplay
    Install-LabSoftwarePackage -Path $sqlClrType.FullName -ComputerName $vms -NoDisplay
    Install-LabSoftwarePackage -Path $sqlSmo.FullName -ComputerName $vms -NoDisplay
    
    [xml]$defaultXml = @"
    <CRMSetup>
    <Server>
    <Patch update="false" />
    <LicenseKey>KKNV2-4YYK8-D8HWD-GDRMW-29YTW</LicenseKey>
    <SqlServer>$sql</SqlServer>
    <Database create="true"/>
    <Reporting URL="http://$sql/ReportServer"/>
    <OrganizationCollation>Latin1_General_CI_AI</OrganizationCollation>
    <basecurrency isocurrencycode="USD" currencyname="US Dollar" currencysymbol="$" currencyprecision="2"/>
    <Organization>AutomatedLab</Organization>
    <OrganizationUniqueName>automatedlab</OrganizationUniqueName>
    <WebsiteUrl create="true" port="5555"> </WebsiteUrl>
    <InstallDir>c:\Program Files\Microsoft Dynamics CRM</InstallDir>
    <CrmServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMAppService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </CrmServiceAccount>
    <SandboxServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMSandboxService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </SandboxServiceAccount>
    <DeploymentServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMDeploymentService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </DeploymentServiceAccount>
    <AsyncServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMAsyncService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </AsyncServiceAccount>
    <VSSWriterServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMVSSWriterService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </VSSWriterServiceAccount>
    <MonitoringServiceAccount type="DomainUser">
      <ServiceAccountLogin>$($defaultDomain.Name)\CRMMonitoringService</ServiceAccountLogin>
      <ServiceAccountPassword>$($lab.DefaultInstallationCredential.Password)</ServiceAccountPassword>
    </MonitoringServiceAccount>
      
      <SQM optin="false"/>
     <muoptin optin="false"/>
      
     <Groups AutoGroupManagementOff="false">
     <PrivUserGroup>CN=PrivUserGroup,OU=CRM,$($defaultDomain.DistinguishedName)</PrivUserGroup>
     <SQLAccessGroup>CN=SQLAccessGroup,OU=CRM,$($defaultDomain.DistinguishedName)</SQLAccessGroup>
     <ReportingGroup>CN=ReportingGroup,OU=CRM,$($defaultDomain.DistinguishedName)</ReportingGroup>
     <PrivReportingGroup>CN=PrivReportingGroup,OU=CRM,$($defaultDomain.DistinguishedName)</PrivReportingGroup>
    </Groups>
     </Server>
    </CRMSetup>
"@

    [xml]$frontendRole = @"
<RoleConfig>
<Roles>
    <Role Name="WebApplicationServer" />
    <Role Name="OrganizationWebService" />
    <Role Name="DiscoveryWebService" />
    <Role Name="HelpServer" />
</Roles>
</RoleConfig>
"@

    [xml]$backendRole = @"
    <RoleConfig>
<Roles>
    <Role Name="AsynchronousProcessingService" />
    <Role Name="EmailConnector" />
    <Role Name="SandboxProcessingService" />
</Roles>
</RoleConfig>
"@

    [xml]$adminRole = @"
    <RoleConfig>
<Roles>
    <Role Name="DeploymentTools" />
    <Role Name="DeploymentWebService" />
    <Role Name="VSSWriter" />
</Roles>
</RoleConfig>
"@


    Write-ScreenInfo -Message "Installing Dynamics 365 CRM on $($vms.Count) machines"
    $orgFirstDeployed = @{ }

    foreach ($vm in $vms)
    {
        $role = $vm.Roles | Where-Object { $_.Name -band [AutomatedLab.Roles]::Dynamics }
        $serverXml = $defaultXml.Clone()

        foreach ($property in $role.Properties.Keys)
        {
            switch ($property.Key)
            {
                'SqlServer'
                { 
                    $sql = Get-LabVm -ComputerName $property.Value
                    $serverXml.CRMSetup.Server.SqlServer = $property.Value 
                }
                'ReportingUrl' { $serverXml.CRMSetup.Server.Reporting.URL = $property.Value }
                'OrganizationCollation' { $serverXml.CRMSetup.Server.OrganizationCollation = $property.Value }
                'IsoCurrencyCode' { $serverXml.CRMSetup.Server.basecurrency.isocurrencycode = $property.Value }
                'CurrencyName' { $serverXml.CRMSetup.Server.currencyname.isocurrencycode = $property.Value }
                'CurrencySymbol' { $serverXml.CRMSetup.Server.basecurrency.currencysymbol = $property.Value }
                'CurrencyPrecision' { $serverXml.CRMSetup.Server.basecurrency.currencyprecision = $property.Value }
                'Organization' { $serverXml.CRMSetup.Server.Organization = $property.Value }
                'OrganizationUniqueName' { $serverXml.CRMSetup.Server.OrganizationUniqueName = $property.Value }
                'CrmServiceAccount' { $serverXml.CRMSetup.Server.CrmServiceAccount.ServiceAccountLogin = $property.Value }
                'SandboxServiceAccount' { $serverXml.CRMSetup.Server.SandboxServiceAccount.ServiceAccountLogin = $property.Value }
                'DeploymentServiceAccount' { $serverXml.CRMSetup.Server.DeploymentServiceAccount.ServiceAccountLogin = $property.Value }
                'AsyncServiceAccount' { $serverXml.CRMSetup.Server.AsyncServiceAccount.ServiceAccountLogin = $property.Value }
                'VSSWriterServiceAccount' { $serverXml.CRMSetup.Server.VSSWriterServiceAccount.ServiceAccountLogin = $property.Value }
                'MonitoringServiceAccount' { $serverXml.CRMSetup.Server.MonitoringServiceAccount.ServiceAccountLogin = $property.Value }
                'CrmServiceAccountPassword' { $serverXml.CRMSetup.Server.CrmServiceAccount.ServiceAccountPassword = $property.Value }
                'SandboxServiceAccountPassword' { $serverXml.CRMSetup.Server.SandboxServiceAccount.ServiceAccountPassword = $property.Value }
                'DeploymentServiceAccountPassword' { $serverXml.CRMSetup.Server.DeploymentServiceAccount.ServiceAccountPassword = $property.Value }
                'AsyncServiceAccountPassword' { $serverXml.CRMSetup.Server.AsyncServiceAccount.ServiceAccountPassword = $property.Value }
                'VSSWriterServiceAccountPassword' { $serverXml.CRMSetup.Server.VSSWriterServiceAccount.ServiceAccountPassword = $property.Value }
                'MonitoringServiceAccountPassword' { $serverXml.CRMSetup.Server.MonitoringServiceAccount.ServiceAccountPassword = $property.Value }
                'IncomingExchangeServer'
                {
                    $node = $serverXml.CreateElement('Email')
                    $incoming = $serverXml.CreateElement('IncomingExchangeServer')
                    $attr = $serverXml.CreateAttribute('name')
                    $attr.InnerText = $property.Value
                    $null = $incoming.Attributes.Append($attr)
                    $null = $node.AppendChild($incoming)
                    $null = $serverXml.CRMSetup.Server.AppendChild($node)
                }
                'PrivUserGroup' { $serverXml.CRMSetup.Server.Groups.PrivUserGroup = $property.Value }
                'SQLAccessGroup' { $serverXml.CRMSetup.Server.Groups.SQLAccessGroup = $property.Value }
                'ReportingGroup' { $serverXml.CRMSetup.Server.Groups.ReportingGroup = $property.Value }
                'PrivReportingGroup' { $serverXml.CRMSetup.Server.Groups.PrivReportingGroup = $property.Value }
                'LicenseKey'
                {
                    $node = $serverXml.CreateElement('LicenseKey')
                    $node.InnerText = $property.Value
                    $null = $serverXml.CRMSetup.Server.AppendChild($node)
                }
            }
        }

        if ($orgFirstDeployed.Contains($serverXml.CRMSetup.Server.OrganizationUniqueName))
        {
            $serverXml.CRMSetup.Server.Database.create = 'False'
        }

        if (-not $orgFirstDeployed.Contains($serverXml.CRMSetup.Server.OrganizationUniqueName))
        {
            $orgFirstDeployed[$serverXml.CRMSetup.Server.OrganizationUniqueName] = $vm.Name
        }

        if ($role.Name -eq [AutomatedLab.Roles]::DynamicsFrontend)
        {
            $lab.AzureSettings.LoadBalancerPortCounter++
            $remotePort = $lab.AzureSettings.LoadBalancerPortCounter
            Write-ScreenInfo -Message ('Connection to dynamics frontend via http://{0}:{1}' -f $vm.AzureConnectionInfo.DnsName, $remotePort)
            Add-LWAzureLoadBalancedPort -ComputerName $vm -DestinationPort $serverXml.CRMSetup.Server.WebsiteUrl.port -Port $remotePort
            $node = $serverXml.ImportNode($frontendRole.RoleConfig, $true)
            $null = $serverXml.CRMSetup.Server.AppendChild($node.Roles)
        }
        if ($role.Name -eq [AutomatedLab.Roles]::DynamicsBackend)
        {
            $node = $serverXml.ImportNode($backendRole.RoleConfig, $true)
            $null = $serverXml.CRMSetup.Server.AppendChild($node.Roles)
        }
        if ($role.Name -eq [AutomatedLab.Roles]::DynamicsAdmin)
        {
            $node = $serverXml.ImportNode($adminRole.RoleConfig, $true)
            $null = $serverXml.CRMSetup.Server.AppendChild($node.Roles)
        }

        # Begin AD Prep
        [hashtable[]] $users = foreach ($node in $serverXml.SelectNodes('/CRMSetup/Server/*[contains(name(), "Account")]'))
        {
            @{
                Name            = $node.ServiceAccountLogin -replace '.*\\'
                AccountPassword = ConvertTo-SecureString -String $node.ServiceAccountPassword -AsPlainText -Force
                Enabled         = $true
                ErrorAction     = 'Stop'
            }
        }

        [hashtable[]] $groups = foreach ($node in $serverXml.SelectNodes('/CRMSetup/Server/Groups/*[contains(name(), "Group")]'))
        {
            $null = $node.InnerText -match 'CN=(\w+),OU'
            @{
                Name          = $Matches.1
                GroupScope    = 'DomainLocal'
                GroupCategory = 'Security'
                Path          = $node.InnerText -replace 'CN=\w+,'
                ErrorAction   = 'Stop'
            }
        }

        $ous = $groups.Path | Sort-Object -Unique

        $sqlRole = $sql.Roles | Where-Object { $_.Name -band [AutomatedLab.Roles]::SQLServer }

        $memberships = @{
            $serverXml.CRMSetup.Server.Groups.PrivUserGroup      = @(
                $serverXml.CRMSetup.Server.CrmServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.DeploymentServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.AsyncServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.VSSWriterServiceAccount.ServiceAccountLogin
                ($sqlRole.Properties.GetEnumerator | Where-Object Key -like *Account).Value
            )
            $serverXml.CRMSetup.Server.Groups.SQLAccessGroup     = @(
                $serverXml.CRMSetup.Server.CrmServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.DeploymentServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.AsyncServiceAccount.ServiceAccountLogin
                $serverXml.CRMSetup.Server.VSSWriterServiceAccount.ServiceAccountLogin
                ($sqlRole.Properties.GetEnumerator | Where-Object Key -like *Account).Value
            )
            $serverXml.CRMSetup.Server.Groups.ReportingGroup     = @(
                $lab.DefaultInstallationCredential.UserName
            )
            $serverXml.CRMSetup.Server.Groups.PrivReportingGroup = @(
                ($sqlRole.Properties.GetEnumerator | Where-Object Key -like *Account).Value
            )
        }

        Invoke-LabCommand -ActivityName 'Enabling SQL Server Agent' -ComputerName $sql -ScriptBlock {
            Get-Service -Name *SQLSERVERAGENT* | Set-Service -StartupType Automatic -Status Running
        } -NoDisplay

        Invoke-LabCommand -ActivityName 'Preparing accounts, groups and OUs' -ComputerName $someDc -ScriptBlock {
            foreach ($ou in $ous)
            {
                $null = $ou -match '^OU=(?<Name>\w+),'
                $ouName = $Matches.Name
                $path = $ou -replace '^OU=(\w+),'
                try
                {
                    New-ADOrganizationalUnit -Name $ouName -Path $path -ErrorAction Stop
                }
                catch {}
            }

            foreach ($user in $users)
            {
                try
                {
                    New-ADUser @user
                }
                catch {}
            }

            foreach ($group in $groups)
            {
                try
                {
                    New-ADGroup @group
                }
                catch {}
            }

            foreach ($membership in $memberships.GetEnumerator())
            {
                if (-not $membership.Value) { continue }
                Add-ADGroupMember -Identity $membership.Key -Members ($membership.Value -replace '.*\\' | Where-Object { $_ })
            }
        } -Variable (Get-Variable groups, ous, users, memberships) -NoDisplay

        Invoke-LabCommand -ComputerName $vm -ScriptBlock {
            # Using SID instead of name 'Performance Log Users' to avoid possible translation issues
            Add-LocalGroupMember -SID 'S-1-5-32-559' -Member $serverxml.crmsetup.Server.AsyncServiceAccount.ServiceAccountLogin, $serverxml.crmsetup.Server.CrmServiceAccount.ServiceAccountLogin
            $serverXml.Save('C:\DeployDebug\Dynamics.xml')
        } -Variable (Get-Variable serverXml) -NoDisplay
    }

    Restart-LabVM -ComputerName $vms -Wait -NoDisplay

    $timeout = if ($lab.DefaultVirtualizationEngine -eq 'Azure') { 60 } else { 45 }
    Install-LabSoftwarePackage -ComputerName $orgFirstDeployed.Values -LocalPath 'C:\DynamicsSetup\SetupServer.exe' -CommandLine '/config C:\DeployDebug\Dynamics.xml /log C:\DeployDebug\DynamicsSetup.log /Q' -ExpectedReturnCodes 0, 3010 -NoDisplay -UseShellExecute -AsScheduledJob -UseExplicitCredentialsForScheduledJob -Timeout $timeout

    $remainingVms = $vms | Where-Object -Property Name -notin $orgFirstDeployed.Values
    if ($remainingVms)
    {
        Install-LabSoftwarePackage -ComputerName $remainingVms -LocalPath 'C:\DynamicsSetup\SetupServer.exe' -CommandLine '/config C:\DeployDebug\Dynamics.xml /log C:\DeployDebug\DynamicsSetup.log /Q' -ExpectedReturnCodes 0, 3010 -NoDisplay -UseShellExecute -AsScheduledJob -UseExplicitCredentialsForScheduledJob -Timeout $timeout
    }

    if ($CreateCheckPoints.IsPresent)
    {
        Checkpoint-LabVM -ComputerName $vms -SnapshotName AfterDynamicsInstall
    }

    Write-LogFunctionExit
}


function Install-LabFailoverCluster
{
    [CmdletBinding()]
    param ( )

    $failoverNodes = Get-LabVm -Role FailoverNode -ErrorAction SilentlyContinue
    $clusters = $failoverNodes | Group-Object { ($_.Roles | Where-Object -Property Name -eq 'FailoverNode').Properties['ClusterName'] }
    $useDiskWitness = $false
    Start-LabVM -Wait -ComputerName $failoverNodes

    Install-LabWindowsFeature -ComputerName $failoverNodes -FeatureName Failover-Clustering, RSAT-Clustering -IncludeAllSubFeature

    Write-ScreenInfo -Message 'Restart post FCI Install'
    Restart-LabVM $failoverNodes -Wait

    if (Get-LabWindowsFeature -ComputerName $failoverNodes -FeatureName Failover-Clustering, RSAT-Clustering | Where InstallState -ne Installed)
    {
        Install-LabWindowsFeature -ComputerName $failoverNodes -FeatureName Failover-Clustering, RSAT-Clustering -IncludeAllSubFeature
        Write-ScreenInfo -Message 'Restart post FCI Install'
        Restart-LabVM $failoverNodes -Wait
    }

    if (Get-LabVm -Role FailoverStorage)
    {
        Write-ScreenInfo -Message 'Waiting for failover storage server to complete installation'
        Install-LabFailoverStorage
        $useDiskWitness = $true
    }

    Write-ScreenInfo -Message 'Waiting for failover nodes to complete installation'

    foreach ($cluster in $clusters)
    {
        $firstNode = $cluster.Group | Select-Object -First 1
        $clusterDomains = $cluster.Group.DomainName | Sort-Object -Unique
        $clusterNodeNames = $cluster.Group | Select-Object -Skip 1 -ExpandProperty Name
        $clusterName = $cluster.Name
        $clusterIp = ($firstNode.Roles | Where-Object -Property Name -eq 'FailoverNode').Properties['ClusterIp'] -split '\s*(?:,|;?),\s*'

        if (-not $clusterIp)
        {
            $adapterVirtualNetwork = Get-LabVirtualNetworkDefinition -Name $firstNode.NetworkAdapters[0].VirtualSwitch
            $clusterIp = $adapterVirtualNetwork.NextIpAddress().AddressAsString
        }

        if (-not $clusterName)
        {
            $clusterName = 'ALCluster'
        }

        $ignoreNetwork = foreach ($network in (Get-Lab).VirtualNetworks)
        {
            $range = Get-NetworkRange -IPAddress $network.AddressSpace.Network.AddressAsString -SubnetMask $network.AddressSpace.Cidr
            $inRange = $clusterIp | Where-Object {$_ -in $range}
            
            if (-not $inRange)
            {
                '{0}/{1}' -f $network.AddressSpace.Network.AddressAsString, $network.AddressSpace.Cidr
            }
        }

        if ($useDiskWitness -and -not ($firstNode.OperatingSystem.Version -lt 6.2))
        {
            Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Preparing cluster storage' -ScriptBlock {
                if (-not (Get-ClusterAvailableDisk -ErrorAction SilentlyContinue))
                {
                    $offlineDisk = Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Select-Object -First 1
                    if ($offlineDisk)
                    {
                        $offlineDisk | Set-Disk -IsOffline $false
                        $offlineDisk | Set-Disk -IsReadOnly $false
                    }

                    if (-not ($offlineDisk | Get-Partition | Get-Volume))
                    {
                        $offlineDisk | New-Volume -FriendlyName quorum -FileSystem NTFS
                    }
                }
            }

            Invoke-LabCommand -ComputerName $clusterNodeNames -ActivityName 'Preparing cluster storage on remaining nodes' -ScriptBlock {
                Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Set-Disk -IsOffline $false
            }
        }

        $storageNode = Get-LabVm -Role FailoverStorage -ErrorAction SilentlyContinue
        $role = $storageNode.Roles | Where-Object Name -eq FailoverStorage

        if((-not $useDiskWitness) -or ($storageNode.Disks.Count -gt 1))
        {
            Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Preparing cluster storage' -ScriptBlock {
                $diskpartCmd = 'LIST DISK'

                $disks = $diskpartCmd | diskpart.exe

                foreach ($line in $disks)
                {
                    if ($line -match 'Disk (?<DiskNumber>\d) \s+(Offline)\s+(?<Size>\d+) GB\s+(?<Free>\d+) GB')
                    {
                        $nextDriveLetter = if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                        {
                            [char[]](67..90) |
                            Where-Object { (Get-CimInstance -Class Win32_LogicalDisk |
                            Select-Object -ExpandProperty DeviceID) -notcontains "$($_):"} |
                            Select-Object -First 1
                        }
                        else
                        {
                            [char[]](67..90) |
                            Where-Object { (Get-WmiObject -Class Win32_LogicalDisk |
                            Select-Object -ExpandProperty DeviceID) -notcontains "$($_):"} |
                            Select-Object -First 1
                        }

                        $diskNumber = $Matches.DiskNumber

                        $diskpartCmd = "@
                            SELECT DISK $diskNumber
                            ATTRIBUTES DISK CLEAR READONLY
                            ONLINE DISK
                            CREATE PARTITION PRIMARY
                            ASSIGN LETTER=$nextDriveLetter
                            EXIT
                        @"

                        $diskpartCmd | diskpart.exe | Out-Null

                        Start-Sleep -Seconds 2

                        cmd.exe /c "echo y | format $($nextDriveLetter): /q /v:DataDisk$diskNumber"
                    }
                }
            }

            Invoke-LabCommand -ComputerName $clusterNodeNames -ActivityName 'Preparing cluster storage' -ScriptBlock {
                $diskpartCmd = 'LIST DISK'

                $disks = $diskpartCmd | diskpart.exe

                foreach ($line in $disks)
                {
                    if ($line -match 'Disk (?<DiskNumber>\d) \s+(Offline)\s+(?<Size>\d+) GB\s+(?<Free>\d+) GB')
                    {
                        $diskNumber = $Matches.DiskNumber

                        $diskpartCmd = "@
                            SELECT DISK $diskNumber
                            ATTRIBUTES DISK CLEAR READONLY
                            ONLINE DISK
                            EXIT
                        @"

                        $diskpartCmd | diskpart.exe | Out-Null
                    }
                }
            }
        }


        $clusterAccessPoint = if ($clusterDomains.Count -ne 1)
        {
            'DNS'
        }
        else
        {
            'ActiveDirectoryAndDns'
        }

        Remove-LabPSSession -ComputerName $failoverNodes
        Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Enabling clustering on first node' -ScriptBlock {
            Import-Module FailoverClusters -ErrorAction Stop -WarningAction SilentlyContinue

            $clusterParameters = @{
                Name                      = $clusterName
                StaticAddress             = $clusterIp
                AdministrativeAccessPoint = $clusterAccessPoint
                ErrorAction               = 'SilentlyContinue'
                WarningAction             = 'SilentlyContinue'
            }

            if ($ignoreNetwork)
            {
                $clusterParameters.IgnoreNetwork = $ignoreNetwork
            }

            $clusterParameters = Sync-Parameter -Command (Get-Command New-Cluster) -Parameters $clusterParameters

            $null = New-Cluster @clusterParameters
        } -Variable (Get-Variable clusterName, clusterNodeNames, clusterIp, useDiskWitness, clusterAccessPoint, ignoreNetwork) -Function (Get-Command Sync-Parameter)

        Remove-LabPSSession -ComputerName $failoverNodes
        Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Adding nodes' -ScriptBlock {
            Import-Module FailoverClusters -ErrorAction Stop -WarningAction SilentlyContinue

            if (-not (Get-Cluster -Name $clusterName -ErrorAction SilentlyContinue))
            {
                Write-Error "Cluster $clusterName was not deployed"
            }

            foreach ($node in $clusterNodeNames)
            {
                Add-ClusterNode -Name $node -Cluster $clusterName -ErrorAction SilentlyContinue
            }

            if (Compare-Object -ReferenceObject $clusterNodeNames -DifferenceObject (Get-ClusterNode -Cluster $clusterName).Name | Where-Object SideIndicator -eq '<=')
            {
                Write-Error -Message "Error deploying cluster $clusterName, not all nodes were added to the cluster"
            }

            if ($useDiskWitness)
            {
                $clusterDisk = Get-ClusterResource -Cluster $clusterName -ErrorAction SilentlyContinue | Where-object -Property ResourceType -eq 'Physical Disk' | Select -First 1

                if ($clusterDisk)
                {
                    Get-Cluster -Name $clusterName | Set-ClusterQuorum -DiskWitness $clusterDisk
                }
            }
        } -Variable (Get-Variable clusterName, clusterNodeNames, clusterIp, useDiskWitness, clusterAccessPoint, ignoreNetwork)
    }
}


function Connect-Lab
{
    [CmdletBinding(DefaultParameterSetName = 'Lab2Lab')]
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $SourceLab,

        [Parameter(Mandatory = $true, ParameterSetName = 'Lab2Lab', Position = 1)]
        [System.String]
        $DestinationLab,

        [Parameter(Mandatory = $true, ParameterSetName = 'Site2Site', Position = 1)]
        [System.String]
        $DestinationIpAddress,

        [Parameter(Mandatory = $true, ParameterSetName = 'Site2Site', Position = 2)]
        [System.String]
        $PreSharedKey,

        [Parameter(ParameterSetName = 'Site2Site', Position = 3)]
        [System.String[]]
        $AddressSpace,

        [Parameter(Mandatory = $false)]
        [System.String]
        $NetworkAdapterName = 'Ethernet'
    )

    Write-LogFunctionEntry

    if ((Get-Lab -List) -notcontains $SourceLab)
    {
        throw "Source lab $SourceLab does not exist."
    }

    if ($DestinationIpAddress)
    {
        Write-PSFMessage -Message ('Connecting {0} to {1}' -f $SourceLab, $DestinationIpAddress)
        Connect-OnPremisesWithEndpoint -LabName $SourceLab -IPAddress $DestinationIpAddress -AddressSpace $AddressSpace -Psk $PreSharedKey
        return
    }

    if ((Get-Lab -List) -notcontains $DestinationLab)
    {
        throw "Destination lab $DestinationLab does not exist."
    }

    $sourceFolder ="$((Get-LabConfigurationItem -Name LabAppDataRoot))\Labs\$SourceLab"
    $sourceFile = Join-Path -Path $sourceFolder -ChildPath Lab.xml -Resolve -ErrorAction SilentlyContinue
    if (-not $sourceFile)
    {
        throw "Lab.xml is missing for $SourceLab"
    }

    $destinationFolder = "$((Get-LabConfigurationItem -Name LabAppDataRoot))\Labs\$DestinationLab"
    $destinationFile = Join-Path -Path $destinationFolder -ChildPath Lab.xml -Resolve -ErrorAction SilentlyContinue
    if (-not $destinationFile)
    {
        throw "Lab.xml is missing for $DestinationLab"
    }

    $sourceHypervisor = ([xml](Get-Content $sourceFile)).Lab.DefaultVirtualizationEngine
    $sourceRoutedAddressSpaces = ([xml](Get-Content $sourceFile)).Lab.VirtualNetworks.VirtualNetwork.AddressSpace | ForEach-Object {
        if (-not [System.String]::IsNullOrWhiteSpace($_.IpAddress.AddressAsString))
        {
            "$($_.IpAddress.AddressAsString)/$($_.SerializationCidr)"
        }
    }

    $destinationHypervisor = ([xml](Get-Content $destinationFile)).Lab.DefaultVirtualizationEngine
    $destinationRoutedAddressSpaces = ([xml](Get-Content $destinationFile)).Lab.VirtualNetworks.VirtualNetwork.AddressSpace | ForEach-Object {
        if (-not [System.String]::IsNullOrWhiteSpace($_.IpAddress.AddressAsString))
        {
            "$($_.IpAddress.AddressAsString)/$($_.SerializationCidr)"
        }
    }

    Write-PSFMessage -Message ('Source Hypervisor: {0}, Destination Hypervisor: {1}' -f $sourceHypervisor, $destinationHypervisor)

    if (-not ($sourceHypervisor -eq 'Azure' -or $destinationHypervisor -eq 'Azure'))
    {
        throw 'On-premises to on-premises connections are currently not implemented. One or both labs need to be Azure'
    }

    if ($sourceHypervisor -eq 'Azure')
    {
        $connectionParameters = @{
            SourceLab           = $SourceLab
            DestinationLab      = $DestinationLab
            AzureAddressSpaces  = $sourceRoutedAddressSpaces
            OnPremAddressSpaces = $destinationRoutedAddressSpaces
        }
    }
    else
    {
        $connectionParameters = @{
            SourceLab           = $DestinationLab
            DestinationLab      = $SourceLab
            AzureAddressSpaces  = $destinationRoutedAddressSpaces
            OnPremAddressSpaces = $sourceRoutedAddressSpaces
        }
    }

    if ($sourceHypervisor -eq 'Azure' -and $destinationHypervisor -eq 'Azure')
    {
        Write-PSFMessage -Message ('Connecting Azure lab {0} to Azure lab {1}' -f $SourceLab, $DestinationLab)
        Connect-AzureLab -SourceLab $SourceLab -DestinationLab $DestinationLab
        return
    }

    Write-PSFMessage -Message ('Connecting on-premises lab to Azure lab. Source: {0} <-> Destination {1}' -f $SourceLab, $DestinationLab)
    Connect-OnPremisesWithAzure @connectionParameters

    Write-LogFunctionExit
}


function Disconnect-Lab
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        $SourceLab,

        [Parameter(Mandatory)]
        $DestinationLab
    )

    Write-LogFunctionEntry

    foreach ($LabName in @($SourceLab, $DestinationLab))
    {
        Import-Lab -Name $LabName -ErrorAction Stop -NoValidation
        $lab = Get-Lab

        Invoke-LabCommand -ActivityName 'Remove conditional forwarders' -ComputerName (Get-LabVM -Role RootDC) -ScriptBlock {
            Get-DnsServerZone | Where-Object -Property ZoneType -EQ Forwarder | Remove-DnsServerZone -Force
        }

        if ($lab.DefaultVirtualizationEngine -eq 'Azure')
        {
            $resourceGroupName = (Get-LabAzureDefaultResourceGroup).ResourceGroupName

            Write-PSFMessage -Message ('Removing VPN resources in Azure lab {0}, Resource group {1}' -f $lab.Name, $resourceGroupName)

            $connection = Get-AzVirtualNetworkGatewayConnection -Name s2sconnection -ResourceGroupName $resourceGroupName -ErrorAction SilentlyContinue
            $gw = Get-AzVirtualNetworkGateway -Name s2sgw -ResourceGroupName $resourceGroupName -ErrorAction SilentlyContinue
            $localgw = Get-AzLocalNetworkGateway -Name onpremgw -ResourceGroupName $resourceGroupName -ErrorAction SilentlyContinue
            $ip = Get-AzPublicIpAddress -Name s2sip -ResourceGroupName $resourceGroupName -ErrorAction SilentlyContinue

            if ($connection)
            {
                $connection | Remove-AzVirtualNetworkGatewayConnection -Force
            }

            if ($gw)
            {
                $gw | Remove-AzVirtualNetworkGateway -Force
            }

            if ($ip)
            {
                $ip | Remove-AzPublicIpAddress -Force
            }

            if ($localgw)
            {
                $localgw | Remove-AzLocalNetworkGateway -Force
            }
        }
        else
        {
            $router = Get-LabVm -Role Routing -ErrorAction SilentlyContinue

            if (-not $router)
            {
                # How did this even work...
                continue
            }

            Write-PSFMessage -Message ('Disabling S2SVPN in on-prem lab {0} on router {1}' -f $lab.Name, $router.Name)

            Invoke-LabCommand -ActivityName "Disabling S2S on $($router.Name)" -ComputerName $router -ScriptBlock {
                Get-VpnS2SInterface -Name AzureS2S -ErrorAction SilentlyContinue | Remove-VpnS2SInterface -Force -ErrorAction SilentlyContinue
                Uninstall-RemoteAccess -VpnType VPNS2S -Force -ErrorAction SilentlyContinue
            }
        }
    }

    Write-LogFunctionExit
}


function Restore-LabConnection
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SourceLab,

        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationLab
    )

    if ((Get-Lab -List) -notcontains $SourceLab)
    {
        throw "Source lab $SourceLab does not exist."
    }

    if ((Get-Lab -List) -notcontains $DestinationLab)
    {
        throw "Destination lab $DestinationLab does not exist."
    }

    $sourceFolder = "$((Get-LabConfigurationItem -Name LabAppDataRoot))\Labs\$SourceLab"
    $sourceFile = Join-Path -Path $sourceFolder -ChildPath Lab.xml -Resolve -ErrorAction SilentlyContinue
    if (-not $sourceFile)
    {
        throw "Lab.xml is missing for $SourceLab"
    }

    $destinationFolder = "$((Get-LabConfigurationItem -Name LabAppDataRoot))\Labs\$DestinationLab"
    $destinationFile = Join-Path -Path $destinationFolder -ChildPath Lab.xml -Resolve -ErrorAction SilentlyContinue
    if (-not $destinationFile)
    {
        throw "Lab.xml is missing for $DestinationLab"
    }

    $sourceHypervisor = ([xml](Get-Content $sourceFile)).Lab.DefaultVirtualizationEngine
    $destinationHypervisor = ([xml](Get-Content $destinationFile)).Lab.DefaultVirtualizationEngine

    if ($sourceHypervisor -eq 'Azure')
    {
        $source = $SourceLab
        $destination = $DestinationLab
    }
    else
    {
        $source = $DestinationLab
        $destination = $SourceLab
    }

    Write-PSFMessage -Message "Checking Azure lab $source"
    Import-Lab -Name $source -NoValidation
    $resourceGroup = (Get-LabAzureDefaultResourceGroup).ResourceGroupName

    $localGateway = Get-AzLocalNetworkGateway -Name onpremgw -ResourceGroupName $resourceGroup -ErrorAction Stop
    $vpnGatewayIp = Get-AzPublicIpAddress -Name s2sip -ResourceGroupName $resourceGroup -ErrorAction Stop

    try
    {
        $labIp = Get-PublicIpAddress -ErrorAction Stop
    }
    catch
    {
        Write-ScreenInfo -Message 'Public IP address could not be determined. Reconnect-Lab will probably not work.' -Type Warning
    }

    if ($localGateway.GatewayIpAddress -ne $labIp)
    {
        Write-PSFMessage -Message "Gateway address $($localGateway.GatewayIpAddress) does not match local IP $labIP and will be changed"
        $localGateway.GatewayIpAddress = $labIp
        [void] ($localGateway | Set-AzLocalNetworkGateway)
    }

    Import-Lab -Name $destination -NoValidation
    $router = Get-LabVm -Role Routing

    Invoke-LabCommand -ActivityName 'Checking S2S connection' -ComputerName $router -ScriptBlock {
        param
        (
            [System.String]
            $azureDestination
        )

        $s2sConnection = Get-VpnS2SInterface -Name AzureS2S -ErrorAction Stop -Verbose

        if ($s2sConnection.Destination -notcontains $azureDestination)
        {
            $s2sConnection.Destination += $azureDestination
            $s2sConnection | Set-VpnS2SInterface -Verbose
        }
    } -ArgumentList @($vpnGatewayIp.IpAddress)
}


function Install-LabHyperV
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCmdlets", "", Justification="Not relevant on Linux")]
    [CmdletBinding()]
    param
    ( )

    Write-LogFunctionEntry

    $vms = Get-LabVm -Role HyperV | Where-Object SkipDeployment -eq $false

    Write-ScreenInfo -Message 'Exposing virtualization extensions...' -NoNewLine
    $hyperVVms = $vms | Where-Object -Property HostType -eq HyperV
    if ($hyperVVms)
    {
        $enableVirt = $vms | Where-Object {-not (Get-VmProcessor -VMName $_.ResourceName).ExposeVirtualizationExtensions}
        if ($enableVirt)
        {
            $vmObjects = Get-LWHypervVM -Name $enableVirt.ResourceName -ErrorAction SilentlyContinue
            Stop-LabVm -Wait -ComputerName $enableVirt
            $vmObjects | Set-VMProcessor -ExposeVirtualizationExtensions $true
            $vmObjects | Get-VMNetworkAdapter | Set-VMNetworkAdapter -MacAddressSpoofing On
        }
    }

    Start-LabVm -Wait -ComputerName $vms # Start all, regardless of Hypervisor
    Write-ScreenInfo -Message 'Done'

    # Enable Feature
    Write-ScreenInfo -Message "Enabling Hyper-V feature and waiting for restart of $($vms.Count) VMs..." -NoNewLine

    $clients, $servers = $vms.Where({$_.OperatingSystem.Installation -eq 'Client'}, 'Split')

    $jobs = @()

    if ($clients)
    {
        $jobs += Install-LabWindowsFeature -ComputerName $clients -FeatureName Microsoft-Hyper-V-All -NoDisplay -AsJob -PassThru
    }

    if ($servers)
    {
        $jobs += Install-LabWindowsFeature -ComputerName $servers -FeatureName Hyper-V -IncludeAllSubFeature -IncludeManagementTools -NoDisplay -AsJob -PassThru
    }

    Wait-LWLabJob -Job $jobs

    # Restart
    Restart-LabVm -ComputerName $vms -Wait -NoDisplay
    Write-ScreenInfo -Message 'Done'

    $jobs = foreach ($vm in $vms)
    {
        Invoke-LabCommand -ActivityName 'Configuring VM Host settings' -ComputerName $vm -Variable (Get-Variable -Name vm) -ScriptBlock {
            Import-Module Hyper-V
            # Correct data types for individual settings
            $parametersAndTypes = @{
                MaximumStorageMigrations                  = [uint32]
                MaximumVirtualMachineMigrations           = [uint32]
                VirtualMachineMigrationAuthenticationType = [Microsoft.HyperV.PowerShell.MigrationAuthenticationType]
                UseAnyNetworkForMigration                 = [bool]
                VirtualMachineMigrationPerformanceOption  = [Microsoft.HyperV.PowerShell.VMMigrationPerformance]
                ResourceMeteringSaveInterval              = [timespan]
                NumaSpanningEnabled                       = [bool]
                EnableEnhancedSessionMode                 = [bool]
            }

            [hashtable]$roleParameters = ($vm.Roles | Where-Object Name -eq HyperV).Properties
            if ($roleParameters.Count -eq 0) { continue }
            $parameters = Sync-Parameter -Command (Get-Command Set-VMHost) -Parameters $roleParameters
            
            foreach ($parameter in $parameters.Clone().GetEnumerator())
            {
                $type = $parametersAndTypes[$parameter.Key]

                if ($type -eq [bool])
                {
                    $parameters[$parameter.Key] = [Convert]::ToBoolean($parameter.Value)
                }
                else
                {
                    $parameters[$parameter.Key] = $parameter.Value -as $type
                }
            }

            Set-VMHost @parameters
        } -Function (Get-Command -Name Sync-Parameter) -AsJob -PassThru -IgnoreAzureLabSources
    }

    Wait-LWLabJob -Job $jobs

    Write-LogFunctionExit
}


function Disable-LabVMFirewallGroup
{

    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [Parameter(Mandatory)]
        [string[]]$FirewallGroup
    )

    Write-LogFunctionEntry

    $machine = Get-LabVM -ComputerName $ComputerName

    Invoke-LabCommand -ComputerName $machine -ActivityName 'Disable firewall group' -NoDisplay -ScriptBlock `
    {
        param
        (
            [string]$FirewallGroup
        )

        $FirewallGroups = $FirewallGroup.Split(';')

        foreach ($group in $FirewallGroups)
        {
            Write-Verbose -Message "Disable firewall group '$group' on '$(hostname)'"
            netsh.exe advfirewall firewall set rule group="$group" new enable=No
        }
    } -ArgumentList ($FirewallGroup -join ';')

    Write-LogFunctionExit
}


function Enable-LabVMFirewallGroup
{

    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [Parameter(Mandatory)]
        [string[]]$FirewallGroup
    )

    Write-LogFunctionEntry

    $machine = Get-LabVM -ComputerName $ComputerName

    Invoke-LabCommand -ComputerName $machine -ActivityName 'Enable firewall group' -NoDisplay -ScriptBlock `
    {
        param
        (
            [string]$FirewallGroup
        )

        $FirewallGroups = $FirewallGroup.Split(';')

        foreach ($group in $FirewallGroups)
        {
            Write-Verbose -Message "Enable firewall group '$group' on '$(hostname)'"
            netsh.exe advfirewall firewall set rule group="$group" new enable=Yes
        }
    } -ArgumentList ($FirewallGroup -join ';')

    Write-LogFunctionExit
}


function Get-LabHyperVAvailableMemory
{
    # .ExternalHelp AutomatedLab.Help.xml
    if ($IsLinux -or $IsMacOS)
    {
        return [int]((Get-Content -Path /proc/meminfo) -replace ':', '=' -replace '\skB' | ConvertFrom-StringData).MemTotal
    }

    [int](((Get-CimInstance -Namespace Root\Cimv2 -Class win32_operatingsystem).TotalVisibleMemorySize) / 1kb)
}


function Get-LabInternetFile
{

    param(
        [Parameter(Mandatory = $true)]
        [string]$Uri,

        [Parameter(Mandatory = $true)]
        [string]$Path,

        [string]$FileName,

        [switch]$Force,

        [switch]$NoDisplay,

        [switch]$PassThru
    )

    function Get-LabInternetFileInternal
    {
        param(
            [Parameter(Mandatory = $true)]
            [string]$Uri,

            [Parameter(Mandatory = $true)]
            [string]$Path,

            [string]$FileName,

            [bool]$NoDisplay,

            [bool]$Force
        )

        if (Test-Path -Path $Path -PathType Container)
        {
            $Path = Join-Path -Path $Path -ChildPath $FileName
        }

        if ((Test-Path -Path $Path -PathType Leaf) -and -not $Force)
        {
            Write-Verbose -Message "The file '$Path' does already exist, skipping the download"
        }
        else
        {
            if (-not ($IsLinux -or $IsMacOS) -and -not (Get-NetConnectionProfile -ErrorAction SilentlyContinue | Where-Object { $_.IPv4Connectivity -eq 'Internet' -or $_.IPv6Connectivity -eq 'Internet' }))
            {
                #machine does not have internet connectivity
                if (-not $offlineNode)
                {
                    Write-Error "Machine is not connected to the internet and cannot download the file '$Uri'"
                }
                return
            }

            Write-Verbose "Uri is '$Uri'"
            Write-Verbose "Path is '$Path'"

            try
            {
                try
                {
                    #https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=netcore-2.0#System_Net_SecurityProtocolType_SystemDefault
                    if ($PSVersionTable.PSVersion.Major -lt 6 -and [Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12')
                    {
                        Write-Verbose -Message 'Adding support for TLS 1.2'
                        [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
                    }
                }
                catch
                {
                    Write-Warning -Message 'Adding TLS 1.2 to supported security protocols was unsuccessful.'
                }

                $bytesProcessed = 0
                $request = [System.Net.WebRequest]::Create($Uri)
                $request.AllowAutoRedirect = $true

                if ($request)
                {
                    Write-Verbose 'WebRequest created'
                    $response = $request.GetResponse()
                    if ($response)
                    {
                        Write-Verbose 'Response received'
                        $remoteStream = $response.GetResponseStream()

                        if ([System.IO.Path]::GetPathRoot($Path) -ne $Path)
                        {
                            $parent = Split-Path -Path $Path
                        }
                        if (-not (Test-Path -Path $parent -PathType Container) -and -not ([System.IO.Path]::GetPathRoot($parent) -eq $parent))
                        {
                            New-Item -Path $parent -ItemType Directory -Force | Out-Null
                        }
                        if ((Test-Path -Path $Path -PathType Container) -and -not $FileName)
                        {
                            $FileName = $response.ResponseUri.Segments[-1]
                            $Path = Join-Path -Path $Path -ChildPath $FileName
                        }
                        if ([System.IO.Path]::GetPathRoot($Path) -eq $Path)
                        {
                            Write-Error "The path '$Path' is the drive root and the file name could not be retrived using the given url. Please provide a file name using the 'FileName' parameter."
                            return
                        }
                        if (-not $FileName)
                        {
                            $FileName = Split-Path -Path $Path -Leaf
                        }
                        if ((Test-Path -Path $Path -PathType Leaf) -and -not $Force)
                        {
                            Write-Verbose -Message "The file '$Path' does already exist, skipping the download"
                        }
                        else
                        {
                            $localStream = [System.IO.File]::Create($Path)

                            $buffer = New-Object System.Byte[] 10MB
                            $bytesRead = 0
                            [int]$percentageCompletedPrev = 0

                            do
                            {
                                $bytesRead = $remoteStream.Read($buffer, 0, $buffer.Length)
                                $localStream.Write($buffer, 0, $bytesRead)
                                $bytesProcessed += $bytesRead

                                [int]$percentageCompleted = $bytesProcessed / $response.ContentLength * 100
                                if ($percentageCompleted -gt 0)
                                {
                                    if ($percentageCompletedPrev -ne $percentageCompleted)
                                    {
                                        $percentageCompletedPrev = $percentageCompleted
                                        Write-Progress -Activity "Downloading file '$FileName'" `
                                        -Status ("{0:P} completed, {1:N2}MB of {2:N2}MB" -f ($percentageCompleted / 100), ($bytesProcessed / 1MB), ($response.ContentLength / 1MB)) `
                                        -PercentComplete ($percentageCompleted)
                                    }
                                }
                                else
                                {
                                    Write-Verbose -Message "Could not determine the ContentLength of '$Uri'"
                                }
                            } while ($bytesRead -gt 0)
                        }
                    }

                    $response | Add-Member -Name FileName -MemberType NoteProperty -Value $FileName -PassThru
                }
            }
            catch
            {
                Write-Error -Exception $_.Exception
            }
            finally
            {

                if ($response) { $response.Close() }
                if ($remoteStream) { $remoteStream.Close() }
                if ($localStream) { $localStream.Close() }
            }
        }
    }

    $start = Get-Date

    #TODO: This needs to go into config
    $offlineNode = $true

    if (-not $FileName)
    {
        $internalUri = New-Object System.Uri($Uri)
        $tempFileName = $internalUri.Segments[$internalUri.Segments.Count - 1]
        if (Test-FileName -Path $tempFileName)
        {
            $FileName = $tempFileName
            $PSBoundParameters.FileName = $FileName
        }
    }

    $lab = Get-Lab -ErrorAction SilentlyContinue
    if (-not $lab)
    {
        $lab = Get-LabDefinition -ErrorAction SilentlyContinue
        $doNotGetVm = $true
    }

    if ($lab.DefaultVirtualizationEngine -eq 'Azure')
    {
        if (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $Path)
        {
            # We need to test first, even if it takes a second longer.
            if (-not $doNotGetVm)
            {
                $machine = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $(Get-LabVM -IsRunning) -ScriptBlock {
                    if (Get-NetConnectionProfile -IPv4Connectivity Internet -ErrorAction SilentlyContinue)
                    {
                        hostname
                    }
                } -ErrorAction SilentlyContinue | Select-Object -First 1
                Write-PSFMessage "Target path is on AzureLabSources, invoking the copy job on the first available Azure machine."

                $argumentList = $Uri, $Path, $FileName

                $argumentList += if ($NoDisplay) { $true } else { $false }
                $argumentList += if ($Force) { $true } else { $false }
            }

            if ($machine)
            {
                $result = Invoke-LabCommand -ActivityName "Downloading file from '$Uri'" -NoDisplay:$NoDisplay.IsPresent -ComputerName $machine -ScriptBlock (Get-Command -Name Get-LabInternetFileInternal).ScriptBlock -ArgumentList $argumentList -PassThru
            }
            elseif (Get-LabAzureSubscription -ErrorAction SilentlyContinue)
            {
                $PSBoundParameters.Remove('PassThru') | Out-Null
                $param = Sync-Parameter -Command (Get-Command Get-LabInternetFileInternal) -Parameters $PSBoundParameters
                $param['Path'] = $Path.Replace((Get-LabSourcesLocation), (Get-LabSourcesLocation -Local))
                $result = Get-LabInternetFileInternal @param

                $fullName = Join-Path -Path $param.Path.Replace($FileName, '') -ChildPath (?? { $FileName } { $FileName } { $result.FileName })
                $pathFilter = $fullName.Replace("$(Get-LabSourcesLocation -Local)\", '')
                Sync-LabAzureLabSources -Filter $pathFilter -NoDisplay
            }
            else
            {
                Write-ScreenInfo -Type Erro -Message "Unable to upload file to Azure lab sources - No VM is available and no Azure subscription was added to the lab`r`n
                Please at least execute New-LabDefinition and Add-LabAzureSubscription before using Get-LabInternetFile"

                return
            }
        }
        else
        {
            Write-PSFMessage "Target path is local, invoking the copy job locally."
            $PSBoundParameters.Remove('PassThru') | Out-Null
            $result = Get-LabInternetFileInternal @PSBoundParameters
        }
    }
    else
    {
        Write-PSFMessage "Target path is local, invoking the copy job locally."
        $PSBoundParameters.Remove('PassThru') | Out-Null
        try
        {
            $result = Get-LabInternetFileInternal @PSBoundParameters

            $end = Get-Date
            Write-PSFMessage "Download has taken: $($end - $start)"
        }
        catch
        {
            Write-Error -ErrorRecord $_
        }
    }

    if ($PassThru)
    {
        New-Object PSObject -Property @{
            Uri      = $Uri
            Path     = $Path
            FileName = ?? { $FileName } { $FileName } { $result.FileName }
            FullName = Join-Path -Path $Path -ChildPath (?? { $FileName } { $FileName } { $result.FileName })
            Length   = $result.ContentLength
        }
    }
}


function Get-LabSourcesLocationInternal
{
    param
    (
        [switch]$Local
    )

    $lab = $global:AL_CurrentLab

    $defaultEngine = 'HyperV'
    $defaultEngine = if ($lab)
    {
        $lab.DefaultVirtualizationEngine
    }

    if ($lab.AzureSettings -and $lab.AzureSettings.IsAzureStack)
    {
        $Local = $true
    }

    if ($defaultEngine -eq 'kvm' -or ($IsLinux -and $Local.IsPresent))
    {
        if (-not (Get-PSFConfigValue -FullName AutomatedLab.LabSourcesLocation))
        {
            Set-PSFConfig -Module AutomatedLab -Name LabSourcesLocation -Description 'Location of lab sources folder' -Value $home/automatedlabsources -PassThru | Register-PSFConfig
        }

        Get-PSFConfigValue -FullName AutomatedLab.LabSourcesLocation
    }
    elseif (($defaultEngine -eq 'HyperV' -or $Local) -and (Get-PSFConfigValue AutomatedLab.LabSourcesLocation))
    {
        Get-PSFConfigValue -FullName AutomatedLab.LabSourcesLocation
    }
    elseif ($defaultEngine -eq 'HyperV' -or $Local)
    {
        $hardDrives = (Get-CimInstance -NameSpace Root\CIMv2 -Class Win32_LogicalDisk | Where-Object DriveType -In 2, 3).DeviceID | Sort-Object -Descending

        $folders = foreach ($drive in $hardDrives)
        {
            if (Test-Path -Path "$drive\LabSources")
            {
                "$drive\LabSources"
            }
        }

        if ($folders.Count -gt 1)
        {
            Write-PSFMessage -Level Warning "The LabSources folder is available more than once ('$($folders -join "', '")'). The LabSources folder must exist only on one drive and in the root of the drive."
        }

        $folders
    }
    elseif ($defaultEngine -eq 'Azure')
    {
        try
        {
            (Get-LabAzureLabSourcesStorage -ErrorAction Stop).Path
        }
        catch
        {
            Get-LabSourcesLocationInternal -Local
        }
    }
    else
    {
        Get-LabSourcesLocationInternal -Local
    }
}


function Register-LabArgumentCompleters
{
    $commands = Get-Command -Module AutomatedLab*, PSFileTransfer | Where-Object { $_.Parameters -and $_.Parameters.ContainsKey('ComputerName') }

    Register-PSFTeppArgumentCompleter -Command $commands -Parameter ComputerName -Name 'AutomatedLab-ComputerName'
}


function Remove-LabDeploymentFiles
{

    Invoke-LabCommand -ComputerName (Get-LabVM) -ActivityName 'Remove deployment files (files used during deployment)' -AsJob -NoDisplay -ScriptBlock `
    {
        $paths = 'C:\Unattend.xml',
            'C:\WSManRegKey.reg',
            'C:\AdditionalDisksOnline.ps1',
            'C:\WinRmCustomization.ps1',
            'C:\DeployDebug',
            'C:\ALLibraries',
            "C:\$($env:COMPUTERNAME).cer"
            
        $paths | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
    }
}


function Reset-AutomatedLab
{
    Remove-Lab -Confirm:$false
    Remove-Module *
}


function Restart-ServiceResilient
{

    [cmdletbinding()]
    param
    (
        [string[]]$ComputerName,
        $ServiceName,
        [switch]$NoNewLine
    )

    Write-LogFunctionEntry

    $jobs = Invoke-LabCommand -ComputerName $ComputerName -AsJob -PassThru -NoDisplay -ActivityName "Restart service '$ServiceName' on computers '$($ComputerName -join ', ')'" -ScriptBlock `
    {
        param
        (
            [string]$ServiceName
        )

        function Get-ServiceRestartInfo
        {
            param
            (
                [string]$ServiceName,
                [switch]$WasStopped,
                [switch]$WasStarted,
                [double]$Index
            )

            $serviceDisplayName = (Get-Service $ServiceName).DisplayName

            $newestEvent = "($((Get-EventLog -LogName System -newest 1).Index)) " + (Get-EventLog -LogName System -newest 1).Message
            Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Get-ServiceRestartInfo - ServiceName: $ServiceName ($serviceDisplayName) - WasStopped: $WasStopped - WasStarted:$WasStarted - Index: $Index - Newest event: $newestEvent"


            $result = $true

            if ($WasStopped)
            {
                $events = @(Get-EventLog -LogName System -Index ($Index..($Index + 10000)) | Where-Object { $_.Message -like "*$serviceDisplayName*entered*stopped*" })
                Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Events found: $($events.count)"
                $result = ($events.count -gt 0)
            }
            if ($WasStarted)
            {
                $events = @(Get-EventLog -LogName System -Index ($Index..($Index + 10000)) | Where-Object { $_.Message -like "*$serviceDisplayName*entered*running*" })
                Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Events found: $($events.count)"
                $result = ($events.count -gt 0)
            }

            Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Result:$result"
            $result
        }


        $BackupVerbosePreference = $VerbosePreference
        $BackupDebugPreference = $DebugPreference
        $VerbosePreference = 'Continue'
        $DebugPreference = 'Continue'

        $ServiceName = 'nlasvc'

        $dependentServices = Get-Service -Name $ServiceName -DependentServices | Where-Object { $_.Status -eq 'Running' } | Select-Object -ExpandProperty Name
        Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Dependent services: '$($dependentServices -join ',')'"


        $serviceDisplayName = (Get-Service $ServiceName).DisplayName
        if ((Get-Service -Name "$ServiceName").Status -eq 'Running')
        {
            $newestEventLogIndex = (Get-EventLog -LogName System -Newest 1).Index
            $retries = 5
            do
            {
                Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Trying to stop service '$ServiceName'"
                $EAPbackup = $ErrorActionPreference
                $WAPbackup = $WarningPreference

                $ErrorActionPreference = 'SilentlyContinue'
                $WarningPreference = 'SilentlyContinue'
                Stop-Service -Name $ServiceName -Force
                $ErrorActionPreference = $EAPbackup
                $WarningPreference = $WAPbackup

                $retries--
                Start-Sleep -Seconds 1
            }
            until ((Get-ServiceRestartInfo -ServiceName $ServiceName -WasStopped -Index $newestEventLogIndex) -or $retries -le 0)
        }

        if ($retries -gt 0)
        {
            Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Service '$ServiceName' has been stopped"
        }
        else
        {
            Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Service '$ServiceName' could NOT be stopped"
            return
        }


        if (-not (Get-ServiceRestartInfo -ServiceName $ServiceName -WasStarted -Index $newestEventLogIndex))
        {
            #if service did not start by itself
            $newestEventLogIndex = (Get-EventLog -LogName System -Newest 1).Index
            $retries = 5
            do
            {
                Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Trying to start service '$ServiceName'"
                Start-Service -Name $ServiceName -ErrorAction SilentlyContinue
                $retries--
                if (-not (Get-ServiceRestartInfo -ServiceName $ServiceName -WasStarted -Index $newestEventLogIndex))
                {
                    Start-Sleep -Seconds 1
                }
            }
            until ((Get-ServiceRestartInfo -ServiceName $ServiceName -WasStarted -Index $newestEventLogIndex) -or $retries -le 0)
        }


        if ($retries -gt 0)
        {
            Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Service '$ServiceName' was started"
        }
        else
        {
            Write-Verbose -Message "$(Get-Date -Format 'mm:dd:ss') - Service '$ServiceName' could NOT be started"
            return
        }

        foreach ($dependentService in $dependentServices)
        {
            if (Get-ServiceRestartInfo -ServiceName $dependentService -WasStarted -Index $newestEventLogIndex)
            {
                Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Dependent service '$dependentService' has already auto-started"
            }
            else
            {
                $newestEventLogIndex = (Get-EventLog -LogName System -Newest 1).Index
                $retries = 5
                do
                {
                    Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Trying to start depending service '$dependentService'"
                    Start-Service $dependentService -ErrorAction SilentlyContinue
                    $retries--
                }
                until ((Get-ServiceRestartInfo -ServiceName $ServiceName -WasStarted -Index $newestEventLogIndex) -or $retries -le 0)

                if (Get-ServiceRestartInfo -ServiceName $ServiceName -WasStarted -Index $newestEventLogIndex)
                {
                    Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Dependent service '$ServiceName' was started"
                }
                else
                {
                    Write-Debug -Message "$(Get-Date -Format 'mm:dd:ss') - Dependent service '$ServiceName' could NOT be started"
                }
            }
        }

        $VerbosePreference = $BackupVerbosePreference
        $DebugPreference = $BackupDebugPreference
    } -ArgumentList $ServiceName

    Wait-LWLabJob -Job $jobs -NoDisplay -Timeout 30 -NoNewLine:$NoNewLine

    Write-LogFunctionExit
}


function Unblock-LabSources
{

    param(
        [string]$Path = $global:labSources
    )

    Write-LogFunctionEntry

    $lab = Get-Lab -ErrorAction SilentlyContinue
    if (-not $lab)
    {
        $lab = Get-LabDefinition -ErrorAction SilentlyContinue
    }

    if ($lab.DefaultVirtualizationEngine -eq 'Azure' -and $Path.StartsWith("\\"))
    {
        Write-PSFMessage 'Skipping the unblocking of lab sources since we are on Azure and lab sources are unblocked during Sync-LabAzureLabSources'
        return
    }

    if (-not (Test-Path -Path $Path))
    {
        Write-Error "The path '$Path' could not be found"
        return
    }

    $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, DateTime

    try
    {
        if ($IsLinux -or $IsMacOs)
        {
            $cache = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $cache = $type::ImportFromRegistry('Cache', 'Timestamps')
        }

        Write-PSFMessage 'Imported Cache\Timestamps from registry/file store'
    }
    catch
    {
        $cache = New-Object $type
        Write-PSFMessage 'No entry found in the registry at Cache\Timestamps'
    }

    if (-not $cache['LabSourcesLastUnblock'] -or $cache['LabSourcesLastUnblock'] -lt (Get-Date).AddDays(-1))
    {
        Write-PSFMessage 'Last unblock more than 24 hours ago, unblocking files'
        if (-not ($IsLinux -or $IsMacOs)) { Get-ChildItem -Path $Path -Recurse | Unblock-File }
        $cache['LabSourcesLastUnblock'] = Get-Date
        if ($IsLinux -or $IsMacOs)
        {
            $cache.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $cache.ExportToRegistry('Cache', 'Timestamps')
        }

        Write-PSFMessage 'LabSources folder unblocked and new timestamp written to Cache\Timestamps'
    }
    else
    {
        Write-PSFMessage 'Last unblock less than 24 hours ago, doing nothing'
    }

    Write-LogFunctionExit
}


function Update-LabSysinternalsTools
{
    if ($IsLinux -or $IsMacOs) { return }
    if (Get-LabConfigurationItem -Name SkipSysInternals) {return}
    #Update SysInternals suite if needed
    $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, DateTime

    try
    {
        #https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=netcore-2.0#System_Net_SecurityProtocolType_SystemDefault
        if ($PSVersionTable.PSVersion.Major -lt 6 -and [Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12')
        {
            Write-PSFMessage -Message 'Adding support for TLS 1.2'
            [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
        }
    }
    catch
    {
        Write-PSFMessage -Level Warning -Message 'Adding TLS 1.2 to supported security protocols was unsuccessful.'
    }

    try
    {
        Write-PSFMessage -Message 'Get last check time of SysInternals suite'
        if ($IsLinux -or $IsMacOs)
        {
            $timestamps = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
        }
        else
        {
            $timestamps = $type::ImportFromRegistry('Cache', 'Timestamps')
        }
        $lastChecked = $timestamps.SysInternalsUpdateLastChecked
        Write-PSFMessage -Message "Last check was '$lastChecked'."
    }
    catch
    {
        Write-PSFMessage -Message 'Last check time could not be retrieved. SysInternals suite never updated'
        $lastChecked = Get-Date -Year 1601
        $timestamps = New-Object $type
    }

    if ($lastChecked)
    {
        $lastChecked = $lastChecked.AddDays(7)
    }

    if ((Get-Date) -gt $lastChecked)
    {
        Write-PSFMessage -Message 'Last check time is more then a week ago. Check web site for update.'

        $sysInternalsUrl = Get-LabConfigurationItem -Name SysInternalsUrl
        $sysInternalsDownloadUrl = Get-LabConfigurationItem -Name SysInternalsDownloadUrl

        try
        {
            Write-PSFMessage -Message 'Web page downloaded'
            $webRequest = Invoke-WebRequest -Uri $sysInternalsURL -UseBasicParsing
            $pageDownloaded = $true
        }
        catch
        {
            Write-PSFMessage -Message 'Web page could not be downloaded'
            Write-ScreenInfo -Message "No connection to '$sysInternalsURL'. Skipping." -Type Error
            $pageDownloaded = $false
        }

        if ($pageDownloaded)
        {
            $updateStart = $webRequest.Content.IndexOf('Updated') + 'Updated:'.Length
            $updateFinish = $webRequest.Content.IndexOf('</p>', $updateStart)
            $updateStringFromWebPage = $webRequest.Content.Substring($updateStart, $updateFinish - $updateStart).Trim()

            Write-PSFMessage -Message "Update string from web page: '$updateStringFromWebPage'"

            $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, String
            try
            {
                if ($IsLinux -or $IsMacOs)
                {
                    $versions = $type::Import((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Versions.xml'))
                }
                else
                {
                    $versions = $type::ImportFromRegistry('Cache', 'Versions')
                }
            }
            catch
            {
                $versions = New-Object $type
            }

            Write-PSFMessage -Message "Update string from registry: '$currentVersion'"

            if ($versions['SysInternals'] -ne $updateStringFromWebPage)
            {
                Write-ScreenInfo -Message 'Performing update of SysInternals suite and lab sources directory now' -Type Warning -TaskStart
                Start-Sleep -Seconds 1

                # Download Lab Sources
                $null = New-LabSourcesFolder -Force -ErrorAction SilentlyContinue

                # Download SysInternals suite

                $tempFilePath = [System.IO.Path]::GetTempFileName()
                $tempFilePath = Rename-Item -Path $tempFilePath -NewName ([System.IO.Path]::ChangeExtension($tempFilePath, '.zip')) -PassThru
                Write-PSFMessage -Message "Temp file: '$tempFilePath'"

                try
                {
                    Invoke-WebRequest -Uri $sysInternalsDownloadURL -UseBasicParsing -OutFile $tempFilePath
                    $fileDownloaded = $true
                    Write-PSFMessage -Message "File '$sysInternalsDownloadURL' downloaded"
                }
                catch
                {
                    Write-ScreenInfo -Message "File '$sysInternalsDownloadURL' could not be downloaded. Skipping." -Type Error -TaskEnd
                    $fileDownloaded = $false
                }

                if ($fileDownloaded)
                {
                    if (-not ($IsLinux -or $IsMacOs)) { Unblock-File -Path $tempFilePath }

                    #Extract files to Tools folder
                    if (-not (Test-Path -Path "$labSources\Tools"))
                    {
                        Write-PSFMessage -Message "Folder '$labSources\Tools' does not exist. Creating now."
                        New-Item -ItemType Directory -Path "$labSources\Tools" | Out-Null
                    }
                    if (-not (Test-Path -Path "$labSources\Tools\SysInternals"))
                    {
                        Write-PSFMessage -Message "Folder '$labSources\Tools\SysInternals' does not exist. Creating now."
                        New-Item -ItemType Directory -Path "$labSources\Tools\SysInternals" | Out-Null
                    }
                    else
                    {
                        Write-PSFMessage -Message "Folder '$labSources\Tools\SysInternals' exist. Removing it now and recreating it."
                        Remove-Item -Path "$labSources\Tools\SysInternals" -Recurse | Out-Null
                        New-Item -ItemType Directory -Path "$labSources\Tools\SysInternals" | Out-Null
                    }

                    Write-PSFMessage -Message 'Extracting files'
                    Microsoft.PowerShell.Archive\Expand-Archive -Path $tempFilePath -DestinationPath "$labSources\Tools\SysInternals" -Force
                    Remove-Item -Path $tempFilePath

                    #Update registry
                    $versions['SysInternals'] = $updateStringFromWebPage
                    if ($IsLinux -or $IsMacOs)
                    {
                        $versions.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Versions.xml'))
                    }
                    else
                    {
                        $versions.ExportToRegistry('Cache', 'Versions')
                    }

                    $timestamps['SysInternalsUpdateLastChecked'] = Get-Date
                    if ($IsLinux -or $IsMacOs)
                    {
                        $timestamps.Export((Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath 'Stores/Timestamps.xml'))
                    }
                    else
                    {
                        $timestamps.ExportToRegistry('Cache', 'Timestamps')
                    }

                    Write-ScreenInfo -Message "SysInternals Suite has been updated and placed in '$labSources\Tools\SysInternals'" -Type Warning -TaskEnd
                }
            }
        }
    }
}


function Install-LabOffice2013
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry
    $lab = Get-Lab
    $roleName = [AutomatedLab.Roles]::Office2013

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role $roleName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message "There is no machine with the role $roleName"
        return
    }

    $isoImage = $lab.Sources.ISOs | Where-Object { $_.Name -eq $roleName }
    if (-not $isoImage)
    {
        Write-LogFunctionExitWithError -Message "There is no ISO image available to install the role '$roleName'. Please add the required ISO to the lab and name it '$roleName'"
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    Mount-LabIsoImage -ComputerName $machines -IsoPath $isoImage.Path -SupressOutput

    $jobs = @()

    foreach ($machine in $machines)
    {

        $parameters = @{ }
        $parameters.Add('ComputerName', $machine.Name)
        $parameters.Add('ActivityName', 'InstallationOffice2013')
        $parameters.Add('Verbose', $VerbosePreference)
        $parameters.Add('Scriptblock', {
                $timeout = 30

                Write-Verbose 'Installing Office 2013...'

                #region Office Installation Config
                $officeInstallationConfig = @'
<Configuration Product="ProPlusr">
<Display Level="basic" CompletionNotice="no" SuppressModal="yes" AcceptEula="yes" />
<AddLanguage Id="en-us" ShellTransform="yes"/>
<Logging Type="standard" Path="C:\" Template="Microsoft Office Professional Plus Setup(*).txt" />
<USERNAME Value="blah" />
<COMPANYNAME Value="blah" />
<!-- <PIDKEY Value="Office product key with no hyphen" /> -->
<!-- <INSTALLLOCATION Value="%programfiles%\Microsoft Office" /> -->
<!-- <LIS CACHEACTION="CacheOnly" /> -->
<!-- <LIS SOURCELIST="\\server1\share\Office;\\server2\share\Office" /> -->
<!-- <DistributionPoint Location="\\server\share\Office" /> -->
<!--Access-->
<OptionState Id="ACCESSFiles" State="local" Children="force" />

<!--Excel-->
<OptionState Id="EXCELFiles" State="local" Children="force" />

<!--InfoPath-->
<OptionState Id="XDOCSFiles" State="local" Children="force" />

<!--Lync-->
<OptionState Id="LyncCoreFiles" State="absent" Children="force" />

<!--OneNote-->
<OptionState Id="OneNoteFiles" State="local" Children="force" />

<!--Outlook-->
<OptionState Id="OUTLOOKFiles" State="local" Children="force" />

<!--PowerPoint-->
<OptionState Id="PPTFiles" State="local" Children="force" />

<!--Publisher-->
<OptionState Id="PubPrimary" State="absent" Children="force" />

<!--SkyDrive Pro-->
<OptionState Id="GrooveFiles2" State="local" Children="force" />

<!--Visio Viewer-->
<OptionState Id="VisioPreviewerFiles" State="absent" Children="force" />

<!--Word-->
<OptionState Id="WORDFiles" State="local" Children="force" />

<!--Shared Files-->
<OptionState Id="SHAREDFiles" State="local" Children="force" />

<!--Tools-->
<OptionState Id="TOOLSFiles" State="local" Children="force" />

<Setting Id="SETUP_REBOOT" Value="never" />
<!-- <Command Path="%windir%\system32\msiexec.exe" Args="/i \\server\share\my.msi" QuietArg="/q" ChainPosition="after" Execute="install" /> -->
</Configuration>
'@

                #endregion Office Installation Config

                $officeInstallationConfig | Out-File -FilePath C:\Office2013Config.xml

                $start = Get-Date

                Push-Location
                Set-Location -Path (Get-WmiObject -Class Win32_CDRomDrive).Drive
                Write-Verbose 'Calling "$($PWD.Path)setup.exe /config C:\Office2013Config.xml"'
                .\setup.exe /config C:\Office2013Config.xml
                Pop-Location

                Start-Sleep -Seconds 5

                while (Get-Process -Name setup -ErrorAction SilentlyContinue)
                {
                    if ((Get-Date).AddMinutes(- $timeout) -gt $start)
                    {
                        Write-LogError -Message "Installation of 'Office 2013' hit the timeout of $Timeout minutes. Killing the setup process"

                        Get-Process -Name setup | Stop-Process -Force

                        Write-Error -Message 'Installation of Office 2013 was not successfull'
                        return
                    }

                    Start-Sleep -Seconds 5
                }


                Write-Verbose '...Installation seems to be done'
            }
        )

        $jobs += Invoke-LabCommand @parameters -asjob -PassThru -NoDisplay
    }

    Write-ScreenInfo -Message 'Waiting for Office 2013 to complete installation' -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 15 -Timeout 30 -NoDisplay

    Dismount-LabIsoImage -ComputerName $machines -SupressOutput

    Write-LogFunctionExit
}


function Install-LabOffice2016
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    $config2016XmlTemplate = @"
<Configuration>
  <Add OfficeClientEdition="32">
    <Product ID="O365ProPlusRetail">
      <Language ID="en-us" />
    </Product>
  </Add>
  <Updates Enabled="TRUE" />
  <Display Level="None" AcceptEULA="TRUE" />
  <Property Name="SharedComputerLicensing" Value="{0}" />
  <Logging Level="Standard" Path="%temp%" />
  <!--Silent install of 32-Bit Office 365 ProPlus with Updates and Logging enabled-->
</Configuration>
"@


    $lab = Get-Lab
    $roleName = [AutomatedLab.Roles]::Office2016

    if (-not (Get-LabVM))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -Role $roleName
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message "There is no machine with the role $roleName"
        return
    }

    $isoImage = $lab.Sources.ISOs | Where-Object { $_.Name -eq $roleName }
    if (-not $isoImage)
    {
        Write-LogFunctionExitWithError -Message "There is no ISO image available to install the role '$roleName'. Please add the required ISO to the lab and name it '$roleName'"
        return
    }

    $officeDeploymentToolFileName = 'OfficeDeploymentTool.exe'
    $officeDeploymentToolFilePath = Join-Path -Path $labSources\SoftwarePackages -ChildPath $officeDeploymentToolFileName
    $officeDeploymentToolUri = Get-LabConfigurationItem -Name OfficeDeploymentTool

    if (-not (Test-Path -Path $officeDeploymentToolFilePath))
    {
        Get-LabInternetFile -Uri $officeDeploymentToolUri -Path $officeDeploymentToolFilePath
    }

    Write-ScreenInfo -Message 'Waiting for machines to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    $jobs = @()

    foreach ($machine in $machines)
    {
        $sharedComputerLicense = $false
        $officeRole = $machine.Roles | Where-Object Name -eq 'Office2016'
        $sharedComputerLicense = [int]($officeRole.Properties.SharedComputerLicensing)
        $config2016Xml = $config2016XmlTemplate -f $sharedComputerLicense

        Write-ScreenInfo "Preparing Office 2016 installation on '$machine'..." -NoNewLine
        $disk = Mount-LabIsoImage -ComputerName $machine -IsoPath $isoImage.Path -PassThru -SupressOutput

        Invoke-LabCommand -ActivityName 'Copy Office to C' -ComputerName $machine -ScriptBlock {
            New-Item -ItemType Directory -Path C:\Office | Out-Null
            Copy-Item -Path "$($args[0])\Office" -Destination C:\Office -Recurse
        } -ArgumentList $disk.DriveLetter

        Install-LabSoftwarePackage -Path $officeDeploymentToolFilePath -CommandLine '/extract:c:\Office /quiet' -ComputerName $machine -NoDisplay

        $tempFile = (Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'Configuration.xml')

        $config2016Xml | Out-File -FilePath $tempFile -Force
        Copy-LabFileItem -Path $tempFile -ComputerName $machine -DestinationFolderPath /Office

        Remove-Item -Path $tempFile

        Dismount-LabIsoImage -ComputerName $machine -SupressOutput
        Write-ScreenInfo 'finished.'
    }

    $jobs = Install-LabSoftwarePackage -LocalPath C:\Office\setup.exe -CommandLine '/configure c:\Office\Configuration.xml' -ComputerName $machines -AsJob -PassThru

    Write-ScreenInfo -Message 'Waiting for Office 2016 to complete installation' -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 15 -Timeout 30 -NoDisplay

    Write-LogFunctionExit
}


function Install-LabRemoteDesktopServices
{
    [CmdletBinding()]
    param ( )

    Write-LogFunctionEntry

    $lab = Get-Lab

    Start-LabVm -Role RemoteDesktopConnectionBroker, RemoteDesktopGateway, RemoteDesktopLicensing, RemoteDesktopSessionHost, RemoteDesktopVirtualizationHost, RemoteDesktopWebAccess -Wait
    $gw = Get-LabVm -Role RemoteDesktopGateway
    $webAccess = Get-LabVm -Role RemoteDesktopWebAccess
    $sessionHosts = Get-LabVm -Role RemoteDesktopSessionHost
    $connectionBroker = Get-LabVm -Role RemoteDesktopConnectionBroker
    $licensing = Get-LabVm -Role RemoteDesktopLicensing
    $virtHost = Get-LabVm -Role RemoteDesktopVirtualizationHost
    
    if (-not $gw)
    {
        $gw = Get-LabVm -Role RDS | Select-Object -First 1
    }
    if (-not $webAccess)
    {
        $webAccess = Get-LabVm -Role RDS | Select-Object -First 1
    }
    if (-not $sessionHosts)
    {
        $sessionHosts = Get-LabVm -Role RDS | Select-Object -First 1
    }
    if (-not $connectionBroker)
    {
        $connectionBroker = Get-LabVm -Role RDS | Select-Object -First 1
    }
    if (-not $licensing)
    {
        $licensing = Get-LabVm -Role RDS | Select-Object -First 1
    }
    if (-not $virtHost)
    {
        $virtHost = Get-LabVm -Role HyperV
    }

    $gwFqdn = if ($lab.DefaultVirtualizationEngine -eq 'Azure') { $gw.AzureConnectionInfo.DnsName } else { $gw.FQDN }
    $gwRole = $gw.Roles | Where-Object Name -eq 'RemoteDesktopGateway'
    if ($gwRole -and $gwRole.Properties.ContainsKey('GatewayExternalFqdn'))
    {
        $gwFqdn = $gwRole.Properties['GatewayExternalFqdn']
    }

    if (Get-LabVm -Role CARoot)
    {
        $certGw = Request-LabCertificate -Subject "CN=$gwFqdn" -SAN ($gw.FQDN -replace $gw.Name, '*') -TemplateName WebServer -ComputerName $gw -PassThru
        $gwCredential = $gw.GetCredential($lab)

        Invoke-LabCommand -ComputerName $gw -ScriptBlock {
            Export-PfxCertificate -Cert (Get-Item cert:\localmachine\my\$($certGw.Thumbprint)) -FilePath C:\cert.pfx -ProtectTo $gwCredential.UserName -Force
            Export-Certificate -Cert (Get-Item cert:\localmachine\my\$($certGw.Thumbprint)) -FilePath C:\cert.cer -Type CERT -Force
        } -Variable (Get-Variable certGw, gwCredential) -NoDisplay
    
        Receive-File -SourceFilePath C:\cert.pfx -DestinationFilePath (Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath cert.pfx) -Session (New-LabPSSession -ComputerName $gw)
        Receive-File -SourceFilePath C:\cert.cer -DestinationFilePath (Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath cert.cer) -Session (New-LabPSSession -ComputerName $gw)
        $certFiles = @(
            Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath cert.pfx
            Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath cert.cer
        )

        $nonGw = Get-LabVM -Filter { $_.Roles.Name -like 'RemoteDesktop*' -and $_.Name -ne $gw.Name }
        Copy-LabFileItem -Path $certFiles -ComputerName $nonGw 
        Invoke-LabCommand -ComputerName $nonGw  -ScriptBlock {
            Import-PfxCertificate -Exportable -FilePath C:\cert.pfx -CertStoreLocation Cert:\LocalMachine\my
        } -NoDisplay
    }

    if ($lab.DefaultVirtualizationEngine -eq 'Azure') 
    {
        Add-LWAzureLoadBalancedPort -Port 443 -DestinationPort 443 -ComputerName $gw
    }

    # Initial deployment
    Install-LabWindowsFeature -ComputerName $gw -FeatureName RDS-Gateway -IncludeManagementTools -NoDisplay

    $gwRole = $gw.Roles | Where-Object Name -eq 'RemoteDesktopGateway'
    $webAccessRole = $webAccess.Roles | Where-Object Name -eq 'RemoteDesktopWebAccess'
    $connectionBrokerRole = $connectionBroker.Roles | Where-Object Name -eq 'RemoteDesktopConnectionBroker'
    $licensingRole = $licensing.Roles | Where-Object Name -eq 'RemoteDesktopLicensing'
    $virtHostRole = $virtHost.Roles | Where-Object Name -eq 'RemoteDesktopVirtualizationHost'

    $gwConfig = @{
        GatewayExternalFqdn  = $gwFqdn
        BypassLocal          = if ($gwRole -and $gwRole.Properties.ContainsKey('BypassLocal')) { [Convert]::ToBoolean($gwRole.Properties['BypassLocal']) } else { $true }
        LogonMethod          = if ($gwRole -and $gwRole.Properties.ContainsKey('LogonMethod')) { $gwRole.Properties['LogonMethod'] } else { 'Password' }
        UseCachedCredentials = if ($gwRole -and $gwRole.Properties.ContainsKey('UseCachedCredentials')) { [Convert]::ToBoolean($gwRole.Properties['UseCachedCredentials']) } else { $true }
        ConnectionBroker     = $connectionBroker.Fqdn
        GatewayMode          = if ($gwRole -and $gwRole.Properties.ContainsKey('GatewayMode')) { $gwRole.Properties['GatewayMode'] } else { 'Custom' }
        Force                = $true
    }

    $sessionHostRoles = $sessionHosts | Group-Object { ($_.Roles | Where-Object Name -eq 'RemoteDesktopSessionHost').Properties['CollectionName'] }
    [hashtable[]]$sessionCollectionConfig = foreach ($sessionhost in $sessionHostRoles)
    {
        $firstRoleMember = ($sessionhost.Group | Select-Object -First 1).Roles | Where-Object Name -eq 'RemoteDesktopSessionHost'
        $param = @{
            CollectionName        = if (-not [string]::IsNullOrWhiteSpace($sessionhost.Name)) { $sessionhost.Name } else { 'AutomatedLab' }
            CollectionDescription = if ($firstRoleMember.Properties.ContainsKey('CollectionDescription')) { $firstRoleMember.Properties['CollectionDescription'] } else { 'AutomatedLab session host collection' }
            ConnectionBroker      = $connectionBroker.Fqdn
            SessionHost           = $sessionhost.Group.Fqdn
            PooledUnmanaged       = $true
        }

        if ($firstRoleMember.Properties.Keys -in 'PersonalUnmanaged', 'AutoAssignUser', 'GrantAdministrativePrivilege')
        {
            $param.Remove('PooledUnmanaged')
            $param['PersonalUnmanaged'] = $true
            $param['AutoAssignUser'] = if ($firstRoleMember.Properties.ContainsKey('AutoAssignUser')) { [Convert]::ToBoolean($firstRoleMember.Properties['AutoAssignUser']) } else { $true }
            $param['GrantAdministrativePrivilege'] = if ($firstRoleMember.Properties.ContainsKey('GrantAdministrativePrivilege')) { [Convert]::ToBoolean($firstRoleMember.Properties['GrantAdministrativePrivilege']) } else { $false }
        }
        elseif ($firstRoleMember.Properties.ContainsKey('PooledUnmanaged'))
        {
            $param['PooledUnmanaged'] = $true
        }
        $param
    }

    $deploymentConfig = @{
        ConnectionBroker = $connectionBroker.Fqdn
        WebAccessServer  = $webAccess.Fqdn
        SessionHost      = $sessionHosts.Fqdn
    }
    $licenseConfig = @{
        Mode             = if ($licensingRole -and $licensingRole.Properties.ContainsKey('Mode')) { $licensingRole.Properties['Mode'] } else { 'PerUser' }
        ConnectionBroker = $connectionBroker.Fqdn 
        LicenseServer    = $licensing.Fqdn 
        Force            = $true
    }
    Invoke-LabCommand -ComputerName $connectionBroker -ScriptBlock {
        New-RDSessionDeployment @deploymentConfig
        foreach ($config in $sessionCollectionConfig)
        {
            New-RDSessionCollection @config
        }
        Set-RDDeploymentGatewayConfiguration @gwConfig
    } -Variable (Get-Variable gwConfig, sessionCollectionConfig, deploymentConfig) -NoDisplay

    Invoke-LabCommand -ComputerName $connectionBroker -ScriptBlock {
        Set-RDLicenseConfiguration @licenseConfig -ErrorAction SilentlyContinue
    } -Variable (Get-Variable licenseConfig) -NoDisplay

    $prefix = if (Get-LabVm -Role CARoot)
    {
        Invoke-LabCommand -ComputerName $connectionBroker -ScriptBlock {        
            Set-RDCertificate -Role RDWebAccess -Thumbprint $certGw.Thumbprint -ConnectionBroker $connectionBroker.Fqdn -Force -ErrorAction SilentlyContinue
            Set-RDCertificate -Role RDGateway -Thumbprint $certGw.Thumbprint -ConnectionBroker $connectionBroker.Fqdn -Force -ErrorAction SilentlyContinue
            Set-RDCertificate -Role RDPublishing -Thumbprint $certGw.Thumbprint -ConnectionBroker $connectionBroker.Fqdn -Force -ErrorAction SilentlyContinue
            Set-RDCertificate -Role RDRedirector -Thumbprint $certGw.Thumbprint -ConnectionBroker $connectionBroker.Fqdn -Force -ErrorAction SilentlyContinue
        } -Variable (Get-Variable connectionBroker, certGw) -NoDisplay
        'https'
    }
    else
    {
        'http'
    }

    # Web Client
    if (-not (Test-LabHostConnected)) 
    {
        Write-LogFunctionExit
        return
    }

    if (-not (Get-Module -Name PowerShellGet -ListAvailable).Where( { $_.Version -ge '2.0.0.0' }))
    {
        Write-LogFunctionExit
        return
    }

    $destination = Join-Path -Path (Get-LabSourcesLocation -Local) -ChildPath SoftwarePackages
    $mod = Find-Module -Name RDWebClientManagement -Repository PSGallery
    $mod | Save-Module -Name RDWebClientManagement -Path $destination -AcceptLicense
    Send-ModuleToPSSession -Module (Get-Module (Join-Path -Path $destination -ChildPath "RDWebClientManagement/$($mod.Version)/RDWebClientManagement.psd1" -Resolve) -ListAvailable) -Session (New-LabPSSession -ComputerName $webAccess)

    $clientInfo = (Invoke-RestMethod -Uri 'https://go.microsoft.com/fwlink/?linkid=2005418' -UseBasicParsing).packages | Sort Version | Select -Last 1

    $client = Get-LabInternetFile -NoDisplay -PassThru -Uri $clientInfo.url -Path $labsources/SoftwarePackages -FileName "rdwebclient-$($clientInfo.version).zip"
    $localPath = Copy-LabFileItem -Path $client.FullName -ComputerName $webAccess -PassThru -DestinationFolderPath C:\

    Invoke-LabCommand -ComputerName $webAccess -ScriptBlock {
        Install-RDWebClientPackage -Source $localPath
        if (Test-Path -Path C:\cert.cer)
        {
            Import-RDWebClientBrokerCert -Path C:\cert.cer
        }

        Publish-RDWebClientPackage -Type Production -Latest
    } -Variable (Get-Variable localPath) -NoDisplay

    Invoke-LabCommand -ComputerName (Get-LabVm -Role CaRoot) -ScriptBlock {
        Get-ChildItem -Path Cert:\LocalMachine\my | Select-Object -First 1 | Export-Certificate -FilePath C:\LabRootCa.cer -Type CERT -Force
    } -NoDisplay
    $certPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath LabRootCa.cer
    Receive-File -SourceFilePath C:\LabRootCa.cer -DestinationFilePath $certPath -Session (New-LabPSSession -ComputerName (Get-LabVm -Role CaRoot))

    Write-ScreenInfo -Message "RDWeb Client available at $($prefix)://$gwFqdn/RDWeb/webclient"
    Write-LogFunctionExit
}


function Enter-LabPSSession
{
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0)]
        [string]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByMachine', Position = 0)]
        [AutomatedLab.Machine]$Machine,

        [switch]$DoNotUseCredSsp,

        [switch]$UseLocalCredential
    )

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        $Machine = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }

    if ($Machine)
    {
        $session = New-LabPSSession -Machine $Machine -DoNotUseCredSsp:$DoNotUseCredSsp -UseLocalCredential:$UseLocalCredential

        $session | Enter-PSSession
    }
    else
    {
        Write-Error 'The specified machine could not be found in the lab.'
    }
}


function Get-LabCimSession
{
    [CmdletBinding()]
    [OutputType([Microsoft.Management.Infrastructure.CimSession])]
    param 
    (
        [string[]]
        $ComputerName,

        [switch]
        $DoNotUseCredSsp
    )

    $pattern = '\w+_[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'

    if ($ComputerName)
    {
        $computers = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }
    else
    {
        $computers = Get-LabVM -IncludeLinux
    }

    if (-not $computers)
    {
        Write-Error 'The machines could not be found' -TargetObject $ComputerName
    }

    foreach ($computer in $computers)
    {
        $session = Get-CimSession | Where-Object { $_.Name -match $pattern -and $_.Name -like "$($computer.Name)_*" }

        if (-not $session -and $ComputerName)
        {
            Write-Error "No session found for computer '$computer'" -TargetObject $computer
        }
        else
        {
            $session
        }
    }
}


function Get-LabPSSession
{
    [cmdletBinding()]
    [OutputType([System.Management.Automation.Runspaces.PSSession])]

    param (
        [string[]]$ComputerName,

        [switch]$DoNotUseCredSsp
    )

    $pattern = '\w+_[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'

    if ($ComputerName)
    {
        $computers = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }
    else
    {
        $computers = Get-LabVM -IncludeLinux
    }

    if (-not $computers)
    {
        Write-Error 'The machines could not be found' -TargetObject $ComputerName
    }

    $sessions = foreach ($computer in $computers)
    {
        $session = Get-PSSession | Where-Object { $_.Name -match $pattern -and $_.Name -like "$($computer.Name)_*" }

        if (-not $session -and $ComputerName)
        {
            Write-Error "No session found for computer '$computer'" -TargetObject $computer
        }
        else
        {
            $session
        }
    }

    if ($DoNotUseCredSsp)
    {
        $sessions | Where-Object { $_.Runspace.ConnectionInfo.AuthenticationMechanism -ne 'CredSsp' }
    }
    else
    {
        $sessions
    }
}


function Get-LabSshKnownHost
{
    [CmdletBinding()]
    param ()

    if (-not (Test-Path -Path $home/.ssh/known_hosts)) { return }

    Get-Content -Path $home/.ssh/known_hosts | ConvertFrom-String -Delimiter ' ' -PropertyNames ComputerName,Cipher,Fingerprint -ErrorAction SilentlyContinue
}


function Install-LabRdsCertificate
{
    [CmdletBinding()]
    param ( )

    $lab = Get-Lab
    if (-not $lab)
    {
        return
    }

    $machines = Get-LabVM -All | Where-Object -FilterScript { $_.OperatingSystemType -eq 'Windows' -and $_.OperatingSystem.Version -ge 6.3 -and -not $_.SkipDeployment }
    if (-not $machines)
    {
        return
    }

    $jobs = foreach ($machine in $machines)
    {
        Invoke-LabCommand -ComputerName $machine -ActivityName 'Exporting RDS certs' -NoDisplay -ScriptBlock {
            [string[]]$SANs = $machine.FQDN
            $cmdlet = Get-Command -Name New-SelfSignedCertificate -ErrorAction SilentlyContinue
            if ($machine.HostType -eq 'Azure' -and $cmdlet)
            {
                $SANs += $machine.AzureConnectionInfo.DnsName
            }

            $cert = if ($cmdlet.Parameters.ContainsKey('Subject'))
            {
                New-SelfSignedCertificate -Subject "CN=$($machine.Name)" -DnsName $SANs -CertStoreLocation 'Cert:\LocalMachine\My' -Type SSLServerAuthentication
            }
            else
            {
                New-SelfSignedCertificate -DnsName $SANs -CertStoreLocation 'Cert:\LocalMachine\my'
            }
            $rdsSettings = Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace ROOT\CIMV2\TerminalServices
            $rdsSettings.SSLCertificateSHA1Hash = $cert.Thumbprint
            $rdsSettings | Set-CimInstance
            $null = $cert | Export-Certificate -FilePath "C:\$($machine.Name).cer" -Type CERT -Force
        } -Variable (Get-Variable machine) -AsJob -PassThru
    }

    Wait-LWLabJob -Job $jobs -NoDisplay
    $tmp = Join-Path -Path $lab.LabPath -ChildPath Certificates
    if (-not (Test-Path -Path $tmp)) { $null = New-Item -ItemType Directory -Path $tmp }
    foreach ($session in (New-LabPSSession -ComputerName $machines))
    {
        $fPath = Join-Path -Path $tmp -ChildPath "$($session.LabMachineName).cer"
        Receive-File -SourceFilePath "C:\$($session.LabMachineName).cer" -DestinationFilePath $fPath -Session $session
        $null = Import-Certificate -FilePath $fPath -CertStoreLocation 'Cert:\LocalMachine\Root'
    }
}


function Install-LabSshKnownHost
{
    [CmdletBinding()]
    param ( )

    $lab = Get-Lab
    if (-not $lab)
    {
        return
    }

    $machines = Get-LabVM -All -IncludeLinux | Where-Object -FilterScript { -not $_.SkipDeployment }
    if (-not $machines)
    {
        return
    }

    if (-not (Test-Path -Path $home/.ssh/known_hosts)) {$null = New-Item -ItemType File -Path $home/.ssh/known_hosts -Force}
    $knownHostContent = Get-LabSshKnownHost

    foreach ($machine in $machines)
    {
        if ((Get-LabVmStatus -ComputerName $machine) -ne 'Started' ) {continue}
        if ($lab.DefaultVirtualizationEngine -eq 'Azure')
        {
            $keyScanHosts = ssh-keyscan -p $machine.LoadBalancerSshPort $machine.AzureConnectionInfo.DnsName 2>$null | ConvertFrom-String -Delimiter ' ' -PropertyNames ComputerName,Cipher,Fingerprint -ErrorAction SilentlyContinue
            $keyScanIps = ssh-keyscan -p $machine.LoadBalancerSshPort $machine.AzureConnectionInfo.VIP 2>$null | ConvertFrom-String -Delimiter ' ' -PropertyNames ComputerName,Cipher,Fingerprint -ErrorAction SilentlyContinue

            foreach ($keyScanHost in $keyScanHosts)
            {
                $sshHostEntry = $knownHostContent | Where-Object {$_.ComputerName -eq "[$($machine.AzureConnectionInfo.DnsName)]:$($machine.LoadBalancerSshPort)" -and $_.Cipher -eq $keyScanHost.Cipher}
                if (-not $sshHostEntry -or $keyScanHost.Fingerprint -ne $sshHostEntry.Fingerprint)
                {
                    Write-ScreenInfo -Type Verbose -Message ("Adding line to $home/.ssh/known_hosts: {0} {1} {2}" -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint)
                    try
                    {
                        '{0} {1} {2}' -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint | Add-Content $home/.ssh/known_hosts -ErrorAction Stop
                    }
                    catch
                    {
                        Start-Sleep -Milliseconds 125
                        '{0} {1} {2}' -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint | Add-Content $home/.ssh/known_hosts
                    }
                }
            }

            foreach ($keyScanIp in $keyScanIps)
            {
                $sshHostEntryIp = $knownHostContent | Where-Object {$_.ComputerName -eq "[$($machine.AzureConnectionInfo.VIP)]:$($machine.LoadBalancerSshPort)" -and $_.Cipher -eq $keyScanIp.Cipher}
                if (-not $sshHostEntryIp -or $keyScanIp.Fingerprint -ne $sshHostEntryIp.Fingerprint)
                {
                    Write-ScreenInfo -Type Verbose -Message ("Adding line to $home/.ssh/known_hosts: {0} {1} {2}" -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint)
                    try
                    {
                        '{0} {1} {2}' -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint | Add-Content $home/.ssh/known_hosts -ErrorAction Stop
                    }
                    catch
                    {
                        Start-Sleep -Milliseconds 125
                        '{0} {1} {2}' -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint | Add-Content $home/.ssh/known_hosts
                    }
                }
            }
        }
        else
        {
            $keyScanHosts = ssh-keyscan $machine.Name 2>$null | ConvertFrom-String -Delimiter ' ' -PropertyNames ComputerName,Cipher,Fingerprint -ErrorAction SilentlyContinue
            foreach ($keyScanHost in $keyScanHosts)
            {
                $sshHostEntry = $knownHostContent | Where-Object {$_.ComputerName -eq $machine.Name -and $_.Cipher -eq $keyScanHost.Cipher}
                if (-not $sshHostEntry -or $keyScanHost.Fingerprint -ne $sshHostEntry.Fingerprint)
                {
                    Write-ScreenInfo -Type Verbose -Message ("Adding line to $home/.ssh/known_hosts: {0} {1} {2}" -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint)
                    try
                    {
                        '{0} {1} {2}' -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint | Add-Content $home/.ssh/known_hosts -ErrorAction Stop
                    }
                    catch
                    {
                        Start-Sleep -Milliseconds 125
                        '{0} {1} {2}' -f $keyScanHost.ComputerName,$keyScanHost.Cipher,$keyScanHost.Fingerprint | Add-Content $home/.ssh/known_hosts
                    }
                }
            }
            if ($machine.IpV4Address)
            {
                $keyScanIps = ssh-keyscan $machine.IpV4Address 2>$null | ConvertFrom-String -Delimiter ' ' -PropertyNames ComputerName,Cipher,Fingerprint -ErrorAction SilentlyContinue
                foreach ($keyScanIp in $keyScanIps)
                {
                    $sshHostEntryIp = $knownHostContent | Where-Object {$_.ComputerName -eq $machine.IpV4Address -and $_.Cipher -eq $keyScanIp.Cipher}
                    if (-not $sshHostEntryIp -or $keyScanIp.Fingerprint -ne $sshHostEntryIp.Fingerprint)
                    {
                        Write-ScreenInfo -Type Verbose -Message ("Adding line to $home/.ssh/known_hosts: {0} {1} {2}" -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint)
                        try
                        {
                            '{0} {1} {2}' -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint | Add-Content $home/.ssh/known_hosts -ErrorAction Stop
                        }
                        catch
                        {
                            Start-Sleep -Milliseconds 125
                            '{0} {1} {2}' -f $keyScanIp.ComputerName,$keyScanIp.Cipher,$keyScanIp.Fingerprint | Add-Content $home/.ssh/known_hosts
                        }
                    }
                }
            }
        }
    }
}

function Invoke-LabCommand
{
    [cmdletBinding()]
    param (
        [string]$ActivityName = '<unnamed>',

        [Parameter(Mandatory, ParameterSetName = 'ScriptBlockFileContentDependency', Position = 0)]
        [Parameter(Mandatory, ParameterSetName = 'ScriptFileContentDependency', Position = 0)]
        [Parameter(Mandatory, ParameterSetName = 'ScriptFileNameContentDependency', Position = 0)]
        [Parameter(Mandatory, ParameterSetName = 'Script', Position = 0)]
        [Parameter(Mandatory, ParameterSetName = 'ScriptBlock', Position = 0)]
        [Parameter(Mandatory, ParameterSetName = 'PostInstallationActivity', Position = 0)]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ScriptBlockFileContentDependency', Position = 1)]
        [Parameter(Mandatory, ParameterSetName = 'ScriptBlock', Position = 1)]
        [scriptblock]$ScriptBlock,

        [Parameter(Mandatory, ParameterSetName = 'ScriptFileContentDependency')]
        [Parameter(Mandatory, ParameterSetName = 'Script')]
        [string]$FilePath,

        [Parameter(Mandatory, ParameterSetName = 'ScriptFileNameContentDependency')]
        [string]$FileName,

        [Parameter(ParameterSetName = 'ScriptFileNameContentDependency')]
        [Parameter(Mandatory, ParameterSetName = 'ScriptBlockFileContentDependency')]
        [Parameter(Mandatory, ParameterSetName = 'ScriptFileContentDependency')]
        [string]$DependencyFolderPath,

        [Parameter(ParameterSetName = 'PostInstallationActivity')]
        [switch]$PostInstallationActivity,

        [Parameter(ParameterSetName = 'PostInstallationActivity')]
        [switch]$PreInstallationActivity,

        [Parameter(ParameterSetName = 'PostInstallationActivity')]
        [string[]]$CustomRoleName,

        [object[]]$ArgumentList,

        [switch]$DoNotUseCredSsp,

        [switch]$UseLocalCredential,

        [pscredential]$Credential,

        [System.Management.Automation.PSVariable[]]$Variable,

        [System.Management.Automation.FunctionInfo[]]$Function,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Parameter(ParameterSetName = 'ScriptBlockFileContentDependency')]
        [Parameter(ParameterSetName = 'ScriptFileContentDependency')]
        [Parameter(ParameterSetName = 'Script')]
        [Parameter(ParameterSetName = 'ScriptFileNameContentDependency')]
        [int]$Retries,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Parameter(ParameterSetName = 'ScriptBlockFileContentDependency')]
        [Parameter(ParameterSetName = 'ScriptFileContentDependency')]
        [Parameter(ParameterSetName = 'Script')]
        [Parameter(ParameterSetName = 'ScriptFileNameContentDependency')]
        [int]$RetryIntervalInSeconds,

        [int]$ThrottleLimit = 32,

        [switch]$AsJob,

        [switch]$PassThru,

        [switch]$NoDisplay,

        [switch]$IgnoreAzureLabSources
    )

    Write-LogFunctionEntry
    $customRoleCount = 0

    $parameterSetsWithRetries = 'Script',
        'ScriptBlock',
        'ScriptFileContentDependency',
        'ScriptBlockFileContentDependency',
        'ScriptFileNameContentDependency',
        'PostInstallationActivity',
        'PreInstallationActivity'

    if ($PSCmdlet.ParameterSetName -in $parameterSetsWithRetries)
    {
        if (-not $Retries)
        {
            $Retries = Get-LabConfigurationItem -Name InvokeLabCommandRetries
        }
        if (-not $RetryIntervalInSeconds)
        {
            $RetryIntervalInSeconds = Get-LabConfigurationItem -Name InvokeLabCommandRetryIntervalInSeconds
        }
    }

    if ($AsJob)
    {
        Write-ScreenInfo -Message "Executing lab command activity: '$ActivityName' on machines '$($ComputerName -join ', ')'" -TaskStart

        Write-ScreenInfo -Message 'Activity started in background' -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message "Executing lab command activity: '$ActivityName' on machines '$($ComputerName -join ', ')'" -TaskStart

        Write-ScreenInfo -Message 'Waiting for completion'
    }

    Write-PSFMessage -Message "Executing lab command activity '$ActivityName' on machines '$($ComputerName -join ', ')'"

    #required to suppress verbose messages, warnings and errors
    Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if (-not (Get-LabVM -IncludeLinux))
    {
        Write-LogFunctionExitWithError -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    if ($FilePath)
    {
        $isLabPathIsOnLabAzureLabSourcesStorage = if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
        {
            Test-LabPathIsOnLabAzureLabSourcesStorage -Path $FilePath
        }
        if ($isLabPathIsOnLabAzureLabSourcesStorage)
        {
            Write-PSFMessage "$FilePath is on Azure. Skipping test."
        }
        elseif (-not (Test-Path -Path $FilePath))
        {
            Write-LogFunctionExitWithError -Message "$FilePath is not on Azure and does not exist"
            return
        }
    }

    if ($PreInstallationActivity)
    {
        $machines = Get-LabVM -ComputerName $ComputerName | Where-Object { $_.PreInstallationActivity -and -not $_.SkipDeployment }
        if (-not $machines)
        {
            Write-PSFMessage 'There are no machine with PreInstallationActivity defined, exiting...'
            return
        }
    }
    elseif ($PostInstallationActivity)
    {
        $machines = Get-LabVM -ComputerName $ComputerName | Where-Object { $_.PostInstallationActivity -and -not $_.SkipDeployment }
        if (-not $machines)
        {
            Write-PSFMessage 'There are no machine with PostInstallationActivity defined, exiting...'
            return
        }
    }
    else
    {
        $machines = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }

    if (-not $machines)
    {
        Write-ScreenInfo "Cannot invoke the command '$ActivityName', as the specified machines ($($ComputerName -join ', ')) could not be found in the lab." -Type Warning
        return
    }

    if ('Stopped' -in (Get-LabVMStatus -ComputerName $machines -AsHashTable).Values)
    {
        Start-LabVM -ComputerName $machines -Wait
    }

    if ($PostInstallationActivity -or $PreInstallationActivity)
    {
        Write-ScreenInfo -Message 'Performing pre/post-installation tasks defined for each machine' -TaskStart -OverrideNoDisplay

        $results = @()

        foreach ($machine in $machines)
        {
            $activities = if ($PreInstallationActivity) { $machine.PreInstallationActivity } elseif ($PostInstallationActivity) { $machine.PostInstallationActivity }
            foreach ($item in $activities)
            {
                if ($item.RoleName -notin $CustomRoleName -and $CustomRoleName.Count -gt 0)
                {
                    Write-PSFMessage "Skipping installing custom role $($item.RoleName) as it is not part of the parameter `$CustomRoleName"
                    continue
                }

                if ($item.IsCustomRole)
                {
                    Write-ScreenInfo "Installing Custom Role '$(Split-Path -Path $item.DependencyFolder -Leaf)' on machine '$machine'" -TaskStart -OverrideNoDisplay
                    $customRoleCount++
                    #if there is a HostStart.ps1 script for the role
                    $hostStartPath = Join-Path -Path $item.DependencyFolder -ChildPath 'HostStart.ps1'
                    if (Test-Path -Path $hostStartPath)
                    {
                        if (-not $script:data) {$script:data = Get-Lab}
                        $hostStartScript = Get-Command -Name $hostStartPath
                        $hostStartParam = Sync-Parameter -Command $hostStartScript -Parameters ($item.SerializedProperties | ConvertFrom-PSFClixml -ErrorAction SilentlyContinue) -ConvertValue
                        if ($hostStartScript.Parameters.ContainsKey('ComputerName'))
                        {
                            $hostStartParam['ComputerName'] = $machine.Name
                        }
                        $results += & $hostStartPath @hostStartParam
                    }
                }

                $ComputerName = $machine.Name

                $param = @{}
                $param.Add('ComputerName', $ComputerName)

                Write-PSFMessage "Creating session to computers) '$ComputerName'"
                $session = New-LabPSSession -ComputerName $ComputerName -DoNotUseCredSsp:$item.DoNotUseCredSsp -IgnoreAzureLabSources:$IgnoreAzureLabSources.IsPresent
                if (-not $session)
                {
                    Write-LogFunctionExitWithError "Could not create a session to machine '$ComputerName'"
                    return
                }
                $param.Add('Session', $session)

                foreach ($serVariable in ($item.SerializedVariables | ConvertFrom-PSFClixml -ErrorAction SilentlyContinue))
                {
                    $existingVariable = Get-Variable -Name $serVariable.Name -ErrorAction SilentlyContinue
                    if ($existingVariable.Value -ne $serVariable.Value)
                    {
                        Set-Variable -Name $serVariable.Name -Value $serVariable.Value -Force
                    }

                    Add-VariableToPSSession -Session $session -PSVariable (Get-Variable -Name $serVariable.Name)
                }

                foreach ($serFunction in ($item.SerializedFunctions | ConvertFrom-PSFClixml -ErrorAction SilentlyContinue))
                {
                    $existingFunction = Get-Command -Name $serFunction.Name -ErrorAction SilentlyContinue
                    if ($existingFunction.ScriptBlock -eq $serFunction.ScriptBlock)
                    {
                        Set-Item -Path "function:\$($serFunction.Name)" -Value $serFunction.ScriptBlock -Force
                    }

                    Add-FunctionToPSSession -Session $session -FunctionInfo (Get-Command -Name $serFunction.Name)
                }

                if ($item.DependencyFolder.Value) { $param.Add('DependencyFolderPath', $item.DependencyFolder.Value) }
                if ($item.ScriptFileName) { $param.Add('ScriptFileName',$item.ScriptFileName) }
                if ($item.ScriptFilePath) { $param.Add('ScriptFilePath', $item.ScriptFilePath) }
                if ($item.KeepFolder) { $param.Add('KeepFolder', $item.KeepFolder) }
                if ($item.ActivityName) { $param.Add('ActivityName', $item.ActivityName) }
                if ($Retries) { $param.Add('Retries', $Retries) }
                if ($RetryIntervalInSeconds) { $param.Add('RetryIntervalInSeconds', $RetryIntervalInSeconds) }
                $param.AsJob      = $true
                $param.PassThru   = $PassThru
                $param.Verbose    = $VerbosePreference
                if ($PSBoundParameters.ContainsKey('ThrottleLimit'))
                {
                    $param.Add('ThrottleLimit', $ThrottleLimit)
                }

                $scriptFullName = Join-Path -Path $param.DependencyFolderPath -ChildPath $param.ScriptFileName
                if ($item.SerializedProperties -and (Test-Path -Path $scriptFullName))
                {
                    $script = Get-Command -Name $scriptFullName
                    $temp = Sync-Parameter -Command $script -Parameters ($item.SerializedProperties | ConvertFrom-PSFClixml -ErrorAction SilentlyContinue)

                    Add-VariableToPSSession -Session $session -PSVariable (Get-Variable -Name temp)
                    $param.ParameterVariableName = 'temp'
                }

                if ($item.IsCustomRole)
                {
                    if (Test-Path -Path $scriptFullName)
                    {
                        $param.PassThru = $true
                        $results += Invoke-LWCommand @param
                    }
                }
                else
                {
                    $results += Invoke-LWCommand @param
                }

                if ($item.IsCustomRole)
                {
                    Wait-LWLabJob -Job ($results | Where-Object { $_ -is [System.Management.Automation.Job]} )-ProgressIndicator 15 -NoDisplay

                    #if there is a HostEnd.ps1 script for the role
                    $hostEndPath = Join-Path -Path $item.DependencyFolder -ChildPath 'HostEnd.ps1'
                    if (Test-Path -Path $hostEndPath)
                    {
                        $hostEndScript = Get-Command -Name $hostEndPath
                        $hostEndParam = Sync-Parameter -Command $hostEndScript -Parameters ($item.SerializedProperties | ConvertFrom-PSFClixml -ErrorAction SilentlyContinue)
                        if ($hostEndScript.Parameters.ContainsKey('ComputerName'))
                        {
                            $hostEndParam['ComputerName'] = $machine.Name
                        }
                        $results += & $hostEndPath @hostEndParam
                    }
                }
            }
        }

        if ($customRoleCount)
        {
            $jobs = $results | Where-Object { $_ -is [System.Management.Automation.Job] -and $_.State -eq 'Running' }
            if ($jobs)
            {
                Write-ScreenInfo -Message "Waiting on $($results.Count) custom role installations to finish..." -NoNewLine -OverrideNoDisplay
                Wait-LWLabJob -Job $jobs -Timeout 60 -NoDisplay
            }
            else
            {
                Write-ScreenInfo -Message "$($customRoleCount) custom role installation finished." -OverrideNoDisplay
            }
        }

        Write-ScreenInfo -Message 'Pre/Post-installations done' -TaskEnd -OverrideNoDisplay
    }
    else
    {
        $param = @{}
        $param.Add('ComputerName', $machines)

        Write-PSFMessage "Creating session to computer(s) '$machines'"
        $session = @(New-LabPSSession -ComputerName $machines -DoNotUseCredSsp:$DoNotUseCredSsp -UseLocalCredential:$UseLocalCredential -Credential $credential -IgnoreAzureLabSources:$IgnoreAzureLabSources.IsPresent)
        if (-not $session)
        {
            Write-LogFunctionExitWithError "Could not create a session to machine '$machines'"
            return
        }

        if ($Function)
        {
            Write-PSFMessage "Adding functions '$($Function -join ',')' to session"
            $Function | Add-FunctionToPSSession -Session $session
        }

        if ($Variable)
        {
            Write-PSFMessage "Adding variables '$($Variable -join ',')' to session"
            $Variable | Add-VariableToPSSession -Session $session
        }

        $param.Add('Session', $session)

        if ($FilePath)
        {
            $scriptContent = if ($isLabPathIsOnLabAzureLabSourcesStorage)
            {
                #if the script is on an Azure file storage, the host machine cannot access it. The read operation is done on the first Azure machine.
                Invoke-LabCommand -ComputerName ($machines | Where-Object HostType -eq 'Azure')[0] -ScriptBlock { Get-Content -Path $FilePath -Raw } -Variable (Get-Variable -Name FilePath) -NoDisplay -PassThru
            }
            else
            {
                Get-Content -Path $FilePath -Raw
            }
            $ScriptBlock = [scriptblock]::Create($scriptContent)
        }

        if ($ScriptBlock)            { $param.Add('ScriptBlock', $ScriptBlock) }
        if ($Retries)                { $param.Add('Retries', $Retries) }
        if ($RetryIntervalInSeconds) { $param.Add('RetryIntervalInSeconds', $RetryIntervalInSeconds) }
        if ($FileName)               { $param.Add('ScriptFileName', $FileName) }
        if ($ActivityName)           { $param.Add('ActivityName', $ActivityName) }
        if ($ArgumentList)           { $param.Add('ArgumentList', $ArgumentList) }
        if ($DependencyFolderPath)   { $param.Add('DependencyFolderPath', $DependencyFolderPath) }

        $param.PassThru   = $PassThru
        $param.AsJob      = $AsJob
        $param.Verbose    = $VerbosePreference
        if ($PSBoundParameters.ContainsKey('ThrottleLimit'))
        {
            $param.Add('ThrottleLimit', $ThrottleLimit)
        }

        $results = Invoke-LWCommand @param
    }

    if ($AsJob)
    {
        Write-ScreenInfo -Message 'Activity started in background' -TaskEnd
    }
    else
    {
        Write-ScreenInfo -Message 'Activity done' -TaskEnd
    }

    if ($PassThru)
    {
        $results
    }

    Write-LogFunctionExit
}


function New-LabCimSession
{
    [CmdletBinding()]
    param 
    (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0)]
        [string[]]
        $ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByMachine')]
        [AutomatedLab.Machine[]]
        $Machine,

        #this is used to recreate a broken session
        [Parameter(Mandatory, ParameterSetName = 'BySession')]
        [Microsoft.Management.Infrastructure.CimSession]
        $Session,

        [switch]
        $UseLocalCredential,

        [switch]
        $DoNotUseCredSsp,

        [pscredential]
        $Credential,

        [int]
        $Retries = 2,

        [int]
        $Interval = 5,

        [switch]
        $UseSSL
    )

    begin
    {
        Write-LogFunctionEntry
        $sessions = @()
        $lab = Get-Lab

        #Due to a problem in Windows 10 not being able to reach VMs from the host
        $testPortTimeout = (Get-LabConfigurationItem -Name Timeout_TestPortInSeconds) * 1000

        $jitTs = Get-LabConfigurationItem -Name AzureJitTimestamp
        if ((Get-LabConfigurationItem -Name AzureEnableJit) -and $lab.DefaultVirtualizationEngine -eq 'Azure' -and (-not $jitTs -or ((Get-Date) -ge $jitTs)) )
        {
            # Either JIT has not been requested, or current date exceeds timestamp
            Request-LabAzureJitAccess
        }
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByName')
        {
            $Machine = Get-LabVM -ComputerName $ComputerName -IncludeLinux

            if (-not $Machine)
            {
                Write-Error "There is no computer with the name '$ComputerName' in the lab"
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'BySession')
        {
            $internalSession = $Session
            $Machine = Get-LabVM -ComputerName $internalSession.LabMachineName -IncludeLinux
        }

        foreach ($m in $Machine)
        {
            $machineRetries = $Retries

            if ($Credential)
            {
                $cred = $Credential
            }
            elseif ($UseLocalCredential -and ($m.IsDomainJoined -and -not $m.HasDomainJoined))
            {
                $cred = $m.GetLocalCredential($true)
            }
            elseif ($UseLocalCredential)
            {
                $cred = $m.GetLocalCredential()
            }
            else
            {
                $cred = $m.GetCredential($lab)
            }

            $param = @{}
            $param.Add('Name', "$($m)_$([guid]::NewGuid())")
            $param.Add('Credential', $cred)

            if ($DoNotUseCredSsp)
            {
                $param.Add('Authentication', 'Default')
            }
            else
            {
                $param.Add('Authentication', 'Credssp')
            }

            if ($m.HostType -eq 'Azure')
            {
                try
                {
                    $azConInfResolved = [System.Net.Dns]::GetHostByName($m.AzureConnectionInfo.DnsName)
                }
                catch
                {

                }

                if (-not $m.AzureConnectionInfo.DnsName -or -not $azConInfResolved)
                {
                    $m.AzureConnectionInfo = Get-LWAzureVMConnectionInfo -ComputerName $m
                }

                $param.Add('ComputerName', $m.AzureConnectionInfo.DnsName)
                Write-PSFMessage "Azure DNS name for machine '$m' is '$($m.AzureConnectionInfo.DnsName)'"
                $param.Add('Port', $m.AzureConnectionInfo.Port)
                if ($UseSSL)
                {
                    $param.Add('SessionOption', (New-CimSessionOption -SkipCACheck -SkipCNCheck -UseSsl))
                }
            }
            elseif ($m.HostType -eq 'HyperV' -or $m.HostType -eq 'VMWare')
            {
                $doNotUseGetHostEntry = Get-LabConfigurationItem -Name DoNotUseGetHostEntryInNewLabPSSession
                if (-not $doNotUseGetHostEntry)
                {
                    $name = (Get-HostEntry -Hostname $m).IpAddress.IpAddressToString
                }
                elseif ($doNotUseGetHostEntry -or -not [string]::IsNullOrEmpty($m.FriendlyName) -or (Get-LabConfigurationItem -Name SkipHostFileModification))
                {
                    $name = $m.IpV4Address
                }

                if ($name)
                {
                    Write-PSFMessage "Connecting to machine '$m' using the IP address '$name'"
                    $param.Add('ComputerName', $name)
                }
                else
                {
                    Write-PSFMessage "Connecting to machine '$m' using the DNS name '$m'"
                    $param.Add('ComputerName', $m)
                }
                $param.Add('Port', 5985)
            }

            if ($m.OperatingSystemType -eq 'Linux')
            {
                Set-Item -Path WSMan:\localhost\Client\Auth\Basic -Value $true -Force
                $param['SessionOption'] = New-CimSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -UseSsl
                $param['Port'] = 5986
                $param['Authentication'] = 'Basic'
            }

            if ($IsLinux -or $IsMacOs)
            {
                $param['Authentication'] = 'Negotiate'
            }

            Write-PSFMessage ("Creating a new CIM Session to machine '{0}:{1}' (UserName='{2}', Password='{3}', DoNotUseCredSsp='{4}')" -f $param.ComputerName, $param.Port, $cred.UserName, $cred.GetNetworkCredential().Password, $DoNotUseCredSsp)

            #session reuse. If there is a session to the machine available, return it, otherwise create a new session
            $internalSession = Get-CimSession | Where-Object {
                $_.ComputerName -eq $param.ComputerName -and
                $_.TestConnection() -and
                $_.Name -like "$($m)_*"
            }

            if ($internalSession)
            {
                if ($internalSession.Runspace.ConnectionInfo.AuthenticationMechanism -eq 'CredSsp' -and (Get-LabVM -ComputerName $internalSession.LabMachineName).HostType -eq 'Azure' -and -not $lab.AzureSettings.IsAzureStack)
                {
                    #remove the existing session if connecting to Azure LabSource did not work in case the session connects to an Azure VM.
                    Write-ScreenInfo "Removing session to '$($internalSession.LabMachineName)' as ALLabSourcesMapped was false" -Type Warning
                    Remove-LabCimSession -ComputerName $internalSession.LabMachineName
                    $internalSession = $null
                }

                if ($internalSession.Count -eq 1)
                {
                    Write-PSFMessage "Session $($internalSession.Name) is available and will be reused"
                    $sessions += $internalSession
                }
                elseif ($internalSession.Count -ne 0)
                {
                    $sessionsToRemove = $internalSession | Select-Object -Skip (Get-LabConfigurationItem -Name MaxPSSessionsPerVM)
                    Write-PSFMessage "Found orphaned sessions. Removing $($sessionsToRemove.Count) sessions: $($sessionsToRemove.Name -join ', ')"
                    $sessionsToRemove | Remove-CimSession

                    Write-PSFMessage "Session $($internalSession[0].Name) is available and will be reused"
                    #Replaced Select-Object with array indexing because of https://github.com/PowerShell/PowerShell/issues/9185
                    $sessions += ($internalSession | Where-Object State -eq 'Opened')[0] #| Select-Object -First 1
                }
            }

            while (-not $internalSession -and $machineRetries -gt 0)
            {
                Write-PSFMessage "Testing port $($param.Port) on computer '$($param.ComputerName)'"
                $portTest = Test-Port -ComputerName $param.ComputerName -Port $param.Port -TCP -TcpTimeout $testPortTimeout
                if ($portTest.Open)
                {
                    Write-PSFMessage 'Port was open, trying to create the session'
                    $internalSession = New-CimSession @param -ErrorAction SilentlyContinue -ErrorVariable sessionError
                    $internalSession | Add-Member -Name LabMachineName -MemberType ScriptProperty -Value { $this.Name.Substring(0, $this.Name.IndexOf('_')) }

                    if ($internalSession)
                    {
                        Write-PSFMessage "Session to computer '$($param.ComputerName)' created"
                        $sessions += $internalSession
                    }
                    else
                    {
                        Write-PSFMessage -Message "Session to computer '$($param.ComputerName)' could not be created, waiting $Interval seconds ($machineRetries retries). The error was: '$($sessionError[0].FullyQualifiedErrorId)'"
                        if ($Retries -gt 1) { Start-Sleep -Seconds $Interval }
                        $machineRetries--
                    }
                }
                else
                {
                    Write-PSFMessage 'Port was NOT open, cannot create session.'
                    Start-Sleep -Seconds $Interval
                    $machineRetries--
                }
            }

            if (-not $internalSession)
            {
                if ($sessionError.Count -gt 0)
                {
                    Write-Error -ErrorRecord $sessionError[0]
                }
                elseif ($machineRetries -lt 1)
                {
                    if (-not $portTest.Open)
                    {
                        Write-Error -Message "Could not create a session to machine '$m' as the port is closed after $Retries retries."
                    }
                    else
                    {
                        Write-Error -Message "Could not create a session to machine '$m' after $Retries retries."
                    }
                }
            }
        }
    }

    end
    {
        Write-LogFunctionExit -ReturnValue "Session IDs: $(($sessions.ID -join ', '))"
        $sessions
    }
}


function New-LabPSSession
{
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0)]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByMachine')]
        [AutomatedLab.Machine[]]$Machine,

        #this is used to recreate a broken session
        [Parameter(Mandatory, ParameterSetName = 'BySession')]
        [System.Management.Automation.Runspaces.PSSession]$Session,

        [switch]$UseLocalCredential,

        [switch]$DoNotUseCredSsp,

        [pscredential]$Credential,

        [int]$Retries = 2,

        [int]$Interval = 5,

        [switch]$UseSSL,

        [switch]$IgnoreAzureLabSources
    )

    begin
    {
        Write-LogFunctionEntry
        $sessions = @()
        $lab = Get-Lab

        #Due to a problem in Windows 10 not being able to reach VMs from the host
        if (-not ($IsLinux -or $IsMacOs)) { netsh.exe interface ip delete arpcache | Out-Null }
        $testPortTimeout = (Get-LabConfigurationItem -Name Timeout_TestPortInSeconds) * 1000

        $jitTs = Get-LabConfigurationItem AzureJitTimestamp
        if ((Get-LabConfigurationItem -Name AzureEnableJit) -and $lab.DefaultVirtualizationEngine -eq 'Azure' -and (-not $jitTs -or ((Get-Date) -ge $jitTs)) )
        {
            # Either JIT has not been requested, or current date exceeds timestamp
            Request-LabAzureJitAccess
        }
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByName')
        {
            $Machine = Get-LabVM -ComputerName $ComputerName -IncludeLinux

            if (-not $Machine)
            {
                Write-Error "There is no computer with the name '$ComputerName' in the lab"
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'BySession')
        {
            $internalSession = $Session
            $Machine = Get-LabVM -ComputerName $internalSession.LabMachineName -IncludeLinux

            if ($internalSession.Runspace.ConnectionInfo.AuthenticationMechanism -ne 'Credssp')
            {
                $DoNotUseCredSsp = $true
            }
            if ($internalSession.Runspace.ConnectionInfo.Credential.UserName -like "$($Machine.Name)*")
            {
                $UseLocalCredential = $true
            }
        }

        foreach ($m in $Machine)
        {
            $machineRetries = $Retries
            $connectionName = $m.Name

            if ($Credential)
            {
                $cred = $Credential
            }
            elseif ($UseLocalCredential -and (($IsLinux -or -not [string]::IsNullOrWhiteSpace($m.SshPrivateKeyPath)) -and $m.IsDomainJoined -and -not $m.HasDomainJoined))
            {
                $cred = $m.GetLocalCredential($true)
            }
            elseif ($UseLocalCredential)
            {
                $cred = $m.GetLocalCredential()
            }
            elseif (($IsLinux -or -not [string]::IsNullOrWhiteSpace($m.SshPrivateKeyPath)) -and $m.IsDomainJoined -and -not $m.HasDomainJoined)
            {
                $cred = $m.GetLocalCredential($true)
            }
            else
            {
                $cred = $m.GetCredential($lab)
            }

            $param = @{}
            $param.Add('Name', "$($m)_$([guid]::NewGuid())")
            $param.Add('Credential', $cred)
            $param.Add('UseSSL', $false)

            if ($DoNotUseCredSsp)
            {
                $param.Add('Authentication', 'Default')
            }
            else
            {
                $param.Add('Authentication', 'Credssp')
            }

            if ($m.HostType -eq 'Azure')
            {
                try
                {
                    $azConInfResolved = [System.Net.Dns]::GetHostByName($m.AzureConnectionInfo.DnsName)
                }
                catch
                {

                }

                if (-not $m.AzureConnectionInfo.DnsName -or -not $azConInfResolved)
                {
                    $m.AzureConnectionInfo = Get-LWAzureVMConnectionInfo -ComputerName $m
                }

                $param.Add('ComputerName', $m.AzureConnectionInfo.DnsName)
                $connectionName = $m.AzureConnectionInfo.DnsName
                Write-PSFMessage "Azure DNS name for machine '$m' is '$($m.AzureConnectionInfo.DnsName)'"
                $param.Add('Port', $m.AzureConnectionInfo.Port)
                if ($UseSSL)
                {
                    $param.Add('SessionOption', (New-PSSessionOption -SkipCACheck -SkipCNCheck))
                    $param.UseSSL = $true
                }
            }
            elseif ($m.HostType -eq 'HyperV' -or $m.HostType -eq 'VMWare')
            {
                # DoNotUseGetHostEntryInNewLabPSSession is used when existing DNS is possible
                # SkipHostFileModification is used when the local hosts file should not be used
                $doNotUseGetHostEntry = Get-LabConfigurationItem -Name DoNotUseGetHostEntryInNewLabPSSession
                if (-not $doNotUseGetHostEntry)
                {
                    $name = (Get-HostEntry -Hostname $m).IpAddress.IpAddressToString
                }
                elseif ($doNotUseGetHostEntry -or -not [string]::IsNullOrEmpty($m.FriendlyName) -or (Get-LabConfigurationItem -Name SkipHostFileModification))
                {
                    $name = $m.IpV4Address
                }

                if ($name)
                {
                    Write-PSFMessage "Connecting to machine '$m' using the IP address '$name'"
                    $param.Add('ComputerName', $name)
                    $connectionName = $name
                }
                else
                {
                    Write-PSFMessage "Connecting to machine '$m' using the DNS name '$m'"
                    $param.Add('ComputerName', $m)
                    $connectionName = $m.Name
                }
                $param.Add('Port', 5985)
            }

            if (((Get-Command New-PSSession).Parameters.Values.Name -notcontains 'HostName') -and -not [string]::IsNullOrWhiteSpace($m.SshPrivateKeyPath))
            {
                Write-ScreenInfo -Type Warning -Message "SSH Transport is not available from within Windows PowerShell."
            }

            if (((Get-Command New-PSSession).Parameters.Values.Name -contains 'HostName') -and -not [string]::IsNullOrWhiteSpace($m.SshPrivateKeyPath))
            {                
                Write-PSFMessage -Message "Using private key from $($m.SshPrivateKeyPath) to connect to $($m.Name)"
                $param['HostName'] = $param['ComputerName']
                $param.Remove('ComputerName')
                $param.Remove('PSSessionOption')
                $param.Remove('Authentication')
                $param.Remove('Credential')
                $param.Remove('UseSsl')
                $param['KeyFilePath'] = $m.SshPrivateKeyPath
                $param['Port'] = if ($m.HostType -eq 'Azure') {$m.AzureConnectionInfo.SshPort} else { 22 }
                $param['UserName'] = $cred.UserName.Replace("$($m.Name)\", '')
            }
            elseif ($m.OperatingSystemType -eq 'Linux')
            {
                Set-Item -Path WSMan:\localhost\Client\Auth\Basic -Value $true -Force
                $param['SessionOption'] = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
                $param['UseSSL'] = $true
                $param['Port'] = if ($m.HostType -eq 'Azure') {$m.AzureConnectionInfo.HttpsPort} else { 5986 }
                $param['Authentication'] = 'Basic'
            }

            if (($IsLinux -or $IsMacOs) -and [string]::IsNullOrWhiteSpace($m.SshPrivateKeyPath))
            {
                $param['Authentication'] = 'Negotiate'
            }

            Write-PSFMessage ("Creating a new PSSession to machine '{0}:{1}' (UserName='{2}', Password='{3}', DoNotUseCredSsp='{4}')" -f $connectionName, $param.Port, $cred.UserName, $cred.GetNetworkCredential().Password, $DoNotUseCredSsp)

            #session reuse. If there is a session to the machine available, return it, otherwise create a new session
            $internalSession = Get-PSSession | Where-Object {
                ($_.ComputerName -eq $param.ComputerName -or $_.ComputerName -eq $param.HostName) -and
                ($_.Runspace.ConnectionInfo.Port -eq $param.Port -or $_.Transport -eq 'SSH') -and
                $_.Availability -eq 'Available' -and
                ($_.Runspace.ConnectionInfo.AuthenticationMechanism -eq $param.Authentication -or $_.Transport -eq 'SSH') -and
                $_.State -eq 'Opened' -and
                $_.Name -like "$($m)_*" -and
                ($_.Runspace.ConnectionInfo.Credential.UserName -eq $param.Credential.UserName -or $_.Runspace.ConnectionInfo.UserName -eq $param.Credential.UserName)
            }

            if ($internalSession)
            {
                if ($internalSession.Runspace.ConnectionInfo.AuthenticationMechanism -eq 'CredSsp' -and
                    -not $IgnoreAzureLabSources.IsPresent -and -not $internalSession.ALLabSourcesMapped -and
                    (Get-LabVM -ComputerName $internalSession.LabMachineName).HostType -eq 'Azure' -and
                    -not $lab.AzureSettings.IsAzureStack
                )
                {
                    #remove the existing session if connecting to Azure LabSource did not work in case the session connects to an Azure VM.
                    Write-ScreenInfo "Removing session to '$($internalSession.LabMachineName)' as ALLabSourcesMapped was false" -Type Warning
                    Remove-LabPSSession -ComputerName $internalSession.LabMachineName
                    $internalSession = $null
                }

                if ($internalSession.Count -eq 1)
                {
                    Write-PSFMessage "Session $($internalSession.Name) is available and will be reused"
                    $sessions += $internalSession
                }
                elseif ($internalSession.Count -ne 0)
                {
                    $sessionsToRemove = $internalSession | Select-Object -Skip (Get-LabConfigurationItem -Name MaxPSSessionsPerVM)
                    Write-PSFMessage "Found orphaned sessions. Removing $($sessionsToRemove.Count) sessions: $($sessionsToRemove.Name -join ', ')"
                    $sessionsToRemove | Remove-PSSession

                    Write-PSFMessage "Session $($internalSession[0].Name) is available and will be reused"
                    #Replaced Select-Object with array indexing because of https://github.com/PowerShell/PowerShell/issues/9185
                    $sessions += ($internalSession | Where-Object State -eq 'Opened')[0] #| Select-Object -First 1
                }
            }

            while (-not $internalSession -and $machineRetries -gt 0)
            {
                if (-not ($IsLinux -or $IsMacOs)) { netsh.exe interface ip delete arpcache | Out-Null }

                Write-PSFMessage "Testing port $($param.Port) on computer '$connectionName'"
                $portTest = Test-Port -ComputerName $connectionName -Port $param.Port -TCP -TcpTimeout $testPortTimeout
                if ($portTest.Open)
                {
                    Write-PSFMessage 'Port was open, trying to create the session'
                    if ($param.HostName -and -not (Get-Item -ErrorAction SilentlyContinue "$home/.ssh/known_hosts" | Select-String -Pattern $param.HostName.Replace('.','\.')))
                    {
                        Install-LabSshKnownHost # First connect
                    }
                    $internalSession = New-PSSession @param -ErrorAction SilentlyContinue -ErrorVariable sessionError
                    $internalSession | Add-Member -Name LabMachineName -MemberType ScriptProperty -Value { $this.Name.Substring(0, $this.Name.IndexOf('_')) }

                    # Additional check here for availability/state due to issues with Azure IaaS
                    if ($internalSession -and $internalSession.Availability -eq 'Available' -and $internalSession.State -eq 'Opened')
                    {
                        Write-PSFMessage "Session to computer '$connectionName' created"
                        $sessions += $internalSession

                        if ((Get-LabVM -ComputerName $internalSession.LabMachineName).HostType -eq 'Azure')
                        {
                            Connect-LWAzureLabSourcesDrive -Session $internalSession
                        }

                    }
                    else
                    {
                        Write-PSFMessage -Message "Session to computer '$connectionName' could not be created, waiting $Interval seconds ($machineRetries retries). The error was: '$($sessionError[0].FullyQualifiedErrorId)'"
                        if ($Retries -gt 1) { Start-Sleep -Seconds $Interval }
                        $machineRetries--
                    }
                }
                else
                {
                    Write-PSFMessage 'Port was NOT open, cannot create session.'
                    Start-Sleep -Seconds $Interval
                    $machineRetries--
                }
            }

            if (-not $internalSession)
            {
                if ($sessionError.Count -gt 0)
                {
                    Write-Error -ErrorRecord $sessionError[0]
                }
                elseif ($machineRetries -lt 1)
                {
                    if (-not $portTest.Open)
                    {
                        Write-Error -Message "Could not create a session to machine '$m' as the port is closed after $Retries retries."
                    }
                    else
                    {
                        Write-Error -Message "Could not create a session to machine '$m' after $Retries retries."
                    }
                }
            }
        }
    }

    end
    {
        Write-LogFunctionExit -ReturnValue "Session IDs: $(($sessions.ID -join ', '))"
        $sessions
    }
}


function Remove-LabCimSession
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string[]]
        $ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByMachine')]
        [AutomatedLab.Machine[]]
        $Machine,

        [Parameter(ParameterSetName = 'All')]
        [switch]
        $All
    )

    Write-LogFunctionEntry

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        $Machine = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }
    if ($PSCmdlet.ParameterSetName -eq 'All')
    {
        $Machine = Get-LabVM -All -IncludeLinux
    }

    $sessions = foreach ($m in $Machine)
    {
        $param = @{}
        if ($m.HostType -eq 'Azure')
        {
            $param.Add('ComputerName', $m.AzureConnectionInfo.DnsName)
            $param.Add('Port', $m.AzureConnectionInfo.Port)
        }
        elseif ($m.HostType -eq 'HyperV' -or $m.HostType -eq 'VMWare')
        {
            if (Get-LabConfigurationItem -Name DoNotUseGetHostEntryInNewLabPSSession)
            {
                $param.Add('ComputerName', $m.Name)
            }
            elseif (Get-LabConfigurationItem -Name SkipHostFileModification)
            {
                $param.Add('ComputerName', $m.IpV4Address)
            }
            else
            {
                $param.Add('ComputerName', (Get-HostEntry -Hostname $m).IpAddress.IpAddressToString)
            }
            $param.Add('Port', 5985)
        }

        Get-CimSession | Where-Object {
            $_.ComputerName -eq $param.ComputerName -and
        $_.Name -like "$($m)_*" }
    }

    $sessions | Remove-CimSession -ErrorAction SilentlyContinue

    Write-PSFMessage "Removed $($sessions.Count) PSSessions..."
    Write-LogFunctionExit
}


function Remove-LabPSSession
{
    [cmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByMachine')]
        [AutomatedLab.Machine[]]$Machine,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        $Machine = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    }
    if ($PSCmdlet.ParameterSetName -eq 'All')
    {
        $Machine = Get-LabVM -All -IncludeLinux
    }

    $sessions = foreach ($m in $Machine)
    {
        $param = @{}
        if ($m.HostType -eq 'Azure')
        {
            $param.Add('ComputerName', $m.AzureConnectionInfo.DnsName)
            $param.Add('Port', $m.AzureConnectionInfo.Port)
        }
        elseif ($m.HostType -eq 'HyperV' -or $m.HostType -eq 'VMWare')
        {
            $doNotUseGetHostEntry = Get-LabConfigurationItem -Name DoNotUseGetHostEntryInNewLabPSSession
            if (-not $doNotUseGetHostEntry)
            {
                $name = (Get-HostEntry -Hostname $m).IpAddress.IpAddressToString
            }
            elseif ($doNotUseGetHostEntry -or -not [string]::IsNullOrEmpty($m.FriendlyName) -or (Get-LabConfigurationItem -Name SkipHostFileModification))
            {
                $name = $m.IpV4Address
            }
            $param['ComputerName'] = $name
            $param['Port'] = 5985
        }

        if (((Get-Command New-PSSession).Parameters.Values.Name -contains 'HostName') )
        {
            $param['HostName'] = $param['ComputerName']
            $param['Port'] = if ($m.HostType -eq 'Azure') {$m.AzureConnectionInfo.SshPort} else { 22 }
            $param.Remove('ComputerName')
            $param.Remove('PSSessionOption')
            $param.Remove('Authentication')
            $param.Remove('Credential')
            $param.Remove('UseSsl')
        }

        Get-PSSession | Where-Object {
            (($_.ComputerName -eq $param.ComputerName) -or ($_.ComputerName -eq $param.HostName)) -and
            ($_.Runspace.ConnectionInfo.Port -eq $param.Port -or ($param.HostName -and $_.Transport -eq 'SSH')) -and
        $_.Name -like "$($m)_*" }
    }

    $sessions | Remove-PSSession -ErrorAction SilentlyContinue

    Write-PSFMessage "Removed $($sessions.Count) PSSessions..."
    Write-LogFunctionExit
}


function Uninstall-LabRdsCertificate
{
    [CmdletBinding()]
    param ( )

    $lab = Get-Lab
    if (-not $lab)
    {
        return
    }

    foreach ($certFile in (Get-ChildItem -File -Path (Join-Path -Path $lab.LabPath -ChildPath Certificates) -Filter *.cer -ErrorAction SilentlyContinue))
    {
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certFile.FullName)
        if ($cert.Thumbprint)
        {
            Get-Item -Path ('Cert:\LocalMachine\Root\{0}' -f $cert.Thumbprint) | Remove-Item
        }

        $certFile | Remove-Item
    }
}


function UnInstall-LabSshKnownHost
{
    [CmdletBinding()]
    param ( )

    if (-not (Test-Path -Path $home/.ssh/known_hosts)) { return }

    $lab = Get-Lab
    if (-not $lab) { return }

    $machines = Get-LabVM -All -IncludeLinux | Where-Object -FilterScript { -not $_.SkipDeployment }
    if (-not $machines) { return }

    $content = Get-Content -Path $home/.ssh/known_hosts
    foreach ($machine in $machines)
    {
        if ($lab.DefaultVirtualizationEngine -eq 'Azure')
        {
            $content = $content | Where {$_ -notmatch "$($machine.AzureConnectionInfo.DnsName.Replace('.','\.'))"}
            $content = $content | Where {$_ -notmatch "$($machine.AzureConnectionInfo.VIP.Replace('.','\.'))"}
        }
        else
        {
            $content = $content | Where {$_ -notmatch "$($machine.Name)\s.*"}
            if ($machine.IpV4Address)
            {
                $content = $content | Where {$_ -notmatch "$($machine.Ipv4Address.Replace('.','\.'))"}
            }
        }
    }
    $content | Set-Content -Path $home/.ssh/known_hosts
}


function Enable-LabInternalRouting
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $RoutingNetworkName
    )

    Write-LogFunctionEntry

    $routes = Get-FullMesh -List (Get-Lab).VirtualNetworks.Where( { $_.Name -ne $RoutingNetworkName }).AddressSpace.Foreach( { '{0}/{1}' -f $_.Network, $_.Cidr })
    $routers = Get-LabVm -Role Routing
    $routingConfig = @{}
    foreach ($router in $routers)
    {
        $routerAdapter = (Get-LabVM $router).NetworkAdapters.Where( { $_.VirtualSwitch.Name -eq $RoutingNetworkName })
        $routerInternalAdapter = (Get-LabVM $router).NetworkAdapters.Where( { $_.VirtualSwitch.Name -ne $RoutingNetworkName })
        $routingConfig[$router.Name] = @{
            Name          = $router.Name
            InterfaceName = $routerAdapter.InterfaceName
            RouterNetwork = [string[]]$routerInternalAdapter.IPV4Address
            TargetRoutes  = @{}
        }
    }

    foreach ($router in $routers)
    {
        $targetRoutes = $routes | Where-Object Source -in $routingConfig[$router.Name].RouterNetwork
        foreach ($route in $targetRoutes)
        {
            $nextHopVm = Get-LabVm $routingConfig.Values.Where( { $_.RouterNetwork -eq $route.Destination }).Name
            $nextHopIp = $nextHopVm.NetworkAdapters.Where( { $_.VirtualSwitch.Name -eq $RoutingNetworkName }).Ipv4Address.IPaddress.AddressAsString
            Write-ScreenInfo -Type Verbose -Message "Route on $($router.Name) to $($route.Destination) via $($nextHopVm.Name)($($nextHopIp))"
            $routingConfig[$router.Name].TargetRoutes[$route.Destination] = $nextHopIp
        }
    }

    Invoke-LabCommand -ComputerName $routers -ActivityName "Creating routes" -ScriptBlock {
        Install-RemoteAccess -VpnType RoutingOnly
        $config = $routingConfig[$env:COMPUTERNAME]
        
        foreach ($route in $config.TargetRoutes.GetEnumerator())
        {
            New-NetRoute -InterfaceAlias $config.InterfaceName -DestinationPrefix $route.Key -AddressFamily IPv4 -NextHop $route.Value -Publish Yes
        }
    } -Variable (Get-Variable routingConfig)

    Write-LogFunctionExit
}


function Install-LabRouting
{
    [CmdletBinding()]
    param (
        [int]$InstallationTimeout = 15,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator)
    )

    Write-LogFunctionEntry

    if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

    $roleName = [AutomatedLab.Roles]::Routing

    if (-not (Get-LabVM))
    {
        Write-ScreenInfo -Message 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first' -Type Warning
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM -Role $roleName | Where-Object HostType -eq 'HyperV'

    if (-not $machines)
    {
        return
    }

    Write-ScreenInfo -Message 'Waiting for machines with Routing Role to startup' -NoNewline
    Start-LabVM -RoleName $roleName -Wait -ProgressIndicator 15

    Write-ScreenInfo -Message 'Configuring Routing role...' -NoNewLine
    $jobs = Install-LabWindowsFeature -ComputerName $machines -FeatureName RSAT ,Routing, RSAT-RemoteAccess -IncludeAllSubFeature -NoDisplay -AsJob -PassThru
    Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -Timeout 15 -NoDisplay -NoNewLine

    Restart-LabVM -ComputerName $machines -Wait -NoDisplay

    $jobs = @()

    foreach ($machine in $machines)
    {
        $externalAdapters = $machine.NetworkAdapters | Where-Object { $_.VirtualSwitch.SwitchType -eq 'External' }

        if ($externalAdapters.Count -gt 1)
        {
            Write-Error "Automatic configuration of routing can only be done if there is 0 or 1 network adapter connected to an external network switch. The machine '$machine' knows about $($externalAdapters.Count) externally connected adapters"
            continue
        }

        if ($externalAdapters)
        {
            $mac = $machine.NetworkAdapters | Where-Object { $_.VirtualSwitch.SwitchType -eq 'External' } | Select-Object -ExpandProperty MacAddress
            $mac = ($mac | Get-StringSection -SectionSize 2) -join ':'
        }

        $parameters = @{}
        $parameters.Add('ComputerName', $machine)
        $parameters.Add('ActivityName', 'ConfigurationRouting')
        $parameters.Add('Verbose', $VerbosePreference)
        $parameters.Add('Scriptblock', {
                $VerbosePreference = 'Continue'
                Write-Verbose 'Setting up routing...'

                Set-Service -Name RemoteAccess -StartupType Automatic
                Start-Service -Name RemoteAccess

                Write-Verbose '...done'

                if (-not $args[0])
                {
                    Write-Verbose 'No externally connected adapter available'
                    return
                }

                Write-Verbose 'Setting up NAT...'

                $externalAdapter = Get-CimInstance -Class Win32_NetworkAdapter -Filter ('MACAddress = "{0}"' -f $args[0]) |
                    Select-Object -ExpandProperty NetConnectionID

                netsh.exe routing ip nat install

                netsh.exe routing ip nat add interface $externalAdapter

                netsh.exe routing ip nat set interface $externalAdapter mode=full

                netsh.exe ras set conf confstate = enabled

                netsh.exe routing ip dnsproxy install

                Restart-Service -Name RemoteAccess

                Write-Verbose '...done'
            }
        )
        $parameters.Add('ArgumentList', $mac)

        $jobs += Invoke-LabCommand @parameters -AsJob -PassThru -NoDisplay
    }

    if (Get-LabVM -Role RootDC)
    {
        Write-PSFMessage "This lab knows about an Active Directory, calling 'Set-LabADDNSServerForwarder'"
        Set-LabADDNSServerForwarder
    }

    Write-ScreenInfo -Message 'Waiting for configuration of routing to complete' -NoNewline

    Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -Timeout $InstallationTimeout -NoDisplay -NoNewLine

    #to make sure the routing service works, restart the routers
    Write-PSFMessage "Restarting machines '$($machines -join ', ')'"
    Restart-LabVM -ComputerName $machines -Wait -NoNewLine

    Write-ProgressIndicatorEnd
    Write-LogFunctionExit
}


function Install-LabScom
{
    [CmdletBinding()]
    param ( )

    Write-LogFunctionEntry

    # defaults
    $iniManagementServer = @{
        ManagementGroupName           = 'SCOM2019'
        SqlServerInstance             = ''
        SqlInstancePort               = '1433'
        DatabaseName                  = 'OperationsManager'
        DwSqlServerInstance           = ''
        InstallLocation               = 'C:\Program Files\Microsoft System Center\Operations Manager'
        DwSqlInstancePort             = '1433'
        DwDatabaseName                = 'OperationsManagerDW'
        ActionAccountUser             = 'OM19AA'
        ActionAccountPassword         = ''
        DASAccountUser                = 'OM19DAS' 
        DASAccountPassword            = ''
        DataReaderUser                = 'OM19READ'
        DataReaderPassword            = ''
        DataWriterUser                = 'OM19WRITE'
        DataWriterPassword            = ''
        EnableErrorReporting          = 'Never'
        SendCEIPReports               = '0'
        UseMicrosoftUpdate            = '0'
        AcceptEndUserLicenseAgreement = '1'
        ProductKey                    = ''        
    }

    $iniAddManagementServer = @{
        SqlServerInstance             = ''
        SqlInstancePort               = '1433'
        DatabaseName                  = 'OperationsManager'
        InstallLocation               = 'C:\Program Files\Microsoft System Center\Operations Manager'
        ActionAccountUser             = 'OM19AA'
        ActionAccountPassword         = ''
        DASAccountUser                = 'OM19DAS' 
        DASAccountPassword            = ''
        DataReaderUser                = 'OM19READ'
        DataReaderPassword            = ''
        DataWriterUser                = 'OM19WRITE'
        DataWriterPassword            = ''
        EnableErrorReporting          = 'Never'
        SendCEIPReports               = '0'
        AcceptEndUserLicenseAgreement = '1'
        UseMicrosoftUpdate            = '0'
    }

    $iniNativeConsole = @{
        EnableErrorReporting          = 'Never'
        InstallLocation               = 'C:\Program Files\Microsoft System Center\Operations Manager'
        SendCEIPReports               = '0'
        UseMicrosoftUpdate            = '0'
        AcceptEndUserLicenseAgreement = '1'
    }

    $iniWebConsole = @{
        ManagementServer              = ''
        WebSiteName                   = 'Default Web Site'
        WebConsoleAuthorizationMode   = 'Mixed'
        SendCEIPReports               = '0'
        UseMicrosoftUpdate            = '0'
        AcceptEndUserLicenseAgreement = '1'
    }

    $iniReportServer = @{
        ManagementServer              = ''
        SRSInstance                   = ''
        DataReaderUser                = 'OM19READ'
        InstallLocation               = 'C:\Program Files\Microsoft System Center\Operations Manager'
        DataReaderPassword            = ''
        SendODRReports                = '0'
        UseMicrosoftUpdate            = '0'
        AcceptEndUserLicenseAgreement = '1'
    }
    
    $lab = Get-Lab
    $all = Get-LabVM -Role Scom
    $scomConsoleRole = Get-LabVM -Role ScomConsole
    $scomManagementServer = Get-LabVm -Role ScomManagement
    $firstmgmt = $scomManagementServer | Select-Object -First 1
    $addtlmgmt = $scomManagementServer | Select-Object -Skip 1
    $scomWebConsoleRole = Get-LabVM -Role ScomWebConsole
    $scomReportingServer = Get-LabVm -Role ScomReporting

    Start-LabVM -ComputerName $all -Wait

    Invoke-LabCommand -ComputerName $all -ScriptBlock {
        if (-not (Test-Path C:\DeployDebug))
        {
            $null = New-Item -ItemType Directory -Path C:\DeployDebug
        }

        $null = New-Item -ItemType Directory -Path HKLM:\software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters -Force
        # Yup, this Setup requires RC4 enabled to be able to "resolve" accounts
        $null = Set-ItemProperty HKLM:\software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters -Name SupportedEncryptionTypes -Value 0x7fffffff
    }

    Restart-LabVM -ComputerName $all -Wait

    # Prerequisites, all
    $odbc = Get-LabConfigurationItem -Name SqlOdbc13
    $SQLSysClrTypes = Get-LabConfigurationItem -Name SqlClrType2014
    $ReportViewer = Get-LabConfigurationItem -Name ReportViewer2015
    $odbcFile = Get-LabInternetFile -Uri $odbc -Path $labsources\SoftwarePackages -FileName odbc.msi -PassThru
    $SQLSysClrTypesFile = Get-LabInternetFile -uri $SQLSysClrTypes -Path $labsources\SoftwarePackages -FileName SQLSysClrTypes.msi -PassThru
    $ReportViewerFile = Get-LabInternetFile -uri $ReportViewer -Path $labsources\SoftwarePackages -FileName ReportViewer.msi -PassThru
    Install-LabSoftwarePackage -Path $odbcFile.FullName -ComputerName $all -CommandLine '/QN ADDLOCAL=ALL IACCEPTMSODBCSQLLICENSETERMS=YES /L*v C:\odbc.log' -NoDisplay
    
    if (Get-LabVm -Role ScomConsole, ScomWebConsole)
    {
        Install-LabSoftwarePackage -path $SQLSysClrTypesFile.FullName -ComputerName (Get-LabVm -Role ScomConsole, ScomWebConsole) -CommandLine '/quiet /norestart /log C:\DeployDebug\SQLSysClrTypes.log' -NoDisplay
        Install-LabSoftwarePackage -path $ReportViewerFile.FullName -ComputerName (Get-LabVm -Role ScomConsole, ScomWebConsole) -CommandLine '/quiet /norestart /log C:\DeployDebug\ReportViewer.log' -NoDisplay
        Install-LabWindowsFeature -Computername (Get-LabVm -Role ScomConsole, ScomWebConsole) NET-WCF-HTTP-Activation45, Web-Static-Content, Web-Default-Doc, Web-Dir-Browsing, Web-Http-Errors, Web-Http-Logging, Web-Request-Monitor, Web-Filtering, Web-Stat-Compression, Web-Mgmt-Console, Web-Metabase, Web-Asp-Net, Web-Windows-Auth  -NoDisplay
    }

    if ($scomReportingServer)
    {
        Invoke-LabCommand -ComputerName $scomReportingServer -ScriptBlock {
            Get-Service -Name SQLSERVERAGENT* | Set-Service -StartupType Automatic -Status Running
        } -NoDisplay
    }

    # Extract SCOM on all machines
    $scomIso = ($lab.Sources.ISOs | Where-Object { $_.Name -like 'Scom*' }).Path
    $isos = Mount-LabIsoImage -ComputerName $all -IsoPath $scomIso -SupressOutput -PassThru
    Invoke-LabCommand -ComputerName $all -Variable (Get-Variable isos) -ActivityName 'Extracting SCOM Server' -ScriptBlock {
        $setup = Get-ChildItem -Path $($isos | Where InternalComputerName -eq $env:COMPUTERNAME).DriveLetter -Filter *.exe | Select-Object -First 1
        Start-Process -FilePath $setup.FullName -ArgumentList '/VERYSILENT', '/DIR=C:\SCOM' -Wait
    } -NoDisplay
    
    # Server
    $installationPaths = @{}
    $jobs = foreach ($vm in $firstmgmt)
    {
        $iniManagement = $iniManagementServer.Clone()
        $role = $vm.Roles | Where-Object Name -eq ScomManagement

        foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*Password' }))
        {
            $iniManagement[$kvp.Key] = $vm.GetCredential((Get-Lab)).GetNetworkCredential().Password # Default lab credential
        }

        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniManagement.ContainsKey($property.Key)) { continue }
            $iniManagement[$property.Key] = $property.Value
        }

        if ($role.Properties.ContainsKey('ProductKey'))
        {
            $iniServer['ProductKey'] = $role.Properties['ProductKey']
        }

        # Create users/groups
        Invoke-LabCommand -ComputerName (Get-LabVm -Role RootDc | Select-Object -First 1) -ScriptBlock {
            foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*User' }))
            {
                if ($kvp.Key -like '*User')
                {
                    $userName = $kvp.Value
                    $password = $iniManagement[($kvp.Key -replace 'User', 'Password')]
                }
                $userAccount = $null # Damn AD cmdlets.

                try
                {
                    $userAccount = Get-ADUser -Identity $userName -ErrorAction Stop
                }
                catch
                { }

                if (-not $userAccount)
                {
                    $userAccount = New-ADUser -Name $userName -SamAccountName $userName -PassThru -Enabled $true -AccountPassword ($password | ConvertTo-SecureString -AsPlainText -Force)
                }
            }

            $group = $iniManagement['ScomAdminGroupName']
            if (-not $group) { return }
            try
            {
                $group = Get-ADGroup -Identity $group -ErrorAction Stop
            }
            catch {}
            if (-not $group)
            {
                New-ADGroup -Name $group -GroupScope Global -GroupType Security
            }
        } -Variable (Get-Variable iniManagement) -NoDisplay

        foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*User' }))
        {
            if ($kvp.Value.Contains('\')) { continue }
            
            $iniManagement[$kvp.Key] = '{0}\{1}' -f $vm.DomainAccountName.Split('\')[0], $kvp.Value
        }
        
        if ($iniManagement['SqlServerInstance'] -like '*\*')
        {
            $sqlMachineName = $iniManagement['SqlServerInstance'].Split('\')[0]
            $sqlMachine = Get-LabVm -ComputerName $sqlMachineName
        }

        if ($iniManagement['DwSqlServerInstance'] -like '*\*')
        {
            $sqlDwMachineName = $iniManagement['DwSqlServerInstance'].Split('\')[0]
            $sqlDwMachine = Get-LabVm -ComputerName $sqlDwMachineName
        }

        if (-not $sqlMachine)
        {
            $sqlMachine = Get-LabVm -Role SQLServer2016, SQLServer2017 | Select-Object -First 1
        }

        if (-not $sqlDwMachine)
        {
            $sqlDwMachine = Get-LabVm -Role SQLServer2016, SQLServer2017 | Select-Object -First 1
        }

        if ([string]::IsNullOrWhiteSpace($iniManagement['SqlServerInstance']))
        {
            $iniManagement['SqlServerInstance'] = $sqlMachine.Name
        }
        if ([string]::IsNullOrWhiteSpace($iniManagement['DwSqlServerInstance']))
        {
            $iniManagement['DwSqlServerInstance'] = $sqlMachine.Name
        }

        # Setup Command Line Management-Server
            
        Invoke-LabCommand -ComputerName $vm -ScriptBlock {
            Add-LocalGroupMember -Sid S-1-5-32-544 -Member $iniManagement['DASAccountUser'] -ErrorAction SilentlyContinue
        } -Variable (Get-Variable iniManagement)
        $CommandlineArgumentsServer = $iniManagement.GetEnumerator() | Where-Object Key -notin ProductKey, ScomAdminGroupName | ForEach-Object { '/{0}:"{1}"' -f $_.Key, $_.Value }
            
        $setupCommandlineServer = "/install /silent /components:OMServer $CommandlineArgumentsServer"
        Invoke-LabCommand -ComputerName $vm -ScriptBlock { Set-Content -Path C:\DeployDebug\SetupScomManagement.cmd -Value "C:\SCOM\setup.exe $setupCommandLineServer" } -Variable (Get-Variable setupCommandlineServer) -NoDisplay
        Install-LabSoftwarePackage -ComputerName $vm -LocalPath C:\SCOM\setup.exe -CommandLine $setupCommandlineServer -AsJob -PassThru -UseShellExecute -UseExplicitCredentialsForScheduledJob -AsScheduledJob -Timeout 20 -NoDisplay
        $isPrimaryManagementServer = $isPrimaryManagementServer - 1
        $installationPaths[$vm.Name] = $iniManagement.InstallLocation
    }
    
    if ($jobs)
    {
        Wait-LWLabJob -Job $jobs
    }

    $jobs = foreach ($vm in $addtlmgmt)
    {
        $iniManagement = $iniAddManagementServer.Clone()
        $role = $vm.Roles | Where-Object Name -eq ScomManagement

        foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*Password' }))
        {
            $iniManagement[$kvp.Key] = $vm.GetCredential((Get-Lab)).GetNetworkCredential().Password # Default lab credential
        }

        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniManagement.ContainsKey($property.Key)) { continue }
            $iniManagement[$property.Key] = $property.Value
        }

        if ($role.Properties.ContainsKey('ProductKey'))
        {
            $iniServer['ProductKey'] = $role.Properties['ProductKey']
        }

        foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*User' }))
        {
            if ($kvp.Value.Contains('\')) { continue }
            
            $iniManagement[$kvp.Key] = '{0}\{1}' -f $vm.DomainAccountName.Split('\')[0], $kvp.Value
        }
        
        if ($iniManagement['SqlServerInstance'] -like '*\*')
        {
            $sqlMachineName = $iniManagement['SqlServerInstance'].Split('\')[0]
            $sqlMachine = Get-LabVm -ComputerName $sqlMachineName
        }

        if ($iniManagement['DwSqlServerInstance'] -like '*\*')
        {
            $sqlDwMachineName = $iniManagement['DwSqlServerInstance'].Split('\')[0]
            $sqlDwMachine = Get-LabVm -ComputerName $sqlDwMachineName
        }

        if (-not $sqlMachine)
        {
            $sqlMachine = Get-LabVm -Role SQLServer2016, SQLServer2017 | Select-Object -First 1
        }

        if (-not $sqlDwMachine)
        {
            $sqlDwMachine = Get-LabVm -Role SQLServer2016, SQLServer2017 | Select-Object -First 1
        }

        if ([string]::IsNullOrWhiteSpace($iniManagement['SqlServerInstance']))
        {
            $iniManagement['SqlServerInstance'] = $sqlMachine.Name
        }
        if ([string]::IsNullOrWhiteSpace($iniManagement['DwSqlServerInstance']))
        {
            $iniManagement['DwSqlServerInstance'] = $sqlMachine.Name
        }
        
        # Setup Command Line Management-Server
        Invoke-LabCommand -ComputerName $vm -ScriptBlock {
            Add-LocalGroupMember -Sid S-1-5-32-544 -Member $iniManagement['DASAccountUser'] -ErrorAction SilentlyContinue
        } -Variable (Get-Variable iniManagement)
        $CommandlineArgumentsServer = $iniManagement.GetEnumerator() | Where-Object Key -notin ProductKey, ScomAdminGroupName | ForEach-Object { '/{0}:"{1}"' -f $_.Key, $_.Value }
            
        $setupCommandlineServer = "/install /silent /components:OMServer $CommandlineArgumentsServer"
        Invoke-LabCommand -ComputerName $vm -ScriptBlock { Set-Content -Path C:\DeployDebug\SetupScomManagement.cmd -Value "C:\SCOM\setup.exe $setupCommandLineServer" } -Variable (Get-Variable setupCommandlineServer) -NoDisplay
        Install-LabSoftwarePackage -ComputerName $vm -LocalPath C:\SCOM\setup.exe -CommandLine $setupCommandlineServer -AsJob -PassThru -UseShellExecute -UseExplicitCredentialsForScheduledJob -AsScheduledJob -Timeout 20 -NoDisplay
        $installationPaths[$vm.Name] = $iniManagement.InstallLocation
    }
    
    if ($jobs)
    {
        Wait-LWLabJob -Job $jobs
    }

    # After SCOM is set up, we need to wait a bit for it to "settle", otherwise there might be timing issues later on
    Start-Sleep -Seconds 30
    Remove-LabPSSession -ComputerName $firstmgmt

    if ($firstmgmt.Count -gt 0 -or $addtlmgmt.Count -gt 0)
    {
        $installationStatus = Invoke-LabCommand -PassThru -NoDisplay -ComputerName ([object[]]$firstmgmt + [object[]]$addtlmgmt) -Variable (Get-Variable installationPaths) -ScriptBlock {
            if (Get-Command -Name Get-Package -ErrorAction SilentlyContinue)
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = [bool](Get-Package -Name 'System Center Operations Manager Server' -ProviderName msi -ErrorAction SilentlyContinue)
                }
            }
            else
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = (Test-Path -Path (Join-Path -Path $installationPaths[$env:COMPUTERNAME] -ChildPath Server))
                }
            }
        }

        foreach ($failedInstall in ($installationStatus | Where-Object { $_.Status -contains $false }))
        {
            Write-ScreenInfo -Type Error -Message "Installation of SCOM Management failed on $($failedInstall.Node). Please refer to the logs in C:\DeployDebug on the VM"
        }

        $cmdAvailable = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $firstmgmt { Get-Command Get-ScomManagementServer -ErrorAction SilentlyContinue }
        if (-not $cmdAvailable)
        {
            Start-Sleep -Seconds 30
            Remove-LabPSSession -ComputerName $firstmgmt
        }

        Invoke-LabCommand -ComputerName $firstmgmt -ActivityName 'Waiting for SCOM Management to get in gear' -ScriptBlock {
            $start = Get-Date
            do
            {
                Start-Sleep -Seconds 10
                if ((Get-Date).Subtract($start) -gt '00:05:00') { throw 'SCOM startup not finished after 5 minutes' }
            }
            until (Get-ScomManagementServer -ErrorAction SilentlyContinue)
        }

        # Licensing
        foreach ($vm in $firstmgmt)
        {
            $role = $vm.Roles | Where-Object Name -eq ScomManagement
            if (-not $role.Properties.ContainsKey('ProductKey')) { continue }
            if ([string]::IsNullOrWhiteSpace($role.Properties['ProductKey'])) { continue }
            $productKey = $role.Properties['ProductKey']
 
            Invoke-LabCommand -ComputerName $vm -Variable (Get-Variable -Name productKey) -ScriptBlock {
                Set-SCOMLicense -ProductId $productKey
            } -NoDisplay
        }
    }

    $installationPaths = @{}
    $jobs = foreach ($vm in $scomConsoleRole)
    {
        $iniConsole = $iniNativeConsole.Clone()
        $role = $vm.Roles | Where-Object Name -in ScomConsole
        
        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniConsole.ContainsKey($property.Key)) { continue }
            $iniConsole[$property.Key] = $property.Value
        }

        $CommandlineArgumentsNativeConsole = $iniNativeConsole.GetEnumerator() | ForEach-Object { '/{0}:"{1}"' -f $_.Key, $_.Value }
        $setupCommandlineNativeConsole = "/install /silent /components:OMConsole $CommandlineArgumentsNativeConsole"
        Invoke-LabCommand -ComputerName $vm -ScriptBlock { Set-Content -Path C:\DeployDebug\SetupScomConsole.cmd -Value "C:\SCOM\setup.exe $setupCommandlineNativeConsole" } -Variable (Get-Variable setupCommandlineNativeConsole) -NoDisplay

        Install-LabSoftwarePackage -ComputerName $vm -LocalPath C:\SCOM\setup.exe -CommandLine $setupCommandlineNativeConsole -AsJob -PassThru -UseShellExecute -UseExplicitCredentialsForScheduledJob -AsScheduledJob -Timeout 20 -NoDisplay
        $installationPaths[$vm.Name] = $iniConsole.InstallLocation
    }

    if ($jobs)
    {
        Wait-LWLabJob -Job $jobs

        $installationStatus = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $scomConsoleRole -Variable (Get-Variable installationPaths) -ScriptBlock {
            if (Get-Command -Name Get-Package -ErrorAction SilentlyContinue)
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = [bool](Get-Package -Name 'System Center Operations Manager Console' -ProviderName msi -ErrorAction SilentlyContinue)
                }
            }
            else
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = (Test-Path -Path (Join-Path -Path $installationPaths[$env:COMPUTERNAME] -ChildPath Console))
                }
            }
        }

        foreach ($failedInstall in ($installationStatus | Where-Object { $_.Status -contains $false }))
        {
            Write-ScreenInfo -Type Error -Message "Installation of SCOM Console failed on $($failedInstall.Node). Please refer to the logs in C:\DeployDebug on the VM"
        }
    }

    $installationPaths = @{}
    $jobs = foreach ($vm in $scomWebConsoleRole)
    {
        $iniWeb = $iniWebConsole.Clone()
        $role = $vm.Roles | Where-Object Name -in ScomWebConsole
        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniWeb.ContainsKey($property.Key)) { continue }
            $iniWeb[$property.Key] = $property.Value
        }

        if (-not [string]::IsNullOrWhiteSpace($iniWeb['ManagementServer']))
        {
            $mgtMachineName = $iniWeb['ManagementServer']
            $mgtMachine = Get-LabVm -ComputerName $mgtMachineName
        }

        if (-not $mgtMachine)
        {
            $mgtMachine = Get-LabVm -Role ScomManagement | Select-Object -First 1
        }

        if ([string]::IsNullOrWhiteSpace($iniWeb['ManagementServer']))
        {
            $iniWeb['ManagementServer'] = $mgtMachine.Name
        }

        $CommandlineArgumentsWebConsole = $iniWeb.GetEnumerator() | ForEach-Object { '/{0}:"{1}"' -f $_.Key, $_.Value }
        $setupCommandlineWebConsole = "/install /silent /components:OMWebConsole $commandlineArgumentsWebConsole"
        Invoke-LabCommand -ComputerName $vm -ScriptBlock { Set-Content -Path C:\DeployDebug\SetupScomWebConsole.cmd -Value "C:\SCOM\setup.exe $setupCommandlineWebConsole" } -Variable (Get-Variable setupCommandlineWebConsole) -NoDisplay

        Install-LabSoftwarePackage -ComputerName $vm -LocalPath C:\SCOM\setup.exe -CommandLine $setupCommandlineWebConsole -AsJob -PassThru -UseShellExecute -UseExplicitCredentialsForScheduledJob -AsScheduledJob -Timeout 20 -NoDisplay
        $installationPaths[$vm.Name] = $iniWeb.WebSiteName
    }

    if ($jobs)
    {
        Wait-LWLabJob -Job $jobs

        $installationStatus = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $scomWebConsoleRole -Variable (Get-Variable installationPaths) -ScriptBlock {
            @{
                Node   = $env:COMPUTERNAME
                Status = [bool]($website = Get-Website -Name $installationPaths[$env:COMPUTERNAME] -ErrorAction SilentlyContinue)
            }
        }

        foreach ($failedInstall in ($installationStatus | Where-Object { $_.Status -contains $false }))
        {
            Write-ScreenInfo -Type Error -Message "Installation of SCOM Web Console failed on $($failedInstall.Node). Please refer to the logs in C:\DeployDebug on the VM"
        }
    }

    $installationPaths = @{}
    $jobs = foreach ($vm in $scomReportingServer)
    {
        $iniReport = $iniReportServer.Clone()
        $role = $vm.Roles | Where-Object Name -in ScomReporting

        foreach ($property in $role.Properties.GetEnumerator())
        {
            if (-not $iniReport.ContainsKey($property.Key)) { continue }
            $iniReport[$property.Key] = $property.Value
        }

        if (-not [string]::IsNullOrWhiteSpace($iniReport['ManagementServer']))
        {
            $mgtMachineName = $iniReport['ManagementServer']
            $mgtMachine = Get-LabVm -ComputerName $mgtMachineName
        }

        if (-not $mgtMachine)
        {
            $mgtMachine = Get-LabVm -Role ScomManagement | Select-Object -First 1
        }

        if ([string]::IsNullOrWhiteSpace($iniReport['ManagementServer']))
        {
            $iniReport['ManagementServer'] = $mgtMachine.Name
        }

        if (-not [string]::IsNullOrWhiteSpace($iniReport['SRSInstance']))
        {
            $ssrsName = $iniReport['SRSInstance'].Split('\')[0]
            $ssrsVm = Get-LabVm -ComputerName $ssrsName
        }

        if (-not $ssrsVm)
        {
            $ssrsVm = Get-LabVm -Role SQLServer2016, SQLServer2017 | Select-Object -First 1
        }

        if ([string]::IsNullOrWhiteSpace($iniReport['SRSInstance']))
        {
            $iniReport['SRSInstance'] = "$ssrsVm\SSRS"
        }

        if ([string]::IsNullOrWhiteSpace($iniReport['DataReaderPassword']))
        {
            $iniReport['DataReaderPassword'] = $vm.GetCredential($lab).GetNetworkCredential().Password
        }

        Invoke-LabCommand -ComputerName (Get-LabVm -Role RootDc | Select-Object -First 1) -ScriptBlock {
            foreach ($kvp in $iniManagement.GetEnumerator().Where( { $_.Key -like '*User' }))
            {
                if ($kvp.Key -like '*User')
                {
                    $userName = $kvp.Value
                    $password = $iniManagement[($kvp.Key -replace 'User', 'Password')]
                }
                $userAccount = $null # Damn AD cmdlets.

                try
                {
                    $userAccount = Get-ADUser -Identity $userName -ErrorAction Stop
                }
                catch
                { }

                if (-not $userAccount)
                {
                    $userAccount = New-ADUser -Name $userName -SamAccountName $userName -PassThru -Enabled $true -AccountPassword ($password | ConvertTo-SecureString -AsPlainText -Force)
                }

            }
        } -Variable (Get-Variable iniReport) -NoDisplay

        if (-not $iniReport['DataReaderUser'].Contains('\'))
        {
            $iniReport['DataReaderUser'] = '{0}\{1}' -f $vm.DomainAccountName.Split('\')[0], $iniReport['DataReaderUser']
        }

        $CommandlineArgumentsReportServer = $iniReport.GetEnumerator() | ForEach-Object { '/{0}:"{1}"' -f $_.Key, $_.Value }
        $setupCommandlineReportServer = "/install /silent /components:OMReporting $commandlineArgumentsReportServer"
        Invoke-LabCommand -ComputerName $vm -ScriptBlock { Set-Content -Path C:\DeployDebug\SetupScomReporting.cmd -Value "C:\SCOM\setup.exe $setupCommandlineReportServer" } -Variable (Get-Variable setupCommandlineReportServer) -NoDisplay
        Invoke-LabCommand -ComputerName $scomReportingServer -ScriptBlock {
            Get-Service -Name SQLSERVERAGENT* | Set-Service -StartupType Automatic -Status Running
        } -NoDisplay

        Install-LabSoftwarePackage -ComputerName $vm -LocalPath C:\SCOM\setup.exe -CommandLine $setupCommandlineReportServer -AsJob -PassThru -UseShellExecute -UseExplicitCredentialsForScheduledJob -AsScheduledJob -Timeout 20 -NoDisplay
        $installationPaths[$vm.Name] = $iniReport.InstallLocation
    }

    if ($jobs)
    {
        Wait-LWLabJob -Job $jobs
        
        $installationStatus = Invoke-LabCommand -PassThru -NoDisplay -ComputerName $scomReportingServer -Variable (Get-Variable installationPaths) -ScriptBlock {
            if (Get-Command -Name Get-Package -ErrorAction SilentlyContinue)
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = [bool](Get-Package -Name 'System Center Operations Manager Reporting Server' -ProviderName msi -ErrorAction SilentlyContinue)
                }
            }
            else
            {
                @{
                    Node   = $env:COMPUTERNAME
                    Status = (Test-Path -Path (Join-Path -Path $installationPaths[$env:COMPUTERNAME] -ChildPath Reporting))
                }
            }
        }

        foreach ($failedInstall in ($installationStatus | Where-Object { $_.Status -contains $false }))
        {
            Write-ScreenInfo -Type Error -Message "Installation of SCOM Reporting failed on $($failedInstall.Node). Please refer to the logs in C:\DeployDebug on the VM"
        }
    }
        
    # Collect installation logs from $env:LOCALAPPDATA\SCOM\Logs
    Write-PSFMessage -Message "====SCOM log content errors begin===="
    $errors = Invoke-LabCommand -ComputerName $all -NoDisplay -ScriptBlock {    
        $null = robocopy (Join-Path -Path $env:LOCALAPPDATA SCOM\Logs) "C:\DeployDebug\SCOMLogs" /S /E
        Get-ChildItem -Path C:\DeployDebug\SCOMLogs -ErrorAction SilentlyContinue | Get-Content
    } -PassThru | Where-Object {$_ -like '*Error*'}
    foreach ($err in $errors) { Write-PSFMessage $err }
    Write-PSFMessage -Message "====SCOM log content errors end===="

    Write-LogFunctionExit
}


function Install-LabScvmm
{
    [CmdletBinding()]
    param ( )

    # defaults
    $iniContentServer = @{
        UserName                    = 'Administrator'
        CompanyName                 = 'AutomatedLab'
        ProgramFiles                = 'C:\Program Files\Microsoft System Center\Virtual Machine Manager {0}'
        CreateNewSqlDatabase        = '1'
        SqlInstanceName             = 'MSSQLSERVER'
        SqlDatabaseName             = 'VirtualManagerDB'
        RemoteDatabaseImpersonation = '0'
        SqlMachineName              = 'REPLACE'
        IndigoTcpPort               = '8100'
        IndigoHTTPSPort             = '8101'
        IndigoNETTCPPort            = '8102'
        IndigoHTTPPort              = '8103'
        WSManTcpPort                = '5985'
        BitsTcpPort                 = '443'
        CreateNewLibraryShare       = '1'
        LibraryShareName            = 'MSSCVMMLibrary'
        LibrarySharePath            = 'C:\ProgramData\Virtual Machine Manager Library Files'
        LibraryShareDescription     = 'Virtual Machine Manager Library Files'
        SQMOptIn                    = '0'
        MUOptIn                     = '0'
        VmmServiceLocalAccount      = '0'
        TopContainerName            = 'CN=VMMServer,DC=contoso,DC=com'
    }
    $iniContentConsole = @{
        ProgramFiles  = 'C:\Program Files\Microsoft System Center\Virtual Machine Manager {0}'
        IndigoTcpPort = '8100'
        MUOptIn       = '0'
    }
    $setupCommandLineServer = '/server /i /f C:\Server.ini /VmmServiceDomain {0} /VmmServiceUserName {1} /VmmServiceUserPassword {2} /SqlDBAdminDomain {0} /SqlDBAdminName {1} /SqlDBAdminPassword {2} /IACCEPTSCEULA'

    $lab = Get-Lab
    # Prerequisites, all
    $all = Get-LabVM -Role SCVMM | Where-Object SkipDeployment -eq $false
    Invoke-LabCommand -ComputerName $all -ScriptBlock {
        if (-not (Test-Path C:\DeployDebug))
        {
            $null = New-Item -ItemType Directory -Path C:\DeployDebug
        }
    }
    $server = $all | Where-Object { -not $_.Roles.Properties.ContainsKey('SkipServer') }
    $consoles = $all | Where-Object { $_.Roles.Properties.ContainsKey('SkipServer') }

    if ($consoles)
    {
        $jobs = Install-ScvmmConsole -Computer $consoles
    }

    if ($server)
    {
        Install-ScvmmServer -Computer $server
    }

    # In case console setup took longer than server...
    if ($jobs) { Wait-LWLabJob -Job $jobs }
}


function Install-LabSqlSampleDatabases
{
    param
    (
        [Parameter(Mandatory)]
        [AutomatedLab.Machine]
        $Machine
    )

    Write-LogFunctionEntry

    $role = $Machine.Roles | Where-Object Name -like SQLServer* | Sort-Object Name -Descending | Select-Object -First 1
    $roleName = ($role).Name
    $roleInstance = if ($role.Properties['InstanceName'])
    {
        $role.Properties['InstanceName']
    }
    else
    {
        'MSSQLSERVER'
    }

    $sqlLink = Get-LabConfigurationItem -Name $roleName.ToString()
    if (-not $sqlLink)
    {
        throw "No SQL link found to download $roleName sample database"
    }

    $targetFolder = Join-Path -Path (Get-LabSourcesLocationInternal -Local) -ChildPath SoftwarePackages\SqlSampleDbs

    if (-not (Test-Path $targetFolder))
    {
        [void] (New-Item -ItemType Directory -Path $targetFolder)
    }

    if ($roleName -like 'SQLServer2008*')
    {
        $targetFile = Join-Path -Path $targetFolder -ChildPath "$roleName.zip"
    }
    else
    {
        [void] (New-Item -ItemType Directory -Path (Join-Path -Path $targetFolder -ChildPath $rolename) -ErrorAction SilentlyContinue)
        $targetFile = Join-Path -Path $targetFolder -ChildPath "$rolename\$roleName.bak"
    }

    Get-LabInternetFile -Uri $sqlLink -Path $targetFile

    $dependencyFolder = Join-Path -Path $targetFolder -ChildPath $roleName

    switch ($roleName)
    {
        'SQLServer2008'
        {
            Microsoft.PowerShell.Archive\Expand-Archive $targetFile -DestinationPath $dependencyFolder -Force

            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $mdf = Get-Item -Path 'C:\SQLServer2008\AdventureWorksLT2008_Data.mdf' -ErrorAction SilentlyContinue
                $ldf = Get-Item -Path 'C:\SQLServer2008\AdventureWorksLT2008_Log.ldf' -ErrorAction SilentlyContinue
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = 'CREATE DATABASE AdventureWorks2008 ON (FILENAME = "{0}"), (FILENAME = "{1}") FOR ATTACH;' -f $mdf.FullName, $ldf.FullName
                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2008R2'
        {
            Microsoft.PowerShell.Archive\Expand-Archive $targetFile -DestinationPath $dependencyFolder -Force

            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $mdf = Get-Item -Path 'C:\SQLServer2008R2\AdventureWorksLT2008R2_Data.mdf' -ErrorAction SilentlyContinue
                $ldf = Get-Item -Path 'C:\SQLServer2008R2\AdventureWorksLT2008R2_Log.ldf' -ErrorAction SilentlyContinue
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = 'CREATE DATABASE AdventureWorks2008R2 ON (FILENAME = "{0}"), (FILENAME = "{1}") FOR ATTACH;' -f $mdf.FullName, $ldf.FullName
                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2012'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2012
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
                USE [master]

                RESTORE DATABASE AdventureWorks2012
                FROM disk= '$($backupFile.FullName)'
                WITH MOVE 'AdventureWorks2012_data' TO 'C:\Program Files\Microsoft SQL Server\MSSQL11.$roleInstance\MSSQL\DATA\AdventureWorks2012.mdf',
                MOVE 'AdventureWorks2012_Log' TO 'C:\Program Files\Microsoft SQL Server\MSSQL11.$roleInstance\MSSQL\DATA\AdventureWorks2012.ldf'
                ,REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2014'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2014
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
        USE [master]

        RESTORE DATABASE AdventureWorks2014
        FROM disk= '$($backupFile.FullName)'
        WITH MOVE 'AdventureWorks2014_data' TO 'C:\Program Files\Microsoft SQL Server\MSSQL12.$roleInstance\MSSQL\DATA\AdventureWorks2014.mdf',
        MOVE 'AdventureWorks2014_Log' TO 'C:\Program Files\Microsoft SQL Server\MSSQL12.$roleInstance\MSSQL\DATA\AdventureWorks2014.ldf'
        ,REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2016'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2016
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
        USE master
        RESTORE DATABASE WideWorldImporters
        FROM disk =
        '$($backupFile.FullName)'
        WITH MOVE 'WWI_Primary' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL13.$roleInstance\MSSQL\DATA\WideWorldImporters.mdf',
        MOVE 'WWI_UserData' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL13.$roleInstance\MSSQL\DATA\WideWorldImporters_UserData.ndf',
        MOVE 'WWI_Log' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL13.$roleInstance\MSSQL\DATA\WideWorldImporters.ldf',
        MOVE 'WWI_InMemory_Data_1' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL13.$roleInstance\MSSQL\DATA\WideWorldImporters_InMemory_Data_1',
        REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2017'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2017
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
        USE master
        RESTORE DATABASE WideWorldImporters
        FROM disk =
        '$($backupFile.FullName)'
        WITH MOVE 'WWI_Primary' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL14.$roleInstance\MSSQL\DATA\WideWorldImporters.mdf',
        MOVE 'WWI_UserData' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL14.$roleInstance\MSSQL\DATA\WideWorldImporters_UserData.ndf',
        MOVE 'WWI_Log' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL14.$roleInstance\MSSQL\DATA\WideWorldImporters.ldf',
        MOVE 'WWI_InMemory_Data_1' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL14.$roleInstance\MSSQL\DATA\WideWorldImporters_InMemory_Data_1',
        REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2019'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2019
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
        USE master
        RESTORE DATABASE WideWorldImporters
        FROM disk =
        '$($backupFile.FullName)'
        WITH MOVE 'WWI_Primary' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL15.$roleInstance\MSSQL\DATA\WideWorldImporters.mdf',
        MOVE 'WWI_UserData' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL15.$roleInstance\MSSQL\DATA\WideWorldImporters_UserData.ndf',
        MOVE 'WWI_Log' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL15.$roleInstance\MSSQL\DATA\WideWorldImporters.ldf',
        MOVE 'WWI_InMemory_Data_1' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL15.$roleInstance\MSSQL\DATA\WideWorldImporters_InMemory_Data_1',
        REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        'SQLServer2022'
        {
            Invoke-LabCommand -ActivityName "$roleName Sample DBs" -ComputerName $Machine -ScriptBlock {
                $backupFile = Get-ChildItem -Filter *.bak -Path C:\SQLServer2022
                $connectionInstance = if ($roleInstance -ne 'MSSQLSERVER') { "localhost\$roleInstance" } else { "localhost" }
                $query = @"
        USE master
        RESTORE DATABASE WideWorldImporters
        FROM disk =
        '$($backupFile.FullName)'
        WITH MOVE 'WWI_Primary' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL16.$roleInstance\MSSQL\DATA\WideWorldImporters.mdf',
        MOVE 'WWI_UserData' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL16.$roleInstance\MSSQL\DATA\WideWorldImporters_UserData.ndf',
        MOVE 'WWI_Log' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL16.$roleInstance\MSSQL\DATA\WideWorldImporters.ldf',
        MOVE 'WWI_InMemory_Data_1' TO
        'C:\Program Files\Microsoft SQL Server\MSSQL16.$roleInstance\MSSQL\DATA\WideWorldImporters_InMemory_Data_1',
        REPLACE
"@

                Invoke-Sqlcmd -ServerInstance $connectionInstance -Query $query
            } -DependencyFolderPath $dependencyFolder -Variable (Get-Variable roleInstance)
        }
        default
        {
            Write-LogFunctionExitWithError -Exception (New-Object System.ArgumentException("$roleName has no sample scripts yet.", 'roleName'))
        }
    }

    Write-LogFunctionExit
}


function Install-LabSqlServers
{
    [CmdletBinding()]
    param (
        [int]$InstallationTimeout = (Get-LabConfigurationItem -Name Timeout_Sql2012Installation),

        [switch]$CreateCheckPoints,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator)
    )

    Write-LogFunctionEntry

    if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

    function Write-ArgumentVerbose
    {
        param
        (
            $Argument
        )

        Write-ScreenInfo -Type Verbose -Message "Argument '$Argument'"
        $Argument
    }

    Write-LogFunctionEntry

    $lab = Get-Lab -ErrorAction SilentlyContinue

    if (-not $lab)
    {
        Write-LogFunctionExitWithError -Message 'No lab definition imported, so there is nothing to do. Please use the Import-Lab cmdlet first'
        return
    }

    $machines = Get-LabVM -Role SQLServer | Where-Object SkipDeployment -eq $false
    
    Invoke-LabCommand -ComputerName $machines -ScriptBlock {
        if (-not (Test-Path C:\DeployDebug))
        {
            $null = New-Item -ItemType Directory -Path C:\DeployDebug
        }
    }

    #The default SQL installation in Azure does not give the standard buildin administrators group access.
    #This section adds the rights. As only the renamed Builtin Admin account has permissions, Invoke-LabCommand cannot be used.
    $azureMachines = $machines | Where-Object {
        $_.HostType -eq 'Azure' -and -not (($_.Roles |
            Where-Object Name -like 'SQL*').Properties.Keys |
    Where-Object {$_ -ne 'InstallSampleDatabase'})}

    if ($azureMachines)
    {
        Write-ScreenInfo -Message 'Waiting for machines to start up' -NoNewLine
        Start-LabVM -ComputerName $azureMachines -Wait -ProgressIndicator 2
        Enable-LabVMRemoting -ComputerName $azureMachines

        Write-ScreenInfo -Message "Configuring Azure SQL Servers '$($azureMachines -join ', ')'"

        foreach ($machine in $azureMachines)
        {
            Write-ScreenInfo -Type Verbose -Message "Configuring Azure SQL Server '$machine'"
            $sqlCmd = {
                $query = @"
USE [master]
GO

CREATE LOGIN [BUILTIN\Administrators] FROM WINDOWS WITH DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english]
GO

-- ALTER SERVER ROLE [sysadmin] ADD MEMBER [BUILTIN\Administrators]
-- The folloing statement works in SQL 2008 to 2016
EXEC master..sp_addsrvrolemember @loginame = N'BUILTIN\Administrators', @rolename = N'sysadmin'
GO
"@

                if ((Get-PSSnapin -Registered -Name SqlServerCmdletSnapin100 -ErrorAction SilentlyContinue) -and -not (Get-PSSnapin -Name SqlServerCmdletSnapin100 -ErrorAction SilentlyContinue))
                {
                    Add-PSSnapin -Name SqlServerCmdletSnapin100
                }
                Invoke-Sqlcmd -Query $query
            }

            Invoke-LabCommand -ComputerName $machine -ActivityName SetupSqlPermissions -ScriptBlock $sqlCmd -UseLocalCredential
        }
        Write-ScreenInfo -Type Verbose -Message "Finished configuring Azure SQL Servers '$($azureMachines -join ', ')'"
    }

    $onPremisesMachines = @($machines | Where-Object HostType -eq HyperV)
    $onPremisesMachines += $machines | Where-Object {$_.HostType -eq 'Azure' -and (($_.Roles |
            Where-Object Name -like 'SQL*').Properties.Keys |
    Where-Object {$_ -ne 'InstallSampleDatabase'})}

    $downloadTargetFolder = "$labSources\SoftwarePackages"
    $dotnet48DownloadLink = Get-LabConfigurationItem -Name dotnet48DownloadLink
    Write-ScreenInfo -Message "Downloading .net Framework 4.8 from '$dotnet48DownloadLink'"
    $dotnet48InstallFile = Get-LabInternetFile -Uri $dotnet48DownloadLink -Path $downloadTargetFolder -PassThru -ErrorAction Stop

    if ($onPremisesMachines)
    {
        $cppRedist64_2017 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist64_2017) -Path $downloadTargetFolder -FileName vcredist_x64_2017.exe -PassThru
        $cppredist32_2017 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist32_2017) -Path $downloadTargetFolder -FileName vcredist_x86_2017.exe -PassThru
        $cppRedist64_2015 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist64_2015) -Path $downloadTargetFolder -FileName vcredist_x64_2015.exe -PassThru
        $cppredist32_2015 = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name cppredist32_2015) -Path $downloadTargetFolder -FileName vcredist_x86_2015.exe -PassThru

        $parallelInstalls = 4
        Write-ScreenInfo -Type Verbose -Message "Parallel installs: $parallelInstalls"
        $machineIndex = 0
        $installBatch = 0
        $totalBatches = [System.Math]::Ceiling($onPremisesMachines.count / $parallelInstalls)

        do
        {
            $jobs = @()

            $installBatch++

            $machinesBatch = $($onPremisesMachines[$machineIndex..($machineIndex + $parallelInstalls - 1)])

            Write-ScreenInfo -Message "Starting machines '$($machinesBatch -join ', ')'" -NoNewLine
            Start-LabVM -ComputerName $machinesBatch -Wait

            Write-ScreenInfo -Message "Starting installation of pre-requisite .Net 3.5 Framework on machine '$($machinesBatch -join ', ')'" -NoNewLine
            $installFrameworkJobs = Install-LabWindowsFeature -ComputerName $machinesBatch -FeatureName Net-Framework-Core -NoDisplay -AsJob -PassThru
            Wait-LWLabJob -Job $installFrameworkJobs -Timeout 10 -NoDisplay -NoNewLine
            Write-ScreenInfo -Message 'done'

            Write-ScreenInfo -Message "Starting installation of pre-requisite C++ 2015 redist on machine '$($machinesBatch -join ', ')'" -NoNewLine
            Install-LabSoftwarePackage -Path $cppredist32_2015.FullName -CommandLine ' /quiet /norestart /log C:\DeployDebug\cpp32_2015.log' -ComputerName $machinesBatch -ExpectedReturnCodes 0,3010 -AsScheduledJob -NoDisplay
            Install-LabSoftwarePackage -Path $cppRedist64_2015.FullName -CommandLine ' /quiet /norestart /log C:\DeployDebug\cpp64_2015.log' -ComputerName $machinesBatch -ExpectedReturnCodes 0,3010 -AsScheduledJob -NoDisplay
            Write-ScreenInfo -Message 'done'

            Write-ScreenInfo -Message "Starting installation of pre-requisite C++ 2017 redist on machine '$($machinesBatch -join ', ')'" -NoNewLine
            Install-LabSoftwarePackage -Path $cppredist32_2017.FullName -CommandLine ' /quiet /norestart /log C:\DeployDebug\cpp32_2017.log' -ComputerName $machinesBatch -ExpectedReturnCodes 0,3010 -AsScheduledJob -NoDisplay
            Install-LabSoftwarePackage -Path $cppRedist64_2017.FullName -CommandLine ' /quiet /norestart /log C:\DeployDebug\cpp64_2017.log' -ComputerName $machinesBatch -ExpectedReturnCodes 0,3010 -AsScheduledJob -NoDisplay
            Write-ScreenInfo -Message 'done'

            Write-ScreenInfo -Message "Restarting '$($machinesBatch -join ', ')'" -NoNewLine
            Restart-LabVM -ComputerName $machinesBatch -Wait -NoDisplay
            Write-ScreenInfo -Message 'done'

            foreach ($machine in $machinesBatch)
            {
                $role = $machine.Roles | Where-Object Name -like SQLServer*

                #Dismounting ISO images to have just one drive later
                Dismount-LabIsoImage -ComputerName $machine -SupressOutput

                $retryCount = 10
                $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name]
                while (-not $autoLogon -and $retryCount -gt 0)
                {
                    Enable-LabAutoLogon -ComputerName $machine
                    Restart-LabVM -ComputerName $machine -Wait -NoDisplay -NoNewLine

                    $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name]
                    $retryCount--
                }

                if (-not $autoLogon)
                {
                    throw "No logon session available for $($machine.InstallationUser.UserName). Cannot continue with SQL Server setup for $machine"
                }
                Write-ScreenInfo 'Done'

                $dvdDrive = Mount-LabIsoImage -ComputerName $machine -IsoPath ($lab.Sources.ISOs | Where-Object Name -eq $role.Name).Path -PassThru -SupressOutput
                Remove-LabPSSession -Machine $machine # Remove session to refresh drives, otherwise FileNotFound even if ISO is mounted

                $global:setupArguments = ' /Q /Action=Install /IndicateProgress'

                Invoke-Ternary -Decider { $role.Properties.ContainsKey('Features') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument " /Features=$($role.Properties.Features.Replace(' ', ''))" } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument ' /Features=SQL,AS,RS,IS,Tools' }

                #Check the usage of SQL Configuration File
                if ($role.Properties.ContainsKey('ConfigurationFile'))
                {
                    $global:setupArguments = ''
                    $fileName = Join-Path -Path 'C:\' -ChildPath (Split-Path -Path $role.Properties.ConfigurationFile -Leaf)
                    $confPath = if ($lab.DefaultVirtualizationEngine -eq 'Azure' -and (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $role.Properties.ConfigurationFile))
                    {
                        $blob = Get-LabAzureLabSourcesContent -Path $role.Properties.ConfigurationFile.Replace($labSources,'')
                        $null = Get-AzStorageFileContent -File $blob -Destination (Join-Path $env:TEMP azsql.ini) -Force
                        Join-Path $env:TEMP azsql.ini
                    }
                    elseif ($lab.DefaultVirtualizationEngine -ne 'Azure' -or ($lab.DefaultVirtualizationEngine -eq 'Azure' -and -not (Test-LabPathIsOnLabAzureLabSourcesStorage -Path $role.Properties.ConfigurationFile)))
                    {
                        $role.Properties.ConfigurationFile
                    }

                    $configurationFileContent = Get-Content $confPath | ConvertFrom-String -Delimiter = -PropertyNames Key, Value
                    Write-PSFMessage -Message ($configurationFileContent | Out-String)
                    try
                    {
                        Copy-LabFileItem -Path $role.Properties.ConfigurationFile -ComputerName $machine -ErrorAction Stop
                        $global:setupArguments += Write-ArgumentVerbose -Argument (" /ConfigurationFile=`"$fileName`"")
                    }
                    catch
                    {
                        Write-PSFMessage -Message ('Could not copy "{0}" to {1}. Skipping configuration file' -f $role.Properties.ConfigurationFile, $machine)
                    }
                }

                Invoke-Ternary -Decider { $role.Properties.ContainsKey('InstanceName') } {
                    $global:setupArguments += Write-ArgumentVerbose -Argument " /InstanceName=$($role.Properties.InstanceName)"
                    $script:instanceName = $role.Properties.InstanceName
                } `
                {
                    if ($null -eq $configurationFileContent.Where({$_.Key -eq 'INSTANCENAME'}).Value)
                    {
                        $global:setupArguments += Write-ArgumentVerbose -Argument ' /InstanceName=MSSQLSERVER'
                        $script:instanceName = 'MSSQLSERVER'
                    }
                    else
                    {
                        $script:instanceName = $configurationFileContent.Where({$_.Key -eq 'INSTANCENAME'}).Value -replace "'|`""
                    }
                }

                $result = Invoke-LabCommand -ComputerName $machine -ScriptBlock {
                    Get-Service -DisplayName "SQL Server ($instanceName)" -ErrorAction SilentlyContinue
                } -Variable (Get-Variable -Name instanceName) -PassThru -NoDisplay

                if ($result)
                {
                    Write-ScreenInfo -Message "Machine '$machine' already has SQL Server installed with requested instance name '$instanceName'" -Type Warning
                    $machine | Add-Member -Name SqlAlreadyInstalled -Value $true -MemberType NoteProperty -Force
                    $machineIndex++
                    continue
                }

                Invoke-Ternary -Decider { $role.Properties.ContainsKey('Collation') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /SQLCollation=" + "$($role.Properties.Collation)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'SQLCollation'}).Value) {$global:setupArguments += Write-ArgumentVerbose -Argument ' /SQLCollation=Latin1_General_CI_AS'} else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('SQLSvcAccount') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /SQLSvcAccount=" + """$($role.Properties.SQLSvcAccount)""") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'SQLSvcAccount'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /SQLSvcAccount="NT Authority\Network Service"' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('SQLSvcPassword') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /SQLSvcPassword=" + """$($role.Properties.SQLSvcPassword)""") } `
                { }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AgtSvcAccount') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AgtSvcAccount=" + """$($role.Properties.AgtSvcAccount)""") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'AgtSvcAccount'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /AgtSvcAccount="NT Authority\System"' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AgtSvcPassword') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AgtSvcPassword=" + """$($role.Properties.AgtSvcPassword)""") } `
                { }
                if($role.Name -notin 'SQLServer2022')
                {
                    Invoke-Ternary -Decider { $role.Properties.ContainsKey('RsSvcAccount') } `
                    { $global:setupArguments += Write-ArgumentVerbose -Argument (" /RsSvcAccount=" + """$($role.Properties.RsSvcAccount)""") } `
                    { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'RsSvcAccount'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /RsSvcAccount="NT Authority\Network Service"' } else {} }
                    Invoke-Ternary -Decider { $role.Properties.ContainsKey('RsSvcPassword') } `
                    { $global:setupArguments += Write-ArgumentVerbose -Argument (" /RsSvcPassword=" + """$($role.Properties.RsSvcPassword)""") } `
                    { }
                    Invoke-Ternary -Decider { $role.Properties.ContainsKey('RsSvcStartupType') } `
                    { $global:setupArguments += Write-ArgumentVerbose -Argument (" /RsSvcStartupType=" + "$($role.Properties.RsSvcStartupType)") } `
                    { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'RsSvcStartupType'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /RsSvcStartupType=Automatic' } else {} }
                }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AgtSvcStartupType') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AgtSvcStartupType=" + "$($role.Properties.AgtSvcStartupType)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'AgtSvcStartupType'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /AgtSvcStartupType=Disabled' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('BrowserSvcStartupType') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /BrowserSvcStartupType=" + "$($role.Properties.BrowserSvcStartupType)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'BrowserSvcStartupType'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /BrowserSvcStartupType=Disabled' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AsSysAdminAccounts') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AsSysAdminAccounts=" + "$($role.Properties.AsSysAdminAccounts)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'AsSysAdminAccounts'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /AsSysAdminAccounts="BUILTIN\Administrators"' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AsSvcAccount') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AsSvcAccount=" + "$($role.Properties.AsSvcAccount)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'AsSvcAccount'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /AsSvcAccount="NT Authority\System"' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('AsSvcPassword') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /AsSvcPassword=" + "$($role.Properties.AsSvcPassword)") } `
                { }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('IsSvcAccount') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /IsSvcAccount=" + "$($role.Properties.IsSvcAccount)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'IsSvcAccount'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /IsSvcAccount="NT Authority\System"' } else {} }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('IsSvcPassword') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /IsSvcPassword=" + "$($role.Properties.IsSvcPassword)") } `
                { }
                Invoke-Ternary -Decider { $role.Properties.ContainsKey('SQLSysAdminAccounts') } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (" /SQLSysAdminAccounts=" + "$($role.Properties.SQLSysAdminAccounts)") } `
                { if ($null -eq $configurationFileContent.Where({$_.Key -eq 'SQLSysAdminAccounts'}).Value) { $global:setupArguments += Write-ArgumentVerbose -Argument ' /SQLSysAdminAccounts="BUILTIN\Administrators"' } else {} }
                Invoke-Ternary -Decider { $machine.Roles.Name -notcontains 'SQLServer2008' } `
                { $global:setupArguments += Write-ArgumentVerbose -Argument (' /IAcceptSQLServerLicenseTerms') } `
                { }

                if ($role.Name -notin 'SQLServer2008R2', 'SQLServer2008')
                {
                    $global:setupArguments += " /UpdateEnabled=`"False`"" # Otherwise we get AccessDenied
                }

                New-LabSqlAccount -Machine $machine -RoleProperties $role.Properties

                $param = @{}
                $param.Add('ComputerName', $machine)
                $param.Add('LocalPath', "$($dvdDrive.DriveLetter)\Setup.exe")
                $param.Add('AsJob', $true)
                $param.Add('PassThru', $true)
                $param.Add('NoDisplay', $true)
                $param.Add('CommandLine', $setupArguments)
                $param.Add('ExpectedReturnCodes', (0,3010))
                $jobs += Install-LabSoftwarePackage @param -UseShellExecute

                $machineIndex++
            }

            if ($jobs)
            {
                Write-ScreenInfo -Message "Waiting $InstallationTimeout minutes until the installation is finished" -Type Verbose
                Write-ScreenInfo -Message "Waiting for installation of SQL server to complete on machines '$($machinesBatch -join ', ')'" -NoNewLine

                #Start other machines while waiting for SQL server to install
                $startTime = Get-Date
                $additionalMachinesToInstall = Get-LabVM -Role SQLServer | Where-Object { (Get-LabVMStatus -ComputerName $_.Name) -eq 'Stopped' }

                if ($additionalMachinesToInstall)
                {
                    Write-PSFMessage -Message 'Preparing more machines while waiting for installation to finish'

                    $machinesToPrepare = Get-LabVM -Role SQLServer |
                    Where-Object { (Get-LabVMStatus -ComputerName $_) -eq 'Stopped' } |
                    Select-Object -First 2

                    while ($startTime.AddMinutes(5) -gt (Get-Date) -and $machinesToPrepare)
                    {
                        Write-PSFMessage -Message "Starting machines '$($machinesToPrepare -join ', ')'"
                        Start-LabVM -ComputerName $machinesToPrepare -Wait -NoNewline

                        Write-PSFMessage -Message "Starting installation of pre-requisite .Net 3.5 Framework on machine '$($machinesToPrepare -join ', ')'"
                        $installFrameworkJobs = Install-LabWindowsFeature -ComputerName $machinesToPrepare -FeatureName Net-Framework-Core -NoDisplay -AsJob -PassThru
                        Write-PSFMessage -Message "Waiting for machines '$($machinesToPrepare -join ', ')' to be finish installation of pre-requisite .Net 3.5 Framework"
                        Wait-LWLabJob -Job $installFrameworkJobs -Timeout 10 -NoDisplay -ProgressIndicator 120 -NoNewLine

                        $machinesToPrepare = Get-LabVM -Role SQLServer |
                        Where-Object { (Get-LabVMStatus -ComputerName $_.Name) -eq 'Stopped' } |
                        Select-Object -First 2
                    }
                    Write-PSFMessage -Message "Resuming waiting for SQL Servers batch ($($machinesBatch -join ', ')) to complete installation and restart"
                }

                $installMachines = $machinesBatch | Where-Object { -not $_.SqlAlreadyInstalled }
                Wait-LWLabJob -Job $jobs -Timeout 40 -NoDisplay -ProgressIndicator 15 -NoNewLine
                Dismount-LabIsoImage -ComputerName $machinesBatch -SupressOutput
                Restart-LabVM -ComputerName $installMachines -NoDisplay

                Wait-LabVM -ComputerName $installMachines -PostDelaySeconds 30 -NoNewLine

                if ($installBatch -lt $totalBatches -and ($machinesBatch | Where-Object HostType -eq 'HyperV'))
                {
                    Write-ScreenInfo -Message "Saving machines '$($machinesBatch -join ', ')' as these are not needed right now" -Type Warning
                    Save-LabVM -Name $machinesBatch
                }
            }

        }
        until ($machineIndex -ge $onPremisesMachines.Count)

        $machinesToPrepare = Get-LabVM -Role SQLServer
        $machinesToPrepare = $machinesToPrepare | Where-Object { (Get-LabVMStatus -ComputerName $_) -ne 'Started' }
        if ($machinesToPrepare)
        {
            Start-LabVM -ComputerName $machinesToPrepare -Wait -NoNewline
        }
        else
        {
            Write-ProgressIndicatorEnd
        }

        Write-ScreenInfo -Message "All SQL Servers '$($onPremisesMachines -join ', ')' have now been installed and restarted. Waiting for these to be ready." -NoNewline

        Wait-LabVM -ComputerName $onPremisesMachines -TimeoutInMinutes 30 -ProgressIndicator 10
        $logResult = Invoke-LabCommand -ComputerName $onPremisesMachines -ScriptBlock {
            $log = Get-ChildItem -Path (Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft SQL Server\*\Setup Bootstrap\Log\summary.txt') | Select-String -Pattern 'Exit code \(Decimal\):\s+(-?\d+)'
            if ($log.Matches.Groups[1].Value -notin 0,3010)
            {
                @{
                    Content  = Get-ChildItem -Path (Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft SQL Server\*\Setup Bootstrap\Log\summary.txt') | Get-Content -Raw
                    Node     = $env:COMPUTERNAME
                    ExitCode = $log.Matches.Groups[1].Value
                }
            }
        } -ActivityName 'Collecting installation logs' -NoDisplay -PassThru
        
        foreach ($log in $logResult)
        {
            New-Variable -Name "$($log.Node)SQLSETUP" -Value $log.Content -Force -Scope Global
            Write-PSFMessage -Message "====$($log.Node) SQL log content begin===="
            Write-PSFMessage -Message $log.Content
            
            Write-PSFMessage -Message "====$($log.Node) SQL log content end===="
            Write-ScreenInfo -Type Error -Message "Installation of SQL Server seems to have failed with exit code $($log.ExitCode) on $($log.Node). Examine the result of `$$($log.Node)SQLSETUP"
        }
    }
        
    $servers = Get-LabVM -Role SQLServer | Where-Object { $_.Roles.Name -ge 'SQLServer2016' }
    foreach ($server in $servers)
    {
        $sqlRole = $server.Roles | Where-Object { $_.Name -band [AutomatedLab.Roles]::SQLServer }
        $sqlRole.Name -match '(?<Version>\d+)' | Out-Null
        $server | Add-Member -Name SqlVersion -MemberType NoteProperty -Value $Matches.Version -Force

        if (($sqlRole.Properties.Features -split ',') -contains 'RS' -or
            (($configurationFileContent | Where-Object Key -eq Features).Value -split ',') -contains 'RS' -or
            (-not $sqlRole.Properties.ContainsKey('ConfigurationFile') -and -not $sqlRole.Properties.Features))
        {
            $server | Add-Member -Name SsRsUri -MemberType NoteProperty -Value (Get-LabConfigurationItem -Name "Sql$($Matches.Version)SSRS") -Force
        }

        if (($sqlRole.Properties.Features -split ',') -contains 'Tools' -or
            (($configurationFileContent | Where-Object Key -eq Features).Value -split ',') -contains 'Tools' -or
            (-not $sqlRole.Properties.ContainsKey('ConfigurationFile') -and -not $sqlRole.Properties.Features))
        {
            $server | Add-Member -Name SsmsUri -MemberType NoteProperty -Value (Get-LabConfigurationItem -Name "Sql$($Matches.Version)ManagementStudio") -Force
        }
    }

    #region install SSRS
    $servers = Get-LabVM -Role SQLServer | Where-Object { $_.SsRsUri }

    if ($servers)
    {
        Write-ScreenInfo -Message "Installing SSRS on'$($servers.Name -join ',')'"
        Write-ScreenInfo -Message "Installing .net Framework 4.8 on '$($servers.Name -join ',')'"
        Install-LabSoftwarePackage -Path $dotnet48InstallFile.FullName -CommandLine '/q /norestart /log c:\DeployDebug\dotnet48.txt' -ComputerName $servers -UseShellExecute
        Restart-LabVM -ComputerName $servers -Wait
    }

    $jobs = @()

    foreach ($server in $servers)
    {
        Write-ScreenInfo "Installing SQL Server Reporting Services on $server" -NoNewLine
        if (-not $server.SsRsUri)
        {
            Write-ScreenInfo -Message "No SSRS URI available for $server. Please provide a valid URI in AutomatedLab.psd1 and try again. Skipping..." -Type Warning
            continue
        }
        $downloadFolder = Join-Path -Path $global:labSources\SoftwarePackages -ChildPath "SQL$($server.SqlVersion)"

        if ($lab.DefaultVirtualizationEngine -ne 'Azure' -and -not (Test-Path $downloadFolder))
        {
            $null = New-Item -ItemType Directory -Path $downloadFolder
        }

        Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name SqlServerReportBuilder) -Path $labSources\SoftwarePackages\ReportBuilder.msi
        Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name Sql$($server.SqlVersion)SSRS) -Path $downloadFolder\SQLServerReportingServices.exe

        Install-LabSoftwarePackage -Path $labsources\SoftwarePackages\ReportBuilder.msi -ComputerName $server
        Install-LabSoftwarePackage -Path $downloadFolder\SQLServerReportingServices.exe -CommandLine '/Quiet /IAcceptLicenseTerms' -ComputerName $server
        Invoke-LabCommand -ActivityName 'Configuring SSRS' -ComputerName $server -FilePath $labSources\PostInstallationActivities\SqlServer\SetupSqlServerReportingServices.ps1
    }
    #endregion

    #region Install Tools
    $servers = Get-LabVM -Role SQLServer | Where-Object { $_.SsmsUri }

    if ($servers)
    {
        Write-ScreenInfo -Message "Installing SQL Server Management Studio on '$($servers.Name -join ',')' in the background."
    }

    $jobs = @()

    foreach ($server in $servers)
    {
        if (-not $server.SsmsUri)
        {
            Write-ScreenInfo -Message "No SSMS URI available for $server. Please provide a valid URI in AutomatedLab.psd1 and try again. Skipping..." -Type Warning
            continue
        }

        $downloadFolder = Join-Path -Path $global:labSources\SoftwarePackages -ChildPath "SQL$($server.SqlVersion)"
        $downloadPath = Join-Path -Path $downloadFolder -ChildPath 'SSMS-Setup-ENU.exe'

        if ($lab.DefaultVirtualizationEngine -ne 'Azure' -and -not (Test-Path $downloadFolder))
        {
            $null = New-Item -ItemType Directory -Path $downloadFolder
        }

        Get-LabInternetFile -Uri $server.SsmsUri -Path $downloadPath -NoDisplay

        $jobs += Install-LabSoftwarePackage -Path $downloadPath -CommandLine '/install /quiet' -ComputerName $server -NoDisplay -AsJob -PassThru
    }

    if ($jobs)
    {
        Write-ScreenInfo 'Waiting for SQL Server Management Studio installation jobs to finish' -NoNewLine
        Wait-LWLabJob -Job $jobs -Timeout 10 -NoDisplay -ProgressIndicator 30
    }
    #endregion

    if ($CreateCheckPoints)
    {
        Checkpoint-LabVM -ComputerName ($machines | Where-Object HostType -eq 'HyperV') -SnapshotName 'Post SQL Server Installation'
    }

    foreach ($machine in $machines)
    {
        $role = $machine.Roles | Where-Object Name -like SQLServer*

        if ([System.Convert]::ToBoolean($role.Properties['InstallSampleDatabase']))
        {
            Install-LabSqlSampleDatabases -Machine $machine
        }
    }

    Write-LogFunctionExit
}


function Get-LabBuildStep
{
    param
    (
        [string]
        $ComputerName
    )

    if (-not (Get-Lab -ErrorAction SilentlyContinue))
    {
        throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server'
    }

    $tfsvm = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1

    if ($ComputerName)
    {
        $tfsVm = Get-LabVm -ComputerName $ComputerName
    }

    if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) }

    $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm
    $defaultParam.ProjectName = $ProjectName

    return (Get-TfsBuildStep @defaultParam)
}


function Get-LabReleaseStep
{
    param
    (
        [string]
        $ComputerName
    )

    if (-not (Get-Lab -ErrorAction SilentlyContinue))
    {
        throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server'
    }

    $tfsvm = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1
    
    if ($ComputerName)
    {
        $tfsVm = Get-LabVm -ComputerName $ComputerName
    }

    if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) }
    
    $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm

    $defaultParam.ProjectName = $ProjectName

    return (Get-TfsReleaseStep @defaultParam)
}


function Get-LabTfsFeed
{
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ComputerName,

        [string]
        $FeedName
    )
    
    $lab = Get-Lab
    $tfsVm = Get-LabVM -ComputerName $ComputerName
    $defaultParam = Get-LabTfsParameter -ComputerName $ComputerName
    
    $defaultParam['FeedName']   = $FeedName
    $defaultParam['ApiVersion'] = '5.0-preview.1'
    
    $feed = Get-TfsFeed @defaultParam
        
    if (-not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        if ($feed.url -match 'http(s?)://(?<Host>[\w\.]+):(?<Port>\d+)/')
        {
            $feed.url = $feed.url.Replace($Matches.Host, $tfsVm.AzureConnectionInfo.DnsName)
            $feed.url = $feed.url.Replace($Matches.Port, $defaultParam.Port)
        }
    }

    if ($feed.url -match '(?<url>http.*)\/_apis')
    {
        $nugetV2Url = '{0}/_packaging/{1}/nuget/v2' -f $Matches.url, $feed.name
        $feed | Add-Member -Name NugetV2Url -MemberType NoteProperty $nugetV2Url
        
        $feed | Add-Member -Name NugetCredential -MemberType NoteProperty ($tfsVm.GetCredential($lab))
        
        $nugetApiKey = '{0}@{1}:{2}' -f $feed.NugetCredential.GetNetworkCredential().UserName, $feed.NugetCredential.GetNetworkCredential().Domain, $feed.NugetCredential.GetNetworkCredential().Password
        $feed | Add-Member -Name NugetApiKey -MemberType NoteProperty -Value $nugetApiKey
    }
    
    $feed
}


function Get-LabTfsParameter
{
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ComputerName,

        [switch]
        $Local
    )
    
    $lab = Get-Lab
    $tfsVm = Get-LabVM -ComputerName $ComputerName
    $role = $tfsVm.Roles | Where-Object -Property Name -match 'Tfs\d{4}|AzDevOps'
    $bwRole = $tfsVm.Roles | Where-Object -Property Name -eq TfsBuildWorker
    $initialCollection = 'AutomatedLab'
    $tfsPort = 8080
    $tfsInstance = if (-not $bwRole) {$tfsVm.FQDN} else {$bwRole.Properties.TfsServer}

    if ($role -and $role.Properties.ContainsKey('Port'))
    {
        $tfsPort = $role.Properties['Port']
    }
    if ($bwRole -and $bwRole.Properties.ContainsKey('Port'))
    {
        $tfsPort = $bwRole.Properties['Port']
    }

    if (-not $Local.IsPresent -and (Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and -not ($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment))
    {
        $tfsPort = if ($bwRole) {
            (Get-LWAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $bwRole.Properties.TfsServer -ErrorAction SilentlyContinue).FrontendPort
        }
        else
        {
            (Get-LWAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $tfsVm -ErrorAction SilentlyContinue).FrontendPort
        }

        if (-not $tfsPort)
        {
            Write-Error -Message 'There has been an error setting the Azure port during TFS installation. Cannot continue rolling out release pipeline'
            return
        }

        $tfsInstance = if ($bwRole) {
            (Get-LabVm $bwRole.Properties.TfsServer).AzureConnectionInfo.DnsName
        }
        else
        {
            $tfsVm.AzureConnectionInfo.DnsName
        }
    }

    if ($role -and $role.Properties.ContainsKey('InitialCollection'))
    {
        $initialCollection = $role.Properties['InitialCollection']
    }

    if ($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment)
    {
        $tfsInstance = 'dev.azure.com'
        $initialCollection = $role.Properties['Organisation']
        $accessToken = $role.Properties['PAT']
        $tfsPort = 443
    }

    if ($bwRole -and $bwRole.Properties.ContainsKey('Organisation'))
    {
        $tfsInstance = 'dev.azure.com'
        $initialCollection = $bwRole.Properties['Organisation']
        $accessToken = $bwRole.Properties['PAT']
        $tfsPort = 443
    }

    if (-not $role)
    {
        $tfsVm = Get-LabVm -ComputerName $bwrole.Properties.TfsServer
        $role = $tfsVm.Roles | Where-Object -Property Name -match 'Tfs\d{4}|AzDevOps'
    }
    $credential = $tfsVm.GetCredential((Get-Lab))
    $useSsl = $tfsVm.InternalNotes.ContainsKey('CertificateThumbprint') -or ($role.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or ($bwRole -and $bwRole.Properties.ContainsKey('Organisation'))
    
    $defaultParam = @{
        InstanceName         = $tfsInstance
        Port                 = $tfsPort
        CollectionName       = $initialCollection
        UseSsl               = $useSsl
        SkipCertificateCheck = $true
    }

    $defaultParam.ApiVersion = switch ($role.Name)
    {
        'Tfs2015' { '2.0'; break }
        'Tfs2017' { '3.0'; break }
        { $_ -match '2018|AzDevOps' } { '4.0'; break }
        default { '2.0' }
    }

    if (($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or ($bwRole -and $bwRole.Properties.ContainsKey('Organisation')))
    {
        $defaultParam.ApiVersion = '5.1'
    }

    if ($accessToken)
    {
        $defaultParam.PersonalAccessToken = $accessToken
    }
    elseif ($credential)
    {
        $defaultParam.Credential = $credential
    }
    else
    {
        Write-ScreenInfo -Type Error -Message 'Neither Credential nor AccessToken are available. Unable to continue'
        return
    }

    $defaultParam
}


function Get-LabTfsUri
{
    [CmdletBinding()]
    param
    (
        [string]
        $ComputerName
    )

    if (-not (Get-Lab -ErrorAction SilentlyContinue))
    {
        throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server'
    }

    $tfsvm = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1

    if ($ComputerName)
    {
        $tfsVm = Get-LabVM -ComputerName $ComputerName
    }

    if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) }

    $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm

    if (($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or $defaultParam.Contains('PersonalAccessToken'))
    {
        'https://{0}/{1}' -f $defaultParam.InstanceName, $defaultParam.CollectionName
    }
    elseif ($defaultParam.UseSsl)
    {
        'https://{0}:{1}@{2}:{3}/{4}' -f $defaultParam.Credential.GetNetworkCredential().UserName, $defaultParam.Credential.GetNetworkCredential().Password, $defaultParam.InstanceName, $defaultParam.Port, $defaultParam.CollectionName
    }
    else
    {
        'http://{0}:{1}@{2}:{3}/{4}' -f $defaultParam.Credential.GetNetworkCredential().UserName, $defaultParam.Credential.GetNetworkCredential().Password, $defaultParam.InstanceName, $defaultParam.Port, $defaultParam.CollectionName
    }
}


function Install-LabBuildWorker
{
    [CmdletBinding()]
    param
    ( )

    $buildWorkers = Get-LabVM -Role TfsBuildWorker
    if (-not $buildWorkers)
    {
        return
    }

    $buildWorkerUri = Get-LabConfigurationItem -Name BuildAgentUri
    $buildWorkerPath = Join-Path -Path $labsources -ChildPath Tools\TfsBuildWorker.zip
    $download = Get-LabInternetFile -Uri $buildWorkerUri -Path $buildWorkerPath -PassThru
    Copy-LabFileItem -ComputerName $buildWorkers -Path $download.Path

    $installationJobs = @()
    foreach ($machine in $buildWorkers)
    {
        $role = $machine.Roles | Where-Object Name -eq TfsBuildWorker
        [int]$numberOfBuildWorkers = $role.Properties.NumberOfBuildWorkers
        $isOnDomainController = $machine -in (Get-LabVM -Role ADDS)
        $cred = $machine.GetLocalCredential()
        $tfsServer = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1

        $tfsPort = 8080
        $skipServerDuringTest = $false # We want to skip testing public Azure DevOps endpoints

        if ($role.Properties.ContainsKey('Organisation') -and $role.Properties.ContainsKey('PAT'))
        {
            Write-ScreenInfo -Message "Deploying agent to Azure DevOps agent pool" -NoNewLine
            $tfsServer = 'dev.azure.com'
            $useSsl = $true
            $tfsPort = 443
            $skipServerDuringTest = $true
        }
        elseif ($role.Properties.ContainsKey('TfsServer'))
        {
            $tfsServer = Get-LabVM -ComputerName $role.Properties['TfsServer'] -ErrorAction SilentlyContinue
            if (-not $tfsServer)
            {
                Write-ScreenInfo -Message "No TFS server called $($role.Properties['TfsServer']) found in lab." -NoNewLine -Type Warning
                $tfsServer = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1
                $role.Properties['TfsServer'] = $tfsServer.Name
                $shouldExport = $true
                Write-ScreenInfo -Message " Selecting $tfsServer instead." -Type Warning
            }

            $useSsl = $tfsServer.InternalNotes.ContainsKey('CertificateThumbprint') -or ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment)
            
            if ($useSsl)
            {
                $machine.InternalNotes.CertificateThumpbrint = 'Use Ssl'
                $shouldExport = $true
            }
        }
        else
        {
            $useSsl = $tfsServer.InternalNotes.ContainsKey('CertificateThumbprint') -or ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment)
            if ($useSsl)
            {
                $machine.InternalNotes.CertificateThumpbrint = 'Use Ssl'
            }
            $role.Properties.Add('TfsServer', $tfsServer.Name)
            $shouldExport = $true
        }

        if ($shouldExport) { Export-Lab }

        $tfsTest = Test-LabTfsEnvironment -ComputerName $tfsServer -NoDisplay -SkipServer:$skipServerDuringTest
        if ($tfsTest.ServerDeploymentOk -and $tfsTest.BuildWorker[$machine.Name].WorkerDeploymentOk)
        {
            Write-ScreenInfo -Message "Build worker $machine assigned to $tfsServer appears to be configured. Skipping..."
            continue
        }

        $tfsRole = $tfsServer.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps'
        if ($tfsRole -and $tfsRole.Properties.ContainsKey('Port'))
        {
            $tfsPort = $tfsRole.Properties['Port']
        }

        [string]$machineName = $tfsServer

        if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and -not ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment))
        {
            $tfsPort = (Get-LabAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $tfsServer -ErrorAction SilentlyContinue).Port
            $machineName = $tfsServer.AzureConnectionInfo.DnsName

            if (-not $tfsPort)
            {
                Write-Error -Message 'There has been an error setting the Azure port during TFS installation. Cannot continue installing build worker.'
                return
            }
        }

        $pat = if ($role.Properties.ContainsKey('PAT'))
        {
            $role.Properties['PAT']
            $machineName = "dev.azure.com/$($role.Properties['Organisation'])"
        }
        elseif ($tfsRole.Properties.ContainsKey('PAT'))
        {
            $tfsRole.Properties['PAT']
            $machineName = "dev.azure.com/$($tfsRole.Properties['Organisation'])"
        }
        else
        {
            [string]::Empty
        }

        $agentPool = if ($role.Properties.ContainsKey('AgentPool'))
        {
            $role.Properties['AgentPool']
        }
        else
        {
            'default'
        }

        $installationJobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock {

            if (-not (Test-Path C:\TfsBuildWorker.zip)) { throw 'Build worker installation files not available' }

            if ($numberOfBuildWorkers)
            {
                $numberOfBuildWorkers = 1..$numberOfBuildWorkers
            }
            else
            {
                $numberOfBuildWorkers = 1
            }
            foreach ($numberOfBuildWorker in $numberOfBuildWorkers)
            {
                Microsoft.PowerShell.Archive\Expand-Archive -Path C:\TfsBuildWorker.zip -DestinationPath "C:\BuildWorker$numberOfBuildWorker" -Force
                $configurationTool = Get-Item "C:\BuildWorker$numberOfBuildWorker\config.cmd" -ErrorAction Stop

                $content = if ($useSsl -and [string]::IsNullOrEmpty($pat))
                {
                    "$configurationTool --unattended --url https://$($machineName):$($tfsPort) --auth Integrated --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --sslskipcertvalidation --gituseschannel"

                }
                elseif ($useSsl -and -not [string]::IsNullOrEmpty($pat))
                {
                    "$configurationTool --unattended --url https://$($machineName) --auth pat --token $pat --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --sslskipcertvalidation --gituseschannel"
                }
                elseif (-not $useSsl -and -not [string]::IsNullOrEmpty($pat))
                {
                    "$configurationTool --unattended --url http://$($machineName) --auth pat --token $pat --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --gituseschannel"
                }
                else
                {
                    "$configurationTool --unattended --url http://$($machineName):$($tfsPort) --auth Integrated --pool $agentPool --agent $env:COMPUTERNAME --runasservice --gituseschannel"
                }

                if ($isOnDomainController)
                {
                    $content += " --windowsLogonAccount $($cred.UserName) --windowsLogonPassword $($cred.GetNetworkCredential().Password)"
                }

                $null = New-Item -ItemType Directory -Path C:\DeployDebug -ErrorAction SilentlyContinue
                Set-Content -Path "C:\DeployDebug\SetupBuildWorker$numberOfBuildWorker.cmd" -Value $content -Force

                $configResult = & "C:\DeployDebug\SetupBuildWorker$numberOfBuildWorker.cmd"

                $log = Get-ChildItem -Path "C:\BuildWorker$numberOfBuildWorker\_diag" -Filter *.log | Sort-Object -Property CreationTime | Select-Object -Last 1

                [pscustomobject]@{
                    ConfigResult = $configResult
                    LogContent   = $log | Get-Content 
                }

                if ($LASTEXITCODE -notin 0, 3010)
                {
                    Write-Warning -Message "Build worker $numberOfBuildWorker on '$env:COMPUTERNAME' failed to install. Exit code was $($LASTEXITCODE). Log is $($Log.FullName)"
                }
            }
        } -AsJob -Variable (Get-Variable machineName, tfsPort, useSsl, pat, isOnDomainController, cred, numberOfBuildWorkers, agentPool) -ActivityName "TFS_Agent_$machine" -PassThru -NoDisplay
    }

    Wait-LWLabJob -Job $installationJobs

    foreach ($job in $installationJobs)
    {
        $name = $job.Name.Replace('TFS_Agent_','')
        $type = if ($job.State -eq 'Completed') { 'Verbose' } else { 'Error' }
        $resultVariable = New-Variable -Name ("AL_TFSAgent_$($name)_$([guid]::NewGuid().Guid)") -Scope Global -PassThru
        Write-ScreenInfo -Type $type -Message "TFS Agent deployment $($job.State.ToLower()) on '$($name)'. The job output of $job can be retrieved with `${$($resultVariable.Name)}"
        $resultVariable.Value = $job | Receive-Job -AutoRemoveJob -Wait
    }
}


function Install-LabTeamFoundationEnvironment
{
    [CmdletBinding()]
    param
    ( )

    $tfsMachines = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object {
        -not $_.SkipDeployment -and -not (Test-LabTfsEnvironment -ComputerName $_.Name -NoDisplay).ServerDeploymentOk
    }
    $azDevOpsService = Get-LabVM -Role AzDevOps | Where-Object SkipDeployment

    foreach ($svcConnection in $azDevOpsService)
    {
        $role = $svcConnection.Roles | Where-Object Name -Match 'AzDevOps'

        # Override port or add if empty
        $role.Properties.Port = 443
        $svcConnection.InternalNotes.Add('CertificateThumbprint', 'use SSL')
        if (-not $role.Properties.ContainsKey('PAT'))
        {
            Write-ScreenInfo -Type Error -Message "No Personal Access Token available for Azure DevOps connection to $svcConnection.
                You will be unable to deploy build workers and you will not be able to use the cmdlets New-LabReleasePipeline, Get-LabBuildStep, Get-LabReleaseStep.
            Consider adding the key PAT to your role properties hashtable."

        }

        if (-not $role.Properties.ContainsKey('Organisation'))
        {
            Write-ScreenInfo -Type Error -Message "No Organisation name available for Azure DevOps connection to $svcConnection.
                You will be unable to deploy build workers and you will not be able to use the cmdlets New-LabReleasePipeline, Get-LabBuildStep, Get-LabReleaseStep.
            Consider adding the key Organisation to your role properties hashtable where Organisation = dev.azure.com/<Organisation>"

        }
    }

    if ($azDevOpsService) { Export-Lab }

    $lab = Get-Lab
    $jobs = @()

    foreach ($machine in $tfsMachines)
    {
        Dismount-LabIsoImage -ComputerName $machine -SupressOutput

        $role = $machine.Roles | Where-Object Name -Match 'Tfs\d{4}|AzDevOps'
        $isoPath = ($lab.Sources.ISOs | Where-Object Name -eq $role.Name).Path

        $retryCount = 3
        $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name]
        while (-not $autoLogon -and $retryCount -gt 0)
        {
            Enable-LabAutoLogon -ComputerName $machine
            Restart-LabVm -ComputerName $machine -Wait

            $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name]
            $retryCount--
        }

        if (-not $autoLogon)
        {
            throw "No logon session available for $($machine.InstallationUser.UserName). Cannot continue with TFS setup for $machine"
        }

        Mount-LabIsoImage -ComputerName $machine -IsoPath $isoPath -SupressOutput

        $jobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock {
            $startTime = (Get-Date)
            while (-not $dvdDrive -and (($startTime).AddSeconds(120) -gt (Get-Date)))
            {
                Start-Sleep -Seconds 2
                if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
                {
                    $dvdDrive = (Get-CimInstance -Class Win32_CDRomDrive | Where-Object MediaLoaded).Drive
                }
                else
                {
                    $dvdDrive = (Get-WmiObject -Class Win32_CDRomDrive | Where-Object MediaLoaded).Drive
                }
            }

            if ($dvdDrive)
            {
                $executable = (Get-ChildItem -Path $dvdDrive -Filter *.exe).FullName
                $installation = Start-Process -FilePath $executable -ArgumentList '/quiet' -Wait -LoadUserProfile -PassThru

                if ($installation.ExitCode -notin 0, 3010)
                {
                    throw "TFS Setup failed with exit code $($installation.ExitCode)"
                }

                Write-Verbose 'TFS Installation finished. Configuring...'
            }
            else
            {
                Write-Error -Message 'No ISO mounted. Cannot continue.'
            }
        } -AsJob -PassThru -NoDisplay
    }

    # If not already set, ignore certificate issues throughout the TFS interactions
    try { [ServerCertificateValidationCallback]::Ignore() } catch { }

    if ($tfsMachines)
    {
        Wait-LWLabJob -Job $jobs
        Restart-LabVm -ComputerName $tfsMachines -Wait
        Install-LabTeamFoundationServer
    }

    Install-LabBuildWorker
    Set-LabBuildWorkerCapability
}


function New-LabReleasePipeline
{
    [CmdletBinding(DefaultParameterSetName = 'CloneRepo')]
    param
    (
        [string]
        $ProjectName = 'ALSampleProject',

        [Parameter(Mandatory, ParameterSetName = 'CloneRepo')]
        [Parameter(ParameterSetName = 'LocalSource')]
        [string]
        $SourceRepository,

        [Parameter(Mandatory, ParameterSetName = 'LocalSource')]
        [string]
        $SourcePath,

        [ValidateSet('Git', 'FileCopy')]
        [string]$CodeUploadMethod = 'Git',

        [string]
        $ComputerName,

        [hashtable[]]
        $BuildSteps,

        [hashtable[]]
        $ReleaseSteps
    )

    if (-not (Get-Lab -ErrorAction SilentlyContinue))
    {
        throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server'
    }

    if ($CodeUploadMethod -eq 'Git' -and -not $SourceRepository)
    {
        throw "Using the code upload method 'Git' requires a source repository to be defined."
    }

    $tfsVm = if ($ComputerName)
    {
        Get-LabVM -ComputerName $ComputerName
    }
    else
    {
        Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1
    }

    if (-not $tfsVm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) }

    $localLabSources = Get-LabSourcesLocationInternal -Local

    $role = $tfsVm.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps'
    $originalPort = 8080
    $tfsInstance = $tfsVm.FQDN

    if ($role.Properties.ContainsKey('Port'))
    {
        $originalPort = $role.Properties['Port']
    }

    $gitBinary = if (Get-Command git) { (Get-Command git).Source } elseif (Test-Path -Path $localLabSources\Tools\git.exe) { "$localLabSources\Tools\git.exe" }
    if (-not $gitBinary)
    {
        Write-ScreenInfo -Message 'Git is not installed. We are not be able to push any code to the remote repository and cannot proceed. Please install Git'
        return
    }

    $defaultParam = Get-LabTfsParameter -ComputerName $tfsVm
    $defaultParam.ProjectName = $ProjectName

    $project = New-TfsProject @defaultParam -SourceControlType Git -TemplateName 'Agile' -Timeout (New-TimeSpan -Minutes 5)
    $repository = Get-TfsGitRepository @defaultParam

    if ($CodeUploadMethod -eq 'git' -and -not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        $repository.remoteUrl = $repository.remoteUrl -replace $originalPort, $defaultParam.Port
        if ($repository.remoteUrl -match 'http(s?)://(?<Host>[\w\.]+):')
        {
            $repository.remoteUrl = $repository.remoteUrl.Replace($Matches.Host, $tfsVm.AzureConnectionInfo.DnsName)
        }
    }

    if ($CodeUploadMethod -eq 'FileCopy' -and -not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure')
    {
        if ($repository.remoteUrl -match 'http(s?)://(?<Host>[\w\.]+):')
        {
            $repository.remoteUrl = $repository.remoteUrl.Replace($Matches.Host, $tfsVm.FQDN)
        }
    }

    if ($SourceRepository)
    {
        if (-not $gitBinary)
        {
            Write-Error "Git.exe could not be located, cannot clone repository from '$SourceRepository'"
            return
        }

        # PAT URLs look like https://{yourPAT}@dev.azure.com/yourOrgName/yourProjectName/_git/yourRepoName
        $repoUrl = if ($defaultParam.Contains('PersonalAccessToken'))
        {
            $tmp = $repository.remoteUrl.Insert($repository.remoteUrl.IndexOf('/') + 2, '{0}@')
            $tmp -f $defaultParam.PersonalAccessToken
        }
        else
        {
            $tmp = $repository.remoteUrl.Insert($repository.remoteUrl.IndexOf('/') + 2, '{0}:{1}@')
            $tmp -f $defaultParam.Credential.GetNetworkCredential().UserName.ToLower(), $defaultParam.Credential.GetNetworkCredential().Password
        }

        Write-ScreenInfo -Type Verbose -Message "Generated repo url $repoUrl"

        if (-not $SourcePath)
        {
            $SourcePath = "$localLabSources\GitRepositories\$((Get-Lab).Name)"
        }

        if (-not (Test-Path -Path $SourcePath))
        {
            Write-ScreenInfo -Type Verbose -Message "Creating $SourcePath to contain your cloned repos"
            [void] (New-Item -ItemType Directory -Path $SourcePath -Force)
        }

        $repositoryPath = Join-Path -Path $SourcePath -ChildPath (Split-Path -Path $SourceRepository -Leaf)
        if (-not (Test-Path $repositoryPath))
        {
            Write-ScreenInfo -Type Verbose -Message "Creating $repositoryPath to contain your cloned repo"
            [void] (New-Item -ItemType Directory -Path $repositoryPath)
        }

        Push-Location
        Set-Location -Path $repositoryPath

        if (Join-Path -Path $repositoryPath -ChildPath '.git' -Resolve -ErrorAction SilentlyContinue)
        {
            Write-ScreenInfo -Type Verbose -Message ('There already is a clone of {0} in {1}. Pulling latest changes from remote if possible.' -f $SourceRepository, $repositoryPath)
            try
            {
                $errorFile = [System.IO.Path]::GetTempFileName()
                $pullResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'pull', 'origin') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile

                if ($pullResult.ExitCode -ne 0)
                {
                    Write-ScreenInfo -Type Warning -Message "Could not pull from $SourceRepository. Git returned: $(Get-Content -Path $errorFile)"
                }
            }
            finally
            {
                Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue
            }
        }
        else
        {
            Write-ScreenInfo -Type Verbose -Message ('Cloning {0} in {1}.' -f $SourceRepository, $repositoryPath)
            try
            {
                $retries = 3
                $errorFile = [System.IO.Path]::GetTempFileName()

                $cloneResult = Start-Process -FilePath $gitBinary -ArgumentList @('clone', $SourceRepository, $repositoryPath, '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile
                while ($cloneResult.ExitCode -ne 0 -and $retries -gt 0)
                {
                    Write-ScreenInfo "Could not clone the repository '$SourceRepository', retrying ($retries)..."
                    Start-Sleep -Seconds 5
                    $cloneResult = Start-Process -FilePath $gitBinary -ArgumentList @('clone', $SourceRepository, $repositoryPath, '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile
                    $retries--
                }

                if ($cloneResult.ExitCode -ne 0)
                {
                    Write-Error "Could not clone from $SourceRepository. Git returned: $(Get-Content -Path $errorFile)"
                }
            }
            finally
            {
                Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue
            }
        }

        Pop-Location
    }

    if ($CodeUploadMethod -eq 'Git')
    {
        Push-Location
        Set-Location -Path $repositoryPath

        try
        {
            $errorFile = [System.IO.Path]::GetTempFileName()
            $addRemoteResult = Start-Process -FilePath $gitBinary -ArgumentList @('remote', 'add', 'tfs', $repoUrl) -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile
            if ($addRemoteResult.ExitCode -ne 0)
            {
                Write-Error "Could not add remote tfs to $repoUrl. Git returned: $(Get-Content -Path $errorFile)"
            }
        }
        finally
        {
            Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue
        }
        try
        {
            $pattern = '(?>remotes\/origin\/)(?<BranchName>[\w\/]+)'
            $branches = git branch -a | Where-Object { $_ -cnotlike '*HEAD*' -and $_ -like ' remotes/origin*' }

            foreach ($branch in $branches)
            {
                $branch -match $pattern | Out-Null

                $null = git checkout $Matches.BranchName 2>&1
                if ($LASTEXITCODE -eq 0)
                {
                    $retries = 3
                    $errorFile = [System.IO.Path]::GetTempFileName()

                    $pushResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'push', 'tfs', '--all', '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile
                    while ($pushResult.ExitCode -ne 0 -and $retries -gt 0)
                    {
                        Write-ScreenInfo "Could not push the repository in '$pwd' to TFS, retrying ($retries)..."
                        Start-Sleep -Seconds 5
                        $pushResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'push', 'tfs', '--all', '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile
                        $retries--
                    }

                    if ($pushResult.ExitCode -ne 0)
                    {
                        Write-Error "Could not push to $repoUrl. Git returned: $(Get-Content -Path $errorFile)"
                    }
                }
            }
        }
        finally
        {
            Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue
        }

        Pop-Location

        Write-ScreenInfo -Type Verbose -Message ('Pushed code from {0} to remote {1}' -f $SourceRepository, $repoUrl)
    }
    else
    {
        $remoteGitBinary = Invoke-LabCommand -ActivityName 'Test Git availibility' -ComputerName $tfsVm -ScriptBlock {

            if (Get-Command git) { (Get-Command git).Source } elseif (Test-Path -Path $localLabSources\Tools\git.exe) { "$localLabSources\Tools\git.exe" }

        } -PassThru

        if (-not $remoteGitBinary)
        {
            Write-ScreenInfo -Message "Git is not installed on '$tfsVm'. We are not be able to push any code to the remote repository and cannot proceed. Please install Git on '$tfsVm'"
            return
        }

        $repoDestination = if ($IsLinux -or $IsMacOs) { "/$ProjectName.temp" } else { "C:\$ProjectName.temp" }
        if ($repositoryPath)
        {
            Copy-LabFileItem -Path $repositoryPath -ComputerName $tfsVm -DestinationFolderPath $repoDestination -Recurse
        }
        else
        {
            Copy-LabFileItem -Path $SourcePath -ComputerName $tfsVm -DestinationFolderPath $repoDestination -Recurse
        }

        Invoke-LabCommand -ActivityName 'Push code to TFS/AZDevOps' -ComputerName $tfsVm -ScriptBlock {

            Set-Location -Path "C:\$ProjectName.temp\$ProjectName"

            git remote add tfs $repoUrl

            $pattern = '(?>remotes\/origin\/)(?<BranchName>[\w\/]+)'
            $branches = git branch -a | Where-Object { $_ -cnotlike '*HEAD*' -and -not $_.StartsWith('*') }

            foreach ($branch in $branches)
            {
                if ($branch -match $pattern)
                {
                    $null = git checkout $Matches.BranchName 2>&1
                    if ($LASTEXITCODE -eq 0)
                    {
                        git add . 2>&1
                        git commit -m 'Initial' 2>&1
                        git -c http.sslVerify=false push --set-upstream tfs $Matches.BranchName 2>&1
                    }
                }
            }

            Set-Location -Path C:\
            Remove-Item -Path "C:\$ProjectName.temp" -Recurse -Force
        } -Variable (Get-Variable -Name repoUrl, ProjectName)
    }

    if (-not ($role.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment))
    {
        Invoke-LabCommand -ActivityName 'Clone local repo from TFS' -ComputerName $tfsVm -ScriptBlock {

            if (-not (Test-Path -Path C:\Git))
            {
                New-Item -ItemType Directory -Path C:\Git | Out-Null
            }
            Set-Location -Path C:\Git
            git -c http.sslVerify=false clone $repoUrl 2>&1

        } -Variable (Get-Variable -Name repoUrl, ProjectName)
    }

    if ($BuildSteps.Count -gt 0)
    {
        $buildParameters = $defaultParam.Clone()
        $buildParameters.DefinitionName = "$($ProjectName)Build"
        $buildParameters.BuildTasks = $BuildSteps
        New-TfsBuildDefinition @buildParameters
    }

    if ($ReleaseSteps.Count -gt 0)
    {
        $releaseParameters = $defaultParam.Clone()
        $releaseParameters.ReleaseName = "$($ProjectName)Release"
        $releaseParameters.ReleaseTasks = $ReleaseSteps
        New-TfsReleaseDefinition @releaseParameters
    }
}


function New-LabTfsFeed
{
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ComputerName,
        
        [Parameter(Mandatory)]
        [string]
        $FeedName,
        
        [object[]]
        $FeedPermissions,
        
        [switch]
        $PassThru
    )
    
    $tfsVm = Get-LabVM -ComputerName $computerName
    $role = $tfsVm.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps'
    $defaultParam = Get-LabTfsParameter -ComputerName $ComputerName
    $defaultParam['FeedName']   = $FeedName
    $defaultParam['ApiVersion'] = '5.0-preview.1'
    
    try
    {
        New-TfsFeed @defaultParam -ErrorAction Stop
        
        if ($FeedPermissions)
        {
            Set-TfsFeedPermission @defaultParam -Permissions $FeedPermissions
        }
    }
    catch
    {
        Write-Error $_
    }
    
    if ($PassThru)
    {
        Get-LabTfsFeed -ComputerName $ComputerName -FeedName $FeedName
    }
}


function Open-LabTfsSite
{
    param
    (
        [string]
        $ComputerName
    )

    Start-Process -FilePath (Get-LabTfsUri @PSBoundParameters)
}


function Test-LabTfsEnvironment
{
    param
    (
        [Parameter(Mandatory)]
        [string]
        $ComputerName,

        [switch]
        $SkipServer,

        [switch]
        $SkipWorker,

        [switch]
        $NoDisplay
    )

    $lab = Get-Lab -ErrorAction Stop
    $machine = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object -Property Name -eq $ComputerName
    $assignedBuildWorkers = Get-LabVm -Role TfsBuildWorker | Where-Object {
        ($_.Roles | Where-Object Name -eq TfsBuildWorker)[0].Properties['TfsServer'] -eq $machine.Name -or `
        ($_.Roles | Where-Object Name -eq TfsBuildWorker)[0].Properties.ContainsKey('PAT')
    }

    if (-not $machine -and -not $SkipServer.IsPresent) { return }

    if (-not $script:tfsDeploymentStatus)
    {
        $script:tfsDeploymentStatus = @{ }
    }

    if (-not $script:tfsDeploymentStatus.ContainsKey($ComputerName))
    {
        $script:tfsDeploymentStatus[$ComputerName] = @{ServerDeploymentOk = $SkipServer.IsPresent; BuildWorker = @{ } }
    }

    if (-not $script:tfsDeploymentStatus[$ComputerName].ServerDeploymentOk)
    {
        $uri = Get-LabTfsUri -ComputerName $machine -ErrorAction SilentlyContinue
        if ($null -eq $uri)
        {
            Write-PSFMessage -Message "TFS URI could not be determined."
            return $script:tfsDeploymentStatus[$ComputerName]
        }

        $defaultParam = Get-LabTfsParameter -ComputerName $machine
        $defaultParam.ErrorAction    = 'Stop'
        $defaultParam.ErrorVariable  = 'apiErr'

        try
        {
            $param = @{
                Method      = 'Get'
                Uri         = $uri
                ErrorAction = 'Stop'
            }

            if ($PSEdition -eq 'Core' -and (Get-Command Invoke-RestMethod).Parameters.ContainsKey('SkipCertificateCheck'))
            {
                $param.SkipCertificateCheck = $true
            }

            if ($accessToken)
            {
                $param.Headers = @{Authorization = Get-TfsAccessTokenString -PersonalAccessToken $accessToken }
            }
            else
            {
                $param.Credential = $defaultParam.credential
            }

            $null = Invoke-RestMethod @param
        }
        catch
        {
            Write-ScreenInfo -Type Error -Message "TFS URI $uri could not be accessed. Exception: $($_.Exception)"
            return $script:tfsDeploymentStatus[$ComputerName]
        }

        try
        {
            $null = Get-TfsProject @defaultParam
        }
        catch
        {
            Write-ScreenInfo -Type Error -Message "TFS URI $uri accessible, but no API call was possible. Exception: $($apiErr)"
            return $script:tfsDeploymentStatus[$ComputerName]
        }

        $script:tfsDeploymentStatus[$ComputerName].ServerDeploymentOk = $true
    }

    foreach ($worker in $assignedBuildWorkers)
    {
        if ($script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name].WorkerDeploymentOk)
        {
            continue
        }
        if (-not $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name])
        {
            $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name] = @{WorkerDeploymentOk = $SkipWorker.IsPresent }
        }

        if ($SkipWorker.IsPresent)
        {
            continue
        }

        $svcRunning = Invoke-LabCommand -PassThru -ComputerName $worker -ScriptBlock { Get-Service -Name *vsts* } -NoDisplay
        $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name].WorkerDeploymentOk = $svcRunning.Status -eq 'Running'
    }

    return $script:tfsDeploymentStatus[$ComputerName]
}


function Checkpoint-LabVM
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [string]$SnapshotName,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    if (-not (Get-LabVM))
    {
        Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $lab = Get-Lab

    if ($ComputerName)
    {
        $machines = Get-LabVM -IncludeLinux | Where-Object { $_.Name -in $ComputerName }
    }
    else
    {
        $machines = Get-LabVm -IncludeLinux
    }

    $machines = $machines | Where-Object SkipDeployment -eq $false

    if (-not $machines)
    {
        $message = 'No machine found to checkpoint. Either the given name is wrong or there is no machine defined yet'
        Write-LogFunctionExitWithError -Message $message
        return
    }

    Remove-LabPSSession -ComputerName $machines

    switch ($lab.DefaultVirtualizationEngine)
    {
        'HyperV' { Checkpoint-LWHypervVM -ComputerName $machines.ResourceName -SnapshotName $SnapshotName}
        'Azure'  { Checkpoint-LWAzureVM -ComputerName $machines.ResourceName -SnapshotName $SnapshotName}
        'VMWare' { Write-ScreenInfo -Type Error -Message 'Snapshotting VMWare VMs is not yet implemented'}
    }

    Write-LogFunctionExit
}


function Connect-LabVM
{
    param (
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [switch]$UseLocalCredential
    )

    $machines = Get-LabVM -ComputerName $ComputerName -IncludeLinux
    $lab = Get-Lab

    foreach ($machine in $machines)
    {
        if ($UseLocalCredential)
        {
            $cred = $machine.GetLocalCredential()
        }
        else
        {
            $cred = $machine.GetCredential($lab)
        }

        if ($machine.OperatingSystemType -eq 'Linux')
        {
            $sshBinary = Get-Command ssh.exe -ErrorAction SilentlyContinue
            if (-not $sshBinary) { Get-ChildItem $labsources\Tools\OpenSSH -Filter ssh.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 }

            if (-not $sshBinary -and -not (Get-LabConfigurationItem -Name DoNotPrompt))
            {
                $download = Read-Choice -ChoiceList 'No','Yes' -Caption 'Download Win32-OpenSSH' -Message 'OpenSSH is necessary to connect to Linux VMs. Would you like us to download Win32-OpenSSH for you?' -Default 1

                if ([bool]$download)
                {
                    $downloadUri = Get-LabConfigurationItem -Name OpenSshUri
                    $downloadPath = Join-Path ([System.IO.Path]::GetTempPath()) -ChildPath openssh.zip
                    $targetPath = "$labsources\Tools\OpenSSH"
                    Get-LabInternetFile -Uri $downloadUri -Path $downloadPath

                    Microsoft.PowerShell.Archive\Expand-Archive -Path $downloadPath -DestinationPath $targetPath -Force
                    $sshBinary = Get-ChildItem $labsources\Tools\OpenSSH -Filter ssh.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
                }
            }

            if ($UseLocalCredential)
            {
                $arguments = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l {0} {1}' -f $cred.UserName,$machine
            }
            else
            {
                $arguments = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l {0}@{2} {1}' -f $cred.UserName,$machine,$cred.GetNetworkCredential().Domain
            }

            Start-Process -FilePath $sshBinary.FullPath -ArgumentList $arguments
            return
        }

        if ($machine.HostType -eq 'Azure')
        {
            $cn = Get-LWAzureVMConnectionInfo -ComputerName $machine
            $cmd = 'cmdkey.exe /add:"TERMSRV/{0}" /user:"{1}" /pass:"{2}"' -f $cn.DnsName, $cred.UserName, $cred.GetNetworkCredential().Password
            Invoke-Expression $cmd | Out-Null
            mstsc.exe "/v:$($cn.DnsName):$($cn.RdpPort)" /f

            Start-Sleep -Seconds 5 #otherwise credentials get deleted too quickly

            $cmd = 'cmdkey /delete:TERMSRV/"{0}"' -f $cn.DnsName
            Invoke-Expression $cmd | Out-Null
        }
        elseif (Get-LabConfigurationItem -Name SkipHostFileModification)
        {
            $cmd = 'cmdkey.exe /add:"TERMSRV/{0}" /user:"{1}" /pass:"{2}"' -f $machine.IpAddress.ipaddress.AddressAsString, $cred.UserName, $cred.GetNetworkCredential().Password
            Invoke-Expression $cmd | Out-Null
            mstsc.exe "/v:$($machine.IpAddress.ipaddress.AddressAsString)" /f

            Start-Sleep -Seconds 1 #otherwise credentials get deleted too quickly

            $cmd = 'cmdkey /delete:TERMSRV/"{0}"' -f $machine.IpAddress.ipaddress.AddressAsString
            Invoke-Expression $cmd | Out-Null
        }
        else
        {
            $cmd = 'cmdkey.exe /add:"TERMSRV/{0}" /user:"{1}" /pass:"{2}"' -f $machine.Name, $cred.UserName, $cred.GetNetworkCredential().Password
            Invoke-Expression $cmd | Out-Null
            mstsc.exe "/v:$($machine.Name)" /f

            Start-Sleep -Seconds 1 #otherwise credentials get deleted too quickly

            $cmd = 'cmdkey /delete:TERMSRV/"{0}"' -f $machine.Name
            Invoke-Expression $cmd | Out-Null
        }
    }
}


function Copy-LabALCommon
{
    [CmdletBinding()]
    param
    ( 
        [Parameter(Mandatory)]
        [string[]]
        $ComputerName
    )
    
    $childPath = foreach ($vm in $ComputerName)
    {
        Invoke-LabCommand -ScriptBlock {
            if ($PSEdition -eq 'Core')
            {
                'core'
            } else
            {
                'full'
            }
        } -ComputerName $vm -NoDisplay -IgnoreAzureLabSources -DoNotUseCredSsp -PassThru |
        Add-Member -MemberType NoteProperty -Name ComputerName -Value $vm -Force -PassThru
    }

    $coreChild = @($childPath) -eq 'core'
    $fullChild = @($childPath) -eq 'full'
    $libLocation = Split-Path -Parent -Path (Split-Path -Path ([AutomatedLab.Common.Win32Exception]).Assembly.Location -Parent)

    if ($coreChild -and @(Invoke-LabCommand -ScriptBlock{
                Get-Item -Path '/ALLibraries/core/AutomatedLab.Common.dll' -ErrorAction SilentlyContinue
    } -ComputerName $coreChild.ComputerName -IgnoreAzureLabSources -NoDisplay -DoNotUseCredSsp -PassThru).Count -ne $coreChild.Count)
    {
        $coreLibraryFolder = Join-Path -Path $libLocation -ChildPath $coreChild[0]
        Copy-LabFileItem -Path $coreLibraryFolder -ComputerName $coreChild.ComputerName -DestinationFolderPath '/ALLibraries' -UseAzureLabSourcesOnAzureVm $false
    }

    if ($fullChild -and @(Invoke-LabCommand -ScriptBlock {
                Get-Item -Path '/ALLibraries/full/AutomatedLab.Common.dll' -ErrorAction SilentlyContinue
    } -ComputerName $fullChild.ComputerName -IgnoreAzureLabSources -NoDisplay -DoNotUseCredSsp -PassThru).Count -ne $fullChild.Count)
    {
        $fullLibraryFolder = Join-Path -Path $libLocation -ChildPath $fullChild[0]
        Copy-LabFileItem -Path $fullLibraryFolder -ComputerName $fullChild.ComputerName -DestinationFolderPath '/ALLibraries' -UseAzureLabSourcesOnAzureVm $false
    }
}


function Disable-LabAutoLogon
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [string[]]
        $ComputerName
    )

    Write-PSFMessage -Message "Disabling autologon on $($ComputerName.Count) machines"

    $Machines = Get-LabVm @PSBoundParameters

    Invoke-LabCommand -ActivityName "Disabling AutoLogon on $($ComputerName.Count) machines" -ComputerName $Machines -ScriptBlock {
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon -Value 0 -Type String -Force
        Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultPassword -Force -ErrorAction SilentlyContinue
    } -NoDisplay
}


function Disable-LabMachineAutoShutdown
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $ComputerName
    )

    $lab = Get-Lab -ErrorAction Stop
    if ($ComputerName.Count -eq 0)
    {
        $ComputerName = Get-LabVm | Where-Object SkipDeployment -eq $false
    }

    switch ($lab.DefaultVirtualizationEngine)
    {
        'Azure' {Disable-LWAzureAutoShutdown @PSBoundParameters -Wait}
        'HyperV' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on HyperV"}
        'VMWare' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on VMWare"}
    }
}


function Dismount-LabIsoImage
{
    param(
        [Parameter(Mandatory, Position = 0)]
        [string[]]$ComputerName,

        [switch]$SupressOutput
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName | Where-Object SkipDeployment -eq $false
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machine(s) $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }
    $machines | Where-Object HostType -notin HyperV, Azure | ForEach-Object {
        Write-ScreenInfo "Using ISO images is only supported with Hyper-V VMs or on Azure. Skipping machine '$($_.Name)'" -Type Warning
    }

    $hypervMachines = $machines | Where-Object HostType -eq HyperV
    $azureMachines = $machines | Where-Object HostType -eq Azure

    if ($azureMachines)
    {
        Dismount-LWAzureIsoImage -ComputerName $azureMachines
    }

    foreach ($hypervMachine in $hypervMachines)
    {
        if (-not $SupressOutput)
        {
            Write-ScreenInfo -Message "Dismounting currently mounted ISO image on computer '$hypervMachine'." -Type Info
        }

        Dismount-LWIsoImage -ComputerName $hypervMachine
    }

    Write-LogFunctionExit
}


function Enable-LabAutoLogon
{
    [CmdletBinding()]
    [Alias('Set-LabAutoLogon')]
    param
    (
        [Parameter()]
        [string[]]
        $ComputerName
    )

    Write-PSFMessage -Message "Enabling autologon on $($ComputerName.Count) machines"

    $machines = Get-LabVm @PSBoundParameters

    foreach ($machine in $machines)
    {
        $parameters = @{
            UserName = $machine.InstallationUser.UserName
            Password = $machine.InstallationUser.Password
        }

        if ($machine.IsDomainJoined)
        {
            if ($machine.Roles.Name -contains 'RootDC' -or $machine.Roles.Name -contains 'FirstChildDC' -or $machine.Roles.Name -contains 'DC')
            {
                $isAdReady = Test-LabADReady -ComputerName $machine
                
                if ($isAdReady)
                {
                    $parameters['DomainName'] = $machine.DomainName
                }
                else
                {
                    $parameters['DomainName'] = $machine.Name
                }
            }
            else
            {
                $parameters['DomainName'] = $machine.DomainName
            }
        }
        else
        {
            $parameters['DomainName'] = $machine.Name
        }

        Invoke-LabCommand -ActivityName "Enabling AutoLogon on $($machine.Name)" -ComputerName $machine.Name -ScriptBlock {
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon -Value 1 -Type String -Force
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoLogonCount -Value 9999 -Type DWORD -Force
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultDomainName -Value $parameters.DomainName -Type String -Force
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultPassword -Value $parameters.Password -Type String -Force
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultUserName -Value $parameters.UserName -Type String -Force
        } -Variable (Get-Variable parameters) -DoNotUseCredSsp -NoDisplay
    }
}


function Enable-LabMachineAutoShutdown
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $ComputerName,

        [Parameter(Mandatory)]
        [TimeSpan]
        $Time,

        [string]
        $TimeZone = (Get-TimeZone).Id
    )

    $lab = Get-Lab -ErrorAction Stop
    if ($ComputerName.Count -eq 0)
    {
        $ComputerName = Get-LabVm | Where-Object SkipDeployment -eq $false
    }

    switch ($lab.DefaultVirtualizationEngine)
    {
        'Azure' {Enable-LWAzureAutoShutdown @PSBoundParameters -Wait}
        'HyperV' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on HyperV"}
        'VMWare' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on VMWare"}
    }
}


function Get-LabMachineAutoShutdown
{
    [CmdletBinding()]
    param
    ( )

    $lab = Get-Lab -ErrorAction Stop

    switch ($lab.DefaultVirtualizationEngine)
    {
        'Azure' {Get-LWAzureAutoShutdown}
        'HyperV' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on HyperV"}
        'VMWare' {Write-ScreenInfo -Type Warning -Message "No auto-shutdown on VMWare"}
    }
}


function Get-LabVM
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    [OutputType([AutomatedLab.Machine])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'ByName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByRole')]
        [AutomatedLab.Roles]$Role,

        [Parameter(Mandatory, ParameterSetName = 'All')]
        [switch]$All,

        [scriptblock]$Filter,

        [switch]$IncludeLinux,

        [switch]$IsRunning,

        [Switch]$SkipConnectionInfo
    )

    begin
    {
        #required to suporess verbose messages, warnings and errors
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        Write-LogFunctionEntry

        $result = @()
        $script:data = Get-Lab -ErrorAction SilentlyContinue
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByName')
        {
            if ($ComputerName)
            {
                foreach ($n in $ComputerName)
                {
                    $machine = $Script:data.Machines | Where-Object Name -Like $n
                    if (-not $machine)
                    {
                        continue
                    }

                    $result += $machine
                }
            }
            else
            {
                $result = $Script:data.Machines
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'ByRole')
        {
            $result = $Script:data.Machines |
            Where-Object { $_.Roles.Name } |
            Where-Object { $_.Roles | Where-Object { $Role.HasFlag([AutomatedLab.Roles]$_.Name) } }

            if (-not $result)
            {
                return
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $result = $Script:data.Machines
        }

        # Skip Linux machines by default
        if (-not $IncludeLinux)
        {
            $result = $result | Where-Object -Property OperatingSystemType -eq Windows
        }
    }

    end
    {
        #Add Azure Connection Info
        $azureVMs = $Script:data.Machines | Where-Object { -not $_.SkipDeployment -and $_.HostType -eq 'Azure' -and -not $_.AzureConnectionInfo.DnsName }
        if ($azureVMs -and -not $SkipConnectionInfo.IsPresent)
        {
            $azureConnectionInfo = Get-LWAzureVMConnectionInfo -ComputerName $azureVMs

            if ($azureConnectionInfo)
            {
                foreach ($azureVM in $azureVMs)
                {
                    $azureVM | Add-Member -Name AzureConnectionInfo -MemberType NoteProperty -Value ($azureConnectionInfo | Where-Object ComputerName -eq $azureVM) -Force
                }
            }
        }

        $result = if ($IsRunning)
        {
            if ($result.Count -eq 1)
            {
                if ((Get-LabVMStatus -ComputerName $result) -eq 'Started')
                {
                    $result
                }
            }
            else
            {
                $startedMachines = (Get-LabVMStatus -ComputerName $result).GetEnumerator() | Where-Object Value -EQ Started
                $Script:data.Machines | Where-Object { $_.Name -in $startedMachines.Name }
            }
        }
        else
        {
            $result
        }
        
        foreach ($machine in ($result | Where-Object HostType -eq 'HyperV'))
        {
            if ($machine.Disks.Count -gt 1)
            {
                $machine.Disks = Get-LabVHDX -Name $machine.Disks.Name -ErrorAction SilentlyContinue
            }
        }

        if ($Filter)
        {
            $result.Where($Filter)
        }
        else
        {
            $result
        }
    }
}


function Get-LabVMDotNetFrameworkVersion
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [switch]$NoDisplay
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName

    if (-not $machines)
    {
        Write-Error 'The given machines could not be found'
        return
    }

    Invoke-LabCommand -ActivityName 'Get .net Framework version' -ComputerName $machines -ScriptBlock {
        Get-DotNetFrameworkVersion
    } -Function (Get-Command -Name Get-DotNetFrameworkVersion) -PassThru -NoDisplay:$NoDisplay

    Write-LogFunctionExit
}


function Get-LabVMRdpFile
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string[]]
        $ComputerName,

        [Parameter()]
        [switch]
        $UseLocalCredential,

        [Parameter(ParameterSetName = 'All')]
        [switch]
        $All,

        [Parameter()]
        [string]
        $Path
    )

    if ($ComputerName)
    {
        $machines = Get-LabVM -ComputerName $ComputerName
    }
    else
    {
        $machines = Get-LabVM -All
    }

    $lab = Get-Lab
    if ([string]::IsNullOrWhiteSpace($Path))
    {
        $Path = $lab.LabPath
    }

    foreach ($machine in $machines)
    {
        Write-PSFMessage "Creating RDP file for machine '$($machine.Name)'"
        $port = 3389
        $name = $machine.Name

        if ($UseLocalCredential)
        {
            $cred = $machine.GetLocalCredential()
        }
        else
        {
            $cred = $machine.GetCredential($lab)
        }

        if ($machine.HostType -eq 'Azure')
        {
            $cn = Get-LWAzureVMConnectionInfo -ComputerName $machine
            $cmd = 'cmdkey.exe /add:"TERMSRV/{0}" /user:"{1}" /pass:"{2}"' -f $cn.DnsName, $cred.UserName, $cred.GetNetworkCredential().Password
            Invoke-Expression $cmd | Out-Null

            $name = $cn.DnsName
            $port = $cn.RdpPort
        }
        elseif ($machine.HostType -eq 'HyperV')
        {
            $cmd = 'cmdkey.exe /add:"TERMSRV/{0}" /user:"{1}" /pass:"{2}"' -f $machine.Name, $cred.UserName, $cred.GetNetworkCredential().Password
            Invoke-Expression $cmd | Out-Null
        }

        $rdpContent = @"
redirectclipboard:i:1
redirectprinters:i:1
redirectcomports:i:0
redirectsmartcards:i:1
devicestoredirect:s:*
drivestoredirect:s:*
redirectdrives:i:1
session bpp:i:32
prompt for credentials on client:i:0
span monitors:i:1
use multimon:i:0
server port:i:$port
allow font smoothing:i:1
promptcredentialonce:i:0
videoplaybackmode:i:1
audiocapturemode:i:1
gatewayusagemethod:i:0
gatewayprofileusagemethod:i:1
gatewaycredentialssource:i:0
full address:s:$name
use redirection server name:i:1
username:s:$($cred.UserName)
authentication level:i:0
"@

        $filePath = Join-Path -Path $Path -ChildPath ($machine.Name + '.rdp')
        $rdpContent | Set-Content -Path $filePath
        Get-Item $filePath
        Write-PSFMessage "RDP file saved to '$filePath'"
    }
}


function Get-LabVMSnapshot
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [string[]]
        $ComputerName,

        [Parameter()]
        [string]
        $SnapshotName
    )

    Write-LogFunctionEntry

    if (-not (Get-LabVM))
    {
        Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $lab = Get-Lab

    if ($ComputerName)
    {
        $machines = Get-LabVM -IncludeLinux | Where-Object -Property Name -in $ComputerName
    }
    else
    {
        $machines = Get-LabVm -IncludeLinux
    }

    $machines = $machines | Where-Object SkipDeployment -eq $false

    if (-not $machines)
    {
        $message = 'No machine found to remove the snapshot. Either the given name is wrong or there is no machine defined yet'
        Write-LogFunctionExitWithError -Message $message
        return
    }

    $parameters = @{
        VMName = $machines
        ErrorAction = 'SilentlyContinue'
    }

    if ($SnapshotName)
    {
        $parameters.Name = $SnapshotName
    }

    switch ($lab.DefaultVirtualizationEngine)
    {
        'HyperV' { Get-LWHypervVMSnapshot @parameters}
        'Azure'  { Get-LWAzureVmSnapshot @parameters}
        'VMWare' { Write-ScreenInfo -Type Warning -Message 'No VMWare snapshots possible, nothing will be listed'}
    }

    Write-LogFunctionExit
}


function Get-LabVMStatus
{
    [CmdletBinding()]
    param (
        [string[]]$ComputerName,

        [switch]$AsHashTable
    )

    Write-LogFunctionEntry

    #required to suporess verbose messages, warnings and errors
    Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if ($ComputerName)
    {
        $vms = Get-LabVM -ComputerName $ComputerName -IncludeLinux | Where-Object { -not $_.SkipDeployment }
    }
    else
    {
        $vms = Get-LabVM -IncludeLinux
    }

    $vms = $vms | Where-Object SkipDeployment -eq $false

    $hypervVMs = $vms | Where-Object HostType -eq 'HyperV'
    if ($hypervVMs) { $hypervStatus = Get-LWHypervVMStatus -ComputerName $hypervVMs.ResourceName }

    $azureVMs = $vms | Where-Object HostType -eq 'Azure'
    if ($azureVMs) { $azureStatus = Get-LWAzureVMStatus -ComputerName $azureVMs.ResourceName }

    $vmwareVMs = $vms | Where-Object HostType -eq 'VMWare'
    if ($vmwareVMs) { $vmwareStatus = Get-LWVMWareVMStatus -ComputerName $vmwareVMs.ResourceName }

    $result = @{ }
    if ($hypervStatus) { $result = $result + $hypervStatus }
    if ($azureStatus) { $result = $result + $azureStatus }
    if ($vmwareStatus) { $result = $result + $vmwareStatus }

    if ($result.Count -eq 1 -and -not $AsHashTable)
    {
        $result.Values[0]
    }
    else
    {
        $result
    }

    Write-LogFunctionExit
}


function Get-LabVMUacStatus
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName

    if (-not $machines)
    {
        Write-Error 'The given machines could not be found'
        return
    }

    Invoke-LabCommand -ActivityName 'Get Uac Status' -ComputerName $machines -ScriptBlock {
        Get-VMUacStatus
    } -Function (Get-Command -Name Get-VMUacStatus) -PassThru

    Write-LogFunctionExit
}


function Get-LabVMUptime
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-LogFunctionEntry

    $cmdGetUptime = {
        if ($IsLinux -or $IsMacOs)
        {
            (Get-Date) - [datetime](uptime -s)
        }
        else
        {
            $lastboottime = (Get-WmiObject -Class Win32_OperatingSystem).LastBootUpTime
            (Get-Date) - [System.Management.ManagementDateTimeconverter]::ToDateTime($lastboottime)
        }
    }

    $uptime = Invoke-LabCommand -ComputerName $ComputerName -ActivityName GetUptime -ScriptBlock $cmdGetUptime -UseLocalCredential -PassThru

    if ($uptime)
    {
        Write-LogFunctionExit -ReturnValue $uptime
        $uptime
    }
    else
    {
        Write-LogFunctionExitWithError -Message 'Uptime could not be retrieved'
    }
}


function Initialize-LabWindowsActivation
{
    [CmdletBinding()]
    param ()

    Write-LogFunctionEntry

    $lab = Get-Lab -ErrorAction SilentlyContinue

    if (-not $lab)
    {
        Write-ScreenInfo -Type Warning -Message 'No lab imported, skipping activation'
        Write-LogFunctionExit
        return
    }

    $machines = Get-LabVM | Where-Object {$_.SkipDeployment -eq $false -and $_.OperatingSystemType -eq 'Windows' -and (($_.Notes.ContainsKey('ActivateWindows') -and $_.Notes['ActivateWindows']) -or $_.Notes.ContainsKey('KmsLookupDomain') -or $_.Notes.ContainsKey('KmsServerName'))}

    if (-not $machines) { Write-LogFunctionExit; return }

    Invoke-LabCommand -ActivityName 'Activating Windows' -ComputerName $machines -Variable (Get-Variable machines) -ScriptBlock {
        $machine = $machines | Where-Object Name -eq $env:COMPUTERNAME

        if (-not $machine) { return }

        $licensing = Get-CimInstance -ClassName SoftwareLicensingService

        if ($machine.Notes.ContainsKey('KmsLookupDomain'))
        {
            $null = $licensing | Invoke-CimMethod -MethodName SetKeyManagementServiceLookupDomain -Arguments @{LookupDomain = $machines.Notes['KmsLookupDomain']}
        }
        elseif ($machines.Notes.ContainsKey('KmsServerName') -and $machines.Notes.ContainsKey('KmsPort'))
        {
            $null = $licensing | Invoke-CimMethod -MethodName SetKeyManagementServiceMachine -Arguments @{MachineName = $machines.Notes['KmsServerName']}
            $null = $licensing | Invoke-CimMethod -MethodName SetKeyManagementServicePort -Arguments @{PortNumber = $machines.Notes['KmsPort']}
        }
        elseif ($machines.Notes.ContainsKey('KmsServerName'))
        {
            $null = $licensing | Invoke-CimMethod -MethodName SetKeyManagementServiceMachine -Arguments @{MachineName = $machines.Notes['KmsServerName']}
        }
        elseif ($machine.ProductKey)
        {
            $null = $licensing | Invoke-CimMethod -MethodName InstallProductKey -Arguments @{ProductKey = $machine.ProductKey}
        }

        $null = $licensing | Invoke-CimMethod -MethodName RefreshLicenseStatus
    }

    Write-LogFunctionExit
}


function Join-LabVMDomain
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [AutomatedLab.Machine[]]$Machine
    )

    Write-LogFunctionEntry

    #region Join-Computer
    function Join-Computer
    {
        [CmdletBinding()]

        param(
            [Parameter(Mandatory = $true)]
            [string]$DomainName,

            [Parameter(Mandatory = $true)]
            [string]$UserName,

            [Parameter(Mandatory = $true)]
            [string]$Password,

            [bool]$AlwaysReboot = $false,

            [string]$SshPublicKey
        )

        if ($IsLinux)
        {
            if ((Get-Command -Name realm -ErrorAction SilentlyContinue) -and (sudo realm list --name-only | Where {$_ -eq $DomainName}))
            {
                return $true
            }

            if (-not (Get-Command -Name realm -ErrorAction SilentlyContinue) -and (Get-Command -Name apt -ErrorAction SilentlyContinue))
            {
                sudo apt install -y realmd libnss-sss libpam-sss sssd sssd-tools adcli samba-common-bin oddjob oddjob-mkhomedir packagekit *>$null
            }
            elseif (-not (Get-Command -Name realm -ErrorAction SilentlyContinue) -and (Get-Command -Name dnf -ErrorAction SilentlyContinue))
            {
                sudo dnf install -y oddjob oddjob-mkhomedir sssd adcli krb5-workstation realmd samba-common samba-common-tools authselect-compat *>$null
            }
            elseif (-not (Get-Command -Name realm -ErrorAction SilentlyContinue) -and (Get-Command -Name yum -ErrorAction SilentlyContinue))
            {
                sudo yum install -y oddjob oddjob-mkhomedir sssd adcli krb5-workstation realmd samba-common samba-common-tools authselect-compat *>$null
            }

            if (-not (Get-Command -Name realm -ErrorAction SilentlyContinue))
            {
                # realm package missing or no known package manager
                return $false
            }

            $null = realm join --one-time-password "'$Password'" $DomainName
            $null = sudo sed -i "/^%wheel.*/a %$($DomainName.ToUpper())\\\\domain\\ admins ALL=(ALL) NOPASSWD: ALL" /etc/sudoers
            $null = sudo mkdir -p "/home/$($UserName)@$($DomainName)"
            $null = sudo chown -R "$($UserName)@$($DomainName):$($UserName)@$($DomainName)" /home/$($UserName)@$($DomainName) 2>$null
            if (-not [string]::IsNullOrWhiteSpace($SshPublicKey))
            {
                $null = sudo mkdir -p "/home/$($UserName)@$($DomainName)/.ssh"
                $null = echo "$($SshPublicKey -replace '\s*$')" | sudo tee --append /home/$($UserName)@$($DomainName)/.ssh/authorized_keys
                $null = sudo chmod 700 /home/$($UserName)@$($DomainName)/.ssh
                $null = sudo chmod 600 /home/$($UserName)@$($DomainName)/.ssh/authorized_keys 2>$null                
                $null = sudo restorecon -R /$($UserName)@$($DomainName)/.ssh 2>$null
            }

            return $true
        }

        $Credential = New-Object -TypeName PSCredential -ArgumentList $UserName, ($Password | ConvertTo-SecureString -AsPlainText -Force)

        try
        {
            if ([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name -eq $DomainName)
            {
                return $true
            }
        }
        catch
        {
            # Empty catch. If we are a workgroup member, it is domain join time.
        }

        try
        {
            Add-Computer -DomainName $DomainName -Credential $Credential -ErrorAction Stop -WarningAction SilentlyContinue
            $true
        }
        catch
        {
            if ($AlwaysReboot)
            {
                $false
                Start-Sleep -Seconds 1
                Restart-Computer -Force
            }
            else
            {
                Write-Error -Exception $_.Exception -Message $_.Exception.Message -ErrorAction Stop
            }
        }

        $logonName = "$DomainName\$UserName"

        New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name AutoAdminLogon -Value 1 -Force | Out-Null
        New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultUserName -Value $logonName -Force | Out-Null
        New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultPassword -Value $Password -Force | Out-Null

        Start-Sleep -Seconds 1

        Restart-Computer -Force
    }
    #endregion

    $lab = Get-Lab
    $jobs = @()
    $startTime = Get-Date

    $machinesToJoin = $Machine | Where-Object SkipDeployment -eq $false
    Write-PSFMessage "Starting joining $($machinesToJoin.Count) machines to domains"
    foreach ($m in $machinesToJoin)
    {
        $domain = $lab.Domains | Where-Object Name -eq $m.DomainName
        $cred = $domain.GetCredential()

        Write-PSFMessage "Joining machine '$m' to domain '$domain'"
        $jobParameters = @{
            ComputerName = $m
            ActivityName = "DomainJoin_$m"
            ScriptBlock = (Get-Command Join-Computer).ScriptBlock
            UseLocalCredential = $true
            ArgumentList = $domain, $cred.UserName, $cred.GetNetworkCredential().Password
            AsJob = $true
            PassThru = $true
            NoDisplay = $true
        }

        if ($m.HostType -eq 'Azure')
        {
            $jobParameters.ArgumentList += $true
        }
        if ($m.SshPublicKey)
        {
            if ($jobParameters.ArgumentList.Count -eq 3)
            {
                $jobParameters.ArgumentList += $false
            }
            $jobParameters.ArgumentList += $m.SshPublicKey
        }
        $jobs += Invoke-LabCommand @jobParameters
    }

    if ($jobs)
    {
        Write-PSFMessage 'Waiting on jobs to finish'
        Wait-LWLabJob -Job $jobs -ProgressIndicator 15 -NoDisplay -NoNewLine

        Write-ProgressIndicatorEnd
        Write-ScreenInfo -Message 'Waiting for machines to restart' -NoNewLine
        Wait-LabVMRestart -ComputerName $machinesToJoin -ProgressIndicator 30 -NoNewLine -MonitoringStartTime $startTime
    }

    foreach ($m in $machinesToJoin)
    {
        $machineJob = $jobs | Where-Object -Property Name -EQ DomainJoin_$m
        $machineResult = $machineJob | Receive-Job -Keep -ErrorAction SilentlyContinue
        if (($machineJob).State -eq 'Failed' -or -not $machineResult)
        {
            Write-ScreenInfo -Message "$m failed to join the domain. Retrying on next restart" -Type Warning
            $m.HasDomainJoined = $false
        }
        else
        {
            $m.HasDomainJoined = $true
            if ($lab.DefaultVirtualizationEngine -eq 'Azure')
            {
                Enable-LabAutoLogon -ComputerName $m
            }
        }
    }

    Export-Lab

    Write-LogFunctionExit
}


function Mount-LabIsoImage
{
    param(
        [Parameter(Mandatory, Position = 0)]
        [string[]]$ComputerName,

        [Parameter(Mandatory, Position = 1)]
        [string]$IsoPath,

        [switch]$SupressOutput,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName | Where-Object SkipDeployment -eq $false
    if (-not $machines)
    {
        Write-LogFunctionExitWithError -Message 'The specified machines could not be found'
        return
    }
    if ($machines.Count -ne $ComputerName.Count)
    {
        $machinesNotFound = Compare-Object -ReferenceObject $ComputerName -DifferenceObject ($machines.Name)
        Write-ScreenInfo "The specified machine(s) $($machinesNotFound.InputObject -join ', ') could not be found" -Type Warning
    }
    $machines | Where-Object HostType -notin HyperV, Azure | ForEach-Object {
        Write-ScreenInfo "Using ISO images is only supported with Hyper-V VMs or on Azure. Skipping machine '$($_.Name)'" -Type Warning
    }

    $machines = $machines | Where-Object HostType -in HyperV,Azure

    foreach ($machine in $machines)
    {
        if (-not $SupressOutput)
        {
            Write-ScreenInfo -Message "Mounting ISO image '$IsoPath' to computer '$machine'" -Type Info
        }

        if ($machine.HostType -eq 'HyperV')
        {
            Mount-LWIsoImage -ComputerName $machine -IsoPath $IsoPath -PassThru:$PassThru
        }
        else
        {
            Mount-LWAzureIsoImage -ComputerName $machine -IsoPath $IsoPath -PassThru:$PassThru
        }
    }

    Write-LogFunctionExit
}


function New-LabVM
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string[]]$Name,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All,

        [switch]$CreateCheckPoints,

        [int]$ProgressIndicator = 20
    )

    Write-LogFunctionEntry

    $lab = Get-Lab
    if (-not $lab)
    {
        Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $machines = Get-LabVM -ComputerName $Name -IncludeLinux -ErrorAction Stop | Where-Object { -not $_.SkipDeployment }

    if (-not $machines)
    {
        $message = 'No machine found to create. Either the given name is wrong or there is no machine defined yet'
        Write-LogFunctionExitWithError -Message $message
        return
    }
    
    Write-ScreenInfo -Message 'Waiting for all machines to finish installing' -TaskStart
    foreach ($machine in $machines.Where({$_.HostType -ne 'Azure'}))
    {
        $fdvDenyWriteAccess = (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE -Name FDVDenyWriteAccess -ErrorAction SilentlyContinue).FDVDenyWriteAccess
        if ($fdvDenyWriteAccess) {
            Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE -Name FDVDenyWriteAccess -Value 0
        }

        Write-ScreenInfo -Message "Creating $($machine.HostType) machine '$machine'" -TaskStart -NoNewLine

        if ($machine.HostType -eq 'HyperV')
        {
            $result = New-LWHypervVM -Machine $machine

            $doNotAddToCluster = Get-LabConfigurationItem -Name DoNotAddVmsToCluster -Default $false
            if (-not $doNotAddToCluster -and (Get-Command -Name Get-Cluster -ErrorAction SilentlyContinue) -and (Get-Cluster -ErrorAction SilentlyContinue -WarningAction SilentlyContinue))
            {
                Write-ScreenInfo -Message "Adding $($machine.Name) ($($machine.ResourceName)) to cluster $((Get-Cluster).Name)"
                if (-not (Get-ClusterGroup -Name $machine.ResourceName -ErrorAction SilentlyContinue))
                {
                    $null = Add-ClusterVirtualMachineRole -VMName $machine.ResourceName -Name $machine.ResourceName
                }
            }

            if ('RootDC' -in $machine.Roles.Name)
            {
                Start-LabVM -ComputerName $machine.Name -NoNewline
            }

            if ($result)
            {
                Write-ScreenInfo -Message 'Done' -TaskEnd
            }
            else
            {
                Write-ScreenInfo -Message "Could not create $($machine.HostType) machine '$machine'" -TaskEnd -Type Error
            }
        }
        elseif ($machine.HostType -eq 'VMWare')
        {
            $vmImageName = (New-Object AutomatedLab.OperatingSystem($machine.OperatingSystem)).VMWareImageName
            if (-not $vmImageName)
            {
                Write-Error "The VMWare image for operating system '$($machine.OperatingSystem)' is not defined in AutomatedLab. Cannot install the machine."
                continue
            }

            New-LWVMWareVM -Name $machine.Name -ReferenceVM $vmImageName -AdminUserName $machine.InstallationUser.UserName -AdminPassword $machine.InstallationUser.Password `
            -DomainName $machine.DomainName -DomainJoinCredential $machine.GetCredential($lab)

            Start-LabVM -ComputerName $machine
        }
        
        if ($fdvDenyWriteAccess) {
            Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE -Name FDVDenyWriteAccess -Value $fdvDenyWriteAccess
        }
    }

    if ($lab.DefaultVirtualizationEngine -eq 'Azure')
    {
        $deployment = New-LabAzureResourceGroupDeployment -Lab $lab -PassThru -Wait -ErrorAction SilentlyContinue -ErrorVariable rgDeploymentFail
        if (-not $deployment)
        {
            $labFolder = Split-Path -Path $lab.LabFilePath -Parent
            Write-ScreenInfo "The deployment failed. To get more information about the following error, please run the following command:"
            Write-ScreenInfo "'New-AzResourceGroupDeployment -ResourceGroupName $($lab.AzureSettings.DefaultResourceGroup.ResourceGroupName) -TemplateFile $labFolder\armtemplate.json'"
            Write-LogFunctionExitWithError -Message "Deployment of resource group '$lab' failed with '$($rgDeploymentFail.Exception.Message)'" -ErrorAction Stop
        }
    }

    Write-ScreenInfo -Message 'Done' -TaskEnd

    $azureVms = Get-LabVM -ComputerName $machines -IncludeLinux | Where-Object { $_.HostType -eq 'Azure' -and -not $_.SkipDeployment }
    $winAzVm, $linuxAzVm = $azureVms.Where({$_.OperatingSystemType -eq 'Windows'})

    if ($azureVMs)
    {
        Write-ScreenInfo -Message 'Initializing machines' -TaskStart

        Write-PSFMessage -Message 'Calling Enable-PSRemoting on machines'
        Enable-LWAzureWinRm -Machine $winAzVm -Wait

        Write-PSFMessage -Message 'Executing initialization script on machines'
        Initialize-LWAzureVM -Machine $azureVMs

        Write-ScreenInfo -Message 'Done' -TaskEnd
    }

    $vmwareVMs = $machines | Where-Object HostType -eq VMWare

    if ($vmwareVMs)
    {
        throw New-Object System.NotImplementedException
    }

    Write-LogFunctionExit
}


function Remove-LabVM
{
    [CmdletBinding(DefaultParameterSetName='ByName')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Name')]
        [string[]]$ComputerName,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All
    )

    begin
    {
        Write-LogFunctionEntry

        $lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }

        $machines = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()

        if ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $machines = $lab.Machines
        }
    }

    process
    {
        $null = $lab.Machines | Where-Object Name -in $ComputerName | Foreach-Object {$machines.Add($_)}
    }

    end
    {
        if (-not $machines)
        {
            $message = 'No machine found to remove'
            Write-LogFunctionExitWithError -Message $message
            return
        }

        foreach ($machine in $machines)
        {
            $doNotUseGetHostEntry = Get-LabConfigurationItem -Name DoNotUseGetHostEntryInNewLabPSSession
            if (-not $doNotUseGetHostEntry)
            {
                $machineName = (Get-HostEntry -Hostname $machine).IpAddress.IpAddressToString
            }

            if (-not [string]::IsNullOrEmpty($machine.FriendlyName) -or (Get-LabConfigurationItem -Name SkipHostFileModification))
            {
                $machineName = $machine.IPV4Address
            }

            Get-PSSession | Where-Object {$_.ComputerName -eq $machineName} | Remove-PSSession

            Write-ScreenInfo -Message "Removing Lab VM '$($machine.Name)' (and its associated disks)"

            if ($virtualNetworkAdapter.HostType -eq 'VMWare')
            {
                Write-Error 'Managing networks is not yet supported for VMWare'
                continue
            }

            if ($machine.HostType -eq 'HyperV')
            {
                Remove-LWHypervVM -Name $machine.ResourceName
            }
            elseif ($machine.HostType -eq 'Azure')
            {
                Remove-LWAzureVM -Name $machine.ResourceName
            }
            elseif ($machine.HostType -eq 'VMWare')
            {
                Remove-LWVMWareVM -Name $machine.ResourceName
            }

            if ((Get-HostEntry -Section (Get-Lab).Name.ToLower() -HostName $machine))
            {
                Remove-HostEntry -Section (Get-Lab).Name.ToLower() -HostName $machine
            }

            Write-ScreenInfo -Message "Lab VM '$machine' has been removed"
        }
    }
}


function Remove-LabVMSnapshot
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByNameAllSnapShots')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByNameSnapshotByName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByNameSnapshotByName')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'AllMachinesSnapshotByName')]
        [string]$SnapshotName,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'AllMachinesSnapshotByName')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'AllMachinesAllSnapshots')]
        [switch]$AllMachines,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ByNameAllSnapShots')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'AllMachinesAllSnapshots')]
        [switch]$AllSnapShots
    )

    Write-LogFunctionEntry

    if (-not (Get-LabVM))
    {
        Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $lab = Get-Lab

    if ($ComputerName)
    {
        $machines = Get-LabVM -IncludeLinux | Where-Object { $_.Name -in $ComputerName }
    }
    else
    {
        $machines = Get-LabVm -IncludeLinux
    }

    $machines = $machines | Where-Object SkipDeployment -eq $false

    if (-not $machines)
    {
        $message = 'No machine found to remove the snapshot. Either the given name is wrong or there is no machine defined yet'
        Write-LogFunctionExitWithError -Message $message
        return
    }

    $parameters = @{
        ComputerName = $machines.ResourceName
    }

    if ($SnapshotName)
    {
        $parameters.SnapshotName = $SnapshotName
    }
    elseif ($AllSnapShots)
    {
        $parameters.All = $true
    }

    switch ($lab.DefaultVirtualizationEngine)
    {
        'HyperV' { Remove-LWHypervVMSnapshot @parameters}
        'Azure'  { Remove-LWAzureVmSnapshot @parameters}
        'VMWare' { Write-ScreenInfo -Type Warning -Message 'No VMWare snapshots possible, nothing will be removed'}
    }

    Write-LogFunctionExit
}


function Restart-LabVM
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,

        [switch]$Wait,

        [double]$ShutdownTimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_RestartLabMachine_Shutdown),

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [switch]$NoDisplay,

        [switch]$NoNewLine
    )

    begin
    {
        Write-LogFunctionEntry

        $lab = Get-Lab
        if (-not $lab.Machines)
        {
            Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }
    }

    process
    {
        $machines = Get-LabVM -ComputerName $ComputerName | Where-Object SkipDeployment -eq $false

        if (-not $machines)
        {
            Write-Error "The machines '$($ComputerName -join ', ')' could not be found in the lab."
            return
        }

        Write-PSFMessage "Stopping machine '$ComputerName' and waiting for shutdown"
        Stop-LabVM -ComputerName $ComputerName -ShutdownTimeoutInMinutes $ShutdownTimeoutInMinutes -Wait -ProgressIndicator $ProgressIndicator -NoNewLine -KeepAzureVmProvisioned
        Write-PSFMessage "Machine '$ComputerName' is stopped"

        Write-Debug 'Waiting 10 seconds'
        Start-Sleep -Seconds 10

        Write-PSFMessage "Starting machine '$ComputerName' and waiting for availability"
        Start-LabVM -ComputerName $ComputerName -Wait:$Wait -ProgressIndicator $ProgressIndicator -NoNewline:$NoNewLine
        Write-PSFMessage "Machine '$ComputerName' is started"
    }

    end
    {
        Write-LogFunctionExit
    }
}


function Restore-LabVMSnapshot
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [string]$SnapshotName,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [switch]$All
    )

    Write-LogFunctionEntry

    if (-not (Get-LabVM))
    {
        Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
        return
    }

    $lab = Get-Lab

    if ($ComputerName)
    {
        $machines = Get-LabVM -IncludeLinux | Where-Object { $_.Name -in $ComputerName }
    }
    else
    {
        $machines = Get-LabVM -IncludeLinux
    }

    $machines = $machines | Where-Object SkipDeployment -eq $false

    if (-not $machines)
    {
        $message = 'No machine found to restore the snapshot. Either the given name is wrong or there is no machine defined yet'
        Write-LogFunctionExitWithError -Message $message
        return
    }

    Remove-LabPSSession -ComputerName $machines

    switch ($lab.DefaultVirtualizationEngine)
    {
        'HyperV' { Restore-LWHypervVMSnapshot -ComputerName $machines.ResourceName -SnapshotName $SnapshotName}
        'Azure'  { Restore-LWAzureVmSnapshot -ComputerName $machines.ResourceName -SnapshotName $SnapshotName}
        'VMWare' { Write-ScreenInfo -Type Error -Message 'Restoring snapshots of VMWare VMs is not yet implemented'}
    }

    Write-LogFunctionExit
}


function Save-LabVM
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByName', Position = 0)]
        [string[]]$Name,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByRole')]
        [AutomatedLab.Roles]$RoleName,

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'All')]
        [switch]$All
    )

    begin
    {
        Write-LogFunctionEntry

        $lab = Get-Lab

        $vms = @()
        $availableVMs = ($lab.Machines  | Where-Object SkipDeployment -eq $false).Name
    }

    process
    {

        if (-not $lab.Machines)
        {
            $message = 'No machine definitions imported, please use Import-Lab first'
            Write-Error -Message $message
            Write-LogFunctionExitWithError -Message $message
            return
        }

        if ($PSCmdlet.ParameterSetName -eq 'ByName')
        {
            $Name | ForEach-Object {
                if ($_ -in $availableVMs)
                {
                    $vms += $_
                }
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ByRole')
        {
            #get all machines that have a role assigned and the machine's role name is part of the parameter RoleName
            $machines = ($lab.Machines |
                Where-Object { $_.Roles.Name } |
            Where-Object { $_.Roles | Where-Object { $RoleName.HasFlag([AutomatedLab.Roles]$_.Name) } }).Name
            $vms = $machines
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $vms = $availableVMs
        }
    }

    end
    {
        $vms = Get-LabVM -ComputerName $vms -IncludeLinux

        #if there are no VMs to start, just write a warning
        if (-not $vms)
        {
            Write-ScreenInfo 'There is no machine to start' -Type Warning
            return
        }

        Write-PSFMessage -Message "Saving VMs '$($vms -join ',')"
        switch ($lab.DefaultVirtualizationEngine)
        {
            'HyperV' { Save-LWHypervVM -ComputerName $vms.ResourceName}
            'VMWare' { Save-LWVMWareVM -ComputerName $vms.ResourceName}
            'Azure'  { Write-PSFMessage -Level Warning -Message "Skipping Azure VMs '$($vms -join ',')' as suspending the VMs is not supported on Azure."}
        }

        Write-LogFunctionExit
    }
}


function Set-LabVMUacStatus
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [bool]$EnableLUA,

        [int]$ConsentPromptBehaviorAdmin,

        [int]$ConsentPromptBehaviorUser,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    $machines = Get-LabVM -ComputerName $ComputerName

    if (-not $machines)
    {
        Write-Error 'The given machines could not be found'
        return
    }

    $functions = Get-Command -Name Get-VMUacStatus, Set-VMUacStatus, Sync-Parameter
    $variables = Get-Variable -Name PSBoundParameters
    $result = Invoke-LabCommand -ActivityName 'Set Uac Status' -ComputerName $machines -ScriptBlock {

        Sync-Parameter -Command (Get-Command -Name Set-VMUacStatus)
        Set-VMUacStatus @ALBoundParameters

    } -Function $functions -Variable $variables -PassThru

    if ($result.UacStatusChanged)
    {
        Write-ScreenInfo "The change requires a reboot of '$ComputerName'." -Type Warning
    }

    if ($PassThru)
    {
        Get-LabMachineUacStatus -ComputerName $ComputerName
    }

    Write-LogFunctionExit
}


function Start-LabVM
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(ParameterSetName = 'ByName', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'ByRole')]
        [AutomatedLab.Roles]$RoleName,

        [Parameter(ParameterSetName = 'All')]
        [switch]$All,

        [switch]$Wait,

        [switch]$DoNotUseCredSsp,

        [switch]$NoNewline,

        [int]$DelayBetweenComputers = 0,

        [int]$TimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_StartLabMachine_Online),

        [int]$StartNextMachines,

        [int]$StartNextDomainControllers,

        [string]$Domain,

        [switch]$RootDomainMachines,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [int]$PreDelaySeconds = 0,

        [int]$PostDelaySeconds = 0
    )

    begin
    {
        Write-LogFunctionEntry

        if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

        $lab = Get-Lab

        if (-not $lab.Machines)
        {
            $message = 'No machine definitions imported, please use Import-Lab first'
            Write-Error -Message $message
            Write-LogFunctionExitWithError -Message $message
            return
        }

        $vms = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()
        $availableVMs = $lab.Machines | Where-Object SkipDeployment -eq $false
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByName' -and -not $StartNextMachines -and -not $StartNextDomainControllers)
        {
            $null = $lab.Machines | Where-Object Name -in $ComputerName | Foreach-Object { $vms.Add($_) }
        }
    }

    end
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByRole' -and -not $StartNextMachines -and -not $StartNextDomainControllers)
        {
            #get all machines that have a role assigned and the machine's role name is part of the parameter RoleName
            $vms = $lab.Machines | Where-Object { $_.Roles.Name } |
            Where-Object { ($_.Roles | Where-Object { $RoleName.HasFlag([AutomatedLab.Roles]$_.Name) }) -and (-not $_.SkipDeployment) }

            if (-not $vms)
            {
                Write-Error "There is no machine in the lab with the role '$RoleName'"
                return
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ByRole' -and $StartNextMachines -and -not $StartNextDomainControllers)
        {
            $vms = $lab.Machines | Where-Object { $_.Roles.Name -and ((Get-LabVMStatus -ComputerName $_.Name) -ne 'Started')} |
            Where-Object { $_.Roles | Where-Object { $RoleName.HasFlag([AutomatedLab.Roles]$_.Name) } }

            if (-not $vms)
            {
                Write-Error "There is no machine in the lab with the role '$RoleName'"
                return
            }
            $vms = $vms | Select-Object -First $StartNextMachines
        }
        elseif (-not ($PSCmdlet.ParameterSetName -eq 'ByRole') -and -not $RootDomainMachines -and -not $StartNextMachines -and $StartNextDomainControllers)
        {
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'FirstChildDC' }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'DC' }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'CaRoot' -and (-not $_.DomainName) }
            $vms = $vms | Where-Object { (Get-LabVMStatus -ComputerName $_.Name) -ne 'Started' } | Select-Object -First $StartNextDomainControllers
        }
        elseif (-not ($PSCmdlet.ParameterSetName -eq 'ByRole') -and -not $RootDomainMachines -and $StartNextMachines -and -not $StartNextDomainControllers)
        {
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'CaRoot' -and $_.DomainName -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'CaSubordinate' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -like 'SqlServer*' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'WebServer' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'Orchestrator' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'VisualStudio2013' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'VisualStudio2015' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'Office2013' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { -not $_.Roles.Name -and $_ -notin $vms }
            $vms = $vms | Where-Object { (Get-LabVMStatus -ComputerName $_.Name) -ne 'Started' } | Select-Object -First $StartNextMachines

            if ($Domain)
            {
                $vms = $vms | Where-Object { (Get-LabVM -ComputerName $_) -eq $Domain }
            }
        }
        elseif (-not ($PSCmdlet.ParameterSetName -eq 'ByRole') -and -not $RootDomainMachines -and $StartNextMachines -and -not $StartNextDomainControllers)
        {
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -like 'SqlServer*' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'WebServer' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'Orchestrator' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'VisualStudio2013' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'VisualStudio2015' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'Office2013' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { -not $_.Roles.Name -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'CaRoot' -and $_ -notin $vms }
            $vms += Get-LabVM -IncludeLinux | Where-Object { $_.Roles.Name -eq 'CaSubordinate' -and $_ -notin $vms }
            $vms = $vms | Where-Object { (Get-LabVMStatus -ComputerName $_.Name) -ne 'Started' } | Select-Object -First $StartNextMachines

            if ($Domain)
            {
                $vms = $vms | Where-Object { (Get-LabVM -IncludeLinux -ComputerName $_) -eq $Domain }
            }
        }
        elseif (-not ($PSCmdlet.ParameterSetName -eq 'ByRole') -and $RootDomainMachines -and -not $StartNextDomainControllers)
        {
            $vms = Get-LabVM -IncludeLinux | Where-Object { $_.DomainName -in (Get-LabVM -Role RootDC).DomainName } | Where-Object { $_.Name -notin (Get-LabVM -Role RootDC).Name -and $_.Roles.Name -notlike '*DC' }
            $vms = $vms | Select-Object -First $StartNextMachines
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $vms = $availableVMs | Where-Object { -not $_.SkipDeployment }
        }

        $vms = $vms | Where-Object SkipDeployment -eq $false

        #if there are no VMs to start, just write a warning
        if (-not $vms)
        {
            return
        }

        $vmsCopy = $vms | Where-Object {-not ($_.OperatingSystemType -eq 'Linux' -and $_.OperatingSystem.OperatingSystemName -match ('Suse|CentOS Linux 8'))}

        #filtering out all machines that are already running
        $vmStates = Get-LabVMStatus -ComputerName $vms -AsHashTable
        foreach ($vmState in $vmStates.GetEnumerator())
        {
            if ($vmState.Value -eq 'Started')
            {
                $vms = $vms | Where-Object Name -ne $vmState.Name
                Write-Debug "Machine '$($vmState.Name)' is already running, removing it from the list of machines to start"
            }
        }

        Write-PSFMessage "Starting VMs '$($vms.Name -join ', ')'"

        $hypervVMs = $vms | Where-Object HostType -eq 'HyperV'
        if ($hypervVMs)
        {
            Start-LWHypervVM -ComputerName $hypervVMs -DelayBetweenComputers $DelayBetweenComputers -ProgressIndicator $ProgressIndicator -PreDelaySeconds $PreDelaySeconds -PostDelaySeconds $PostDelaySeconds -NoNewLine:$NoNewline
            
            foreach ($vm in $hypervVMs)
            {
                $machineMetadata = Get-LWHypervVMDescription -ComputerName $vm.ResourceName
                if (($machineMetadata.InitState -band [AutomatedLab.LabVMInitState]::NetworkAdapterBindingCorrected) -ne [AutomatedLab.LabVMInitState]::NetworkAdapterBindingCorrected)
                {
                    Repair-LWHypervNetworkConfig -ComputerName $vm
                    $machineMetadata.InitState = [AutomatedLab.LabVMInitState]::NetworkAdapterBindingCorrected
                    Set-LWHypervVMDescription -Hashtable $machineMetadata -ComputerName $vm.ResourceName
                }
            }
        }

        $azureVms = $vms | Where-Object HostType -eq 'Azure'
        if ($azureVms)
        {
            Start-LWAzureVM -ComputerName $azureVms -DelayBetweenComputers $DelayBetweenComputers -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewline
        }

        $vmwareVms = $vms | Where-Object HostType -eq 'VmWare'
        if ($vmwareVms)
        {
            Start-LWVMWareVM -ComputerName $vmwareVms -DelayBetweenComputers $DelayBetweenComputers
        }

        if ($Wait -and $vmsCopy)
        {
            Wait-LabVM -ComputerName ($vmsCopy) -Timeout $TimeoutInMinutes -DoNotUseCredSsp:$DoNotUseCredSsp -ProgressIndicator $ProgressIndicator -NoNewLine
        }

        Write-ProgressIndicatorEnd

        Write-LogFunctionExit
    }
}


function Stop-LabVM
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0, ValueFromPipelineByPropertyName, ValueFromPipeline)]
        [string[]]$ComputerName,

        [double]$ShutdownTimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_StopLabMachine_Shutdown),

        [Parameter(ParameterSetName = 'All')]
        [switch]$All,

        [switch]$Wait,

        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [switch]$NoNewLine,

        [switch]$KeepAzureVmProvisioned
    )

    begin
    {
        Write-LogFunctionEntry

        $lab = Get-Lab
        if (-not $lab.Machines)
        {
            Write-Error 'No machine definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }

        $machines = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()
    }

    process
    {
        if ($ComputerName)
        {
            $null = Get-LabVM -ComputerName $ComputerName -IncludeLinux | Where-Object SkipDeployment -eq $false | Foreach-Object {$machines.Add($_)}
        }
    }

    end
    {
        if ($All)
        {
            $null = Get-LabVM -IncludeLinux | Where-Object { -not $_.SkipDeployment }| Foreach-Object {$machines.Add($_)}
        }

        #filtering out all machines that are already stopped
        $vmStates = Get-LabVMStatus -ComputerName $machines -AsHashTable
        foreach ($vmState in $vmStates.GetEnumerator())
        {
            if ($vmState.Value -eq 'Stopped')
            {
                $machines = $machines | Where-Object Name -ne $vmState.Name
                Write-Debug "Machine $($vmState.Name) is already stopped, removing it from the list of machines to stop"
            }
        }

        if (-not $machines)
        {
            return
        }

        Remove-LabPSSession -ComputerName $machines

        $hypervVms = $machines | Where-Object HostType -eq 'HyperV'
        $azureVms = $machines | Where-Object HostType -eq 'Azure'
        $vmwareVms = $machines | Where-Object HostType -eq 'VMWare'

        if ($hypervVms)
        {
            Stop-LWHypervVM -ComputerName $hypervVms -TimeoutInMinutes $ShutdownTimeoutInMinutes -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewLine `
            -ErrorVariable hypervErrors -ErrorAction SilentlyContinue
        }
        if ($azureVms)
        {
            Stop-LWAzureVM -ComputerName $azureVms -ErrorVariable azureErrors -ErrorAction SilentlyContinue -StayProvisioned $KeepAzureVmProvisioned
        }
        if ($vmwareVms)
        {
            Stop-LWVMWareVM -ComputerName $vmwareVms -ErrorVariable vmwareErrors -ErrorAction SilentlyContinue
        }

        $remainingTargets = @()
        if ($hypervErrors) { $remainingTargets += $hypervErrors.TargetObject }
        if ($azureErrors) { $remainingTargets += $azureErrors.TargetObject }
        if ($vmwareErrors) { $remainingTargets += $vmwareErrors.TargetObject }
        
        $remainingTargets = if ($remainingTargets.Count -gt 0) {
            foreach ($remainingTarget in $remainingTargets)
            { 
                if ($remainingTarget -is [string])
                {
                    $remainingTarget
                }
                elseif ($remainingTarget -is [AutomatedLab.Machine])
                {
                    $remainingTarget
                }
                elseif ($remainingTarget -is [System.Management.Automation.Runspaces.Runspace])
                {
                    $remainingTarget.ConnectionInfo.ComputerName
                }
                else
                {
                    Write-ScreenInfo "Unknown error in 'Stop-LabVM'. Cannot call 'Stop-LabVM2'" -Type Warning
                }
            }
            
        }

        if ($remainingTargets.Count -gt 0) {
            Stop-LabVM2 -ComputerName $remainingTargets
        }

        if ($Wait)
        {
            Wait-LabVMShutdown -ComputerName $machines -TimeoutInMinutes $ShutdownTimeoutInMinutes
        }

        Write-LogFunctionExit
    }
}


function Test-LabAutoLogon
{
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string[]]
        $ComputerName,

        [switch]
        $TestInteractiveLogonSession
    )

    Write-PSFMessage -Message "Testing autologon on $($ComputerName.Count) machines"

    [void]$PSBoundParameters.Remove('TestInteractiveLogonSession')
    $machines = Get-LabVM @PSBoundParameters
    $returnValues = @{}

    foreach ($machine in $machines)
    {
        $parameters = @{
            Username = $machine.InstallationUser.UserName
            Password = $machine.InstallationUser.Password
        }

        if ($machine.IsDomainJoined)
        {
            if ($machine.Roles.Name -contains 'RootDC' -or $machine.Roles.Name -contains 'FirstChildDC' -or $machine.Roles.Name -contains 'DC')
            {
                $isAdReady = Test-LabADReady -ComputerName $machine
                
                if ($isAdReady)
                {
                    $parameters['DomainName'] = $machine.DomainName
                }
                else
                {
                    $parameters['DomainName'] = $machine.Name
                }
            }
            else
            {
                $parameters['DomainName'] = $machine.DomainName
            }
        }
        else
        {
            $parameters['DomainName'] = $machine.Name
        }

        $settings = Invoke-LabCommand -ActivityName "Testing AutoLogon on $($machine.Name)" -ComputerName $machine.Name -ScriptBlock {
            $values = @{}
            $values['AutoAdminLogon'] = try { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction Stop).AutoAdminLogon } catch { }
            $values['DefaultDomainName'] = try { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction Stop).DefaultDomainName } catch { }
            $values['DefaultUserName'] = try { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction Stop).DefaultUserName } catch { }
            $values['DefaultPassword'] = try { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction Stop).DefaultPassword } catch { }
            if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)
            {
                $values['LoggedOnUsers'] = (Get-CimInstance -ClassName win32_logonsession -Filter 'logontype=2' | Get-CimAssociatedInstance -Association Win32_LoggedOnUser).Caption
            }
            else
            {
                $values['LoggedOnUsers'] = (Get-WmiObject -Class Win32_LogonSession -Filter 'LogonType=2').GetRelationships('Win32_LoggedOnUser').Antecedent |
                ForEach-Object {
                    # For deprecated OS versions...
                    # Output is convoluted vs the CimInstance variant: \\.\root\cimv2:Win32_Account.Domain="contoso",Name="Install"
                    $null = $_ -match 'Domain="(?<Domain>.+)",Name="(?<Name>.+)"'
                    -join ($Matches.Domain, '\', $Matches.Name)
                } | Select-Object -Unique
            }

            $values
        } -PassThru -NoDisplay

        Write-PSFMessage -Message ('Encountered the following values on {0}:{1}' -f $machine.Name, ($settings | Out-String))

        if ($settings.AutoAdminLogon -ne 1 -or
            $settings.DefaultDomainName -ne $parameters.DomainName -or
            $settings.DefaultUserName -ne $parameters.Username -or
        $settings.DefaultPassword -ne $parameters.Password)
        {
            $returnValues[$machine.Name] = $false
            continue
        }


        if ($TestInteractiveLogonSession)
        {
            $interactiveSessionUserName = '{0}\{1}' -f ($parameters.DomainName -split '\.')[0], $parameters.Username

            if ($settings.LoggedOnUsers -notcontains $interactiveSessionUserName)
            {
                $returnValues[$Machine.Name] = $false
                continue
            }
        }

        $returnValues[$machine.Name] = $true
    }

    return $returnValues
}


function Test-LabMachineInternetConnectivity
{
    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ComputerName,

        [int]$Count = 3,

        [switch]$AsJob
    )

    $cmd = {
        $result = 1..$Count |
        ForEach-Object {
            Test-NetConnection www.microsoft.com -CommonTCPPort HTTP -InformationLevel Detailed -WarningAction SilentlyContinue
            Start-Sleep -Seconds 1
        }
    
        #if 75% of the results are negative, return the first negative result, otherwise return the first positive result
        if (($result | Where-Object TcpTestSucceeded -eq $false).Count -ge ($count * 0.75))
        {
            $result | Where-Object TcpTestSucceeded -eq $false | Select-Object -First 1
        }
        else
        {
            $result | Where-Object TcpTestSucceeded -eq $true | Select-Object -First 1
        }
    }

    if ($AsJob)
    {
        $job = Invoke-LabCommand -ComputerName $ComputerName -ActivityName "Testing Internet Connectivity of '$ComputerName'" `
        -ScriptBlock $cmd -Variable (Get-Variable -Name Count) -PassThru -NoDisplay -AsJob

        return $job
    }
    else
    {
        $result = Invoke-LabCommand -ComputerName $ComputerName -ActivityName "Testing Internet Connectivity of '$ComputerName'" `
        -ScriptBlock $cmd -Variable (Get-Variable -Name Count) -PassThru -NoDisplay

        return $result.TcpTestSucceeded
    }
}


function Wait-LabVM
{
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,

        [double]$TimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_WaitLabMachine_Online),

        [int]$PostDelaySeconds = 0,

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [switch]$DoNotUseCredSsp,

        [switch]$NoNewLine
    )

    begin
    {
        if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

        Write-LogFunctionEntry

        $lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }

        $vms = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()
    }

    process
    {
        $null = Get-LabVM -ComputerName $ComputerName -IncludeLinux | Foreach-Object {$vms.Add($_) }

        if (-not $vms)
        {
            Write-Error 'None of the given machines could be found'
            return
        }
    }

    end
    {
        if ((Get-Command -ErrorAction SilentlyContinue -Name New-PSSession).Parameters.Values.Name -contains 'HostName' )
        {
            # Quicker than reading in the file on unsupported configurations
            $sshHosts = (Get-LabSshKnownHost -ErrorAction SilentlyContinue).ComputerName
        }
        $jobs = foreach ($vm in $vms)
        {
            $session = $null
            #remove the existing sessions to ensure a new one is created and the existing one not reused.
            if ((Get-Command -ErrorAction SilentlyContinue -Name New-PSSession).Parameters.Values.Name -contains 'HostName' -and $sshHosts -and $vm.Name -notin $sshHosts)
            {
                Install-LabSshKnownHost
                $sshHosts = (Get-LabSshKnownHost -ErrorAction SilentlyContinue).ComputerName
            }
            Remove-LabPSSession -ComputerName $vm

            if (-not ($IsLinux -or $IsMacOs)) { netsh.exe interface ip delete arpcache | Out-Null }

            #if called without using DoNotUseCredSsp and the machine is not yet configured for CredSsp, call Wait-LabVM again but with DoNotUseCredSsp. Wait-LabVM enables CredSsp if called with DoNotUseCredSsp switch.
            if (-not $vm.SkipDeployment -and $lab.DefaultVirtualizationEngine -eq 'HyperV')
            {
                $machineMetadata = Get-LWHypervVMDescription -ComputerName $vm.ResourceName
                if (($machineMetadata.InitState -band [AutomatedLab.LabVMInitState]::EnabledCredSsp) -ne [AutomatedLab.LabVMInitState]::EnabledCredSsp -and -not $DoNotUseCredSsp)
                {
                    Wait-LabVM -ComputerName $vm -TimeoutInMinutes $TimeoutInMinutes -PostDelaySeconds $PostDelaySeconds -ProgressIndicator $ProgressIndicator -DoNotUseCredSsp -NoNewLine:$NoNewLine
                }
            }

            $session = New-LabPSSession -ComputerName $vm -UseLocalCredential -Retries 1 -DoNotUseCredSsp:$DoNotUseCredSsp -ErrorAction SilentlyContinue

            if ($session)
            {
                Write-PSFMessage "Computer '$vm' was reachable"
                Start-Job -Name "Waiting for machine '$vm'" -ScriptBlock {
                    param (
                        [string]$ComputerName
                    )

                    $ComputerName
                } -ArgumentList $vm.Name
            }
            else
            {
                Write-PSFMessage "Computer '$($vm.ComputerName)' was not reachable, waiting..."
                Start-Job -Name "Waiting for machine '$vm'" -ScriptBlock {
                    param(
                        [Parameter(Mandatory)]
                        [byte[]]$LabBytes,

                        [Parameter(Mandatory)]
                        [string]$ComputerName,

                        [Parameter(Mandatory)]
                        [bool]$DoNotUseCredSsp
                    )

                    $VerbosePreference = $using:VerbosePreference

                    Import-Module -Name Az* -ErrorAction SilentlyContinue
                    Import-Module -Name AutomatedLab.Common -ErrorAction Stop
                    Write-Verbose "Importing Lab from $($LabBytes.Count) bytes"
                    Import-Lab -LabBytes $LabBytes -NoValidation -NoDisplay

                    #do 5000 retries. This job is cancelled anyway if the timeout is reached
                    Write-Verbose "Trying to create session to '$ComputerName'"
                    $session = New-LabPSSession -ComputerName $ComputerName -UseLocalCredential  -Retries 5000 -DoNotUseCredSsp:$DoNotUseCredSsp

                    return $ComputerName
                } -ArgumentList $lab.Export(), $vm.Name, $DoNotUseCredSsp
            }
        }

        Write-PSFMessage "Waiting for $($jobs.Count) machines to respond in timeout ($TimeoutInMinutes minute(s))"

        Wait-LWLabJob -Job $jobs -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewLine -NoDisplay -Timeout $TimeoutInMinutes

        $completed = $jobs | Where-Object State -eq Completed | Receive-Job -ErrorAction SilentlyContinue -Verbose:$VerbosePreference

        if ($completed)
        {
            $notReadyMachines = (Compare-Object -ReferenceObject $completed -DifferenceObject $vms.Name).InputObject
            $jobs | Remove-Job -Force
        }
        else
        {
            $notReadyMachines = $vms.Name
        }

        if ($notReadyMachines)
        {
            $message = "The following machines are not ready: $($notReadyMachines -join ', ')"
            Write-LogFunctionExitWithError -Message $message
        }
        else
        {
            Write-PSFMessage "The following machines are ready: $($completed -join ', ')"

            foreach ($machine in (Get-LabVM -ComputerName $completed))
            {
                if ($machine.SkipDeployment -or $machine.HostType -ne 'HyperV') { continue }
                $machineMetadata = Get-LWHypervVMDescription -ComputerName $machine.ResourceName
                if ($machineMetadata.InitState -eq [AutomatedLab.LabVMInitState]::Uninitialized)
                {
                    $machineMetadata.InitState = [AutomatedLab.LabVMInitState]::ReachedByAutomatedLab
                    Set-LWHypervVMDescription -Hashtable $machineMetadata -ComputerName $machine.ResourceName
                    Enable-LabAutoLogon -ComputerName $ComputerName
                }

                if ($DoNotUseCredSsp -and ($machineMetadata.InitState -band [AutomatedLab.LabVMInitState]::EnabledCredSsp) -ne [AutomatedLab.LabVMInitState]::EnabledCredSsp)
                {
                    $credSspEnabled = Invoke-LabCommand -ComputerName $machine -ScriptBlock {

                        if ($PSVersionTable.PSVersion.Major -eq 2)
                        {
                            $d = "{0:HH:mm}" -f (Get-Date).AddMinutes(1)
                            $jobName = "AL_EnableCredSsp"
                            $Path = 'PowerShell'
                            $CommandLine = '-Command Enable-WSManCredSSP -Role Server -Force; Get-WSManCredSSP | Out-File -FilePath C:\EnableCredSsp.txt'
                            schtasks.exe /Create /SC ONCE /ST $d /TN $jobName /TR "$Path $CommandLine" | Out-Null
                            schtasks.exe /Run /TN $jobName | Out-Null
                            Start-Sleep -Seconds 1
                            while ((schtasks.exe /Query /TN $jobName) -like '*Running*')
                            {
                                Write-Host '.' -NoNewline
                                Start-Sleep -Seconds 1
                            }
                            Start-Sleep -Seconds 1
                            schtasks.exe /Delete /TN $jobName /F | Out-Null

                            Start-Sleep -Seconds 5

                            [bool](Get-Content -Path C:\EnableCredSsp.txt | Where-Object { $_ -eq 'This computer is configured to receive credentials from a remote client computer.' })
                        }
                        else
                        {
                            Enable-WSManCredSSP -Role Server -Force | Out-Null
                            [bool](Get-WSManCredSSP | Where-Object { $_ -eq 'This computer is configured to receive credentials from a remote client computer.' })
                        }


                    } -PassThru -DoNotUseCredSsp -NoDisplay

                    if ($credSspEnabled)
                    {
                        $machineMetadata.InitState = $machineMetadata.InitState -bor [AutomatedLab.LabVMInitState]::EnabledCredSsp
                    }
                    else
                    {
                        Write-ScreenInfo "CredSsp could not be enabled on machine '$machine'" -Type Warning
                    }

                    Set-LWHypervVMDescription -Hashtable $machineMetadata -ComputerName $(Get-LabVM -ComputerName $machine).ResourceName
                }
            }

            Write-LogFunctionExit
        }

        if ($PostDelaySeconds)
        {
            $job = Start-Job -Name "Wait $PostDelaySeconds seconds" -ScriptBlock { Start-Sleep -Seconds $Using:PostDelaySeconds }
            Wait-LWLabJob -Job $job -ProgressIndicator $ProgressIndicator -NoDisplay -NoNewLine:$NoNewLine
        }
    }
}


function Wait-LabVMRestart
{
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,

        [switch]$DoNotUseCredSsp,

        [double]$TimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_WaitLabMachine_Online),

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [AutomatedLab.Machine[]]$StartMachinesWhileWaiting,

        [switch]$NoNewLine,

        $MonitorJob,

        [DateTime]$MonitoringStartTime = (Get-Date)
    )

    begin
    {
        Write-LogFunctionEntry

        if (-not $PSBoundParameters.ContainsKey('ProgressIndicator')) { $PSBoundParameters.Add('ProgressIndicator', $ProgressIndicator) } #enables progress indicator

        $lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }

        $vms = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()
    }

    process
    {
        $null = Get-LabVM -ComputerName $ComputerName | Where-Object SkipDeployment -eq $false | Foreach-Object {$vms.Add($_)}
    }

    end
    {
        $azureVms = $vms | Where-Object HostType -eq 'Azure'
        $hypervVms = $vms | Where-Object HostType -eq 'HyperV'
        $vmwareVms = $vms | Where-Object HostType -eq 'VMWare'

        if ($azureVms)
        {
            Wait-LWAzureRestartVM -ComputerName $azureVms -DoNotUseCredSsp:$DoNotUseCredSsp -TimeoutInMinutes $TimeoutInMinutes `
            -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewLine -ErrorAction SilentlyContinue -ErrorVariable azureWaitError -MonitoringStartTime $MonitoringStartTime
        }

        if ($hypervVms)
        {
            Wait-LWHypervVMRestart -ComputerName $hypervVms -TimeoutInMinutes $TimeoutInMinutes -ProgressIndicator $ProgressIndicator -NoNewLine:$NoNewLine -StartMachinesWhileWaiting $StartMachinesWhileWaiting -ErrorAction SilentlyContinue -ErrorVariable hypervWaitError -MonitorJob $MonitorJob
        }

        if ($vmwareVms)
        {
            Wait-LWVMWareRestartVM -ComputerName $vmwareVms -TimeoutInMinutes $TimeoutInMinutes -ProgressIndicator $ProgressIndicator -ErrorAction SilentlyContinue -ErrorVariable vmwareWaitError
        }

        $waitError = New-Object System.Collections.ArrayList
        if ($azureWaitError) { $waitError.AddRange($azureWaitError) }
        if ($hypervWaitError) { $waitError.AddRange($hypervWaitError) }
        if ($vmwareWaitError) { $waitError.AddRange($vmwareWaitError) }

        $waitError = $waitError | Where-Object { $_.Exception.Message -like 'Timeout while waiting for computers to restart*' }
        if ($waitError)
        {
            $nonRestartedMachines = $waitError.TargetObject

            Write-Error "The following machines have not restarted in the timeout of $TimeoutInMinutes minute(s): $($nonRestartedMachines -join ', ')"
        }

        Write-LogFunctionExit
    }
}


function Wait-LabVMShutdown
{
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,

        [double]$TimeoutInMinutes = (Get-LabConfigurationItem -Name Timeout_WaitLabMachine_Online),

        [ValidateRange(0, 300)]
        [int]$ProgressIndicator = (Get-LabConfigurationItem -Name DefaultProgressIndicator),

        [switch]$NoNewLine
    )

    begin
    {
        Write-LogFunctionEntry

        $start = Get-Date
        $lab = Get-Lab
        if (-not $lab)
        {
            Write-Error 'No definitions imported, so there is nothing to do. Please use Import-Lab first'
            return
        }

        $vms = [System.Collections.Generic.List[AutomatedLab.Machine]]::new()
    }

    process
    {
        $null = Get-LabVM -ComputerName $ComputerName |
            Add-Member -Name HasShutdown -MemberType NoteProperty -Value $false -Force -PassThru |
            Foreach-Object {$vms.Add($_)}
    }

    end
    {
        $ProgressIndicatorTimer = Get-Date
        do
        {
            foreach ($vm in $vms)
            {
                $status = Get-LabVMStatus -ComputerName $vm -Verbose:$false

                if ($status -eq 'Stopped')
                {
                    $vm.HasShutdown = $true
                }
                else
                {
                    Start-Sleep -Seconds 5
                }
            }
            if (((Get-Date) - $ProgressIndicatorTimer).TotalSeconds -ge $ProgressIndicator)
            {
                Write-ProgressIndicator
                $ProgressIndicatorTimer = (Get-Date)
            }
        }
        until (($vms | Where-Object { $_.HasShutdown }).Count -eq $vms.Count -or (Get-Date).AddMinutes(- $TimeoutInMinutes) -gt $start)

        foreach ($vm in ($vms | Where-Object { -not $_.HasShutdown }))
        {
            Write-Error -Message "Timeout while waiting for computer '$($vm.Name)' to shutdown." -TargetObject $vm.Name -ErrorVariable shutdownError
        }

        if ($shutdownError)
        {
            Write-Error "The following machines have not shutdown in the timeout of $TimeoutInMinutes minute(s): $($shutdownError.TargetObject -join ', ')"
        }

        Write-LogFunctionExit
    }
}


function Add-LabVMWareSettings
{
    param (
        [Parameter(Mandatory)]
        [string]$DataCenterName,

        [Parameter(Mandatory)]
        [string]$DataStoreName,

        [Parameter(Mandatory)]
        [string]$ResourcePoolName,

        [Parameter(Mandatory)]
        [string]$VCenterServerName,

        [Parameter(Mandatory)]
        [pscredential]$Credential,

        [switch]$PassThru
    )

    Write-LogFunctionEntry

    Update-LabVMWareSettings

    #loading a snaping twice results in: Add-PSSnapin : An item with the same key has already been added
    #Add-PSSnapin -Name VMware.VimAutomation.Core, VMware.VimAutomation.Vds -ErrorAction Stop

    if (-not $script:lab.VMWareSettings)
    {
        $script:lab.VMWareSettings = New-Object AutomatedLab.VMWareConfiguration
    }

    Connect-VIServer -Server $VCenterServerName -Credential $Credential -ErrorAction Stop

    $script:lab.VMWareSettings.DataCenter = Get-Datacenter -Name $DataCenterName -ErrorAction Stop
    $Script:lab.VMWareSettings.DataCenterName = $DataCenterName

    $script:lab.VMWareSettings.DataStore = Get-Datastore -Name $DataStoreName -ErrorAction SilentlyContinue
    $script:lab.VMWareSettings.DataStoreName = $DataStoreName
    if (-not $script:lab.VMWareSettings.DataStore)
    {
        $script:lab.VMWareSettings.DataStore = Get-DatastoreCluster -Name $DataStoreName -ErrorAction SilentlyContinue
    }
    if (-not $script:lab.VMWareSettings.DataStore)
    {
        throw "Could not find a DataStore nor a DataStoreCluster with the name '$DataStoreName'"
    }

    $script:lab.VMWareSettings.ResourcePool = Get-ResourcePool -Name $ResourcePoolName -Location $script:lab.VMWareSettings.DataCenter -ErrorAction Stop
    $script:lab.VMWareSettings.ResourcePoolName = $ResourcePoolName

    $script:lab.VMWareSettings.VCenterServerName = $VCenterServerName
    $script:lab.VMWareSettings.Credential = [System.Management.Automation.PSSerializer]::Serialize($Credential)

    if ($PassThru)
    {
        $script:lab.VMWareSettings
    }

    Write-LogFunctionExit
}


function Add-LabWacManagedNode
{
    [CmdletBinding()]
    param
    ( )

    $lab = Get-Lab -ErrorAction SilentlyContinue

    if (-not $lab)
    {
        Write-Error -Message 'Please deploy a lab first.'
        return
    }

    $machines = Get-LabVM -Role WindowsAdminCenter

    # Add hosts through REST API
    foreach ($machine in $machines)
    {
        $role = $machine.Roles.Where( { $_.Name -eq 'WindowsAdminCenter' })
        # In case machine deployment is skipped, we are using the installation user credential of the machine to make the connection
        $wacCredential = if ($machine.SkipDeployment)
        {
            $machine.GetLocalCredential()
        }
        else
        {
            $machine.GetCredential($lab)
        }

        $useSsl = $true
        if ($role.Properties.ContainsKey('UseSsl'))
        {
            $useSsl = [Convert]::ToBoolean($role.Properties['UseSsl'])
        }

        $Port = 443
        if (-not $useSsl)
        {
            $Port = 80
        }
        if ($role.Properties.ContainsKey('Port'))
        {
            $Port = $role.Properties['Port']
        }

        if (-not $machine.SkipDeployment -and $lab.DefaultVirtualizationEngine -eq 'Azure')
        {
            $azPort = Get-LabAzureLoadBalancedPort -DestinationPort $Port -ComputerName $machine
            $Port = $azPort.Port
        }

        $filteredHosts = if ($role.Properties.ContainsKey('ConnectedNode'))
        {
            Get-LabVM | Where-Object -Property Name -in ($role.Properties['ConnectedNode'] | ConvertFrom-Json)
        }
        else
        {
            Get-LabVM | Where-Object -FilterScript { $_.Name -ne $machine.Name -and -not $_.SkipDeployment }
        }

        if ($filteredHosts.Count -eq 0) { return }

        $wachostname = if (-not $machine.SkipDeployment -and $lab.DefaultVirtualizationEngine -eq 'Azure') 
        {
            $machine.AzureConnectionInfo.DnsName
        }
        elseif ($machine.SkipDeployment)
        {
            $machine.Name
        }
        else
        {
            $machine.FQDN
        }
        Write-ScreenInfo -Message "Adding $($filteredHosts.Count) hosts to the admin center for user $($wacCredential.UserName)"
        $apiEndpoint = "http$(if($useSsl){'s'})://$($wachostname):$Port/api/connections"

        $bodyHash = foreach ($vm in $filteredHosts)
        {
            @{
                id   = "msft.sme.connection-type.server!$($vm.FQDN)"
                name = $vm.FQDN
                type = "msft.sme.connection-type.server"
            }
        }

        try
        {
            [ServerCertificateValidationCallback]::Ignore()

            $paramIwr = @{
                Method      = 'PUT'
                Uri         = $apiEndpoint
                Credential  = $wacCredential
                Body        = $($bodyHash | ConvertTo-Json)
                ContentType = 'application/json'
                ErrorAction = 'Stop'
            }

            if ($PSEdition -eq 'Core' -and (Get-Command Invoke-RestMethod).Parameters.COntainsKey('SkipCertificateCheck'))
            {
                $paramIwr.SkipCertificateCheck = $true
            }

            $response = Invoke-RestMethod @paramIwr
            if ($response.changes.Count -ne $filteredHosts.Count)
            {
                Write-ScreenInfo -Type Error -Message "Result set too small, there has likely been an issue adding the managed nodes. Server response:`r`n`r`n$($response.changes)"
            }

            Write-ScreenInfo -Message "Successfully added $($filteredHosts.Count) machines as connections for $($wacCredential.UserName)"
        }
        catch
        {
            Write-ScreenInfo -Type Error -Message "Could not add server connections. Invoke-RestMethod says: $($_.Exception.Message)"
        }
    }
}


function Install-LabWindowsAdminCenter
{
    [CmdletBinding()]
    param
    ( )

    $lab = Get-Lab -ErrorAction SilentlyContinue

    if (-not $lab)
    {
        Write-Error -Message 'Please deploy a lab first.'
        return
    }

    $machines = (Get-LabVM -Role WindowsAdminCenter).Where( { -not $_.SkipDeployment })
    
    if ($machines)
    {
        Start-LabVM -ComputerName $machines -Wait
        $wacDownload = Get-LabInternetFile -Uri (Get-LabConfigurationItem -Name WacDownloadUrl) -Path "$labSources\SoftwarePackages" -FileName WAC.msi -PassThru -NoDisplay
        Copy-LabFileItem -Path $wacDownload.FullName -ComputerName $machines

        $jobs = foreach ($labMachine in $machines)
        {
            if ((Invoke-LabCommand -ComputerName $labMachine -ScriptBlock { Get-Service -Name ServerManagementGateway -ErrorAction SilentlyContinue } -PassThru -NoDisplay))
            {
                Write-ScreenInfo -Type Verbose -Message "$labMachine already has Windows Admin Center installed"
                continue
            }

            $role = $labMachine.Roles.Where( { $_.Name -eq 'WindowsAdminCenter' })
            $useSsl = $true
            if ($role.Properties.ContainsKey('UseSsl'))
            {
                $useSsl = [Convert]::ToBoolean($role.Properties['UseSsl'])
            }


            if ($useSsl -and $labMachine.IsDomainJoined -and (Get-LabIssuingCA -DomainName $labMachine.DomainName -ErrorAction SilentlyContinue) )
            {
                $san = @(
                    $labMachine.Name
                    if ($lab.DefaultVirtualizationEngine -eq 'Azure') { $labMachine.AzureConnectionInfo.DnsName }
                )
                $cert = Request-LabCertificate -Subject "CN=$($labMachine.FQDN)" -SAN $san -TemplateName WebServer -ComputerName $labMachine -PassThru -ErrorAction Stop
            }

            $Port = 443
            if (-not $useSsl)
            {
                $Port = 80
            }
            if ($role.Properties.ContainsKey('Port'))
            {
                $Port = $role.Properties['Port']
            }

            $arguments = @(
                '/qn'
                '/L*v C:\wacLoc.txt'
                "SME_PORT=$Port"
            )

            if ($role.Properties.ContainsKey('EnableDevMode'))
            {
                $arguments += 'DEV_MODE=1'
            }

            if ($cert.Thumbprint)
            {
                $arguments += "SME_THUMBPRINT=$($cert.Thumbprint)"
                $arguments += "SSL_CERTIFICATE_OPTION=installed"
            }
            elseif ($useSsl)
            {
                $arguments += "SSL_CERTIFICATE_OPTION=generate"
            }

            if (-not $machine.SkipDeployment -and $lab.DefaultVirtualizationEngine -eq 'Azure')
            {
                if (-not (Get-LabAzureLoadBalancedPort -DestinationPort $Port -ComputerName $labMachine))
                {
                    $lab.AzureSettings.LoadBalancerPortCounter++
                    $remotePort = $lab.AzureSettings.LoadBalancerPortCounter
                    Add-LWAzureLoadBalancedPort -ComputerName $labMachine -DestinationPort $Port -Port $remotePort
                    $Port = $remotePort
                }
            }

            if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12')
            {
                Write-Verbose -Message 'Adding support for TLS 1.2'
                [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
            }

            Write-ScreenInfo -Type Verbose -Message "Starting installation of Windows Admin Center on $labMachine"
            Install-LabSoftwarePackage -LocalPath C:\WAC.msi -CommandLine $($arguments -join ' ') -ComputerName $labMachine -ExpectedReturnCodes 0, 3010 -AsJob -PassThru -NoDisplay
        }

        if ($jobs)
        {
            Write-ScreenInfo -Message "Waiting for the installation of Windows Admin Center to finish on $machines"
            Wait-LWLabJob -Job $jobs -ProgressIndicator 5 -NoNewLine -NoDisplay

            if ($jobs.State -contains 'Failed')
            {
                $jobs.Where( { $_.State -eq 'Failed' }) | Receive-Job -Keep -ErrorAction SilentlyContinue -ErrorVariable err
                if ($err[0].Exception -is [System.Management.Automation.Remoting.PSRemotingTransportException])
                {
                    Write-ScreenInfo -Type Verbose -Message "WAC setup has restarted WinRM. The setup of WAC should be completed"
                }
                else
                {
                    Write-ScreenInfo -Type Error -Message "Installing Windows Admin Center on $($jobs.Name.Replace('WAC_')) failed. Review the errors with Get-Job -Id $($installation.Id) | Receive-Job -Keep"
                    return
                }
            }

            Restart-LabVM -ComputerName $machines -Wait -NoDisplay
        }
    }

    Add-LabWacManagedNode
}


if ($PSEdition -eq 'Core')
{
    Add-Type -Path $PSScriptRoot/lib/core/AutomatedLab.dll

    # These modules SHOULD be marked as Core compatible, as tested with Windows 10.0.18362.113
    # However, if they are not, they need to be imported.
    $requiredModules = @('Dism')
    $requiredModulesImplicit = @('International') # These modules should be imported via implicit remoting. Might suffer from implicit sessions getting removed though

    $ipmoErr = $null # Initialize, otherwise Import-MOdule -Force will extend this variable indefinitely
    if ($requiredModulesImplicit)
    {
        try
        {
            if ((Get-Command Import-Module).Parameters.ContainsKey('UseWindowsPowerShell'))
            {
                Import-Module -Name $requiredModulesImplicit -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop -Force -ErrorVariable +ipmoErr
            }
            else
            {
                Import-WinModule -Name $requiredModulesImplicit -WarningAction SilentlyContinue -ErrorAction Stop -Force -ErrorVariable +ipmoErr
            }
        }
        catch
        {
            Remove-Module -Name $requiredModulesImplicit -Force -ErrorAction SilentlyContinue
            Clear-Variable -Name ipmoErr -ErrorAction SilentlyContinue
            foreach ($m in $requiredModulesImplicit)
            {
                Get-ChildItem -Directory -Path ([IO.Path]::GetTempPath()) -Filter "RemoteIpMoProxy_$($m)*_localhost_*" | Remove-Item -Recurse -Force
            }

            if ((Get-Command Import-Module).Parameters.ContainsKey('UseWindowsPowerShell'))
            {
                Import-Module -Name $requiredModulesImplicit -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -Force -ErrorVariable +ipmoErr
            }
            else
            {
                Import-WinModule -Name $requiredModulesImplicit -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -Force -ErrorVariable +ipmoErr
            }
        }
    }

    if ($requiredModules)
    {
        Import-Module -Name $requiredModules -SkipEditionCheck -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -Force -ErrorVariable +ipmoErr
    }

    if ($ipmoErr)
    {
        Write-PSFMessage -Level Warning -Message "Could not import modules: $($ipmoErr.TargetObject -join ',') - your experience might be impacted."
    }
}
else
{
    Add-Type -Path $PSScriptRoot/lib/full/AutomatedLab.dll
}

if ((Get-Module -ListAvailable Ships) -and (Get-Module -ListAvailable AutomatedLab.Ships))
{
    Import-Module Ships, AutomatedLab.Ships
    [void] (New-PSDrive -PSProvider SHiPS -Name Labs -Root "AutomatedLab.Ships#LabHost" -WarningAction SilentlyContinue -ErrorAction SilentlyContinue)
}

Set-Item -Path Env:\SuppressAzurePowerShellBreakingChangeWarnings -Value true

#region Register default configuration if not present
Set-PSFConfig -Module 'AutomatedLab' -Name LabAppDataRoot -Value (Join-Path ([System.Environment]::GetFolderPath('CommonApplicationData')) -ChildPath "AutomatedLab") -Initialize -Validation string -Description "Root folder to Labs, Assets and Stores"
Set-PSFConfig -Module 'AutomatedLab' -Name 'DisableVersionCheck' -Value $false -Initialize -Validation bool -Description 'Set to true to skip checking GitHub for an updated AutomatedLab release'

if (-not (Get-PSFConfigValue -FullName AutomatedLab.DisableVersionCheck))
{
    $usedRelease = (Split-Path -Leaf -Path $PSScriptRoot) -as [version]
    $currentRelease = try { ((Invoke-RestMethod -Method Get -Uri https://api.github.com/repos/AutomatedLab/AutomatedLab/releases/latest -ErrorAction Stop).tag_Name -replace 'v') -as [Version] } catch {}

    if ($currentRelease -and $usedRelease -lt $currentRelease)
    {
        Write-PSFMessage -Level Host -Message "Your version of AutomatedLab is outdated. Consider updating to the recent version, $currentRelease"
    }
}


Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Ifttt.Key' -Value 'Your IFTTT key here' -Initialize -Validation string -Description "IFTTT Key Name"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Ifttt.EventName' -Value 'The name of your IFTTT event' -Initialize -Validation String -Description "IFTTT Event Name"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.Port' -Value 25 -Initialize -Validation integer -Description "Port of your SMTP Server"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.SmtpServer' -Value 'your SMTP server here' -Initialize -Validation string -Description "Adress of your SMTP server"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.To' -Value @('Recipients here') -Initialize -Validation stringarray -Description "A list of default recipients"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.From' -Value "$($env:USERNAME)@localhost" -Initialize -Validation string -Description "Your sender address"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.Priority' -Value 'Normal' -Initialize -Validation string -Description "Priority of your message"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Mail.CC' -Value @('Recipients here') -Initialize -Validation stringarray -Description "A list of default CC recipients"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Toast.Image' -Value 'https://raw.githubusercontent.com/AutomatedLab/AutomatedLab/master/Assets/Automated-Lab_icon512.png' -Initialize -Validation string -Description "The image for your toast notification"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Voice.Culture' -Value 'en-us' -Initialize -Validation string -Description "Voice culture, needs to be available and defaults to en-us"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.NotificationProviders.Voice.Gender' -Value 'female' -Initialize -Validation string -Description "Gender of voice to use"
Set-PSFConfig -Module 'AutomatedLab' -Name 'Notifications.SubscribedProviders' -Value @('Toast') -Initialize -Validation stringarray -Description 'List of subscribed providers'
Set-PSFConfig -Module 'AutomatedLab' -Name 'MachineFileName' -Value 'Machines.xml' -Initialize -Validation string -Description 'The file name for the deserialized machines. Do not change unless you know what you are doing.'
Set-PSFConfig -Module 'AutomatedLab' -Name 'DiskFileName' -Value 'Disks.xml' -Initialize -Validation string -Description 'The file name for the deserialized disks. Do not change unless you know what you are doing.'
Set-PSFConfig -Module 'AutomatedLab' -Name 'LabFileName' -Value 'Lab.xml' -Initialize -Validation string -Description 'The file name for the deserialized labs. Do not change unless you know what you are doing.'
Set-PSFConfig -Module 'AutomatedLab' -Name 'DefaultAddressSpace' -Value '192.168.10.0/24' -Initialize -Validation string -Description 'Default address space if no address space is selected'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_WaitLabMachine_Online -Value 60 -Initialize -Validation integer -Description 'Timeout in minutes for Wait-LabVm'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_StartLabMachine_Online -Value 60 -Initialize -Validation integer -Description 'Timeout in minutes for Start-LabVm'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_RestartLabMachine_Shutdown -Value 30 -Initialize -Validation integer -Description 'Timeout in minutes for Restart-LabVm'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_StopLabMachine_Shutdown -Value 30 -Initialize -Validation integer -Description 'Timeout in minutes for Stop-LabVm'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_TestPortInSeconds -Value 2 -Initialize -Validation integer -Description 'Timeout in seconds for Test-Port'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_InstallLabCAInstallation -Value 40 -Initialize -Validation integer -Description 'Timeout in minutes for CA setup'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_DcPromotionRestartAfterDcpromo -Value 60 -Initialize -Validation integer -Description 'Timeout in minutes for restart after DC Promo'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_DcPromotionAdwsReady -Value 20 -Initialize -Validation integer -Description 'Timeout in minutes for availability of ADWS after DC Promo'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_Sql2008Installation -Value 90 -Initialize -Validation integer -Description 'Timeout in minutes for SQL 2008'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_Sql2012Installation -Value 90 -Initialize -Validation integer -Description 'Timeout in minutes for SQL 2012'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_Sql2014Installation -Value 90 -Initialize -Validation integer -Description 'Timeout in minutes for SQL 2014'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_ConfigurationManagerInstallation -Value 60 -Initialize -Validation integer -Description 'Timeout in minutes to wait for the installation of Configuration Manager. Default value 60.'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_VisualStudio2013Installation -Value 90 -Initialize -Validation integer -Description 'Timeout in minutes for VS 2013'
Set-PSFConfig -Module 'AutomatedLab' -Name Timeout_VisualStudio2015Installation -Value 90 -Initialize -Validation integer -Description 'Timeout in minutes for VS 2015'
Set-PSFConfig -Module 'AutomatedLab' -Name DefaultProgressIndicator -Value 10 -Initialize -Validation integer -Description 'After how many minutes will a progress indicator be written'
Set-PSFConfig -Module 'AutomatedLab' -Name DisableConnectivityCheck -Value $false -Initialize -Validation bool -Description 'Indicates whether connectivity checks should be skipped. Certain systems like Azure DevOps build workers do not send ICMP packges and the method might always fail'
Set-PSFConfig -Module 'AutomatedLab' -Name 'VmPath' -Value $null -Validation string -Initialize -Description 'VM storage location'
$osroot = if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
{
    'C:\'
}
else
{
    '/'
}
Set-PSFConfig -Module 'AutomatedLab' -Name OsRoot -Value $osroot -Initialize -Validation string
Set-PSFConfig -Module 'AutomatedLab' -Name OverridePowerPlan -Value $true -Initialize -Validation bool -Description 'On Windows: Indicates that power settings will be set to High Power during lab deployment'
Set-PSFConfig -Module 'AutomatedLab' -Name SendFunctionTelemetry -Value $false -Initialize -Validation bool -Description 'Indicates if function call telemetry is sent' -Hidden
Set-PSFConfig -Module 'AutomatedLab' -Name DoNotWaitForLinux -Value $false -Initialize -Validation bool -Description 'Indicates that you will not wait for Linux VMs to be ready, e.g. because you are offline and PowerShell cannot be installed.'
Set-PSFConfig -Module 'AutomatedLab' -Name DoNotPrompt -Value $false -Initialize -Validation bool -Description 'Indicates that AutomatedLab should not display prompts. Workaround for environments that register as interactive, even if they are not. Skips enabling telemetry, skips Azure lab sources sync, forcibly configures remoting' -Hidden

#PSSession settings
Set-PSFConfig -Module 'AutomatedLab' -Name InvokeLabCommandRetries -Value 3 -Initialize -Validation integer -Description 'Number of retries for Invoke-LabCommand'
Set-PSFConfig -Module 'AutomatedLab' -Name InvokeLabCommandRetryIntervalInSeconds -Value 10 -Initialize -Validation integer -Description 'Retry interval for Invoke-LabCommand'
Set-PSFConfig -Module 'AutomatedLab' -Name MaxPSSessionsPerVM -Value 5 -Initialize -Validation integer -Description 'Maximum number of sessions per VM'
Set-PSFConfig -Module 'AutomatedLab' -Name DoNotUseGetHostEntryInNewLabPSSession -Value $true -Initialize -Validation bool -Description 'Do not use hosts file for session creation'

#DSC
Set-PSFConfig -Module 'AutomatedLab' -Name DscMofPath -Value 'DscConfigurations' -Initialize -Validation string -Description 'Default path for MOF files on Pull server'
Set-PSFConfig -Module 'AutomatedLab' -Name DscPullServerRegistrationKey -Value 'ec717ee9-b343-49ee-98a2-26e53939eecf'  -Initialize -Validation string  -Description 'DSC registration key used on all Dsc Pull servers and clients'

#General VM settings
Set-PSFConfig -Module 'AutomatedLab' -Name DisableWindowsDefender -Value $true -Initialize -Validation bool -Description 'Indicates that Windows Defender should be disabled on the lab VMs'
Set-PSFConfig -Module 'AutomatedLab' -Name DoNotSkipNonNonEnglishIso -Value $false -Initialize -Validation bool  -Description 'Indicates that non English ISO files will not be skipped'
Set-PSFConfig -Module 'AutomatedLab' -Name DefaultDnsForwarder1 -Value 1.1.1.1 -Initialize -Description 'If routing is installed on a Root DC, this forwarder is used'
Set-PSFConfig -Module 'AutomatedLab' -Name DefaultDnsForwarder2 -Value 8.8.8.8 -Initialize -Description 'If routing is installed on a Root DC, this forwarder is used'
Set-PSFConfig -Module 'AutomatedLab' -Name WinRmMaxEnvelopeSizeKb -Value 500 -Validation integerpositive -Initialize -Description 'CAREFUL! Fiddling with the defaults will likely result in errors if you do not know what you are doing! Configure a different envelope size on all lab machines if necessary.'
Set-PSFConfig -Module 'AutomatedLab' -Name WinRmMaxConcurrentOperationsPerUser -Value 1500 -Validation integerpositive -Initialize -Description 'CAREFUL! Fiddling with the defaults will likely result in errors if you do not know what you are doing! Configure a different number of per-user concurrent operations on all lab machines if necessary.'
Set-PSFConfig -Module 'AutomatedLab' -Name WinRmMaxConnections -Value 300 -Validation integerpositive -Initialize -Description 'CAREFUL! Fiddling with the defaults will likely result in errors if you do not know what you are doing! Configure a different max number of connections on all lab machines if necessary.'

#Hyper-V VM Settings
Set-PSFConfig -Module 'AutomatedLab' -Name SetLocalIntranetSites -Value 'All'  -Initialize -Validation string  -Description 'All, Forest, Domain, None'
Set-PSFConfig -Module 'AutomatedLab' -Name DisableClusterCheck -Value $false -Initialize -Validation bool -Description 'Set to true to disable checking cluster with Get-LWHyperVVM in case you are suffering from performance issues. Caution: While this speeds up deployment, the likelihood for errors increases when machines are migrated away from the host!'
Set-PSFConfig -Module 'AutomatedLab' -Name DoNotAddVmsToCluster -Value $false -Initialize -Validation bool -Description 'Set to true to skip adding VMs to a cluster if AutomatedLab is being run on a cluster node'

#Hyper-V Network settings
Set-PSFConfig -Module 'AutomatedLab' -Name MacAddressPrefix -Value '0017FB' -Initialize -Validation string -Description 'The MAC address prefix for Hyper-V labs' -Handler { if ($args[0].Length -eq 0 -or $args[0].Length -gt 11) { Write-PSFMessage -Level Error -Message "Invalid prefix length for MacAddressPrefix! $($args[0]) needs to be at least one character and at most 11 characters"; throw "Invalid prefix length for MacAddressPrefix! $($args[0]) needs to be at least one character and at most 11 characters" } }
Set-PSFConfig -Module 'AutomatedLab' -Name DisableDeviceNaming -Value $false -Validation bool -Initialize -Description 'Disables Device Naming for VM NICs. Enabled by default for Hosts > 2016 and Gen 2 Guests > 2016'

#Hyper-V Disk Settings
Set-PSFConfig -Module 'AutomatedLab' -Name CreateOnlyReferencedDisks -Value $true -Initialize -Validation bool -Description 'Disks that are not references by a VM will not be created'

#Admin Center
Set-PSFConfig -Module 'AutomatedLab' -Name WacDownloadUrl -Value 'http://aka.ms/WACDownload' -Validation string -Initialize -Description 'Windows Admin Center Download URL'

#Host Settings
Set-PSFConfig -Module 'AutomatedLab' -Name DiskDeploymentInProgressPath -Value (Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath "LabDiskDeploymentInProgress.txt") -Initialize -Validation string -Description 'The file indicating that Hyper-V disks are being configured to reduce disk congestion'
Set-PSFConfig -Module 'AutomatedLab' -Name SwitchDeploymentInProgressPath -Value (Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath "VSwitchDeploymentInProgress.txt") -Initialize -Validation string -Description 'The file indicating that VM switches are being deployed in case multiple lab deployments are started in parallel'
Set-PSFConfig -Module 'AutomatedLab' -Name SkipHostFileModification -Value $false -Initialize -Validation bool -Description 'Indicates that the hosts file should not be modified when deploying a new lab.'

#Azure
Set-PSFConfig -Module 'AutomatedLab' -Name MinimumAzureModuleVersion -Value '4.1.0' -Initialize -Validation string -Description 'The minimum expected Azure module version'
Set-PSFConfig -Module 'AutomatedLab' -Name DefaultAzureRoleSize -Value 'D' -Initialize -Validation string -Description 'The default Azure role size, e.g. from Get-LabAzureAvailableRoleSize'
Set-PSFConfig -Module 'AutomatedLab' -Name LabSourcesMaxFileSizeMb -Value 50 -Initialize -Validation integer -Description 'The default file size for Sync-LabAzureLabSources'
Set-PSFConfig -Module 'AutomatedLab' -Name AutoSyncLabSources -Value $false -Initialize -Validation bool -Description 'Toggle auto-sync of Azure lab sources in Azure labs'
Set-PSFConfig -Module 'AutomatedLab' -Name LabSourcesSyncIntervalDays -Value 60 -Initialize -Validation integerpositive -Description 'Interval in days for lab sources auto-sync'
Set-PSFConfig -Module 'AutomatedLab' -Name AzureDiskSkus -Value @('Standard_LRS', 'Premium_LRS', 'StandardSSD_LRS') # 'UltraSSD_LRS' is not allowed!
Set-PSFConfig -Module 'AutomatedLab' -Name AzureEnableJit -Value $false -Initialize -Validation bool -Description 'Enable this setting to have AutomatedLab configure ports 22, 3389 and 5986 for JIT access. Can be done manually with Enable-LabAzureJitAccess and requested (after enabling) with Request-LabAzureJitAccess'
Set-PSFConfig -Module 'AutomatedLab' -Name RequiredAzModules -Value @(
    # Syntax: Name, MinimumVersion, RequiredVersion
    @{
        Name           = 'Az.Accounts'
        MinimumVersion = '2.7.6'
    }
    @{
        Name           = 'Az.Storage'
        MinimumVersion = '4.5.0'
    }
    @{
        Name           = 'Az.Compute'
        MinimumVersion = '4.26.0'
    }
    @{
        Name           = 'Az.Network'
        MinimumVersion = '4.16.1'
    }
    @{
        Name           = 'Az.Resources'
        MinimumVersion = '5.6.0'
    }
    @{
        Name           = 'Az.Websites'
        MinimumVersion = '2.11.1'
    }
    @{
        Name           = 'Az.Security'
        MinimumVersion = '1.2.0'
    }
) -Initialize -Description 'Required Az modules'

Set-PSFConfig -Module 'AutomatedLab' -Name RequiredAzStackModules -Value @(
    @{
        Name           = 'Az.Accounts'
        MinimumVersion = '2.2.8'
    }
    @{
        Name           = 'Az.Storage'
        MinimumVersion = '2.6.2'
    }
    @{
        Name           = 'Az.Compute'
        MinimumVersion = '3.3.0'
    }
    @{
        Name           = 'Az.Network'
        MinimumVersion = '1.2.0'
    }
    @{
        Name           = 'Az.Resources'
        MinimumVersion = '0.12.0'
    }
    @{
        Name           = 'Az.Websites'
        MinimumVersion = '0.11.0'
    }
) -Initialize -Description 'Required Az Stack Hub modules'
Set-PSFConfig -Module 'AutomatedLab' -Name UseLatestAzureProviderApi -Value $true -Description 'Indicates that the latest provider API versions available in the labs region should be used' -Initialize -Validation bool

#Office
Set-PSFConfig -Module 'AutomatedLab' -Name OfficeDeploymentTool -Value 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_12827-20268.exe' -Initialize -Validation string -Description 'Link to Microsoft Office deployment tool'

#SysInternals
Set-PSFConfig -Module 'AutomatedLab' -Name SkipSysInternals -Value $false -Initialize -Validation bool -Description 'Set to true to skip downloading Sysinternals'
Set-PSFConfig -Module 'AutomatedLab' -Name SysInternalsUrl -Value 'https://technet.microsoft.com/en-us/sysinternals/bb842062' -Initialize -Validation string -Description 'Link to SysInternals to check for newer versions'
Set-PSFConfig -Module 'AutomatedLab' -Name SysInternalsDownloadUrl -Value 'https://download.sysinternals.com/files/SysinternalsSuite.zip' -Initialize -Validation string -Description 'Link to download of SysInternals'

#.net Framework
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet452DownloadLink -Value 'https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe' -Initialize -Validation string -Description 'Link to .NET 4.5.2'
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet46DownloadLink -Value 'http://download.microsoft.com/download/6/F/9/6F9673B1-87D1-46C4-BF04-95F24C3EB9DA/enu_netfx/NDP46-KB3045557-x86-x64-AllOS-ENU_exe/NDP46-KB3045557-x86-x64-AllOS-ENU.exe' -Initialize -Validation string -Description 'Link to .NET 4.6'
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet462DownloadLink -Value 'https://download.microsoft.com/download/F/9/4/F942F07D-F26F-4F30-B4E3-EBD54FABA377/NDP462-KB3151800-x86-x64-AllOS-ENU.exe' -Initialize -Validation string -Description 'Link to .NET 4.6.2'
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet471DownloadLink -Value 'https://download.microsoft.com/download/9/E/6/9E63300C-0941-4B45-A0EC-0008F96DD480/NDP471-KB4033342-x86-x64-AllOS-ENU.exe' -Initialize -Validation string -Description 'Link to .NET 4.7.1'
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet472DownloadLink -Value 'https://download.microsoft.com/download/6/E/4/6E48E8AB-DC00-419E-9704-06DD46E5F81D/NDP472-KB4054530-x86-x64-AllOS-ENU.exe' -Initialize -Validation string -Description 'Link to .NET 4.7.2'
Set-PSFConfig -Module 'AutomatedLab' -Name dotnet48DownloadLink -Value 'https://download.visualstudio.microsoft.com/download/pr/7afca223-55d2-470a-8edc-6a1739ae3252/abd170b4b0ec15ad0222a809b761a036/ndp48-x86-x64-allos-enu.exe' -Initialize -Validation string -Description 'Link to .NET 4.8'

# C++ redist
Set-PSFConfig -Module 'AutomatedLab' -Name cppredist64_2017 -Value 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2017 (x64)'
Set-PSFConfig -Module 'AutomatedLab' -Name cppredist32_2017 -Value 'https://aka.ms/vs/15/release/vc_redist.x86.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2017 (x86)'

Set-PSFConfig -Module 'AutomatedLab' -Name cppredist64_2015 -Value 'https://download.microsoft.com/download/6/A/A/6AA4EDFF-645B-48C5-81CC-ED5963AEAD48/vc_redist.x64.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2015 (x64)'
Set-PSFConfig -Module 'AutomatedLab' -Name cppredist32_2015 -Value 'https://download.microsoft.com/download/6/A/A/6AA4EDFF-645B-48C5-81CC-ED5963AEAD48/vc_redist.x86.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2015 (x86)'

Set-PSFConfig -Module 'AutomatedLab' -Name cppredist64_2013 -Value 'https://aka.ms/highdpimfc2013x64enu' -Initialize -Validation string -Description 'Link to VC++ redist 2013 (x64)'
Set-PSFConfig -Module 'AutomatedLab' -Name cppredist32_2013 -Value 'https://aka.ms/highdpimfc2013x86enu' -Initialize -Validation string -Description 'Link to VC++ redist 2013 (x86)'

Set-PSFConfig -Module 'AutomatedLab' -Name cppredist64_2012 -Value 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2012 (x64)'
Set-PSFConfig -Module 'AutomatedLab' -Name cppredist32_2012 -Value 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe' -Initialize -Validation string -Description 'Link to VC++ redist 2012 (x86)'

Set-PSFConfig -Module 'AutomatedLab' -Name cppredist64_2010 -Value 'http://go.microsoft.com/fwlink/?LinkId=404264&clcid=0x409' -Initialize -Validation string -Description 'Link to VC++ redist 2010 (x64)'

# IIS URL Rewrite Module
Set-PSFConfig -Module automatedlab -Name IisUrlRewriteDownloadUrl -Value "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi" -Validation string -Description 'Link to IIS URL Rewrite Module needed for Exchange 2016 and 2019'

#SQL Server 2016 Management Studio
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2016ManagementStudio -Value 'https://go.microsoft.com/fwlink/?LinkID=840946' -Initialize -Validation string -Description 'Link to SSMS 2016'
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2017ManagementStudio -Value 'https://go.microsoft.com/fwlink/?linkid=2099720' -Initialize -Validation string -Description 'Link to SSMS 2017 18.2'
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2019ManagementStudio -Value 'https://aka.ms/ssmsfullsetup' -Initialize -Validation string -Description 'Link to SSMS latest'
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2022ManagementStudio -Value 'https://aka.ms/ssmsfullsetup' -Initialize -Validation string -Description 'Link to SSMS latest'

# SSRS
Set-PSFConfig -Module 'AutomatedLab' -Name SqlServerReportBuilder -Value https://download.microsoft.com/download/5/E/B/5EB40744-DC0A-47C0-8B0A-1830E74D3C23/ReportBuilder.msi
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2017SSRS -Value https://download.microsoft.com/download/E/6/4/E6477A2A-9B58-40F7-8AD6-62BB8491EA78/SQLServerReportingServices.exe
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2019SSRS -Value https://download.microsoft.com/download/1/a/a/1aaa9177-3578-4931-b8f3-373b24f63342/SQLServerReportingServices.exe
Set-PSFConfig -Module 'AutomatedLab' -Name Sql2022SSRS -Value https://download.microsoft.com/download/8/3/2/832616ff-af64-42b5-a0b1-5eb07f71dec9/SQLServerReportingServices.exe

#SQL Server sample database contents
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2008 -Value 'http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=msftdbprodsamples&DownloadId=478218&FileTime=129906742909030000&Build=21063' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2008'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2008R2 -Value 'http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=msftdbprodsamples&DownloadId=478218&FileTime=129906742909030000&Build=21063' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2008 R2'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2012 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2012.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2012'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2014 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2014.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2014'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2016 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2016'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2017 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2017'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2019 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2019'
Set-PSFConfig -Module 'AutomatedLab' -Name SQLServer2022 -Value 'https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak' -Initialize -Validation string -Description 'Link to SQL sample DB for SQL 2022'

#Access Database Engine
Set-PSFConfig -Module 'AutomatedLab' -Name AccessDatabaseEngine2016x86 -Value 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/AccessDatabaseEngine.exe' -Initialize -Validation string -Description 'Link to Access Database Engine (required for DSC Pull)'
#TFS Build Agent
Set-PSFConfig -Module 'AutomatedLab' -Name BuildAgentUri -Value 'https://vstsagentpackage.azureedge.net/agent/2.153.1/vsts-agent-win-x64-2.153.1.zip' -Initialize -Validation string -Description 'Link to Azure DevOps/VSTS Build Agent'

# SCVMM
Set-PSFConfig -Module 'AutomatedLab' -Name SqlOdbc11 -Value 'https://download.microsoft.com/download/5/7/2/57249A3A-19D6-4901-ACCE-80924ABEB267/ENU/x64/msodbcsql.msi'
Set-PSFConfig -Module 'AutomatedLab' -Name SqlOdbc13 -Value 'https://download.microsoft.com/download/D/5/E/D5EEF288-A277-45C8-855B-8E2CB7E25B96/x64/msodbcsql.msi'
Set-PSFConfig -Module 'AutomatedLab' -Name SqlCommandLineUtils -Value 'https://download.microsoft.com/download/C/8/8/C88C2E51-8D23-4301-9F4B-64C8E2F163C5/x64/MsSqlCmdLnUtils.msi'
Set-PSFConfig -Module 'AutomatedLab' -Name WindowsAdk -Value 'https://download.microsoft.com/download/8/6/c/86c218f3-4349-4aa5-beba-d05e48bbc286/adk/adksetup.exe'
Set-PSFConfig -Module 'AutomatedLab' -Name WindowsAdkPe -Value 'https://download.microsoft.com/download/3/c/2/3c2b23b2-96a0-452c-b9fd-6df72266e335/adkwinpeaddons/adkwinpesetup.exe'

# SCOM
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2014 -Value 'https://download.microsoft.com/download/6/7/8/67858AF1-B1B3-48B1-87C4-4483503E71DC/ENU/x64/SQLSysClrTypes.msi' -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2016 -Value "https://download.microsoft.com/download/6/4/5/645B2661-ABE3-41A4-BC2D-34D9A10DD303/ENU/x64/SQLSysClrTypes.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2019 -Value "https://download.microsoft.com/download/d/d/1/dd194c5c-d859-49b8-ad64-5cbdcbb9b7bd/SQLSysClrTypes.msi" -Initialize -Validation string
Set-PSFConfig -Module 'AutomatedLab' -Name ReportViewer2015 -Value 'https://download.microsoft.com/download/A/1/2/A129F694-233C-4C7C-860F-F73139CF2E01/ENU/x86/ReportViewer.msi'

# OpenSSH
Set-PSFConfig -Module 'AutomatedLab' -Name OpenSshUri -Value 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/v7.6.0.0p1-Beta/OpenSSH-Win64.zip' -Initialize -Validation string -Description 'Link to OpenSSH binaries'
Set-PSFConfig -Module 'AutomatedLab' -Name 'AzureLocationsUrls' -Value @{
    'East US'             = 'speedtesteus'
    'East US 2'           = 'speedtesteus2'
    'South Central US'    = 'speedtestscus'
    'West US 2'           = 'speedtestwestus2'
    'Australia East'      = 'speedtestoze'
    'Southeast Asia'      = 'speedtestsea'
    'North Europe'        = 'speedtestne'
    'Sweden Central'      = 'speedtestesc'
    'UK South'            = 'speedtestuks'
    'West Europe'         = 'speedtestwe'
    'Central US'          = 'speedtestcus'
    'South Africa North'  = 'speedtestsan'
    'Central India'       = 'speedtestcentralindia'
    'East Asia'           = 'speedtestea'
    'Japan East'          = 'speedtestjpe'
    'Canada Central'      = 'speedtestcac'
    'France Central'      = 'speedtestfrc'
    'Norway East'         = 'azspeednoeast'
    'Switzerland North'   = 'speedtestchn'
    'UAE North'           = 'speedtestuaen'
    'Brazil'              = 'speedtestnea'
    'North Central US'    = 'speedtestnsus'
    'West US'             = 'speedtestwus'
    'West Central US'     = 'speedtestwestcentralus'
    'Australia Southeast' = 'speedtestozse'
    'Japan West'          = 'speedtestjpw'
    'Korea South'         = 'speedtestkoreasouth'
    'South India'         = 'speedtesteastindia'
    'West India'          = 'speedtestwestindia'
    'Canada East'         = 'speedtestcae'
    'Germany North'       = 'speedtestden'
    'Switzerland West'    = 'speedtestchw'
    'UK West'             = 'speedtestukw'
} -Initialize -Description 'Hashtable containing all Azure Speed Test URLs for automatic region placement'

Set-PSFConfig -Module 'AutomatedLab' -Name SupportGen2VMs -Value $true -Initialize -Validation bool -Description 'Indicates that Gen2 VMs are supported'
Set-PSFConfig -Module 'AutomatedLab' -Name AzureRetryCount -Value 3 -Initialize -Validation integer -Description 'The number of retries for Azure actions like creating a virtual network'

# SharePoint
Set-PSFConfig -Module AutomatedLab -Name SharePoint2013Key -Value 'N3MDM-DXR3H-JD7QH-QKKCR-BY2Y7' -Validation String -Initialize -Description 'SP 2013 trial key'
Set-PSFConfig -Module AutomatedLab -Name SharePoint2016Key -Value 'NQGJR-63HC8-XCRQH-MYVCH-3J3QR' -Validation String -Initialize -Description 'SP 2016 trial key'
Set-PSFConfig -Module AutomatedLab -Name SharePoint2019Key -Value 'M692G-8N2JP-GG8B2-2W2P7-YY7J6' -Validation String -Initialize -Description 'SP 2019 trial key'

Set-PSFConfig -Module AutomatedLab -Name SharePoint2013Prerequisites -Value @(
    'https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe'
    "http://download.microsoft.com/download/9/1/3/9138773A-505D-43E2-AC08-9A77E1E0490B/1033/x64/sqlncli.msi",
    "http://download.microsoft.com/download/8/F/9/8F93DBBD-896B-4760-AC81-646F61363A6D/WcfDataServices.exe",
    "http://download.microsoft.com/download/9/1/D/91DA8796-BE1D-46AF-8489-663AB7811517/setup_msipc_x64.msi",
    "http://download.microsoft.com/download/E/0/0/E0060D8F-2354-4871-9596-DC78538799CC/Synchronization.msi",
    "http://download.microsoft.com/download/1/C/A/1CAA41C7-88B9-42D6-9E11-3C655656DAB1/WcfDataServices.exe",
    "http://download.microsoft.com/download/0/1/D/01D06854-CA0C-46F1-ADBA-EBF86010DCC6/r2/MicrosoftIdentityExtensions-64.msi",
    "http://download.microsoft.com/download/D/7/2/D72FD747-69B6-40B7-875B-C2B40A6B2BDD/Windows6.1-KB974405-x64.msu",
    "http://download.microsoft.com/download/A/6/7/A678AB47-496B-4907-B3D4-0A2D280A13C0/WindowsServerAppFabricSetup_x64.exe",
    "http://download.microsoft.com/download/7/B/5/7B51D8D1-20FD-4BF0-87C7-4714F5A1C313/AppFabric1.1-RTM-KB2671763-x64-ENU.exe"
) -Initialize -Description 'List of prerequisite urls for SP2013' -Validation stringarray

Set-PSFConfig -Module AutomatedLab -Name SharePoint2016Prerequisites -Value @(
    "https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi",
    "https://download.microsoft.com/download/3/C/F/3CF781F5-7D29-4035-9265-C34FF2369FA2/setup_msipc_x64.exe",
    "https://download.microsoft.com/download/B/9/D/B9D6E014-C949-4A1E-BA6B-2E0DEBA23E54/SyncSetup_en.x64.zip",
    "https://download.microsoft.com/download/1/C/A/1CAA41C7-88B9-42D6-9E11-3C655656DAB1/WcfDataServices.exe",
    "https://download.microsoft.com/download/0/1/D/01D06854-CA0C-46F1-ADBA-EBF86010DCC6/rtm/MicrosoftIdentityExtensions-64.msi",
    "https://download.microsoft.com/download/A/6/7/A678AB47-496B-4907-B3D4-0A2D280A13C0/WindowsServerAppFabricSetup_x64.exe",
    "https://download.microsoft.com/download/F/1/0/F1093AF6-E797-4CA8-A9F6-FC50024B385C/AppFabric-KB3092423-x64-ENU.exe",
    'https://download.microsoft.com/download/5/7/2/57249A3A-19D6-4901-ACCE-80924ABEB267/ENU/x64/msodbcsql.msi'
    'https://download.microsoft.com/download/F/9/4/F942F07D-F26F-4F30-B4E3-EBD54FABA377/NDP462-KB3151800-x86-x64-AllOS-ENU.exe'
) -Initialize -Description 'List of prerequisite urls for SP2013' -Validation stringarray

Set-PSFConfig -Module AutomatedLab -Name SharePoint2019Prerequisites -Value @(
    'https://download.microsoft.com/download/F/3/C/F3C64941-22A0-47E9-BC9B-1A19B4CA3E88/ENU/x64/sqlncli.msi',
    'https://download.microsoft.com/download/3/C/F/3CF781F5-7D29-4035-9265-C34FF2369FA2/setup_msipc_x64.exe',
    'https://download.microsoft.com/download/E/0/0/E0060D8F-2354-4871-9596-DC78538799CC/Synchronization.msi',
    'https://download.microsoft.com/download/1/C/A/1CAA41C7-88B9-42D6-9E11-3C655656DAB1/WcfDataServices.exe',
    'https://download.microsoft.com/download/0/1/D/01D06854-CA0C-46F1-ADBA-EBF86010DCC6/rtm/MicrosoftIdentityExtensions-64.msi',
    'https://download.microsoft.com/download/A/6/7/A678AB47-496B-4907-B3D4-0A2D280A13C0/WindowsServerAppFabricSetup_x64.exe',
    'https://download.microsoft.com/download/F/1/0/F1093AF6-E797-4CA8-A9F6-FC50024B385C/AppFabric-KB3092423-x64-ENU.exe',
    'https://download.microsoft.com/download/5/7/2/57249A3A-19D6-4901-ACCE-80924ABEB267/ENU/x64/msodbcsql.msi',
    'https://download.visualstudio.microsoft.com/download/pr/1f5af042-d0e4-4002-9c59-9ba66bcf15f6/089f837de42708daacaae7c04b7494db/ndp472-kb4054530-x86-x64-allos-enu.exe'
) -Initialize -Description 'List of prerequisite urls for SP2013' -Validation stringarray

# Dynamics 365 CRM
Set-PSFConfig -Module AutomatedLab -Name SqlServerNativeClient2012 -Value "https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2014 -Value "https://download.microsoft.com/download/1/3/0/13089488-91FC-4E22-AD68-5BE58BD5C014/ENU/x64/SQLSysClrTypes.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2016 -Value "https://download.microsoft.com/download/6/4/5/645B2661-ABE3-41A4-BC2D-34D9A10DD303/ENU/x64/SQLSysClrTypes.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlClrType2019 -Value "https://download.microsoft.com/download/d/d/1/dd194c5c-d859-49b8-ad64-5cbdcbb9b7bd/SQLSysClrTypes.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name SqlSmo2016 -Value "https://download.microsoft.com/download/6/4/5/645B2661-ABE3-41A4-BC2D-34D9A10DD303/ENU/x64/SharedManagementObjects.msi" -Initialize -Validation string
Set-PSFConfig -Module AutomatedLab -Name Dynamics365Uri -Value 'https://download.microsoft.com/download/B/D/0/BD0FA814-9885-422A-BA0E-54CBB98C8A33/CRM9.0-Server-ENU-amd64.exe' -Initialize -Validation String

# Exchange Server
Set-PSFConfig -Module AutomatedLab -Name Exchange2013DownloadUrl -Value 'https://download.microsoft.com/download/7/F/D/7FDCC96C-26C0-4D49-B5DB-5A8B36935903/Exchange2013-x64-cu23.exe'
Set-PSFConfig -Module AutomatedLab -Name Exchange2016DownloadUrl -Value 'https://download.microsoft.com/download/8/d/2/8d2d01b4-5bbb-4726-87da-0e331bc2b76f/ExchangeServer2016-x64-CU23.ISO'
Set-PSFConfig -Module AutomatedLab -Name Exchange2019DownloadUrl -Value 'https://download.microsoft.com/download/b/c/7/bc766694-8398-4258-8e1e-ce4ddb9b3f7d/ExchangeServer2019-x64-CU12.ISO'

# ConfigMgr
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerWmiExplorer -Value 'https://github.com/vinaypamnani/wmie2/releases/download/v2.0.0.2/WmiExplorer_2.0.0.2.zip'
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl1902CB -Value 'http://download.microsoft.com/download/1/B/C/1BCADBD7-47F6-40BB-8B1F-0B2D9B51B289/SC_Configmgr_SCEP_1902.exe'
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl1902TP -Value 'http://download.microsoft.com/download/1/B/C/1BCADBD7-47F6-40BB-8B1F-0B2D9B51B289/SC_Configmgr_SCEP_1902.exe'
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2002CB -Value "https://download.microsoft.com/download/e/0/a/e0a2dd5e-2b96-47e7-9022-3030f8a1807b/MEM_Configmgr_2002.exe"
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2002TP -Value "https://download.microsoft.com/download/D/8/E/D8E795CE-44D7-40B7-9067-D3D1313865E5/Configmgr_TechPreview2010.exe"
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2103CB -Value "https://download.microsoft.com/download/8/8/8/888d525d-5523-46ba-aca8-4709f54affa8/MEM_Configmgr_2103.exe"
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2103TP -Value "https://download.microsoft.com/download/D/8/E/D8E795CE-44D7-40B7-9067-D3D1313865E5/Configmgr_TechPreview2103.exe"
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2203CB -Value 'https://download.microsoft.com/download/f/5/5/f55e3b9c-781d-493b-932b-16aa1b2f6371/MEM_Configmgr_2203.exe'
Set-PSFConfig -Module AutomatedLab -Name ConfigurationManagerUrl2210TP -Value "https://download.microsoft.com/download/D/8/E/D8E795CE-44D7-40B7-9067-D3D1313865E5/Configmgr_TechPreview2210.exe"
# Validation
Set-PSFConfig -Module AutomatedLab -Name ValidationSettings -Value @{
    ValidRoleProperties     = @{
        Orchestrator2012         = @(
            'DatabaseServer'
            'DatabaseName'
            'ServiceAccount'
            'ServiceAccountPassword'
        )
        DC                       = @(
            'IsReadOnly'
            'SiteName'
            'SiteSubnet'
            'DatabasePath'
            'LogPath'
            'SysvolPath'
            'DsrmPassword'
        )
        CaSubordinate            = @(
            'ParentCA'
            'ParentCALogicalName'
            'CACommonName'
            'CAType'
            'KeyLength'
            'CryptoProviderName'
            'HashAlgorithmName'
            'DatabaseDirectory'
            'LogDirectory'
            'ValidityPeriod'
            'ValidityPeriodUnits'
            'CertsValidityPeriod'
            'CertsValidityPeriodUnits'
            'CRLPeriod'
            'CRLPeriodUnits'
            'CRLOverlapPeriod'
            'CRLOverlapUnits'
            'CRLDeltaPeriod'
            'CRLDeltaPeriodUnits'
            'UseLDAPAIA'
            'UseHTTPAIA'
            'AIAHTTPURL01'
            'AIAHTTPURL02'
            'AIAHTTPURL01UploadLocation'
            'AIAHTTPURL02UploadLocation'
            'UseLDAPCRL'
            'UseHTTPCRL'
            'CDPHTTPURL01'
            'CDPHTTPURL02'
            'CDPHTTPURL01UploadLocation'
            'CDPHTTPURL02UploadLocation'
            'InstallWebEnrollment'
            'InstallWebRole'
            'CPSURL'
            'CPSText'
            'InstallOCSP'
            'OCSPHTTPURL01'
            'OCSPHTTPURL02'
            'DoNotLoadDefaultTemplates'
        )
        Office2016               = 'SharedComputerLicensing'
        DSCPullServer            = @(
            'DoNotPushLocalModules'
            'DatabaseEngine'
            'SqlServer'
            'DatabaseName'
        )
        FirstChildDC             = @(
            'ParentDomain'
            'NewDomain'
            'DomainFunctionalLevel'
            'SiteName'
            'SiteSubnet'
            'NetBIOSDomainName'
            'DatabasePath'
            'LogPath'
            'SysvolPath'
            'DsrmPassword'
        )
        ADFS                     = @(
            'DisplayName'
            'ServiceName'
            'ServicePassword'
        )
        RootDC                   = @(
            'DomainFunctionalLevel'
            'ForestFunctionalLevel'
            'SiteName'
            'SiteSubnet'
            'NetBiosDomainName'
            'DatabasePath'
            'LogPath'
            'SysvolPath'
            'DsrmPassword'
        )
        CaRoot                   = @(
            'CACommonName'
            'CAType'
            'KeyLength'
            'CryptoProviderName'
            'HashAlgorithmName'
            'DatabaseDirectory'
            'LogDirectory'
            'ValidityPeriod'
            'ValidityPeriodUnits'
            'CertsValidityPeriod'
            'CertsValidityPeriodUnits'
            'CRLPeriod'
            'CRLPeriodUnits'
            'CRLOverlapPeriod'
            'CRLOverlapUnits'
            'CRLDeltaPeriod'
            'CRLDeltaPeriodUnits'
            'UseLDAPAIA'
            'UseHTTPAIA'
            'AIAHTTPURL01'
            'AIAHTTPURL02'
            'AIAHTTPURL01UploadLocation'
            'AIAHTTPURL02UploadLocation'
            'UseLDAPCRL'
            'UseHTTPCRL'
            'CDPHTTPURL01'
            'CDPHTTPURL02'
            'CDPHTTPURL01UploadLocation'
            'CDPHTTPURL02UploadLocation'
            'InstallWebEnrollment'
            'InstallWebRole'
            'CPSURL'
            'CPSText'
            'InstallOCSP'
            'OCSPHTTPURL01'
            'OCSPHTTPURL02'
            'DoNotLoadDefaultTemplates'
        )
        Tfs2015                  = @('Port', 'InitialCollection', 'DbServer')
        Tfs2017                  = @('Port', 'InitialCollection', 'DbServer')
        Tfs2018                  = @('Port', 'InitialCollection', 'DbServer')
        AzDevOps                 = @('Port', 'InitialCollection', 'DbServer', 'PAT', 'Organisation')
        TfsBuildWorker           = @(
            'NumberOfBuildWorkers'
            'TfsServer'
            'AgentPool'
            'PAT'
            'Organisation'
            'Capabilities'
        )
        WindowsAdminCenter       = @('Port', 'EnableDevMode', 'ConnectedNode', 'UseSsl')
        Scvmm2016                = @(
            'MUOptIn'
            'SqlMachineName'
            'LibraryShareDescription'
            'UserName'
            'CompanyName'
            'IndigoHTTPSPort'
            'SQMOptIn'
            'TopContainerName'
            'SqlInstanceName'
            'RemoteDatabaseImpersonation'
            'LibraryShareName'
            'SqlDatabaseName'
            'VmmServiceLocalAccount'
            'IndigoNETTCPPort'
            'CreateNewLibraryShare'
            'WSManTcpPort'
            'IndigoHTTPPort'
            'ProductKey'
            'BitsTcpPort'
            'CreateNewSqlDatabase'
            'ProgramFiles'
            'LibrarySharePath'
            'IndigoTcpPort'
            'SkipServer'
            'ConnectHyperVRoleVms'
            'ConnectClusters'
        )
        Scvmm2019                = @(
            'MUOptIn'
            'SqlMachineName'
            'LibraryShareDescription'
            'UserName'
            'CompanyName'
            'IndigoHTTPSPort'
            'SQMOptIn'
            'TopContainerName'
            'SqlInstanceName'
            'RemoteDatabaseImpersonation'
            'LibraryShareName'
            'SqlDatabaseName'
            'VmmServiceLocalAccount'
            'IndigoNETTCPPort'
            'CreateNewLibraryShare'
            'WSManTcpPort'
            'IndigoHTTPPort'
            'ProductKey'
            'BitsTcpPort'
            'CreateNewSqlDatabase'
            'ProgramFiles'
            'LibrarySharePath'
            'IndigoTcpPort'
            'SkipServer'
            'ConnectHyperVRoleVms'
            'ConnectClusters'
        )
        DynamicsFull             = @(
            'SqlServer',
            'ReportingUrl',
            'OrganizationCollation',
            'IsoCurrencyCode'
            'CurrencyName'
            'CurrencySymbol'
            'CurrencyPrecision'
            'Organization'
            'OrganizationUniqueName'
            'CrmServiceAccount'
            'SandboxServiceAccount'
            'DeploymentServiceAccount'
            'AsyncServiceAccount'
            'VSSWriterServiceAccount'
            'MonitoringServiceAccount'
            'CrmServiceAccountPassword'
            'SandboxServiceAccountPassword'
            'DeploymentServiceAccountPassword'
            'AsyncServiceAccountPassword'
            'VSSWriterServiceAccountPassword'
            'MonitoringServiceAccountPassword'
            'IncomingExchangeServer',
            'PrivUserGroup',
            'SQLAccessGroup',
            'ReportingGroup',
            'PrivReportingGroup'
            'LicenseKey'
        )
        DynamicsFrontend         = @(
            'SqlServer',
            'ReportingUrl',
            'OrganizationCollation',
            'IsoCurrencyCode'
            'CurrencyName'
            'CurrencySymbol'
            'CurrencyPrecision'
            'Organization'
            'OrganizationUniqueName'
            'CrmServiceAccount'
            'SandboxServiceAccount'
            'DeploymentServiceAccount'
            'AsyncServiceAccount'
            'VSSWriterServiceAccount'
            'MonitoringServiceAccount'
            'CrmServiceAccountPassword'
            'SandboxServiceAccountPassword'
            'DeploymentServiceAccountPassword'
            'AsyncServiceAccountPassword'
            'VSSWriterServiceAccountPassword'
            'MonitoringServiceAccountPassword'
            'IncomingExchangeServer',
            'PrivUserGroup',
            'SQLAccessGroup',
            'ReportingGroup',
            'PrivReportingGroup'
            'LicenseKey'
        )
        DynamicsBackend          = @(
            'SqlServer',
            'ReportingUrl',
            'OrganizationCollation',
            'IsoCurrencyCode'
            'CurrencyName'
            'CurrencySymbol'
            'CurrencyPrecision'
            'Organization'
            'OrganizationUniqueName'
            'CrmServiceAccount'
            'SandboxServiceAccount'
            'DeploymentServiceAccount'
            'AsyncServiceAccount'
            'VSSWriterServiceAccount'
            'MonitoringServiceAccount'
            'CrmServiceAccountPassword'
            'SandboxServiceAccountPassword'
            'DeploymentServiceAccountPassword'
            'AsyncServiceAccountPassword'
            'VSSWriterServiceAccountPassword'
            'MonitoringServiceAccountPassword'
            'IncomingExchangeServer',
            'PrivUserGroup',
            'SQLAccessGroup',
            'ReportingGroup',
            'PrivReportingGroup'
            'LicenseKey'
        )
        DynamicsAdmin            = @(
            'SqlServer',
            'ReportingUrl',
            'OrganizationCollation',
            'IsoCurrencyCode'
            'CurrencyName'
            'CurrencySymbol'
            'CurrencyPrecision'
            'Organization'
            'OrganizationUniqueName'
            'CrmServiceAccount'
            'SandboxServiceAccount'
            'DeploymentServiceAccount'
            'AsyncServiceAccount'
            'VSSWriterServiceAccount'
            'MonitoringServiceAccount'
            'CrmServiceAccountPassword'
            'SandboxServiceAccountPassword'
            'DeploymentServiceAccountPassword'
            'AsyncServiceAccountPassword'
            'VSSWriterServiceAccountPassword'
            'MonitoringServiceAccountPassword'
            'IncomingExchangeServer',
            'PrivUserGroup',
            'SQLAccessGroup',
            'ReportingGroup',
            'PrivReportingGroup'
            'LicenseKey'
        )
        ScomManagement           = @(
            'ManagementGroupName'
            'SqlServerInstance'
            'SqlInstancePort'
            'DatabaseName'
            'DwSqlServerInstance'
            'InstallLocation'
            'DwSqlInstancePort'
            'DwDatabaseName'
            'ActionAccountUser'
            'ActionAccountPassword'
            'DASAccountUser'
            'DASAccountPassword'
            'DataReaderUser'
            'DataReaderPassword'
            'DataWriterUser'
            'DataWriterPassword'
            'EnableErrorReporting'
            'SendCEIPReports'
            'UseMicrosoftUpdate'
            'AcceptEndUserLicenseAgreement'
            'ProductKey'
        )

        ScomConsole              = @(
            'EnableErrorReporting'
            'InstallLocation'
            'SendCEIPReports'
            'UseMicrosoftUpdate'
            'AcceptEndUserLicenseAgreement'
        )

        ScomWebConsole           = @(
            'ManagementServer'
            'WebSiteName'
            'WebConsoleAuthorizationMode'
            'SendCEIPReports'
            'UseMicrosoftUpdate'
            'AcceptEndUserLicenseAgreement'
        )

        ScomReporting            = @(
            'ManagementServer'
            'SRSInstance'
            'DataReaderUser'
            'DataReaderPassword'
            'SendODRReports'
            'UseMicrosoftUpdate'
            'AcceptEndUserLicenseAgreement'
        )
        RemoteDesktopSessionHost = @(
            'CollectionName'
            'CollectionDescription'
            'PersonalUnmanaged'
            'AutoAssignUser'
            'GrantAdministrativePrivilege'
            'PooledUnmanaged'
        )
        RemoteDesktopGateway     = @(
            'GatewayExternalFqdn'
            'BypassLocal'
            'LogonMethod'
            'UseCachedCredentials'
            'GatewayMode'
        )
        RemoteDesktopLicensing   = @(
            'Mode'
        )
        ConfigurationManager     = @(
            'Version'
            'Branch'
            'Roles'
            'SiteName'
            'SiteCode'
            'SqlServerName'
            'DatabaseName'
            'WsusContentPath'
            'AdminUser'
        )
    }
    MandatoryRoleProperties = @{
        ADFSProxy = @(
            'AdfsFullName'
            'AdfsDomainName'
        )
    }
} -Initialize -Description 'Validation settings for lab validation. Please do not modify unless you know what you are doing.'

# Product key file path
$fPath = Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath 'Assets/ProductKeys.xml'
$fcPath = Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath 'Assets/ProductKeysCustom.xml'
if (-not (Test-Path -Path $fPath -ErrorAction SilentlyContinue))
{
    $null = if (-not (Test-Path -Path (Split-Path $fPath -Parent))) { New-Item -Path (Split-Path $fPath -Parent) -ItemType Directory } 
    Copy-Item -Path "$PSScriptRoot/ProductKeys.xml" -Destination $fPath -Force -ErrorAction SilentlyContinue
}
Set-PSFConfig -Module AutomatedLab -Name ProductKeyFilePath -Value $fPath -Initialize -Validation string -Description 'Destination of the ProductKeys file for Windows products'
Set-PSFConfig -Module AutomatedLab -Name ProductKeyFilePathCustom -Value $fcPath -Initialize -Validation string -Description 'Destination of the ProductKeysCustom file for Windows products'

# LabSourcesLocation
# Set-PSFConfig -Module AutomatedLab -Name LabSourcesLocation -Description 'Location of lab sources folder' -Validation string -Value ''

#endregion

#region Linux folder
if ($IsLinux -or $IsMacOs -and -not (Test-Path (Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath 'Stores')))
{
    $null = New-Item -ItemType Directory -Path (Join-Path -Path (Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot) -ChildPath 'Stores')
}
#endregion



#download the ProductKeys.xml file if it does not exist. The installer puts the file into 'C:\ProgramData\AutomatedLab\Assets'
#but when installing AL using the PowerShell Gallery, this file is missing.
$productKeyFileLink = 'https://raw.githubusercontent.com/AutomatedLab/AutomatedLab/master/Assets/ProductKeys.xml'
$productKeyFileName = 'ProductKeys.xml'
$productKeyFilePath = Get-PSFConfigValue AutomatedLab.ProductKeyFilePath

if (-not (Test-Path -Path (Split-Path $productKeyFilePath -Parent)))
{
    New-Item -Path (Split-Path $productKeyFilePath -Parent) -ItemType Directory | Out-Null
}

if (-not (Test-Path -Path $productKeyFilePath))
{
    try { Invoke-RestMethod -Method Get -Uri $productKeyFileLink -OutFile $productKeyFilePath -ErrorAction Stop } catch {}
}

$productKeyCustomFilePath = Get-PSFConfigValue AutomatedLab.ProductKeyFilePathCustom

if (-not (Test-Path -Path $productKeyCustomFilePath))
{
    $store = New-Object 'AutomatedLab.ListXmlStore[AutomatedLab.ProductKey]'

    $dummyProductKey = New-Object AutomatedLab.ProductKey -Property @{ Key = '123'; OperatingSystemName = 'OS'; Version = '1.0' }
    $store.Add($dummyProductKey)
    $store.Export($productKeyCustomFilePath)
}

#region ArgumentCompleter
Register-PSFTeppScriptblock -Name AutomatedLab-NotificationProviders -ScriptBlock {
    (Get-PSFConfig -Module AutomatedLab -Name Notifications.NotificationProviders*).FullName |
    Foreach-Object { ($_ -split '\.')[3] } | Select-Object -Unique
}

Register-PSFTeppScriptblock -Name AutomatedLab-OperatingSystem -ScriptBlock {
    $lab = if (Get-Lab -ErrorAction SilentlyContinue)
    {
        Get-Lab -ErrorAction SilentlyContinue
    }
    elseif (Get-LabDefinition -ErrorAction SilentlyContinue)
    {
        Get-LabDefinition -ErrorAction SilentlyContinue
    }

    $param = @{
        UseOnlyCache = $true
        NoDisplay    = $true
    }

    if (-not $lab -or $lab -and $lab.DefaultVirtualizationEngine -eq 'HyperV')
    {        
        $param['Path'] = "$labSources/ISOs"
    }
    if ($lab.DefaultVirtualizationEngine -eq 'Azure')
    {
        $param['Azure'] = $true
    }
    if ($lab.DefaultVirtualizationEngine -eq 'Azure' -and $lab.AzureSettings.DefaultLocation)
    {
        $param['Location'] = $lab.AzureSettings.DefaultLocation.DisplayName
    }

    if (-not $global:AL_OperatingSystems)
    {
        $global:AL_OperatingSystems = Get-LabAvailableOperatingSystem @param
    }

    $global:AL_OperatingSystems.OperatingSystemName
}

Register-PSFTeppscriptblock -Name AutomatedLab-Labs -ScriptBlock {
    $path = "$(Get-PSFConfigValue -FullName AutomatedLab.LabAppDataRoot)/Labs"
    (Get-ChildItem -Path $path -Directory).Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-Roles -ScriptBlock {
    [System.Enum]::GetNames([AutomatedLab.Roles])
}

Register-PSFTeppScriptblock -Name AutomatedLab-Domains -ScriptBlock {
    (Get-LabDefinition -ErrorAction SilentlyContinue).Domains.Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-ComputerName -ScriptBlock {
    (Get-LabVM -All -IncludeLinux -SkipConnectionInfo).Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-VMSnapshot -ScriptBlock {
    (Get-LabVMSnapshot).SnapshotName | Select-Object -Unique
}

Register-PSFTeppScriptblock -Name AutomatedLab-Subscription -ScriptBlock {
    (Get-AzSubscription -WarningAction SilentlyContinue).Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-CustomRole -ScriptBlock {
    (Get-ChildItem -Path (Join-Path -Path (Get-LabSourcesLocationInternal -Local) -ChildPath 'CustomRoles' -ErrorAction SilentlyContinue) -Directory -ErrorAction SilentlyContinue).Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-AzureRoleSize -ScriptBlock {
    $defaultLocation = (Get-LabAzureDefaultLocation -ErrorAction SilentlyContinue).Location
    (Get-AzVMSize -Location $defaultLocation -ErrorAction SilentlyContinue |
    Where-Object -Property Name -notlike *basic* | Sort-Object -Property Name).Name
}

Register-PSFTeppScriptblock -Name AutomatedLab-TimeZone -ScriptBlock {
    [System.TimeZoneInfo]::GetSystemTimeZones().Id | Sort-Object
}

Register-PSFTeppScriptblock -Name AutomatedLab-RhelPackage -ScriptBlock {
    (Get-LabAvailableOperatingSystem -UseOnlyCache -ErrorAction SilentlyContinue |
    Where-Object { $_.OperatingSystemType -eq 'Linux' -and $_.LinuxType -eq 'RedHat' } |
    Sort-Object Version | Select-Object -Last 1).LinuxPackageGroup
}

Register-PSFTeppScriptblock -Name AutomatedLab-SusePackage -ScriptBlock {
    (Get-LabAvailableOperatingSystem -UseOnlyCache -ErrorAction SilentlyContinue |
    Where-Object { $_.OperatingSystemType -eq 'Linux' -and $_.LinuxType -eq 'SuSE' } |
    Sort-Object Version | Select-Object -Last 1).LinuxPackageGroup

}

Register-PSFTeppScriptblock -Name AutomatedLab-UbuntuPackage -ScriptBlock {
    (Get-LabAvailableOperatingSystem -UseOnlyCache -ErrorAction SilentlyContinue |
    Where-Object { $_.OperatingSystemType -eq 'Linux' -and $_.LinuxType -eq 'Ubuntu' } |
    Sort-Object Version | Select-Object -Last 1).LinuxPackageGroup

}

Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter OperatingSystem -Name 'AutomatedLab-OperatingSystem'
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter Roles -Name AutomatedLab-Roles
Register-PSFTeppArgumentCompleter -Command Get-Lab, Remove-Lab, Import-Lab, Import-LabDefinition -Parameter Name -Name AutomatedLab-Labs
Register-PSFTeppArgumentCompleter -Command Connect-Lab -Parameter SourceLab, DestinationLab -Name AutomatedLab-Labs
Register-PSFTeppArgumentCompleter -Command Send-ALNotification -Parameter Provider -Name AutomatedLab-NotificationProviders
Register-PSFTeppArgumentCompleter -Command Add-LabAzureSubscription -Parameter SubscriptionName -Name AutomatedLab-Subscription
Register-PSFTeppArgumentCompleter -Command Get-LabPostInstallationActivity -Parameter CustomRole -Name AutomatedLab-CustomRole
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter AzureRoleSize -Name AutomatedLab-AzureRoleSize
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition, Enable-LabMachineAutoShutdown -Parameter TimeZone -Name AutomatedLab-TimeZone
Register-PSFTeppArgumentCompleter -Command Add-LabAzureSubscription -Parameter AutoShutdownTimeZone -Name AutomatedLab-TimeZone
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter RhelPackage -Name AutomatedLab-RhelPackage
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter SusePackage -Name AutomatedLab-SusePackage
Register-PSFTeppArgumentCompleter -Command Add-LabMachineDefinition -Parameter SusePackage -Name AutomatedLab-UbuntuPackage
Register-PSFTeppArgumentCompleter -Command Get-LabVMSnapshot, Checkpoint-LabVM, Restore-LabVMSnapshot -Parameter SnapshotName -Name AutomatedLab-VMSnapshot
#endregion


$dynamicLabSources = New-Object AutomatedLab.DynamicVariable 'global:labSources', { Get-LabSourcesLocationInternal }, { $null }
$executioncontext.SessionState.PSVariable.Set($dynamicLabSources)
Set-Alias -Name ?? -Value Invoke-Ternary -Option AllScope -Description "Ternary Operator like '?' in C#"


$certStoreTypes = @'
using System;
using System.Runtime.InteropServices;

namespace System.Security.Cryptography.X509Certificates
{
    public class Win32
    {
        [DllImport("crypt32.dll", EntryPoint="CertOpenStore", CharSet=CharSet.Auto, SetLastError=true)]
        public static extern IntPtr CertOpenStore(
            int storeProvider,
            int encodingType,
            IntPtr hcryptProv,
            int flags,
            String pvPara);

        [DllImport("crypt32.dll", EntryPoint="CertCloseStore", CharSet=CharSet.Auto, SetLastError=true)]
        [return : MarshalAs(UnmanagedType.Bool)]
        public static extern bool CertCloseStore(
            IntPtr storeProvider,
            int flags);
    }

    public enum CertStoreLocation
    {
        CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000,
        CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000,
        CERT_SYSTEM_STORE_SERVICES = 0x00050000,
        CERT_SYSTEM_STORE_USERS = 0x00060000
    }

    [Flags]
    public enum CertStoreFlags
    {
        CERT_STORE_NO_CRYPT_RELEASE_FLAG = 0x00000001,
        CERT_STORE_SET_LOCALIZED_NAME_FLAG = 0x00000002,
        CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG = 0x00000004,
        CERT_STORE_DELETE_FLAG = 0x00000010,
        CERT_STORE_SHARE_STORE_FLAG = 0x00000040,
        CERT_STORE_SHARE_CONTEXT_FLAG = 0x00000080,
        CERT_STORE_MANIFOLD_FLAG = 0x00000100,
        CERT_STORE_ENUM_ARCHIVED_FLAG = 0x00000200,
        CERT_STORE_UPDATE_KEYID_FLAG = 0x00000400,
        CERT_STORE_BACKUP_RESTORE_FLAG = 0x00000800,
        CERT_STORE_READONLY_FLAG = 0x00008000,
        CERT_STORE_OPEN_EXISTING_FLAG = 0x00004000,
        CERT_STORE_CREATE_NEW_FLAG = 0x00002000,
        CERT_STORE_MAXIMUM_ALLOWED_FLAG = 0x00001000
    }

    public enum CertStoreProvider
    {
        CERT_STORE_PROV_MSG = 1,
        CERT_STORE_PROV_MEMORY = 2,
        CERT_STORE_PROV_FILE = 3,
        CERT_STORE_PROV_REG = 4,
        CERT_STORE_PROV_PKCS7 = 5,
        CERT_STORE_PROV_SERIALIZED = 6,
        CERT_STORE_PROV_FILENAME_A = 7,
        CERT_STORE_PROV_FILENAME_W = 8,
        CERT_STORE_PROV_FILENAME = CERT_STORE_PROV_FILENAME_W,
        CERT_STORE_PROV_SYSTEM_A = 9,
        CERT_STORE_PROV_SYSTEM_W = 10,
        CERT_STORE_PROV_SYSTEM = CERT_STORE_PROV_SYSTEM_W,
        CERT_STORE_PROV_COLLECTION = 11,
        CERT_STORE_PROV_SYSTEM_REGISTRY_A = 12,
        CERT_STORE_PROV_SYSTEM_REGISTRY_W = 13,
        CERT_STORE_PROV_SYSTEM_REGISTRY = CERT_STORE_PROV_SYSTEM_REGISTRY_W,
        CERT_STORE_PROV_PHYSICAL_W = 14,
        CERT_STORE_PROV_PHYSICAL = CERT_STORE_PROV_PHYSICAL_W,
        CERT_STORE_PROV_SMART_CARD_W = 15,
        CERT_STORE_PROV_SMART_CARD = CERT_STORE_PROV_SMART_CARD_W,
        CERT_STORE_PROV_LDAP_W = 16,
        CERT_STORE_PROV_LDAP = CERT_STORE_PROV_LDAP_W
    }
}
'@


$pkiInternalsTypes = @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;

namespace Pki
{
    public static class Period
    {
        public static TimeSpan ToTimeSpan(byte[] value)
        {
            var period = BitConverter.ToInt64(value, 0); period /= -10000000;
            return TimeSpan.FromSeconds(period);
        }

        public static byte[] ToByteArray(TimeSpan value)
        {
            var period = value.TotalSeconds;
            period *= -10000000;
            return BitConverter.GetBytes((long)period);
        }
    }
}

namespace Pki.CATemplate
{
    /// <summary>
    /// 2.27 msPKI-Private-Key-Flag Attribute
    /// https://msdn.microsoft.com/en-us/library/cc226547.aspx
    /// </summary>
    [Flags]
    public enum PrivateKeyFlags
    {
        None = 0, //This flag indicates that attestation data is not required when creating the certificate request. It also instructs the server to not add any attestation OIDs to the issued certificate. For more details, see [MS-WCCE] section 3.2.2.6.2.1.4.5.7.
        RequireKeyArchival = 1, //This flag instructs the client to create a key archival certificate request, as specified in [MS-WCCE] sections 3.1.2.4.2.2.2.8 and 3.2.2.6.2.1.4.5.7.
        AllowKeyExport = 16, //This flag instructs the client to allow other applications to copy the private key to a .pfx file, as specified in [PKCS12], at a later time.
        RequireStrongProtection = 32, //This flag instructs the client to use additional protection for the private key.
        RequireAlternateSignatureAlgorithm = 64, //This flag instructs the client to use an alternate signature format. For more details, see [MS-WCCE] section 3.1.2.4.2.2.2.8.
        ReuseKeysRenewal = 128, //This flag instructs the client to use the same key when renewing the certificate.<35>
        UseLegacyProvider = 256, //This flag instructs the client to process the msPKI-RA-Application-Policies attribute as specified in section 2.23.1.<36>
        TrustOnUse = 512, //This flag indicates that attestation based on the user's credentials is to be performed. For more details, see [MS-WCCE] section 3.2.2.6.2.1.4.5.7.
        ValidateCert = 1024, //This flag indicates that attestation based on the hardware certificate of the Trusted Platform Module (TPM) is to be performed. For more details, see [MS-WCCE] section 3.2.2.6.2.1.4.5.7.
        ValidateKey = 2048, //This flag indicates that attestation based on the hardware key of the TPM is to be performed. For more details, see [MS-WCCE] section 3.2.2.6.2.1.4.5.7.
        Preferred = 4096, //This flag informs the client that it SHOULD include attestation data if it is capable of doing so when creating the certificate request. It also instructs the server that attestation may or may not be completed before any certificates can be issued. For more details, see [MS-WCCE] sections 3.1.2.4.2.2.2.8 and 3.2.2.6.2.1.4.5.7.
        Required = 8192, //This flag informs the client that attestation data is required when creating the certificate request. It also instructs the server that attestation must be completed before any certificates can be issued. For more details, see [MS-WCCE] sections 3.1.2.4.2.2.2.8 and 3.2.2.6.2.1.4.5.7.
        WithoutPolicy = 16384, //This flag instructs the server to not add any certificate policy OIDs to the issued certificate even though attestation SHOULD be performed. For more details, see [MS-WCCE] section 3.2.2.6.2.1.4.5.7.
        xxx = 0x000F0000
    }

    [Flags]
    public enum KeyUsage
    {
        DIGITAL_SIGNATURE = 0x80,
        NON_REPUDIATION = 0x40,
        KEY_ENCIPHERMENT = 0x20,
        DATA_ENCIPHERMENT = 0x10,
        KEY_AGREEMENT = 0x8,
        KEY_CERT_SIGN = 0x4,
        CRL_SIGN = 0x2,
        ENCIPHER_ONLY_KEY_USAGE = 0x1,
        DECIPHER_ONLY_KEY_USAGE = (0x80 << 8),
        NO_KEY_USAGE = 0x0
    }

    public enum KeySpec
    {
        KeyExchange = 1, //Keys used to encrypt/decrypt session keys
        Signature = 2 //Keys used to create and verify digital signatures.
    }

    /// <summary>
    /// 2.26 msPKI-Enrollment-Flag Attribute
    /// https://msdn.microsoft.com/en-us/library/cc226546.aspx
    /// </summary>
    [Flags]
    public enum EnrollmentFlags
    {
        None = 0,
        IncludeSymmetricAlgorithms = 1, //This flag instructs the client and server to include a Secure/Multipurpose Internet Mail Extensions (S/MIME) certificate extension, as specified in RFC4262, in the request and in the issued certificate.
        CAManagerApproval = 2, // This flag instructs the CA to put all requests in a pending state.
        KraPublish = 4, // This flag instructs the CA to publish the issued certificate to the key recovery agent (KRA) container in Active Directory.
        DsPublish = 8, // This flag instructs clients and CA servers to append the issued certificate to the userCertificate attribute, as specified in RFC4523, on the user object in Active Directory.
        AutoenrollmentCheckDsCert = 16, // This flag instructs clients not to do autoenrollment for a certificate based on this template if the user's userCertificate attribute (specified in RFC4523) in Active Directory has a valid certificate based on the same template.
        Autoenrollment = 32, //This flag instructs clients to perform autoenrollment for the specified template.
        ReenrollExistingCert = 64, //This flag instructs clients to sign the renewal request using the private key of the existing certificate.
        RequireUserInteraction = 256, // This flag instructs the client to obtain user consent before attempting to enroll for a certificate that is based on the specified template.
        RemoveInvalidFromStore = 1024, // This flag instructs the autoenrollment client to delete any certificates that are no longer needed based on the specific template from the local certificate storage.
        AllowEnrollOnBehalfOf = 2048, //This flag instructs the server to allow enroll on behalf of(EOBO) functionality.
        IncludeOcspRevNoCheck = 4096, // This flag instructs the server to not include revocation information and add the id-pkix-ocsp-nocheck extension, as specified in RFC2560 section 4.2.2.2.1, to the certificate that is issued. Windows Server 2003 - this flag is not supported.
        ReuseKeyTokenFull = 8192, //This flag instructs the client to reuse the private key for a smart card-based certificate renewal if it is unable to create a new private key on the card.Windows XP, Windows Server 2003 - this flag is not supported. NoRevocationInformation 16384 This flag instructs the server to not include revocation information in the issued certificate. Windows Server 2003, Windows Server 2008 - this flag is not supported.
        BasicConstraintsInEndEntityCerts = 32768, //This flag instructs the server to include Basic Constraints extension in the end entity certificates. Windows Server 2003, Windows Server 2008 - this flag is not supported.
        IgnoreEnrollOnReenrollment = 65536, //This flag instructs the CA to ignore the requirement for Enroll permissions on the template when processing renewal requests. Windows Server 2003, Windows Server 2008, Windows Server 2008 R2 - this flag is not supported.
        IssuancePoliciesFromRequest = 131072 //This flag indicates that the certificate issuance policies to be included in the issued certificate come from the request rather than from the template. The template contains a list of all of the issuance policies that the request is allowed to specify; if the request contains policies that are not listed in the template, then the request is rejected. Windows Server 2003, Windows Server 2008, Windows Server 2008 R2 - this flag is not supported.
    }

    /// <summary>
    /// 2.28 msPKI-Certificate-Name-Flag Attribute
    /// https://msdn.microsoft.com/en-us/library/cc226548.aspx
    /// </summary>
    [Flags]
    public enum NameFlags
    {
        EnrolleeSuppliesSubject = 1, //This flag instructs the client to supply subject information in the certificate request
        OldCertSuppliesSubjectAndAltName = 8, //This flag instructs the client to reuse values of subject name and alternative subject name extensions from an existing valid certificate when creating a certificate renewal request. Windows Server 2003, Windows Server 2008 - this flag is not supported.
        EnrolleeSuppluiesAltSubject = 65536, //This flag instructs the client to supply subject alternate name information in the certificate request.
        AltSubjectRequireDomainDNS = 4194304, //This flag instructs the CA to add the value of the requester's FQDN and NetBIOS name to the Subject Alternative Name extension of the issued certificate.
        AltSubjectRequireDirectoryGUID = 16777216, //This flag instructs the CA to add the value of the objectGUID attribute from the requestor's user object in Active Directory to the Subject Alternative Name extension of the issued certificate.
        AltSubjectRequireUPN = 33554432, //This flag instructs the CA to add the value of the UPN attribute from the requestor's user object in Active Directory to the Subject Alternative Name extension of the issued certificate.
        AltSubjectRequireEmail = 67108864, //This flag instructs the CA to add the value of the e-mail attribute from the requestor's user object in Active Directory to the Subject Alternative Name extension of the issued certificate.
        AltSubjectRequireDNS = 134217728, //This flag instructs the CA to add the value obtained from the DNS attribute of the requestor's user object in Active Directory to the Subject Alternative Name extension of the issued certificate.
        SubjectRequireDNSasCN = 268435456, //This flag instructs the CA to add the value obtained from the DNS attribute of the requestor's user object in Active Directory as the CN in the subject of the issued certificate.
        SubjectRequireEmail = 536870912, //This flag instructs the CA to add the value of the e-mail attribute from the requestor's user object in Active Directory as the subject of the issued certificate.
        SubjectRequireCommonName = 1073741824, //This flag instructs the CA to set the subject name to the requestor's CN from Active Directory.
        SubjectrequireDirectoryPath = -2147483648 //This flag instructs the CA to set the subject name to the requestor's distinguished name (DN) from Active Directory.
    }

    /// <summary>
    /// 2.4 flags Attribute
    /// https://msdn.microsoft.com/en-us/library/cc226550.aspx
    /// </summary>
    [Flags]
    public enum Flags
    {
        Undefined = 1, //Undefined.
        AddEmail = 2, //Reserved. All protocols MUST ignore this flag.
        Undefined2 = 4, //Undefined.
        DsPublish = 8, //Reserved. All protocols MUST ignore this flag.
        AllowKeyExport = 16, //Reserved. All protocols MUST ignore this flag.
        Autoenrollment = 32, //This flag indicates whether clients can perform autoenrollment for the specified template.
        MachineType = 64, //This flag indicates that this certificate template is for an end entity that represents a machine.
        IsCA = 128, //This flag indicates a certificate request for a CA certificate.
        AddTemplateName = 512, //This flag indicates that a certificate based on this section needs to include a template name certificate extension.
        DoNotPersistInDB = 1024, //This flag indicates that the record of a certificate request for a certificate that is issued need not be persisted by the CA. Windows Server 2003, Windows Server 2008 - this flag is not supported.
        IsCrossCA = 2048, //This flag indicates a certificate request for cross-certifying a certificate.
        IsDefault = 65536, //This flag indicates that the template SHOULD not be modified in any way.
        IsModified = 131072 //This flag indicates that the template MAY be modified if required.
    }
}

namespace Pki.Certificates
{
    public enum CertificateType
    {
        Cer,
        Pfx
    }

    public class CertificateInfo
    {
        private X509Certificate2 certificate;
        private byte[] rawContentBytes;


        public string ComputerName { get; set; }
        public string Location { get; set; }
        public string ServiceName { get; set; }
        public string Store { get; set; }
        public string Password { get; set; }


        public X509Certificate2 Certificate
        {
            get { return certificate; }
        }

        public List<string> DnsNameList
        {
            get
            {
                return ParseSujectAlternativeNames(Certificate).ToList();
            }
        }

        public string Thumbprint
        {
            get
            {
                return Certificate.Thumbprint;
            }
        }

        public byte[] CertificateBytes
        {
            get
            {
                return certificate.RawData;
            }
        }

        public byte[] RawContentBytes
        {
            get
            {
                return rawContentBytes;
            }
        }

        public CertificateInfo(X509Certificate2 certificate)
        {
            this.certificate = certificate;
            rawContentBytes = new byte[0];
        }

        public CertificateInfo(byte[] bytes)
        {
            rawContentBytes = bytes;
            certificate = new X509Certificate2(rawContentBytes);
        }

        public CertificateInfo(byte[] bytes, SecureString password)
        {
            rawContentBytes = bytes;
            certificate = new X509Certificate2(rawContentBytes, password, X509KeyStorageFlags.Exportable);
            Password = ConvertToString(password);
        }

        public CertificateInfo(string fileName)
        {
            rawContentBytes = File.ReadAllBytes(fileName);
            certificate = new X509Certificate2(rawContentBytes);
        }

        public CertificateInfo(string fileName, SecureString password)
        {
            rawContentBytes = File.ReadAllBytes(fileName);
            certificate = new X509Certificate2(rawContentBytes, password, X509KeyStorageFlags.Exportable);
            Password = ConvertToString(password);
        }

        public X509ContentType Type
        {
            get
            {
                if (rawContentBytes.Length > 0)
                    return X509Certificate2.GetCertContentType(rawContentBytes);
                else
                    return X509Certificate2.GetCertContentType(CertificateBytes);
            }
        }

        public static IEnumerable<string> ParseSujectAlternativeNames(X509Certificate2 cert)
        {
            Regex sanRex = new Regex(@"^DNS Name=(.*)", RegexOptions.Compiled | RegexOptions.CultureInvariant);

            var sanList = from X509Extension ext in cert.Extensions
                          where ext.Oid.FriendlyName.Equals("Subject Alternative Name", StringComparison.Ordinal)
                          let data = new AsnEncodedData(ext.Oid, ext.RawData)
                          let text = data.Format(true)
                          from line in text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                          let match = sanRex.Match(line)
                          where match.Success && match.Groups.Count > 0 && !string.IsNullOrEmpty(match.Groups[1].Value)
                          select match.Groups[1].Value;

            return sanList;
        }

        private string ConvertToString(SecureString s)
        {
            var bstr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(s);
            return System.Runtime.InteropServices.Marshal.PtrToStringAuto(bstr);
        }
    }
}
'@


$gpoType = @'
    using System;
    using System.Collections.Generic;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using Microsoft.Win32;

    namespace GPO
    {
        /// <summary>
        /// Represent the result of group policy operations.
        /// </summary>
        public enum ResultCode
        {
            Succeed = 0,
            CreateOrOpenFailed = -1,
            SetFailed = -2,
            SaveFailed = -3
        }

        /// <summary>
        /// The WinAPI handler for GroupPlicy operations.
        /// </summary>
        public class WinAPIForGroupPolicy
        {
            // Group Policy Object open / creation flags
            const UInt32 GPO_OPEN_LOAD_REGISTRY = 0x00000001; // Load the registry files
            const UInt32 GPO_OPEN_READ_ONLY = 0x00000002; // Open the GPO as read only

            // Group Policy Object option flags
            const UInt32 GPO_OPTION_DISABLE_USER = 0x00000001; // The user portion of this GPO is disabled
            const UInt32 GPO_OPTION_DISABLE_MACHINE = 0x00000002; // The machine portion of this GPO is disabled

            const UInt32 REG_OPTION_NON_VOLATILE = 0x00000000;

            const UInt32 ERROR_MORE_DATA = 234;

            // You can find the Guid in <Gpedit.h>
            static readonly Guid REGISTRY_EXTENSION_GUID = new Guid("35378EAC-683F-11D2-A89A-00C04FBBCFA2");
            static readonly Guid CLSID_GPESnapIn = new Guid("8FC0B734-A0E1-11d1-A7D3-0000F87571E3");

            /// <summary>
            /// Group Policy Object type.
            /// </summary>
            enum GROUP_POLICY_OBJECT_TYPE
            {
                GPOTypeLocal = 0, // Default GPO on the local machine
                GPOTypeRemote, // GPO on a remote machine
                GPOTypeDS, // GPO in the Active Directory
                GPOTypeLocalUser, // User-specific GPO on the local machine
                GPOTypeLocalGroup // Group-specific GPO on the local machine
            }

            #region COM

            /// <summary>
            /// Group Policy Interface definition from COM.
            /// You can find the Guid in <Gpedit.h>
            /// </summary>
            [Guid("EA502723-A23D-11d1-A7D3-0000F87571E3"),
            InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
            interface IGroupPolicyObject
            {
                void New(
                [MarshalAs(UnmanagedType.LPWStr)] String pszDomainName,
                [MarshalAs(UnmanagedType.LPWStr)] String pszDisplayName,
                UInt32 dwFlags);

                void OpenDSGPO(
                    [MarshalAs(UnmanagedType.LPWStr)] String pszPath,
                    UInt32 dwFlags);

                void OpenLocalMachineGPO(UInt32 dwFlags);

                void OpenRemoteMachineGPO(
                    [MarshalAs(UnmanagedType.LPWStr)] String pszComputerName,
                    UInt32 dwFlags);

                void Save(
                    [MarshalAs(UnmanagedType.Bool)] bool bMachine,
                    [MarshalAs(UnmanagedType.Bool)] bool bAdd,
                    [MarshalAs(UnmanagedType.LPStruct)] Guid pGuidExtension,
                    [MarshalAs(UnmanagedType.LPStruct)] Guid pGuid);

                void Delete();

                void GetName(
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName,
                    Int32 cchMaxLength);

                void GetDisplayName(
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName,
                    Int32 cchMaxLength);

                void SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] String pszName);

                void GetPath(
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPath,
                    Int32 cchMaxPath);

                void GetDSPath(
                    UInt32 dwSection,
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPath,
                    Int32 cchMaxPath);

                void GetFileSysPath(
                    UInt32 dwSection,
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPath,
                    Int32 cchMaxPath);

                UInt32 GetRegistryKey(UInt32 dwSection);

                Int32 GetOptions();

                void SetOptions(UInt32 dwOptions, UInt32 dwMask);

                void GetType(out GROUP_POLICY_OBJECT_TYPE gpoType);

                void GetMachineName(
                    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName,
                    Int32 cchMaxLength);

                UInt32 GetPropertySheetPages(out IntPtr hPages);
            }

            /// <summary>
            /// Group Policy Class definition from COM.
            /// You can find the Guid in <Gpedit.h>
            /// </summary>
            [ComImport, Guid("EA502722-A23D-11d1-A7D3-0000F87571E3")]
            class GroupPolicyObject { }

            #endregion

            #region WinAPI You can find definition of API for C# on: http://pinvoke.net/

            /// <summary>
            /// Opens the specified registry key. Note that key names are not case sensitive.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724897(VS.85).aspx for more info about the parameters.<br/>
            [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
            public static extern Int32 RegOpenKeyEx(
            UIntPtr hKey,
            String subKey,
            Int32 ulOptions,
            RegSAM samDesired,
            out UIntPtr hkResult);

            /// <summary>
            /// Retrieves the type and data for the specified value name associated with an open registry key.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724911(VS.85).aspx for more info about the parameters and return value.<br/>
            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "RegQueryValueExW", SetLastError = true)]
            static extern Int32 RegQueryValueEx(
            UIntPtr hKey,
            String lpValueName,
            Int32 lpReserved,
            out UInt32 lpType,
            [Out] byte[] lpData,
            ref UInt32 lpcbData);

            /// <summary>
            /// Sets the data and type of a specified value under a registry key.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724923(VS.85).aspx for more info about the parameters and return value.<br/>
            [DllImport("advapi32.dll", SetLastError = true)]
            static extern Int32 RegSetValueEx(
            UInt32 hKey,
            [MarshalAs(UnmanagedType.LPStr)] String lpValueName,
            Int32 Reserved,
            Microsoft.Win32.RegistryValueKind dwType,
            IntPtr lpData,
            Int32 cbData);

            /// <summary>
            /// Creates the specified registry key. If the key already exists, the function opens it. Note that key names are not case sensitive.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724844(v=VS.85).aspx for more info about the parameters and return value.<br/>
            [DllImport("advapi32.dll", SetLastError = true)]
            static extern Int32 RegCreateKeyEx(
            UInt32 hKey,
            String lpSubKey,
            UInt32 Reserved,
            String lpClass,
            RegOption dwOptions,
            RegSAM samDesired,
            IntPtr lpSecurityAttributes,
            out UInt32 phkResult,
            out RegResult lpdwDisposition);

            /// <summary>
            /// Closes a handle to the specified registry key.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724837(VS.85).aspx for more info about the parameters and return value.<br/>
            [DllImport("advapi32.dll", SetLastError = true)]
            static extern Int32 RegCloseKey(
            UInt32 hKey);

            /// <summary>
            /// Deletes a subkey and its values from the specified platform-specific view of the registry. Note that key names are not case sensitive.
            /// </summary>
            /// See http://msdn.microsoft.com/en-us/library/ms724847(VS.85).aspx for more info about the parameters and return value.<br/>
            [DllImport("advapi32.dll", EntryPoint = "RegDeleteKeyEx", SetLastError = true)]
            public static extern Int32 RegDeleteKeyEx(
            UInt32 hKey,
            String lpSubKey,
            RegSAM samDesired,
            UInt32 Reserved);

            #endregion

            /// <summary>
            /// Registry creating volatile check.
            /// </summary>
            [Flags]
            public enum RegOption
            {
                NonVolatile = 0x0,
                Volatile = 0x1,
                CreateLink = 0x2,
                BackupRestore = 0x4,
                OpenLink = 0x8
            }

            /// <summary>
            /// Access mask the specifies the platform-specific view of the registry.
            /// </summary>
            [Flags]
            public enum RegSAM
            {
                QueryValue = 0x00000001,
                SetValue = 0x00000002,
                CreateSubKey = 0x00000004,
                EnumerateSubKeys = 0x00000008,
                Notify = 0x00000010,
                CreateLink = 0x00000020,
                WOW64_32Key = 0x00000200,
                WOW64_64Key = 0x00000100,
                WOW64_Res = 0x00000300,
                Read = 0x00020019,
                Write = 0x00020006,
                Execute = 0x00020019,
                AllAccess = 0x000f003f
            }

            /// <summary>
            /// Structure for security attributes.
            /// </summary>
            [StructLayout(LayoutKind.Sequential)]
            public struct SECURITY_ATTRIBUTES
            {
                public Int32 nLength;
                public IntPtr lpSecurityDescriptor;
                public Int32 bInheritHandle;
            }

            /// <summary>
            /// Flag returned by calling RegCreateKeyEx.
            /// </summary>
            public enum RegResult
            {
                CreatedNewKey = 0x00000001,
                OpenedExistingKey = 0x00000002
            }

            /// <summary>
            /// Class to create an object to handle the group policy operation.
            /// </summary>
            public class GroupPolicyObjectHandler
            {
                public const Int32 REG_NONE = 0;
                public const Int32 REG_SZ = 1;
                public const Int32 REG_EXPAND_SZ = 2;
                public const Int32 REG_BINARY = 3;
                public const Int32 REG_DWORD = 4;
                public const Int32 REG_DWORD_BIG_ENDIAN = 5;
                public const Int32 REG_MULTI_SZ = 7;
                public const Int32 REG_QWORD = 11;

                // Group Policy interface handler
                IGroupPolicyObject iGroupPolicyObject;
                // Group Policy object handler.
                GroupPolicyObject groupPolicyObject;

                #region constructor

                /// <summary>
                /// Constructor.
                /// </summary>
                /// <param name="remoteMachineName">Target machine name to operate group policy</param>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public GroupPolicyObjectHandler(String remoteMachineName)
                {
                    groupPolicyObject = new GroupPolicyObject();
                    iGroupPolicyObject = (IGroupPolicyObject)groupPolicyObject;
                    try
                    {
                        if (String.IsNullOrEmpty(remoteMachineName))
                        {
                            iGroupPolicyObject.OpenLocalMachineGPO(GPO_OPEN_LOAD_REGISTRY);
                        }
                        else
                        {
                            iGroupPolicyObject.OpenRemoteMachineGPO(remoteMachineName, GPO_OPEN_LOAD_REGISTRY);
                        }
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                }

                #endregion

                #region interface related methods

                /// <summary>
                /// Retrieves the display name for the GPO.
                /// </summary>
                /// <returns>Display name</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public String GetDisplayName()
                {
                    StringBuilder pszName = new StringBuilder(Byte.MaxValue);
                    try
                    {
                        iGroupPolicyObject.GetDisplayName(pszName, Byte.MaxValue);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                    return pszName.ToString();
                }

                /// <summary>
                /// Retrieves the computer name of the remote GPO.
                /// </summary>
                /// <returns>Machine name</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public String GetMachineName()
                {
                    StringBuilder pszName = new StringBuilder(Byte.MaxValue);
                    try
                    {
                        iGroupPolicyObject.GetMachineName(pszName, Byte.MaxValue);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                    return pszName.ToString();
                }

                /// <summary>
                /// Retrieves the options for the GPO.
                /// </summary>
                /// <returns>Options flag</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public Int32 GetOptions()
                {
                    try
                    {
                        return iGroupPolicyObject.GetOptions();
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                }

                /// <summary>
                /// Retrieves the path to the GPO.
                /// </summary>
                /// <returns>The path to the GPO</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public String GetPath()
                {
                    StringBuilder pszName = new StringBuilder(Byte.MaxValue);
                    try
                    {
                        iGroupPolicyObject.GetPath(pszName, Byte.MaxValue);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                    return pszName.ToString();
                }

                /// <summary>
                /// Retrieves a handle to the root of the registry key for the machine section.
                /// </summary>
                /// <returns>A handle to the root of the registry key for the specified GPO computer section</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public UInt32 GetMachineRegistryKey()
                {
                    UInt32 handle;
                    try
                    {
                        handle = iGroupPolicyObject.GetRegistryKey(GPO_OPTION_DISABLE_MACHINE);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                    return handle;
                }

                /// <summary>
                /// Retrieves a handle to the root of the registry key for the user section.
                /// </summary>
                /// <returns>A handle to the root of the registry key for the specified GPO user section</returns>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public UInt32 GetUserRegistryKey()
                {
                    UInt32 handle;
                    try
                    {
                        handle = iGroupPolicyObject.GetRegistryKey(GPO_OPTION_DISABLE_USER);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                    return handle;
                }

                /// <summary>
                /// Saves the specified registry policy settings to disk and updates the revision number of the GPO.
                /// </summary>
                /// <param name="isMachine">Specifies the registry policy settings to be saved. If this parameter is TRUE, the computer policy settings are saved. Otherwise, the user policy settings are saved.</param>
                /// <param name="isAdd">Specifies whether this is an add or delete operation. If this parameter is FALSE, the last policy setting for the specified extension pGuidExtension is removed. In all other cases, this parameter is TRUE.</param>
                /// <exception cref="System.Runtime.InteropServices.COMException">Throw when com execution throws exceptions</exception>
                public void Save(bool isMachine, bool isAdd)
                {
                    try
                    {
                        iGroupPolicyObject.Save(isMachine, isAdd, REGISTRY_EXTENSION_GUID, CLSID_GPESnapIn);
                    }
                    catch (COMException e)
                    {
                        throw e;
                    }
                }

                #endregion

                #region customized methods

                /// <summary>
                /// Set the group policy value.
                /// </summary>
                /// <param name="isMachine">Specifies the registry policy settings to be saved. If this parameter is TRUE, the computer policy settings are saved. Otherwise, the user policy settings are saved.</param>
                /// <param name="subKey">Group policy config full path</param>
                /// <param name="valueName">Group policy config key name</param>
                /// <param name="value">If value is null, it will envoke the delete method</param>
                /// <returns>Whether the config is successfully set</returns>
                public ResultCode SetGroupPolicy(bool isMachine, String subKey, String valueName, object value)
                {
                    UInt32 gphKey = (isMachine) ? GetMachineRegistryKey() : GetUserRegistryKey();
                    UInt32 gphSubKey;
                    UIntPtr hKey;
                    RegResult flag;

                    if (null == value)
                    {
                        // check the key's existance
                        if (RegOpenKeyEx((UIntPtr)gphKey, subKey, 0, RegSAM.QueryValue, out hKey) == 0)
                        {
                            RegCloseKey((UInt32)hKey);
                            // delete the GPO
                            Int32 hr = RegDeleteKeyEx(
                            gphKey,
                            subKey,
                            RegSAM.Write,
                            0);
                            if (0 != hr)
                            {
                                RegCloseKey(gphKey);
                                return ResultCode.CreateOrOpenFailed;
                            }
                            Save(isMachine, false);
                        }
                        else
                        {
                            // not exist
                        }

                    }
                    else
                    {
                        // set the GPO
                        Int32 hr = RegCreateKeyEx(
                        gphKey,
                        subKey,
                        0,
                        null,
                        RegOption.NonVolatile,
                        RegSAM.Write,
                        IntPtr.Zero,
                        out gphSubKey,
                        out flag);
                        if (0 != hr)
                        {
                            RegCloseKey(gphSubKey);
                            RegCloseKey(gphKey);
                            return ResultCode.CreateOrOpenFailed;
                        }

                        Int32 cbData = 4;
                        IntPtr keyValue = IntPtr.Zero;

                        if (value.GetType() == typeof(Int32))
                        {
                            keyValue = Marshal.AllocHGlobal(cbData);
                            Marshal.WriteInt32(keyValue, (Int32)value);
                            hr = RegSetValueEx(gphSubKey, valueName, 0, RegistryValueKind.DWord, keyValue, cbData);
                        }
                        else if (value.GetType() == typeof(String))
                        {
                            keyValue = Marshal.StringToHGlobalAnsi(value.ToString());
                            cbData = System.Text.Encoding.UTF8.GetByteCount(value.ToString()) + 1;
                            hr = RegSetValueEx(gphSubKey, valueName, 0, RegistryValueKind.String, keyValue, cbData);
                        }
                        else
                        {
                            RegCloseKey(gphSubKey);
                            RegCloseKey(gphKey);
                            return ResultCode.SetFailed;
                        }

                        if (0 != hr)
                        {
                            RegCloseKey(gphSubKey);
                            RegCloseKey(gphKey);
                            return ResultCode.SetFailed;
                        }
                        try
                        {
                            Save(isMachine, true);
                        }
                        catch (COMException e)
                        {
                            RegCloseKey(gphSubKey);
                            RegCloseKey(gphKey);
                            return ResultCode.SaveFailed;
                        }
                        RegCloseKey(gphSubKey);
                        RegCloseKey(gphKey);
                    }

                    return ResultCode.Succeed;
                }

                /// <summary>
                /// Get the config of the group policy.
                /// </summary>
                /// <param name="isMachine">Specifies the registry policy settings to be saved. If this parameter is TRUE, get from the computer policy settings. Otherwise, get from the user policy settings.</param>
                /// <param name="subKey">Group policy config full path</param>
                /// <param name="valueName">Group policy config key name</param>
                /// <returns>The setting of the specified config</returns>
                public object GetGroupPolicy(bool isMachine, String subKey, String valueName)
                {
                    UIntPtr gphKey = (UIntPtr)((isMachine) ? GetMachineRegistryKey() : GetUserRegistryKey());
                    UIntPtr hKey;
                    object keyValue = null;
                    UInt32 size = 1;

                    if (RegOpenKeyEx(gphKey, subKey, 0, RegSAM.QueryValue, out hKey) == 0)
                    {
                        UInt32 type;
                        byte[] data = new byte[size]; // to store retrieved the value's data

                        if (RegQueryValueEx(hKey, valueName, 0, out type, data, ref size) == 234)
                        {
                            //size retreived
                            data = new byte[size]; //redefine data
                        }

                        if (RegQueryValueEx(hKey, valueName, 0, out type, data, ref size) != 0)
                        {
                            return null;
                        }

                        switch (type)
                        {
                            case REG_NONE:
                            case REG_BINARY:
                                keyValue = data;
                                break;
                            case REG_DWORD:
                                keyValue = (((data[0] | (data[1] << 8)) | (data[2] << 16)) | (data[3] << 24));
                                break;
                            case REG_DWORD_BIG_ENDIAN:
                                keyValue = (((data[3] | (data[2] << 8)) | (data[1] << 16)) | (data[0] << 24));
                                break;
                            case REG_QWORD:
                                {
                                    UInt32 numLow = (UInt32)(((data[0] | (data[1] << 8)) | (data[2] << 16)) | (data[3] << 24));
                                    UInt32 numHigh = (UInt32)(((data[4] | (data[5] << 8)) | (data[6] << 16)) | (data[7] << 24));
                                    keyValue = (long)(((ulong)numHigh << 32) | (ulong)numLow);
                                    break;
                                }
                            case REG_SZ:
                                var s = Encoding.Unicode.GetString(data, 0, (Int32)size);
                                keyValue = s.Substring(0, s.Length - 1);
                                break;
                            case REG_EXPAND_SZ:
                                keyValue = Environment.ExpandEnvironmentVariables(Encoding.Unicode.GetString(data, 0, (Int32)size));
                                break;
                            case REG_MULTI_SZ:
                                {
                                    List<string> strings = new List<String>();
                                    String packed = Encoding.Unicode.GetString(data, 0, (Int32)size);
                                    Int32 start = 0;
                                    Int32 end = packed.IndexOf("", start);
                                    while (end > start)
                                    {
                                        strings.Add(packed.Substring(start, end - start));
                                        start = end + 1;
                                        end = packed.IndexOf("", start);
                                    }
                                    keyValue = strings.ToArray();
                                    break;
                                }
                            default:
                                throw new NotSupportedException();
                        }

                        RegCloseKey((UInt32)hKey);
                    }

                    return keyValue;
                }

                #endregion

            }
        }

        public class Helper
        {
            private static object _returnValueFromSet, _returnValueFromGet;

            /// <summary>
            /// Set policy config
            /// It will start a single thread to set group policy.
            /// </summary>
            /// <param name="isMachine">Whether is machine config</param>
            /// <param name="configFullPath">The full path configuration</param>
            /// <param name="configKey">The configureation key name</param>
            /// <param name="value">The value to set, boxed with proper type [ String, Int32 ]</param>
            /// <returns>Whether the config is successfully set</returns>
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            public static ResultCode SetGroupPolicy(bool isMachine, String configFullPath, String configKey, object value)
            {
                Thread worker = new Thread(SetGroupPolicy);
                worker.SetApartmentState(ApartmentState.STA);
                worker.Start(new object[] { isMachine, configFullPath, configKey, value });
                worker.Join();
                return (ResultCode)_returnValueFromSet;
            }

            /// <summary>
            /// Thread start for seting group policy.
            /// Called by public static ResultCode SetGroupPolicy(bool isMachine, WinRMGPConfigName configName, object value)
            /// </summary>
            /// <param name="values">
            /// values[0] - isMachine<br/>
            /// values[1] - configFullPath<br/>
            /// values[2] - configKey<br/>
            /// values[3] - value<br/>
            /// </param>
            private static void SetGroupPolicy(object values)
            {
                object[] valueList = (object[])values;
                bool isMachine = (bool)valueList[0];
                String configFullPath = (String)valueList[1];
                String configKey = (String)valueList[2];
                object value = valueList[3];

                WinAPIForGroupPolicy.GroupPolicyObjectHandler gpHandler = new WinAPIForGroupPolicy.GroupPolicyObjectHandler(null);

                _returnValueFromSet = gpHandler.SetGroupPolicy(isMachine, configFullPath, configKey, value);
            }

            /// <summary>
            /// Get policy config.
            /// It will start a single thread to get group policy
            /// </summary>
            /// <param name="isMachine">Whether is machine config</param>
            /// <param name="configFullPath">The full path configuration</param>
            /// <param name="configKey">The configureation key name</param>
            /// <returns>The group policy setting</returns>
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            public static object GetGroupPolicy(bool isMachine, String configFullPath, String configKey)
            {
                Thread worker = new Thread(GetGroupPolicy);
                worker.SetApartmentState(ApartmentState.STA);
                worker.Start(new object[] { isMachine, configFullPath, configKey });
                worker.Join();
                return _returnValueFromGet;
            }

            /// <summary>
            /// Thread start for geting group policy.
            /// Called by public static object GetGroupPolicy(bool isMachine, WinRMGPConfigName configName)
            /// </summary>
            /// <param name="values">
            /// values[0] - isMachine<br/>
            /// values[1] - configFullPath<br/>
            /// values[2] - configKey<br/>
            /// </param>
            public static void GetGroupPolicy(object values)
            {
                object[] valueList = (object[])values;
                bool isMachine = (bool)valueList[0];
                String configFullPath = (String)valueList[1];
                String configKey = (String)valueList[2];

                WinAPIForGroupPolicy.GroupPolicyObjectHandler gpHandler = new WinAPIForGroupPolicy.GroupPolicyObjectHandler(null);

                _returnValueFromGet = gpHandler.GetGroupPolicy(isMachine, configFullPath, configKey);
            }
        }
    }
'@

#endregion .net Types

try
{
    [Pki.Period]$temp = $null
}
catch
{
    Add-Type -TypeDefinition $pkiInternalsTypes
}

try
{
    [GPO.Helper]$temp = $null
}
catch
{
    Add-Type -TypeDefinition $gpoType -IgnoreWarnings
}

try
{
    [System.Security.Cryptography.X509Certificates.Win32]$temp = $null
}
catch
{
    Add-Type -TypeDefinition $certStoreTypes
}


$adInstallRootDcScriptPre2012 = {
    param (
        [string]$DomainName,
        [string]$Password,
        [string]$ForestFunctionalLevel,
        [string]$DomainFunctionalLevel,
        [string]$NetBiosDomainName,
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    $dcpromoAnswerFile = @"
      [DCInstall]
      ; New forest promotion
      ReplicaOrNewDomain=Domain
      NewDomain=Forest
      NewDomainDNSName=$DomainName
      ForestLevel=$($ForestFunctionalLevel)
      DomainNetbiosName=$($NetBiosDomainName)
      DomainLevel=$($DomainFunctionalLevel)
      InstallDNS=Yes
      ConfirmGc=Yes
      CreateDNSDelegation=No
      DatabasePath=$DatabasePath
      LogPath=$LogPath
      SYSVOLPath=$SysvolPath
      ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
      SafeModeAdminPassword=$DsrmPassword
      ; Run-time flags (optional)
      ;RebootOnCompletion=No
"@


    $VerbosePreference = $using:VerbosePreference

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    Import-Module -Name ServerManager
    Add-WindowsFeature -Name DNS
    $result = Add-WindowsFeature -Name AD-Domain-Services
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($DomainName) | Out-Null

    $dcpromoAnswerFile | Out-File -FilePath C:\DcpromoAnswerFile.txt -Force

    dcpromo /unattend:'C:\DcpromoAnswerFile.txt'

    if ($LASTEXITCODE -ge 11)
    {
        throw 'Could not install new domain'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord

    Write-Verbose -Message 'finished installing the Root Domain Controller'
}

$adInstallRootDcScript2012 = {
    param (
        [string]$DomainName,
        [string]$Password,
        [string]$ForestFunctionalLevel,
        [string]$DomainFunctionalLevel,
        [string]$NetBiosDomainName,
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    $VerbosePreference = $using:VerbosePreference

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($DomainName) | Out-Null

    Write-Verbose -Message "Starting installation of Root Domain Controller on '$(HOSTNAME.EXE)'"

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    $result = Install-WindowsFeature AD-Domain-Services, DNS -IncludeManagementTools
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    $safeDsrmPassword = ConvertTo-SecureString -String $DsrmPassword -AsPlainText -Force

    Write-Verbose -Message "Creating a new forest named '$DomainName' on the machine '$(HOSTNAME.EXE)'"
    $result = Install-ADDSForest -DomainName $DomainName `
    -SafeModeAdministratorPassword $safeDsrmPassword `
    -InstallDNS `
    -DomainMode $DomainFunctionalLevel `
    -Force `
    -ForestMode $ForestFunctionalLevel `
    -DomainNetbiosName $NetBiosDomainName `
    -SysvolPath $SysvolPath `
    -DatabasePath $DatabasePath `
    -LogPath $LogPath

    if ($result.Status -eq 'Error')
    {
        throw 'Could not install new domain'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord

    Write-Verbose -Message 'finished installing the Root Domain Controller'
}

$adInstallFirstChildDc2012 = {
    param (
        [string]$NewDomainName,
        [string]$ParentDomainName,
        [System.Management.Automation.PSCredential]$RootDomainCredential,
        [string]$DomainMode,
        [int]$Retries,
        [int]$SecondsBetweenRetries,
        [string]$SiteName = 'Default-First-Site-Name',
        [string]$NetBiosDomainName,
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    $VerbosePreference = $using:VerbosePreference

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($DomainName) | Out-Null

    Write-Verbose -Message "Starting installation of First Child Domain Controller of domain '$NewDomainName' on '$(HOSTNAME.EXE)'"
    Write-Verbose -Message "NewDomainName is '$NewDomainName'"
    Write-Verbose -Message "ParentDomainName is '$ParentDomainName'"
    Write-Verbose -Message "RootCredential UserName is '$($RootDomainCredential.UserName)'"
    Write-Verbose -Message "RootCredential Password is '$($RootDomainCredential.GetNetworkCredential().Password)'"
    Write-Verbose -Message "DomainMode is '$DomainMode'"

    Write-Verbose -Message "Trying to reach domain $ParentDomainName"
    while (-not $result -and $count -lt 15)
    {
        $result = Test-Connection -ComputerName $ParentDomainName -Count 1 -Quiet

        if ($result)
        {
            Write-Verbose -Message "Domain $ParentDomainName was reachable ($count)"
        }
        else
        {
            Write-ScreenInfo "Domain $ParentDomainName was not reachable ($count)" -Type Warning
        }

        Start-Sleep -Seconds 1

        Clear-DnsClientCache

        $count++
    }
    if (-not $result)
    {
        Write-Error "The domain '$ParentDomainName' could not be contacted. Trying DC promotion anyway"
    }
    else
    {
        Write-Verbose -Message "The domain '$ParentDomainName' could be reached"
    }

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    $result = Install-Windowsfeature AD-Domain-Services, DNS -IncludeManagementTools
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    $retriesDone = 0
    do
    {
        Write-Verbose "The first try to promote '$(HOSTNAME.EXE)' did not work. The error was '$($result.Message)'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." -Type Warning
        ipconfig.exe /flushdns | Out-Null

        try
        {
            #if there is a '.' inside the domain name, it is a new domain tree, otherwise a child domain
            if ($NewDomainName.Contains('.'))
            {
                if (-not $NetBiosDomainName)
                {
                    $NetBiosDomainName = $NewDomainName.Substring(0, $NewDomainName.IndexOf('.'))
                }
                $domainType = 'TreeDomain'
                $createDNSDelegation = $false
            }
            else
            {
                if (-not $NetBiosDomainName)
                {
                    $newDomainNetBiosName = $NewDomainName.ToUpper()
                }
                $domainType = 'ChildDomain'
                $createDNSDelegation = $true
            }

            Start-Sleep -Seconds $SecondsBetweenRetries

            $safeDsrmPassword = ConvertTo-SecureString -String $DsrmPassword -AsPlainText -Force

            $result = Install-ADDSDomain -NewDomainName $NewDomainName `
            -NewDomainNetbiosName $NetBiosDomainName `
            -ParentDomainName $ParentDomainName `
            -SiteName $SiteName `
            -InstallDNS `
            -CreateDnsDelegation:$createDNSDelegation `
            -SafeModeAdministratorPassword $safeDsrmPassword `
            -Force `
            -Credential $RootDomainCredential `
            -DomainType $domainType `
            -DomainMode $DomainMode `
            -SysvolPath $SysvolPath `
            -DatabasePath $DatabasePath `
            -LogPath $LogPath
        }
        catch
        {
            Start-Sleep -Seconds $SecondsBetweenRetries
        }

        $retriesDone++
    }
    until ($result.Status -ne 'Error' -or $retriesDone -ge $Retries)

    if ($result.Status -eq 'Error')
    {
        Write-Error "Could not install new domain '$NewDomainName' on computer '$(HOSTNAME.EXE)' in $Retries retries. Aborting the promotion of '$(HOSTNAME.EXE)'"
        return
    }
    else
    {
        Write-Verbose -Message "Active Directory installed successfully on computer '$(HOSTNAME.EXE)'"
    }

    Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord

    Write-Verbose -Message 'Finished installing the first child Domain Controller'
}

$adInstallFirstChildDcPre2012 = {
    param (
        [string]$NewDomainName,
        [string]$ParentDomainName,
        [System.Management.Automation.PSCredential]$RootDomainCredential,
        [string]$DomainMode,
        [int]$Retries,
        [int]$SecondsBetweenRetries,
        [string]$SiteName = 'Default-First-Site-Name',
        [string]$NetBiosDomainName,
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    Import-Module -Name ServerManager
    Add-WindowsFeature -Name DNS
    $result = Add-WindowsFeature -Name AD-Domain-Services
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($ParentDomainName) | Out-Null

    Write-Verbose -Message "Starting installation of First Child Domain Controller of domain '$NewDomainName' on '$(HOSTNAME.EXE)'"
    Write-Verbose -Message "NewDomainName is '$NewDomainName'"
    Write-Verbose -Message "ParentDomainName is '$ParentDomainName'"
    Write-Verbose -Message "RootCredential UserName is '$($RootDomainCredential.UserName)'"
    Write-Verbose -Message "RootCredential Password is '$($RootDomainCredential.GetNetworkCredential().Password)'"
    Write-Verbose -Message "DomainMode is '$DomainMode'"

    Write-Verbose -Message "Starting installation of First Child Domain Controller of domain '$NewDomainName' on '$(HOSTNAME.EXE)'"

    Write-Verbose -Message "Trying to reach domain $ParentDomainName"
    while (-not $result -and $count -lt 15)
    {
        $result = Test-Connection -ComputerName $ParentDomainName -Count 1 -Quiet

        if ($result)
        {
            Write-Verbose -Message "Domain $ParentDomainName was reachable ($count)"
        }
        else
        {
            Write-ScreenInfo "Domain $ParentDomainName was not reachable ($count)" -Type Warning
        }

        Start-Sleep -Seconds 1

        ipconfig.exe /flushdns | Out-Null

        $count++
    }
    if (-not $result)
    {
        Write-Error "The domain $ParentDomainName could not be contacted. Trying the DCPromo anyway"
    }
    else
    {
        Write-Verbose -Message "The domain $ParentDomainName could be reached"
    }

    Write-Verbose -Message "Credentials prepared for user $logonName"

    $tempName = $NewDomainName #using seems not to work in a if statement
    if ($tempName.Contains('.'))
    {
        $domainType = 'Tree'
        if (-not $NetBiosDomainName)
        {
            $NetBiosDomainName = $NewDomainName.Substring(0, $NewDomainName.IndexOf('.')).ToUpper()
        }
    }
    else
    {
        $domainType = 'Child'
        if (-not $NetBiosDomainName)
        {
            $NetBiosDomainName = $NewDomainName.ToUpper()
        }
    }

    $dcpromoAnswerFile = @"
      [DCInstall]
      ; New child domain promotion
      ReplicaOrNewDomain=Domain
      NewDomain=$domainType
      ParentDomainDNSName=$($ParentDomainName)
      NewDomainDNSName=$($NetBiosDomainName)
      ChildName=$($NewDomainName)
      DomainNetbiosName=$($NetBiosDomainName)
      DomainLevel=$($DomainMode)
      SiteName=$($SiteName)
      InstallDNS=Yes
      ConfirmGc=Yes
      UserDomain=$($RootDomainCredential.UserName.Split('\')[0])
      UserName=$($RootDomainCredential.UserName.Split('\')[1])
      Password=$($RootDomainCredential.GetNetworkCredential().Password)
      DatabasePath=$DatabasePath
      LogPath=$LogPath
      SYSVOLPath=$SysvolPath
      ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
      SafeModeAdminPassword=$DsrmPassword
      ; Run-time flags (optional)
      ; RebootOnCompletion=No
"@


    if ($domainType -eq 'Child')
    {
        $dcpromoAnswerFile += ("
                CreateDNSDelegation=Yes
                DNSDelegationUserName=$($RootDomainCredential.UserName)
        DNSDelegationPassword=$($RootDomainCredential.GetNetworkCredential().Password)"
)
    }
    else
    {
        $dcpromoAnswerFile += ('
        CreateDNSDelegation=No'
)
    }

    $dcpromoAnswerFile | Out-File -FilePath C:\DcpromoAnswerFile.txt -Force
    Copy-Item -Path C:\DcpromoAnswerFile.txt -Destination C:\DcpromoAnswerFileBackup.txt

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    Write-Verbose -Message "Promoting machine '$(HOSTNAME.EXE)' to domain $($NewDomainName)"
    dcpromo /unattend:'C:\DcpromoAnswerFile.txt'

    $retriesDone = 0
    while ($LASTEXITCODE -ge 11 -and $retriesDone -lt $Retries)
    {
        Write-ScreenInfo "Promoting the Domain Controller '$(HOSTNAME.EXE)' did not work. The error code was '$LASTEXITCODE'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." -Type Warning
        ipconfig.exe /flushdns | Out-Null

        Start-Sleep -Seconds $SecondsBetweenRetries

        Copy-Item -Path C:\DcpromoAnswerFileBackup.txt -Destination C:\DcpromoAnswerFile.txt
        dcpromo /unattend:'C:\DcpromoAnswerFile.txt'
        Write-Verbose -Message "Return code of DCPromo was '$LASTEXITCODE'"

        $retriesDone++
    }

    if ($LASTEXITCODE -ge 11)
    {
        Write-Error "Could not install new domain '$NewDomainName' on computer '$(HOSTNAME.EXE)' in $Retries retries. Aborting the promotion of '$(HOSTNAME.EXE)'"
        return
    }
    else
    {
        Write-Verbose -Message "AD-Domain-Services windows feature installed successfully on computer '$(HOSTNAME.EXE)'"
    }

    Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters `
    -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord -ErrorAction Stop

    Write-Verbose -Message 'finished installing the the first child Domain Controller'
}

$adInstallDc2012 = {
    param (
        [string]$DomainName,
        [System.Management.Automation.PSCredential]$RootDomainCredential,
        [bool]$IsReadOnly,
        [int]$Retries,
        [int]$SecondsBetweenRetries,
        [string]$SiteName = 'Default-First-Site-Name',
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    $VerbosePreference = $using:VerbosePreference

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($DomainName) | Out-Null

    Write-Verbose -Message "Starting installation of an additional Domain Controller on '$(HOSTNAME.EXE)'"
    Write-Verbose -Message "DomainName is '$DomainName'"
    Write-Verbose -Message "RootCredential UserName is '$($RootDomainCredential.UserName)'"
    Write-Verbose -Message "RootCredential Password is '$($RootDomainCredential.GetNetworkCredential().Password)'"

    #The random delay is very important when promoting more than one Domain Controller.
    Start-Sleep -Seconds (Get-Random -Minimum 60 -Maximum 180)

    Write-Verbose -Message "Trying to reach domain $DomainName"
    $count = 0
    while (-not $result -and $count -lt 15)
    {
        Clear-DnsClientCache

        $result = Test-Connection -ComputerName $DomainName -Count 1 -Quiet

        if ($result)
        {
            Write-Verbose -Message "Domain $DomainName was reachable ($count)"
        }
        else
        {
            Write-ScreenInfo "Domain $DomainName was not reachable ($count)" -Type Warning
        }

        Start-Sleep -Seconds 1

        $count++
    }
    if (-not $result)
    {
        Write-Error "The domain '$DomainName' could not be contacted. Trying DC promotion anyway"
    }
    else
    {
        Write-Verbose -Message "The domain '$DomainName' could be reached"
    }

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    $result = Install-WindowsFeature AD-Domain-Services, DNS -IncludeManagementTools
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    $safeDsrmPassword = ConvertTo-SecureString -String $DsrmPassword -AsPlainText -Force

    Write-Verbose -Message "Promoting machine '$(HOSTNAME.EXE)' to domain '$DomainName'"

    #this is required for RODCs
    $expectedNetbionDomainName = ($DomainName -split '\.')[0]

    $param = @{
        DomainName = $DomainName
        SiteName = $SiteName
        SafeModeAdministratorPassword = $safeDsrmPassword
        Force = $true
        Credential = $RootDomainCredential
        SysvolPath = $SysvolPath
        DatabasePath = $DatabasePath
        LogPath = $LogPath
    }


    if ($IsReadOnly)
    {
        $param.Add('ReadOnlyReplica', $true)

        $param.Add('DenyPasswordReplicationAccountName',
            @('BUILTIN\Administrators',
                'BUILTIN\Server Operators',
                'BUILTIN\Backup Operators',
                'BUILTIN\Account Operators',
        "$expectedNetbionDomainName\Denied RODC Password Replication Group"))

        $param.Add('AllowPasswordReplicationAccountName', @("$expectedNetbionDomainName\Allowed RODC Password Replication Group"))
    }
    else
    {
        $param.Add('CreateDnsDelegation', $false)
    }

    try
    {
        $result = Install-ADDSDomainController @param
    }
    catch
    {
        Write-Error -Message 'Error occured in installation of Domain Controller. Error:'
        Write-Error -Message $_
    }

    Write-Verbose -Message 'First attempt of installation finished'
    $retriesDone = 0
    while ($result.Status -eq 'Error' -and $retriesDone -lt $Retries)
    {
        Write-ScreenInfo "The first try to promote '$(HOSTNAME.EXE)' did not work. The error was '$($result.Message)'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." -Type Warning
        ipconfig.exe /flushdns | Out-Null

        Start-Sleep -Seconds $SecondsBetweenRetries
        try
        {
            $result = Install-ADDSDomainController @param
        }
        catch { }

        $retriesDone++
    }

    if ($result.Status -eq 'Error')
    {
        Write-Error "The problem could not be solved in $Retries retries. Aborting the promotion of '$(HOSTNAME.EXE)'"
        return
    }

    Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord

    Write-Verbose -Message 'finished installing the Root Domain Controller'
}

$adInstallDcPre2012 = {
    param (
        [string]$DomainName,
        [System.Management.Automation.PSCredential]$RootDomainCredential,
        [bool]$IsReadOnly,
        [int]$Retries,
        [int]$SecondsBetweenRetries,
        [string]$SiteName = 'Default-First-Site-Name',
        [string]$DatabasePath,
        [string]$LogPath,
        [string]$SysvolPath,
        [string]$DsrmPassword
    )

    $VerbosePreference = $using:VerbosePreference

    Start-Transcript -Path C:\DeployDebug\ALDCPromo.log

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    Import-Module -Name ServerManager
    Add-WindowsFeature -Name DNS
    $result = Add-WindowsFeature -Name AD-Domain-Services
    if (-not $result.Success)
    {
        throw 'Could not install AD-Domain-Services windows feature'
    }
    else
    {
        Write-Verbose -Message 'AD-Domain-Services windows feature installed successfully'
    }

    ([WMIClass]'Win32_NetworkAdapterConfiguration').SetDNSSuffixSearchOrder($DomainName) | Out-Null

    Write-Verbose -Message "Starting installation of an additional Domain Controller on '$(HOSTNAME.EXE)'"
    Write-Verbose -Message "DomainName is '$DomainName'"
    Write-Verbose -Message "RootCredential UserName is '$($RootDomainCredential.UserName)'"
    Write-Verbose -Message "RootCredential Password is '$($RootDomainCredential.GetNetworkCredential().Password)'"

    #$type is required for the pre-2012 installatioon
    if ($IsReadOnly)
    {
        $type = 'ReadOnlyReplica'
    }
    else
    {
        $type = 'Replica'
    }

    Start-Sleep -Seconds (Get-Random -Minimum 60 -Maximum 180)

    $dcpromoAnswerFile = @"
      [DCInstall]
      ; Read-Only Replica DC promotion
      ReplicaOrNewDomain=$type
      ReplicaDomainDNSName=$DomainName
      SiteName=$SiteName
      InstallDNS=Yes
      ConfirmGc=Yes
      UserDomain=$($RootDomainCredential.UserName.Split('\')[0])
      UserName=$($RootDomainCredential.UserName.Split('\')[1])
      Password=$($RootDomainCredential.GetNetworkCredential().Password)
      DatabasePath=$DatabasePath
      LogPath=$LogPath
      SYSVOLPath=$SysvolPath
      ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
      SafeModeAdminPassword=$DsrmPassword
      ; RebootOnCompletion=No
"@


    if ($type -eq 'ReadOnlyReplica')
    {
        $dcpromoAnswerFile += ('
                PasswordReplicationDenied="BUILTIN\Administrators"
                PasswordReplicationDenied="BUILTIN\Server Operators"
                PasswordReplicationDenied="BUILTIN\Backup Operators"
                PasswordReplicationDenied="BUILTIN\Account Operators"
                PasswordReplicationDenied="{0}\Denied RODC Password Replication Group"
        PasswordReplicationAllowed="{0}\Allowed RODC Password Replication Group"'
 -f $DomainName)
    }
    else
    {
        $dcpromoAnswerFile += ('
        CreateDNSDelegation=No'
)
    }

    $dcpromoAnswerFile | Out-File -FilePath C:\DcpromoAnswerFile.txt -Force
    #The backup file is required to be able to start dcpromo a second time as the passwords are getting
    #removed by dcpromo
    Copy-Item -Path C:\DcpromoAnswerFile.txt -Destination C:\DcpromoAnswerFileBackup.txt

    #For debug
    Copy-Item -Path C:\DcpromoAnswerFile.txt -Destination C:\DeployDebug\DcpromoAnswerFile.txt

    Write-Verbose -Message "Starting installation of an additional Domain Controller on '$(HOSTNAME.EXE)'"

    Write-Verbose -Message "Trying to reach domain $DomainName"
    $count = 0
    while (-not $result -and $count -lt 15)
    {
        ipconfig.exe /flushdns | Out-Null

        $result = Test-Connection -ComputerName $DomainName -Count 1 -Quiet

        if ($result)
        {
            Write-Verbose -Message "Domain $DomainName was reachable ($count)"
        }
        else
        {
            Write-ScreenInfo "Domain $DomainName was not reachable ($count)" -Type Warning
        }

        Start-Sleep -Seconds 1

        $count++
    }
    if (-not $result)
    {
        Write-Error "The domain $DomainName could not be contacted. Trying the DCPromo anyway"
    }
    else
    {
        Write-Verbose -Message "The domain $DomainName could be reached"
    }

    Write-Verbose -Message 'Installing AD-Domain-Services windows feature'
    Write-Verbose -Message "Promoting machine '$(HOSTNAME.EXE)' to domain '$($DomainName)'"
    Copy-Item -Path C:\DcpromoAnswerFileBackup.txt -Destination C:\DcpromoAnswerFile.txt
    dcpromo /unattend:'C:\DcpromoAnswerFile.txt'
    Write-Verbose -Message "Return code of DCPromo was '$LASTEXITCODE'"

    $retriesDone = 0
    while ($LASTEXITCODE -ge 11 -and $retriesDone -lt $Retries)
    {
        Write-ScreenInfo "The first try to promote '$(HOSTNAME.EXE)' did not work. The error code was '$LASTEXITCODE'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." -Type Warning
        ipconfig.exe /flushdns | Out-Null

        Start-Sleep -Seconds $SecondsBetweenRetries

        Copy-Item -Path C:\DcpromoAnswerFileBackup.txt -Destination C:\DcpromoAnswerFile.txt
        dcpromo /unattend:'C:\DcpromoAnswerFile.txt'
        Write-Verbose -Message "Return code of DCPromo was '$LASTEXITCODE'"

        $retriesDone++
    }

    if ($LASTEXITCODE -ge 11)
    {
        Write-Error "The problem could not be solved in $Retries retries. Aborting the promotion of '$(HOSTNAME.EXE)'"
        return
    }
    else
    {
        Write-Verbose -Message 'finished installing the Domain Controller'

        Set-ItemProperty -Path Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters `
        -Name 'Repl Perform Initial Synchronizations' -Value 0 -Type DWord -ErrorAction Stop
    }
}

[hashtable]$configurationManagerContent = @{
    '[Identification]'           = @{
        Action = 'InstallPrimarySite'
    }          
    '[Options]'                  = @{
        ProductID                 = 'EVAL'
        SiteCode                  = 'AL1'
        SiteName                  = 'AutomatedLab-01'
        SMSInstallDir             = 'C:\Program Files\Microsoft Configuration Manager'
        SDKServer                 = ''
        RoleCommunicationProtocol = 'HTTPorHTTPS'
        ClientsUsePKICertificate  = 0
        PrerequisiteComp          = 1
        PrerequisitePath          = 'C:\Install\CM-Prereqs'
        AdminConsole              = 1
        JoinCEIP                  = 0
    }
           
    '[SQLConfigOptions]'         = @{
        SQLServerName = ''
        DatabaseName  = ''
    }
           
    '[CloudConnectorOptions]'    = @{
        CloudConnector       = 1
        CloudConnectorServer = ''
        UseProxy             = 0
    }
           
    '[SystemCenterOptions]'      = @{}
           
    '[HierarchyExpansionOption]' = @{}
}

$configurationManagerAVExcludedPaths = @(
    'C:\Install'
    'C:\Install\ADK\adksetup.exe'
    'C:\Install\WinPE\adkwinpesetup.exe'
    'C:\InstallCM\SMSSETUP\BIN\X64\setup.exe'
    'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\Binn\sqlservr.exe'
    'C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer\bin\ReportingServicesService.exe'
    'C:\Program Files\Microsoft Configuration Manager'
    'C:\Program Files\Microsoft Configuration Manager\Inboxes'
    'C:\Program Files\Microsoft Configuration Manager\Logs'
    'C:\Program Files\Microsoft Configuration Manager\EasySetupPayload'
    'C:\Program Files\Microsoft Configuration Manager\MP\OUTBOXES'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smsexec.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Sitecomp.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smswriter.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smssqlbkup.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Cmupdate.exe'
    'C:\Program Files\SMS_CCM'
    'C:\Program Files\SMS_CCM\Logs'
    'C:\Program Files\SMS_CCM\ServiceData'
    'C:\Program Files\SMS_CCM\PolReqStaging\POL00000.pol'
    'C:\Program Files\SMS_CCM\ccmexec.exe'
    'C:\Program Files\SMS_CCM\Ccmrepair.exe'
    'C:\Program Files\SMS_CCM\RemCtrl\CmRcService.exe'
    'C:\Windows\CCMSetup'
    'C:\Windows\CCMSetup\ccmsetup.exe'
    'C:\Windows\CCMCache'
)
$configurationManagerAVExcludedProcesses = @(
    'C:\Install\ADK\adksetup.exe'
    'C:\Install\WinPE\adkwinpesetup.exe'
    'C:\Install\CM\SMSSETUP\BIN\X64\setup.exe'
    'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\Binn\sqlservr.exe'
    'C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer\bin\ReportingServicesService.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smsexec.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Sitecomp.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smswriter.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Smssqlbkup.exe'
    'C:\Program Files\Microsoft Configuration Manager\bin\x64\Cmupdate.exe'
    'C:\Program Files\SMS_CCM\ccmexec.exe'
    'C:\Program Files\SMS_CCM\Ccmrepair.exe'
    'C:\Program Files\SMS_CCM\RemCtrl\CmRcService.exe'
    'C:\Windows\CCMSetup\ccmsetup.exe'
)

$iniContentServerScvmm = @{
    UserName                    = 'Administrator'
    CompanyName                 = 'AutomatedLab'
    ProgramFiles                = 'C:\Program Files\Microsoft System Center\Virtual Machine Manager {0}'
    CreateNewSqlDatabase        = '1'
    SqlInstanceName             = 'MSSQLSERVER'
    SqlDatabaseName             = 'VirtualManagerDB'
    RemoteDatabaseImpersonation = '0'
    SqlMachineName              = 'REPLACE'
    IndigoTcpPort               = '8100'
    IndigoHTTPSPort             = '8101'
    IndigoNETTCPPort            = '8102'
    IndigoHTTPPort              = '8103'
    WSManTcpPort                = '5985'
    BitsTcpPort                 = '443'
    CreateNewLibraryShare       = '1'
    LibraryShareName            = 'MSSCVMMLibrary'
    LibrarySharePath            = 'C:\ProgramData\Virtual Machine Manager Library Files'
    LibraryShareDescription     = 'Virtual Machine Manager Library Files'
    SQMOptIn                    = '0'
    MUOptIn                     = '0'
    VmmServiceLocalAccount      = '0'
    TopContainerName            = 'CN=VMMServer,DC=contoso,DC=com'
}
$iniContentConsoleScvmm = @{
    ProgramFiles  = 'C:\Program Files\Microsoft System Center\Virtual Machine Manager {0}'
    IndigoTcpPort = '8100'
    MUOptIn       = '0'
}

$setupCommandLineServerScvmm = '/server /i /f C:\Server.ini /VmmServiceDomain {0} /VmmServiceUserName {1} /VmmServiceUserPassword {2} /SqlDBAdminDomain {0} /SqlDBAdminName {1} /SqlDBAdminPassword {2} /IACCEPTSCEULA'
$spsetupConfigFileContent = '<Configuration>
    <Package Id="sts">
        <Setting Id="LAUNCHEDFROMSETUPSTS" Value="Yes"/>
    </Package>

    <Package Id="spswfe">
        <Setting Id="SETUPCALLED" Value="1"/>
    </Package>

    <Logging Type="verbose" Path="%temp%" Template="SharePoint Server Setup(*).log"/>
    <PIDKEY Value="{0}" />
    <Display Level="none" CompletionNotice="no" />
    <Setting Id="SERVERROLE" Value="APPLICATION"/>
    <Setting Id="USINGUIINSTALLMODE" Value="0"/>
    <Setting Id="SETUP_REBOOT" Value="Never" />
    <Setting Id="SETUPTYPE" Value="CLEAN_INSTALL"/>
</Configuration>'


$SharePoint2013InstallScript = {
    param
    (
        [string]
        $Mode = '/unattended'
    )
    $exitCode = (Start-Process -PassThru -Wait "C:\SPInstall\PrerequisiteInstaller.exe" –ArgumentList "$Mode /SQLNCli:C:\SPInstall\PrerequisiteInstallerFiles\sqlncli.msi `
               /IDFX:C:\SPInstall\PrerequisiteInstallerFiles\Windows6.1-KB974405-x64.msu `
               /IDFX11:C:\SPInstall\PrerequisiteInstallerFiles\MicrosoftIdentityExtensions-64.msi `
               /Sync:C:\SPInstall\PrerequisiteInstallerFiles\Synchronization.msi `
               /AppFabric:C:\SPInstall\PrerequisiteInstallerFiles\WindowsServerAppFabricSetup_x64.exe `
               /KB2671763:C:\SPInstall\PrerequisiteInstallerFiles\AppFabric1.1-RTM-KB2671763-x64-ENU.exe `
               /MSIPCClient:C:\SPInstall\PrerequisiteInstallerFiles\setup_msipc_x64.msi `
               /WCFDataServices:C:\SPInstall\PrerequisiteInstallerFiles\WcfDataServices.exe `
               /WCFDataServices56:C:\SPInstall\PrerequisiteInstallerFiles\WcfDataServices56.exe"
).ExitCode

    return @{
        ExitCode = $exitCode
        Hostname = $env:COMPUTERNAME
    }
}
$SharePoint2016InstallScript = {
    param
    (
        [string]
        $Mode = '/unattended'
    )
    $exitCode = (Start-Process -PassThru -Wait "C:\SPInstall\PrerequisiteInstaller.exe" –ArgumentList "$Mode /SQLNCli:C:\SPInstall\PrerequisiteInstallerFiles\sqlncli.msi `
    /IDFX11:C:\SPInstall\PrerequisiteInstallerFiles\MicrosoftIdentityExtensions-64.msi `
    /Sync:C:\SPInstall\PrerequisiteInstallerFiles\Synchronization.msi `
    /AppFabric:C:\SPInstall\PrerequisiteInstallerFiles\WindowsServerAppFabricSetup_x64.exe `
    /KB3092423:C:\SPInstall\PrerequisiteInstallerFiles\AppFabric-KB3092423-x64-ENU.exe `
    /MSIPCClient:C:\SPInstall\PrerequisiteInstallerFiles\setup_msipc_x64.exe `
    /WCFDataServices56:C:\SPInstall\PrerequisiteInstallerFiles\WcfDataServices.exe `
    /DotNetFx:C:\SPInstall\PrerequisiteInstallerFiles\NDP462-KB3151800-x86-x64-AllOS-ENU.exe `
    /ODBC:C:\SPInstall\PrerequisiteInstallerFiles\msodbcsql.msi `
    /MSVCRT11:C:\SPInstall\PrerequisiteInstallerFiles\vcredist_64_2012.exe `
    /MSVCRT14:C:\SPInstall\PrerequisiteInstallerFiles\vcredist_64_2015.exe"
).ExitCode

    return @{
        ExitCode = $exitCode
        Hostname = $env:COMPUTERNAME
    }
}
$SharePoint2019InstallScript = {
    param
    (
        [string]
        $Mode = '/unattended'
    )
    $exitCode = (Start-Process -Wait -PassThru "C:\SPInstall\PrerequisiteInstaller.exe" –ArgumentList "$Mode /SQLNCli:C:\SPInstall\PrerequisiteInstallerFiles\sqlncli.msi `
    /IDFX11:C:\SPInstall\PrerequisiteInstallerFiles\MicrosoftIdentityExtensions-64.msi `
    /Sync:C:\SPInstall\PrerequisiteInstallerFiles\Synchronization.msi `
    /AppFabric:C:\SPInstall\PrerequisiteInstallerFiles\WindowsServerAppFabricSetup_x64.exe `
    /KB3092423:C:\SPInstall\PrerequisiteInstallerFiles\AppFabric-KB3092423-x64-ENU.exe `
    /MSIPCClient:C:\SPInstall\PrerequisiteInstallerFiles\setup_msipc_x64.exe `
    /WCFDataServices56:C:\SPInstall\PrerequisiteInstallerFiles\WcfDataServices.exe `
    /DotNet472:C:\SPInstall\PrerequisiteInstallerFiles\NDP472-KB4054530-x86-x64-AllOS-ENU.exe `
    /MSVCRT11:C:\SPInstall\PrerequisiteInstallerFiles\vcredist_64_2012.exe `
    /MSVCRT141:C:\SPInstall\PrerequisiteInstallerFiles\vcredist_64_2017.exe"
).ExitCode

    return @{
        ExitCode = $exitCode
        Hostname = $env:COMPUTERNAME
    }
}
$ExtendedKeyUsages = @{
    OldAuthorityKeyIdentifier = '.29.1'
    OldPrimaryKeyAttributes = '2.5.29.2'
    OldCertificatePolicies = '2.5.29.3'
    PrimaryKeyUsageRestriction = '2.5.29.4'
    SubjectDirectoryAttributes = '2.5.29.9'
    SubjectKeyIdentifier = '2.5.29.14'
    KeyUsage = '2.5.29.15'
    PrivateKeyUsagePeriod = '2.5.29.16'
    SubjectAlternativeName = '2.5.29.17'
    IssuerAlternativeName = '2.5.29.18'
    BasicConstraints = '2.5.29.19'
    CRLNumber = '2.5.29.20'
    Reasoncode = '2.5.29.21'
    HoldInstructionCode = '2.5.29.23'
    InvalidityDate = '2.5.29.24'
    DeltaCRLindicator = '2.5.29.27'
    IssuingDistributionPoint = '2.5.29.28'
    CertificateIssuer = '2.5.29.29'
    NameConstraints = '2.5.29.30'
    CRLDistributionPoints = '2.5.29.31'
    CertificatePolicies = '2.5.29.32'
    PolicyMappings = '2.5.29.33'
    AuthorityKeyIdentifier = '2.5.29.35'
    PolicyConstraints = '2.5.29.36'
    Extendedkeyusage = '2.5.29.37'
    FreshestCRL = '2.5.29.46'
    X509version3CertificateExtensionInhibitAny = '2.5.29.54'
}

$ApplicationPolicies = @{
    # Remote Desktop
    'Remote Desktop' = '1.3.6.1.4.1.311.54.1.2'
    # Windows Update
    'Windows Update' = '1.3.6.1.4.1.311.76.6.1'
    # Windows Third Party Applicaiton Component
    'Windows Third Party Application Component' = '1.3.6.1.4.1.311.10.3.25'
    # Windows TCB Component
    'Windows TCB Component' = '1.3.6.1.4.1.311.10.3.23'
    # Windows Store
    'Windows Store' = '1.3.6.1.4.1.311.76.3.1'
    # Windows Software Extension verification
    ' Windows Software Extension Verification' = '1.3.6.1.4.1.311.10.3.26'
    # Windows RT Verification
    'Windows RT Verification' = '1.3.6.1.4.1.311.10.3.21'
    # Windows Kits Component
    'Windows Kits Component' = '1.3.6.1.4.1.311.10.3.20'
    # ROOT_PROGRAM_NO_OCSP_FAILOVER_TO_CRL
    'No OCSP Failover to CRL' = '1.3.6.1.4.1.311.60.3.3'
    # ROOT_PROGRAM_AUTO_UPDATE_END_REVOCATION
    'Auto Update End Revocation' = '1.3.6.1.4.1.311.60.3.2'
    # ROOT_PROGRAM_AUTO_UPDATE_CA_REVOCATION
    'Auto Update CA Revocation' = '1.3.6.1.4.1.311.60.3.1'
    # Revoked List Signer
    'Revoked List Signer' = '1.3.6.1.4.1.311.10.3.19'
    # Protected Process Verification
    'Protected Process Verification' = '1.3.6.1.4.1.311.10.3.24'
    # Protected Process Light Verification
    'Protected Process Light Verification' = '1.3.6.1.4.1.311.10.3.22'
    # Platform Certificate
    'Platform Certificate' = '2.23.133.8.2'
    # Microsoft Publisher
    'Microsoft Publisher' = '1.3.6.1.4.1.311.76.8.1'
    # Kernel Mode Code Signing
    'Kernel Mode Code Signing' = '1.3.6.1.4.1.311.6.1.1'
    # HAL Extension
    'HAL Extension' = '1.3.6.1.4.1.311.61.5.1'
    # Endorsement Key Certificate
    'Endorsement Key Certificate' = '2.23.133.8.1'
    # Early Launch Antimalware Driver
    'Early Launch Antimalware Driver' = '1.3.6.1.4.1.311.61.4.1'
    # Dynamic Code Generator
    'Dynamic Code Generator' = '1.3.6.1.4.1.311.76.5.1'
    # Domain Name System (DNS) Server Trust
    'DNS Server Trust' = '1.3.6.1.4.1.311.64.1.1'
    # Document Encryption
    'Document Encryption' = '1.3.6.1.4.1.311.80.1'
    # Disallowed List
    'Disallowed List' = '1.3.6.1.4.1.10.3.30'
    # Attestation Identity Key Certificate
    # System Health Authentication
    'System Health Authentication' = '1.3.6.1.4.1.311.47.1.1'
    # Smartcard Logon
    'IdMsKpScLogon' = '1.3.6.1.4.1.311.20.2.2'
    # Certificate Request Agent
    'ENROLLMENT_AGENT' = '1.3.6.1.4.1.311.20.2.1'
    # CTL Usage
    'AUTO_ENROLL_CTL_USAGE' = '1.3.6.1.4.1.311.20.1'
    # Private Key Archival
    'KP_CA_EXCHANGE' = '1.3.6.1.4.1.311.21.5'
    # Key Recovery Agent
    'KP_KEY_RECOVERY_AGENT' = '1.3.6.1.4.1.311.21.6'
    # Secure Email
    'PKIX_KP_EMAIL_PROTECTION' = '1.3.6.1.5.5.7.3.4'
    # IP Security End System
    'PKIX_KP_IPSEC_END_SYSTEM' = '1.3.6.1.5.5.7.3.5'
    # IP Security Tunnel Termination
    'PKIX_KP_IPSEC_TUNNEL' = '1.3.6.1.5.5.7.3.6'
    # IP Security User
    'PKIX_KP_IPSEC_USER' = '1.3.6.1.5.5.7.3.7'
    # Time Stamping
    'PKIX_KP_TIMESTAMP_SIGNING' = '1.3.6.1.5.5.7.3.8'
    # OCSP Signing
    'KP_OCSP_SIGNING' = '1.3.6.1.5.5.7.3.9'
    # IP security IKE intermediate
    'IPSEC_KP_IKE_INTERMEDIATE' = '1.3.6.1.5.5.8.2.2'
    # Microsoft Trust List Signing
    'KP_CTL_USAGE_SIGNING' = '1.3.6.1.4.1.311.10.3.1'
    # Microsoft Time Stamping
    'KP_TIME_STAMP_SIGNING' = '1.3.6.1.4.1.311.10.3.2'
    # Windows Hardware Driver Verification
    'WHQL_CRYPTO' = '1.3.6.1.4.1.311.10.3.5'
    # Windows System Component Verification
    'NT5_CRYPTO' = '1.3.6.1.4.1.311.10.3.6'
    # OEM Windows System Component Verification
    'OEM_WHQL_CRYPTO' = '1.3.6.1.4.1.311.10.3.7'
    # Embedded Windows System Component Verification
    'EMBEDDED_NT_CRYPTO' = '1.3.6.1.4.1.311.10.3.8'
    # Root List Signer
    'ROOT_LIST_SIGNER' = '1.3.6.1.4.1.311.10.3.9'
    # Qualified Subordination
    'KP_QUALIFIED_SUBORDINATION' = '1.3.6.1.4.1.311.10.3.10'
    # Key Recovery
    'KP_KEY_RECOVERY' = '1.3.6.1.4.1.311.10.3.11'
    # Document Signing
    'KP_DOCUMENT_SIGNING' = '1.3.6.1.4.1.311.10.3.12'
    # Lifetime Signing
    'KP_LIFETIME_SIGNING' = '1.3.6.1.4.1.311.10.3.13'
    'DRM' = '1.3.6.1.4.1.311.10.5.1'
    'DRM_INDIVIDUALIZATION' = '1.3.6.1.4.1.311.10.5.2'
    # Key Pack Licenses
    'LICENSES' = '1.3.6.1.4.1.311.10.6.1'
    # License Server Verification
    'LICENSE_SERVER' = '1.3.6.1.4.1.311.10.6.2'
    'Server Authentication' = '1.3.6.1.5.5.7.3.1' #The certificate can be used for OCSP authentication.
    KP_IPSEC_USER = '1.3.6.1.5.5.7.3.7' #The certificate can be used for an IPSEC user.
    'Code Signing' = '1.3.6.1.5.5.7.3.3' #The certificate can be used for signing code.
    'Client Authentication' = '1.3.6.1.5.5.7.3.2' #The certificate can be used for authenticating a client.
    KP_EFS = '1.3.6.1.4.1.311.10.3.4' #The certificate can be used to encrypt files by using the Encrypting File System.
    EFS_RECOVERY = '1.3.6.1.4.1.311.10.3.4.1' #The certificate can be used for recovery of documents protected by using Encrypting File System (EFS).
    DS_EMAIL_REPLICATION = '1.3.6.1.4.1.311.21.19' #The certificate can be used for Directory Service email replication.
    ANY_APPLICATION_POLICY = '1.3.6.1.4.1.311.10.12.1' #The applications that can use the certificate are not restricted.
}