BitTitan.Runbooks.AzureRM.psm1

<#
.SYNOPSIS
    PowerShell module for common Azure Resource Manager (AzureRM) functions and resources used in BitTitan Runbooks
.NOTES
    Version: 0.2.7
    Last updated: 11 March 2019
 
    Copyright (c) BitTitan, Inc. All rights reserved.
    Licensed under the MIT License.
#>


# Install/import BitTitan.Runbooks.Modules to bootstrap the install/import of the other modules
if ("BitTitan.Runbooks.Modules" -notIn (Get-Module).Name) {
    Install-Module BitTitan.Runbooks.Modules -Scope CurrentUser -AllowClobber -Force
    Import-Module -Name "$($env:USERPROFILE)\Documents\WindowsPowerShell\Modules\BitTitan.Runbooks.Modules" -Force
}

# Install/import the other BitTitan.Runbooks modules

# Install/import external modules
Import-ExternalModule AzureRM -RequiredVersion 6.8.1

# Enums for several frequently used Azure resource types
enum AzureResourceType {
    Disk
    NetworkInterface
    NetworkSecurityGroup
    PublicIpAddress
    ResourceGroup
    SqlDatabase
    SqlServer
    StorageAccount
    StorageContainer
    VirtualMachine
    VirtualMachineExtension
    VirtualNetwork
}

# This function returns the display name of the Azure resource type,
# making it suitable for logging
function Get-AzureResourceTypeDisplayName {
    param (
        # The Azure resource type
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$azureResourceType
    )
    switch ($azureResourceType) {
        "Disk" { "Disk" }
        "NetworkInterface" { "Network Interface" }
        "NetworkSecurityGroup" { "Network Security Group" }
        "PublicIpAddress" { "Public IP Address" }
        "ResourceGroup" { "Resource Group" }
        "SqlDatabase" { "SQL Database" }
        "SqlServer" { "SQL Server" }
        "StorageAccount" { "Storage Account" }
        "StorageContainer" { "Storage Container" }
        "VirtualMachine" { "Virtual Machine" }
        "VirtualMachineExtension" { "Virtual Machine Extension" }
        "VirtualNetwork" { "Virtual Network" }
    }
}

# This function examines an Azure resource object
# and returns if an Azure resource type enum matches the type of the object.
function Compare-AzureResourceAndResourceType {
    param (
        # The Azure resource
        [Parameter(Mandatory=$true)]
        $resource,

        # The Azure resource type
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$azureResourceType
    )

    # Extract out the name of the type of the resource
    $resourceTypeName = $resource.GetType().Name

    # Check if the type matches
    switch ($azureResourceType) {
        "Disk" { return $resourceTypeName -eq "PSDisk" }
        "NetworkInterface" { return $resourceTypeName -eq "PSNetworkInterface" }
        "NetworkSecurityGroup" { return $resourceTypeName -eq "PSNetworkSecurityGroup" }
        "PublicIpAddress" { return $resourceTypeName -eq "PSPublicIpAddress" }
        "ResourceGroup" { return $resourceTypeName -eq "PSResourceGroup" }
        "SqlDatabase" { return $resourceTypeName -eq "AzureSqlDatabaseModel" }
        "SqlDatabase" { return $resourceTypeName -eq "AzureSqlServerModel" }
        "StorageAccount" { return $resourceTypeName -eq "PSStorageAccount" }
        "StorageContainer" { return $resourceTypeName -eq "AzureStorageContainer" }
        "VirtualMachine" { return $resourceTypeName -eq "PSVirtualMachine" -or $resourceTypeName -eq "PSVirtualMachineInstanceView" }
        "VirtualMachineExtension" { return $resourceTypeName -eq "PSVirtualMachineExtension" }
        "VirtualNetwork" { return $resourceTypeName -eq "PSVirtualNetwork" }
    }
    return $false
}

# This function fetches the name of the Azure resource
function Get-AzureResourceName {
    param (
        # The name will be extracted from this resource
        [Parameter(Mandatory=$true)]
        $resource,

        # The type of the resource
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$resourceType
    )
    switch ($resourceType) {
        "Disk" {
            return $resource.Name
        }
        "NetworkInterface" {
            return $resource.Name
        }
        "NetworkSecurityGroup" {
            return $resource.Name
        }
        "PublicIpAddress" {
            return $resource.Name
        }
        "ResourceGroup" {
            return $resource.ResourceGroupName
        }
        "SqlDatabase" {
            return $resource.DatabaseName
        }
        "SqlServer" {
            return $resource.ServerName
        }
        "StorageAccount" {
            return $resource.StorageAccountName
        }
        "StorageContainer" {
            return $resource.StorageAccountName
        }
        "VirtualMachine" {
            return $resource.Name
        }
        "VirtualMachineExtension" {
            return $resource.Name
        }
        "VirtualNetwork" {
            return $resource.Name
        }
    }
}

# This function generates the Azure resource removal expression for a resource
function Get-AzureResourceRemovalExpression {
    param (
        # The resource which the generated expression will remove
        [Parameter(Mandatory=$true)]
        $resource,

        # The type of the resource
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$resourceType,

        # Additional parameters
        [Parameter(Mandatory=$false)]
        [String]$additionalParams = "-Force -ErrorAction Stop"
    )
    $resourceGroupName = $resource.ResourceGroupName
    $resourceName = Get-AzureResourceName $resource $resourceType
    switch ($resourceType) {
        "Disk" {
            return "Remove-AzureRmDisk -ResourceGroupName `"$($resourceGroupName)`" -DiskName `"$($resourceName)`" $($additionalParams)"
        }
        "NetworkInterface" {
            return "Remove-AzureRmNetworkInterface -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "NetworkSecurityGroup" {
            return "Remove-AzureRmNetworkSecurityGroup -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "PublicIpAddress" {
            return "Remove-AzureRmNetworkSecurityGroup -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "ResourceGroup" {
            return "Remove-AzureRmResourceGroup -Name `"$($resourceGroupName)`" $($additionalParams)"
        }
        "SqlDatabase" {
            return "Remove-AzureRmSqlDatabase -ResourceGroupName `"$($resourceGroupName)`" -ServerName `"$($resource.ServerName)`" -DatabaseName `"$($resourceName)`" $($additionalParams)"
        }
        "SqlServer" {
            return "Remove-AzureRmSqlServer -ResourceGroupName `"$($resourceGroupName)`" -ServerName `"$($resourceName)`" $($additionalParams)"
        }
        "StorageAccount" {
            return "Remove-AzureRmStorageAccount -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "VirtualMachine" {
            return "Remove-AzureRmVm -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "VirtualMachineExtension" {
            return "Remove-AzureRmVMExtension -ResourceGroupName `"$($resourceGroupName)`" -VMName `"$($resource.VMName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
        "VirtualNetwork" {
            return "Remove-AzureRmVirtualNetwork -ResourceGroupName `"$($resourceGroupName)`" -Name `"$($resourceName)`" $($additionalParams)"
        }
    }
}

# This class keeps track of the Azure resources that have been created,
# and can roll back created resources in the reverse order of creation.
class AzureResourceCreatedStack {
    # The stack containing the expressions to remove the resources
    # as well as the names of the resources
    [System.Collections.Stack]$ResourceStack

    # Constructor for the class
    AzureResourceCreatedStack() {
        # Create a new stack
        $this.ResourceStack = New-Object System.Collections.Stack
    }

    # This method pushes a custom rollback expression and name for a resource to the stack
    # It returns whether the information was pushed to the stack successfully
    #
    # This alternative to PushResource is necessary because some resource types
    # don't contain all the necessary information in the object to generate the removal command,
    # like storage blobs.
    [Boolean]PushResourceRollbackExpression(
        # The expression to rollback the resource
        $expression,

        # The name of the resource
        [String]$resourceName
    ) {
        try {
            # Push the expression and the name to the stack
            $this.ResourceStack.Push(
                @{
                    "Expression" = $expression;
                    "Name"       = $resourceName
                }
            )
            # Return that the push was successful
            return $true
        }
        catch {
            # Output warning message and return that the push was unsuccessful
            Write-Warning ("Unable to push $($resourceName) to the resource stack. " `
                    + "Automatic rollback (if necessary) will not occur for this resource.")
            return $false
        }
    }

    # This method pushes a newly created resource to the stack
    # It returns whether the resource was pushed to the stack successfully
    [Boolean]PushResource(
        # The newly created resource
        $resource,

        # The type of the resource
        [AzureResourceType]$resourceType
    ) {
        # Type of resource doesn't match resource type provided, abort method
        if (!(Compare-AzureResourceAndResourceType $resource $resourceType)) {
            Write-Error ("Type of `$resource and `$resourceType do not match. " `
                    + "`$resource type is '$($resource.GetType().Name)' while `$resourceType is '$($resourceType)'.")
            return $false
        }

        # Get the resource type display name and resource name
        $resourceTypeDisplayName = Get-AzureResourceTypeDisplayName $resourceType
        $resourceName = Get-AzureResourceName $resource $resourceType
        try {
            # Push the expression to remove the resource and the resource name to the stack
            $this.ResourceStack.Push(
                @{
                    "Expression" = Get-AzureResourceRemovalExpression $resource $resourceType;
                    "Name"       = "$($resourceTypeDisplayName) '$($resourceName)'"
                }
            )
            # Return that the push was successful
            return $true
        }
        catch {
            # Output warning message and return that the push was unsuccessful
            Write-Warning ("Unable to push $($resourceType) '$($resourceName)' to the resource stack. " `
                    + "Automatic rollback (if necessary) will not occur for this resource.")
            return $false
        }
    }

    # This method performs rollback of the created resources in the reverse order of creation
    # It returns whether the rollback was successful
    [Boolean]Rollback() {
        # Keep track of how many failed rollbacks we have
        $numFailedRollbacks = 0

        # Keep removing resources while the resource stack is not empty
        while ($this.ResourceStack.Count -gt 0) {
            # Get the most recently created resource
            $resource = $this.ResourceStack.Pop()
            Write-Information "Rollback: Removing $($resource.Name)."

            # Try to remove the resource
            try {
                Invoke-Expression $resource.Expression
                Write-Information "Rollback: Removed $($resource.Name) successfully."
            }
            catch {
                # Output warning message and increment number of failed rollbacks
                Write-Warning ("Rollback: Failed to remove $($resource.Name). Please remove it manually. " `
                        + "$($Error[0].Exception.Message)")
                $numFailedRollbacks += 1
            }
        }

        # Return whether the rollback was successful (i.e. no failed rollbacks)
        return ($numFailedRollbacks -eq 0)
    }
}

# Function to return a new instance of an AzureResourceCreatedStack
# This function exists because classes are not automatically imported together with
# the rest of the module
function New-AzureResourceCreatedStack {
    return [AzureResourceCreatedStack]::New()
}

# This function connects to AzureRM using admin account credentials or a MSPComplete Endpoint
# Returns if the connection and logon was successful

<#
.SYNOPSIS
    This function connects to Azure RM using admin account credentials or a MSPComplete Endpoint.
.DESCRIPTION
    This function connects to Azure RM using admin account credentials or a MSPComplete Endpoint.
    It returns whether the connection and logon was successful.
.PARAMETER username
    The username of the Azure RM admin account.
.PARAMETER password
    The password of the Azure RM admin account.
.PARAMETER subscriptionId
    The subscription ID of the Azure RM admin account
.PARAMETER endpoint
    The MSPComplete Endpoint for the Azure RM admin credentials.
    This endpoint can be masked or unmasked.
.EXAMPLE
    Connect-AzureRMAdminAccount -Endpoint $Endpoint
.EXAMPLE
    $Endpoint | Connect-AzureRMAdminAccount
.EXAMPLE
    Connect-AzureRMAdminAccount -Username $username -Password $password
#>

function Connect-AzureRMAdminAccount {
    param (
        # The username of the Azure RM admin account.
        [Parameter(Mandatory=$true, ParameterSetName="credential")]
        [String]$username,

        # The password of the Azure RM admin account.
        [Parameter(Mandatory=$true, ParameterSetName="credential")]
        [SecureString]$password,

        # The subscription ID of the AzureRM admin account.
        [Parameter(Mandatory=$false, ParameterSetName="credential")]
        $subscriptionId,

        # The MSPComplete Endpoint for the Azure RM admin credentials.
        [Parameter(Mandatory=$true, ParameterSetName="endpoint", ValueFromPipeline=$true)]
        $endpoint
    )

    # If given endpoint, retrieve credential directly
    if ($PSCmdlet.ParameterSetName -eq "endpoint") {
        $azureRMCredential = $endpoint.Credential
        $username = $azureRMCredential.Username
    }
    # Create the AzureRM credential from the given username and password
    else {
        $azureRMCredential = New-Object System.Management.Automation.PSCredential -ArgumentList $username, $password
    }

    # Logon to AzureRM
    try {
        # If $SubscriptionId is "0" or blank, i.e. the endpoint does not contain a valid SubscriptionId
        if ($SubscriptionId -eq "0" -or [string]::IsNullOrWhiteSpace($SubscriptionId)) {
            Connect-AzureRmAccount -Credential $azureRMCredential -Environment "AzureCloud" -ErrorAction Stop
        }

        # If a valid SubscriptionId exists
        else {
            Connect-AzureRmAccount -Credential $azureRMCredential -SubscriptionId $subscriptionId -Environment "AzureCloud" -ErrorAction Stop
        }

        # Logon was successful
        Write-Information "Connection and logon to Azure successful with '$($username)' using the '$($(Get-AzureRmContext).Subscription.Name)' Subscription."
        return $true
    }
    catch {
        # Logon was unsuccessful
        Write-Error "Failed AzureRM account logon with user '$($username)'. $($Error[0].Exception.Message)"
        return $false
    }
}

# Validate the Azure resource name
# Reference: https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions
# Returns the validated resource name, or $null if the resource name is not valid
function Validate-AzureResourceName {
    param (
        # The Azure resource name
        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [string]$azureResourceName,

        # The Azure resource name prefix
        [Parameter(Mandatory=$true)]
        [string]$azureResourceNamePrefix,

        # The Azure resource type
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$azureResourceType,

        # Indicates if the Azure resource name has to be in lowercase
        [switch]$isLowercase,

        # The Azure resource name minimum length
        [Parameter(Mandatory=$true)]
        [int]$azureResourceNameMinimumLength,

        # The Azure resource name maximum length
        [Parameter(Mandatory=$true)]
        [int]$azureResourceNameMaximumLength,

        # The Azure resource name regex
        [Parameter(Mandatory=$true)]
        [string]$azureResourceNameRegex
    )

    # Check if the Azure resource name is null or white space
    if ([string]::IsNullOrWhiteSpace($azureResourceName)) {
        # Generate a resource name with a timestamp
        $azureResourceName = $azureResourceNamePrefix + (Get-Date).ToString("yyyyMMddHHmm")

        # Display the Azure resource name
        Write-Information ("You have not provided a $($azureResourceType). `r`n" `
                + "The $($azureResourceType) will be '$($azureResourceName)'. ")

        # Return the Azure resource name
        return $azureResourceName
    }

    # Trim the Azure Resource name
    $azureResourceName = $azureResourceName.Trim();

    # Check and convert to lowercase, if required
    if ($isLowercase) {
        $azureResourceName = $azureResourceName.ToLower();
    }

    # Check if the length is between the minimum and maximum length required
    if (($azureResourceName.length -lt $azureResourceNameMinimumLength) -or ($azureResourceName.length -gt $azureResourceNameMaximumLength)) {
        # Display the error message and exit the runbook if the resource name is not valid
        Write-Error ("The $($azureResourceType) '$($azureResourceName) ' has an invalid length of '$($azureResourceName.length)' characters. " `
                + "Please check that the $($azureResourceType) name is between $($azureResourceNameMinimumLength) to " `
                + "$($azureResourceNameMaximumLength) alphanumeric characters.")
        return $null
    }

    # Check if the Azure resource conforms to the naming convention
    if ($azureResourceName -notmatch $azureResourceNameRegex ) {
        Write-Error ("The $($azureResourceType) provided '$($azureResourceName)' is invalid. " `
                + "Please check that the $($azureResourceType) consists of alphanumeric characters only.")
        return $null
    }

    # Return the Azure resource name
    return $azureResourceName
}

# This function creates a Virtual Machine and its related Azure resources
function Create-VirtualMachine {
    param (
        # The Resource Group name
        [Parameter(Mandatory=$true)]
        [string]$resourceGroupName,

        # The Azure Location
        [Parameter(Mandatory=$true)]
        [string]$location,

        # The Virtual Machine name
        [Parameter(Mandatory=$true)]
        [string]$virtualMachineName,

        # The size of the Virtual Machine
        # Please refer to the link for more information about the Virtual Machine size
        # https://docs.microsoft.com/en-us/azure/cloud-services/cloud-services-sizes-specs#size-tables
        [Parameter(Mandatory=$true)]
        [string]$virtualMachineSize,

        # The credential to be used for the Virtual Machine
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.PSCredential]$virtualMachineCredential,

        # The name of the publisher of the Virtual Machine Image
        # Please refer to the link for more information about Publisher, Offer, SKU and Version
        # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage#terminology
        [Parameter(Mandatory=$true)]
        [string]$virtualMachinePublisherName,

        # The type of the Virtual Machine Image offer
        [Parameter(Mandatory=$true)]
        [string]$virtualMachineOffer,

        # The Virtual Machine Image SKU
        [Parameter(Mandatory=$true)]
        [string]$virtualMachineSKU,

        # The version of a Virtual Machine Image
        [Parameter(Mandatory=$true)]
        [string]$virtualMachineVersion,

        # The DNS label used for the Virtual Machine
        [Parameter(Mandatory = $true)]
        [string]$virtualMachineDnsLabel,

        # The IP Address allocation method ("Dynamic" or "Static")
        [Parameter(Mandatory = $true)]
        [string]$ipAddressAllocationMethod
    )

    # Keep track of the Virtual Machine related resources created to remove in case of Virtual Machine creation fails
    $virtualMachineResourceStack = [AzureResourceCreatedStack]::New()

    # Create a Virtual Network
    $virtualNetworkName = $virtualMachineName + "-vnet"
    try {
        Write-Information "Create the Virtual Network '$($virtualNetworkName)'."

        # Create a Virtual Network subnet configuration
        $subnetConfig = New-AzureRmVirtualNetworkSubnetConfig -Name $virtualNetworkName `
            -AddressPrefix 10.0.0.0/24 `
            -ErrorAction Stop

        # Create a Virtual Network
        $virtualNetwork = New-AzureRmVirtualNetwork -ResourceGroupName $resourceGroupName `
            -Location $location `
            -Name $virtualNetworkName `
            -AddressPrefix 10.0.0.0/16 `
            -Subnet $subnetConfig `
            -ErrorAction Stop

        # Push the newly created Azure resource to the stack of created resources
        $virtualMachineResourceStack.PushResource($virtualNetwork, [AzureResourceType]::VirtualNetwork)
        Write-Information "Created the Virtual Network '$($virtualNetworkName)' successfully."
    }
    catch {
        # Display error message and roll back
        Write-Error ("Failed to create the Virtual Network '$($virtualNetworkName)'. " `
                + "$($Error[0].Exception.Message)")
        $virtualMachineResourceStack.Rollback()
        return $false
    }

    # Create a Public IP Address
    $publicIpAddressName = $virtualMachineName + "-ip"
    try {
        Write-Information "Create the Public IP Address '$($publicIpAddressName)'."

        # Create a Public IP Address and specify a DNS name
        $publicIpAddress = New-AzureRmPublicIpAddress -ResourceGroupName $resourceGroupName `
            -Location $location `
            -AllocationMethod $ipAddressAllocationMethod `
            -Name $publicIpAddressName `
            -DomainNameLabel $virtualMachineDnsLabel `
            -ErrorAction Stop

        # Push the newly created Azure resource to the stack of created resources
        $virtualMachineResourceStack.PushResource($publicIpAddress, [AzureResourceType]::PublicIpAddress)
        Write-Information "Created the Public IP Address '$($publicIpAddressName)' successfully."
    }
    catch {
        # Display error message and roll back
        Write-Error ("Failed to create the Public IP Address '$($publicIpAddressName)'. " `
                + "$($Error[0].Exception.Message)")
        $virtualMachineResourceStack.Rollback()
        return $false
    }

    # Create a Network Security Group
    $networkSecurityGroupName = $virtualMachineName + "-nsg"
    try {
        Write-Information "Create the Network Security Group '$($networkSecurityGroupName)'."

        # Create an inbound Network Security Group rule for port 3389
        $networkSecurityRuleRdp = New-AzureRmNetworkSecurityRuleConfig -Name myNetworkSecurityGroupRuleRDP `
            -Protocol Tcp `
            -Direction Inbound `
            -Priority 1000 `
            -SourceAddressPrefix * -SourcePortRange * `
            -DestinationAddressPrefix * `
            -DestinationPortRange 3389 `
            -Access Allow `
            -ErrorAction Stop

        # Create an inbound Network Security Group rule for port 80
        $networkSecurityRuleWeb = New-AzureRmNetworkSecurityRuleConfig -Name myNetworkSecurityGroupRuleWWW `
            -Protocol Tcp `
            -Direction Inbound `
            -Priority 1001 `
            -SourceAddressPrefix * `
            -SourcePortRange * `
            -DestinationAddressPrefix * `
            -DestinationPortRange 80 `
            -Access Allow `
            -ErrorAction Stop

        # Create an inbound Network Security Group rule for port 443
        $networkSecurityRuleWebSecure = New-AzureRmNetworkSecurityRuleConfig -Name myNetworkSecurityGroupRuleWWWSecure `
            -Protocol Tcp `
            -Direction Inbound `
            -Priority 1002 `
            -SourceAddressPrefix * `
            -SourcePortRange * `
            -DestinationAddressPrefix * `
            -DestinationPortRange 443 `
            -Access Allow `
            -ErrorAction Stop

        # Create a Network Security Group
        $networkSecurityGroup = New-AzureRmNetworkSecurityGroup -ResourceGroupName $resourceGroupName `
            -Location $location `
            -Name $networkSecurityGroupName `
            -SecurityRules $networkSecurityRuleRdp, $networkSecurityRuleWeb, $networkSecurityRuleWebSecure `
            -ErrorAction Stop

        # Push the newly created Azure resource to the stack of created resources
        $virtualMachineResourceStack.PushResource($networkSecurityGroup, [AzureResourceType]::NetworkSecurityGroup)
        Write-Information "Created the Network Security Group '$($networkSecurityGroupName)' successfully."
    }
    catch {
        # Display error message and roll back
        Write-Error ("Failed to create the Network Security Group '$($networkSecurityGroupName)'. " `
                + "$($Error[0].Exception.Message)")
        $virtualMachineResourceStack.Rollback()
        return $false
    }

    # Create a Network Interface
    $networkInterfaceName = $virtualMachineName
    try {
        Write-Information "Create the Network Interface '$($networkInterfaceName)'."

        # Create a virtual network card and associate with public IP address and NSG
        $networkInterface = New-AzureRmNetworkInterface -Name $networkInterfaceName `
            -ResourceGroupName $resourceGroupName `
            -Location $location `
            -SubnetId $virtualNetwork.Subnets[0].Id `
            -PublicIpAddressId $publicIpAddress.Id `
            -NetworkSecurityGroupId $networkSecurityGroup.Id `
            -ErrorAction Stop

        # Push the newly created Azure resource to the stack of created resources
        $virtualMachineResourceStack.PushResource($networkInterface, [AzureResourceType]::NetworkInterface)
        Write-Information "Created the Network Interface '$($networkInterfaceName)' successfully."
    }
    catch {
        # Display error message and roll back
        Write-Error ("Failed to create the Network Interface '$($networkInterfaceName)'. " `
                + "$($Error[0].Exception.Message)")
        $virtualMachineResourceStack.Rollback()
        return $false
    }

    # Create the Virtual Machine
    try {
        Write-Information "Create the Virtual Machine '$($virtualMachineName)'."

        # Create a Virtual Machine Configuration
        $virtualMachineConfig = New-AzureRmVMConfig -VMName $virtualMachineName -VMSize $virtualMachineSize -ErrorAction Stop| `
            Set-AzureRmVMOperatingSystem -Windows -ComputerName $virtualMachineName -Credential $virtualMachineCredential -ProvisionVMAgent -EnableAutoUpdate -ErrorAction Stop| `
            Set-AzureRmVMSourceImage -PublisherName $virtualMachinePublisherName -Offer $virtualMachineOffer -Skus $virtualMachineSKU -Version $virtualMachineVersion -ErrorAction Stop| `
            Add-AzureRmVMNetworkInterface -Id $networkInterface.Id -ErrorAction Stop| `
            Set-AzureRmVMBootDiagnostics -Disable -ErrorAction Stop

        # Create a virtual machine
        $virtualMachine = New-AzureRmVM -ResourceGroupName $resourceGroupName -Location $location -VM $virtualMachineConfig -ErrorAction Stop

        # Push the newly created Azure resources to the stack of created resources
        $virtualMachine = Get-AzureRmVM -ResourceGroupName $resourceGroupName -Name $virtualMachineName -ErrorAction Stop
        $vmDisk = $virtualMachine.StorageProfile.OsDisk
        $virtualMachineResourceStack.PushResource($vmDisk, [AzureResourceType]::Disk)

        $extensions = $virtualMachine.Extensions
        foreach ($extension in $extensions) {
            $virtualMachineResourceStack.PushResource($extension, [AzureResourceType]::VirtualMachineExtension)
        }
        $virtualMachineResourceStack.PushResource($virtualMachine, [AzureResourceType]::VirtualMachine)
        Write-Information "Created the Virtual Machine '$($virtualMachineName)' successfully."
    }
    catch {
        # Display error message and roll back
        Write-Error ("Failed to create the Virtual Machine '$($virtualMachineName)'. " `
                + "$($Error[0].Exception.Message)")
        $virtualMachineResourceStack.Rollback()
        return $false
    }

    # Return true after the Virtual Machine has been created
    return $true
}

# This function generates a name for an Azure resource different from a list of existing names
# This function is not applicable for Azure resource names that need to be globally unique
function Generate-AzureResourceName {
    param (
        # The Azure resource name
        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [string]$azureResourceName,

        # The Azure resource type
        [Parameter(Mandatory=$true)]
        [AzureResourceType]$azureResourceType,

        # The list of existing Azure resource names
        [Parameter(Mandatory=$false)]
        $existingNames
    )

    # Trim the Azure resource name
    $baseName = $azureResourceName.Trim()

    # Call Generate-StorageAccountName function to generate an available name for Storage Account
    # As Storage Account name needs to be globally unique and there is a function to check the uniqueness
    if ($azureResourceType -eq [AzureResourceType]::StorageAccount) {
        $tempName = Generate-StorageAccountName -basename $baseName
    }
    else {
        # Increment the counter till a valid name is generated
        $counter = 0
        $tempName = $baseName
        do {
            # Break if tempName is not in the list of existing names
            $matchResult = $existingNames | Where-Object {$_.ToLower() -eq $tempName.ToLower()}
            if ([string]::IsNullOrEmpty($matchResult)) {
                break
            }

            # Generate a temporary name
            $tempName = $baseName + ++$counter
        } while ($true)
    }

    # Display the message if the original name already exists
    if ($azureResourceName -ne $tempName) {
        Write-Information ("The $($azureResourceType) Name '$($azureResourceName)' already exists. `r`n" `
                + "The new $($azureResourceType) Name will be '$($tempName)'. ")
    }

    # Return the unique name
    return $tempName
}

# This function generates an available Storage Account Name
# Returns the generated storage account name, or $null if generating one is not possible
function Generate-StorageAccountName {
    param (
        # The base name
        [Parameter(Mandatory=$true)]
        [string]$baseName
    )

    # Maximum number of tries to get an available name
    $maxTries = 20

    # Trim the base name
    $baseName = $baseName.Trim()

    # Increment the number till a valid Storage Account name is generated
    $tempStorageAccountName = $baseName
    $counter = 0

    do {
        # Set isValidName to false if the tempStorageAccountName is not available
        try {
            # Get the StorageAccountName availability
            $storageAccountNameAvailability = Get-AzureRmStorageAccountNameAvailability -Name $tempStorageAccountName -ErrorAction Stop

            # If the storage account name is available
            if ($storageAccountNameAvailability.NameAvailable) {
                break
            }

            # Exit if the name is not available not because it already exists
            if ($storageAccountNameAvailability.Reason -ne "AlreadyExists") {
                Write-Error ("The Storage Account Name '$($tempStorageAccountName)' is not valid. " `
                        + "The runbook will abort. " `
                        + "Reason: '$($storageAccountNameAvailability.Reason)'. Message: '$($storageAccountNameAvailability.Message)'.")
                return $null
            }
        }
        catch {
            Write-Error ("Cannot verify the Storage Account Name availability. " `
                    + "The runbook will abort. $($Error[0].Exception.Message)")
            return $null
        }

        # Increment the counter
        $counter += 1

        # Generate a temporary tempStorageAccountName
        $tempStorageAccountName = $baseName + $counter
    } while ($counter -lt $maxTries)

    # Check if a valid Storage Account name has been generated
    if ($counter -lt $maxTries) {
        return $tempStorageAccountName
    }

    # Display error message as the maximum number of tries has been used
    Write-Error ("Failed to generate an available Storage Account name, as the tried Storage Account names already exist. " `
            + "Please provide a less commonly used Storage Account name.")
}

# This function invokes a custom script extension on a Virtual Machine
# Returns if the invoke was successful
function Invoke-VirtualMachineCustomScriptExtension {
    param (
        # Resource Group name
        [Parameter(Mandatory = $true)]
        [string]$resourceGroupName,

        # Virtual Machine name
        [Parameter(Mandatory = $true)]
        [string]$virtualMachineName,

        # The custom script to be invoked
        [Parameter(Mandatory = $true)]
        [string]$customScript
    )

    # Keep track of temporary Azure resources to remove once the script is done
    $customScriptResourceStack = [AzureResourceCreatedStack]::New()

    # Get the location of the Virtual Machine
    $tempLocation = (Get-AzureRmVM -Name $VirtualMachineName -ResourceGroupName $ResourceGroupName).Location

    # Create a temporary Storage Account name
    do {
        # Generate a random Storage Account name
        $tempStorageAccountName = -Join ((97..122) | Get-Random -Count 15 | ForEach-Object {[char]$_}) + (Get-Date).ToString("HHmmssfff")

        # Break if the random Storage Account name is available or failed to check its availability
        $tempStorageAccountNameAvailability = Get-AzureRmStorageAccountNameAvailability -Name $tempStorageAccountName -ErrorAction SilentlyContinue
        if ($null -eq $tempStorageAccountNameAvailability -or $tempStorageAccountNameAvailability.NameAvailable) {
            break
        }
    } while ($true)

    # Create the temporary Storage Account
    try {
        Write-Information "Create a temporary Storage Account '$($tempStorageAccountName)' in the Resource Group '$($ResourceGroupName)'."
        $tempStorageAccount = New-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName `
            -Name $tempStorageAccountName `
            -Kind "BlobStorage" `
            -Location $tempLocation `
            -SkuName "Standard_LRS" `
            -AccessTier "Hot" `
            -ErrorAction Stop
        Write-Information "Created a temporary Storage Account '$($tempStorageAccountName)' in the Resource Group '$($ResourceGroupName)' successfully."

        # Push the newly created Storage Account to the stack of created resources
        $customScriptResourceStack.PushResource($tempStorageAccount, [AzureResourceType]::StorageAccount)
    }
    catch {
        # Rollback and exit
        Write-Error ("Failed to create the temporary Storage Account '$($tempStorageAccountName)'. " `
                + "$($Error[0].Exception.Message)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Create a temporary Blob Container
    $tempBlobContainerName = "scriptcontainer"
    try {
        Write-Information "Create a temporary Blob Container '$($tempBlobContainerName)' in the Storage Account '$($tempStorageAccountName)'."
        New-AzureStorageContainer -Name $tempBlobContainerName `
            -Context $tempStorageAccount.Context `
            -ErrorAction Stop
        Write-Information "Created a temporary Blob Container '$($tempBlobContainerName)' in the Storage Account '$($tempStorageAccountName)' successfully."

        # Push the newly created Blob Container to the stack of created resources
        $customScriptResourceStack.PushResourceRollbackExpression(
            "Remove-AzureStorageContainer -Name $($tempBlobContainerName) " `
                + "-Context (Get-AzureRmStorageAccount -ResourceGroupName $($resourceGroupName)" `
                + "-AccountName $($tempStorageAccountName)).Context",
            "Blob Container '$($tempBlobContainerName)'"
        )
    }
    catch {
        # Rollback and exit
        Write-Error ("Failed to create the Blob Container'$($tempBlobContainerName)'. " `
                + "$($Error[0].Exception.Message)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Create a PowerShell script on the server
    $tempFileName = [GUID]::NewGuid().Guid + ".ps1"
    $tempFilePath = ".\" + $tempFileName
    try {
        Write-Information "Create a temporary file '$($tempFileName)' containing the script on the server."
        $customScript | Out-File -FilePath $tempFilePath -Encoding ASCII -ErrorAction Stop
        Write-Information "Created a temporary file '$($tempFileName)' containing the script on the server successfully."

        # Push the newly created file to the stack of created resources
        $customScriptResourceStack.PushResourceRollbackExpression(
            "del $($tempFilePath)",
            "file '$($tempFileName)'"
        )
    }
    catch {
        # Rollback and exit
        Write-Error ("Failed to create a temporary file '$($tempFileName)'. " `
                + "$($Error[0].Exception.Message)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Upload the local file to the Blob Container
    try {
        Write-Information "Upload the file '$($tempFileName)' to the Blob Container '$($tempBlobContainerName)'."
        Set-AzureStorageBlobContent -Container $tempBlobContainerName `
            -File $tempFilePath `
            -Context $tempStorageAccount.Context `
            -Force -ErrorAction Stop
        Write-Information "Uploaded the file '$($tempFileName)' to the Blob Container '$($tempBlobContainerName)' successfully."

        # Push the newly created Blob to the stack of created resources
        $customScriptResourceStack.PushResourceRollbackExpression(
            "Remove-AzureStorageBlob -Container $($tempBlobContainerName) -Blob $($tempFileName) " `
                + "-Context (Get-AzureRmStorageAccount -ResourceGroupName $($ResourceGroupName) " `
                + "-AccountName $($tempStorageAccountName)).Context",
            "Blob '$($tempFileName)'"
        )
    }
    catch {
        # Rollback and exit
        Write-Error ("Failed to upload the file '$($tempFilePath)' to the Blob Container '$($tempBlobContainerName)'. " `
                + "$($Error[0].Exception.Message)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Wait for the Virtual Machine to run
    $maxTry = 10
    $tryCounter = 0
    while ($tryCounter -lt $maxTry) {
        try {
            # Check if the Virtual Machine is running
            $isVirtualMachineRunning = Get-AzureRmVM -Name $VirtualMachineName -ResourceGroupName $ResourceGroupName -Status -ErrorAction Stop `
                | Where-Object {$_.Statuses.DisplayStatus -eq "VM Running"}
            if ($isVirtualMachineRunning) {
                # Display the user message and break the loop
                Write-Information "The Virtual Machine '$($VirtualMachineName)' is running."
                break
            }
            else {
                # Increment the counter, wait for 2 minutes and update the error message
                $tryCounter++
                Start-Sleep -Seconds 120
                $errorMessage = "The Virtual Machine '$($VirtualMachineName)' is not running."
            }
        }
        catch {
            # Increment the counter, wait for 2 minutes and update the error message
            $tryCounter++
            Start-Sleep -Seconds 120
            $errorMessage = "Error: $($Error[0].Exception.Message)"
        }
    }

    # Rollback and exit if the while loop was broken because the maximum number of tries has been reached
    if ($tryCounter -eq $maxTry) {
        Write-Error ("Encountered a problem verifying that the Virtual Machine '$($VirtualMachineName)' is running. " `
                + "$($errorMessage)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Set the script extension to the Virtual Machine
    try {
        Write-Information "Invoke the custom script extension on the Virtual Machine '$($virtualMachineName)'."
        $VMRemoteInstall = Set-AzureRmVMCustomScriptExtension -ResourceGroupName $ResourceGroupName `
            -VMName $virtualMachineName `
            -StorageAccountName $tempStorageAccountName `
            -ContainerName $tempBlobContainerName `
            -FileName $tempFileName `
            -Run $tempFileName `
            -Name "Invoke-CustomScriptExtension" `
            -Location $tempLocation `
            -ErrorAction Stop
        if (($VMRemoteInstall.IsSuccessStatusCode -eq $True) -and ($VMRemoteInstall.StatusCode -eq "OK")) {
            Write-Information "Invoked the custom script extension on the Virtual Machine '$($virtualMachineName)' successfully."
        }
        Else {
            Write-Warning ("Encountered a problem invoking the custom script extension on the Virtual Machine '$($virtualMachineName)'. " `
                    + "The status code is '$($VMRemoteInstall.StatusCode)' and the reason is '$($VMRemoteInstall.ReasonPhrase)'. " `
                    + "Please set the extension on the Virtual Machine '$($virtualMachineName)' manually.")
        }
    }
    catch {
        # Rollback and exit
        Write-Error ("Failed to invoke the custom script extension on the Virtual Machine '$($virtualMachineName)'. " `
                + "$($Error[0].Exception.Message)")
        $customScriptResourceStack.Rollback()
        return $false
    }

    # Remove temporary resources
    Write-Information "Remove the temporary Azure resources."
    $customScriptResourceStack.Rollback()
    return $true
}