Back in August I created the following post that showed an example Cloudbase-init multi-part user data script for use within an Aria Automation Template (vRealize Cloud Template) - vRA - Windows Cloudbase-init - Multi-part User Data Example
This post is a follow-on from that which expands on the Cloudbase-init code by utilising PowerShell functions. Cloudbase-init itself provides no guarantees that things will run, this can cause issues if you are relying on it to provide the intial customisation of virtual machines, so by using function we can provide some sort of control around when the next stages of the Cloudbase-init code are run - for example, we can wait for the network to be up before we attempt a domain join.
I will not be going into any detail around the Aria Automation Templates or the compute resources, instead we will focus just on the Cloudbase-init code. Refer back to the first post if you want to see the rest of the Template code.
#Establish the multi-part user data
You can find the full Cloudbase-init code at the bottom, but first we will break down what each main part is doing.
This is the start of our multi-part code. First we set our boundary ==NewPart==
. When this appears we start a new “part” (section). Section 1 is using cloud-config and we set the hostname using the set_hostname Cloudbase-init plugin. Section 2 uses powershell, from this point down our code is run in PowerShell, we specifically use #ps1_sysnative.
Content-Type: multipart/mixed; boundary="==NewPart=="
MIME-Version: 1.0
--==NewPart==
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
set_hostname: ${self.resourceName}
--==NewPart==
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="INIT.ps1"
#ps1_sysnative
#The Functions
Next up is a logging function. Cloudbase-init itself creates a log output but it is easy to miss what is actually happening. This function is used throughout the Cloudbase-init code to write to a seperate log file. We have used this function quite heavily throughout our Cloudbase-init code, even within other functions, it will look similar to this: CustomLog("Create Local User - Started")
.
function CustomLog ($LogMessage) {
$LogFilePath = "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\CustomLog.txt"
Write-Output "$((Get-Date).ToString('T')) $LogMessage" | Out-File -FilePath $LogFilePath -Append
}
We establish our first of the “check” type functions. In this one we take in a property from a Property Group which is the FQDN of our Domain Server.
function DomainDnsTest {
$DomainDnsTestResult = (Test-NetConnection -ComputerName "${propgroup.defaultServerValues.windowsDomainServer}" -Port 389).TcpTestSucceeded
return $DomainDnsTestResult
}
This function check a property within Windows to see if it has domain joined.
function DomainJoinStatus {
$DomainJoinStatusResult = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
return $DomainJoinStatusResult
}
This function waits for the network interface status to return as “Up”. This status alone is not enough to know if a network is ready, hence why we create a DNS check too.
function WaitForNetwork {
CustomLog("-- Starting WaitForNetwork. Starting status $((Get-NetAdapter -Name Ethernet0).Status)")
While ((Get-NetAdapter -Name Ethernet0).Status -ne "Up") {
CustomLog("-- Waiting for network Ethernet0 to be up. Current status $((Get-NetAdapter -Name Ethernet0).Status)")
Start-Sleep -Seconds 2
}
CustomLog("-- Ending WaitForNetwork. Ending status $((Get-NetAdapter -Name Ethernet0).Status)")
}
This function utilises another nested function and waits for DNS to be ready.
function WaitForDns {
CustomLog("-- Starting WaitForDns. Starting status $(DomainDnsTest)")
While ((DomainDnsTest) -ne "True") {
CustomLog("-- Waiting for DNS to succeed. Current status $(DomainDnsTest)")
Start-Sleep -Seconds 2
}
CustomLog("-- Ending WaitForDns. Ending status $(DomainDnsTest)")
}
This functions code has some values replaced by the users Input or from Property Group values. It attempts to domain join the server. We use the Add-Computer
cmdlet because the computer object already exists within Active Directory because Aria Automation has created it via its native integration.
function JoinDomain {
CustomLog("-- Starting JoinDomain")
WaitForNetwork
WaitForDns
$DomainJoinCreds = New-Object System.Management.Automation.PSCredential "${propgroup.defaultServerValues.windowsDomainJoinUsername}@${propgroup.defaultServerValues.windowsDomain}" ,(ConvertTo-SecureString -String "${input.domainPassword}" -AsPlainText -Force)
Add-Computer -DomainName "${propgroup.defaultServerValues.windowsDomain}"-Credential $DomainJoinCreds -Server "${propgroup.defaultServerValues.windowsDomainServer}" -Restart:$false -Force
CustomLog("-- Ending JoinDomain")
}
Our final function, we use one of our earlier created functions (DomainJoinStatus
) and we wait for that property to return true.
function WaitForDomainJoin {
CustomLog("-- Starting WaitForDomainJoin. Starting status $(DomainJoinStatus)")
While ((DomainJoinStatus) -ne "True") {
CustomLog("---- Waiting for Domain to be True. Current status $(DomainJoinStatus)")
CustomLog("---- Starting Sleep")
Start-Sleep -Seconds 10
CustomLog("---- Ending Sleep")
JoinDomain
}
CustomLog("-- Ending WaitForDomainJoin. Ending status $(DomainJoinStatus)")
}
#The Remaining Parts
I have added comments inline with the rest of the code and each major section.
## We create a local user and add the user to the Administrators group.## Only the CustomLog function is used here.CustomLog("Create Local User - Started")
$localUserPassword = ConvertTo-SecureString "${input.serverPassword}" -AsPlainText -Force
New-LocalUser -Name "${input.serverUser}" -Password $localUserPassword
Add-LocalGroupMember -Group "Administrators" -Member "${input.serverUser}"
CustomLog("Create Local User - Finished")
## We set some Windows Firewall settings.## Only the CustomLog function is used here.CustomLog("Set Local Firewall - Started")
Set-NetFirewallRule -DisplayName "File and Printer Sharing (Echo Request - ICMPv4-In)" -enabled True
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'NV Domain' -Value "${propgroup.defaultServerValues.fqdnDomain}"
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name SyncDomainWithMembership -Value "0"
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
CustomLog("Set Local Firewall - Finished")
## We format any additional disk drives that were added as part of the VM deployment request.## Only the CustomLog function is used here.CustomLog("Initialise Disk - Started")
Get-Disk | Where-Object PartitionStyle -Eq "RAW" | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -NewFileSystemLabel "DATA"
CustomLog("Initialise Disk - Finished")
## We configure some DNS settings. Only the CustomLog function is used here.## Only the CustomLog function is used here.CustomLog("Set DNS Settings - Started")
Get-NetAdapter | Set-DnsClient -RegisterThisConnectionsAddress $False
Get-NetAdapter | Set-DnsClient -UseSuffixWhenRegistering $False
Get-NetAdapter | Set-DnsClient -ConnectionSpecificSuffix ${propgroup.defaultServerValues.fqdnDomain}
Set-DnsClientGlobalSetting -SuffixSearchList @("domain.com", "sub.domain.com")
CustomLog("Set DNS Settings - Finished")
## We set the IP address, this uses the vRA IPAM and is injected at deployment time. ## Along with the CustomLog function we also use WaitForNetwork and WaitForDns.CustomLog("Set IP Settings - Started")
$adapter = Get-NetAdapter | ? { $_.Name -eq "Ethernet0" }
$adapter | Remove-NetIpAddress -Confirm:$false
$adapter | Remove-NetRoute -Confirm:$false
$adapter | New-NetIpAddress -IPAddress ${self.networks[0].address} -PrefixLength ${resource.Network.prefixLength} -DefaultGateway ${resource.Network.gateway}
$adapter | Set-DnsClientServerAddress -ServerAddresses("${join(resource.Network.dns,'", "')}")
$adapter | Disable-NetAdapterBinding -ComponentID ms_tcpip6
WaitForNetwork
WaitForDns
Set-NetConnectionProfile -InterfaceAlias Ethernet0 -NetworkCategory "Private"
CustomLog("Set IP Settings - Finished")
## At this point both Network and DNS should be ready so we attempt to domain join. ## We use the WaitForDomainJoin and CustomLog functions.CustomLog("Domain Join - Started")
WaitForDomainJoin
CustomLog("Domain Join - Finished")
## We tidy up some of the log files. ## Only the CustomLog function is used here.CustomLog("Tidying Logs - Started")
(Get-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log") -replace "${input.domainPassword}","<DOMAIN-JOIN-PASSWORD>" | Set-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log" -Verbose
(Get-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log") -replace "${input.serverPassword}","<LOCAL-USER-PASSWORD>" | Set-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log" -Verbose
CustomLog("Tidying Logs - Finished")
## We eject the CD drive that was attached automatically by vRA. ## Only the CustomLog function is used here.CustomLog("Ejecting CD Drives - Started")
$drives = Get-WmiObject Win32_Volume -Filter "DriveType=5"
$drives | ForEach-Object { (New-Object -ComObject Shell.Application).Namespace(17).ParseName($_.Name).InvokeVerb("Eject") } -ErrorAction SilentlyContinue
CustomLog("Ejecting CD Drives - Finished")
## We reboot the server. ## Only the CustomLog function is used here.CustomLog("Preparing to Reboot - Started")
shutdown /r /t 10
CustomLog("Preparing to Reboot - Finished")
#Cloud-config in full
cloudConfig: |
Content-Type: multipart/mixed; boundary="==NewPart=="
MIME-Version: 1.0
--==NewPart==
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
set_hostname: ${self.resourceName}
--==NewPart==
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="INIT.ps1"
#ps1_sysnative
function CustomLog ($LogMessage) {
$LogFilePath = "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\CustomLog.txt"
Write-Output "$((Get-Date).ToString('T')) $LogMessage" | Out-File -FilePath $LogFilePath -Append
}
function DomainDnsTest {
$DomainDnsTestResult = (Test-NetConnection -ComputerName "${propgroup.defaultServerValues.windowsDomainServerDc3}" -Port 389).TcpTestSucceeded
return $DomainDnsTestResult
}
function DomainJoinStatus {
$DomainJoinStatusResult = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
return $DomainJoinStatusResult
}
function WaitForNetwork {
CustomLog("-- Starting WaitForNetwork. Starting status $((Get-NetAdapter -Name Ethernet0).Status)")
While ((Get-NetAdapter -Name Ethernet0).Status -ne "Up") {
CustomLog("-- Waiting for network Ethernet0 to be up. Current status $((Get-NetAdapter -Name Ethernet0).Status)")
Start-Sleep -Seconds 2
}
CustomLog("-- Ending WaitForNetwork. Ending status $((Get-NetAdapter -Name Ethernet0).Status)")
}
function WaitForDns {
CustomLog("-- Starting WaitForDns. Starting status $(DomainDnsTest)")
While ((DomainDnsTest) -ne "True") {
CustomLog("-- Waiting for DNS to succeed. Current status $(DomainDnsTest)")
Start-Sleep -Seconds 2
}
CustomLog("-- Ending WaitForDns. Ending status $(DomainDnsTest)")
}
function JoinDomain {
CustomLog("-- Starting JoinDomain")
WaitForNetwork
WaitForDns
$DomainJoinCreds = New-Object System.Management.Automation.PSCredential "${propgroup.defaultServerValues.windowsDomainJoinUsername}@${propgroup.defaultServerValues.windowsDomain}" ,(ConvertTo-SecureString -String "${input.domainPassword}" -AsPlainText -Force)
Add-Computer -DomainName "${propgroup.defaultServerValues.windowsDomain}"-Credential $DomainJoinCreds -Server "${propgroup.defaultServerValues.windowsDomainServerDc3}" -Restart:$false -Force
CustomLog("-- Ending JoinDomain")
}
function WaitForDomainJoin {
CustomLog("-- Starting WaitForDomainJoin. Starting status $(DomainJoinStatus)")
While ((DomainJoinStatus) -ne "True") {
CustomLog("---- Waiting for Domain to be True. Current status $(DomainJoinStatus)")
CustomLog("---- Starting Sleep")
Start-Sleep -Seconds 10
CustomLog("---- Ending Sleep")
JoinDomain
}
CustomLog("-- Ending WaitForDomainJoin. Ending status $(DomainJoinStatus)")
}
CustomLog("Create Local User - Started")
$localUserPassword = ConvertTo-SecureString "${input.serverPassword}" -AsPlainText -Force
New-LocalUser -Name "${input.serverUser}" -Password $localUserPassword
Add-LocalGroupMember -Group "Administrators" -Member "${input.serverUser}"
CustomLog("Create Local User - Finished")
CustomLog("Set Local Firewall - Started")
Set-NetFirewallRule -DisplayName "File and Printer Sharing (Echo Request - ICMPv4-In)" -enabled True
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'NV Domain' -Value "${propgroup.defaultServerValues.fqdnDomain}"
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name SyncDomainWithMembership -Value "0"
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
CustomLog("Set Local Firewall - Finished")
CustomLog("Initialise Disk - Started")
Get-Disk | Where-Object PartitionStyle -Eq "RAW" | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -NewFileSystemLabel "DATA"
CustomLog("Initialise Disk - Finished")
CustomLog("Set DNS Settings - Started")
Get-NetAdapter | Set-DnsClient -RegisterThisConnectionsAddress $False
Get-NetAdapter | Set-DnsClient -UseSuffixWhenRegistering $False
Get-NetAdapter | Set-DnsClient -ConnectionSpecificSuffix ${propgroup.defaultServerValues.fqdnDomain}
Set-DnsClientGlobalSetting -SuffixSearchList @("domain.com", "sub.domain.com")
CustomLog("Set DNS Settings - Finished")
CustomLog("Set IP Settings - Started")
$adapter = Get-NetAdapter | ? { $_.Name -eq "Ethernet0" }
$adapter | Remove-NetIpAddress -Confirm:$false
$adapter | Remove-NetRoute -Confirm:$false
$adapter | New-NetIpAddress -IPAddress ${self.networks[0].address} -PrefixLength ${resource.Network.prefixLength} -DefaultGateway ${resource.Network.gateway}
$adapter | Set-DnsClientServerAddress -ServerAddresses("${join(resource.Network.dns,'", "')}")
$adapter | Disable-NetAdapterBinding -ComponentID ms_tcpip6
WaitForNetwork
WaitForDns
Set-NetConnectionProfile -InterfaceAlias Ethernet0 -NetworkCategory "Private"
CustomLog("Set IP Settings - Finished")
CustomLog("Domain Join - Started")
WaitForDomainJoin
CustomLog("Domain Join - Finished")
CustomLog("Tidying Logs - Started")
(Get-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log") -replace "${input.domainPassword}","<DOMAIN-JOIN-PASSWORD>" | Set-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log" -Verbose
(Get-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log") -replace "${input.serverPassword}","<LOCAL-USER-PASSWORD>" | Set-Content "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\cloudbase-init.log" -Verbose
CustomLog("Tidying Logs - Finished")
CustomLog("Ejecting CD Drives - Started")
$drives = Get-WmiObject Win32_Volume -Filter "DriveType=5"
$drives | ForEach-Object { (New-Object -ComObject Shell.Application).Namespace(17).ParseName($_.Name).InvokeVerb("Eject") } -ErrorAction SilentlyContinue
CustomLog("Ejecting CD Drives - Finished")
CustomLog("Preparing to Reboot - Started")
shutdown /r /t 10
CustomLog("Preparing to Reboot - Finished")