Public/Deploy/IaaS/vm/New-CmAzIaasVm.ps1

function New-CmAzIaasVm {

    <#
        .Synopsis
         Deploys multiple virtual machines, over multiple resource groups.
 
        .Description
         Completes the following:
             * Deploys multiple virtual machines over multiple resource groups.
             * Encrypts all os and data disks using a key encryption key from a specified keyvault.
             * Mounts all hard drives set up in the vms.
             * Enables azure monitor and links all vms to the core log analytics workspace.
             * Automatically accepts terms for using custom images.
 
        .Parameter SettingsFile
         File path for the settings file to be converted into a settings object.
 
        .Parameter SettingsObject
         Object containing the configuration values required to run this cmdlet.
 
        .Parameter LocalAdminUsername
         Local admin username for deployed vms, max length 20 characters.
 
        .Parameter LocalAdminPassword
         Local admin passwords for deployed vms, requires three of the following character types:
            * Uppercase
            * Lowercase
            * Numeric
            * Special
 
        .Parameter TagSettingsFile
         File path for the tag settings file to be converted into a tag settings object.
 
        .Component
         IaaS
 
        .Example
         New-CmAzIaasCompute -SettingsFile "c:/directory/settingsFile.yml" -LocalAdminUsername "username" -LocalAdminPassword "password"
 
        .Example
          New-CmAzIaasCompute -SettingsObject $settings -LocalAdminUsername "username" -LocalAdminPassword "password"
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium")]
    param(
        [parameter(Mandatory = $true, ParameterSetName = "Settings File")]
        [String]$SettingsFile,
        [parameter(Mandatory = $true, ParameterSetName = "Settings Object")]
        [Object]$SettingsObject,
        [parameter(Mandatory = $true)]
        [SecureString]$LocalAdminUsername,
        [parameter(Mandatory = $true)]
        [SecureString]$LocalAdminPassword,
        [String]$TagSettingsFile
    )

    $ErrorActionPreference = "Stop"

    try {

        $commandName = $MyInvocation.MyCommand.Name

        Write-CommandStatus -CommandName $commandName

        $SettingsObject = Get-Settings -SettingsFile $SettingsFile -SettingsObject $SettingsObject -CmdletName $commandName

        if ($PSCmdlet.ShouldProcess((Get-CmAzSubscriptionName), "Deploy Virtual Machines")) {

            [Hashtable]$keyVaultDetails = $null

            $keyVaultService = Get-CmAzService -Service $SettingsObject.service.dependencies.keyvault -ThrowIfUnavailable -ThrowIfMultiple

            $keyVault = Get-AzKeyVault -Name $keyVaultService.name

            if (!$keyVault) {
                Write-Error "Cannot find key vault resource, ensure the provided tag is set on a keyvault." -Category InvalidArgument -CategoryTargetName "KeyVault.Tag"
            }
            else {
                Write-Verbose "Keyvault found.."
            }

            $keyEncryptionKey = Get-AzKeyVaultKey -VaultName $keyVault.vaultName -Name $SettingsObject.diskEncryptionKey

            if (!$keyEncryptionKey) {
                Write-Error "Cannot find key encryption key in keyvault." -Category InvalidArgument -CategoryTargetName "KeyVault.DiskEncryptionKey"
            }
            else {
                Write-Verbose "Encryption key found.."
            }

            $keyVaultDetails = @{
                KeyEncryptionKeyUrl = $keyEncryptionKey.id
                ResourceId          = $keyVault.resourceId;
                VaultUri            = $keyVault.vaultUri
            }

            $automationAccount = Get-CmAzService -Service $SettingsObject.service.dependencies.automation -ThrowIfUnavailable -ThrowIfMultiple

            $automationAccountRegistration = Get-AzAutomationRegistrationInfo `
                -ResourceGroupName $automationAccount.resourceGroupName `
                -AutomationAccountName $automationAccount.name  `
                -ErrorAction SilentlyContinue

            if (!$automationAccountRegistration) {
                Write-Error "Cannot find automation registration details." -Category InvalidArgument -CategoryTargetName "AutomationAccountTag"
            }
            else {
                Write-Verbose "Automation Registration details found.."
            }

            $automationAccount.registrationUrl = $automationAccountRegistration.endpoint
            $automationAccount.primaryKey = $automationAccountRegistration.primaryKey
            $automationAccount.nodeConfigurationName = $SettingsObject.desiredConfigName

            $workspace = Get-CmAzService -Service $SettingsObject.service.dependencies.workspace -ThrowIfUnavailable -ThrowIfMultiple

            $allResourceGroups = @()
            $allVirtualMachines = @()
            $allProximityPlacementGroups = @()
            $allAvailabilitySets = @()

            $daysOfWeek = [DayOfWeek].GetEnumNames()

            $localConfigs = Get-CmAzSettingsFile -Path "$PSScriptRoot/localConfigs.yml"

            foreach ($resourceGroup in $SettingsObject.groups) {

                if (!$resourceGroup.location) {
                    $resourceGroup.location = $SettingsObject.location
                }

                $resourceGroup.name = Get-CmAzResourceName -Resource "ResourceGroup" -Architecture "IaaS" -Location $resourceGroup.location -Name $resourceGroup.name

                if ($resourceGroup.proximityPlacementGroups) {

                    foreach ($placementGroup in $resourceGroup.proximityPlacementGroups) {
                        $placementGroup.location ??= $resourceGroup.location

                        $placementGroup.templateName = Get-CmAzResourceName -Resource "deployment" -Architecture "IaaS" -Location $placementGroup.location -Name "$commandName-pg-$($placementGroup.name)"
                        $placementGroup.generatedName = Get-CmAzResourceName -Resource "ProximityPlacementGroup" -Architecture "IaaS" -Location $placementGroup.location -Name $placementGroup.name
                        Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "ProximityPlacementGroup" -ResourceServiceContainer $placementGroup

                        $placementGroup.resourceGroupName = $resourceGroup.name

                        $allProximityPlacementGroups += $placementGroup
                    }
                }
                else {

                    $placementGroup = @{
                        resourceGroupName = $resourceGroup.name
                        generatedName     = "none";
                        templateName      = "none";
                        location          = "uksouth";
                        service           = @{
                            publish = @{
                                proximityPlacementGroup = "none"
                            }
                        }
                    }

                    $allProximityPlacementGroups += $placementGroup
                }

                if ($resourceGroup.availabilitySets) {

                    foreach ($set in $resourceGroup.availabilitySets) {
                        $set.templateName = Get-CmAzResourceName -Resource "deployment" -Architecture "IaaS" -Location $set.location -Name "$commandName-avs-$($set.name)"
                        $set.generatedName = Get-CmAzResourceName -Resource "AvailabilitySet" -Architecture "IaaS" -Location $set.location -Name $set.name

                        if ($set.proximityPlacementGroup) {
                            $set.proximityPlacementGroup = ($resourceGroup.proximityPlacementGroups | Where-Object { $_.name -eq $set.proximityPlacementGroup }).generatedName

                            if (!$set.proximityPlacementGroup) {
                                Write-Error "Proximity group not found in settings..."
                            }
                        }
                        else {
                            $set.proximityPlacementGroup = ""
                        }

                        $set.location ??= $resourceGroup.location
                        $set.resourceGroupName = $resourceGroup.name

                        Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "availabilitySet" -ResourceServiceContainer $set

                        $allAvailabilitySets += $set
                    }
                }
                else {
                    $set = @{
                        resourceGroupName         = $resourceGroup.name;
                        generatedName             = "none";
                        templateName              = "none;"
                        location                  = "uksouth";
                        platformFaultDomainCount  = "2";
                        platformUpdateDomainCount = "2";
                        sku                       = "aligned";
                        service                   = @{
                            publish = @{
                                proximityPlacementGroup = "none"
                            }
                        }
                    }

                    $allAvailabilitySets += $set
                }

                foreach ($virtualMachine in $resourceGroup.virtualMachines) {

                    if ($virtualMachine.zone -And $virtualMachine.availabilitySet) {
                        Write-Error "Virtual Machines with both Availability Zones and Availability Sets are not supported by Azure..." -Category InvalidArgument -CategoryTargetName "VirtualMachines"
                    }

                    if (!$virtualMachine.location) {
                        $virtualMachine.location = $resourceGroup.location
                    }

                    if (!$virtualMachine.plan) {
                        $virtualMachine.plan = ""
                    }
                    else {

                        Write-Verbose "Using custom image: $($virtualMachine.plan.publisher) $($virtualMachine.plan.product)"
                        $terms = Get-AzMarketplaceTerms -Publisher $virtualMachine.plan.publisher -Product $virtualMachine.plan.product -Name $virtualMachine.plan.name

                        if (!$terms.Accepted) {

                            Write-Warning "Image usage terms will be accepted automatically.."
                            Set-AzMarketplaceTerms -Publisher $virtualMachine.plan.publisher -Product $virtualMachine.plan.product -Name $virtualMachine.plan.name -Terms $terms -Accept
                        }
                    }

                    $virtualMachine.resourceGroupName = $resourceGroup.name

                    Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "vnet" -ResourceServiceContainer $virtualMachine.networking -IsDependency

                    $virtualNetwork = Get-CmAzService -Service $virtualMachine.networking.service.dependencies.vnet -Location $virtualMachine.location -ThrowIfUnavailable -ThrowIfMultiple

                    $virtualMachine.networking.virtualNetworkId = $virtualNetwork.resourceId

                    Write-Verbose "Generating standardised resource names..."
                    $virtualMachine.templateName = Get-CmAzResourceName -Resource "deployment" -Architecture "Core" -Location $virtualMachine.location -Name "$commandName-$($virtualMachine.name)"
                    $virtualMachine.computerName = Get-CmAzResourceName -Resource "ComputerName" -Architecture "Core" -Location $virtualMachine.location -Name $virtualMachine.name -MaxLength 15
                    $virtualMachine.fullName = Get-CmAzResourceName -Resource "VirtualMachine" -Architecture "IaaS" -Location $virtualMachine.location -Name $virtualMachine.name
                    $virtualMachine.nicName = Get-CmAzResourceName -Resource "NetworkInterfaceCard" -Architecture "IaaS" -Location $virtualMachine.location -Name $virtualMachine.fullName
                    $virtualMachine.osDisk.Name = Get-CmAzResourceName -Resource "OSDisk" -Architecture "IaaS" -Location $virtualMachine.location -Name $virtualMachine.fullName

                    $virtualMachine.osDisk.caching ??= "None"
                    $virtualMachine.zone ??= "none"

                    if ($virtualMachine.timeZone) {

                        Write-Verbose "TimeZone settings found."
                        $virtualMachine.timeZone = $localConfigs.timeZones.contains($virtualMachine.timeZone) ? $virtualMachine.timeZone : (Write-Error "Please provide valid Windows timezone format...")
                    }
                    else {
                        $virtualMachine.timeZone = "UTC"
                    }

                    if ($virtualMachine.antimalware) {

                        Write-Verbose "Antimalware settings found."
                        $virtualMachine.antimalware.exclusions = @{
                            paths      = $virtualMachine.antimalware.exclusions.paths -join (';');
                            extensions = $virtualMachine.antimalware.exclusions.extensions -join (';');
                            processes  = $virtualMachine.antimalware.exclusions.processes -join (';')
                        }

                        if ($virtualMachine.antimalware.schedule) {
                            $virtualMachine.antimalware.schedule.scanType ??= "Quick"
                            $virtualMachine.antimalware.schedule.day = $localConfigs.days.$($virtualMachine.antimalware.schedule.day)
                            $virtualMachine.antimalware.schedule.time = $virtualMachine.antimalware.schedule.time.toString()
                            $virtualMachine.antimalware.schedule.isEnabled = "true"
                        }
                        else {
                            $virtualMachine.antimalware.schedule = @{ isEnabled = "false" }
                        }

                        $virtualMachine.antimalware.realtimeProtectionEnabled = $virtualMachine.antimalware.realtimeProtectionEnabled ? "true" : "false"
                    }
                    else {
                        $virtualMachine.antimalware = @{
                            enable     = $false
                            exclusions = @{
                                Path       = "";
                                Extensions = "";
                                Processes  = "";
                            }
                            schedule   = @{ isEnabled = "false" }
                        }
                    }

                    if ($virtualMachine.availabilitySet) {

                        $virtualMachine.availabilitySet = ($resourceGroup.availabilitySets | Where-Object { $_.name -eq $virtualMachine.availabilitySet }).generatedName

                        if (!$virtualMachine.availabilitySet) {
                            Write-Error "Availability Set not found in settings..."
                        }
                    }
                    else {
                        $virtualMachine.availabilitySet = ""
                    }

                    $virtualMachine.vulnerabilityScan ??= $false

                    Write-Verbose "Building data disks..."

                    if ($virtualMachine.dataDisks) {

                        for ($i = 0; $i -lt $virtualMachine.dataDisks.Count; $i++) {
                            $virtualMachine.dataDisks[$i].name = Get-CmAzResourceName -Resource "DataDisk" -Architecture "IaaS" -Location $virtualMachine.location -Name "$($virtualMachine.name)$($i + 1)";
                            $virtualMachine.dataDisks[$i].Lun = $i + 1;
                            $virtualMachine.dataDisks[$i].CreateOption = "Empty";
                            $virtualMachine.dataDisks[$i].caching ??= "None"
                        }
                    }
                    else {
                        $virtualMachine.dataDisks = @()
                    }

                    Write-Verbose "Building update tag.."
                    if ($virtualMachine.updateGroup -and $virtualMachine.updateFrequency) {

                        $scheduleSettings = Get-CmAzSettingsFile -Path "$PSScriptRoot/scheduleTypes.yml"

                        $inValidScheduleSettings = !$scheduleSettings -or !$scheduleSettings.updateGroups[$virtualMachine.updateGroup] -or    (!$scheduleSettings.updateFrequencies[$virtualMachine.updateFrequency] -and $daysOfWeek -notcontains $virtualMachine.updateFrequency)

                        if ($inValidScheduleSettings) {
                            Write-Error "No valid schedule settings." -Category ObjectNotFound -CategoryTargetName "scheduleTypeSettingsObject"
                        }

                        $virtualMachine.updateTag = "$($virtualMachine.updateGroup)-$($virtualMachine.updateFrequency)".ToLower()
                    }
                    else {
                        $virtualMachine.updateTag = ""
                    }

                    Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "vm" -ResourceServiceContainer $virtualMachine
                    Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "nic" -ResourceServiceContainer $virtualMachine
                }

                Set-GlobalServiceValues -GlobalServiceContainer $SettingsObject -ServiceKey "resourceGroup" -ResourceServiceContainer $resourceGroup

                $allResourceGroups += $resourceGroup;
                $allVirtualMachines += $resourceGroup.virtualMachines
            }

            Write-Verbose "Deploying virtual machines..."

            if (!$allVirtualMachines) {
                Write-Verbose "No valid virtual machines available for deployment..."
            }
            else {

                $deploymentNameRgs = Get-CmAzResourceName -Resource "Deployment" -Architecture "IaaS" -Location $SettingsObject.location -Name "$commandName-Rgs"

                New-AzDeployment `
                    -Name $deploymentNameRgs `
                    -TemplateFile "$PSScriptRoot\New-CmAzIaasVm.ResourceGroups.json" `
                    -Location $SettingsObject.location `
                    -TemplateParameterObject @{
                        ResourceGroups = $allResourceGroups
                    }

                # Cross resource group deployments for VMs appear to still to require the use of New-AzResourceGroupDeployment, instead of subscription level deployment
                # through New-AzDeployment, which doesn't seem right.
                # Deploying the same template through New-AzDeployment triggers a BadRequest: InvalidRequestFormat error.

                $credentials = @{
                    "LocalAdminUsername" = ConvertFrom-SecureString $LocalAdminUsername -AsPlainText;
                    "LocalAdminPassword" = ConvertFrom-SecureString $LocalAdminPassword -AsPlainText;
                }

                $deploymentNameVm = Get-CmAzResourceName -Resource "Deployment" -Architecture "IaaS" -Location $SettingsObject.location -Name $commandName

                New-AzResourceGroupDeployment `
                    -Name $deploymentNameVm  `
                    -ResourceGroupName $allResourceGroups[0].name `
                    -TemplateFile "$PSScriptRoot\New-CmAzIaasVm.json" `
                    -TemplateParameterObject @{
                        Credentials              = $credentials
                        VirtualMachines          = $allVirtualMachines
                        ProximityPlacementGroups = $allProximityPlacementGroups
                        AvailabilitySets         = $allAvailabilitySets
                        WorkspaceId              = $workspace.resourceId
                        KeyVault                 = $keyVaultDetails
                        AutomationAccount        = $automationAccount
                    }
            }

            Set-DeployedResourceTags -TagSettingsFile $TagSettingsFile -ResourceGroupIds $allResourceGroups.name

            Write-CommandStatus -CommandName $commandName -Start $false
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($PSItem)
    }
}