Nerdio provides an Azure Runbook as a Scripted Action, which deletes profiles on a storage account if they are older than a defined number of days. The following parameters are required for execution:
- StorageAccountName
- ShareName
- DaysOld
- StorageKeySecureVar
I took a closer look and saw the potential for improvement.
Authorizations
With a Nerdio Manager for Enterprise, I usually also provide the storage accounts. The autoscaling of the storage account is also carried out by Nerdio as a premium feature in order to optimize size, performance and costs.
This gives the Nerdio service principal sufficient rights on the storage account to create a temporary shared access signature.
This means that we do not need to store a storage key as a variable and use it when executing the Scripted Action.
Scheduled task
Another option with Nerdio is to execute a Scripted Action as a scheduled task. Currently, there is still the restriction that a Scripted Action can only be executed with one configuration. This means that I can only clean up one storage account with the existing script. This restriction has been recognized and a solution is being worked on. Only several Scripted Actions can solve the problem.
I also wanted to avoid this limitation.
WhatIf
Before letting the Scripted Action delete data, I wanted to check what the script would delete. This was not possible with the existing variant.
And so I have also enabled this in the new variants.
New versions of the script
First, I developed a new version of the Scripted Action that does not require a Storage Access Key. I created another version to be able to operate several storage accounts. All information must be stored in a variable in order to be able to use these values. Both versions also make it possible to output only what is deleted and what is ignored without deleting data. I have now created two versions of the script, which I would like to share with you.
Extension with Shared Access Signature
In this version, I have taken advantage of the fact that I already have administrative access to the storage accounts with the Service Principal. However, it is still possible to work with a storage access key.
The script can be executed as follows:
Here is the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
#name: Delete Old FSLogix Profiles enhanced #description: Deletes FSlogix .vhd(x) files older than specified days and removes any empty directories in the specified Azure Files share. #tags: beckmann.ch, FSLogix <# Variables: { "ResourceGroupName": { "Description": "Name of the Resource Group.", "IsRequired": true }, "StorageAccountName": { "Description": "Name of the Azure Storage Account.", "IsRequired": true }, "ShareName": { "Description": "Name of the Azure Files share.", "IsRequired": true }, "DaysOld": { "Description": "Age of files to check for deletion.", "IsRequired": true }, "StorageKeySecureVar": { "Description": "Secure variable containing the storage account key. Make sure this secure variable is passed to this script. If not available, the Nerdio Service Principal is used.", "IsRequired": false }, "WhatIf": { "Description": "If set to true, no changes will be made", "Type": "bool", "IsRequired": true, "DefaultValue": true } } #> $ErrorActionPreference = 'Stop' If ($WhatIf -eq $false) { Write-Output "WhatIf is set to false, changes will be made" } ElseIf ($WhatIf -eq $true) { Write-Output "WhatIf is set to true, no changes will be made" } Else { Write-Output "WhatIf is not set to true or false, no changes will be made" Exit } function New-BesAzureFilesSASToken { param ( [string]$ResourceGroupName, [string]$StorageAccountName, [string]$FileShareName, [string]$Permissions = "rwdl", # Read, write, delete and list permissions [int]$TokenLifeTime = 60 # Token lifetime in minutes ) begin { $date = Get-Date $actDate = $date.ToUniversalTime() $expiringDate = $actDate.AddMinutes($TokenLifeTime ) $expiringDate = (Get-Date $expiringDate -Format 'yyyy-MM-ddTHH:mm:ssZ') } process { # Retrieve storage account key $storageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName -Name $StorageAccountName)[0].Value # Create storage context $storageContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $storageAccountKey # Create SAS token $sasToken = New-AzStorageShareSASToken -Context $storageContext -ShareName $FileShareName -Permission $Permissions -ExpiryTime $expiringDate } end { return $sasToken } } If ($StorageKeySecureVar) { # Create a new storage context using the storage account key $StorageContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageKeySecureVar Write-Output "Storage Account Connected" } Else { # Get the current Azure context $azContext = Get-AzContext # Write the current Azure context to the output Write-Output "Current Azure Subscription: $($azContext.Subscription.Name)" Write-Output "Current Azure Tenant: $($azContext.Tenant.Id)" Write-Output "Current Azure Account: $($azContext.Account.Id)" # Create a new SAS token for the storage account $sasToken = New-BesAzureFilesSASToken -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -FileShareName $ShareName Write-Output ("SAS Token: " + $sasToken.Substring(0, 70) + "...") # Create a new storage context using the SAS token $StorageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken Write-Output "Storage Account Connected" } $Dirs = $StorageContext | Get-AzStorageFile -ShareName "$ShareName" | Where-Object { $_.GetType().Name -eq "AzureStorageFileDirectory" } Write-Verbose "Directories in $ShareName" $Dirs | ForEach-Object { Write-Verbose $_.Name } # Get files from each directory, check if older than $DaysOld, delete it if it is foreach ($dir in $Dirs) { $Files = Get-AzStorageFile -ShareName "$ShareName" -Path $dir.Name -Context $StorageContext | Get-AzStorageFile foreach ($file in $Files) { # check if file is not .vhd, if so, skip and move to next iteration if ($file.Name -notmatch '\.vhd') { Write-Output "$($file.Name) is not a VHD file, skipping..." continue } # get lastmodified property using Get-AzStorageFile; if lastmodified is older than $DaysOld, delete the file $File = Get-AzStorageFile -ShareName "$ShareName" -Path $($dir.name + '/' + $file.Name) -Context $StorageContext $LastModified = $file.LastModified.DateTime $DaysSinceModified = (Get-Date) - $LastModified if ($DaysSinceModified.Days -gt $DaysOld) { Write-Output "$($file.Name) is older than $DaysOld days, deleting..." If ($WhatIf -eq $false) { $File | Remove-AzStorageFile } } else { Write-Output "$($file.Name) is not older than $DaysOld days, skipping..." } } # if directory is now empty, delete it $Files = Get-AzStorageFile -ShareName "$ShareName" -Path $dir.Name -Context $StorageContext | Get-AzStorageFile if ($Files.Count -eq 0) { Write-Output "$($dir.Name) is empty, deleting..." If ($WhatIf -eq $false) { Remove-AzStorageDirectory -Context $StorageContext -ShareName "$ShareName" -Path $dir.name } } } |
Extension with Secure Variable
In order to be able to clean up multiple storage accounts, I have created a secure variable in Nerdio, which stores the parameters in a JSON formatted string. When executing, the timeout must be increased depending on the number of storage accounts.
A variable is now required, this should also be restricted for the script:
This script can now be executed as follows (note the timeout):
Content of the variable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[ { "StorageAccountName": "storageaccountA", "ResourceGroupName": "rg-hostpoolA", "StorageAccountKey": "key", "SubscriptionId": "abcdefgh-ijkl-mnop-qrst-uvwxyz012345", "ShareName": "profiles", "DaysOld": "90", "WhatIf": "true" }, { "StorageAccountName": "storageaccountB", "ResourceGroupName": "rg-hostpoolB", "SubscriptionId": "abcdefgh-ijkl-mnop-qrst-uvwxyz012345", "ShareName": "profiles", "DaysOld": "90", "WhatIf": "true" } ] |
Now also the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
#name: Delete Old FSLogix Profiles on multiple Azure Files shares #description: Deletes FSlogix .vhd(x) files older than specified days and removes any empty directories on multiple Azure Files shares. #tags: beckmann.ch, FSLogix <# Variables: { "DeleteOldProfiles": { "Description": "JSON object with the following properties: SubscriptionId, ResourceGroupName, StorageAccountName, ShareName, DaysOld, WhatIf, and optional the StorageAccountKey. If StorageAccountKey is not specified, a SAS token will be generated.", "IsRequired": true } } #> $ErrorActionPreference = 'Stop' # Convert JSON string to PowerShell object $DeleteOldProfiles = $DeleteOldProfiles | ConvertFrom-Json function New-BesAzureFilesSASToken { param ( [string]$ResourceGroupName, [string]$StorageAccountName, [string]$FileShareName, [string]$Permissions = "rwdl", # Read, write, delete and list permissions [int]$TokenLifeTime = 60 # Token lifetime in minutes ) begin { $date = Get-Date $actDate = $date.ToUniversalTime() $expiringDate = $actDate.AddMinutes($TokenLifeTime ) $expiringDate = (Get-Date $expiringDate -Format 'yyyy-MM-ddTHH:mm:ssZ') } process { # Retrieve storage account key $storageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName -Name $StorageAccountName)[0].Value # Create storage context $storageContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $storageAccountKey # Create SAS token $sasToken = New-AzStorageShareSASToken -Context $storageContext -ShareName $FileShareName -Permission $Permissions -ExpiryTime $expiringDate } end { return $sasToken } } ForEach ($DeleteOldProfile in $DeleteOldProfiles) { $SubscriptionId = $DeleteOldProfile.SubscriptionId $ResourceGroupName = $DeleteOldProfile.ResourceGroupName $StorageAccountName = $DeleteOldProfile.StorageAccountName $StorageAccountKey = $DeleteOldProfile.StorageAccountKey $ShareName = $DeleteOldProfile.ShareName $DaysOld = $DeleteOldProfile.DaysOld $WhatIf = $DeleteOldProfile.WhatIf If ($WhatIf.ToLower() -eq "true") { $WhatIf = $true Write-Output "WhatIf is set to true, no changes will be made" } ElseIf ($WhatIf.ToLower() -eq "false") { Write-Output "WhatIf is set to false, changes will be made" $WhatIf = $false } Else { $WhatIf = $true Write-Output "WhatIf has no permitted value, the default value is true, no changes are made" } # Change the current Azure context to the specified subscription $azContext = Set-AzContext -SubscriptionId $SubscriptionId -Force # Write the current Azure context to the output Write-Output "Current Azure Subscription: $($azContext.Subscription.Name)" Write-Output "Current Azure Tenant: $($azContext.Tenant.Id)" Write-Output "Current Azure Account: $($azContext.Account.Id)" If ($StorageAccountKey) { # Create a new storage context using the storage account key $StorageContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey Write-Output "Storage Account $StorageAccountName Connected" } Else { # Create a new SAS token for the storage account $sasToken = New-BesAzureFilesSASToken -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -FileShareName $ShareName Write-Output ("SAS Token: " + $sasToken.Substring(0, 70) + "...") # Create a new storage context using the SAS token $StorageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken Write-Output "Storage Account $StorageAccountName Connected" } $Dirs = $StorageContext | Get-AzStorageFile -ShareName "$ShareName" | Where-Object { $_.GetType().Name -eq "AzureStorageFileDirectory" } Write-Verbose "Directories in $ShareName" $Dirs | ForEach-Object { Write-Verbose $_.Name } # Get files from each directory, check if older than $DaysOld, delete it if it is foreach ($dir in $Dirs) { $Files = Get-AzStorageFile -ShareName "$ShareName" -Path $dir.Name -Context $StorageContext | Get-AzStorageFile foreach ($file in $Files) { # check if file is not .vhd, if so, skip and move to next iteration if ($file.Name -notmatch '\.vhd') { Write-Output "$($file.Name) is not a VHD file, skipping..." continue } # get lastmodified property using Get-AzStorageFile; if lastmodified is older than $DaysOld, delete the file $File = Get-AzStorageFile -ShareName "$ShareName" -Path $($dir.name + '/' + $file.Name) -Context $StorageContext $LastModified = $file.LastModified.DateTime $DaysSinceModified = (Get-Date) - $LastModified if ($DaysSinceModified.Days -gt $DaysOld) { Write-Output "$($file.Name) is older than $DaysOld days, deleting..." If ($WhatIf -eq $false) { $File | Remove-AzStorageFile } } else { Write-Output "$($file.Name) is not older than $DaysOld days, skipping..." } } # if directory is now empty, delete it $Files = Get-AzStorageFile -ShareName "$ShareName" -Path $dir.Name -Context $StorageContext | Get-AzStorageFile if ($Files.Count -eq 0) { Write-Output "$($dir.Name) is empty, deleting..." If ($WhatIf -eq $false) { Remove-AzStorageDirectory -Context $StorageContext -ShareName "$ShareName" -Path $dir.name } } } } |
Conclusion
Both variants can be set up as scheduled tasks, the parameters are identical, only the corresponding time zone and time must be specified.
I hope one of the two scripts can help you to clean up your old profiles. We have the extended version with the Secure Variable in use at a large customer with over 20 storage accounts. If you like it, I look forward to comments and sharing in the community.