Browse Source

Ansible role for creating VM(s) on Nutanix

cybergavin 4 years ago
parent
commit
c4dc543ac5

+ 62 - 0
cg_nutanix_vm_create/README.md

@@ -0,0 +1,62 @@
+Role Name - cg_nutanix_vm_create
+=================================
+
+This ansible role enables the creation of one or more VMs on a single Nutanix cluster.
+
+Requirements
+------------
+
+-- A user-provided tilde-separated-values file containing the specifications of one or more VMs. Refer the sample files/vm.tsv in the role dircetory.
+-- A user-provided variables file containing appropriate values for required variables. Refer the defaults/main.yml in the role directory.
+
+Role Variables
+--------------
+
+-- pc_cluster_fqdn   # FQDN/IP for Prism Central
+-- pe_cluster_fqdn   # FQDN/IP for Prism Element
+-- prism_user        # An account with permissions to provision on the cluster
+-- prism_password    # The account's password. Do NOT store this in the clear. Use ansible vault or an external vault. 
+-- cluster_name      # Name of the nutanix cluster on which VMs will be created 
+-- vm_data_tsv       # Relative path and name of the tilde-separated-values file containing details of the VMs to be deployed
+-- global_debug      # Global Debug flag
+
+
+Dependencies
+------------
+
+Although this role uses the v1 API on Prism Element to mount and enable Nutanix Guest Tools on the created VM(s), the NGT status
+will be displayed as "Latest" only after the VM(s) reboot or the "Nutanix Guest Agent" service on the VM(s) is restarted.
+
+Example Playbook
+----------------
+
+Create a vars/main.yml with the required variables (above) and a files/vm.tsv before
+using the following playbook.
+
+---
+- name: Create VMs on Nutanix AHV
+  hosts: localhost
+  gather_facts: false
+  tasks:
+    - name: Include variables
+      include_vars: dir=vars
+    - name: Use ansible role cg_nutanix_vm_create
+      include_role:
+        name: cg_nutanix_vm_create
+
+
+License
+-------
+
+GPLv3.0
+
+Author Information
+------------------
+
+Cybergavin - https://cybergav.in - https://github.com/cybergav.in
+
+
+References
+-----------
+
+Influenced/Inspired by the Ansible role mbach04.nutanix_vm_provisioner at https://github.com/mbach04/nutanix_vm_provisioner.

+ 20 - 0
cg_nutanix_vm_create/defaults/main.yml

@@ -0,0 +1,20 @@
+---
+# defaults file for cg_nutanix_vm_create
+# These variables must be defined with their appropriate values in a 
+# variables filed used by the calling playbook.
+#
+# Nutanix Platform
+pc_cluster_fqdn: "prismcentral.contoso.com"   # FQDN/IP for Prism Central
+pe_cluster_fqdn: "prismelement.contoso.com"   # FQDN/IP for Prism Element
+prism_user: "ntxclusteradmin@contoso.com"     # An account with permissions to provision on the cluster
+prism_password: "nutanix"                     # The account's password. Do NOT store this in the clear. Use ansible vault or an external vault.
+cluster_name: "mynutanix"                     # Name of the nutanix cluster on which VMs will be created
+
+pc_api_url: "https://{{ pc_cluster_fqdn }}:9440/api/nutanix/v3"
+pe_api_url: "https://{{ pe_cluster_fqdn }}:9440/PrismGateway/services/rest/v1"
+
+# User-provided VM specifications
+vm_data_tsv: "files/vm.tsv"                   # Relative path and name of the tilde-separated-values file containing details of the VMs to be deployed
+
+# Debug 
+global_debug: false                           # Global Debug flag

+ 23 - 0
cg_nutanix_vm_create/files/vm.tsv

@@ -0,0 +1,23 @@
+# This is a sample data file that is to be used along with the cg_nutanix_vm_create
+# ansible role for provisioning VMs on a Nutanix cluster. Such a file should be created
+# with the proper values and called by your main playbook.
+# This is a tilde-separated-values file containing the list of VMs to be created.
+# In the example below:
+#  vm_name       => VM's Name
+#  ip            => VM's IP address
+#  subnet_name   => VM's subnet
+#  image_name    => Name of the disk image used to create the VM
+#  num_vcpu      => Number of vCPU sockets for the VM
+#  memory_mib    => VM's memory size in MiB
+#  disk_list_mib => Additional VM disk sizes in MiB (comma-separated)
+#
+#  NOTE: All attributes are mandatory, except disk_list_mib. If you do not wish
+#  to add disks to the VM, keep the placeholder empty (~~)
+
+# Example 1: A VM with 2 vCPUs/4 GB RAM and 2 added disks
+vm_name~ip~subnet_name~image_name~num_vcpu~memory_mib~disk_list_mib
+testvm~10.1.1.10~webnet~rhel8-image~2~4096~10240,20480
+
+# Example 2: A VM with 2 vCPUs/4 GB RAM and no added disks
+vm_name~ip~subnet_name~image_name~num_vcpu~memory_mib~disk_list_mib
+testvm~10.1.1.10~webnet~rhel8-image~2~4096~~

+ 53 - 0
cg_nutanix_vm_create/meta/main.yml

@@ -0,0 +1,53 @@
+galaxy_info:
+  author: Cybergavin (https://github.com/cybergavin)
+  description: An Ansible role for creating Nutanix VMs on a single cluster
+  company: 
+
+  # If the issue tracker for your role is not on github, uncomment the
+  # next line and provide a value
+  # issue_tracker_url: http://example.com/issue/tracker
+
+  # Choose a valid license ID from https://spdx.org - some suggested licenses:
+  # - BSD-3-Clause (default)
+  # - MIT
+  # - GPL-2.0-or-later
+  # - GPL-3.0-only
+  # - Apache-2.0
+  # - CC-BY-4.0
+  license: GPL-3.0-only
+
+  min_ansible_version: 2.5
+
+  # If this a Container Enabled role, provide the minimum Ansible Container version.
+  # min_ansible_container_version:
+
+  #
+  # Provide a list of supported platforms, and for each platform a list of versions.
+  # If you don't wish to enumerate all versions for a particular platform, use 'all'.
+  # To view available platforms and versions (or releases), visit:
+  # https://galaxy.ansible.com/api/v1/platforms/
+  #
+  # platforms:
+  # - name: Fedora
+  #   versions:
+  #   - all
+  #   - 25
+  # - name: SomePlatform
+  #   versions:
+  #   - all
+  #   - 1.0
+  #   - 7
+  #   - 99.99
+
+  galaxy_tags: [nutanix, AHV, cloud]
+    # List tags for your role here, one per line. A tag is a keyword that describes
+    # and categorizes the role. Users find roles by searching for tags. Be sure to
+    # remove the '[]' above, if you add tags to this list.
+    #
+    # NOTE: A tag is limited to a single word comprised of alphanumeric characters.
+    #       Maximum 20 tags per role.
+
+dependencies: []
+  # List your role dependencies here, one per line. Be sure to remove the '[]' above,
+  # if you add dependencies to this list.
+  

+ 47 - 0
cg_nutanix_vm_create/tasks/create_vm.yml

@@ -0,0 +1,47 @@
+---
+# Create VM(s) on Nutanix
+
+- name: Store VM template contents
+  set_fact: 
+    vm_body: "{{ lookup('template', 'vm-body.yml.j2') | from_yaml }}"
+  loop: "{{ ntx_vm_defs }}"
+  register: templates
+  loop_control:
+    loop_var: vm
+
+- name: Debug | Print Template for VM(s)
+  debug:
+    msg: "{{ item.ansible_facts.vm_body }}"
+  when: global_debug|bool
+  with_items: "{{ templates.results }}"
+
+- name: Create VM(s) from template
+  uri:
+    url: "{{ pc_api_url }}/vms"
+    body:
+      "{{ template.ansible_facts.vm_body }}"
+    method: POST
+    validate_certs: no
+    body_format: json
+    headers:
+      Cookie: "{{ pc_session_cookie }}"
+    status_code: 202
+  register: json_create_result
+  with_items: "{{ templates.results }}"
+  loop_control:
+    loop_var: template
+
+- name: Debug | Print VM(s) creation result
+  debug:
+    msg: "VM(s) created : {{ json_create_result }}"
+  when: global_debug|bool
+
+- name: Store created VM(s) UUID(s)
+  set_fact:
+    vm_uuids: "{{ vm_uuids|default([]) + [ item.json.metadata.uuid  ] }}"
+  with_items: "{{ json_create_result.results }}"
+  
+- name: Debug | Print VM(s) UUID(s)
+  debug:
+    msg: "Created VM(s) UUID(s) : {{ vm_uuids }}"
+  when: global_debug|bool

+ 52 - 0
cg_nutanix_vm_create/tasks/get_session_cookie.yml

@@ -0,0 +1,52 @@
+---
+# Authenticate with Prism Central and Prism Element and store session cookies.
+- name: Authenticate to Prism Central 
+  uri:
+    url: "{{ pc_api_url }}/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: pc_login
+  ignore_errors: yes
+
+- name: Store session cookie for Prism Central
+  set_fact:
+    pc_session_cookie: "{{ pc_login.set_cookie }}"
+
+- name: Debug | Print session cookie for Prism Central
+  debug:
+    msg: "Session cookie for Prism Central is {{ pc_session_cookie }}"
+  when: global_debug|bool
+
+- name: Authenticate to Prism Element 
+  uri:
+    url: "{{ pe_api_url }}/clusters"
+    method: GET 
+    validate_certs: no
+    force_basic_auth: yes
+    user: "{{ prism_user }}"
+    password: "{{ prism_password }}"
+    status_code: 200
+    return_content: yes
+  register: pe_login
+  ignore_errors: yes
+
+- name: Store session cookie for Prism Element
+  set_fact:
+    pe_session_cookie: "{{ pe_login.set_cookie }}"
+
+- name: Debug | Print session cookie for Prism Element
+  debug:
+    msg: "Session cookie for Prism Element is {{ pe_session_cookie }}"
+  when: global_debug|bool

+ 22 - 0
cg_nutanix_vm_create/tasks/get_user_vm_defs.yml

@@ -0,0 +1,22 @@
+---
+# Parse user-provided VM specifications and store them in a list of dictionaries,
+# with each VM's specifications stored in a dictionary.
+- name: Read user-provided VM specs from {{ vm_data_tsv }}
+  shell: /usr/bin/awk -F'~' '!/^#/ && !/^$/ && NR!=1 {print $0}' {{ vm_data_tsv }}
+  register: tsvout
+- name: Load VM specs from tsv into the list user_vm_defs 
+  set_fact:
+    user_vm_defs: "{{ user_vm_defs|default([]) + [ { \
+                     'vm_name' : item.split('~').0, \
+                     'vm_ip' : item.split('~').1, \
+                     'vm_subnet_name' : item.split('~').2, \
+                     'vm_image_name' : item.split('~').3, \
+                     'vm_num_sockets' : item.split('~').4, \
+                     'vm_memory' : item.split('~').5, \
+                     'vm_disk_list' : item.split('~').6} ] }}"
+  with_items: "{{ tsvout.stdout_lines }}"
+
+- name: Debug | User-provided VM Specifications
+  debug:
+    msg: "User-provided VM specifications: {{ user_vm_defs }}"
+  when: global_debug|bool

+ 36 - 0
cg_nutanix_vm_create/tasks/list_cluster_uuids.yml

@@ -0,0 +1,36 @@
+---
+# Calls Prism Central to obtain a list of clusters
+# Returns a list of dictionaries containing the name and uuid of the clusters
+# Example output:
+# [
+#   {'name': 'ntx-web', 'uuid': '00053e3-0255-d94a-457a-ecf4bdfgfb8g0'},
+#   {'name': 'ntx-app', 'uuid': '00522e15-0234-d94a-457a-ecf4bbdfb8g1'}
+# ]
+  
+- name: Get clusters list
+  uri:
+    url: "{{ pc_api_url }}/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: "{{ pc_session_cookie }}"
+  register: json_clusters_result
+  ignore_errors: yes
+
+- name: Store the cluster names/UUIDs
+  set_fact:
+    cluster_uuids: "{{ cluster_uuids|default([]) + [ {'name': item.spec.name, 'uuid': item.metadata.uuid } ] }}"
+  with_items: "{{ json_clusters_result.json.entities }}"
+
+- name: Debug | Print cluster name/UUIDs
+  debug:
+    msg: "Cluster names/uuids : {{ cluster_uuids }}"
+  when: global_debug|bool

+ 34 - 0
cg_nutanix_vm_create/tasks/list_image_uuids.yml

@@ -0,0 +1,34 @@
+---
+# Calls Prism Central to obtain a list of images
+# Returns a list of dictionaries containing the name and uuid of the images
+# Example output:
+# [
+#   {'name': 'imageA', 'uuid': '00056e15-0223-d74a-497a-ecf4bbd9b8f0'},
+#   {'name': 'imageB', 'uuid': '00056e15-0223-d74a-497a-ecf4bbd9b8f1'}
+# ]
+
+- name: Get Images list
+  uri:
+    url: "{{ pc_api_url }}/images/list"
+    body:
+      length: 100
+      offset: 0
+      filter: ""
+    method: POST
+    validate_certs: no
+    body_format: json
+    status_code: 200
+    headers: 
+      Cookie: "{{ pc_session_cookie }}"
+  register: json_images_result
+  ignore_errors: yes
+
+- name: Store the image names/UUIDs
+  set_fact:
+    image_uuids: "{{ image_uuids | default([]) + [ {'name': item.spec.name, 'uuid': item.metadata.uuid } ] }}"
+  with_items: "{{ json_images_result.json.entities }}"
+
+- name: Debug | Print image names/UUIDs
+  debug:
+    msg: "Image names/uuids are: {{ image_uuids }}"
+  when: global_debug|bool

+ 34 - 0
cg_nutanix_vm_create/tasks/list_subnet_uuids.yml

@@ -0,0 +1,34 @@
+---
+# Calls Prism Central to obtain a list of networks/subnets
+# Returns a list of dictionaries containing the name and uuid of the subnets 
+# Example output:
+# [
+#   {'name': 'subnetA', 'uuid': '00056e15-0223-d74a-497a-ecf4bbd9b8f0'},
+#   {'name': 'subnetB', 'uuid': '00056e15-0223-d74a-497a-ecf4bbd9b8f1'}
+# ]
+
+- name: Get Subnet List
+  uri:
+    url: "{{ pc_api_url }}/subnets/list"
+    body:
+      length: 100
+      offset: 0
+      filter: ""
+    method: POST
+    validate_certs: no
+    body_format: json
+    status_code: 200
+    headers: 
+      Cookie: "{{ pc_session_cookie }}"
+  register: json_images_result
+  ignore_errors: yes
+
+- name: Store the subnet names/UUIDs
+  set_fact:
+    subnet_uuids: "{{ subnet_uuids|default([]) + [ {'name': item.spec.name, 'uuid': item.metadata.uuid } ] }}"
+  with_items: "{{ json_images_result.json.entities }}"
+
+- name: Debug | Print Subnet names/UUIDs
+  debug:
+    msg: "Subnet names/uuids are: {{ subnet_uuids }}"
+  when: global_debug|bool

+ 26 - 0
cg_nutanix_vm_create/tasks/main.yml

@@ -0,0 +1,26 @@
+---
+# Main tasks file
+##########################################
+# Parse user-provided VM(s) specifications
+- import_tasks: get_user_vm_defs.yml
+
+# Authenticate against Nutanix Prism Central/Element and store session cookies
+- import_tasks: get_session_cookie.yml
+
+# Obtain a list of Nutanix cluster names and UUIDs
+- import_tasks: list_cluster_uuids.yml
+
+# Obtain a list of image names and UUIDs
+- import_tasks: list_image_uuids.yml
+
+# Obtain a list of network/subnet names and UUIDs
+- import_tasks: list_subnet_uuids.yml
+
+# Update VM specifications with Nutanix UUIDs
+- import_tasks: update_vm_defs.yml 
+
+# Create VM(s)
+- import_tasks: create_vm.yml
+
+# Update Nutanix Guest Tools on VM(s)
+- import_tasks: update_ngt.yml

+ 6 - 0
cg_nutanix_vm_create/tasks/update_image_uuids.yml

@@ -0,0 +1,6 @@
+---
+# Update VM specifications with image UUIDs
+- set_fact:
+    vm_defs_2: "{{ vm_defs_2 |default([])  + [my_vm | combine({ 'vm_image_uuid' : item. uuid  })] }}"
+  when: my_vm.vm_image_name == item.name
+  with_items: "{{ image_uuids }}"

+ 47 - 0
cg_nutanix_vm_create/tasks/update_ngt.yml

@@ -0,0 +1,47 @@
+---
+# Calls Prism Element (functionality unavailable in Prism Central) to mount
+# and enable Nutanix Guest Tools for created VM(s).
+- name: Pause to allow Nutanix to create VM(s) before mounting and enabling Nutanix Guest Tools
+  pause:
+    seconds: 30
+- name: Mount Nutanix Guest Tools
+  uri:
+    url: "{{ pe_api_url }}/vms/{{ item }}/guest_tools/mount"
+    body:
+      length: 100
+      offset: 0
+      filter: ""
+    method: POST
+    validate_certs: no
+    body_format: json
+    status_code: 200
+    headers: 
+      Cookie: "{{ pe_session_cookie }}"
+  register: json_mount_ngt_result
+  ignore_errors: yes
+  with_items: "{{ vm_uuids }}"
+
+- name: Debug | Mount NGT 
+  debug:
+    msg: "Result of mounting NGT : {{ json_mount_ngt_result }}" 
+  when: global_debug|bool
+
+- name: Enable Nutanix Guest Tools
+  uri:
+    url: "{{ pe_api_url }}/vms/{{ item }}/guest_tools"
+    body:
+      "{{ lookup('template', 'vm-ngt.yml.j2') | from_yaml }}"
+    method: POST
+    validate_certs: no
+    body_format: json
+    status_code: [200,201,202]
+    headers: 
+      Cookie: "{{ pe_session_cookie }}"
+  register: json_enable_ngt_result
+  ignore_errors: yes
+  with_items: "{{ vm_uuids }}"
+
+- name: Debug | Enable NGT 
+  debug:
+    msg: "Enabling NGT : {{ json_enable_ngt_result }}" 
+  when: global_debug|bool

+ 6 - 0
cg_nutanix_vm_create/tasks/update_subnet_uuids.yml

@@ -0,0 +1,6 @@
+---
+# Update VM specifications with subnet UUIDs
+- set_fact:
+    vm_defs_1: "{{ vm_defs_1 |default([])  + [my_vm | combine({ 'vm_subnet_uuid' : item. uuid  })] }}"
+  when: my_vm.vm_subnet_name == item.name
+  with_items: "{{ subnet_uuids }}"

+ 32 - 0
cg_nutanix_vm_create/tasks/update_vm_defs.yml

@@ -0,0 +1,32 @@
+---
+# Update Cluster and VM(s) specifications with UUIDs for creation
+- name: Define Cluster UUID to use from name
+  set_fact:
+    cluster_uuid: "{{ item['uuid'] }}"
+  when: "item['name'] == cluster_name"
+  loop: "{{ cluster_uuids }}"
+
+- name: Update VM(s) specifications with subnet UUIDs 
+  include: update_subnet_uuids.yml
+  loop: "{{ user_vm_defs }}"
+  loop_control:
+    loop_var: my_vm
+
+- name: Debug | Print updated VM(s) specifications with subnet UUIDs
+  debug:
+    msg: "Updated VM specifications with subnet UUIDs : {{ vm_defs_1 }}"
+  when: global_debug|bool
+
+- name: Update VM specifications with image UUIDs 
+  include: update_image_uuids.yml
+  loop: "{{ vm_defs_1 }}"
+  loop_control:
+    loop_var: my_vm
+
+- name: Store updated VM(s) specifications
+  set_fact:
+    ntx_vm_defs: "{{ vm_defs_2 }}"
+
+- name: Debug | Print final user VM(s) specifications for creation
+  debug:
+    msg: "VM(s) shall be created as per: {{ ntx_vm_defs }}"

+ 37 - 0
cg_nutanix_vm_create/templates/vm-body.yml.j2

@@ -0,0 +1,37 @@
+# A base template for VM creation
+# 
+api_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: {{ vm.vm_image_uuid }}
+    - device_properties:
+        disk_address:
+          adapter_type: "IDE"
+          device_index: 0
+        device_type: "CDROM"
+{% if vm.vm_disk_list %}
+{% for disk in vm.vm_disk_list.split(',') %}
+    - device_properties:
+        device_type: "DISK"
+      disk_size_mib: {{ disk }}
+{% endfor %}
+{% endif %}
+    memory_size_mib: {{ vm.vm_memory }}
+    nic_list:
+    - ip_endpoint_list:
+      - ip: {{ vm.vm_ip }}
+      subnet_reference:
+        kind: subnet
+        uuid: {{ vm.vm_subnet_uuid }}
+    num_sockets: {{ vm.vm_num_sockets }}
+    num_vcpus_per_socket: 1 
+    power_state: 'ON'

+ 5 - 0
cg_nutanix_vm_create/templates/vm-ngt.yml.j2

@@ -0,0 +1,5 @@
+vmUuid: "{{ item }}"
+enabled: "true"
+applications:
+  file_level_restore: "true"
+  vss_snapshot: "true"