<# .DESCRIPTION This scipt highlights the missing backups after analyzing all backup policies schedules and verifies backup catalog with available list of backups. Steps to execute the script: ---------------------------- 1. Open powershell, create a new folder & change directory to the folder. mkdir C:\scripts\StorSimpleSDKTools cd C:\scripts\StorSimpleSDKTools 2. Download nuget CLI under the same folder in Step1. Various versions of nuget.exe are available on nuget.org/downloads. Each download link points directly to an .exe file, so be sure to right-click and save the file to your computer rather than running it from the browser. wget https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -Out C:\scripts\StorSimpleSDKTools\nuget.exe 3. Download the dependent SDK C:\scripts\StorSimpleSDKTools\nuget.exe install Microsoft.Azure.Management.Storsimple8000series C:\scripts\StorSimpleSDKTools\nuget.exe install Microsoft.IdentityModel.Clients.ActiveDirectory -Version 2.28.3 C:\scripts\StorSimpleSDKTools\nuget.exe install Microsoft.Rest.ClientRuntime.Azure.Authentication -Version 2.2.9-preview 4. Download the script from script center. wget https://github.com/anoobbacker/storsimpledevicemgmttools/raw/master/Verify-BackupScheduleAndBackup.ps1 -Out Verify-BackupScheduleAndBackup.ps1 .\Verify-BackupScheduleAndBackup.ps1 -SubscriptionId [subid] -TenantId [tenantid] -ResourceGroupName [resource group] -ManagerName [device manager] -DeviceName [device name] -AuthNType [Type of auth] -AADAppId [AAD app Id] -AADAppAuthNKey [AAD App Auth Key] ---------------------------- .PARAMS SubscriptionId: Input the Subscription ID where the StorSimple 8000 series device manager is deployed. TenantId: Input the Tenant ID of the subscription. Get Tenant ID using Get-AzureRmSubscription cmdlet or go to the documentation https://aka.ms/ss8000-script-tenantid. DeviceName: Input the name of the StorSimple device on which to create/update the volume. ResourceGroupName: Input the name of the resource group on which to create/update the volume. ManagerName: Input the name of the resource (StorSimple device manager) on which to create/update the volume. AuthNType: Input if you want to go with username, AAD authentication key or certificate. Refer https://aka.ms/ss8000-script-sp. Possible values: [UserNamePassword, AuthenticationKey, Certificate] AADAppId: Input application ID for which the service principal was set. Refer https://aka.ms/ss8000-script-sp. AADAppAuthNKey: Input application authentication key for which the AAD application. Refer https://aka.ms/ss8000-script-sp. AADAppAuthNCertPath: Input the service principal certificate for the AAD application. Refer https://aka.ms/ss8000-script-spcert. AADAppAuthNCertPassword: Input the service principal ceritifcate password for the AAD application. Refer https://aka.ms/ss8000-script-spcert. #> Param ( [parameter(Mandatory = $true, HelpMessage = "Input the Subscription ID where the StorSimple 8000 series device manager is deployed.")] [String] $SubscriptionId, [parameter(Mandatory = $true, HelpMessage = "Input the ID of the tenant of the subscription. Get using Get-AzureRmSubscription cmdlet.")] [String] $TenantId, [parameter(Mandatory = $true, HelpMessage = "Input the name of the resource group on which to read backup schedules and backup catalogs.")] [String] $ResourceGroupName, [parameter(Mandatory = $true, HelpMessage = "Input the name of the resource (StorSimple device manager) on which to read backup schedules and backup catalogs.")] [String] $ManagerName, [parameter(Mandatory = $true, HelpMessage = "Input the name of the StorSimple device on which to read backup schedules and backup catalogs.")] [String] $DeviceName, [parameter(Mandatory = $false, HelpMessage = "Input if you want to go with username, AAD authentication key or certificate. Refer https://aka.ms/ss8000-script-sp.")] [ValidateSet('UserNamePassword', 'AuthenticationKey', 'Certificate')] [String] $AuthNType = 'UserNamePassword', [parameter(Mandatory = $false, HelpMessage = "Input application ID for which the service principal was set. Refer https://aka.ms/ss8000-script-sp.")] [String] $AADAppId, [parameter(Mandatory = $false, HelpMessage = "Input application authentication key for which the AAD application. Refer https://aka.ms/ss8000-script-sp.")] [String] $AADAppAuthNKey, [parameter(Mandatory = $false, HelpMessage = "Input the service principal certificate for the AAD application.")] [String] $AADAppAuthNCertPath, [parameter(Mandatory = $false, HelpMessage = "Input the service principal ceritifcate password for the AAD application.")] [String] $AADAppAuthNCertPassword ) # Set Current directory path $ScriptDirectory = (Get-Location).Path # Set dll path $ActiveDirectoryPath = Join-Path $ScriptDirectory "Microsoft.IdentityModel.Clients.ActiveDirectory.2.28.3\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" $ClientRuntimeAzurePath = Join-Path $ScriptDirectory "Microsoft.Rest.ClientRuntime.Azure.3.3.7\lib\net452\Microsoft.Rest.ClientRuntime.Azure.dll" $ClientRuntimePath = Join-Path $ScriptDirectory "Microsoft.Rest.ClientRuntime.2.3.8\lib\net452\Microsoft.Rest.ClientRuntime.dll" $NewtonsoftJsonPath = Join-Path $ScriptDirectory "Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll" $AzureAuthenticationPath = Join-Path $ScriptDirectory "Microsoft.Rest.ClientRuntime.Azure.Authentication.2.2.9-preview\lib\net45\Microsoft.Rest.ClientRuntime.Azure.Authentication.dll" $StorSimple8000SeresePath = Join-Path $ScriptDirectory "Microsoft.Azure.Management.Storsimple8000series.1.0.0\lib\net452\Microsoft.Azure.Management.Storsimple8000series.dll" # Load all required assemblies [System.Reflection.Assembly]::LoadFrom($ActiveDirectoryPath) | Out-Null [System.Reflection.Assembly]::LoadFrom($ClientRuntimeAzurePath) | Out-Null [System.Reflection.Assembly]::LoadFrom($ClientRuntimePath) | Out-Null [System.Reflection.Assembly]::LoadFrom($NewtonsoftJsonPath) | Out-Null [System.Reflection.Assembly]::LoadFrom($AzureAuthenticationPath) | Out-Null [System.Reflection.Assembly]::LoadFrom($StorSimple8000SeresePath) | Out-Null # Print method Function PrettyWriter($Content, $Color = "Yellow") { Write-Host $Content -Foregroundcolor $Color } # Define constant variables (DO NOT CHANGE BELOW VALUES) $FrontdoorUrl = "urn:ietf:wg:oauth:2.0:oob" $TokenUrl = "https://management.azure.com" # Run 'Get-AzureRmEnvironment | Select-Object Name, ResourceManagerUrl' cmdlet to get the Fairfax url. $DomainId = "1950a258-227b-4e31-a9cf-717495945fc2" $FrontdoorUri = New-Object System.Uri -ArgumentList $FrontdoorUrl $TokenUri = New-Object System.Uri -ArgumentList $TokenUrl # Set Synchronization context $SyncContext = New-Object System.Threading.SynchronizationContext [System.Threading.SynchronizationContext]::SetSynchronizationContext($SyncContext) # Verify Credentials if ("UserNamePassword".Equals($AuthNType)) { # Username password $AADClient = [Microsoft.Rest.Azure.Authentication.ActiveDirectoryClientSettings]::UsePromptOnly($DomainId, $FrontdoorUri) $Credentials = [Microsoft.Rest.Azure.Authentication.UserTokenProvider]::LoginWithPromptAsync($TenantId, $AADClient).GetAwaiter().GetResult() } elseif ("AuthenticationKey".Equals($AuthNType) ) { # AAD Application authentication key if ( [string]::IsNullOrEmpty($AADAppId) -or [string]::IsNullOrEmpty($AADAppAuthNKey) ) { throw "Invalid inputs! Ensure that you input the arguments -AADAppId and -AADAppAuthNKey." } $Credentials =[Microsoft.Rest.Azure.Authentication.ApplicationTokenProvider]::LoginSilentAsync($TenantId, $AADAppId, $AADAppAuthNKey).GetAwaiter().GetResult(); } elseif ("Certificate".Equals($AuthNType) ) { # AAD Service Principal Certificates if ( [string]::IsNullOrEmpty($AADAppId) -or [string]::IsNullOrEmpty($AADAppAuthNCertPassword) -or [string]::IsNullOrEmpty($AADAppAuthNCertPath) ) { throw "Invalid inputs! Ensure that you input the arguments -AADAppId, -AADAppAuthNCertPath and -AADAppAuthNCertPassword." } if ( !(Test-Path $AADAppAuthNCertPath) ) { throw "Certificate file $AADAppAuthNCertPath couldn't found!" } $CertPassword = ConvertTo-SecureString $AADAppAuthNCertPassword -AsPlainText -Force $ClientCertificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($AADAppAuthNCertPath, $CertPassword) $ClientAssertionCertificate = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate -ArgumentList $AADAppId, $ClientCertificate $Credentials = [Microsoft.Rest.Azure.Authentication.ApplicationTokenProvider]::LoginSilentWithCertificateAsync($TenantId, $ClientAssertionCertificate).GetAwaiter().GetResult() } if ($Credentials -eq $null) { throw "Failed to authenticate!" } # Get StorSimpleClient instance $StorSimpleClient = New-Object Microsoft.Azure.Management.StorSimple8000Series.StorSimple8000SeriesManagementClient -ArgumentList $TokenUri, $Credentials # Set SubscriptionId $StorSimpleClient.SubscriptionId = $SubscriptionId # Get all backup policies by Device try { $policies = [Microsoft.Azure.Management.StorSimple8000Series.BackupPoliciesOperationsExtensions]::ListByDevice($StorSimpleClient.BackupPolicies, $DeviceName, $ResourceGroupName, $ManagerName) } catch { # Print error details Write-Error $_.Exception.Message break } # Filter enabled scheduled backup policies $BackupPolicies = $policies | Where-Object { $_.ScheduledBackupStatus -eq 'Enabled'} if ($policies -eq $null -or $policies.Count -eq 0) { Write-Error "No backup policy is configured." break }elseIf ($BackupPolicies -eq $null) { Write-Error "Either all backup schedules are disabled or no backup schedule is configured." break } $Schedules = @() $ExpectedBackups = @() $WeeklyBufferTimeInMinutes = 1440 $DailyBufferTimeInMinutes = 1440 $HourlyBufferTimeInMinutes = 120 $MinutesBufferTimeInMinutes = 60 # Populate all expected backups set by current backup schedules try { $BackupPolicies | ForEach-Object { # Policy Info $BackupPolicyName = $_.Name $BackupPolicyId = $_.Id $RetentionCount = $_.RetentionCount # Get all schedules in current Backup Policy $sch = [Microsoft.Azure.Management.StorSimple8000Series.BackupSchedulesOperationsExtensions]::ListByBackupPolicy($StorSimpleClient.BackupSchedules, $DeviceName, $BackupPolicyName, $ResourceGroupName, $ManagerName) # Filter disabled schedules & no last successful run (backups) $Schedules += $sch | Where-Object { $_.ScheduleStatus -eq 'Enabled' -and $_.LastSuccessfulRun -ne $null } $Schedules | ForEach-Object { $StartTime = $_.StartTime $RetentionCount = $_.RetentionCount $ExpectedScheduleTime = [datetime]$_.LastSuccessfulRun # Set Minutes/Seconds to Zero if ($ExpectedScheduleTime.Minute -gt 0) { $ExpectedScheduleTime = $ExpectedScheduleTime.AddMinutes(-$ExpectedScheduleTime.Minute) } if ($ExpectedScheduleTime.Second -gt 0) { $ExpectedScheduleTime = $ExpectedScheduleTime.AddSeconds(-$ExpectedScheduleTime.Second) } $_.ScheduleRecurrence | ForEach-Object { $RecurrenceType = $_.RecurrenceType $RecurrenceValue = $_.RecurrenceValue $WeeklyDaysList = $_.WeeklyDaysList $Index = 1 while ($Index -le $RetentionCount -and $ExpectedScheduleTime -ge $StartTime) { $ExpectedBackupObj = New-Object psobject -Property @{ Id = [guid]::NewGuid() BackupPolicyName = $BackupPolicyName BackupPolicyId = $BackupPolicyId RecurrenceType = $RecurrenceType ScheduleTime = $ExpectedScheduleTime BufferTimeInMinutes = $null BackupName = $null ActualBackupTime = $null SearchOrder = $null } # Read previous schedule(s) if ($_.RecurrenceType -eq "Weekly") { $WeekDayFound = $false # Find previous backup by verifying WeekDaysList do { $ExpectedScheduleTime = $ExpectedScheduleTime.AddDays(-1) $WeekDayFound = $WeeklyDaysList -contains $ExpectedScheduleTime.DayOfWeek } while (!$WeekDayFound) # Set Buffer time & search order $ExpectedBackupObj.BufferTimeInMinutes = $WeeklyBufferTimeInMinutes $ExpectedBackupObj.SearchOrder = 4 } elseIf ($_.RecurrenceType -eq "Daily") { # Set previous schedule time by RecurrenceValue $ExpectedScheduleTime = $ExpectedScheduleTime.AddDays(-$RecurrenceValue) # Set Buffer time & search order $ExpectedBackupObj.BufferTimeInMinutes = $DailyBufferTimeInMinutes $ExpectedBackupObj.SearchOrder = 3 } elseIf ($_.RecurrenceType -eq "Hourly") { # Set previous schedule time by RecurrenceValue $ExpectedScheduleTime = $ExpectedScheduleTime.AddHours(-$RecurrenceValue) # Set Buffer time & search order $ExpectedBackupObj.BufferTimeInMinutes = $HourlyBufferTimeInMinutes $ExpectedBackupObj.SearchOrder = 2 } elseIf($_.RecurrenceType -eq "Minutes") { # Set previous schedule time by RecurrenceValue $ExpectedScheduleTime = $ExpectedScheduleTime.AddMinutes(-$RecurrenceValue) # Set Buffer time & search order $ExpectedBackupObj.BufferTimeInMinutes = $MinutesBufferTimeInMinutes $ExpectedBackupObj.SearchOrder = 1 } # Add expected schedule info $ExpectedBackups += $ExpectedBackupObj $Index++ } } } } } catch { # Print error details Write-Error $_.Exception.Message break } if ($ExpectedBackups -eq $null -or $ExpectedBackups.Length -eq 0) { Write-Error "No successfully backup(s) available for scheduled backup policy." break } # Get all currently available backup catalogs try { $ActualBackups = [Microsoft.Azure.Management.StorSimple8000Series.BackupsOperationsExtensions]::ListByDevice($StorSimpleClient.Backups, $DeviceName, $ResourceGroupName, $ManagerName) $SnapshotType = 'BySchedule' $MinimumDate = ($ExpectedBackups | sort ScheduleTime)[0].ScheduleTime $MaximumDate = ($ExpectedBackups | sort ScheduleTime -Descending)[0].ScheduleTime # Filter data by BackupJobCreationType and date range $ActualBackups = $ActualBackups | Where-Object { $_.BackupJobCreationType -eq $SnapshotType -and $_.CreatedOn -gt $MinimumDate -and $_.CreatedOn -lt $MaximumDate} # Add IsTagged member $ActualBackups | ForEach-Object { $_ | Add-Member –MemberType NoteProperty –Name IsTagged –Value $false } } catch { # Print error details Write-Error $_.Exception.Message break } # Compare Expected & Actual backups and Tag matched objects for ($LoopIndex=0; $LoopIndex -lt $ExpectedBackups.Length; $LoopIndex++) { $ExpectedBackupObj = $ExpectedBackups[$LoopIndex] $Backup = ($ActualBackups | Where-Object {$_.IsTagged -eq $false -and $_.CreatedOn -gt $ExpectedBackupObj.ScheduleTime -and $_.CreatedOn -lt $ExpectedBackupObj.ScheduleTime.AddMinutes($ExpectedBackupObj.BufferTimeInMinutes)} | sort CreatedOn) if ($Backup -ne $null -and $Backup.Length -gt 0) { # Set IsTagged property ($ActualBackups | Where-Object {$_.IsTagged -eq $false -and $_.CreatedOn -gt $ExpectedBackupObj.ScheduleTime -and $_.CreatedOn -lt $ExpectedBackupObj.ScheduleTime.AddMinutes($ExpectedBackupObj.BufferTimeInMinutes)} | sort CreatedOn)[0].IsTagged = $true # Set Actual BackupName & CreatedOn ($ExpectedBackups | where Id -eq $ExpectedBackupObj.Id)| ForEach-Object { $_.BackupName = $Backup[0].Name; $_.ActualBackupTime = $Backup[0].CreatedOn } } } # Display summery info $AvailableBacksupsCount = [int]([object[]]($ExpectedBackups | where BackupName -ne $null)).Length $MissedBacksupsCount = [int]([object[]]($ExpectedBackups | where BackupName -eq $null)).Length $AvailableBacksupsCount = @{$true=0;$false=$AvailableBacksupsCount}[$AvailableBacksupsCount -eq 0] $MissedBacksupsCount = @{$true=0;$false=$MissedBacksupsCount}[$MissedBacksupsCount -eq 0] if ($MissedBacksupsCount -gt 0) { PrettyWriter "`nMissed backups details by RecurrenceType" ($ExpectedBackups | where BackupName -eq $null | Sort RecurrenceType | Format-Table BackupPolicyName,RecurrenceType,ScheduleTime -GroupBy RecurrenceType) } ## Uncomment during dubeg time #PrettyWriter "`nExpected backups:" #($ExpectedBackups | Sort-Object ScheduleTime | Format-Table BackupPolicyName, RecurrenceType, ScheduleTime, ActualBackupTime, BackupName -GroupBy {ScheduleTime.ToString("yyyy-MM-dd")}) ## Uncomment during dubeg time #PrettyWriter "`nActual backups:" #$ActualBackups | group IsTagged -NoElement #$ActualBackups | sort CreatedOn | Format-Table Name,CreatedOn,IsTagged #-GroupBy CreatedOn PrettyWriter "`nSummary info:" Write-Output "Total expected backups: $($ExpectedBackups.Length)" Write-Output "Total available backups: $($AvailableBacksupsCount)" Write-Output "Total missed backups: $($MissedBacksupsCount)`n" $BackupsCount = ([object[]]$ActualBackups).Count $MaxBackupsCount = 100 if ($ActualBackups.NextPageLink -or $BackupsCount -eq $MaxBackupsCount) { PrettyWriter "`n`n Note:" Write-Output "Compared only latest $($MaxBackupsCount) actual backups. `nRequire to read all backups to accomplish the verification." }