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 = @() $param = @{ Context = $StorageContext.Context } if ($Path) { $param.Path = $Path $param.ErrorAction = 'SilentlyContinue' } if ($StorageContext.ShareDirectoryClient) { $param.ShareDirectoryClient = $StorageContext.ShareDirectoryClient} if ($StorageContext.ShareClient) { $param.ShareClient = $StorageContext.ShareClient} $temporaryContent = Get-AzStorageFile @param foreach ($item in $temporaryContent) { if ($item.ShareDirectoryClient) { $content += $item $content += Get-LabAzureLabSourcesContentRecursive -StorageContext $item } elseif ($item.ShareFileClient) { $content += $item } 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['[Options]'].SiteCode = $CMSiteCode $CMSetupConfig['[Options]'].SiteName = $CMSiteName $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 "configurationManagerAVExcludedPaths", "configurationManagerAVExcludedProcesses") -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.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFileShare] $Share ) $container = Split-Path -Path $RelativePath if (-not $container) { New-AzStorageDirectory -ShareClient $share.ShareClient -Context $Share.Context -Path $RelativePath -ErrorAction SilentlyContinue return } if (-not (Get-AzStorageFile -ShareClient $Share.ShareClient -Context $share.Context -Path $container -ErrorAction SilentlyContinue)) { New-LabSourcesPath -RelativePath $container -Share $Share New-AzStorageDirectory -ShareClient $share.ShareClient -Context $Share.Context -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' } $virtualNetworks = Get-LabVirtualNetwork -Name $virtualNetworks.Name -ErrorAction SilentlyContinue 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 { if (-not $virtualNetwork.Notes) { Write-Error -Message "Cannot remove virtual network '$virtualNetwork' because lab meta data for this object could not be retrieved" } elseif ($virtualNetwork.Notes.LabName -ne $labName) { Write-Error -Message "Cannot remove virtual network '$virtualNetwork' because it does not belong to this lab" } 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 | Hyper-V\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 -ErrorAction SilentlyContinue $jobs | Wait-Job -Timeout ($ShutdownTimeoutInMinutes * 60) | Out-Null if (-not $jobs -or ($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 -and $global:al_PreviousDefaultLocationName -eq $DefaultLocationName) { 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 } $global:al_PreviousDefaultLocationName = $DefaultLocationName 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 'No OS cache available. Querying available operating system images from Azure. This takes some time, as we need to query for the VM Generation as well. The information will be cached.' $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'" return } $availableRoleSizes = Get-AzComputeResourceSku -Location $azLocation.Location | Where-Object { $_.ResourceType -eq 'virtualMachines' -and ($_.Restrictions | Where-Object Type -eq Location).ReasonCode -ne '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 # For each publisher, get latest image AND REQUEST THAT SPECIFIC IMAGE to be able to see the Hyper-V generation. Marvelous. $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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null # 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 | Get-AzVmImage } | Where-Object PurchasePlan -eq $null } 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 if ($script:lab.AzureSettings.DefaultResourceGroup) { return $script:lab.AzureSettings.DefaultResourceGroup } $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 { $_.Name -match $RegexFilter } } if ($File) { $content = $content | Where-Object -FilterScript { $_ -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFile] } } if ($Directory) { $content = $content | Where-Object -FilterScript { $_ -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFileDirectory] } } $content = $content | Add-Member -MemberType ScriptProperty -Name FullName -Value { $this.ShareFileClient.Uri.AbsoluteUri } -Force -PassThru | Add-Member -MemberType ScriptProperty -Name Length -Force -Value { $this.FileProperties.ContentLength } -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 | Select-Object -Property 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 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 -ShareClient $share.ShareClient -Context $share.Context -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() $localHash = (Get-FileHash -Path $file.FullName -Algorithm MD5).Hash 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 -ShareClient $share.ShareClient -Context $share.Context -Path $fileName -Source $file.FullName -ErrorAction SilentlyContinue -Force Write-PSFMessage "Azure file $fileName successfully uploaded. Updating file hash..." } 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) { Write-Warning "Azure LabSources storage '$($azureLabSources.StorageAccountName)' does not exist in the subscription '$($azureLabSources.SubscriptionName)'" return $false } if (-not $azureLabSources.AllowSharedKeyAccess) { Write-Warning "Azure LabSources storage '$($azureLabSources.StorageAccountName)' does not allow shared key access" 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 } $installedModules = Get-Module -ListAvailable [hashtable[]] $modulesMissing = @() foreach ($module in $modules) { $param = @{ Name = $module.Name } $isPresent = if ($module.MinimumVersion) { $installedModules | Where-Object { $_.Name -eq $module.Name -and $_.Version -ge $module.MinimumVersion } $param.MinimumVersion = $module.MinimumVersion } elseif ($module.RequiredVersion) { $installedModules | Where-Object { $_.Name -eq $module.Name -and $_.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) { $cachedOs = [AutomatedLab.OperatingSystem]::new($os.AutomatedLabOperatingSystemName) 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) { $azureOs = [AutomatedLab.OperatingSystem]::new($sku.AutomatedLabOperatingSystemName) 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 updated with $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 } if ($engine -eq 'Azure') { foreach ($network in $script:data.VirtualNetworks) { $remoteNet = Get-AzVirtualNetwork -Name $network.ResourceName foreach ($externalPeer in $network.PeeringVnetResourceIds) { $peerName = $externalPeer -split '/' | Select-Object -Last 1 $vNet = Get-AzResource -Id $externalPeer | Get-AzVirtualNetwork Write-ScreenInfo -Type Verbose -Message ('Adding peering from {0} to {1} to VNet' -f $network.ResourceName, $peerName) $null = Add-AzVirtualNetworkPeering -VirtualNetwork $vnet -RemoteVirtualNetworkId $remoteNet.id -Name "$($network.ResourceName)To$($peerName)" } } } #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" foreach ($network in $(Get-Lab).VirtualNetworks) { $remoteNet = Get-AzVirtualNetwork -Name $network.ResourceName foreach ($externalPeer in $network.PeeringVnetResourceIds) { $peerName = $externalPeer -split '/' | Select-Object -Last 1 $vNet = Get-AzResource -Id $externalPeer | Get-AzVirtualNetwork Write-ScreenInfo -Type Verbose -Message ('Adding peering from {0} to {1} to VNet' -f $network.ResourceName, $peerName) $null = Remove-AzVirtualNetworkPeering -VirtualNetworkName $vnet.Name -ResourceGroupname $vnet.ResourceGroupName -Name "$($network.ResourceName)To$($peerName)" -Force } } #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]$Summary ) 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 ($Summary) { 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'}) } else { # Do a quick check with HEAD only, more reliable across OSes $response = Invoke-WebRequest -Method Head -Uri https://automatedlab.org -TimeoutSec 5 -ErrorAction SilentlyContinue $null -ne $response } } 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 } } $mofTempPath = [System.IO.Path]::GetTempFileName() $metaMofTempPath = [System.IO.Path]::GetTempFileName() Remove-Item -Path $mofTempPath Remove-Item -Path $metaMofTempPath New-Item -ItemType Directory -Path $mofTempPath | Out-Null New-Item -ItemType Directory -Path $metaMofTempPath | 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 } $mofs = & $Configuration.Name @param $mof = $mofs | Where-Object { $_.Name -like "*$c*" -and $_.Name -notlike '*.meta.mof' } $metaMof = $mofs | Where-Object { $_.Name -like "*$c*" -and $_.Name -like '*.meta.mof' } if ($null -ne $mof) { $mof = $mof | Rename-Item -NewName "$($Configuration.Name)_$c.mof" -Force -PassThru $mof | Move-Item -Destination $outputPath -Force Remove-Item -Path $mofTempPath -Force -Recurse } if ($null -ne $metaMof) { $metaMof = $metaMof | Rename-Item -NewName "$($Configuration.Name)_$c.meta.mof" -Force -PassThru $metaMof | Move-Item -Destination $outputPath -Force Remove-Item -Path $metaMofTempPath -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 } } } $metaMofFiles = Get-ChildItem -Path $outputPath -Filter *.mof | Where-Object Name -Match '(?<ConfigurationName>\w+)_(?<ComputerName>[\w-_]+)\.meta.mof' foreach ($c in $ComputerName) { foreach ($metaMofFile in $metaMofFiles) { if ($metaMofFile.Name -match "(?<ConfigurationName>$($Configuration.Name))_(?<ComputerName>$c)\.meta.mof") { Send-File -Source $metaMofFile.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 Remove-Item -Path "$path\localhost.meta.mof" -ErrorAction SilentlyContinue $mofFiles = Get-ChildItem -Path $path -Filter *.mof | Where-Object Name -notlike *.meta.mof if ($mofFiles.Count -gt 1) { throw "There is more than one MOF file in the folder '$path'. Expected is only one file." } $metaMofFiles = Get-ChildItem -Path $path -Filter *.mof | Where-Object Name -like *.meta.mof if ($metaMofFiles.Count -gt 1) { throw "There is more than one Meta MOF file in the folder '$path'. Expected is only one file." } if ($null -ne $metaMofFiles) { $metaMofFiles | Rename-Item -NewName localhost.meta.mof Set-DscLocalConfigurationManager -Path $path -Force:$Force } if ($null -ne $mofFiles) { $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 { [CmdletBinding()] param ( ) 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 = New-TemporaryFile $tempFilePath | Remove-Item $tempFilePath = [System.IO.Path]::ChangeExtension($tempFilePath.FullName, '.zip') Write-PSFMessage -Message "Temp file: '$tempFilePath'" try { Invoke-WebRequest -Uri $sysInternalsDownloadURL -UseBasicParsing -OutFile $tempFilePath -ErrorAction Stop $fileDownloaded = Test-Path -Path $tempFilePath Write-PSFMessage -Message "File '$sysInternalsDownloadURL' downloaded: $fileDownloaded" } 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 -T 1 $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 -T 1 $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-ScreenInfo -Type Error -Message "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 -SuppressErrors } } 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 -TrustServerCertificate } -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 -TrustServerCertificate } -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, [switch]$UseAzureUrl ) $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+)/') { if ($UseAzureUrl) { $feed.url = $feed.url.Replace($Matches.Host, $tfsVm.AzureConnectionInfo.DnsName) $feed.url = $feed.url.Replace($Matches.Port, $defaultParam.Port) } else { $feed.url = $feed.url.Replace($Matches.Host, $tfsVm.Name) } } } 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 ( [Parameter()] [switch]$UseAzureUrl ) $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 -and $UseAzureUrl)) { $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 -Module FailoverClusters -CommandType Cmdlet -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 -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] -and $remainingTarget.ConnectionInfo.ComputerName -as [ipaddress]) { # Special case - return value is an IP address instead of a host name. We need to look it up. $machines | Where-Object Ipv4Address -eq $remainingTarget.ConnectionInfo.ComputerName } 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 | Sort-Object -Unique) } 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 VMConnect Settings Set-PSFConfig -Module 'AutomatedLab' -Name VMConnectWriteConfigFile -Value $true -Initialize -Validation string -Description "Enable the writing of VMConnect config files by default" Set-PSFConfig -Module 'AutomatedLab' -Name VMConnectDesktopSize -Value '1366, 768' -Initialize -Validation string -Description "The default resolution for Hyper-V's VMConnect.exe" Set-PSFConfig -Module 'AutomatedLab' -Name VMConnectFullScreen -Value $false -Initialize -Validation string -Description "Enable full screen mode for VMConnect.exe" Set-PSFConfig -Module 'AutomatedLab' -Name VMConnectUseAllMonitors -Value $false -Initialize -Validation string -Description "Use all monitors for VMConnect.exe" Set-PSFConfig -Module 'AutomatedLab' -Name VMConnectRedirectedDrives -Value 'none' -Initialize -Validation string -Description "Drives to mount in a VMConnect session. Use '*' for all drives or a semicolon seperated list." #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 = '4.0.0' } @{ Name = 'Az.Storage' MinimumVersion = '8.0.0' } @{ Name = 'Az.Compute' MinimumVersion = '9.0.0' } @{ Name = 'Az.Network' MinimumVersion = '7.11.0' } @{ Name = 'Az.Resources' MinimumVersion = '7.7.0' } @{ Name = 'Az.Websites' MinimumVersion = '3.2.2' } @{ Name = 'Az.Security' MinimumVersion = '1.7.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) { var error = e.ErrorCode; 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 3>&1 } 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-Warning "Domain $ParentDomainName was not reachable ($count)" } 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." 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 ` -AllowDomainReinstall ` -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-Warning "Domain $ParentDomainName was not reachable ($count)" } 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 AllowDomainReinstall=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-Warning "Promoting the Domain Controller '$(HOSTNAME.EXE)' did not work. The error code was '$LASTEXITCODE'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." 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-Warning "Domain $DomainName was not reachable ($count)" } 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 AllowDomainControllerReinstall = $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-Warning "The first try to promote '$(HOSTNAME.EXE)' did not work. The error was '$($result.Message)'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." 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 AllowDomainControllerReinstall=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-Warning "Domain $DomainName was not reachable ($count)" } 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-Warning "The first try to promote '$(HOSTNAME.EXE)' did not work. The error code was '$LASTEXITCODE'. Retrying after $SecondsBetweenRetries seconds. Retry count $retriesDone of $Retries." 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. } |