Nutanix

CREATING CENTOS VM ON NUTANIX USING ANSIBLE

  • Nutanix Prism login credentials
  • Nutanix cluster URL, cluster name, subnet name, and disk image name
17 min read
CREATING CENTOS VM ON NUTANIX USING ANSIBLE

CREATING CENTOS VM ON NUTANIX USING ANSIBLE## INTRODUCTION

Nutanix Prism is a management solution for virtualized data center environments. Prism manages the entire stack from the storage and computes infrastructure up to virtual machines (VMs).

Here, we’ll discuss how to create a CentOS VM on Nutanix using the Ansible playbook. Ansible is a high-level automation engine that automates cloud provisioning, configuration management, and application deployment. 

Fig: Stages of creating CentOS VM on Nutanix using Ansible

PREREQUISITES

  • Nutanix Prism login credentials
  • Nutanix cluster URL, cluster name, subnet name, and disk image name

ANSIBLE FOR NUTANIX VM CREATION

Ansible roles are sets of Ansible defaults, files, tasks, templates, variables, and other Ansible components that work together. ‘nutanix_vm_provisioner*’ is a fundamental Ansible role to provision VMs on a Nutanix AHV using APIv3.

Variables Required:

  • api_version:                # Add API version, it’s best to use v3 (if available)
  • cluster_url:                 # IP address where you would typically log in to PRISM
  • prism_user:                # An account with permissions to provision on the cluster
  • prism_password:      # The password to your account
  • cluster_name:           # Name of the cluster to provision against
  • subnet_name:           # Name of the Subnet to add VMs to
  • image_name:            # Name of the disk image or ISO to use
  • vm_defs:                    # The list of dicts that defines the VMs to be created

Ansible Roles Required

A single role, ‘nutanix_vm’ is required for the VM creation. 

nutanix_vm’ role deals with creating a VM with the provided subnet, cluster, and the OS image by authenticating with a session cookie for logging into the Prism. The variables used in the code are placed in the ‘defaults’ folder of the role *‘nutanix_vm’. *The ‘defaults/main.yaml’ file used is shown below: 

Filename: defaults/main.yaml

.fusion-syntax-highlighter-24 > .CodeMirror, .fusion-syntax-highlighter-24 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-24 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-24 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighterglobal_debug: true api_version: "3.0" host_name: "{{ lookup('env','NUTANIX_VM_INVENTORY_HOST')}}" cluster_url: "{{ lookup('env','NUTANIX_PRISM_ADDRESS')}}" api_url_v3: "https://{{ cluster_url }}:9440/api/nutanix/v3" cluster_port: "{{ lookup('env','NUTANIX_PRISM_PORT')}}" prism_user: "{{ lookup('env','NUTANIX_PRISM_USER_NAME')}}" prism_password: "{{ lookup('env','NUTANIX_PRISM_USER_PASSWORD')}}" cluster_name: "{{ lookup('env','NUTANIX_PRISM_CLUSTER_NAME')}}" subnet_name: "{{ lookup('env','NUTANIX_VM_SUBNET_NAME')}}" image_name: "{{ lookup('env','NUTANIX_VM_IMAGE')}}" vm_net_domain: "{{ lookup('env','NUTANIX_VM_DOMAIN')}}" root_disk_size: "{{ lookup('env','NUTANIX_VM_ROOT_DISK_SIZE')}}" data_disk_size: "{{ lookup('env','NUTANIX_VM_DATA_DISK_SIZE')}}" vm_name: "{{ lookup('env','NUTANIX_VM_NAME')}}" vm_ram: "{{ lookup('env','NUTANIX_VM_RAM') }}" vm_num_cpu_per_socket: "{{ lookup('env','NUTANIX_VM_CPU_PER_SOCKET')}}" vm_num_sockets: "{{ lookup('env','NUTANIX_VM_CPU_SOCKET')}}" ansible_user: "{{ lookup('env','NUTANIX_VM_ANSIBLE_USER_NAME')}}" ansible_key: "{{ lookup('env','NUTANIX_VM_ANSIBLE_KEY')}}" ansible_pub: "{{ lookup('env','NUTANIX_VM_ANSIBLE_PUB_KEY')}}" vm_root_password: "{{ lookup('env','NUTANIX_VM_ROOT_PASSWORD')}}" vm_defs: - {vm_name: "{{ vm_name }}",vm_ram: "{{ vm_ram }}", vm_num_cpu_per_socket: "{{ vm_num_cpu_per_socket }}", vm_num_sockets: "{{ vm_num_sockets }}"} ansible_ssh_public_key: "{{ lookup('file', '{{ ansible_pub }}') }}"The templates used for the playbook are placed in the ‘templates’ folder of the role ‘nutanix_vm’. Each of the template files ( which is passed as Jinja2 file/j2 file ) are listed below: 

J2 Filename 1: templates/cloud-init.yml.j2

.fusion-syntax-highlighter-25 > .CodeMirror, .fusion-syntax-highlighter-25 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-25 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-25 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter#cloud-config

Hostname Configuration

hostname: {{ vm_name }}.{{ vm_net_domain }}

Group/User Configuration

groups:

  • cloud-users

users:

  • default
  • name: {{ ansible_user }} groups: [ wheel ] shell: /bin/bash sudo: ['ALL=(ALL) NOPASSWD:ALL'] lock_passwd: false ssh-authorized-keys:
    • {{ ansible_ssh_public_key }}

#sshd Configuration chpasswd: list: | root:{{ vm_root_password }} expire: False ssh_pwauth: False

power_state: delay: now mode: reboot condition: TrueJ2 Filename 2: templates/nutanix.py.j2

.fusion-syntax-highlighter-26 > .CodeMirror, .fusion-syntax-highlighter-26 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-26 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-26 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter#!/usr/bin/env python

import os import sys from time import time import argparse import socket import requests import yaml

try: import json except ImportError: import simplejson as json

class NutanixInventory(object):

def __init__(self):
    ''' Main execution path '''

    requests.packages.urllib3.disable_warnings()
    self.session = requests.Session()

    # Parse CLI arguments

self.parse_cli_args() # Get environment variable nutanix_mode = os.environ.get('NUTANIX_MODE')

    if self.args.host:
        self.nutanix_inventory('host')
    elif self.args.names or bool(nutanix_mode == 'names'):
        self.names = True
        if self.args.refresh_cache:
            self.nutanix_cache('refresh')
        else:
            self.nutanix_cache('cache')
    else:
        self.names = False
        if self.args.refresh_cache:
            self.nutanix_cache('refresh')
        else:
            self.nutanix_cache('cache')

    if self.args.pretty:
        print(json.dumps(self.inventory, sort_keys=True, indent=2))
    else:
        print(json.dumps(self.inventory, sort_keys=True, indent=2))
        #print(self.inventory)

def parse_cli_args(self):
    ''' Command line argument processing '''

    parser = argparse.ArgumentParser(
        description='Produce an Ansible inventory file from Nutanix')

    parser.add_argument('--list', action='store_true', default=True ,
                        help='List instances by IP address (default: True)')
    parser.add_argument('--host', action='store',
                        help='Get all variables about a VM')
    parser.add_argument('--names', action='store_true',
                        help='List instances by VM name')
    parser.add_argument('--pretty', action='store_true',
                        help='Pretty-print results')
    parser.add_argument('--refresh-cache', action='store_true',
                        help='Force refresh of cache by making API requests to Nutanix (default: False - use cache files)')

    self.args = parser.parse_args()

@staticmethod
def get_settings():
    ''' Retrieve settings from nutanix.yml '''

    nutanix_default_yml_path = '/tmp/nutanix.yml'

    nutanix_yml_path = os.path.expanduser(
        os.path.expandvars(
            os.environ.get('NUTANIX_YML_PATH', nutanix_default_yml_path)))

    try:
        with open(nutanix_yml_path) as nutanix_yml_file:

config = yaml.safe_load(nutanix_yml_file) except IOError: print('Could not find nutanix.yml file at {}' .format(nutanix_yml_path)) sys.exit(1)

    return config

def authenticate(self):
    ''' Authenticate to the Nutanix cluster API '''

    payload = {
        'j_username': self.nutanix_username,
        'j_password': self.nutanix_password
    }

    response = self.session.post(
        'https://{}:{}/PrismGateway/j_spring_security_check'
        .format(self.nutanix_address, self.nutanix_port),
        data=payload, verify=self.verify_ssl)

    if response.status_code == 401:
        print("Failed to authenticate to {}.".format(self.nutanix_address))
        sys.exit(1)

@staticmethod
def validate_auth(data):
    ''' Validate API authentication '''

    if 'An Authentication object was not found in the SecurityContext' not in data:
        return True

def get_ahv_list(self):
    ''' Pull from the Nutanix Acropolis API '''

    ahv = self.session.get(
        'https://{}:{}/api/nutanix/v0.8/vms'
        .format(self.nutanix_address, self.nutanix_port),
        verify=self.verify_ssl)

    if not self.validate_auth(ahv.text):
        self.authenticate()
        self.get_ahv_list()
    else:
        self.ahv_list = json.loads(ahv.text)

def get_prism_list(self):
    ''' Pull from the Nutanix Prism API '''

    prism = self.session.get(
        'https://{}:{}/PrismGateway/services/rest/v1/vms'
        .format(self.nutanix_address, self.nutanix_port),
        verify=self.verify_ssl)

    if not self.validate_auth(prism.text):
        self.authenticate()

self.get_prism_list() else: self.prism_list = json.loads(prism.text)

def get_vm_details(self):
    ''' Get details of a VM '''

    vm = self.session.get(
        'https://{}:{}/PrismGateway/services/rest/v1/vms/{}'
        .format(self.nutanix_address, self.nutanix_port, self.vm_uuid),
        verify=self.verify_ssl)

    if not self.validate_auth(vm.text):
        self.authenticate()
        self.get_vm_details()
    else:
        self.vm_details = json.loads(vm.text)

def get_host_info(self):
    ''' Return information about a VM '''

    # Needed for VM IP and name
    self.get_prism_list()

    host = self.args.host

    # Check if --host arg is IP or VM name
    try:
        socket.inet_aton(host)
        host_by_ip = True
    except socket.error:
        host_by_ip = False

    # Find host by IP
    if host_by_ip:
        for entity in self.prism_list['entities']:
            for address in entity['ipAddresses']:
                if address == host:
                    self.vm_uuid = entity['uuid']
                    self.get_vm_details()
                    self.inventory.update(self.vm_details)
                    break
            else:
                continue
            break
    # Find host by VM name
    else:
        for entity in self.prism_list['entities']:
            if entity['vmName'].lower() == host.lower():
                self.vm_uuid = entity['uuid']
                self.get_vm_details()
                self.inventory.update(self.vm_details)
                break

    return self.inventory

def get_cluster_info(self): ''' Get details of a cluster '''

    cluster = self.session.get(
        'https://{}:{}/PrismGateway/services/rest/v1/cluster'
        .format(self.nutanix_address, self.nutanix_port),
        verify=self.verify_ssl)

    if not self.validate_auth(cluster.text):
        self.authenticate()
        self.get_cluster_info()
    else:
        self.cluster_info = json.loads(cluster.text)

def build_cluster_inventory(self):
    ''' Generate inventory per cluster '''

    # Needed for VM IPs
    self.get_prism_list()
    # Needed for VM descriptions
    self.get_ahv_list()
    # Needed to get the cluster name
    self.get_cluster_info()

    for prism_entity in self.prism_list['entities']:
        for ahv_entity in self.ahv_list['entities']:
            if prism_entity['uuid'] == ahv_entity['uuid']:
                # Only build inventory by VM name, or by IP when IPs present
                if (self.names is True) or (self.names is False and prism_entity['ipAddresses']):
                    if '_meta' not in self.inventory:
                        self.inventory['_meta'] = {'hostvars': {}}
                    if self.cluster_info['name'].lower() not in self.inventory:
                        self.inventory[self.cluster_info['name'].lower()] = {'hosts': []}
                    if self.names:
                        # Only add unique VM names to inventory
                        if prism_entity['vmName'].lower() not in self.inventory['_meta']['hostvars']:
                            self.inventory['_meta']['hostvars'][prism_entity['vmName'].lower()] = {}
                            self.inventory['_meta']['hostvars'][prism_entity['vmName'].lower()].update(prism_entity)
                            self.inventory[self.cluster_info['name'].lower()]['hosts'].append(prism_entity['vmName'].lower())
                        # Fail otherwise
                        else:
                            print('{} exists more than once. Stopping.'.format(prism_entity['vmName'].lower()))
                            sys.exit(1)
                        # Group VMs by power state
                        if ('powerstate_{}'.format(prism_entity['powerState'])) not in self.inventory:
                            self.inventory['powerstate_{}'.format(prism_entity['powerState'])] = {'hosts': []}
                        self.inventory['powerstate_{}'.format(prism_entity['powerState'])]['hosts'].append(prism_entity['vmName'].lower())
                    else:
                        for address in prism_entity['ipAddresses']:
                            # Only add unique IPs to inventory
                            if address not in self.inventory['_meta']['hostvars']:
                                self.inventory['_meta']['hostvars'][address] = {}
                                self.inventory['_meta']['hostvars'][address].update(prism_entity)
                                self.inventory[self.cluster_info['name'].lower()]['hosts'].append(address)
                            # Fail otherwise

else: print('{} exists more than once. Stopping'.format(address)) sys.exit(1) # Look for VMs with a description field if 'description' in ahv_entity['config']: # Look for non-empty descriptions if ahv_entity['config']['description']: # Look for a JSON object in the description try: # Strip outer quotes, if present description = json.loads(ahv_entity['config']['description'].strip('"')) # Register any specified groups if 'groups' in description: for group in description['groups']: if group.lower() not in self.inventory: self.inventory[group.lower()] = {'hosts': []} if self.names: self.inventory[group.lower()]['hosts'].append(prism_entity['vmName'].lower()) else: for address in prism_entity['ipAddresses']: self.inventory[group.lower()]['hosts'].append(address) # Register any specified hostvars if 'hostvars' in description: if '_meta' not in self.inventory: self.inventory['_meta'] = {'hostvars': {}} if self.names: for var in description['hostvars']: self.inventory['_meta']['hostvars'][prism_entity['vmName'].lower()].update({var: description['hostvars'][var]}) else: for var in description['hostvars']: self.inventory['_meta']['hostvars'][address].update({var: description['hostvars'][var]}) # Ignore VMs whose description is not in JSON except ValueError: pass

    return self.inventory

def nutanix_inventory(self, inventory_type):
    ''' Generate inventory from one or more configured clusters '''

    config = self.get_settings()

    self.inventory = {}

    try:
        cluster_list = config.get('clusters')

        for cluster in cluster_list:
            cluster_details = cluster_list.get(cluster)
            # Get cluster address
            try:
                self.nutanix_address = cluster_details['address']
            except KeyError:
                print('An address must be configured for cluster {}.'
                      .format(cluster))

sys.exit(1) # API port defaults to 9440 unless specified otherwise if 'port' in cluster_details: self.nutanix_port = cluster_details['port'] else: self.nutanix_port = 9440 # Get cluster username try: self.nutanix_username = cluster_details['username'] except KeyError: print('A username must be configured for cluster {}.' .format(cluster)) sys.exit(1) # Get cluster password try: self.nutanix_password = cluster_details['password'] except KeyError: print('A password must be configured for cluster {}.' .format(cluster)) sys.exit(1) # SSL verification defaults to True unless specified otherwise if 'verify_ssl' in cluster_details: self.verify_ssl = cluster_details['verify_ssl'] else: self.verify_ssl = True

            if inventory_type == 'all':
                self.inventory.update(self.build_cluster_inventory())
            elif inventory_type == 'host':
                self.inventory.update(self.get_host_info())

        return self.inventory

    except TypeError:
        print('No cluster found in the nutanix.yml configuration file.')
        sys.exit(1)

def validate_cache(self, filename):
    ''' Determines whether cache file has expired '''

    if os.path.isfile(filename):
        mod_time = os.path.getmtime(filename)
        current_time = time()
        if (mod_time + int(self.cache_max_age)) > current_time:
            return True
    return False

@staticmethod
def write_to_cache(data, filename):
    ''' Writes inventory data to a file '''

    cache = open(filename, 'w')
    cache.write(json.dumps(data))
    cache.close()

def load_from_cache(self, filename):
''' Reads inventory data from cache file '''

    cache = open(filename, 'r')
    json_data = cache.read()
    cache.close()
    self.inventory = json.loads(json_data)

def nutanix_cache(self, status):
    ''' Retrieve or refresh cache '''

    config = self.get_settings()

    try:
        cache_settings = config.get('caching')

        try:
            self.cache_max_age = cache_settings['cache_max_age']
        except KeyError:
            print('A caching time must be set, even if set to 0.')
            sys.exit(1)
        try:
            self.cache_path = cache_settings['cache_path']
        except KeyError:
            print('A path must be set for cached inventory files.')
            sys.exit(1)
        try:
            self.cache_base_name = cache_settings['cache_base_name']
        except KeyError:
            print('A base name must be set for cached inventory files.')
            sys.exit(1)

    except TypeError:
        print('Could not load caching settings from nutanix.yml.')
        sys.exit(1)

    if self.names:
        file_extension = '-names'
    else:
        file_extension = '-list'

    # Pull from cache or update cache if expired
    if status == 'cache':
        if self.validate_cache(self.cache_path
                               + self.cache_base_name
                               + file_extension):
            self.load_from_cache(self.cache_path
                                 + self.cache_base_name
                                 + file_extension)
        else:
            self.nutanix_inventory('all')
            try:
                self.write_to_cache(self.inventory, self.cache_path
                                    + self.cache_base_name
                                    + file_extension)
            except IOError as error:
                print(error)

sys.exit(1) self.nutanix_cache('cache') # Force refresh elif status == 'refresh': self.nutanix_inventory('all') try: self.write_to_cache(self.inventory, self.cache_path + self.cache_base_name + file_extension) except IOError as error: print(error) sys.exit(1) self.nutanix_cache('cache')

NutanixInventory()Given below is the sample template we used to create the VM:

J2 Filename 3: templates/nutanix.yml.j2

.fusion-syntax-highlighter-27 > .CodeMirror, .fusion-syntax-highlighter-27 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-27 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-27 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter# Cluster settings clusters: "{{ cluster_name }}": address: "{{ cluster_url }}" port: "{{ cluster_port }}" username: "{{ prism_user }}" password: "{{ prism_password }}" verify_ssl: False

Global caching settings

caching: cache_max_age: 300 cache_path: '/tmp/' cache_base_name: 'ansible-nutanix.cache'J2 Filename 3: templates/vm-body.yml.j2

.fusion-syntax-highlighter-28 > .CodeMirror, .fusion-syntax-highlighter-28 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-28 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-28 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighterapi_version: '3.0' metadata: kind: vm spec: cluster_reference: kind: cluster uuid: {{ cluster_uuid }} name: {{ vm.vm_name }} resources: disk_list: - data_source_reference: kind: image uuid: {{ image_uuid }} device_properties: device_type: DISK disk_address: adapter_type: SCSI device_index: 0 - disk_size_mib: {{ root_disk_size }} - disk_size_mib: {{ data_disk_size }} memory_size_mib: {{ vm.vm_ram }} nic_list: - nic_type: NORMAL_NIC subnet_reference: kind: subnet uuid: {{ subnet_uuid }} num_sockets: {{ vm.vm_num_sockets }} num_vcpus_per_socket: {{ vm.vm_num_cpu_per_socket }} power_state: 'ON' guest_customization: cloud_init: user_data: {{ lookup('template','cloud-init.yml.j2') | b64encode }}Once the above setup and configuration are done, the following tasks (by default, tasks are available in the tasks folder) are executed by Ansible one by one. The ‘tasks’ for the role ‘nutanix_vm’ are explained below:

TASK 1:  LOG IN TO NUTANIX PRISM VIA SESSION COOKIE

This step shows how to log in to the Prism by creating a session cookie. A session cookie is used to store information in temporary memory and is deleted after the login is completed. This makes the authentication process more secure. The following task is executed by Ansible to log in to the Prism using a session cookie. 

Filename**: tasks/session_cookie.yml* ***

.fusion-syntax-highlighter-29 > .CodeMirror, .fusion-syntax-highlighter-29 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-29 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-29 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Auth to the cluster uri: url: "{{ api_url_v3 }}/clusters/list" body: kind: cluster sort_order: ASCENDING offset: 0 length: 10 sort_attribute: '' method: POST validate_certs: no force_basic_auth: yes body_format: json user: "{{ prism_user }}" password: "{{ prism_password }}" status_code: 200 return_content: yes register: login ignore_errors: yes

  • name: Debug | Print Auth Cookie debug: var: login.set_cookie when: global_debug

  • name: Set fact for session cookie set_fact: session_cookie: “{{ login.set_cookie}}” register: cookie

  • debug: var=cookie.failed### **TASK 2:  FIND THE NUTANIX CLUSTER LISTS AND UNIQUE IDENTIFIERS  ** The following task shows how to get the list of clusters and their unique identifiers (UUIDs). It also shows you how to find the required cluster name and the corresponding UUID for creating the VM.

Filename: ***tasks/cluster_uuid.yml ***

.fusion-syntax-highlighter-30 > .CodeMirror, .fusion-syntax-highlighter-30 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-30 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-30 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Get clusters list uri: url: "{{ api_url_v3 }}/clusters/list" body: kind: cluster sort_order: ASCENDING offset: 0 length: 10 sort_attribute: '' method: POST validate_certs: no body_format: json status_code: 200 headers: Cookie: “{{ session_cookie }}” register: json_cluster_result ignore_errors: yes

  • name: Stash the Cluster UUIDs set_fact: cluster_uuids: “{{ cluster_uuids|default([]) + [{‘name’: item.spec.name, ‘uuid’: item.metada.uuid} ] }}”

with_items: “{{ json_clusters_result.json.entities }}” no_log: True

  • name: Debug|Printcluster name/UUIDs debug: var: cluster_uuids when: global_debug### TASK 3:  FIND THE NUTANIX SUBNET LISTS AND UNIQUE IDENTIFIERS This task shows how to get the list of subnets and their unique identifiers (UUIDs). It also shows you how to find the required subnet name and the corresponding UUID for creating the VM.

Filename**:** ***tasks/subnet_uuid.yml ***

.fusion-syntax-highlighter-31 > .CodeMirror, .fusion-syntax-highlighter-31 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-31 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-31 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Get Subnet List uri: url: "{{ api_url_v3}}/subnets/list" body: length: 100 offset: 0 filter: "" method: POST validate_certs: no body_format: json status_code: 200 headers: Cookie: “{{ session_cookie }}” register: json_images_result ignore_errors: yes

  • debug: var=json_images_result.json.entities.0.status

  • debug: var=json_images_result.json.entities.1.status

  • debug: var=json_images_result.json.entities.2.status

  • name: Stash the subnet UUIDs set_fact: subnet_uuids: "{{ subnet_uuids|default([]) + [ {'name': item.spec.name, 'uuid': item.metadata.uuid } ] }}" with_items: "{{ json_images_result.json.entities }}" no_log: True

  • name: Debug | Print Subnet name/UUIDs debug: var: subnet_uuids when: global_debug### TASK 4:  FIND THE NUTANIX OS IMAGE FOR THE VM CREATION This task shows how to get the list of OS images and their unique identifiers (UUIDs). It also shows you how to find the required image name and the corresponding UUID for creating the VM.

Filename: tasks/image_uuid.yml

.fusion-syntax-highlighter-32 > .CodeMirror, .fusion-syntax-highlighter-32 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-32 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-32 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Get Images list uri: url: "{{ api_url_v3 }}/images/list" body: length: 100 offset: 0 filter: "" method: POST validate_certs: no body_format: json status_code: 200 headers: Cookie: "{{ session_cookie }}" register: json_images_result ignore_errors: yes

  • name: Stash the image UUIDs set_fact: image_uuids: "{{ image_uuids | default([]) + [ {'name': item.spec.name, 'uuid': item.metadata.uuid } ] }}" with_items: "{{ json_images_result.json.entities }}" no_log: True

  • name: Debug | Print image name/UUIDs debug: var: image_uuids when: global_debug### TASK 5: CONFIGURE THE CLUSTER, IMAGE, AND SUBNET UNIQUE IDENTIFIERS This step shows you how to configure and use the universally unique identifiers (UUIDs) of the cluster, image, and subnet found from the previous steps for the VM creation.

Filename: tasks/configure_uuids.yml*** ***

.fusion-syntax-highlighter-33 > .CodeMirror, .fusion-syntax-highlighter-33 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-33 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-33 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Define Cluster UUID to use from name set_fact: cluster_uuid: "{{ item['uuid'] }}" when: "item['name'] == cluster_name" loop: "{{ cluster_uuids }}" register: cluster_uuid - debug: var=cluster_uuid

  • name: Define Image UUID to use from name set_fact: image_uuid: "{{ item['uuid'] }}" when: "item['name'] == image_name" loop: "{{ image_uuids }}" register: image_uuid

  • debug: var= image_uuid

  • name: Define Subnet UUID to use from name set_fact: subnet_uuid: "{{ item['uuid'] }}" when: "item['name'] == subnet_name" loop: "{{ subnet_uuids }}" register: subnet_uuid

  • debug: var= subnet_uuid### TASK 6: PROVISION NUTANIX VM WITH CENTOS IMAGE This task shows how to provision a new VM (uses CentOS image here) in the Nutanix cluster. You can do this by running the python script ‘nutanix.py’ (referred to above in the templates section), after providing the configuration parameters and image template as defined above.

 

*‘nutanix.py’ *is a Python script that generates a dynamic inventory that Ansible can understand by making API requests to related Nutanix clusters. To get a better idea about this, please refer: h**ttps://github.com/mattkeeler/ansible-nutanix-inventory

Filename: tasks/provision_vm.yml*** ***

.fusion-syntax-highlighter-34 > .CodeMirror, .fusion-syntax-highlighter-34 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-34 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-34 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- name: Copy python with owner and permissions template: src: nutanix.py.j2 dest: /tmp/nutanix.py owner: root group: root mode: u=rwx,g=rx,o=r

  • name: Copy yaml with owner and permissions template: src: nutanix.yml.j2 dest: /tmp/nutanix.yml owner: root group: root mode: u=rwx,g=rx,o=r

  • name: run python script shell: /tmp/nutanix.py --host {{ vm_name }} register: output

  • debug: var=output.stdout

  • name: Debug | Print VM definitions debug: var: vm_defs when: global_debug

  • name: Create fact with VM template contents set_fact: vm_body: "{{ lookup('template', 'vm-body.yml.j2') | from_yaml }}" loop: "{{ vm_defs }}"
    register: templates loop_control: loop_var: vm

  • name: Debug | Print Template lookup result debug: msg: "{{ item.ansible_facts.vm_body }}" when: global_debug with_items: "{{ templates.results }}"

  • name: Create a VM from a template uri: url: "{{ api_url_v3 }}/vms" body: "{{ template.ansible_facts.vm_body }}" method: POST validate_certs: no body_format: json headers: Cookie: "{{ session_cookie }}" status_code: 202 register: json_create_result with_items: "{{ templates.results }}" loop_control: loop_var: template when: 'output.stdout_lines == [ "{}" ]'

  • name: Debug | Print VM creation result debug: msg="{{json_create_result}}"

  • name: Debug | Display VM creation response debug: msg: "{{ item.json.metadata.uuid }}" when: 'output.stdout_lines == [ "{}" ]' with_items: "{{ json_create_result.results }}"

  • name: Register the created vm uuid's for future use set_fact: _uuids: "{{ item.json.metadata.uuid }}" with_items: "{{ json_create_result.results }}" register: vm_uuids when: 'output.stdout_lines == [ "{}" ]'
    no_log: True

  • name: Debug | Print VM uuids debug: var: vm_uuids when: 'output.stdout_lines == [ "{}" ]' ### TASK 7: MAIN TASK TO RUN OTHER TASKS AND CREATE CENTOS VM

This is the main task, which will initiate and execute other tasks defined in the tasks folder (as explained above) to create the CentOS VM.

Filename: tasks/main.yaml

.fusion-syntax-highlighter-35 > .CodeMirror, .fusion-syntax-highlighter-35 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-35 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-35 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter# tasks file for nutanix_provisioner

  • import_tasks: get_session_cookie.yml
  • import_tasks: get_cluster_uuids.yml
  • import_tasks: get_subnet_uuids.yml
  • import_tasks: get_image_uuids.yml
  • import_tasks: define_uuids_from_names.yml
  • import_tasks: provision_vm.yml
  • import_tasks: inventory.yml ### HOW TO CALL THE ROLE This step shows how to execute the role ‘nutanix_vm’. Create a yaml file ‘call.yaml’ in the location where the ‘nutanix_vm’ role is present. Then, add the code given below in the yaml file.

.fusion-syntax-highlighter-36 > .CodeMirror, .fusion-syntax-highlighter-36 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-36 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-36 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighter- become: true hosts: localhost name: playbook for running nutanix_vm tasks:

  • include_role: name: nutanix_vm name: task for nutanix_vm Execute the role by running the command given below:

.fusion-syntax-highlighter-37 > .CodeMirror, .fusion-syntax-highlighter-37 > .CodeMirror .CodeMirror-gutters {background-color:var(--awb-color1);}.fusion-syntax-highlighter-37 > .CodeMirror .CodeMirror-gutters { background-color: var(--awb-color2); }.fusion-syntax-highlighter-37 > .CodeMirror .CodeMirror-linenumber { color: var(--awb-color8); }Copy to ClipboardSyntax Highlighteransible-playbook call.yamlWhen the CentOS creation is completed, the IP address and name of the new CentOS will be printed in the terminal as follows:

CONCLUSION

Ansible is an efficient and relatively simple tool for automating infrastructure (IaC – Infrastructure as Code). In this tutorial, I used the Ansible role ‘**nutanix_vm_provisioner’ *to create a CentOS VM on the Nutanix cluster. Ansible can also help with tasks like app deployments, configuration management, and workflow orchestration.

REFERENCES

About The Author### Sreedevi J SCloud Dev-Ops Engineer | Cloud Control*

Cloud DevOps Engineer with more than three years of experience supporting, automating and optimizing deployments to hybrid cloud platforms using DevOps processes, tools, CI/CD, containers, and Kubernetes in both Production and Development environments.

Nutanix- * On Linked-In