Last active
May 1, 2025 18:47
-
-
Save blakedrumm/ac877428cf10f887e8262ada0efc94eb to your computer and use it in GitHub Desktop.
This PowerShell script is designed to migrate runbooks, variables, schedules, modules, certificates, connections, credentials, webhooks, private endpoints, and their tags across tenants. All PS 5 and PS 7 runbooks will be imported as PS 5 runtime; you can change it via the runtime environment later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| This PowerShell script migrates runbooks, variables, schedules, modules, certificates, connections, credentials, webhooks, private endpoints, and their tags across tenants. | |
| .DESCRIPTION | |
| This PowerShell script is designed to migrate runbooks, variables, schedules, modules, certificates, connections, credentials, webhooks, private endpoints, and their tags across tenants. All PS 5 and PS 7 runbooks will be imported as PS 5 runtime; you can change it via the runtime environment later. | |
| .PARAMETER oldSubscriptionId | |
| Required. Source subscription of the Azure Automation account. | |
| .PARAMETER newSubscriptionId | |
| Required. Target subscription of the Azure Automation account. | |
| .PARAMETER oldRGName | |
| Required. The name of the source resource group of the Azure Automation account. | |
| .PARAMETER newRGName | |
| Required. The name of the target resource group of the Azure Automation account. | |
| .PARAMETER oldAAName | |
| Required. The name of the Azure Automation account under the source subscription. | |
| .PARAMETER newAAName | |
| Required. The name of the Azure Automation account under the target subscription. | |
| .PARAMETER newAALocation | |
| Required. The location for the new Azure Automation account if it needs to be created. (eastus, eastus2, westus, westus2, centralus, northcentralus, southcentralus, westeurope, northeurope, southeastasia, eastasia, australiaeast, australiasoutheast, japaneast, japanwest) | |
| .PARAMETER TempFolder | |
| Required. The temporary folder to store exported runbooks. | |
| .PARAMETER oldTenantId | |
| Required. The tenant ID of the source subscription. | |
| .PARAMETER newTenantId | |
| Required. The tenant ID of the target subscription. | |
| .PARAMETER OnlyEnabledSchedules | |
| Optional. A switch parameter that, if specified, will only migrate enabled schedules. | |
| .NOTES | |
| Author: Blake Drumm (blakedrumm@microsoft.com) | |
| Original Author: Nina Li | |
| Last Edited: August 8th, 2024 | |
| EXAMPLE: | |
| .\Migrate-AutomationAccounts.ps1 -oldSubscriptionId <source sub id> -newSubscriptionId <target sub id> -oldRGName <source resource group name> -oldAAName <source AA name> -newRGName <target resource group name> -newAAName <target AA name> -newAALocation <target location> -TempFolder <temp folder path> -oldTenantId <source tenant id> -newTenantId <target tenant id> -OnlyEnabledSchedules | |
| .\Migrate-AutomationAccounts.ps1 -oldSubscriptionId "c9f2b7a1-4821-4f8d-a9e4-6272b0a4f8e6" -newSubscriptionId "f0a1d2c3-95b4-46e9-bf37-7a1d2c3e4b5f" -oldRGName "Old-ResourceGroup" -oldAAName "OldAutomationAccount" -newRGName "New-ResourceGroup" -newAAName "NewAutomationAccount" -newAALocation "eastus" -TempFolder "C:\Temp\AutomationMigration" -oldTenantId "8b7d3a4e-5f6a-4b8c-b9d0-12345678abcd" -newTenantId "1f2e3d4c-5678-90ab-cdef-112233445566" -OnlyEnabledSchedules | |
| -------------------------------------------------------------------------------- | |
| Copyright (c) Microsoft Corporation. MIT License | |
| 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, 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: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| 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. 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. | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [string]$oldSubscriptionId, | |
| [string]$newSubscriptionId, | |
| [string]$oldRGName, | |
| [string]$newRGName, | |
| [string]$oldAAName, | |
| [string]$newAAName, | |
| [string]$newAALocation, | |
| [string]$TempFolder, | |
| [string]$oldTenantId, | |
| [string]$newTenantId, | |
| [switch]$OnlyEnabledSchedules | |
| ) | |
| $ErrorActionPreference = "Stop" | |
| $script:varList = @() | |
| $script:schedules = @() | |
| $script:moduleList = @() | |
| $script:certificateList = @() | |
| $script:connectionList = @() | |
| $script:credentialList = @() | |
| $script:webhookList = @() | |
| $script:privateEndpointList = @() | |
| function Set-ExecutionPolicyIfNeeded | |
| { | |
| try | |
| { | |
| $execPolicy = Get-ExecutionPolicy -Scope Process | |
| if ($execPolicy -ne 'Unrestricted' -and $execPolicy -ne 'Bypass') | |
| { | |
| Write-Host "Current execution policy is $execPolicy, setting to Unrestricted temporarily." | |
| Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted -Force | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to set execution policy: $_" | |
| exit 1 | |
| } | |
| } | |
| function Login-AzAccountWithSubscription | |
| { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$SubscriptionId, | |
| [Parameter(Mandatory = $true)] | |
| [ValidateSet("old", "new")] | |
| [string]$SubscriptionType, | |
| [Parameter(Mandatory = $true)] | |
| [string]$TenantId | |
| ) | |
| try | |
| { | |
| Write-Verbose -Verbose "Logging into Azure with $SubscriptionType subscription ID $SubscriptionId..." | |
| $null = Connect-AzAccount -SubscriptionId $SubscriptionId -TenantId $TenantId -ErrorAction Stop | |
| $null = Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop | |
| Write-Verbose -Verbose "Set Az Context to $SubscriptionId..." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to login to Azure with subscription ID $SubscriptionId`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Export-Runbooks | |
| { | |
| Write-Verbose "Starting export of runbooks from $oldAAName..." | |
| try | |
| { | |
| Write-Verbose "Retrieving runbooks from $oldAAName in resource group $oldRGName..." | |
| $runbooks = Get-AzAutomationRunbook -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| Write-Verbose "Successfully retrieved runbooks." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to get runbooks from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| $runbookList = @() | |
| foreach ($runbook in $runbooks) | |
| { | |
| $runbookName = $runbook.Name | |
| $runbookType = $runbook.RunbookType | |
| $runbookTags = $runbook.Tags | |
| $runbookState = $runbook.State | |
| Write-Verbose "Processing runbook: $runbookName" | |
| Write-Verbose "Runbook Type: $runbookType" | |
| Write-Verbose "Runbook Tags: $runbookTags" | |
| switch ($runbookType) | |
| { | |
| "Graph" { $runbookExtension = ".graphrunbook" } | |
| "GraphPowerShell" { $runbookExtension = ".graphrunbook" } | |
| "GraphPowerShellWorkflow" { $runbookExtension = ".graphrunbook" } | |
| "PowerShell" { $runbookExtension = ".ps1" } | |
| "PowerShell72" { $runbookExtension = ".ps1" } | |
| "Python2" { $runbookExtension = ".py" } | |
| "Python3" { $runbookExtension = ".py" } | |
| "PowerShellWorkflow" { $runbookExtension = ".ps1" } | |
| default { $runbookExtension = ".ps1" } # Default to PowerShell script | |
| } | |
| Write-Verbose "Runbook extension: $runbookExtension" | |
| $runbookPath = Join-Path -Path $TempFolder -ChildPath "$runbookName$runbookExtension" | |
| Write-Verbose "Runbook will be exported to: $runbookPath" | |
| try | |
| { | |
| Write-Verbose "Exporting runbook $runbookName..." | |
| $null = Export-AzAutomationRunbook -ResourceGroupName $oldRGName -AutomationAccountName $oldAAName -Name $runbookName -OutputFolder $TempFolder -Force -ErrorAction Stop | |
| Write-Verbose "Successfully exported runbook $runbookName." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export runbook $runbookName`: $_" | |
| continue | |
| } | |
| if ($runbookExtension -eq ".py") | |
| { | |
| try | |
| { | |
| Write-Verbose "Renaming Python file to .py for $runbookName..." | |
| Move-Item -Path "$TempFolder\$runbookName.ps1" -Destination "$TempFolder\$runbookName.py" -Force | |
| Write-Verbose "Successfully exported runbook $runbookName." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to rename runbook script $runbookName`: $_" | |
| continue | |
| } | |
| } | |
| try | |
| { | |
| Write-Verbose "Reading content of runbook $runbookName from $runbookPath..." | |
| $runbookContent = Get-Content -Path $runbookPath -Raw | |
| Write-Verbose "Successfully read content of runbook $runbookName." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to read content of runbook $runbookName from $runbookPath`: $_" | |
| continue | |
| } | |
| $runbookList += [PSCustomObject]@{ | |
| Name = $runbookName | |
| State = $runbookState | |
| Type = $runbookType | |
| Content = $runbookContent | |
| Tags = $runbookTags | |
| Path = $runbookPath | |
| } | |
| } | |
| Write-Verbose "Completed export of runbooks from $oldAAName." | |
| return $runbookList | |
| } | |
| function Import-Runbooks | |
| { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [array]$Runbooks | |
| ) | |
| Write-Verbose "Starting import of runbooks to $newAAName..." | |
| foreach ($runbook in $Runbooks) | |
| { | |
| $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($runbook.Name) | |
| $runbookState = $runbook.State | |
| $runbookType = $runbook.Type | |
| $runbookContent = $runbook.Content | |
| $runbookTags = $runbook.Tags | |
| $runbookPath = $runbook.Path | |
| Write-Verbose "Processing runbook: $runbookName" | |
| Write-Verbose "Runbook state: $runbookState" | |
| Write-Verbose "Runbook Type: $runbookType" | |
| Write-Verbose "Runbook Path: $runbookPath" | |
| Write-Verbose "Runbook Tags: $runbookTags" | |
| if (-not $runbookType) | |
| { | |
| Write-Error "Runbook $runbookName has a null or empty Type property. Skipping import." | |
| continue | |
| } | |
| switch ($runbookType) | |
| { | |
| "GraphPowerShell" { $runbookType = 'GraphicalPowerShell' } | |
| "GraphPowerShellWorkflow" { $runbookType = 'GraphicalPowerShellWorkflow' } | |
| } | |
| try | |
| { | |
| Write-Verbose "Checking if runbook $runbookName already exists in $newAAName..." | |
| $existingRunbook = Get-AzAutomationRunbook -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $runbookName -ErrorAction SilentlyContinue | |
| if ($existingRunbook) | |
| { | |
| # Append old Automation Account name to the runbook name to avoid conflicts | |
| $runbookName = "${runbookName}_$oldAAName" | |
| Write-Verbose "Runbook with the same name exists. New runbook name: $runbookName" | |
| } | |
| Write-Verbose "Importing runbook $runbookName..." | |
| if ($runbookState -eq "Published") | |
| { | |
| Write-Verbose "Publishing runbook $runbookName" | |
| Import-AzAutomationRunbook -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $runbookName -Type $runbookType -Path $runbookPath -Tags $runbookTags -Published | |
| } | |
| else | |
| { | |
| Write-Verbose "Runbook $runbookName in $runbookState state.." | |
| Import-AzAutomationRunbook -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $runbookName -Type $runbookType -Path $runbookPath -Tags $runbookTags | |
| } | |
| Write-Verbose "Runbook $runbookName imported successfully." | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import runbook $runbookName`: $_" | |
| } | |
| } | |
| Write-Verbose "Completed import of runbooks to $newAAName." | |
| } | |
| function Export-Variables | |
| { | |
| Write-Verbose "Exporting variables from $oldAAName..." | |
| try | |
| { | |
| $variables = Get-AzAutomationVariable -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| foreach ($variable in $variables) | |
| { | |
| $script:varList += [PSCustomObject]@{ | |
| Name = $variable.Name | |
| Value = $variable.Value | |
| Encrypted = $variable.Encrypted | |
| Description = $variable.Description | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export variables from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Variables | |
| { | |
| Write-Verbose "Importing variables to $newAAName..." | |
| foreach ($var in $script:varList) | |
| { | |
| try | |
| { | |
| $existingVariable = Get-AzAutomationVariable -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $var.Name -ErrorAction SilentlyContinue | |
| if (-not $existingVariable) | |
| { | |
| New-AzAutomationVariable -AutomationAccountName $newAAName -Name $var.Name -Encrypted $var.Encrypted -Value $var.Value -Description $var.Description -ResourceGroupName $newRGName | |
| } | |
| else | |
| { | |
| Write-Verbose "Variable $($var.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import variable $($var.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-Schedules | |
| { | |
| param | |
| ( | |
| $OnlyEnabledSchedules | |
| ) | |
| Write-Verbose "Exporting schedules from $oldAAName..." | |
| try | |
| { | |
| $script:schedules = Get-AzAutomationSchedule -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| if ($OnlyEnabledSchedules) | |
| { | |
| $script:schedules = $script:schedules | Where-Object { $_.IsEnabled -eq $true } | |
| Write-Verbose "Filtered to only include enabled schedules." | |
| } | |
| return $script:schedules | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export schedules from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Schedules | |
| { | |
| Write-Verbose "Importing schedules to $newAAName..." | |
| foreach ($schedule in $script:schedules) | |
| { | |
| try | |
| { | |
| $existingSchedule = Get-AzAutomationSchedule -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $schedule.Name -ErrorAction SilentlyContinue | |
| if (-not $existingSchedule) | |
| { | |
| Write-Verbose "Schedule $($schedule.Name) does not exist in $newAAName. Creating it now..." | |
| switch ($schedule.Frequency) | |
| { | |
| "Minute" { | |
| New-AzAutomationSchedule -AutomationAccountName $newAAName -Name $schedule.Name -Description $schedule.Description -StartTime $schedule.StartTime -ExpiryTime $schedule.ExpiryTime -MinuteInterval $schedule.Interval -TimeZone $schedule.TimeZone -ResourceGroupName $newRGName | |
| } | |
| "Hour" { | |
| New-AzAutomationSchedule -AutomationAccountName $newAAName -Name $schedule.Name -Description $schedule.Description -StartTime $schedule.StartTime -ExpiryTime $schedule.ExpiryTime -HourInterval $schedule.Interval -TimeZone $schedule.TimeZone -ResourceGroupName $newRGName | |
| } | |
| "Day" { | |
| New-AzAutomationSchedule -AutomationAccountName $newAAName -Name $schedule.Name -Description $schedule.Description -StartTime $schedule.StartTime -ExpiryTime $schedule.ExpiryTime -DayInterval $schedule.Interval -TimeZone $schedule.TimeZone -ResourceGroupName $newRGName | |
| } | |
| "Week" { | |
| New-AzAutomationSchedule -AutomationAccountName $newAAName -Name $schedule.Name -Description $schedule.Description -StartTime $schedule.StartTime -ExpiryTime $schedule.ExpiryTime -WeekInterval $schedule.Interval -TimeZone $schedule.TimeZone -ResourceGroupName $newRGName | |
| } | |
| "Month" { | |
| New-AzAutomationSchedule -AutomationAccountName $newAAName -Name $schedule.Name -Description $schedule.Description -StartTime $schedule.StartTime -ExpiryTime $schedule.ExpiryTime -MonthInterval $schedule.Interval -TimeZone $schedule.TimeZone -ResourceGroupName $newRGName | |
| } | |
| default { | |
| Write-Error "Unsupported frequency type: $($schedule.Frequency)." | |
| } | |
| } | |
| Write-Verbose "Schedule $($schedule.Name) created successfully." | |
| } | |
| else | |
| { | |
| Write-Verbose "Schedule $($schedule.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import schedule $($schedule.Name): $_" | |
| } | |
| } | |
| Write-Verbose "Completed importing schedules to $newAAName." | |
| } | |
| function Export-Modules | |
| { | |
| $script:moduleList = @() # Properly initialize as an empty array | |
| Write-Verbose "Exporting custom modules from $oldAAName..." | |
| try | |
| { | |
| $modules = Get-AzAutomationModule -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | Where-Object { $_.IsGlobal -eq $false } | |
| foreach ($module in $modules) | |
| { | |
| # Convert size to a human-readable format | |
| $sizeInBytes = $module.SizeInBytes | |
| if ($sizeInBytes -ge 1GB) | |
| { | |
| $size = "{0:N2} GB" -f ($sizeInBytes / 1GB) | |
| } | |
| elseif ($sizeInBytes -ge 1MB) | |
| { | |
| $size = "{0:N2} MB" -f ($sizeInBytes / 1MB) | |
| } | |
| elseif ($sizeInBytes -ge 1KB) | |
| { | |
| $size = "{0:N2} KB" -f ($sizeInBytes / 1KB) | |
| } | |
| else | |
| { | |
| $size = "{0:N2} Bytes" -f $sizeInBytes | |
| } | |
| $script:moduleList += [PSCustomObject]@{ | |
| Name = $module.Name | |
| Version = $module.Version | |
| Custom = $true | |
| Size = $size | |
| SizeInBytes = $sizeInBytes | |
| CreationTime = $module.CreationTime | |
| LastModifiedTime = $module.LastModifiedTime | |
| ProvisioningState = $module.ProvisioningState | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export modules from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Get-ModuleDetails | |
| { | |
| [CmdletBinding()] | |
| param ( | |
| # The name of a package to find | |
| [Parameter(Mandatory = $true)] | |
| $Name, | |
| # The repository api URL -- like https://www.powershellgallery.com/api/v2/ or https://www.nuget.org/api/v2/ | |
| $PackageSourceUrl = 'https://www.powershellgallery.com/api/v2/', | |
| #If specified takes precedence over version | |
| [switch]$Latest, | |
| [version]$Version | |
| ) | |
| if ($Version) | |
| { | |
| $CheckVersion = $true | |
| } | |
| else | |
| { | |
| $CheckVersion = $false | |
| } | |
| switch ($true) | |
| { | |
| $Latest { | |
| Write-Verbose "Searching for latest [$Name] module" | |
| $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$Name' and IsLatestVersion" | |
| } | |
| $CheckVersion { | |
| Write-Verbose "Searching for version [$Version] of [$Name]" | |
| $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$Name' and Version eq '$Version'" | |
| } | |
| default { | |
| Write-Verbose "Searching for all versions of [$Name] module" | |
| $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$Name'" | |
| } | |
| } | |
| Invoke-RestMethod $URI | | |
| Select-Object @{ n = 'Name'; ex = { $_.title.('#text') } }, | |
| @{ n = 'Author'; ex = { $_.author.name } }, | |
| @{ n = 'Version'; ex = { $_.properties.NormalizedVersion } }, | |
| @{ n = 'Url'; ex = { $_.Content.src } }, | |
| @{ n = 'Description'; ex = { $_.properties.Description } }, | |
| @{ n = 'Properties'; ex = { $_.properties } } | |
| } | |
| function Import-Modules | |
| { | |
| Write-Verbose "Importing modules to $newAAName..." | |
| foreach ($module in $script:moduleList) | |
| { | |
| try | |
| { | |
| $existingModule = Get-AzAutomationModule -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $module.Name -ErrorAction SilentlyContinue | |
| if (-not $existingModule) | |
| { | |
| $PSGalleryDetails = Get-ModuleDetails -Name $module.Name -Version $module.Version | |
| New-AzAutomationModule -AutomationAccountName $newAAName -Name $module.Name -ResourceGroupName $newRGName -ContentLinkUri $PSGalleryDetails.Url | |
| } | |
| else | |
| { | |
| Write-Verbose "Module $($module.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import module $($module.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-Certificates | |
| { | |
| Write-Verbose "Exporting certificates from $oldAAName..." | |
| try | |
| { | |
| $certificates = Get-AzAutomationCertificate -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| foreach ($certificate in $certificates) | |
| { | |
| $script:certificateList += [PSCustomObject]@{ | |
| Name = $certificate.Name | |
| Thumbprint = $certificate.Thumbprint | |
| IsExportable = $certificate.IsExportable | |
| ExpiryTime = $certificate.ExpiryTime | |
| Description = $certificate.Description | |
| # Assuming you store the certificate file path and password somewhere secure for re-import | |
| Path = $certificate.Path | |
| Password = $certificate.Password | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export certificates from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Certificates | |
| { | |
| Write-Verbose "Importing certificates to $newAAName..." | |
| foreach ($certificate in $script:certificateList) | |
| { | |
| try | |
| { | |
| $existingCertificate = Get-AzAutomationCertificate -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $certificate.Name -ErrorAction SilentlyContinue | |
| if (-not $existingCertificate) | |
| { | |
| # Validate and convert password | |
| if (-not $certificate.Password) | |
| { | |
| Write-Warning "Password for certificate $($certificate.Name) is null. Skipping import." | |
| continue | |
| } | |
| $password = ConvertTo-SecureString -String $certificate.Password -AsPlainText -Force | |
| # Validate path | |
| if (-not $certificate.Path) | |
| { | |
| Write-Warning "Path for certificate $($certificate.Name) is null. Skipping import." | |
| continue | |
| } | |
| New-AzAutomationCertificate -AutomationAccountName $newAAName -Name $certificate.Name -Path $certificate.Path -Password $password -Exportable:$certificate.IsExportable -Description $certificate.Description -ResourceGroupName $newRGName | |
| Write-Verbose "Certificate $($certificate.Name) imported successfully." | |
| } | |
| else | |
| { | |
| Write-Verbose "Certificate $($certificate.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import certificate $($certificate.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-Connections | |
| { | |
| Write-Verbose "Exporting connections from $oldAAName..." | |
| try | |
| { | |
| $connections = Get-AzAutomationConnection -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| foreach ($connection in $connections) | |
| { | |
| $script:connectionList += [PSCustomObject]@{ | |
| Name = $connection.Name | |
| ConnectionTypeName = $connection.ConnectionTypeName | |
| FieldDefinitionValues = $connection.FieldDefinitionValues | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export connections from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Connections | |
| { | |
| Write-Verbose "Importing connections to $newAAName..." | |
| foreach ($connection in $script:connectionList) | |
| { | |
| try | |
| { | |
| $existingConnection = Get-AzAutomationConnection -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $connection.Name -ErrorAction SilentlyContinue | |
| if (-not $existingConnection) | |
| { | |
| if ($connection.FieldDefinitionValues) | |
| { | |
| New-AzAutomationConnection -AutomationAccountName $newAAName -Name $connection.Name -ConnectionTypeName $connection.ConnectionTypeName -FieldDefinitionValues $connection.FieldDefinitionValues -ResourceGroupName $newRGName | |
| } | |
| else | |
| { | |
| New-AzAutomationConnection -AutomationAccountName $newAAName -Name $connection.Name -ConnectionTypeName $connection.ConnectionTypeName -ResourceGroupName $newRGName | |
| } | |
| } | |
| else | |
| { | |
| Write-Verbose "Connection $($connection.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import connection $($connection.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-Credentials | |
| { | |
| Write-Verbose "Exporting credentials from $oldAAName..." | |
| try | |
| { | |
| $credentials = Get-AzAutomationCredential -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| foreach ($credential in $credentials) | |
| { | |
| $script:credentialList += [PSCustomObject]@{ | |
| Name = $credential.Name | |
| UserName = $credential.UserName | |
| Description = $credential.Description | |
| Tags = $credential.Tags | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export credentials from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Credentials | |
| { | |
| Write-Verbose "Importing credentials to $newAAName..." | |
| foreach ($credential in $script:credentialList) | |
| { | |
| try | |
| { | |
| $existingCredential = Get-AzAutomationCredential -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $credential.Name -ErrorAction SilentlyContinue | |
| if (-not $existingCredential) | |
| { | |
| New-AzAutomationCredential -AutomationAccountName $newAAName -Name $credential.Name -Value (Get-Credential $credential.UserName) -Description $credential.Description -ResourceGroupName $newRGName | |
| } | |
| else | |
| { | |
| Write-Verbose "Credential $($credential.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import credential $($credential.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-Webhooks | |
| { | |
| Write-Verbose "Exporting webhooks from $oldAAName..." | |
| try | |
| { | |
| $webhooks = Get-AzAutomationWebhook -AutomationAccountName $oldAAName -ResourceGroupName $oldRGName | |
| foreach ($webhook in $webhooks) | |
| { | |
| $script:webhookList += [PSCustomObject]@{ | |
| Name = $webhook.Name | |
| Uri = $webhook.Uri | |
| IsEnabled = $webhook.IsEnabled | |
| ExpiryTime = $webhook.ExpiryTime | |
| RunbookName = $webhook.RunbookName | |
| Parameters = $webhook.Parameters | |
| Tags = $webhook.Tags | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export webhooks from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-Webhooks | |
| { | |
| Write-Verbose "Importing webhooks to $newAAName..." | |
| foreach ($webhook in $script:webhookList) | |
| { | |
| try | |
| { | |
| $existingWebhook = Get-AzAutomationWebhook -ResourceGroupName $newRGName -AutomationAccountName $newAAName -Name $webhook.Name -ErrorAction SilentlyContinue | |
| if (-not $existingWebhook) | |
| { | |
| New-AzAutomationWebhook -AutomationAccountName $newAAName -Name $webhook.Name -Uri $webhook.Uri -IsEnabled $webhook.IsEnabled -ExpiryTime $webhook.ExpiryTime -RunbookName $webhook.RunbookName -Parameters $webhook.Parameters -ResourceGroupName $newRGName | |
| Set-AzAutomationWebhook -AutomationAccountName $newAAName -Name $webhook.Name -Tags $webhook.Tags -ResourceGroupName $newRGName | |
| } | |
| else | |
| { | |
| Write-Verbose "Webhook $($webhook.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import webhook $($webhook.Name): $_" | |
| } | |
| } | |
| } | |
| function Export-PrivateEndpoints | |
| { | |
| Write-Verbose "Exporting private endpoints from $oldAAName..." | |
| try | |
| { | |
| $privateEndpoints = Get-AzPrivateEndpointConnection -ResourceGroupName $oldRGName -Name $oldAAName -PrivateLinkResourceType 'Microsoft.Automation/automationAccounts' | |
| foreach ($privateEndpoint in $privateEndpoints) | |
| { | |
| $script:privateEndpointList += [PSCustomObject]@{ | |
| Name = $privateEndpoint.Name | |
| PrivateLinkServiceConnectionState = $privateEndpoint.PrivateLinkServiceConnectionState | |
| GroupIds = $privateEndpoint.GroupIds | |
| RequestMessage = $privateEndpoint.RequestMessage | |
| Tags = $privateEndpoint.Tags | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to export private endpoints from $oldAAName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Import-PrivateEndpoints | |
| { | |
| Write-Verbose "Importing private endpoints to $newAAName..." | |
| foreach ($privateEndpoint in $script:privateEndpointList) | |
| { | |
| try | |
| { | |
| $existingPrivateEndpoint = Get-AzPrivateEndpointConnection -ResourceGroupName $newRGName -Name $newAAName -PrivateLinkResourceType 'Microsoft.Automation/automationAccounts' -ErrorAction SilentlyContinue | |
| if (-not $existingPrivateEndpoint) | |
| { | |
| New-AzPrivateEndpoint -ResourceGroupName $newRGName -Name $privateEndpoint.Name -Tag $privateEndpoint.Tags -PrivateLinkServiceConnectionState $privateEndpoint.PrivateLinkServiceConnectionState -GroupIds $privateEndpoint.GroupIds -RequestMessage $privateEndpoint.RequestMessage -PrivateLinkResourceType 'Microsoft.Automation/automationAccounts' | |
| } | |
| else | |
| { | |
| Write-Verbose "Private endpoint $($privateEndpoint.Name) already exists in $newAAName. Skipping import." | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to import private endpoint $($privateEndpoint.Name): $_" | |
| } | |
| } | |
| } | |
| function Create-AutomationAccount | |
| { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$AccountName, | |
| [Parameter(Mandatory = $true)] | |
| [string]$ResourceGroupName, | |
| [Parameter(Mandatory = $true)] | |
| [string]$Location | |
| ) | |
| Write-Verbose "Creating Azure Automation account $AccountName in resource group $ResourceGroupName at location $Location..." | |
| try | |
| { | |
| New-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AccountName -Location $Location | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to create automation account $AccountName in resource group $ResourceGroupName`: $_" | |
| exit 1 | |
| } | |
| } | |
| function Main | |
| { | |
| Set-ExecutionPolicyIfNeeded | |
| Write-Host "Please verify if you have sufficient permissions to access both old subscription $oldSubscriptionId and new subscription $newSubscriptionId" | |
| Login-AzAccountWithSubscription -SubscriptionId $oldSubscriptionId -SubscriptionType old -TenantId $oldTenantId | |
| try | |
| { | |
| $oldAAinfo = Get-AzAutomationAccount -ResourceGroupName $oldRGName -Name $oldAAName | |
| if (-not $oldAAinfo) | |
| { | |
| Write-Error "Automation account $oldAAName under resource group $oldRGName does not exist in subscription $oldSubscriptionId. Please check the name and restart the script!" | |
| return | |
| } | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to get automation account $oldAAName in subscription $oldSubscriptionId`: $_" | |
| exit 1 | |
| } | |
| $runbooks = Export-Runbooks | |
| Export-Variables | |
| Export-Schedules -OnlyEnabledSchedules:$OnlyEnabledSchedules | |
| Export-Modules | |
| #Export-Certificates | |
| #Export-Connections | |
| Export-Credentials | |
| Export-Webhooks | |
| #Export-PrivateEndpoints | |
| function Get-ErrorDetails | |
| { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [System.Management.Automation.ErrorRecord]$ErrorRecord | |
| ) | |
| return @{ | |
| ErrorMessage = $ErrorRecord.Exception.Message | |
| ErrorType = $ErrorRecord.Exception.GetType().FullName | |
| ScriptStackTrace = $ErrorRecord.ScriptStackTrace | |
| } | |
| } | |
| Login-AzAccountWithSubscription -SubscriptionId $newSubscriptionId -SubscriptionType new -TenantId $newTenantId | |
| try | |
| { | |
| $newAAinfo = Get-AzAutomationAccount -ResourceGroupName $newRGName -Name $newAAName | |
| if (-not $newAAinfo) | |
| { | |
| Write-Host "Automation account $newAAName does not exist in resource group $newRGName. Creating it now..." | |
| Create-AutomationAccount -AccountName $newAAName -ResourceGroupName $newRGName -Location $newAALocation | |
| } | |
| } | |
| catch | |
| { | |
| $errorDetails = Get-ErrorDetails -ErrorRecord $_ | |
| if ($errorDetails.ErrorType -eq "Microsoft.Azure.Commands.Automation.Common.ResourceNotFoundException") | |
| { | |
| Write-Warning "The Automation account $newAAName was not found in resource group $newRGName. Attempting to create it." | |
| try | |
| { | |
| Create-AutomationAccount -AccountName $newAAName -ResourceGroupName $newRGName -Location $newAALocation | |
| } | |
| catch | |
| { | |
| Write-Error "Failed to create automation account $newAAName in resource group $newRGName`: $_" | |
| exit 1 | |
| } | |
| } | |
| else | |
| { | |
| Write-Error "Failed to get automation account $newAAName in subscription $newSubscriptionId`: $_" | |
| exit 1 | |
| } | |
| } | |
| Import-Variables | |
| Import-Runbooks -Runbooks $runbooks | |
| Import-Schedules | |
| Import-Modules | |
| #Import-Certificates | |
| #Import-Connections | |
| Import-Credentials | |
| Import-Webhooks | |
| #Import-PrivateEndpoints | |
| Write-Host "All done! Please check your automation account $newAAName under subscription $newSubscriptionId to verify the imported runbooks, variables, schedules, modules, certificates, connections, credentials, webhooks, and private endpoints." | |
| } | |
| Main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment