SfBAutomatedLab.psm1

function Start-SfBLabDeployment
{
    param
    (
        [Parameter(Mandatory)]
        [string]$TopologyFilePath,

        [Parameter(Mandatory)]
        [string]$LabName,
        
        [string]$OutputScriptPath
    )

    if (-not (Test-Path -Path $TopologyFilePath))
    {
        Write-Error "The file '$TopologyFilePath' could not be found"
        return
    }
    
    if ($LabName -in (Get-Lab -List))
    {
        Write-Error "A lab with the name '$LabName' does already exist"
        return
    }

    if (-not $OutputScriptPath)
    {
        $OutputScriptPath= '{0}\{1}.ps1' -f (Get-LabSourcesLocation), $LabName  
    }
  
    Write-Host "Saving the AutomatedLab deployment script to '$OutputScriptPath'"
    New-SfBLab -TopologyFilePath $TopologyFilePath -LabName $LabName -OutputScriptPath $OutputScriptPath

    Write-Host
    Write-Host 'The AutomatedLab deployment script is ready. You can either invoke it right away or modify the script to further customize your lab.' -ForegroundColor Yellow
    Write-Host "Do you want to start the deployment now? Type 'Y' to start the deployment or any other key to stop this script: " -ForegroundColor Yellow -NoNewline
    
    $startScript = if ($global:SfBALTestMode -eq 2)
    {
        $true
    }
    else
    {
        if ((Read-Host) -eq 'y')
        {
            $true    
        }
    }

    if ($startScript)
    {
        Invoke-SfBLabScript
    }
    else
    {
        Write-Host "OK, the AutomatedLab deployment script is stored here: $OutputScriptPath. You can call it whenever you want to start the lab deployment." -ForegroundColor Yellow
    }    
}

function New-SfBLab
{
    [OutputType([System.Management.Automation.ScriptBlock])]
    param(
        [Parameter(Mandatory)]
        [string]$TopologyFilePath,

        [Parameter(Mandatory)]
        [string]$LabName,
        
        [Parameter(Mandatory)]
        [string]$OutputScriptPath,
        
        [switch]$ExportOnly,
        
        [switch]$PassThru
    )

    if (-not (Test-Path -Path $TopologyFilePath))
    {
        Write-Error "The file '$TopologyFilePath' could not be found"
        return
    }
    
    Write-Host '-------------------------------------------------------------'
    Write-Host 'Checking for prerequisites...' -NoNewline
    
    $testPrerequisites = Test-SfBLabRequirements
    if (-not $testPrerequisites)
    { Write-Host 'NOT FOUND' } else { Write-Host 'found' }
    Write-Host '-------------------------------------------------------------'
    
    if (-not $testPrerequisites)
    {
        try
        {
            Set-SfBLabRequirements -ErrorAction Stop
        }
        catch
        {
            throw "The cmdlet 'Set-SfBLabRequirements' did not complete. Please finish this task first."
        }
    }
    else
    {
        $script:prerequisites = Get-SfBLabRequirements -ErrorAction Stop
    }
    
    Write-Host '-------------------------------------------------------------'
    Write-Host "Importing SfB topoloigy file '$TopologyFilePath'"
    Write-Host '-------------------------------------------------------------'
    
    Import-SfBTopology -Path $TopologyFilePath -ErrorAction Stop
    $script:labName = $LabName
    $script:discoveredNetworks = @()
    
    $script:sb = New-Object System.Text.StringBuilder
    
    $script:machines = New-Object System.Collections.ArrayList
    $machines.AddRange((Get-SfBTopologyCluster | Get-SfBTopologyMachine))
    
    Add-SfBLabFundamentals
    
    Add-SfBLabInternalNetworks
    Add-SfBLabExternalNetworks
    
    Add-SfBLabDomains

    Add-SfBLabExchangeServers
    
    Add-SfBLabOfficeClients
    
    Write-Host "Found $($machines.Count) machines in the topology file"
    foreach ($machine in $machines)
    {        
        $name = if ($machine.Fqdn) { $machine.Fqdn } else { $machine.ClusterFqdn }
        if ($name -like '*.*')
        {
            $name = $name.Substring(0, $name.IndexOf('.'))
        }
        $domain = $machine.Fqdn.Substring($machine.Fqdn.IndexOf('.') + 1)        

        $roles = $machine | Get-SfBMachineRoleString

        if ($roles)
        {
            Write-Host ">> Adding machine '$($machine.Fqdn)' with roles '$roles'" 
        }
        else
        {
            Write-Host ">> Adding machine '$($machine.Fqdn)'" 
        }
        
        
        $netInterfaces = @()
        $machine.NetInterface | Where-Object InterfaceSide -in 'Primary', 'Internal' | ForEach-Object { $netInterfaces += $_ }
        if ($netInterfaces.Count -eq 0)
        {
            $netInterfaces += New-Object PSObject -Property @{ 
                'InterfaceSide' = 'Internal'
            }
        }

        if ($machine.NetInterface | Where-Object InterfaceSide -in 'External')
        {
            $netInterfaces += New-Object PSObject -Property @{ 
                'InterfaceSide' = 'External'
                'InterfaceNumber' = '1'
                'IPAddress' = ($machine.NetInterface | Where-Object InterfaceSide -eq 'External').IPAddress
            }
        }

        if ($netInterfaces)
        {
            $sb.AppendLine('$netAdapter = @()') | Out-Null
            foreach ($netInterface in $netInterfaces)
            {
                $connectedSwitch = if ($netInterface.InterfaceSide -eq 'External')
                {
                    '$external'
                }
                else
                {
                    '$internal'
                }
                
                if ($netInterface.IPAddress -eq [AutomatedLab.IPAddress]::Null -or -not $netInterface.IPAddress)
                {
                    if ($connectedSwitch -like '$external')
                    {
                        $line = '$netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch {0} -UseDhcp' -f $connectedSwitch
                    }
                    else
                    {
                        $line = '$netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch {0}' -f $connectedSwitch
                    }
                }
                else
                {
                    $ipAddressesStrings = foreach ($ipAddress in $netInterface.IPAddress)
                    {
                        $prefix = ($discoveredNetworks | Where-Object { [AutomatedLab.IPNetwork]::Contains($_, [AutomatedLab.IPAddress]$ipAddress) }).Cidr
                        $ipAddress + '/' + $prefix
                    }
                    
                    $line = '$netAdapter += New-LabNetworkAdapterDefinition -VirtualSwitch {0} -Ipv4Address {1}' -f $connectedSwitch, ($ipAddressesStrings -join ', ')
                }
                
                $sb.AppendLine($line) | Out-Null
            }

            $os = if ($machine.IsClient)
            { $(Get-Module SfbAutomatedLab).PrivateData.OS.Client }
            else
            { $(Get-Module SfbAutomatedLab).PrivateData.OS.Server }

            if (($machine.Roles -band [SfBAutomatedLab.SfBServerRole]::Edge) -eq [SfBAutomatedLab.SfBServerRole]::Edge)
            {
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -NetworkAdapter $netAdapter -OperatingSystem "{1}" -Notes @{{ SfBRoles = "{2}" }}' -f $name, $os, $machine.Roles
            }
            elseif(($machine.Roles -band [SfBAutomatedLab.SfBServerRole]::SqlServer) -eq [SfBAutomatedLab.SfBServerRole]::SqlServer -and [bool]($machine.PSobject.Properties.Name -eq "AlwaysOnPartner"))
            {                
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -NetworkAdapter $netAdapter -DomainName {1}{2} -OperatingSystem "{3}" -Notes @{{ SfBRoles = "{4}"; AlwaysOnPartner = "{5}" }}' -f $name, $domain, $roles, $os, $machine.Roles,$machine.AlwaysOnPartner
            }
            else
            {
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -NetworkAdapter $netAdapter -DomainName {1}{2} -OperatingSystem "{3}" -Notes @{{ SfBRoles = "{4}" }}' -f $name, $domain, $roles, $os, $machine.Roles
            }
        }
        else
        {
            if ($machine.IsEdgeServer)
            {
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -Network $internal -OperatingSystem "{1}" -Notes @{{ SfBRoles = "{2}" }}' -f $name, $os, $machine.Roles
            }
            elseif(($machine.Roles -band [SfBAutomatedLab.SfBServerRole]::SqlServer) -eq [SfBAutomatedLab.SfBServerRole]::SqlServer -and [bool]($machine.PSobject.Properties.Name -eq "AlwaysOnPartner"))
            {                
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -Network $internal -DomainName {1}{2} -OperatingSystem "{3}" -Notes @{{ SfBRoles = "{4}"; AlwaysOnPartner = "{5}" }}' -f $name, $domain, $roles, $os, $machine.Roles,$machine.AlwaysOnPartner
            }
            else
            {
                $line = 'Add-LabMachineDefinition -Name {0} -Memory 2GB -Network $internal -DomainName {1}{2} -OperatingSystem "{3}" -Notes @{{ SfBRoles = "{4}" }}' -f $name, $domain, $roles, $os, $machine.Roles
            }
        }
        $sb.AppendLine($line ) | Out-Null
        $sb.AppendLine() | Out-Null
    }
    Write-Host

    if ($ExportOnly)
    {
        $sb.AppendLine('Export-LabDefinition -Force') | Out-Null
    }
    else
    {
        $sb.AppendLine('Install-Lab') | Out-Null
        
        $sb.AppendLine("Import-SfBTopology -Path '$((Get-SfBTopology).Path)'") | Out-Null
        
        $sb.AppendLine('Add-SfbClusterDnsRecords') | Out-Null

        $sb.AppendLine('Add-SfbFileShares') | Out-Null

        $sb.AppendLine('Install-SfBLabRequirements') | Out-Null

        $sb.AppendLine('Install-SfbLabActiveDirectory') | Out-Null

        $sb.AppendLine('Install-SfbLabSfbComponents') | Out-Null
    
        $sb.AppendLine('Show-LabDeploymentSummary -Detailed') | Out-Null
    }

    [scriptblock]::Create($sb.ToString()) | Out-File -FilePath $OutputScriptPath -Width 5000
    $script:scriptFilePath = $OutputScriptPath
    Write-Host
    Write-Host "Script for AutomatedLab stored in '$scriptFilePath'"
    
    Write-Host
    Write-Host '############################################################################'
    Write-Host '# The script to deploy the SfB lab using AutomatedLab is completed #'
    Write-Host '# Please alter the script if required and call Invoke-SfBLabScript then #'
    Write-Host '# The next steps are: #'
    Write-Host '# - Call Invoke-SfBLabScript (this may take one or two hours) #'
    Write-Host '# - Call Install-SfbLabSfbComponents (this may take an hour) #'
    Write-Host '############################################################################'
    
    if ($PassThru)
    {
        [scriptblock]::Create($sb.ToString())
    }
}

function Install-SfBLabRequirements
{
    if (-not (Get-Lab))
    {
        Write-Error "Lab not imported. Use 'Import-Lab' first"
        return
    }
    if (-not $prerequisites) { $script:prerequisites = Get-SfBLabRequirements -ErrorAction Stop }
    
    $frontendServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*FrontEnd*' }
    $edgeServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*Edge*' }
    $wacServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*WacService*' }
    
    Write-Host "Installing required features on Frontend Servers"
    Install-LabWindowsFeature -ComputerName $frontendServers -FeatureName NET-Framework-Core, RSAT-ADDS, Windows-Identity-Foundation, Web-Server, Web-Static-Content, Web-Default-Doc, Web-Http-Errors, Web-Dir-Browsing, Web-Asp-Net, Web-Net-Ext, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Http-Logging, Web-Log-Libraries, Web-Request-Monitor, Web-Http-Tracing, Web-Basic-Auth, Web-Windows-Auth, Web-Client-Auth, Web-Filtering, Web-Stat-Compression, Web-Dyn-Compression, NET-WCF-HTTP-Activation45, Web-Asp-Net45, Web-Mgmt-Tools, Web-Scripting-Tools, Web-Mgmt-Compat, Server-Media-Foundation, BITS -NoDisplay
    Write-Host "Installing required features on Edge Servers"
    Install-LabWindowsFeature -ComputerName $edgeServers -FeatureName RSAT-ADDS, Web-Server, Web-Static-Content, Web-Default-Doc, Web-Http-Errors, Web-Asp-Net, Web-Net-Ext, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Http-Logging, Web-Log-Libraries, Web-Request-Monitor, Web-Http-Tracing, Web-Basic-Auth, Web-Windows-Auth, Web-Client-Auth, Web-Filtering, Web-Stat-Compression, NET-WCF-HTTP-Activation45, Web-Asp-Net45, Web-Scripting-Tools, Web-Mgmt-Compat, Desktop-Experience, Telnet-Client -NoDisplay
    Write-Host "Installing required features on Office Online Servers"
    Install-LabWindowsFeature -ComputerName $wacServers -FeatureName Web-Server, Web-Mgmt-Tools, Web-Mgmt-Console, Web-WebServer, Web-Common-Http, Web-Default-Doc, Web-Static-Content, Web-Performance, Web-Stat-Compression, Web-Dyn-Compression, Web-Security, Web-Filtering, Web-Windows-Auth, Web-App-Dev, Web-Net-Ext45, Web-Asp-Net45, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Includes, InkandHandwritingServices  -NoDisplay
    Write-Host "Installing Windows features completed"
    
    Restart-LabVM -ComputerName $wacServers -Wait
    
    Write-Host
    
    foreach ($requiredWindowsFix in $prerequisites.RequiredWindowsFixes.GetEnumerator())
    {
        $jobs = @()
        Write-Host "Installing required fix on Frontend Servers '$($requiredWindowsFix.Key)' on Frontend Servers"
        $jobs += Install-LabSoftwarePackage -ComputerName $frontendServers -Path $requiredWindowsFix.Value -CommandLine /quiet -AsJob -PassThru
        
        Write-Host "Installing required fix on Frontend Servers '$($requiredWindowsFix.Key)' on Edge Servers"
        $jobs += Install-LabSoftwarePackage -ComputerName $edgeServers -Path $requiredWindowsFix.Value -CommandLine /quiet -AsJob -PassThru
        
        Write-Host "Installing required fix on Frontend Servers '$($requiredWindowsFix.Key)' on Office Online Servers"
        $jobs += Install-LabSoftwarePackage -ComputerName $wacServers -Path $requiredWindowsFix.Value -CommandLine /quiet -AsJob -PassThru
        
        $jobs | Wait-Job | Out-Null
        
        Write-Host "Installation of '$requiredWindowsFix' finished"
    }
    Write-Host
    
    Write-Host 'Installing SilverLight on Frontend and Edge Servers...'
    
    $silverLightDownloadUrl = $(Get-Module SfbAutomatedLab).PrivateData.DownloadUrls['SilverLight']
    $silverLightPath = "$labSources\SoftwarePackages\Silverlight_x64.exe"
    Get-LabInternetFile -Uri $silverLightDownloadUrl -Path $silverLightPath
    
    $jobs = @()
    Install-LabSoftwarePackage -Path $SilverLightPath -CommandLine '/q /doNotRequireDRMPrompt /ignorewarnings' -ComputerName $edgeServers -AsJob -PassThru
    Install-LabSoftwarePackage -Path $SilverLightPath -CommandLine '/q /doNotRequireDRMPrompt /ignorewarnings' -ComputerName $frontendServers -AsJob -PassThru
    $jobs | Wait-Job | Out-Null
    
    Write-Host 'finished installing SilverLight on Frontend and Edge Servers'
    
    Write-Host "Installing Office Online Server on '$($wacServers.Name -join "', '")'"
    $drive = Mount-LabIsoImage -ComputerName $wacServers -IsoPath $prerequisites.ISOs.OfficeOnline2016Iso -PassThru -SupressOutput
    Install-LabSoftwarePackage -ComputerName $wacServers -LocalPath "$($drive.DriveLetter)\setup.exe" -CommandLine "/config $($drive.DriveLetter)\Files\SetupSilent\config.xml" -UseShellExecute -Timeout 30 -NoDisplay
    Dismount-LabIsoImage -ComputerName $wacServers -SupressOutput
    
    Write-Host "Installing .net 3.5 on all lab machines"
    $machines = Get-LabVm
    Install-LabWindowsFeature -ComputerName $machines -FeatureName NET-Framework-Features -IncludeAllSubFeature -AsJob -PassThru | Wait-Job | Out-Null
}

function Install-SfBLabActiveDirectory
{
    if (-not (Get-Lab))
    {
        Write-Error "Lab in not imported. Use 'Import-Lab' first"
        return
    }
    if (-not $prerequisites) { $script:prerequisites = Get-SfBLabRequirements -ErrorAction Stop }
    
    $rootDc = Get-LabVm -Role RootDC
    
    Write-Host "Installing SfB Management Tools on '$rootDc'"
    $drive = Mount-LabIsoImage -ComputerName $rootDc -IsoPath $prerequisites.ISOs.SfB2015Iso -PassThru -SupressOutput
    Install-LabSoftwarePackage -ComputerName $rootDc -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstrapcore -Timeout 30 -UseShellExecute -NoDisplay
    Dismount-LabIsoImage -ComputerName $rootDc -SupressOutput

    #The existing session must be removed to use the newly installed module
    Remove-LabPSSession -ComputerName $rootDc

    Write-Host
    Write-Host "Preparing AD Schema"
    Invoke-LabCommand -ComputerName $rootDc -ScriptBlock { Install-CSAdServerSchema -Confirm:$false -Report C:\SfBSchemaPrep.html } -NoDisplay

    Write-Host "Preparing AD Forest"
    Invoke-LabCommand -ComputerName $rootDc -ScriptBlock { Enable-CSAdForest -Confirm:$false -Report C:\SfBForestPrep.html } -NoDisplay

    Write-Host "Preparing AD Domain"
    Invoke-LabCommand -ComputerName $rootDc -ScriptBlock { Enable-CSAdDomain -Confirm:$false -Report C:\SfBDomainPrep.html } -NoDisplay

    Write-Host "Adding Install user to CSAdministrators"
    $installUser = ((Get-Lab).Domains | Where-Object Name -eq $rootDc.DomainName).Administrator.UserName
    Invoke-LabCommand -ComputerName $rootDc -ScriptBlock { Get-ADGroup CSAdministrator | Add-ADGroupMember -Members $installUser } -Variable (Get-Variable -Name installUser) -NoDisplay

    #TODO: Creating DNS entries

    Write-Host "AD Preparations finished"
    Write-Host
}

function Install-SfbLabSfbComponents
{
    if (-not (Get-Lab))
    {
        Write-Error "Lab in not imported. Use 'Import-Lab' first"
        return
    }
    if (-not $prerequisites) { $script:prerequisites = Get-SfBLabRequirements -ErrorAction Stop }
    
    $lab = Get-Lab
    $frontEndServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*frontend*' }
    $databaseServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*SqlServer*'} | Sort-Object -Property Name
    $1stFrontendServer = $frontEndServers | Select-Object -First 1
    $firstDbServer = $databaseServers | Select-Object -First 1

    Write-Host "Restarting machine '$($frontEndServers -join ',')'..." -NoNewline
    Restart-LabVM -ComputerName $frontEndServers -Wait
    Write-Host 'done'
    
    Write-Host "Installing SfB Management Tools on '$1stFrontendServer'"
    $drive = Mount-LabIsoImage -ComputerName $1stFrontendServer -IsoPath $prerequisites.ISOs.SfB2015Iso -PassThru -SupressOutput
    
    Write-Host "Calling SfB 'setup.exe /bootstrapcore' on '$1stFrontendServer'"
    Install-LabSoftwarePackage -ComputerName $1stFrontendServer -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstrapcore -UseShellExecute -Timeout 30 -NoDisplay
    
    Write-Host "Calling SfB 'admintools.msi' on '$1stFrontendServer'"
    Install-LabSoftwarePackage -ComputerName $1stFrontendServer -LocalPath C:\Windows\System32\msiexec.exe -CommandLine "/i $($drive.DriveLetter)\Setup\amd64\Setup\admintools.msi ADDLOCAL=Feature_AdminTools REBOOT=ReallySuppress /qb! /L*v C:\Feature_AdminTools.log INSTALLDIR=`"C:\Program Files\Skype for Business Server 2015\`"" -NoDisplay

    Write-Host "Calling SfB 'setup.exe /bootstraplocalmgmt' on '$1stFrontendServer'..."
    Install-LabSoftwarePackage -ComputerName $1stFrontendServer -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstraplocalmgmt -UseShellExecute -Timeout 30 -NoDisplay
    Write-Host
    
    Copy-LabFileItem -Path $lab.Notes.SfBTopologyPath -ComputerName $1stFrontendServer
    Write-Host "SfB Topology copied to '$1stFrontendServer' (C:\)"

    Dismount-LabIsoImage -ComputerName $1stFrontendServer -SupressOutput

    Write-Host "Removing PSSessions in order to use the newly installed modules"
    Remove-LabPSSession -ComputerName $1stFrontendServer
    
    Write-Host "Calling 'Install-CsDatabase'..." -NoNewline
    Invoke-LabCommand -ComputerName $1stFrontendServer -ScriptBlock { Install-CsDatabase -CentralManagementDatabase -SqlServerFqdn ($args[0]) } -ArgumentList $firstDbServer.Fqdn
    Write-Host 'done'
    
    Write-Host "Calling 'Set-CsConfigurationStoreLocation'..." -NoNewline
    Invoke-LabCommand -ComputerName $1stFrontendServer -ScriptBlock { Set-CsConfigurationStoreLocation -SqlServerFqdn ($args[0]) } -ArgumentList $firstDbServer.Fqdn
    Write-Host 'done'

    Write-Host
    Write-Host '############################################################################'
    Write-Host '# SfBAutomatedLab has created the following based on the given topology #'
    Write-Host '# - created all virtual machines #'
    Write-Host '# - installed all required features and hotfixes #'
    Write-Host '# - created DNS records #'
    Write-Host '# - created file shares #'
    Write-Host '# - installed Office Online Server #'
    Write-Host '# - prepared AD forest and domain #'
    Write-Host '# - created SQL database #'
    Write-Host '# The next steps are: #'
    Write-Host '# - Manually publish the SfB topology on the 1st frontend server using the #'
    Write-Host '# Topology Builder. The topology is stored in c:\ on the 1st Frontned. #'
    Write-Host '############################################################################'
    Write-Host '# Press enter to continue the deployment process after manually #'
    Write-Host '# publishing the topology or press CTRL + C to exit #'    
    Write-Host '############################################################################'
    Read-Host | Out-Null
    Write-Host
    Write-Host "SfbAutomatedLab is continuing the deployment..."
    Write-Host

    foreach ($frontEndServer in $frontEndServers)
    {
        $drive = Mount-LabIsoImage -ComputerName $frontEndServer -IsoPath $prerequisites.ISOs.SfB2015Iso -PassThru -SupressOutput

        Write-Host "Calling SfB 'setup.exe /bootstrapcore' on '$frontEndServer'"
        Install-LabSoftwarePackage -ComputerName $frontEndServer -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstrapcore -UseShellExecute -Timeout 30 -NoDisplay
        
        Write-Host "Calling SfB 'setup.exe /bootstraplocalmgmt' on '$frontEndServer'..."
        Install-LabSoftwarePackage -ComputerName $frontEndServer -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstraplocalmgmt -UseShellExecute -Timeout 30 -NoDisplay
    
        #The existing session must be removed to use the newly installed module
        Remove-LabPSSession -ComputerName $frontEndServer
    
        Write-Host "Calling 'Export-CsConfiguration' on '$frontEndServer'"
        Invoke-LabCommand -ComputerName $frontEndServer -ScriptBlock { Export-CsConfiguration -FileName C:\CsConfigData.zip } -NoDisplay
        Write-Host "Calling 'Import-CSConfiguration' on '$frontEndServer'"
        Invoke-LabCommand -ComputerName $frontEndServer -ScriptBlock { Import-CSConfiguration -FileName C:\CsConfigData.zip -LocalStore } -NoDisplay
        Write-Host "Calling 'Enable-CSReplica' on '$frontEndServer'"
        Invoke-LabCommand -ComputerName $frontEndServer -ScriptBlock { Enable-CSReplica -Confirm:$false -Report C:\Enable-CSReplica.html }

        Write-Host "Calling SfB 'setup.exe /bootstrap' on '$frontEndServer'..." -NoNewline
        Install-LabSoftwarePackage -ComputerName $frontEndServer -LocalPath "$($drive.DriveLetter)\Setup\amd64\Setup.exe" -CommandLine /bootstrap -UseShellExecute -Timeout 30 -NoDisplay -PassThru
        Write-Host 'done'
    
        Dismount-LabIsoImage -ComputerName $frontEndServer -SupressOutput
        
        Restart-LabVM -ComputerName $frontEndServer -Wait
        
        Write-Host "Requesting and assigning default certificate on '$frontEndServer'"
        $ca = Get-LabIssuingCA -DomainName $frontEndServer.DomainName
        Invoke-LabCommand -ComputerName $frontEndServer -ScriptBlock {
            $cert = Request-CSCertificate -New -Type Default,WebServicesInternal,WebServicesExternal -CA $args[0] -FriendlyName "Skype for Business Server 2015 Default certificate" -KeySize 2048 -PrivateKeyExportable $false -Organization "NA" -OU "NA" -DomainName "sip.sipdomain.com" -AllSipDomain -Report C:\Request-CSCertificateDefault.html
            Set-CSCertificate -Type Default,WebServicesInternal,WebServicesExternal -Thumbprint $cert.Thumbprint -Confirm:$false -Report C:\Set-CSCertificateDefault.html
        } -ArgumentList $ca.CaPath -PassThru -NoDisplay
        
        Write-Host "Requesting and assigning OAuth certificate on '$frontEndServer'"
        Invoke-LabCommand -ComputerName $frontEndServer -ScriptBlock {
            $cert = Request-CSCertificate -New -Type OAuthTokenIssuer -CA $args[0] -FriendlyName "Skype for Business Server 2015 OAuthTokenIssuer" -KeySize 2048 -PrivateKeyExportable $true -AllSipDomain -Report C:\Request-CSCertificateOAuth.html
            Set-CSCertificate -Identity Global -Type OAuthTokenIssuer -Thumbprint $cert.Thumbprint -Confirm:$false -Report C:\Set-CSCertificateOAuth.html
        } -ArgumentList $ca.CaPath -PassThru -NoDisplay
    }

    # Enable monitoring. Report deployment script can exclusively only work with domain\username.
    # Neither domainfqdn\username, nor username@domainfqdn nor username work.
    $monitoringUserDomain = ($1stFrontendServer.DomainName -split "\.")[0]
    $monitoringUser = '{0}\{1}' -f $monitoringUserDomain, ($lab.Domains | Where-Object -Property Name -eq $1stFrontendServer.DomainName).Administrator.UserName
    $monitoringPassword = ($lab.Domains | Where-Object -Property Name -eq $1stFrontendServer.DomainName).Administrator.Password

    Invoke-LabCommand -ActivityName 'Enabling monitoring on first frontend' -ComputerName $1stFrontendServer -ScriptBlock {
        param
        (
            $UserName,
            $Password,
            $SqlFqdn
        )

        $deploymentScriptPath = Join-Path -Path $env:ProgramFiles -ChildPath 'Skype for Business Server 2015\Deployment\Setup\DeployReports.ps1'
        if (-not (Test-Path $deploymentScriptPath))
        {
            Write-Error -Message ('Monitoring reports could not be deployed because the script DeployReports.ps1 did not exist on {0}' -f $env:COMPUTERNAME)
            exit
        }

        try 
        {
            & $deploymentScriptPath -storedUserName $UserName -storedPassword $Password -reportServerSqlInstance $SqlFqdn -monitoringDatabaseId "MonitoringDatabase:$sqlFqdn"
        }
        catch 
        {
            throw $_
        }

        $reportingConfiguration = (Get-CsReportingConfiguration -ErrorAction SilentlyContinue).ReportingUrl
        if (-not $reportingConfiguration)
        {
            throw 'Reporting configuration could not be found. Enabling monitoring and reporting has not been successful'
        }

        Set-CsQoEConfiguration -Identity 'global' -EnableQoE $true -Force -ErrorAction Stop
        Set-CsCdrConfiguration -Identity 'global' -EnableCDR $true -Force -ErrorAction Stop
    } -ArgumentList $monitoringUser, $monitoringPassword, $firstDbServer.FQDN

    # Enable Call Quality Dashboard
    $cqdUrl = $(Get-Module SfbAutomatedLab).PrivateData.DownloadUrls.CallQualityDashboard
    $cqdPath = "$labSources\SoftwarePackages\CallQualityDashboard.msi"

     Get-LabInternetFile -Uri $cqdUrl -Path $cqdPath

    if(-not (Test-Path $cqdPath))
    {
        Write-Warning -Message ('Cannot enable Call Quality Dashboard on {0} because the download from {1} failed. Hint: Test if the download link is accessible and change it in the module private data ({2}) if not.' -f `
            $firstDbServer.Name, $cqdUrl, (Join-Path $(Get-Module SfbAutomatedLab).ModuleBase 'SfbAutomatedLab.psd1'))
        return
    }

    # Build rather long command line for cqd
    $cqdCommandLine = @(
        'PORTAL_ARCHIVE_SERVER={0}' -f $firstDbServer.Name
        'PORTAL_ANALYSIS_SERVER={0}' -f $firstDbServer.Name
        'REPOSITORY_SQL_SERVER={0}' -f $firstDbServer.Name
        'QOE_METRICS_SQL_SERVER={0}' -f $firstDbServer.Name
        'CUBE_ANALYSIS_SERVER={0}' -f $firstDbServer.Name
        'CUBE_ARCHIVE_SERVER={0}' -f $firstDbServer.Name
        'FQDN={0}' -f $firstDbServer.FQDN
        'ARCHIVE_SQL_SERVER={0}' -f $firstDbServer.Name
        'IIS_APP_POOL_USER={0}' -f $monitoringUser
        'IIS_APP_POOL_PASSWORD={0}' -f $monitoringPassword
        'CUBE_USER={0}' -f $monitoringUser
        'CUBE_PASSWORD={0}' -f $monitoringPassword
        'ARCHIVE_SQL_AGENT_USER={0}' -f $monitoringUser
        'ARCHIVE_SQL_AGENT_PASSWORD={0}' -f $monitoringPassword
    )

    Install-LabSoftwarePackage -Path $cqdPath -CommandLine ($cqdCommandLine -join ' ') -ComputerName $firstDbServer -AsJob -PassThru
}

function Start-SfbLabPool
{
    $lab = Get-Lab -ErrorAction SilentlyContinue
    if (-not $lab)
    {
        Write-Error "Lab in not imported. Use 'Import-Lab' first"
        return
    }
    if (-not $prerequisites) { $script:prerequisites = Get-SfBLabRequirements -ErrorAction Stop }
    
    Import-SfBTopology -Path $lab.Notes.SfBTopologyPath

    $frontendServers = Get-LabVm | Where-Object { $_.Notes.SfBRoles -like '*frontend*' }
    $1stFrontendServer = $frontendServers | Select-Object -First 1
    $frontendPool = Get-SfBTopologyCluster | Where-Object { ($_ | Get-SfBTopologyClusterService).RoleName -eq 'UserServices' }
    
    #Reset-CsPoolRegistrarState -PoolFqdn pool.domain.local -ResetType FullReset

    Invoke-LabCommand -ActivityName 'Setting Sfb services startup type to manual' -ComputerName $frontendServers -ScriptBlock { Get-Service master, fta, rtc* | Set-Service -StartupType Manual }
    
    Get-SfBTopologyCluster | Select-Object -First 1 | Get-SfBTopologyMachine | Select-Object -ExpandProperty ClusterFqdn -Unique | ForEach-Object {
    
        Write-Host "Starting Pool '$_'"

        Invoke-LabCommand -ComputerName $1stFrontendServer -ScriptBlock { Start-CsPool -PoolFqdn $args[0] -Confirm:$false } -ArgumentList $frontendPool.Fqdn

    }
}

function Invoke-SfBLabScript
{
    if (-not $script:scriptFilePath)
    {
        Write-Error "No SfB Install script for AutomatedLab created yet. Use the cmdlet 'New-SfBLab' first"
        return
    }
    
    &$script:scriptFilePath
}

function Add-SfBLabInternalNetworks
{
    $internalIps = Get-SfBTopologyCluster |
    Get-SfBTopologyMachine |
    ForEach-Object { $_.NetInterface } |
    Where-Object { ($_.InterfaceSide -eq 'Internal' -or $_.InterfaceSide -eq 'Primary') -and $_.IPAddress -ne [AutomatedLab.IPAddress]::Null } |
    Select-Object -Property IPAddress, Prefix

    $internalNetworks = foreach ($internalIp in $internalIps)
    {
        foreach ($discoveredInternalNetwork in $discoveredNetworks)
        {
            if ([AutomatedLab.IPNetwork]::Contains($discoveredInternalNetwork, [AutomatedLab.IPAddress]$internalIp.IPAddress))
            {
                Write-Host ">> Assigning prefix $($discoveredInternalNetwork.Cidr ) to IP address $($internalIp.IPAddress)"
                $internalIp.Prefix = $discoveredInternalNetwork.Cidr 
            }
        }
        
        if (-not $internalIp.Prefix)
        {
            if ($global:SfBALTestMode -gt 0)
            {
                $internalIp.Prefix = 24
            }
            else
            {
                $internalIp.Prefix = Read-Host -Prompt "The IP address $($internalIp.IPAddress) is defined. What is the subnet prefix, for example 24 for 255.255.255.0?"
            }
            $script:discoveredNetworks += [AutomatedLab.IPNetwork]"$($internalIp.IPAddress)/$($internalIp.Prefix)"
        }

        [AutomatedLab.IPNetwork]"$($internalIp.IPAddress)/$($internalIp.Prefix)"
    }
    
    Write-Host

    $internalNetworks = $internalNetworks | Sort-Object -Property Network -Unique

    if (-not $internalNetworks)
    {
        Write-Host 'No IP addresses found in the topology to derive an IP network. AutomatedLab will auto-generate networks.'
    }

    Write-Host 'Defining the following networks'
    $i = 1
    if ($internalNetworks)
    {
        foreach ($network in $internalNetworks)
        {
            Write-Host (">> '{0}-{1}'. The host adapter's IP is {2}/{3}" -f $labName, $i, $network.Network, $network.Cidr)
            $line = '$internal = Add-LabVirtualNetworkDefinition -Name "{0}-{1}" -AddressSpace {2}/{3} -PassThru' -f $labName, $i, $network.Network, $network.Cidr
            $sb.AppendLine($line) | Out-Null
        }
    }
    else
    {
        Write-Host (">> '{0}-1'. The host adapter's IP is {1}/{2}" -f $labName, 'auto', 'auto')
        $line = '$internal = Add-LabVirtualNetworkDefinition -Name "{0}-1" -PassThru' -f $labName
        $sb.AppendLine($line) | Out-Null
    }
    
    $sb.AppendLine() | Out-Null
    Write-Host
}

function Add-SfBLabExternalNetworks
{
    $externalIps = Get-SfBTopologyCluster |
    Get-SfBTopologyMachine |
    ForEach-Object { $_.NetInterface } |
    Where-Object { $_.InterfaceSide -eq 'External' -and $_.IPAddress -ne [AutomatedLab.IPAddress]::Null } |
    Select-Object -Property IPAddress, Prefix

    $hasExternalNetworks = [bool]$externalIps
    $externalSwitches = Get-VMSwitch -SwitchType External
    $physicalAdapters = Get-NetAdapter -Physical

    foreach ($externalIp in $externalIps)
    {
        foreach ($discoveredExternalNetwork in $discoveredNetworks)
        {
            if ([AutomatedLab.IPNetwork]::Contains($discoveredExternalNetwork, [AutomatedLab.IPAddress]$externalIp.IPAddress))
            {
                Write-Host ">> Assigning prefix $($discoveredExternalNetwork.Cidr ) to IP address $($externalIp.IPAddress)"
                $externalIp.Prefix = $discoveredExternalNetwork.Cidr
            }
        }
        
        if (-not $externalIp.Prefix)
        {
            if ($global:SfBALTestMode -gt 0)
            {
                $externalIp.Prefix = 24
            }
            else
            {
                $externalIp.Prefix = Read-Host -Prompt "The IP address $($externalIp.IPAddress) is defined. What is the subnet prefix, for example 24 for 255.255.255.0?"
            }
            $script:discoveredNetworks += [AutomatedLab.IPNetwork]"$($externalIp.IPAddress)/$($externalIp.Prefix)"
        }
    }
    
    Write-Host

    if ($hasExternalNetworks -and $externalSwitches)
    {
        $choices = @()
        
        $i = 0
        foreach ($externalSwitch in $externalSwitches)
        {
            $choices += New-Object System.Management.Automation.Host.ChoiceDescription("&$i Existing Switch '$($externalSwitch.Name)'")
            $i++
        }
        foreach ($netAdapter in $physicalAdapters)
        {
            $choices += New-Object System.Management.Automation.Host.ChoiceDescription("&$i New Switch bridging '$($netAdapter.Name)'")
            $i++
        }
        $choices += New-Object System.Management.Automation.Host.ChoiceDescription('&Cancel')

        $result = $host.UI.PromptForChoice(
            'External Virtual Switch',
            'The topology requires an external virtual switch. There is already an external virtual switch existing. Do you want to connect this lab to the existing switch or create a new one?',
        $choices, 0)
        
        if (($result -eq $choices.Count - 1) -or $result -eq -1)
        {
            throw 'Lab deployment aborted'
        }
        
        Write-Host "You have choose option '$($choices[$result].Label.Substring(1))'"
        Write-Host
            
        if ($result -lt $externalSwitches.Count)
        {
            $externalSwitch = $externalSwitches[$result]
            $externalAdapter = Get-NetAdapter -Physical | Where-Object InterfaceDescription -eq $externalSwitch.NetAdapterInterfaceDescription
            $sb.AppendLine(("`$external = Add-LabVirtualNetworkDefinition -Name '{0}' -HyperVProperties @{{ SwitchType = 'External'; AdapterName = '{1}' }} -PassThru" -f $externalSwitch.Name, $externalAdapter.Name)) | Out-Null
                
        }
        else
        {
            $physicalAdapter = $physicalAdapters[$result - $externalSwitches.Count]
            $sb.AppendLine(("`$external = Add-LabVirtualNetworkDefinition -Name External -HyperVProperties @{{ SwitchType = 'External'; AdapterName = '{0}' }} -PassThru" -f $physicalAdapter.Name)) | Out-Null
        }
        
        $sb.AppendLine() | Out-Null
    }
    elseif ($hasExternalNetworks -and -not $externalSwitches)
    {
        $choices = @()
        
        $i = 0
        foreach ($netAdapter in $physicalAdapters)
        {
            $choices += New-Object System.Management.Automation.Host.ChoiceDescription("&$i New Switch bridging '$($netAdapter.Name)'")
            $i++
        }
        $choices += New-Object System.Management.Automation.Host.ChoiceDescription('&Cancel')

        $result = $host.UI.PromptForChoice(
            'External Virtual Switch',
            'The topology requires an external virtual switch. There is no external virtual switch existing and a new one needs to be created. Which adapter shall be used?',
        $choices, 0)
        
        if (($result -eq $choices.Count - 1) -or $result -eq -1)
        {
            throw 'Lab deployment aborted'
        }

        $physicalAdapter = $physicalAdapters[$result - $externalSwitches.Count]
        $sb.AppendLine(("`$external = Add-LabVirtualNetworkDefinition -Name External -HyperVProperties @{{ SwitchType = 'External'; AdapterName = '{0}' }} -PassThru" -f $physicalAdapter.Name)) | Out-Null
        
        $sb.AppendLine() | Out-Null
    }
}

function Add-SfBLabDomains
{
    $domains = Get-SfBTopologyActiveDirectoryDomains
    Write-Host "Domains found in the topology: $($domains)"
    
    foreach ($domain in $domains)
    {
        Write-Host "Setting default installation credentials for domain '$($domain)' machines to user 'Install' with password 'Somepass1'"
        $line = 'Add-LabDomainDefinition -Name {0} -AdminUser Install -AdminPassword Somepass1' -f $domain
        $sb.AppendLine($line) | Out-Null
    }

    Write-Host
    $i = 1
    foreach ($domain in $domains)
    {
        $numberOfDcs = if ($global:SfBALTestMode -gt 0)
        {
            1
        }
        else
        {
            Read-Host -Prompt "How many Domain Controllers do you want to have for domain '$domain'?"
        }
        
        if ($numberOfDcs -le 0)
        {
            $numberOfDcs = 1
        }

        Write-Host "You have chosen $numberOfDcs domain controllers for domain '$domain'"

        foreach ($i in (1..$numberOfDcs))
        {
            $fqdn = "DC$i.$($domain)"
            $machine = $machines | Where-Object FQDN -eq $fqdn
            $domainRole = if ($i -eq 1) { 'RootDC' } else { 'DC' }
            
            if ($machine)
            {
                $machine | Add-Member -Name DomainRole -MemberType NoteProperty -Value $domainRole
            }
            else
            {
                $machine = New-Object PSObject -Property @{ DomainRole = $domainRole; FQDN = $fqdn }
                $machines.Add($machine) | Out-Null
            }
        }
    }
    
    Write-Host
}

function Add-SfBLabOfficeClients
{
    $domains = Get-SfBTopologyActiveDirectoryDomains
    Write-Host "Domains found in the topology: $($domains)"

    Write-Host
    $i = 1
    foreach ($domain in $domains)
    {
        $numberOfOfficeClients = if ($global:SfBALTestMode -gt 0)
        {
            1
        }
        else
        {
            Read-Host -Prompt "How many Office 2016 Clients do you want to have in domain '$domain'?"
        }
        
        Write-Host "You have chosen $numberOfOfficeClients Office 2016 Clients to be added to domain '$domain'"

        if ($numberOfOfficeClients -le 0)
        {
            continue
        }

        foreach ($i in (1..$numberOfOfficeClients))
        {
            $fqdn = "Client$i.$($domain)"
            $role = 'Office2016'
            
            $machine = New-Object PSObject -Property @{ 
                DomainRole = $role
                FQDN = $fqdn
                IsClient = $true
            }
            $machines.Add($machine) | Out-Null
        }
    }
    
    Write-Host
}

function Add-SfBLabExchangeServers
{
    $domains = Get-SfBTopologyActiveDirectoryDomains
    Write-Host "Domains found in the topology: $($domains)"

    Write-Host
    $i = 1
    foreach ($domain in $domains)
    {
        $numberOfExchangeServers = if ($global:SfBALTestMode -gt 0)
        {
            1
        }
        else
        {
            Read-Host -Prompt "How many Exchange 2016 Servers do you want to have in domain '$domain'?"
        }
        
        Write-Host "You have chosen $numberOfExchangeServers Exchange 2016 servers to be added to domain '$domain'"

        if ($numberOfExchangeServers -le 0)
        {
            continue
        }

        foreach ($i in (1..$numberOfExchangeServers))
        {
            $fqdn = "Exchange$i.$($domain)"
            $role = 'Exchange2016'
            
            $machine = New-Object PSObject -Property @{ 
                DomainRole = $role
                FQDN = $fqdn
                IsClient = $false
            }
            $machines.Add($machine) | Out-Null
        }
    }
    
    Write-Host
}

function Add-SfBLabFundamentals
{
    $sb.AppendLine(('$labName = "{0}"' -f $LabName)) | Out-Null
    $sb.AppendLine('$labSources = Get-LabSourcesLocation') | Out-Null

    $line = 'New-LabDefinition -Name $labName -DefaultVirtualizationEngine HyperV -Notes @{{ SfBTopologyPath = "{0}" }}' -f $TopologyFilePath
    $sb.AppendLine($line) | Out-Null
    $sb.AppendLine("Add-LabIsoImageDefinition -Name SQLServer2014 -Path $($prerequisites.ISOs.SqlServer2014)") | Out-Null
    $sb.AppendLine("Add-LabIsoImageDefinition -Name Office2016 -Path $($prerequisites.ISOs.Office2016)") | Out-Null

    Write-Host "Setting default installation credentials for machines to user 'Install' with password 'Somepass1'"
    $sb.AppendLine('Set-LabInstallationCredential -Username Install -Password Somepass1') | Out-Null
    $sb.AppendLine() | Out-Null
    
    Write-Host
}

function Add-SfBClusterDnsRecords
{
    $clusters = Get-SfBTopologyCluster | Where-Object { $_.IsSingleMachineOnly -eq 'false' }

    foreach ($cluster in $clusters)
    {
        $clusterMachines = $cluster | Get-SfBTopologyMachine
        $clusterName = $cluster.Fqdn.Substring(0, $cluster.Fqdn.IndexOf('.'))
        $clusterDnsZone = $cluster.Fqdn.Substring($cluster.Fqdn.IndexOf('.') + 1)
        $dc = Get-LabVm -Role RootDC | Where-Object DomainName -eq $clusterDnsZone

        foreach ($clusterMachine in $clusterMachines)
        {
            $name = $clusterMachine.Fqdn.Substring(0, $clusterMachine.Fqdn.IndexOf('.'))
            $labMachine = Get-LabVm -ComputerName $name

            $dnsCmd = 'Add-DnsServerResourceRecord -Name {0} -ZoneName {1} -IPv4Address {2} -A' -f $clusterName, $clusterDnsZone, $labMachine.IpV4Address

            Invoke-LabCommand -ActivityName "AddClusterDnsRecord ($clusterName -> $($labMachine.IpV4Address))" -ComputerName $dc -ScriptBlock ([scriptblock]::Create($dnsCmd))
        }
    }
}

function Add-SfBEdgeServerDnsRecords
{
    $edgeServers = Get-SfBTopologyCluster | Get-SfBTopologyMachine | Where-Object Roles -like *Edge*
    
    foreach ($edgeServer in ($edgeServers | Where-Object { $_.NetInterface.InterfaceSide -eq 'Internal' }))
    {
        $domainName = $edgeServer.Fqdn.Substring($edgeServer.Fqdn.IndexOf('.') + 1)
        $dc = Get-LabVm -Role RootDC | Where-Object DomainName -eq $domainName
        $name = $edgeServer.Fqdn.Substring(0, $edgeServer.Fqdn.IndexOf('.'))
        $ipAddress = $edgeServers[0].NetInterface | Where-Object InterfaceSide -eq Internal | Select-Object -ExpandProperty IPAddress
        
        Invoke-LabCommand -ActivityName "AddEdgeServerDnsRecord ($name -> $ipAddress)" -ComputerName $dc -ScriptBlock ([scriptblock]::Create($dnsCmd))
    }
}

function Add-SfBFileShares
{
    $cmd = {
        param(
            [Parameter(Mandatory)]
            [string]$Name
        )

        $data = mkdir c:\data -Force

        $newFolder = mkdir -Path (Join-Path -Path $data -ChildPath $name)
        New-SmbShare -Path $newFolder -Name $name -Description SfB -FullAccess Everyone
    }

    $fileStores = Get-SfBTopologyFileStore

    foreach ($fileStore in $fileStores)
    {
        $installedOnMachines = $fileStore.InstalledOnMachines.Substring(0, $fileStore.InstalledOnMachines.IndexOf('.'))
        Invoke-LabCommand -ActivityName NewFileStore -ComputerName $installedOnMachines -ScriptBlock $cmd -ArgumentList $fileStore.ShareName
    }
}

function Test-SfBLabRequirements
{
    [CmdletBinding()]
    
    param()
    
    $regCache = Get-SfBLabRequirements
    
    return [bool]$regCache
}

function Get-SfBLabRequirements
{
    [CmdletBinding()]
    
    param()
    
    $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, (Get-Type -GenericType AutomatedLab.SerializableDictionary -T string,string)
    
    try
    {
        $type::ImportFromRegistry('Cache', 'SfB')
    }
    catch
    {
        Write-Error 'No settings found in the registry'
        return
    }
}

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

    $labSources = Get-LabSourcesLocation
    
    $requiredIsos = $(Get-Module SfbAutomatedLab).PrivateData.RequiredIsos
    $requiredWindowsFixes = $(Get-Module SfbAutomatedLab).PrivateData.RequiredWindowsFixes
    
    $type = Get-Type -GenericType AutomatedLab.DictionaryXmlStore -T String, (Get-Type -GenericType AutomatedLab.SerializableDictionary -T string,string)
    
    Write-Verbose 'Trying to find existing settings...'
    
    try
    {
        $regCache = $type::ImportFromRegistry('Cache', 'SfB')        
        $result = Read-Choice -ChoiceList '&Yes', '&No' -Caption 'Do you want to change and overwrite the existing settings?' -Default 1
    }
    catch
    {
        $result = 0
    }
    
    if ($result -eq 0)
    {
        Write-Host
        Write-Host 'In order to install the lab, some ISO files need to be present and known to SfBAutomatedLab. Please provide the paths to:'
        $data = Read-HashTable -ChoiceList $requiredIsos -Caption 'Please copy and paste the paths to the required ISO files here:'
    }
    
    if ($data)
    {
        $isos = New-Object (Get-Type -GenericType AutomatedLab.SerializableDictionary -T string,string)
        $data.GetEnumerator() | ForEach-Object {
            $isos.Add($_.Key, $_.Value)
        }
        
        $regCache = New-Object $type
        $regCache.Add('ISOs', $isos)
    }
    
    $regCache.ISOs.GetEnumerator() | ForEach-Object {
        if (-not $_.Value)
        {
            throw "The path for '$($_.Key)' is empty. Please start the function 'Set-SfBLabRequirements' again and overwrite the existing settings."
        }
        if (-not (Test-Path -Path $_.Value -PathType Leaf))
        {
            throw "The path for $($_.Key) ($($_.Value)) could not be validated. Please start the function 'Set-SfBLabRequirements' again"
        }
    }
    
    #-------------------------------
    
    $allRequiredFixesAvailable = 1
    
    Write-Host
    Write-Host "Checking for required fixes..."
    
    $fixes = New-Object (Get-Type -GenericType AutomatedLab.SerializableDictionary -T string,string)
    
    $isOneFixMissing = $false
    foreach ($requiredWindowsFix in $requiredWindowsFixes)
    {
        Write-Host "$requiredWindowsFix - " -NoNewline
        $exists = (Get-ChildItem -Path $labSources -Filter $requiredWindowsFix -Recurse | Select-Object -First 1).FullName
        
        if ($exists)
        {
            Write-Host 'ok'
        }
        else
        {
            try
            {
                $exists = (Get-LabInternetFile -Uri $(Get-Module SfbAutomatedLab).PrivateData.DownloadUrls[$requiredWindowsFix] -Path $labSources\OSUpdates -PassThru -ErrorAction Stop -NoDisplay).FullName
                Write-Host 'ok'
            }
            catch
            {
                Write-Host 'NOT FOUND';$isOneFixMissing = $true
            }
        }

        $fixes.Add($requiredWindowsFix, $exists)
    }
    
    Write-Host
    if ($isOneFixMissing)
    {
        throw 'Required fixes are missing. Please put the files into the OSUpdates folder which is inside the LabSources folder'
    }
    
    $regCache.RequiredWindowsFixes = $fixes
    
    $regCache.ExportToRegistry('Cache', 'SfB')
}