diff --git a/SUPPORT.md b/SUPPORT.md index f171143..9842c84 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -6,7 +6,7 @@ RGCOPY relies on several PowerShell modules. A malfunction of these modules might directly result in a failure of RGCOPY. Therefore, you should make sure that you always use the newest version of: - PowerShell -- PowerShell modules **Az**, **Az.NetAppFiles** and **Az.HanaOnAzure** +- PowerShell modules **Az** and **Az.NetAppFiles** - RGCOPY #### Analyze deployment errors diff --git a/examples/examplePostDeployment.ps1 b/examples/examplePostDeployment.ps1 index 719ef04..658e30f 100644 --- a/examples/examplePostDeployment.ps1 +++ b/examples/examplePostDeployment.ps1 @@ -23,7 +23,3 @@ Write-Output '' Write-Output "Array count: $($rgcopyParameters.count)" Write-Output ($rgcopyParameters -as [string]) Write-Output '' -# [SecureString] parameter (if supplied): -if ($Null -ne $dbPassword) { - Write-Output (ConvertFrom-SecureString -SecureString $dbPassword -AsPlainText) -} diff --git a/rgcopy-docu.md b/rgcopy-docu.md index 339c782..700a8e2 100644 --- a/rgcopy-docu.md +++ b/rgcopy-docu.md @@ -1,5 +1,5 @@ # RGCOPY documentation -**Version: 0.9.38
June 2022** +**Version: 0.9.40
December 2022** *** ### Introduction @@ -142,7 +142,7 @@ Step|parameter
skip switch|usage :clock1: *create snapshots*|**`skipSnapshots`**|This step creates snapshots of disks (and [NetApp Volumes](./rgcopy-docu.md#NetApp-Volumes-and-Ultra-SSD-Disks)) in the source RG. During this time, VMs with more than one data disk must be stopped. See section [Application Consistency](./rgcopy-docu.md#Application-Consistency) for details.
:bulb: **Tip:** When setting parameter switch **`stopVMsSourceRG`**, RGCOPY stops *all* VMs in the source RG before creating snapshots. :clock2: *create backups*|**`skipBackups`**|This step is only needed when using (or converting) [NetApp Volumes](./rgcopy-docu.md#NetApp-Volumes-and-Ultra-SSD-Disks) on LINUX. A file backup of specified mount points is created on an Azure SMB file share in the source RG. :clock3: *create BLOBs*|**`skipBlobs`**|This step is needed when the source RG and the target RG are not in the same region. The snapshots in the source RG are copied as [BLOBs](./rgcopy-docu.md#Parameters-for-BLOB-Copy) into a storage account in the target RG. Dependent on the disk sizes and the region, this might take several hours. -:clock4: *deployment*|**`skipDeployment`**|The deployment consists of several part steps: +:clock4: *deployment*|**`skipDeployment`**|The deployment consists of several part steps: :clock5: *start workload*| *optional step* | This step is used for testing SAP Workload. It has to be explicitly activated using switch **`startWorkload`**. :clock6: *cleanup*| *optional step* | By default, created snapshots are not deleted by RGCOPY.
:bulb: **Tip:** you can activate a cleanup using RGCOPY parameters. See section [Cost Efficiency](./rgcopy-docu.md#Cost-Efficiency) for details. @@ -237,16 +237,13 @@ The following resource configuration parameters exist: parameter|usage (data type is always [string] or [array]) :---|:--- **`setVmSize`** =
`@("size@vm1,vm2,...", ...)` |Set VM Size: -**`setDiskSku`** =
`@("sku@disk1,disk2,...", ...)` |Set Disk SKU: +**`setDiskSku`** =
`@("sku@disk1,disk2,...", ...)` |Set Disk SKU (default value is **Premium_LRS**): :warning: **Warning:** If you want to avoid using the default value then you must explicitly set **setDiskSku** = `$Null`. **`setDiskSize`** =
`@("size@disk1,disk1,...", ...)` |Set Disk Size: :warning: **Warning:** It's only possible to *increase* the size of a disk. Partitions on the disk are not changed. This parameter was originally intended for increasing disk I/O on the target RG. Nowadays, you better should use parameter `setDiskTier` instead. **`setDiskTier`** =
`@("tier@disk1,disk1,...", ...)` |Set Disk Performance Tier::memo: **Note:** To remove existing performance tier configuration, set tier to P0. **`setDiskBursting`** = `@("bool@disk1,disk2,...", ...)`|Set Disk Bursting: **`setDiskMaxShares`** =
`@("number@disk1,disk2,...", ...)`|Set maximum number of shares for a Shared Disk: :memo: **Note:** For number = 1, it is not a Shared Disk anymore **`setDiskCaching`** =
`@("caching/wa@disk1,disk2...", ...)` |Set Disk Caching: :memo: **Examples:** **`setVmDeploymentOrder`** =
`@("prio@vm1,vm2,...", ...)` |Set VM deployment Order: :memo: **Note:** This parameter is used during ARM template creation. You can define priories for deploying VMs. A VM with higher priority (lower number) will be deployed before VMs with lower priority. Hereby, you can ensure that an important VM (for example a domain controller) will be deployed before other VMs. -**`setLoadBalancerSku`** =
`@("sku@lb1,lb2,...", ...)` |Set Load Balancer SKU: -**`setPublicIpSku`** =
`@("sku@ip1,ip2,...", ...)` |Set Public IP SKU: -**`setPublicIpAlloc`** =
`@("allocation@ip1,ip2,...", ...)` |Set Public IP Allocation Method: **`setPrivateIpAlloc`** =
`@("allocation@ip1,ip2,...", ...)` |Set Private IP Allocation Method: **`removeFQDN`** =
`@("bool@ip1,ip2,...", ...)` |Remove Fully Qualified Domain Names: **`setAcceleratedNetworking`** =
`@("bool@nic1,nic2,...", ...)` |Set Accelerated Networking: @@ -259,11 +256,14 @@ parameter|default value|default behavior :---|:---:|:--- **`setDiskSku`** |'Premium_LRS' |converts all disks to Premium SSD
(except Ultra SSD disks - they cannot be copied directly) **`setVmZone`** |0 |removes zone configuration from VMs -**`setLoadBalancerSku`**|'Standard' |sets SKU of Load Balancers to Standard **`setPrivateIpAlloc`** |'Static' |sets allocation of Private IP Addresses to Static **`setAcceleratedNetworking`**|$True |enables Accelerated Networking +**`removeFQDN`**|$True |Removes the Fully Qualified Domain Name
(even when `skipDefaultValues` is set) -Parameter **`removeFQDN`** always has the default value `$True` (even when `skipDefaultValues` is set) +In addition, RGCOPY always performs the following changes: +- set the IP Allocation Method of Public IP Addresses to `Static` +- set the SKU of Public IP Addresses to `Standard` +- set the SKU of Load Balancers to `Standard`
@@ -315,9 +315,10 @@ parameter|[DataType]: usage **`skipVmssFlex`**|**[switch]**: do not copy existing VM Scale Sets Flexible.
Hereby, the target RG does not contain any VM Scale Set. **`skipAvailabilitySet`**|**[switch]**: do not copy existing Availability Sets.
Hereby, the target RG does not contain any Availability Set. **`skipProximityPlacementGroup`**|**[switch]**: do not copy existing Proximity Placement Groups.
Hereby, the target RG does not contain any Proximity Placement Group. -**`createVmssFlex`** =
`@("vmss/fault/zones@vm1,vm2,...", ...)` |Create a VMSS Flex (VM Scale Set with Flexible orchestration mode) for given VMs: :memo: **Note:** When you are using this parameter for creating new VM Scale Sets then all existing VM Scale Sets are removed first.
:warning: **Warning:** Either *zones* or *fault* must have the value *none*
:bulb: **Tip:** A fault domain count of *max* automatically creates the maximum number of fault domains in the target region. +**`createVmssFlex`** =
`@("vmss/fault/zones@vm1,vm2,...", ...)` |Create a VMSS Flex (VM Scale Set with Flexible orchestration mode) for given VMs: :memo: **Note:** When you are using this parameter for creating new VM Scale Sets then all existing VM Scale Sets are removed first.
:warning: **Warning:** Either *zones* or *fault* must have the value `none`.
For SAP, *fault* must have the value `none`.
:bulb: **Tip:** A fault domain count of *max* automatically creates the maximum number of fault domains in the target region. +**`singlePlacementGroup`**|Set property `singlePlacementGroup` for all VMSS Flex.
Allowed values in {`$Null`, `$True`, `$False`}
Setting this parameter is normally not needed. **`createAvailabilitySet`** =
`@("avset/fault/update@vm1,vm2,...", ...)` |Create Azure Availability Set for given VMs::memo: **Note:** When you are using this parameter for creating new Availability Sets then all existing Availability Sets *and* Proximity Placement Groups are removed first. -**`createProximityPlacementGroup`** =
`@("ppg@res1,res2,...", ...)` |Create Azure Proximity Placement Group for given VMs or Availability Sets: :memo: **Note:** When you are using this parameter for creating new Proximity Placement Groups then all existing Proximity Placement Groups *and* Availability Sets are removed first.
:warning: **Warning:** You might use the same name for a VM and an Availability Set and add this name as resource name to this parameter. In this case, the VM as well as the Availability Set will be added to the Proximity Placement Group. +**`createProximityPlacementGroup`** =
`@("ppg@res1,res2,...", ...)` |Create Azure Proximity Placement Group for given resources: :memo: **Note:** When you are using this parameter for creating new Proximity Placement Groups then all existing Proximity Placement Groups *and* Availability Sets are removed first.
:warning: **Warning:** You might use the same name for a VM and an Availability Set and add this name as resource name to this parameter. In this case, the VM as well as the Availability Set will be added to the Proximity Placement Group. In Azure you cannot directly configure the Availability Zone for an Availability Set. However, you can indirectly pin an Availability Set to an Availability Zone. The trick is to deploy an additional VM that is in the Availability Zone. If this VM and the Availability Set are in the same Proximity Placement Group then they are also in the same Availability Zone. However, this only works if the VM is deployed first. If you deploy the Availability Set first then it might be deployed in a different Availability Zone. Afterwards, the deployment of the VM fails because the requirements for Availability Zone and Proximity Placement Group cannot be fulfilled at the same time. Luckily, you can define the deployment order in RGCOPY: @@ -367,51 +368,6 @@ $rgcopyParameter = @{ .\rgcopy.ps1 @rgcopyParameter ``` -An alternative for using Azure Availability Sets is using **VMSS Flex with fault domains**. In this case you can even define the fault domain individually per VM (which is not possible when using an Azure Availability Set). You cannot define the zone number in this case. However, all VMs of the VMSS Flex with fault domains are deployed in the same zone. This is much more flexible compared with an Availability Set (which would require that all VMs fit into the same cluster). - -Example of VMSS Flex **with fault domains**: - -```powershell -$rgcopyParameter = @{ - sourceRG = 'SAP_master' - targetRG = 'SAP_copy' - targetLocation = 'westus' - - createVmssFlex = @( - 'vmss/2/none @ hana1, hana2, ascs1, ascs2, app1a, app1b, app2a, app2b' - ) - setVmFaultDomain = @( - '0 @ hana1, ascs1, app1a, app1b', - '1 @ hana2, ascs2, app2a, app2b' - ) -} -.\rgcopy.ps1 @rgcopyParameter -``` - -When using more than one instance of VMSS Flex with fault domains, you can use a Proximity Placement Group for pinning them to the same zone (this is not possible for VMSS Flex with zones). - -Example of VMSS Flex **with fault domains** and **PPG**: - -```powershell -$rgcopyParameter = @{ - sourceRG = 'SAP_master' - targetRG = 'SAP_copy' - targetLocation = 'westus' - - createProximityPlacementGroup = 'ppg @ vmss1, vmss2' - - createVmssFlex = @( - 'vmss1/2/none @ hana1, hana2', - 'vmss2/2/none @ ascs1, ascs2, app1a, app1b, app2a, app2b' - ) - setVmFaultDomain = @( - '0 @ hana1, ascs1, app1a, app1b', - '1 @ hana2, ascs2, app2a, app2b' - ) -} -.\rgcopy.ps1 @rgcopyParameter -``` - ### Parameters for BLOB Copy When the source RG and the target RG are in different regions (or tenants) then RGCOPY cannot use snapshots for creating the disks. In this case, the workflow looks like this: 1. create snapshots in the source RG @@ -459,7 +415,6 @@ parameter|[DataType]: usage :---|:--- **`pathExportFolder`**
|**[string]**: By default, RGCOPY creates all files in the user home directory. You can change the path for all RGCOPY files by setting parameter `pathExportFolder`. **`pathArmTemplate`**|**[string]**: You can deploy an existing (main) ARM template by setting this parameter. No snapshots are created, no ARM template is created and no resource configuration changes are possible. -**`pathArmTemplateAms`**|**[string]**: You can provide an existing ARM template for deploying the AMS instance and providers.
:warning: **Warning:** *Only* ARM templates that were created by RGCOPY can be used here because the ARM template parameter `amsInstanceName` is required.
@@ -845,7 +800,6 @@ RGCOPY writes the working directory of `Invoke-AzVMRunCommand` to stdout respect ### Starting SAP For starting SAP, you must write your own script. This script must contain `systemctl start sapinit` if you are using NetApp volumes. The path of the script has to be specified using parameter `scriptStartSapPath` (see above). This script will be started by RGCOPY in the following cases: 1. In the source RG: before running the local script specified by parameter `pathPreSnapshotScript` -2. In the target RG: before installing an AMS instance 3. In the target RG: before running the local script specified by parameter `pathPostDeploymentScript` 4. In the target RG: at the beginning of step *Workload and Analysis* (if parameter `startWorkload` is set) @@ -887,6 +841,9 @@ $rgcopyParameter = @{ ### Merging and Cloning VMs Normally, RGCOPY is used for copying all resources of a source RG into a new (or empty) target RG. By using parameter `setVmMerge` you can copy discrete VMs and attach them to an existing subnet in the target RG. You can use this to copy a standard VM (e.g. jump box) into several other resource groups. + +> :warning: **Warning:** `setVmMerge` is an experimental parameter that might or might not work. + By using parameter `setVmName`, you can rename the VMs in the target RG. Herby, you can copy a VM even in the source RG (source RG = target RG). The VM disks are automatically renamed. You might use this for cloning application servers. Be aware that parameter `setVmName` does not rename the VM on OS level. parameter|[DataType]: usage @@ -920,28 +877,6 @@ parameter|[DataType]: usage **`installExtensionsAzureMonitor`** |**[array]**: Names of VMs for deploying the Azure Agents
(AzureMonitorWindowsAgent or AzureMonitorLinuxAgent).
The Azure Agent is intalled on all VMs when setting the parameter to `@('*')` **`installExtensionsSapMonitor`** |**[array]**: Names of VMs for deploying the SAP Monitor Extension.
Alternatively, you can set the Azure tag `rgcopy.Extension.SapMonitor` for the VM. If you do not want to install the SAP Monitor Extension although the Azure tag has been set, use switch `ignoreTags`. -### Azure Monitor for SAP -RGCOPY can copy *up to one* AMS instance and multiple AMS providers. For installing the SapHana provider, SAP HANA must already be running. However, this is not guarantied during the ARM deployment. Therefore, RGCOPY creates a separate ARM template just for the AMS instance and providers. This ARM template will be deployed in the target RG after SAP HANA has been started. Therefore, RGCOPY is using the script `scriptStartSapPath` as described above. - ->:warning: **Warning:** **Azure Monitor for SAP (AMS) is currently in public review with version v1. Version v2 will probably be in public review in 2022. RGCOPY only supports version v1. Once version v2 is available, RGCOPY might remove its support for AMS completely.** - -The following RGCOPY parameters exist for AMS: -parameter|[DataType]: usage -:---|:--- -**`createArmTemplateAms`**|**[switch]**: Export an ARM template for the AMS resources from the source RG.
If you want to copy the AMS instance and providers then you must either use parameter `createArmTemplateAms` or `pathArmTemplateAms`. -**`pathArmTemplateAms`**|**[string]**: Use an exsisting AMS ARM template for deploying AMS in the target RG -**`amsInstanceName`**|**[string]**: Name of the AMS instance in the target RG.
When not setting this parameter, RGCOPY calculates an AMS instance name based on the target RG name. -**`amsWsName`** |**[string]**: Name of an existing log analytics workspace that should be used by AMS.
If not set, then AMS creates a new workspace in the managed resource group. -**`amsWsRG`** |**[string]**: Resource group name of the existing log analytics workspace used by AMS.
It must be in the target subscription. -**`amsWsKeep`** |**[switch]**: By setting this switch, the AMS instance in the target RG is using the same log analytics workspace as the source RG. No new workspace is created. Parameters `amsWsName` and `amsWsRG`are ignored. -**`amsShareAnalytics`** |**[switch]**: When setting this switch then AMS enables Customer Analytics. In this case, collected AMS data is visible for Microsoft support. This is not the case by default (without setting this switch). -**`dbPassword`**|**[SecureString]**: For AMS providers SapHana and MsSqlServer, you must provide the database password to RGCOPY as a secure string as follows:
`dbPassword = (ConvertTo-SecureString -String 'secure-password' -AsPlainText -Force)` -**`amsUsePowerShell`**|**[boolean]**: (default value: \$True): This parameter just defines, *how* RGCOPY is installing AMS: - -### Virtual Network Peerings for AMS in the source RG -RGCOPY can copy Azure virtual network peerings for AMS instances. This is useful because AMS is only supported in some specific regions yet, for example in eastus. If your resource group is located in an unsupported region then you can create an AMS instance in an additional virtual network in a supported region. Afterwards, you create a network peering between your main virtual network and the additional virtual network. **The AMS instance and all virtual networks must be located in the source RG.** RGCOPY does not support an AMS instance for monitoring resources in different resource groups. However, AMS instances in different resource groups can share the same log analytics workspace. - - ### Cost Efficiency You can save Azure costs by using RGCOPY [Archive Mode](./rgcopy-docu.md#Archive-Mode) and [Update Mode](./rgcopy-docu.md#Update-Mode) as described above. This chapter describes how to save costs caused by RGCOPY. @@ -957,7 +892,7 @@ parameter|[DataType]: usage **`stopVMsTargetRG`** |**[switch]**: When setting this switch in **Copy Mode**, RGCOPY stops all VMs in the target RG after deploying it. Typically, this is not what you want. However, it might be useful for saving costs when deploying a resource group that is not used immediately. Tip: You can use the following RGCOPY parameters for reducing cost in the target RG: `setVmSize`, `setDiskSku`, `setDiskTier`, `createDisksTier`, `netAppServiceLevel`, `netAppPoolGB`, `smbTier`, and `skipBastion`. -The *default* values of some RGCOPY parameters also have some cost impact. See parameters `createDisksTier`, `setDiskSku`, and `setLoadBalancerSku` above. +The *default* values of some RGCOPY parameters also have some cost impact. See parameters `createDisksTier` and `setDiskSku` above. The behavior of RGCOPY changed for copying NetApp volumes or Ultra SSD disks. It now starts only *needed* Ms in the source RG. These VMs are stopped again by RGCOPY. In earlier versions of RGCOPY *all* VMs were started in the source RG and you had to stop them on your own. @@ -987,8 +922,6 @@ The following ARM resources are copied from the source RG: - Microsoft.Network/bastionHosts - Microsoft.Network/natGateways - Microsoft.Network/publicIPPrefixes -- Microsoft.HanaOnAzure/sapMonitors -- Microsoft.HanaOnAzure/sapMonitors/providerInstances **\* RGCOPY does not support** Microsoft.Network/loadBalancers/**loadBalancerInboundNatRules** yet @@ -1045,7 +978,6 @@ file|*[DataType]*: usage :---|:--- `rgcopy..SOURCE.json`|Exported ARM template(s) from the source RG(s). `rgcopy..TARGET.json`
`rgcopy..TARGET.json`|ARM template which is generated by RGCOPY
*Ditto* when using **Archive Mode** -`rgcopy..AMS.json`

`rgcopy..AMS.json`|ARM template for Azure Monitor for SAP (AMS).
It will only be created if the source RG contains an AMS Instance.
*Ditto* when using **Archive Mode** `rgcopy..TARGET.log`
`rgcopy..SOURCE.log`|Standard RGCOPY log file
*Ditto* when using **Update Mode** `rgcopy.txt`|Backup of the running script rgcopy.ps1 used for support `rgcopy..RESTORE.ps1.txt`|Generated PowerShell script when using **Archive Mode** diff --git a/rgcopy.ps1 b/rgcopy.ps1 index 35c9942..9583e2b 100644 --- a/rgcopy.ps1 +++ b/rgcopy.ps1 @@ -1,7 +1,7 @@ <# rgcopy.ps1: Copy Azure Resource Group -version: 0.9.38 -version date: October 2022 +version: 0.9.40 +version date: December 2022 Author: Martin Merdes Public Github: https://github.com/Azure/RGCOPY Microsoft intern: https://github.com/Azure/RGCOPY-MS-intern @@ -110,24 +110,10 @@ param ( # file locations #-------------------------------------------------------------- ,[string] $pathArmTemplate # given ARM template file - ,[string] $pathArmTemplateAms # given ARM template file for AMS deployment ,[string] $pathExportFolder = '~' # default folder for all output files (log-, config-, ARM template-files) ,[string] $pathPreSnapshotScript # running before ARM template creation on sourceRG (after starting VMs and SAP) ,[string] $pathPostDeploymentScript # running after deployment on targetRG - #-------------------------------------------------------------- - # AMS and script parameter - #-------------------------------------------------------------- - # AMS (Azure Monitoring for SAP) parameters - ,[switch] $createArmTemplateAms # create AMS ARM template and deploy AMS - ,[string] $amsInstanceName # Name of AMS instance to be created in the target RG - ,[string] $amsWsName # Name of existing Log Analytics workspace for AMS - ,[string] $amsWsRG # Resource Group of existing Log Analytics workspace for AMS - ,[switch] $amsWsKeep # Keep existing Log Analytics workspace of sourceRG - ,[switch] $amsShareAnalytics # Sharing Customer Analytics Data with Microsoft - ,[SecureString] $dbPassword # = (ConvertTo-SecureString -String 'secure-password' -AsPlainText -Force) - ,[boolean] $amsUsePowerShell = $True # use PowerShell cmdlets rather than ARM template for AMS deployment - # script location of shell scripts inside the VM ,[string] $scriptStartSapPath # if not set, then calculated from vm tag rgcopy.ScriptStartSap ,[string] $scriptStartLoadPath # if not set, then calculated from vm tag rgcopy.ScriptStartLoad @@ -249,6 +235,7 @@ param ( # with $vmss: name of VM Scale Set Flexible # $zones: Allowed Zones in {none, 1+2, 1+3, 2+3, 1+2+3} # $fault: Fault domain count in in {none, 2, 3, max} + ,$singlePlacementGroup # in {Null, True, False} ,$createAvailabilitySet = @() # usage: $createAvailabilitySet = @("$avSet/$fd/$ud$@$vm1,$vm2,...", ...) @@ -298,18 +285,6 @@ param ( # with $net as virtual network name, $subnet as subnet name in target resource group # merge VM jumpbox into target RG: @("vnet/default@jumpbox") - ,$setLoadBalancerSku = 'Standard' # default value in COPY MODE - # usage: $setLoadBalancerSku = @("$sku@$loadBalancer1,$loadBalancer12,...", ...) - # with $sku -in @('Basic', 'Standard') - - ,$setPublicIpSku = @() - #usage: $setPublicIpSku = @("$sku@$ipName1,$ipName12,...", ...) - # with $sku -in @('Basic', 'Standard') - - ,$setPublicIpAlloc = @() - # usage: $setPublicIpAlloc = @("$allocation@$ipName1,$ipName12,...", ...) - # with $allocation -in @('Dynamic', 'Static') - ,$setPrivateIpAlloc = 'Static' # default value in COPY MODE # usage: $setPrivateIpAlloc = @("$allocation@$ipName1,$ipName12,...", ...) # with $allocation -in @('Dynamic', 'Static') @@ -347,9 +322,6 @@ param ( #-------------------------------------------------------------- # experimental parameters: DO NOT USE! #-------------------------------------------------------------- - # use Parameter Set updateMode when switch justRedeployAms is set - ,[Parameter(ParameterSetName='updateMode')] - [switch] $justRedeployAms ,$setVmTipGroup = @() ,$setGroupTipSession = @() ,[switch] $allowRunningVMs @@ -397,9 +369,6 @@ $variableTypeParameters = @( 'setDiskBursting', 'setDiskCaching', 'setAcceleratedNetworking', - 'setLoadBalancerSku', - 'setPublicIpSku', - 'setPublicIpAlloc', 'setPrivateIpAlloc', 'removeFQDN', 'createProximityPlacementGroup', @@ -435,7 +404,6 @@ $workflowParameters = @( 'justStopCopyBlobs', 'justCreateSnapshots', 'justDeleteSnapshots', - 'justRedeployAms', 'skipStartSAP' ) @@ -459,7 +427,7 @@ else { } # process only sourceRG ? -if ($updateMode -or $justCreateSnapshots -or $justDeleteSnapshots -or $justRedeployAms) { +if ($updateMode -or $justCreateSnapshots -or $justDeleteSnapshots) { $targetRG = $sourceRG $enableSourceRgMode = $True $rgcopyMode = 'Update Mode' @@ -617,30 +585,6 @@ function test-names { test-match 'netAppPoolName' $script:netAppPoolName $match } - #-------------------------------------------------------------- - # amsInstanceName - # Can include alphanumeric (,underscore, hyphen) - # length: 6-30 - $match = '^[a-zA-Z0-9_\-]{6,30}$' - - if ($script:amsInstanceName.Length -eq 0) { - $name = $script:targetRG -replace '[_\.\-\(\)]', '-' -replace '\-+', '-' ` - - # truncate name - $len = (30, $name.Length | Measure-Object -Minimum).Minimum - $name = $name.SubString(0,$len) - - # name too short - if ($len -lt 6) { - $name += '-ams-inst' - } - - $script:amsInstanceName = $name - } - else { - test-match 'amsInstanceName' $script:amsInstanceName $match - } - #-------------------------------------------------------------- # archiveContainer # This name may only contain lowercase letters, numbers, and hyphens, and must begin with a letter or a number. @@ -1185,6 +1129,9 @@ function write-logFileHashTable { $paramKey = $_.Key $paramValue = $_.Value + if (($paramValue -is [array]) -and ($paramValue.length -eq 0)) { + $paramValue = $Null + } # array if ($paramValue -is [array]) { @@ -2070,6 +2017,7 @@ function save-skuProperties { # save properties of each VM size $script:vmSkus = @{} + $script:MaxRegionFaultDomains = 3 if ($skipVmChecks) { return } $savedSub = $script:currentSub @@ -2093,7 +2041,7 @@ function save-skuProperties { $script:MaxRegionFaultDomains = ` ($_.Capabilities | Where-Object Name -eq 'MaximumPlatformFaultDomainCount').Value -as [int] } - if ($script:MaxRegionFaultDomains -eq 0) { + if ($script:MaxRegionFaultDomains -le 0) { write-logFileWarning "Could not get MaximumPlatformFaultDomainCount for region '$targetLocation'" $script:MaxRegionFaultDomains = 2 } @@ -3708,6 +3656,7 @@ function update-paramSetVmTipGroup { } # update from tag (if parameter setVmTipGroup was NOT used) + $numberTags = 0 if (($setVmTipGroup.count -eq 0) ` -and ($createVmssFlex.count -eq 0) ` -and ($createAvailabilitySet.count -eq 0) ` @@ -3721,10 +3670,19 @@ function update-paramSetVmTipGroup { $tipGroup = $_.Tags.$azTagTipGroup -as [int] if ($tipGroup -gt 0) { $_.Group = $tipGroup + $numberTags++ } } } + if ($numberTags -gt 0) { + write-logFileWarning "VM Tag 'rgcopy.TipGroup' was used" ` + "Use RGCOPY parameter 'ignoreTags' for preventing this" + + write-logFileWarning "ProximityPlacementGroups, AvailabilitySets and VmssFlex are removed" ` + "Use RGCOPY parameter 'ignoreTags' for preventing this" + } + $script:tipVMs = convertTo-array (($script:copyVMs.values | Where-Object Group -gt 0).Name) if ($script:tipVMs.count -ne 0) { $script:skipProximityPlacementGroup = $True @@ -5632,98 +5590,32 @@ function update-NICs { #-------------------------------------------------------------- function update-SKUs { #-------------------------------------------------------------- - set-parameter 'setLoadBalancerSku' $setLoadBalancerSku 'Microsoft.Network/loadBalancers' # process loadBalancers $script:resourcesALL | Where-Object type -eq 'Microsoft.Network/loadBalancers' | ForEach-Object -Process { - $value = $script:paramValues[$_.name] - if ($Null -ne $value) { - test-values 'setLoadBalancerSku' $value @('Basic', 'Standard') 'sku' - - $old = $Null - if ($Null -ne $_.sku) { - $old = $_.sku.name - } - - if ($old -ne $value) { - write-logFileUpdates 'loadBalancers' $_.name 'set SKU' $value - $_.sku = @{ name = $value } - $script:countLoadBalancerSku++ - } - else { - write-logFileUpdates 'loadBalancers' $_.name 'keep SKU' $value - } + if ($_.sku.name -ne 'Standard') { + write-logFileUpdates 'loadBalancers' $_.name 'set SKU' 'Standard' + $_.sku = @{ name = 'Standard' } } } - set-parameter 'setPublicIpSku' $setPublicIpSku 'Microsoft.Network/publicIPAddresses' - # process publicIPAddresses - $script:resourcesALL - | Where-Object type -eq 'Microsoft.Network/publicIPAddresses' - | ForEach-Object -Process { + # # remove SKU from bastionHosts (used to be required during rollout of SKU) + # $script:resourcesALL + # | Where-Object type -eq 'Microsoft.Network/bastionHosts' + # | ForEach-Object -Process { - $value = $script:paramValues[$_.name] - if ($Null -ne $value) { - test-values 'setPublicIpSku' $value @('Basic', 'Standard') 'sku' - - $old = $Null - if ($Null -ne $_.sku) { - $old = $_.sku.name - } - - if ($old -ne $value) { - write-logFileUpdates 'publicIPAddresses' $_.name 'set SKU' $value - $_.sku = @{ name = $value } - } - else { - write-logFileUpdates 'publicIPAddresses' $_.name 'keep SKU' $value - } - } - } - - # remove SKU from bastionHosts - $script:resourcesALL - | Where-Object type -eq 'Microsoft.Network/bastionHosts' - | ForEach-Object -Process { - - if ($_.sku.count -ne 0) { - $_.sku = $Null - write-logFileUpdates 'bastionHosts' $_.name 'delete Sku' '' '' '(SKU not supported in all regions)' - } - } + # if ($_.sku.count -ne 0) { + # $_.sku = $Null + # write-logFileUpdates 'bastionHosts' $_.name 'delete Sku' '' '' '(SKU not supported in all regions)' + # } + # } } #-------------------------------------------------------------- function update-IpAllocationMethod { #-------------------------------------------------------------- - set-parameter 'setPublicIpAlloc' $setPublicIpAlloc 'Microsoft.Network/publicIPAddresses' - # process publicIPAddresses - $script:resourcesALL - | Where-Object type -eq 'Microsoft.Network/publicIPAddresses' - | ForEach-Object -Process { - - $value = $script:paramValues[$_.name] - if ($Null -ne $value) { - test-values 'setPublicIpAlloc' $value @('Dynamic', 'Static') 'allocation type' - - if ($_.properties.publicIPAllocationMethod -ne $value) { - $_.properties.publicIPAllocationMethod = $value - write-logFileUpdates 'publicIPAddresses' $_.name 'set Allocation Method' $value - } - else { - write-logFileUpdates 'publicIPAddresses' $_.name 'keep Allocation Method' $value - } - } - - # remove IP Address VALUE (for Static AND Dynamic) - if ($_.properties.ContainsKey('ipAddress')) { - write-logFileUpdates 'publicIPAddresses' $_.name 'delete old ipAddress' '' '' $_.properties.ipAddress - $_.properties.Remove('ipAddress') - } - } - set-parameter 'setPrivateIpAlloc' $setPrivateIpAlloc 'Microsoft.Network/networkInterfaces' # process networkInterfaces $script:resourcesALL @@ -6148,64 +6040,88 @@ function update-FQDN { } #-------------------------------------------------------------- -function new-vmssFlex { +function get-vmssFlex { #-------------------------------------------------------------- - # fill [hashtable] $script:paramValues - set-parameter 'createVmssFlex ' $createVmssFlex 'Microsoft.Compute/virtualMachines' - $script:vmssProperties = @{} - $deleted = @() - $remaining = @() - $created = @() - $max = $script:MaxRegionFaultDomains - - #-------------------------------------------------------------- - # remove vmss + $script:deletedVmss = @() + $script:resourcesALL | Where-Object type -eq 'Microsoft.Compute/virtualMachineScaleSets' | ForEach-Object { - if ($skipVmssFlex -or ($_.properties.orchestrationMode -ne 'Flexible')) { - write-logFileUpdates 'vmScaleSets' $_.name 'delete' - $deleted += $_.name + $vmssName = $_.name + $faultDomainCount = $_.properties.platformFaultDomainCount + if ($faultDomainCount -lt 2) { + $faultDomainCount = 1 } + + # remove unneeded VMSS + if ($skipVmssFlex -or ($_.properties.orchestrationMode -ne 'Flexible')) { + + write-logFileUpdates 'vmScaleSets' $_.name 'delete' + $script:deletedVmss += $_.name + } + + # get existing VMSS else { - $remaining += $_.name + $properties = "(FD Count=$faultDomainCount; Zones=$($_.zones -as [string]))" + + write-logFileUpdates 'vmScaleSets' $_.name 'keep' $properties + + # save properties of existing VMSS + $script:vmssProperties[$vmssName] = @{ + name = $vmssName + faultDomainCount = $faultDomainCount + zones = $_.zones + } } } - if (($remaining.count -ne 0) -and ('setVmZone' -notin $boundParameterNames)) { + if (($script:vmssPropertie.count -ne 0) -and ('setVmZone' -notin $boundParameterNames)) { # write-logFileWarning "VM Scale Sets exists and parameter 'setVmZone' is not set" ` # "Default value 'none' of parameter 'setVmZone' is not used" $script:setVmZone = @() } - # delete resource - foreach ($vmss in $deleted) { + # delete unneeded resources + foreach ($vmss in $script:deletedVmss) { remove-resources 'Microsoft.Compute/virtualMachineScaleSets' $vmss } # vmss FLEX does not have this subresource (although it is exported from source RG) remove-resources 'Microsoft.Compute/virtualMachineScaleSets/virtualMachines' - # update VMs (remove vmss) + #-------------------------------------------------------------- + # update VMs for VMSS Flex $script:resourcesALL | Where-Object type -eq 'Microsoft.Compute/virtualMachines' | ForEach-Object { + $vmName = $_.name $id = $_.properties.virtualMachineScaleSet.id if ($Null -ne $id) { - $vmss = (get-resourceComponents $id).mainResourceName - if ($vmss -in $deleted) { + $vmssName = (get-resourceComponents $id).mainResourceName - write-logFileUpdates 'virtualMachines' $_.name 'remove vmScaleSet' + # vmss not found in same resource group + if ($vmssName -notin $script:vmssProperties.values.name) { + # remove vmss from VM $_.properties.virtualMachineScaleSet = $Null - $_.properties.platformFaultDomain = $Null $_.dependsOn = remove-dependencies $_.dependsOn 'Microsoft.Compute/virtualMachineScaleSets' } + + # save VMSS name + else { + $script:copyVMs[$vmName].VmssName = $vmssName + } } } +} - #-------------------------------------------------------------- - # create new vmssFlex +#-------------------------------------------------------------- +function new-vmssFlex { +#-------------------------------------------------------------- + $createdVmss = @() + + # fill [hashtable] $script:paramValues + set-parameter 'createVmssFlex ' $createVmssFlex 'Microsoft.Compute/virtualMachines' foreach ($config in $script:paramAllConfigs) { $vmssName = $config.paramConfig1 @@ -6233,15 +6149,15 @@ function new-vmssFlex { $numDomains = 1 } elseif ($faultDomains -eq 'max') { - $numDomains = $max + $numDomains = $script:MaxRegionFaultDomains } else { $numDomains = $faultDomains -as [int] } - if ($numDomains -gt $max) { - write-logFileWarning "Region '$targetLocation' only supports $max fault domains" - $numDomains = $max + if ($numDomains -gt $script:MaxRegionFaultDomains) { + write-logFileWarning "Region '$targetLocation' only supports $script:MaxRegionFaultDomains fault domains" + $numDomains = $script:MaxRegionFaultDomains } # create ARM resource @@ -6266,102 +6182,254 @@ function new-vmssFlex { $res.zones = $zoneArray } - # save properties for setMergeVMs + # save ARM resource + if (($vmssName -notin $createdVmss) -and ($setVmMerge.count -eq 0)) { + $createdVmss += $vmssName + $properties = "(FD Count=$numDomains; Zones=$($zoneArray -as [string]))" + write-logFileUpdates 'vmScaleSets' $vmssName 'create' $properties + [array] $script:resourcesALL += $res + } + + # save properties of new VMSS $script:vmssProperties[$vmssName] = @{ + name = $vmssName faultDomainCount = $numDomains zones = $zoneArray } - # VMSS has not been already created - # (the same name might occur 2 times in the RGCOPY array-parameter) - if (($vmssName -notin $created) -and ($setVmMerge.count -eq 0)) { - $created += $vmssName - write-logFileUpdates 'vmScaleSets' $vmssName 'create' - [array] $script:resourcesALL += $res + # update VMs with new vmss + $script:resourcesALL + | Where-Object type -eq 'Microsoft.Compute/virtualMachines' + | ForEach-Object { + + $vmName = $_.name + $vmssName, $x = $script:paramValues[$vmName] -split '/' + + if ($vmssName.length -ne 0) { + + # get ID function + $vmssID = get-resourceFunction ` + 'Microsoft.Compute' ` + 'virtualMachineScaleSets' $vmssName + + # set property + $_.properties.virtualMachineScaleSet = @{ id = $vmssID } + + # add new dependency + [array] $_.dependsOn += $vmssID + + # save VMSS name + $script:copyVMs[$vmName].VmssName = $vmssName + } } } +} - #-------------------------------------------------------------- - # update VMs with new vmss - # zone will be set by parameter setVmZone - # platformFaultDomain will be set by parameter setVmFaultDomain - $script:resourcesALL - | Where-Object type -eq 'Microsoft.Compute/virtualMachines' - | ForEach-Object { - - $vmName = $_.name - $vmssName, $x = $script:paramValues[$vmName] -split '/' - - if ($vmssName.length -ne 0) { - - # get ID function - $vmssID = get-resourceFunction ` - 'Microsoft.Compute' ` - 'virtualMachineScaleSets' $vmssName - - # set property - $_.properties.virtualMachineScaleSet = @{ id = $vmssID } - write-logFileUpdates 'virtualMachines' $vmName 'set vmScaleSet' $vmssName - - # add new dependency - [array] $_.dependsOn += $vmssID - } - } - - #-------------------------------------------------------------- +#-------------------------------------------------------------- +function update-faultDomainCount { +#-------------------------------------------------------------- # update fault domain count (of new and existing VMSS) $script:resourcesALL | Where-Object type -eq 'Microsoft.Compute/virtualMachineScaleSets' | ForEach-Object -Process { # check fault domain count - if ($_.properties.platformFaultDomainCount -gt $max ) { - write-logFileWarning "The maximum fault domain count in region '$targetLocation' is $max" -stopWhenForceVmChecks - write-logFileUpdates 'vmScaleSets' $_.name 'set faultDomainCount' $max - $_.properties.platformFaultDomainCount = $max + if ($_.properties.platformFaultDomainCount -gt $script:MaxRegionFaultDomains ) { + $_.properties.platformFaultDomainCount = $script:MaxRegionFaultDomains + $script:vmssProperties[$_.name].faultDomainCount = $script:MaxRegionFaultDomains + write-logFileWarning "The maximum fault domain count in region '$targetLocation' is $script:MaxRegionFaultDomains" -stopWhenForceVmChecks + write-logFileUpdates 'vmScaleSets' $_.name 'set faultDomainCount' $script:MaxRegionFaultDomains + } + } +} + +#-------------------------------------------------------------- +function update-vmFaultDomain { +#-------------------------------------------------------------- + # process RGCOPY parameter + set-parameter 'setVmFaultDomain' $setVmFaultDomain + get-ParameterRule + while ($Null -ne $script:paramConfig) { + + $faultDomain = $script:paramConfig + test-values 'setVmFaultDomain' $faultDomain @('none', '0', '1', '2') 'faultDomain' + # convert to internal syntax + if ($faultDomain -eq 'none') { + $faultDomain = -1 + } + $faultDomain = $faultDomain -as [int] + + $script:copyVMs.values + | Where-Object Name -in $script:paramVMs + | ForEach-Object { + + $_.PlatformFaultDomainNew = $faultDomain + } + get-ParameterRule + } + + $script:MseriesWithFaultDomain = $False + + # output of changes + $script:copyVMs.Values + | Where-Object Skip -ne $True + | Sort-Object Name + | ForEach-Object { + + $vmName = $_.Name + $vmssName = $_.VmssName + + $current = $_.PlatformFaultDomain + $wanted = $_.PlatformFaultDomainNew + if ($Null -eq $wanted) { + $wanted = $current } - # save properties - $script:vmssProperties[$_.name] = @{ - faultDomainCount = $_.properties.platformFaultDomainCount - zones = $_.zones + # check for vmss + if (($Null -eq $vmssName) -and ($wanted -ne -1)) { + write-logFileWarning "VM '$vmName' is not part of a VM Scale Set" + $wanted = -1 + } + + # check for maximum value + if ($Null -ne $vmssName) { + $max = $script:vmssProperties[$vmssName].faultDomainCount + if (($max -le 1) -and ($wanted -ne -1)) { + write-logFileWarning "VM Scale Set '$vmssName' does not support fault domains" + $wanted = -1 + } + if ($wanted -ge $max) { + write-logFileWarning "VM Scale Set '$vmssName' only supports $max fault domains" + $wanted = -1 + } + } + + # get M-series + if ($Null -ne $vmssName) { + if ($_.VmSize -like 'Standard_M*') { + # save VM size property + $script:vmssProperties[$vmssName].SeriesM = $True + + if ($wanted -ne -1) { + $script:MseriesWithFaultDomain = $True + } + } + else { + # save VM size property + $script:vmssProperties[$vmssName].SeriesOther = $True + } + } + + + # update + $_.PlatformFaultDomain = $wanted + + # output + if ($current -ne $wanted) { + $action = 'set' + } + else { + $action = 'keep' + } + if ($_.PlatformFaultDomain -eq -1) { + write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" 'none' -defaultValue + } + else { + write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" $_.PlatformFaultDomain } } - # update fault domain in VMs + # update VM ARM resources $script:resourcesALL | Where-Object type -eq 'Microsoft.Compute/virtualMachines' | ForEach-Object { - $vmName = $_.name - $current = $_.properties.platformFaultDomain - if ($Null -eq $current) { - $current = -1 - } - $current = $current -as [int] + $vmName = $_.name + $platformFaultDomain = $script:copyVMs[$vmName].PlatformFaultDomain - # check fault domain count - if ($current -ge $max) { - write-logFileWarning "Region '$targetLocation' only supports $max Fault Domains" - write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" 'none' -defaultValue + # update platformFaultDomain + if ($platformFaultDomain -lt 0) { $_.properties.platformFaultDomain = $Null - $script:copyVMs[$vmName].PlatformFaultDomain = -1 } - if (($current -ge 0) -and ($Null -eq $_.properties.virtualMachineScaleSet.id)){ - write-logFileWarning "VM '$vmName' is not member of a VM Scale Set" - write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" 'none' -defaultValue - $_.properties.platformFaultDomain = $Null - $script:copyVMs[$vmName].PlatformFaultDomain = -1 - } - - # save vmss name - if ($Null -ne $_.properties.virtualMachineScaleSet.id) { - $vmssName = (get-resourceComponents $_.properties.virtualMachineScaleSet.id).mainResourceName - $script:copyVMs[$vmName].VmssName = $vmssName + else{ + $_.properties.platformFaultDomain = $platformFaultDomain } } } +#-------------------------------------------------------------- +function set-singlePlacementGroup { +#-------------------------------------------------------------- + $script:seriesMixed = $False + + #-------------------------------------------------------------- + # set singlePlacementGroup + $script:resourcesALL + | Where-Object type -eq 'Microsoft.Compute/virtualMachineScaleSets' + | ForEach-Object -Process { + + $vmssName = $_.name + $singlePG = $Null + + if ($_.properties.platformFaultDomainCount -gt 1) { + if ($script:vmssProperties[$vmssName].SeriesOther -eq $True) { + $singlePG = $False + if ($script:vmssProperties[$vmssName].SeriesM -eq $True) { + $script:seriesMixed = $True + } + } + } + + if ('singlePlacementGroup' -in $boundParameterNames) { + test-values 'singlePlacementGroup' $singlePlacementGroup @($Null, $True, $False) + $singlePG = $singlePlacementGroup + } + elseif ($singlePG -eq $False) { + write-logFileWarning "setting 'singlePlacementGroup' of VMSS '$vmssName' to false because of used VM size" ` + "You can override this by using RGCOPY parameter 'singlePlacementGroup'" + } + + $_.properties.singlePlacementGroup = $singlePG + $script:vmssProperties[$vmssName].singlePlacementGroup = $_.properties.singlePlacementGroup + $script:vmssProperties[$vmssName].platformFaultDomainCount = $_.properties.platformFaultDomainCount + } + + if ($script:MseriesWithFaultDomain -eq $True) { + write-logFileWarning "M-series VMs do CURRENTLY not support setting fault domain" ` + "Use parameter 'setVmFaultDomain' for setting fault domain to 'none'" + } + + if ($script:seriesMixed) { + write-logFileWarning "VMSS Flex (with FD Count >1) does CURRENTLY not support mixing M-Series VMs with other VMs" + } + + #-------------------------------------------------------------- + # save singlePlacementGroup in copyVMs for later output + $script:copyVMs.Values + | Where-Object {$Null -ne $_.VmssName} + | ForEach-Object { + + $_.singlePlacementGroup = $script:vmssProperties[$_.VmssName].singlePlacementGroup + $_.platformFaultDomainCount = $script:vmssProperties[$_.VmssName].platformFaultDomainCount + } + + #-------------------------------------------------------------- + # output of VMSS + $script:copyVMs.Values + | Where-Object {$Null -ne $_.VmssName} + | Sort-Object VmssName, Name + | Select-Object ` + @{label="VMSS name"; expression={get-shortOutput (write-secureString $_.VmssName) 16}}, ` + @{label="VM name"; expression={get-shortOutput (write-secureString $_.Name) 42}}, ` + @{label="Size"; expression={$_.VmSize}}, ` + @{label="Zone"; expression={get-replacedOutput $_.VmZone 0}}, ` + @{label="Fault Domain"; expression={get-replacedOutput $_.PlatformFaultDomain -1}}, ` + @{label="FD Count"; expression={$_.platformFaultDomainCount}}, ` + @{label="singlePlacementGroup"; expression={get-replacedOutput $_.singlePlacementGroup $Null}} + | Format-Table + | write-LogFilePipe +} + #-------------------------------------------------------------- function new-proximityPlacementGroup { #-------------------------------------------------------------- @@ -6371,7 +6439,7 @@ function new-proximityPlacementGroup { 'Microsoft.Compute/availabilitySets' ` 'Microsoft.Compute/virtualMachineScaleSets' -ignoreMissingResources $script:ppgOfAvset = @{} - $created = @() + $createdPPG = @() #-------------------------------------------------------------- # remove all ProximityPlacementGroups @@ -6434,8 +6502,8 @@ function new-proximityPlacementGroup { # PPG has not been already created # (the same name might occur 2 times in the RGCOPY array-parameter) - if (($ppgName -notin $created) -and ($setVmMerge.count -eq 0)) { - $created += $ppgName + if (($ppgName -notin $createdPPG) -and ($setVmMerge.count -eq 0)) { + $createdPPG += $ppgName write-logFileUpdates 'proximityPlacementGroups' $ppgName 'create' [array] $script:resourcesALL += $res } @@ -6447,8 +6515,8 @@ function new-availabilitySet { #-------------------------------------------------------------- # fill [hashtable] $script:paramValues set-parameter 'createAvailabilitySet' $createAvailabilitySet 'Microsoft.Compute/virtualMachines' - $deleted = @() - $created = @() + $deletedAvSet = @() + $createdAvSet = @() #-------------------------------------------------------------- # remove avsets @@ -6458,12 +6526,12 @@ function new-availabilitySet { if ($skipAvailabilitySet -or ($_.name -like 'rgcopy.tipGroup*')) { write-logFileUpdates 'availabilitySets' $_.name 'delete' - $deleted += $_.name + $deletedAvSet += $_.name } } # delete resource - foreach ($asName in $deleted) { + foreach ($asName in $deletedAvSet) { remove-resources 'Microsoft.Compute/availabilitySets' $asName } @@ -6475,7 +6543,7 @@ function new-availabilitySet { $id = $_.properties.availabilitySet.id if ($Null -ne $id) { $asName = (get-resourceComponents $id).mainResourceName - if ($asName -in $deleted) { + if ($asName -in $deletedAvSet) { write-logFileUpdates 'virtualMachines' $_.name 'remove availabilitySet' $_.properties.availabilitySet = $Null @@ -6528,8 +6596,8 @@ function new-availabilitySet { # AvSet has not been already created # (the same name might occur 2 times in the RGCOPY array-parameter) - if (($asName -notin $created) -and ($setVmMerge.count -eq 0)) { - $created += $asName + if (($asName -notin $createdAvSet) -and ($setVmMerge.count -eq 0)) { + $createdAvSet += $asName write-logFileUpdates 'availabilitySets' $asName 'create' [array] $script:resourcesALL += $res } @@ -6542,11 +6610,10 @@ function new-availabilitySet { | ForEach-Object { # check fault domain count - $max = $script:MaxRegionFaultDomains - if ($_.properties.platformFaultDomainCount -gt $max ) { - write-logFileWarning "The maximum fault domain count in region '$targetLocation' is $max" -stopWhenForceVmChecks - write-logFileUpdates 'availabilitySets' $_.name 'set faultDomainCount' $max - $_.properties.platformFaultDomainCount = $max + if ($_.properties.platformFaultDomainCount -gt $script:MaxRegionFaultDomains ) { + write-logFileWarning "The maximum fault domain count in region '$targetLocation' is $script:MaxRegionFaultDomains" -stopWhenForceVmChecks + write-logFileUpdates 'availabilitySets' $_.name 'set faultDomainCount' $script:MaxRegionFaultDomains + $_.properties.platformFaultDomainCount = $script:MaxRegionFaultDomains } } @@ -6708,100 +6775,6 @@ function update-proximityPlacementGroup { } } -#-------------------------------------------------------------- -function update-vmFaultDomain { -#-------------------------------------------------------------- - # process RGCOPY parameter - set-parameter 'setVmFaultDomain' $setVmFaultDomain - get-ParameterRule - while ($Null -ne $script:paramConfig) { - - $faultDomain = $script:paramConfig - test-values 'setVmFaultDomain' $faultDomain @('none', '0', '1', '2') 'faultDomain' - # convert to internal syntax - if ($faultDomain -eq 'none') { - $faultDomain = -1 - } - $faultDomain = $faultDomain -as [int] - - $script:copyVMs.values - | Where-Object Name -in $script:paramVMs - | ForEach-Object { - - $_.PlatformFaultDomainNew = $faultDomain - } - get-ParameterRule - } - - # output of changes - $script:copyVMs.Values - | Where-Object Skip -ne $True - | Sort-Object Name - | ForEach-Object { - - $vmName = $_.Name - $vmssName = $_.VmssName - - $current = $_.PlatformFaultDomain - $wanted = $_.PlatformFaultDomainNew - if ($Null -eq $wanted) { - $wanted = $current - } - - # check for vmss - if (($Null -eq $vmssName) -and ($wanted -ne -1)) { - write-logFileWarning "VM '$vmName' is not part of a VM Scale Set" - $wanted = -1 - } - - if ($Null -ne $vmssName) { - # check for maximum value - $max = $script:vmssProperties[$vmssName].faultDomainCount - - if ($wanted -ge $max) { - write-logFileWarning "VM Scale Set '$vmssName' only supports $max fault domains" - $wanted = -1 - } - elseif (($max -eq 1) -and ($wanted -ne -1)) { - write-logFileWarning "VM Scale Set '$vmssName' does not support fault domains" - $wanted = -1 - } - } - - # update - if ($current -ne $wanted) { - $_.PlatformFaultDomain = $wanted - $action = 'set' - } - else { - $action = 'keep' - } - # output - if ($_.PlatformFaultDomain -eq -1) { - write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" 'none' -defaultValue - } - else { - write-logFileUpdates 'virtualMachines' $vmName "$action fault domain" $_.PlatformFaultDomain - } - } - - # update virtualMachines - $script:resourcesALL - | Where-Object type -eq 'Microsoft.Compute/virtualMachines' - | ForEach-Object { - - $vmName = $_.name - $platformFaultDomain = $script:copyVMs[$vmName].PlatformFaultDomain - - if ($platformFaultDomain -eq -1) { - $_.properties.platformFaultDomain = $Null - } - else{ - $_.properties.platformFaultDomain = $platformFaultDomain - } - } -} - #-------------------------------------------------------------- function update-vmZone { #-------------------------------------------------------------- @@ -8291,8 +8264,6 @@ function new-greenlist { add-greenlist 'Microsoft.Network/networkSecurityGroups' '*' add-greenlist 'Microsoft.Network/networkSecurityGroups/securityRules' '*' add-greenlist 'Microsoft.Network/bastionHosts' '*' - add-greenlist 'Microsoft.HanaOnAzure/sapMonitors' '*' - add-greenlist 'Microsoft.HanaOnAzure/sapMonitors/providerInstances' '*' # will be always removed by RGCOPY: # only added to green list to prevent warning @@ -9321,30 +9292,6 @@ function update-remoteSourceRG { update-remoteSourceIP 'Microsoft.Network/networkInterfaces' 'ipConfigurations' update-remoteSourceIP 'Microsoft.Network/loadBalancers' 'frontendIPConfigurations' update-remoteSourceIP 'Microsoft.Network/bastionHosts' 'ipConfigurations' - - # sapMonitors - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors' - | ForEach-Object -Process { - - if ($_.properties.monitorSubnet -like '/*') { - - # convert resource ID to to resource function - $r = get-resourceComponents $_.properties.monitorSubnet - - # convert name - $resKey = "virtualNetworks/$($r.resourceGroup)/$($r.mainResourceName)" - $vnetName = $script:remoteNames[$resKey].newName - $resFunction = get-resourceFunction ` - 'Microsoft.Network' ` - 'virtualNetworks' $vnetName ` - 'subnets' $r.subResourceName - - # set ID and dependency - $_.properties.monitorSubnet = $resFunction - [array] $_.dependsOn += $resFunction - } - } } #-------------------------------------------------------------- @@ -9525,13 +9472,11 @@ function new-templateTarget { # count parameter changes caused by default values: $script:countDiskSku = 0 $script:countVmZone = 0 - $script:countLoadBalancerSku = 0 $script:countPrivateIpAlloc = 0 $script:countAcceleratedNetworking = 0 # do not count modifications if parameter was supplied explicitly if ('setDiskSku' -in $boundParameterNames) { $script:countDiskSku = -999999} if ('setVmZone' -in $boundParameterNames) { $script:countVmZone = -999999} - if ('setLoadBalancerSku' -in $boundParameterNames) { $script:countLoadBalancerSku = -999999} if ('setPrivateIpAlloc' -in $boundParameterNames) { $script:countPrivateIpAlloc = -999999} if ('setAcceleratedNetworking' -in $boundParameterNames) { $script:countAcceleratedNetworking = -999999} @@ -9564,15 +9509,6 @@ function new-templateTarget { update-paramSetDiskCaching update-paramSetAcceleratedNetworking - # change LOCATION - $script:resourcesALL - | Where-Object type -ne 'Microsoft.HanaOnAzure/sapMonitors' - | ForEach-Object -Process { - if ($_.location.length -ne 0) { - $_.location = $targetLocation - } - } - # remove identity $script:resourcesALL | ForEach-Object -Process { @@ -9597,6 +9533,24 @@ function new-templateTarget { } } + # set publicIPAddresses Standard/Static: needed in newer APIs for VMs in Availability Zone + $script:resourcesALL + | Where-Object type -eq 'Microsoft.Network/publicIPAddresses' + | ForEach-Object -Process { + + if ($_.sku.name -ne 'Standard') { + $_.sku = @{ name = 'Standard' } + write-logFileUpdates 'publicIPAddresses' $_.name 'set SKU' 'Standard' + } + if ($_.properties.publicIPAllocationMethod -ne 'Static') { + $_.properties.publicIPAllocationMethod = 'Static' + write-logFileUpdates 'publicIPAddresses' $_.name 'set AllocationMethod' 'Static' + } + if ($Null -ne $_.properties.ipAddress) { + $_.properties.ipAddress = $Null + } + } + # remove tags $script:resourcesALL | ForEach-Object -Process { @@ -9626,19 +9580,11 @@ function new-templateTarget { $script:snapshotNames = convertTo-array (($script:resourcesALL | Where-Object ` type -eq 'Microsoft.Compute/snapshots').name) - # save AMS - $script:supportedProviders = @('SapHana', 'MsSqlServer', 'PrometheusOS', 'PrometheusHaCluster') # 'SapNetweaver' not supported yet - $script:resourcesAMS = convertTo-array (($script:resourcesALL | Where-Object { ` - ($_.type -eq 'Microsoft.HanaOnAzure/sapMonitors') ` - -or (($_.type -eq 'Microsoft.HanaOnAzure/sapMonitors/providerInstances') -and ($_.properties.type -in $script:supportedProviders)) ` - })) - update-skipVMs remove-resources 'Microsoft.Storage/storageAccounts*' remove-resources 'Microsoft.Compute/snapshots' remove-resources 'Microsoft.Compute/disks' remove-resources 'Microsoft.Compute/images*' - remove-resources 'Microsoft.HanaOnAzure/sapMonitors*' remove-resources 'Microsoft.Compute/virtualMachines/extensions' remove-resources 'Microsoft.Network/loadBalancers/backendAddressPools' remove-resources 'Microsoft.Compute/virtualMachines' $script:skipVMs @@ -9656,13 +9602,11 @@ function new-templateTarget { $script:resourcesALL = convertTo-array ($script:resourcesALL | Where-Object { ` ($_.type -eq 'Microsoft.Compute/virtualMachines') ` -and ($_.name -in $script:mergeVMs) }) - - $script:resourcesAMS = @() } update-netApp - if ($script:MaxRegionFaultDomains -eq 1) { + if ($script:MaxRegionFaultDomains -lt 2) { write-logFileWarning "Region '$targetLocation' does not support VM Scale Sets Flexible" $script:skipVmssFlex = $True $script:createVmssFlex = @() @@ -9680,12 +9624,18 @@ function new-templateTarget { # create PPG before AvSet and vmssFlex new-proximityPlacementGroup - new-vmssFlex + + $script:vmssProperties = @{} + get-vmssFlex # get or remove existing VMSS + new-vmssFlex # after removing ALL existing VMSS + update-faultDomainCount + update-vmZone + update-vmFaultDomain + set-singlePlacementGroup + new-availabilitySet update-proximityPlacementGroup - update-vmZone update-diskZone - update-vmFaultDomain update-vmTipGroup update-vmSize @@ -9698,9 +9648,6 @@ function new-templateTarget { update-subnets update-networkPeerings - # when creating targetAms template, the target template will be changed, too (region of vnet) - new-templateTargetAms - update-SKUs update-IpAllocationMethod update-securityRules @@ -9751,7 +9698,6 @@ function new-templateTarget { if ($copyMode) { if ($script:countDiskSku -gt 0) { write-changedByDefault "setDiskSku = $setDiskSku" } if ($script:countVmZone -gt 0) { write-changedByDefault "setVmZone = $setVmZone" } - if ($script:countLoadBalancerSku -gt 0) { write-changedByDefault "setLoadBalancerSku = $setLoadBalancerSku" } if ($script:countPrivateIpAlloc -gt 0) { write-changedByDefault "setPrivateIpAlloc = $setPrivateIpAlloc" } if ($script:countAcceleratedNetworking -gt 0) { write-changedByDefault "setAcceleratedNetworking = $setAcceleratedNetworking" } write-logFile @@ -9901,71 +9847,6 @@ function update-skipVMs { # - $_.dependsOn } -#-------------------------------------------------------------- -function get-amsProviderProperty { -#-------------------------------------------------------------- - param ( - [string] $string - ) - - $property = @{} - if ($string.length -eq 0) { - return $property - } - - $all = $string -split '"' - $i = 1 - while ($i -lt $all.Count) { - # get key - $key = $all[$i] - $i++ - if ($i -ge $all.Count) { break } - - # value is string - if (($all[$i] -replace '\s', '') -eq ':') { - $i++ - if ($i -ge $all.Count) { break } - $value = $all[$i] - $property.$key = $value - $i += 2 - } - # value is numeric - else { - $value = $all[$i] -replace '\D', '' - $property.$key = $value -as [int32] - $i++ - } - } - - return $property -} - -#-------------------------------------------------------------- -function set-amsProviderProperty { -#-------------------------------------------------------------- - param ( - [hashtable] $hash - ) - - [string] $string = '' - $sep = '' - - $hash.GetEnumerator() - | ForEach-Object { - - $string += $sep - $sep = ',' - $string += "`"$($_.Key)`":" - if ($_.Value -is [string]) { - $string += "`"$($_.Value)`"" - } - else { - $string += "$($_.Value)" - } - } - return "{$string}" -} - #-------------------------------------------------------------- function update-vnetLocation { #-------------------------------------------------------------- @@ -10027,335 +9908,6 @@ function update-vnetLocation { } } -#-------------------------------------------------------------- -function new-templateTargetAms { -#-------------------------------------------------------------- - if (!$createArmTemplateAms) { - $script:resourcesAMS = @() - return - } - $locationsAMS = @('East US 2', 'West US 2', 'East US', 'West Europe') - - # Rename needed because of the following deployment error: - # keyvault.VaultsClient#CreateOrUpdate: Failure sending request - # "Exist soft deleted vault with the same name." - - # process AMS instance - $i = 0 - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors' - | ForEach-Object { - - if ($i++ -gt 0) { - write-logFileError 'Only one AMS Instance per resource group supported by RGCOPY' - } - - # rename AMS instance - write-logFileUpdates 'sapMonitors' $_.name 'rename to' '' '' ' (in AMS template)' - $_.name = "[parameters('amsInstanceName')]" - - # remove dependencies - $_.dependsOn = @() - - # use existing WS defined by parameters - if ( ($amsWsName.length -ne 0) ` - -and ($amsWsRG.length -ne 0) ) ` { - - $wsId = "/subscriptions/$targetSubID/resourceGroups/$amsWsRG/providers/Microsoft.OperationalInsights/workspaces/$amsWsName" - $_.properties.logAnalyticsWorkspaceArmId = $wsId - } - # create new WS in Managed Resource Group - elseif (!$amsWsKeep) { - $_.properties.logAnalyticsWorkspaceArmId = $Null - } - # keep existing WS - else { - if ($sourceSub -ne $targetSub) { - write-logFileWarning "Cannot keep AMS Analytical Workspace when copying to different subscription" - $_.properties.logAnalyticsWorkspaceArmId = $Null - } - } - - # https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/azure-monitor-overview - if ($amsShareAnalytics) { - $_.properties.enableCustomerAnalytics = $True - write-logFileWarning 'Sharing AMS Customer Analytics Data with Microsoft' - } - else { - $_.properties.enableCustomerAnalytics = $False - } - - # set AMS location - $amsLocationOld = $_.location - $monitorVnet = (get-resourceComponents $_.properties.monitorSubnet).mainResourceName - - # AMS vnet is peered - if ($monitorVnet -in $script:peeredVnets) { - # change VNET location - write-logFileUpdates 'sapMonitors' '' 'set location' $amsLocationOld - update-vnetLocation $monitorVnet $amsLocationOld - } - - # AMS vnet is not peered - else { - # change AMS location - write-logFileUpdates 'sapMonitors' '' 'set location' $targetLocation - $_.location = $targetLocation - } - - $script:amsLocationNew = $_.location - } - - # no AMS instance found - if ($i -eq 0) { - $script:resourcesAMS = @() - return - } - - # Module installed? - if ($amsUsePowerShell) { - $azHoaVersion = (Get-InstalledModule Az.HanaOnAzure -MinimumVersion 0.3 -ErrorAction 'SilentlyContinue') - if ($azHoaVersion.count -eq 0) { - write-logFileError 'Minimum required version of module Az.HanaOnAzure is 0.3' ` - 'Run "Install-Module -Name Az.HanaOnAzure -AllowClobber" to install or update' - } - } - - # AMS not supported in region - $amsLocationDisplayName = (Get-AzLocation | Where-Object Location -eq $script:amsLocationNew).DisplayName - if ($amsLocationDisplayName -notin $locationsAMS) { - write-logFileWarning "AMS not supported in region $script:amsLocationNew ($amsLocationDisplayName)" - write-logFileWarning "Update source RG $sourceRG`: Use Network Peering and locate AMS in a peered, supported region" - $script:resourcesAMS = @() - return - } - - # rename AMS providers - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors/providerInstances' - | ForEach-Object { - - $x, $providerName = $_.name -split '/' - $_.name = "[concat(parameters('amsInstanceName'),'/$providerName')]" - - [array] $_.dependsOn = get-resourceFunction ` - 'Microsoft.HanaOnAzure' ` - 'sapMonitors' "parameters('amsInstanceName')" - } - - # set HANA/SQL password - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors/providerInstances' - | Where-Object {$_.properties.type -in ('SapHana','MsSqlServer')} - | ForEach-Object { - - if ($amsUsePowerShell) { - $dbPass = '' - } - elseif ($dbPassword.Length -eq 0) { - write-logFileWarning "RGCOPY parameter 'dbPassword' missing for AMS provider type $($_.properties.type)" - $dbPass = '' - } - else { - write-logFileWarning "RGCOPY parameter 'dbPassword' stored as plain text in ARM template" - $dbPass = ConvertFrom-SecureString -SecureString $dbPassword -AsPlainText - } - - if ($dbPass -like '*"*') { - write-logFileWarning "RGCOPY parameter 'dbPassword' must not contain double quotes (`")" - $dbPass = '' - } - - $instanceProperty = get-amsProviderProperty $_.properties.properties - if ($_.properties.type -eq 'SapHana') {$instanceProperty.hanaDbPassword = $dbPass} - if ($_.properties.type -eq 'MsSqlServer') {$instanceProperty.sqlPassword = $dbPass} - $_.properties.properties = set-amsProviderProperty $instanceProperty - - $metadata = get-amsProviderProperty $_.properties.metadata - if ($metadata.count -ne 0) {$_.properties.metadata = set-amsProviderProperty $metadata} - } - - # set parameters - $templateParameters = @{ - amsInstanceName = @{ - defaultValue = "[concat('ams-',toLower(take(concat(replace(replace(replace(replace(replace(resourceGroup().name,'_',''),'-',''),'.',''),'(',''),')',''),'4rgcopy'),24)))]" - type = 'String' - } - } - write-logFileUpdates 'template parameter' '' 'create' '' '' '(in AMS template)' - - # create template - $script:amsTemplate = @{ - contentVersion = '1.0.0.0' - parameters = $templateParameters - resources = $script:resourcesAMS - } - $script:amsTemplate.Add('$schema', "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#") -} - -#-------------------------------------------------------------- -function new-amsProviders { -#-------------------------------------------------------------- - # get providers from AMS template - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors/providerInstances' - | ForEach-Object { - - $providerType = $_.properties.type - $providerName = $_.name - - if ($providerType -in $script:supportedProviders) { - - # provider name using ARM template variable - if ($providerName[0] -eq '[') { - - [array] $a = $providerName -split '\)' - if ($a.count -gt 1) { - [array] $b = $a[1] -split "'" - if ($b.count -gt 1) { - [array] $c = $b[1] -split '/' - if ($c.count -gt 1) { - $providerName = $c[1] - } - else { - write-logFileError "Invalid ARM template for AMS provider" ` - "Invalid provider name '$providerName'" - } - } - } - } - # provider name as plain text: / - else { - [array] $a = $providerName -split '/' - if ($a.count -gt 1) { - $providerName = $a[1] - } - else { - write-logFileError "Invalid ARM template for AMS provider" ` - "Invalid provider name '$providerName'" - } - } - - $parameter = @{ - ResourceGroupName = $targetRG - SapMonitorName = $amsInstanceName - Name = $providerName - ProviderType = $providerType - } - - # get instance properties - $instanceProperty = get-amsProviderProperty $_.properties.properties - if ($providerType -in ('SapHana','MsSqlServer')) { - if ($dbPassword.Length -eq 0) { - write-logFileWarning "Resource Group contains AMS provider type $providerType, but RGCOPY parameter 'dbPassword' is misssing" - $dbPass = '' - } - else { - $dbPass = ConvertFrom-SecureString -SecureString $dbPassword -AsPlainText - } - if ($providerType -eq 'SapHana') { - $instanceProperty.hanaDbPassword = $dbPass - } - if ($providerType -eq 'MsSqlServer') { - $instanceProperty.sqlPassword = $dbPass - } - } - $parameter.Add('InstanceProperty', $instanceProperty) - - # get instance meta data - $metadata = get-amsProviderProperty $_.properties.metadata - if ($metadata.count -ne 0) { - $parameter.Add('Metadata', $metadata) - } - - Write-Output "Creating AMS Provider '$providerName' ..." - write-logFileHashTable $parameter - - # create AMS Provider - $res = New-AzSapMonitorProviderInstance @parameter - if ($res.ProvisioningState -ne 'Succeeded') { - write-logFileError "Creation of AMS Provider '$providerName' failed" ` - "Check the Azure Activity Log in resource group $targetRG" - } - } - } -} - -#-------------------------------------------------------------- -function new-amsInstance { -#-------------------------------------------------------------- - $parameter = @{ - ResourceGroupName = $targetRG - Name = $amsInstanceName - ErrorAction = 'SilentlyContinue' - ErrorVariable = '+myDeploymentError' - } - - # get parameters from AMS template (single AMS instance) - $script:resourcesAMS - | Where-Object type -eq 'Microsoft.HanaOnAzure/sapMonitors' - | ForEach-Object { - - if ($_.properties.enableCustomerAnalytics -eq $True) { - $parameter.EnableCustomerAnalytic = $True - } - - $parameter.Location = $_.location - - $r = get-resourceComponents $_.properties.monitorSubnet - $parameter.MonitorSubnet = get-resourceString ` - $targetSubID $targetRG ` - 'Microsoft.Network' ` - 'virtualNetworks' $r.mainResourceName ` - 'subnets' $r.subResourceName - - $wsID = $_.properties.logAnalyticsWorkspaceArmId - if ($Null -ne $wsID) { - - $r = get-resourceComponents $wsID - $wsName = $r.mainResourceName - $wsRG = $r.resourceGroup - - $ws = Get-AzOperationalInsightsWorkspace ` - -ResourceGroupName $wsRG ` - -Name $wsName - - $wsKey = Get-AzOperationalInsightsWorkspaceSharedKey ` - -ResourceGroupName $wsRG ` - -Name $wsName - - $parameter.Add('LogAnalyticsWorkspaceSharedKey', $wsKey.PrimarySharedKey) - $parameter.Add('LogAnalyticsWorkspaceId', $ws.CustomerId) - $parameter.Add('LogAnalyticsWorkspaceResourceId', $ws.ResourceId) - } - } - - write-logFile "Creating AMS Instance '$amsInstanceName' ..." - write-logFileHashTable $parameter - - # create AMS Instance - $res = New-AzSapMonitor @parameter - if ($res.ProvisioningState -ne 'Succeeded') { - - $NewName = get-NewName $amsInstanceName 30 - write-logFile $myDeploymentError -ForegroundColor 'Yellow' - write-logFile - write-logFile "Repeat failed deployment using AMS instance name '$NewName'..." - write-logFile - - # repeat deployment - $parameter.Name = $NewName - $script:amsInstanceName = $NewName - $res = New-AzSapMonitor @parameter - if ($res.ProvisioningState -ne 'Succeeded') { - write-logFile $myDeploymentError -ForegroundColor 'Yellow' - write-logFile - write-logFileError "Creation of AMS Instance '$amsInstanceName' failed" ` - "Check the Azure Activity Log in resource group $targetRG" - } - } -} - #-------------------------------------------------------------- function get-NewName { #-------------------------------------------------------------- @@ -10383,35 +9935,6 @@ function get-NewName { } } -#-------------------------------------------------------------- -function remove-amsInstance { -#-------------------------------------------------------------- - param ( - $resourceGroup - ) - - $ams = Get-AzSapMonitor -ErrorAction 'SilentlyContinue' - test-cmdlet 'Get-AzSapMonitor' "Could not get AMS instances" - - $ams | ForEach-Object { - $amsName = $_.Name - $ids = $_.Id -split '/' - if ($ids.count -gt 4) { - $amsRG = $ids[4] - if ($amsRG -eq $resourceGroup) { - write-logFile "Removing AMS instance '$amsName'..." - - Remove-AzSapMonitor ` - -Name $amsName ` - -ResourceGroupName $amsRG ` - -ErrorAction 'SilentlyContinue' - test-cmdlet 'Remove-AzSapMonitor' "Could not delete AMS instance '$amsName'" - } - } - } - write-logFile -} - #-------------------------------------------------------------- function set-deploymentParameter { #-------------------------------------------------------------- @@ -10524,53 +10047,6 @@ function deploy-templateTarget { write-stepEnd } -#-------------------------------------------------------------- -function deploy-templateTargetAms { -#-------------------------------------------------------------- - param ( - $DeploymentPath, - $DeploymentName - ) - - # deploy using ARM template - write-logFile "Deploying ARM template '$DeploymentPath'..." - - $parameter = @{ - ResourceGroupName = $targetRG - Name = $DeploymentName - TemplateFile = $DeploymentPath - ErrorAction = 'SilentlyContinue' - ErrorVariable = '+myDeploymentError' - } - # add template parameter - $parameter.TemplateParameterObject = @{ amsInstanceName = $amsInstanceName } - - # display ARM deployment parameters - write-logFileHashTable $parameter - - # deploy - New-AzResourceGroupDeployment @parameter - | write-LogFilePipe - if (!$?) { - $NewName = get-NewName $amsInstanceName 30 - write-logFile $myDeploymentError -ForegroundColor 'Yellow' - write-logFile - write-logFile "Repeating failed deployment using AMS instance name '$NewName'..." - write-logFile - - # repeat deployment - $parameter.TemplateParameterObject = @{ amsInstanceName = $NewName } - $parameter.name = "$DeploymentName`2" - New-AzResourceGroupDeployment @parameter - | write-LogFilePipe - if (!$?) { - write-logFile $myDeploymentError -ForegroundColor 'yellow' - write-logFileError "AMS deployment '$DeploymentName' failed" ` - "Check the Azure Activity Log in resource group $targetRG" - } - } -} - #-------------------------------------------------------------- function deploy-linuxDiagnostic { #-------------------------------------------------------------- @@ -11758,36 +11234,6 @@ function get-subscriptionFeatures { } } -#-------------------------------------------------------------- -function test-justRedeployAms { -#-------------------------------------------------------------- - if (!$justRedeployAms) { return } - - # required steps: - $script:skipSnapshots = $True - $script:skipBlobs = $True - $script:skipDeploymentVMs = $True - $script:skipExtensions = $True - $script:createArmTemplateAms = $True - $script:skipArmTemplate = $False - - # forbidden parameters: - write-logFileForbidden 'justRedeployAms' @( - 'amsWsKeep', 'amsWsName', 'amsWsRG', - 'skipDeployment', - 'updateMode', 'archiveMode', - 'justCreateSnapshots', 'justDeleteSnapshots', - 'skipArmTemplate', - 'createVolumes', 'createDisks','stopRestore', 'continueRestore', - 'startWorkload', 'deleteTargetSA', 'deleteSourceSA') - - - # DB password needed for HANA and SQL Server AMS provider - if ($dbPassword.Length -eq 0) { - write-logFileWarning "parameter 'dbPassword' missing. It might be needed for AMS" - write-logFile - } -} #------------------------------------------------------------- function test-archiveMode { @@ -11820,7 +11266,7 @@ function test-archiveMode { # forbidden parameters: write-logFileForbidden 'archiveMode' @( 'updateMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'pathArmTemplate', 'skipArmTemplate', 'createVolumes', 'createDisks','stopRestore', 'continueRestore', 'startWorkload', 'deleteTargetSA', 'deleteSourceSA', 'stopVMsTargetRG', @@ -11849,7 +11295,7 @@ function test-justCopyBlobs { # forbidden parameters: write-logFileForbidden 'justCopyBlobs' @( 'updateMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'pathArmTemplate', 'createVolumes', 'createDisks','stopRestore', 'continueRestore', 'stopVMsTargetRG', 'stopVMsSourceRG', 'deleteSnapshots', @@ -11872,7 +11318,7 @@ function test-justCopyDisks { # forbidden parameters: write-logFileForbidden 'justCopyDisks' @( 'updateMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'pathArmTemplate', 'createVolumes', 'createDisks','stopRestore', 'continueRestore', 'stopVMsTargetRG', 'stopVMsSourceRG', 'deleteSnapshots', @@ -11894,7 +11340,7 @@ function test-restartBlobs { # forbidden parameters: write-logFileForbidden 'restartBlobs' @( 'updateMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'pathArmTemplate', 'skipBlobs') } @@ -11915,7 +11361,7 @@ function test-stopRestore { # forbidden parameters: write-logFileForbidden 'continueRestore' @( 'updateMode', 'archiveMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'restartBlobs', 'justCopyBlobs', 'justStopCopyBlobs', 'stopRestore') } @@ -11930,7 +11376,7 @@ function test-stopRestore { # forbidden parameters: write-logFileForbidden 'stopRestore' @( 'updateMode', 'archiveMode', - 'justCreateSnapshots', 'justDeleteSnapshots', 'justRedeployAms', + 'justCreateSnapshots', 'justDeleteSnapshots', 'restartBlobs', 'justCopyBlobs', 'justStopCopyBlobs', 'continueRestore', 'startWorkload', 'deleteSourceSA') @@ -11947,7 +11393,7 @@ function test-setVmMerge { # forbidden parameters: write-logFileForbidden 'setVmMerge' @( - 'pathArmTemplate', 'pathArmTemplateAms', 'createArmTemplateAms', 'skipArmTemplate', + 'pathArmTemplate', 'skipArmTemplate', 'startWorkload', 'skipVMs', 'skipDisks','stopVMsTargetRG') } @@ -12788,7 +12234,7 @@ function step-armTemplate { write-stepStart "Create ARM templates" # create JSON template new-templateSource - new-templateTarget # includes new-templateTargetAms + new-templateTarget write-logFile # write target ARM template to local file @@ -12800,38 +12246,13 @@ function step-armTemplate { } write-logFile -ForegroundColor 'Cyan' "Target ARM template saved: $exportPath" $script:armTemplateFilePaths += $exportPath - - # special case: Redeployment of AMS with existing AMS template ($exportPathAms = $pathArmTemplateAms) - if ($justRedeployAms -and ($pathArmTemplateAms.length -ne 0)) { - write-logFile -ForegroundColor 'yellow' "Using given Azure Monitoring for SAP template: $exportPathAms" - } - # special case: Redeployment of AMS - elseif ($justRedeployAms -and ($script:resourcesAMS.count -eq 0)) { - write-logFileError "Invalid parameter 'justRedeployAms'" ` - "Resource group '$sourceRG' does not contain an AMS instance" ` - "You might use parameter 'pathArmTemplateAms' for supplying a given AMS template" - } - - # write AMS template to local file - elseif ($script:resourcesAMS.count -ne 0) { - $text = $script:amsTemplate | ConvertTo-Json -Depth 20 - Set-Content -Path $exportPathAms -Value $text -ErrorAction 'SilentlyContinue' - if (!$?) { - write-logFileError "Could not save Azure Monitoring for SAP template" ` - "Failed writing file '$exportPathAms'" - } - write-logFile -ForegroundColor 'Cyan' "Azure Monitoring for SAP template saved: $exportPathAms" - $script:armTemplateFilePaths += $exportPathAms - } write-stepEnd #-------------------------------------------------------------- - if (!$justRedeployAms) { - write-stepStart "Configured VMs/disks for Target Resource Group $targetRG" - show-targetVMs - compare-quota - write-stepEnd - } + write-stepStart "Configured VMs/disks for Target Resource Group $targetRG" + show-targetVMs + compare-quota + write-stepEnd } #-------------------------------------------------------------- @@ -13021,72 +12442,6 @@ function step-deployment { wait-mountPoint } - #-------------------------------------------------------------- - # Deploy Azure Monitor for SAP - if ($exportPathAms -in $script:armTemplateFilePaths) { - - #-------------------------------------------------------------- - # just re-deploy AMS (in source RG) - if ($justRedeployAms) { - - # start VMs in the source RG - if (!$skipStartSAP) { - start-VMs $sourceRG - } - # start SAP in the source RG - $done = start-sap $sourceRG - if (!$done) { - write-logFileError "Azure Monitor for SAP (AMS) could not be re-deployed because SAP is not running" - } - - write-stepStart "Re-deploy AMS instance" - # delete all AMS instances of source RG - remove-amsInstance $sourceRG - - # rename AMS instance if parameter amsInstanceName was not supplied - # avoid error: A vault with the same name already exists in deleted state. - if ('amsInstanceName' -notin $boundParameterNames) { - $script:amsInstanceName = get-NewName $script:amsInstanceName 30 - } - } - - #-------------------------------------------------------------- - # normal AMS deployment (in target RG) - else { - - # VMs already started in the target RG - # start SAP in the target RG - $done = start-sap $targetRG - if (!$done) { - write-logFileError "Azure Monitor for SAP (AMS) could not be deployed because SAP is not running" - } - write-stepStart "Deploy AMS instance" - } - - #-------------------------------------------------------------- - # deploy using ARM template - if (!$amsUsePowerShell) { - deploy-templateTargetAms $exportPathAms "$sourceRG`-AMS.$timestampSuffix" - } - # deploy using cmdlets - else { - - $text = Get-Content -Path $exportPathAms -ErrorAction 'SilentlyContinue' - if (!$?) { - write-logFileError "Could not read file '$DeploymentPath'" - } - $json = $text | ConvertFrom-Json -AsHashtable -Depth 20 - $script:resourcesAMS = $json.resources - new-amsInstance - new-amsProviders - } - # finish AMS deployment - if ($justRedeployAms) { - write-logFileWarning "All VMs in source resource group '$sourceRG' are still running" - } - write-stepEnd - } - #-------------------------------------------------------------- # Deploy Extensions if (!$skipExtensions) { @@ -13213,7 +12568,6 @@ $targetRG2 = write-securestring ($targetRG -replace '\.', '-' -replace '[^\w-_ # default file paths $importPath = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$sourceRG2.SOURCE.json" $exportPath = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$targetRG2.TARGET.json" -$exportPathAms = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$targetRG2.AMS.json" $logPath = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$targetRG2.TARGET.log" $timestampSuffix = (Get-Date -Format 'yyyy-MM-dd__HH-mm-ss') @@ -13233,7 +12587,6 @@ if ($enableSourceRgMode) { # file names for backup RG if ($archiveMode) { $exportPath = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$sourceRG2.TARGET.json" - $exportPathAms = Join-Path -Path $pathExportFolder -ChildPath "rgcopy.$sourceRG2.AMS.json" } # create logfile @@ -13286,7 +12639,6 @@ try { write-logFileForbidden 'updateMode' @('targetRG', 'targetLocation') write-logFileForbidden 'justCreateSnapshots' @('targetRG', 'targetLocation') write-logFileForbidden 'justDeleteSnapshots' @('targetRG', 'targetLocation') - write-logFileForbidden 'justRedeployAms' @('targetRG', 'targetLocation') # check name-parameter values test-names @@ -13316,23 +12668,6 @@ try { $script:armTemplateFilePaths += $pathArmTemplate } - # given AMS ARM tempalte - if ($pathArmTemplateAms.length -ne 0) { - - if ($(Test-Path -Path $pathArmTemplateAms) -ne $True) { - write-logFileError "Invalid parameter 'pathArmTemplateAms'" ` - "File not found: '$pathArmTemplateAms'" - } - $exportPathAms = $pathArmTemplateAms - $script:armTemplateFilePaths += $pathArmTemplateAms - - if ( ($pathArmTemplate.length -eq 0) ` - -and !$skipDeployment -and !$skipDeploymentVMs -and !$justRedeployAms ) { - write-logFileError "Invalid parameter 'pathArmTemplateAms'" ` - "Parameter 'pathArmTemplate' must also be supplied" - } - } - #-------------------------------------------------------------- # check software version #-------------------------------------------------------------- @@ -13357,13 +12692,6 @@ try { } } - # display Az.HanaOnAzure version - $azHoaVersion = (Get-InstalledModule Az.HanaOnAzure -ErrorAction 'SilentlyContinue') - if ($azHoaVersion.count -ne 0) { - $azHoaVersionString = $azHoaVersion.version - } - # check Az.HanaOnAzure version (minimum 0.3) in function new-templateTargetAms if AMS resource exists - # check for running in Azure Cloud Shell if (($env:ACC_LOCATION).length -ne 0) { write-logFile 'RGCOPY running on Azure Cloud Shell:' -ForegroundColor 'yellow' @@ -13376,7 +12704,6 @@ try { write-logFileTab 'Powershell version' $psVersionString -noColor write-logFileTab 'Az version' $azVersion.version -noColor write-logFileTab 'Az.NetAppFiles' $azAnfVersionString -noColor - write-logFileTab 'Az.HanaOnAzure' $azHoaVersionString -noColor write-logFileTab 'OS version' $PSVersionTable.OS -noColor write-logFile @@ -13644,9 +12971,6 @@ try { if ('setVmZone' -notin $boundParameterNames) { $setVmZone = @() } - if ('setLoadBalancerSku' -notin $boundParameterNames) { - $setLoadBalancerSku = @() - } if ('setPrivateIpAlloc' -notin $boundParameterNames) { $setPrivateIpAlloc = @() } @@ -13708,14 +13032,13 @@ try { } # given templates - if (($pathArmTemplate.length -ne 0) -or ($pathArmTemplateAms.length -ne 0)) { + if ($pathArmTemplate.length -ne 0) { $skipArmTemplate = $True $skipSnapshots = $True $skipBlobs = $True } # special cases: - test-justRedeployAms test-archiveMode test-justCopyBlobs test-justCopyDisks @@ -13770,13 +13093,6 @@ try { if ($deleteTargetSA ) {$doDeleteTargetSA = '[X]'} else {$doDeleteTargetSA = '[ ]'} if ($stopVMsTargetRG ) {$doStopVMsTargetRG = '[X]'} else {$doStopVMsTargetRG = '[ ]'} - if ($justRedeployAms -or $createArmTemplateAms -or ('pathArmTemplateAms' -in $boundParameterNames)) { - $doDeploymentAms = '[X]' - } - else { - $doDeploymentAms = '[ ]' - } - write-logFile 'Required steps:' write-logFile " $doArmTemplate Create ARM Template (refering to snapshots or BLOBs)" write-logFile " $doSnapshots Create snapshots (of disks and volumes in source RG)" @@ -13788,7 +13104,6 @@ try { else { write-logFile " Deployment: $doDeploymentVMs Deploy Virtual Machines" write-logFile " $doRestore Restore files" - write-logFile " $doDeploymentAms Deploy Azure Monitor for SAP" write-logFile " $doExtensions Deploy Extensions" } write-logFile " $doWorkload Workload and Analysis" diff --git a/version_history.md b/version_history.md index df4e1e9..8fd0ff5 100644 --- a/version_history.md +++ b/version_history.md @@ -88,5 +88,29 @@ bug fix| Increased minimum required PowerShell version to 7.2 (required for $PsS feature| New parameter `justCopyDisks` feature| Improved quota check documentation|Clarification about moving customer SAP landscapes to a different region using RGCOPY. -bug fix| RGCOPY exports an ARM template from the source RG and modifies it.
**The structure of this exported ARM template has changed:**. It now contains circular dependencies between:
  • vnets and their subnet
  • network security groups and their rules
  • NAT Gateways and their Public IP Prefixes
Therefore, a workaround had to be implemented in RGCOPY. All older versions of RGCOPY do not work anymore (at least in some regions). +bug fix| RGCOPY exports an ARM template from the source RG and modifies it.
**The structure of this exported ARM template has changed:**. It now contains `circular dependencies` between:
  • vnets and their subnet
  • network security groups and their rules
  • NAT Gateways and their Public IP Prefixes
Therefore, a workaround had to be implemented in RGCOPY. All older versions of RGCOPY do not work anymore. +#### RGCOPY 0.9.40 December 2023 +type|change +:---|:--- +warning|**Always install the newest version of PowerShell *and* az-cmdlets!**
When installing the newest PowerShell (7.3.0) with older az-cmdlets then RGCOPY might terminate with the following error:
`GenericArguments[0], 'Microsoft.Azure.Management.Compute.Models.VirtualMachine', on 'T MaxInteger[T](System.Collections.Generic.IEnumerable1[T])' violates the constraint of type 'T'.`
If you install the newest az version 9.1.1 then RGCOPY works fine even with the newest PowerShell version 7.3.0 +UI| Removed support for AMS v1 as announced in February 2022.
Remove parameters `pathArmTemplateAms`, `createArmTemplateAms`, `amsInstanceName`, `amsWsName`, `amsWsRG`, `amsWsKeep`, `amsShareAnalytics`, `dbPassword`, `amsUsePowerShell` and `justRedeployAms`. +UI | Added a warning that ProximityPlacementGroups, AvailabilitySets and VmssFlex are removed if VM Tag `rgcopy.TipGroup` was used. +bug fix|**The behavior of VMSS Flex has changed for Fault Domain Count FD>1:**
The **current** behavior is the following:
  • For M-Series VMs:
    You must not set the fault domain for the VM. If you do so, RGCOPY gives a warning: use parameter 'setVmFaultDomain' for setting fault domain to 'none'.
    Hereby, a VMSS Flex with FD>1 that contains M-series VMs behaves like an Availability Set.
  • For non M-Series VMs:
    You must now set the VMSS Flex property `singlePlacementGroup` = `False`.
    This is done now automatically by RGCOPY. However, you can use RGCOPY parameter `singlePlacementGroup` for changing this (once the VMSS Flex behavior changes in the future).
  • Mixing M-Series VMs with other VMs is not allowed inside a VMSS Flex
    In this case, RGCOPY gives a warning.
In older versions of RGCOPY you might get the deployment error:
`Cannot set 'platformFaultDomain' on Virtual Machine 'hana2' because the Virtual Machine Scale Set 'vmss' that it references has 'singlePlacementGroup' = true. (Code:BadRequest)` + bug fix| **The semantic of zone definition for Public IP Addresses has changed (see below):**
As a workaround, RGCOPY now always sets SKU = `Standard` and IPAllocationMethod = `Static` for Public IP Addresses. Parameters `setPublicIpSku` and `setPublicIpAlloc` have been removed.
In older versions of RGCOPY you will see the following error when deploying a VM with a public IP address to an Availability Zone:
`Compute resource /subscriptions/.../virtualMachines/... has a zone constraint 3 but the PublicIPAddress /subscriptions/... used by the compute resource via NetworkInterface or LoadBalancer has a different zone constraint Regional. (Code: ComputeResourceZoneConstraintDoesNotMatchPublicIPAddressZoneConstraint)` + UI| RGCOPY now always sets SKU = `Standard` for Load Balancers. Parameter `setLoadBalancerSku` has been removed. + + Semantic changes of zone definition for Public IP Addresses: +``` +WARNING: Upcoming breaking changes in the cmdlet ‘New-AzPublicIpAddress’ : +Default behaviour of Zone will be changed +Cmdlet invocation changes : + Old Way : Sku = Standard means the Standard Public IP is zone-redundant. + New Way : Sku = Standard and Zone = {} means the Standard Public IP has no zones. + If you want to create a zone-redundant Public IP address, please specify + all the zones in the region. For example, Zone = [‘1’, ‘2’, ‘3’]. + ``` + + + + \ No newline at end of file