Automated infrastructure pipeline for SLTN → Doetinchem (ESET ICAP) built entirely with Ansible / AWX. All secrets (Prism credentials, sltnadmin data) and dynamic targets (Nutanix host, Image Name) are natively injected straight into the playbooks by AWX without needing third-party vault plugins in the code.
AWX Workflow Template
│
├── Job 1 ── create-vms.yml (hosts: localhost)
│ └─ Prism Central API via nutanix.ncp
│ └─ 4× Ubuntu 24.04 VMs with cloud-init
│
├── Job 2 ── Approval (not a playbook)
│ └─ "Setup NGT in Prism Central" — human mounts NGT ISO; then Approve in AWX
│
├── Job 3 ── general-server-config.yml (hosts: doet_icap, SSH)
│ └─ OS baseline: timezone, NTP verify, Nutanix Guest Tools (NGT), fastfetch, UFW, apt upgrade
│
└── Job 4 ── install-eset.yml (hosts: doet_icap, SSH)
└─ Download + unattended ESET install, enable eraagent, reboot when install ran
doet/
├── ansible.cfg # host_key_checking=False, test inventory path by default
├── collections/
│ └── requirements.yml # AWX native collection dependencies
├── inventories/
│ ├── production/ # Production IPs & UUIDs
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ └── all.yml
│ └── test/ # Test IPs & UUIDs
│ ├── hosts.yml
│ └── group_vars/
│ └── all.yml
├── playbooks/
│ ├── create-vms.yml
│ ├── general-server-config.yml
│ ├── install-eset.yml
│ ├── group_vars/
│ │ └── doet_icap.yml # Shared App Config (ESET, UFW, SSH)
│ └── templates/
│ └── cloud-init.j2
└── README.md
| Namespace | File | Used by |
|---|---|---|
vm_* |
inventories/*/group_vars/all.yml |
Environment-specific network & hardware specs |
ansible_*, eset_*, ufw_* |
playbooks/group_vars/doet_icap.yml |
Shared application/OS config for the doet_icap group |
| Secrets / Dynamic Vars | AWX at runtime | Injected via Custom Credentials and Surveys |
Single Source of Truth:
all.ymlcontains only variables that change per environment (UUIDs, Subnets, IPs).doet_icap.ymlcontains application variables that remain the same (UFW ports, ESET installers). This layout keeps the environment folders minimal and easy to clone for new projects.Native Precedence: Ansible allows you to selectively override these shared values. If a specific environment (e.g., Production) ever requires a different SSH private key path or unique ESET URL, simply recreate
doet_icap.ymlinside that environment'sgroup_vars/folder. Inventory variables take precedence over playbook-level defaults.
Prism Central URL: slcd-waatnpc01.dpb.sltncloud.local (adjustable via vars)
| VM Name | IP | CPU | RAM | OS Disk | Data Disk |
|---|---|---|---|---|---|
| doet-gropicap01-test | 10.128.8.41 | 2s × 3c = 6c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap02-test | 10.128.8.42 | 2s × 3c = 6c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap03-test | 10.128.8.43 | 2s × 3c = 6c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap04-test | 10.128.8.44 | 2s × 3c = 6c | 24 GB | 50 GB | 300 GB /opt/eset |
Network: subnet SBHP-Servers (7421ee02-9b91-4a57-946b-b91f756fedb9) · gateway 10.128.8.1 · DNS 10.128.8.3 / 10.128.8.4 ·
NTP 10.128.8.3 / 10.128.8.4 (fallback ntp.ubuntu.com) · timezone Europe/Amsterdam
Nutanix UUIDs (Test):
- Cluster:
000648fa-6f4b-66b0-60a2-4cd98f907be6 - Base Image:
29825f72-f2fb-449e-b7aa-9481a955fa2b(noble-server-cloudimg-amd64.img) - Subnet eth0 (SBHP-Servers):
7421ee02-9b91-4a57-946b-b91f756fedb9
Prism Central URL: doet-gropnpc01.doetinchem-sc.sltncloud.local (adjustable via vars)
Dual-NIC layout — each VM has two network interfaces:
- eth0 (
DOET:DOET:EPG-DOET-vSphere-DCU VLAN 0,10.130.100.x) — external / internet-facing. Carries the default route (10.130.100.1) and all DNS. Ansible SSH connects on this interface.- eth1 (
NFSC VLAN 779,10.128.40.x) — internal only. Traffic between Nutanix and ESET flows here. No default route. No internet access.
| VM Name | eth0 IP (external / Ansible) | eth1 IP (internal) | CPU | RAM | OS Disk | Data Disk |
|---|---|---|---|---|---|---|
| doet-gropicap01-prod | 10.130.100.191 | 10.128.40.31 | 2s × 6c = 12c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap02-prod | 10.130.100.192 | 10.128.40.32 | 2s × 6c = 12c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap03-prod | 10.130.100.193 | 10.128.40.33 | 2s × 6c = 12c | 24 GB | 50 GB | 300 GB /opt/eset |
| doet-gropicap04-prod | 10.130.100.194 | 10.128.40.34 | 2s × 6c = 12c | 24 GB | 50 GB | 300 GB /opt/eset |
eth0 — External Network:
Subnet DOET:DOET:EPG-DOET-vSphere-DCU (VLAN 0) (26d75bac-450c-4f99-99c7-19397cec1807) · gateway 10.130.100.1 · prefix /24
DNS 172.20.10.1 / 172.16.10.1 · NTP 172.20.10.1 / 172.16.10.1 (fallback ntp.ubuntu.com) · timezone Europe/Amsterdam
eth1 — Internal Network:
Subnet NFSC (VLAN 779) (e16571d2-0199-4bef-8c06-13525de192db) · no gateway (internal-only) · prefix /24
Nutanix UUIDs (Production):
- Cluster:
00062f9b-7a59-04cd-4837-1423f3232900 - Base Image:
ed2a849c-5a85-49ed-ad51-c7c1f828334b(noble-server-cloudimg-amd64.img) - Subnet eth0 (EPG-DOET-vSphere-DCU):
26d75bac-450c-4f99-99c7-19397cec1807 - Subnet eth1 (NFSC):
e16571d2-0199-4bef-8c06-13525de192db
Ansible Collections:
ansible-galaxy collection install \
nutanix.ncp:2.4.0 \
community.generalPython: playbooks/create-vms.yml installs ntnx-vmm-py-client in a pre_tasks pip step (required by the Nutanix collection for Prism API calls). The AWX execution environment must allow ansible.builtin.pip to run there, or bake that package into the EE image and adjust the playbook if you pre-install it.
Authentication for the sltnadmin account should be handled via SSH keys rather than passwords. An ED25519 key-pair is recommended for its high security and performance.
Run this command on your management workstation:
ssh-keygen -t ed25519 -C "sltnadmin@doet-icap" -f ./id_ed25519_sltnadminFor seamless Ansible provisioning, the pipeline splits your SSH key across two places:
- Public Key (
id_ed25519_sltnadmin.pub): Copy the contents of this file and paste it into thesltnadmin_ssh_pubkeyvariable inplaybooks/group_vars/all.yml(or your environment-specific file). Cloud-init will burn this into the VM's~/.ssh/authorized_keysfile at boot. - Private Key (
id_ed25519_sltnadmin): Do NOT commit this to Git! Instead, upload it as a Machine Credential in AWX. You will attach this credential to the configuration and health-check jobs.
Cloud-init requires a salted SHA-512 hash of the user's password, rather than cleartext.
To generate this hash safely on any Linux or Mac system, use one of the following methods (replace YourPasswordHere with your actual secure password):
Method A: Python (Universal)
python3 -c "import crypt; print(crypt.crypt('YourPasswordHere', crypt.mksalt(crypt.METHOD_SHA512)))"Method B: mkpasswd (Common on Linux)
echo "YourPasswordHere" | mkpasswd -m sha-512 -sYou will get a long string starting with $6$. Copy that entire string — you will paste this into AWX later as the SLTN Admin Password Hash.
Rather than hardcoding credentials or using lookup plugins in the playbook, we instruct AWX to natively inject secrets into the playbook's execution memory as extra_vars.
Create an AWX Custom Credential Type that securely injects the Prism Central credentials into nutanix_username and nutanix_password
Input configuration (YAML)
fields:
- id: nutanix_user
label: Nutanix Username
type: string
- id: nutanix_pass
label: Nutanix Password
type: string
secret: true
required:
- nutanix_user
- nutanix_passInjector configuration (YAML)
extra_vars:
nutanix_username: "{{ nutanix_user }}"
nutanix_password: "{{ nutanix_pass }}"(You will bind this credential to the doet-create-vms Job Template).
This credential type securely injects the sltnadmin login secrets. It is designed to handle both SSH Key-based and Password-based authentication, ensuring connectivity even when ssh_pwauth is disabled.
Input configuration (YAML)
fields:
- id: admin_password
label: SLTN Admin Cleartext Password
type: string
secret: true
- id: admin_key
label: SLTN Admin SSH Private Key
type: string
multiline: true
secret: true
- id: admin_hash
label: SLTN Admin Password Hash (SHA-512)
type: string
secret: true
required:
- admin_hashInjector configuration (YAML)
extra_vars:
ansible_user: "sltnadmin"
ansible_password: "{{ admin_password }}"
ansible_ssh_private_key_file: "{{ admin_key }}"
sltnadmin_password_hash: "{{ admin_hash }}"Note
If ssh_pwauth is set to false, the SSH Private Key field must be populated for Ansible to connect. If true, the Cleartext Password will be used for authentication.
Creating the Credential Type only creates a blueprint. You must now use those blueprints to input your actual secrets. Since the test and production environments have different credentials, you should create one for each.
For the Test Environment:
- Go to Credentials → Add.
- Name:
Nutanix API - Test - Credential Type: Select Nutanix Auth.
- Fill in the Nutanix Username and Nutanix Password for the test environment.
- Click Save.
For the Production Environment:
- Go to Credentials → Add.
- Name:
Nutanix API - Production - Credential Type: Select Nutanix Auth.
- Fill in the Nutanix Username and Nutanix Password for the production environment.
- Click Save.
Repeat this process for the sltnadmin hash (create one for each environment if they differ, or one if it's the same).
Syncing the Git project only downloads the files. You must create two AWX Inventories so the pipeline knows how to map to both environments cleanly.
First: Create the Test Inventory
- Go to Inventories → Add → Add Inventory.
- Name:
doet-testand click Save. - Go to the Sources tab inside the new inventory and click Add.
- Name:
test-source. Source:Project(Select your synced Git project). - Inventory file: Select
inventories/test/from the drop-down. - Check Overwrite and Update on Launch, then Save & Sync.
Second: Create the Production Inventory
- Go to Inventories → Add → Add Inventory.
- Name:
doet-productionand click Save. - Go to the Sources tab and click Add.
- Name:
production-source. Source:Project(Select your synced Git project). - Inventory file: Select
inventories/production/from the drop-down. - Check Overwrite and Update on Launch, then Save & Sync.
(AWX can now invisibly swap all hardcoded IPs and specs just by alternating between these two!)
Rather than creating 6 duplicate playbooks for Test and Prod, you only need to create 3 Job Templates and tell AWX to prompt you for the environment!
- Go to Templates → Add → Add Job Template.
- Name:
doet-create-vms - Job Type: Run
- Inventory: Select
doet-test(as a placeholder), but immediately click the Prompt on Launch checkbox right next to it! This ensures AWX asks you whether to deploy to Test or Prod. - Project: Select your synced Git project.
- Playbook: Click the drop-down and select
playbooks/create-vms.yml. - Credentials:
- Click the glass and select BOTH the
Nutanix APIandsltnadmin-hashcategories. - For each one, CHECK the "Prompt on Launch" box right next to the category label.
- (Alternatively, you can select specific ones as defaults, but checking Prompt ensures you always select the right one for the chosen environment).
- Click the glass and select BOTH the
- Save.
Now, create the second Job Template:
- Go to Templates → Add → Add Job Template.
- Name:
doet-general-server-config - Job Type: Run
- Inventory: Select
doet-test(as a placeholder), and CHECK the "Prompt on Launch" box next to the Inventory field. - Project: Select your synced Git project.
- Playbook: Select
playbooks/general-server-config.yml. - Credentials: Attach the standard Machine Credential you made in the Prerequisites containing your private SSH key.
- Save.
Finally, create the third Job Template:
- Go to Templates → Add → Add Job Template.
- Name:
doet-install-eset - Job Type: Run
- Inventory: Select
doet-test(as a placeholder), and CHECK the "Prompt on Launch" box next to the Inventory field. - Project: Select your synced Git project.
- Playbook: Select
playbooks/install-eset.yml. - Credentials: Attach the standard Machine Credential containing your private SSH key.
- Save.
All hardware specs and subnets are hardcoded inside the inventories/ folders so they perfectly match their environments. The only thing we prompt for dynamically is the Image you wish to deploy.
- Open the
doet-create-vmsJob Template you just saved. - Click the Survey tab at the top.
- Click Add to add the following prompt:
- Base Image Name
- Answer Variable Name:
vm_image_name - Question Type: Text
- (Note: The playbook dynamically resolves this into the Prism UUID.)
- Answer Variable Name:
- Base Image UUID (Optional alternative)
- Answer Variable Name:
vm_image_uuid - Question Type: Text
- Answer Variable Name:
- Base Image Name
- Save the questions, and ensure you flip the toggle to Survey Enabled at the top right of the screen!
- CRUCIAL STEP: Go back to the Details tab of the
doet-create-vmsJob Template, click Edit, and check the Prompt on Launch checkbox under the "Variables" or "Survey" section.
If the automatic vm_image_name lookup fails, you can bypass it using vm_image_uuid. Here is how to find your image's exact UUID:
Method 1: Prism Central Web UI
- Log in to your Prism Central interface.
- Navigate to Compute & Storage → Images.
- Click on the name of the image you want to use.
- Look at the URL in your browser address bar. It will look like:
https://prism-central-ip/infrastructure/virtual_infrastructure/images/5d1b...-....-....-.... - The very last string of text in the URL is the UUID (
ext_id).
Method 2: Command Line (CVM)
- SSH into any Nutanix CVM as the
nutanixuser. - Run
acli image.list - Copy the UUID next to the name of your image.
A Workflow Job Template connects your standalone Job Templates together so they run automatically in sequence.
- Go to Templates → Add → Add Workflow Job Template.
- Name:
Doetinchem ESET Deployment Pipeline - Select your Organization and click Save.
- The Workflow Visualizer will automatically open. This is where you draw the sequence.
- Click Start, then select the
doet-create-vmsJob Template from the list, and click Save. - Hover over the
doet-create-vmsnode you just placed, and click the (+) icon to add the next step.- Set the Run Type condition to On Success.
- Under "Node Type", select Approval.
- Name it
"Setup NGT in Prism Central"and click Save.
- Hover over the
"Setup NGT"approval node, click (+), and set the Run Type condition to On Success.- Select the
doet-general-server-configJob Template and click Save.
- Select the
- Hover over the
doet-general-server-confignode, click (+), and set the Run Type condition to On Success.- Select the
doet-install-esetJob Template and click Save.
- Select the
- Once your visualizer looks like a chain of linked boxes with an Approval node in the middle, click Save at the top right to close the visualizer.
When you click Launch on your Workflow Template, AWX will ask you three things:
- Which Inventory? — Choose either the
doet-productionordoet-testinventory. Everything else (subnets, IPs, hostnames) is instantly loaded from thegroup_varslinked to that choice! - Which Credentials? — Choose the Nutanix and sltnadmin credentials that correspond to the environment you selected in step 1.
- Survey Prompts — It will ask you for the
vm_image_name(orvm_image_uuid).
After you hit Next:
- Job 1 runs
doet-create-vmsto create the servers. - Natively export checking statuses and the IP endpoints (
set_stats). - Job 2 (Approval) — The workflow pauses. Follow the Mid-Deployment NGT Setup instructions below to mount the ISO in Prism Central. When done, click Approve in the AWX UI.
- Job 3 runs
doet-general-server-configover SSH (NGT install from CD-ROM, baseline, UFW, etc.). - Job 4 runs
doet-install-esetover SSH to install ESET and restart services (conditional reboot when install ran).
While this project is optimized for AWX, you can also execute the provisioning pipeline directly from your local terminal using the Ansible CLI.
Ensure you have the same collections as in AWX Prerequisites (Nutanix + community.general):
ansible-galaxy collection install \
nutanix.ncp:2.4.0 \
community.generalRun the playbooks in order: provision with the API, manually set up NGT in Prism (same pause as the AWX Approval step), then configure the OS and install ESET over SSH.
ansible-playbook -i inventories/test/hosts.yml playbooks/create-vms.yml \
-e "nutanix_username=YOUR_PC_USER" \
-e "nutanix_password=YOUR_PC_PASS" \
-e "sltnadmin_password_hash='YOUR_SHA512_HASH'"Mount the Nutanix Guest Tools ISO in Prism Central for each VM (see Mid-Deployment: Setup NGT in Prism Central below). This matches the AWX Approval node between Job 1 and Job 3.
Option A: Using SSH Key-Pairs (Recommended)
ansible-playbook -i inventories/test/hosts.yml playbooks/general-server-config.yml \
--private-key ~/.ssh/id_ed25519_sltnadmin \
-u sltnadminOption B: Using Passwords
ansible-playbook -i inventories/test/hosts.yml playbooks/general-server-config.yml \
-u sltnadmin \
-k -K(Note: -k prompts for the SSH password; -K prompts for the sudo password.)
ansible-playbook -i inventories/test/hosts.yml playbooks/install-eset.yml \
--private-key ~/.ssh/id_ed25519_sltnadmin \
-u sltnadmin(Use the same SSH key or password options as in Step 3.)
| Topic | Decision |
|---|---|
| Static IP | Custom Netplan via write_files + netplan apply in runcmd (bypasses schema validation) |
| Dual NIC | eth0 = external with default route + DNS matched on ens3 (in dual-nic); eth1 = internal only (no route, no DNS) matched on ens4 — both set-name renamed in a single Netplan file |
| Gateway | routes: [{to: default, via: ...}] on eth0 only — gateway4 is forbidden in Ubuntu 24.04 |
| NTP | Native ntp: key + timesyncd — correctly synced with runcmd task |
| Data disk | Native cloud-init fs_setup + mounts — idempotent, no wipefs/mkfs in runcmd |
| hostname/fqdn | Derived inline from item.name + vm_domain — no redundant fields in VM list |
| MOTD | No static /etc/motd.d/ banner from cloud-init. runcmd disables most /etc/update-motd.d/ scripts and re-enables only 85-fwupd, 90-updates-available, 97-overlayroot, 98-fsck-at-reboot, and 98-reboot-required (see cloud-init.j2). |
| NGT Install | Provision an additional empty CDROM in the disk spec. Because we append this drive before the cloud-init payload, the empty drive manifests as /dev/sr0, while cloud-init claims the second drive (/dev/sr1). Note: Prism Central pc.7.3.1.x lacks v4 APIs for automation. You must add an Approval Node to the AWX workflow between the Provisioning and Configuration jobs to pause the pipeline so you can manually "Setup NGT" in Prism Central. |
Because API automation for NGT is unsupported on this version of Prism Central, the AWX pipeline will pause at an Approval Node as soon as the VMs are booted. During this pause, you must manually mount the NGT installer to each VM:
- Log into Prism Central.
- Navigate to Compute & Storage → VMs.
- Select your newly created VMs (e.g.,
doet-gropicap01-test, etc.). - Under the Guest Tools menu, click Set Up NGT.
- Step 1 (Choose Preference): Select New NGT Installation and click Next.
- Step 2 (Configure Applications): Leave VSS and SSR unchecked (unless specifically required by your backup strategy) and click Next.
- Step 3 (Install): Select Mount Installer (do not select "Install Automatically").
- Click Complete Set Up.
- Return to the AWX Workflow UI and click Approve on the pause node to resume the pipeline!
Once the Ansible pipeline finishes, the servers are built, firewalled, and running ESET. However, ICAP must be manually activated on both ends to connect them.
By default, ESET Server Security for Linux installs with the ICAP service turned off. You must enable it via your ESET PROTECT central console (recommended) or the local WebGUI:
- Log into your ESET PROTECT console.
- Select your newly deployed linux servers (
doet-gropicap...) or their group. - Edit their Configuration Policy.
- Navigate to Server → ICAP.
- Toggle Enable ICAP server to On.
- Ensure the listening port is set to
1344(this is the port we automatically allowed through UFW via Ansible). - Apply the policy. ESET will now listen on port 1344 for incoming files.
Now you must tell your Nutanix File Server to send files to your new ESET machines for virus scanning.
- Log into Prism Central.
- Navigate to Infrastructure → Storage → File Server (this may vary slightly depending on your AOS/PC version).
- Select the target File Server and click Update (or select it and click Antivirus).
- Navigate to the Antivirus or ICAP configuration tab.
- Check the box to Enable Antivirus.
- Under ICAP Servers, click + Add ICAP Server.
- Input the internal IP addresses of your deployed VMs and Port
1344:- Nutanix communicates with ESET exclusively over the internal NIC (
eth1in Production,eth0in Test). - Use
10.128.40.31through10.128.40.34for Production (not the eth0 IPs).
- Nutanix communicates with ESET exclusively over the internal NIC (
- Check the box to allow Nutanix to block/quarantine infected files based on ESET's response.
- Click Save / Update. Nutanix will verify the connection to the ESET VMs.
Both disks are designed to be trivially expandable if you ever run low on space!
The Ubuntu cloud image is built with growpart natively active!
- Go into Prism Central.
- Edit the VM and increase the OS Disk 1 (SCSI 0) size (e.g., to 80GB).
- Reboot the VM. Cloud-init will automatically detect the new space, grow the partition, and expand the filesystem during boot. No manual CLI work required!
Because the Ansible playbook formats this secondary disk as a raw ext4 filesystem directly onto /dev/sdb (without creating rigid partition boundaries), expanding it is incredibly easy and can be done entirely online.
- Go into Prism Central.
- Edit the VM and increase the ESET Disk 2 (SCSI 1) size (e.g., to 500GB).
- If Linux doesn't pick up the new size automatically, trigger a rescan:
echo 1 | sudo tee /sys/class/block/sdb/device/rescan
- Expand the filesystem:
sudo resize2fs /dev/sdb
It instantly grabs 100% of the newly added Nutanix space natively without any downtime.
| Stage | Guard |
|---|---|
| VM creation | ntnx_vms_v2 state: present |
| Disk format | fs_setup overwrite: false |
| NGT install | ansible.builtin.stat: /usr/local/sbin/ngt_cli |
| ESET install | creates: /opt/eset/esets/sbin/eraagent |
| UFW rules | community.general.ufw is idempotent |
no_log: trueon every task that touches fetched secrets.- Secrets are never written to disk — they are seamlessly passed into Ansible memory space via AWX Custom Credentials.
ansible-vaultis not needed — AWX manages the encrypted at-rest states natively.- SSH password auth is enabled (
ssh_pwauth: truein cloud-init) for fallback CLI access. - Root login is disabled (
disable_root: truein cloud-init). - UFW default-deny inbound (ports 22 + 1344 allowed) applied before ESET installation.
# Lint all playbooks
ansible-lint playbooks/*.yml
# Dry-run general-server-config against a single host
ansible-playbook playbooks/general-server-config.yml --limit doet-gropicap01-test --check