├── .github └── FUNDING.yml ├── CVAD REST.ps1 ├── DailyChecks.ps1 ├── Direct2Events.csv ├── Direct2Events.ps1 ├── End Disconnected sessions.ps1 ├── Get Citrix OData data.ps1 ├── Get Citrix admin logs.ps1 ├── Get PVS boot time stats.ps1 ├── Get PVS device info.ps1 ├── Ghost Hunter.ps1 ├── Guys.Common.Functions.psm1 ├── LICENSE ├── Modify and launch file.ps1 ├── New MCS catalog machines delivery group.ps1 ├── README.md ├── Recreate PVS XML manifest.ps1 ├── Remove Ghost NICs.ps1 ├── Set Citrix DDC from OU.ps1 ├── Show PVS audit trail.ps1 ├── Show Studio Access.ps1 ├── StoreFront Log Levels.ps1 ├── Studio Selector.ps1 ├── Update Snapshot for Citrix MCS.ps1 ├── nsconmsg.txt └── parse storefront log files.ps1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: guyrleech 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: guyrleech 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /CVAD REST.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3 2 | 3 | <# 4 | .SYNOPSIS 5 | Example script using Citrix CVAD REST API 6 | 7 | .DESCRIPTION 8 | Use the API documented at https://developer.cloud.com/citrixworkspace/virtual-apps-and-desktops/cvad-rest-apis for accessing Citrix Cloud CVAD 9 | 10 | .PARAMETER customerId 11 | Citrix Cloud customer id to query. customer id is shown in the Cloud management portal in the "API Access" tab of "Identity and Access Management" menu. 12 | Can also be passed, without parameter, via the %CustomerId% environment variable 13 | 14 | .PARAMETER clientId 15 | Client id generated by creating a Secure Client on the "API Access" tab of "Identity and Access Management" menu 16 | 17 | .PARAMETER secret 18 | Secret generated by creating a Secure Client on the "API Access" tab of "Identity and Access Management" menu 19 | 20 | .PARAMETER secureclientcsv 21 | csv file downloaded from Citrix Cloud portal containing client id and secret 22 | 23 | .PARAMETER deliveryGroup 24 | Delivery group name/regex to fetch details of and optionally modify 25 | 26 | .PARAMETER updatedDeliveryGroupDescription 27 | 28 | .PARAMETER iterations 29 | Numer of times to run the script 30 | 31 | .PARAMETER millisecondsPause 32 | Pause in milliseconds between each invocation of the script 33 | 34 | .EXAMPLE 35 | & '.\CVAD REST.ps1' -customerId yourcloudcustomerid -clientId 'f111319a-beef-4ebc-b9e4-fafe6196c88b' -secret 'QZXVERYSECRETQYXZ==' -Verbose 36 | 37 | Connect to the Citrix Cloud REST API with customer id "yourcloudcustomerid" and the clientid and secret previously created from a Secure Client instance in the Cloud console 38 | and show the number of delivery groups and machine catalogs 39 | 40 | .EXAMPLE 41 | & '.\CVAD REST.ps1' -secureclientcsv C:\@guyrleech\secureclient.converge2021.csv -customerId %customerid% -deliveryGroup Leech -updatedDeliveryGroupDescription "Updated for Converge demo @ $(Get-Date -Format G)" -enableDeliveryGroup $true 42 | 43 | Authenticate to the Citrix Cloud for the customer name stored in the %customerid% environment variable with the client id & secret in the specified csv file then find the delivery group whose name matches "Leech" and update its description and enable it. 44 | 45 | .NOTES 46 | This is a demonstration script only. If I were to write a production script for CVAD API, I would not allow passing of the client secret in clear text on the command line since it will persist in the PowerShell history file contained in the user's Windows profile annd be visible to task manager and process creation auditing events 47 | 48 | Modification History: 49 | @guyrleech 03/11/2020 Initial release 50 | @guyrleech 26/10/2021 Change specified delivery group attributes. Allow credential passing via csv 51 | #> 52 | 53 | [CmdletBinding()] 54 | 55 | Param 56 | ( 57 | [Parameter(Mandatory=$true,HelpMessage='Citrix Cloud customer id')] 58 | [string]$customerId , 59 | [Parameter(Mandatory=$true,ParameterSetName='Explicit',HelpMessage='API Client Id')] 60 | [string]$clientId , 61 | [Parameter(Mandatory=$true,ParameterSetName='Explicit',HelpMessage='API Client Secret')] 62 | [string]$secret , 63 | [Parameter(Mandatory=$true,ParameterSetName='CSV',HelpMessage='Secure client csv file containing API Client Secret')] 64 | [string]$secureclientcsv , 65 | [string]$deliveryGroup , 66 | [string]$updatedDeliveryGroupDescription , 67 | [bool]$enableDeliveryGroup , 68 | [int]$iterations = 1 , 69 | [int]$millisecondsPause = 15000 , 70 | ## these should not need to be changed 71 | [string]$authURL = 'https://trust.citrixworkspacesapi.net' , 72 | [string]$baseURL = 'https://api-us.cloud.com/cvad/manage' ## 'https://api-us.cloud.com/cvadapis' 73 | ) 74 | 75 | #region Functions 76 | 77 | <# 78 | .SYNOPSIS 79 | Wrapper for Invoke-RestMethod to time request and trap and report errors 80 | 81 | .PARAMETER RESTparams 82 | Hashtable of parameters to pass to the REST API 83 | 84 | .PARAMETER itemDetails 85 | Text description for messages 86 | 87 | .PARAMETER body 88 | Optional body to pass to REST API 89 | 90 | .PARAMETER success 91 | Set to true if no errors otherwise false 92 | 93 | .PARAMETER noRetry 94 | Do not retyry on error 95 | 96 | #> 97 | Function 98 | Invoke-CVADRestMethod 99 | { 100 | [CmdletBinding()] 101 | 102 | Param 103 | ( 104 | [Parameter(Mandatory=$true)] 105 | [hashtable]$RESTparams , 106 | [Parameter(Mandatory=$true)] 107 | [string]$itemDetails , 108 | [hashtable]$body , 109 | [ref]$success , 110 | [switch]$noRetry 111 | ) 112 | 113 | if( $PSBoundParameters[ 'success '] ) 114 | { 115 | $success.Value = $false 116 | } 117 | $result = $null 118 | [datetime]$startRequest = [datetime]::Now 119 | [datetime]$endRequest = [datetime]::MaxValue 120 | # hashtable parameters are passed by reference so we must not pollute it 121 | [hashtable]$bodyParams = @{} 122 | 123 | if( $body -and $body.Count ) 124 | { 125 | $bodyParams.Add( 'body' , ($body | ConvertTo-Json )) 126 | } 127 | 128 | try 129 | { 130 | $result = Invoke-RestMethod @RESTparams @bodyParams 131 | $endRequest = [datetime]::Now 132 | if( $PSBoundParameters[ 'success'] ) 133 | { 134 | $success.Value = $true 135 | } 136 | } 137 | catch 138 | { 139 | $endRequest = [datetime]::Now 140 | Write-Error "Failed to $($RESTparams.Method) $itemDetails via $($RESTparams.uri) ($(($endRequest - $startRequest).TotalMilliseconds) ms) : $_" 141 | if( $responseHeaders = (Get-Variable -Name $RESTparams.ResponseHeadersVariable -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value)) 142 | { 143 | ( $responseHeaders.GetEnumerator() | Select-Object -Property Key,@{n='Value';e={$_.Value}} | Out-String ) | Write-Error 144 | } 145 | ## retry if server side error and not been told not to retry 146 | if( -Not $noRetry -and ($_.ToString()|ConvertFrom-Json -ErrorAction SilentlyContinue | Select-Object -ExpandProperty statuscode -ErrorAction SilentlyContinue) -ge 500 ) 147 | { 148 | Write-Verbose -Message "Retrying $($RESTparams.Method) to $($RESTparams.URI) ..." 149 | if( ( $result = Invoke-RestMethod @RESTparams ) -and $PSBoundParameters[ 'success'] ) 150 | { 151 | $success.Value = $true 152 | } 153 | } 154 | } 155 | 156 | $result ## return 157 | } 158 | 159 | #endregion Functions 160 | 161 | #region Main 162 | 163 | ## Sanity check parameters 164 | if( -Not $PSBoundParameters[ 'deliveryGroup' ] -and ( $PSBoundParameters[ 'updatedDeliveryGroupDescription' ] -or $PSBoundParameters[ 'enableDeliveryGroup' ] ) ) 165 | { 166 | Throw "Must specify -deliveryGroup when updating description or enabling/disabling" 167 | } 168 | 169 | #region SSL Stuff 170 | ##https://stackoverflow.com/questions/41897114/unexpected-error-occurred-running-a-simple-unauthorized-rest-query?rq=1 171 | Add-Type -Verbose:$false -TypeDefinition @' 172 | public class SSLHandler 173 | { 174 | public static System.Net.Security.RemoteCertificateValidationCallback GetSSLHandler() 175 | { 176 | return new System.Net.Security.RemoteCertificateValidationCallback((sender, certificate, chain, policyErrors) => { return true; }); 177 | } 178 | } 179 | '@ 180 | 181 | [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [SSLHandler]::GetSSLHandler() 182 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 183 | 184 | #endregion SSL Stuff 185 | 186 | $sessionVariable = $null 187 | 188 | [hashtable]$RESTparams = @{ 189 | 'ContentType' = 'application/json' 190 | 'Method' = 'POST' 191 | 'Verbose' = $false 192 | 'ErrorAction' = 'Continue' 193 | } 194 | 195 | if( $PSBoundParameters[ 'secureclientcsv' ]) 196 | { 197 | if( -Not ( $secureclient = Import-Csv -Path $secureclientcsv ) ) 198 | { 199 | Throw "Failed to import from secure client csv `"$secureclientcsv`"" 200 | } 201 | 202 | if( $secureclient -is [array]) 203 | { 204 | Throw "Found $($secureclient.Count) identities in `"$secureclientcsv`" - there can be only one" 205 | } 206 | 207 | if( -Not $secureclient.PSObject.Properties[ 'ID'] -or [string]::IsNullOrEmpty( $secureclient.ID )) 208 | { 209 | Throw "ID field missing from `"$secureclientcsv`"" 210 | } 211 | 212 | if( -Not $secureclient.PSObject.Properties[ 'Secret'] -or [string]::IsNullOrEmpty( $secureclient.Secret )) 213 | { 214 | Throw "Secret field missing from `"$secureclientcsv`"" 215 | } 216 | 217 | $clientId = $secureclient.ID 218 | $secret = $secureclient.secret 219 | 220 | Write-Verbose -Message "Using `"$($secureclient.Name)`" identity from `"$secureclientcsv`"" 221 | } 222 | 223 | ## https://developer.cloud.com/getting-started/docs/overview 224 | [hashtable]$authParams = @{ 225 | ClientId = $clientId 226 | ClientSecret = $secret 227 | } 228 | 229 | ## allow customerid to be stored in environment variable so it's not visible whilst demoing 230 | if( $customerId.IndexOf( '%') -ne $customerId.LastIndexOf( '%' ) ) 231 | { 232 | $customerId = [System.Environment]::ExpandEnvironmentVariables( $customerId ) 233 | } 234 | 235 | $RESTparams.Body = ( $authParams | ConvertTo-Json ).ToString() 236 | $RESTparams.Uri = "$authURL/$customerId/tokens/clients" 237 | 238 | $responseHeaders = $null 239 | 240 | if( $PSVersionTable.PSVersion.Major -ge 7 ) 241 | { 242 | $RESTparams.Add( 'ResponseHeadersVariable' , 'responseHeaders' ) 243 | } 244 | 245 | try 246 | { 247 | $authenticated = Invoke-RestMethod @RESTparams -SessionVariable sessionVariable 248 | } 249 | catch 250 | { 251 | $authenticated = $null 252 | Throw "Failed to logon to $($RESTparams.uri) as customer $customerId : $_" 253 | } 254 | 255 | if( ! $authenticated -or ! $authenticated.PSObject.Properties[ 'token' ] ) 256 | { 257 | Throw "Failed to get authentication token from $($RESTparams.uri) as customer $customerId, got $authenticated" 258 | } 259 | 260 | Write-Verbose -Message "Authenticated ok, session expires at $(Get-Date -Date ((Get-Date).AddSeconds( $authenticated.expiresIn )) -Format G)" 261 | 262 | $RESTparams.websession = $sessionVariable 263 | $RESTparams.Remove( 'Body' ) ## no longer require the authentication fields (unless we need to reauthenticate) 264 | 265 | For( [int]$iteration = 1 ; $iteration -le $iterations ; $iteration++ ) 266 | { 267 | Write-Verbose -message "$iteration / $iterations : $(Get-Date -Format G)" 268 | 269 | ## https://developer.cloud.com/citrixworkspace/virtual-apps-and-desktops/cvad-rest-apis/docs/how-to-get-site-id 270 | $RESTparams.Uri = "$baseURL/me" 271 | $RESTparams.Method = 'GET' 272 | $RESTparams.Headers = @{ 273 | 'Authorization' = "CwsAuth Bearer $($authenticated.token)" 274 | 'Accept' = 'application/json' 275 | 'Content-Type' = 'application/json' 276 | 'Citrix-CustomerId' = $customerId 277 | 'charset' = 'utf-8' } 278 | 279 | if( $sites = Invoke-CVADRestMethod -RESTparams $RESTparams -itemDetails 'sites' ) 280 | { 281 | Write-Verbose -Message "Got $($sites.Customers.Count) sites for `"$($sites.DisplayName)`"" 282 | 283 | ForEach( $site in $sites.Customers.Sites ) 284 | { 285 | ## https://developer.cloud.com/citrixworkspace/virtual-apps-and-desktops/cvad-rest-apis/docs/how-to-get-machine-catalogs 286 | Write-Verbose -Message "Querying machine catalogs for site $($site.Id)" 287 | 288 | $RESTparams.Headers[ 'Citrix-InstanceId' ] = $site.Id 289 | $RESTparams.Uri = "$baseURL/MachineCatalogs" 290 | 291 | if( ( $machineCatalogs = Invoke-CVADRestMethod -RESTparams $RESTparams -itemDetails 'machine catalogs' ) -and $machineCatalogs.PSObject.Properties[ 'items' ] -and $machineCatalogs.Items.Count ) 292 | { 293 | Write-Verbose -Message "Got $($machineCatalogs.Items.Count) machine catalogs" 294 | } 295 | 296 | ## https://developer.cloud.com/citrixworkspace/virtual-apps-and-desktops/cvad-rest-apis/docs/how-to-get-delivery-groups 297 | Write-Verbose -Message "Querying delivery groups for site $($site.Id)" 298 | 299 | $RESTparams.Uri = "$baseURL/DeliveryGroups" 300 | 301 | if( ( $deliveryGroups = Invoke-CVADRestMethod -RESTparams $RESTparams -itemDetails 'delivery groups' ) -and $deliveryGroups.PSObject.Properties[ 'items' ] -and $deliveryGroups.Items.Count ) 302 | { 303 | Write-Verbose -Message "Got $($deliveryGroups.Items.Count) delivery groups" 304 | 305 | if( $PSBoundParameters[ 'deliveryGroup' ] ) 306 | { 307 | if( $selectedDeliveryGroup = $deliveryGroups.Items.Where( { $_.Name -match $deliveryGroup } ) ) 308 | { 309 | if( $selectedDeliveryGroup -is [array] ) 310 | { 311 | Write-Verbose -Message "Got $($deliveryGroups.Count) matching `"$deliveryGroup`" - $(($selectedDeliveryGroup | Select-Object -ExpandProperty Name) -join ',')" 312 | } 313 | else 314 | { 315 | Write-Verbose -Message "Got delivery group `"$($selectedDeliveryGroup.Name) matching `"$deliveryGroup`"" 316 | 317 | $selectedDeliveryGroup ## output delivery group object to the pipeline 318 | 319 | [hashtable]$body = @{} 320 | 321 | if( $PSBoundParameters[ 'updatedDeliveryGroupDescription' ] ) 322 | { 323 | $body.Add( 'Description' , $updatedDeliveryGroupDescription ) 324 | } 325 | if( $PSBoundParameters.ContainsKey( 'enableDeliveryGroup' )) 326 | { 327 | $body.Add( 'Enabled' , $enableDeliveryGroup ) 328 | } 329 | 330 | if( $body.Count -gt 0 ) 331 | { 332 | ## https://developer.cloud.com/citrixworkspace/virtual-apps-and-desktops/cvad-rest-apis/docs/how-to-update-a-delivery-group 333 | $RESTparams.Uri = "$($RESTparams.URI)/$($selectedDeliveryGroup.Id)" 334 | $RESTparams.Method = 'PATCH' 335 | [bool]$success = $false 336 | 337 | Invoke-CVADRestMethod -RESTparams $RestParams -Body $body -itemDetails " delivery group `"$($selectedDeliveryGroup.Name)`"" ([ref]$success) 338 | if( $success ) 339 | { 340 | Write-Verbose -Message "Updated delivery group `"$($selectedDeliveryGroup.Name)`" ok" 341 | } 342 | } 343 | } 344 | } 345 | else 346 | { 347 | Write-Warning -Message "None of the $($deliveryGroups.Items.Count) delivery groups matched `"$deliveryGroup`"" 348 | } 349 | } 350 | } 351 | } 352 | } 353 | if( $iteration -ne $iterations) 354 | { 355 | Start-Sleep -Milliseconds $millisecondsPause ## don't swamp the poor Cloud instance if we are repeating :-) 356 | } 357 | } 358 | 359 | Write-Verbose -message "$iterations finished : $(Get-Date -Format G) " 360 | 361 | #endregion main 362 | -------------------------------------------------------------------------------- /Direct2Events.csv: -------------------------------------------------------------------------------- 1 | Scope,Name,Value,Warning,Critical,RightClickName,RightClickAction,RightClickInputs 2 | Machine,RAM (GB),"[math]::Round( $computerinfo.TotalPhysicalMemory / 1GB , 2 )",,$value -lt 26,,, 3 | Machine,Free RAM (GB),"[math]::Round( $osinfo.FreePhysicalMemory / 1MB , 2 )",$value -lt 10,$value -lt 5,,, 4 | Machine,Virtual memory (GB),"[math]::Round( $osinfo.TotalVirtualMemorySize / 1MB , 2 )",,,,, 5 | Machine,Free virtual memory (GB),"[math]::Round( $osinfo.FreeVirtualMemory / 1MB ,2 )",,,,, 6 | Machine,Processors,$computerinfo.NumberOfLogicalProcessors,,,,, 7 | Machine,Hypervisor,$vmInfo.VMHost,,,,, 8 | Machine,Install Date,$osinfo.InstallDate,,,,, 9 | Machine,OS,$osinfo.Caption,,,,, 10 | Machine,Description,$osinfo.Description,,,,, 11 | Machine,OS version,$osinfo.Version,,,,, 12 | Machine,Service pack version,$osinfo.ServicePackMajorVersion,,,,, 13 | Machine,Delivery Group,$deliveryGroup,[string]::IsNullOrEmpty( $value ),,,, 14 | Machine,Machine Catalogue,$machineCatalogue,,,,, 15 | Machine,Registration State,$brokerMachine.RegistrationState,,$value -ne 'Registered',,, 16 | Machine,AppSense Deployment Group,$appsenseGroup.Name,$value -eq $null,,, 17 | Machine,AppSense Deployment Group events,$appsenseGroup.EnterpriseAuditing.Count,$value -gt 30,$value -gt 50,,, 18 | Machine,Last AppSense poll time,$appsenseMachine.LastPollTime,($value - (Get-Date)).TotalDays -gt 1,($value - (Get-Date)).TotalDays -gt 7,,, 19 | Machine,AppSense download poll period (s),$appsenseGroup.PollPeriodSeconds,$value -lt 3600,$value -le 300,,, 20 | Machine,AppSense upload poll period (s),$appsenseGroup.UploadPollPeriodSeconds,$value -lt 600,$value -le 300,,, 21 | Machine,Published Applications,"$brokerMachine.PublishedApplications -join ""`n""",$value -eq $null,,,, 22 | Machine,Zone,$brokerMachine.ZoneName,,,,, 23 | Machine,Connected Users,$activeUsers,,,,, 24 | Machine,Load Index,$brokerMachine.LoadIndex,,,,, 25 | Machine,Disconnected Users,$disconnectedUsers,,,,, 26 | Machine,Maintenance mode,$brokerMachine.InMaintenanceMode,,,,, 27 | Machine,Logon mode,$brokerMachine.WindowsConnectionSetting,$value -ne 'LogonEnabled',,,, 28 | Machine,Power State,$brokerMachine.PowerState,,,,, 29 | Machine,Last boot time,$osinfo.LastBootUpTime,($value - (Get-Date)).TotalDays -gt 7,($value - (Get-Date)).TotalDays -gt 14,,, 30 | Machine,Tags,"$brokermachine.Tags -join ""\n""",$unqualifiedMachineName -match '^YWCX[DA]' -and $brokermachine.Tags -and $brokermachine.Tags.Count -gt 1,$unqualifiedMachineName -match '^YWCX[DA]' -and ! $brokermachine.Tags,,, 31 | Machine,vDisk in use,$vdisk.Name,,$value -ne $vdisks.Name,,, 32 | Machine,vDisk assigned,$vdisks.Name,,,,, 33 | Machine,vDisk mode,if (( ( $vdiskInfo | Select-String '^_DiskMode=')-split '=')[1] -eq 'P' ) { 'Private' } else { 'Shared' },$value -eq 'P',,,, 34 | Machine,vDisk booted version,$pvsDeviceInfo.DiskVersion,,$value -ne $bootVersion,,, 35 | Machine,vDisk version to boot,$bootVersion,,,,, 36 | Machine,PVS Device Collection,$pvsDeviceInfo.CollectionName,,[string]::IsNullOrEmpty( $value ),,, 37 | Machine,PVS Site Name,$pvsDeviceInfo.SiteName,,[string]::IsNullOrEmpty( $value ),,, 38 | Machine,PVS RAM Cache (MB),$vdisk.WriteCacheSize,$value -lt 2048,$value -lt 512,,, 39 | Machine,PVS Disk Size (GB),[math]::round($vdisk.DiskSize / 1GB),$value -lt 30,$value -lt 20,,, 40 | Machine,PVS Disk Modified,$vdisk.Date,,,,, 41 | Machine,Group Memberships Count,$machineDetails.MemberOf.Count,,$value -lt 1,,, 42 | Machine,Group Memberships,"($machineDetails.MemberOf | Sort-Object | %{ (($_ -split ',')[0] -split '^CN=')[-1]}) -join ""`n""",,,,, 43 | Machine,Account Created,$machineDetails.whenCreated,,,,, 44 | Machine,Description,$machineDetails.Description,,,,, 45 | Machine,Managed By,$machineDetails.ManagedBy,,,,, 46 | Machine,"Ping Time (ms)","[math]::Round((Test-Connection $unqualifiedMachineName -Count 3 |Measure-Object -sum -Property ResponseTime).Sum/3,1)",$value -gt 10,$value -eq $null,,, 47 | User,Account Locked,$userDetails.LockedOut,,$value,Unlock,C:Unlock-ADAccount -Identity $value, 48 | User,Enabled,$userDetails.Enabled,,!$value,Enable,C:Enable-ADAccount -Identity $value, 49 | User,Expires,$userDetails.AccountExpirationDate,($value - (Get-Date)).TotalDays -lt 30,($value - (Get-Date)).TotalDays -lt 10,Extend Account,Set-ADAccountExpiration $user -DateTime '##Input1',DatePickerXAML:DatePicked.SelectedDate:Choose New Account Expiry Date 50 | User,Home Drive,$userDetails.HomeDrive,,[string]::IsNullOrEmpty( $value ),,, 51 | User,Logon Script,$userDetails.ScriptPath,,,,, 52 | User,Home Directory,$userDetails.HomeDirectory,,[string]::IsNullOrEmpty( $value ),Explore,explorer.exe $value, 53 | User,Created,$userDetails.Created,,,,, 54 | User,Account Changed,$userDetails.WhenChanged,,,,, 55 | User,Password Changed,$userDetails.PasswordLastSet,,,,, 56 | User,Password Expired,$userDetails.PasswordExpired,,$value,Reset Password,Set-ADAccountPassword -Identity $user -Reset -NewPassword (ConvertTo-SecureString -AsPlainText '##Input1') -Force,PasswordPickerXAML:passwordBox.Password:Choose New Password 57 | User,Password Never Expires,$userDetails.PasswordNeverExpires,$value,,,, 58 | User,Group Memberships Count,$userDetails.MemberOf.Count,$value -lt 10,$value -le 1,,, 59 | User,Group Memberships,"($userDetails.MemberOf | Sort-Object | %{ (($_ -split ',')[0] -split '^CN=')[-1]}) -join ""`n""",,,,, 60 | User,Primary Group,$userDetails.PrimaryGroup,,,,, 61 | User,Name,$userDetails.Name,,,,, 62 | User,SID,$userDetails.SID,,,,, 63 | User,OU,$userDetails.DistinguishedName,,,,, 64 | User,Full Name,($userdetails.GivenName + ' ' + $userDetails.Surname).Trim(),,,,, 65 | User,Title,$userDetails.Title,,,,, 66 | User,Department,$userDetails.Department,,,,, 67 | User,Email,$userDetails.EmailAddress,,,Email,Start-Process -FilePath mailto:$value, 68 | User,Office Phone,$userDetails.OfficePhone,,,,, 69 | User,Telephone Number,$userDetails.TelephoneNumber,,,,, 70 | -------------------------------------------------------------------------------- /End Disconnected sessions.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3.0 2 | <# 3 | Script to find disconnected sessions and end them if they have been disconnected over specified period. 4 | Will aslo atttempt to terminate any processes running for users specified on the command line 5 | 6 | Guy Leech, 2018 7 | 8 | Use this script at your own risk - no warranty provided 9 | #> 10 | 11 | 12 | <# 13 | .SYNOPSIS 14 | 15 | Find disconnected XenApp sessions disconnected over a specified threshold and terminate them. Can also terminate specified processes in case they are preventing logoff. 16 | 17 | .PARAMETER threshold 18 | 19 | Disconnection time in hours and minutes, e.g. 6:30, over which the session will be disconnected 20 | 21 | .PARAMETER ddc 22 | 23 | The Desktop Delivery Controller to query to get disconnected session information 24 | 25 | .PARAMETER forceIt 26 | 27 | Do not prompt for confirmation before terminating stuck processes and disconnecting processes 28 | 29 | .PARAMETER logfile 30 | 31 | A csv file that will be appended to with details of the sessions terminated 32 | 33 | .PARAMETER killProcesses 34 | 35 | A comma separated list of processes to terminate if they are running in the users session 36 | 37 | .EXAMPLE 38 | 39 | & '.\End Disconnected sessions.ps1' -threshold 8:30 -ddc ctxddc01 -killProcesses stuckprocess,anotherstuckprocess -logfile c:\support\disconnected.terminations.csv 40 | 41 | End all disconnected sessions found via delivery controller ctxddc01 which have been disconnected for more than 8 hours 30 minutes. 42 | If there are processes still running, before the session is ternminated, called stuckprocessor or anotherstuckprocess they will be terminated. 43 | Results will be appended to the CSV file c:\support\disconnected.terminations.csv 44 | 45 | #> 46 | 47 | [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')] 48 | 49 | Param 50 | ( 51 | [Parameter(Mandatory=$false,HelpMessage='Disconnected threshold in -hours:minutes')] 52 | [string]$threshold , 53 | [string]$ddc = 'localhost' , 54 | [switch]$forceIt , 55 | [string]$logfile , 56 | [string[]]$killProcesses 57 | ) 58 | 59 | [string[]]$snapins = @( 'Citrix.Broker.Admin.*' ) 60 | 61 | if( $killProcesses -and $killProcesses -contains 'csrss' ) 62 | { 63 | Write-Error 'Kiling csrss causes BSoDs so not continuing' 64 | return 65 | } 66 | 67 | [int]$hours,[int]$minutes = $threshold -split ':' 68 | 69 | if( ( ! $hours -and ! $minutes ) -or $minutes -lt 0 -or $minutes -gt 59 ) 70 | { 71 | Write-Error "Bad threshold of $threshold specified" 72 | return 73 | } 74 | 75 | ## Reform in case minutes wasn't specified 76 | $threshold = "-$([math]::Abs( $hours )):$minutes" 77 | 78 | ForEach( $snapin in $snapins ) 79 | { 80 | Add-PSSnapin $snapin -ErrorAction Stop 81 | } 82 | 83 | if( $forceIt ) 84 | { 85 | $ConfirmPreference = 'None' 86 | } 87 | 88 | # reform incase flattened by scheduled task engine 89 | if( $killProcesses -and $killProcesses.Count -eq 1 -and $killProcesses.IndexOf(',') -ge 0 ) 90 | { 91 | $killProcesses = $killProcesses -split ',' 92 | } 93 | 94 | $disconnected = @( Get-BrokerSession -AdminAddress $ddc -SessionState 'Disconnected' -Filter { SessionStateChangeTime -lt $threshold } ) 95 | 96 | Write-Verbose "Got $($disconnected.Count) disconnected sessions over $threshold`n$($disconnected|select username,UntrustedUserName,HostedMachineName,StartTime,SessionStateChangeTime|Format-Table -AutoSize|Out-String)" 97 | 98 | if( $disconnected -and $disconnected.Count -gt 0 ) 99 | { 100 | [array]$processes = @() 101 | if( $killProcesses -and $killProcesses.Count ) 102 | { 103 | ForEach( $session in $disconnected ) 104 | { 105 | [string]$username = $session.Username 106 | if( [string]::IsNullOrEmpty( $username ) ) 107 | { 108 | $username = $session.UntrustedUsername 109 | } 110 | $username = ($username -split '\\')[-1] ## strip domain name off 111 | if( ! [string]::IsNullOrEmpty( $session.HostedMachineName ) -and ! [string]::IsNullOrEmpty( $username ) ) 112 | { 113 | if( (quser /server:$($session.HostedMachineName)|select -skip 1| Where-Object{ $_ -match "[^a-z0-9_]$username\s+(\d+)\s" }) ) 114 | { 115 | [int]$sessionId = $Matches[1].Trim() 116 | if( $sessionId -gt 0 ) 117 | { 118 | ## have to remote it as doesn't return session ids if run via -ComputerName. Can't check username as may be system processes in that session 119 | $processes = @( Invoke-Command -ComputerName $session.HostedMachineName -ScriptBlock { Get-Process -IncludeUserName -Name $using:killProcesses | Where-Object { $_.SessionId -eq $using:sessionId } } ) 120 | if( $processes -and $processes.Count ) 121 | { 122 | Add-Member -InputObject $session -MemberType NoteProperty -Name ProcessesKilled -Value ( ( $processes | select -ExpandProperty Name ) -join ',' ) 123 | if( $PSCmdlet.ShouldProcess( "Session $sessionId for $username on $($session.HostedMachineName)" , "Kill processes $(($processes|Select -ExpandProperty Name) -join ',')" ) ) 124 | { 125 | Invoke-Command -ComputerName $session.HostedMachineName -ScriptBlock { $using:processes | Stop-Process -Force -PassThru } 126 | } 127 | } 128 | else 129 | { 130 | Write-Warning "Found no $($killProcesses -join ',') processes to kill in session $sessionId for $username on $($session.HostedMachineName)" 131 | } 132 | } 133 | else 134 | { 135 | Write-Warning "Failed to get session id via quser for $username on $($session.HostedMachineName)" 136 | } 137 | } 138 | else 139 | { 140 | Write-Warning "Failed to get session via quser for $username on $($session.HostedMachineName)" 141 | } 142 | } 143 | else 144 | { 145 | Write-Warning "Couldn't get username `"$username`" or host `"$($session.HostedMachineName)`"" 146 | } 147 | } 148 | } 149 | if( $PSCmdlet.ShouldProcess( "$($disconnected.Count) disconnected sessions" , 'Log off' ) ) 150 | { 151 | if( ! [String]::IsNullOrEmpty($logfile) ) 152 | { 153 | $disconnected | Select-Object -Property @{n='Sampled';e={Get-Date}},Username,StartTime,UntrustedUserName,SessionStateChangeTime,HostedMachineName,ClientName,ClientAddress,CatalogName,DesktopGroupName,ControllerDNSName,HostingServerName,ProcessesKilled | Export-Csv -NoTypeInformation -Append -Path $logfile 154 | } 155 | 156 | $disconnected | Stop-BrokerSession 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Get Citrix admin logs.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3 2 | <# 3 | Retrieve Citrix Studio logs 4 | 5 | @guyrleech 2018 6 | #> 7 | 8 | <# 9 | .SYNOPSIS 10 | 11 | Produce grid view or csv report of Citrix XenApp/XenDesktop admin logs such as from actions in Studio or Director 12 | 13 | .PARAMETER ddc 14 | 15 | The Delivery Controller to connect to, defaults to the local machine which must have the Citrix Studio PowerShell modules available 16 | 17 | .PARAMETER username 18 | 19 | Only return records for the specified user 20 | 21 | .PARAMETER operation 22 | 23 | Only return records which match the specified operation such as "log off" or "shadow" 24 | 25 | .PARAMETER start 26 | 27 | Only return records created on or after the given date/time 28 | 29 | .PARAMETER end 30 | 31 | Only return records created on or before the given date/time 32 | 33 | .PARAMETER last 34 | 35 | Only return records created in the last x seconds/minutes/hours/days/weeks years, e.g. 1d for 1 day or 12h for 12 hours 36 | 37 | .PARAMETER outputfile 38 | 39 | Write the returned records to the csv file named 40 | 41 | .PARAMETER gridview 42 | 43 | Display the returned results in an on screen filterable and sortable grid view 44 | 45 | .PARAMETER configChange 46 | 47 | Only return records which are for configuration changes 48 | 49 | .PARAMETER adminActions 50 | 51 | Only return records for administrative actions like shadowing or logging off 52 | 53 | .PARAMETER studioOnly 54 | 55 | Only return records for operations performed via Studio 56 | 57 | .PARAMETER directorOnly 58 | 59 | Only return records for operations performed via Director 60 | 61 | .PARAMETER maxRecordCount 62 | 63 | Returns at most this number of records. If more records are available than have been returned then a warning message will be displayed. 64 | 65 | .EXAMPLE 66 | 67 | & '.\Get Citrix admin logs.ps1' -username manuel -gridview -last 14d -operation "Shadow" 68 | 69 | Show all shadow operations performed by the user manuel in the last 14 days and display in a grid view 70 | 71 | .EXAMPLE 72 | 73 | & '.\Get Citrix admin logs.ps1' -start "01/01/2018" -end "31/01/2018" -configChange -outputfile c:\temp\citrix.changes.csv 74 | 75 | Show all configuration changes made between the 1st and 31st of January 2018 and write the results to c:\temp\citrix.changes.csv 76 | 77 | #> 78 | 79 | [CmdletBinding()] 80 | 81 | Param 82 | ( 83 | [string]$ddc = 'localhost' , 84 | [string]$username , 85 | [switch]$configChange , 86 | [switch]$adminAction , 87 | [switch]$studioOnly , 88 | [switch]$directorOnly , 89 | [string]$operation , 90 | [Parameter(Mandatory=$true, ParameterSetName = "TimeSpan")] 91 | [datetime]$start , 92 | [Parameter(Mandatory=$false, ParameterSetName = "TimeSpan")] 93 | [datetime]$end = [datetime]::Now , 94 | [Parameter(Mandatory=$true, ParameterSetName = "Last")] 95 | [string]$last , 96 | [string]$outputFile , 97 | [int]$maxRecordCount = 5000 , 98 | [switch]$gridview 99 | ) 100 | 101 | if( $studioOnly -and $directorOnly ) 102 | { 103 | Throw "Cannot specify both -studioOnly and -directorOnly" 104 | } 105 | 106 | if( ! [string]::IsNullOrEmpty( $last ) ) 107 | { 108 | [long]$multiplier = 0 109 | switch( $last[-1] ) 110 | { 111 | "s" { $multiplier = 1 } 112 | "m" { $multiplier = 60 } 113 | "h" { $multiplier = 3600 } 114 | "d" { $multiplier = 86400 } 115 | "w" { $multiplier = 86400 * 7 } 116 | "y" { $multiplier = 86400 * 365 } 117 | default { Throw "Unknown multiplier `"$($last[-1])`"" } 118 | } 119 | if( $last.Length -le 1 ) 120 | { 121 | $start = $end.AddHours( -$multiplier ) 122 | } 123 | else 124 | { 125 | $start = $end.AddSeconds( - ( ( $last.Substring( 0 ,$last.Length - 1 ) -as [long] ) * $multiplier ) ) 126 | } 127 | } 128 | elseif( ! $PSBoundParameters[ 'start' ] ) 129 | { 130 | $start = (Get-Date).AddDays( -7 ) 131 | } 132 | 133 | Add-PSSnapin -Name 'Citrix.ConfigurationLogging.Admin.*' 134 | 135 | if( ! ( Get-Command -Name 'Get-LogHighLevelOperation' -ErrorAction SilentlyContinue ) ) 136 | { 137 | Throw "Unable to find the Citrix Get-LogHighLevelOperation cmdlet required" 138 | } 139 | 140 | [hashtable]$queryparams = @{ 141 | 'AdminAddress' = $ddc 142 | 'SortBy' = '-StartTime' 143 | 'MaxRecordCount' = $maxRecordCount 144 | 'ReturnTotalRecordCount' = $true 145 | } 146 | if( $configChange -and ! $adminAction ) 147 | { 148 | $queryparams.Add( 'OperationType' , 'ConfigurationChange' ) 149 | } 150 | elseif( ! $configChange -and $adminAction ) 151 | { 152 | $queryparams.Add( 'OperationType' , 'AdminActivity' ) 153 | } 154 | if( ! [string]::IsNullOrEmpty( $username ) ) 155 | { 156 | if( $username.IndexOf( '\' ) -lt 0 ) 157 | { 158 | $username = $env:USERDOMAIN + '\' + $username 159 | } 160 | $queryparams.Add( 'User' , $username ) 161 | } 162 | if( $directorOnly ) 163 | { 164 | $queryparams.Add( 'Source' , 'Citrix Director' ) 165 | } 166 | if( $studioOnly ) 167 | { 168 | $queryparams.Add( 'Source' , 'Studio' ) 169 | } 170 | 171 | $recordCount = $null 172 | 173 | [array]$results = @( Get-LogHighLevelOperation -Filter { StartTime -ge $start -and EndTime -le $end } @queryparams -ErrorAction SilentlyContinue -ErrorVariable RecordCount | ForEach-Object -Process ` 174 | { 175 | if( [string]::IsNullOrEmpty( $operation ) -or $_.Text -match $operation ) 176 | { 177 | $result = [pscustomobject][ordered]@{ 178 | 'Started' = $_.StartTime 179 | 'Duration (s)' = [math]::Round( (New-TimeSpan -Start $_.StartTime -End $_.EndTime).TotalSeconds , 2 ) 180 | 'User' = $_.User 181 | 'From' = $_.AdminMachineIP 182 | 'Operation' = $_.text 183 | 'Source' = $_.Source 184 | 'Type' = $_.OperationType 185 | 'Targets' = $_.TargetTypes -join ',' 186 | 'Successful' = $_.IsSuccessful 187 | } 188 | if( ! $configChange ) 189 | { 190 | Add-Member -InputObject $result -NotePropertyMembers @{ 191 | 'Target Process' = $_.Parameters[ 'ProcessName' ] 192 | 'Target Machine' = $_.Parameters[ 'MachineName' ] 193 | 'Target User' = $_.Parameters[ 'UserName' ] 194 | } 195 | } 196 | $result 197 | } 198 | } ) 199 | 200 | if( $recordCount -and $recordCount.Count ) 201 | { 202 | if( $recordCount[0] -match 'Returned (\d+) of (\d+) items' ) 203 | { 204 | if( [int]$matches[1] -lt [int]$matches[2] ) 205 | { 206 | Write-Warning "Only retrieved $($matches[1]) of a total of $($matches[2]) items, use -maxRecordCount to return more" 207 | } 208 | ## else we got all the records 209 | } 210 | else 211 | { 212 | Write-Error $recordCount[0] 213 | } 214 | } 215 | 216 | if( ! $results -or ! $results.Count ) 217 | { 218 | Write-Warning "No log entries found between $(Get-Date $start -Format G) and $(Get-Date $end -Format G)" 219 | } 220 | else 221 | { 222 | if( ! [string]::IsNullOrEmpty( $outputFile ) ) 223 | { 224 | $results | Export-Csv -Path $outputFile -NoClobber -NoTypeInformation 225 | } 226 | elseif( $gridview ) 227 | { 228 | $selected = $results | Out-GridView -Title "$($results.Count) events from $(Get-Date $start -Format G) and $(Get-Date $end -Format G)" -PassThru 229 | if( $selected ) 230 | { 231 | $selected | clip.exe 232 | } 233 | } 234 | else 235 | { 236 | $results 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Get PVS boot time stats.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3.0 2 | 3 | <# 4 | Get Citrix PVS target boot time events from event log and convert to CSV for reporting or alerting purposes 5 | 6 | Ensure that each PVS server's stream service has event logging enabled 7 | 8 | Guy Leech, 2017 9 | 10 | Modification history: 11 | 12 | 13/02/18 GL Added chart view option 13 | #> 14 | 15 | <# 16 | .SYNOPSIS 17 | 18 | Get Citrix PVS target boot time events from event logs and output to CSV or a chart for reporting or alerting, by email, purposes 19 | 20 | .DESCRIPTION 21 | 22 | .PARAMETER computers 23 | 24 | A comma separated list of PVS servers to query. Defaults to the computer running the script if none is given. 25 | 26 | .PARAMETER last 27 | 28 | Show boot times in the preceding period where 's' is seconds, 'm' is minutes, 'h' is hours, 'd' is days, 'w' is weeks and 'y' is years so 7d will show all events in the last 7 days. The default is all events. 29 | 30 | .PARAMETER output 31 | 32 | CSV file to create with the results. 33 | 34 | .PARAMETER meanAbove 35 | 36 | If the mean (average) time (in seconds) exceeds this value then an email alert is sent. 37 | 38 | .PARAMETER medianAbove 39 | 40 | If the median time (in seconds) exceeds this value then an email alert is sent. 41 | 42 | .PARAMETER modeAbove 43 | 44 | If the mode time (in seconds) exceeds this value then an email alert is sent. 45 | 46 | .PARAMETER slowestAbove 47 | 48 | If the slowest time (in seconds) exceeds this value then an email alert is sent. 49 | 50 | .PARAMETER gridView 51 | 52 | Output the results to a graphical grid view where they can be sorted/filtered 53 | 54 | .PARAMETER chartView 55 | 56 | Display the results in a chart 57 | 58 | .PARAMETER mailserver 59 | 60 | The SMTP email server to use for sending the email. 61 | 62 | .PARAMETER recipients 63 | 64 | Comma separated list of email addresses to send the email to. 65 | 66 | .PARAMETER subject 67 | 68 | The subject of the email. A default one is provided if none is specified. 69 | 70 | .PARAMETER alertSubject 71 | 72 | The subject of the alert email, if one is sent. A default one is provided if none is specified. 73 | 74 | .PARAMETER from 75 | 76 | The email address of the sender. A default one is provided if none is specified. 77 | 78 | .PARAMETER useSSL 79 | 80 | If specified, will communicate with the email server using SSL. 81 | 82 | .PARAMETER search 83 | 84 | Regex pattern to use when searching and replacing server names when they need sanitising for security purposes. 85 | 86 | .PARAMETER replace 87 | 88 | Regex pattern to replace in server names when they need sanitising for security purposes. 89 | 90 | .EXAMPLE 91 | 92 | & '.\Get PVS boot time stats.ps1' -last 7d -output c:\boot.times.csv 93 | 94 | Will show PVS boot times in the last 7 days for the PVS server running the script and also output them to the file c:\boot.times.csv 95 | 96 | .EXAMPLE 97 | 98 | & '.\Get PVS boot time stats.ps1' -last 90d -chartview -computers pvsserver1,pvsserver2 99 | 100 | Will show PVS boot times in a chart in the last 90 days for the PVS servers pvsserver1 and pvsserver2 101 | 102 | .EXAMPLE 103 | 104 | & '.\Get PVS boot time stats.ps1' -last 7d -output c:\boot.times.csv -mailserver mailserver -recipients someone@somewhere.com -meanAbove 180 -computers pvsserver1,pvsserver2 105 | 106 | Will show PVS boot times in the last 7 days for the PVS servers pvsserver1 and pvsserver2 and if the average boot time is longer than 3 minutes it will send an email with the details to someone@somewhere.com. 107 | 108 | .NOTES 109 | 110 | Ensure that each PVS server's stream service has event logging enabled in order for the events this script looks for are generated. 111 | 112 | #> 113 | 114 | [CmdletBinding()] 115 | 116 | Param 117 | ( 118 | [string[]]$computers = @( 'localhost' ) , 119 | [string]$last , 120 | [string]$output , 121 | [switch]$gridView , 122 | [switch]$chartView , 123 | [int]$chartType = -1 , 124 | [int]$meanAbove = -1 , 125 | [int]$medianAbove = -1 , 126 | [int]$modeAbove = -1 , 127 | [int]$slowestAbove = -1 , 128 | [string]$mailserver , 129 | [string[]]$recipients , 130 | [string]$subject = "Citrix PVS boot times from $env:COMPUTERNAME" , 131 | [string]$alertSubject = "Citrix PVS boot times alert from $env:COMPUTERNAME" , 132 | [string]$from = "$($env:COMPUTERNAME)@$($env:USERDNSDOMAIN)" , 133 | [switch]$useSSL , 134 | [string]$providerName = 'StreamProcess' , 135 | [int]$eventId = 10 , 136 | [string]$eventLog = 'Application' , 137 | [string]$search , 138 | [string]$replace 139 | ) 140 | 141 | [array]$events = @() 142 | [int]$slowest = 0 143 | [int]$fastest = [int]::MaxValue 144 | [long]$totalTime = 0 145 | [int]$count = 1 146 | [hashtable]$modes = @{} 147 | [dateTime]$startDate = (Get-Date).AddYears( -20 ) ## Should be long enough ago! 148 | 149 | if( ! [string]::IsNullOrEmpty( $last ) ) 150 | { 151 | ## see what last character is as will tell us what units to work with 152 | [int]$multiplier = 0 153 | switch( $last[-1] ) 154 | { 155 | "s" { $multiplier = 1 } 156 | "m" { $multiplier = 60 } 157 | "h" { $multiplier = 3600 } 158 | "d" { $multiplier = 86400 } 159 | "w" { $multiplier = 86400 * 7 } 160 | "y" { $multiplier = 86400 * 365 } 161 | default { Write-Error "Unknown multiplier `"$($last[-1])`"" ; return } 162 | } 163 | $endDate = Get-Date 164 | if( $last.Length -le 1 ) 165 | { 166 | $startDate = $endDate.AddSeconds( -$multiplier ) 167 | } 168 | else 169 | { 170 | $startDate = $endDate.AddSeconds( - ( ( $last.Substring( 0 ,$last.Length - 1 ) -as [int] ) * $multiplier ) ) 171 | } 172 | } 173 | 174 | $events = ForEach( $computer in $computers ) 175 | { 176 | Write-Verbose "$count / $($computers.Count ) : processing $computer from $startDate" 177 | @( Get-WinEvent -ComputerName $computer -FilterHashtable @{Logname=$eventLog;ID=$eventId;ProviderName=$providerName;StartTime=$startDate} | Where-Object { $_.Message -match 'boot time'}|select TimeCreated,Message | ForEach-Object ` 178 | { 179 | ## Message will be "Device xxxxx boot time: 2 minutes 50 seconds." 180 | if( $_.Message -match '^Device (?[^\s]+) boot time: (?\d+) minutes (?\d+) seconds\.$' ) 181 | { 182 | [int]$boottime = ( $matches[ 'minutes' ] -as [int] ) * 60 + ( $matches[ 'seconds' ] -as [int] ) 183 | New-Object -TypeName PSCustomObject -Property (@{ 'TimeCreated' = $_.TimeCreated ; 'Server' = $computer ; 'Target' = $matches[ 'Target' ] ; 'BootTime' = $boottime }) 184 | $totalTime += $boottime 185 | if( $boottime -gt $slowest ) 186 | { 187 | $slowest = $boottime 188 | } 189 | if( $boottime -lt $fastest ) 190 | { 191 | $fastest = $boottime 192 | } 193 | ## Add to hash table for mode calculation 194 | try 195 | { 196 | $modes.Add( $boottime , 1 ) 197 | } 198 | catch 199 | { 200 | $modes.Set_Item( $boottime , $modes[ $boottime ] + 1 ) 201 | } 202 | } 203 | }) 204 | $count++ 205 | } 206 | 207 | if( $events.Count -gt 0 ) 208 | { 209 | ## See if we need to transmogrify names to protect sensitive information 210 | if( ! [string]::IsNullOrEmpty( $search ) ) 211 | { 212 | $events | ForEach-Object ` 213 | { 214 | $_.Server = $_.Server -replace $search , $replace 215 | } 216 | $computers = $computers -replace $search , $replace 217 | $subject = $subject -replace $search , $replace 218 | } 219 | 220 | ## Now find median (middle) value 221 | [array]$sorted = $events | select BootTime | sort BootTime 222 | 223 | ## Now find mode (commonest) value 224 | [int]$mode = 0 225 | [int]$lastHighestCount = 0 226 | [int]$highestCount = 0 227 | 228 | $modes.GetEnumerator() | ForEach-Object ` 229 | { 230 | if( $_.Value -gt $highestCount ) 231 | { 232 | $lastHighestCount = $highestCount 233 | $highestCount = $_.Value 234 | $mode = $_.Key 235 | } 236 | } 237 | 238 | if( $highestCount -eq $lastHighestCount -or ( $highestCount -eq 1 -and $modes.Count -gt 1 ) ) 239 | { 240 | $mode = 0 ## no single most common boot time 241 | } 242 | 243 | [int]$median = $sorted[$sorted.Count / 2].BootTime 244 | [int]$mean = [math]::Round( $totalTime / $events.Count ) 245 | 246 | [string]$summary = "Got $($events.Count) events from $($computers.Count) machines : fastest $fastest s slowest $slowest s mean $mean s median $median s mode $mode s ($highestCount instances)" 247 | 248 | Write-Output $summary 249 | 250 | if( ! [string]::IsNullOrEmpty( $output ) ) 251 | { 252 | $events | Export-Csv -Path $output -NoTypeInformation -NoClobber 253 | } 254 | 255 | [bool]$alert = $false 256 | [string]$cause = $null 257 | [bool]$alerting = $meanAbove -ge 0 -or $medianAbove -ge 0 -or $modeAbove -ge 0 -or $slowestAbove -ge 0 258 | [int]$threshold = 0 259 | 260 | if( $meanAbove -ge 0 -and $mean -gt $meanAbove ) 261 | { 262 | $alert = $true 263 | $cause = 'Mean of ' + $mean 264 | $threshold = $meanAbove 265 | } 266 | elseif( $medianAbove -ge 0 -and $median -gt $medianAbove ) 267 | { 268 | $alert = $true 269 | $cause = 'Median of ' + $median 270 | $threshold = $medianAbove 271 | } 272 | elseif( $modeAbove -ge 0 -and $mode -gt $modeAbove ) 273 | { 274 | $alert = $true 275 | $cause = 'Mode of ' + $mode 276 | $threshold = $modeAbove 277 | } 278 | elseif( $slowestAbove -ge 0 -and $slowest -gt $slowestAbove ) 279 | { 280 | $alert = $true 281 | $cause = 'Slowest of ' + $slowest 282 | $threshold = $slowestAbove 283 | } 284 | 285 | if( ! [string]::IsNullOrEmpty( $mailserver ) -And $recipients -And ( ! $alerting -or ( $alerting -and $alert ) ) ) 286 | { 287 | ## workaround for scheduled task not passing array through properly 288 | if( $recipients.Count -eq 1 -And $recipients[0].IndexOf(",") -ge 0 ) 289 | { 290 | $recipients = $recipients[0] -split "," 291 | } 292 | 293 | if( $recipients.Count -gt 0 ) 294 | { 295 | [hashtable]$params = @{} 296 | if( $alert ) 297 | { 298 | $params.Add( 'Body' , "$cause seconds exceeds threshold of $threshold for Citrix PVS target boot times since $(Get-Date -Date $startDate -Format F) on $computers" + "`n`n" + $summary ) 299 | $params.Add( 'Subject' , $alertSubject ) 300 | } 301 | else 302 | { 303 | $params.Add( 'Body' , "Citrix PVS target boot times since $(Get-Date -Date $startDate -Format F) on $computers" + "`n`n" + $summary ) 304 | $params.Add( 'Subject' , $subject ) 305 | } 306 | if( ! [string]::IsNullOrEmpty( $output ) ) 307 | { 308 | $params.Add( 'Attachments' , $output ) 309 | } 310 | 311 | Send-MailMessage -SmtpServer $mailserver -To $recipients -From $from -UseSsl:$useSSL @params 312 | } 313 | } 314 | 315 | if( $gridView ) 316 | { 317 | $events | Out-GridView -Title $subject 318 | } 319 | if( $chartView ) 320 | { 321 | Add-Type -AssemblyName System.Windows.Forms 322 | Add-Type -AssemblyName System.Windows.Forms.DataVisualization 323 | 324 | if( $chartType -lt 0 ) 325 | { 326 | $chartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Range 327 | } 328 | $Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart 329 | $chart.width = 900 330 | $chart.Height = 600 331 | [void]$chart.Titles.Add( ( $subject + " since $(Get-Date $startDate -Format 'G')" ) ) 332 | $ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea 333 | $Chart.ChartAreas.Add($ChartArea) 334 | $ChartArea.AxisY.Title = "Boot time (seconds)" 335 | ForEach( $computer in $computers ) 336 | { 337 | [void]$Chart.Series.Add($computer) 338 | $Chart.Series[$computer].ChartType = $chartType 339 | $legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend 340 | $legend.name = $computer 341 | $Chart.Legends.Add($legend) 342 | $Chart.Series[$computer].ToolTip = $computer 343 | 344 | $events | Where-Object { $_.Server -eq $computer } | ForEach-Object ` 345 | { 346 | $null = $Chart.Series[$computer].Points.AddXY( $_.TimeCreated , $_.BootTime ) 347 | } 348 | } 349 | 350 | $AnchorAll = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor 351 | [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left 352 | $Form = New-Object Windows.Forms.Form 353 | $Form.Width = $chart.Width 354 | $Form.Height = $chart.Height + 50 355 | $Form.controls.add($Chart) 356 | $Chart.Anchor = $AnchorAll 357 | 358 | $Form.Add_Shown({$Form.Activate()}) 359 | [void]$Form.ShowDialog() 360 | } 361 | } 362 | else 363 | { 364 | Write-Output "Found no events" 365 | } 366 | -------------------------------------------------------------------------------- /Ghost Hunter.ps1: -------------------------------------------------------------------------------- 1 | #Requires -version 3.0 2 | <# 3 | Check that all disconnected sessions still exist and if not report the gorey details 4 | 5 | Guy Leech, 2018 6 | 7 | Modification History: 8 | 9 | 29/05/18 GL Added help and remoting of Citrix cmdlets if not available locally 10 | 11 | 30/05/18 GL Added error checking for quser so does not report as ghost if error returned 12 | #> 13 | 14 | <# 15 | .SYNOPSIS 16 | 17 | Search for sessions that Citrix report as being disconnected where that session no longer exists on the specified server. These are referred to as "ghost" sessions. 18 | Optionally, set the state of that session to "hidden" which disables session sharing for that specific session allowing subsequently launched applications for that user to launch ok. 19 | 20 | .DESCRIPTION 21 | 22 | Ghost sessions should not occur but it has been observed to happen on at least 7.13. 23 | The script will also attempt to find the user's session logoff in the User Profile Service event log on the server reporting the ghost session and also if they have sessions on any other server. 24 | 25 | .PARAMETER ddc 26 | 27 | The Delivery Controller to query for disconnected sessions. 28 | 29 | .PARAMETER hide 30 | 31 | Any disconnected session found not to exist will have its Citrix "hidden" flag set so affected users launching new applications won't get an error due to session sharing failure. 32 | 33 | .PARAMETER mailServer 34 | 35 | Name of the SMTP server to use to send email notifying of ghost sessions. If not specified then the results will be displayed on screen in a grid view 36 | 37 | .PARAMETER proxyMailServer 38 | 39 | If the mail server only allows SMTP connections from specific machines, use this option to proxy the email via that machine 40 | 41 | .PARAMETER from 42 | 43 | The email address which the email will be sent from 44 | 45 | .PARAMETER subject 46 | 47 | The subject of the email. This can include PowerShell expressions which will be evaluated 48 | 49 | .PARAMETER forceIt 50 | 51 | Suppresses prompting to confirm that a session should be hidden 52 | 53 | .PARAMETER historyFile 54 | 55 | A file which keeps track of which sessions are already hidden so newly discovered ghost sessions can be highlighted and an email sent 56 | 57 | .PARAMETER subject 58 | 59 | The subject of the email sent to inform of new ghost sessions 60 | 61 | .EXAMPLE 62 | 63 | & '.\ghost hunter.ps1' -ddc ctxddc001 -hide -mailserver smtp001 -recipients guy.leech@mars.com,sarah.kennedy@venus.com -historyfile c:\scripts\ghosties.csv 64 | 65 | Query disconnected sessions from the Delivery Controller ctxddc001 and any ghost sessions will be set to hidden. If any of these are new since the script was last run, 66 | since they will have been recorded in the c:\scripts\ghosties.csv file, send an email to the listed recipients via SMTP mail server smtp001 67 | 68 | .EXAMPLE 69 | 70 | & '.\ghost hunter.ps1' -ddc ctxddc001 71 | 72 | Query disconnected sessions from the Delivery Controller ctxddc001 and any ghost sessions will be displayed in an on screen grid view. 73 | Any sessions selected when the "OK" button is clicked in the grid view will be placed in the clipboard. 74 | 75 | .NOTES 76 | 77 | If running the script as a scheduled task and using -hide then specify the -forceIt parameter otherwise the script will hang since it will prompt to confirm the hidden setting is to be set. 78 | The user running the script must have sufficient privileges to query the sessions via Get-BrokerSession, set the session state to "hidden" and to query remote event logs. 79 | 80 | #> 81 | 82 | [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')] 83 | 84 | Param 85 | ( 86 | [string]$ddc = 'localhost' , 87 | [switch]$hide , 88 | [string]$proxyMailServer = 'localhost' , 89 | [string]$mailserver , 90 | [string[]]$recipients , 91 | [switch]$forceIt , 92 | [string]$historyFile , 93 | [string]$subject = "Ghost XenApp sessions detected" , 94 | [string]$from = "$($env:COMPUTERNAME)@$($env:USERDNSDOMAIN)" , 95 | [int]$maxRecordCount = 10000 , 96 | [string[]]$snapins = @( 'Citrix.Broker.Admin.*' ) 97 | ) 98 | 99 | ForEach( $snapin in $snapins ) 100 | { 101 | Add-PSSnapin $snapin -ErrorAction Continue ## if this fails then we will try to import the snapin from the DDC specified 102 | } 103 | 104 | if( $forceIt ) 105 | { 106 | $ConfirmPreference = 'None' 107 | } 108 | 109 | ## if no local Citrix PoSH cmdlets then try and pull in from DDC 110 | $DDCsession = $null 111 | if( ( Get-Command -Name Get-BrokerSession -ErrorAction SilentlyContinue ) -eq $null ) 112 | { 113 | if( $ddc -eq 'localhost' ) 114 | { 115 | Write-Error "Unable to find required Citrix cmdlet Get-BrokerSession - aborting" ## we have already tried a direct Add-PSSnapin 116 | Exit 1 117 | } 118 | else 119 | { 120 | $DDCsession = New-PSSession -ComputerName $ddc 121 | if( $DDCsession ) 122 | { 123 | $null = Invoke-Command -Session $DDCsession -ScriptBlock { Add-PSSnapin $using:snapins } 124 | $null = Import-PSSession -Session $DDCsession -Module $snapins 125 | } 126 | else 127 | { 128 | Write-Error "Unable to remote to $ddc to add required Citrix cmdlet Get-BrokerSession - aborting" 129 | Exit 1 130 | } 131 | } 132 | } 133 | 134 | [int]$count = 0 135 | 136 | [hashtable]$sessions = @{} 137 | [datetime]$startTime = Get-Date 138 | 139 | [array]$results = @( Get-BrokerSession -AdminAddress $ddc -MaxRecordCount $maxRecordCount -SessionState Disconnected | ForEach-Object ` 140 | { 141 | $session = $_ 142 | [bool]$gotQuserError = $false 143 | [string]$domainname,$username = $session.UserName -split '\\' 144 | if( [string]::IsNullOrEmpty( $username ) ) 145 | { 146 | ## don't know why, it just does this occasionally 147 | $domainname,$username = $session.UntrustedUserName -split '\\' 148 | } 149 | $count++ 150 | Write-Verbose "$count : checking $UserName on $($session.HostedMachineName)" 151 | if( [string]::IsNullOrEmpty( $username ) ) 152 | { 153 | Write-Warning "No user name found for session on $($session.HostedMachineName) via client $($session.ClientName)" 154 | } 155 | else 156 | { 157 | [array]$serverSessions = $sessions[ $session.HostedMachineName ] 158 | if( ! $serverSessions ) 159 | { 160 | ## Get users from machine - if we just run quser then get error for no users so this method make it squeaky clean 161 | $pinfo = New-Object System.Diagnostics.ProcessStartInfo 162 | $pinfo.FileName = "quser.exe" 163 | $pinfo.Arguments = "/server:$($session.HostedMachineName)" 164 | $pinfo.RedirectStandardError = $true 165 | $pinfo.RedirectStandardOutput = $true 166 | $pinfo.UseShellExecute = $false 167 | $pinfo.WindowStyle = 'Hidden' 168 | $pinfo.CreateNoWindow = $true 169 | $process = New-Object System.Diagnostics.Process 170 | $process.StartInfo = $pinfo 171 | $null = $process.Start() 172 | $process.WaitForExit() 173 | ## Output of quser is fixed width but can't do simple parse as SESSIONNAME is empty when session is disconnected so we break it up based on header positions 174 | [string[]]$fieldNames = @( 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME' ) 175 | [string[]]$allOutput = $process.StandardOutput.ReadToEnd() -split "`n" 176 | [string]$errorOutput = $process.StandardError.ReadToEnd() 177 | [string]$header = $allOutput[0] 178 | if( ! [string]::IsNullOrEmpty( $errorOutput ) -and $errorOutput.IndexOf( 'Error' ) -ge 0 ) 179 | { 180 | Write-Warning "Got error from quser on $($session.HostedMachineName) so cannot tell if ghost session or not: $errorOutput" 181 | $gotQuserError = $true 182 | } 183 | else 184 | { 185 | $serverSessions = @( $allOutput | Select -Skip 1 | ForEach-Object ` 186 | { 187 | [string]$line = $_ 188 | if( ! [string]::IsNullOrEmpty( $line ) ) 189 | { 190 | $result = New-Object -TypeName PSCustomObject 191 | For( [int]$index = 0 ; $index -lt $fieldNames.Count ; $index++ ) 192 | { 193 | [int]$startColumn = $header.IndexOf($fieldNames[$index]) 194 | ## if last column then can't look at start of next field so use overall line length 195 | [int]$endColumn = if( $index -eq $fieldNames.Count - 1 ) { $line.Length } else { $header.IndexOf( $fieldNames[ $index + 1 ] ) } 196 | try 197 | { 198 | Add-Member -InputObject $result -MemberType NoteProperty -Name $fieldNames[ $index ] -Value ( $line.Substring( $startColumn , $endColumn - $startColumn ).Trim() ) 199 | } 200 | catch 201 | { 202 | throw $_ 203 | } 204 | } 205 | $result 206 | } 207 | }) 208 | $sessions.Add( $session.HostedMachineName , $serverSessions ) 209 | } 210 | } 211 | $usersActualSession = $null 212 | if( $serverSessions ) 213 | { 214 | ForEach( $serverSession in $serverSessions ) 215 | { 216 | if( $serverSession.Username -eq $UserName ) 217 | { 218 | $usersActualSession = $serverSession 219 | break 220 | } 221 | } 222 | } 223 | if( ! $usersActualSession -and ! $gotQuserError ) 224 | { 225 | $otherSessions = @( Get-BrokerSession -AdminAddress $ddc -UserName "$domainname\$UserName" | ?{ $_.SessionKey -ne $session.SessionKey } ) 226 | Add-Member -InputObject $session -MemberType NoteProperty -Name OtherSessions -Value ( ( $otherSessions | Select -ExpandProperty HostedMachineName ) -join ',' ) 227 | Write-Warning "No session found on server $($session.HostedMachineName) for user $username, has $($otherSessions.Count) other sessions" 228 | if( $hide -and ! $session.Hidden -and $PSCmdlet.ShouldProcess( $username , "Hide session on $($session.HostedMachineName)" ) ) 229 | { 230 | Write-Verbose "Setting hidden property" 231 | Set-BrokerSession -InputObject $session -Hidden $true 232 | } 233 | ## Get user logon and logoff events from that server to add to output 234 | $sid = (New-Object System.Security.Principal.NTAccount($domainname + '\' + $username)).Translate([System.Security.Principal.SecurityIdentifier]).value 235 | $events = @( Get-WinEvent -ComputerName $session.HostedMachineName -FilterHashtable @{ LogName = 'Microsoft-Windows-User Profile Service/Operational' ; id = 1,4 ; UserId = $sid } ) 236 | if( $events -and $events.Count ) 237 | { 238 | Add-Member -InputObject $session -MemberType NoteProperty -Name ActualLogonTime -Value ( $events | Where-Object { $_.Id -eq 1 } | Select -First 1 -ExpandProperty TimeCreated ) 239 | Add-Member -InputObject $session -MemberType NoteProperty -Name ActualLogoffTime -Value ( $events | Where-Object { $_.Id -eq 4 } | Select -First 1 -ExpandProperty TimeCreated ) 240 | } 241 | else 242 | { 243 | Write-Warning "Unable to find logon and logoff events for user $username (sid $sid) on $($session.HostedMachineName)" 244 | } 245 | $session 246 | } 247 | } 248 | }) 249 | 250 | if( $DDCsession ) 251 | { 252 | $null = Remove-PSSession -Session $DDCsession 253 | $DDCsession = $null 254 | } 255 | 256 | [string]$status = "Found $($results.Count) ghost sessions out of $count disconnected across $($sessions.Count) servers at $(Get-Date $startTime -Format G)" 257 | 258 | Write-Verbose $status 259 | 260 | if( $results -and $results.Count ) 261 | { 262 | if( [string]::IsNullOrEmpty( $mailserver ) ) 263 | { 264 | $selected = @( $results | Out-GridView -Title $status -PassThru ) 265 | if( $selected -and $selected.Count ) 266 | { 267 | $selected | Clip.exe 268 | } 269 | } 270 | else 271 | { 272 | [int]$alreadyAlerted = 0 273 | 274 | if( ! [string]::IsNullOrEmpty( $historyFile ) ) 275 | { 276 | [array]$existing = $null 277 | if( Test-Path -Path $historyFile -PathType Leaf -ErrorAction SilentlyContinue ) 278 | { 279 | $existing = Import-Csv -Path $historyFile 280 | } 281 | ForEach( $result in $results ) 282 | { 283 | ForEach( $ghost in $existing ) 284 | { 285 | if( $ghost.SessionId -eq $result.SessionId -and $ghost.hostedmachinename -eq $result.HostedMachineName ) 286 | { 287 | $alreadyAlerted++ 288 | Write-Verbose "Already alerted on $($result.Username) ($($result.untrustedusername)) on $($result.HostedMachineName)" 289 | break 290 | } 291 | } 292 | } 293 | 294 | $results | Export-Csv -Path $historyFile 295 | } 296 | 297 | if( $alreadyAlerted -ne $results.Count ) 298 | { 299 | if( $recipients[0].IndexOf( ',' ) -ge 0 ) 300 | { 301 | $recipients = $recipients -split ',' 302 | } 303 | 304 | [string]$style = "" 309 | 310 | [string]$body = ($results | Select UserName,UntrustedUserName,HostedMachineName,ActualLogonTime,ActualLogoffTime,OtherSessions,Hidden) | ConvertTo-Html -Head $style 311 | if( [string]::IsNullOrEmpty( $subject ) ) 312 | { 313 | $subject = Invoke-Expression $status ## expands any cmdlets such as Get-Date 314 | } 315 | Invoke-Command -ComputerName $proxyMailServer -ScriptBlock { Send-MailMessage -SmtpServer $using:mailserver -To $using:recipients -From $using:from -Subject $using:subject -Body $using:body -BodyAsHtml } 316 | } 317 | else 318 | { 319 | Write-Verbose "Not emailing as have already alerted on all $($results.Count) ghost sessions" 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Guy Leech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Modify and launch file.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | 4 | Make given changes to given file and then launch it via FTA. Can also create a shortcut in the sendto folder to a dynamically created wrapper script 5 | 6 | .DESCRIPTION 7 | 8 | Useful for ICA files from Citrix Virtual Apps & Desktops (XenApp/XenDesktop) when a published app doesn't launch in the resolution you need or across more monitors than you want. 9 | 10 | .PARAMETER path 11 | 12 | The path to the file to modify 13 | 14 | .PARAMETER replacements 15 | 16 | A comma separated list of strings to replace of the form sourcetext=newtext where the = delimiter can be changed by the -delimiter argument 17 | 18 | .PARAMETER install 19 | 20 | Install an explorer sendto menu shortcut with the name of the shortcut being the string passed to this parameter. A new script will be created with this name in the same folder as this script 21 | 22 | .PARAMETER uninstall 23 | 24 | Remove the given shortcut name from the user's sendto menu 25 | 26 | .PARAMETER description 27 | 28 | Description put in the shortcut 29 | 30 | .PARAMETER deleter 31 | 32 | The prefix to use to specify that if the following string is found in the input file that it will be deleted 33 | 34 | .PARAMETER encoding 35 | 36 | The encoding of the source file. If not specified the source file will be examined to determine the coding 37 | 38 | .PARAMETER deleteOriginal 39 | 40 | Delete the file specified by -path 41 | 42 | .PARAMETER failOnFail 43 | 44 | If no changes are made to the source file then do not launch the file 45 | 46 | .PARAMETER force 47 | 48 | Overwrite the temporary file that will be created from the source file by applying the specified replacements if it exists already 49 | 50 | .EXAMPLE 51 | 52 | & '.\Modify and launch file.ps1' -path c:\temp\fwerf.ica -replacements DesiredVRES=1360,DesiredHRES=2500,DesiredColor=24,DesiredColor=No -deleteOriginal 53 | 54 | Look for strings "DesiredVRES", "DesiredHRES" , "DesiredColor" and "DesiredColor" in the file c:\temp\fwerf.ica, create a new temporary file with the specified replacements for these strings and launch it via 55 | File Type Association, deleting the original source file 56 | 57 | .EXAMPLE 58 | 59 | & '.\Modify and launch file.ps1' -Install "Send ICA file to WQHD" -replacements DesiredVRES=1360,DesiredHRES=2500,DesiredColor=24,DesiredColor=No -deleteOriginal 60 | 61 | Create a shortcut called "Send ICA file to WQHD" in the calling user's explorer send to menu which runs a dynamically created script of the same name which calls this script with the specified arguments 62 | 63 | .NOTES 64 | 65 | Modification History: 66 | 67 | @guyrleech 21/02/20 Initial release 68 | 69 | #> 70 | 71 | [CmdletBinding()] 72 | 73 | Param 74 | ( 75 | [string]$path , 76 | [string[]]$replacements , 77 | [string]$delimiter = '=' , 78 | [string]$install , 79 | [string]$uninstall , 80 | [string]$description = "Modify and launch ICA file" , 81 | [string]$deleter , 82 | [string]$encoding , 83 | [switch]$deleteOriginal , 84 | [switch]$failOnFail , 85 | [switch]$force 86 | ) 87 | 88 | ## https://docs.microsoft.com/en-gb/archive/blogs/samdrey/determine-the-file-encoding-of-a-file-csv-file-with-french-accents-or-other-exotic-characters-that-youre-trying-to-import-in-powershell 89 | Function Get-FileEncoding 90 | { 91 | 92 | [CmdletBinding()] 93 | Param 94 | ( 95 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 96 | [string]$Path 97 | ) 98 | 99 | [byte[]]$byte = Get-Content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path 100 | 101 | if( $byte -and $byte.Count -eq 4 ) 102 | { 103 | if ( $byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf ) 104 | { 'UTF8' } 105 | elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) 106 | { 'Unicode' } 107 | elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) 108 | { 'UTF32' } 109 | elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) 110 | { 'UTF7'} 111 | else 112 | { 'ASCII' } 113 | } 114 | } 115 | 116 | if( $PSBoundParameters[ 'install' ] -or $PSBoundParameters[ 'uninstall' ] ) 117 | { 118 | [string]$launcherScriptContents = @' 119 | <# 120 | Pass file list via explorer and send to to another script 121 | 122 | @guyrleech 2020 123 | #> 124 | 125 | [string]$otherScript = Join-Path -Path ( Split-Path -Path (& { $myInvocation.ScriptName }) -Parent) -ChildPath '##ScriptName##' 126 | 127 | ForEach( $file in $args ) 128 | { 129 | & $otherScript ##Arguments## -path $file 130 | } 131 | '@ 132 | [string]$sendToFolder = [Environment]::GetFolderPath( [Environment+SpecialFolder]::SendTo ) 133 | [string]$lnkFile = Join-Path -Path $sendToFolder -ChildPath ( $(if( $PSBoundParameters[ 'install' ] ) { $install } else { $uninstall }) + '.lnk' ) 134 | 135 | if( $PSBoundParameters[ 'install' ] ) 136 | { 137 | if( Test-Path -Path $lnkFile -ErrorAction SilentlyContinue ) 138 | { 139 | Throw "Shortcut `"$lnkFile`" already exists" 140 | } 141 | [string]$launcherScript = Join-Path -Path (Split-Path -Path (& { $myInvocation.ScriptName }) -Parent) -ChildPath ($install + '.ps1') 142 | if( Test-Path -Path $launcherScript -ErrorAction SilentlyContinue ) 143 | { 144 | Throw "Launcher script `"$launcherScript`" already exists and not overwriting" 145 | } 146 | $self = Get-WmiObject -Class Win32_Process -Filter "ProcessId = '$pid'" 147 | [string]$powershellExecutable = $self.executablePath 148 | if( $powershellExecutable -match '^(.*)_ise(\.exe)$' ) 149 | { 150 | $powershellExecutable = $Matches[1] + $Matches[2] 151 | } 152 | 153 | ## build command line for worker script 154 | ## -replacements DesiredVRES=1360,DesiredHRES=2500,DesiredColor=24,RemoveICAFile=No -deleteOriginal 155 | [string]$commandLine = "-replacements `"$($replacements -join ',')`" -delimiter $delimiter" 156 | if( $PSBoundParameters[ 'deleter' ] ) 157 | { 158 | $commandLine += " -deleter $deleter" 159 | } 160 | if( $PSBoundParameters[ 'encoding' ] ) 161 | { 162 | $commandLine += " -encoding $encoding" 163 | } 164 | if( $deleteOriginal ) 165 | { 166 | $commandLine += " -deleteOriginal" 167 | } 168 | if( $failOnFail ) 169 | { 170 | $commandLine += " -failOnFail" 171 | } 172 | if( $force ) 173 | { 174 | $commandLine += " -force" 175 | } 176 | 177 | $launcherScriptContents -replace '##ScriptName##' , $(Split-Path -Path (& { $myInvocation.ScriptName }) -Leaf) -replace '##Arguments##' , $commandLine | Set-Content -Path $launcherScript 178 | $shellObject = New-Object -ComObject Wscript.Shell 179 | $shortcut = $shellObject.CreateShortcut( $lnkFile ) 180 | $shortcut.TargetPath = $powershellExecutable 181 | $shortcut.WorkingDirectory = Split-Path -Path $powershellExecutable -Parent 182 | $shortcut.Arguments = "-WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -file `"$launcherScript`"" 183 | $shortcut.Description = $description 184 | $shortcut.WindowStyle = 7 ## minimised 185 | $shortcut.Save() 186 | } 187 | elseif( $PSBoundParameters[ 'uninstall' ] ) 188 | { 189 | if( ! ( Test-Path -Path $shortcut -ErrorAction SilentlyContinue ) ) 190 | { 191 | Throw "Shortcut `"$shortcut`" does not exist" 192 | } 193 | Remove-Item -Path $shortcut 194 | } 195 | } 196 | else 197 | { 198 | [string]$extension = [System.IO.Path]::GetExtension( $path ) 199 | [string]$tempFile = [System.IO.Path]::GetTempFileName() + $extension 200 | 201 | if( ! ( Test-Path -Path $path -ErrorAction SilentlyContinue ) ) 202 | { 203 | Throw "Unable to open source file `"$path`"" 204 | } 205 | 206 | if( ( Test-Path -Path $tempFile -ErrorAction SilentlyContinue ) -and ! $force ) 207 | { 208 | Throw "Temp file `"$tempFile`" already exists and -force not specified" 209 | } 210 | 211 | if( $PSBoundParameters[ 'deleter' ] -and $delimiter.Length -ne 1 ) 212 | { 213 | Throw "Deleter must be a single character" 214 | } 215 | 216 | if( ! $PSBoundParameters[ 'encoding' ] ) 217 | { 218 | if( ! ( $encoding = Get-FileEncoding -Path $path ) ) 219 | { 220 | Throw "Unable to determine encoding of `"$path`" so specify with -encoding" 221 | } 222 | } 223 | 224 | ## may not be passed as an array so spit it out again although this will fail if there are commas, in quotes, in the array items 225 | if( $replacements[0].IndexOf( ',' ) -ge 0 ) 226 | { 227 | $replacements = $replacements -split ',' 228 | } 229 | 230 | ## One time splitting of replacements so we can match quickly when processing the file 231 | [hashtable]$itemsToMatch = @{} 232 | ForEach( $replacement in $replacements ) 233 | { 234 | Try 235 | { 236 | ## if the replacement starts with the deleter character then we delete the whole line 237 | if( $PSBoundParameters[ 'deleter' ] -and $replacement[0] -eq $deleter ) 238 | { 239 | $itemsToMatch.Add( $replacement.SubString( 1 ) , [int]-1 ) 240 | } 241 | else ## regular replacment/removal 242 | { 243 | [string[]]$split = $replacement -split $delimiter , 2 244 | ## if nothing after delimiter then it will replace the supplied value with the empty string 245 | $itemsToMatch.Add( $split[ 0 ] , [string]$(if( $split.Count -lt 2 -or [string]::IsNullOrEmpty( $split[1]) ) { "" } else { [Environment]::ExpandEnvironmentVariables( $split[ 1 ] ) } ) ) 246 | } 247 | } 248 | Catch 249 | { 250 | Throw "Duplicated element $replacement" 251 | } 252 | } 253 | 254 | Try 255 | { 256 | [int]$changes = 0 257 | $newContent = [IO.File]::ReadAllLines( $path ) | ForEach-Object ` 258 | { 259 | [string]$line = $_ 260 | $split = $line -split $delimiter , 2 261 | if( $split -and $split.Count -eq 2 ) 262 | { 263 | $matchedItem = $itemsToMatch[ $split[ 0 ] ] 264 | if( $matchedItem -ne $null ) 265 | { 266 | if( $matchedItem -is [int] ) 267 | { 268 | $line = $null 269 | $changes++ 270 | } 271 | else 272 | { 273 | $line = $split[ 0 ] + $delimiter + $matchedItem 274 | if( $matchedItem -eq $split[1] ) 275 | { 276 | Write-Warning "Not changed $($split[0]) as already `"$matchedItem`"" 277 | } 278 | else 279 | { 280 | $changes++ 281 | } 282 | } 283 | } 284 | } 285 | 286 | if( $line ) 287 | { 288 | $line 289 | } 290 | } 291 | 292 | if( ! $changes ) 293 | { 294 | if( $failOnFail ) 295 | { 296 | Throw "No changes made to file `"$path`"" 297 | } 298 | else 299 | { 300 | Write-Warning -Message "No changes made to file `"$path`"" 301 | } 302 | } 303 | 304 | ## Need to maintain encoding 305 | $newContent | Set-Content -Encoding $encoding -Path $tempFile 306 | 307 | if( Test-Path -Path $tempFile -ErrorAction SilentlyContinue ) 308 | { 309 | Write-Verbose -Message "Launching `"$newContent`"" 310 | $launched = Start-Process -FilePath $tempFile -Verb Open -PassThru 311 | if( ! $launched ) 312 | { 313 | Throw "Failed to launch `"$newContent`"" 314 | } 315 | elseif( $deleteOriginal ) 316 | { 317 | Remove-Item -Path $path -Force 318 | } 319 | } 320 | 321 | } 322 | Catch 323 | { 324 | Throw $_ 325 | } 326 | 327 | ## don't remove file as process using it may not have processed it yet 328 | } 329 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Citrix 2 | Automation for Citrix Virtual Apps and Desktops (formerly known as XenApp & XenDesktop) 3 | 4 | Other scripts I have written but are available elsewhere include: 5 | 6 | **New PVS Devices.ps1** - Create new Citrix PVS devices from a VMware vSphere template and optionally add to a Citrix machine catalogue and delivery group and optionally add to a published desktop. https://www.scriptrunner.com/en/blog/using-powershell-to-create-new-citrix-pvs-machines/ 7 | -------------------------------------------------------------------------------- /Recreate PVS XML manifest.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | 4 | Create a new XML manifest file for the given PVS vdisk, fetching version details from SQL and/or the vhdx/avhdx files on disk. This can then be used to import the disk into the PVS console. 5 | For use when there are multiple versions of the vdisk rather than a single, monolithic vhdx file. 6 | 7 | .DESCRIPTION 8 | 9 | A disk can be absent in the PVS console but still exist in the PVS SQL database. If not in SQL then the versions can be pieced togther from the vhdx/avhdx files albeit without descriptions and some assumptions 10 | 11 | .PARAMETER sqlServer 12 | 13 | The SQL server (and instance name) where the PVS database resides 14 | 15 | .PARAMETER database 16 | 17 | The PVS SQL database name 18 | 19 | .PARAMETER diskPath 20 | 21 | Full path to the vhdx or avhdx which is the first disk version to be added 22 | 23 | .PARAMETER credential 24 | 25 | Use this and pass a PSCredential object containing the credentials if SQL authentication is required rather than integrated Windows auth 26 | 27 | .PARAMETER production 28 | 29 | Make the highest disk version production rather than maintenance mode 30 | 31 | .PARAMETER startingVersion 32 | 33 | The lowest version of the disk to be included in the XML manifest file 34 | 35 | .PARAMETER pvsServer 36 | 37 | The PVS server to query if the PVS snapins are availble on the machine running the script. It will default to the server the script is running on. 38 | 39 | .PARAMETER pvsDll 40 | 41 | The full path to the Citrix dll providing the PVS snapin 42 | 43 | .PARAMETER secondsBeforeLastWrite 44 | 45 | If greater than or equal to zero will set the creation time of the vhdx/avhdx file to this number of seconds before the last write time of the disk 46 | 47 | .EXAMPLE 48 | 49 | & '.\Recreate PVS XML manifest.ps1' -sqlserver GRL-SQL01\Instance2 -database CitrixProvisioning -diskPath S:\Store\xaserver2016.vhdx -production 50 | 51 | Locate details for the disk xaserver2016 in the given SQL database, creating an importable XML file where the highest version of the disk is set to production mode 52 | 53 | & '.\Recreate PVS XML manifest.ps1' -diskPath S:\Store\xaserver2016.vhdx 54 | 55 | Find all versions of the base disk xaserver2016.vhdx in S:\Store creating an importable XML file where the highest version of the disk is set to maintenance mode 56 | 57 | .NOTES 58 | 59 | This script is used entirely at your own risk. The author accepts no responsibility for loss or damage caused by its use. Always backup existing XML files before running although the script will fail if one already exists for the given disk - it will not be overwritten 60 | 61 | Modification History: 62 | 63 | @guyrleech 11/02/20 Initial publice release 64 | 65 | #> 66 | 67 | [CmdletBinding()] 68 | 69 | Param 70 | ( 71 | [string]$sqlServer , 72 | [string]$database , 73 | [Parameter(mandatory=$true,HelpMessage='Path to base vdisk')] 74 | [string]$diskPath , 75 | [System.Management.Automation.PSCredential]$credential , 76 | [switch]$production , 77 | [string]$pvsServer , 78 | [int]$startingVersion = 0 , 79 | [string]$pvsDll = "$env:ProgramFiles\Citrix\Provisioning Services Console\Citrix.PVS.SnapIn.dll" , 80 | [int]$secondsBeforeLastWrite = -1 81 | ) 82 | 83 | Function Convert-XMLSpecialCharacters 84 | { 85 | Param 86 | ( 87 | [string]$string 88 | ) 89 | 90 | $string -replace '&' , '&' -replace '\<' , '<' -replace '\>' , '>' 91 | } 92 | 93 | [string]$queryDiskDetails = @' 94 | SELECT * 95 | FROM [DiskVersion] 96 | WHERE diskFileName = @vdiskname + '.vhdx' or diskFileName like @vdiskname + '.%.vhdx' or diskFileName like @vdiskname + '.%.avhdx' or @vdiskname + '.vhd' or diskFileName like @vdiskname + '.%.vhd' or diskFileName like @vdiskname + '.%.avhd' 97 | ORDER BY version DESC , diskId 98 | '@ 99 | 100 | [string]$diskName = [IO.Path]::GetFileNameWithoutExtension( $diskPath ) 101 | [string]$diskFolder = Split-Path -Path $diskPath -Parent 102 | [string]$XMLManifest = Join-Path -Path $diskFolder -ChildPath ( $diskName + '.xml' ) 103 | 104 | if( Test-Path -Path $XMLManifest -ErrorAction SilentlyContinue ) 105 | { 106 | Throw "XML manifest file `"$XMLManifest`" already exists" 107 | } 108 | 109 | if( ( Test-Path $pvsDll -ErrorAction SilentlyContinue ) -and (Import-Module -Name "$env:ProgramFiles\Citrix\Provisioning Services Console\Citrix.PVS.SnapIn.dll" -PassThru -Verbose:$false) ) 110 | { 111 | if( $PSBoundParameters[ 'pvsServer' ] ) 112 | { 113 | Set-PvsConnection -Server $pvsServer 114 | } 115 | if( $existingDisk = Get-PvsSite | Get-PvsDiskInfo | Where-Object { $_.DiskLocatorName -eq $diskName } ) 116 | { 117 | Throw "Disk $diskname already exists in PVS in site $($existingDisk.SiteName), store $($existingDisk.StoreName) created by $($existingDisk.Author) on $(Get-Date -Date ([datetime]$existingDisk.Date) -Format G)" 118 | } 119 | } 120 | 121 | if( $PSBoundParameters[ 'sqlServer' ] -or $PSBoundParameters[ 'database' ] ) 122 | { 123 | $connectionString = "Data Source=$sqlServer;Initial Catalog=$database;" 124 | 125 | if( $PSBoundParameters[ 'credential' ] ) 126 | { 127 | ## will only work for SQL auth, Windows must be done via RunAs 128 | $connectionString += "Integrated Security=no;" 129 | $connectionString += "uid=$($credential.UserName);" 130 | $connectionString += "pwd=$($credential.GetNetworkCredential().Password);" 131 | } 132 | else 133 | { 134 | $connectionString += "Integrated Security=SSPI;" 135 | } 136 | 137 | $dbConnection = New-Object -TypeName System.Data.SqlClient.SqlConnection 138 | $dbConnection.ConnectionString = $connectionString 139 | 140 | try 141 | { 142 | $dbConnection.open() 143 | } 144 | catch 145 | { 146 | Write-Error "Failed to connect with `"$connectionString`" : $($_.Exception.Message)" 147 | $connectionString = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 148 | Exit 1 149 | } 150 | 151 | $connectionString = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 152 | 153 | $cmd = New-Object -TypeName System.Data.SqlClient.SqlCommand 154 | $cmd.connection = $dbConnection 155 | 156 | $cmd.CommandText = $queryDiskDetails 157 | 158 | $null = $cmd.Parameters.AddWithValue( "@vdiskname" , $diskName ) 159 | 160 | if( $sqlreader = $cmd.ExecuteReader() ) 161 | { 162 | $datatable = New-Object System.Data.DataTable 163 | $datatable.Load( $sqlreader ) 164 | 165 | $sqlreader.Close() 166 | $dbConnection.Close() 167 | 168 | ## First pass to see if we have duplicate disk names 169 | [hashtable]$disks = @{} 170 | [int]$duplicates = 0 171 | $duplicateId = $null 172 | 173 | if( ! $datatable.Rows -or ! $datatable.Rows.Count ) 174 | { 175 | Throw "Found no results in SQL for `"$diskName`"" 176 | } 177 | 178 | ForEach( $version in $datatable.Rows ) 179 | { 180 | Try 181 | { 182 | $disks.Add($version.diskFileName , $version.diskId ) 183 | } 184 | Catch 185 | { 186 | $duplicates++ 187 | $duplicateId = $version.diskId 188 | } 189 | } 190 | 191 | if( $duplicates ) 192 | { 193 | Throw "Disk `"$diskPath`" exists more than once in SQL" 194 | } 195 | 196 | [string[]]$versions = @( ForEach( $version in $datatable.Rows ) 197 | { 198 | [string]$diskFile = Join-Path -Path $diskFolder -ChildPath $version.diskFileName 199 | $diskProperties = Get-ItemProperty -Path $diskFile -ErrorAction SilentlyContinue 200 | 201 | if( $secondsBeforeLastWrite -ge 0 -and $diskProperties -and $diskProperties.CreationTime -gt $diskProperties.LastWriteTime ) 202 | { 203 | Set-ItemProperty -Path $diskFile -Name CreationTime -Value ( $diskProperties.LastWriteTime.AddSeconds( -$secondsBeforeLastWrite )) 204 | } 205 | 206 | Write-Verbose -Message "Version $($version.version), description `"$($version.description)`"" 207 | 208 | @" 209 | 210 | $($version.version) 211 | $(Convert-XMLSpecialCharacters -string $version.description) 212 | $($version.type) 213 | $($version.access) 214 | $((Get-Date -Date ($version.createDate.ToUniversalTime() -as [datetime]) -Format s) -replace 'T' , ' ') 215 | $($version.scheduledDate) 216 | $(if( $version.deleteWhenFree -eq 'True' ) { 1 } else { 0 } ) 217 | $($version.diskFileName) 218 | $(if( $diskProperties ) { $diskProperties.Length }) 219 | $(if( $diskProperties ) { Get-Date -Date $diskProperties.LastWriteTime.ToUniversalTime() -Format s }) 220 | 221 | "@ 222 | }) 223 | } 224 | } 225 | else ## no SQL so go off files found only 226 | { 227 | if( Test-Path -Path $diskPath -ErrorAction SilentlyContinue ) 228 | { 229 | [string]$extension = $null 230 | [System.Collections.Generic.List[string]]$disks = $null 231 | if( $diskPath -match '\.([\d])+.vhdx?$' -or $diskPath -match '\.([\d]+).avhdx?$' ) 232 | { 233 | $startingVersion = $Matches[ 1 ] 234 | } 235 | else ## no number in file name so version zero 236 | { 237 | $startingVersion = 0 238 | $disks += (Split-Path -Path $diskPath -Leaf) 239 | } 240 | 241 | [int]$highestVesion = 0 242 | [string]$baseDiskName = $diskName -replace "\.$startingVersion$" 243 | $disks += @( Get-ChildItem -Path $diskFolder -Name "*$baseDiskName.*" | ForEach-Object ` 244 | { 245 | $disk = $_ 246 | $extension = [IO.Path]::GetExtension( $disk ) 247 | if( $extension -eq '.vhdx' -or $extension -eq '.avhdx' -or $extension -eq '.vhd' -or $extension -eq '.avhd') 248 | { 249 | [int]$thisVersion = -1 250 | if( $disk -match "$baseDiskName\.([\d])+$([regex]::Escape( $extension))`$" -and ($thisVersion = ($matches[1] -as [int])) -ge $startingVersion ) 251 | { 252 | if( $thisVersion -gt $highestVesion ) 253 | { 254 | $highestVesion = $thisVersion 255 | } 256 | $disk 257 | } 258 | } 259 | }) 260 | if( $disks -and $disks.Count ) 261 | { 262 | Write-Verbose -Message "Highest version is $highestVesion" 263 | 264 | [string[]]$versions = @( ForEach( $disk in $disks ) 265 | { 266 | [string]$diskFile = Join-Path -Path $diskFolder -ChildPath $disk 267 | $diskProperties = Get-ItemProperty -Path $diskFile -ErrorAction SilentlyContinue 268 | 269 | if( $diskProperties -and $diskProperties.CreationTime -gt $diskProperties.LastWriteTime ) 270 | { 271 | Set-ItemProperty -Path $diskFile -Name CreationTime -Value ( $diskProperties.LastWriteTime.AddSeconds( -$secondsBeforeLastWrite )) 272 | } 273 | 274 | $extension = [IO.Path]::GetExtension( $disk ) 275 | [int]$version = 0 276 | if( $disk -match "\.([\d])+$([regex]::Escape( $extension))`$" ) 277 | { 278 | $version = $Matches[1] 279 | } 280 | [int]$type = 1 ## Manual 281 | if( $extension -match '\.vhd' ) 282 | { 283 | if( $version -eq 0 ) 284 | { 285 | $type = 0 ## Base 286 | } 287 | else 288 | { 289 | $type = 4 ## MergeBase 290 | } 291 | } 292 | 293 | ## can only have one maintenance version so make all production except the last one unless -production specified 294 | [int]$access = 0 295 | if( $version -eq $highestVesion -and ! $production ) 296 | { 297 | $access = 1 298 | } 299 | @" 300 | 301 | $version 302 | 303 | $type 304 | $access 305 | $(if( $diskProperties ) { Get-Date -Date $diskProperties.CreationTimeUtc -Format s }) 306 | 307 | 0 308 | $disk 309 | $(if( $diskProperties ) { $diskProperties.Length }) 310 | $(if( $diskProperties ) { Get-Date -Date $diskProperties.LastWriteTimeUtc -Format s }) 311 | 312 | "@ 313 | }) 314 | } 315 | else 316 | { 317 | Throw "No disks found" 318 | } 319 | } 320 | else 321 | { 322 | Throw "Unable to find disk `"$diskPath`"" 323 | } 324 | } 325 | 326 | if( $versions -and $versions.Count ) 327 | { 328 | &{ 329 | '' 330 | '' 331 | "$startingVersion" 332 | $versions 333 | '' 334 | } | Out-File -Filepath $XMLManifest -Encoding utf8 335 | } 336 | -------------------------------------------------------------------------------- /Remove Ghost NICs.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | 5 | Remove any ghost NICs as in those which show in device manager when view->show hidden devices is enabled 6 | 7 | .DESCRIPTION 8 | 9 | Ghost NICs can cause Citrix Provisioning Services (PVS) target devices to fail to boot 10 | 11 | .PARAMETER nicRegex 12 | 13 | A regular expression to match the name of ghost NICs to remove. If not specified, all ghost NICs will be removed. It is recommended that this parameter is used to specify the expected ghost NICs 14 | 15 | .EXAMPLE 16 | 17 | . '.\Remove Ghost NICs.ps1' -nicRegex Intel.*Gigabit 18 | 19 | Remove all ghost NICS containing the given regular expression in their name after prompting for confirmation to perform the action 20 | 21 | .EXAMPLE 22 | 23 | . '.\Remove Ghost NICs.ps1' -nicRegex Intel.*Gigabit -Confirm:$false 24 | 25 | Remove all ghost NICS containing the given regular expression in their name without prompting for confirmation to perform the action 26 | 27 | .NOTES 28 | 29 | Removal code adapted from http://www.dwarfsoft.com/blog/2012/12/09/network-interface-removal-and-renaming/ 30 | 31 | NICs may be ghosts because the driver is installed but currently there is no device for it which may change if a NIC of that type is added so do not remove ghost NICs unless you are certain 32 | that a NIC of that type will never be used on this machine. 33 | 34 | Another cause of PVS target device boot failure can be that the PCI slot number for the NIC has changed - https://github.com/guyrleech/VMware/blob/master/Change%20NIC%20PCI%20slot%20number.ps1 35 | 36 | Modification History: 37 | 38 | 2021/08/23 @guyrleech Initial release 39 | #> 40 | 41 | <# 42 | Copyright © 2021 Guy Leech 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, 45 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 50 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 51 | #> 52 | 53 | [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')] 54 | 55 | Param 56 | ( 57 | [String]$nicRegex 58 | ) 59 | 60 | function RemoveDevice([string]$DeviceID) 61 | { 62 | $RemoveDeviceSource = @' 63 | 64 | using System; 65 | using System.Runtime.InteropServices; 66 | using System.Text; 67 | namespace Microsoft.Windows.Diagnosis 68 | { 69 | public sealed class DeviceManagement_Remove 70 | { 71 | public const UInt32 ERROR_CLASS_MISMATCH = 0xE0000203; 72 | 73 | [DllImport("setupapi.dll", SetLastError = true, EntryPoint = "SetupDiOpenDeviceInfo", CharSet = CharSet.Auto)] 74 | static extern UInt32 SetupDiOpenDeviceInfo(IntPtr DeviceInfoSet, [MarshalAs(UnmanagedType.LPWStr)]string DeviceID, IntPtr Parent, UInt32 Flags, ref SP_DEVINFO_DATA DeviceInfoData); 75 | 76 | [DllImport("setupapi.dll", SetLastError = true, EntryPoint = "SetupDiCreateDeviceInfoList", CharSet = CharSet.Unicode)] 77 | static extern IntPtr SetupDiCreateDeviceInfoList(IntPtr ClassGuid, IntPtr Parent); 78 | 79 | [DllImport("setupapi.dll", SetLastError = true, EntryPoint = "SetupDiDestroyDeviceInfoList", CharSet = CharSet.Unicode)] 80 | static extern UInt32 SetupDiDestroyDeviceInfoList(IntPtr DevInfo); 81 | 82 | [DllImport("setupapi.dll", SetLastError = true, EntryPoint = "SetupDiRemoveDevice", CharSet = CharSet.Auto)] 83 | public static extern int SetupDiRemoveDevice(IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData); 84 | 85 | [StructLayout(LayoutKind.Sequential)] 86 | public struct SP_DEVINFO_DATA 87 | { 88 | public UInt32 Size; 89 | public Guid ClassGuid; 90 | public UInt32 DevInst; 91 | public IntPtr Reserved; 92 | } 93 | 94 | private DeviceManagement_Remove() 95 | { 96 | } 97 | 98 | public static UInt32 GetDeviceInformation(string DeviceID, ref IntPtr DevInfoSet, ref SP_DEVINFO_DATA DevInfo) 99 | { 100 | DevInfoSet = SetupDiCreateDeviceInfoList(IntPtr.Zero, IntPtr.Zero); 101 | if (DevInfoSet == IntPtr.Zero) 102 | { 103 | return (UInt32)Marshal.GetLastWin32Error(); 104 | } 105 | 106 | DevInfo.Size = (UInt32)Marshal.SizeOf(DevInfo); 107 | 108 | if(0 == SetupDiOpenDeviceInfo(DevInfoSet, DeviceID, IntPtr.Zero, 0, ref DevInfo)) 109 | { 110 | SetupDiDestroyDeviceInfoList(DevInfoSet); 111 | return ERROR_CLASS_MISMATCH; 112 | } 113 | return 0; 114 | } 115 | 116 | public static void ReleaseDeviceInfoSet(IntPtr DevInfoSet) 117 | { 118 | SetupDiDestroyDeviceInfoList(DevInfoSet); 119 | } 120 | 121 | public static UInt32 RemoveDevice(string DeviceID) 122 | { 123 | UInt32 ResultCode = 0; 124 | IntPtr DevInfoSet = IntPtr.Zero; 125 | SP_DEVINFO_DATA DevInfo = new SP_DEVINFO_DATA(); 126 | 127 | ResultCode = GetDeviceInformation(DeviceID, ref DevInfoSet, ref DevInfo); 128 | 129 | if (0 == ResultCode) 130 | { 131 | if (1 != SetupDiRemoveDevice(DevInfoSet, ref DevInfo)) 132 | { 133 | ResultCode = (UInt32)Marshal.GetLastWin32Error(); 134 | } 135 | ReleaseDeviceInfoSet(DevInfoSet); 136 | } 137 | 138 | return ResultCode; 139 | } 140 | } 141 | } 142 | '@ 143 | Add-Type -TypeDefinition $RemoveDeviceSource 144 | 145 | $DeviceManager = [Microsoft.Windows.Diagnosis.DeviceManagement_Remove] 146 | $ErrorCode = $DeviceManager::RemoveDevice($DeviceID) 147 | return $ErrorCode 148 | } 149 | 150 | [array]$ghostNICs = @( Get-WmiObject -Class win32_networkadapter -Filter "ServiceName IS NULL" -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $nicRegex } ) 151 | 152 | if( ! $ghostNICs -or ! $ghostNICs.Count ) 153 | { 154 | [string]$warning = "No ghost NICs found" 155 | if( $PSBoundParameters[ 'nicRegex' ] ) 156 | { 157 | $warning += " matching $nicRegex" 158 | } 159 | Write-Warning -Message $warning 160 | exit 0 161 | } 162 | 163 | Write-Verbose -Message "Found $($ghostNICs.Count) ghost NICs" 164 | 165 | if( ! ( $netclass = Get-ChildItem -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Class\*" | Get-ItemProperty -Name Class | Where-Object Class -ceq 'Net' ) ) 166 | { 167 | Throw "Unable to find Net class in registry" 168 | } 169 | 170 | ForEach( $ghostNIC in $ghostNICs ) 171 | { 172 | [string]$index = "$($ghostNIC.Index)".PadLeft( 4 , '0' ) 173 | 174 | if( ! ( $nic = Get-ItemProperty -Path (Join-Path -Path $netclass.PSPath -ChildPath $index) ) ) 175 | { 176 | Write-Warning -Message "Unable to find `"$($ghostNIC.Name)`" index $index in $($class.PSPath)" 177 | } 178 | elseif( $PSCmdlet.ShouldProcess( $ghostNIC.Name , 'Remove' ) ) 179 | { 180 | $guid = $nic.NetCfgInstanceId 181 | $devid = $nic.DeviceInstanceId 182 | Write-Verbose -Message "Removing $($ghostNIC.Name) : GUID $guid device id $devid" 183 | 184 | [int]$result = RemoveDevice( $devid ) 185 | if( $result -ne 0 ) 186 | { 187 | Write-Error -Message "Error $result returned from removing device" 188 | } 189 | else 190 | { 191 | if( ( $netcfg = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Network\$($netclass.PSChildName)\$guid\Connection" -ErrorAction SilentlyContinue ) -and $netcfg.PnpInstanceID -eq $devid) 192 | { 193 | Remove-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Network\$($netclass.PSChildName)\$guid" -Recurse 194 | } 195 | else 196 | { 197 | Write-Warning -Message "No corresponding Connection key found in registry so network registry key not removed for guid $guid" 198 | } 199 | } 200 | } 201 | } 202 | 203 | # SIG # Begin signature block 204 | # MIINRQYJKoZIhvcNAQcCoIINNjCCDTICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 205 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 206 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUX5r/s/zrgGS8460HGVBdhCIk 207 | # 5omgggqHMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0B 208 | # AQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD 209 | # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk 210 | # IElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQsw 211 | # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu 212 | # ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg 213 | # Q29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 214 | # +NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ 215 | # 1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0 216 | # sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6s 217 | # cKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4Tz 218 | # rGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg 219 | # 0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIB 220 | # ADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUH 221 | # AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI 222 | # KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz 223 | # c3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0 224 | # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaG 225 | # NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD 226 | # QS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0 227 | # dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYE 228 | # FFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en 229 | # IZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06 230 | # GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5j 231 | # DhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgC 232 | # PC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIy 233 | # sjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4Gb 234 | # T8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIFTzCC 235 | # BDegAwIBAgIQBP3jqtvdtaueQfTZ1SF1TjANBgkqhkiG9w0BAQsFADByMQswCQYD 236 | # VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln 237 | # aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29k 238 | # ZSBTaWduaW5nIENBMB4XDTIwMDcyMDAwMDAwMFoXDTIzMDcyNTEyMDAwMFowgYsx 239 | # CzAJBgNVBAYTAkdCMRIwEAYDVQQHEwlXYWtlZmllbGQxJjAkBgNVBAoTHVNlY3Vy 240 | # ZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMRgwFgYDVQQLEw9TY3JpcHRpbmdIZWF2 241 | # ZW4xJjAkBgNVBAMTHVNlY3VyZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMIIBIjAN 242 | # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr20nXdaAALva07XZykpRlijxfIPk 243 | # TUQFAxQgXTW2G5Jc1YQfIYjIePC6oaD+3Zc2WN2Jrsc7bj5Qe5Nj4QHHHf3jopLy 244 | # g8jXl7Emt1mlyzUrtygoQ1XpBBXnv70dvZibro6dXmK8/M37w5pEAj/69+AYM7IO 245 | # Fz2CrTIrQjvwjELSOkZ2o+z+iqfax9Z1Tv82+yg9iDHnUxZWhaiEXk9BFRv9WYsz 246 | # qTXQTEhv8fmUI2aZX48so4mJhNGu7Vp1TGeCik1G959Qk7sFh3yvRugjY0IIXBXu 247 | # A+LRT00yjkgMe8XoDdaBoIn5y3ZrQ7bCVDjoTrcn/SqfHvhEEMj1a1f0zQIDAQAB 248 | # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O 249 | # BBYEFE16ovlqIk5uX2JQy6og0OCPrsnJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE 250 | # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp 251 | # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny 252 | # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw 253 | # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl 254 | # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw 255 | # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v 256 | # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp 257 | # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAU9zO 258 | # 9UpTkPL8DNrcbIaf1w736CgWB5KRQsmp1mhXbGECUCCpOCzlYFCSeiwH9MT0je3W 259 | # aYxWqIpUMvAI8ndFPVDp5RF+IJNifs+YuLBcSv1tilNY+kfa2OS20nFrbFfl9QbR 260 | # 4oacz8sBhhOXrYeUOU4sTHSPQjd3lpyhhZGNd3COvc2csk55JG/h2hR2fK+m4p7z 261 | # sszK+vfqEX9Ab/7gYMgSo65hhFMSWcvtNO325mAxHJYJ1k9XEUTmq828ZmfEeyMq 262 | # K9FlN5ykYJMWp/vK8w4c6WXbYCBXWL43jnPyKT4tpiOjWOI6g18JMdUxCG41Hawp 263 | # hH44QHzE1NPeC+1UjTGCAigwggIkAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD 264 | # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv 265 | # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC 266 | # EAT946rb3bWrnkH02dUhdU4wCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI 267 | # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB 268 | # CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFGW/X1vxLa/aHVA/28yq 269 | # 3wVMN0CKMA0GCSqGSIb3DQEBAQUABIIBAIu2dH+Aw/VogyUHbn3fZpWXgzQup4u/ 270 | # +dqYvnjAfkRYYHtpKzeCPxaAH3K6IK3Uo45JwbaWjDq0GHXW7WMEp0JfD2ETnA+f 271 | # 3tm3mNncYMgR8UJu0YARcMO0mmqR70O+lq7uQBB7/LNcZXakRXFuU7M62/+V6fov 272 | # PLJTQWwwx1WhB5vtOxEXXdAsJBMgUBg4swp7gcBXdd+OWd1bxckelzJxhwH+rPw/ 273 | # J9mHpGuyZbPfp4wOvL4/u4VmxTImn+sYKsxKPTc6o6PRg3pRbWKpVjqPAXo0Vm5/ 274 | # 2KmfGSvZu6b74Iuu79wJ5xMCef5YQb729LPsAZOA4h0J8FD2WSl4gvY= 275 | # SIG # End signature block 276 | -------------------------------------------------------------------------------- /Set Citrix DDC from OU.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get the OU for the computer running the script and set the ListOfDDCs value in the registry for Citrix VDA based on that - have as GPO startup script so single script can work across environments - on-prem and cloud 4 | 5 | .DESCRIPTION 6 | Modify the $OUtoDDC hashtable definition in this script to match your OU and DDC requirements where multiple DDCs must be delimited with a space 7 | 8 | .PARAMETER overrideOU 9 | Use for special cases to specify a value which overrides the OU based determination 10 | 11 | .PARAMETER fallbackValue 12 | If no value is specified, use this value instead otherwise no change will be made 13 | 14 | .PARAMETER deleteValue 15 | Delete the value 16 | 17 | .PARAMETER stopAndDisable 18 | Stop the VDA service and set it to disabled 19 | 20 | .PARAMETER noEnable 21 | Do not enable the service if it is disabled 22 | 23 | .PARAMETER forceStart 24 | If service is not already running, start it anyway. 25 | 26 | .PARAMETER serviceName 27 | The service name for the VDA - set to null or empty and the service will not be restarted after the registry is changed 28 | 29 | .EXAMPLE 30 | & '.\Set Citrix DDC from OU.ps1' 31 | 32 | Set the value of the ListOfDDCs based on the OU of the computer and if the OU is not in the lookup dictionary the value will not be changed and the VDA not restarted otherwise it will be restarted after the change. 33 | If the computer is in the OU "OU=Cloud,OU=MCS,OU=RDS,OU=Computers,OU=Wakefield,OU=Sites,DC=guyrleech,DC=local", the script will look in the $OUtoDDC hashtable for the key "Cloud" 34 | 35 | .EXAMPLE 36 | & '.\Set Citrix DDC from OU.ps1' -overrideOU "Test" 37 | 38 | Set the value of the ListOfDDCs for the OU "Test" in the lookup dictionary - if the OU is not in the dictionary, the value will not be changed and the VDA not restarted otherwise it will be restarted after the change 39 | 40 | .EXAMPLE 41 | & '.\Set Citrix DDC from OU.ps1' -fallback "GRL-XADDCDev01.guyrleech.local" 42 | 43 | Set the value of the ListOfDDCs based on the OU of the computer and if the OU is not in the lookup dictionary the value will be set to "GRL-XADDCDev01.guyrleech.local" and the VDA restarted 44 | 45 | .EXAMPLE 46 | & '.\Set Citrix DDC from OU.ps1' -stopAndDisable 47 | 48 | Stop and disable the VDA. Do not channge the value of ListOfDDCs 49 | 50 | .EXAMPLE 51 | & '.\Set Citrix DDC from OU.ps1' -forceStart 52 | 53 | Set the value of the ListOfDDCs based on the OU of the computer and if the OU is not in the lookup dictionary the value will not be changed. The VDA will be (re)started regardless 54 | 55 | .NOTES 56 | 57 | Modification History: 58 | 59 | @guyrleech 2021/12/08 Initial release 60 | #> 61 | 62 | <# 63 | Copyright © 2021 Guy Leech 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, 66 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 67 | 68 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 71 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 72 | #> 73 | 74 | [CmdletBinding()] 75 | 76 | Param 77 | ( 78 | [string]$overrideOU , 79 | [string]$fallbackValue , 80 | [switch]$deleteValue , 81 | [switch]$stopAndDisable , 82 | [switch]$noEnable , 83 | [int]$registryOverride , 84 | [switch]$forceStart , 85 | [string]$serviceName = 'BrokerAgent' 86 | ) 87 | 88 | ## This could be in a csv in the netlogon share instead 89 | 90 | ## Key is the bottom most OU for the computer 91 | ## eg. "Cloud" for "OU=Cloud,OU=MCS,OU=RDS,OU=Computers,OU=Wakefield,OU=Sites,DC=guyrleech,DC=local" 92 | 93 | [hashtable]$OUtoDDC = @{ 94 | 'Cloud' = 'GRL-CTXCLDCON1.guyrleech.local GLCTXCLDCON3.guyrleech.local' 95 | 'CR On-Prem' = 'GRL-XADDC02.guyrleech.local' 96 | 'PVS' = 'GRL-XADDC02.guyrleech.local' 97 | 'LTSR On-Prem' = 'GRL-XADDC01.guyrleech.local' 98 | 'Test' = 'GRL-XADDCTest01.guyrleech.local' 99 | } 100 | 101 | ## DO NOT MODIFY BELOW HERE 102 | 103 | ####################################################################################################################################### 104 | 105 | $VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'Continue' }) 106 | 107 | [string]$baseKey = 'HKLM:\SOFTWARE\Citrix\VirtualDesktopAgent' 108 | [string]$valueName = 'ListOfDDCs' 109 | [string]$scriptname = & { $MyInvocation.ScriptName } 110 | 111 | try 112 | { 113 | Start-Transcript -Path (Join-Path -Path $env:temp -ChildPath ( (Split-Path -Path $scriptname -Leaf) -replace '\.[^\.]+' , '.log')) 114 | 115 | Write-Verbose -Message "$(Get-Date -Format G): script started on $env:COMPUTERNAME" 116 | 117 | if( $currentValue = Get-ItemProperty -Path $baseKey -Name $valueName | Select-Object -ExpandProperty $valueName ) 118 | { 119 | Write-Verbose -Message "Initial value of $baseKey\$valueName is `"$currentValue`"" 120 | } 121 | 122 | [string]$thisOU = $overrideOU 123 | [string]$fullOU = $null 124 | 125 | if( $deleteValue ) 126 | { 127 | Write-Verbose -Message "Deleting value" 128 | Remove-ItemProperty -Path $baseKey -Name $valueName -Force 129 | } 130 | elseif( $stopAndDisable ) 131 | { 132 | if( $service = Get-Service -Name $serviceName ) 133 | { 134 | Write-Verbose -Message "Service `"$($service.Name)`" is currently $($service.Status) and startup $($service.StartType)" 135 | if(-Not ( $service | Set-Service -StartupType Disabled -PassThru )) 136 | { 137 | Write-Warning -Message "Failed to set service `"$($service.Name)`" to disabled" 138 | } 139 | if( $service.Status -ne 'Stopped' ) 140 | { 141 | Write-Verbose -Message "$(Get-Date -Format G): stopping service $serviceName" 142 | $service | Stop-Service 143 | } 144 | } 145 | else 146 | { 147 | Throw "Unable to get service `"$serviceName`"" 148 | } 149 | } 150 | else 151 | { 152 | if( -Not $PSBoundParameters[ 'overrideOU' ] ) 153 | { 154 | ## courtesy of Shay Levy @shaylevy 155 | $filter = "(&(objectCategory=computer)(objectClass=computer)(cn=$env:COMPUTERNAME))" 156 | if( $thisComputer = ([adsisearcher]$filter).FindOne().Properties ) 157 | { 158 | $fullOU = $thisComputer.distinguishedname 159 | if( [string]::IsNullOrEmpty( ( $thisOU = $fullOU -split ',OU='|Select-Object -Skip 1 -First 1 ) ) ) 160 | { 161 | Write-Warning "Unable to isolate OU from $fullOU" 162 | } 163 | } 164 | else 165 | { 166 | Write-Warning "Failed to find $env:COMPUTERNAME in AD" 167 | } 168 | } 169 | else 170 | { 171 | Write-Verbose -Message "Override OU specified" 172 | } 173 | 174 | Write-Verbose -Message "Base OU is `"$thisOU`" from `"$fullOU`"" 175 | 176 | [string]$listOfDDCsValue = $OUtoDDC[ $thisOU ] 177 | [string]$newValue = $null 178 | 179 | if( [string]::IsNullOrEmpty( $listOfDDCsValue ) ) 180 | { 181 | Write-Warning -Message "No mapping for OU `"$thisOU`"" 182 | if( -Not [string]::IsNullOrEmpty( $fallbackValue ) ) 183 | { 184 | $newValue = $fallbackValue 185 | Write-Verbose -Message "Using fallback value of `"$newValue`"" 186 | } 187 | } 188 | else 189 | { 190 | $newValue = $listOfDDCsValue 191 | Write-Verbose -Message "Value will be set to `"$newValue`"" 192 | } 193 | 194 | if( $newValue ) 195 | { 196 | if( $newValue -eq $currentValue ) 197 | { 198 | Write-Verbose -Message "Values has not changed from `"$currentValue`" so not changing or restarting" 199 | $newValue = $null 200 | } 201 | else 202 | { 203 | ## if key doesn't exist, suggests VDA not installed so bigger problems! 204 | Set-ItemProperty -Path $baseKey -Name $valueName -Value $newValue -Force 205 | 206 | $currentValue = Get-ItemProperty -Path $baseKey -Name $valueName | Select-Object -ExpandProperty $valueName 207 | 208 | Write-Verbose -Message "Value now of $baseKey\$valueName is `"$currentValue`"" 209 | } 210 | } 211 | else 212 | { 213 | Write-Warning -Message "Value not set by this script" 214 | } 215 | 216 | if( $PSBoundParameters[ 'registryOverride' ] ) 217 | { 218 | [string]$RegistryOverridesAutoUpdateValueName = 'RegistryOverridesAutoUpdate' 219 | } 220 | 221 | if( -Not [string]::IsNullOrEmpty( $serviceName ) -and ( $newValue -or $deleteValue -or $forceStart ) ) 222 | { 223 | if( $service = Get-Service -Name $serviceName ) 224 | { 225 | Write-Verbose -Message "Service `"$($service.Name)`" is currently $($service.Status) and startup $($service.StartType)" 226 | if( -Not $noEnable -and $service.StartType -eq 'Disabled' ) 227 | { 228 | if(-Not ( $service | Set-Service -StartupType Automatic -PassThru )) 229 | { 230 | Write-Warning -Message "Failed to set service `"$($service.Name)`" to automatic" 231 | } 232 | else 233 | { 234 | Write-Verbose -Message "Service `"$($service.Name)`" startup changed to automatic" 235 | } 236 | } 237 | ## as we are probably doing this at boot, if service is not yet started, don't start it here because of service order & dependency 238 | if( $service.Status -ne 'Stopped' -or $forceStart ) 239 | { 240 | Write-Verbose -Message "$(Get-Date -Format G): restarting service $serviceName" 241 | if( -Not ( $service | Restart-Service -PassThru ) ) 242 | { 243 | Write-Warning -Message "Problem starting service `"$($service.Name)`"" 244 | } 245 | else 246 | { 247 | Write-Warning -Message "Service `"$($service.Name)`" restarted ok" 248 | } 249 | } 250 | } 251 | } 252 | else 253 | { 254 | Write-Verbose -Message "Not changing service state" 255 | } 256 | 257 | Write-Verbose -Message "$(Get-Date -Format G): script finished" 258 | } 259 | } 260 | catch 261 | { 262 | Throw $_ 263 | } 264 | finally 265 | { 266 | Stop-Transcript 267 | } 268 | 269 | # SIG # Begin signature block 270 | # MIIjcAYJKoZIhvcNAQcCoIIjYTCCI10CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 271 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 272 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU7dF76L8+rog8ohHjs//ZIfob 273 | # xC+ggh2OMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0B 274 | # AQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD 275 | # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk 276 | # IElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQsw 277 | # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu 278 | # ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg 279 | # Q29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 280 | # +NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ 281 | # 1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0 282 | # sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6s 283 | # cKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4Tz 284 | # rGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg 285 | # 0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIB 286 | # ADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUH 287 | # AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI 288 | # KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz 289 | # c3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0 290 | # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaG 291 | # NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD 292 | # QS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0 293 | # dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYE 294 | # FFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en 295 | # IZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06 296 | # GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5j 297 | # DhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgC 298 | # PC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIy 299 | # sjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4Gb 300 | # T8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIFTzCC 301 | # BDegAwIBAgIQBP3jqtvdtaueQfTZ1SF1TjANBgkqhkiG9w0BAQsFADByMQswCQYD 302 | # VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln 303 | # aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29k 304 | # ZSBTaWduaW5nIENBMB4XDTIwMDcyMDAwMDAwMFoXDTIzMDcyNTEyMDAwMFowgYsx 305 | # CzAJBgNVBAYTAkdCMRIwEAYDVQQHEwlXYWtlZmllbGQxJjAkBgNVBAoTHVNlY3Vy 306 | # ZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMRgwFgYDVQQLEw9TY3JpcHRpbmdIZWF2 307 | # ZW4xJjAkBgNVBAMTHVNlY3VyZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMIIBIjAN 308 | # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr20nXdaAALva07XZykpRlijxfIPk 309 | # TUQFAxQgXTW2G5Jc1YQfIYjIePC6oaD+3Zc2WN2Jrsc7bj5Qe5Nj4QHHHf3jopLy 310 | # g8jXl7Emt1mlyzUrtygoQ1XpBBXnv70dvZibro6dXmK8/M37w5pEAj/69+AYM7IO 311 | # Fz2CrTIrQjvwjELSOkZ2o+z+iqfax9Z1Tv82+yg9iDHnUxZWhaiEXk9BFRv9WYsz 312 | # qTXQTEhv8fmUI2aZX48so4mJhNGu7Vp1TGeCik1G959Qk7sFh3yvRugjY0IIXBXu 313 | # A+LRT00yjkgMe8XoDdaBoIn5y3ZrQ7bCVDjoTrcn/SqfHvhEEMj1a1f0zQIDAQAB 314 | # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O 315 | # BBYEFE16ovlqIk5uX2JQy6og0OCPrsnJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE 316 | # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp 317 | # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny 318 | # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw 319 | # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl 320 | # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw 321 | # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v 322 | # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp 323 | # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAU9zO 324 | # 9UpTkPL8DNrcbIaf1w736CgWB5KRQsmp1mhXbGECUCCpOCzlYFCSeiwH9MT0je3W 325 | # aYxWqIpUMvAI8ndFPVDp5RF+IJNifs+YuLBcSv1tilNY+kfa2OS20nFrbFfl9QbR 326 | # 4oacz8sBhhOXrYeUOU4sTHSPQjd3lpyhhZGNd3COvc2csk55JG/h2hR2fK+m4p7z 327 | # sszK+vfqEX9Ab/7gYMgSo65hhFMSWcvtNO325mAxHJYJ1k9XEUTmq828ZmfEeyMq 328 | # K9FlN5ykYJMWp/vK8w4c6WXbYCBXWL43jnPyKT4tpiOjWOI6g18JMdUxCG41Hawp 329 | # hH44QHzE1NPeC+1UjTCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJ 330 | # KoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu 331 | # YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg 332 | # QXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1 333 | # OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE 334 | # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBS 335 | # b290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+Rd 336 | # SjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20d 337 | # q7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7f 338 | # gvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRA 339 | # X7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raR 340 | # mECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzU 341 | # vK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2 342 | # mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkr 343 | # fsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaA 344 | # sPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxf 345 | # jT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEe 346 | # xcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQF 347 | # MAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaA 348 | # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcB 349 | # AQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggr 350 | # BgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz 351 | # dXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5k 352 | # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQK 353 | # MAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3 354 | # v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy 355 | # 3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cn 356 | # RNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3 357 | # WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2 358 | # zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCC 359 | # Bq4wggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjEL 360 | # MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 361 | # LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0 362 | # MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMx 363 | # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVz 364 | # dGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZI 365 | # hvcNAQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD 366 | # 0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39 367 | # Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decf 368 | # BmWNlCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RU 369 | # CyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+x 370 | # tVhNef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OA 371 | # e3VuJyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRA 372 | # KKtzQ87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++b 373 | # Pf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+ 374 | # OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2Tj 375 | # Y+Cm4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZ 376 | # DNIztM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW 377 | # BBS6FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/ 378 | # 57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYI 379 | # KwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j 380 | # b20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp 381 | # Q2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9j 382 | # cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1Ud 383 | # IAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEA 384 | # fVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnB 385 | # zx0H6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXO 386 | # lWk/R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBw 387 | # CnzvqLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q 388 | # 6/aesXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJ 389 | # uXdmkfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEh 390 | # QNC3EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo4 391 | # 6Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3 392 | # v5gA3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHz 393 | # V9m8BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZV 394 | # VCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAM 395 | # TWlyS5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcw 396 | # FQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3Rl 397 | # ZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAw 398 | # MDAwWhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGln 399 | # aUNlcnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIw 400 | # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0 401 | # MxomrNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z 402 | # 7GGK6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG 403 | # 8b7gL307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRN 404 | # lYRo44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVH 405 | # hoV5PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCW 406 | # fk3h3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/Nurl 407 | # fDHn88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6 408 | # Vs/g9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXl 409 | # LimQprdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5 410 | # zGcTB5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd 411 | # 6kVzHIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0T 412 | # AQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeB 413 | # DAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+e 414 | # yG8wHQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BN 415 | # oEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT 416 | # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGA 417 | # MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUH 418 | # MAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH 419 | # NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQAD 420 | # ggIBAFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiI 421 | # Y1UJRjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ 422 | # /Bu1nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/ 423 | # 350Qp+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iy 424 | # JpU4GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFk 425 | # nADC6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7 426 | # mxNfarXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBF 427 | # YDOA4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u9 428 | # 2ByaUcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie 429 | # 1FqYyJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j86 430 | # 2uXl9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFTDCCBUgCAQEw 431 | # gYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE 432 | # CxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1 433 | # cmVkIElEIENvZGUgU2lnbmluZyBDQQIQBP3jqtvdtaueQfTZ1SF1TjAJBgUrDgMC 434 | # GgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYK 435 | # KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG 436 | # 9w0BCQQxFgQUzD4OoAu2terKEJ0xmUkVRd2Uh5gwDQYJKoZIhvcNAQEBBQAEggEA 437 | # TXxxTuKNY0E6cf2Bk0M+KRy5tEYO1YZQd5911W9BQq6gXmVIDauKun+u4YP6uQd5 438 | # k5drQR5caH2BhS4gDwCZUQptWzJPueYRUJqOWFcQC8o51794wzJuDNGuwMEYy87c 439 | # HZzYl8CZsKmaEgjAHED36OY8gQceH7Q33c4IXca0vCI/d5QZpPWb498H629zQhO3 440 | # zMm66X/H9Vbo2NtgptKpcWgiZqWA7LtNIQfe85Z2ssj2SMJnvxoxqts2D/jEVtZq 441 | # FkfOPEvzikUBPXmH5m+1UANxpBdobTZgJl7VIr4P3SiUJjoav8ewQfU9RiNNJ4/M 442 | # YUimgV0LzYp4+CrP3sTMZKGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3 443 | # MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UE 444 | # AxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBp 445 | # bmcgQ0ECEAxNaXJLlPo8Kko9KQeAPVowDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG 446 | # 9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMjEwMDUxMDE2MDha 447 | # MC8GCSqGSIb3DQEJBDEiBCAWatJjRii7IpbADd6prZs9bALGMTl+ynkufC02liAr 448 | # zDANBgkqhkiG9w0BAQEFAASCAgBEGU/jHafMIEwkLxa8TwKo1fDgad9Hs9PSeSuU 449 | # gF32BybXenkHM8Ha3QlG4uGwhFqZW9ZMrMYtTwUeQaKEyunVYPP3nALU5g7DSb5Q 450 | # WkkHRjeNO/hplA38+1Y+K4eycqD0fLIs1lGSlCUFUDPpQ/6pALjl708Cf7pVAVv5 451 | # uc5TxXK9oZX364IkQ05YheK9Yr6Jk23wMZE3Rmcg8fadYg+la3QZrHKQqxjmOvlW 452 | # Q/X37BMGbb78VpfewQ5eYBSTFBf6Y8dxpGtxuknE7hgxDQ+WgLduSgCrsvGNFxo/ 453 | # Ot9U9hXK74VpR/z+hnm4Djizv1MRYAJE4m960bMYh1YGygj5NrKe2fz7351hLI0N 454 | # xs8XDRUI7b1KXyyi/375qpI5H0MEnQf6/lyCsIl3qtgySp3dt9ucyuo7w6Gg2xAb 455 | # ytj3BNYqQ4UkHRPUsQu+3Us3Tu0mK5LHxC1v5ygOSQriY2Zh2SEDjbFZJQs0LyhL 456 | # PYwiIpoIPqe1ZVM4eRfVj1b1SJ+h9GPtaihNDM6Zpyal+r5YYCvJ/o6xJ6NqboiX 457 | # aeitXHxZWbSpY0rQcXyi87X+GTpnvM5+xmyTkYgqQhEWkdeT5yLoESiI9bcGs5ip 458 | # SC5FxRxepCvaq9sOoUCpbLdAftPz1p5kyNIeHAUzdr6NTLoDhKMAKVQEzSR2RYV8 459 | # IF52xw== 460 | # SIG # End signature block 461 | -------------------------------------------------------------------------------- /Show PVS audit trail.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3.0 2 | <# 3 | Show or export PVS audit logs 4 | 5 | Guy Leech, 2018 6 | 7 | Modification history: 8 | 9 | 21/05/18 GL Initial release 10 | 11 | 21/05/18 GL Added check for auditing enabled and to enable it if disabled 12 | #> 13 | 14 | <# 15 | .SYNOPSIS 16 | 17 | Produce grid view or csv report of Citrix Provisioning Services audit logs 18 | 19 | .DESCRIPTION 20 | 21 | Logs can be exported using the Export-PvsAuditTrail cmdlet but this only exports in XML format so may not be as easy to read/filter as csv or grid view format. 22 | Auditing must be enabled for the farm which can be done in the Options tab for the farm properties in the PVS console. 23 | 24 | .PARAMETER pvsServers 25 | 26 | Comma separated list of PVS servers to contact. Do not specify multiple servers if they use the same SQL database 27 | 28 | .PARAMETER csv 29 | 30 | Path to a csv file that will have the results written to it. 31 | 32 | .PARAMETER gridview 33 | 34 | Show the results in an on screen grid view. 35 | 36 | .PARAMETER enableAuditing 37 | 38 | If auditing on the farm(s) connected to via the specified PVS server(s) is not enabled then enabel it 39 | 40 | .PARAMETER startDate 41 | 42 | The start date for audit events to be retrieved. If not specified then the Citrix cmdlet defaults to one week prior to the current date/time 43 | 44 | .PARAMETER endDate 45 | 46 | The end date for audit events to be retrieved. If not specified then the Citrix cmdlet defaults to the current date/time 47 | 48 | .NOTES 49 | 50 | If the PVS cmdlets are not available where the script is run from, e.g. the PVS console is not installed, then the script will attempt to load the required module remotely from the PVS server(s) specified. 51 | 52 | Details on the cmdlets used can be found here - https://docs.citrix.com/content/dam/docs/en-us/provisioning-services/7-15/PvsSnapInCommands.pdf 53 | 54 | #> 55 | 56 | [CmdletBinding()] 57 | 58 | Param 59 | ( 60 | [string[]]$pvsServers = @( 'localhost' ) , 61 | [string]$csv , 62 | [switch]$gridView , 63 | [switch]$enableAuditing , 64 | [string]$startDate , 65 | [string]$endDate , 66 | [string]$pvsModule = "$env:ProgramFiles\Citrix\Provisioning Services Console\Citrix.PVS.SnapIn.dll" 67 | ) 68 | 69 | [int]$ERROR_INVALID_PARAMETER = 87 70 | [int]$defaultDaysBack = 7 71 | 72 | [string[]]$audittypes = @( 73 | 'Many' , 74 | 'AuthGroup' , 75 | 'Collection' , 76 | 'Device' , 77 | 'Disk' , 78 | 'DiskLocator' , 79 | 'Farm' , 80 | 'FarmView' , 81 | 'Server' , 82 | 'Site' , 83 | 'SiteView' , 84 | 'Store' , 85 | 'System' , 86 | 'UserGroup' 87 | ) 88 | 89 | [hashtable]$auditActions = @{ 90 | 1 = 'AddAuthGroup' 91 | 2 = 'AddCollection' 92 | 3 = 'AddDevice' 93 | 4 = 'AddDiskLocator' 94 | 5 = 'AddFarmView' 95 | 6 = 'AddServer' 96 | 7 = 'AddSite' 97 | 8 = 'AddSiteView' 98 | 9 = 'AddStore' 99 | 10 = 'AddUserGroup' 100 | 11 = 'AddVirtualHostingPool' 101 | 12 = 'AddUpdateTask' 102 | 13 = 'AddDiskUpdateDevice' 103 | 1001 = 'DeleteAuthGroup' 104 | 1002 = 'DeleteCollection' 105 | 1003 = 'DeleteDevice' 106 | 1004 = 'DeleteDeviceDiskCacheFile' 107 | 1005 = 'DeleteDiskLocator' 108 | 1006 = 'DeleteFarmView' 109 | 1007 = 'DeleteServer' 110 | 1008 = 'DeleteServerStore' 111 | 1009 = 'DeleteSite' 112 | 1010 = 'DeleteSiteView' 113 | 1011 = 'DeleteStore' 114 | 1012 = 'DeleteUserGroup' 115 | 1013 = 'DeleteVirtualHostingPool' 116 | 1014 = 'DeleteUpdateTask' 117 | 1015 = 'DeleteDiskUpdateDevice' 118 | 1016 = 'DeleteDiskVersion' 119 | 2001 = 'RunAddDeviceToDomain' 120 | 2002 = 'RunApplyAutoUpdate' 121 | 2003 = 'RunApplyIncrementalUpdate' 122 | 2004 = 'RunArchiveAuditTrail' 123 | 2005 = 'RunAssignAuthGroup' 124 | 2006 = 'RunAssignDevice' 125 | 2007 = 'RunAssignDiskLocator' 126 | 2008 = 'RunAssignServer' 127 | 2009 = 'RunWithReturnBoot' 128 | 2010 = 'RunCopyPasteDevice' 129 | 2011 = 'RunCopyPasteDisk' 130 | 2012 = 'RunCopyPasteServer' 131 | 2013 = 'RunCreateDirectory' 132 | 2014 = 'RunCreateDiskCancel' 133 | 2015 = 'RunDisableCollection' 134 | 2016 = 'RunDisableDevice' 135 | 2017 = 'RunDisableDeviceDiskLocator' 136 | 2018 = 'RunDisableDiskLocator' 137 | 2019 = 'RunDisableUserGroup' 138 | 2020 = 'RunDisableUserGroupDiskLocator' 139 | 2021 = 'RunWithReturnDisplayMessage' 140 | 2022 = 'RunEnableCollection' 141 | 2023 = 'RunEnableDevice' 142 | 2024 = 'RunEnableDeviceDiskLocator' 143 | 2025 = 'RunEnableDiskLocator' 144 | 2026 = 'RunEnableUserGroup' 145 | 2027 = 'RunEnableUserGroupDiskLocator' 146 | 2028 = 'RunExportOemLicenses' 147 | 2029 = 'RunImportDatabase' 148 | 2030 = 'RunImportDevices' 149 | 2031 = 'RunImportOemLicenses' 150 | 2032 = 'RunMarkDown' 151 | 2033 = 'RunWithReturnReboot' 152 | 2034 = 'RunRemoveAuthGroup' 153 | 2035 = 'RunRemoveDevice' 154 | 2036 = 'RunRemoveDeviceFromDomain' 155 | 2037 = 'RunRemoveDirectory' 156 | 2038 = 'RunRemoveDiskLocator' 157 | 2039 = 'RunResetDeviceForDomain' 158 | 2040 = 'RunResetDatabaseConnection' 159 | 2041 = 'RunRestartStreamingService' 160 | 2042 = 'RunWithReturnShutdown' 161 | 2043 = 'RunStartStreamingService' 162 | 2044 = 'RunStopStreamingService' 163 | 2045 = 'RunUnlockAllDisk' 164 | 2046 = 'RunUnlockDisk' 165 | 2047 = 'RunServerStoreVolumeAccess' 166 | 2048 = 'RunServerStoreVolumeMode' 167 | 2049 = 'RunMergeDisk' 168 | 2050 = 'RunRevertDiskVersion' 169 | 2051 = 'RunPromoteDiskVersion' 170 | 2052 = 'RunCancelDiskMaintenance' 171 | 2053 = 'RunActivateDevice' 172 | 2054 = 'RunAddDiskVersion' 173 | 2055 = 'RunExportDisk' 174 | 2056 = 'RunAssignDisk' 175 | 2057 = 'RunRemoveDisk' 176 | 2058 = 'RunDiskUpdateStart' 177 | 2059 = 'RunDiskUpdateCancel' 178 | 2060 = 'RunSetOverrideVersion' 179 | 2061 = 'RunCancelTask' 180 | 2062 = 'RunClearTask' 181 | 2063 = 'RunForceInventory' 182 | 2064 = 'RunUpdateBDM' 183 | 2065 = 'RunStartDeviceDiskTempVersionMode' 184 | 2066 = 'RunStopDeviceDiskTempVersionMode' 185 | 3001 = 'RunWithReturnCreateDisk' 186 | 3002 = 'RunWithReturnCreateDiskStatus' 187 | 3003 = 'RunWithReturnMapDisk' 188 | 3004 = 'RunWithReturnRebalanceDevices' 189 | 3005 = 'RunWithReturnCreateMaintenanceVersion' 190 | 3006 = 'RunWithReturnImportDisk' 191 | 4001 = 'RunByteArrayInputImportDevices' 192 | 4002 = 'RunByteArrayInputImportOemLicenses' 193 | 5001 = 'RunByteArrayOutputArchiveAuditTrail' 194 | 5002 = 'RunByteArrayOutputExportOemLicenses' 195 | 6001 = 'SetAuthGroup' 196 | 6002 = 'SetCollection' 197 | 6003 = 'SetDevice' 198 | 6004 = 'SetDisk' 199 | 6005 = 'SetDiskLocator' 200 | 6006 = 'SetFarm' 201 | 6007 = 'SetFarmView' 202 | 6008 = 'SetServer' 203 | 6009 = 'SetServerBiosBootstrap' 204 | 6010 = 'SetServerBootstrap' 205 | 6011 = 'SetServerStore' 206 | 6012 = 'SetSite' 207 | 6013 = 'SetSiteView' 208 | 6014 = 'SetStore' 209 | 6015 = 'SetUserGroup' 210 | 6016 = 'SetVirtualHostingPool' 211 | 6017 = 'SetUpdateTask' 212 | 6018 = 'SetDiskUpdateDevice' 213 | 7001 = 'SetListDeviceBootstraps' 214 | 7002 = 'SetListDeviceBootstrapsDelete' 215 | 7003 = 'SetListDeviceBootstrapsAdd' 216 | 7004 = 'SetListDeviceCustomProperty' 217 | 7005 = 'SetListDeviceCustomPropertyDelete' 218 | 7006 = 'SetListDeviceCustomPropertyAdd' 219 | 7007 = 'SetListDeviceDiskPrinters' 220 | 7008 = 'SetListDeviceDiskPrintersDelete' 221 | 7009 = 'SetListDeviceDiskPrintersAdd' 222 | 7010 = 'SetListDevicePersonality' 223 | 7011 = 'SetListDevicePersonalityDelete' 224 | 7012 = 'SetListDevicePersonalityAdd' 225 | 7013 = 'SetListDiskLocatorCustomProperty' 226 | 7014 = 'SetListDiskLocatorCustomPropertyDelete' 227 | 7015 = 'SetListDiskLocatorCustomPropertyAdd' 228 | 7016 = 'SetListServerCustomProperty' 229 | 7017 = 'SetListServerCustomPropertyDelete' 230 | 7018 = 'SetListServerCustomPropertyAdd' 231 | 7019 = 'SetListUserGroupCustomProperty' 232 | 7020 = 'SetListUserGroupCustomPropertyDelete' 233 | 7021 = 'SetListUserGroupCustomPropertyAdd' 234 | } 235 | 236 | [hashtable]$auditParams = @{} 237 | 238 | if( [string]::IsNullOrEmpty( $csv ) -and ! $gridView ) 239 | { 240 | Write-Warning "Neither -csv nor -gridview specified so there will be no output produced" 241 | } 242 | 243 | if( ! [string]::IsNullOrEmpty( $startDate ) ) 244 | { 245 | $auditParams.Add( 'BeginDate' , [datetime]::Parse( $startDate ) ) 246 | } 247 | 248 | if( ! [string]::IsNullOrEmpty( $endDate ) ) 249 | { 250 | $auditParams.Add( 'EndDate' , [datetime]::Parse( $endDate ) ) 251 | if( ! [string]::IsNullOrEmpty( $startDate ) ) 252 | { 253 | if( $auditParams[ 'EndDate' ] -lt $auditParams[ 'BeginDate' ] ) 254 | { 255 | Write-Error "End date $endDate earlier than start date $startDate" 256 | Exit $ERROR_INVALID_PARAMETER 257 | } 258 | } 259 | elseif( $auditParams[ 'EndDate' ] -lt (Get-Date).AddDays( -$defaultDaysBack ) ) 260 | { 261 | Write-Error "End date $endDate earlier than default start date $((Get-Date).AddDays( -$defaultDaysBack ))" 262 | Exit $ERROR_INVALID_PARAMETER 263 | } 264 | } 265 | 266 | if( ! [string]::IsNullOrEmpty( $pvsModule ) ) 267 | { 268 | Import-Module $pvsModule -ErrorAction SilentlyContinue 269 | } 270 | 271 | $PVSSession = $null 272 | 273 | [hashtable]$sites = @{} 274 | [hashtable]$stores = @{} 275 | [hashtable]$collections = @{} 276 | [bool]$localPVScmdlets = ( Get-Command -Name Set-PvsConnection -ErrorAction SilentlyContinue ) -ne $null 277 | 278 | [array]$auditevents = @( ForEach( $pvsServer in $pvsServers ) 279 | { 280 | ## See if we have cmdlets we need and if not try and get them from the PVS server 281 | if( ! $localPVScmdlets ) 282 | { 283 | $PVSSession = New-PSSession -ComputerName $pvsServer 284 | if( $PVSSession ) 285 | { 286 | $null = Invoke-Command -Session $PVSSession -ScriptBlock { Import-Module $using:pvsModule } 287 | $null = Import-PSSession -Session $PVSSession -Module 'Citrix.PVS.SnapIn' 288 | } 289 | } 290 | else 291 | { 292 | $PVSSession = $null 293 | } 294 | 295 | Set-PvsConnection -Server $pvsServer 296 | 297 | if( ! $? ) 298 | { 299 | Write-Output "Cannot connect to PVS server $pvsServer - aborting" 300 | continue 301 | } 302 | 303 | ## Check if auditing is enabled 304 | $farm = Get-PvsFarm 305 | if( $farm -and ! $farm.AuditingEnabled ) 306 | { 307 | Write-Warning "Auditing is not enabled on farm `"$($farm.Name)`" via $pvsServer" 308 | if( $enableAuditing ) 309 | { 310 | Set-PvsFarm -FarmId $farm.FarmId -AuditingEnabled:$true 311 | } 312 | } 313 | 314 | ## Lookup table for site id to name 315 | Get-PvsSite | ForEach-Object ` 316 | { 317 | $sites.Add( $_.SiteId , $_.SiteName ) 318 | } 319 | Get-PvsCollection | ForEach-Object ` 320 | { 321 | $collections.Add( $_.CollectionId , $_.CollectionName ) 322 | } 323 | Get-PvsStore | ForEach-Object ` 324 | { 325 | $stores.Add( $_.StoreId , $_.StoreName ) 326 | } 327 | Get-PvsAuditTrail @auditParams | ForEach-Object ` 328 | { 329 | $auditItem = $_ 330 | [string]$subItem = $null 331 | if( ! [string]::IsNullOrEmpty( $auditItem.SubId ) ) ## GUID of the Collection or Store of the action 332 | { 333 | $subItem = $collections[ $auditItem.SubId ] 334 | if( [string]::IsNullOrEmpty( $subItem ) ) 335 | { 336 | $subItem = $stores[ $auditItem.SubId ] 337 | } 338 | } 339 | [string]$parameters = $null 340 | [string]$properties = $null 341 | if( $auditItem.Attachments -band 0x4 ) ## parameters 342 | { 343 | $parameters = ( Get-PvsAuditActionParameter -AuditActionId $auditItem.AuditActionId | ForEach-Object ` 344 | { 345 | "$($_.name)=$($_.value) " 346 | } ) 347 | } 348 | if( $auditItem.Attachments -band 0x8 ) ## properties 349 | { 350 | $properties = ( Get-PvsAuditActionProperty -AuditActionId $auditItem.AuditActionId | ForEach-Object ` 351 | { 352 | "$($_.name):$($_.OldValue)=>$($_.NewValue) " 353 | } ) 354 | } 355 | [PSCustomObject]@{ 356 | 'Time' = $auditItem.Time 357 | 'PVS Server' = $pvsServer 358 | 'Domain' = $auditItem.Domain 359 | 'User' = $auditItem.UserName 360 | 'Type' = $audittypes[ $auditItem.Type ] 361 | 'Action' = $auditActions[ $auditItem.Action -as [int] ] 362 | 'Object Name' = $auditItem.ObjectName 363 | 'Sub Item' = $subItem 364 | 'Path' = $auditItem.Path 365 | 'Site' = $sites[ $auditItem.SiteId ] 366 | 'Properties' = $properties 367 | 'Parameters' = $parameters } 368 | } 369 | if( $PVSSession ) 370 | { 371 | $null = Remove-PSSession -Session $PVSSession 372 | $PVSSession = $null 373 | } 374 | } ) | Sort Time 375 | 376 | [string]$title = "Got $($auditevents.Count) audit events from $($pvsServers -join ' ')" 377 | 378 | Write-Verbose $title 379 | 380 | if( ! [string]::IsNullOrEmpty( $csv ) ) 381 | { 382 | $auditevents | Export-Csv -Path $csv -NoClobber -NoTypeInformation 383 | } 384 | 385 | if( $gridView ) 386 | { 387 | $selected = $auditevents | Out-GridView -Title $title -PassThru 388 | if( $selected -and $selected.Count ) 389 | { 390 | $selected | Set-Clipboard 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /Show Studio Access.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3.0 2 | 3 | <# 4 | Retrieve all user accounts which have Citrix Studio admin access 5 | 6 | Guy Leech 7 | #> 8 | 9 | <# 10 | .SYNOPSIS 11 | 12 | Show all individual Citrix XenApp 7.x admins as defined in Studio by using the Active Directory PowerShell module to recursively expand groups. 13 | 14 | .DESCRIPTION 15 | 16 | Will write to a csv file or display on screen in a grid view, including various AD attributes for each user/ 17 | 18 | .PARAMETER ddc 19 | 20 | The Desktop Delivery Controller to connect to to retrieve the list of administrators from. 21 | 22 | .PARAMETER csv 23 | 24 | The name of a csv file to write the results to. Must not exist already. If not specified then results will be dispplayed in an on-screen grid view 25 | 26 | .PARAMETER ADProperties 27 | 28 | A commas separated list of the AD properties to include as returned by Get-ADUser 29 | 30 | .PARAMETER name 31 | 32 | The name of a single account (user or group) that you wish to only report on. This is as displayed in Studio. 33 | 34 | .EXAMPLE 35 | 36 | & '.\scripts\Show Studio access.ps1' -ddc ctxddc01 37 | 38 | Retrieve the list of Citrix admins from Delivery Controller cxtddc01 and display every user in an on-screen grid view 39 | 40 | .EXAMPLE 41 | 42 | & '.\scripts\Show Studio access.ps1' -ddc ctxddc01 -csv h:\citrix.admins.csv 43 | 44 | Retrieve the list of Citrix admins from Delivery Controller cxtddc01 and save to file h:\citrix.admins.csv 45 | 46 | .NOTES 47 | 48 | The Citrix XenApp 7.x PowerShell cmdlets must be available so run where Studio is installed. 49 | Also requires the ActiveDirectory PowerShell module. 50 | 51 | #> 52 | 53 | [CmdletBinding()] 54 | 55 | Param 56 | ( 57 | [string]$ddc = 'localhost' , 58 | [string]$csv , 59 | [string[]]$ADProperties = @( 'Description','Office','Info','LockedOut','Created','Enabled','LastLogonDate' ) , 60 | [string]$name 61 | ) 62 | 63 | Function Get-ADProperties( [string[]]$ADProperties , [string]$SamAccountName , $ADObject ) 64 | { 65 | [hashtable]$properties = @{} 66 | 67 | if( ! $ADObject ) 68 | { 69 | $ADObject = Get-ADUser -Identity $SamAccountName -Properties $ADProperties 70 | } 71 | if( $ADObject ) 72 | { 73 | ForEach( $ADProperty in $ADProperties ) 74 | { 75 | $properties.Add( $ADProperty , ( $ADObject | select -ExpandProperty $ADProperty ) ) 76 | } 77 | } 78 | else 79 | { 80 | Write-Warning "Failed to get $SamAccountName from AD" 81 | } 82 | $properties 83 | } 84 | 85 | [string[]]$snapins = @( 'Citrix.DelegatedAdmin.Admin.*' ) 86 | [string[]]$modules = @( 'ActiveDirectory' ) 87 | 88 | if( $snapins -and $snapins.Count -gt 0 ) 89 | { 90 | ForEach( $snapin in $snapins ) 91 | { 92 | Add-PSSnapin $snapin -ErrorAction Stop 93 | } 94 | } 95 | 96 | if( $modules -and $modules.Count -gt 0 ) 97 | { 98 | ForEach( $module in $modules ) 99 | { 100 | Import-Module $module -ErrorAction Stop 101 | } 102 | } 103 | 104 | [hashtable]$params = @{} 105 | if( ! [string]::IsNullOrEmpty( $name ) ) 106 | { 107 | $params.Add( 'Name' , $name ) 108 | } 109 | 110 | $admins = @( Get-AdminAdministrator -AdminAddress $ddc @params ) 111 | 112 | Write-Verbose "Got $($admins.Count) admin entries from $ddc" 113 | 114 | [int]$counter = 0 115 | 116 | $results = @( ForEach( $admin in $admins ) 117 | { 118 | $counter++ 119 | ## Now get all user accounts of this entity 120 | [string]$account = ($admin.Name -split '\\')[-1] 121 | Write-Verbose "$counter / $($admins.Count) : $account" 122 | $user = $null 123 | $group = $null 124 | [string]$role,[string]$scope = $admin.Rights -split ':' 125 | [hashtable]$commonProperties = @{ 'Role' = $role ; 'Scope' = $scope } 126 | 127 | try 128 | { 129 | $user = Get-ADUser -Identity $account -Properties $ADProperties 130 | } 131 | catch 132 | { 133 | $group = Get-ADGroupMember -Identity $account -Recursive 134 | } 135 | 136 | if( $group ) 137 | { 138 | $group | ForEach-Object ` 139 | { 140 | $thisGroup = $_ 141 | $result = [pscustomobject][ordered]@{ 'Name'=$thisGroup.SamAccountName ; 'Via Group'= $account } 142 | Add-Member -InputObject $result -NotePropertyMembers $commonProperties 143 | $extras = Get-ADProperties -ADProperties $ADProperties -SamAccountName $thisGroup.SamAccountName -ADObject $null 144 | if( $extras -and $extras.Count ) 145 | { 146 | Add-Member -InputObject $result -NotePropertyMembers $extras 147 | } 148 | $result 149 | } 150 | } 151 | elseif( $user ) 152 | { 153 | $result = [pscustomobject][ordered]@{ 'Name'=$user.SamAccountName ; 'Via Group'= $null } 154 | Add-Member -InputObject $result -NotePropertyMembers $commonProperties 155 | $extras = Get-ADProperties -ADProperties $ADProperties -SamAccountName $user.SamAccountName -ADObject $user 156 | if( $extras -and $extras.Count ) 157 | { 158 | Add-Member -InputObject $result -NotePropertyMembers $extras 159 | } 160 | $result 161 | } 162 | else 163 | { 164 | Write-Warning "Unable to find AD entity `"$account`"" 165 | } 166 | }) 167 | 168 | [string]$message = "Got $($results.count) individual admins via $($admins.Count) entries via $ddc" 169 | 170 | Write-Verbose $message 171 | 172 | if( $results -and $results.Count ) 173 | { 174 | if( [string]::IsNullOrEmpty( $csv ) ) 175 | { 176 | $selected = $results | Out-GridView -Title $message -PassThru 177 | if( $selected -and $selected.Count ) 178 | { 179 | $selected | clip.exe 180 | } 181 | } 182 | else 183 | { 184 | $results | Export-Csv -Path $csv -NoTypeInformation -NoClobber 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /StoreFront Log Levels.ps1: -------------------------------------------------------------------------------- 1 | #Requires -version 3.0 2 | 3 | <# 4 | Change or show StoreFront tracing settings on one or more SF servers. 5 | Note that this will restart various services so a brief loss of service is possible when seting the trace level 6 | 7 | Use of this script is entirely at your own risk - the author cannot be held responsible for any undesired effects deemed to have been caused by this script. 8 | 9 | Guy Leech, 2018 10 | #> 11 | 12 | <# 13 | .SYNOPSIS 14 | 15 | Change the logging level of StoreFront servers or show the current trace level(s). Will warn when the settings are not consistent on a server. 16 | 17 | .DESCRIPTION 18 | 19 | Setting the trace level will cause some of the StoreFront services to be restarted which could result in a short loss of service. 20 | Remember to set the trace level back to what it was when debugging has been completed. 21 | 22 | .PARAMETER servers 23 | 24 | A comma separated list of StoreFront servers to operate on. 25 | If StoreFront clusters are in use then only one server in each cluster needs to be specified if the -cluster option is specified. 26 | 27 | .PARAMETER cluster 28 | 29 | Uses StoreFront cmdlets to get a list of the cluster members and adds them to the servers list. 30 | 31 | .PARAMETER traceLevel 32 | 33 | The trace level to set on the given list of servers. If not specified then the current logging state(s) are reported. 34 | 35 | .PARAMETER grid 36 | 37 | Output any inconsistent results to an on screen grid view and copy all selected rows into the clipboard when OK is pressed. 38 | 39 | .PARAMETER webConfig 40 | 41 | The name of the config file to interrogate. There should ne no need to change this. 42 | 43 | .PARAMETER installDirKey 44 | 45 | The registry key where the StoreFront installation directory is stored. Only used when -cluster specified and there should ne no need to change this. 46 | 47 | .PARAMETER installDirValue 48 | 49 | The registry value in which the StoreFront installation directory is stored. Only used when -cluster specified and there should ne no need to change this. 50 | 51 | .PARAMETER moduleInstaller 52 | 53 | The StoreFront script, relative to the installation directory, which loads the StoreFront modules containing the Get-DSClusterMembersName cmdlet. 54 | Only used when -cluster specified and there should ne no need to change this. 55 | 56 | .PARAMETER diagnosticsNode 57 | 58 | The XML node in the web.config files which contains the trace settings. Only used when -cluster specified and there should ne no need to change this. 59 | 60 | .EXAMPLE 61 | 62 | & '.\StoreFront Logging' -servers storefront01,storefront02 -grid 63 | 64 | Report the state of logging on the listed StoreFront servers and display in a sortbable and filterable grid view. 65 | 66 | .EXAMPLE 67 | 68 | & '.\StoreFront Logging' -servers storefront01 -cluster -traceLevel Error 69 | 70 | Set the state of logging to 'Error' on the listed server and any cluster members. 71 | This will cause StoreFront services to be restarted regardless of whether the trace level is changing or not. 72 | 73 | .LINK 74 | 75 | https://docs.citrix.com/en-us/storefront/3/sf-troubleshoot.html 76 | 77 | .NOTES 78 | 79 | There doesn't seem to be a cmdlet for reading trace settings so the web.config files are parsed in order to retrieve the settings. 80 | 81 | Can be runfrom any server, not necessarily a StoreFront one. 82 | 83 | Use -verbose to get more detailed information. 84 | #> 85 | 86 | [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] 87 | 88 | Param 89 | ( 90 | [switch]$cluster , 91 | [string[]]$servers = @( 'locahost' ) , 92 | [ValidateSet('Off', 'Error','Warning','Info','Verbose')] 93 | [string]$traceLevel , 94 | [switch]$grid , 95 | ## Parameters below here generally should not need to be changed. Only really here as I detest hard coding of strings but I like scripts to be adaptable without code changes where possible 96 | [string]$webConfig = 'web.config' , 97 | [string]$installDirKey = 'SOFTWARE\Citrix\DeliveryServices' , 98 | [string]$installDirValue = 'InstallDir' , 99 | [string]$moduleInstaller = 'Scripts\ImportModules.ps1' , 100 | [string]$diagnosticsNode = 'configuration/system.diagnostics/switches/add' , 101 | [string]$logfileNode = 'configuration/system.diagnostics/sharedListeners/add' 102 | ) 103 | 104 | ## The name of the attribute we add to the web.config XML to store the name of that file. Must not exist already in the XML. Does not change the web.config file itself. 105 | Set-Variable -Name 'fileNameAttribute' -Value '__FileName' -Option Constant 106 | 107 | if( [string]::IsNullOrEmpty( $traceLevel ) ) 108 | { 109 | [string]$snapin = 'Citrix.DeliveryServices.Web.Commands' 110 | } 111 | else 112 | { 113 | [string]$snapin = 'Citrix.DeliveryServices.Framework.Commands' 114 | } 115 | 116 | ## retrieve versions so store for second pass 117 | [hashtable]$StoreFrontVersions = @{} 118 | 119 | ## We will retrieve all the cluster members - although multiple SF servers may have been specified, they may be members of different clusters so we get all unique names 120 | if( $cluster ) 121 | { 122 | $newServers = New-Object -TypeName System.Collections.ArrayList 123 | ForEach( $server in $servers ) 124 | { 125 | ## Read install dir from registry so we don't have to load all SF cmdlets 126 | $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,$server) 127 | if( $reg ) 128 | { 129 | $RegSubKey = $Reg.OpenSubKey($installDirKey) 130 | 131 | if( $RegSubKey ) 132 | { 133 | $installDir = $RegSubKey.GetValue($installDirValue) 134 | if( ! [string]::IsNullOrEmpty( $installDir ) ) 135 | { 136 | $script = Join-Path $installDir $moduleInstaller 137 | [string]$version,[string[]]$clusterMembers = Invoke-Command -ComputerName $server -ScriptBlock ` 138 | { 139 | & $using:script 140 | (Get-DSVersion).StoreFrontVersion 141 | @( (Get-DSClusterMembersName).Hostnames ) 142 | } 143 | if( ! [string]::IsNullOrEmpty( $version ) ) 144 | { 145 | $StoreFrontVersions.Add( $server , $version ) 146 | } 147 | if( $clusterMembers -and $clusterMembers.Count ) 148 | { 149 | ## now iterate through and add to servers array if not present already although indirectly since we are already iterating over this array so cannot change it 150 | $clusterMembers | ForEach-Object ` 151 | { 152 | if( $servers -notcontains $_ -and $newServers -notcontains $_ ) 153 | { 154 | $null = $newServers.Add( $_ ) 155 | } 156 | } 157 | } 158 | else 159 | { 160 | Write-Warning "No cluster members found via $server" 161 | } 162 | } 163 | else 164 | { 165 | Write-Error "Failed to read value `"$installDirValue`" from key HKLM\$installDirKey on $server" 166 | } 167 | $RegSubKey.Close() 168 | } 169 | else 170 | { 171 | Write-Error "Failed to open key HKLM\$installDirKey on $server" 172 | } 173 | $reg.Close() 174 | } 175 | else 176 | { 177 | Write-Error "Failed to open key HKLM on $server" 178 | } 179 | } 180 | if( $newServers -and $newServers.Count ) 181 | { 182 | Write-Verbose "Adding $($newServers.Count) servers to action list: $($newServers -join ',')" 183 | $servers += $newServers 184 | } 185 | } 186 | 187 | [int]$badServers = 0 188 | 189 | [array]$results = @( ForEach( $server in $servers ) 190 | { 191 | if( [string]::IsNullOrEmpty( $traceLevel ) ) 192 | { 193 | ## keyed on file name with value as the XML from that file 194 | [hashtable]$configFiles = Invoke-Command -ComputerName $server -ScriptBlock ` 195 | { 196 | Add-PSSnapin $using:snapin 197 | [hashtable]$files = @{} 198 | $dsWebSite = Get-DSWebSite 199 | $dsWebSite.Applications | ForEach-Object ` 200 | { 201 | $app = $_ 202 | [string]$configFile = Join-Path ( Join-Path $dsWebSite.PhysicalPath $app.VirtualPath ) $using:webConfig 203 | [xml]$content = $null 204 | 205 | if( Test-Path $configFile -ErrorAction SilentlyContinue ) 206 | { 207 | $content = (Get-Content $configFile) 208 | } 209 | $files.Add( $configFile , $content ) 210 | } 211 | $files 212 | } 213 | Write-Verbose "Got $($configFiles.Count) $webConfig files from $server" 214 | [hashtable]$states = @{} 215 | $configFiles.GetEnumerator() | ForEach-Object ` 216 | { 217 | [xml]$node = $_.Value 218 | [string]$fileName = $_.Key 219 | $diags = $null 220 | try 221 | { 222 | $diags = @( $node.SelectNodes( "//$diagnosticsNode" ) ) 223 | $logFile = $node.SelectSingleNode( "//$logfileNode" ) ## should only be one 224 | } 225 | catch { } 226 | if( $diags ) 227 | { 228 | $diags | ForEach-Object ` 229 | { 230 | $thisSwitch = $_ 231 | [string]$module = ($thisSwitch.Name -split '\.')[-1] 232 | $info = $null 233 | try 234 | { 235 | $info = [pscustomobject]@{ 'Server' = $server ; 'Trace Level' = $thisSwitch.Value ; 'Config File' = $fileName ; 'Module' = $module } 236 | [string]$version = $StoreFrontVersions[ $server ] 237 | if( ! [string]::IsNullOrEmpty( $version ) ) 238 | { 239 | Add-Member -InputObject $info -MemberType NoteProperty -Name 'StoreFront Version' -Value $version 240 | } 241 | if( $logFile ) 242 | { 243 | Add-Member -InputObject $info -NotePropertyMembers @{ 'Log File' = $logFile.initializeData ; 'Max Size (KB)' = $logFile.maxFileSizeKB } 244 | ## Seems this isn't present for all SF versions 245 | if( Get-Member -InputObject $logFile -Name fileCount -ErrorAction SilentlyContinue ) 246 | { 247 | Add-Member -InputObject $info -MemberType NoteProperty -Name 'Log File Count' -Value $logfile.fileCount 248 | } 249 | } 250 | $states.Add( $thisSwitch.Value , [System.Collections.ArrayList]( @( $info ) ) ) 251 | } 252 | catch 253 | { 254 | if( ! [string]::IsNullOrEmpty( $thisSwitch.Name ) -and $info ) 255 | { 256 | $null = $states[ $thisSwitch.Value ].Add( $info ) 257 | } 258 | } 259 | } 260 | } 261 | } 262 | $states.GetEnumerator() | select -ExpandProperty Value ## push into results array 263 | if( $states.Count -gt 1 ) 264 | { 265 | Write-Warning "Trace levels are inconsistent on $server - $(($states.GetEnumerator()|Select -ExpandProperty Name) -join ',')" 266 | $states.GetEnumerator() | ForEach-Object { Write-Verbose "$($_.Name) :`n`t$(($_.Value|select -ExpandProperty 'Config File') -join ""`n`t"" )" } 267 | $badServers++ 268 | } 269 | elseif( ! $states.Count ) 270 | { 271 | Write-Warning "No trace levels found on $server" 272 | } 273 | Write-Host "$server : logging level is $(($states.GetEnumerator()|select -ExpandProperty Name ) -join ',')" ## Can't be Write-Output otherwise will be captured into the results array 274 | } 275 | elseif( $PSCmdlet.ShouldProcess( $server , "Set trace level to $tracelevel & restart services" ) ) 276 | { 277 | Write-Verbose "Setting trace level to $traceLevel on $server" 278 | Invoke-Command -ComputerName $server -ScriptBlock { Add-PSSnapin $using:snapin ; Set-DSTraceLevel -All -TraceLevel $using:traceLevel } 279 | } 280 | } ) 281 | 282 | if( $grid -and $results.Count ) 283 | { 284 | [string]$title = $( if( $badServers ) 285 | { 286 | "Inconsistent settings found on $badServers out of" 287 | } 288 | else 289 | { 290 | ## Now check it's the same consistent setting across all servers 291 | [string]$lastLevel = $null 292 | [bool]$matching = $true 293 | [int]$different = 0 294 | ForEach( $server in $servers ) 295 | { 296 | [string]$thisLevel = $results | Where-Object { $_.Server -eq $server } | Select -First 1 -ExpandProperty 'Trace Level' 297 | if( $lastLevel -and $thisLevel -ne $lastLevel) 298 | { 299 | $matching = $false 300 | $different++ 301 | } 302 | $lastLevel = $thisLevel 303 | } 304 | if( $matching ) 305 | { 306 | "Consistent settings found on all" 307 | } 308 | else 309 | { 310 | "Different settings found on $different out of" 311 | } 312 | } ) + " $($servers.Count) StoreFront servers" 313 | 314 | $selected = $results | Select * | Out-GridView -Title $title -PassThru 315 | if( $selected ) 316 | { 317 | $selected | Clip.exe 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Studio Selector.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | 5 | Allow user to select what CVAD Delivery Controller to connect Citrix Studio to, either interactively or via script arguments 6 | 7 | .DESCRIPTION 8 | 9 | Citrix Studio only prompts once for the delivery controller to connect to & stores it in %AppData%\Microsoft\MMC\Studio. 10 | This script allows copies of this file to be made which store different delivery controllers which can be picked interactively at run time or via -server argument. 11 | 12 | .PARAMETER server 13 | 14 | The delivery controller to connect to or to produce a config file for if -save is specified (and it is assumed that this is the server last connected to). 15 | If not specified a grid view will be presented with all previously configured delivery controllers to chose one from. 16 | 17 | .PARAMETER save 18 | 19 | Save the existing file produced by running Studio to a file with the name specified by the -server argument. 20 | Keeps the existing file too so that Studio can be run outside of this script and it will connect to the same server as is stored in the file. 21 | 22 | .PARAMETER move 23 | 24 | Move the existing file produced by running Studio to a file with the name specified by the -server argument. 25 | Removes the existing file too so that Studio when run outside of this script will prompt for the delivery controller to connect to. 26 | 27 | .PARAMETER delete 28 | 29 | Delete the default Studio config file or for a specific delivery controller if -server is specified. 30 | 31 | .PARAMETER console 32 | 33 | The path to the Studio executable. If not specified will be retrieved from the registry. 34 | 35 | .EXAMPLE 36 | 37 | & '.\Studio Selector.ps1' 38 | 39 | Prompt the user for which delivery controller to connect to. If no previously saved Studio.server files are found Studio will be launched normally. 40 | 41 | .EXAMPLE 42 | 43 | & '.\Studio Selector.ps1' -save -server grl-xaddc01 44 | 45 | Save the current Studio configuration file to Studio.grl-xaddc01 so it can be picked later. 46 | Assumes the server that Studio was last connected to is the one mentioned - it does not check (yet) 47 | 48 | .EXAMPLE 49 | 50 | & '.\Studio Selector.ps1' -server grl-xaddc01 51 | 52 | Launch Studio to connect to delivery controller grl-xaddc01, as long as Studio was previously connected to this at some juncture and a Studio.grl-xaddc01 file produced via -save or -move 53 | 54 | .EXAMPLE 55 | 56 | & '.\Studio Selector.ps1' -delete -server grl-xaddc01 57 | 58 | Delete the saved Studio file for delivery controller grl-xaddc01, prompting for confirmation 59 | 60 | .EXAMPLE 61 | 62 | & '.\Studio Selector.ps1' -delete -server grl-xaddc01 -confirm:$false 63 | 64 | Delete the saved Studio file for delivery controller grl-xaddc01 without prompting for confirmation 65 | 66 | .NOTES 67 | 68 | The delivery controller name is stored in a base64 encoded XML text string in the Studio file which is itself an XML file 69 | If a Studio file already exists, it will be renamed to GUID.Studio in the same folder 70 | 71 | Modification History: 72 | 73 | 12/01/2021 @guyrleech Initial release 74 | 75 | #> 76 | 77 | [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High',DefaultParameterSetName='None')] 78 | 79 | Param 80 | ( 81 | [string]$server , 82 | [string]$configFileName = 'Studio' , 83 | [Parameter(ParameterSetName='Save')] 84 | [switch]$save , 85 | [Parameter(ParameterSetName='Save')] 86 | [switch]$move , 87 | [Parameter(ParameterSetName='Delete')] 88 | [switch]$delete , 89 | [Parameter(ParameterSetName='None')] 90 | [string]$console 91 | ) 92 | 93 | if( ! $PSBoundParameters[ 'console' ] ) 94 | { 95 | if( [string]::IsNullOrEmpty( ( $console = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Citrix\DesktopStudio' -Name LaunchPath | Select-Object -ExpandProperty LaunchPath ) ) ) 96 | { 97 | Throw "Unable to find console registry value LaunchPath in HKLM\SOFTWARE\Citrix\DesktopStudio" 98 | } 99 | } 100 | 101 | if( ! ( Test-Path -Path $console -PathType Leaf -ErrorAction SilentlyContinue ) ) 102 | { 103 | Throw "Unable to find console launcher `"$console`"" 104 | } 105 | 106 | [string]$configFileFolder = [System.IO.Path]::Combine( ([Environment]::GetFolderPath( [Environment+SpecialFolder]::ApplicationData )) , 'Microsoft' , 'MMC' ) 107 | 108 | if( ! ( Test-Path -Path $configFileFolder -PathType Container -ErrorAction SilentlyContinue ) ) 109 | { 110 | Throw "Folder `"$configFileFolder`" does not exist - has MMC ever been run for this user?" 111 | } 112 | 113 | [string]$originalConfigFilePath = Join-Path -Path $configFileFolder -ChildPath $configFileName 114 | 115 | if( $save -or $move ) 116 | { 117 | if( ! $PSBoundParameters[ 'server' ] ) 118 | { 119 | Throw "Must specify server name via -server when saving file" 120 | } 121 | if( ! ( Test-Path -Path $originalConfigFilePath -PathType Leaf -ErrorAction SilentlyContinue ) ) 122 | { 123 | Throw "File `"$originalConfigFilePath`" does not exist - has Studio ever been run for this user?" 124 | } 125 | 126 | [string]$newStudioFile = Join-Path -Path $configFileFolder -ChildPath ( $configFileName + '.' + $server) 127 | if( ! ( Test-Path -Path $newStudioFile -ErrorAction SilentlyContinue ) -or $PSCmdlet.ShouldProcess( $newStudioFile , 'Overwrite' ) ) 128 | { 129 | if( $move ) 130 | { 131 | Move-Item -Path $originalConfigFilePath -Destination $newStudioFile -Force 132 | } 133 | else 134 | { 135 | Copy-Item -Path $originalConfigFilePath -Destination $newStudioFile -Force 136 | } 137 | } 138 | } 139 | elseif( $delete ) 140 | { 141 | [string]$fileName = $originalConfigFilePath 142 | if( $PSBoundParameters[ 'server' ] ) 143 | { 144 | $fileName = Join-Path -Path $configFileFolder -ChildPath ($configFileName + ".$server") 145 | } 146 | 147 | if( ! ( Test-Path -Path $fileName -PathType Leaf -ErrorAction SilentlyContinue ) ) 148 | { 149 | Write-Warning -Message "$fileName does not exist so cannot delete" 150 | } 151 | elseif( $PSCmdlet.ShouldProcess( $fileName , 'Delete' ) ) 152 | { 153 | Remove-Item -Path $fileName -Force 154 | } 155 | } 156 | else 157 | { 158 | [array]$studioFiles = @( ( Get-ChildItem -Path "$originalConfigFilePath.*" | Select-Object -ExpandProperty Name ) -replace "^$configFileName\." ) 159 | if( ! $studioFiles -or ! $studioFiles.Count ) 160 | { 161 | Write-Warning -Message "No saved $configFileName files found `"$originalConfigFilePath.*`" - have you run $configFileName and then this script with -save?" 162 | 163 | if( ! ( $launched = Start-Process -FilePath $console -WorkingDirectory (Split-Path -Path $console) -PassThru ) ) 164 | { 165 | Throw "Failed to launch $console" 166 | } 167 | Write-Verbose -Message "$console launched as pid $($launched.Id) at $(Get-Date -Format G)" 168 | } 169 | else 170 | { 171 | Write-Verbose -Message "Found $($studioFiles.Count) studio files ($($studioFiles -join ' , '))" 172 | $chosen = $null 173 | if( $PSBoundParameters[ 'server' ] ) 174 | { 175 | $chosen = $server 176 | } 177 | else 178 | { 179 | if( $chosenItem = $studioFiles | Select-Object @{n='Server';e={$_}} | Out-GridView -Title "Choose server to connect to with $configFileName" -PassThru ) 180 | { 181 | if( $chosenItem -is [array] ) 182 | { 183 | Throw "Only 1 server should be selected - $($chosenItem.Count) were selected" 184 | } 185 | else 186 | { 187 | $chosen = $chosenItem.Server 188 | } 189 | } 190 | } 191 | 192 | if( ! [string]::IsNullOrEmpty( $chosen ) ) 193 | { 194 | Write-Verbose -Message "$chosen chosen" 195 | [string]$studioFileChosen = Join-Path -Path $configFileFolder -ChildPath ($configFileName + ".$chosen") 196 | if( ! ( Test-Path -Path $studioFileChosen -ErrorAction SilentlyContinue ) ) 197 | { 198 | Throw "Unable to find chosen file `"$studioFileChosen`"" 199 | } 200 | [bool]$continue = $true 201 | [string]$renamedStudioFile = $null 202 | if( Test-Path -Path $originalConfigFilePath -ErrorAction SilentlyContinue ) 203 | { 204 | $renamedStudioFile = Join-Path -Path $configFileFolder -ChildPath ((New-Guid).Guid + ".$configFileName" ) 205 | Write-Verbose -Message "Renamed original is $renamedStudioFile" 206 | Move-Item -Path $originalConfigFilePath -Destination $renamedStudioFile -Force 207 | $continue = $? -and ( Test-Path -Path $renamedStudioFile -ErrorAction SilentlyContinue ) 208 | } 209 | if( $continue ) 210 | { 211 | Copy-Item -Path $studioFileChosen -Destination $originalConfigFilePath -Force 212 | if( $? -and ( Test-Path -Path $originalConfigFilePath -ErrorAction SilentlyContinue ) ) 213 | { 214 | if( ! ( $launched = Start-Process -FilePath $console -WorkingDirectory (Split-Path -Path $console) -PassThru ) ) 215 | { 216 | Throw "Failed to launch $console" 217 | } 218 | Write-Verbose -Message "$console launched as pid $($launched.Id) at $(Get-Date -Format G)" 219 | } 220 | else 221 | { 222 | Throw "Failed to copy `"$studioFileChosen`" to `"$originalConfigFilePath`"" 223 | } 224 | } 225 | else 226 | { 227 | Throw "Failed to copy original file `"$originalConfigFilePath`" to `"$renamedStudioFile`"" 228 | } 229 | } 230 | else 231 | { 232 | Write-Verbose -Message "Cancel pressed in grid view" 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /Update Snapshot for Citrix MCS.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | 4 | Change the snapshot being used for a specified Citrix MCS machine catalog and optionally reboot the machines in the updated machine catalog 5 | 6 | .PARAMETER catalog 7 | 8 | The name, or pattern, of the single machine catalog to update 9 | 10 | .PARAMETER snapshotName 11 | 12 | The name of the snapshot to use (regular expressions supported) 13 | 14 | .PARAMETER vmName 15 | 16 | The name of the virtual machine to use the snapshot from. If not specified will use the existing VM set for the catalog. 17 | 18 | .PARAMETER ddc 19 | 20 | The delivery controller to use 21 | 22 | .PARAMETER reportOnly 23 | 24 | Do not change anything, just report on the current status and snapshot 25 | 26 | .PARAMETER rebootdurationMinutes 27 | 28 | The period, in minutes, over which to reboot all machines in the catalog once updated 29 | 30 | .PARAMETER warningDurationMinutes 31 | 32 | Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine 33 | 34 | .PARAMETER warningTitle 35 | 36 | The title of the warning message 37 | 38 | .PARAMETER warningMessage 39 | 40 | The contents of the warning message. Environment variables will be expanded 41 | 42 | .PARAMETER warningRepeatIntervalMinutes 43 | 44 | Repeat the warning message at this interval in minutes 45 | 46 | .PARAMETER async 47 | 48 | Do not wait for the catalog update to complete. Cannot be used with reboot parameters. 49 | 50 | .EXAMPLE 51 | 52 | & '.\Update Snapshot for Citrix MCS.ps1' -catalog "MCS Server 2019" -ddc grl-xaddc02 -reportonly 53 | 54 | Show the current snapshot, application date and other information about the machine catalog via the delivery controller grl-xaddc02 55 | 56 | .EXAMPLE 57 | 58 | & '.\Update Snapshot for Citrix MCS.ps1' -catalog "MCS Server 2019" -ddc grl-xaddc02 -snapshotName "FSlogix 2.9.7838.44263, WU" -rebootdurationMinutes 60 59 | 60 | Update the named machine catalog with the named snapshot and when complete initiate a reboot cycle of the machines in the catlog which should last no longer than 60 minutes. 61 | 62 | .EXAMPLE 63 | 64 | & '.\Update Snapshot for Citrix MCS.ps1' -catalog "MCS Server 2019" -ddc grl-xaddc02 -async 65 | 66 | Update the named machine catalog with the latest snapshot but do not initiate reboots of the machines in the catalog. 67 | Note that rebooting within the OS or hypervisor will not cause the new snapshot to be used - the reboot must be initiated via Studio or Start-BrokerRebootCycle 68 | 69 | .NOTES 70 | 71 | Requires CVAD PowerShell cmdlets (installed with Studio or available as separate msi files on the product ISO) 72 | 73 | Use -confirm:$false to suppress prompting to take actions (use at your own risk) 74 | 75 | https://support.citrix.com/article/CTX129205 76 | 77 | Modification History 78 | 79 | 2021/12/05 @guyrleech Fixed multiple -verbose to Publish-ProvMasterVMImage 80 | 2022/01/08 @guyrleech Added extra fields to reportonly 81 | #> 82 | 83 | <# 84 | Copyright © 2021 Guy Leech 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, 87 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 88 | 89 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 92 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 93 | #> 94 | 95 | [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] 96 | 97 | Param 98 | ( 99 | [Parameter(Mandatory=$true,HelpMessage='Name of machine catalog to list/update')] 100 | [string]$catalog , 101 | [Parameter(Mandatory=$false,ParameterSetName='Async')] 102 | [Parameter(Mandatory=$false,ParameterSetName='Reboot')] 103 | [string]$snapshotName , 104 | [string]$vmName , 105 | [string]$hostingUnitName , 106 | [string]$ddc , 107 | [string]$profileName , 108 | [Parameter(Mandatory=$true,ParameterSetName='ReportOnly')] 109 | [switch]$reportOnly , 110 | [Parameter(Mandatory=$true,ParameterSetName='Reboot')] 111 | [int]$rebootdurationMinutes , 112 | [Parameter(Mandatory=$false,ParameterSetName='Reboot')] 113 | [int]$warningDurationMinutes , 114 | [Parameter(Mandatory=$false,ParameterSetName='Reboot')] 115 | [string]$warningTitle , 116 | [Parameter(Mandatory=$false,ParameterSetName='Reboot')] 117 | [string]$warningMessage , 118 | [Parameter(Mandatory=$false,ParameterSetName='Reboot')] 119 | [int]$warningRepeatIntervalMinutes , 120 | [Parameter(Mandatory=$true,ParameterSetName='Async')] 121 | [switch]$async 122 | ) 123 | 124 | Add-PSSnapin -Name Citrix.MachineCreation.* , Citrix.Host.* 125 | 126 | [hashtable]$citrixParams = @{ 'Verbose' = $false } 127 | 128 | if( $PSBoundParameters[ 'profileName' ] ) 129 | { 130 | Get-XDAuthentication -ProfileName $profileName -ErrorAction Stop 131 | $ddc = 'Cloud' 132 | } 133 | elseif( $PSBoundParameters[ 'ddc' ] ) 134 | { 135 | $citrixParams.Add( 'AdminAddress' , $ddc ) 136 | } 137 | 138 | [array]$machineCatalogs = @( Get-BrokerCatalog -Name $catalog -ProvisioningType MCS @citrixParams ) 139 | 140 | if( ! $machineCatalogs -or ! $machineCatalogs.Count ) 141 | { 142 | Throw "Found no MCS machine catalogues for `"$catalog`"" 143 | } 144 | 145 | if( $machineCatalogs.Count -ne 1 ) 146 | { 147 | Throw "Found $($machineCatalogs.Count) MCS machine catalogues for `"$catalog`" - `"$(($machineCatalogs|Select-Object -ExpandProperty Name) -join '" , "')`"" 148 | } 149 | 150 | $machineCatalog = $machineCatalogs[0] 151 | 152 | if( $provScheme = Get-ProvScheme @citrixParams -ProvisioningSchemeUid $machineCatalog.ProvisioningSchemeId ) 153 | { 154 | ## get requested or deepest snapshot - Filter is not supported and -Include doesn't match everything we need 155 | 156 | ## The Get-ChildItem is very verbose so we'll lose that 157 | [array]$snapshots = @( Get-ChildItem -Path "$(($provScheme.MasterImageVM -split '\.vm\\')[0]).vm" -Recurse -Verbose:$false | Where-Object { $_.Name -match $snapshotName } -Verbose:$false ) ## if snapshot is null , will match everything 158 | 159 | if( ! $PSBoundParameters[ 'snapshotName' ] -and $snapshots.Count ) 160 | { 161 | ## if not matching on name then get deepest snapshot only 162 | $snapshots = @( $snapshots[ -1 ] ) 163 | } 164 | 165 | [string]$currentSnapshot = ($provScheme.MasterImageVM -split '\\')[-1] -replace '\.snapshot$' 166 | 167 | if( $PSBoundParameters[ 'vmName' ] ) 168 | { 169 | if( ! $PSBoundParameters[ 'hostingUnitName' ] ) 170 | { 171 | $hostingUnitName = $provScheme.HostingUnitName 172 | } 173 | 174 | ## Find this VM and get its snapshots instead 175 | if( ! ( $baseVM = Get-ChildItem -Path "XDHyp:\HostingUnits\$hostingUnitName\" | Where-Object { $_.Objecttype -eq 'vm' -and $_.PSChildName -eq "$vmName.vm" } ) ) 176 | { 177 | Throw "Unable to find vm $vmName in hosting unit $hostingUnitName" 178 | } 179 | if( $baseVM -is [array] -and $baseVM.Count -gt 1 ) 180 | { 181 | Throw "Found $($baseVM.Count) vms for $vmName in hosting unit $hostingUnitName" 182 | } 183 | $snapshots = @( Get-ChildItem -Path $baseVM.PSPath -Recurse -Verbose:$false | Where-Object { $_.Name -match $snapshotName } ) ## if snapshotis null , will match everything 184 | if( ! $snapshots -or ! $snapshots.Count ) 185 | { 186 | [string]$exception = "No snapshots found for $vmName" 187 | if( ! [string]::IsNullOrEmpty( $snapshotName ) ) 188 | { 189 | $exception += " for snapshot `"$snapshotName`"" 190 | } 191 | Throw $exception 192 | } 193 | if( ! $PSBoundParameters[ 'snapshotName' ] -and $snapshots.Count ) 194 | { 195 | ## if not matching on name then get deepest snapshot only 196 | $snapshots = @( $snapshots[ -1 ] ) 197 | } 198 | } 199 | 200 | Write-Verbose -Message "Current snapshot is '$currentSnapshot'" 201 | 202 | if( $reportOnly ) 203 | { 204 | New-Object -TypeName pscustomobject -ArgumentList @{ 205 | ## XDHyp:\HostingUnits\Internal Network\GLCTXMCSMAST19.vm\CVAD VDA 2012, VMtools 11.2.1.snapshot\Updated UWM 2020.3 & FSlogix, CVAD 2012.snapshot 206 | ## ^^^^^^^^^^^^^^ 207 | 'VM' = $provScheme.MasterImageVM -replace '^XDhyp:\\[^\\]+\\[^\\]+\\([^\\]+)\.vm\\.*$' , '$1' 208 | 'Current Snapshot' = $currentSnapshot 209 | 'Applied Date' = $provScheme.MasterImageVMDate 210 | 'vCPU' = $provScheme.CpuCount 211 | 'MemoryGB' = [math]::Round( $provScheme.MemoryMB / 1024 , 2 ) 212 | 'DiskGB' = $provScheme.DiskSize 213 | 'HostingUnitName' = $provScheme.HostingUnitName 214 | 'WriteBackCacheDiskSize' = $provScheme.WriteBackCacheDiskSize 215 | 'WriteBackCacheMemorySize' = $provScheme.WriteBackCacheMemorySize 216 | } 217 | } 218 | elseif( ! $snapshots -or ! $snapshots.Count ) 219 | { 220 | [array]$allSnapshots = ForEach( $snapshot in ((Get-ChildItem -Path "$(($provScheme.MasterImageVM -split '\.vm\\')[0]).vm" -Recurse -Verbose:$false)|Select-Object -ExpandProperty FullPath )) { ($snapshot -split '\\')[-1] -replace '\.snapshot$' } 221 | [string]$extraMessage = if( $PSBoundParameters[ 'snapshotName' ] ) { " out of the $($allSnapshots.Count) found matching `"$snapshotName`", snapshots are '$( $allSnapshots -join "' , '" )'" } 222 | Throw "Found no snapshots$extraMessage" 223 | } 224 | elseif( $snapshots.Count -gt 1 ) 225 | { 226 | [string]$snapshotDetails = "Found $($snapshots.Count) matching snapshots for `"$snapshotName`" - '$(((Split-Path -Path $snapshots.FullPath -Leaf -Verbose:$false) -replace '\.snapshot$') -join "' , '" )'" 227 | Throw $snapshotDetails 228 | } 229 | else 230 | { 231 | $snapshot = $snapshots[ 0 ] 232 | [string]$newSnapshotName = $snapshot.FullPath -replace '\.snapshot(\\|$)' , '\\' -replace '\\+$' 233 | 234 | if( $snapshot.FullPath -eq $provScheme.MasterImageVM ) 235 | { 236 | Throw "Already using snapshot `"$($snapshot.FullPath)`"" 237 | } 238 | elseif( $PSCmdlet.ShouldProcess( "Catalog '$catalog'" , "Publish snapshot '$newSnapshotName'" ) ) 239 | { 240 | Write-Verbose -Message "$(Get-Date -Format G): starting provisioning snapshot '$(Split-Path -Path $newSnapshotName -Leaf -Verbose:$false)' ..." 241 | if( ! ( $publishedResult = Publish-ProvMasterVMImage -ProvisioningSchemeName $provScheme.IdentityPoolName -MasterImageVM $snapshot.FullPath -RunAsynchronously:$async @citrixParams ) ) 242 | { 243 | Throw "Null returned from Publish-ProvMasterVMImage - provisoning most likely failed" 244 | } 245 | elseif( ! [string]::IsNullOrEmpty( $publishedResult.TerminatingError ) ) 246 | { 247 | Throw "Provisioning task failed with `"$($publishedResult.TerminatingError)`" at $(Get-Date -Date $publishedResult.DateFinished -Format G) after $($publishedResult.ActiveElapsedTime) seconds" 248 | } 249 | elseif( ! $async ) 250 | { 251 | if( $PSBoundParameters[ 'rebootdurationMinutes' ] ) 252 | { 253 | if( $PSCmdlet.ShouldProcess( "Catalogue `"$($machineCatalog.Name)`" with $($machineCatalog.UsedCount) machines" , 'Start Reboots' ) ) 254 | { 255 | [hashtable]$rebootArguments = @{ InputObject = $machineCatalog ; RebootDuration = $rebootdurationMinutes } 256 | $rebootArguments += $citrixParams 257 | if( $PSBoundParameters[ 'warningDurationMinutes' ] ) 258 | { 259 | $rebootArguments.Add( 'WarningDuration' , $warningDurationMinutes ) 260 | } 261 | if( $PSBoundParameters[ 'warningRepeatIntervalMinutes' ] ) 262 | { 263 | $rebootArguments.Add( 'WarningRepeatInterval' , $warningRepeatIntervalMinutes ) 264 | } 265 | if( ! [string]::IsNullOrEmpty( $warningTitle ) ) 266 | { 267 | $rebootArguments.Add( 'WarningTitle' , $warningTitle ) 268 | } 269 | if( ! [string]::IsNullOrEmpty( $warningMessage ) ) 270 | { 271 | $rebootArguments.Add( 'WarningMessage' , [System.Environment]::ExpandEnvironmentVariables( $warningMessage ) ) 272 | } 273 | if( ! ( $rebootCycle = Start-BrokerRebootCycle @rebootArguments ) ) 274 | { 275 | Write-Warning -Message "Failed to initiate reboot cycle for catalogue `"$($machineCatalog.Name)`"" 276 | } 277 | else 278 | { 279 | $rebootCycle 280 | } 281 | } 282 | else 283 | { 284 | Write-Warning -Message "Updated to snapshot ok but unable to find catalogue with provisioning scheme uid $($publishedResult.ProvisioningSchemeUid)" 285 | } 286 | } 287 | Write-Verbose -Message "Finished ok at $(Get-Date -Date $publishedResult.DateFinished -Format G), taking $($publishedResult.ActiveElapsedTime) seconds" 288 | } 289 | elseif( ! ($taskStatus = Get-ProvTask @citrixParams -TaskId $publishedResult ) ) 290 | { 291 | Throw "Failed to get task status" 292 | } 293 | elseif( ! $taskStatus.Active -or $taskStatus.Status -ne 'Running' -or $taskStatus.TerminatingError ) 294 | { 295 | Throw "Async task not running - status is $($taskStatus.Status) error is '$($taskStatus.TerminatingError)'" 296 | } 297 | else 298 | { 299 | $taskStatus 300 | } 301 | } 302 | } 303 | } 304 | else 305 | { 306 | Throw "Failed to find provisioning scheme for delivery group `"$catalog`"" 307 | } 308 | 309 | # SIG # Begin signature block 310 | # MIIZsAYJKoZIhvcNAQcCoIIZoTCCGZ0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 311 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 312 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU1j4Nj/wEFWI5JCy6qIO5frbn 313 | # Jv+gghS+MIIE/jCCA+agAwIBAgIQDUJK4L46iP9gQCHOFADw3TANBgkqhkiG9w0B 314 | # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD 315 | # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz 316 | # c3VyZWQgSUQgVGltZXN0YW1waW5nIENBMB4XDTIxMDEwMTAwMDAwMFoXDTMxMDEw 317 | # NjAwMDAwMFowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu 318 | # MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMTCCASIwDQYJKoZIhvcN 319 | # AQEBBQADggEPADCCAQoCggEBAMLmYYRnxYr1DQikRcpja1HXOhFCvQp1dU2UtAxQ 320 | # tSYQ/h3Ib5FrDJbnGlxI70Tlv5thzRWRYlq4/2cLnGP9NmqB+in43Stwhd4CGPN4 321 | # bbx9+cdtCT2+anaH6Yq9+IRdHnbJ5MZ2djpT0dHTWjaPxqPhLxs6t2HWc+xObTOK 322 | # fF1FLUuxUOZBOjdWhtyTI433UCXoZObd048vV7WHIOsOjizVI9r0TXhG4wODMSlK 323 | # XAwxikqMiMX3MFr5FK8VX2xDSQn9JiNT9o1j6BqrW7EdMMKbaYK02/xWVLwfoYer 324 | # vnpbCiAvSwnJlaeNsvrWY4tOpXIc7p96AXP4Gdb+DUmEvQECAwEAAaOCAbgwggG0 325 | # MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG 326 | # AQUFBwMIMEEGA1UdIAQ6MDgwNgYJYIZIAYb9bAcBMCkwJwYIKwYBBQUHAgEWG2h0 327 | # dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAfBgNVHSMEGDAWgBT0tuEgHf4prtLk 328 | # YaWyoiWyyBc1bjAdBgNVHQ4EFgQUNkSGjqS6sGa+vCgtHUQ23eNqerwwcQYDVR0f 329 | # BGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl 330 | # ZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz 331 | # c3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6 332 | # Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMu 333 | # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NB 334 | # LmNydDANBgkqhkiG9w0BAQsFAAOCAQEASBzctemaI7znGucgDo5nRv1CclF0CiNH 335 | # o6uS0iXEcFm+FKDlJ4GlTRQVGQd58NEEw4bZO73+RAJmTe1ppA/2uHDPYuj1UUp4 336 | # eTZ6J7fz51Kfk6ftQ55757TdQSKJ+4eiRgNO/PT+t2R3Y18jUmmDgvoaU+2QzI2h 337 | # F3MN9PNlOXBL85zWenvaDLw9MtAby/Vh/HUIAHa8gQ74wOFcz8QRcucbZEnYIpp1 338 | # FUL1LTI4gdr0YKK6tFL7XOBhJCVPst/JKahzQ1HavWPWH1ub9y4bTxMd90oNcX6X 339 | # t/Q/hOvB46NJofrOp79Wz7pZdmGJX36ntI5nePk2mOHLKNpbh6aKLzCCBTAwggQY 340 | # oAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE 341 | # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj 342 | # ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X 343 | # DTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT 344 | # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx 345 | # MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD 346 | # QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/DhGvZ3cH0wsx 347 | # SRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2qvCchqXYJawO 348 | # eSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrskacLCUvIUZ4qJ 349 | # RdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/6XzLkqHlOzEc 350 | # z+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE94zRICUj6whk 351 | # PlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8np+mM6n9Gd8l 352 | # k9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD 353 | # AgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0wazAkBggrBgEF 354 | # BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRw 355 | # Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu 356 | # Y3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20v 357 | # RGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5k 358 | # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsME8GA1UdIARI 359 | # MEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp 360 | # Y2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7KgqjpepxA8Bg 361 | # +S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG 362 | # 9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh134LYP3DPQ/E 363 | # r4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63XX0R58zYUBor3 364 | # nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPAJRHinBRHoXpo 365 | # aK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC/i9yfhzXSUWW 366 | # 6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG/AeB+ova+YJJ 367 | # 92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBTEwggQZoAMCAQICEAqhJdbW 368 | # Mht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV 369 | # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG 370 | # A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTE2MDEwNzEyMDAw 371 | # MFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD 372 | # ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln 373 | # aUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTCCASIwDQYJKoZI 374 | # hvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnFOVQoV7YjSsQOB0Uz 375 | # URB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+ 376 | # X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPu 377 | # XciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQjMF287DxgaqwvB8z9 378 | # 8OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQ 379 | # hBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4w 380 | # ggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF 381 | # 66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB 382 | # /wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEFBQcBAQRtMGswJAYI 383 | # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3 384 | # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v 385 | # dENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQu 386 | # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2Ny 387 | # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBQBgNV 388 | # HSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cu 389 | # ZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggEB 390 | # AHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafDDiBCLK938ysfDCFa 391 | # KrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6HHssIeLWWywUNUME 392 | # aLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4H9YLFKWA1xJHcLN1 393 | # 1ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHKeZR+WfyMD+NvtQEm 394 | # tmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR7 395 | # 9VYzIi8iNrJLokqV2PWmjlIwggVPMIIEN6ADAgECAhAE/eOq2921q55B9NnVIXVO 396 | # MA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy 397 | # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD 398 | # ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjAwNzIwMDAw 399 | # MDAwWhcNMjMwNzI1MTIwMDAwWjCBizELMAkGA1UEBhMCR0IxEjAQBgNVBAcTCVdh 400 | # a2VmaWVsZDEmMCQGA1UEChMdU2VjdXJlIFBsYXRmb3JtIFNvbHV0aW9ucyBMdGQx 401 | # GDAWBgNVBAsTD1NjcmlwdGluZ0hlYXZlbjEmMCQGA1UEAxMdU2VjdXJlIFBsYXRm 402 | # b3JtIFNvbHV0aW9ucyBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 403 | # AQCvbSdd1oAAu9rTtdnKSlGWKPF8g+RNRAUDFCBdNbYbklzVhB8hiMh48LqhoP7d 404 | # lzZY3YmuxztuPlB7k2PhAccd/eOikvKDyNeXsSa3WaXLNSu3KChDVekEFee/vR29 405 | # mJuujp1eYrz8zfvDmkQCP/r34Bgzsg4XPYKtMitCO/CMQtI6Rnaj7P6Kp9rH1nVO 406 | # /zb7KD2IMedTFlaFqIReT0EVG/1ZizOpNdBMSG/x+ZQjZplfjyyjiYmE0a7tWnVM 407 | # Z4KKTUb3n1CTuwWHfK9G6CNjQghcFe4D4tFPTTKOSAx7xegN1oGgifnLdmtDtsJU 408 | # OOhOtyf9Kp8e+EQQyPVrV/TNAgMBAAGjggHFMIIBwTAfBgNVHSMEGDAWgBRaxLl7 409 | # KgqjpepxA8Bg+S32ZXUOWDAdBgNVHQ4EFgQUTXqi+WoiTm5fYlDLqiDQ4I+uyckw 410 | # DgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGA1UdHwRwMG4w 411 | # NaAzoDGGL2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3Mt 412 | # ZzEuY3JsMDWgM6Axhi9odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1 413 | # cmVkLWNzLWcxLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwDATAqMCgGCCsGAQUF 414 | # BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEEATCBhAYI 415 | # KwYBBQUHAQEEeDB2MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j 416 | # b20wTgYIKwYBBQUHMAKGQmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp 417 | # Q2VydFNIQTJBc3N1cmVkSURDb2RlU2lnbmluZ0NBLmNydDAMBgNVHRMBAf8EAjAA 418 | # MA0GCSqGSIb3DQEBCwUAA4IBAQBT3M71SlOQ8vwM2txshp/XDvfoKBYHkpFCyanW 419 | # aFdsYQJQIKk4LOVgUJJ6LAf0xPSN7dZpjFaoilQy8Ajyd0U9UOnlEX4gk2J+z5i4 420 | # sFxK/W2KU1j6R9rY5LbScWtsV+X1BtHihpzPywGGE5eth5Q5TixMdI9CN3eWnKGF 421 | # kY13cI69zZyyTnkkb+HaFHZ8r6binvOyzMr69+oRf0Bv/uBgyBKjrmGEUxJZy+00 422 | # 7fbmYDEclgnWT1cRROarzbxmZ8R7Iyor0WU3nKRgkxan+8rzDhzpZdtgIFdYvjeO 423 | # c/IpPi2mI6NY4jqDXwkx1TEIbjUdrCmEfjhAfMTU094L7VSNMYIEXDCCBFgCAQEw 424 | # gYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE 425 | # CxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1 426 | # cmVkIElEIENvZGUgU2lnbmluZyBDQQIQBP3jqtvdtaueQfTZ1SF1TjAJBgUrDgMC 427 | # GgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYK 428 | # KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG 429 | # 9w0BCQQxFgQU5ZXw7eYZG/RYZvBUoLM1CrAMHKMwDQYJKoZIhvcNAQEBBQAEggEA 430 | # o7tCC8LSJXak2eVke0Q9KbF3nVaqazeVlZzyKxGf06Gq3yGAw6ABMV3+Y7MhAy4V 431 | # JHyf6P4MaRYNjNcStYrnlayBGNeXk8lwIY3R/+KS9jvbVW4pxUn2FZUw0l3PsMxQ 432 | # qicTkfPL3Bch2m+spJxmNMO1D6Rc56Ocs6UhFolFaGZmrSCxWCBIt+ZcyPo29X+4 433 | # DKpS6iUCMFi5qpeoLo0fRoP1ZBrv/k2YvXpJ0lejF2w8qxRpwe5GOVzlzl7+3J01 434 | # 1nZMhJCe41G0Hy8XtnMSu0SIy2OKFYPThz3iJK9wkL/J9ijHj9JETN43yy3eqfBY 435 | # iiR9HwyMHqNNrD44UUJmDqGCAjAwggIsBgkqhkiG9w0BCQYxggIdMIICGQIBATCB 436 | # hjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL 437 | # ExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3Vy 438 | # ZWQgSUQgVGltZXN0YW1waW5nIENBAhANQkrgvjqI/2BAIc4UAPDdMA0GCWCGSAFl 439 | # AwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUx 440 | # DxcNMjIwMTA4MTYxMzM4WjAvBgkqhkiG9w0BCQQxIgQgJNZT9BwdwzlGc5hzeQj8 441 | # 8F7qg4/HSBDQrtrgCxFKdIgwDQYJKoZIhvcNAQEBBQAEggEAunloKUHWmaGM1TVI 442 | # SBQg5Mj0BeiPojGCjgm93CRlIRo/Ak4XAzYEvqK1U2xVVc6pW2xkGWrzHM3xptuc 443 | # CC0rStfBJhXohfLYwVTzhWR17FQVcvjBqeWtwaFAZL1cImniF///RSIXmwNgk7wV 444 | # kegT8HwBEVfwFaEWX+uFGOTN+7mEWROgNhISXLxTv2pA7qP3Tcbjzu6JmIYdSCI1 445 | # bcqYeOcioqAq7jMk8Sn883BhpMxuM8dVPRvluS68X0h7M3Xqb6SgHyCSPOZZGOGq 446 | # JapbWrUd1/o/2GRdqX8Xy6DMaAze6o7cWUXaN9ZgNI12vfS+q52OTH4TP2d2nToI 447 | # JA6b8A== 448 | # SIG # End signature block 449 | -------------------------------------------------------------------------------- /nsconmsg.txt: -------------------------------------------------------------------------------- 1 | NS version: NetScaler NS13.0: Build 83.27.nc, Date: Sep 27 2021, 07:06:36 (64-bit) 2 | usage: nsconmsg [-U] 3 | d : display performance data 4 | current - display the current performance data 5 | past - display the performance data from begining 6 | stats - display current statistics counters 7 | finalstats - display final newnslog statistics counters 8 | last - display last newnslog statistics counters 9 | memstats - display current memory statistics 10 | statswt0 - display current statistics counters, 11 | excluding counters with 0 value 12 | lastwt0 - display last newnslog statistics counters, 13 | excluding counters with 0 value 14 | finalstatswt0 - display final newnslog statistics counters, 15 | excluding counters with 0 value (same as '-d lastwt0') 16 | devcr - display device creation/removal information 17 | devlink - display device link information 18 | devname - display all device names 19 | symname - display all symbol names & decoding scheme 20 | devsymrel - display all device and symbol relationship 21 | event - display events 22 | nicdata - display NIC debug data 23 | eventwait - display events (real time wake) 24 | consmsg - display console message 25 | oldconmsg - display netscaler old console message 26 | (use -s options for different debug level) 27 | setime - display the start and end time of data file 28 | auditedcmd - display audited command 29 | logfromnfw - display seleced log messages from new logging framework 30 | copy - alias to copyr 31 | copyr - copy data from one file to another file with filter and sample rate change 32 | old2new - convert old performance data to new format 33 | new2old - convert new performance data to old format 34 | v20tov21 - convert v2.0 performance data to v2.1 format 35 | v21tov20 - convert v2.1 performance data to v2.0 format 36 | v20tov22 - convert v2.0 performance data to v2.2 format 37 | v20toGRP - convert v2.0 performance data to v3.0(Group Record) format 38 | maxvalue - display highest value and time 39 | minvalue - display lowest value and time 40 | maxrate - display highest rate value and time 41 | minrate - display lowest rate value and time 42 | distrconmsg - display distribution of counter 43 | (use -s options for different debug level) 44 | f : display only these symbols with full pattern match 45 | symname- is string, can have wild card '*' and '?' 46 | F : do not display these symbols with full pattern match 47 | symname- is string, can have wild card '*' and '?' 48 | g : display only these symbols with partial pattern match 49 | symname- is string, can have wild card '?' 50 | G : do not display these symbols with partial pattern match 51 | symname- is string, can have wild card '?' 52 | h : display help information 53 | i : display only these devices with full pattern match 54 | devname- is string, can have wild card '*' and '?' 55 | I : do not display these devices with full pattern match 56 | devname- is string, can have wild card '*' and '?' 57 | j : display only these devices with partial pattern match 58 | devname- is string, can have wild card '?' 59 | J : do not display these devices with partial pattern match 60 | devname- is string, can have wild card '?' 61 | O : overwrite output file (used before the k option) 62 | k : keep performance information in a file(STDOUT if name 'pipe' 63 | K : Display performance information from kept data file (STDIN if name 'pipe' 64 | B : Base file for delta compression 65 | s : set different debug parameters 66 | verbose - set/reset verbose mode 67 | nsdebug_pe - setup nsdebug_pe value for debugmsg 68 | nsdebug_pe_mask - setup nsdebug_pe_mask value for debugmsg 69 | nsppeid - PPE from which to collect the data 70 | nspartid - Partition ID to display the data 71 | ConDebug - setup ConDebug value for debugmsg 72 | ConLb - setup ConLb value for debugmsg 73 | ConMon - setup ConMon value for debugmsg 74 | ConMEM - setup ConMEM value for debugging memory details 75 | ConCSW - setup ConCSW value for debugmsg 76 | ConSSL - setup ConSSL value for debugmsg 77 | ConCMP - setup ConCMP value for debugmsg 78 | 1 : for new compression method 79 | 2 : for old compression method 80 | ConIC - setup ConIC value for debugmsg 81 | 1 : display basic cache stats 82 | disptime - display time information if needed 83 | allstats - display events and auditedcmd also 84 | only applicable for current, past, oldconmsg 85 | csv - display current/past in csv format 86 | deflate - 1 : deflate compression (no crc, length) 87 | compressbuf - setup compression input buffer size 88 | compressdebug - setup compress debug flags 89 | compressdepth - setup compression depth size for match 90 | compressmode - setup compress mode 'static', 'dynamic', 91 | 'beststatic','bestdynamic','block','full', 92 | 'bestcompression' 93 | compressfreq - display compression frequencies 94 | totalcount - display if total count is above or equal 95 | logsize - size of log file in BYTES 96 | deltacount - display if delta count is above or equal 97 | ratecount - display if rate count is above or equal 98 | totalcountlow - display if total count is below or equal 99 | deltacountlow - display if delta count is below or equal 100 | ratecountlow - display if rate count is below or equal 101 | syslog - set to redirect events into syslog messages 102 | syslogfacility - set syslog facility to 0 to 7 (LOCAL0 to LOCAL7) 103 | syslogtime - Log time information(actual time event happend)into in syslog 104 | time - set operation start time as ddmmmyyyy:hh:mm 105 | e.g. 07DEC2001:10:17 106 | pedist - display stats from multiple PEs simultaneously. 107 | It works for '-d stats', '-d current', '-d past, '-d minrate', '-d maxrate', '-d minvalue', -d 'maxvalue' 108 | e.g. nsconmsg -s pedist=1 -g netio_tot_called -d current 109 | timeintervalms - Set display time interval for operation in milliseconds 110 | grouprecord - Set/reset group record conversion option 111 | S : save the performance data 112 | current - save starting from current data 113 | past - save starting from begining 114 | compress - save compressed data (option -K & -k needed) 115 | 116 | dcompress - save decompressed data (option -K & -k needed) 117 | dmux - work on mux block which includes (option -K & -k needed) 118 | -K to define input file, -k output file 119 | delta - delta compress (files: -B base, -K update, -k delta) 120 | dedelta - dedelta (uncompress) (files: -B base, -K delta, -k update 121 | js_b2a - convert input to JS 7-8 ASCII (-K input, -k output 122 | js_a2b - convert JS 7-8 ASCII to binary (-K input, -k output 123 | t