I'm pretty late to the party but I found much less useful information on this topic than I expected. This is my how-to use Aria Automation to deploy Windows Server using Cloudbase-init
Overview
This guide assumes you already have vSphere and Aria Automation installed and configured.
We will be creating an Aria Automation Assembler Template that will clone a sysprepped Windows Server VM with Cloudbase-Init installed to bootstrap the OS. The VM will be on the network and joined to Active Directory.
The process will look something like:
- User requests a VM in Aria Automation
- An Event Broker subscription creates an AD object in a specified OU
- An Event Broker subscription sets the Cloudbase-Init metadata and userdata advanced properties on the VM.
- Aria Automation requests the VM be created in vCenter
- The VM boots up and runs sysprep which then asks for Cloudbase-Init to run.
- Cloudbase-Init runs the unattend conf file using the provided metadata to rename the computer and configure the NIC
- Sysprep finishes and the VM is rebooted
- Cloudbase-Init runs the conf file to execute a script to join the computer to the domain.
- The VM reboots
- User requests the VM to be destroyed
- An Event Broker subscription deletes the AD object for the computer
Cloudbase-init
Cloudbase-Init is installed in the template and executes during sysprep using the cloudbase-init-unattend.conf file and at all subsequent boots using the cloudbase-init.conf file.
Cloudbase-Init can get data from VMware two ways:
- OVF – This is the default method for Aria Automation and is available anywhere you can use an OVF. The metadata and userdata are stored in an OVF and mounted to the VMs virtual CD-ROM.
- VMware GuestInfo Service – This method stores the metadata and userdata as advanced properties of the VM using gzip base64 encoded strings.
We are going to use the VMware GuestInfo Service in this example.
In this example we will be doing the following to the OS:
- Sysprep
- Rename the computer
- Configure the NIC with a static IP address
- First Boot
- Join the VM to an Active Directory domain
Active Directory
Create a new AD user that can be used to create and destroy computer objects and join computers to the domain.
Add the user to an OU with the following advanced permissions in addition to the defaults
- Create computer objects
- Delete computer objects
Join Domain Powershell Script
Create the script and place it in the Cloudbase-init LocalScripts directory of the template server as described in step 5 of the "Virtual Machine Template" section. If you copy the script from another server, be sure to unblock the file so it can be executed.
try {
$cloud_init_dir = "$env:programfiles\cloudbase solutions\cloudbase-init"
$log = $cloud_init_dir + "\log\join_domain.log"
New-Item $log -ErrorAction SilentlyContinue
write-output "Starting Join Domain Script" | Out-File $log -Append
write-output "installing YAML Module" | Out-File $log -Append
install-packageprovider -name nuget -force
install-module -name powershell-yaml -force
import-module powershell-yaml -force
write-output "installed YAML Module" | Out-File $log -Append
write-output "Getting cloudbase-init metadata" | Out-File $log -Append
cd "$env:programfiles\vmware\vmware tools"
$metadata = & .\rpctool.exe "info-get guestinfo.metadata"
$bytes = [System.Convert]::FromBase64String($metadata)
$memory_stream = New-Object System.IO.MemoryStream(,$bytes)
$gzip_stream = New-Object System.IO.Compression.GzipStream($memory_stream, [System.IO.Compression.CompressionMode]::Decompress)
$stream_reader = New-Object System.IO.StreamReader($gzip_stream)
$decompressed_string = $stream_reader.ReadToEnd()
write-output "Got cloudbase-init metadata" | Out-File $log -Append
$yaml = convertfrom-yaml $decompressed_string
$hostname = $yaml.hostname
write-output "Got hostname from metadata: $hostname" | Out-File $log -Append
write-output "Computername: $env:computername"
if ($env:computername -ne $hostname) {
write-output "Computer name has not been updated yet, exiting and rebooting" | Out-File $log -Append
$runs = (select-string -path $log -pattern 'Computer name has not been updated yet, exiting and rebooting').count
if ($runs -lt 3) {
exit 1003
}
else {
throw "Computer name has not been updated after 3 runs, exiting"
}
}
else {
write-output "Computer name has been updated to $env:computername, joining to domain" | Out-File $log -Append
$creds = new-object system.management.automation.pscredential ('<username>@<domain>', (convertto-securestring '<password>' -asplaintext -force))
add-computer -DomainName '<domain>' -Credential $creds -Force
write-output "Joined to domain" | Out-File $log -Append
remove-item "$env:programfiles\cloudbase solutions\cloudbase-init\localscripts\join_domain.ps1" -force
write-output "Removed join_domain.ps1" | Out-File $log -Append
write-output "Set Execution Policy to restricted" | Out-File $log -Append
write-output "Exiting with code 1001" | Out-File $log -Append
exit 1001
}
}
catch {
Write-Output "Error: $_" | Out-File $log -Append
write-output "Removing join_domain.ps1" | Out-File $log -Append
remove-item "$env:programfiles\cloudbase solutions\cloudbase-init\localscripts\join_domain.ps1" -force
write-output "Set Execution Policy to restricted" | Out-File $log -Append
}
Virtual Machine Template
- Create a Virtual machine and manually install Windows Server (or use your favorite machine image creation tool, such as packer)
- Install Cloudbase-Init
- Configuration Options
- Username
- Administrator
- Close the installer without running sysprep
- Configure the cloudbase-init-unattend.conf file.
- Add the following lines to use the VMware GuestInfo Service to get the metadata and userdata then use the SetHostNamePlugin and NetworkConfigPlugin.
- metadata_services= cloudbaseinit.metadata.services.vmwareguestintoservice.VMwareGuestInfoService
- plugins=cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin
- Configure the cloudbase-init.conf file.
- Add the following lines to use the VMware GuestInfo Service to get the metadata and userdata then use the LocalScriptsPlugin to run a powershell script to join the computer to the domain
- metadata_services= cloudbaseinit.metadata.services.vmwareguestintoservice.VMwareGuestInfoService
- plugins=cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin
- Copy the join domain powershell script to the Cloudbase-Init LocalScripts directory
- Make sure to unblock the file so it can be executed.
- Delete any log files in the Cloudbase-Init log directory
- Run sysprep using the Cloudbase-Init unattend.xml file
- You can run these commands anytime you need to update the template.
- 1. Convert the VM to a template
Aria Automation
Orchestrator
Create Aria Orchestrator workflows to create and delete AD computer objects.
Create AD Computer Object
- inputs
- inputProperties
- type: Properties
- direction: input
- variables
- domainName
- value: <domain_name>
- type: string
- ou
- type: AD:OrganizationalUnit
- computerName
- schema
- scriptable task
- inputs
- outputs
- script
-
var cps = inputProperties.get('customProperties')
computerName = cps.get('hostname')
var ou_name = JSON.parse(cps.get('ou')).label
var ou_dn = JSON.parse(cps.get('ou')).id.split('#')[7]
ous = ActiveDirectory.searchExactMatch('OrganizationalUnit', ou_name)
for each (ou_attr in ous) {
if (ou_attr.distinguishedName == ou_dn) {
ou = ou_attr
}
}
if (!ou) {
throw 'no ou found with dn ' + ou_dn
}
- workflow
- Create a computer in an organizational unit
- inputs
- ou: ou
- computerName: computerName
- domainName: domainName
- END
Delete AD Computer Object
- inputs
- variables
- schema
- scriptable task
- inputs
- outputs
- script
-
var cps = inputProperties.get('customProperties')
var hostname = cps.get('hostname')
var computer = ActiveDirectory.searchExactMatch('ComputerAD', hostname, 1)[0]
- decision
Cloud Account
Sync the Cloud Account images for the cloud account you created the template in.
Image Mapping
Create a new image mapping that points to the newly created template
Networks
Add a tag to a network so that it can be referenced in the template
Storage
Add a tag to a datastore or datastore cluster so that it can be referenced in the template
Template
Create a new template.
formatVersion: 1
resources:
Cloud_vSphere_Network_1:
type: Cloud.vSphere.Network
properties:
constraints:
- tag: <network_tag>
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine
properties:
name: ${env.deploymentName}
hostname: ${split(env.deploymentName, '.')[0]}
image: <image_mapping>
cpuCount: 2
totalMemoryMB: 4096
ip: 192.168.1.2
cidr: 24
gateway: 192.168.1.1
constraints:
- tag: <storage_tag>
networks:
- network: ${resource.Cloud_vSphere_Network_1.id}
ou: <OU_DN>
Extensibility
Actions
Create actions
make cloudbase-init guestinfo data
python
import requests
import json
import gzip
import base64
def handler(context, inputs):
outputs = {}
cps = inputs['customProperties']
vm_name = cps['hostname']
metadata_string = f'''
instance-id: {vm_name}
local-hostname: {vm_name}
hostname: {vm_name}
network:
version: 2
ethernets:
Ethernet0:
match:
macaddress: "{inputs['macAddresses'][0][0]}"
addresses: [{cps['ip']}/{cps['cidr']}]
gateway4: {cps['gateway']}
nameservers:
addresses: [{inputs['ad_nameserver_1']}, {inputs['ad_nameserver_2']}]
'''
print(metadata_string)
def base64_encode_gzip(data):
"""Encodes data with gzip and then Base64."""
compressed_data = gzip.compress(data.encode())
encoded_data = base64.b64encode(compressed_data)
return encoded_data.decode()
outputs['metadata'] = base64_encode_gzip(metadata_string)
outputs['userdata'] = base64_encode_gzip(cps['userdata'])
return outputs
set cloudbase-init guestinfo data
powershell
function handler($context, $inputs) {
$outputs = @{}
$vc = $inputs.vcenter
write-host "connecting to vcenter $vc"
connect-viserver -Server $inputs.vcenter -user $inputs.vcenter_username -password $context.getSecret($inputs.vcenter_password) -protocol https -force
$vm = get-vm $inputs.resourceNames[0]
$vm | new-advancedsetting -name "guestinfo.metadata.encoding" -value "gzip+base64" -confirm:$false
$vm | new-advancedsetting -name "guestinfo.userdata.encoding" -value "gzip+base64" -confirm:$false
$vm | new-advancedsetting -name "guestinfo.metadata" -value $inputs.metadata -confirm:$false
$vm | new-advancedsetting -name "guestinfo.userdata" -value $inputs.userdata -confirm:$false
disconnect-viserver
return $outputs
}
set cloudbase-init guestinfo
flow
---
version: 1
flow:
flow_start:
next: action1
action1:
action: make_cloudinit_guestinfo_data
next: action2
action2:
action: set_cloudinit_guestinfo_data
next: flow_end
Subscriptions
create ad computer object
- event topic: Compute Provision
- Workflow
- Create AD Computer Object
set cloudbase-init data
- event topic: Compute Initial power on
- action
- set cloudbase-init guestinfo
delete ad computer object
- event topic: Compute removal
- workflow
- Delete AD Computer Object