Deploying Nutanix AHV VMs with Ansible

Start here: validate the inputs before creating a VM

This revised example uses nutanix.ncp 2.5.0 and requires explicit opt-in before creation. Prove read-only access first, verify the resource IDs, and plan for an ambiguous or partially completed request.

Jump to project setup, the guarded playbook, validation and execution, or readiness checks.

Introduction

This is a guarded lab example for creating one AHV VM from an existing, prepared disk image through Prism Central. Complete the read-only Ansible foundations exercise first. Use the same project-local nutanix.ncp 2.5.0 collection, Python/Ansible environment, and installed SDK dependencies. Check the release compatibility table against your actual AOS and Prism Central versions. This example is not a production-certified deployment role.


My Personal Repository on GitHub

Nutanix Repository on GitHub


Diagram: VM Deployment Workflow

AHV lab workflow: validate inputs and read access, check for an existing VM, explicitly approve creation with nutanix.ncp.ntnx_vms_v2 through Prism Central, then validate the new VM with its NIC disconnected and no power-on requested. Connection and startup require separate approval.
Guarded lab creation with nutanix.ncp.ntnx_vms_v2. Check inputs and existing VMs before explicit creation approval; validate the result before separately approved connection or startup.

Step 1: Define Inventory

Place inventory.yml, nutanix_credentials.yml, vm_spec.yml, and deploy_vm.yml together beside the project-local collections directory from Part 1. The local interpreter below keeps module execution in the same Python environment as Ansible.

all:
  hosts:
    localhost:
      ansible_connection: local
      ansible_python_interpreter: "{{ ansible_playbook_python }}"

Step 2: Define Credentials File

Reuse the encrypted Vault file from Part 1, or create it with ansible-vault create nutanix_credentials.yml. Enter the placeholders below inside the editor, replace them there, and use ansible-vault edit for later changes. Keep passwords out of source control, command arguments, and logs. The approved account needs inventory visibility and the permissions required for VM creation; read access alone is insufficient.

nutanix_pc_hostname: "prism-central.example.com"
nutanix_pc_port: "9440"
nutanix_username: "REPLACE_WITH_APPROVED_ACCOUNT"
nutanix_password: "REPLACE_INSIDE_VAULT_EDITOR"

Step 3: Create Your VM Deployment Playbook

Create vm_spec.yml with the inputs below. Resolve each UUID in the intended Prism Central environment and verify that the subnet belongs to the chosen placement and the prepared UEFI disk image is available there. UUID syntax alone does not prove that a resource exists, is visible, or is compatible. The image must contain the guest drivers and customization needed for your workload.

vm_name: "lab-app-01"
cluster_ext_id: "REPLACE_WITH_CLUSTER_UUID"
subnet_ext_id: "REPLACE_WITH_SUBNET_UUID"
image_ext_id: "REPLACE_WITH_IMAGE_UUID"

Save this as deploy_vm.yml. It uses ntnx_vms_v2, assigns 2 vCPUs and 8 GiB RAM, clones the image without requesting a disk resize, and leaves the NIC disconnected. It does not request power-on. Validate the guest and network identity before separately approving connection and startup.

- name: Create one approved AHV lab VM
  hosts: localhost
  gather_facts: false
  vars_files:
    - nutanix_credentials.yml
    - vm_spec.yml
  vars:
    allow_create: false
    uuid_pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'

  tasks:
    - name: Require explicit creation approval and a normal run
      ansible.builtin.assert:
        that:
          - allow_create | bool
          - not ansible_check_mode
        fail_msg: "Creation is disabled. Review the lab change before opting in; check mode is not supported by this workflow."

    - name: Validate the supplied name and reference formats
      ansible.builtin.assert:
        that:
          - vm_name is match('^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$')
          - cluster_ext_id is match(uuid_pattern)
          - subnet_ext_id is match(uuid_pattern)
          - image_ext_id is match(uuid_pattern)
        fail_msg: "Replace every reference with a verified UUID and use a simple, approved VM name."

    - name: Look for an existing visible VM with this name
      nutanix.ncp.ntnx_vms_info_v2:
        nutanix_host: "{{ nutanix_pc_hostname }}"
        nutanix_port: "{{ nutanix_pc_port }}"
        nutanix_username: "{{ nutanix_username }}"
        nutanix_password: "{{ nutanix_password }}"
        validate_certs: true
        filter: "name eq '{{ vm_name }}'"
        limit: 1
      register: vm_lookup
      no_log: true

    - name: Stop if the name is already in use
      ansible.builtin.assert:
        that:
          - vm_lookup.response is defined
          - vm_lookup.response is sequence
          - vm_lookup.response is not string
          - vm_lookup.response is not mapping
          - vm_lookup.response | length == 0
        fail_msg: "A VM already exists or discovery did not return an empty list. Review Prism Central before proceeding."
      no_log: true

    - name: Create the VM from the approved UEFI disk image
      nutanix.ncp.ntnx_vms_v2:
        nutanix_host: "{{ nutanix_pc_hostname }}"
        nutanix_port: "{{ nutanix_pc_port }}"
        nutanix_username: "{{ nutanix_username }}"
        nutanix_password: "{{ nutanix_password }}"
        validate_certs: true
        state: present
        wait: true
        name: "{{ vm_name }}"
        description: "Approved Ansible lab deployment"
        cluster:
          ext_id: "{{ cluster_ext_id }}"
        num_sockets: 1
        num_cores_per_socket: 2
        memory_size_bytes: 8589934592
        boot_config:
          uefi_boot:
            boot_order:
              - DISK
        disks:
          - disk_address:
              bus_type: SCSI
              index: 0
            backing_info:
              vm_disk:
                data_source:
                  reference:
                    image_reference:
                      image_ext_id: "{{ image_ext_id }}"
        nics:
          - nic_backing_info:
              virtual_ethernet_nic:
                model: VIRTIO
                is_connected: false
            nic_network_info:
              virtual_ethernet_nic_network_info:
                nic_type: NORMAL_NIC
                subnet:
                  ext_id: "{{ subnet_ext_id }}"
      register: created_vm
      no_log: true

    - name: Require a returned VM identifier
      ansible.builtin.assert:
        that:
          - created_vm.ext_id is defined
          - created_vm.ext_id is string
          - created_vm.ext_id | length > 0
        fail_msg: "Creation may have succeeded, but the VM ID is missing. Inspect Prism Central; do not retry blindly."
      no_log: true

    - name: Record only the non-secret identifier in the change record
      ansible.builtin.debug:
        msg: "Created VM external ID: {{ created_vm.ext_id }}"

Retry boundary: creation without ext_id is not name-based reconciliation. The name check is only a guard against visible duplicates, not a lock; two concurrent runs or restricted inventory visibility can defeat it. Allow one authorized run, retain the returned ID, and inspect the task and VM in Prism Central after any ambiguous failure before considering another attempt. Updates and deletion need separate, reviewed workflows.


Step 4: Add Extra Configs (Optional)

  • Add optional fields only after checking ansible-doc nutanix.ncp.ntnx_vms_v2 for the installed release.
  • Categories use a list of category ext_id references in this module. Do not copy the earlier name/value or arbitrary metadata fragment into this schema.
  • Treat guest customization, network connection, power actions, and backup assignment as separate changes with their own acceptance checks. A created VM is not automatically a configured or protected application.

Step 5: Run the Playbook

Keep creation disabled while preparing the files. The final command below deliberately enables a real VM-creation request; it is not a dry run. Do not use --check as a substitute: the information module used by this workflow does not support check mode.

# Inspect the installed module before editing or running the playbook.
ANSIBLE_COLLECTIONS_PATH=./collections ansible-doc nutanix.ncp.ntnx_vms_v2

# Local syntax validation; this does not prove permissions or API compatibility.
ansible-playbook -i inventory.yml deploy_vm.yml --syntax-check --ask-vault-pass

# WRITES to Prism Central: run once, only after approval in the intended lab.
ansible-playbook -i inventory.yml deploy_vm.yml --ask-vault-pass --extra-vars '{"allow_create":true}'

Tips for Production Readiness

  • Approve the target Prism Central, placement, subnet, prepared image, resource sizing, and account scope. Use an isolated lab network first.
  • Validate certificate trust instead of disabling it. Keep secret-bearing tasks under no_log: true, and protect the Vault password and editor temporary files.
  • Serialize creation runs. Check for existing or partially created VMs after any timeout, and preserve the VM/task identifiers in the change record.
  • Confirm the actual CPU, RAM, cloned disk, boot configuration, NIC state, and power state before allowing the guest onto a network.
  • Test guest uniqueness, application health, backup enrollment, and a separately approved cleanup procedure before turning the lab recipe into a deployment role.

Summary

This recipe prepares a controlled, version-specific AHV creation request—not a claim of production readiness. A successful API task must still be followed by configuration and guest validation. Keep lifecycle actions, concurrent-run protection, secret management, backup enrollment, and recovery procedures explicit before expanding beyond a single lab VM.

External Documentation:

Keep exploring

Choose your next step

Continue with the path that best matches the architecture or operating challenge in front of you.

Leave a Reply

Discover more from Digital Thought Disruption

Subscribe now to keep reading and get access to the full archive.

Continue reading