diff --git a/Add-WVDHostToHostpoolSpring24h.ps1 b/Add-WVDHostToHostpoolSpring24h.ps1 new file mode 100644 index 0000000..5c3abe7 --- /dev/null +++ b/Add-WVDHostToHostpoolSpring24h.ps1 @@ -0,0 +1,275 @@ +<# +.SYNOPSIS + Adds an WVD Session Host to an existing WVD Hostpool *** SPRING UPDATE 2020*** +.DESCRIPTION + This scripts adds an WVD Session Host to an existing WVD Hostpool by performing the following action: + - Download the WVD agent + - Download the WVD Boot Loader + - Install the WVD Agent, using the provided hostpoolRegistrationToken + - Install the WVD Boot Loader + - Set the WVD Host into drain mode (optionally) + - Create the Workspace <-> App Group Association (optionally) + The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. +.NOTES + File Name : add-WVDHostToHostpoolSpringV3.ps1 + Author : Freek Berson - Wortell - RDSGurus + Version : v1.0.0 +.EXAMPLE + .\add-WVDHostToHostpoolSpringV3.ps11 existingWVDWorkspaceName existingWVDHostPoolName ` + existingWVDAppGroupName servicePrincipalApplicationID servicePrincipalPassword azureADTenantID + resourceGroupName azureSubscriptionID Drainmode createWorkspaceAppGroupAsso >> \dd-WVDHostToHostpoolSpring.log +.DISCLAIMER + Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied + warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk + arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved + in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss + of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability + to use the this script. +#> + + +<# +.DESCRIPTION +Runs defined msi's to deploy RDAgent and Bootloader +.PARAMETER programDisplayName +.PARAMETER argumentList +.PARAMETER msiOutputLogPath +.PARAMETER isUninstall +.PARAMETER msiLogVerboseOutput +#> +function RunMsiWithRetry { + param( + [Parameter(mandatory = $true)] + [string]$programDisplayName, + + [Parameter(mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String[]]$argumentList, + + [Parameter(mandatory = $true)] + [string]$msiOutputLogPath, + + [Parameter(mandatory = $false)] + [switch]$isUninstall, + + [Parameter(mandatory = $false)] + [switch]$msiLogVerboseOutput + ) + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + if ($msiLogVerboseOutput) { + $argumentList += "/l*vx+ ""$msiOutputLogPath""" + } + else { + $argumentList += "/l*+ ""$msiOutputLogPath""" + } + + $retryTimeToSleepInSec = 30 + $retryCount = 0 + $sts = $null + do { + $modeAndDisplayName = ($(if ($isUninstall) { "Uninstalling" } else { "Installing" }) + " $programDisplayName") + + if ($retryCount -gt 0) { + Log -Message "Retrying $modeAndDisplayName in $retryTimeToSleepInSec seconds because it failed with Exit code=$sts This will be retry number $retryCount" + Start-Sleep -Seconds $retryTimeToSleepInSec + } + + Log -Message ( "$modeAndDisplayName" + $(if ($msiLogVerboseOutput) { " with verbose msi logging" } else { "" })) + + + $processResult = Start-Process -FilePath "msiexec.exe" -ArgumentList $argumentList -Wait -Passthru + $sts = $processResult.ExitCode + + $retryCount++ + } + while ($sts -eq 1618 -and $retryCount -lt 20) + + if ($sts -eq 1618) { + Log -Err "Stopping retries for $modeAndDisplayName. The last attempt failed with Exit code=$sts which is ERROR_INSTALL_ALREADY_RUNNING" + throw "Stopping because $modeAndDisplayName finished with Exit code=$sts" + } + else { + Log -Message "$modeAndDisplayName finished with Exit code=$sts" + } + + return $sts +} + +<# +.DESCRIPTION +Uninstalls any existing RDAgent BootLoader and RD Infra Agent installations and then installs the RDAgent BootLoader and RD Infra Agent using the specified registration token. +.PARAMETER AgentInstallerFolder +Required path to MSI installer file +.PARAMETER AgentBootServiceInstallerFolder +Required path to MSI installer file +#> +function InstallRDAgents { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentBootServiceInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RegistrationToken, + + [Parameter(mandatory = $false)] + [switch]$EnableVerboseMsiLogging + ) + + $ErrorActionPreference = "Stop" + + Log -Message "Boot loader folder is $AgentBootServiceInstallerFolder" + $AgentBootServiceInstaller = $AgentBootServiceInstallerFolder + '\WVD-BootLoader.msi' + + Log -Message "Agent folder is $AgentInstallerFolder" + $AgentInstaller = $AgentInstallerFolder + '\WVD-Agent.msi' + + if (!$RegistrationToken) { + throw "No registration token specified" + } + + $msiNamesToUninstall = @( + @{ msiName = "Remote Desktop Services Infrastructure Agent"; displayName = "RD Infra Agent"; logPath = "$AgentInstallerFolder\AgentUninstall.txt"}, + @{ msiName = "Remote Desktop Agent Boot Loader"; displayName = "RDAgentBootLoader"; logPath = "$AgentInstallerFolder\AgentBootLoaderUnInstall.txt"} + ) + + foreach($u in $msiNamesToUninstall) { + while ($true) { + try { + $installedMsi = Get-Package -ProviderName msi -Name $u.msiName + } + catch { + if ($PSItem.FullyQualifiedErrorId -eq "NoMatchFound,Microsoft.PowerShell.PackageManagement.Cmdlets.GetPackage") { + break + } + + throw; + } + + $oldVersion = $installedMsi.Version + $productCodeParameter = $installedMsi.FastPackageReference + + RunMsiWithRetry -programDisplayName "$($u.displayName) $oldVersion" -isUninstall -argumentList @("/x $productCodeParameter", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath $u.logPath -msiLogVerboseOutput:$EnableVerboseMsiLogging + } + } + + Log -Message "Installing RD Infra Agent on VM $AgentInstaller" + RunMsiWithRetry -programDisplayName "RD Infra Agent" -argumentList @("/i $AgentInstaller", "/quiet", "/qn", "/norestart", "/passive", "REGISTRATIONTOKEN=$RegistrationToken") -msiOutputLogPath "C:\Users\AgentInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + Log -Message "Installing RDAgent BootLoader on VM $AgentBootServiceInstaller" + RunMsiWithRetry -programDisplayName "RDAgent BootLoader" -argumentList @("/i $AgentBootServiceInstaller", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath "C:\Users\AgentBootLoaderInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + $bootloaderServiceName = "RDAgentBootLoader" + $startBootloaderRetryCount = 0 + while ( -not (Get-Service $bootloaderServiceName -ErrorAction SilentlyContinue)) { + $retry = ($startBootloaderRetryCount -lt 6) + $msgToWrite = "Service $bootloaderServiceName was not found. " + if ($retry) { + $msgToWrite += "Retrying again in 30 seconds, this will be retry $startBootloaderRetryCount" + Log -Message $msgToWrite + } + else { + $msgToWrite += "Retry limit exceeded" + Log -Err $msgToWrite + throw $msgToWrite + } + + $startBootloaderRetryCount++ + Start-Sleep -Seconds 30 + } + + Log -Message "Starting service $bootloaderServiceName" + Start-Service $bootloaderServiceName +} + +#Get Parameters +$existingWVDWorkspaceName = $args[0] +$existingWVDHostPoolName = $args[1] +$existingWVDAppGroupName = $args[2] +$servicePrincipalApplicationID = $args[3] +$servicePrincipalPassword = $args[4] +$azureADTenantID = $args[5] +$resourceGroupName = $args[6] +$azureSubscriptionID = $args[7] +$drainmode = $args[8] +$createWorkspaceAppGroupAsso = $args[9] + +#Set Variables +$RootFolder = "C:\Packages\Plugins\" +$WVDAgentInstaller = $RootFolder+"WVD-Agent.msi" +$WVDBootLoaderInstaller = $RootFolder+"WVD-BootLoader.msi" + +#Create Folder structure +if (!(Test-Path -Path $RootFolder)){New-Item -Path $RootFolder -ItemType Directory} + +#Download and Import Modules +install-packageProvider -Name NuGet -MinimumVErsion 2.8.5.201 -force +Install-Module -Name Az.DesktopVirtualization -AllowClobber -Force +Import-Module -Name Az.DesktopVirtualization + +#Configure logging +function log +{ + param([string]$message) + "`n`n$(get-date -f o) $message" +} + +#Create ServicePrincipal Credential +log "Creating credentials" +$ServicePrincipalCreds = New-Object System.Management.Automation.PSCredential($servicePrincipalApplicationID, (ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force)) + +#Download all source file async and wait for completion +log "Download WVD Agent & bootloader" +$files = @( + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv"; path = $WVDAgentInstaller} + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"; path = $WVDBootLoaderInstaller} +) +$workers = foreach ($f in $files) +{ + $wc = New-Object System.Net.WebClient + Write-Output $wc.DownloadFileTaskAsync($f.url, $f.path) +} +$workers.Result + +#Authenticatie against the WVD Tenant +log "Authenticatie against the WVD Tenant" +Connect-AzAccount -ServicePrincipal -Credential $ServicePrincipalCreds -Tenant $azureADTenantID + +#Obtain RdsRegistrationInfotoken +log "Obtain RdsRegistrationInfotoken" +$Registered = Get-AzWvdRegistrationInfo -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -HostPoolName $existingWVDHostPoolName +if (-not(-Not $Registered.Token)){$registrationTokenValidFor = (NEW-TIMESPAN -Start (get-date) -End $Registered.ExpirationTime | select Days,Hours,Minutes,Seconds)} +log "Token is valid for:$registrationTokenValidFor" +if ((-Not $Registered.Token) -or ($Registered.ExpirationTime -le (get-date))) +{ + $Registered = New-AzWvdRegistrationInfo -SubscriptionId $azureSubscriptionID -ResourceGroupName $resourceGroupName -HostPoolName $existingWVDHostPoolName -ExpirationTime (Get-Date).AddHours(24) -ErrorAction SilentlyContinue +} +$RdsRegistrationInfotoken = $Registered.Token + +Log "Calling functions to install agent and bootloader" +InstallRDAgents -AgentBootServiceInstallerFolder $RootFolder -AgentInstallerFolder $RootFolder -RegistrationToken $RdsRegistrationInfotoken -EnableVerboseMsiLogging:$false + +#Set WVD Session Host in drain mode +if ($drainmode -eq "Yes") +{ + #Wait 1 minute to let the WVD host register before configuring Drain mode + Start-sleep 60 + Log "Set WVD Session Host in drain mode" + $CurrentHostName = [System.Net.Dns]::GetHostByName($env:computerName).hostname + Update-AzWvdSessionHost -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -HostPoolName $existingWVDHostPoolName -Name $CurrentHostName -AllowNewSession:$false +} + +#Create Workspace-AppGroup Association +if ($createWorkspaceAppGroupAsso -eq "Yes") +{ + Update-AzWvdWorkspace -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -Name $existingWVDWorkspaceName -ApplicationGroupReference (Get-AzWvdApplicationGroup -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -Name $existingWVDAppGroupName | select id).id +} + +Log "Finished" diff --git a/Add-WVDHostToHostpoolSpringV5.ps1 b/Add-WVDHostToHostpoolSpringV5.ps1 new file mode 100644 index 0000000..985c2f7 --- /dev/null +++ b/Add-WVDHostToHostpoolSpringV5.ps1 @@ -0,0 +1,244 @@ +<# +.SYNOPSIS + Adds an WVD Session Host to an existing WVD Hostpool using a provided registrationKey *** SPRING UPDATE 2020*** +.DESCRIPTION + This scripts adds an WVD Session Host to an existing WVD Hostpool by performing the following action: + - Download the WVD agent + - Download the WVD Boot Loader + - Install the WVD Agent, using the provided hostpoolRegistrationToken + - Install the WVD Boot Loader + The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. + V1 of this script generates its own host pool registrationkey, this V2 version accepts the registrationkey as a parameter +.NOTES + File Name : add-WVDHostToHostpoolSpringV5.ps1 + Author : Freek Berson - Wortell - RDSGurus + Version : v1.0.0 +.EXAMPLE + .\add-WVDHostToHostpoolSpringV5.ps1 ` + registrationKey ` + adDomainName ` + domainJoinUPN ` + domainJoinPassword ` + ouPath ` + >> \add-WVDHostToHostpoolSpringV2.log +.DISCLAIMER + Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied + warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk + arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved + in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss + of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability + to use the this script. +#> + +#Get Parameters +$registrationKey = $args[0] +$adDomainName = $args[1] +$domainJoinUPN = $args[2] +$domainJoinPassword = $args[3] +$ouPath = $args[4] +$creds = New-Object System.Management.Automation.PSCredential($domainJoinUPN,(ConvertTo-SecureString $domainJoinPassword -AsPlainText -Force)) + + +#Set Variables +$RootFolder = "C:\Packages\Plugins\" +$WVDAgentInstaller = $RootFolder+"WVD-Agent.msi" +$WVDBootLoaderInstaller = $RootFolder+"WVD-BootLoader.msi" + +<# +.DESCRIPTION +Runs defined msi's to deploy RDAgent and Bootloader +.PARAMETER programDisplayName +.PARAMETER argumentList +.PARAMETER msiOutputLogPath +.PARAMETER isUninstall +.PARAMETER msiLogVerboseOutput +#> +function RunMsiWithRetry { + param( + [Parameter(mandatory = $true)] + [string]$programDisplayName, + + [Parameter(mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String[]]$argumentList, + + [Parameter(mandatory = $true)] + [string]$msiOutputLogPath, + + [Parameter(mandatory = $false)] + [switch]$isUninstall, + + [Parameter(mandatory = $false)] + [switch]$msiLogVerboseOutput + ) + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + if ($msiLogVerboseOutput) { + $argumentList += "/l*vx+ ""$msiOutputLogPath""" + } + else { + $argumentList += "/l*+ ""$msiOutputLogPath""" + } + + $retryTimeToSleepInSec = 30 + $retryCount = 0 + $sts = $null + do { + $modeAndDisplayName = ($(if ($isUninstall) { "Uninstalling" } else { "Installing" }) + " $programDisplayName") + + if ($retryCount -gt 0) { + Log "Retrying $modeAndDisplayName in $retryTimeToSleepInSec seconds because it failed with Exit code=$sts This will be retry number $retryCount" + Start-Sleep -Seconds $retryTimeToSleepInSec + } + + Log ( "$modeAndDisplayName" + $(if ($msiLogVerboseOutput) { " with verbose msi logging" } else { "" })) + + + $processResult = Start-Process -FilePath "msiexec.exe" -ArgumentList $argumentList -Wait -Passthru + $sts = $processResult.ExitCode + + $retryCount++ + } + while ($sts -eq 1618 -and $retryCount -lt 20) + + if ($sts -eq 1618) { + Log "Stopping retries for $modeAndDisplayName. The last attempt failed with Exit code=$sts which is ERROR_INSTALL_ALREADY_RUNNING" + throw "Stopping because $modeAndDisplayName finished with Exit code=$sts" + } + else { + Log "$modeAndDisplayName finished with Exit code=$sts" + } + + return $sts +} + +<# +.DESCRIPTION +Uninstalls any existing RDAgent BootLoader and RD Infra Agent installations and then installs the RDAgent BootLoader and RD Infra Agent using the specified registration token. +.PARAMETER AgentInstallerFolder +Required path to MSI installer file +.PARAMETER AgentBootServiceInstallerFolder +Required path to MSI installer file +#> +function InstallRDAgents { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentBootServiceInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RegistrationToken, + + [Parameter(mandatory = $false)] + [switch]$EnableVerboseMsiLogging + ) + + $ErrorActionPreference = "Stop" + + Log "Boot loader folder is $AgentBootServiceInstallerFolder" + $AgentBootServiceInstaller = $AgentBootServiceInstallerFolder + '\WVD-BootLoader.msi' + + Log "Agent folder is $AgentInstallerFolder" + $AgentInstaller = $AgentInstallerFolder + '\WVD-Agent.msi' + + if (!$RegistrationToken) { + throw "No registration token specified" + } + + $msiNamesToUninstall = @( + @{ msiName = "Remote Desktop Services Infrastructure Agent"; displayName = "RD Infra Agent"; logPath = "$AgentInstallerFolder\AgentUninstall.txt"}, + @{ msiName = "Remote Desktop Agent Boot Loader"; displayName = "RDAgentBootLoader"; logPath = "$AgentInstallerFolder\AgentBootLoaderUnInstall.txt"} + ) + + foreach($u in $msiNamesToUninstall) { + while ($true) { + try { + $installedMsi = Get-Package -ProviderName msi -Name $u.msiName + } + catch { + if ($PSItem.FullyQualifiedErrorId -eq "NoMatchFound,Microsoft.PowerShell.PackageManagement.Cmdlets.GetPackage") { + break + } + + throw; + } + + $oldVersion = $installedMsi.Version + $productCodeParameter = $installedMsi.FastPackageReference + + RunMsiWithRetry -programDisplayName "$($u.displayName) $oldVersion" -isUninstall -argumentList @("/x $productCodeParameter", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath $u.logPath -msiLogVerboseOutput:$EnableVerboseMsiLogging + } + } + + Log "Installing RD Infra Agent on VM $AgentInstaller" + RunMsiWithRetry -programDisplayName "RD Infra Agent" -argumentList @("/i $AgentInstaller", "/quiet", "/qn", "/norestart", "/passive", "REGISTRATIONTOKEN=$RegistrationToken") -msiOutputLogPath "C:\Users\AgentInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + Log "Installing RDAgent BootLoader on VM $AgentBootServiceInstaller" + RunMsiWithRetry -programDisplayName "RDAgent BootLoader" -argumentList @("/i $AgentBootServiceInstaller", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath "C:\Users\AgentBootLoaderInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + $bootloaderServiceName = "RDAgentBootLoader" + $startBootloaderRetryCount = 0 + while ( -not (Get-Service $bootloaderServiceName -ErrorAction SilentlyContinue)) { + $retry = ($startBootloaderRetryCount -lt 6) + $msgToWrite = "Service $bootloaderServiceName was not found. " + if ($retry) { + $msgToWrite += "Retrying again in 30 seconds, this will be retry $startBootloaderRetryCount" + Log $msgToWrite + } + else { + $msgToWrite += "Retry limit exceeded" + Log $msgToWrite + throw $msgToWrite + } + + $startBootloaderRetryCount++ + Start-Sleep -Seconds 30 + } + + Log "Starting service $bootloaderServiceName" + Start-Service $bootloaderServiceName +} + +#Create Folder structure +if (!(Test-Path -Path $RootFolder)){New-Item -Path $RootFolder -ItemType Directory} + +#Configure logging +function log +{ + param([string]$message) + "`n`n$(get-date -f o) $message" +} + +#Download all source file async and wait for completion +log "Download WVD Agent & bootloader" +$files = @( + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv"; path = $WVDAgentInstaller} + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"; path = $WVDBootLoaderInstaller} +) +$workers = foreach ($f in $files) +{ + $wc = New-Object System.Net.WebClient + Write-Output $wc.DownloadFileTaskAsync($f.url, $f.path) +} +$workers.Result + +Log "Calling functions to install agent and bootloader" +InstallRDAgents -AgentBootServiceInstallerFolder $RootFolder -AgentInstallerFolder $RootFolder -RegistrationToken $registrationKey -EnableVerboseMsiLogging:$false + +log "Wait for IsRegistered status" +$isRegistered = 0 +DO { + $isRegistered = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\RDInfraAgent' -Name 'Isregistered' -ErrorAction Ignore).IsRegistered + Start-Sleep -Seconds 15 +} While ($isRegistered -ne 1) + +log "Join host to domain" +Add-Computer -DomainName $adDomainName -Credential $creds -OUPath $ouPath -Restart -Force + +Log "Finished" diff --git a/Add-WVDHostToHostpoolSpringV6.ps1 b/Add-WVDHostToHostpoolSpringV6.ps1 new file mode 100644 index 0000000..77cb114 --- /dev/null +++ b/Add-WVDHostToHostpoolSpringV6.ps1 @@ -0,0 +1,292 @@ +<# +.SYNOPSIS + Adds an WVD Session Host to an existing WVD Hostpool *** SPRING UPDATE 2020*** +.DESCRIPTION + This scripts adds an WVD Session Host to an existing WVD Hostpool by performing the following action: + - Download the WVD agent + - Download the WVD Boot Loader + - Install the WVD Agent, using the provided hostpoolRegistrationToken + - Install the WVD Boot Loader + - Set the WVD Host into drain mode (optionally) + - Create the Workspace <-> App Group Association (optionally) + The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. +.NOTES + File Name : add-WVDHostToHostpoolSpringV6.ps1 + Author : Freek Berson - Wortell - RDSGurus + Version : v1.0.0 +.EXAMPLE + .\add-WVDHostToHostpoolSpringV3.ps11 existingWVDWorkspaceName existingWVDHostPoolName ` + existingWVDAppGroupName servicePrincipalApplicationID servicePrincipalPassword azureADTenantID + resourceGroupName azureSubscriptionID Drainmode createWorkspaceAppGroupAsso adDomainName domainJoinUPN domainJoinPassword ouPath ` + >> \dd-WVDHostToHostpoolSpring.log +.DISCLAIMER + Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied + warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk + arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved + in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss + of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability + to use the this script. +#> + + +<# +.DESCRIPTION +Runs defined msi's to deploy RDAgent and Bootloader +.PARAMETER programDisplayName +.PARAMETER argumentList +.PARAMETER msiOutputLogPath +.PARAMETER isUninstall +.PARAMETER msiLogVerboseOutput +#> +function RunMsiWithRetry { + param( + [Parameter(mandatory = $true)] + [string]$programDisplayName, + + [Parameter(mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String[]]$argumentList, + + [Parameter(mandatory = $true)] + [string]$msiOutputLogPath, + + [Parameter(mandatory = $false)] + [switch]$isUninstall, + + [Parameter(mandatory = $false)] + [switch]$msiLogVerboseOutput + ) + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + if ($msiLogVerboseOutput) { + $argumentList += "/l*vx+ ""$msiOutputLogPath""" + } + else { + $argumentList += "/l*+ ""$msiOutputLogPath""" + } + + $retryTimeToSleepInSec = 30 + $retryCount = 0 + $sts = $null + do { + $modeAndDisplayName = ($(if ($isUninstall) { "Uninstalling" } else { "Installing" }) + " $programDisplayName") + + if ($retryCount -gt 0) { + Log -Message "Retrying $modeAndDisplayName in $retryTimeToSleepInSec seconds because it failed with Exit code=$sts This will be retry number $retryCount" + Start-Sleep -Seconds $retryTimeToSleepInSec + } + + Log -Message ( "$modeAndDisplayName" + $(if ($msiLogVerboseOutput) { " with verbose msi logging" } else { "" })) + + + $processResult = Start-Process -FilePath "msiexec.exe" -ArgumentList $argumentList -Wait -Passthru + $sts = $processResult.ExitCode + + $retryCount++ + } + while ($sts -eq 1618 -and $retryCount -lt 20) + + if ($sts -eq 1618) { + Log -Err "Stopping retries for $modeAndDisplayName. The last attempt failed with Exit code=$sts which is ERROR_INSTALL_ALREADY_RUNNING" + throw "Stopping because $modeAndDisplayName finished with Exit code=$sts" + } + else { + Log -Message "$modeAndDisplayName finished with Exit code=$sts" + } + + return $sts +} + +<# +.DESCRIPTION +Uninstalls any existing RDAgent BootLoader and RD Infra Agent installations and then installs the RDAgent BootLoader and RD Infra Agent using the specified registration token. +.PARAMETER AgentInstallerFolder +Required path to MSI installer file +.PARAMETER AgentBootServiceInstallerFolder +Required path to MSI installer file +#> +function InstallRDAgents { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentBootServiceInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RegistrationToken, + + [Parameter(mandatory = $false)] + [switch]$EnableVerboseMsiLogging + ) + + $ErrorActionPreference = "Stop" + + Log -Message "Boot loader folder is $AgentBootServiceInstallerFolder" + $AgentBootServiceInstaller = $AgentBootServiceInstallerFolder + '\WVD-BootLoader.msi' + + Log -Message "Agent folder is $AgentInstallerFolder" + $AgentInstaller = $AgentInstallerFolder + '\WVD-Agent.msi' + + if (!$RegistrationToken) { + throw "No registration token specified" + } + + $msiNamesToUninstall = @( + @{ msiName = "Remote Desktop Services Infrastructure Agent"; displayName = "RD Infra Agent"; logPath = "$AgentInstallerFolder\AgentUninstall.txt"}, + @{ msiName = "Remote Desktop Agent Boot Loader"; displayName = "RDAgentBootLoader"; logPath = "$AgentInstallerFolder\AgentBootLoaderUnInstall.txt"} + ) + + foreach($u in $msiNamesToUninstall) { + while ($true) { + try { + $installedMsi = Get-Package -ProviderName msi -Name $u.msiName + } + catch { + if ($PSItem.FullyQualifiedErrorId -eq "NoMatchFound,Microsoft.PowerShell.PackageManagement.Cmdlets.GetPackage") { + break + } + + throw; + } + + $oldVersion = $installedMsi.Version + $productCodeParameter = $installedMsi.FastPackageReference + + RunMsiWithRetry -programDisplayName "$($u.displayName) $oldVersion" -isUninstall -argumentList @("/x $productCodeParameter", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath $u.logPath -msiLogVerboseOutput:$EnableVerboseMsiLogging + } + } + + Log -Message "Installing RD Infra Agent on VM $AgentInstaller" + RunMsiWithRetry -programDisplayName "RD Infra Agent" -argumentList @("/i $AgentInstaller", "/quiet", "/qn", "/norestart", "/passive", "REGISTRATIONTOKEN=$RegistrationToken") -msiOutputLogPath "C:\Users\AgentInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + Log -Message "Installing RDAgent BootLoader on VM $AgentBootServiceInstaller" + RunMsiWithRetry -programDisplayName "RDAgent BootLoader" -argumentList @("/i $AgentBootServiceInstaller", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath "C:\Users\AgentBootLoaderInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + $bootloaderServiceName = "RDAgentBootLoader" + $startBootloaderRetryCount = 0 + while ( -not (Get-Service $bootloaderServiceName -ErrorAction SilentlyContinue)) { + $retry = ($startBootloaderRetryCount -lt 6) + $msgToWrite = "Service $bootloaderServiceName was not found. " + if ($retry) { + $msgToWrite += "Retrying again in 30 seconds, this will be retry $startBootloaderRetryCount" + Log -Message $msgToWrite + } + else { + $msgToWrite += "Retry limit exceeded" + Log -Err $msgToWrite + throw $msgToWrite + } + + $startBootloaderRetryCount++ + Start-Sleep -Seconds 30 + } + + Log -Message "Starting service $bootloaderServiceName" + Start-Service $bootloaderServiceName +} + +#Get Parameters +$existingWVDWorkspaceName = $args[0] +$existingWVDHostPoolName = $args[1] +$existingWVDAppGroupName = $args[2] +$servicePrincipalApplicationID = $args[3] +$servicePrincipalPassword = $args[4] +$azureADTenantID = $args[5] +$resourceGroupName = $args[6] +$azureSubscriptionID = $args[7] +$drainmode = $args[8] +$createWorkspaceAppGroupAsso = $args[9] +$adDomainName = $args[10] +$domainJoinUPN = $args[11] +$domainJoinPassword = $args[12] +$ouPath = $args[13] +$creds = New-Object System.Management.Automation.PSCredential($domainJoinUPN,(ConvertTo-SecureString $domainJoinPassword -AsPlainText -Force)) + + +#Set Variables +$RootFolder = "C:\Packages\Plugins\" +$WVDAgentInstaller = $RootFolder+"WVD-Agent.msi" +$WVDBootLoaderInstaller = $RootFolder+"WVD-BootLoader.msi" + +#Create Folder structure +if (!(Test-Path -Path $RootFolder)){New-Item -Path $RootFolder -ItemType Directory} + +#Download and Import Modules +install-packageProvider -Name NuGet -MinimumVErsion 2.8.5.201 -force +Install-Module -Name Az.DesktopVirtualization -AllowClobber -Force +Import-Module -Name Az.DesktopVirtualization + +#Configure logging +function log +{ + param([string]$message) + "`n`n$(get-date -f o) $message" +} + +#Create ServicePrincipal Credential +log "Creating credentials" +$ServicePrincipalCreds = New-Object System.Management.Automation.PSCredential($servicePrincipalApplicationID, (ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force)) + +#Download all source file async and wait for completion +log "Download WVD Agent & bootloader" +$files = @( + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv"; path = $WVDAgentInstaller} + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"; path = $WVDBootLoaderInstaller} +) +$workers = foreach ($f in $files) +{ + $wc = New-Object System.Net.WebClient + Write-Output $wc.DownloadFileTaskAsync($f.url, $f.path) +} +$workers.Result + +#Authenticatie against the WVD Tenant +log "Authenticatie against the WVD Tenant" +Connect-AzAccount -ServicePrincipal -Credential $ServicePrincipalCreds -Tenant $azureADTenantID + +#Obtain RdsRegistrationInfotoken +log "Obtain RdsRegistrationInfotoken" +$Registered = Get-AzWvdRegistrationInfo -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -HostPoolName $existingWVDHostPoolName +if (-not(-Not $Registered.Token)){$registrationTokenValidFor = (NEW-TIMESPAN -Start (get-date) -End $Registered.ExpirationTime | select Days,Hours,Minutes,Seconds)} +log "Token is valid for:$registrationTokenValidFor" +if ((-Not $Registered.Token) -or ($Registered.ExpirationTime -le (get-date))) +{ + $Registered = New-AzWvdRegistrationInfo -SubscriptionId $azureSubscriptionID -ResourceGroupName $resourceGroupName -HostPoolName $existingWVDHostPoolName -ExpirationTime (Get-Date).AddHours(4) -ErrorAction SilentlyContinue +} +$RdsRegistrationInfotoken = $Registered.Token + +Log "Calling functions to install agent and bootloader" +InstallRDAgents -AgentBootServiceInstallerFolder $RootFolder -AgentInstallerFolder $RootFolder -RegistrationToken $RdsRegistrationInfotoken -EnableVerboseMsiLogging:$false + +#Set WVD Session Host in drain mode +if ($drainmode -eq "Yes") +{ + #Wait 1 minute to let the WVD host register before configuring Drain mode + Start-sleep 60 + Log "Set WVD Session Host in drain mode" + $CurrentHostName = [System.Net.Dns]::GetHostByName($env:computerName).hostname + Update-AzWvdSessionHost -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -HostPoolName $existingWVDHostPoolName -Name $CurrentHostName -AllowNewSession:$false +} + +#Create Workspace-AppGroup Association +if ($createWorkspaceAppGroupAsso -eq "Yes") +{ + Update-AzWvdWorkspace -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -Name $existingWVDWorkspaceName -ApplicationGroupReference (Get-AzWvdApplicationGroup -SubscriptionId "$azureSubscriptionID" -ResourceGroupName "$resourceGroupName" -Name $existingWVDAppGroupName | select id).id +} + +log "Wait for IsRegistered status" +$isRegistered = 0 +DO { + $isRegistered = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\RDInfraAgent' -Name 'Isregistered' -ErrorAction Ignore).IsRegistered + Start-Sleep -Seconds 15 +} While ($isRegistered -ne 1) + +log "Join host to domain" +Add-Computer -DomainName $adDomainName -Credential $creds -OUPath $ouPath -Restart -Force + +Log "Finished" diff --git a/Add-WVDHostToHostpoolSpringV7.ps1 b/Add-WVDHostToHostpoolSpringV7.ps1 new file mode 100644 index 0000000..6d0ef7f --- /dev/null +++ b/Add-WVDHostToHostpoolSpringV7.ps1 @@ -0,0 +1,225 @@ +<# +.SYNOPSIS + Adds an WVD Session Host to an existing WVD Hostpool using a provided registrationKey *** SPRING UPDATE 2020*** +.DESCRIPTION + This scripts adds an WVD Session Host to an existing WVD Hostpool by performing the following action: + - Download the WVD agent + - Download the WVD Boot Loader + - Install the WVD Agent, using the provided hostpoolRegistrationToken + - Install the WVD Boot Loader + The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. + V1 of this script generates its own host pool registrationkey, this V2 version accepts the registrationkey as a parameter +.NOTES + File Name : add-WVDHostToHostpoolSpringV7.ps1 + Author : Freek Berson - Wortell - RDSGurus + Version : v1.0.0 +.EXAMPLE + .\add-WVDHostToHostpoolSpringV7.ps1 registrationKey >> \add-WVDHostToHostpoolSpringV7.log +.DISCLAIMER + Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied + warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk + arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved + in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss + of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability + to use the this script. +#> + +#Get Parameters +$registrationKey = $args[0] + +#Set Variables +$RootFolder = "C:\Packages\Plugins\" +$WVDAgentInstaller = $RootFolder+"WVD-Agent.msi" +$WVDBootLoaderInstaller = $RootFolder+"WVD-BootLoader.msi" + +<# +.DESCRIPTION +Runs defined msi's to deploy RDAgent and Bootloader +.PARAMETER programDisplayName +.PARAMETER argumentList +.PARAMETER msiOutputLogPath +.PARAMETER isUninstall +.PARAMETER msiLogVerboseOutput +#> +function RunMsiWithRetry { + param( + [Parameter(mandatory = $true)] + [string]$programDisplayName, + + [Parameter(mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String[]]$argumentList, + + [Parameter(mandatory = $true)] + [string]$msiOutputLogPath, + + [Parameter(mandatory = $false)] + [switch]$isUninstall, + + [Parameter(mandatory = $false)] + [switch]$msiLogVerboseOutput + ) + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + if ($msiLogVerboseOutput) { + $argumentList += "/l*vx+ ""$msiOutputLogPath""" + } + else { + $argumentList += "/l*+ ""$msiOutputLogPath""" + } + + $retryTimeToSleepInSec = 30 + $retryCount = 0 + $sts = $null + do { + $modeAndDisplayName = ($(if ($isUninstall) { "Uninstalling" } else { "Installing" }) + " $programDisplayName") + + if ($retryCount -gt 0) { + Log "Retrying $modeAndDisplayName in $retryTimeToSleepInSec seconds because it failed with Exit code=$sts This will be retry number $retryCount" + Start-Sleep -Seconds $retryTimeToSleepInSec + } + + Log ( "$modeAndDisplayName" + $(if ($msiLogVerboseOutput) { " with verbose msi logging" } else { "" })) + + + $processResult = Start-Process -FilePath "msiexec.exe" -ArgumentList $argumentList -Wait -Passthru + $sts = $processResult.ExitCode + + $retryCount++ + } + while ($sts -eq 1618 -and $retryCount -lt 20) + + if ($sts -eq 1618) { + Log "Stopping retries for $modeAndDisplayName. The last attempt failed with Exit code=$sts which is ERROR_INSTALL_ALREADY_RUNNING" + throw "Stopping because $modeAndDisplayName finished with Exit code=$sts" + } + else { + Log "$modeAndDisplayName finished with Exit code=$sts" + } + + return $sts +} + +<# +.DESCRIPTION +Uninstalls any existing RDAgent BootLoader and RD Infra Agent installations and then installs the RDAgent BootLoader and RD Infra Agent using the specified registration token. +.PARAMETER AgentInstallerFolder +Required path to MSI installer file +.PARAMETER AgentBootServiceInstallerFolder +Required path to MSI installer file +#> +function InstallRDAgents { + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AgentBootServiceInstallerFolder, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RegistrationToken, + + [Parameter(mandatory = $false)] + [switch]$EnableVerboseMsiLogging + ) + + $ErrorActionPreference = "Stop" + + Log "Boot loader folder is $AgentBootServiceInstallerFolder" + $AgentBootServiceInstaller = $AgentBootServiceInstallerFolder + '\WVD-BootLoader.msi' + + Log "Agent folder is $AgentInstallerFolder" + $AgentInstaller = $AgentInstallerFolder + '\WVD-Agent.msi' + + if (!$RegistrationToken) { + throw "No registration token specified" + } + + $msiNamesToUninstall = @( + @{ msiName = "Remote Desktop Services Infrastructure Agent"; displayName = "RD Infra Agent"; logPath = "$AgentInstallerFolder\AgentUninstall.txt"}, + @{ msiName = "Remote Desktop Agent Boot Loader"; displayName = "RDAgentBootLoader"; logPath = "$AgentInstallerFolder\AgentBootLoaderUnInstall.txt"} + ) + + foreach($u in $msiNamesToUninstall) { + while ($true) { + try { + $installedMsi = Get-Package -ProviderName msi -Name $u.msiName + } + catch { + if ($PSItem.FullyQualifiedErrorId -eq "NoMatchFound,Microsoft.PowerShell.PackageManagement.Cmdlets.GetPackage") { + break + } + + throw; + } + + $oldVersion = $installedMsi.Version + $productCodeParameter = $installedMsi.FastPackageReference + + RunMsiWithRetry -programDisplayName "$($u.displayName) $oldVersion" -isUninstall -argumentList @("/x $productCodeParameter", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath $u.logPath -msiLogVerboseOutput:$EnableVerboseMsiLogging + } + } + + Log "Installing RD Infra Agent on VM $AgentInstaller" + RunMsiWithRetry -programDisplayName "RD Infra Agent" -argumentList @("/i $AgentInstaller", "/quiet", "/qn", "/norestart", "/passive", "REGISTRATIONTOKEN=$RegistrationToken") -msiOutputLogPath "C:\Users\AgentInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + Log "Installing RDAgent BootLoader on VM $AgentBootServiceInstaller" + RunMsiWithRetry -programDisplayName "RDAgent BootLoader" -argumentList @("/i $AgentBootServiceInstaller", "/quiet", "/qn", "/norestart", "/passive") -msiOutputLogPath "C:\Users\AgentBootLoaderInstall.txt" -msiLogVerboseOutput:$EnableVerboseMsiLogging + + $bootloaderServiceName = "RDAgentBootLoader" + $startBootloaderRetryCount = 0 + while ( -not (Get-Service $bootloaderServiceName -ErrorAction SilentlyContinue)) { + $retry = ($startBootloaderRetryCount -lt 6) + $msgToWrite = "Service $bootloaderServiceName was not found. " + if ($retry) { + $msgToWrite += "Retrying again in 30 seconds, this will be retry $startBootloaderRetryCount" + Log $msgToWrite + } + else { + $msgToWrite += "Retry limit exceeded" + Log $msgToWrite + throw $msgToWrite + } + + $startBootloaderRetryCount++ + Start-Sleep -Seconds 30 + } + + Log "Starting service $bootloaderServiceName" + Start-Service $bootloaderServiceName +} + +#Create Folder structure +if (!(Test-Path -Path $RootFolder)){New-Item -Path $RootFolder -ItemType Directory} + +#Configure logging +function log +{ + param([string]$message) + "`n`n$(get-date -f o) $message" +} + +log $registrationKey +Log "registrationKey: $registrationKey" + +#Download all source file async and wait for completion +log "Download WVD Agent & bootloader" +$files = @( + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv"; path = $WVDAgentInstaller} + @{url = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"; path = $WVDBootLoaderInstaller} +) +$workers = foreach ($f in $files) +{ + $wc = New-Object System.Net.WebClient + Write-Output $wc.DownloadFileTaskAsync($f.url, $f.path) +} +$workers.Result + +Log "Calling functions to install agent and bootloader" +InstallRDAgents -AgentBootServiceInstallerFolder $RootFolder -AgentInstallerFolder $RootFolder -RegistrationToken $registrationKey -EnableVerboseMsiLogging:$false + +Log "Finished" diff --git a/Create-MSIXAppAttachContainer.ps1 b/MSIX app attach/Create-MSIXAppAttachContainer.ps1 similarity index 57% rename from Create-MSIXAppAttachContainer.ps1 rename to MSIX app attach/Create-MSIXAppAttachContainer.ps1 index 6ae67c7..1c412d9 100644 --- a/Create-MSIXAppAttachContainer.ps1 +++ b/MSIX app attach/Create-MSIXAppAttachContainer.ps1 @@ -1,78 +1,77 @@ -<# -.SYNOPSIS - Creates an MSIX app attach (vhd) container for a given MSIX application -.DESCRIPTION - This scripts creates an MSIX app attach (vhd) container for a given MSIX files by: - - Creating a new VHD disk - - Initializing the disk - - Creating a partition on the disk - - Formatting the partition, including a customized label - - Creating the MSIX parent folder - - Extracting the MSIX into the parent folder on the mounted disk - - Output the Volume ID and Package name needed for the Staging step of MSIX app attach - - Dismount the disk - -.NOTES - File Name : create-MSIXAppAttachContainer.ps1 - Author : Freek Berson - Wortell - Version : v1.0.3 -.EXAMPLE - .\create-MSIXAppAttachContainer.ps1 -.DISCLAIMER - Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied - warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk - arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved - in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss - of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability - to use the this script. -#> - -#Variables (below are sample values, change them accordingly before running the script) -$MSIXSourceLocation = "C:\Install\MSIX Sources\" -$MSIXSourceFile = "GoogleChromev67_67.228.49301.0_x64__584h4tg0qqenr.msix" -$MSIXappattachContainerSizeMb = 1000MB -$MSIXappattachContainerLabel = "GoogleChrome" -$MSIXappattachContainerRootFolder = "GoogleChrome" -$MSIXMGRLocation = "C:\Program Files\msixmgr\" -$MSIXappattachContainerExtension = ".vhd" - -CLS -$Starttime = get-date - -#Create new VHD -if (Test-Path -path ($MSIXSourceLocation+($MSIXSourceFile.Substring(0,$MSIXSourceFile.Length-5))+$MSIXappattachContainerExtension) -PathType Leaf){Write-Host "VHD Already exists, exiting" -ForegroundColor Red;return} -New-VHD -SizeBytes $MSIXappattachContainerSizeMb -Path ($MSIXSourceLocation+($MSIXSourceFile.Substring(0,$MSIXSourceFile.Length-5))+$MSIXappattachContainerExtension) -Dynamic -Confirm:$false - -#Mount the VHD -$vhdObject = Mount-VHD ($MSIXSourceLocation+($MSIXSourceFile.Substring(0,$MSIXSourceFile.Length-5))+$MSIXappattachContainerExtension) -Passthru - -#Initialize disk -$disk = Initialize-Disk -Passthru -Number $vhdObject.Number - -#Create parition -$partition = New-Partition -AssignDriveLetter -UseMaximumSize -DiskNumber $disk.Number - -#Format Partition -Format-Volume -FileSystem NTFS -Confirm:$false -DriveLetter $partition.DriveLetter -NewFileSystemLabel $MSIXappattachContainerLabel -Force - -#Create MSIX Root Folder -New-Item -Path ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) -ItemType Directory - -#Extract the MSIX into the app attach container (vhd) -$msixmgRresult = Start-Process -FilePath ("`"" + $MSIXMGRLocation + "msixmgr.exe" +"`"") -ArgumentList ("-Unpack -packagePath `"" + ($MSIXSourceLocation+$MSIXSourceFile) + "`" -destination `"" + ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) +"`" -applyacls") -Wait | Wait-process - -#Grab the Volume info needed for the Staging part of MSIX app attach -Write-Host "Completed transforming:"$MSIXSourceFile -ForegroundColor green -Write-Host "Disk Volume ID:" ((Get-Volume -DriveLetter $partition.DriveLetter | select UniqueId).UniqueId | out-string).Substring(11,((Get-Volume -DriveLetter $partition.DriveLetter | select UniqueId).UniqueId.Length-13)) -ForegroundColor Cyan -Write-Host "Package Name:" (Get-ChildItem -Path ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) | select Name).name -ForegroundColor Cyan - -#Dismount the VHD -Dismount-VHD ($MSIXSourceLocation+($MSIXSourceFile.Substring(0,$MSIXSourceFile.Length-5))+$MSIXappattachContainerExtension) -Write-Host "Disk is dismounted and ready" - -Write-Host "Finished! Total transformation time:"((get-date) - $Starttime).Minutes "Minute(s) and" ((get-date) - $Starttime).seconds "Seconds." - - - - - +<# +.SYNOPSIS + Creates an MSIX app attach (vhd) container for a given MSIX application +.DESCRIPTION + This scripts creates an MSIX app attach (vhd) container for a given MSIX files by: + - Creating a new VHD disk + - Initializing the disk + - Creating a partition on the disk + - Formatting the partition, including a customized label + - Creating the MSIX parent folder + - Extracting the MSIX into the parent folder on the mounted disk + - Output the Volume ID and Package name needed for the Staging step of MSIX app attach + - Dismount the disk + +.NOTES + File Name : create-MSIXAppAttachContainer.ps1 + Author : Freek Berson - Wortell + Version : v1.0.3 +.EXAMPLE + .\create-MSIXAppAttachContainer.ps1 +.DISCLAIMER + Use at your own risk. This scripts are provided AS IS without warranty of any kind. The author further disclaims all implied + warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk + arising out of the use or performance of the scripts and documentation remains with you. In no event shall the author, or anyone else involved + in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss + of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability + to use the this script. +#> + +#Variables (below are sample values, change them accordingly before running the script) +$MSIXSourceLocation = "C:\MSIX\2.MSIX-packages\" +$MSIXAppAttachDestinationPath = "C:\MSIX\3.MSIX-app-attach\" +$MSIXSourceFiles = @("NotepadPlusPlus.msix") +$MSIXappattachContainerSizeMb = 100MB +$MSIXappattachContainerLabel = "NotepadPlusplus" +$MSIXappattachContainerRootFolder = "NotepadPlusplus" +$MSIXMGRLocation = "C:\Program Files\msixmgr\x64\" +$MSIXappattachContainerExtension = ".vhd" + +Clear-Host +$Starttime = get-date + +#Create new VHD +if (Test-Path -path ($MSIXAppAttachDestinationPath+$MSIXappattachContainerLabel+$MSIXappattachContainerExtension) -PathType Leaf){Write-Host "VHD Already exists, exiting" -ForegroundColor Red;return} +New-VHD -SizeBytes $MSIXappattachContainerSizeMb -Path ($MSIXAppAttachDestinationPath+$MSIXappattachContainerLabel+$MSIXappattachContainerExtension) -Dynamic -Confirm:$false + +#Mount the VHD +$vhdObject = Mount-VHD ($MSIXAppAttachDestinationPath+$MSIXappattachContainerLabel+$MSIXappattachContainerExtension) -Passthru + +#Initialize disk +$disk = Initialize-Disk -Passthru -Number $vhdObject.Number + +#Create parition +$partition = New-Partition -AssignDriveLetter -UseMaximumSize -DiskNumber $disk.Number + +#Format Partition +Format-Volume -FileSystem NTFS -Confirm:$false -DriveLetter $partition.DriveLetter -NewFileSystemLabel $MSIXappattachContainerLabel -Force + +#Create MSIX Root Folder +New-Item -Path ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) -ItemType Directory + +#Extract the MSIX into the app attach container (vhd) +foreach ($MSIXSourceFile in $MSIXSourceFiles) +{ + $msixmgRresult = Start-Process -FilePath ("`"" + $MSIXMGRLocation + "msixmgr.exe" +"`"") -ArgumentList ("-Unpack -packagePath `"" + ($MSIXSourceLocation+$MSIXSourceFile) + "`" -destination `"" + ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) +"`" -applyacls") -Wait | Wait-process + #Grab the Volume info needed for the Staging part of MSIX app attach + Write-Host "Completed transforming:"$MSIXSourceFile -ForegroundColor green +} +Write-Host "Disk Volume ID:" ((Get-Volume -DriveLetter $partition.DriveLetter | Select-Object UniqueId).UniqueId | out-string).Substring(11,((Get-Volume -DriveLetter $partition.DriveLetter | select UniqueId).UniqueId.Length-13)) -ForegroundColor Cyan +Write-Host "Package Names:" -ForegroundColor green +(Get-ChildItem -Path ($partition.DriveLetter+":\"+$MSIXappattachContainerRootFolder) | Select-Object Name).name + +#Dismount the VHD +Dismount-VHD ($MSIXAppAttachDestinationPath+$MSIXappattachContainerLabel+$MSIXappattachContainerExtension) +Write-Host "Disk is dismounted and ready" + +Write-Host "Finished! Total transformation time:"((get-date) - $Starttime).Minutes "Minute(s) and" ((get-date) - $Starttime).seconds "Seconds." diff --git a/MSIX app attach/DeStage.ps1 b/MSIX app attach/DeStage.ps1 new file mode 100644 index 0000000..8db0570 --- /dev/null +++ b/MSIX app attach/DeStage.ps1 @@ -0,0 +1,14 @@ +#MSIX app attach de staging sample + +#region variables +$packageName = "Notepadplusplus_1.0.0.0_x64__vcbnmdqcr7aap" +$msixJunction = "C:\temp\AppAttach\" +$vhdSrc="\\share\AppAttach\NotepadPlusPlus.vhd" +#endregion + +#region deregister +Remove-AppxPackage -AllUsers -Package $packageName +cd $msixJunction +Remove-Item $packageName -Force -Recurse +dismount-vhd -Path $vhdSrc +#endregion \ No newline at end of file diff --git a/MSIX app attach/Deregister.ps1 b/MSIX app attach/Deregister.ps1 new file mode 100644 index 0000000..891115e --- /dev/null +++ b/MSIX app attach/Deregister.ps1 @@ -0,0 +1,9 @@ +#MSIX app attach deregistration sample + +#region variables +$packageName = "Notepadplusplus_1.0.0.0_x64__vcbnmdqcr7aap" +#endregion + +#region deregister +Remove-AppxPackage -PreserveRoamableApplicationData $packageName +#endregion \ No newline at end of file diff --git a/MSIX app attach/Register.ps1 b/MSIX app attach/Register.ps1 new file mode 100644 index 0000000..79ebe83 --- /dev/null +++ b/MSIX app attach/Register.ps1 @@ -0,0 +1,10 @@ +#MSIX app attach registration sample + +#region variables +$packageName = "Notepadplusplus_1.0.0.0_x64__vcbnmdqcr7aap" +$path = "C:\Program Files\WindowsApps\" + $packageName + "\AppxManifest.xml" +#endregion + +#region register +Add-AppxPackage -Path $path -DisableDevelopmentMode -Register +#endregion \ No newline at end of file diff --git a/MSIX app attach/Stage.ps1 b/MSIX app attach/Stage.ps1 new file mode 100644 index 0000000..7cbd0b5 --- /dev/null +++ b/MSIX app attach/Stage.ps1 @@ -0,0 +1,47 @@ +#MSIX app attach staging sample + +#region variables +$vhdSrc="\\share\AppAttach\NotepadPlusPLus.vhd" +$packageName = "Notepadplusplus_1.0.0.0_x64__vcbnmdqcr7aap" +$parentFolder = "NotepadPlusPLus" +$parentFolder = "\" + $parentFolder + "\" +$volumeGuid = "53e46305-4746-428e-a80f-2f8046a29e84" +$msixJunction = "C:\temp\AppAttach\" +#endregion + +#region mountvhd +try +{ +Mount-VHD -Path $vhdSrc -NoDriveLetter -ReadOnly +Write-Host ("Mounting of " + $vhdSrc + " was completed!") -BackgroundColor Green +} +catch +{ +Write-Host ("Mounting of " + $vhdSrc + " has failed!") -BackgroundColor Red +} +#endregion + +#region makelink +$msixDest = "\\?\Volume{" + $volumeGuid + "}\" +if (!(Test-Path $msixJunction)) +{ +md $msixJunction +} +$msixJunction = $msixJunction + $packageName +cmd.exe /c mklink /j $msixJunction $msixDest +#endregion + +#region stage +[Windows.Management.Deployment.PackageManager,Windows.Management.Deployment,ContentType=WindowsRuntime] | Out-Null +Add-Type -AssemblyName System.Runtime.WindowsRuntime +$asTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where { $_.ToString() -eq 'System.Threading.Tasks.Task`1[TResult] AsTask[TResult,TProgress](Windows.Foundation.IAsyncOperationWithProgress`2[TResult,TProgress])'})[0] +$asTaskAsyncOperation = +$asTask.MakeGenericMethod([Windows.Management.Deployment.DeploymentResult], +[Windows.Management.Deployment.DeploymentProgress]) +$packageManager = [Windows.Management.Deployment.PackageManager]::new() +$path = $msixJunction + $parentFolder + $packageName # needed if we do the pbisigned.vhd +$path = ([System.Uri]$path).AbsoluteUri +$asyncOperation = $packageManager.StagePackageAsync($path, $null, "StageInPlace") +$task = $asTaskAsyncOperation.Invoke($null, @($asyncOperation)) +$task +#endregion \ No newline at end of file diff --git a/README.md b/README.md index 544c8ea..b226999 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,12 @@ ---- -page_type: resources -languages: - - md - - powershell -description: | - Windows Virtual Desktop (WVD) - resources and scripts -products: - - azure - - windows-virtual-desktop ---- +# Windows Virtual Desktop related content +This repository contains Windows Virtual Desktop (WVD) related resources and scripts. You can find ARM Templates, Bicep Tenmplates and PowerShell scripts to automate and manage +your Windows Virtual Desktop environment. -# Windows Virtual Desktop (WVD) - resources and scripts + +## Descriptions -## Contents - - -| File/folder | Description | -|-------------------------------------|------------------------------------------------------------------------------------------------| -| `Add-WVDHostToHostpoolSpring.ps1` | Adds an WVD Session Host to an existing WVD Hostpool | -| `Add-WVDHostToHostpoolSpringV2.ps1` | Adds an WVD Session Host to an existing WVD Hostpool accepting the registation key as a param | -| `Create-MSIXAppAttachContainer.ps1` | Creates an MSIX app attach (vhd) container for a given MSIX application | -| `WVD-Rest-Api-Functions.ps1` | Contains functions and exmaples to get & create WVD object using REST API | -| `Create-WVD-Backplane.bicep` | To build the ARM JSON code to create WVD back plane components in Azure | -| `BicepModules` | Contains various Bicep Modules to create an WVD environment incl other components | - - -## Add-WVDHostToHostpoolSpring.ps1 +### Add-WVDHostToHostpoolSpring.ps1 This script adds an WVD Session Host to an existing WVD Hostpool by performing the following action: - Download the WVD agent - Download the WVD Boot Loader @@ -36,7 +16,7 @@ This script adds an WVD Session Host to an existing WVD Hostpool by performing t - Create the Workspace <-> App Group Association (optionally) The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. -## Add-WVDHostToHostpoolSpringV2.ps1 +### Add-WVDHostToHostpoolSpringV2.ps1 Adds an WVD Session Host to an existing WVD Hostpool using a provided registrationKey by performing the following action: - Download the WVD agent - Download the WVD Boot Loader @@ -45,7 +25,7 @@ Adds an WVD Session Host to an existing WVD Hostpool using a provided registrati The script is designed and optimized to run as PowerShell Extension as part of a JSON deployment. V1 of this script generates its own host pool registrationkey, this V2 version accepts the registrationkey as a parameter -## Create-MSIXAppAttachContainer.ps1 +### Create-MSIXAppAttachContainer.ps1 This script creates an MSIX app attach (vhd) container for a given MSIX file by: - Creating a new VHD disk - Initializing the disk @@ -56,7 +36,7 @@ This script creates an MSIX app attach (vhd) container for a given MSIX file by: - Output the Volume ID and Package name needed for the Staging step of MSIX app attach - Dismount the disk - ## WVD-Rest-Api-Functions.ps1 + ### WVD-Rest-Api-Functions.ps1 This script contains the following functions that use REST API for WVD - Get-WorkSpace - Get-Hostpool @@ -67,14 +47,14 @@ This script contains the following functions that use REST API for WVD - New-AppGroup - New-App - ## Create-WVD-Backplane.bicep + ### Create-WVD-Backplane.bicep This Azure .bicep file (based on 0.1.1 Alpha Preview release of Project Bicep) generates the ARM Template (JSON) to create: - A WVD Workspace - A WVD Host pool - A WVD App Group And connect the AppGroup with the Host pool -## Contributing +### Contributing This project welcomes contributions and suggestions. diff --git a/avdScalingPlan.bicep b/avdScalingPlan.bicep new file mode 100644 index 0000000..144b16c --- /dev/null +++ b/avdScalingPlan.bicep @@ -0,0 +1,183 @@ +//Scaling plan parameters +param scalingPlanName string +param scalingPlanLocation string +param scalingPlanDescription string +param scalingPlanExclusionTag string +param scalingPlanFriendlyName string +@allowed([ + 'Pooled' +]) +param scalingPlanHostPoolType string +param scalingPlanTimeZone string +param scalingPlanhostPoolArmPath string +param scalingPlanEnabled bool = true + +//Weekdays schedule parameters +param weekdaysScheduleName string +param weekdaysSchedulerampUpStartTimeHour int +param weekdaysSchedulerampUpStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekdaysSchedulerampUpLoadBalancingAlgorithm string +param weekdaysSchedulerampUpMinimumHostsPct int +param weekdaysSchedulerampUpCapacityThresholdPct int +param weekdaysSchedulepeakStartTimeHour int +param weekdaysSchedulepeakStartTimeMinute int +param weekdaysSchedulerampDownStartTimeHour int +param weekdaysSchedulerampDownStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekdaysSchedulerampDownLoadBalancingAlgorithm string +param weekdaysScheduleoffPeakStartTimeHour int +param weekdaysScheduleoffPeakStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekdaysSchedulepeakLoadBalancingAlgorithm string +param weekdaysSchedulerampDownMinimumHostsPct int +param weekdaysSchedulerampDownCapacityThresholdPct int +param weekdaysSchedulerampDownForceLogoffUsers bool +@allowed([ + 'ZeroSessions' + 'ZeroActiveSessions' +]) +param weekdaysSchedulerampDownStopHostsWhen string +param weekdaysrampDownWaitTimeMinutes int +param weekdaysrampDownNotificationMessage string + +//weekends schedule parameters +param weekendsScheduleName string +param weekendsSchedulerampUpStartTimeHour int +param weekendsSchedulerampUpStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekendsSchedulerampUpLoadBalancingAlgorithm string +param weekendsSchedulerampUpMinimumHostsPct int +param weekendsSchedulerampUpCapacityThresholdPct int +param weekendsSchedulepeakStartTimeHour int +param weekendsSchedulepeakStartTimeMinute int +param weekendsSchedulerampDownStartTimeHour int +param weekendsSchedulerampDownStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekendsSchedulerampDownLoadBalancingAlgorithm string +param weekendsScheduleoffPeakStartTimeHour int +param weekendsScheduleoffPeakStartTimeMinute int +@allowed([ + 'BreadthFirst' + 'DepthFirst' +]) +param weekendsSchedulepeakLoadBalancingAlgorithm string +param weekendsSchedulerampDownMinimumHostsPct int +param weekendsSchedulerampDownCapacityThresholdPct int +param weekendsSchedulerampDownForceLogoffUsers bool +@allowed([ + 'ZeroSessions' + 'ZeroActiveSessions' +]) +param weekendsSchedulerampDownStopHostsWhen string +param weekendsrampDownWaitTimeMinutes int +param weekendsrampDownNotificationMessage string + +@description('Create an VD Scaling plan including a weekdays and weekends schedule') +resource sp 'Microsoft.DesktopVirtualization/scalingPlans@2021-09-03-preview' = { + name: scalingPlanName + location: scalingPlanLocation + properties: { + description: scalingPlanDescription + exclusionTag: scalingPlanExclusionTag + friendlyName: scalingPlanFriendlyName + hostPoolType: scalingPlanHostPoolType + schedules: [ + { + rampUpStartTime: { + hour: weekdaysSchedulerampUpStartTimeHour + minute: weekdaysSchedulerampUpStartTimeMinute + } + peakStartTime: { + hour: weekdaysSchedulepeakStartTimeHour + minute: weekdaysSchedulepeakStartTimeMinute + } + rampDownStartTime: { + hour: weekdaysSchedulerampDownStartTimeHour + minute: weekdaysSchedulerampDownStartTimeMinute + } + offPeakStartTime: { + hour: weekdaysScheduleoffPeakStartTimeHour + minute: weekdaysScheduleoffPeakStartTimeMinute + } + name: weekdaysScheduleName + daysOfWeek: [ + 'Monday' + 'Tuesday' + 'Wednesday' + 'Thursday' + 'Friday' + ] + rampUpLoadBalancingAlgorithm: weekdaysSchedulerampUpLoadBalancingAlgorithm + rampUpMinimumHostsPct: weekdaysSchedulerampUpMinimumHostsPct + rampUpCapacityThresholdPct: weekdaysSchedulerampUpCapacityThresholdPct + peakLoadBalancingAlgorithm: weekdaysSchedulepeakLoadBalancingAlgorithm + rampDownLoadBalancingAlgorithm: weekdaysSchedulerampDownLoadBalancingAlgorithm + rampDownMinimumHostsPct: weekdaysSchedulerampDownMinimumHostsPct + rampDownCapacityThresholdPct: weekdaysSchedulerampDownCapacityThresholdPct + rampDownForceLogoffUsers: weekdaysSchedulerampDownForceLogoffUsers + rampDownWaitTimeMinutes: weekdaysrampDownWaitTimeMinutes + rampDownNotificationMessage: weekdaysrampDownNotificationMessage + rampDownStopHostsWhen: weekdaysSchedulerampDownStopHostsWhen + offPeakLoadBalancingAlgorithm: weekdaysSchedulepeakLoadBalancingAlgorithm + } + { + rampUpStartTime: { + hour: weekendsSchedulerampUpStartTimeHour + minute: weekendsSchedulerampUpStartTimeMinute + } + peakStartTime: { + hour: weekendsSchedulepeakStartTimeHour + minute: weekendsSchedulepeakStartTimeMinute + } + rampDownStartTime: { + hour: weekendsSchedulerampDownStartTimeHour + minute: weekendsSchedulerampDownStartTimeMinute + } + offPeakStartTime: { + hour: weekendsScheduleoffPeakStartTimeHour + minute: weekendsScheduleoffPeakStartTimeMinute + } + name: weekendsScheduleName + daysOfWeek: [ + 'Saturday' + 'Sunday' + ] + rampUpLoadBalancingAlgorithm: weekendsSchedulerampUpLoadBalancingAlgorithm + rampUpMinimumHostsPct: weekendsSchedulerampUpMinimumHostsPct + rampUpCapacityThresholdPct: weekendsSchedulerampUpCapacityThresholdPct + peakLoadBalancingAlgorithm: weekendsSchedulepeakLoadBalancingAlgorithm + rampDownLoadBalancingAlgorithm: weekendsSchedulerampDownLoadBalancingAlgorithm + rampDownMinimumHostsPct: weekendsSchedulerampDownMinimumHostsPct + rampDownCapacityThresholdPct: weekendsSchedulerampDownCapacityThresholdPct + rampDownForceLogoffUsers: weekendsSchedulerampDownForceLogoffUsers + rampDownWaitTimeMinutes: weekendsrampDownWaitTimeMinutes + rampDownNotificationMessage: weekendsrampDownNotificationMessage + rampDownStopHostsWhen: weekendsSchedulerampDownStopHostsWhen + offPeakLoadBalancingAlgorithm: weekendsSchedulepeakLoadBalancingAlgorithm + } + ] + timeZone: scalingPlanTimeZone + hostPoolReferences: [ + { + hostPoolArmPath: scalingPlanhostPoolArmPath + scalingPlanEnabled: scalingPlanEnabled + } + ] + } +}