Migrating Salt Minions to a New Master

This is a follow-up to a previous post, where I stood up a new single-node VMware Salt deployment to replace my old Aria Automation Config setup. This post covers the actual cutover: moving every existing minion in the homelab from the old master (cm-config-01) to the new one (cm-vcfsalt-01).

The Salt minion can be manually installed on a system or included as part of the VMware Tools install. When deployed through VMware Tools, settings are typically configured by advanced settings on the VM, instead of maintaining configuration files inside of the VM. From my prior setup, I had a mismatch of configurations, with some VMs using manually installed, older versions of the minion, and other VMs using the VMware Tools option. As part of this migration, I wanted to standardize on one option and decided on the VMware Tools path.

Step 1: Inventory what’s currently configured

Before changing anything, I pulled a report of every VM’s current master setting via PowerCLI. This is limited to only the existing VMs that were configured to use the VMware Tools salt minion option.

$saltSettings = Get-VM | Get-AdvancedSetting -Name "guestinfo./vmware.components.salt_minion.*"
$groupedSettings = $saltSettings | Group-Object -Property Entity
$results = foreach ($group in $groupedSettings) {
    [PSCustomObject]@{
        VMName                 = $group.Name
        SaltMinionArgs         = ($group.Group | Where-Object Name -match 'salt_minion.args$').Value
        SaltMinionDesiredState = ($group.Group | Where-Object Name -match 'salt_minion.desiredstate$').Value
    }
}
$results | Format-Table -AutoSize

Step 2: Bulk-update the advanced setting

For VMs that already had the setting defined, updating in bulk is straightforward. We get the current setting and set its value. This can be done with mainly one change, assuming the ‘DesiredState’ value is already set to ‘present’. Similar syntax can be used to change the desired state setting if needed.

Get-VM net-wanrtr-02,svcs-mongo-1* | Get-AdvancedSetting -Name 'guestinfo./vmware.components.salt_minion.args' | Set-AdvancedSetting -Value 'master=cm-vcfsalt-01.lab.enterpriseadmins.org' -Confirm:$false

For VMs where the setting didn’t exist yet (never previously managed by Salt and/or used guest OS manual salt minion installs), Set-AdvancedSetting will fail silently on that host as the existing setting is not found to set. Instead, we need to create the advanced setting using New-AdvancedSetting instead:

Get-VM h331-minion-01 | New-AdvancedSetting -Name 'guestinfo./vmware.components.salt_minion.args' -Value 'master=cm-vcfsalt-01.lab.enterpriseadmins.org' -Confirm:$false

Get-VM h331-minion-01 | New-AdvancedSetting -Name 'guestinfo./vmware.components.salt_minion.desiredstate' -Value 'present' -Confirm:$false 

Step 3: Re-trigger the minion install

For new VMs where salt was not previously configured, the installation/configuration of the minion happened automatically (as the salt-minion was already installed as part of VMware Tools, toggling the ‘desiredstate’ key triggered the configuration. However, on most of the other previoulsy configured VMs, the configuration didn’t occur automatically. I suspect removing the desiredstate key, waiting a bit, and then re-adding the key may have triggered an uninstall/reinstall, but I didn’t test that path. Instead I manually triggered the component script inside the guest. For example, on some Ubuntu Linux VMs I ran the following:

Get-VM $vmList | Invoke-VMScript -ScriptText "sudo /usr/lib/x86_64-linux-gnu/open-vm-tools/componentMgr/saltMinion/svtminion.sh" -GuestUser '<user>' -GuestPassword '<pass>'

A few things I ran into here:

  • Powered-off VMs caused error messages to appear. I could have filtered them out with a filter like ?{$_.PowerState -eq 'PoweredOn'} first.
  • -RunAsync is possible for the Invoke-VMscript cmdlet. If running this for a long list of VMs, the code above will process one VM at a time. For a lab this is fine, but for a longer list of VMs, adding -RunAsync will kick the commands off much faster.

“Invalid master key” errors

After repointing several minions, some did not show up in my Salt console. Checking those minions with systemctl status salt-minion showed the error Unable to sign_in to master: Invalid master key. The fix was removing the cached master public key and restarting the service:

sudo rm /etc/salt/pki/minion/minion_master.pub
sudo systemctl restart salt-minion

Windows minion troubleshooting

On Linux, reconfiguring VMs used a svtminion.sh script. When troubleshooting a couple of Windows VMs, I found a: C:\Program Files\VMware\VMware Tools\componentMgr\saltMinion\svtminion.ps1 that has similar functionality. Even after a clean reinstall (svtminion.ps1 -Remove then -Install), a couple of Windows minions kept showing repeated warnings in C:\ProgramData\Salt Project\Salt\var\log\salt\minion:

The minion failed to return the job information for job <id>. This is often due to the master being shut down or overloaded.

Deleting the stale minion_master.pub, restarting the service, even running salt-minion.exe -l debug to watch debug level logs didn’t shed much light. A simple reboot of the guest OS resolved this issue in both cases. If you hit the same “reinstalled fine, keys accepted, but still no data” symptom on Windows, try a reboot before going deeper down the debugging rabbit hole.

Conclusion

Setting up the new Salt service didn’t take long, and moving minions over took less time than one round of manual patching. With all my VMs moved over to a new Salt instance, I can now enable automated patch again. To set this up, I used the same steps that I documented ~3 years ago in this blog post. Other than a more frequent/automated patch cycle, on additional benefit is that I now have documentation showing prior/current package versions and a date/time stamp when they were updated.

Posted in Lab Infrastructure, Scripting | Leave a comment

Replacing Aria Automation Config with VMware Salt (Single-Node) in My Homelab

A couple of years ago I ran SaltStack Config (later rebranded Aria Automation Config) in my homelab for automated Linux patching. For various reasons, I stopped maintaining it and reverted to rather infrequent, manual patching. Recently I decided it was time to get back into automated patching. My old Salt deployment had been neglected for some time so instead of just restarting it, I decided to setup VMware Salt (the productized version included in the Advanced Cyber Compliance addon), from scratch.

Picking a deployment model

VMware Salt supports a few deployment topologies. For a homelab, the choice was easy: the single-node (all-in-one) model, where the Salt Master, Salt RaaS server (including Postgres and Valkey/Redis), all run on one VM. It’s the simplest option to stand up since there’s only one VM, but it’s worth knowing the trade-offs before you commit to it even for a lab:

  • Everything competes for the same CPU/RAM/disk I/O, so it can bog down under heavy load (lots of minions, frequent jobs)
  • It’s a single point of failure and a single attack surface
  • VMware explicitly does not consider this a production-grade topology; it’s meant for PoC, testing, and small labs

For a homelab with a modest minion count, that’s a completely reasonable set of trade-offs. If you’re planning something larger, look at the Basic Enterprise or Distributed Enterprise models instead. Details about each option can be found in the official documentation.

Building the VM

The documentation specifically calls out support for Red Hat Enterprise Linux (RHEL) 9. I built the target VM on Rocky Linux 9.8, using the template created in this post. Rocky Linux is an open-source enterprise operating system designed to be 100% bug-for-bug compatible with Red Hat Enterprise Linux. I then installed some prerequisites called out in the documentation, and enabled persistent firewall rules using the following commands:

sudo dnf install python3 python3.11-devel
sudo dnf config-manager --set-enabled crb
sudo dnf install epel-release
sudo dnf install libsodium

firewall-cmd --zone=public --add-service=https --permanent
firewall-cmd --zone=public --add-port=4505-4506/tcp --permanent
firewall-cmd --reload

Installing VMware Salt

With prerequisites in place, I extracted the installer and ran the single-node setup script:

tar -xzf VMware_Salt_RaaS-8.18.3-25253633.el9_Installer.tar.gz
cd sse-installer
./setup_single_node.sh

Licensing

After installing, I noticed a warning banner that the license would expire soon (2 weeks). I added a license key using the following command:

echo "<your-license-key>" > /etc/raas/vra_license && chown raas:raas /etc/raas/vra_license && systemctl restart raas

Replacing the default SSL cert

By default, the web UI ships with a self-signed certificate (localhost.crt/localhost.key). I don’t like clicking the ‘continue anyway’ link every time I access a ‘secure’ page in my lab, so I generated a proper CSR, got it signed, and swapped it in:

cd /etc/pki/raas/certs/
vi cm-vcfsalt-01.crt  # <press i & paste cert contents>
vi cm-vcfsalt-01.key  # <press i & paste key contents>
chown raas:raas cm-vcfsalt-01.crt cm-vcfsalt-01.key
chmod 400 cm-vcfsalt-01.crt cm-vcfsalt-01.key

Then point /etc/raas/raas at the new files by editing the tls_crt and tls_key properties and restart the raas service:

tls_crt: /etc/pki/raas/certs/cm-vcfsalt-01.crt
tls_key: /etc/pki/raas/certs/cm-vcfsalt-01.key
systemctl restart raas

Accepting the master key

Last step for a fresh install: log in to the web UI, go to Administration –> Master Keys (or use the “pending master keys” banner), and accept the default master key so minions can actually check in.

The next post will cover repointing existing salt minions from the old master to this new one.

Posted in Lab Infrastructure, Virtualization | 1 Comment

Building a Rocky Linux 9.8 Template VM by Hand

Most of my lab runs on Ubuntu VMs built and updated through Packer, but a recent project called for a Rocky Linux template. Since this isn’t something I expect to repeat often, I didn’t want to invest time building out a Packer pipeline for it – I built the template manually in vSphere instead. Here’s the process I used, documented for the next time I (or you) need it.

Creating the VM

Start with a new VM in vSphere using these settings:

  • Name: template-rocky98-minimal
  • Compatibility: ESXi 8.0 U2 and later (vmx-21)
  • Guest OS Family: Linux
  • Guest OS Version: Rocky Linux (64-bit)

When customizing the hardware, the only change I made was bumping the hard disk from the default 16GB to 20GB. Everything else was left at its default.

Power on the VM and attach the Rocky-9.8-x86_64-minimal.iso installer image.

Installing Rocky Linux

From the boot menu, select Install Rocky Linux Minimal 9.8.

In the installer:

  1. Confirm the automatic installation destination — it should default to the 20GB disk created earlier.
  2. Create a user account:
    • Username: template-admin
    • Grant this user administrator privileges.
    • Require a password to use the account: VMware1!
    • Leave the root account disabled.
  3. Begin the installation.

Post-Install Configuration

Once the install finishes and you’ve rebooted into the new system, run the following as root:

sudo -i

dnf update -y
dnf install open-vm-tools cloud-init -y

systemctl enable --now vmtoolsd

echo "ssh_pwauth: unchanged" | tee /etc/cloud/cloud.cfg.d/99-ssh-pwauth.cfg
rm -f /etc/ssh/ssh_host_*

shutdown -h now

A quick rundown of what this does:

  • Updates the system and installs open-vm-tools and cloud-init (so future clones can be provisioned with customization specifications).
  • Enables and starts the vmtoolsd service.
  • Drops a cloud-init override so it doesn’t touch the SSH password-authentication setting on first boot of a cloned VM.
  • Removes the host’s SSH host keys, so each VM cloned from this template generates its own unique keys on first boot instead of sharing the template’s.
  • Shuts the VM down cleanly, ready for conversion.

Finalizing the Template

Before converting, add a VM note for future reference — something like:

2026-09-19: Created template

Then convert the VM to a template.

Updating the Template Later

If you ever need to make changes inside the guest OS, such as patching, adding packages, etc., you’ll need to convert the template back to a VM first. Before converting it back to a template again, repeat the cleanup steps so the next clone doesn’t inherit stale SSH configuration or host keys:

sudo -i

rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf
rm -f /etc/ssh/ssh_host_*

shutdown -h now

Once it’s powered off, convert it back to a template.

Conclusion

That’s it! A simple, repeatable manual process for a template I don’t need to fully automate. If this becomes a more frequent need, it’d be a good candidate to eventually fold into the same Packer-based workflow I use for Ubuntu.

Posted in Lab Infrastructure, Virtualization | Leave a comment

Troubleshooting a “Dead Timeout” NVMe Device on ESX

I’ve been spending some time working with VMware Cloud Foundation (VCF) 9.1 in my lab environment. The lab consists of several nested ESXi hosts that use NFS storage provided by a virtual machine. The VM providing the NFS storage, in turn, uses a consumer-grade NVMe SSD for its storage.

Recently, I ran into an interesting problem where the datastore backing this NFS appliance would occasionally become unavailable.

An NVMe Controller Failure

The first time I encountered the problem, I started looking through the ESXi logs to see what had happened. In vmkwarning.log, I found several messages related to the NVMe controller:

WARNING: NVMEDEV:9464 Controller 257 failed to recover after multiple attempts, switch to periodical recovery. 
WARNING: NVMEPSA:217 Complete vmkNvmeCmd: 0x45d9a265b8c0, vmkPsaCmd: 0x45d9c7dd5fc0, cmdId.initiator=0x45395c79ba58, CmdSN: 0x0, status: 0x80d 
WARNING: NvmeDevice: 573: Get identify namespace data failed for device Status %s. 
WARNING: NvmeUtil: 151: Error on Cmd(0x45d9c7dd5fc0) 0x6, CmdSN 0x0 from world 0 to component "nqn.1994-11.com.samsung:nvme:990PRO:M.2:<REDACTED>" H:0xe D:0x0 P:0x0 
WARNING: NVMEPSA:217 Complete vmkNvmeCmd: 0x45d9a26d4ac0, vmkPsaCmd: 0x45d9c7dd5fc0, cmdId.initiator=0x45395c79ba58, CmdSN: 0x0, status: 0x80d 
WARNING: NVMEPSA:217 Complete vmkNvmeCmd: 0x45d9a27100c0, vmkPsaCmd: 0x45d9c7dd5fc0, cmdId.initiator=0x45395c79ba58, CmdSN: 0x0, status: 0x80d

The first line was particularly interesting:

Controller 257 failed to recover after 21 attempts

My interpretation of these messages is that ESX detected a problem communicating with the NVMe controller and attempted to recover it. After 21 unsuccessful recovery attempts, ESX stopped trying to recover the controller normally and switched to periodic recovery.

The subsequent messages indicate that ESXi was unable to retrieve information from the NVMe namespace and was encountering command failures.

At this point, the underlying NVMe device was effectively unavailable to ESX, which explained why the VMFS datastore disappeared, which in turn made the guest hosted NFS mount disappear.

Checking the NVMe Device

A reboot of the ESX host brought the device back online and the datastore became available again. However, a few days later the error returned. This time I saw similar vmkwarning.log entries, but I wanted to take a closer look at the NVMe device. I used the following command:

esxcli storage core device list | grep -i "samsung" -B 1 -A 12

The output included the following:

t10.NVMe____Samsung_SSD_990_PRO_with_Heatsink_4TB___<redacted>
   Display Name: Local NVMe Disk (t10.NVMe____Samsung_SSD_990_PRO_with_Heatsink_4TB___<redacted>)
   Has Settable Display Name: true
   Size: 3815447
   Device Type: Direct-Access
   Multipath Plugin: HPP
   Devfs Path:
   Vendor: NVMe
   Model: Samsung SSD 990 PRO with Heatsink 4TB
   Revision: 4B2QJXD7
   SCSI Level: 0
   Is Pseudo: false
   Status: dead timeout
   Is RDM Capable: false
   Is Local: true
   Is Removable: false
   Is SSD: true
   Is VVOL PE: false
   Is Offline: false
   Is Perennially Reserved: false
   Queue Full Sample Size: 0

There were two things that caught my attention here.

First, the device status was:

Status: dead timeout

That was consistent with what I had seen in vmkwarning.log.

Second, the firmware revision was:

Revision: 4B2QJXD7

Checking the Samsung Firmware

I then checked Samsung’s support site for firmware updates for the 990 PRO.

Samsung had newer firmware available for the drive, including firmware versions that addressed issues involving intermittent drive recognition and blue-screen problems.

That seemed particularly interesting given the behavior I was seeing. The drive wasn’t permanently failing; it would occasionally stop responding to the host and then become available again after a reboot.

At that point, I decided that updating the firmware was worth trying.

I updated the drive to the latest firmware available at the time, 8B2QJXD7, using the ISO image provided by Samsung.

Has the Firmware Update Fixed It?

The real test was simply to wait and see what happened.

I’ve continued using the NVMe drive in the lab for roughly a month since updating the firmware. During that time, I’ve performed a number of tests and continued using the drive in the same general configuration.

So far, the device has remained stable.

I haven’t seen the NVMe controller recovery messages return, and I haven’t had another occurrence where the device went into a dead timeout state and caused the NFS datastore to become unavailable.

While that doesn’t prove conclusively that the firmware was the root cause, the combination of the symptoms, the older firmware version, Samsung’s firmware notes, and the fact that the problem has not returned after the update makes the firmware a strong suspect.

Conclusion

This was a good reminder that when an ESX storage device suddenly disappears, it is worth looking beyond the datastore and filesystem layers.

In my case, the NFS datastore was only the symptom. The underlying problem was an NVMe device that had stopped responding to ESX.

The vmkwarning.log messages showed ESX repeatedly attempting to recover the NVMe controller, while esxcli storage core device list subsequently showed the device in a dead timeout state.

Checking the firmware version ultimately revealed that the Samsung 990 PRO was running an older firmware revision. Updating it to 8B2QJXD7 appears to have resolved the intermittent failures in my lab.

If you are seeing similar NVMe controller recovery messages or dead timeout states on ESXi, checking the firmware version of the affected drive would be a worthwhile troubleshooting step.

Posted in Lab Infrastructure, Virtualization | Leave a comment

Troubleshooting Aria Automation Orchestrator Deployment

While deploying VMware Aria Automation Orchestrator 8.18.1 in standalone mode through Aria Suite Lifecycle Manager (LCM), I encountered the following error during configuration:

Error Code: LCMVROVACONFIG100034
Failed to set VMware Aria Automation as authentication provider in VMware Aria Automation Orchestrator.

The deployment completed successfully, I was able to login to Orchestrator and it looked healthy, but the request was in a failed state, and the product was not added to the environment in LCM.

Initial Investigation

A search led me to the following Broadcom Knowledge Base article: https://knowledge.broadcom.com/external/article/427647/aria-automation-orchestrator-integration.html

The article references reviewing the following log file on the Lifecycle Manager appliance:

/var/log/vrlcm/vmware_vrlcm.log

While monitoring this log during a retry, I noticed LCM executing the following command on the Orchestrator appliance:

Command: vracli vro authentication

The command itself appeared to complete successfully:

exit-status: 0
Command executed successfully

However, immediately afterward, the following error appeared in the LCM logs:

com.fasterxml.jackson.core.JsonParseException:
Unexpected character ('-' (code 45)):
Expected space separating root-level values

This suggested that LCM was attempting to parse the command output as JSON and failing.

Looking Closer at the Command Output

The log contained the full response returned by the command. At first glance, the output looked like valid JSON. However, there was something interesting before the JSON payload:

2026-06-16T18:56:56.062818368Z main INFO Starting configuration...
2026-06-16T18:56:56.065478499Z main INFO Start watching for changes...
2026-06-16T18:56:56.075515092Z main INFO Configuration started...

Only after these INFO level log messages did the JSON object begin.

To confirm this behavior, I ran the command directly on the Aria Automation Orchestrator appliance:

vracli vro authentication

The output showed several Log4j informational messages before the JSON configuration data:

2026-06-16T19:04:13.657392968Z main INFO Starting configuration...
2026-06-16T19:04:13.659806004Z main INFO Start watching for changes...
2026-06-16T19:04:13.668725416Z main INFO Configuration started...
{
  ...
}

From a human perspective, this output is readable. From LCM’s perspective, however, it is invalid JSON because the response does not begin with a {.

This explained the parsing exception perfectly.

Finding the Source

The log messages referenced the following configuration file:

/usr/lib/thin-cfg-cli/conf/log4j2.xml

That path did not exist directly on my appliance. Using the following command:

find / | grep -i log4j2.xml

I located the file within a Docker overlay filesystem.

Important: Editing files directly inside Docker overlay storage is generally not recommended. Container updates, restarts, or image replacements can overwrite these changes. The following modification was performed only as a troubleshooting test to validate the root cause.

I temporarily changed the log4j status logging level from INFO to WARN:

sed -i 's/status="INFO"/status="WARN"/i' \
/data/docker/overlay2/782e95577448c9191f0d0f2ac4744f55fc0537ff96f2ccdff55a201adb0ac377/diff/usr/lib/thin-cfg-cli/conf/log4j2.xml

Validation

After making the change, I reran the command from the Orchestrator appliance:

vracli vro authentication

This time the output contained only JSON:

{
  "ch.dunes.authentication.provider": "vsphere",
  ...
}

No log4j informational messages appeared before the JSON payload.

With that result, I retried the failed task in Lifecycle Manager.

The retry completed successfully and the Aria Automation Orchestrator deployment finished without error.

Root Cause

Lifecycle Manager executes:

vracli vro authentication

and expects the response to be valid JSON.

On my deployment, the command emitted log4j initialization messages before the JSON payload. Although the command itself completed successfully with an exit code of zero, the additional logging caused JSON parsing to fail, resulting in:

LCMVROVACONFIG100034

Suppressing the log4j status messages allowed the command to return valid JSON and enabled LCM to complete the authentication provider configuration.

Conclusion

This issue serves as a reminder that a successful command execution does not always mean an automated workflow will succeed. In this case, the actual authentication configuration was valid, and the command returned the expected data. The failure occurred because additional logging output contaminated what Lifecycle Manager expected to be a machine-readable JSON response.

If you encounter LCMVROVACONFIG100034 during a standalone Aria Automation Orchestrator deployment, it may be worth checking the output of vracli vro authentication directly on the appliance. If informational log4j messages appear before the JSON payload, Lifecycle Manager may be failing during JSON parsing rather than during the authentication configuration itself.

While directly modifying files inside Docker overlay storage should not be considered a permanent solution, this troubleshooting exercise helped isolate the root cause and provided a path toward a successful deployment. Hopefully this saves someone else a few hours of digging through logs and chasing what initially appears to be an authentication problem.

Posted in Lab Infrastructure, Virtualization | Leave a comment