With the introduction of SaltStack Config to the VMware portfolio, I have had to learn a whole new way of building and customising VM templates using modules like cloud-init and cloudbase-init to provide salt minion configurations. This journey has been quite bumpy with many a pit-hole fallen into and blind alley followed – mainly having to do with lack of native integration between vRA and SaltStack Config (in the early versions) and even differences between Linux and Windows syntax for cloud(base)-init.
Even though this space is evolving fast with VMware introducing new ways to deploy minions and blueprint schema enhancements for Salt, I still am not happy with the current state of affairs, so I came up with an alternate method to meet the following objectives:
- Deploy a Linux or Windows VM from a single vRA blueprint where you can control the Salt Minion version
- Add Salt grains to the deployed VM as part of the blueprint deployment
- Execute a highstate command as part of the deployment
- De-register the minion if the vRA deployment is deleted.
My investigation on how to deploy and configure Salt minions found multiple methods using vRA blueprint schema additions introduced in vRA 8.4.1, or native cloud-init modules, or other blogs with more creative examples (like run-once commands in vCenter Customisation Specs executing scripts in the OS image), but ultimately I had issues with all of them, including things like:
- Installation of salt minion using bootstrap.sh or repo.saltstack.com resulted in different versions of the agent installed
- Remote installation of minions downloaded from the Internet meant opening up firewall ports (yuck) or creating an on-prem software download repository (more yuck).
- Reboots of VMs during customisation would break the run-cmd commands
- Complexity when setting up VM templates and “sealing” the VM before shutting down and templating (e.g. cloud-init clean on Linux)
- Run-cmd syntax differences between Windows and Linux, meaning you could not have a unified vRA template for multiple OSs.
- Run-once commands in the VMware Guest Customisation script
- …and the list goes on
Finally, I came up with the idea to simplify the deployment by only using VMware Guest Customisation (no cloud(base)-init), and to build a custom ABX action that retrieves grain configuration from the blueprint custom properties. Here’s how it works:
Features
- Same ABX Action for Linux or Windows OS
- Action Subscriptions based on event topics
- Custom property definition for grains. You can specify an array of grain key/value pairs which are written to the grain configuration file
- Custom property for minion configuration using the in-built SaltStackConfiguration property group. This allows for the minion to be pre-installed in any Windows or Linux template with the default configuration.
- Custom property to optionally execute a highstate as part of the deployment
- Action constants defining environmental variables such as template users and passwords and vCenter service account credentials
Pre-requisites
- Windows or Linux template with OS, patches, any other base software installed (I like to have mine as vanilla as possible)
- Salt Minion version of your choice pre-installed with default configuration
- A common template user password for Linux and Windows images with local admin privileges
- A vCenter service account
- VMware Guest Customisation as desired
Script Execution
This is the flow of the PowerShell script:
- Import the action constants
- Test if the script is called in the compute.removal.pre event topic. If yes, then execute on the minion “
salt-call saltutil.revoke_auth
” and exit, else: - Connect to vCenter and check that an inventory object exists for the deployed VM and that VM Tools are running
- Reads values in “grains:” and “minionconfig” literal blocks in the blueprint and write them to tempfiles
- Copy temp files to deployed OS
- Windows: “c:\salt\conf\grains” and “c:\salt\conf\minion.d\minion.conf”
- Linux: “/etc/salt/grains” and “/etc/salt/minion.d/minion.conf”
- Restart the salt minion
- Check if the Custom Property “highstate” is “True”, If yes, then execute “
salt-call state.highstate
” on the minion
Action
Create the Minion Config Action as follows:
1. Copy the following PowerShell code:
# ABX Action to configure the Salt Minion and tun a highstate as part of a vRA deployment
# Created by Dennis Gerolymatos and Guillermo Martinez
# Version 1.3.3 - 20.12.2021
function handler($context, $inputs) {
#global Variables definition
$vcuser = $context.getSecret($inputs.vcUsername) # get user for vCenter connection
$vcpassword = $context.getSecret($inputs.vcPassword) # get password for vCenter connection
$vcfqdn = $context.getSecret($inputs.vcfqdn) # get FQDN of vCenter
$name = $inputs.resourceNames[0] # get name of the VM from the resourcenames array
$ip = $inputs.addresses[0] # get first IP address of the VM from the addresses array
$tempfilegrains = New-TemporaryFile # initialize tempfile for grains
$tempfileminion = New-TemporaryFile # initialize tempfile for minion config
#vCenter connection
write-host "Connecting to vCenter..."
Connect-VIServer $vcfqdn -User $vcuser -Password $vcpassword -Force
write-host “Waiting for VM Tools to Start”
do {
$toolsStatus = (Get-vm -name $name | Get-View).Guest.ToolsStatus
write-host $toolsStatus
sleep 3
} until ( $toolsStatus -eq ‘toolsOk’ )
$vm = Get-vm -name $name
write-host "Running script on server "$name" with IP address "$ip
#Variables definition per operatinge system
if ($inputs.customProperties.osType -eq "WINDOWS")
{
$tmpl_pass = $context.getSecret($inputs.tmpl_user_windows_password) # get password for OS image
$vmusername=$context.getSecret($inputs.tmpl_user_windows) # get username for Windows image
# Evaluates the correct path for minion installation
if ((Invoke-VMScript -VM $vm -ScriptText "test-path c:\salt" -GuestUser $vmusername -GuestPassword $tmpl_pass).ScriptOutput.trim() -eq "True")
{$saltpath = 'c:\salt'}
elseif ((Invoke-VMScript -VM $vm -ScriptText "test-path 'c:\ProgramData\Salt Project\salt'" -GuestUser $vmusername -GuestPassword $tmpl_pass).ScriptOutput.trim() -eq "True")
{$saltpath = 'c:\ProgramData\Salt Project\salt'}
else
{write-host "no minion detected in this image"}
$restartservicescript = "restart-service salt-minion -force" # get command for restarting minion on Windows
$highstatescript = "$($saltpath)\salt-call state.highstate" # get command for executing a highstate on windows
$filepathgrains = "$($saltpath)\conf\grains" # path to grains file on Windows
$filepathminion = "$($saltpath)\conf\minion.d\minion.conf" # path to minion.conf file on Windows
$scriptrevoke = "$($saltpath)\salt-call saltutil.revoke_auth" # get command for revoking minion key on Windows
}
else
{
$tmpl_pass = $context.getSecret($inputs.tmpl_user_linux_password) # get password for OS image
$vmusername=$context.getSecret($inputs.tmpl_user_linux) # get username for Linux image
$restartservicescript = "service salt-minion restart" # get command for restarting minion on Linux
$highstatescript = "salt-call state.highstate" # get command for executing a highstate on Linux
$filepathgrains = "//etc/salt/grains" # path to grains file on Linux
$filepathminion = "//etc/salt/minion.d/minion.conf" # path to minion.conf file on Linux
$scriptrevoke = "salt-call saltutil.revoke_auth" # get command for revoking minion key on Linux
}
# Test if the script is called in the compute.removal.pre event topic. If yes, then run saltutil.revoke_auth
$event = $inputs.__metadata.eventTopicId
if ($event -eq "compute.removal.pre")
{
write-host "Executing salt-call saltutil.revoke_auth..."
$runscript = Invoke-VMScript -VM $vm -ScriptText $scriptrevoke -GuestUser $vmusername -GuestPassword $tmpl_pass
Write-Host $runscript.ScriptOutput
}
else
{
# Reads Values in "grains:" and "minionconfig" literal blocks and write them to the temp files
write-host "Collecting key:value pairs..."
$inputs.customProperties.minionconfig | add-content $tempfileminion
$inputs.customProperties.grains | add-content $tempfilegrains
# copy temp files to target OS
write-host "Copy minion.conf file to target OS..."
$runscript = copy-vmguestfile -source $tempfileminion -destination $filepathminion -localtoguest -VM $vm -GuestUser $vmusername -GuestPassword $tmpl_pass -Force
Write-Host $runscript.ScriptOutput
write-host "Copy grains file to target OS..."
$runscript = copy-vmguestfile -source $tempfilegrains -destination $filepathgrains -localtoguest -VM $vm -GuestUser $vmusername -GuestPassword $tmpl_pass -Force
Write-Host $runscript.ScriptOutput
# Restart minion
Write-Host "Restarting minion..."
$runscript = Invoke-VMScript -VM $vm -ScriptText $restartservicescript -GuestUser $vmusername -GuestPassword $tmpl_pass
Write-Host $runscript.ScriptOutput
# Run highstate
if($inputs.customProperties.highstate -eq "true")
{
Write-Host "Running salt highstate..."
$runscript = Invoke-VMScript -VM $vm -ScriptText $highstatescript -GuestUser $vmusername -GuestPassword $tmpl_pass
Write-Host $runscript.ScriptOutput
}
# Remove temp files
remove-item $tempfilegrains -force
remove-item $tempfileminion -force
}
}
2. Create the Action Constants to present to the script handler:

3. Create custom limits as PowerShell requires more memory

Subscriptions
There are two subscriptions needed for this action:
1. Salt-Minion-Configure
- Subscribe to the Compute post provision Event Topic
- Set a condition
event.data.customProperties['min_install'] == "true"
. This custom property must be in the vRA blueprint to trigger this subscription

2. Salt-Minion-Remove
- Subscribe to the Compute post removal Event Topic
- Set a condition
event.data.customProperties[‘min_install’] == “true”
- Turn on Blocking

Blueprint
Create the following custom properties in your blueprint:
- min_install: (boolean). Used to target the Subscriptions
- grains
- Enter here an array of any key/value pairs you want to set as grains for this VM. These could be statically defined, or entered by the user in the request form. Take special note of the “|” after the “grains” property – this is what is needed to create the array
- minionconfig
- This section uses the pre-configured property groups for SaltStack Config if it is installed with vRealize Suite Lifecycle Manager (vRSLCM)
- highstate: (boolean). Used to optionally perform a
salt-call state.highstate
after the minion is fully configured

I hope you found this a useful addition to your ABX toolkit – please feel free to share.
thank you !!