diff --git a/.github/workflows/sanity-tests.yml b/.github/workflows/sanity-tests.yml new file mode 100644 index 0000000..5bff023 --- /dev/null +++ b/.github/workflows/sanity-tests.yml @@ -0,0 +1,36 @@ +###### To-do ###### +# Fetch namespace, collection and version details dynamically +# Skip tests for python 2.6 and 2.7 + +name: Run compile and sanity tests + +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Setup Docker + uses: docker-practice/actions-setup-docker@master + - name: Print docker version + run: | + set -x + docker version + - name: Install ansible + run: pip install ansible + - name: Build and install the collection + run: ansible-galaxy collection build && ansible-galaxy collection install nutanix-nutanix-0.0.1-rc1.tar.gz + - name: Run tests + run: | + cd /home/${USER}/.ansible/collections/ansible_collections/nutanix/nutanix + ansible-test sanity --docker default --python 3.9 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87db572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.tar.gz +*.pyc diff --git a/README.md b/README.md index 81744a5..24a4363 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ -# ansible-ahv-provider-plugin- -Ansible plugins to interact with AHV APIs +# ansible-ahv-provider-plugin +Ansible plugin to integrate with Nutanix Enterprise Cloud + + +# Building and installing the collection locally +``` +ansible-galaxy collection build +ansible-galaxy collection install nutanix-nutanix-0.0.1.tar.gz +``` +_Add `--force` option for rebuilding or reinstalling to overwrite existing data_ + +# Included modules +``` +nutanix_image_info +nutanix_image +nutanix_vm_info +nutanix_vm +``` + +# Inventory plugin +`nutanix_vm_inventory` + +# Module documentation and examples +``` +ansible-doc nutanix.nutanix. +``` + +# Examples +## Playbook to print name of vms in PC +``` +- hosts: localhost + collections: + - nutanix.nutanix + tasks: + - nutanix_vm_info: + pc_hostname: {{ pc_hostname }} + pc_username: {{ pc_username }} + pc_password: {{ pc_password }} + validate_certs: False + register: result + - debug: + msg: "{{ result.vms }}" +``` diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..4c9d173 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,11 @@ +namespace: "nutanix" +name: "nutanix" +version: "0.0.1-rc1" +readme: "README.md" +authors: + - "Balu George (@balugeorge)" + - "Sarath Kumar K (@kumarsarath588)" +license: +- GPL-2.0-or-later +tags: [nutanix, ahv] +repository: "https://www.github.com/ideadevice/ansible-ahv-provider-plugin" diff --git a/plugins/inventory/nutanix_vm_inventory.py b/plugins/inventory/nutanix_vm_inventory.py new file mode 100644 index 0000000..a21456c --- /dev/null +++ b/plugins/inventory/nutanix_vm_inventory.py @@ -0,0 +1,155 @@ +# Copyright: (c) 2021, Balu George +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' + name: nutanix_vm_inventory + plugin_type: inventory + short_description: Returns nutanix vms ansible inventory + requirements: + - requests + description: Returns nutanix vms ansible inventory + options: + plugin: + description: Name of the plugin + required: true + choices: ['nutanix_vm_inventory', 'nutanix.nutanix.nutanix_vm_inventory'] + pc_hostname: + description: PC hostname or IP address + required: true + type: str + env: + - name: PC_HOSTNAME + pc_username: + description: PC username + required: true + type: str + env: + - name: PC_USERNAME + pc_password: + description: PC password + required: true + type: str + env: + - name: PC_PASSWORD + pc_port: + description: PC port + default: 9440 + type: str + env: + - name: PC_PORT + validate_certs: + description: + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup + default: True + type: boolean + env: + - name: VALIDATE_CERTS +''' + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +from ansible.errors import AnsibleError +from ansible.plugins.inventory import BaseInventoryPlugin + + +class InventoryModule(BaseInventoryPlugin): + '''Nutanix VM dynamic invetory parser for ansible''' + + NAME = 'nutanix.nutanix.nutanix_vm_inventory' + + def __init__(self): + super(InventoryModule, self).__init__() + self.session = None + + def _get_create_session(self): + '''Create session''' + if not self.session: + self.session = requests.Session() + if not self.validate_certs: + self.session.verify = self.validate_certs + from urllib3.exceptions import InsecureRequestWarning + requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) + + return self.session + + def _get_vm_list(self): + '''Get a list of existing VMs''' + api_url = "https://{0}:{1}/api/nutanix/v3/vms/list".format(self.pc_hostname, self.pc_port) + auth = (self.pc_username, self.pc_password) + headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} + payload = '{"offset": 0, "length": 100}' + + session = self._get_create_session() + vm_list_response = session.post(url=api_url, auth=auth, headers=headers, data=payload) + + return vm_list_response.json() + + def _build_inventory(self): + '''Build inventory from API response''' + vars_to_remove = ["disk_list", "vnuma_config", "nic_list", "power_state_mechanism", "host_reference", + "serial_port_list", "gpu_list", "storage_config", "boot_config", "guest_customization"] + vm_list_resp = self._get_vm_list() + + for entity in vm_list_resp["entities"]: + nic_count = 0 + cluster = entity["status"]["cluster_reference"]["name"] + # self.inventory.add_host(f"{vm_name}-{vm_uuid}") + vm_name = entity["status"]["name"] + vm_uuid = entity["metadata"]["uuid"] + + # Get VM IP + for nics in entity["status"]["resources"]["nic_list"]: + if nics["nic_type"] == "NORMAL_NIC" and nic_count == 0: + for endpoint in nics["ip_endpoint_list"]: + if endpoint["type"] == "ASSIGNED": + vm_ip = endpoint["ip"] + nic_count += 1 + continue + + # Add inventory groups and hosts to inventory groups + self.inventory.add_group(cluster) + self.inventory.add_child('all', cluster) + self.inventory.add_host(vm_name, group=cluster) + self.inventory.set_variable(vm_name, 'ansible_host', vm_ip) + self.inventory.set_variable(vm_name, 'uuid', vm_uuid) + + # Add hostvars + for var in vars_to_remove: + try: + del entity["status"]["resources"][var] + except KeyError: + pass + for key, value in entity["status"]["resources"].items(): + self.inventory.set_variable(vm_name, key, value) + + def verify_file(self, path): + '''Verify inventory configuration file''' + valid = False + if super(InventoryModule, self).verify_file(path): + if path.endswith(('nutanix.yaml', 'nutanix.yml', 'nutanix_host_inventory.yaml', 'nutanix_host_inventory.yml')): + valid = True + return valid + + def parse(self, inventory, loader, path, cache): + '''Parse inventory''' + if not HAS_REQUESTS: + raise AnsibleError("Missing python 'requests' package") + + super(InventoryModule, self).parse(inventory, loader, path, cache) + self._read_config_data(path) + + self.pc_hostname = self.get_option('pc_hostname') + self.pc_username = self.get_option('pc_username') + self.pc_password = self.get_option('pc_password') + self.pc_port = self.get_option('pc_port') + self.validate_certs = self.get_option('validate_certs') + + self._build_inventory() diff --git a/plugins/module_utils/nutanix_api_client.py b/plugins/module_utils/nutanix_api_client.py new file mode 100644 index 0000000..ff82b4c --- /dev/null +++ b/plugins/module_utils/nutanix_api_client.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Nutanix +# Copyright: (c) 2021, Balu George + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json +import traceback +import time +from ansible.module_utils.basic import missing_required_lib + +try: + import requests + import requests.exceptions + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + REQUESTS_IMPORT_ERROR = traceback.format_exc() + + +class NutanixApiError(Exception): + pass + + +class NutanixApiClient(object): + """Nutanix Rest API client""" + + def __init__(self, module): + self.module = module + pc_hostname = module.params["pc_hostname"] + pc_username = module.params["pc_username"] + pc_password = module.params["pc_password"] + pc_port = module.params["pc_port"] + self.validate_certs = module.params["validate_certs"] + self.api_base = "https://{0}:{1}/api/nutanix".format( + pc_hostname, pc_port) + self.auth = (pc_username, pc_password) + # Ensure that all deps are present + self.check_dependencies() + # Create session + self.session = requests.Session() + if not self.validate_certs: + from urllib3.exceptions import InsecureRequestWarning + requests.packages.urllib3.disable_warnings( + category=InsecureRequestWarning) + + def request(self, api_endpoint, method, data, timeout=20): + self.api_url = "{0}/{1}".format(self.api_base, api_endpoint) + headers = {'Content-Type': 'application/json', + 'Accept': 'application/json'} + try: + response = self.session.request(method=method, url=self.api_url, auth=self.auth, + data=data, headers=headers, verify=self.validate_certs, timeout=timeout) + except requests.exceptions.RequestException as cerr: + raise NutanixApiError("Request failed {0}".format(str(cerr))) + + if response.ok: + return response + else: + raise NutanixApiError("Request failed to complete, response code {0}, content {1}".format( + response.status_code, response.content)) + + def check_dependencies(self): + if not HAS_REQUESTS: + self.module.fail_json( + msg=missing_required_lib('requests'), + exception=REQUESTS_IMPORT_ERROR) + + +def task_poll(task_uuid, client): + while True: + response = client.request( + api_endpoint="v3/tasks/{0}".format(task_uuid), method="GET", data=None) + if response.json()["status"] == "SUCCEEDED": + return None + elif response.json()["status"] == "FAILED": + error_out = response.json()["error_detail"] + return error_out + time.sleep(5) + + +def list_vms(filter, client): + vm_list_response = client.request( + api_endpoint="v3/vms/list", method="POST", data=json.dumps(filter)) + return vm_list_response.json() + + +def get_vm_uuid(params, client): + length = 100 + offset = 0 + total_matches = 99999 + vm_name = params['name'] + vm_uuid = [] + while offset < total_matches: + filter = {"filter": "vm_name=={0}".format( + vm_name), "length": length, "offset": offset} + vms_list = list_vms(filter, client) + for vm in vms_list["entities"]: + if vm["status"]["name"] == vm_name: + vm_uuid.append(vm["metadata"]["uuid"]) + + total_matches = vms_list["metadata"]["total_matches"] + offset += length + + return vm_uuid + + +def get_vm(vm_uuid, client): + get_virtual_machine = client.request( + api_endpoint="v3/vms/{0}".format(vm_uuid), method="GET", data=None) + return get_virtual_machine.json() + + +def create_vm(data, client): + response = client.request( + api_endpoint="v3/vms", + method="POST", + data=json.dumps(data) + ) + json_content = response.json() + return ( + json_content["status"]["execution_context"]["task_uuid"], + json_content["metadata"]["uuid"] + ) + + +def update_vm(vm_uuid, data, client): + response = client.request( + api_endpoint="v3/vms/{0}".format(vm_uuid), method="PUT", data=json.dumps(data)) + return response.json()["status"]["execution_context"]["task_uuid"] + + +def delete_vm(vm_uuid, client): + response = client.request( + api_endpoint="v3/vms/{0}".format(vm_uuid), method="DELETE", data=None) + return response.json()["status"]["execution_context"]["task_uuid"] + + +def list_images(filter, client): + image_list_response = client.request( + api_endpoint="v3/images/list", method="POST", data=json.dumps(filter)) + return image_list_response.json() + + +def get_image(image_uuid, client): + get_image = client.request( + api_endpoint="v3/images/{0}".format(image_uuid), method="GET", data=None) + return get_image.json() + + +def create_image(data, client): + response = client.request( + api_endpoint="v3/images", + method="POST", + data=json.dumps(data) + ) + json_content = response.json() + return ( + json_content["status"]["execution_context"]["task_uuid"], + json_content["metadata"]["uuid"] + ) + + +def update_image(image_uuid, data, client): + response = client.request( + api_endpoint="v3/images/{0}".format(image_uuid), method="PUT", data=json.dumps(data)) + return response.json()["status"]["execution_context"]["task_uuid"] + + +def delete_image(image_uuid, client): + response = client.request( + api_endpoint="v3/images/{0}".format(image_uuid), method="DELETE", data=None) + return response.json()["status"]["execution_context"]["task_uuid"] diff --git a/plugins/modules/nutanix_image.py b/plugins/modules/nutanix_image.py new file mode 100644 index 0000000..4124e1e --- /dev/null +++ b/plugins/modules/nutanix_image.py @@ -0,0 +1,482 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Balu George +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: nutanix_image + +short_description: Images module which supports image crud operations + +version_added: "0.0.1" + +description: Create, update and delete Nutanix images + +options: + pc_hostname: + description: + - PC hostname or IP address + type: str + required: True + pc_username: + description: + - PC username + type: str + required: True + pc_password: + description: + - PC password + required: True + type: str + pc_port: + description: + - PC port + type: str + default: 9440 + required: False + image_name: + description: + - Image name + type: str + required: True + image_type: + description: + - Image type, ISO_IMAGE or DISK_IMAGE + - Auto detetected based on image extension + type: str + required: False + image_url: + description: + - Image url + type: str + required: True + force: + description: + - Used with C(present) or C(absent) + - Creates of multiple images with same name when set to true with C(present) + - Deletes all image with the same name when set to true with C(absent) + type: bool + default: False + required: False + image_uuid: + description: + - Image UUID + - Specify image for update if there are multiple images with the same name + type: str + required: False + new_image_name: + description: + - New image name for image update + type: str + required: False + new_image_type: + description: + - New image name for image update + - Accepts ISO_IMAGE or DISK_IMAGE + type: str + required: False + validate_certs: + description: + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup + type: bool + default: True + state: + description: + - Specify state of image + - If C(state) is set to C(present) the image is created, nutanix supports multiple images with the same name + - If C(state) is set to C(absent) and the image is present, all images with the specified name are removed + type: str + default: present + data: + description: + - Filter payload + - 'Valid attributes are:' + - ' - C(length) (int): length' + - ' - C(offset) (str): offset' + type: dict + required: False + suboptions: + length: + description: + - Length + type: int + offset: + description: + - Offset + type: int +author: + - Balu George (@balugeorge) +''' + +EXAMPLES = r''' + - name: Create image + nutanix.nutanix.nutanix_image: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + image_name: "{{ image_name }}" + image_type: "{{ image_type }}" + image_url: "{{ image_url }}" + state: present + delegate_to: localhost + register: create_image + async: 600 + poll: 0 + - name: Wait for image creation + async_status: + jid: "{{ create_image.ansible_job_id }}" + register: job_result + until: job_result.finished + retries: 30 + delay: 5 + + - name: Delete image + nutanix.nutanix.nutanix_image: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + image_name: "{{ image_name }}" + state: absent + delegate_to: localhost + register: delete_image + async: 600 + poll: 0 + - name: Wait for image deletion + async_status: + jid: "{{ delete_image.ansible_job_id }}" + register: job_result + until: job_result.finished + retries: 30 + delay: 5 + + - name: Update image + nutanix.nutanix.nutanix_image: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + image_name: "{{ image_name }}" + new_image_name: "{{ new_image_name }}" + new_image_type: "{{ new_image_type }}" + state: present + delegate_to: localhost + register: update_image + async: 600 + poll: 0 + - name: Wait for image update + async_status: + jid: "{{ update_image.ansible_job_id }}" + register: job_result + until: job_result.finished + retries: 30 + delay: 5 +''' + +RETURN = r''' +## TO-DO +''' + +import json +import copy +import time +from os.path import splitext +from urllib.parse import urlparse +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible_collections.nutanix.nutanix.plugins.module_utils.nutanix_api_client import ( + NutanixApiClient, + create_image, + update_image, + list_images, + delete_image, + task_poll) + + +CREATE_PAYLOAD = '''{ + "spec": { + "name": "IMAGE_NAME", + "resources": { + "image_type": "IMAGE_TYPE", + "source_uri": "IMAGE_URL", + "source_options": { + "allow_insecure_connection": false + } + }, + "description": "" + }, + "api_version": "3.1.0", + "metadata": { + "kind": "image", + "name": "IMAGE_NAME" + } +}''' + + +def set_list_payload(data): + length = 100 + offset = 0 + payload = {"length": length, "offset": offset} + + if data and "length" in data: + payload["length"] = data["length"] + if data and "offset" in data: + payload["offset"] = data["offset"] + + return payload + + +def generate_argument_spec(result): + # define available arguments/parameters a user can pass to the module + module_args = dict( + pc_hostname=dict(type='str', required=True, + fallback=(env_fallback, ["PC_HOSTNAME"])), + pc_username=dict(type='str', required=True, + fallback=(env_fallback, ["PC_USERNAME"])), + pc_password=dict(type='str', required=True, no_log=True, + fallback=(env_fallback, ["PC_PASSWORD"])), + pc_port=dict(default="9440", type='str', required=False), + image_name=dict(type='str', required=True), + image_type=dict(type='str', required=False), + image_url=dict(type='str', required=True), + image_uuid=dict(type='str', required=False), + state=dict(default='present', type='str', required=False), + force=dict(default=False, type='bool', required=False), + new_image_name=dict(type='str', required=False), + new_image_type=dict(type='str', required=False), + data=dict( + type='dict', + required=False, + options=dict( + length=dict(type='int'), + offset=dict(type='int'), + ) + ), + validate_certs=dict(default=True, type='bool', required=False), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # return initial result dict for dry run + if module.check_mode: + module.exit_json(**result) + + return module + + +def create_image_spec(module): + + image_name = module.params.get("image_name") + image_url = module.params.get("image_url") + image_type = module.params.get("image_type") + if not image_type: + parsed_url = urlparse(image_url) + path, extension = splitext(parsed_url.path) + if extension == ".iso": + image_type = "ISO_IMAGE" + elif extension == ".qcow2": + image_type = "DISK_IMAGE" + else: + module.fail_json( + "Unable to identify image_type, specify the value manually") + + create_payload = json.loads(CREATE_PAYLOAD) + + create_payload["metadata"]["name"] = image_name + create_payload["spec"]["name"] = image_name + create_payload["spec"]["resources"]["image_type"] = image_type + create_payload["spec"]["resources"]["source_uri"] = image_url + + return create_payload + + +def _create(module, client, result): + image_count = 0 + image_spec = create_image_spec(module) + image_uuid_list = [] + data = set_list_payload(module.params['data']) + image_name = module.params.get("image_name") + force_create = module.params.get("force") + + if image_name: + image_list_data = list_images(data, client) + for entity in image_list_data["entities"]: + if image_name == entity["status"]["name"]: + image_uuid = entity["metadata"]["uuid"] + image_uuid_list.append(image_uuid) + image_update_spec = entity + image_count += 1 + if image_count > 0 and not force_create: + result["msg"] = "Found existing images with name {0}, use force option to create new image".format( + image_name) + result["failed"] = True + return result + + # Create Image + task_uuid, image_uuid = create_image(image_spec, client) + + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + result["image_uuid"] = image_uuid + result["changed"] = True + return result + + +def _update(module, client, result): + image_count = 0 + task_uuid_list, image_list, image_uuid_list = [], [], [] + data = set_list_payload(module.params['data']) + image_name = module.params.get("image_name") + new_image_name = module.params.get("new_image_name") + new_image_type = module.params.get("new_image_type") + image_uuid_for_update = module.params.get("image_uuid") + + if image_name and (new_image_name or new_image_type): + image_list_data = list_images(data, client) + for entity in image_list_data["entities"]: + if image_name == entity["status"]["name"]: + image_uuid = entity["metadata"]["uuid"] + image_uuid_list.append(image_uuid) + image_update_spec = entity + # Remove status and update image name + del image_update_spec["status"] + if new_image_name: + image_update_spec["spec"]["name"] = new_image_name + if new_image_type: + image_update_spec["spec"]["resources"]["image_type"] = new_image_type + update = True + image_count += 1 + elif image_count > 1 and not image_uuid_for_update: + result["msg"] = "Found multiple images with name {0}, specify image_uuid".format( + image_name) + result["failed"] = True + return result + if image_count > 1 and image_uuid_for_update: + image_uuid = image_uuid_for_update + elif image_count == 0: + result["msg"] = "Could not find any image with name {0}".format( + image_name) + result["failed"] = True + return result + if not image_uuid_list: + result["msg"] = "Could not find UUID for image {0}".format( + image_name) + result["failed"] = True + return result + if update: + task_uuid = update_image(image_uuid, image_update_spec, client) + + # Check task status for image update + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + result["changed"] = True + return result + + +def _delete(module, client, result): + data = set_list_payload(module.params['data']) + force_delete = module.params.get("force") + + task_uuid_list, image_list, image_uuid_list = [], [], [] + image_count = 0 + + image_name = module.params.get("image_name") + if image_name: + image_list_data = list_images(data, client) + for entity in image_list_data["entities"]: + if image_name == entity["status"]["name"]: + image_uuid = entity["metadata"]["uuid"] + image_uuid_list.append(image_uuid) + image_update_spec = entity + image_count += 1 + if image_count > 1 and not force_delete: + result["msg"] = "Found multiple images with name {0}, specify image_uuid or use force option to remove all images".format( + image_name) + result["failed"] = True + return result + if image_count == 0: + result["msg"] = "Did not find any image with name {0}".format( + image_name) + result["failed"] = True + return result + if not image_uuid_list: + result["msg"] = "Could not find UUID for image {0}".format( + image_name) + result["failed"] = True + return result + # Delete all images with duplicate names when force is set to true + if image_count > 1 and force_delete: + for uuid in image_uuid_list: + task_uuid = delete_image(uuid, client) + task_uuid_list.append(task_uuid) + elif image_count == 1: + result["image_count"] = 1 + result["changed"] = True + task_uuid = delete_image(image_uuid, client) + # Check task status for removal of a single image + if task_uuid: + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + else: + result["failed"] = True + return result + + # Check status of all deletion tasks for removal of multiple images with duplicate names + if task_uuid_list: + result["msg"] = [] + for tuuid in task_uuid_list: + task_status = task_poll(tuuid, client) + if task_status: + result["failed"] = True + result["msg"].append(task_status) + return result + + return result + + +def main(): + # Seed result dict + result_init = dict( + changed=False, + ansible_facts=dict(), + ) + + # Generate arg spec and call function + arg_spec = generate_argument_spec(result_init) + nimage_name = arg_spec.params.get("new_image_name") + nimage_type = arg_spec.params.get("new_image_type") + + # Instantiate api client + api_client = NutanixApiClient(arg_spec) + if arg_spec.params.get("state") == "present" and (nimage_name or nimage_type): + result = _update(arg_spec, api_client, result_init) + elif arg_spec.params.get("state") == "present": + result = _create(arg_spec, api_client, result_init) + elif arg_spec.params.get("state") == "absent": + result = _delete(arg_spec, api_client, result_init) + + arg_spec.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/nutanix_image_info.py b/plugins/modules/nutanix_image_info.py new file mode 100644 index 0000000..a77da13 --- /dev/null +++ b/plugins/modules/nutanix_image_info.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Balu George +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: nutanix_image_info + +short_description: Basic imageinfo module which supports imagelist operation + +version_added: "0.0.1" + +description: Longer description for nutanix info module. + +options: + pc_hostname: + description: + - PC hostname or IP address + type: str + required: True + pc_username: + description: + - PC username + type: str + required: True + pc_password: + description: + - PC password + required: True + type: str + pc_port: + description: + - PC port + type: str + default: 9440 + required: False + image_name: + description: + - Image name + type: str + required: False + validate_certs: + description: + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup + default: True + type: bool + data: + description: + - Filter payload + - 'Valid attributes are:' + - ' - C(length) (int): length' + - ' - C(offset) (str): offset' + type: dict + required: False + suboptions: + length: + description: + - Length + type: int + offset: + description: + - Offset + type: int +author: + - Balu George (@balugeorge) +''' + +EXAMPLES = r''' + - name: List images + nutanix.nutanix.nutanix_image_info: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + validate_certs: False + register: image_list + - debug: + var: "{{ image_list.image }}" + + - name: Get image details + nutanix.nutanix.nutanix_image_info: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + image_name: "{{ image_name }}" + validate_certs: False + register: image_details + - debug: + var: "{{ image_details.image }}" +''' + +RETURN = r''' +## TO-DO +''' + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible_collections.nutanix.nutanix.plugins.module_utils.nutanix_api_client import NutanixApiClient, list_images + + +def set_list_payload(data): + length = 100 + offset = 0 + payload = {"length": length, "offset": offset} + + if data and "length" in data: + payload["length"] = data["length"] + if data and "offset" in data: + payload["offset"] = data["offset"] + + return payload + + +def get_image_list(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + pc_hostname=dict(type='str', required=True, + fallback=(env_fallback, ["PC_HOSTNAME"])), + pc_username=dict(type='str', required=True, + fallback=(env_fallback, ["PC_USERNAME"])), + pc_password=dict(type='str', required=True, no_log=True, + fallback=(env_fallback, ["PC_PASSWORD"])), + pc_port=dict(default="9440", type='str', required=False), + image_name=dict(type='str', required=False), + data=dict( + type='dict', + required=False, + options=dict( + length=dict(type='int'), + offset=dict(type='int'), + ) + ), + validate_certs=dict(default=True, type='bool', required=False), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # Seed result dict + result = dict( + changed=False, + ansible_facts=dict(), + ) + + # return initial result dict for dry run without execution + if module.check_mode: + module.exit_json(**result) + + # Instantiate api client + client = NutanixApiClient(module) + + # Get image list/details + image_name = module.params.get("image_name") + spec_list, status_list, image_list, meta_list = [], [], [], [] + data = set_list_payload(module.params['data']) + image_list_data = list_images(data, client) + + for entity in image_list_data["entities"]: + # Identify image list operation from image spec request + if image_name == entity["status"]["name"]: + result["image"] = entity + result["image_uuid"] = entity["metadata"]["uuid"] + break + else: + spec_list.append(entity["spec"]) + status_list.append(entity["status"]) + image_list.append(entity["status"]["name"]) + meta_list.append(entity["metadata"]) + + if spec_list and status_list and image_list and meta_list: + result["image_spec"] = spec_list + result["image_status"] = status_list + result["images"] = image_list + result["meta_list"] = meta_list + + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + get_image_list() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/nutanix_vm.py b/plugins/modules/nutanix_vm.py new file mode 100644 index 0000000..b132bba --- /dev/null +++ b/plugins/modules/nutanix_vm.py @@ -0,0 +1,732 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Sarat Kumar +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: nutanix_vm + +short_description: VM module which suports VM CRUD operations + +version_added: "0.0.1" + +description: Create, Update, Delete, Power-on, Power-off Nutanix VM's + +options: + pc_hostname: + description: + - PC hostname or IP address + type: str + required: True + pc_username: + description: + - PC username + type: str + required: True + pc_password: + description: + - PC password + required: True + type: str + pc_port: + description: + - PC port + type: str + default: 9440 + required: False + validate_certs: + description: + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup + type: bool + default: True + state: + description: + - Specify state of Virtual Machine + - If C(state) is set to C(present) the VM is created, if VM with same name already exists it will updated the VM. + - If C(state) is set to C(absent) and the VM exists in the cluster, VM with specified name is removed. + - If C(state) is set to C(poweron) and the VM exists in the cluster, VM with specified name is Powered On. + - If C(state) is set to C(poweroff) and the VM exists in the cluster, VM with specified name is Powered Off. + choices: + - present + - absent + - poweron + - poweroff + type: str + default: present + name: + description: + - Name of the Virtual Machine + type: str + required: True + vm_uuid: + description: + - Used during VM update, only needed if VM's with same name exits in the cluster. + type: str + required: False + cpu: + description: + - Number of CPU's. + type: int + required: True + vcpu: + description: + - Number of Cores per CPU. + type: int + required: True + memory: + description: + - Virtual Machine memory in (mib), E.g 2048 for 2GB. + type: int + required: True + cluster: + description: + - PE Cluster uuid/name where you want to place the VM. + type: str + required: True + dry_run: + description: + - Set value to C(True) to skip vm creation and print the spec for verification. + type: bool + default: False + disk_list: + description: + - Virtual Machine Disk list + type: list + elements: dict + suboptions: + clone_from_image: + description: + - Name/UUID of the image + type: str + size_mib: + description: + - Disk Size + type: int + device_type: + description: + - Disk Device type + - 'Accepted value for this field:' + - ' - C(DISK)' + - ' - C(CDROM)' + choices: + - DISK + - CDROM + type: str + required: True + adapter_type: + description: + - Disk Adapter type + - 'Accepted value for this field:' + - ' - C(SCSI)' + - ' - C(PCI)' + - ' - C(SATA)' + - ' - C(IDE)' + choices: + - SCSI + - PCI + - SATA + - IDE + type: str + required: True + required: True + nic_list: + description: + - Virtual Machine Nic list + type: list + elements: dict + suboptions: + uuid: + description: + - Subnet UUID + type: str + required: True + required: True + guest_customization: + description: + - Virtual Machine Guest Customization + - 'Valid attributes are:' + - ' - C(cloud_init) (str): Path of the cloud-init yaml file.' + - ' - C(sysprep) (str): Path of the sysprep xml file.' + type: dict + required: False + suboptions: + cloud_init: + description: + - Cloud init content + type: str + required: False + sysprep: + description: + - Sysprep content + type: str + required: False + sysprep_install_type: + description: + - Sysprep Install type + - 'Accepted value for this field:' + - ' - C(FRESH)' + - ' - C(PREPARED)' + type: str + required: False + default: PREPARED + choices: + - FRESH + - PREPARED +author: + - Sarat Kumar (@kumarsarath588) +''' + +EXAMPLES = r''' + - name: Create VM + nutanix.nutanix.nutanix_vm: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + validate_certs: False + name: "vm-0001" + cpu: 2 + vcpu: 2 + memory: 2048 + cluster: "{{ cluster name or uuid }}" + disk_list: + - device_type: DISK + clone_from_image: "{{ image name or uuid }}" + adapter_type: SCSI + - device_type: DISK + adapter_type: SCSI + size_mib: 10240 + nic_list: + - uuid: "{{ nic name or uuid }}" + guest_customization: + cloud_init: |- + #cloud-config + users: + - name: centos + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + chpasswd: + list: | + centos:nutanix/4u + expire: False + ssh_pwauth: true + delegate_to: localhost + register: vm + - debug: + msg: "{{ vm }}" +''' + + +RETURN = r''' +#TO-DO +''' + +import json +import time +import base64 +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible_collections.nutanix.nutanix.plugins.module_utils.nutanix_api_client import ( + NutanixApiClient, + get_vm_uuid, + get_vm, + create_vm, + update_vm, + delete_vm, + task_poll +) + + +CREATE_PAYLOAD = { + "metadata": { + "kind": "vm", + "spec_version": 0 + }, + "spec": { + "cluster_reference": { + "kind": "kind", + "name": "name", + "uuid": "uuid" + }, + "name": "name", + "resources": { + "disk_list": [], + "memory_size_mib": 0, + "nic_list": [], + "num_sockets": 0, + "num_vcpus_per_socket": 0, + "power_state": "power_state" + } + } +} + + +def main(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + pc_hostname=dict( + type='str', required=True, fallback=(env_fallback, ["PC_HOSTNAME"]) + ), + pc_username=dict( + type='str', required=True, fallback=(env_fallback, ["PC_USERNAME"]) + ), + pc_password=dict( + type='str', required=True, no_log=True, fallback=(env_fallback, ["PC_PASSWORD"]) + ), + pc_port=dict(default="9440", type='str', required=False), + validate_certs=dict(default=True, type='bool'), + state=dict( + default="present", + type='str', + choices=[ + "present", + "absent", + "poweron", + "poweroff" + ] + ), + name=dict(type='str', required=True), + vm_uuid=dict(type='str', required=False), + cpu=dict(type='int', required=True), + vcpu=dict(type='int', required=True), + memory=dict(type='int', required=True), + cluster=dict(type='str', required=True), + dry_run=dict( + default=False, + type='bool', + required=False + ), + disk_list=dict( + type='list', + required=True, + elements='dict', + options=dict( + clone_from_image=dict( + type='str' + ), + size_mib=dict( + type='int' + ), + device_type=dict( + type='str', + required=True, + choices=["DISK", "CDROM"] + ), + adapter_type=dict( + type='str', + required=True, + choices=["SCSI", "PCI", "SATA", "IDE"] + ) + ) + ), + nic_list=dict( + type='list', + required=True, + elements='dict', + options=dict( + uuid=dict( + type='str', + required=True + ) + ) + ), + guest_customization=dict( + type='dict', + required=False, + options=dict( + cloud_init=dict( + type='str', + required=False + ), + sysprep=dict( + type='str', + required=False + ), + sysprep_install_type=dict( + type='str', + required=False, + choices=["FRESH", "PREPARED"], + default="PREPARED" + ) + ) + ) + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + if not module.params["pc_hostname"]: + module.fail_json("pc_hostname cannot be empty") + if not module.params["pc_username"]: + module.fail_json("pc_username cannot be empty") + if not module.params["pc_password"]: + module.fail_json("pc_password cannot be empty") + + # Instantiate api client + client = NutanixApiClient(module) + result = entry_point(module, client) + module.exit_json(**result) + + +def entry_point(module, client): + if module.params["state"] == "present": + operation = "create" + elif module.params["state"] == "absent": + operation = "delete" + else: + operation = module.params["state"] + + func = globals()["_" + operation] + + return func(module.params, client) + + +def create_vm_spec(params, vm_spec): + nic_list = [] + disk_list = [] + + for nic in params['nic_list']: + nic_list.append({ + "nic_type": "NORMAL_NIC", + "vlan_mode": "ACCESS", + "subnet_reference": { + "kind": "subnet", + "uuid": nic["uuid"] + }, + "is_connected": True + }) + + scsi_counter = 0 + sata_counter = 0 + for disk in params['disk_list']: + if disk["adapter_type"] == "SCSI": + counter = scsi_counter + scsi_counter += 1 + elif disk["adapter_type"] == "SATA": + counter = sata_counter + sata_counter += 1 + + if disk["clone_from_image"]: + disk_list.append({ + "device_properties": { + "disk_address": { + "device_index": counter, + "adapter_type": disk["adapter_type"] + }, + "device_type": disk["device_type"] + }, + "data_source_reference": { + "kind": "image", + "uuid": disk["clone_from_image"] + } + }) + else: + disk_list.append({ + "device_properties": { + "disk_address": { + "device_index": counter, + "adapter_type": disk["adapter_type"] + }, + "device_type": disk["device_type"] + }, + "disk_size_mib": disk["size_mib"] + }) + + vm_spec["spec"]["name"] = params['name'] + vm_spec["spec"]["resources"]["num_sockets"] = params['cpu'] + vm_spec["spec"]["resources"]["num_vcpus_per_socket"] = params['vcpu'] + vm_spec["spec"]["resources"]["memory_size_mib"] = params['memory'] + vm_spec["spec"]["resources"]["power_state"] = "ON" + vm_spec["spec"]["resources"]["nic_list"] = nic_list + vm_spec["spec"]["resources"]["disk_list"] = disk_list + + if params["guest_customization"]: + if params["guest_customization"]["cloud_init"]: + cloud_init_encoded = base64.b64encode( + params["guest_customization"]["cloud_init"].encode('ascii') + ) + vm_spec["spec"]["resources"]["guest_customization"] = { + "cloud_init": { + "user_data": cloud_init_encoded.decode('ascii') + } + } + + if params["guest_customization"]["sysprep"]: + sysprep_init_encoded = base64.b64encode( + params["guest_customization"]["sysprep"].encode('ascii') + ) + vm_spec["spec"]["resources"]["guest_customization"] = { + "sysprep": { + "install_type": params["guest_customization"]["sysprep_install_type"], + "unattend_xml": sysprep_init_encoded.decode('ascii') + } + } + + vm_spec["spec"]["cluster_reference"] = {"kind": "cluster", "uuid": params['cluster']} + + return vm_spec + + +def update_vm_spec(params, vm_data): + + nic_list = [] + disk_list = [] + guest_customization_cdrom = None + vm_spec = vm_data["spec"] + spec_nic_list = vm_spec["resources"]["nic_list"] + spec_disk_list = vm_spec["resources"]["disk_list"] + + param_disk_list = params['disk_list'] + param_nic_list = params['nic_list'] + + if params["guest_customization"]: + if ( + params["guest_customization"]["cloud_init"] or + params["guest_customization"]["sysprep"] + ): + guest_customization_cdrom = spec_disk_list.pop() + + scsi_counter = 0 + sata_counter = 0 + for i, disk in enumerate(param_disk_list): + if disk["adapter_type"] == "SCSI": + counter = scsi_counter + scsi_counter += 1 + elif disk["adapter_type"] == "SATA": + counter = sata_counter + sata_counter += 1 + + if disk["clone_from_image"]: + try: + spec_disk = spec_disk_list[i] + disk_list.append(spec_disk) + except IndexError: + disk_list.append({ + "device_properties": { + "disk_address": { + "device_index": counter, + "adapter_type": disk["adapter_type"] + }, + "device_type": disk["device_type"] + }, + "data_source_reference": { + "kind": "image", + "uuid": disk["clone_from_image"] + } + }) + else: + try: + spec_disk = spec_disk_list[i] + if spec_disk["disk_size_mib"] != disk["size_mib"]: + spec_disk["disk_size_mib"] = disk["size_mib"] + disk_list.append(spec_disk) + except IndexError: + disk_list.append({ + "device_properties": { + "disk_address": { + "device_index": counter, + "adapter_type": disk["adapter_type"] + }, + "device_type": disk["device_type"] + }, + "disk_size_mib": disk["size_mib"] + }) + + if guest_customization_cdrom: + disk_list.append(guest_customization_cdrom) + + for i, nic in enumerate(param_nic_list): + try: + spec_nic = spec_nic_list[i] + nic_list.append(spec_nic) + except IndexError: + nic_list.append({ + "nic_type": "NORMAL_NIC", + "vlan_mode": "ACCESS", + "subnet_reference": { + "kind": "subnet", + "uuid": nic["uuid"] + }, + "is_connected": True + }) + + vm_data["spec"]["resources"]["num_sockets"] = params['cpu'] + vm_data["spec"]["resources"]["num_vcpus_per_socket"] = params['vcpu'] + vm_data["spec"]["resources"]["memory_size_mib"] = params['memory'] + vm_data["spec"]["resources"]["power_state"] = "ON" + vm_data["spec"]["resources"]["nic_list"] = nic_list + vm_data["spec"]["resources"]["disk_list"] = disk_list + vm_data["metadata"]["spec_version"] += 1 + + return vm_data + + +def _create(params, client): + + vm_uuid = None + + if params["vm_uuid"]: + vm_uuid = params["vm_uuid"] + + result = dict( + changed=False, + vm_uuid='', + vm_ip_address='', + vm_status={} + ) + + # Check VM existance + vm_uuid_list = get_vm_uuid(params, client) + + if len(vm_uuid_list) > 1 and not vm_uuid: + result["failed"] = True + result["msg"] = """Multiple Vm's with same name '%s' exists in the cluster. + please give different name or specify vm_uuid if you want to update vm""" % params["name"] + result["vm_uuid"] = vm_uuid_list + return result + elif len(vm_uuid_list) >= 1 or vm_uuid: + return _update(params, client, vm_uuid=vm_uuid) + + # Create VM Spec + vm_spec = CREATE_PAYLOAD + vm_spec = create_vm_spec(params, vm_spec) + + if params['dry_run'] is True: + result["vm_spec"] = vm_spec + return result + + # Create VM + task_uuid, vm_uuid = create_vm(vm_spec, client) + + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + while True: + response = client.request(api_endpoint="v3/vms/%s" % vm_uuid, method="GET", data=None) + json_content = json.loads(response.content) + if len(json_content["status"]["resources"]["nic_list"]) > 0: + if len(json_content["status"]["resources"]["nic_list"][0]["ip_endpoint_list"]) > 0: + if json_content["status"]["resources"]["nic_list"][0]["ip_endpoint_list"][0]["ip"] != "": + result["vm_status"] = json_content["status"] + result["vm_ip_address"] = json_content["status"]["resources"]["nic_list"][0]["ip_endpoint_list"][0]["ip"] + break + time.sleep(5) + + result["vm_uuid"] = vm_uuid + result["changed"] = True + + return result + + +def _update(params, client, vm_uuid=None): + + result = dict( + changed=False, + vm_spec={}, + updated_vm_spec={}, + task_uuid='' + ) + + if not vm_uuid: + vm_uuid = get_vm_uuid(params, client)[0] + + vm_json = get_vm(vm_uuid, client) + + # Poweroff the VM + if vm_json["status"]["resources"]["power_state"] == "ON": + + del vm_json["status"] + vm_json["spec"]["resources"]["power_state"] = "OFF" + vm_json["metadata"]["spec_version"] += 1 + + task_uuid = update_vm(vm_uuid, vm_json, client) + + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + vm_json["metadata"]["entity_version"] = "%d" % ( + int(vm_json["metadata"]["entity_version"]) + 1 + ) + + # Update the VM + if "status" in vm_json: + del vm_json["status"] + updated_vm_spec = update_vm_spec(params, vm_json) + updated_vm_spec["spec"]["resources"]["power_state"] = "ON" + result["updated_vm_spec"] = updated_vm_spec + + if params['dry_run'] is True: + return result + + task_uuid = update_vm(vm_uuid, updated_vm_spec, client) + result["task_uuid"] = task_uuid + + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + result["changed"] = True + + return result + + +def _delete(params, client): + + vm_uuid = None + + if params["vm_uuid"]: + vm_uuid = params["vm_uuid"] + + result = dict( + changed=False, + task_uuid='', + ) + + vm_uuid_list = get_vm_uuid(params, client) + if len(vm_uuid_list) > 1 and not vm_uuid: + result["failed"] = True + result["msg"] = """Multiple Vm's with same name '%s' exists in the cluster. + Specify vm_uuid of the VM you want to delete.""" % params["name"] + result["vm_uuid"] = vm_uuid_list + return result + + if not vm_uuid: + vm_uuid = vm_uuid_list[0] + + # Delete VM + task_uuid = delete_vm(vm_uuid, client) + + result["task_uuid"] = task_uuid + + task_status = task_poll(task_uuid, client) + if task_status: + result["failed"] = True + result["msg"] = task_status + return result + + result["changed"] = True + + return result + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/nutanix_vm_info.py b/plugins/modules/nutanix_vm_info.py new file mode 100644 index 0000000..88ce9f7 --- /dev/null +++ b/plugins/modules/nutanix_vm_info.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Balu George +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: nutanix_vm_info + +short_description: Basic vm info module which supports vm list operation + +version_added: "0.0.1" + +description: List Nutanix vms and fetch vm info + +options: + pc_hostname: + description: + - PC hostname or IP address + type: str + required: True + pc_username: + description: + - PC username + type: str + required: True + pc_password: + description: + - PC password + required: True + type: str + pc_port: + description: + - PC port + type: str + default: 9440 + required: False + validate_certs: + description: + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup + type: bool + default: True + data: + description: + - List filter payload. + - 'Valid attributes are:' + - ' - C(filter) (str): filter string' + - ' - C(length) (int): length' + - ' - C(offset) (str): offset' + - ' - C(sort_attribute) (str): sort attribute' + - ' - C(sort_order) (str): sort order' + - ' - Accepted values:' + - ' - ASCENDING' + - ' - DESCENDING' + type: dict + required: False + suboptions: + filter: + description: + - Filter + type: str + length: + description: + - Length + type: int + offset: + description: + - Offset + type: int + sort_attribute: + description: + - Sort Attribute, specify ASCENDING or DESCENDING + type: str + sort_order: + description: + - Sort Order + type: str + +author: + - Balu George (@balugeorge) +''' + +EXAMPLES = r''' + - name: List vms + nutanix.nutanix.nutanix_vm_info: + pc_hostname: "{{ pc_hostname }}" + pc_username: "{{ pc_username }}" + pc_password: "{{ pc_password }}" + pc_port: 9440 + validate_certs: False + data: + filter: "vm_name=={{ vm_name }}" + offset: 0 + length: 100 + register: result + - debug: + var: "{{ result.vms }}" + +''' + +RETURN = r''' +## TO-DO +''' + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible_collections.nutanix.nutanix.plugins.module_utils.nutanix_api_client import ( + NutanixApiClient, + list_vms +) + + +def set_list_payload(data): + length = 100 + offset = 0 + filter = '' + + payload = {"filter": filter, "length": length, "offset": offset} + + if data and "length" in data: + payload["length"] = data["length"] + if data and "offset" in data: + payload["offset"] = data["offset"] + if data and "filter" in data: + payload["filter"] = data["filter"] + if data and "sort_attribute" in data: + payload["sort_attribute"] = data["sort_attribute"] + if data and "sort_order" in data: + payload["sort_order"] = data["sort_order"] + + return payload + + +def get_vm_list(): + + module_args = dict( + pc_hostname=dict(type='str', required=True, + fallback=(env_fallback, ["PC_HOSTNAME"])), + pc_username=dict(type='str', required=True, + fallback=(env_fallback, ["PC_USERNAME"])), + pc_password=dict(type='str', required=True, no_log=True, + fallback=(env_fallback, ["PC_PASSWORD"])), + pc_port=dict(default="9440", type='str', required=False), + data=dict( + type='dict', + required=False, + options=dict( + filter=dict(type='str'), + length=dict(type='int'), + offset=dict(type='int'), + sort_attribute=dict(type='str'), + sort_order=dict(type='str') + ) + ), + validate_certs=dict(default=True, type='bool', required=False), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # Seed result dict + result = dict( + changed=False, + ansible_facts=dict(), + vms_spec={}, + vm_status={}, + vms={}, + meta={} + ) + + # return initial result dict for dry run without execution + if module.check_mode: + module.exit_json(**result) + + # Instantiate api client + client = NutanixApiClient(module) + + # List VMs + spec_list, status_list, vm_list, meta_list = [], [], [], [] + data = set_list_payload(module.params['data']) + length = data["length"] + offset = data["offset"] + total_matches = 99999 + + while offset < total_matches: + data["offset"] = offset + vms_list = list_vms(data, client) + for entity in vms_list["entities"]: + spec_list.append(entity["spec"]) + status_list.append(entity["status"]) + vm_list.append(entity["status"]["name"]) + meta_list.append(entity["metadata"]) + + total_matches = vms_list["metadata"]["total_matches"] + offset += length + + result["vms_spec"] = spec_list + result["vm_status"] = status_list + result["vms"] = vm_list + result["meta"] = meta_list + + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + get_vm_list() + + +if __name__ == '__main__': + main()