AzureRm.AvailabilitySet.CoreHelper.psm1

<#
.SYNOPSIS
    AzureRm.AvailabilitySet.CoreHelper.psm1 - Contains helper functions.
.DESCRIPTION
    Contains helper functions.
#>

function GetParameterNameFromValue
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]$VMName,
        [Parameter(Mandatory=$true)]
        $parameterSection
    )

    foreach ($prop in $parameterSection.psobject.Properties)
    {
        if ($prop.value.defaultValue -ieq $VMName.ToLower())
        {
            return $prop.Name
        }
    }
}

function Add-AzureRmAvSetVmToAvailabilitySet
{
    <#
    .SYNOPSIS
        Add-AzureRmAvSetVmToAvailabilitySet - This sample cmdlet adds/moves a VM(s) to an availability sets through exporting, deleting the original VM and importing it back.
    .DESCRIPTION
        Add-AzureRmAvSetVmToAvailabilitySet - This sample cmdlet adds/moves a VM(s) to an availability sets through exporting, deleting the original VM and importing it back.
    .PARAMETER ResourceGroup
        Name of the resource group where the VM resides
    .PARAMETER VMName
        List of VMs to be included in the same AV Set, worth it to notice that this VMs needs to be of the same VM Size.
        e.g. "vm1","vm2"
    .PARAMETER OsType
        Which OS type are those VMs, Windows or Linux, this is required when attached an existing OS Disk to a newly created VM.
        Setting up the wrong OS Name may leave the VM in an unsupported state and unpredictable issues may happen.
    .PARAMETER AvailabilitySet
        Name of the existing AvailabilitySet, must be in the same resource group where the VMs resides.
    .EXAMPLE
        Add-AzureRmAvSetVmToAvailabilitySet -ResourceGroupName rg-avset-test -VMName vm1 -OsType windows -AvailabilitySet avset
    .NOTES
        * Export-AzureRmResourceGroup will export the VM resources whitout any Extension and Diagnostics, so when it is imported back those items will need to be manually added back.
        * This script deletes the original VM configuration to import it back, a backup of the template is always made and can be used to reinstantiate the VM as they were before.
        * It is strongly recommended to have a full backup of the VM and test it before allowing the script to delete the VM.
        * Execution of this script is at your own risk.
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="High")]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName,
    
        [Parameter(Mandatory=$true)]
        [string[]]$VMName,

        [Parameter(Mandatory=$true)]
        [validateSet("windows","linux",IgnoreCase=$true)]
        [string]$OsType,

        [Parameter(Mandatory=$true)]
        [string]$AvailabilitySet
    )
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $ExecutionTimeStamp = Get-Date -Format 'yyyy-MM-dd_hhmmss'

    $originalTemplateFile = [System.IO.Path]::Combine($PSScriptRoot,[string]::Format("{0}-{1}.json","OriginalTemplate",$ExecutionTimeStamp))
    Write-Verbose "Original resource group ARM template file name: $originalTemplateFile" -Verbose

    $newTemplateFile = [System.IO.Path]::Combine($PSScriptRoot,[string]::Format("{0}-{1}.json","NewTemplate",$ExecutionTimeStamp))
    Write-Verbose "New resource group ARM template file name: $newTemplateFile" -Verbose

    try
    {
        # Getting the existing AvailabilitySet
        Write-Verbose "Getting the existing AvailabilitySet" -Verbose
        $avset = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroupName -Name $AvailabilitySet -ErrorAction SilentlyContinue

        if ($avSet -eq $null)
        {
            throw "Availability Set $AvailabilitySet not found at Resource Group $ResourceGroupName."
        }

        # Exporting resource group
        Write-Verbose "Exporting resource group" -Verbose
        Export-AzureRmResourceGroup -ResourceGroupName $ResourceGroupName -Path $originalTemplateFile -IncludeParameterDefaultValue -force

        # Reading original template file
        $rgdef = Get-Content $originalTemplateFile | ConvertFrom-Json

        # Removing AdminPassword parameters that are not used when importing an existing Os Disk
        foreach ($prop in $rgdef.parameters.psobject.Properties)
        {
            if ($prop.name.contains("adminPassword") -or $prop.name.contains("primary") -or $prop.name.contains("extensions_Microsoft."))
            {
                $rgdef.parameters.psobject.Properties.Remove($prop.Name)
            }
        }

        # Filtering resources for only VMs that will be included
        $resources = @()
        foreach ($vm in $VMName)
        {
            $vmNameParameterName = GetParameterNameFromValue -VMName $vm -parameterSection $rgdef.parameters
            $resources += $rgdef.resources | ? {$_.type -eq "Microsoft.Compute/virtualMachines" -and $_.name.Contains($vmNameParameterName)}
        }
        $rgdef.resources = $resources

        # Checking if all VMs are of the same size
        $vmSize = @()
        $vmSize += $rgdef.resources.properties.hardwareProfile | Select-Object -Unique

        if ($vmSize -eq 1)
        {
            throw "Not all VMs are the same size, Availability Sets only accepts VMs of the same size"
        }

        # Changing original VM resources to attach disks and add to the availability set
        foreach ($vmResource in $rgdef.resources)
        {
            # Checking Availability Set Alignment according to managed or unmanaged disks
            if ($vmResource.properties.storageProfile.osDisk.psobject.Properties["vhd"] -eq $null)
            {
                if ($avset.Sku -ne "Aligned")
                {
                    throw "VM is using Managed disks and the Availability is not aligned with Managed Disks"
                }
            }
            else
            {
                if ($avset.Sku -ne "Classic")
                {
                    throw "VM is using UnManaged Disks and Availability set should be Classic type."
                }
            }

            # Removing any dependencies since they already exists
            $vmResource.dependsOn = $null

            # Changing OS Disk to attach insted of using FromImage since this VM was already deployed
            $vmResource.properties.storageProfile.osDisk.createOption = "Attach"
        
            # Nullifying osProfile since this is used during a new deployment only
            if ($vmResource.properties.psobject.Properties["osProfile"] -ne $null)
            {
                $vmResource.properties.osProfile = $null
            }

            # Required by the Platform, adding the OsType parameter
            if ([string]::IsNullOrEmpty($vmResource.properties.storageProfile.osDisk.osType))
            {
                $vmResource.properties.storageProfile.osDisk | Add-Member -Type NoteProperty -Name "osType" -Value $osType.ToLower()
            }
        
            # Nullifying ImageReference since this is only for new VMs
            if ($vmResource.properties.storageProfile.psobject.Properties["imageReference"] -ne $null)
            {
                $vmResource.properties.storageProfile.imageReference = $null
            }

            # Changing creation option of data disks to Attach instead of new
            if ($vmResource.properties.storageProfile.dataDisks.Count -gt 0)
            {
                foreach ($datadisk in $vmResource.properties.storageProfile.dataDisks)
                {
                    $datadisk.createOption = "Attach"
                }
            }

            # Removing Primary attribute of network Id
            foreach ($nic in $vmResource.properties.networkProfile.networkInterfaces)
            {
                if ($nic.properties -ne $null)
                {
                    if ($nic.properties.psobject.Properties["primary"] -ne $null)
                    {
                        $nic.properties.psobject.properties.remove("primary")
                    }
                }
            }

            # Adding Availability Set Id to VM
            foreach ($properties in $vmResource.Properties)
            {
                if ([string]::IsNullOrEmpty($vmResource.properties.availabilitySet))
                {
                    $properties | Add-Member -Type NoteProperty -Name "availabilitySet" -Value @{"id"=$avset.id}
                }
                else
                {
                    $properties.availabilitySet = @{"id"=$avset.id}
                }
            }
        }

        # Generating the new JSON Template to be executed to import the VMs back.
        Write-Verbose "Generating the new JSON Template to be executed to import the VMs back." -Verbose

        if ($rgdef.resources.count -gt 0)
        {

            $rgdef | ConvertTo-Json -Depth 100 | % {$_.replace("\u0027","`'")} | Out-File $newTemplateFile

            if ($PSCmdlet.ShouldProcess($ResourceGroupName,"Confirm that VMs can be excluded from resource group (VHDs are preserved)?"))
            {

                Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName `
                                                    -Mode Incremental `
                                                    -TemplateFile $newTemplateFile `
                                                    -Verbose

                foreach ($item in $VMName)
                {
                    # Stopping VMs and removing their deployment
                    Write-Verbose "Stopping VM $item" -Verbose
                    
                    $vm = Get-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $item -Status
                    
                    if ($vm -ne $null)
                    {
                        if ($vm.statuses[1].Code -ne "PowerState/deallocated")
                        {
                            Stop-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $item -Force
                        }
                        
                        Write-Verbose "Deleting VM $item from Resource Group $ResourceGroupName (VHDs are preserved and VMs will be imported in the next steps)" -Verbose
                        Remove-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $item -Force
                    }

                }

                Write-Verbose "Deploying the new template." -Verbose
                New-AzureRmResourceGroupDeployment -Name "SettingUpAvailabilitySets-$ExecutionTimeStamp" `
                                                       -ResourceGroupName $ResourceGroupName `
                                                       -Mode Incremental `
                                                       -TemplateFile $newTemplateFile `
                                                       -Force -Verbose  
            }
        }
        else
        {
            throw "Resouces section of template is empty after transformations, aborting operation."
        }
    }
    catch
    {
        Write-Error "An error ocurred: $_"
    
    }
}

function Remove-AzureRmAvSetVmFromAvailabilitySet
{
    <#
    .SYNOPSIS
        Remove-AzureRmAvSetVmFromAvailabilitySet - This sample cmdlet removes a VM(s) from an availability set through exporting, deleting the original VM and importing it back.
    .DESCRIPTION
        Remove-AzureRmAvSetVmFromAvailabilitySet - This sample cmdlet removes a VM(s) from an availability set through exporting, deleting the original VM and importing it back.
    .PARAMETER ResourceGroup
        Name of the resource group where the VM resides
    .PARAMETER VMName
        VM to be included in the same AV Set, worth it to notice that this VMs needs to be of the same VM Size.
        e.g. "vm1"
    .PARAMETER OsType
        Which OS type are those VMs, Windows or Linux, this is required when attached an existing OS Disk to a newly created VM.
        Setting up the wrong OS Name may leave the VM in an unsupported state and unpredictable issues may happen.
    .EXAMPLE
            Remove-AzureRmAvSetVmFromAvailabilitySet -ResourceGroupName rg-avset-test -VMName vm1 -OsType windows
    .NOTES
        * Export-AzureRmResourceGroup will export the VM resources whitout any Extension and Diagnostics, so when it is imported back those items will need to be manually added back.
        * This script deletes the original VM configuration to import it back, a backup of the template is always made and can be used to reinstantiate the VM as they were before.
        * It is strongly recommended to have a full backup of the VM and test it before allowing the script to delete the VM.
        * Execution of this script is at your own risk.
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="High")]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName,
    
        [Parameter(Mandatory=$true)]
        [string]$VMName,

        [Parameter(Mandatory=$true)]
        [validateSet("windows","linux",IgnoreCase=$true)]
        [string]$OsType
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $ExecutionTimeStamp = Get-Date -Format 'yyyy-MM-dd_hhmmss'

    $originalTemplateFile = [System.IO.Path]::Combine($PSScriptRoot,[string]::Format("{0}-{1}.json","OriginalTemplate",$ExecutionTimeStamp))
    Write-Verbose "Original resource group ARM template file name: $originalTemplateFile" -Verbose

    $newTemplateFile = [System.IO.Path]::Combine($PSScriptRoot,[string]::Format("{0}-{1}.json","NewTemplate",$ExecutionTimeStamp))
    Write-Verbose "New resource group ARM template file name: $newTemplateFile" -Verbose

    try
    {
        # Exporting resource group
        Write-Verbose "Exporting resource group" -Verbose
        Export-AzureRmResourceGroup -ResourceGroupName $resourceGroupName -Path $originalTemplateFile -IncludeParameterDefaultValue -force

        # Reading original template file
        $rgdef = Get-Content $originalTemplateFile | ConvertFrom-Json

        # Removing AdminPassword parameters that are not used when importing an existing Os Disk
        foreach ($prop in $rgdef.parameters.psobject.Properties)
        {
            if ($prop.name.contains("adminPassword") -or $prop.name.contains("primary") -or $prop.name.contains("extensions_Microsoft."))
            {
                $rgdef.parameters.psobject.Properties.Remove($prop.Name)
            }
        }

        # Filtering resources for only VM that will be included
        $resources = @()
        
        $vmNameParameterName = GetParameterNameFromValue -VMName $VMName -parameterSection $rgdef.parameters

        $resources += $rgdef.resources | ? {$_.type -eq "Microsoft.Compute/virtualMachines" -and $_.name.Contains($vmNameParameterName)}

        # Checking if VM is attached to a loadbalancer and removes the NIC from it
        if ($resources.properties.networkProfile.networkInterfaces.Count -gt 1)
        {
            throw "This script supports VMs with only one nic."
        }

        $networkId = $resources.properties.networkProfile.networkInterfaces.id
        $regex = "`'(\w*)[`']"
        $templateNicRef = (([regex]$regex).Matches($networkId))[0].Value

        $nicResource = $rgdef.resources | ? {$_.type -eq "Microsoft.Network/networkInterfaces" -and $_.name.Contains($templateNicRef)}

        if (!([string]::IsNullOrEmpty($nicResource.properties.ipconfigurations.properties.psobject.Properties["loadBalancerBackendAddressPools"])) -or !([string]::IsNullOrEmpty($nicResource.properties.ipconfigurations.properties.psobject.Properties["loadBalancerInboundNatRules"])))
        {
            if (!([string]::IsNullOrEmpty($nicResource.properties.ipconfigurations.properties.psobject.Properties["loadBalancerBackendAddressPools"])))
            {
                Write-Warning "VM is attached to a load balancer, removing NIC from load balancer backend address pool, consider reviewing this VM later to make sure any manual procedure must be executed in order to restablish connectivity. E.g. creating a public ip address and associating with the NIC plus Network Security Groups." -Verbose
                $nicResource.properties.ipconfigurations.properties.psobject.Properties.Remove("loadBalancerBackendAddressPools")
            
            }

            if (!([string]::IsNullOrEmpty($nicResource.properties.ipconfigurations.properties.psobject.Properties["loadBalancerInboundNatRules"])))
            {
                Write-Warning "VM is attached to a load balancer, removing NIC from load balancer inbond net rules, consider reviewing this VM later to make sure any manual procedure must be executed in order to restablish connectivity. E.g. creating a public ip address and associating with the NIC plus Network Security Groups." -Verbose
                $nicResource.properties.ipconfigurations.properties.psobject.Properties.Remove("loadBalancerInboundNatRules")
            }

            $nicResource.dependsOn = $null
            $resources += $nicResource
            $resources
        }

        $rgdef.resources = $resources

        # Checking if any VM has returned from the initial filtering

        $vm = $rgdef.resources | ? {$_.type -eq "Microsoft.Compute/virtualMachines"}
        if ($vm -eq $null)
        {
            throw "VM $VMName does not exist, exiting processing"
        }

        # Getting Nic resource
        $nic = $rgdef.resources | ? {$_.type -eq "Microsoft.Network/networkInterfaces"}

        # Changing original VM resources to attach disks and to remove the availability set
        foreach ($vmResource in ($rgdef.resources | ? {$_.type -eq "Microsoft.Compute/virtualMachines"}))
        {
            # Removing any dependencies since they already exists adding nic
            $vmResource.dependsOn = @()
        
            if($nic -ne $null)
            {
                 $vmResource.dependsOn += [string]::Format("[concat('Microsoft.Network/networkInterfaces/','{0}')]",$rgdef.parameters.psobject.Properties[$templateNicRef.replace("'","")].Value.defaultValue)
            }

            # Changing OS Disk to attach insted of using FromImage since this VM was already deployed
            $vmResource.properties.storageProfile.osDisk.createOption = "Attach"
        
            # Nullifying osProfile since this is used during a new deployment only
            if ($vmResource.properties.psobject.Properties["osProfile"] -ne $null)
            {
                $vmResource.properties.osProfile = $null
            }

            # Required by the Platform, adding the OsType parameter
            if ([string]::IsNullOrEmpty($vmResource.properties.storageProfile.osDisk.osType))
            {
                $vmResource.properties.storageProfile.osDisk | Add-Member -Type NoteProperty -Name "osType" -Value $osType.ToLower()
            }
        
            # Nullifying ImageReference since this is only for new VMs
            if ($vmResource.properties.storageProfile.psobject.Properties["imageReference"] -ne $null)
            {
                $vmResource.properties.storageProfile.imageReference = $null
            }

            # Changing creation option of data disks to Attach instead of new
            if ($vmResource.properties.storageProfile.dataDisks.Count -gt 0)
            {
                foreach ($datadisk in $vmResource.properties.storageProfile.dataDisks)
                {
                    $datadisk.createOption = "Attach"
                }
            }

            # Removing Primary attribute of network Id
            foreach ($vmNicSetting in $vmResource.properties.networkProfile.networkInterfaces)
            {
                if ($vmNicSetting.properties -ne $null)
                {
                    if ($vmNicSetting.properties.psobject.Properties["primary"] -ne $null)
                    {
                        $vmNicSetting.properties.psobject.properties.remove("primary")
                    }
                }
            }

            # Removing Availability Set from VM
            foreach ($properties in $vmResource.Properties)
            {
                if (!([string]::IsNullOrEmpty($properties.availabilitySet)))
                {
                    $properties.psobject.Properties.Remove("availabilitySet")
                }
            }
        }

        # Generating the new JSON Template to be executed to import the VMs back.
        Write-Verbose "Generating the new JSON Template to be executed to import the VMs back." -Verbose
        if ($rgdef.resources.count -gt 0)
        {
            $rgdef | ConvertTo-Json -Depth 100 | % {$_.replace("\u0027","`'")} | Out-File $newTemplateFile

            if ($PSCmdlet.ShouldProcess($ResourceGroupName,"Confirm that VMs can be excluded from resource group (VHDs are preserved)?"))
            {

                $vm = Get-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $VMName -Status -ErrorAction SilentlyContinue
                
                if ($vm -ne $null)
                {
                    if ($vm.statuses[1].Code -ne "PowerState/deallocated")
                    {
                        Stop-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $VMName -Force
                    }
                    
                    Write-Verbose "Deleting VM $vmName from Resource Group $ResourceGroupName (VHDs are preserved and VMs will be imported in the next steps)" -Verbose
                    Remove-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $VMName -Force
                }

                # deleting Nic if it is attached to a load balancer, nic will be recreated through the modified template
                if ($nic -ne $null)
                {
                    Remove-AzureRmNetworkInterface -ResourceGroupName $ResourceGroupName -Name $rgdef.parameters.psobject.Properties[$templateNicRef.replace("'","")].Value.defaultValue -Force
                }

                Write-Verbose "Deploying the new template with VM removed from avset" -Verbose
                New-AzureRmResourceGroupDeployment -Name "RemovingAvailabilitySets-$ExecutionTimeStamp" `
                                                       -ResourceGroupName $resourceGroupName `
                                                       -Mode Incremental `
                                                       -TemplateFile $newTemplateFile `
                                                       -Force -Verbose  
            }
        }
        else
        {
            throw "Resouces section of template is empty after transformations, aborting operation."
        }
    }
    catch
    {
        Write-Error "An error ocurred: $_"
    }
}