msix-app-attach/MigrationScript/Migrate-MsixPackagesToAppAttach.ps1 (623 lines of code) (raw):

<# .SYNOPSIS Script to migrate MSIX Package objects to app attach package objects. .DESCRIPTION This script will create a new app attach package object and delete the original MSIX package object if requested. It can also copy permissions from the application group(s) associated with the hostpool that the MSIX package is associated with. It can also copy the location and resource group of the hostpool that the MSIX package is associated with if not specified. It will write logs to a file in the temp folder by default, but this can be changed with the -LogFilePath parameter. .PARAMETER MsixPackage This is Msix package to migrate to an app attach package, can be passed in via pipeline. .PARAMETER PermissionSource Where to get permissions from for the new package, defaults to no permissions granted. The options are: 1) The DAG associated with the hostpool that the MSIX package is associated with 2) The RAGs associated with the hostpool that the MSIX package is associated with In either case it will grant permissions to all users and groups with any permission that is scoped specifically to the application group. .PARAMETER HostpoolsForNewPackage ResourceIds of hostpools to associate new object with, defaults to no hostpools. Hostpools have to be in the same location as the app attach packages they are associated with .PARAMETER TargetResourceGroupName Resource group to put new package in, defaults to resource group of hostpool that the MSIX package is associated with. .PARAMETER Location Location to create new package in, defaults to location of hostpool that the MSIX package is associated with. App attach packages have to be in the same location as the hostpool they are associated with. .PARAMETER DeleteOrigin Delete source msix package after migration. .PARAMETER DeactivateOrigin Deactivates source msix package after migration. .PARAMETER IsActive Creates new app attach package as active. .PARAMETER PassThru Passes new app attach package thru. .PARAMETER LogInJSON Write to log file in JSON Format. .PARAMETER LogFilePath Path of logfile, defaults to MsixMigration[Timestamp].log in a temp folder. The path being logged to will be written to the console at the beginning of the script run. .EXAMPLE Get-AzWvdMsixPackage -SubscriptionId $SubscriptionId -ResourceGroupName $MsixResourceGroupName -HostPoolName $HostpoolName -FullName $MsixName | .\Migrate-MsixPackagesToAppAttach Migrates a specific MSIX package to an app attach package in the same resource group and location as the MSIX package it came from, but assigning it no permissions or to any hostpools, leaving the old package present and active, and writing logs to the default file path. .EXAMPLE .\Migrate-MsixPackagesToAppAttach -MsixPackage $msixPackage -PermissionSource "DAG" -HostpoolsForNewPackage $hostpoolId -TargetResourceGroupName $newResourceGroup -Location $newLocation -DeleteOrigin -PassThru -LogInJSON -LogFilePath $logFilePath Migrates the specified MSIX package to an app attach package, copying permissions from the DAG associated with the hostpool that the MSIX package is associated with, and deleting the original MSIX package. .EXAMPLE .\Migrate-MsixPackagesToAppAttach -MsixPackage $msixPackage -PermissionSource "DAG" -HostpoolsForNewPackage $hostpoolId -TargetResourceGroupName $newResourceGroup -Location $newLocation -DeactivateOrigin -PassThru -LogInJSON -LogFilePath $logFilePath Migrates the specified MSIX package to an app attach package, copying permissions from the DAG associated with the hostpool that the MSIX package is associated with, and deactivates the original MSIX package so it will no longer be staged on hostpools. .EXAMPLE Get-AzWvdMsixPackage -SubscriptionId $SubscriptionId -ResourceGroupName $MsixResourceGroupName -HostPoolName $HostpoolName | .\Migrate-MsixPackagesToAppAttach -PermissionSource "DAG" -HostpoolsForNewPackage $hostpoolId -TargetResourceGroupName $newResourceGroup -Location $newLocation -DeleteOrigin -LogFilePath $logFilePath Gets all MSIX packages associated with the specified hostpool and migrates them to app attach packages, copying permissions from the DAG associated with the hostpool that the MSIX package is associated with, and deleting the original MSIX packages. #> #Requires -Version 5.0 #Requires -Modules Az.DesktopVirtualization, Microsoft.Graph.Authentication, Az.Resources, Az.Accounts [CmdletBinding(DefaultParameterSetName= 'Default')] param( [Parameter(Mandatory, ValueFromPipeline, HelpMessage = "MSIX Package to migrate")] $MsixPackage, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Where to get permissions from for the new package, defaults to no permissions granted")] [ValidateSet("DAG", "RAG", "NONE")] [System.String] $PermissionSource = "NONE", [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "ResourceIds of hostpools to associate new object with, defaults to no hostpools")] [System.String[]] $HostpoolsForNewPackage, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Resource group to put new package in, defaults to resource group of hostpool that the MSIX package is associated with")] [System.String] $TargetResourceGroupName, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Location to create new package in, defaults to location of hostpool that the MSIX package is associated with")] [System.String] $Location, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'DeleteOrigin', Mandatory, HelpMessage = "Delete source msix package object after migration")] [System.Management.Automation.SwitchParameter] $DeleteOrigin, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'DeactivateOrigin', Mandatory, HelpMessage = "Deactivates source msix package object after migration")] [System.Management.Automation.SwitchParameter] $DeactivateOrigin, [Parameter(HelpMessage = "Creates new app attach package as active")] [System.Management.Automation.SwitchParameter] $IsActive, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Passes new app attach package thru")] [System.Management.Automation.SwitchParameter] $PassThru, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Log in JSON Format")] [System.Management.Automation.SwitchParameter] $LogInJSON, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Path of logfile, defaults to MsixMigration[Timestamp].log in a temp folder")] [System.String] $LogFilePath = "$env:temp\MsixMigration" + (Get-Date -Format "yyyyMMddhhmmss") + ".log" ) begin { # Copied with Jim's permission from https://github.com/JimMoyle/YetAnotherWriteLog/blob/master/Write-Log.ps1 function Write-Log { <# .SYNOPSIS Single function to enable logging to file .DESCRIPTION The Log file can be output to any directory. A single log entry looks like this: 2018-01-30 14:40:35 INFO: 'My log text' Log entries can be Info, Warning, Error or Debug The function takes pipeline input and you can pipe exceptions straight to the function for automatic error logging. It's not part of this function, but it can be useful to use the $PSDefaultParameterValues built-in Variable can be used to conveniently set the path and/or JSONformat switch at the top of the script: $PSDefaultParameterValues = @{"Write-Log:Path" = 'C:\YourPathHere.log'} $PSDefaultParameterValues = @{"Write-Log:JSONformat" = $true} .PARAMETER Message This is the body of the log line and should contain the information you wish to log. .PARAMETER Level One of four logging levels: INFO, WARNING, ERROR or DEBUG. This is an optional parameter and defaults to INFO .PARAMETER Path The path where you want the log file to be created. This is an optional parameter and defaults to "$env:temp\PowershellScript.log" .PARAMETER StartNew This will blank any current log in the path, it should be used at the start of your code if you don't want to append to an existing log. .PARAMETER Exception Used to pass a powershell exception to the logging function for automatic logging, this will log the excption message as an error. .PARAMETER JSONFormat Used to change the logging format from human readable to machine readable format, this will be a single line like the example format below: In this format the timestamp will include a much more granular time which will also include timezone information. The format is optimised for Splunk input, but should work for any other platform. {"TimeStamp":"2018-02-01T12:01:24.8908638+00:00","Level":"Warning","Message":"My message"} .EXAMPLE Write-Log -StartNew Starts a new logfile in the default location .EXAMPLE Write-Log -StartNew -Path c:\logs\new.log Starts a new logfile in the specified location .EXAMPLE Write-Log 'This is some information' Appends a new information line to the log. .EXAMPLE Write-Log -level Warning 'This is a warning' Appends a new warning line to the log. .EXAMPLE Write-Log -level Error 'This is an Error' Appends a new Error line to the log. .EXAMPLE Write-Log -Exception $error[0] Appends a new Error line to the log with the message being the contents of the exception message. .EXAMPLE $error[0] | Write-Log Appends a new Error line to the log with the message being the contents of the exception message. .EXAMPLE 'My log message' | Write-Log Appends a new Info line to the log with the message being the contents of the string. .EXAMPLE Write-Log 'My log message' -JSONFormat Appends a new Info line to the log with the message. The line will be in JSONFormat. #> [CmdletBinding(DefaultParametersetName = "LOG")] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'LOG', Position = 0)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'LOG', Position = 1 )] [ValidateSet('Error', 'Warning', 'Info', 'Debug')] [string]$Level = "Info", [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)] [Alias('PSPath')] [string]$Path, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [switch]$JSONFormat, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'STARTNEW')] [switch]$StartNew, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'EXCEPTION')] [System.Management.Automation.ErrorRecord]$Exception ) BEGIN { Set-StrictMode -version Latest #Enforces most strict best practice. } PROCESS { #Switch on parameter set switch ($PSCmdlet.ParameterSetName) { LOG { #Get human readable date $formattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" switch ( $Level ) { 'Info' { $levelText = "INFO: "; break } 'Error' { $levelText = "ERROR: "; break } 'Warning' { $levelText = "WARNING:"; break } 'Debug' { $levelText = "DEBUG: "; break } } #Build an object so we can later convert it $logObject = @{ #TimeStamp = Get-Date -Format o #Get machine readable date Level = $levelText Message = $Message } if ($JSONFormat) { $logobject = [PSCustomObject][ordered]@{ TimeStamp = Get-Date -Format o Level = $levelText Message = $Message } #Convert to a single line of JSON and add it to the file $logMessage = $logObject | ConvertTo-Json -Compress $logMessage | Add-Content -Path $Path } else { $logobject = [PSCustomObject][ordered]@{ TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Level = $levelText Message = $Message } $logMessage = "$formattedDate`t$levelText`t$Message" #Build human readable line $logObject | Export-Csv -Path $Path -Delimiter "`t" -NoTypeInformation -Append } Write-Verbose $logMessage #Only verbose line in the function } #LOG EXCEPTION { #Splat parameters $writeLogParams = @{ Level = 'Error' Message = $Exception.Exception.Message Path = $Path JSONFormat = $JSONFormat } Write-Log @writeLogParams #Call itself to keep code clean break } #EXCEPTION STARTNEW { if (Test-Path $Path) { Remove-Item $Path -Force } #Splat parameters $writeLogParams = @{ Level = 'Info' Message = 'Starting Logfile' Path = $Path JSONFormat = $JSONFormat } Write-Log @writeLogParams break } #STARTNEW } #switch Parameter Set } END { } } #function Write-Log $PSDefaultParameterValues = @{ "Write-Log:JSONformat" = $LogInJSON "Write-Log:Path" = $LogFilePath } $locations = Get-AzLocation | Select-Object -ExpandProperty Location } process { Write-Warning "Logging output to $LogFilePath" Write-Log "Starting migration of package $($MsixPackage.Name)" $PSBoundParameters.GetEnumerator() | ForEach-Object { if ($_.Key -eq "MsixPackage") { $paramValue = $_.Value.Name } else { $paramValue = $_.Value.ToString() } Write-Log "Parameter: $($_.Key) Value: $($paramValue)" } # Start Validation if ($PermissionSource -ne 'NONE') { try { $mgContext = Get-MgContext -ErrorAction Stop if ($null -eq $mgContext) { $message = "No Microsoft Graph context found, please run Connect-MgGraph -Scopes 'Group.Read.All' before running this script" Write-Log -Level 'Error' -Message $message Write-Error $message return } elseif ($mgContext.Scopes -notcontains "Group.Read.All") { $message = "Microsoft Graph context found, but it does not have the required Group.Read.All scope, please run Connect-MgGraph -Scopes 'Group.Read.All' before running this script" Write-Log -Level 'Error' -Message $message Write-Error $message return } } catch { $Error[0] | Write-Log Write-Error $Error[0] return } } # Get relevant information from the id of the msix package $idArray = $msixPackage.Id.Split("/") $SubscriptionId = $idArray[2] $MsixResourceGroupName = $idArray[4] $HostpoolName = $idArray[8] $MsixName = $idArray[10] Write-Log "SubscriptionId: $SubscriptionId" Write-Log "MsixResourceGroupName: $MsixResourceGroupName" Write-Log "HostpoolName: $HostpoolName" Write-Log "MsixName: $MsixName" $newLocation = $Location $newResourceGroup = $TargetResourceGroupName # Fetch the hostpool the package is associated with try { $hostpool = Get-AzWvdHostPool -SubscriptionId $SubscriptionId -ResourceGroupName $MsixResourceGroupName -Name $HostpoolName -ErrorAction Stop } catch { $Error[0] | Write-Log Write-Error $Error[0] return } # You cannot have two copies of the same full name active at the same time on the same hostpool $setActiveAfterCreate = $false if ($null -ne $HostpoolsForNewPackage -and $HostpoolsForNewPackage -Contains ($hostpool.Id) -and $IsActive -and $MsixPackage.IsActive) { $setActiveAfterCreate = $true if (-not $DeactivateOrigin -and -not $DeleteOrigin) { $message = "Hostpools can only be associated with one active package per msix full name. Please either remove the -IsActive flag or set the -DeactivateOrigin flag." Write-Log -Level 'Error' -Message $message Write-Error $message return } } if (-not ($Location)) { $newLocation = $hostpool.Location Write-Log "Location not specified for package $MsixName, using location of $HostpoolName : $newLocation" } else { try { if ($locations -notcontains $newLocation) { $message = "Location $newLocation is not a valid Azure location" Write-Log -Level 'Error' -Message $message Write-Error $message return } } catch { $Error[0] | Write-Log Write-Error $Error[0] return } } foreach ($newHostpool in $HostpoolsForNewPackage) { $hpIdArray = $newHostpool.Split("/") if ($hpIdArray.Count -lt 8) { $message = "Hostpool $newHostpool not in correct format, please provide resource id of hostpool" Write-Log -Level 'Error' -Message $message Write-Error $message return } $hpSubscriptionId = $hpIdArray[2] $hpResourceGroupName = $hpIdArray[4] $hpName = $hpIdArray[8] try { $hp = Get-AzWvdHostPool -SubscriptionId $hpSubscriptionId -ResourceGroupName $hpResourceGroupName -Name $hpName -ErrorAction Stop if ($null -eq $hp) { $message = "Hostpool $newHostpool does not exist" Write-Log -Level 'Error' -Message $message Write-Error $message return } elseif ($hp.Location -ne $newLocation) { $message = "Hostpool $newHostpool is in a different location than the new package, please provide hostpools in the same location as the new package" Write-Log -Level 'Error' -Message $message Write-Error $message return } } catch { $Error[0] | Write-Log Write-Error $Error[0] return } } if (-not ($newResourceGroup)) { $newResourceGroup = $MsixResourceGroupName Write-Log "Resource group not specified for package $MsixName, using resource group of package: $newResourceGroup" } else { try { $rg = Get-AzResourceGroup -Name $newResourceGroup -ErrorAction Stop if (-not ($rg)) { $message = "Resource group $newResourceGroup does not exist" Write-Log -Level 'Error' -Message $message Write-Error $message return } } catch { $Error[0] | Write-Log Write-Error $Error[0] return } } if ($null -eq $hostpool.ApplicationGroupReference -and $PermissionSource -ne 'NONE') { $message = "No application groups are associated with hostpool $($hostpool.Name), cannot copy permissions." Write-Log -Level 'Error' -Message $message Write-Error $message return } $appGroups = foreach ($applicationGroupReference in $hostpool.ApplicationGroupReference) { $idArray = $applicationGroupReference.Split("/") $AgSubscriptionId = $idArray[2] $AgResourceGroupName = $idArray[4] $AgName = $idArray[8] try { Get-AzWvdApplicationGroup -SubscriptionId $AgSubscriptionId -ResourceGroupName $AgResourceGroupName -Name $AgName -ErrorAction Stop } catch { $message = "Failed to get application group information for $appName associated with hostpool $($hostpool.Name): $_" Write-Log -Level 'Error' -Message $message Write-Error $message return } } switch ($PermissionSource) { NONE { $appGroup = $null ; break } DAG { $appGroup = $appGroups | Where-Object { $_.ApplicationGroupType -eq 'Desktop' } ; break } RAG { $ragGroups = $appGroups | Where-Object { $_.ApplicationGroupType -eq 'RemoteApp' } try { $remoteApps = foreach ($ragGroup in $ragGroups) { $idArray = $ragGroups.Id.Split("/") $AgSubscriptionId = $idArray[2] $AgResourceGroupName = $idArray[4] Get-AzWvdApplication -SubscriptionId $AgSubscriptionId -ResourceGroupName $AgResourceGroupName -GroupName $ragGroup.Name -ErrorAction Stop } } catch { $message = "Failed to get application information for $($ragGroup.Name): $_" Write-Log -Level 'Error' -Message $message Write-Error $message return } $msixRemoteApps = $remoteApps | Where-Object { $_.MsixPackageFamilyName -eq $MsixPackage.PackageFamilyName } if ($null -ne $msixRemoteApps) { $remoteAppNames = $msixRemoteApps.Name $appGroupNames = $remoteAppNames | ForEach-Object { $_.Split("/")[0] } $appGroup = $ragGroups | Where-Object { $appGroupNames -contains $_.Name } } break } Default {} } if ($null -eq $appGroup -and $PermissionSource -ne 'NONE') { $message = "No application group found to copy permissions from for package $MsixName" Write-Log -Level 'Error' -Message $message Write-Error $message return } # We add permissions for all users and groups with any permission that is scoped specifically to the application group if ($appGroup) { Write-Log "Applying permissions from application group $($appGroup.Name) to new package $MsixName" try { $roleAssignments = Get-AzRoleAssignment -Scope $appGroup.Id -ErrorAction Stop Write-Log "Got role assignments for application group $($appGroup.Name)" } catch { $Error[0] | Write-Log Write-Error $Error[0] return } $permissionsToAdd = foreach ($roleAssignment in $roleAssignments) { if ($roleAssignment.Scope -eq $appGroup.Id) { if ($roleAssignment.ObjectType -eq "User") { $roleAssignment.SignInName Write-Log ("Granting user " + $roleAssignment.SignInName + "access to package $MsixName") } if ($roleAssignment.ObjectType -eq "Group") { $roleAssignment.DisplayName Write-Log ("Granting group " + $roleAssignment.DisplayName + "access to package $MsixName") } } } } else { $permissionsToAdd = $null } #App attach package creation paramters $appAttachParameters = @{ SubscriptionId = $SubscriptionId ResourceGroupName = $newResourceGroup Name = $MsixName Location = $newLocation FailHealthCheckOnStagingFailure = 'NeedsAssistance' ImageDisplayName = $MsixPackage.DisplayName HostPoolReference = $HostpoolsForNewPackage ImagePath = $MsixPackage.ImagePath ImageLastUpdated = $MsixPackage.LastUpdated ImagePackageApplication = $MsixPackage.PackageApplication ImagePackageDependency = $MsixPackage.PackageDependency ImagePackageFamilyName = $MsixPackage.PackageFamilyName ImagePackageFullName = $MsixName ImagePackageName = $MsixPackage.PackageName ImagePackageRelativePath = $MsixPackage.PackageRelativePath ImageVersion = $MsixPackage.Version ImageIsActive = $IsActive -and -not $setActiveAfterCreate ImageIsRegularRegistration = $MsixPackage.IsRegularRegistration } try { # catch the debug output in a variable so we can extract the object in a success and the activity id in a failure $newOutput = New-AzWvdAppAttachPackage @appAttachParameters -ErrorAction Stop 5>&1 $appAttachPackage = $newOutput | Where-Object { $_.GetType().Name -eq "AppAttachPackage" } Write-Log "Package $MsixName migrated to app attach package object type" } catch { if ($null -ne $_.Message) { # this would complain if there was an app attach package object in the output but there won't be in this case $activityIds = $newOutput | Where-Object { $_.Message.Contains("x-ms-correlation-id") } if ($null -ne $activityIds) { $activityId = $activityIds.Message.Split("x-ms-correlation-id")[1].Trim().Substring(2, 38).Trim() $logOutput = ("ActivityId: " + $activityId + " " + $Error[0]) } } else { $logOutput = $Error[0] } $logOutput | Write-Log Write-Error $logOutput return } foreach ($item in $permissionsToAdd) { try { # check if it is an email address $emailRegex = "^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+" if ($item -match $emailRegex) { $role = Get-AzRoleAssignment -SignInName $item -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id if ($null -eq $role) { $null = New-AzRoleAssignment -SignInName $item -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id } } # if not email, assume group else { $group = Get-MgGroup -Filter "DisplayName eq '$item'" if ($null -ne $group) { # this is a nullable field and at least some groups where this is null are assignable if ($group.IsAssignableToRole -ne $false) { $retryCount = 0 $retryMax = 5 $retryDelay = 2 $maximalWait = (Get-Date).AddSeconds(10) # add a retry policy here because sometimes the group is not assignable immediately after creation while ($retryCount -lt $retryMax -and (Get-Date) -lt $maximalWait) { try { $retryCount++ $role = Get-AzRoleAssignment -ObjectId $group.Id -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id if ($null -eq $role) { $null = New-AzRoleAssignment -ObjectId $group.Id -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id } } catch { if ($retryCount -eq $retryMax -or (Get-Date) -gt $maximalWait) { throw $_ } else { Start-Sleep -Seconds $retryDelay } } } } else { Write-Log ("Group $item is not assignable to a role, skipping assigning permissions to this group") } } else { Write-Log ("Unable to find group $item, skipping assigning permissions to this group") } } } catch { $message = "An exception occurred adding permissions for $item $_ Please manually check permissions." Write-Log -Level Warning $message Write-Warning $message if ($DeleteOrigin) { $message = "Skipping deletion for package $MsixName because permissions could not be added to the new package" Write-Log -Level Warning $message $DeleteOrigin = $false } if ($DeactivateOrigin -and -not $DeleteOrigin) { $message = "Skipping deactivation for package $MsixName because permissions could not be added to the new package" Write-Log -Level Warning $message $DeleteOrigin = $false } } } if ($DeactivateOrigin -and -not $DeleteOrigin) { try { Update-AzWvdMsixPackage -SubscriptionId $SubscriptionId -ResourceGroupName $MsixResourceGroupName -HostPoolName $HostpoolName -FullName $MsixName -IsActive:$false -ErrorAction Stop Write-Log "Origin package $MsixName deactivated" } catch { if ($setActiveAfterCreate) { Write-Log "Error deactivating origin package $MsixName, not activating new package object" } $Error[0] | Write-Log Write-Error $Error[0] return } } if ($DeleteOrigin) { try { Remove-AzWvdMsixPackage -SubscriptionId $SubscriptionId -ResourceGroupName $MsixResourceGroupName -HostPoolName $HostpoolName -FullName $MsixName -ErrorAction Stop Write-Log "Package $MsixName deleted" } catch { if ($setActiveAfterCreate) { Write-Log "Error deleting origin package $MsixName, not activating new package object" } $Error[0] | Write-Log Write-Error $Error[0] return } } if ($setActiveAfterCreate) { try { Update-AzWvdAppAttachPackage -SubscriptionId $SubscriptionId -ResourceGroupName $newResourceGroup -Name $MsixName -IsActive -ErrorAction Stop Write-Log "Package $MsixName activated" } catch { $Error[0] | Write-Log Write-Error $Error[0] return } } if ($PassThru) { Write-Output $appAttachPackage } }