Nerdio stellt ein Azure Runbook als Scripted Action bereit, welches Profile auf einem Storage Account löscht, wenn dies älter als eine zu definierende Anzahl Tage alt ist. Für das Ausführen werden folgende Parameter benötigt:
- StorageAccountName
- ShareName
- DaysOld
- StorageKeySecureVar
Ich habe mir das genauer angesehen und das Verbesserungspotential gesehen.
Berechtigungen
Mit einer Nerdio Manager for Enterprise stelle ich in der Regel auch die Storage Accounts bereit. Auch das Autoscaling des Storage Accounts wird von Nerdio als Premium Feature durchgeführt, um die Grösse, Performance und Kosten zu optimieren.
Dadurch hat das Service Principal von Nerdio genügend Rechte auf dem Storage Account, um eine temporäre Shared Access Signature zu erstellen.
Damit sind nun die Vorrausetzungen geschaffen, dass wir keinen Storage Key als Variable hinterlegen, und beim Ausführen verwenden müssen.
Geplanter Task
Eine weiter Möglichkeit mit Nerdio ist es, eine Scripted Action als Geplanter Task ausführen zu lassen. Aktuell gibt es noch die Einschränkung, dass eine Scripted Action nur mit einer Konfiguration ausgeführt werden kann. Dies bedeutet, dass ich mit dem bestehenden Script nur einen Storage Account bereinigen kann. Diese Einschränkung ist erkannt, und es wird an der Lösung gearbeitet. Nur mehrere Kopien der Scripted Actions können das Problem lösen.
Auch diese Limitierung wollte ich umgehen.
WhatIf
Bevor ich die Scripted Action Daten löschen lasse, wollte ich erst prüfen, was denn das Script löschen würde. Dies war mit der bestehenden Variante nicht möglich.
Und so habe ich in den neuen Varianten dies ebenfalls ermöglicht.
Neue Versionen des Scripts
Als erstes habe ich eine neue Version der Scripted Action entwickelt, dass ohne Storage Access Key auskommt. Eine weitere Version habe ich erstellt, um mehrere Storages Accounts bedienen zu können. Alle Informationen müssen dazu in einer Variable gespeichert werden, um dann diese Werte verwenden zu können. Beide Versionen ermöglichen ebenfalls erst nur auszugeben was gelöscht, und was ignoriert wird, ohne Daten zu löschen. Ich habe nun zwei Versionen des Scripts erstellt, welches ich nun gerne mit euch teilen würde.
Erweiterung um Shared Access Signature
In dieser Version habe ich es mir zunutze gemacht, dass ich mit dem Service Principal schon administrativen Zugang zu den Storage Accounts habe. Es ist aber weiterhin möglich mit einem Storage Access Key zu arbeiten.
Das Script kann wie folgt ausgeführt werden:
Hier nun das Script dazu:
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 } } } |
Erweiterung um Secure Variable
Um nun aber mehrere Storage Accounts bereinigen zu können, habe ich eine Secure Variable in Nerdio erstellt, welche die Parameter in einem JSON formatierten String gespeichert sind. Beim Ausführen muss je nach Anzahl Storage Accounts das Timeout erhöht werden.
Es wird nun eine Variable benötigt, diese sollte auch nur für das Script eingeschränkt werden:
Inhalt der 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" } ] |
Dieses Script kann nun wie folgt ausgeführt werden (beachte dabei das Timeout):
Nun auch noch das 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 } } } } |
Abschluss
Beide Varianten können als geplante Aufgabe eingerichtet werden, die Parameter sind identisch, lediglich die entsprechende Zeitzone und Uhrzeit muss angegeben werden.
Ich hoffe das eine der beiden Script kann dir helfen, deine alten Profile zu bereinigen. Wir haben das die Erweitere Version mit der Secure Variable bei einem grossen Kunden in Einsatz, mit über 20 Storage Accounts. Wenn es gefällt freue ich mich über Kommentare und ein Teilen in der Community.