0004-New-AzureRmVmAvailabilitySetPsg.ps1

<#PSScriptInfo
 
.VERSION 1.0
 
.GUID 01c58ba7-37f8-40de-98fb-1495ea5b27dd
 
.AUTHOR Preston K. Parsard
 
.COMPANYNAME Microsoft
 
.COPYRIGHT Copyright (c) 2016 Preston K. Parsard
 
.TAGS Azure, Deploy, VM
 
.LICENSEURI https://opensource.org/licenses/MIT
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES Initial Release
 
.DESCRIPTION
 This script creates an availability set of 1-4 Windows Server 2016 VMs with Network Security Groups for RDP access
 
#>
 

<#
****************************************************************************************************************************************************************************
SYNOPSIS:
Creates a new lab of 1-4 VMs and associated resources using the ARM deployment model.
 
DESCRIPTION :
This script deploys a set of 1-4 Windows Server 2016 TP5 servers as part of an availability set that can be used to create DCs. This script can be used to demonstrate how
PowerShell can be used to imperatively create resources in Azure as part of an initial process of building a functional environment consisting of compute, storage and netorking
components. Since this script will be used primarily for demonstration purposes, comments, logging and verbose console output has been included.
 
REQUIREMENTS:
PowerShell Version 4.0
 
LIMITATIONS : TBD
 
AUTHOR(S):
Preston K. Parsard prestopa@microsoft.com
 
EDITOR(S):
Preston K. Parsard prestopa@microsoft.com
 
REFERENCES:
1. https://gallery.technet.microsoft.com/scriptcenter/Build-AD-Forest-in-Windows-3118c100
2. http://blogs.technet.com/b/heyscriptingguy/archive/2013/06/22/weekend-scripter-getting-started-with-windows-azure-and-powershell.aspx
3. http://michaelwasham.com/windows-azure-powershell-reference-guide/configuring-disks-endpoints-vms-powershell/
4. http://blog.powershell.no/2010/03/04/enable-and-configure-windows-powershell-remoting-using-group-policy/
5. http://azure.microsoft.com/blog/2014/05/13/deploying-antimalware-solutions-on-azure-virtual-machines/
6. http://blogs.msdn.com/b/powershell/archive/2014/08/07/introducing-the-azure-powershell-dsc-desired-state-configuration-extension.aspx
7. http://trevorsullivan.net/2014/08/21/use-powershell-dsc-to-install-dsc-resources/
8. http://blogs.msdn.com/b/powershell/archive/2014/07/21/creating-a-secure-environment-using-powershell-desired-state-configuration.aspx
9. http://blogs.technet.com/b/ashleymcglone/archive/2015/03/20/deploy-active-directory-with-powershell-dsc-a-k-a-dsc-promo.aspx
10.http://blogs.technet.com/b/heyscriptingguy/archive/2013/03/26/decrypt-powershell-secure-string-password.aspx
11.http://blogs.msdn.com/b/powershell/archive/2014/09/10/secure-credentials-in-the-azure-powershell-desired-state-configuration-dsc-extension.aspx
12.http://blogs.technet.com/b/keithmayer/archive/2014/10/24/end-to-end-iaas-workload-provisioning-in-the-cloud-with-azure-automation-and-powershell-dsc-part-1.aspx
13.http://blogs.technet.com/b/keithmayer/archive/2014/07/24/step-by-step-auto-provision-a-new-active-directory-domain-in-the-azure-cloud-using-the-vm-agent-custom-script-extension.aspx
14.https://blogs.msdn.microsoft.com/cloud_solution_architect/2015/05/05/creating-azure-vms-with-arm-powershell-cmdlets/
 
KEYWORDS: Mnemonic; [R]esilient<[R]esource Group> [S]ervers<[S]torage Account> [N]eed<Virtual [N]etwork> [V]irtual Machines<[VMs] with [N]etworks<[N]etwork Security Groups> and [A]vailability Sets<[A]vailability Sets>
 
LICENSE:
 
The MIT License (MIT)
Copyright (c) 2016 Preston K. Parsard
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
DISCLAIMER:
THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive,
royalty-free right to use and modify the Sample Code and to reproduce and distribute the Sample Code, provided that You agree: (i) to not use Our name,
logo, or trademarks to market Your software product in which the Sample Code is embedded;
(ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless,
and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees,
that arise or result from the use or distribution of the Sample Code.
****************************************************************************************************************************************************************************
#>


<# WORK ITEMS
TASK-INDEX:
#>


<#
***************************************************************************************************************************************************************************
REVISION/CHANGE RECORD
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DATE VERSION Name CHANGE
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
26 APR 2016 00.00.0001 Preston K. Parsard Initial release
30 JUN 2016 00.00.0002 Preston K. Parsard Updated script file name, plus minor edits; removed non-functional comments
11 JUL 2016 00.00.0003 Preston K. Parsard Added code to select subscription based on matching subscription ID of subscription name provided in response to prompt
11 JUL 2016 00.00.0004 Preston K. Parsard Updated script to use external PSGallery module for logging functions and enhanced logging activity
11 JUL 2016 01.00.0000 Preston K. Parsard Applying the verbose common parameter where possible for more details
14 JUL 2016 01.00.0001 Preston K. Parsard Added reference for WriteToLogs module (https://www.powershellgallery.com/packages/WriteToLogs/1.0.19)
14 JUL 2016 01.00.0002 Preston K. Parsard Applied PSScriptInfo to header for publishing to PSGallery
#>


# Resets profiles in case you have multiple Azure Subscriptions and connects to your Azure Account [Uncomment if you haven't already authenticated to your Azure subscription]
Clear-AzureProfile -Force
Login-AzureRmAccount

# If the WriteToLogs module doesn't already exist, install and import it for use later in the script for logging operations
If (!(Get-Module -Name WriteToLogs))
{
 # https://www.powershellgallery.com/packages/WriteToLogs/1.0.19
 Install-Module -Name WriteToLogs -Verbose
 Import-Module -Name WriteToLogs -Verbose
} #end If

#region INITIALIZE VALUES

$BeginTimer = Get-Date -Verbose

Do
{
 # Subscription name
 (Get-AzureRmSubscription).SubscriptionName
 [string] $Subscription = Read-Host "Please enter your subscription name [MSDN] "
 $Subscription = $Subscription.ToUpper()
} #end Do
Until (($Subscription) -ne $null)

# Selects subscription based on subscription name provided in response to the prompt above
Select-AzureRmSubscription -SubscriptionId (Get-AzureRmSubscription -SubscriptionName $Subscription).SubscriptionId

Do
{
 # Resource Group name
 [string] $rg = Read-Host "Please enter a new resource group name [rg##] "
} #end Do
Until (($rg) -match '[RG]{2}[0-9][0-9]')

Do 
{
 # This is a uniquely assigned number for each course attendee so that the domain and Azure resources will also have unique names within the same course
 # For class-wide demo scripts, this number will be the last 4 digits of the request number
 [string]$AttendeeNum = Read-Host "Please enter your 4 digit attendee or request number, i.e. [0000] "
}
Until ($AttendeeNum -match '[0-9][0-9][0-9][0-9]' -AND $AttendeeNum.Length -eq 4)

Do
{
 # The site code refers to a 3 letter airport code of the nearest major airport to the training site
 [string]$SiteCode = Read-Host "Please enter your 3 character site code, i.e. [ATL] "
 $SiteCode = $SiteCode.ToUpper()
} #end Do
Until ($SiteCode -match '[A-Z]{3}' -AND $SiteCode.Length -eq 3)

Do
{
 # The site code refers to a 3 letter airport code of the nearest major airport to the training site
 [int]$InstanceCount = Read-Host "Please enter the total number of DC instances required [1-4] "
} #end Do
Until ($InstanceCount -le 4 -AND $InstanceCount -ne $null)

# Create and populate prompts object with property-value pairs
# PROMPTS (PromptsObj)
$PromptsObj = [PSCustomObject]@{
 pVerifySummary = "Is this information correct? [YES/NO]"
 pAskToOpenLog = "Would you like to open the deployment log now ? [YES/NO]"
} #end $PromptsObj

# Create and populate responses object with property-value pairs
# RESPONSES (ResponsesObj): Initialize all response variables with null value
$ResponsesObj = [PSCustomObject]@{
 pProceed = $null
 pOpenLogNow = $null
} #end $ResponsesObj

# Construct custom path for log file
$LogPath = $env:HOMEPATH + "\" + $SiteCode + "-" + $AttendeeNum
If (!(Test-Path $LogPath))
{
 New-Item -Path $LogPath -ItemType Directory
} #End If

# Create log file with a "u" formatted time-date stamp
$StartTime = (((get-date -format u).Substring(0,16)).Replace(" ", "-")).Replace(":","")
$24hrTime = $StartTime.Substring(11,4)
$LogFile = "New-AzureRmVmAS" + "-" + $StartTime + ".log"
$Log = Join-Path -Path $LogPath -ChildPath $LogFile
New-Item -Path $Log -ItemType File -Verbose

# Region is specified directly in script
$Region = "East US 2"
New-AzureRmResourceGroup -Name $rg -Location $Region -Verbose
# Storage account name prefix
$SaPrefix = "sto"

# Generate storage account name based on prefix and attendee number
$StorageAcctName = $SaPrefix + $AttendeeNum 

# VM image details
$Publisher = "MicrosoftWindowsServer"
$offer = "WindowsServer"
[string]$sku = "Windows-Server-Technical-Preview"
$ImageName2016TP = Get-AzureRmVMImage –Location $Region –Offer $offer –PublisherName $publisher –SKUs $sku
$Version = "latest"

# User name is specified directly in script
$UniversalAdmName = "ent.g001.s001"
# Virtual Machine size
$VmSize = "Standard_A1"
# Availability set
$AvSetDcName = "AvSetDC"
# NTDS volume drive
$NtdsDiskName = "NTDS"
# SYSVOL volume drive
$SysvDiskName = "SYSV"
# This is the generic top-level domain that will be used in the FQDN of a new domain that can be created later if desired
$gtld = ".lab"
$SiteNamePrefix = "net"

$cred = Get-Credential -UserName $UniversalAdmName -Message "Enter password for user: $UniversalAdmName :"
# $UniversalPW = $cred.GetNetworkCredential().password

$DelimDouble = ("=" * 100 )
$Header = "AZURE RM DC DEPLOYMENT DEMO: " + $StartTime

# Create and populate site, subnet and VM properties of the FR01 domain with property-value pairs
$ObjDomain = [PSCustomObject]@{
 pFQDN = "R" + $AttendeeNum + $gtld
 pDomainName = "R" + $AttendeeNum
 pSite = $SiteNamePrefix + $AttendeeNum
 # Subnet names matches the VM roles (DC = Domain Controller, AP = Application servers or member servers)
 pSubNetDC = "DC"
 pSubNetAP = "AP"
 pDC = $SiteCode + "DC" # Based on the latest image of Windows Server 2016
} #end $ObjDomain

# Subnet for domain controllers
$DcSubnet = New-AzureRmVirtualNetworkSubnetConfig -Name $ObjDomain.pSubnetDC -AddressPrefix 10.0.0.0/28 -Verbose
# Subnet for member servers (AP = Application servers)
$ApSubnet = New-AzureRmVirtualNetworkSubnetConfig -Name $ObjDomain.pSubnetAP -AddressPrefix 10.0.0.16/28 -Verbose

$Vnet = New-AzureRmVirtualNetwork -Name $ObjDomain.pSite -ResourceGroupName $rg -Location $Region -AddressPrefix 10.0.0.0/26 -Subnet $DcSubnet,$ApSubnet -Verbose

# NSG Configuration
# https://www.petri.com/create-azure-network-security-group-using-arm-powershell

# Create the NSG names using 'NSG-' as a prefix
$NsgDcSubnetName = "NSG-$($ObjDomain.pSubnetDC)"
$NsgApSubnetName = "NSG-$($ObjDomain.pSubnetAP)"

# Create the AllowRdpInbound rules
$NsgRuleAllowRdpIn = New-AzureRmNetworkSecurityRuleConfig -Name "AllowRdpInbound" -Direction Inbound -Priority 100 -Access Allow -SourceAddressPrefix "Internet" -SourcePortRange "*" `
-DestinationAddressPrefix "VirtualNetwork" -DestinationPortRange 3389 -Protocol Tcp -Verbose
$NsgDcSubnetObj = New-AzureRmNetworkSecurityGroup -Name $NsgDcSubnetName -ResourceGroupName $rg -Location $Region -SecurityRules $NsgRuleAllowRdpIn -Verbose
$NsgApSubnetObj = New-AzureRmNetworkSecurityGroup -Name $NsgApSubnetName -ResourceGroupName $rg -Location $Region -SecurityRules $NsgRuleAllowRdpIn -Verbose

Set-AzureRmVirtualNetworkSubnetConfig -VirtualNetwork $Vnet -Name $ObjDomain.pSubnetDC -AddressPrefix $DcSubnet.AddressPrefix -NetworkSecurityGroup $NsgDcSubnetObj | Set-AzureRmVirtualNetwork -Verbose
Set-AzureRmVirtualNetworkSubnetConfig -VirtualNetwork $Vnet -Name $ObjDomain.pSubnetAP -AddressPrefix $ApSubnet.AddressPrefix -NetworkSecurityGroup $NsgApSubnetObj | Set-AzureRmVirtualNetwork -Verbose

# Specify disk size as 10 GiB
[int]$DataDiskSize = 10

# Create the avialability set for the [future] DCs
$DcAvSet = New-AzureRmAvailabilitySet -ResourceGroupName $rg -Name $AvSetDcName -Location $Region -Verbose

# Populate Summary Display Object
# Add properties and values
# Make all values upper-case
 $SummObj = [PSCustomObject]@{
 SUBSCRIPTION = $Subscription.ToUpper()
 RESOURCEGROUP = $rg
 SITECODE = $SiteCode.ToUpper()
 ATTENDEENUM = $AttendeeNum.ToUpper()
 DOMAINFQDN = $ObjDomain.pFQDN.ToUpper()
 DOMAINNETBIOS = $ObjDomain.pDomainName.ToUpper()
 SITENAME = $ObjDomain.pSite.ToUpper()
 DCSUBNET = $ObjDomain.pSubNetDC.ToUpper()
 NSGDC = $NsgDcSubnetName.ToUpper()
 APSUBNET = $ObjDomain.pSubNetAP.ToUpper()
 NSGAP = $NsgApSubnetName.ToUpper()
 DCPREFIX = $ObjDomain.pDC.ToUpper()
 # This is the number of VMs and associated VM resources that will be created
 INSTANCES = $InstanceCount
 STORAGEACCT = $StorageAcctName.ToUpper()
 REGION = $Region.ToUpper()
 NETCONFIGDIR = $NetConfigDir
 LOGPATH = $Log
 } #end $SummObj
 
#endregion INITIALIZE VALUES

#region FUNCTIONS

# Create DC VM
Function Add-VM
{
 # If the number of servers will be less than 9, pad with 0, so that the 3rd server would have a pulbic ip of dcvip03 instead of dcvip3 or a nic of dcnic03 as opposed to dcnic3.
 # This keeps the alignment consistent where all resources will have the same name lengths
 Write-WithTime -Output "Padding public IP and NIC resource names if necessary..." -Log $Log
 Switch ($i)
 {
  { $i -le 9 } 
  { 
   $DcVipPrefix = "dcvip0" 
   $DcNicPrefix = "dcnic0"
  } #end condition
  default 
  { 
   $DcVipPrefix = "dcvip" 
   $DcNicPrefix = "dcnic"
  } #end default
 } #end Switch

 # Create the public ip (VIP) and NIC names based on the prefix and index
 Write-WithTime -Output "Creating public IP name..." -Log $Log
 $DcVipName = $DcVipPrefix + $i
 Write-WithTime -Output "Creating NIC name..." -Log $Log
 $DcNicName = $DcNicPrefix + $i

 # Construct the drive names for the SYSTEM, NTDS and SYSVOL drives
 Write-WithTime -Output "Constructing SYSTEM drive name page blob..." -Log $Log
 $DCSYSTvhdUri = $sa.PrimaryEndpoints.Blob.ToString() + "vhds/" + "$($ObjDomain.pDC)-SYST.vhd"
 Write-WithTime -Output "Constructing NTDS drive name page blob..." -Log $Log
 $DCNTDSvhdUri = $sa.PrimaryEndpoints.Blob.ToString() + "vhds/" + "$($ObjDomain.pDC)-NTDS.vhd"
 Write-WithTime -Output "Constructing SYSVOL drive name page blob..." -Log $Log
 $DCSYSVvhdUri = $sa.PrimaryEndpoints.Blob.ToString() + "vhds/" + "$($ObjDomain.pDC)-SYSV.vhd"

 # $x represents the value of the last octect of the private IP address. We skip the first 3 addresses in the network address because they are always reserved in Azure
 $x = $i + 3

 # NOTE: Domain labels have to be lower case
 Write-WithTime -Output "Creating DNS domain label..." -Log $Log
 $DomainLabel = $objDomain.pDC.ToLower() + "-pip"

 Write-WithTime -Output "Creating public IP..." -Log $Log
 # Now we can string all the pre-requisites together to construct both the VIP and NIC
 $DCvip = New-AzureRmPublicIpAddress -ResourceGroupName $rg -Name $DcVipName -Location $Region -AllocationMethod Static -DomainNameLabel $DomainLabel -Verbose
 Write-WithTime -Output "Creating NIC..." -Log $Log
 $DCnic = New-AzureRmNetworkInterface -ResourceGroupName $rg -Name $DcNicName -Location $Region -PrivateIpAddress "10.0.0.$x" -SubnetId $Vnet.Subnets[0].Id -PublicIpAddressId $DCvip.Id -Verbose
 
 # If the VM doesn't aready exist, configure and create it
 If (!((Get-AzureRmVM -ResourceGroupName $rg).Name -match $ObjDomain.pDC))
 {
  Write-WithTime -Output "VM $($ObjDomain.pDC) doesn't already exist. Configuring..." -Log $Log
  # Setup new vm configuration
  $DcvmConfig = New-AzureRmVMConfig –VMName $ObjDomain.pDC -VMSize $vmSize -AvailabilitySetId $DcAvSet.Id | 
  Set-AzureRmVMOperatingSystem -Windows -ComputerName $ObjDomain.pDC -Credential $cred -ProvisionVMAgent -EnableAutoUpdate | 
  Set-AzureRmVMSourceImage -PublisherName $publisher -Offer $offer -Skus $sku -Version $version | 
  Set-AzureRmVMOSDisk -Name $ObjDomain.pDC -VhdUri $DCSYSTvhdUri -Caching ReadWrite -CreateOption fromImage | 
  Add-AzureRmVMNetworkInterface -Id $DCnic.Id -Verbose

  # Create new VM
  Write-WithTime -Output "Creating VM from configuration..." -Log $Log
  New-AzureRmVM -ResourceGroupName $rg -Location $Region -VM $DcvmConfig -Verbose
  
  # Add NIC
  Write-WithTime -Output "Adding NIC..." -Log $Log
  Set-AzureRmNetworkInterface -NetworkInterface $DCnic -Verbose

  # Add data disks
  Write-WithTime -Output "Adding data disks..." -Log $Log
  $vmdc = Get-AzureRmVM -ResourceGroupName $rg -Name $ObjDomain.pDC
  Write-WithTime -Output "Adding NTDS disk..." -Log $Log
  Add-AzureRmVMDataDisk -VM $vmdc -Name $NtdsDiskName -VhdUri $DCNTDSvhdUri -LUN 0 -Caching None -DiskSizeinGB $DataDiskSize -CreateOption Empty -Verbose
  Write-WithTime -Output "Adding SYSVOL disk..." -Log $Log
  Add-AzureRmVMDataDisk -VM $vmdc -Name $SysvDiskName -VhdUri $DCSYSVvhdUri -LUN 1 -Caching None -DiskSizeinGB $DataDiskSize -CreateOption Empty -Verbose
  
  # Update disk configuration
  Write-WithTime -Output "Applying new disk configurations..." -Log $Log
  Update-AzureRmVM -ResourceGroupName $rg -VM $vmdc -Verbose
 } #end If
 else
 {
  Write-ToConsoleAndLog -Output "$($ObjDomain.pDC) already exists..." -Log $Log
 } #end else
} #End function

#endregion FUNCTIONS

#region MAIN

# Clear screen
# Clear-Host

# Display header
Write-ToConsoleAndLog -Output $DelimDouble -Log $Log
Write-ToConsoleAndLog -Output $Header -Log $Log
Write-ToConsoleAndLog -Output $DelimDouble -Log $Log

# Display Summary
Write-ToConsoleAndLog -Output $SummObj -Log $Log
Write-ToConsoleAndLog -Output $DelimDouble -Log $Log

# Verify parameter values
Do {
$ResponsesObj.pProceed = read-host $PromptsObj.pVerifySummary
$ResponsesObj.pProceed = $ResponsesObj.pProceed.ToUpper()
}
Until ($ResponsesObj.pProceed -eq "Y" -OR $ResponsesObj.pProceed -eq "YES" -OR $ResponsesObj.pProceed -eq "N" -OR $ResponsesObj.pProceed -eq "NO")

# Record prompt and response in log
Write-ToLogOnly -Output $PromptsObj.pVerifySummary -Log $Log
Write-ToLogOnly -Output $ResponsesObj.pProceed -Log $Log

# Exit if user does not want to continue

if ($ResponsesObj.pProceed -eq "N" -OR $ResponsesObj.pProceed -eq "NO")
{
  Write-ToConsoleAndLog -Output "Deployment terminated by user..." -Log $Log
  PAUSE
  EXIT
 } #end if ne Y
else 
{
 # Proceed with deployment
 Write-ToConsoleAndLog -Output "Deploying environment..." -Log $Log

 # TASK-ITEM: 0005 Comment 2 lines below before production run, uncomment for testing/debugging only
 # pause
 # exit

 # Storage
 Write-WithTime -Output "Creating storage account $StorageAcctName ..." -Log $Log

 # The following error will be displayed, due to the ARM PowerShell module missing the Test-Azure command. See: Symptom: https://github.com/Azure/azure-powershell/issues/639
 # Test-AzureName : No default subscription has been designated. Use Select-AzureSubscription -Default <subscriptionName> to set the default subscription...
 
 If (!(Get-AzureRmStorageAccount -ResourceGroupName $rg -Name $StorageAcctName -ErrorAction SilentlyContinue))
 { 
  Write-WithTime -Output "Storage account $StorageAcctName does not already exist. Creating..." -Log $Log
  New-AzureRmStorageAccount -ResourceGroupName $rg -Name $StorageAcctName -Location $Region -Type Standard_LRS -Verbose
 } #end If
 else
 {
  Write-WithTime -Output "Storage Account: $StorageAcctName already exist. Skipping..." -Log $Log
 } #end else

 $sa = Get-AzureRmStorageAccount -ResourceGroupName $rg -Name $StorageAcctName

 # Create DC VM(s). Note that we pad the VM name here again, as we did for the VIPs and NICs above to ensure a consistent name length for VM resources
 Write-WithTime -Output "Padding name of VM for a consistent length if necessary..." -Log $Log
 For ($i = 1;$i -le $InstanceCount;$i++)
 {
  Switch ($i) 
  {
   { $i -le 9 } { $ObjDomain.pDC = $SiteCode + "DC0" + $i }
   default 
    { 
     # The VM name is constructed from the site code, "DC" role prefix and the numeric index $i
     $ObjDomain.pDC = $SiteCode + "DC" + $i 
    } #end default
  } #end switch

  Write-WithTime -Output "Building $($ObjDomain.pDC)..." -Log $Log
  Add-VM
 } #end For ($i...)

} #end else

#endregion MAIN

#region FOOTER

# Calculate elapsed time
Write-WithTime -Output "Calculating script execution time..." -Log $Log
Write-WithTime -Output "Getting current date/time..." -Log $Log
$StopTimer = Get-Date
Write-WithTime -Output "Formating date/time to replace commas(,) with dashes(-)..." -Log $Log
$EndTime = (((Get-Date -format u).Substring(0,16)).Replace(" ", "-")).Replace(":","")
Write-WithTime -Output "Calculating elapsed time..." -Log $Log
$ExecutionTime = New-TimeSpan -Start $BeginTimer -End $StopTimer

$Footer = "SCRIPT COMPLETED AT: "

Write-ToConsoleAndLog -Output $DelimDouble -Log $Log
Write-ToConsoleAndLog -Output "$Footer + $EndTime" -Log $Log
Write-ToConsoleAndLog -Output "TOTAL SCRIPT EXECUTION TIME: $ExecutionTime" -Log $Log
Write-ToConsoleAndLog -Output $DelimDouble -Log $Log

# Prompt to open log
Do 
{
 $ResponsesObj.pOpenLogNow = read-host $PromptsObj.pAskToOpenLog
 $ResponsesObj.pOpenLogNow = $ResponsesObj.pOpenLogNow.ToUpper()
}
Until ($ResponsesObj.pOpenLogNow -eq "Y" -OR $ResponsesObj.pOpenLogNow -eq "YES" -OR $ResponsesObj.pOpenLogNow -eq "N" -OR $ResponsesObj.pOpenLogNow -eq "NO")

# Exit if user does not want to continue
if ($ResponsesObj.pOpenLogNow -eq "Y" -OR $ResponsesObj.pOpenLogNow -eq "YES") 
{
 Start-Process notepad.exe $Log
} #end if

# End of script
Write-WithTime -Output "END OF SCRIPT!" -Log $Log

#endregion FOOTER

Pause
EXIT