From 8771eadb5f5441556d4ffafa9235c0f026862107 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 2 Dec 2021 14:56:53 +0530 Subject: [PATCH 01/17] initial client sdk with inventory plugin includes 1. Client SDK 2. Inventory plugin 3. Collection skeleton code 4. examples for plugin Signed-off-by: Prem Karat --- README.md | 42 +++++ examples/ansible.cfg | 5 + examples/nutanix.yml | 6 + galaxy.yml | 16 ++ plugins/README.md | 31 ++++ plugins/inventory/ntnx_prism_vm_inventory.py | 157 +++++++++++++++++++ plugins/module_utils/entity.py | 104 ++++++++++++ plugins/module_utils/prism/images.py | 10 ++ plugins/module_utils/prism/prism.py | 12 ++ plugins/module_utils/prism/vms.py | 10 ++ tests/test_entity.py | 23 +++ 11 files changed, 416 insertions(+) create mode 100644 README.md create mode 100644 examples/ansible.cfg create mode 100644 examples/nutanix.yml create mode 100644 galaxy.yml create mode 100644 plugins/README.md create mode 100644 plugins/inventory/ntnx_prism_vm_inventory.py create mode 100644 plugins/module_utils/entity.py create mode 100644 plugins/module_utils/prism/images.py create mode 100644 plugins/module_utils/prism/prism.py create mode 100644 plugins/module_utils/prism/vms.py create mode 100644 tests/test_entity.py diff --git a/README.md b/README.md new file mode 100644 index 000000000..e1d4c0503 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Nutanix Ansible Collection - nutanix.ncp +Ansible collections to automate Nutanix Cloud Platform (ncp). + +# Building and installing the collection locally +``` +ansible-galaxy collection build +ansible-galaxy collection install nutanix.ncp-1.0.0.tar.gz +``` +_Add `--force` option for rebuilding or reinstalling to overwrite existing data_ + +# Included modules +``` +ncp_prism_image_info +ncp_prism_image +ncp_prism_vm_info +ncp_prism_vm +``` + +# Inventory plugin +`ncp_prism_vm_inventory` + +# Module documentation and examples +``` +ansible-doc nutanix.ncp. +``` + +# Examples +## Playbook to print name of vms in PC +``` +- hosts: localhost + collections: + - nutanix.ncp + tasks: + - ncp_prism_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/examples/ansible.cfg b/examples/ansible.cfg new file mode 100644 index 000000000..0dd8c2275 --- /dev/null +++ b/examples/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = nutanix.yaml + +[inventory] +enable_plugins = nutanix.ncp.ntnx_prism_vm_inventory diff --git a/examples/nutanix.yml b/examples/nutanix.yml new file mode 100644 index 000000000..714160841 --- /dev/null +++ b/examples/nutanix.yml @@ -0,0 +1,6 @@ +--- +plugin: nutanix.ncp.ntnx_prism_vm_inventory +validate_certs: False +data: + offset: 0 + length: 100 \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 000000000..d43b4ea70 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,16 @@ +namespace: "nutanix" +name: "ncp" +version: "1.0.0" +readme: "README.md" +authors: + - "Balu George (@balugeorge)" + - "Sarath Kumar K (@kumarsarath588)" + - "Prem Karat (@premkarat)" +description: Ansible collection for v3 Nutanix APIs https://www.nutanix.dev/api-reference-v3/ +license_file: 'LICENSE' +tags: [nutanix, prism, ahv] +repository: https://github.com/nutanix/nutanix.ansible +documentation: https://github.com/nutanix/nutanix.ansible/nutanix/prism/docs +homepage: https://github.com/nutanix/nutanix.ansible/nutanix/prism +issues: https://github.com/nutanix/nutanix.ansible/issues +build_ignore: [] \ No newline at end of file diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 000000000..97642535d --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/devel/plugins/plugins.html). diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py new file mode 100644 index 000000000..ffcb405c4 --- /dev/null +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021 [Balu George, Prem Karat] +# 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: ntnx_prism_vm_inventory + plugin_type: inventory + short_description: Get a list of Nutanix VMs for ansible dynamic inventory. + description: + - Get a list of Nutanix VMs for ansible dynamic inventory. + version_added: "1.0.0" + author: + - "Balu George (@balugeorge)" + - "Prem Karat (@premkarat)" + inventory: ntnx_prism_vm_inventory + options: + plugin: + description: Name of the plugin + required: true + choices: ['ntnx_prism_vm_inventory', 'nutanix.ncp.ntnx_prism_vm_inventory'] + nutanix_hostname: + description: Prism central hostname or IP address + required: true + type: str + env: + - name: NUTANIX_HOSTNAME + nutanix_username: + description: Prism central username + required: true + type: str + env: + - name: NUTANIX_USERNAME + nutanix_password: + description: Prism central password + required: true + type: str + env: + - name: NUTANIX_PASSWORD + nutanix_port: + description: Prism central port + default: 9440 + type: str + env: + - name: NUTANIX_PORT + data: + description: + - Pagination support for listing VMs + - Default length(number of records to retrieve) has been set to 500 + default: {"offset": 0, "length": 500} + type: dict + 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 + notes: "null" + requirements: "null" +''' + +import json +import tempfile +from ansible.errors import AnsibleError +from ansible.plugins.inventory import BaseInventoryPlugin + +from ..module_utils.prism import vms + + +class Mock_Module: + def __init__(self, host, port, username, password, validate_certs=False): + self.tmpdir = tempfile.gettempdir() + self.params = {'nutanix_host': host, + 'nutanix_port': port, + 'nutanix_username': username, + 'nutanix_password': password, + 'validate_certs': validate_certs} + + def jsonify(self, data): + return json.dumps(data) + + +class InventoryModule(BaseInventoryPlugin): + '''Nutanix VM dynamic invetory module for ansible''' + + NAME = 'nutanix.ncp.ntnx_prism_vm_inventory' + + def verify_file(self, path): + '''Verify inventory configuration file''' + if not super().verify_file(path): + return False + + inventory_file_fmts = ('nutanix.yaml', 'nutanix.yml', + 'nutanix_host_inventory.yaml', + 'nutanix_host_inventory.yml') + return path.endswith(inventory_file_fmts) + + def parse(self, inventory, loader, path, cache=True): + super().parse(inventory, loader, path, cache=cache) + self._read_config_data(path) + + self.nutanix_hostname = self.get_option('nutanix_hostname') + self.nutanix_username = self.get_option('nutanix_username') + self.nutanix_password = self.get_option('nutanix_password') + self.nutanix_port = self.get_option('nutanix_port') + self.data = self.get_option('data') + self.validate_certs = self.get_option('validate_certs') + + module = Mock_Module(self.nutanix_hostname, self.nutanix_port, + self.nutanix_username, self.nutanix_password, + self.validate_certs) + vm = vms.VM(module) + resp, status_code = vm.list(self.data) + keys_to_strip_from_resp = ["disk_list", "vnuma_config", "nic_list", + "power_state_mechanism", "host_reference", + "serial_port_list", "gpu_list", + "storage_config", "boot_config", + "guest_customization"] + + for entity in resp["entities"]: + cluster = entity["status"]["cluster_reference"]["name"] + vm_name = entity["status"]["name"] + vm_uuid = entity["metadata"]["uuid"] + vm_ip = None + + # Get VM IP + nic_count = 0 + 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 key in keys_to_strip_from_resp: + try: + del entity["status"]["resources"][key] + except KeyError: + pass + + for key, value in entity["status"]["resources"].items(): + self.inventory.set_variable(vm_name, key, value) diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py new file mode 100644 index 000000000..f162c896c --- /dev/null +++ b/plugins/module_utils/entity.py @@ -0,0 +1,104 @@ +# This file is part of Ansible +# 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 + +import json +from ansible.module_utils.urls import fetch_url +from ansible.module_utils._text import to_text +from base64 import b64encode +from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse + + +class Entity(object): + def __init__(self, module, resource_type, scheme="https", + cookies=None, additional_headers=None): + self.module = module + self.base_url = self._build_url(module, scheme, resource_type) + self.headers = self._build_headers(module, additional_headers) + self.cookies = cookies + + def create(self, data=None, endpoint=None, query=None, timeout=30): + url = self.base_url + '/{0}'.format(endpoint) if endpoint else self.base_url + if query: + url = self._build_url_with_query(url, query) + return self._fetch_url(url, method="POST", data=data, timeout=timeout) + + def read(self, uuid=None, endpoint=None, query=None, timeout=30): + url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + if endpoint: + url = url + '/{}'.format(endpoint) + if query: + url = self._build_url_with_query(url, query) + return self._fetch_url(url, method="GET", timeout=timeout) + + def update(self, data=None, uuid=None, endpoint=None, query=None, timeout=30): + url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + if endpoint: + url = url + '/{0}'.format(endpoint) + if query: + url = self._build_url_with_query(url, query) + return self._fetch_url(url, method="PUT", data=data, timeout=timeout) + + def delete(self, uuid=None, endpoint=None, query=None, timeout=30): + url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + if endpoint: + url = url + '/{0}'.format(endpoint) + if query: + url = self._build_url_with_query(url, query) + return self._fetch_url(url, method="DELETE", timeout=timeout) + + def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): + url = self.base_url if use_base_url else self.base_url + '/list' + if endpoint: + url = url + '/{0}'.format(endpoint) + return self._fetch_url(url, method="POST", data=data, timeout=timeout) + + def _build_url(self, module, scheme, resource_type): + host = module.params.get("nutanix_host") + url = "{proto}://{host}".format(proto=scheme, host=host) + port = module.params.get("nutanix_port") + if port: + url += ":{port}".format(port=port) + if resource_type.startswith('/'): + url += resource_type + else: + url += "/{resource_type}".format(resource_type=resource_type) + return url + + def _build_headers(self, module, additional_headers): + headers = {"Content-Type": "application/json", + "Accept": "application/json"} + if additional_headers: + headers.update(additional_headers) + usr = module.params.get("nutanix_username") + pas = module.params.get("nutanix_password") + if usr and pas: + cred = f"{usr}:{pas}".format(usr=usr, pas=pas) + encoded_cred = b64encode(bytes(cred, encoding='ascii')).decode('ascii') + auth_header = "Basic " + encoded_cred + headers.update({"Authorization": auth_header}) + return headers + + def _build_url_with_query(self, url, query): + url = urlparse(url) + query_ = dict(parse_qsl(url.query)) + query_.update(query) + query_ = urlencode(query_) + url = url._replace(query=query_) + return urlunparse(url) + + def _fetch_url(self, url, method, data=None, timeout=30): + data = self.module.jsonify(data) if data else None + resp, info = fetch_url(self.module, url, data=data, method=method, + headers=self.headers, cookies=self.cookies, + timeout=timeout) + + status_code = info.get("status") + body = resp.read() if resp else info.get("body") + try: + resp_json = json.loads(to_text(body)) if body else None + except ValueError: + resp_json = None + return resp_json, status_code diff --git a/plugins/module_utils/prism/images.py b/plugins/module_utils/prism/images.py new file mode 100644 index 000000000..f9f3ef4c8 --- /dev/null +++ b/plugins/module_utils/prism/images.py @@ -0,0 +1,10 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class Image(Prism): + def __init__(self, module): + resource_type = '/images' + super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/prism.py b/plugins/module_utils/prism/prism.py new file mode 100644 index 000000000..9304443f0 --- /dev/null +++ b/plugins/module_utils/prism/prism.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ..entity import Entity + + +class Prism(Entity): + __BASEURL__ = "/api/nutanix/v3" + + def __init__(self, module, resource_type): + resource_type = self.__BASEURL__ + resource_type + super().__init__(module, resource_type) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py new file mode 100644 index 000000000..f6f9676d0 --- /dev/null +++ b/plugins/module_utils/prism/vms.py @@ -0,0 +1,10 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class VM(Prism): + def __init__(self, module): + resource_type = '/vms' + super().__init__(module, resource_type=resource_type) diff --git a/tests/test_entity.py b/tests/test_entity.py new file mode 100644 index 000000000..37acc75c4 --- /dev/null +++ b/tests/test_entity.py @@ -0,0 +1,23 @@ +from plugins.module_utils.prism.vms import VM +from plugins.module_utils.prism.images import Image + +import json +from ansible.module_utils.basic import AnsibleModule + + +class Module: + def __init__(self): + self.params = {'nutanix_host': '10.46.34.230', 'nutanix_port': 9440, 'nutanix_username': 'admin', 'nutanix_password': 'Nutanix.123'} + self.tmpdir = '/tmp' + + def jsonify(self, data): + return json.dumps(data) + +def main(): + module = Module() + vm = VM(module) + data = {"kind": "vm", "offset": 0, "length": 500} + print(vm.list(data)) + +if __name__ == '__main__': + main() \ No newline at end of file From 4d7bf19cd9a66f5492d5f2fcd7b822700338e3a8 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 27 Jan 2022 00:58:45 +0530 Subject: [PATCH 02/17] refactored code for vms sdk and vms module add support for projects, subnets, groups, clusters Signed-off-by: Prem Karat --- plugins/module_utils/entity.py | 7 + plugins/module_utils/prism/clusters.py | 10 + plugins/module_utils/prism/groups.py | 20 ++ plugins/module_utils/prism/projects.py | 10 + plugins/module_utils/prism/subnets.py | 10 + plugins/module_utils/prism/vms.py | 295 +++++++++++++++++++++++++ plugins/modules/base_module.py | 29 +++ plugins/modules/prism/ntnx_vms.py | 269 ++++++++++++++++++++++ 8 files changed, 650 insertions(+) create mode 100644 plugins/module_utils/prism/clusters.py create mode 100644 plugins/module_utils/prism/groups.py create mode 100644 plugins/module_utils/prism/projects.py create mode 100644 plugins/module_utils/prism/subnets.py create mode 100644 plugins/modules/base_module.py create mode 100644 plugins/modules/prism/ntnx_vms.py diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index f162c896c..b2d16f90e 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -55,6 +55,13 @@ def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): url = url + '/{0}'.format(endpoint) return self._fetch_url(url, method="POST", data=data, timeout=timeout) + def get_uuid(self, name): + data = {"filter": f"name=={name}", "length": 1} + resp, _ = self.list(data) + if resp.get("entities"): + return resp["entities"][0]["metadata"]["uuid"] + return None + def _build_url(self, module, scheme, resource_type): host = module.params.get("nutanix_host") url = "{proto}://{host}".format(proto=scheme, host=host) diff --git a/plugins/module_utils/prism/clusters.py b/plugins/module_utils/prism/clusters.py new file mode 100644 index 000000000..a1e40b755 --- /dev/null +++ b/plugins/module_utils/prism/clusters.py @@ -0,0 +1,10 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class Cluster(Prism): + def __init__(self, module): + resource_type = '/clusters' + super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/groups.py b/plugins/module_utils/prism/groups.py new file mode 100644 index 000000000..764995384 --- /dev/null +++ b/plugins/module_utils/prism/groups.py @@ -0,0 +1,20 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class Groups(Prism): + def __init__(self, module): + resource_type = '/groups' + super().__init__(module, resource_type=resource_type) + + def get_uuid(self, entity_type, filter): + data = { + "entity_type": entity_type, + "filter_criteria": filter + } + resp, _ = self.list(data, use_base_url=True) + if resp.get("group_results"): + return resp["group_results"][0]["entity_results"][0]["entity_id"] + return None \ No newline at end of file diff --git a/plugins/module_utils/prism/projects.py b/plugins/module_utils/prism/projects.py new file mode 100644 index 000000000..0fde59de5 --- /dev/null +++ b/plugins/module_utils/prism/projects.py @@ -0,0 +1,10 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class Project(Prism): + def __init__(self, module): + resource_type = '/projects' + super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/subnets.py b/plugins/module_utils/prism/subnets.py new file mode 100644 index 000000000..4bdacd309 --- /dev/null +++ b/plugins/module_utils/prism/subnets.py @@ -0,0 +1,10 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from .prism import Prism + + +class Subnet(Prism): + def __init__(self, module): + resource_type = '/subnets' + super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index f6f9676d0..8d4e368b4 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -1,10 +1,305 @@ # This file is part of Ansible # 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 + +from copy import deepcopy + +import base64 +import os + +from plugins.module_utils.prism.clusters import Cluster from .prism import Prism +from .projects import Project +from .subnets import Subnet +from .groups import Groups +from .images import Image class VM(Prism): def __init__(self, module): resource_type = '/vms' super().__init__(module, resource_type=resource_type) + self.update_spec_methods = { + "name": self._update_spec_name, + "desc": self._update_spec_desc, + "project": self._update_spec_project, + "cluster": self._update_spec_cluster, + "vcpus": self._update_spec_vcpus, + "cores_per_vcpu": self._update_spec_cores, + "memory_gb": self._update_spec_mem, + "networks": self._update_spec_networks, + "disks": self._update_spec_disks, + "boot_config": self._update_spec_boot_config, + "guest_customization": self._update_spec_gc, + "timezone": self._update_spec_timezone, + "categories": self._update_spec_categories + } + + def get_spec(self): + spec = self._get_default_spec() + for ansible_param, ansible_value in self.module.params.items(): + update_spec = self.update_spec_methods.get(ansible_param) + _, error = update_spec(spec, ansible_value) + if error: + return None, error + return spec, None + + def _get_default_spec(self): + return deepcopy({ + "api_version": "3.1.0", + "metadata": {"kind": "vm"}, + "spec": { + "cluster_reference": { + "kind": "cluster", + "uuid": None + }, + "name": None, + "resources": { + "num_sockets": 1, + "num_vcpus_per_socket": 1, + "memory_size_mib": 4096, + "power_state": "ON", + "disk_list": [], + "nic_list": [], + "gpu_list": [], + "boot_config": { + "boot_type": "LEGACY", + "boot_device_order_list": ["CDROM", "DISK", "NETWORK"] + }, + "hardware_clock_timezone": "UTC" + } + } + }) + + def _get_default_network_spec(self): + return deepcopy({ + "ip_endpoint_list": [], + "subnet_reference": { + "kind": "subnet", + "uuid": None + }, + "is_connected": True, + }) + + def _get_default_disk_spec(self): + return deepcopy({ + "device_properties": { + "device_type": "DISK", + "disk_address": { + "adapter_type": None, + "device_index": None + } + }, + "disk_size_bytes": None, + "storage_config": { + "storage_container_reference": { + "kind": "storage_container", + "uuid": None + } + }, + "data_source_reference": { + "kind": "image", + "uuid": None + } + }) + + def _update_spec_name(self, payload, value): + payload["spec"]["name"] = value + return payload, None + + def _update_spec_desc(self, payload, value): + payload["spec"]["description"] = value + return payload, None + + def _update_spec_project(self, payload, param): + if 'name' in param: + project = Project(self.module) + name = param["name"] + uuid = project.get_uuid(name) + if not uuid: + error = "Failed to get UUID for project name: {}".format(name]) + return None, error + + elif 'uuid' in param: + uuid = param['uuid'] + + payload["metadata"]["project_reference"]["uuid"] = uuid + return payload, None + + def _update_spec_cluster(self, payload, param): + if 'name' in param: + cluster = Cluster(self.module) + name = param["name"] + uuid = cluster.get_uuid(name) + if not uuid: + error = "Failed to get UUID for cluster name: {}".format(name) + return None, error + + elif 'uuid' in param: + uuid = param['uuid'] + + payload["spec"]["cluster_reference"]["uuid"] = uuid + return payload, None + + def _update_spec_vcpus(self, payload, value): + payload["spec"]["resources"]["num_sockets"] = value + return payload, None + + def _update_spec_cores(self, payload, value): + payload["spec"]["resources"]["num_vcpus_per_socket"] = value + return payload, None + + def _update_spec_mem(self, payload, value): + payload["spec"]["resources"]["memory_size_mib"] = value * 1024 + return payload, None + + def _update_spec_networks(self, payload, networks): + nics = [] + for network in networks: + nic = self._get_default_network_spec() + + if 'private_ip' in network: + nic["ip_endpoint_list"]["ip"] = network["private_ip"] + + if 'is_connected' in network: + nic["is_connected"] = network["is_connected"] + + if 'name' in network["subnet"]: + subnet = Subnet(self.module) + name = network["subnet"]["name"] + uuid = subnet.get_uuid(name) + if not uuid: + error = "Failed to get UUID for subnet name: {}".format(name) + return None, error + + elif 'uuid' in network["subnet"]: + uuid = network["subnet"]["uuid"] + + nic["subnet_reference"]["uuid"] = uuid + + nics.append(nic) + + payload["spec"]["resources"]["nic_list"] = nics + return payload, None + + def _update_spec_disks(self, payload, vdisks): + disks = [] + scsi_index = sata_index = pci_index = ide_index = 0 + + for vdisk in vdisks: + disk = self._get_default_disk_spec() + + if 'type' in vdisk: + disk["device_properties"]["device_type"] = vdisk["type"] + + if 'bus' in vdisk: + if vdisk["bus"] == "SCSI": + device_index = scsi_index + scsi_index += 1 + elif vdisk["bus"] == "SATA": + device_index = sata_index + sata_index += 1 + elif vdisk["bus"] == "PCI": + device_index = pci_index + pci_index += 1 + elif vdisk["bus"] == "IDE": + device_index = ide_index + ide_index += 1 + + disk["device_properties"]["disk_address"]["adapter_type"] = vdisk["bus"] + disk["device_properties"]["disk_address"]["device_index"] = device_index + + if 'empty_cdrom' in vdisk: + disk.pop('disk_size_bytes') + disk.pop('data_source_reference') + disk.pop('storage_config') + + else: + disk["disk_size_bytes"] = vdisk["size_gb"] * 1024 * 1024 * 1024 + + if 'storage_container' in vdisk: + disk.pop('data_source_reference') + if 'name' in vdisk["storage_container"]: + groups = Groups(self.module) + name = vdisk["storage_container"]["name"] + uuid = groups.get_uuid(entity_type="storage_container", + filter=f"container_name=={name}") + if not uuid: + error = "Failed to get UUID for storgae container: {}".format(name) + return None, error + + elif 'uuid' in vdisk["storage_container"]: + uuid = vdisk["storage_container"]["uuid"] + + disk["storage_config"]["storage_container_reference"]["uuid"] = uuid + + elif 'clone_image' in vdisk: + disk.pop('storage_config') + if 'name' in vdisk["clone_image"]: + image = Image(self.module) + name = vdisk["clone_image"]["name"] + uuid = image.get_uuid(name) + if not uuid: + error = "Failed to get UUID for image: {}".format(name) + return None, error + + elif 'uuid' in vdisk["clone_image"]: + uuid = vdisk["clone_image"]["uuid"] + + disk["data_source_reference"]["uuid"] = uuid + + disks.append(disk) + + payload["spec"]["resources"]["disk_list"] = disks + return payload, None + + def _update_spec_boot_config(self, payload, param): + boot_config = payload["spec"]["resources"]["boot_config"] + if 'LEGACY' in param["boot_config"]["boot_type"] and 'boot_order' in param["boot_config"]: + boot_config["boot_device_order_list"] = param["boot_config"]["boot_order"] + + elif "UEFI" in param["boot_config"]["boot_type"]: + boot_config.pop("boot_device_order_list") + + elif "SECURE_BOOT" in param["boot_config"]["boot_type"]: + boot_config.pop("boot_device_order_list") + payload["spec"]["resources"]["machine_type"] = "Q35" + + def _update_spec_gc(self, payload, param): + if 'script_path' in param["guest_customization"]: + fpath = param["guest_customization"]["script_path"] + + if not os.path.exists(fpath): + error = "File not found: {}".format(fpath) + return None, error + + with open(fpath, "rb", encoding="utf_8") as f: + content = base64.b64encode(f.read()) + + gc_spec = payload["spec"]["resources"]["guest_customization"] + + if 'sysprep' in param["type"]: + gc_spec = { + "sysprep": { + "install_type": "PREPARED", + "unattend_xml": content + } + } + elif 'cloud_init' in param["type"]: + gc_spec = { + "cloud_init": {"user_data": content} + } + if 'is_overridable' in param: + gc_spec["is_overridable"] = param["is_overridable"] + + return payload, None + + def _update_spec_timezone(self, payload, value): + payload["spec"]["resources"]["hardware_clock_timezone"] = value + return payload, None + + def _update_spec_categories(self, payload, value): + payload["metadata"]["categories_mapping"] = value + payload["metadata"]["use_categories_mapping"] = True + return payload, None diff --git a/plugins/modules/base_module.py b/plugins/modules/base_module.py new file mode 100644 index 000000000..b568b4189 --- /dev/null +++ b/plugins/modules/base_module.py @@ -0,0 +1,29 @@ +# Copyright: 2021, Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause ) +from __future__ import absolute_import, division, print_function + +from ansible.module_utils.basic import AnsibleModule + +__metaclass__ = type + + +class BaseModule(AnsibleModule): + """Basic module with common arguments""" + + argument_spec = dict( + action=dict(type="str", required=True, aliases=["state"]), + auth=dict(type="dict", required=True), + wait=dict(type="bool", required=False, default=True), + wait_timeout=dict(type="int", required=False, default=300), + validate_certs=dict(type="bool", required=False, default=False)) + + def __init__(self, **kwargs): + if kwargs.get("argument_spec"): + kwargs["argument_spec"].update(self.argument_spec) + else: + kwargs["argument_spec"] = self.argument_spec + + if not kwargs.get["supports_check_mode"]: + kwargs["support_check_mode"] = True + + super(BaseModule, self).__init__(**kwargs) \ No newline at end of file diff --git a/plugins/modules/prism/ntnx_vms.py b/plugins/modules/prism/ntnx_vms.py new file mode 100644 index 000000000..aa81fffba --- /dev/null +++ b/plugins/modules/prism/ntnx_vms.py @@ -0,0 +1,269 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Prem Karat +# 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: "1.0.0" + +description: Create, Update, Delete, Power-on, Power-off Nutanix VM's + +options: + nutanix_host: + description: + - PC hostname or IP address + type: str + required: True + nutanix_port: + description: + - PC port + type: str + default: 9440 + required: False + nutanix_username: + description: + - PC username + type: str + required: True + nutanix_password: + description: + - PC password; + required: True + type: str + 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 + wait_timeout: + description: This is the wait_timeout description. + required: false + type: int + default: 300 + wait: + description: This is the wait description. + required: false + type: bool + default: true + name: + description: VM Name + required: true + type: str + desc: + description: A description for VM. + required: false + type: str + project: + description: Name or UUID of the project. + required: false + suboptions: + name: + description: + - Project Name + - Mutually exclusive with C(uuid) + type: str + required: true + uuid: + description: + - Project UUID + - Mutually exclusive with C(name) + type: str + required: true + cluster: + description: Name or UUID of the cluster on which the VM will be placed. + required: true + suboptions: + name: + description: + - Cluster Name + - Mutually exclusive with C(uuid) + type: str + required: true + uuid: + description: + - Cluster UUID + - Mutually exclusive with C(name) + type: str + required: true + vcpus: + description: Number of vCPUs + required: false + type: int + default: 1 + cores_per_vcpu: + description: This is the num_vcpus_per_socket. + type: int + default: 1 + memory_gb: + description: Memory size in GB + type: int + default: 1 + memory_overcommit_enabled: + description: This is the memory_overcommit_enabled description + type: bool + default: false + networks: + type: list + elements: dict + required: False + suboptions: + subnet: + description: Name or UUID of the subnet to which the VM should be connnected. + suboptions: + name: + description: + - Cluster Name + - Mutually exclusive with C(uuid) + type: str + required: true + uuid: + description: + - Cluster UUID + - Mutually exclusive with C(name) + type: str + required: true + private_ip: + description: Optionally assign static IP to the VM. + type: str + required: False + connected: + type: bool + required: False + default: True + disks: + description: Disks attached to the VM + type: list + elements: dict + default: [] + suboptions: + type: + description: 'CDROM or DISK' + choices: [ 'CDROM', 'DISK' ] + default: disk + type: str + size_gb: + description: + - The Disk Size in GB. + - This option is applicable for only disk type above. + type: int + bus: + description: 'Bus type of the device' + choices: [ 'SCSI', 'PCI', 'SATA', 'IDE' ] for disk type. + choices: [ 'SATA', 'IDE' ] for cdrom type. + type: str + storage_container: + description: + - Mutually exclusive with C(clone_image) and C(empty_cdrom) + suboptions: + name: + description: + - Storage containter Name + - Mutually exclusive with C(uuid) + type: str + required: true + uuid: + description: + - Storage container UUID + - Mutually exclusive with C(name) + type: str + required: true + clone_image: + description: + - Mutually exclusive with C(storage_container) and C(empty_cdrom) + suboptions: + name: + description: + - Image Name + - Mutually exclusive with C(uuid) + type: str + required: true + uuid: + description: + - Image UUID + - Mutually exclusive with C(name) + type: str + required: true + empty_cdrom: + type: bool + description: Mutually exclusive with C(clone_image) and C(storage_container) + boot_config: + description: + - Indicates whether the VM should use Secure boot, UEFI boot or Legacy boot. + required: False + suboptions: + boot_type: + description: Boot type of VM. + choices: [ "LEGACY", "UEFI", "SECURE_BOOT" ] + default: "LEGACY" + type: str + boot_order: + description: + - Applicable only for LEGACY boot_type + - Boot device order list + type: list + default: + - "CDROM", + - "DISK", + - "NETWORK" + guest_customization: + description: + type: dict + suboptions: + type: + type: str + choices: [ sysprep, cloud_init ] + default: sysprep + description: The Customization type + script_path: + type: str + required: True + description: The Absolute Script Path + is_overridable: + type: bool + default: False + description: Flag to allow override of customization during deployment. + timezone: + description: VM's hardware clock timezone in IANA TZDB format (America/Los_Angeles). + type: str + default: UTC + + categories: + type: list + elements: str + required: False +''' + + +from ..base_module import BaseModule +from ....plugins.module_utils.prism.vms import VM + + +def run_module(): + module_args = {} + module = BaseModule(argument_spec=module_args) + result = {} + vm = VM(module) + spec = vm.get_spec() + response = vm.create(spec) + result = response + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() From 7e97eb594257a1d8b48dab5b0f8e09b2f6fc0313 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 27 Jan 2022 07:34:08 +0530 Subject: [PATCH 03/17] change from _update_spec to _build_spec method Signed-off-by: Prem Karat --- plugins/module_utils/prism/vms.py | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 8d4e368b4..86559f086 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -20,27 +20,27 @@ class VM(Prism): def __init__(self, module): resource_type = '/vms' super().__init__(module, resource_type=resource_type) - self.update_spec_methods = { - "name": self._update_spec_name, - "desc": self._update_spec_desc, - "project": self._update_spec_project, - "cluster": self._update_spec_cluster, - "vcpus": self._update_spec_vcpus, - "cores_per_vcpu": self._update_spec_cores, - "memory_gb": self._update_spec_mem, - "networks": self._update_spec_networks, - "disks": self._update_spec_disks, - "boot_config": self._update_spec_boot_config, - "guest_customization": self._update_spec_gc, - "timezone": self._update_spec_timezone, - "categories": self._update_spec_categories + self.build_spec_methods = { + "name": self._build_spec_name, + "desc": self._build_spec_desc, + "project": self._build_spec_project, + "cluster": self._build_spec_cluster, + "vcpus": self._build_spec_vcpus, + "cores_per_vcpu": self._build_spec_cores, + "memory_gb": self._build_spec_mem, + "networks": self._build_spec_networks, + "disks": self._build_spec_disks, + "boot_config": self._build_spec_boot_config, + "guest_customization": self._build_spec_gc, + "timezone": self._build_spec_timezone, + "categories": self._build_spec_categories } def get_spec(self): spec = self._get_default_spec() for ansible_param, ansible_value in self.module.params.items(): - update_spec = self.update_spec_methods.get(ansible_param) - _, error = update_spec(spec, ansible_value) + build_spec_method = self.build_spec_methods.get(ansible_param) + _, error = build_spec_method(spec, ansible_value) if error: return None, error return spec, None @@ -104,15 +104,15 @@ def _get_default_disk_spec(self): } }) - def _update_spec_name(self, payload, value): + def _build_spec_name(self, payload, value): payload["spec"]["name"] = value return payload, None - def _update_spec_desc(self, payload, value): + def _build_spec_desc(self, payload, value): payload["spec"]["description"] = value return payload, None - def _update_spec_project(self, payload, param): + def _build_spec_project(self, payload, param): if 'name' in param: project = Project(self.module) name = param["name"] @@ -127,7 +127,7 @@ def _update_spec_project(self, payload, param): payload["metadata"]["project_reference"]["uuid"] = uuid return payload, None - def _update_spec_cluster(self, payload, param): + def _build_spec_cluster(self, payload, param): if 'name' in param: cluster = Cluster(self.module) name = param["name"] @@ -142,19 +142,19 @@ def _update_spec_cluster(self, payload, param): payload["spec"]["cluster_reference"]["uuid"] = uuid return payload, None - def _update_spec_vcpus(self, payload, value): + def _build_spec_vcpus(self, payload, value): payload["spec"]["resources"]["num_sockets"] = value return payload, None - def _update_spec_cores(self, payload, value): + def _build_spec_cores(self, payload, value): payload["spec"]["resources"]["num_vcpus_per_socket"] = value return payload, None - def _update_spec_mem(self, payload, value): + def _build_spec_mem(self, payload, value): payload["spec"]["resources"]["memory_size_mib"] = value * 1024 return payload, None - def _update_spec_networks(self, payload, networks): + def _build_spec_networks(self, payload, networks): nics = [] for network in networks: nic = self._get_default_network_spec() @@ -183,7 +183,7 @@ def _update_spec_networks(self, payload, networks): payload["spec"]["resources"]["nic_list"] = nics return payload, None - def _update_spec_disks(self, payload, vdisks): + def _build_spec_disks(self, payload, vdisks): disks = [] scsi_index = sata_index = pci_index = ide_index = 0 @@ -254,7 +254,7 @@ def _update_spec_disks(self, payload, vdisks): payload["spec"]["resources"]["disk_list"] = disks return payload, None - def _update_spec_boot_config(self, payload, param): + def _build_spec_boot_config(self, payload, param): boot_config = payload["spec"]["resources"]["boot_config"] if 'LEGACY' in param["boot_config"]["boot_type"] and 'boot_order' in param["boot_config"]: boot_config["boot_device_order_list"] = param["boot_config"]["boot_order"] @@ -266,7 +266,7 @@ def _update_spec_boot_config(self, payload, param): boot_config.pop("boot_device_order_list") payload["spec"]["resources"]["machine_type"] = "Q35" - def _update_spec_gc(self, payload, param): + def _build_spec_gc(self, payload, param): if 'script_path' in param["guest_customization"]: fpath = param["guest_customization"]["script_path"] @@ -295,11 +295,11 @@ def _update_spec_gc(self, payload, param): return payload, None - def _update_spec_timezone(self, payload, value): + def _build_spec_timezone(self, payload, value): payload["spec"]["resources"]["hardware_clock_timezone"] = value return payload, None - def _update_spec_categories(self, payload, value): + def _build_spec_categories(self, payload, value): payload["metadata"]["categories_mapping"] = value payload["metadata"]["use_categories_mapping"] = True return payload, None From 9848a026a0f41cbea0af46db1b58c3d7cad912da Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 27 Jan 2022 07:53:16 +0530 Subject: [PATCH 04/17] fix build spec for boot_config for UEFI and SECURE_BOOT Signed-off-by: Prem Karat --- plugins/module_utils/prism/vms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 86559f086..9378052c9 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -261,9 +261,11 @@ def _build_spec_boot_config(self, payload, param): elif "UEFI" in param["boot_config"]["boot_type"]: boot_config.pop("boot_device_order_list") + boot_config["boot_type"] = "UEFI" elif "SECURE_BOOT" in param["boot_config"]["boot_type"]: boot_config.pop("boot_device_order_list") + boot_config["boot_type"] = "SECURE_BOOT" payload["spec"]["resources"]["machine_type"] = "Q35" def _build_spec_gc(self, payload, param): From df13822342459972323e552463f5c58bb868ed97 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 27 Jan 2022 07:59:06 +0530 Subject: [PATCH 05/17] fix build spec for guest_customization Signed-off-by: Prem Karat --- plugins/module_utils/prism/vms.py | 41 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 9378052c9..81796594d 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -269,31 +269,32 @@ def _build_spec_boot_config(self, payload, param): payload["spec"]["resources"]["machine_type"] = "Q35" def _build_spec_gc(self, payload, param): - if 'script_path' in param["guest_customization"]: - fpath = param["guest_customization"]["script_path"] + fpath = param["guest_customization"]["script_path"] - if not os.path.exists(fpath): - error = "File not found: {}".format(fpath) - return None, error + if not os.path.exists(fpath): + error = "File not found: {}".format(fpath) + return None, error - with open(fpath, "rb", encoding="utf_8") as f: - content = base64.b64encode(f.read()) + with open(fpath, "rb", encoding="utf_8") as f: + content = base64.b64encode(f.read()) - gc_spec = payload["spec"]["resources"]["guest_customization"] + gc_spec = payload["spec"]["resources"]["guest_customization"] - if 'sysprep' in param["type"]: - gc_spec = { - "sysprep": { - "install_type": "PREPARED", - "unattend_xml": content - } - } - elif 'cloud_init' in param["type"]: - gc_spec = { - "cloud_init": {"user_data": content} + if 'sysprep' in param["type"]: + gc_spec = { + "sysprep": { + "install_type": "PREPARED", + "unattend_xml": content } - if 'is_overridable' in param: - gc_spec["is_overridable"] = param["is_overridable"] + } + + elif 'cloud_init' in param["type"]: + gc_spec = { + "cloud_init": {"user_data": content} + } + + if 'is_overridable' in param: + gc_spec["is_overridable"] = param["is_overridable"] return payload, None From a9f9e8f5fe755f4ce85ee4f35e4641780913a092 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Thu, 27 Jan 2022 18:13:27 +0530 Subject: [PATCH 06/17] 1. enhance entity for additional error handling 2. fix errors in vms sdk 3. implement vms ansible module Signed-off-by: Prem Karat --- plugins/module_utils/entity.py | 9 +- plugins/module_utils/prism/tasks.py | 45 +++++ plugins/module_utils/prism/vms.py | 4 +- plugins/modules/prism/ntnx_vms.py | 271 +++++++++++++++++++++------- 4 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 plugins/module_utils/prism/tasks.py diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index b2d16f90e..11581a2a8 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -103,9 +103,16 @@ def _fetch_url(self, url, method, data=None, timeout=30): timeout=timeout) status_code = info.get("status") + self.module.debug('API Response code: {}'.format(status_code)) body = resp.read() if resp else info.get("body") try: resp_json = json.loads(to_text(body)) if body else None except ValueError: resp_json = None - return resp_json, status_code + + if 199 < status_code < 300: + err = None + else: + err = info.get("msg", "Refer error detail in response") + status = {"error": err, "code": status_code} + return resp_json, status diff --git a/plugins/module_utils/prism/tasks.py b/plugins/module_utils/prism/tasks.py new file mode 100644 index 000000000..da015e1e3 --- /dev/null +++ b/plugins/module_utils/prism/tasks.py @@ -0,0 +1,45 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from os import stat +import time + +from .prism import Prism + +class Task(Prism): + def __init__(self, module): + resource_type = '/tasks' + super().__init__(module, resource_type=resource_type) + + def create(self, data=None, endpoint=None, query=None, timeout=30): + raise NotImplementedError('Create not permitted') + + def update(self, data=None, uuid=None, endpoint=None, query=None, timeout=30): + raise NotImplementedError('Update not permitted') + + def delete(self, uuid=None, endpoint=None, query=None, timeout=30): + raise NotImplementedError('Delete not permitted') + + def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): + raise NotImplementedError('List not permitted') + + def get_uuid(self, name): + raise NotImplementedError('get_uuid not permitted') + + def wait_for_completion(self, uuid): + state = "" + while state != "SUCCEEDED": + time.sleep(2) + response, status = self.read(uuid) + if status["error"]: + return response, status + + state = response.get("status") + if state == "FAILED": + status = { + "error": response["error_detail"], + "code": response["error_code"] + } + return response, status + + return response, status diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 81796594d..71ef5dbd1 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -8,7 +8,7 @@ import base64 import os -from plugins.module_utils.prism.clusters import Cluster +from .clusters import Cluster from .prism import Prism from .projects import Project from .subnets import Subnet @@ -118,7 +118,7 @@ def _build_spec_project(self, payload, param): name = param["name"] uuid = project.get_uuid(name) if not uuid: - error = "Failed to get UUID for project name: {}".format(name]) + error = "Failed to get UUID for project name: {}".format(name) return None, error elif 'uuid' in param: diff --git a/plugins/modules/prism/ntnx_vms.py b/plugins/modules/prism/ntnx_vms.py index aa81fffba..bb32738f3 100644 --- a/plugins/modules/prism/ntnx_vms.py +++ b/plugins/modules/prism/ntnx_vms.py @@ -22,41 +22,45 @@ options: nutanix_host: description: - - PC hostname or IP address + - PC hostname or IP address type: str - required: True + required: true nutanix_port: description: - - PC port + - PC port type: str default: 9440 - required: False + required: false nutanix_username: description: - - PC username + - PC username type: str - required: True + required: true nutanix_password: description: - - PC password; - required: True + - PC password; + required: true type: str validate_certs: description: - - Set value to C(False) to skip validation for self signed certificates - - This is not recommended for production setup + - Set value to C(False) to skip validation for self signed certificates + - This is not recommended for production setup type: bool - default: True - wait_timeout: - description: This is the wait_timeout description. - required: false - type: int - default: 300 + default: true + state: + description: + - Specify state of Virtual Machine + - If C(state) is set to C(present) the VM is created. + - If C(state) is set to C(absent) and the VM exists in the cluster, VM with specified name is removed. + choices: + - present + - absent + type: str + default: present wait: description: This is the wait description. required: false - type: bool - default: true + default: false name: description: VM Name required: true @@ -68,6 +72,7 @@ project: description: Name or UUID of the project. required: false + type: dict suboptions: name: description: @@ -82,8 +87,9 @@ type: str required: true cluster: - description: Name or UUID of the cluster on which the VM will be placed. - required: true + description: + - Name or UUID of the cluster on which the VM will be placed. + required: false suboptions: name: description: @@ -98,70 +104,78 @@ type: str required: true vcpus: - description: Number of vCPUs + description: + - Number number of sockets. required: false type: int default: 1 cores_per_vcpu: - description: This is the num_vcpus_per_socket. + description: + - This is the number of vcpus per socket. + required: false type: int default: 1 memory_gb: - description: Memory size in GB + description: + - Memory size in GB + required: false type: int default: 1 - memory_overcommit_enabled: - description: This is the memory_overcommit_enabled description - type: bool - default: false networks: + description: + - list of subnets to which the VM needs to connect to. type: list elements: dict - required: False + required: false suboptions: subnet: - description: Name or UUID of the subnet to which the VM should be connnected. + description: + - Name or UUID of the subnet to which the VM should be connnected. suboptions: name: description: - - Cluster Name + - Subnet Name - Mutually exclusive with C(uuid) type: str required: true uuid: description: - - Cluster UUID + - Subnet UUID - Mutually exclusive with C(name) type: str required: true private_ip: - description: Optionally assign static IP to the VM. + description: + - Optionally assign static IP to the VM. type: str required: False - connected: + is_connected: + description: + - connect or disconnect the VM to the subnet. type: bool required: False default: True disks: - description: Disks attached to the VM + description: + - List of disks attached to the VM type: list elements: dict - default: [] suboptions: type: - description: 'CDROM or DISK' + description: + - 'CDROM or DISK' choices: [ 'CDROM', 'DISK' ] - default: disk + default: DISK type: str size_gb: description: - The Disk Size in GB. - - This option is applicable for only disk type above. + - This option is applicable for only DISK type above. type: int bus: description: 'Bus type of the device' - choices: [ 'SCSI', 'PCI', 'SATA', 'IDE' ] for disk type. - choices: [ 'SATA', 'IDE' ] for cdrom type. + choices: [ 'SCSI', 'PCI', 'SATA', 'IDE' ] for DISK type. + choices: [ 'SATA', 'IDE' ] for CDROM type. type: str storage_container: description: @@ -197,14 +211,16 @@ required: true empty_cdrom: type: bool - description: Mutually exclusive with C(clone_image) and C(storage_container) + description: + - Mutually exclusive with C(clone_image) and C(storage_container) boot_config: description: - Indicates whether the VM should use Secure boot, UEFI boot or Legacy boot. required: False suboptions: boot_type: - description: Boot type of VM. + description: + - Boot type of VM. choices: [ "LEGACY", "UEFI", "SECURE_BOOT" ] default: "LEGACY" type: str @@ -213,51 +229,178 @@ - Applicable only for LEGACY boot_type - Boot device order list type: list - default: - - "CDROM", - - "DISK", - - "NETWORK" + default: ["CDROM", "DISK", "NETWORK"] guest_customization: description: + - cloud-init or sysprep guest customization type: dict + required: false suboptions: type: + description: + - cloud-init or sysprep type type: str - choices: [ sysprep, cloud_init ] + choices: [sysprep, cloud-init] default: sysprep - description: The Customization type + required: true script_path: - type: str - required: True - description: The Absolute Script Path + description: + - Absolute file path to the script. + type: path + required: true is_overridable: + description: + - Flag to allow override of customization during deployment. type: bool - default: False - description: Flag to allow override of customization during deployment. + default: false + required: false timezone: - description: VM's hardware clock timezone in IANA TZDB format (America/Los_Angeles). + description: + - VM's hardware clock timezone in IANA TZDB format (America/Los_Angeles). type: str default: UTC - + required: false categories: - type: list - elements: str - required: False + description: + - categories to be attached to the VM. + type: dict + required: false +''' + +EXAMPLES = r''' +# TODO +''' + +RETURN = r''' +# TODO ''' +from ansible.module_utils.basic import env_fallback + from ..base_module import BaseModule from ....plugins.module_utils.prism.vms import VM +from ....plugins.module_utils.prism.tasks import Task def run_module(): - module_args = {} - module = BaseModule(argument_spec=module_args) - result = {} + entity_by_spec = dict( + name=dict(type='str'), + uuid=dict(type='str'), + mutually_exclusive=[('name', 'uuid'),], + required_one_of=[('name', 'uuid'),], + ) + + network_spec = dict( + subnet=dict(type='dict', options=entity_by_spec), + private_ip=dict(type='str'), + is_connected=dict(type='bool', default=True), + ) + + disk_spec = dict( + # TODO + # if type='CDROM', then bus==['SATA', 'IDE'] + # this check needs to be implemented. + type=dict(type='str', choices=['CDROM', 'DISK'], default='DISK'), + size_gb=dict(type='int'), + bus=dict(type='str', choices=['SCSI', 'PCI', 'SATA', 'IDE'], default='SCSI'), + storage_container=dict(type='dict', options=entity_by_spec), + clone_image=dict(type='dict', options=entity_by_spec), + empty_cdrom=dict(type='bool'), + mutually_exclusive=[ + ('storage_container', 'empty_cdrom', 'clone_image'), + ('empty_cdrom', 'size_gb') + ], + required_one_of=[('storage_container', 'empty_cdrom', 'clone_image'),], + required_by={ + 'storage_container': 'size_gb', + 'clone_image': 'size_gb', + }, + + ) + + boot_config_spec = dict( + # TODO + # if boot_type=UEFI OR SECURE_BOOT, then boot_order is not required. + boot_type=dict(type='str', choices=[ "LEGACY", "UEFI", "SECURE_BOOT" ]), + boot_order=dict(type='list', elements=str, default=["CDROM", "DISK", "NETWORK"]), + ) + + gc_spec = dict( + type=dict(type='str', choices=['cloud-init', 'sysprep'], required=True), + script_path=dict(type='path', required=True), + is_overridable=dict(type='bool', default=False), + ) + + module_args = dict( + nutanix_host=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_HOST"])), + nutanix_port=dict(default="9440", type='str'), + nutanix_username=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"])), + nutanix_password=dict(type='str', required=True, no_log=True, fallback=(env_fallback, ["NUTANIX_PASSWORD"])), + validate_certs=dict(type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"])), + state=dict(type=str, choices=['present', 'absent'], default='present'), + wait=dict(type=bool, default=True), + name=dict(type='str', required=True), + desc=dict(type='str'), + project=dict(type='dict', options=entity_by_spec), + cluster=dict(type='dict', options=entity_by_spec), + vcpus=dict(type='int', default=1), + cores_per_vcpu=dict(type='int', default=1), + memory_gb=dict(type='int', default=1), + networks=dict(type='list', elements=dict, options=network_spec), + disks=dict(type='list', elements=dict, options=disk_spec), + boot_config=dict(type='dict', options=boot_config_spec), + guest_customization=dict(type='dict', options=gc_spec), + timezone=dict(type='str', default="UTC"), + categories=dict(type='dict'), + ) + + module = BaseModule(argument_spec=module_args, + supports_check_mode=True) + + result = { + 'changed': False, + 'error': None, + 'response': None, + 'vm_uuid': None, + 'task_uuid': None + } + vm = VM(module) - spec = vm.get_spec() - response = vm.create(spec) - result = response + + module.debug('Generating VM spec') + spec, error = vm.get_spec() + if error: + module.debug(error) + result['error'] = error + module.fail_json(**result) + + if module.check_mode: + result['response'] = spec + return module.exit_json(**result) + + module.debug('VM spec: {}'.format(spec)) + + resp, status = vm.create(spec) + if status['error']: + module.debug(status["error"]) + result["error"] = status["error"] + result["response"] = resp + module.fail_json(**result) + + task_uuid = resp["status"]["execution_context"]["task_uuid"] + result['task_uuid'] = task_uuid + result["vm_uuid"] = resp["metadata"]["uuid"] + + if module.param["wait"]: + task = Task(module) + resp, status = task.wait_for_completion(task_uuid) + if status['error']: + module.debug(status["error"]) + result["error"] = status["error"] + result["response"] = resp + module.fail_json(**result) + module.exit_json(**result) From f91f7695cc04b7a5e89faec6d4b1d5ce0a764bb3 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Thu, 27 Jan 2022 18:37:21 +0400 Subject: [PATCH 07/17] fixes --- .gitignore | 180 ++++++++++++++++++ create_vm.yml | 46 +++++ delete_vm.yml | 23 +++ plugins/__init__.py | 0 plugins/module_utils/__init__.py | 0 .../{modules => module_utils}/base_module.py | 0 plugins/module_utils/prism/__init__.py | 0 plugins/module_utils/prism/vms.py | 22 +-- plugins/modules/__init__.py | 0 plugins/modules/{prism => }/ntnx_vms.py | 27 ++- 10 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 .gitignore create mode 100644 create_vm.yml create mode 100644 delete_vm.yml create mode 100644 plugins/__init__.py create mode 100644 plugins/module_utils/__init__.py rename plugins/{modules => module_utils}/base_module.py (100%) create mode 100644 plugins/module_utils/prism/__init__.py create mode 100644 plugins/modules/__init__.py rename plugins/modules/{prism => }/ntnx_vms.py (95%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8d4f6540b --- /dev/null +++ b/.gitignore @@ -0,0 +1,180 @@ +Skip to content +Search or jump to… +Pull requests +Issues +Marketplace +Explore + +@Gevorg-Khachatryan-97 +nutanix +/ +nutanix.ansible +Internal +Code +Issues +12 +Pull requests +Actions +Projects +1 +Wiki +Security +Insights +Settings +nutanix.ansible/.gitignore +@kumarsarath588 +kumarsarath588 Add CICD github actions +Latest commit 0765ecc 6 days ago + History + 1 contributor +136 lines (109 sloc) 1.8 KB + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + + +# emacs stuff +*~ + +# vs code configs +.vscode +© 2022 GitHub, Inc. +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About +Loading complete \ No newline at end of file diff --git a/create_vm.yml b/create_vm.yml new file mode 100644 index 000000000..5bfa4d1ac --- /dev/null +++ b/create_vm.yml @@ -0,0 +1,46 @@ +--- +- name: Auto Generated Playbook + hosts: localhost + vars_files: credentials.yml + gather_facts: false + collections: + - nutanix.ncp + tasks: + - name: create Vm + ntnx_vms: + state: present + auth: + credentials: '{{credentials}}' + url: '{{config.ip_address}}:{{config.port}}' + nutanix_host: '{{config.ip_address}}' + nutanix_username: '{{credentials.username}}' + nutanix_password: '{{credentials.password}}' + name: "test_2" + desc: "test_description" + categories: + AppType: + - "Apache_Spark" + cluster: + uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" + networks: + - is_connected: True + subnet: + name: "vlan.800" + disks: + - type: "DISK" + size_gb: 30 + bus: "SATA" + clone_image: + name: "SQLServer2014SP2-FullSlipstream-x64-ENU.iso" + vcpus: 1 + cores_per_vcpu: 1 + memory_gb: 1 + guest_customization: + type: "cloud_init" + script_path: "test.json" + is_overridable: True + register: output + + - name: output of list Subnets + debug: + msg: '{{ output }}' diff --git a/delete_vm.yml b/delete_vm.yml new file mode 100644 index 000000000..e464edd8a --- /dev/null +++ b/delete_vm.yml @@ -0,0 +1,23 @@ +--- +- name: Auto Generated Playbook + hosts: localhost + vars_files: credentials.yml + gather_facts: false + collections: + - nutanix.ncp + tasks: + - name: delete Vm + ntnx_vms: + state: absent + auth: + credentials: '{{credentials}}' + url: '{{config.ip_address}}:{{config.port}}' + nutanix_host: '{{config.ip_address}}' + nutanix_username: '{{credentials.username}}' + nutanix_password: '{{credentials.password}}' + uuid: "63f7115e-e071-4dc3-b417-69fe6d6ff37f" + register: output + + - name: output of delete vm + debug: + msg: '{{ output }}' diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/modules/base_module.py b/plugins/module_utils/base_module.py similarity index 100% rename from plugins/modules/base_module.py rename to plugins/module_utils/base_module.py diff --git a/plugins/module_utils/prism/__init__.py b/plugins/module_utils/prism/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 71ef5dbd1..457cb9c60 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -40,9 +40,8 @@ def get_spec(self): spec = self._get_default_spec() for ansible_param, ansible_value in self.module.params.items(): build_spec_method = self.build_spec_methods.get(ansible_param) - _, error = build_spec_method(spec, ansible_value) - if error: - return None, error + if build_spec_method and ansible_value: + _, error = build_spec_method(spec, ansible_value) return spec, None def _get_default_spec(self): @@ -165,7 +164,7 @@ def _build_spec_networks(self, payload, networks): if 'is_connected' in network: nic["is_connected"] = network["is_connected"] - if 'name' in network["subnet"]: + if network.get("subnet") and 'name' in network["subnet"]: subnet = Subnet(self.module) name = network["subnet"]["name"] uuid = subnet.get_uuid(name) @@ -269,19 +268,18 @@ def _build_spec_boot_config(self, payload, param): payload["spec"]["resources"]["machine_type"] = "Q35" def _build_spec_gc(self, payload, param): - fpath = param["guest_customization"]["script_path"] + fpath = param["script_path"] if not os.path.exists(fpath): error = "File not found: {}".format(fpath) return None, error - with open(fpath, "rb", encoding="utf_8") as f: + with open(fpath, "rb") as f: content = base64.b64encode(f.read()) - - gc_spec = payload["spec"]["resources"]["guest_customization"] + gc_spec = {"guest_customization": {}} if 'sysprep' in param["type"]: - gc_spec = { + gc_spec["guest_customization"] = { "sysprep": { "install_type": "PREPARED", "unattend_xml": content @@ -289,13 +287,13 @@ def _build_spec_gc(self, payload, param): } elif 'cloud_init' in param["type"]: - gc_spec = { + gc_spec["guest_customization"] = { "cloud_init": {"user_data": content} } if 'is_overridable' in param: - gc_spec["is_overridable"] = param["is_overridable"] - + gc_spec["guest_customization"]["is_overridable"] = param["is_overridable"] + payload["spec"]["resources"].update(gc_spec) return payload, None def _build_spec_timezone(self, payload, value): diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/modules/prism/ntnx_vms.py b/plugins/modules/ntnx_vms.py similarity index 95% rename from plugins/modules/prism/ntnx_vms.py rename to plugins/modules/ntnx_vms.py index bb32738f3..176595ce7 100644 --- a/plugins/modules/prism/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -11,7 +11,7 @@ DOCUMENTATION = r''' --- -module: nutanix_vm +module: ntnx_vm short_description: VM module which suports VM CRUD operations @@ -232,15 +232,15 @@ default: ["CDROM", "DISK", "NETWORK"] guest_customization: description: - - cloud-init or sysprep guest customization + - cloud_init or sysprep guest customization type: dict required: false suboptions: type: description: - - cloud-init or sysprep type + - cloud_init or sysprep type type: str - choices: [sysprep, cloud-init] + choices: [sysprep, cloud_init] default: sysprep required: true script_path: @@ -278,17 +278,15 @@ from ansible.module_utils.basic import env_fallback -from ..base_module import BaseModule -from ....plugins.module_utils.prism.vms import VM -from ....plugins.module_utils.prism.tasks import Task +from ..module_utils.base_module import BaseModule +from ..module_utils.prism.vms import VM +from ..module_utils.prism.tasks import Task def run_module(): entity_by_spec = dict( name=dict(type='str'), uuid=dict(type='str'), - mutually_exclusive=[('name', 'uuid'),], - required_one_of=[('name', 'uuid'),], ) network_spec = dict( @@ -327,7 +325,7 @@ def run_module(): ) gc_spec = dict( - type=dict(type='str', choices=['cloud-init', 'sysprep'], required=True), + type=dict(type='str', choices=['cloud_init', 'sysprep'], required=True), script_path=dict(type='path', required=True), is_overridable=dict(type='bool', default=False), ) @@ -338,8 +336,8 @@ def run_module(): nutanix_username=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"])), nutanix_password=dict(type='str', required=True, no_log=True, fallback=(env_fallback, ["NUTANIX_PASSWORD"])), validate_certs=dict(type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"])), - state=dict(type=str, choices=['present', 'absent'], default='present'), - wait=dict(type=bool, default=True), + state=dict(type='str', choices=['present', 'absent'], default='present'), + wait=dict(type='bool', default=True), name=dict(type='str', required=True), desc=dict(type='str'), project=dict(type='dict', options=entity_by_spec), @@ -363,7 +361,8 @@ def run_module(): 'error': None, 'response': None, 'vm_uuid': None, - 'task_uuid': None + 'task_uuid': None, + 'msg': '' } vm = VM(module) @@ -392,7 +391,7 @@ def run_module(): result['task_uuid'] = task_uuid result["vm_uuid"] = resp["metadata"]["uuid"] - if module.param["wait"]: + if module.params["wait"]: task = Task(module) resp, status = task.wait_for_completion(task_uuid) if status['error']: From 36ac4fdcabda64ecd56086683a790802d213f2da Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Thu, 27 Jan 2022 19:21:26 +0400 Subject: [PATCH 08/17] fixes --- plugins/module_utils/base_module.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/base_module.py b/plugins/module_utils/base_module.py index b568b4189..9b77a4d90 100644 --- a/plugins/module_utils/base_module.py +++ b/plugins/module_utils/base_module.py @@ -12,7 +12,6 @@ class BaseModule(AnsibleModule): argument_spec = dict( action=dict(type="str", required=True, aliases=["state"]), - auth=dict(type="dict", required=True), wait=dict(type="bool", required=False, default=True), wait_timeout=dict(type="int", required=False, default=300), validate_certs=dict(type="bool", required=False, default=False)) @@ -23,7 +22,7 @@ def __init__(self, **kwargs): else: kwargs["argument_spec"] = self.argument_spec - if not kwargs.get["supports_check_mode"]: - kwargs["support_check_mode"] = True + if not kwargs.get("supports_check_mode"): + kwargs["supports_check_mode"] = True super(BaseModule, self).__init__(**kwargs) \ No newline at end of file From 29738b650d88d2b51f3d1cbc5037466b3bce0839 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Thu, 27 Jan 2022 21:02:53 +0400 Subject: [PATCH 09/17] fixes --- plugins/module_utils/base_module.py | 2 +- plugins/module_utils/prism/vms.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/base_module.py b/plugins/module_utils/base_module.py index 9b77a4d90..9932bc469 100644 --- a/plugins/module_utils/base_module.py +++ b/plugins/module_utils/base_module.py @@ -25,4 +25,4 @@ def __init__(self, **kwargs): if not kwargs.get("supports_check_mode"): kwargs["supports_check_mode"] = True - super(BaseModule, self).__init__(**kwargs) \ No newline at end of file + super(BaseModule, self).__init__(**kwargs) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 457cb9c60..e9421810f 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -234,7 +234,6 @@ def _build_spec_disks(self, payload, vdisks): disk["storage_config"]["storage_container_reference"]["uuid"] = uuid elif 'clone_image' in vdisk: - disk.pop('storage_config') if 'name' in vdisk["clone_image"]: image = Image(self.module) name = vdisk["clone_image"]["name"] @@ -247,6 +246,10 @@ def _build_spec_disks(self, payload, vdisks): uuid = vdisk["clone_image"]["uuid"] disk["data_source_reference"]["uuid"] = uuid + if not disk["storage_config"]["storage_container_reference"]["uuid"]: + disk.pop('storage_config') + if not disk["data_source_reference"]["uuid"]: + disk.pop('data_source_reference') disks.append(disk) @@ -255,17 +258,18 @@ def _build_spec_disks(self, payload, vdisks): def _build_spec_boot_config(self, payload, param): boot_config = payload["spec"]["resources"]["boot_config"] - if 'LEGACY' in param["boot_config"]["boot_type"] and 'boot_order' in param["boot_config"]: - boot_config["boot_device_order_list"] = param["boot_config"]["boot_order"] + if 'LEGACY' == param["boot_type"] and 'boot_order' in param: + boot_config["boot_device_order_list"] = param["boot_order"] - elif "UEFI" in param["boot_config"]["boot_type"]: + elif "UEFI" == param["boot_type"]: boot_config.pop("boot_device_order_list") boot_config["boot_type"] = "UEFI" - elif "SECURE_BOOT" in param["boot_config"]["boot_type"]: + elif "SECURE_BOOT" == param["boot_type"]: boot_config.pop("boot_device_order_list") boot_config["boot_type"] = "SECURE_BOOT" payload["spec"]["resources"]["machine_type"] = "Q35" + return payload, None def _build_spec_gc(self, payload, param): fpath = param["script_path"] From 9acddbae92917a875e21f2acbbabfe8a46fcb793 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Thu, 27 Jan 2022 21:59:49 +0400 Subject: [PATCH 10/17] fixes --- README.md | 15 ++++++--------- create_vm.yml => examples/create_vm.yml | 8 +++----- delete_vm.yml => examples/delete_vm.yml | 0 3 files changed, 9 insertions(+), 14 deletions(-) rename create_vm.yml => examples/create_vm.yml (80%) rename delete_vm.yml => examples/delete_vm.yml (100%) diff --git a/README.md b/README.md index e1d4c0503..6c7d535cf 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,13 @@ Ansible collections to automate Nutanix Cloud Platform (ncp). # Building and installing the collection locally ``` ansible-galaxy collection build -ansible-galaxy collection install nutanix.ncp-1.0.0.tar.gz +ansible-galaxy collection install nutanix-ncp-1.0.0.tar.gz ``` _Add `--force` option for rebuilding or reinstalling to overwrite existing data_ # Included modules ``` -ncp_prism_image_info -ncp_prism_image -ncp_prism_vm_info -ncp_prism_vm +ntnx_vms ``` # Inventory plugin @@ -31,10 +28,10 @@ ansible-doc nutanix.ncp. collections: - nutanix.ncp tasks: - - ncp_prism_vm_info: - pc_hostname: {{ pc_hostname }} - pc_username: {{ pc_username }} - pc_password: {{ pc_password }} + - ntnx_vms: + nutanix_host: '{{config.ip_address}}' + nutanix_username: '{{credentials.username}}' + nutanix_password: '{{credentials.password}}' validate_certs: False register: result - debug: diff --git a/create_vm.yml b/examples/create_vm.yml similarity index 80% rename from create_vm.yml rename to examples/create_vm.yml index 5bfa4d1ac..7fe1f5e42 100644 --- a/create_vm.yml +++ b/examples/create_vm.yml @@ -9,9 +9,6 @@ - name: create Vm ntnx_vms: state: present - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' nutanix_host: '{{config.ip_address}}' nutanix_username: '{{credentials.username}}' nutanix_password: '{{credentials.password}}' @@ -21,7 +18,8 @@ AppType: - "Apache_Spark" cluster: - uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" +# uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" + name: "auto_cluster_prod_1aa888141361" networks: - is_connected: True subnet: @@ -31,7 +29,7 @@ size_gb: 30 bus: "SATA" clone_image: - name: "SQLServer2014SP2-FullSlipstream-x64-ENU.iso" + name: "CentOS-7-cloudinit" vcpus: 1 cores_per_vcpu: 1 memory_gb: 1 diff --git a/delete_vm.yml b/examples/delete_vm.yml similarity index 100% rename from delete_vm.yml rename to examples/delete_vm.yml From 9d81d98ddd04bb7c422de9102c042b34da3b7bd0 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Fri, 28 Jan 2022 12:21:39 +0530 Subject: [PATCH 11/17] Bug fixes and enhancements ansbile spec validatar to remove null values enahanced api call error handling fixed bugs in vms moudule and vms sdk Signed-off-by: Prem Karat --- examples/create_vm.yml | 23 +++++++------- plugins/module_utils/entity.py | 11 +++++-- plugins/module_utils/prism/vms.py | 26 +++++++++------- plugins/module_utils/utils.py | 18 +++++++++++ plugins/modules/ntnx_vms.py | 51 ++++++++++++------------------- 5 files changed, 72 insertions(+), 57 deletions(-) create mode 100644 plugins/module_utils/utils.py diff --git a/examples/create_vm.yml b/examples/create_vm.yml index 7fe1f5e42..bf72cd4dc 100644 --- a/examples/create_vm.yml +++ b/examples/create_vm.yml @@ -1,7 +1,6 @@ --- -- name: Auto Generated Playbook +- name: VM Creation using all poissible options hosts: localhost - vars_files: credentials.yml gather_facts: false collections: - nutanix.ncp @@ -9,16 +8,16 @@ - name: create Vm ntnx_vms: state: present - nutanix_host: '{{config.ip_address}}' - nutanix_username: '{{credentials.username}}' - nutanix_password: '{{credentials.password}}' - name: "test_2" - desc: "test_description" + nutanix_host: '10.44.76.131' + nutanix_username: 'admin' + nutanix_password: 'Nutanix.123' + name: "ansible_automation_demo" + desc: "ansible_vm_description" categories: AppType: - "Apache_Spark" cluster: -# uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" + #uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" name: "auto_cluster_prod_1aa888141361" networks: - is_connected: True @@ -33,10 +32,10 @@ vcpus: 1 cores_per_vcpu: 1 memory_gb: 1 - guest_customization: - type: "cloud_init" - script_path: "test.json" - is_overridable: True + #guest_customization: + # type: "cloud_init" + # script_path: "test.json" + # is_overridable: True register: output - name: output of list Subnets diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index 11581a2a8..330bf5fc7 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -58,7 +58,7 @@ def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): def get_uuid(self, name): data = {"filter": f"name=={name}", "length": 1} resp, _ = self.list(data) - if resp.get("entities"): + if resp and resp.get("entities"): return resp["entities"][0]["metadata"]["uuid"] return None @@ -103,7 +103,6 @@ def _fetch_url(self, url, method, data=None, timeout=30): timeout=timeout) status_code = info.get("status") - self.module.debug('API Response code: {}'.format(status_code)) body = resp.read() if resp else info.get("body") try: resp_json = json.loads(to_text(body)) if body else None @@ -113,6 +112,12 @@ def _fetch_url(self, url, method, data=None, timeout=30): if 199 < status_code < 300: err = None else: - err = info.get("msg", "Refer error detail in response") + err=info.get("msg", "Status code != 2xx") + self.module.fail_json( + msg="Failed fetching URL: {}".format(url), + status_code=status_code, + error=err, + response=resp_json + ) status = {"error": err, "code": status_code} return resp_json, status diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index e9421810f..68956b4a2 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -42,6 +42,8 @@ def get_spec(self): build_spec_method = self.build_spec_methods.get(ansible_param) if build_spec_method and ansible_value: _, error = build_spec_method(spec, ansible_value) + if error: + return None, error return spec, None def _get_default_spec(self): @@ -157,14 +159,14 @@ def _build_spec_networks(self, payload, networks): nics = [] for network in networks: nic = self._get_default_network_spec() + if network.get('private_ip'): + nic["ip_endpoint_list"].append({ + "ip": network["private_ip"] + }) - if 'private_ip' in network: - nic["ip_endpoint_list"]["ip"] = network["private_ip"] - - if 'is_connected' in network: - nic["is_connected"] = network["is_connected"] + nic["is_connected"] = network["is_connected"] - if network.get("subnet") and 'name' in network["subnet"]: + if network.get("subnet", {}).get('name'): subnet = Subnet(self.module) name = network["subnet"]["name"] uuid = subnet.get_uuid(name) @@ -172,7 +174,7 @@ def _build_spec_networks(self, payload, networks): error = "Failed to get UUID for subnet name: {}".format(name) return None, error - elif 'uuid' in network["subnet"]: + elif network.get("subnet", {}).get('uuid'): uuid = network["subnet"]["uuid"] nic["subnet_reference"]["uuid"] = uuid @@ -246,10 +248,12 @@ def _build_spec_disks(self, payload, vdisks): uuid = vdisk["clone_image"]["uuid"] disk["data_source_reference"]["uuid"] = uuid - if not disk["storage_config"]["storage_container_reference"]["uuid"]: - disk.pop('storage_config') - if not disk["data_source_reference"]["uuid"]: - disk.pop('data_source_reference') + + if not disk.get("storage_config", {}).get("storage_container_reference", {}).get("uuid"): + disk.pop('storage_config', None) + + if not disk.get("data_source_reference", {}).get("uuid"): + disk.pop('data_source_reference', None) disks.append(disk) diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py new file mode 100644 index 000000000..f68238646 --- /dev/null +++ b/plugins/module_utils/utils.py @@ -0,0 +1,18 @@ +# This file is part of Ansible +# 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 + + +def remove_param_with_none_value(d): + for k, v in d.copy().items(): + if v is None: + d.pop(k) + elif isinstance(v, dict): + remove_param_with_none_value(v) + elif isinstance(v, list): + for e in v: + if isinstance(e, dict): + remove_param_with_none_value(e) + break diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 176595ce7..fb0fa65ee 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -281,47 +281,33 @@ from ..module_utils.base_module import BaseModule from ..module_utils.prism.vms import VM from ..module_utils.prism.tasks import Task +from ..module_utils.utils import remove_param_with_none_value def run_module(): entity_by_spec = dict( name=dict(type='str'), - uuid=dict(type='str'), + uuid=dict(type='str') ) network_spec = dict( subnet=dict(type='dict', options=entity_by_spec), - private_ip=dict(type='str'), - is_connected=dict(type='bool', default=True), + private_ip=dict(type='str', required=False), + is_connected=dict(type='bool', default=True) ) disk_spec = dict( - # TODO - # if type='CDROM', then bus==['SATA', 'IDE'] - # this check needs to be implemented. type=dict(type='str', choices=['CDROM', 'DISK'], default='DISK'), size_gb=dict(type='int'), bus=dict(type='str', choices=['SCSI', 'PCI', 'SATA', 'IDE'], default='SCSI'), storage_container=dict(type='dict', options=entity_by_spec), clone_image=dict(type='dict', options=entity_by_spec), - empty_cdrom=dict(type='bool'), - mutually_exclusive=[ - ('storage_container', 'empty_cdrom', 'clone_image'), - ('empty_cdrom', 'size_gb') - ], - required_one_of=[('storage_container', 'empty_cdrom', 'clone_image'),], - required_by={ - 'storage_container': 'size_gb', - 'clone_image': 'size_gb', - }, - + empty_cdrom=dict(type='bool') ) boot_config_spec = dict( - # TODO - # if boot_type=UEFI OR SECURE_BOOT, then boot_order is not required. boot_type=dict(type='str', choices=[ "LEGACY", "UEFI", "SECURE_BOOT" ]), - boot_order=dict(type='list', elements=str, default=["CDROM", "DISK", "NETWORK"]), + boot_order=dict(type='list', elements=str, default=["CDROM", "DISK", "NETWORK"]) ) gc_spec = dict( @@ -345,16 +331,21 @@ def run_module(): vcpus=dict(type='int', default=1), cores_per_vcpu=dict(type='int', default=1), memory_gb=dict(type='int', default=1), - networks=dict(type='list', elements=dict, options=network_spec), - disks=dict(type='list', elements=dict, options=disk_spec), + networks=dict(type='list', elements='dict', options=network_spec), + disks=dict(type='list', elements='dict', options=disk_spec), boot_config=dict(type='dict', options=boot_config_spec), guest_customization=dict(type='dict', options=gc_spec), timezone=dict(type='str', default="UTC"), - categories=dict(type='dict'), + categories=dict(type='dict') ) + mutually_exclusive = [("name", "uuid")] + module = BaseModule(argument_spec=module_args, - supports_check_mode=True) + supports_check_mode=True, + mutually_exclusive=mutually_exclusive) + + remove_param_with_none_value(module.params) result = { 'changed': False, @@ -362,7 +353,6 @@ def run_module(): 'response': None, 'vm_uuid': None, 'task_uuid': None, - 'msg': '' } vm = VM(module) @@ -372,33 +362,32 @@ def run_module(): if error: module.debug(error) result['error'] = error - module.fail_json(**result) + module.fail_json(msg="Failed generating VM Spec", **result) if module.check_mode: result['response'] = spec return module.exit_json(**result) - module.debug('VM spec: {}'.format(spec)) - resp, status = vm.create(spec) if status['error']: module.debug(status["error"]) result["error"] = status["error"] result["response"] = resp - module.fail_json(**result) + module.fail_json(msg="Failed creating VM", **result) task_uuid = resp["status"]["execution_context"]["task_uuid"] result['task_uuid'] = task_uuid result["vm_uuid"] = resp["metadata"]["uuid"] + result["changed"] = True - if module.params["wait"]: + if module.params.get("wait"): task = Task(module) resp, status = task.wait_for_completion(task_uuid) if status['error']: module.debug(status["error"]) result["error"] = status["error"] result["response"] = resp - module.fail_json(**result) + module.fail_json(msg="Failed creating VM", **result) module.exit_json(**result) From 82176b1ea795edf766535b7527710af665202db8 Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Fri, 28 Jan 2022 12:24:02 +0530 Subject: [PATCH 12/17] removed env values from yml file Signed-off-by: Prem Karat --- examples/create_vm.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/create_vm.yml b/examples/create_vm.yml index bf72cd4dc..9699c4d0e 100644 --- a/examples/create_vm.yml +++ b/examples/create_vm.yml @@ -8,9 +8,9 @@ - name: create Vm ntnx_vms: state: present - nutanix_host: '10.44.76.131' - nutanix_username: 'admin' - nutanix_password: 'Nutanix.123' + nutanix_host: '' + nutanix_username: '' + nutanix_password: '' name: "ansible_automation_demo" desc: "ansible_vm_description" categories: @@ -32,10 +32,10 @@ vcpus: 1 cores_per_vcpu: 1 memory_gb: 1 - #guest_customization: - # type: "cloud_init" - # script_path: "test.json" - # is_overridable: True + guest_customization: + type: "cloud_init" + script_path: "test.json" + is_overridable: True register: output - name: output of list Subnets From f0819ffb029180d3ba5cab713715582d1981a98a Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Fri, 28 Jan 2022 15:56:52 +0530 Subject: [PATCH 13/17] fix for project spec and return result for vms modules Signed-off-by: Prem Karat --- plugins/module_utils/prism/vms.py | 7 ++++++- plugins/modules/ntnx_vms.py | 12 +++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 68956b4a2..2ca44f902 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -125,7 +125,12 @@ def _build_spec_project(self, payload, param): elif 'uuid' in param: uuid = param['uuid'] - payload["metadata"]["project_reference"]["uuid"] = uuid + payload["metadata"].update({ + "project_reference": { + "uuid": uuid, + "kind": "project" + } + }) return payload, None def _build_spec_cluster(self, payload, param): diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index fb0fa65ee..aa49987c1 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -5,6 +5,8 @@ # 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) +import re +from urllib import response __metaclass__ = type @@ -357,10 +359,8 @@ def run_module(): vm = VM(module) - module.debug('Generating VM spec') spec, error = vm.get_spec() if error: - module.debug(error) result['error'] = error module.fail_json(msg="Failed generating VM Spec", **result) @@ -370,24 +370,26 @@ def run_module(): resp, status = vm.create(spec) if status['error']: - module.debug(status["error"]) result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed creating VM", **result) task_uuid = resp["status"]["execution_context"]["task_uuid"] + vm_uuid = resp["metadata"]["uuid"] result['task_uuid'] = task_uuid - result["vm_uuid"] = resp["metadata"]["uuid"] + result["vm_uuid"] = vm_uuid result["changed"] = True + if module.params.get("wait"): task = Task(module) resp, status = task.wait_for_completion(task_uuid) if status['error']: - module.debug(status["error"]) result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed creating VM", **result) + resp, _ = vm.read(vm_uuid) + result["response"] = resp module.exit_json(**result) From 47894e9dc1dbf5844eb5acffbeb8fde909467ac5 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Fri, 28 Jan 2022 16:12:38 +0400 Subject: [PATCH 14/17] fixed disks parsing functionality, black fixes --- plugins/inventory/ntnx_prism_vm_inventory.py | 81 ++++---- plugins/module_utils/base_module.py | 3 +- plugins/module_utils/entity.py | 52 +++-- plugins/module_utils/prism/clusters.py | 2 +- plugins/module_utils/prism/groups.py | 9 +- plugins/module_utils/prism/images.py | 2 +- plugins/module_utils/prism/prism.py | 1 + plugins/module_utils/prism/projects.py | 2 +- plugins/module_utils/prism/subnets.py | 2 +- plugins/module_utils/prism/tasks.py | 15 +- plugins/module_utils/prism/vms.py | 192 +++++++++---------- plugins/module_utils/utils.py | 1 + plugins/modules/ntnx_vms.py | 150 +++++++++------ tests/test_entity.py | 15 +- 14 files changed, 291 insertions(+), 236 deletions(-) diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py index ffcb405c4..11a0a57b1 100644 --- a/plugins/inventory/ntnx_prism_vm_inventory.py +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -4,10 +4,11 @@ # Copyright: (c) 2021 [Balu George, Prem Karat] # 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) +from __future__ import absolute_import, division, print_function + __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" name: ntnx_prism_vm_inventory plugin_type: inventory short_description: Get a list of Nutanix VMs for ansible dynamic inventory. @@ -63,7 +64,7 @@ - name: VALIDATE_CERTS notes: "null" requirements: "null" -''' +""" import json import tempfile @@ -76,52 +77,68 @@ class Mock_Module: def __init__(self, host, port, username, password, validate_certs=False): self.tmpdir = tempfile.gettempdir() - self.params = {'nutanix_host': host, - 'nutanix_port': port, - 'nutanix_username': username, - 'nutanix_password': password, - 'validate_certs': validate_certs} + self.params = { + "nutanix_host": host, + "nutanix_port": port, + "nutanix_username": username, + "nutanix_password": password, + "validate_certs": validate_certs, + } def jsonify(self, data): return json.dumps(data) class InventoryModule(BaseInventoryPlugin): - '''Nutanix VM dynamic invetory module for ansible''' + """Nutanix VM dynamic invetory module for ansible""" - NAME = 'nutanix.ncp.ntnx_prism_vm_inventory' + NAME = "nutanix.ncp.ntnx_prism_vm_inventory" def verify_file(self, path): - '''Verify inventory configuration file''' + """Verify inventory configuration file""" if not super().verify_file(path): return False - inventory_file_fmts = ('nutanix.yaml', 'nutanix.yml', - 'nutanix_host_inventory.yaml', - 'nutanix_host_inventory.yml') + inventory_file_fmts = ( + "nutanix.yaml", + "nutanix.yml", + "nutanix_host_inventory.yaml", + "nutanix_host_inventory.yml", + ) return path.endswith(inventory_file_fmts) def parse(self, inventory, loader, path, cache=True): super().parse(inventory, loader, path, cache=cache) self._read_config_data(path) - self.nutanix_hostname = self.get_option('nutanix_hostname') - self.nutanix_username = self.get_option('nutanix_username') - self.nutanix_password = self.get_option('nutanix_password') - self.nutanix_port = self.get_option('nutanix_port') - self.data = self.get_option('data') - self.validate_certs = self.get_option('validate_certs') - - module = Mock_Module(self.nutanix_hostname, self.nutanix_port, - self.nutanix_username, self.nutanix_password, - self.validate_certs) + self.nutanix_hostname = self.get_option("nutanix_hostname") + self.nutanix_username = self.get_option("nutanix_username") + self.nutanix_password = self.get_option("nutanix_password") + self.nutanix_port = self.get_option("nutanix_port") + self.data = self.get_option("data") + self.validate_certs = self.get_option("validate_certs") + + module = Mock_Module( + self.nutanix_hostname, + self.nutanix_port, + self.nutanix_username, + self.nutanix_password, + self.validate_certs, + ) vm = vms.VM(module) resp, status_code = vm.list(self.data) - keys_to_strip_from_resp = ["disk_list", "vnuma_config", "nic_list", - "power_state_mechanism", "host_reference", - "serial_port_list", "gpu_list", - "storage_config", "boot_config", - "guest_customization"] + keys_to_strip_from_resp = [ + "disk_list", + "vnuma_config", + "nic_list", + "power_state_mechanism", + "host_reference", + "serial_port_list", + "gpu_list", + "storage_config", + "boot_config", + "guest_customization", + ] for entity in resp["entities"]: cluster = entity["status"]["cluster_reference"]["name"] @@ -141,10 +158,10 @@ def parse(self, inventory, loader, path, cache=True): # Add inventory groups and hosts to inventory groups self.inventory.add_group(cluster) - self.inventory.add_child('all', 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) + self.inventory.set_variable(vm_name, "ansible_host", vm_ip) + self.inventory.set_variable(vm_name, "uuid", vm_uuid) # Add hostvars for key in keys_to_strip_from_resp: diff --git a/plugins/module_utils/base_module.py b/plugins/module_utils/base_module.py index 9932bc469..0615e3941 100644 --- a/plugins/module_utils/base_module.py +++ b/plugins/module_utils/base_module.py @@ -14,7 +14,8 @@ class BaseModule(AnsibleModule): action=dict(type="str", required=True, aliases=["state"]), wait=dict(type="bool", required=False, default=True), wait_timeout=dict(type="int", required=False, default=300), - validate_certs=dict(type="bool", required=False, default=False)) + validate_certs=dict(type="bool", required=False, default=False), + ) def __init__(self, **kwargs): if kwargs.get("argument_spec"): diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index 330bf5fc7..3f850bd15 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -2,6 +2,7 @@ # 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 import json @@ -12,47 +13,53 @@ class Entity(object): - def __init__(self, module, resource_type, scheme="https", - cookies=None, additional_headers=None): + def __init__( + self, + module, + resource_type, + scheme="https", + cookies=None, + additional_headers=None, + ): self.module = module self.base_url = self._build_url(module, scheme, resource_type) self.headers = self._build_headers(module, additional_headers) self.cookies = cookies def create(self, data=None, endpoint=None, query=None, timeout=30): - url = self.base_url + '/{0}'.format(endpoint) if endpoint else self.base_url + url = self.base_url + "/{0}".format(endpoint) if endpoint else self.base_url if query: url = self._build_url_with_query(url, query) return self._fetch_url(url, method="POST", data=data, timeout=timeout) def read(self, uuid=None, endpoint=None, query=None, timeout=30): - url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + url = self.base_url + "/{0}".format(uuid) if uuid else self.base_url if endpoint: - url = url + '/{}'.format(endpoint) + url = url + "/{}".format(endpoint) if query: url = self._build_url_with_query(url, query) return self._fetch_url(url, method="GET", timeout=timeout) def update(self, data=None, uuid=None, endpoint=None, query=None, timeout=30): - url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + url = self.base_url + "/{0}".format(uuid) if uuid else self.base_url if endpoint: - url = url + '/{0}'.format(endpoint) + url = url + "/{0}".format(endpoint) if query: url = self._build_url_with_query(url, query) return self._fetch_url(url, method="PUT", data=data, timeout=timeout) def delete(self, uuid=None, endpoint=None, query=None, timeout=30): - url = self.base_url + '/{0}'.format(uuid) if uuid else self.base_url + url = self.base_url + "/{0}".format(uuid) if uuid else self.base_url if endpoint: - url = url + '/{0}'.format(endpoint) + url = url + "/{0}".format(endpoint) if query: url = self._build_url_with_query(url, query) return self._fetch_url(url, method="DELETE", timeout=timeout) def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): - url = self.base_url if use_base_url else self.base_url + '/list' + url = self.base_url if use_base_url else self.base_url + "/list" if endpoint: - url = url + '/{0}'.format(endpoint) + url = url + "/{0}".format(endpoint) return self._fetch_url(url, method="POST", data=data, timeout=timeout) def get_uuid(self, name): @@ -68,22 +75,21 @@ def _build_url(self, module, scheme, resource_type): port = module.params.get("nutanix_port") if port: url += ":{port}".format(port=port) - if resource_type.startswith('/'): + if resource_type.startswith("/"): url += resource_type else: url += "/{resource_type}".format(resource_type=resource_type) return url def _build_headers(self, module, additional_headers): - headers = {"Content-Type": "application/json", - "Accept": "application/json"} + headers = {"Content-Type": "application/json", "Accept": "application/json"} if additional_headers: headers.update(additional_headers) usr = module.params.get("nutanix_username") pas = module.params.get("nutanix_password") if usr and pas: cred = f"{usr}:{pas}".format(usr=usr, pas=pas) - encoded_cred = b64encode(bytes(cred, encoding='ascii')).decode('ascii') + encoded_cred = b64encode(bytes(cred, encoding="ascii")).decode("ascii") auth_header = "Basic " + encoded_cred headers.update({"Authorization": auth_header}) return headers @@ -98,9 +104,15 @@ def _build_url_with_query(self, url, query): def _fetch_url(self, url, method, data=None, timeout=30): data = self.module.jsonify(data) if data else None - resp, info = fetch_url(self.module, url, data=data, method=method, - headers=self.headers, cookies=self.cookies, - timeout=timeout) + resp, info = fetch_url( + self.module, + url, + data=data, + method=method, + headers=self.headers, + cookies=self.cookies, + timeout=timeout, + ) status_code = info.get("status") body = resp.read() if resp else info.get("body") @@ -112,12 +124,12 @@ def _fetch_url(self, url, method, data=None, timeout=30): if 199 < status_code < 300: err = None else: - err=info.get("msg", "Status code != 2xx") + err = info.get("msg", "Status code != 2xx") self.module.fail_json( msg="Failed fetching URL: {}".format(url), status_code=status_code, error=err, - response=resp_json + response=resp_json, ) status = {"error": err, "code": status_code} return resp_json, status diff --git a/plugins/module_utils/prism/clusters.py b/plugins/module_utils/prism/clusters.py index a1e40b755..4ad573e14 100644 --- a/plugins/module_utils/prism/clusters.py +++ b/plugins/module_utils/prism/clusters.py @@ -6,5 +6,5 @@ class Cluster(Prism): def __init__(self, module): - resource_type = '/clusters' + resource_type = "/clusters" super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/groups.py b/plugins/module_utils/prism/groups.py index 764995384..382f93440 100644 --- a/plugins/module_utils/prism/groups.py +++ b/plugins/module_utils/prism/groups.py @@ -6,15 +6,12 @@ class Groups(Prism): def __init__(self, module): - resource_type = '/groups' + resource_type = "/groups" super().__init__(module, resource_type=resource_type) def get_uuid(self, entity_type, filter): - data = { - "entity_type": entity_type, - "filter_criteria": filter - } + data = {"entity_type": entity_type, "filter_criteria": filter} resp, _ = self.list(data, use_base_url=True) if resp.get("group_results"): return resp["group_results"][0]["entity_results"][0]["entity_id"] - return None \ No newline at end of file + return None diff --git a/plugins/module_utils/prism/images.py b/plugins/module_utils/prism/images.py index f9f3ef4c8..b616fa3e5 100644 --- a/plugins/module_utils/prism/images.py +++ b/plugins/module_utils/prism/images.py @@ -6,5 +6,5 @@ class Image(Prism): def __init__(self, module): - resource_type = '/images' + resource_type = "/images" super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/prism.py b/plugins/module_utils/prism/prism.py index 9304443f0..177dff054 100644 --- a/plugins/module_utils/prism/prism.py +++ b/plugins/module_utils/prism/prism.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type from ..entity import Entity diff --git a/plugins/module_utils/prism/projects.py b/plugins/module_utils/prism/projects.py index 0fde59de5..546c8c040 100644 --- a/plugins/module_utils/prism/projects.py +++ b/plugins/module_utils/prism/projects.py @@ -6,5 +6,5 @@ class Project(Prism): def __init__(self, module): - resource_type = '/projects' + resource_type = "/projects" super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/subnets.py b/plugins/module_utils/prism/subnets.py index 4bdacd309..25e8da896 100644 --- a/plugins/module_utils/prism/subnets.py +++ b/plugins/module_utils/prism/subnets.py @@ -6,5 +6,5 @@ class Subnet(Prism): def __init__(self, module): - resource_type = '/subnets' + resource_type = "/subnets" super().__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/tasks.py b/plugins/module_utils/prism/tasks.py index da015e1e3..ddcc8a0ad 100644 --- a/plugins/module_utils/prism/tasks.py +++ b/plugins/module_utils/prism/tasks.py @@ -6,25 +6,26 @@ from .prism import Prism + class Task(Prism): def __init__(self, module): - resource_type = '/tasks' + resource_type = "/tasks" super().__init__(module, resource_type=resource_type) def create(self, data=None, endpoint=None, query=None, timeout=30): - raise NotImplementedError('Create not permitted') + raise NotImplementedError("Create not permitted") def update(self, data=None, uuid=None, endpoint=None, query=None, timeout=30): - raise NotImplementedError('Update not permitted') + raise NotImplementedError("Update not permitted") def delete(self, uuid=None, endpoint=None, query=None, timeout=30): - raise NotImplementedError('Delete not permitted') + raise NotImplementedError("Delete not permitted") def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): - raise NotImplementedError('List not permitted') + raise NotImplementedError("List not permitted") def get_uuid(self, name): - raise NotImplementedError('get_uuid not permitted') + raise NotImplementedError("get_uuid not permitted") def wait_for_completion(self, uuid): state = "" @@ -38,7 +39,7 @@ def wait_for_completion(self, uuid): if state == "FAILED": status = { "error": response["error_detail"], - "code": response["error_code"] + "code": response["error_code"], } return response, status diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 2ca44f902..ae53d0aa2 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -18,7 +18,7 @@ class VM(Prism): def __init__(self, module): - resource_type = '/vms' + resource_type = "/vms" super().__init__(module, resource_type=resource_type) self.build_spec_methods = { "name": self._build_spec_name, @@ -33,7 +33,7 @@ def __init__(self, module): "boot_config": self._build_spec_boot_config, "guest_customization": self._build_spec_gc, "timezone": self._build_spec_timezone, - "categories": self._build_spec_categories + "categories": self._build_spec_categories, } def get_spec(self): @@ -47,63 +47,57 @@ def get_spec(self): return spec, None def _get_default_spec(self): - return deepcopy({ - "api_version": "3.1.0", - "metadata": {"kind": "vm"}, - "spec": { - "cluster_reference": { - "kind": "cluster", - "uuid": None - }, - "name": None, - "resources": { - "num_sockets": 1, - "num_vcpus_per_socket": 1, - "memory_size_mib": 4096, - "power_state": "ON", - "disk_list": [], - "nic_list": [], - "gpu_list": [], - "boot_config": { - "boot_type": "LEGACY", - "boot_device_order_list": ["CDROM", "DISK", "NETWORK"] + return deepcopy( + { + "api_version": "3.1.0", + "metadata": {"kind": "vm"}, + "spec": { + "cluster_reference": {"kind": "cluster", "uuid": None}, + "name": None, + "resources": { + "num_sockets": 1, + "num_vcpus_per_socket": 1, + "memory_size_mib": 4096, + "power_state": "ON", + "disk_list": [], + "nic_list": [], + "gpu_list": [], + "boot_config": { + "boot_type": "LEGACY", + "boot_device_order_list": ["CDROM", "DISK", "NETWORK"], + }, + "hardware_clock_timezone": "UTC", }, - "hardware_clock_timezone": "UTC" - } + }, } - }) + ) def _get_default_network_spec(self): - return deepcopy({ - "ip_endpoint_list": [], - "subnet_reference": { - "kind": "subnet", - "uuid": None - }, - "is_connected": True, - }) + return deepcopy( + { + "ip_endpoint_list": [], + "subnet_reference": {"kind": "subnet", "uuid": None}, + "is_connected": True, + } + ) def _get_default_disk_spec(self): - return deepcopy({ - "device_properties": { - "device_type": "DISK", - "disk_address": { - "adapter_type": None, - "device_index": None - } - }, - "disk_size_bytes": None, - "storage_config": { - "storage_container_reference": { - "kind": "storage_container", - "uuid": None - } - }, - "data_source_reference": { - "kind": "image", - "uuid": None + return deepcopy( + { + "device_properties": { + "device_type": "DISK", + "disk_address": {"adapter_type": None, "device_index": None}, + }, + "disk_size_bytes": None, + "storage_config": { + "storage_container_reference": { + "kind": "storage_container", + "uuid": None, + } + }, + "data_source_reference": {"kind": "image", "uuid": None}, } - }) + ) def _build_spec_name(self, payload, value): payload["spec"]["name"] = value @@ -114,7 +108,7 @@ def _build_spec_desc(self, payload, value): return payload, None def _build_spec_project(self, payload, param): - if 'name' in param: + if "name" in param: project = Project(self.module) name = param["name"] uuid = project.get_uuid(name) @@ -122,19 +116,16 @@ def _build_spec_project(self, payload, param): error = "Failed to get UUID for project name: {}".format(name) return None, error - elif 'uuid' in param: - uuid = param['uuid'] + elif "uuid" in param: + uuid = param["uuid"] - payload["metadata"].update({ - "project_reference": { - "uuid": uuid, - "kind": "project" - } - }) + payload["metadata"].update( + {"project_reference": {"uuid": uuid, "kind": "project"}} + ) return payload, None def _build_spec_cluster(self, payload, param): - if 'name' in param: + if "name" in param: cluster = Cluster(self.module) name = param["name"] uuid = cluster.get_uuid(name) @@ -142,8 +133,8 @@ def _build_spec_cluster(self, payload, param): error = "Failed to get UUID for cluster name: {}".format(name) return None, error - elif 'uuid' in param: - uuid = param['uuid'] + elif "uuid" in param: + uuid = param["uuid"] payload["spec"]["cluster_reference"]["uuid"] = uuid return payload, None @@ -164,14 +155,12 @@ def _build_spec_networks(self, payload, networks): nics = [] for network in networks: nic = self._get_default_network_spec() - if network.get('private_ip'): - nic["ip_endpoint_list"].append({ - "ip": network["private_ip"] - }) + if network.get("private_ip"): + nic["ip_endpoint_list"].append({"ip": network["private_ip"]}) nic["is_connected"] = network["is_connected"] - if network.get("subnet", {}).get('name'): + if network.get("subnet", {}).get("name"): subnet = Subnet(self.module) name = network["subnet"]["name"] uuid = subnet.get_uuid(name) @@ -179,7 +168,7 @@ def _build_spec_networks(self, payload, networks): error = "Failed to get UUID for subnet name: {}".format(name) return None, error - elif network.get("subnet", {}).get('uuid'): + elif network.get("subnet", {}).get("uuid"): uuid = network["subnet"]["uuid"] nic["subnet_reference"]["uuid"] = uuid @@ -196,10 +185,10 @@ def _build_spec_disks(self, payload, vdisks): for vdisk in vdisks: disk = self._get_default_disk_spec() - if 'type' in vdisk: + if "type" in vdisk: disk["device_properties"]["device_type"] = vdisk["type"] - if 'bus' in vdisk: + if "bus" in vdisk: if vdisk["bus"] == "SCSI": device_index = scsi_index scsi_index += 1 @@ -216,32 +205,36 @@ def _build_spec_disks(self, payload, vdisks): disk["device_properties"]["disk_address"]["adapter_type"] = vdisk["bus"] disk["device_properties"]["disk_address"]["device_index"] = device_index - if 'empty_cdrom' in vdisk: - disk.pop('disk_size_bytes') - disk.pop('data_source_reference') - disk.pop('storage_config') + if vdisk.get("empty_cdrom"): + disk.pop("disk_size_bytes") + disk.pop("data_source_reference") + disk.pop("storage_config") else: disk["disk_size_bytes"] = vdisk["size_gb"] * 1024 * 1024 * 1024 - if 'storage_container' in vdisk: - disk.pop('data_source_reference') - if 'name' in vdisk["storage_container"]: + if vdisk.get("storage_container"): + disk.pop("data_source_reference") + if "name" in vdisk["storage_container"]: groups = Groups(self.module) name = vdisk["storage_container"]["name"] - uuid = groups.get_uuid(entity_type="storage_container", - filter=f"container_name=={name}") + uuid = groups.get_uuid( + entity_type="storage_container", + filter=f"container_name=={name}", + ) if not uuid: - error = "Failed to get UUID for storgae container: {}".format(name) + error = "Failed to get UUID for storgae container: {}".format( + name + ) return None, error - elif 'uuid' in vdisk["storage_container"]: + elif "uuid" in vdisk["storage_container"]: uuid = vdisk["storage_container"]["uuid"] disk["storage_config"]["storage_container_reference"]["uuid"] = uuid - elif 'clone_image' in vdisk: - if 'name' in vdisk["clone_image"]: + elif vdisk.get("clone_image"): + if "name" in vdisk["clone_image"]: image = Image(self.module) name = vdisk["clone_image"]["name"] uuid = image.get_uuid(name) @@ -249,16 +242,20 @@ def _build_spec_disks(self, payload, vdisks): error = "Failed to get UUID for image: {}".format(name) return None, error - elif 'uuid' in vdisk["clone_image"]: + elif "uuid" in vdisk["clone_image"]: uuid = vdisk["clone_image"]["uuid"] disk["data_source_reference"]["uuid"] = uuid - if not disk.get("storage_config", {}).get("storage_container_reference", {}).get("uuid"): - disk.pop('storage_config', None) + if ( + not disk.get("storage_config", {}) + .get("storage_container_reference", {}) + .get("uuid") + ): + disk.pop("storage_config", None) if not disk.get("data_source_reference", {}).get("uuid"): - disk.pop('data_source_reference', None) + disk.pop("data_source_reference", None) disks.append(disk) @@ -267,7 +264,7 @@ def _build_spec_disks(self, payload, vdisks): def _build_spec_boot_config(self, payload, param): boot_config = payload["spec"]["resources"]["boot_config"] - if 'LEGACY' == param["boot_type"] and 'boot_order' in param: + if "LEGACY" == param["boot_type"] and "boot_order" in param: boot_config["boot_device_order_list"] = param["boot_order"] elif "UEFI" == param["boot_type"]: @@ -291,20 +288,15 @@ def _build_spec_gc(self, payload, param): content = base64.b64encode(f.read()) gc_spec = {"guest_customization": {}} - if 'sysprep' in param["type"]: + if "sysprep" in param["type"]: gc_spec["guest_customization"] = { - "sysprep": { - "install_type": "PREPARED", - "unattend_xml": content - } + "sysprep": {"install_type": "PREPARED", "unattend_xml": content} } - elif 'cloud_init' in param["type"]: - gc_spec["guest_customization"] = { - "cloud_init": {"user_data": content} - } + elif "cloud_init" in param["type"]: + gc_spec["guest_customization"] = {"cloud_init": {"user_data": content}} - if 'is_overridable' in param: + if "is_overridable" in param: gc_spec["guest_customization"]["is_overridable"] = param["is_overridable"] payload["spec"]["resources"].update(gc_spec) return payload, None diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py index f68238646..d9b301c1d 100644 --- a/plugins/module_utils/utils.py +++ b/plugins/module_utils/utils.py @@ -2,6 +2,7 @@ # 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 diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index aa49987c1..ac7b13a17 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -4,14 +4,14 @@ # Copyright: (c) 2021, Prem Karat # 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) +from __future__ import absolute_import, division, print_function import re from urllib import response __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: ntnx_vm @@ -267,15 +267,15 @@ - categories to be attached to the VM. type: dict required: false -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # TODO -''' +""" -RETURN = r''' +RETURN = r""" # TODO -''' +""" from ansible.module_utils.basic import env_fallback @@ -287,104 +287,130 @@ def run_module(): - entity_by_spec = dict( - name=dict(type='str'), - uuid=dict(type='str') - ) + + mutually_exclusive = [("name", "uuid")] + + entity_by_spec = dict(name=dict(type="str"), uuid=dict(type="str")) network_spec = dict( - subnet=dict(type='dict', options=entity_by_spec), - private_ip=dict(type='str', required=False), - is_connected=dict(type='bool', default=True) + subnet=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + private_ip=dict(type="str", required=False), + is_connected=dict(type="bool", default=True), ) disk_spec = dict( - type=dict(type='str', choices=['CDROM', 'DISK'], default='DISK'), - size_gb=dict(type='int'), - bus=dict(type='str', choices=['SCSI', 'PCI', 'SATA', 'IDE'], default='SCSI'), - storage_container=dict(type='dict', options=entity_by_spec), - clone_image=dict(type='dict', options=entity_by_spec), - empty_cdrom=dict(type='bool') + type=dict(type="str", choices=["CDROM", "DISK"], default="DISK"), + size_gb=dict(type="int"), + bus=dict(type="str", choices=["SCSI", "PCI", "SATA", "IDE"], default="SCSI"), + storage_container=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + clone_image=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + empty_cdrom=dict(type="bool"), ) boot_config_spec = dict( - boot_type=dict(type='str', choices=[ "LEGACY", "UEFI", "SECURE_BOOT" ]), - boot_order=dict(type='list', elements=str, default=["CDROM", "DISK", "NETWORK"]) + boot_type=dict(type="str", choices=["LEGACY", "UEFI", "SECURE_BOOT"]), + boot_order=dict( + type="list", elements=str, default=["CDROM", "DISK", "NETWORK"] + ), ) gc_spec = dict( - type=dict(type='str', choices=['cloud_init', 'sysprep'], required=True), - script_path=dict(type='path', required=True), - is_overridable=dict(type='bool', default=False), + type=dict(type="str", choices=["cloud_init", "sysprep"], required=True), + script_path=dict(type="path", required=True), + is_overridable=dict(type="bool", default=False), ) module_args = dict( - nutanix_host=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_HOST"])), - nutanix_port=dict(default="9440", type='str'), - nutanix_username=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"])), - nutanix_password=dict(type='str', required=True, no_log=True, fallback=(env_fallback, ["NUTANIX_PASSWORD"])), - validate_certs=dict(type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"])), - state=dict(type='str', choices=['present', 'absent'], default='present'), - wait=dict(type='bool', default=True), - name=dict(type='str', required=True), - desc=dict(type='str'), - project=dict(type='dict', options=entity_by_spec), - cluster=dict(type='dict', options=entity_by_spec), - vcpus=dict(type='int', default=1), - cores_per_vcpu=dict(type='int', default=1), - memory_gb=dict(type='int', default=1), - networks=dict(type='list', elements='dict', options=network_spec), - disks=dict(type='list', elements='dict', options=disk_spec), - boot_config=dict(type='dict', options=boot_config_spec), - guest_customization=dict(type='dict', options=gc_spec), - timezone=dict(type='str', default="UTC"), - categories=dict(type='dict') + nutanix_host=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_HOST"]) + ), + nutanix_port=dict(default="9440", type="str"), + nutanix_username=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"]) + ), + nutanix_password=dict( + type="str", + required=True, + no_log=True, + fallback=(env_fallback, ["NUTANIX_PASSWORD"]), + ), + validate_certs=dict( + type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"]) + ), + state=dict(type="str", choices=["present", "absent"], default="present"), + wait=dict(type="bool", default=True), + name=dict(type="str", required=True), + desc=dict(type="str"), + project=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + cluster=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + vcpus=dict(type="int", default=1), + cores_per_vcpu=dict(type="int", default=1), + memory_gb=dict(type="int", default=1), + networks=dict(type="list", elements="dict", options=network_spec), + disks=dict( + type="list", + elements="dict", + options=disk_spec, + mutually_exclusive=[ + ("storage_container", "clone_image", "empty_cdrom"), + ("size_gb", "empty_cdrom"), + ], + ), + boot_config=dict(type="dict", options=boot_config_spec), + guest_customization=dict(type="dict", options=gc_spec), + timezone=dict(type="str", default="UTC"), + categories=dict(type="dict"), ) - mutually_exclusive = [("name", "uuid")] - - module = BaseModule(argument_spec=module_args, - supports_check_mode=True, - mutually_exclusive=mutually_exclusive) + module = BaseModule(argument_spec=module_args, supports_check_mode=True) remove_param_with_none_value(module.params) result = { - 'changed': False, - 'error': None, - 'response': None, - 'vm_uuid': None, - 'task_uuid': None, + "changed": False, + "error": None, + "response": None, + "vm_uuid": None, + "task_uuid": None, } vm = VM(module) spec, error = vm.get_spec() if error: - result['error'] = error + result["error"] = error module.fail_json(msg="Failed generating VM Spec", **result) if module.check_mode: - result['response'] = spec + result["response"] = spec return module.exit_json(**result) resp, status = vm.create(spec) - if status['error']: + if status["error"]: result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed creating VM", **result) task_uuid = resp["status"]["execution_context"]["task_uuid"] vm_uuid = resp["metadata"]["uuid"] - result['task_uuid'] = task_uuid + result["task_uuid"] = task_uuid result["vm_uuid"] = vm_uuid result["changed"] = True - if module.params.get("wait"): task = Task(module) resp, status = task.wait_for_completion(task_uuid) - if status['error']: + if status["error"]: result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed creating VM", **result) @@ -398,5 +424,5 @@ def main(): run_module() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/test_entity.py b/tests/test_entity.py index 37acc75c4..1535ad0fd 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -7,17 +7,24 @@ class Module: def __init__(self): - self.params = {'nutanix_host': '10.46.34.230', 'nutanix_port': 9440, 'nutanix_username': 'admin', 'nutanix_password': 'Nutanix.123'} - self.tmpdir = '/tmp' + self.params = { + "nutanix_host": "10.46.34.230", + "nutanix_port": 9440, + "nutanix_username": "admin", + "nutanix_password": "Nutanix.123", + } + self.tmpdir = "/tmp" def jsonify(self, data): return json.dumps(data) + def main(): module = Module() vm = VM(module) data = {"kind": "vm", "offset": 0, "length": 500} print(vm.list(data)) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() From fa07ef75ae9732319e38e87f58cf485c8035981c Mon Sep 17 00:00:00 2001 From: Prem Karat Date: Fri, 28 Jan 2022 18:33:03 +0530 Subject: [PATCH 15/17] Add support for deletion Signed-off-by: Prem Karat --- plugins/modules/ntnx_vms.py | 93 ++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index aa49987c1..18fe6028b 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -5,6 +5,7 @@ # 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) +from operator import mod import re from urllib import response @@ -67,6 +68,11 @@ description: VM Name required: true type: str + vm_uuid: + description: + - VM UUID + - Required for VM deletion + required: false desc: description: A description for VM. required: false @@ -286,7 +292,7 @@ from ..module_utils.utils import remove_param_with_none_value -def run_module(): +def get_module_spec(): entity_by_spec = dict( name=dict(type='str'), uuid=dict(type='str') @@ -327,6 +333,7 @@ def run_module(): state=dict(type='str', choices=['present', 'absent'], default='present'), wait=dict(type='bool', default=True), name=dict(type='str', required=True), + vm_uuid=dict(type='str'), desc=dict(type='str'), project=dict(type='dict', options=entity_by_spec), cluster=dict(type='dict', options=entity_by_spec), @@ -341,24 +348,11 @@ def run_module(): categories=dict(type='dict') ) - mutually_exclusive = [("name", "uuid")] + return module_args - module = BaseModule(argument_spec=module_args, - supports_check_mode=True, - mutually_exclusive=mutually_exclusive) - - remove_param_with_none_value(module.params) - - result = { - 'changed': False, - 'error': None, - 'response': None, - 'vm_uuid': None, - 'task_uuid': None, - } +def create_vm(module, result): vm = VM(module) - spec, error = vm.get_spec() if error: result['error'] = error @@ -366,7 +360,7 @@ def run_module(): if module.check_mode: result['response'] = spec - return module.exit_json(**result) + return resp, status = vm.create(spec) if status['error']: @@ -374,23 +368,68 @@ def run_module(): result["response"] = resp module.fail_json(msg="Failed creating VM", **result) - task_uuid = resp["status"]["execution_context"]["task_uuid"] vm_uuid = resp["metadata"]["uuid"] - result['task_uuid'] = task_uuid - result["vm_uuid"] = vm_uuid result["changed"] = True - + result["response"] = resp + result["vm_uuid"] = vm_uuid + result['task_uuid'] = resp["status"]["execution_context"]["task_uuid"] if module.params.get("wait"): - task = Task(module) - resp, status = task.wait_for_completion(task_uuid) - if status['error']: - result["error"] = status["error"] - result["response"] = resp - module.fail_json(msg="Failed creating VM", **result) + wait_for_task_completion(module, result) resp, _ = vm.read(vm_uuid) result["response"] = resp + +def delete_vm(module, result): + vm_uuid = module.params["vm_uuid"] + if not vm_uuid: + result["error"] = "Missing parameter vm_uuid in playbook" + module.fail_json(msg="Failed deleting VM", **result) + + vm = VM(module) + resp, status = vm.delete(vm_uuid) + if status['error']: + result["error"] = status["error"] + result["response"] = resp + module.fail_json(msg="Failed deleting VM", **result) + + result["changed"] = True + result["response"] = resp + result["vm_uuid"] = vm_uuid + result['task_uuid'] = resp["status"]["execution_context"]["task_uuid"] + + if module.params.get("wait"): + wait_for_task_completion(module, result) + + +def wait_for_task_completion(module, result): + task = Task(module) + task_uuid = result['task_uuid'] + resp, status = task.wait_for_completion(task_uuid) + result["response"] = resp + if status['error']: + result["error"] = status["error"] + result["response"] = resp + module.fail_json(msg="Failed creating VM", **result) + + +def run_module(): + module = BaseModule(argument_spec=get_module_spec(), + supports_check_mode=True) + remove_param_with_none_value(module.params) + result = { + 'changed': False, + 'error': None, + 'response': None, + 'vm_uuid': None, + 'task_uuid': None, + } + state = module.params["state"] + if state == "present": + create_vm(module, result) + elif state == "absent": + delete_vm(module, result) + module.exit_json(**result) From 9ab92d438f9f9fef3cddca5eb1a559be09a5a9e4 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Fri, 28 Jan 2022 17:25:04 +0400 Subject: [PATCH 16/17] mutually exclusive fixes --- plugins/modules/ntnx_vms.py | 101 +++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 3c9b4637c..869eb4657 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -10,7 +10,6 @@ __metaclass__ = type - DOCUMENTATION = r""" --- module: ntnx_vm @@ -282,7 +281,6 @@ # TODO """ - from ansible.module_utils.basic import env_fallback from ..module_utils.base_module import BaseModule @@ -292,10 +290,9 @@ def get_module_spec(): - entity_by_spec = dict( - name=dict(type='str'), - uuid=dict(type='str') - ) + mutually_exclusive = [("name", "uuid")] + + entity_by_spec = dict(name=dict(type="str"), uuid=dict(type="str")) network_spec = dict( subnet=dict( @@ -332,27 +329,50 @@ def get_module_spec(): ) module_args = dict( - nutanix_host=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_HOST"])), - nutanix_port=dict(default="9440", type='str'), - nutanix_username=dict(type='str', required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"])), - nutanix_password=dict(type='str', required=True, no_log=True, fallback=(env_fallback, ["NUTANIX_PASSWORD"])), - validate_certs=dict(type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"])), - state=dict(type='str', choices=['present', 'absent'], default='present'), - wait=dict(type='bool', default=True), - name=dict(type='str', required=True), - vm_uuid=dict(type='str'), - desc=dict(type='str'), - project=dict(type='dict', options=entity_by_spec), - cluster=dict(type='dict', options=entity_by_spec), - vcpus=dict(type='int', default=1), - cores_per_vcpu=dict(type='int', default=1), - memory_gb=dict(type='int', default=1), - networks=dict(type='list', elements='dict', options=network_spec), - disks=dict(type='list', elements='dict', options=disk_spec), - boot_config=dict(type='dict', options=boot_config_spec), - guest_customization=dict(type='dict', options=gc_spec), - timezone=dict(type='str', default="UTC"), - categories=dict(type='dict') + nutanix_host=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_HOST"]) + ), + nutanix_port=dict(default="9440", type="str"), + nutanix_username=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"]) + ), + nutanix_password=dict( + type="str", + required=True, + no_log=True, + fallback=(env_fallback, ["NUTANIX_PASSWORD"]), + ), + validate_certs=dict( + type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"]) + ), + state=dict(type="str", choices=["present", "absent"], default="present"), + wait=dict(type="bool", default=True), + name=dict(type="str", required=True), + vm_uuid=dict(type="str"), + desc=dict(type="str"), + project=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + cluster=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + vcpus=dict(type="int", default=1), + cores_per_vcpu=dict(type="int", default=1), + memory_gb=dict(type="int", default=1), + networks=dict(type="list", elements="dict", options=network_spec), + disks=dict( + type="list", + elements="dict", + options=disk_spec, + mutually_exclusive=[ + ("storage_container", "clone_image", "empty_cdrom"), + ("size_gb", "empty_cdrom"), + ], + ), + boot_config=dict(type="dict", options=boot_config_spec), + guest_customization=dict(type="dict", options=gc_spec), + timezone=dict(type="str", default="UTC"), + categories=dict(type="dict"), ) return module_args @@ -366,7 +386,7 @@ def create_vm(module, result): module.fail_json(msg="Failed generating VM Spec", **result) if module.check_mode: - result['response'] = spec + result["response"] = spec return resp, status = vm.create(spec) @@ -379,7 +399,7 @@ def create_vm(module, result): result["changed"] = True result["response"] = resp result["vm_uuid"] = vm_uuid - result['task_uuid'] = resp["status"]["execution_context"]["task_uuid"] + result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"] if module.params.get("wait"): wait_for_task_completion(module, result) @@ -395,7 +415,7 @@ def delete_vm(module, result): vm = VM(module) resp, status = vm.delete(vm_uuid) - if status['error']: + if status["error"]: result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed deleting VM", **result) @@ -403,7 +423,7 @@ def delete_vm(module, result): result["changed"] = True result["response"] = resp result["vm_uuid"] = vm_uuid - result['task_uuid'] = resp["status"]["execution_context"]["task_uuid"] + result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"] if module.params.get("wait"): wait_for_task_completion(module, result) @@ -411,28 +431,27 @@ def delete_vm(module, result): def wait_for_task_completion(module, result): task = Task(module) - task_uuid = result['task_uuid'] + task_uuid = result["task_uuid"] resp, status = task.wait_for_completion(task_uuid) result["response"] = resp - if status['error']: + if status["error"]: result["error"] = status["error"] result["response"] = resp module.fail_json(msg="Failed creating VM", **result) def run_module(): - module = BaseModule(argument_spec=get_module_spec(), - supports_check_mode=True) + module = BaseModule(argument_spec=get_module_spec(), supports_check_mode=True) remove_param_with_none_value(module.params) result = { - 'changed': False, - 'error': None, - 'response': None, - 'vm_uuid': None, - 'task_uuid': None, + "changed": False, + "error": None, + "response": None, + "vm_uuid": None, + "task_uuid": None, } state = module.params["state"] - if state == "present": + if state == "present": create_vm(module, result) elif state == "absent": delete_vm(module, result) From 4e838de367a7a7fbaff6eb6d7dd9103e8610a206 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Fri, 28 Jan 2022 17:42:02 +0400 Subject: [PATCH 17/17] argument spec updates --- plugins/module_utils/base_module.py | 23 +++++++++++++++++++---- plugins/modules/ntnx_vms.py | 20 -------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/base_module.py b/plugins/module_utils/base_module.py index 0615e3941..e0bae4b8b 100644 --- a/plugins/module_utils/base_module.py +++ b/plugins/module_utils/base_module.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback __metaclass__ = type @@ -11,10 +12,24 @@ class BaseModule(AnsibleModule): """Basic module with common arguments""" argument_spec = dict( - action=dict(type="str", required=True, aliases=["state"]), - wait=dict(type="bool", required=False, default=True), - wait_timeout=dict(type="int", required=False, default=300), - validate_certs=dict(type="bool", required=False, default=False), + nutanix_host=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_HOST"]) + ), + nutanix_port=dict(default="9440", type="str"), + nutanix_username=dict( + type="str", required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"]) + ), + nutanix_password=dict( + type="str", + required=True, + no_log=True, + fallback=(env_fallback, ["NUTANIX_PASSWORD"]), + ), + validate_certs=dict( + type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"]) + ), + state=dict(type="str", choices=["present", "absent"], default="present"), + wait=dict(type="bool", default=True), ) def __init__(self, **kwargs): diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 869eb4657..19e58c33a 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -281,8 +281,6 @@ # TODO """ -from ansible.module_utils.basic import env_fallback - from ..module_utils.base_module import BaseModule from ..module_utils.prism.vms import VM from ..module_utils.prism.tasks import Task @@ -329,24 +327,6 @@ def get_module_spec(): ) module_args = dict( - nutanix_host=dict( - type="str", required=True, fallback=(env_fallback, ["NUTANIX_HOST"]) - ), - nutanix_port=dict(default="9440", type="str"), - nutanix_username=dict( - type="str", required=True, fallback=(env_fallback, ["NUTANIX_USERNAME"]) - ), - nutanix_password=dict( - type="str", - required=True, - no_log=True, - fallback=(env_fallback, ["NUTANIX_PASSWORD"]), - ), - validate_certs=dict( - type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"]) - ), - state=dict(type="str", choices=["present", "absent"], default="present"), - wait=dict(type="bool", default=True), name=dict(type="str", required=True), vm_uuid=dict(type="str"), desc=dict(type="str"),