AZSBTools.psm1

function New-SBAZServicePrincipal {
<#
 .SYNOPSIS
  Function to create Azure AD Service Principal
 
 .DESCRIPTION
  Function to create Azure AD Service Principal
  The use case intended for this function is to use the Service Principal to run PowerShell scripts against an Azure subscription
 
 .PARAMETER ServicePrincipalName
  One or more Service Principal Names
 
 .PARAMETER Environment
  Name of the Azure cloud. This parameter default to Azure Commercial cloud.
  As of 15 March 2018 that list is:
    AzureGermanCloud
    AzureCloud
    AzureUSGovernment
    AzureChinaCloud
    To see an updated list, use:
        (Get-AzureRMEnvironment).Name
 
 .PARAMETER Role
  This parameter is used to assign Role/Permissions for te Service Principal in the current subscription.
  The default value is 'Owner' role.
  As of 16 March 2018 the following default roles are defined:
    API Management Service Contributor
    Application Insights Component Contributor
    Automation Operator
    BizTalk Contributor
    Classic Network Contributor
    Classic Storage Account Contributor
    Classic Storage Account Key Operator Service Role
    Classic Virtual Machine Contributor
    ClearDB MySQL DB Contributor
    Contributor
    Cosmos DB Account Reader Role
    Data Factory Contributor
    Data Lake Analytics Developer
    DevTest Labs User
    DNS Zone Contributor
    DocumentDB Account Contributor
    Intelligent Systems Account Contributor
    Log Analytics Contributor
    Log Analytics Reader
    Network Contributor
    New Relic APM Account Contributor
    Owner
    Reader
    Redis Cache Contributor
    Scheduler Job Collections Contributor
    Search Service Contributor
    Security Manager
    SQL DB Contributor
    SQL Security Manager
    SQL Server Contributor
    Storage Account Contributor
    Storage Account Key Operator Service Role
    Traffic Manager Contributor
    User Access Administrator
    Virtual Machine Contributor
    Web Plan Contributor
    Website Contributor
  For more details on roles, type in:
    Get-AzureRmRoleDefinition | select name,description,actions | Out-GridView
 
 .EXAMPLE
  $SPList = New-SBAZServicePrincipal -ServicePrincipalName samtest1,sam1demo
 
 .EXAMPLE
  $SPN = New-SBAZServicePrincipal -ServicePrincipalName PowerShell05 -Environment AzureUSGovernment
  # The above line creates the SPN and gives it 'Owner' permission/role in the current subscription
  $SPN | Export-Csv .\PowerShell05-SPN.csv -NoTypeInformation # This line saves the $SPN to CSV (not the password)
 
  # To use the SPN in future automations:
  # $SPN = Import-Csv .\PowerShell05-SPN.csv
  # Login-AzureRmAccount -Credential (Get-SBCredential $SPN.ServicePrincipalName) -ServicePrincipal -TenantId $SPN.TenantID -Environment $SPN.Environment
 
 .OUTPUTS
  The function returns a PS Object for each input Service Principal Name containing the following properties:
    ServicePrincipalName
    TenantId
    Environment
    Role
 
 .LINK
  https://superwidgets.wordpress.com/2018/03/15/new-sbazserviceprincipal-cmdlet-to-create-new-azure-ad-service-principal-added-to-azsbtools-powershell-module/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 14 March 2018
  v0.2 - 15 March 2018 - Added 'Environment' parameter
  v0.3 - 16 March 2018 - Added 'Role' parameter, changed output to a custom PS Object
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String[]]$ServicePrincipalName,
        [Parameter(Mandatory=$false)][ValidateSet('AzureCloud','AzureUSGovernment','AzureGermanCloud','AzureChinaCloud')][String]$Environment = 'AzureCloud',
        [Parameter(Mandatory=$false)][String]$Role = 'Owner'
    )

    Begin { 
        $Subscription = Connect-AzureRmAccount -Environment $Environment  
    }

    Process {
        
        if ($Subscription.Context.Subscription.Name) { 

            Write-Log 'Identified',$Subscription.Context.Subscription.Name,'subscription in the',$Subscription.Context.Environment.Name,'cloud' Green,Cyan,Green,Cyan,Green

            $SPList = foreach ($AppName in $ServicePrincipalName) {

                $AppCred = Get-SBCredential -UserName $AppName
        
                #region Create/Validate Azure AD App
                Remove-Variable App -EA 0 
                if ($App = Get-AzureRmADApplication -DisplayName $AppName) {
                    Write-Log 'Validated app:',$App.Displayname Green,Cyan 
                } else {
                    $App = New-AzureRmADApplication -DisplayName $AppName -IdentifierUris $AppName
                    Write-Log 'Created app:',$App.Displayname Green,Cyan 
                }            
                #endregion

                #region Create/Validate Azure AD Service Principal
                Remove-Variable ServicePrincipal -EA 0 
                if ($ServicePrincipal = Get-AzureRmADServicePrincipal | where { $PSItem.ApplicationId -eq $App.ApplicationId.Guid }) {
                    Write-Log 'Validated Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan 
                } else {
                    $ServicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $App.ApplicationId.Guid -Password $AppCred.Password
                    Write-Log 'Created Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan 
                }            
                #endregion

                #region Assign Role (Permissions)
                Write-Log 'Assigning role',$Role Green,Cyan -NoNewLine
                $Result = try {
                    New-AzureRmRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $Role -Scope "/subscriptions/$($Subscription.Context.Subscription.Id)" -EA 1
                    Write-Log 'done' Green
                } catch {
                    Write-Log $PSItem.Exception.Message Yellow
                }
                #endregion

                [PSCustomObject][Ordered]@{
                    ServicePrincipalName = $AppName
                    TenantId             = (Get-AzureRmTenant).Id 
                    Environment          = $Environment
                    Role                 = $Role
                }

            }
                    
        } else {
            Write-Log 'No subscriptions found for account',$Subscription.Context.Account.Id,'in the',$Subscription.Context.Environment.Name,'cloud' Magenta,Yellow,Magenta,Yellow,Magenta
        }        

    }

    End {
        $SPList
    }
}

function Deploy-AzureARMVM {
<#
 .SYNOPSIS
  Function to automate provisioning of Azure ARM VM(s)
 
 .DESCRIPTION
  Function to automate provisioning of Azure ARM VM(s)
 
 .PARAMETER SubscriptionName
    Name of existing Azure subscription
 
 .PARAMETER Location
    Name of Azure Data center/Location
    Example: 'eastus'
    To see location list use:
        Get-AzureRmLocation | sort Location | Select Location
 
 .PARAMETER ResourceGroup
    Name of Resource Group.
    Example: 'VMGroup17'
    The script will create it if it does not exist
 
 .PARAMETER AvailabilitySetName
    Example: 'Availability17'
     The script will create it if it does not exist
 
 .PARAMETER ConfirmShutdown
    This switch accepts $true or $False, and defaaults to $False
    If adding existing VMs to Availaibility set, the script must shut down the VMs
 
 .PARAMETER StorageAccountPrefix
    Only lower case letters and numbers, must be Azure (globally) unique
 
 .PARAMETER AdminName
    Example: 'myAdmin17'
    This will be the new VM local administrator
 
 .PARAMETER VMName
    Example: ('vm01','vm02')
    Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set
 
 .PARAMETER VMSize
    Example: 'Standard_A1_v2'
    To see available sizes in this Azure location use:
        (Get-AzureRoleSize).RoleSizeLabel
 
 .PARAMETER WinOSImage
    This defaults to '2012-R2-Datacenter'
    Available options:
        '2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server'
    To see current options in a given Azure Location use:
        (Get-AzureRMVMImageSku -Location usgovvirginia -Publisher MicrosoftWindowsServer -Offer WindowsServer).Skus
    For more information see https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage
 
 .PARAMETER vNetName
    Example: 'Seventeen'
    This will be the name of the virtual network to be created/updated if exist
 
 .PARAMETER vNetPrefix
    Example: '10.17.0.0/16'
    To be created/updated
 
 .PARAMETER SubnetName
    Example: 'vmSubnet'
    This will be the name of the subnet to be created/updated
 
 .PARAMETER SubnetPrefix
    Example: '10.17.0.0/24'
    Must be subset of vNetPrefix above - to be created/updated
 
 .PARAMETER LogFile'
    Path to log file where this scrit will log its commands and output
    Default is ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
 
 .EXAMPLE
    Connect-AzureRmAccount -Environment AzureUSGovernment
    $myParamters = @{
        SubscriptionName = 'Azure Government T1'
        Location = 'usgovvirginia'
        ResourceGroup = 'EncryptionTest01'
        AvailabilitySetName = 'AvailabilityTest01'
        ConfirmShutdown = $false
        StorageAccountPrefix = 'sam150318a'
        AdminName = 'myAdmin150318a'
        VMName = @('vm01','vm02','vm03')
        VMSize = 'Standard_A0'
        WinOSImage = '2016-Datacenter'
        vNetName = 'EncryptionTest01VNet'
        vNetPrefix = '10.3.0.0/16'
        SubnetName = 'vmSubnet'
        SubnetPrefix = '10.3.15.0/24'
    }
    Deploy-AzureARMVM @myParamters
 
 .LINK
  http://www.exigent.net/blog/microsoft-azure/provisioning-and-tearing-down-azure-virtual-machines/
 
 .NOTES
  Function by Sam Boutros
    3 January 2017 - v0.1 - Initial release
    19 January 2017 - v0.2
        Updated parameters - set to mandatory
        Updated Storage Account creation region, create a separate storage account for each VM
        Updated Initialize region; removing subscription login, adding input echo, adding error handling
        Added functionality to configure VMs in availability set
    5 March 2018 - v0.3 Cosmetic updates
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$SubscriptionName         , # Example: 'Sam Test 1' # Name of existing Azure subscription
        [Parameter(Mandatory=$true)][String]$Location                 , # Example: 'eastus' # Get-AzureRmLocation | sort Location | Select Location
        [Parameter(Mandatory=$true)][String]$ResourceGroup            , # Example: 'VMGroup17' # To be created if not exist
        [Parameter(Mandatory=$false)][String]$AvailabilitySetName     , # Example: 'Availability17' # To be created if not exist
        [Parameter(Mandatory=$false)][Switch]$ConfirmShutdown = $false, # If adding existing VMs to Availaibility set, the script must shut down the VMs
        [Parameter(Mandatory=$false)][String]$StorageAccountPrefix    , # To be created if not exist, only lower case letters and numbers, must be Azure unique
        [Parameter(Mandatory=$true)][String]$AdminName                , # Example: 'myAdmin17' # This will be the new VM local administrator
        [Parameter(Mandatory=$true)][String[]]$VMName                 , # Example: ('vm01','vm02') # Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set
        [Parameter(Mandatory=$true)][String]$VMSize                   , # Example: 'Standard_A1_v2' # (Get-AzureRoleSize).RoleSizeLabel to see available sizes in this Azure location
        [Parameter(Mandatory=$false)][ValidateSet('2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server')]
            [String]$WinOSImage = '2012-R2-Datacenter'               , # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage
        [Parameter(Mandatory=$true)][String]$vNetName                 , # Example: 'Seventeen' # This will be the name of the virtual network to be created/updated if exist
        [Parameter(Mandatory=$true)][String]$vNetPrefix               , # Example: '10.17.0.0/16' # To be created/updated
        [Parameter(Mandatory=$true)][String]$SubnetName               , # Example: 'vmSubnet' # This will be the name of the subnet to be created/updated
        [Parameter(Mandatory=$true)][String]$SubnetPrefix             , # Example: '10.17.0.0/24' # Must be subset of vNetPrefix above - to be created/updated
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {

        #region Initialize
        if (!(Test-Path (Split-Path $LogFile))) { New-Item -Path (Split-Path $LogFile) -ItemType directory -Force | Out-Null }
        Write-Log 'Input received:' Green $LogFile
        write-log " SubscriptionName: $SubscriptionName" Cyan $LogFile
        write-log " Location: $Location" Cyan $LogFile
        write-log " ResourceGroup: $ResourceGroup" Cyan $LogFile
        write-log " AvailabilitySetName: $AvailabilitySetName" Cyan $LogFile
        write-log " ConfirmShutdown: $ConfirmShutdown" Cyan $LogFile
        write-log " StorageAccountPrefix: $StorageAccountPrefix" Cyan $LogFile
        write-log " AdminName: $AdminName" Cyan $LogFile
        write-log " VMName(s): $($VMName -join ', ')" Cyan $LogFile
        write-log " VMSize: $VMSize" Cyan $LogFile
        write-log " vNetName: $vNetName" Cyan $LogFile
        write-log " vNetPrefix: $vNetPrefix" Cyan $LogFile
        write-log " SubnetName: $SubnetName" Cyan $LogFile
        write-log " SubnetPrefix: $SubnetPrefix"  Cyan $LogFile
        $Cred = Get-SBCredential -UserName $AdminName 
        #endregion

        #region Connect to Azure subscription
        Write-Log 'Connecting to Azure subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine
        try { 
            $Result = Get-AzureRmSubscription â€“SubscriptionName $SubscriptionName -ErrorAction Stop | Select-AzureRmSubscription 
            Write-Log 'done' Green $LogFile    
            Write-Log ($Result | Out-String).Trim() Cyan $LogFile
        } catch {
            throw "unable to get Azure Subscription '$SubscriptionName'"
        }
        #endregion

        #region Create/Update Resource group
        Write-Log 'Create/Update Resource group',$ResourceGroup Green,Cyan $LogFile -NoNewLine
        try {
            $Result = New-AzureRmResourceGroup -Name $ResourceGroup -Location $Location -Force -ErrorAction Stop
            Write-Log 'done' Green $LogFile    
            Write-Log ($Result | Out-String).Trim() Cyan $LogFile
        } catch {
            throw "Failed to create Resource Group '$ResourceGroup'"
        }
        #endregion

        #region Create/Update Subnet and vNet
        Write-Log 'Creating/updating vNet',$vNetName,$vNetPrefix,'and subnet',$SubnetName,$SubnetPrefix Cyan,Green,DarkYellow,Cyan,Green,DarkYellow $LogFile -NoNewLine
        $Subnet = New-AzureRmVirtualNetworkSubnetConfig -Name $SubnetName -AddressPrefix $SubnetPrefix
        $vNet = New-AzureRmVirtualNetwork -Name $vNetName -ResourceGroupName $ResourceGroup -Location $Location -AddressPrefix $vNetPrefix -Subnet $Subnet -Force
        Write-Log 'done' Green
        #endregion

    }

    Process {
        foreach ($Name in $VMName) { # Provision Azure VM(s)

            #region Create Storage Account if it does not exist
            $StorageAccountName = "stor$($StorageAccountPrefix.ToLower())$($Name.ToLower())"
            if ($StorageAccountName.Length -gt 20) { 
                Write-Log 'Storage account name',$StorageAccountName,'is too long, using first 20 characters only..' Green,Yellow,Green $LogFile
                $StorageAccountName = $StorageAccountName.Substring(0,19) 
            }  
            Write-Log 'Creating Storage Account',$StorageAccountName Green,Cyan $LogFile
            try {
                $StorageAccount = Get-AzureRmStorageAccount -Name $StorageAccountName -ResourceGroupName $ResourceGroup -ErrorAction Stop
                Write-Log 'Using existing storage account',$StorageAccountName Green,Cyan $LogFile
            } catch {
                $i=0
                $DesiredStorageAccountName = $StorageAccountName
                while (!(Get-AzureRmStorageAccountNameAvailability $StorageAccountName).NameAvailable) {
                    $i++
                    $StorageAccountName = "$StorageAccountName$i"
                }
                if ($DesiredStorageAccountName -ne $StorageAccountName ) {
                    Write-Log 'Storage account',$DesiredStorageAccountName,'is taken, using',$StorageAccountName,'instead (available)' Greem,Yellow,Green,Cyan,Green $LogFile
                }
                try {
                    $Splatt = @{
                        ResourceGroupName = $ResourceGroup
                        Name              = $StorageAccountName 
                        SkuName           = 'Standard_LRS' 
                        Kind              = 'Storage' 
                        Location          = $Location 
                        ErrorAction       = 'Stop'
                    }
                    $StorageAccount = New-AzureRmStorageAccount @Splatt
                    Write-Log 'Created storage account',$StorageAccountName Green,Cyan $LogFile
                } catch {
                    Write-Log 'Failed to create storage account',$StorageAccountName Magenta,Yellow $LogFile
                    throw $PSItem.exception.message
                }
            }
            #endregion

            #region Create/validate Availability Set
            if ($AvailabilitySetName) {
                Write-Log 'Creating/verifying Availability Set',$AvailabilitySetName Green,Cyan $LogFile
                try {
                    $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -ErrorAction Stop
                    Write-Log 'Availability Set',$AvailabilitySetName,'already exists' Green,Yellow,Green $LogFile
                    Write-Log ($AvailabilitySet | Out-String).Trim() Cyan $LogFile
                } catch {
                    try {
                        $AvailabilitySet = New-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -Location $Location -ErrorAction Stop
                        Write-Log 'Created Availability Set',$AvailabilitySetName Green,Cyan $LogFile
                    } catch {
                        Write-Log 'Failed to create Availability Set',$AvailabilitySetName Magenta,Yellow $LogFile
                        throw $PSItem.exception.message
                    }
                }
                if ($AvailabilitySet.Location -ne $Location) {
                    Write-Log 'Unable to proceed, Availability set must be in the same location',$AvailabilitySet.Location,'as the desired VM location',$Location Magenta,Yellow,Magenta,Yellow $LogFile
                    break
                }
            }
        #endregion

            try {
                $ExistingVM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -ErrorAction Stop
                Write-Log 'VM',$ExistingVM.Name,'already exists' Green,Yellow,Gree $LogFile
                if ($AvailabilitySetName) {
                    if ($ConfirmShutdown) {
                        Write-Log 'Shutting down VM',$Name,'to add it to Availability set',$AvailabilitySetName Green,Cayn,Green,Cyan $LogFile
                        Stop-AzureRmVM -Name $Name -Force -StayProvisioned -ResourceGroupName $ResourceGroup -Confirm:$false

                        # Remove current VM
                        Remove-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -Force -Confirm:$false

                        # Prepare to recreate VM
                        $VM = New-AzureRmVMConfig -VMName $ExistingVM.Name -VMSize $ExistingVM.HardwareProfile.VmSize -AvailabilitySetId $AvailabilitySet.Id
                        Set-AzureRmVMOSDisk -VM $VM -VhdUri $ExistingVM.StorageProfile.OsDisk.Vhd.Uri -Name $ExistingVM.Name -CreateOption Attach -Windows

                        #Add Data Disks
                        foreach ($Disk in $ExistingVM.StorageProfile.DataDisks) { 
                            Add-AzureRmVMDataDisk -VM $VM -Name $Disk.Name -VhdUri $Disk.Vhd.Uri -Caching $Disk.Caching -Lun $Disk.Lun -CreateOption Attach -DiskSizeInGB $Disk.DiskSizeGB
                        }

                        #Add NIC(s)
                        foreach ($NIC in $ExistingVM.NetworkInterfaceIDs) {
                            Add-AzureRmVMNetworkInterface -VM $VM -Id $NIC
                        }

                        # Recreate the VM as part of the Availability Set
                        New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $ExistingVM.Location -VM $VM -DisableBginfoExtension
                    } else {
                        Write-Log 'To add existing VM(s) to availability set, the VM(s) must be shut down. Use the','-ConfirmShutdown:$true','switch' Yellow,Cyan,Yellow $LogFile
                        break
                    }
                }
            } catch {
                Write-Log 'Preparing to create new VM',$Name Green,Cyan $LogFile

                Write-Log 'Requesting/updating public IP address assignment',"$Name-PublicIP" Green,Cyan $LogFile 
                $PublicIp = New-AzureRmPublicIpAddress -Name "$Name-PublicIP" -ResourceGroupName $ResourceGroup -Location $Location -AllocationMethod Dynamic -Force

                Write-Log 'Provisining/updating vNIC',"$Name-vNIC" Green,Cyan $LogFile
                $vNIC = New-AzureRmNetworkInterface -Name "$Name-vNIC" -ResourceGroupName $ResourceGroup -Location $Location -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id -Force

                Write-Log 'Provisioning VM configuration object for VM',$Name Green,Cyan $LogFile
                if ($AvailabilitySetName) {
                    $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize -AvailabilitySetId $AvailabilitySet.Id
                } else {
                    $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize 
                }

                Write-Log 'Configuring VM OS (Windows),',$Cred.UserName,'local admin' Green,Cyan,Green $LogFile
                $VM = Set-AzureRmVMOperatingSystem -VM $VM -Windows -ComputerName $Name -Credential $Cred -ProvisionVMAgent -EnableAutoUpdate

                Write-Log 'Selecting VM image - Latest',$WinOSImage Green,Cyan $LogFile
                $VM = Set-AzureRmVMSourceImage -VM $VM -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus $WinOSImage -Version "latest"

                Write-Log 'Adding vNIC' Green $LogFile
                $VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $vNIC.Id 

                $VhdUri = "$($StorageAccount.PrimaryEndpoints.Blob.ToString())vhds/$($Name)-OsDisk1.vhd"
                Write-Log 'Configuring OS Disk',$VhdUri Green,Cyan $LogFile
                $VM = Set-AzureRmVMOSDisk -VM $VM -Name 'OSDisk' -VhdUri $VhdUri -CreateOption FromImage

                Write-Log 'Creating VM..' Green -NoNewLine
                New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $Location -VM $VM 
                Write-Log 'done' Green $LogFile
                $DoneVM = Get-AzureRmVM | where { $_.Name -eq $Name } | FT -a 
                Write-Log ($DoneVM | Out-String).Trim() cyan $LogFile
            }
        }
    }

    End {
        if ($AvailabilitySetName) {
            $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName
            $VMDomains = $AvailabilitySet.VirtualMachinesReferences | foreach { 
                $VM = Get-AzureRMVM -Name (Get-AzureRmResource -Id $_.id).Name -ResourceGroup $ResourceGroup -Status
                [PSCustomObject][Ordered]@{
                    Name         = $VM.Name
                    FaultDomain  = $VM.PlatformFaultDomain
                    UpdateDomain = $VM.PlatformUpdateDomain
                }
            }
            Write-Log ($VMDomains | sort Name | FT -a | Out-String).Trim() Cyan $LogFile
        } 
    }

}

function Expand-JSON {
<#
 .SYNOPSIS
  Function to expand a custom PowerShell object in a more readable format
 
 .DESCRIPTION
  Function to expand a custom PowerShell object in a more readable format
  The ConvertFrom-Json cmdlet of the Microsoft.PowerShell.Utility module outputs
  a PS Custom Object that often contains sub objects and so on.
  This function expands all objects and displays the key/value pairs in a
  more humanly readable format - see the example
 
 .PARAMETER JSON
  PS Custom Object, typically the output of ConvertFrom-Json cmdlet - see the example
 
 .PARAMETER Parent
  This is optional parameter used to show sub-objects when using the function recursively
 
 .EXAMPLE
  Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-JSON
  where the contents of Storage1.json file are:
 
    {
      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "storageAccountType": {
          "type": "string",
          "defaultValue": "Standard_LRS",
          "allowedValues": [
            "Standard_LRS",
            "Standard_GRS",
            "Standard_ZRS",
            "Premium_LRS"
          ],
          "metadata": {
            "description": "Storage Account type"
          }
        }
      },
      "variables": {
        "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]"
      },
      "resources": [
        {
          "type": "Microsoft.Storage/storageAccounts",
          "name": "[variables('storageAccountName')]",
          "apiVersion": "2016-01-01",
          "location": "[resourceGroup().location]",
          "sku": {
              "name": "[parameters('storageAccountType')]"
          },
          "kind": "Storage",
          "properties": {
          }
        }
      ],
      "outputs": {
          "storageAccountName": {
              "type": "string",
              "value": "[variables('storageAccountName')]"
          }
      }
    }
   
  The output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json would look like:
 
    $schema : https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#
    contentVersion : 1.0.0.0
    parameters : @{storageAccountType=}
    variables : @{storageAccountName=[concat(uniquestring(resourceGroup().id), 'standardsa')]}
    resources : {@{type=Microsoft.Storage/storageAccounts; name=[variables('storageAccountName')]; apiVersion=2016-01-01; location=[resourceGroup().location]; sku=; kind=Storage; properties=}}
    outputs : @{storageAccountName=}
 
  which does not show sub-objects such as parameters.storageAccountType.allowedValues, parameters.storageAccountType.defaultValue, ...
 
  However, the output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-JSON
  shows all objects, sub-objects, and their key/pair values:
 
    $schema: https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#
    contentVersion: 1.0.0.0
    outputs.storageAccountName.type: string
    outputs.storageAccountName.value: [variables('storageAccountName')]
    parameters.storageAccountType.allowedValues: Standard_LRS, Standard_GRS, Standard_ZRS, Premium_LRS
    parameters.storageAccountType.defaultValue: Standard_LRS
    parameters.storageAccountType.metadata.description: Storage Account type
    parameters.storageAccountType.type: string
    resources.apiVersion: 2016-01-01
    resources.kind: Storage
    resources.location: [resourceGroup().location]
    resources.name: [variables('storageAccountName')]
    resources.sku.name: [parameters('storageAccountType')]
    resources.type: Microsoft.Storage/storageAccounts
    variables.storageAccountName: [concat(uniquestring(resourceGroup().id), 'standardsa')]
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 28 March 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)][PSCustomObject]$JSON,
        [Parameter(Mandatory=$false)][String[]]$Parent
    )

    Begin {
        Write-Verbose "JSON: $($JSON | Out-String)"
        Write-Verbose "Parent: $($Parent -join '.')"
    }

    Process {
        foreach ($NoteProperty in ($JSON | Get-Member -MemberType NoteProperty)) {
            if ($NoteProperty.Definition -match 'PSCustomObject') {
                Expand-JSON -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name)
            } else {
                if ($JSON.($NoteProperty.Name) -join ', ') {
                    Write-Log "$(($Parent + $NoteProperty.Name) -join '.'):",($JSON.($NoteProperty.Name) -join ', ') Green,Cyan
                } else {
                    Expand-JSON -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name)
                }
            } 
        }
    }

    End { }
}


Export-ModuleMember -Function * -Variable *