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")
Share to TwitterShare to FacebookShare to LinkedinShare to PocketCopy URL address
Written by

Sam Perrin@samperrin

Automation Consultant, currently working at Xtravirt. Interested in all things automation/devops related.

Related Posts