From ad89681b12510b7c08e17a5b1305478c40236382 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 09:18:09 +0000 Subject: [PATCH 01/15] Sanity and python fix --- plugins/inventory/ntnx_prism_vm_inventory.py | 1 - plugins/inventory/ntnx_prism_vm_inventory.pyc | Bin 0 -> 6374 bytes plugins/module_utils/entity.py | 25 +- plugins/module_utils/prism/clusters.py | 5 +- plugins/module_utils/prism/groups.py | 7 +- plugins/module_utils/prism/images.py | 5 +- plugins/module_utils/prism/prism.py | 2 +- plugins/module_utils/prism/projects.py | 5 +- plugins/module_utils/prism/subnets.py | 5 +- plugins/module_utils/prism/tasks.py | 5 +- plugins/module_utils/prism/vms.py | 25 +- plugins/modules/ntnx_vms.py | 490 +++++++++--------- tests/test_entity.py | 4 + 13 files changed, 312 insertions(+), 267 deletions(-) create mode 100644 plugins/inventory/ntnx_prism_vm_inventory.pyc diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py index 11a0a57b1..a13cdf232 100644 --- a/plugins/inventory/ntnx_prism_vm_inventory.py +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2021 [Balu George, Prem Karat] diff --git a/plugins/inventory/ntnx_prism_vm_inventory.pyc b/plugins/inventory/ntnx_prism_vm_inventory.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea87b9b59f040ce1c837ea18d263cf05fdbc0545 GIT binary patch literal 6374 zcmd5=ZFA(r5guvn^~?F)aNNNG!la6&id$c*APKG{V9tiPz1xT~b70IQWW)6>(_-P3QQ#y=P5|MKy_{6LI; z4ZJ`6$`a~tXoUD%A{HXC#EB&mTb$S;X^4}Cu;i3DnHC{HO~z}8ct)JeRP9-DGRy5L z5zmQaUYyJei|9>@_=ZRp#K{8sm^vfkMUgCtlO^;qWkG~<;%gzE3i11sWf9H`xgffh z2yfKT6%jVXVQUc_M#OfDnAXd9_@^q1OD(-9$+JT1S-`^RB2rP7>LqmYB1*N_Ez>{) zq=FeC<{kV}KapEbs?$pkC@S$T5-&_w@kuW}$x4yiuY@oqp>b=dILiRyz&y_Os9))pSSLUO!Wsji?^^55^D3u$%xkRrs+} zO>kfjdj}5orI1QZ%zln2#tfA%CML-AVp4I$!`+c_cRoMb?(BWx{qDixQD=L9x1L#v zP%2scPa;$kYD%=+|A*<%4|m<~WIFeidXg34q}&q*GpYA#!Vkdo!S>!nDg8K@^JgJy`Al& zUGM&`dvwV2z!b&PjkJtolNv_u4<@0%YM|4yC_^vcv29Y{Zc$`~fvyAkk+0-lm2M9> z(?&Mn27}W3MWXZ#y!W%<+}nrPu?*1VA7Oq(HOs4LNI|8Fw5j;`8f8m^N*L4=CsV>b zY!PbE#Di|4d!OcgfQA<0%%ZAeAcSX&Xj%+VbXejS1CX-u0-*+8QzD$g3#HFAUcd!h zpc)~99Tu_ja*FU104_;ZfLCEu0aRZHRso9Zl}UqaO%bD1)M%OH-6)oN9-W@l)DNRV z6Bl~i%#n~2#k^^nOLoT%=8YhAnN;&a_>+(wMXDf9 zqwdu)nu=r;R*U!^>4nm^=dm$(Uc%miE+KFna$iKty^Y6M9&Y`38|sp!eui6-6*jGf zc`o4VAn+^~e;e4w#Y6ZsT+_I?J#ujgzihDIH_6@E=G&OksVne_qMNFF$LQ;NVXof9 zl#!`EuMT93qbNe_u*DI3usDPf1QTO)>ZZ`PI7GM@+5|dl2!s^2M$!SV^-vbr-0?ZU z39_^s^-7e}oHF3)ilS%5)0LmZihQ#Mp#RFa&*kX&0Mlmtm!C79#70*?#3F2$C=-zP zNCccrAiGD=%)Ns?n>2EqdJ>^3AnlG_GWT^~Q^KfY2G8@liB@CN{t>PEb@28TZWEsD zHERk-WxASnK<@YgZVK6KEdPKbUpE$*1!Wl+&XjoCuqK2Gi}Z{|0{0mlJhMb)a?UfaH>q zyR-DTMGtaxO#@-1&OJI8aAkCXqTfe>K*5gvK8*r1N*OE9p2)&enhNqn27P=(Q3;nt zw3b)Mt}J947&dTtjr`c7dd5(o#d~>aI#ewvM0>x7+TJWjETIP0RiDC2uaqDhlt4|Q zKXULLg(Ds52xA4D^?sW!N?>bj+lF&bar=;Vh|Pchzo5BntmMf zM)iuU%vsr0Jx0oF-2{sJ8Xk;haH}pJ!n$KEBFEL2-LRUYYr$T&ZlV9}Yp$^h{3Tq?M$@_t z3ai$NHIHkxv0|-TH|c6DV&1y7XwBMhSS`|_OT~*z6%MP{^TI4Zg-fnLpM_k?yWFG0 zu1j2w)y1tJu_mBX>6f?_`788eH|@Da!yb9usA}()x-3w{y8sE7+}I_TcHhTC(?JG> zJ;F&4_8xRx_Gt1Fj*uf~Z%aC8E0?TE(*dA@j}Bk$+7n$4 zD{DX*ogYda#j4F;!`Y*aZ<8=T8P#$O__g8pw;Y;@|2uYbt7$J-3oxt|d!@0y{BPWP BW!wM& literal 0 HcmV?d00001 diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index 3f850bd15..7a561444f 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -9,7 +9,11 @@ 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 + +try: + from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse +except ImportError: + from urlparse import urlparse # python2 class Entity(object): @@ -35,7 +39,7 @@ def create(self, data=None, endpoint=None, query=None, timeout=30): 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) + url = url + "/{0}".format(endpoint) if query: url = self._build_url_with_query(url, query) return self._fetch_url(url, method="GET", timeout=timeout) @@ -63,8 +67,8 @@ def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): 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) + data = {"filter": "name=={0}".format(name), "length": 1} + resp, tmp = self.list(data) if resp and resp.get("entities"): return resp["entities"][0]["metadata"]["uuid"] return None @@ -74,11 +78,11 @@ def _build_url(self, module, scheme, resource_type): url = "{proto}://{host}".format(proto=scheme, host=host) port = module.params.get("nutanix_port") if port: - url += ":{port}".format(port=port) + url += ":{0}".format(port) if resource_type.startswith("/"): url += resource_type else: - url += "/{resource_type}".format(resource_type=resource_type) + url += "/{0}".format(resource_type) return url def _build_headers(self, module, additional_headers): @@ -88,8 +92,11 @@ def _build_headers(self, module, 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") + cred = "{0}:{1}".format(usr, pas) + try: + encoded_cred = b64encode(bytes(cred, encoding="ascii")).decode("ascii") + except BaseException: + encoded_cred = b64encode(bytes(cred).encode("ascii")).decode("ascii") auth_header = "Basic " + encoded_cred headers.update({"Authorization": auth_header}) return headers @@ -126,7 +133,7 @@ def _fetch_url(self, url, method, data=None, timeout=30): else: err = info.get("msg", "Status code != 2xx") self.module.fail_json( - msg="Failed fetching URL: {}".format(url), + msg="Failed fetching URL: {0}".format(url), status_code=status_code, error=err, response=resp_json, diff --git a/plugins/module_utils/prism/clusters.py b/plugins/module_utils/prism/clusters.py index 4ad573e14..c95d82bb6 100644 --- a/plugins/module_utils/prism/clusters.py +++ b/plugins/module_utils/prism/clusters.py @@ -1,5 +1,8 @@ # 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 from .prism import Prism @@ -7,4 +10,4 @@ class Cluster(Prism): def __init__(self, module): resource_type = "/clusters" - super().__init__(module, resource_type=resource_type) + super(Cluster, self).__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/groups.py b/plugins/module_utils/prism/groups.py index 382f93440..dc36f4ab3 100644 --- a/plugins/module_utils/prism/groups.py +++ b/plugins/module_utils/prism/groups.py @@ -1,5 +1,8 @@ # 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 from .prism import Prism @@ -7,11 +10,11 @@ class Groups(Prism): def __init__(self, module): resource_type = "/groups" - super().__init__(module, resource_type=resource_type) + super(Groups, self).__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) + resp, tmp = self.list(data, use_base_url=True) if resp.get("group_results"): return resp["group_results"][0]["entity_results"][0]["entity_id"] return None diff --git a/plugins/module_utils/prism/images.py b/plugins/module_utils/prism/images.py index b616fa3e5..f3d7236d8 100644 --- a/plugins/module_utils/prism/images.py +++ b/plugins/module_utils/prism/images.py @@ -1,5 +1,8 @@ # 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 from .prism import Prism @@ -7,4 +10,4 @@ class Image(Prism): def __init__(self, module): resource_type = "/images" - super().__init__(module, resource_type=resource_type) + super(Image, self).__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/prism.py b/plugins/module_utils/prism/prism.py index 177dff054..fc1892bf5 100644 --- a/plugins/module_utils/prism/prism.py +++ b/plugins/module_utils/prism/prism.py @@ -10,4 +10,4 @@ class Prism(Entity): def __init__(self, module, resource_type): resource_type = self.__BASEURL__ + resource_type - super().__init__(module, resource_type) + super(Prism, self).__init__(module, resource_type) diff --git a/plugins/module_utils/prism/projects.py b/plugins/module_utils/prism/projects.py index 546c8c040..eec9f92bc 100644 --- a/plugins/module_utils/prism/projects.py +++ b/plugins/module_utils/prism/projects.py @@ -1,5 +1,8 @@ # 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 from .prism import Prism @@ -7,4 +10,4 @@ class Project(Prism): def __init__(self, module): resource_type = "/projects" - super().__init__(module, resource_type=resource_type) + super(Project, self).__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/subnets.py b/plugins/module_utils/prism/subnets.py index 25e8da896..06a08731e 100644 --- a/plugins/module_utils/prism/subnets.py +++ b/plugins/module_utils/prism/subnets.py @@ -1,5 +1,8 @@ # 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 from .prism import Prism @@ -7,4 +10,4 @@ class Subnet(Prism): def __init__(self, module): resource_type = "/subnets" - super().__init__(module, resource_type=resource_type) + super(Subnet, self).__init__(module, resource_type=resource_type) diff --git a/plugins/module_utils/prism/tasks.py b/plugins/module_utils/prism/tasks.py index ddcc8a0ad..c40904e3a 100644 --- a/plugins/module_utils/prism/tasks.py +++ b/plugins/module_utils/prism/tasks.py @@ -1,5 +1,8 @@ # 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 from os import stat import time @@ -10,7 +13,7 @@ class Task(Prism): def __init__(self, module): resource_type = "/tasks" - super().__init__(module, resource_type=resource_type) + super(Task, self).__init__(module, resource_type=resource_type) def create(self, data=None, endpoint=None, query=None, timeout=30): raise NotImplementedError("Create not permitted") diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index ae53d0aa2..bfe6b9457 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -1,8 +1,9 @@ # 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 + from copy import deepcopy import base64 @@ -19,7 +20,7 @@ class VM(Prism): def __init__(self, module): resource_type = "/vms" - super().__init__(module, resource_type=resource_type) + super(VM, self).__init__(module, resource_type=resource_type) self.build_spec_methods = { "name": self._build_spec_name, "desc": self._build_spec_desc, @@ -41,7 +42,7 @@ def get_spec(self): for ansible_param, ansible_value in self.module.params.items(): build_spec_method = self.build_spec_methods.get(ansible_param) if build_spec_method and ansible_value: - _, error = build_spec_method(spec, ansible_value) + tmp, error = build_spec_method(spec, ansible_value) if error: return None, error return spec, None @@ -113,7 +114,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: {0}".format(name) return None, error elif "uuid" in param: @@ -130,7 +131,7 @@ def _build_spec_cluster(self, payload, param): name = param["name"] uuid = cluster.get_uuid(name) if not uuid: - error = "Failed to get UUID for cluster name: {}".format(name) + error = "Failed to get UUID for cluster name: {0}".format(name) return None, error elif "uuid" in param: @@ -165,7 +166,7 @@ def _build_spec_networks(self, payload, networks): name = network["subnet"]["name"] uuid = subnet.get_uuid(name) if not uuid: - error = "Failed to get UUID for subnet name: {}".format(name) + error = "Failed to get UUID for subnet name: {0}".format(name) return None, error elif network.get("subnet", {}).get("uuid"): @@ -220,11 +221,13 @@ def _build_spec_disks(self, payload, vdisks): name = vdisk["storage_container"]["name"] uuid = groups.get_uuid( entity_type="storage_container", - filter=f"container_name=={name}", + filter="container_name=={0}".format(name), ) if not uuid: - error = "Failed to get UUID for storgae container: {}".format( - name + error = ( + "Failed to get UUID for storgae container: {0}".format( + name + ) ) return None, error @@ -239,7 +242,7 @@ def _build_spec_disks(self, payload, vdisks): name = vdisk["clone_image"]["name"] uuid = image.get_uuid(name) if not uuid: - error = "Failed to get UUID for image: {}".format(name) + error = "Failed to get UUID for image: {0}".format(name) return None, error elif "uuid" in vdisk["clone_image"]: @@ -281,7 +284,7 @@ def _build_spec_gc(self, payload, param): fpath = param["script_path"] if not os.path.exists(fpath): - error = "File not found: {}".format(fpath) + error = "File not found: {0}".format(fpath) return None, error with open(fpath, "rb") as f: diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 36a701312..2fa782eff 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -3,274 +3,280 @@ # 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 -import re -from urllib import response __metaclass__ = type DOCUMENTATION = r""" --- -module: ntnx_vm - +module: ntnx_vms 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 - +version_added: 1.0.0 +description: 'Create, Update, Delete, Power-on, Power-off Nutanix VM''s' options: - nutanix_host: + 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 + 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. + type: bool + required: false + default: True + name: + description: VM Name + required: False + type: str + vm_uuid: + description: VM UUID + type: str + desc: + description: A description for VM. + required: false + type: str + project: + description: Name or UUID of the project. + required: false + type: dict + suboptions: + name: description: - - PC hostname or IP address + - Project Name + - Mutually exclusive with C(uuid) type: str - required: true - nutanix_port: + uuid: description: - - PC port + - Project UUID + - Mutually exclusive with C(name) type: str - default: 9440 - required: false - nutanix_username: + cluster: + description: + - Name or UUID of the cluster on which the VM will be placed. + type: dict + required: false + suboptions: + name: description: - - PC username + - Cluster Name + - Mutually exclusive with C(uuid) 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 - state: + uuid: 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 - default: false - name: - description: VM Name - required: true + - Cluster UUID + - Mutually exclusive with C(name) type: str - vm_uuid: + vcpus: + description: + - Number number of sockets. + required: false + type: int + default: 1 + cores_per_vcpu: + description: + - This is the number of vcpus per socket. + required: false + type: int + default: 1 + memory_gb: + description: + - Memory size in GB + required: false + type: int + default: 1 + networks: + description: + - list of subnets to which the VM needs to connect to. + type: list + elements: dict + required: false + suboptions: + subnet: description: - - VM UUID - - Required for VM deletion - required: false - desc: - description: A description for VM. - required: false - type: str - project: - description: Name or UUID of the project. - required: false + - Name or UUID of the subnet to which the VM should be connnected. type: dict 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: + name: + description: + - Subnet Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Subnet UUID + - Mutually exclusive with C(name) + type: str + private_ip: description: - - Name or UUID of the cluster on which the VM will be placed. + - Optionally assign static IP to the VM. + type: str required: false - 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: + is_connected: description: - - Number number of sockets. + - connect or disconnect the VM to the subnet. + type: bool required: false - type: int - default: 1 - cores_per_vcpu: + default: true + disks: + description: + - List of disks attached to the VM + type: list + elements: dict + suboptions: + type: description: - - This is the number of vcpus per socket. - required: false - type: int - default: 1 - memory_gb: + - CDROM or DISK + choices: + - CDROM + - DISK + default: DISK + type: str + size_gb: description: - - Memory size in GB - required: false + - The Disk Size in GB. + - This option is applicable for only DISK type above. type: int - default: 1 - networks: + bus: + description: Bus type of the device + default: SCSI + choices: + - SCSI + - PCI + - SATA + - IDE + type: str + storage_container: description: - - list of subnets to which the VM needs to connect to. - type: list - elements: dict - required: false + - Mutually exclusive with C(clone_image) and C(empty_cdrom) + type: dict suboptions: - subnet: - description: - - Name or UUID of the subnet to which the VM should be connnected. - suboptions: - name: - description: - - Subnet Name - - Mutually exclusive with C(uuid) - type: str - required: true - uuid: - description: - - Subnet UUID - - Mutually exclusive with C(name) - type: str - required: true - private_ip: - description: - - Optionally assign static IP to the VM. - type: str - required: False - is_connected: - description: - - connect or disconnect the VM to the subnet. - type: bool - required: False - default: True - disks: + name: + description: + - Storage containter Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Storage container UUID + - Mutually exclusive with C(name) + type: str + clone_image: description: - - List of disks attached to the VM - type: list - elements: dict + - Mutually exclusive with C(storage_container) and C(empty_cdrom) + type: dict 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: + name: + description: + - Image Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Image UUID + - Mutually exclusive with C(name) + type: str + empty_cdrom: + type: bool 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: + - 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. + type: dict + required: false + suboptions: + boot_type: 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] - default: sysprep - required: true - 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 - required: false - timezone: + - 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 + elements: str + default: + - CDROM + - DISK + - NETWORK + guest_customization: + description: + - cloud_init or sysprep guest customization + type: dict + suboptions: + type: description: - - VM's hardware clock timezone in IANA TZDB format (America/Los_Angeles). + - cloud_init or sysprep type type: str - default: UTC - required: false - categories: + required: True + choices: + - cloud_init + - sysprep + script_path: description: - - categories to be attached to the VM. - type: dict + - 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 required: false + timezone: + description: + - VM's hardware clock timezone in IANA TZDB format (America/Los_Angeles). + type: str + default: UTC + required: false + categories: + description: + - categories to be attached to the VM. + type: dict + required: false +author: + - Alaa Bishtawi (@alaa-bish) """ EXAMPLES = r""" @@ -285,6 +291,7 @@ from ..module_utils.prism.vms import VM from ..module_utils.prism.tasks import Task from ..module_utils.utils import remove_param_with_none_value +import re def get_module_spec(): @@ -314,9 +321,11 @@ def get_module_spec(): ) boot_config_spec = dict( - boot_type=dict(type="str", choices=["LEGACY", "UEFI", "SECURE_BOOT"]), + boot_type=dict( + type="str", choices=["LEGACY", "UEFI", "SECURE_BOOT"], default="LEGACY" + ), boot_order=dict( - type="list", elements=str, default=["CDROM", "DISK", "NETWORK"] + type="list", elements="str", default=["CDROM", "DISK", "NETWORK"] ), ) @@ -383,7 +392,7 @@ def create_vm(module, result): if module.params.get("wait"): wait_for_task_completion(module, result) - resp, _ = vm.read(vm_uuid) + resp, tmp = vm.read(vm_uuid) result["response"] = resp @@ -421,9 +430,14 @@ def wait_for_task_completion(module, result): def run_module(): - module = BaseModule(argument_spec=get_module_spec(), supports_check_mode=True, - required_if=[("state", "present", ("name",)), - ("state", "absent", ("vm_uuid",))]) + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_if=[ + ("state", "present", ("name",)), + ("state", "absent", ("vm_uuid",)), + ], + ) remove_param_with_none_value(module.params) result = { "changed": False, diff --git a/tests/test_entity.py b/tests/test_entity.py index 1535ad0fd..79d45de8b 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + from plugins.module_utils.prism.vms import VM from plugins.module_utils.prism.images import Image From 005e793f3f74c984ea342458ed9996b6eb48d946 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 09:20:34 +0000 Subject: [PATCH 02/15] remove bin file --- plugins/inventory/ntnx_prism_vm_inventory.pyc | Bin 6374 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/inventory/ntnx_prism_vm_inventory.pyc diff --git a/plugins/inventory/ntnx_prism_vm_inventory.pyc b/plugins/inventory/ntnx_prism_vm_inventory.pyc deleted file mode 100644 index ea87b9b59f040ce1c837ea18d263cf05fdbc0545..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6374 zcmd5=ZFA(r5guvn^~?F)aNNNG!la6&id$c*APKG{V9tiPz1xT~b70IQWW)6>(_-P3QQ#y=P5|MKy_{6LI; z4ZJ`6$`a~tXoUD%A{HXC#EB&mTb$S;X^4}Cu;i3DnHC{HO~z}8ct)JeRP9-DGRy5L z5zmQaUYyJei|9>@_=ZRp#K{8sm^vfkMUgCtlO^;qWkG~<;%gzE3i11sWf9H`xgffh z2yfKT6%jVXVQUc_M#OfDnAXd9_@^q1OD(-9$+JT1S-`^RB2rP7>LqmYB1*N_Ez>{) zq=FeC<{kV}KapEbs?$pkC@S$T5-&_w@kuW}$x4yiuY@oqp>b=dILiRyz&y_Os9))pSSLUO!Wsji?^^55^D3u$%xkRrs+} zO>kfjdj}5orI1QZ%zln2#tfA%CML-AVp4I$!`+c_cRoMb?(BWx{qDixQD=L9x1L#v zP%2scPa;$kYD%=+|A*<%4|m<~WIFeidXg34q}&q*GpYA#!Vkdo!S>!nDg8K@^JgJy`Al& zUGM&`dvwV2z!b&PjkJtolNv_u4<@0%YM|4yC_^vcv29Y{Zc$`~fvyAkk+0-lm2M9> z(?&Mn27}W3MWXZ#y!W%<+}nrPu?*1VA7Oq(HOs4LNI|8Fw5j;`8f8m^N*L4=CsV>b zY!PbE#Di|4d!OcgfQA<0%%ZAeAcSX&Xj%+VbXejS1CX-u0-*+8QzD$g3#HFAUcd!h zpc)~99Tu_ja*FU104_;ZfLCEu0aRZHRso9Zl}UqaO%bD1)M%OH-6)oN9-W@l)DNRV z6Bl~i%#n~2#k^^nOLoT%=8YhAnN;&a_>+(wMXDf9 zqwdu)nu=r;R*U!^>4nm^=dm$(Uc%miE+KFna$iKty^Y6M9&Y`38|sp!eui6-6*jGf zc`o4VAn+^~e;e4w#Y6ZsT+_I?J#ujgzihDIH_6@E=G&OksVne_qMNFF$LQ;NVXof9 zl#!`EuMT93qbNe_u*DI3usDPf1QTO)>ZZ`PI7GM@+5|dl2!s^2M$!SV^-vbr-0?ZU z39_^s^-7e}oHF3)ilS%5)0LmZihQ#Mp#RFa&*kX&0Mlmtm!C79#70*?#3F2$C=-zP zNCccrAiGD=%)Ns?n>2EqdJ>^3AnlG_GWT^~Q^KfY2G8@liB@CN{t>PEb@28TZWEsD zHERk-WxASnK<@YgZVK6KEdPKbUpE$*1!Wl+&XjoCuqK2Gi}Z{|0{0mlJhMb)a?UfaH>q zyR-DTMGtaxO#@-1&OJI8aAkCXqTfe>K*5gvK8*r1N*OE9p2)&enhNqn27P=(Q3;nt zw3b)Mt}J947&dTtjr`c7dd5(o#d~>aI#ewvM0>x7+TJWjETIP0RiDC2uaqDhlt4|Q zKXULLg(Ds52xA4D^?sW!N?>bj+lF&bar=;Vh|Pchzo5BntmMf zM)iuU%vsr0Jx0oF-2{sJ8Xk;haH}pJ!n$KEBFEL2-LRUYYr$T&ZlV9}Yp$^h{3Tq?M$@_t z3ai$NHIHkxv0|-TH|c6DV&1y7XwBMhSS`|_OT~*z6%MP{^TI4Zg-fnLpM_k?yWFG0 zu1j2w)y1tJu_mBX>6f?_`788eH|@Da!yb9usA}()x-3w{y8sE7+}I_TcHhTC(?JG> zJ;F&4_8xRx_Gt1Fj*uf~Z%aC8E0?TE(*dA@j}Bk$+7n$4 zD{DX*ogYda#j4F;!`Y*aZ<8=T8P#$O__g8pw;Y;@|2uYbt7$J-3oxt|d!@0y{BPWP BW!wM& From 48bf665b07e6cf97af508c7a23641fb34f8183fe Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 09:34:28 +0000 Subject: [PATCH 03/15] Some Flake 8 Fixes --- plugins/module_utils/prism/tasks.py | 1 - plugins/modules/ntnx_vms.py | 9 ++++----- tests/test_entity.py | 2 -- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/prism/tasks.py b/plugins/module_utils/prism/tasks.py index c40904e3a..18f87b4fd 100644 --- a/plugins/module_utils/prism/tasks.py +++ b/plugins/module_utils/prism/tasks.py @@ -4,7 +4,6 @@ __metaclass__ = type -from os import stat import time from .prism import Prism diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 2fa782eff..d8ebbbe71 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -287,11 +287,10 @@ # TODO """ -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 -import re +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 +from ..module_utils.prism.vms import VM # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 def get_module_spec(): diff --git a/tests/test_entity.py b/tests/test_entity.py index 79d45de8b..bd0e2da8f 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -3,10 +3,8 @@ __metaclass__ = type 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: From 82a03126af7d1b4a5e90c888ee207ffc93de935f Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 09:40:58 +0000 Subject: [PATCH 04/15] some flake8 fix --- plugins/inventory/ntnx_prism_vm_inventory.py | 9 ++++----- plugins/modules/ntnx_vms.py | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py index a13cdf232..cfb12ac18 100644 --- a/plugins/inventory/ntnx_prism_vm_inventory.py +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -65,12 +65,11 @@ requirements: "null" """ -import json -import tempfile -from ansible.errors import AnsibleError -from ansible.plugins.inventory import BaseInventoryPlugin +import json # noqa: E402 +import tempfile # noqa: E402 +from ansible.plugins.inventory import BaseInventoryPlugin # noqa: E402 -from ..module_utils.prism import vms +from ..module_utils.prism import vms # noqa: E402 class Mock_Module: diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index d8ebbbe71..91010b028 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -287,10 +287,10 @@ # TODO """ -from ..module_utils.base_module import BaseModule # noqa: E402 -from ..module_utils.prism.tasks import Task # noqa: E402 -from ..module_utils.prism.vms import VM # noqa: E402 -from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 +from ..module_utils.prism.vms import VM # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 def get_module_spec(): From f555d389662393870515cdf8873b28ef7485999f Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 10:21:09 +0000 Subject: [PATCH 05/15] Issirt fixes --- plugins/module_utils/base_module.py | 3 +-- plugins/module_utils/entity.py | 8 ++++---- tests/test_entity.py | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/base_module.py b/plugins/module_utils/base_module.py index e0bae4b8b..bfd85bf9b 100644 --- a/plugins/module_utils/base_module.py +++ b/plugins/module_utils/base_module.py @@ -2,8 +2,7 @@ # 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 -from ansible.module_utils.basic import env_fallback +from ansible.module_utils.basic import AnsibleModule, env_fallback __metaclass__ = type diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index 7a561444f..48ba9aa3d 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -1,17 +1,17 @@ # 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 ansible.module_utils._text import to_text +from ansible.module_utils.urls import fetch_url + try: - from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse + from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse except ImportError: from urlparse import urlparse # python2 diff --git a/tests/test_entity.py b/tests/test_entity.py index bd0e2da8f..ae1db0a01 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -2,9 +2,10 @@ __metaclass__ = type +import json + from plugins.module_utils.prism.vms import VM -import json class Module: From 7f5bcbb8814f4fac702a001cda45d986f664ea42 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 10:28:23 +0000 Subject: [PATCH 06/15] sanity fixes --- plugins/inventory/ntnx_prism_vm_inventory.py | 1 + plugins/module_utils/entity.py | 2 +- plugins/module_utils/prism/vms.py | 9 ++++----- tests/test_entity.py | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py index cfb12ac18..5b67d261e 100644 --- a/plugins/inventory/ntnx_prism_vm_inventory.py +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -67,6 +67,7 @@ import json # noqa: E402 import tempfile # noqa: E402 + from ansible.plugins.inventory import BaseInventoryPlugin # noqa: E402 from ..module_utils.prism import vms # noqa: E402 diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index 48ba9aa3d..fe0edd928 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -6,10 +6,10 @@ import json from base64 import b64encode + from ansible.module_utils._text import to_text from ansible.module_utils.urls import fetch_url - try: from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse except ImportError: diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index bfe6b9457..e6d7a9ee8 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -4,17 +4,16 @@ __metaclass__ = type -from copy import deepcopy - import base64 import os - +from copy import deepcopy from .clusters import Cluster +from .groups import Groups +from .images import Image from .prism import Prism from .projects import Project from .subnets import Subnet -from .groups import Groups -from .images import Image + class VM(Prism): diff --git a/tests/test_entity.py b/tests/test_entity.py index ae1db0a01..82cb518ac 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -7,7 +7,6 @@ from plugins.module_utils.prism.vms import VM - class Module: def __init__(self): self.params = { From fb64f5f06318be01c3fd19d259013a0ba1169e05 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 10:31:35 +0000 Subject: [PATCH 07/15] sanity fix --- plugins/module_utils/prism/vms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index e6d7a9ee8..ae373dbd6 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -7,6 +7,7 @@ import base64 import os from copy import deepcopy + from .clusters import Cluster from .groups import Groups from .images import Image @@ -15,7 +16,6 @@ from .subnets import Subnet - class VM(Prism): def __init__(self, module): resource_type = "/vms" From 1e3414e7e50785d7eff78b673090ad08cabded3b Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Sun, 30 Jan 2022 10:45:42 +0000 Subject: [PATCH 08/15] Flake8 Fix --- plugins/module_utils/prism/vms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index ae373dbd6..4129abac3 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -178,6 +178,7 @@ def _build_spec_networks(self, payload, networks): payload["spec"]["resources"]["nic_list"] = nics return payload, None + # flake8: noqa: C901 def _build_spec_disks(self, payload, vdisks): disks = [] scsi_index = sata_index = pci_index = ide_index = 0 From fc44e6011d4d4558a3d2e70ccbeb24ba250ddc72 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Tue, 1 Feb 2022 06:09:21 +0000 Subject: [PATCH 09/15] integration test --- .../targets/nutanix_vms/tasks/create.yml | 682 +++++++++++++----- .../targets/nutanix_vms/tasks/delete.yml | 50 +- .../targets/nutanix_vms/vars/main.yml | 32 +- 3 files changed, 528 insertions(+), 236 deletions(-) diff --git a/tests/integration/targets/nutanix_vms/tasks/create.yml b/tests/integration/targets/nutanix_vms/tasks/create.yml index 185846f67..7d1001dbf 100644 --- a/tests/integration/targets/nutanix_vms/tasks/create.yml +++ b/tests/integration/targets/nutanix_vms/tasks/create.yml @@ -1,195 +1,487 @@ -- name: create Vm With Mannaged subnet and subnet_name - nutanix_vms: - state: present - name: Vm_Net_Name - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - cluster: - cluster_uuid: '{{cluster.uuid}}' - networks: - - connected: true - subnet_name: '{{networks.static.name}}' - private_ip: '{{networks.static.ip}}' - register: result - ignore_errors: true -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: 'Unable to Create Vm With Mannaged subnet and subnet_name ' - success_msg: 'VM with With Mannaged subnet and subnet_name Created successfully ' -- set_fact: - todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - when: result.response.status.state == 'SUCCEEDED' -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: Unable to delete Vm - success_msg: VM Deleted successfully -- name: create VM with Minimum Requiremnts - nutanix_vms: - state: present - name: MinVm - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - cluster: - cluster_uuid: '{{cluster.uuid}}' - register: result - ignore_errors: true -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: Unable to Create Vm with Minimum Requiremnts - success_msg: VM with Minimum Requiremnts Created successfully -- set_fact: - todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - when: result.response.status.state == 'SUCCEEDED' -- name: Create Vm With Unamannaged vlan - nutanix_vms: - desc: This is vm test description - state: present - name: Vm-Nics - timezone: UTC - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - cluster: - cluster_uuid: '{{cluster.uuid}}' - networks: - - connected: false - subnet_uuid: '{{networks.dhcp.uuid}}' - boot_type: LEGACY - boot_device_order_list: - - DISK - - CDROM - - NETWORK - vcpus: 1 - cores_per_vcpu: 1 - memory_gb: 1 - register: result - ignore_errors: true -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: Unable to Create Vm With Unamannaged vlan - success_msg: VM With Unamannaged vlan Created successfully -- set_fact: - todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - when: result.response.status.state == 'SUCCEEDED' -- name: 'Create Vm with diffrent disk types and diffrent sizes ,UEFI' - nutanix_vms: - state: present - name: Vm_disks - timezone: GMT - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - cluster: - cluster_uuid: '{{cluster.uuid}}' - categories: - AppType: - - Apache_Spark - disks: - - type: "DISK" - clone_image: "CentOS-7-cloudinit" - bus: "SCSI" - - type: DISK - size_gb: 1 - bus: SCSI - - type: DISK - size_gb: 2 - bus: PCI - storage_container: - uuid: '{{storage_config.uuid}}' - - type: DISK - size_gb: 3 - bus: SATA - boot_type: UEFI - boot_device_order_list: - - DISK - - CDROM - - NETWORK - vcpus: 2 - cores_per_vcpu: 1 - memory_gb: 1 - register: result - ignore_errors: true -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: 'Unable to Create Vm with diffrent disk types and diffrent sizes ,UEFI ' - success_msg: >- - VM with with diffrent disk types and diffrent sizes ,UEFI Created - successfully -- set_fact: - todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - when: result.response.status.state == 'SUCCEEDED' -- name: create Vm With different disks and both Mannaged and UnMannaged network - nutanix_vms: - state: present - name: Vm_Disks_NIC - timezone: UTC - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - cluster: - cluster_uuid: '{{cluster.uuid}}' - networks: - - connected: true - subnet_uuid: '{{networks.dhcp.uuid}}' - - connected: true - subnet_uuid: '{{networks.static.uuid}}' - disks: - - type: DISK - size_gb: 1 - bus: SCSI - - type: DISK - size_gb: 3 - bus: PCI - - type: CDROM - bus: SATA - - type: CDROM - bus: IDE - boot_type: UEFI - boot_device_order_list: - - DISK - - CDROM - - NETWORK - vcpus: 2 - cores_per_vcpu: 2 - memory_gb: 2 - memory_overcommit_enabled: true - register: result - ignore_errors: true -- name: assert when status not complete - assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: Unable to Create Vm with both Mannaged and UnMannaged network - success_msg: >- - VM with With different disks and both Mannaged and UnMannaged network - Created successfully -- set_fact: - todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - when: result.response.status.state == 'SUCCEEDED' -- name: Delete all Created VM - nutanix_vms: - uuid: '{{ item }}' - state: absent - auth: - credentials: '{{credentials}}' - url: '{{config.ip_address}}:{{config.port}}' - register: result - loop: '{{ todelete }}' \ No newline at end of file + + - name: Add Script to attached file test.yml + copy: + dest: "test.yml" + content: | + #cloud-config + chpasswd: + list: | + root:Nutanix.123 + expire: False + fqdn: myNutanixVM + + + + ################################################################ + + - name: VM with ubuntu image and different specifications + ntnx_vms: + state: present + nutanix_host: "{{ IP }}" + validate_certs: False + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + name: "VM with Ubuntu image" + desc: "VM with cluster, network, category, disk with Ubuntu image, guest customization " + categories: + AppType: + - "Apache_Spark" + cluster: + name: "{{ClusterName}}" + networks: + - is_connected: True + subnet: + name: "{{ NetworkName }}" + disks: + - type: "DISK" + size_gb: 30 + bus: "SATA" + clone_image: + name: "{{ Ubuntu }}" + vcpus: 1 + cores_per_vcpu: 1 + memory_gb: 1 + guest_customization: + type: "cloud_init" + script_path: "./test.yml" + is_overridable: True + register: result + + + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: 'Unable to Create VM with Ubuntu image and different specifications ' + success_msg: 'VM with Ubuntu image and different specifications created successfully ' + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + + ######################################################################################### + + + - name: VM with CentOS-7-cloud-init image + ntnx_vms: + state: present + name: VM with CentOS-7-cloud-init image + timezone: "UTC" + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + cluster: + name: "{{ ClusterName }}" + disks: + - type: "DISK" + size_gb: 10 + clone_image: + name: "{{ CentOS }}" + bus: "SCSI" + guest_customization: + type: "cloud_init" + script_path: "./test.yml" + is_overridable: True + register: result + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: 'Unable to create VM with CentOS-7-cloud-init image' + success_msg: 'VM with CentOS-7-cloud-init image created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + ################################################################################# + + + - name: VM with Cluster, Network, Universal time zone, one Disk + ntnx_vms: + state: present + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "VM with Cluster Network and Disk" + timezone: "Universal" + cluster: + name: "{{ ClusterName }}" + networks: + - is_connected: True + subnet: + name: "{{ NetworkName }}" + disks: + - type: "DISK" + size_gb: 10 + bus: "PCI" + register: result + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: 'Unable to create VM with Cluster , Network, Universal time zone, one Disk' + success_msg: 'VM with Cluster , Network, Universal time zone, one Disk created successfully ' + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + ######################################################################################## + + - name: VM with Cluster, different Disks, Memory size + ntnx_vms: + state: present + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "VM with different disks" + timezone: "UTC" + cluster: + name: "{{ ClusterName }}" + disks: + - type: "DISK" + size_gb: 10 + bus: "SATA" + - type: "DISK" + size_gb: 30 + bus: "SCSI" + memory_gb: 20 + register: result + ignore_errors: True + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: 'Unable to create VM with Cluster, different Disks, Memory size' + success_msg: 'VM with Cluster, different Disks, Memory size created successfully ' + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + ##################################################################################### + + - name: VM with Cluster, different CDROMs + ntnx_vms: + state: present + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "VM with multiple CDROMs" + cluster: + name: "{{ ClusterName }}" + disks: + - type: "CDROM" + size_gb: 10 + bus: "SATA" + - type: "CDROM" + size_gb: 10 + bus: "IDE" + cores_per_vcpu: 1 + register: result + ignore_errors: True + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to Create VM with Cluster, different CDROMS ' + success_msg: ' VM with Cluster, different CDROMS created successfully ' + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + #################################################################################### + + - name: VM with all specification + ntnx_vms: + state: present + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "All specification" + timezone: "GMT" + cluster: + name: "{{ ClusterName }}" + disks: + - type: "DISK" + size_gb: 2 + bus: "SCSI" + - type: "DISK" + size_gb: 10 + bus: "PCI" + - type: "DISK" + size_gb: 2 + bus: "SATA" + - type: "DISK" + size_gb: 10 + bus: "SCSI" + - type: "CDROM" + size_gb: 10 + bus: "IDE" + boot_config: + boot_type: "LEGACY" + boot_order: + - "DISK" + - "CDROM" + - "NETWORK" + vcpus: 2 + cores_per_vcpu: 2 + memory_gb: 2 + register: result + ignore_errors: True + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with all specification ' + success_msg: ' VM with all specification created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + ################################################################################################## + + + - name: VM with managed subnet + ntnx_vms: + state: present + name: VM with managed subnet + nutanix_host: "{{ IP }}" + validate_certs: False + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + cluster: + name: "{{ ClusterName }}" + networks: + - is_connected: true + subnet: + name: "{{ StaticNetworkName }}" + private_ip: "{{ StaticNetworkIP }}" + register: result + ignore_errors: true + + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with managed subnet ' + success_msg: ' VM with with managed subnet created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + ################################################################################################### + + - name: VM with minimum requiremnts + ntnx_vms: + state: present + name: MinReqVM + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + cluster: + name: "{{ ClusterName }}" + register: result + ignore_errors: true + + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with minimum requiremnts ' + success_msg: ' VM with minimum requiremnts created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + ################################################################################################## + + - name: VM with unmanaged vlan + ntnx_vms: + desc: "VM with unmanaged vlan" + state: present + name: VM with unmanaged vlan + timezone: UTC + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + cluster: + name: "{{ ClusterName }}" + networks: + - is_connected: false + subnet: + uuid: "{{ dhcpUUID }}" + boot_config: + boot_type: LEGACY + boot_order: + - DISK + - CDROM + - NETWORK + vcpus: 1 + cores_per_vcpu: 1 + memory_gb: 1 + register: result + ignore_errors: true + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with unmanaged vlan ' + success_msg: ' VM with unmanaged vlan created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + ################################################################################################# + + - name: VM with diffrent disk types and diffrent sizes with UEFI boot type + ntnx_vms: + state: present + name: VM with UEFI boot type + timezone: GMT + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + cluster: + name: "{{ ClusterName }}" + categories: + AppType: + - Apache_Spark + disks: + - type: "DISK" + clone_image: + name: "{{ Ubuntu }}" + bus: "SCSI" + size_gb: 20 + - type: DISK + size_gb: 1 + bus: SCSI + - type: DISK + size_gb: 2 + bus: PCI + storage_container: + name: "{{ StorageContainerName }}" + - type: DISK + size_gb: 3 + bus: SATA + boot_config: + boot_type: UEFI + boot_order: + - DISK + - CDROM + - NETWORK + vcpus: 2 + cores_per_vcpu: 1 + memory_gb: 1 + register: result + + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with diffrent disk types and diffrent sizes with UEFI boot type ' + success_msg: ' VM with diffrent disk types and diffrent sizes with UEFI boot type created successfully ' + + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + ###################################################################################### + + - name: VM with managed and unmanaged network + ntnx_vms: + state: present + name: VM_NIC + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + timezone: UTC + cluster: + name: "{{ ClusterName }}" + networks: + - is_connected: true + subnet: + uuid: "{{ dhcpUUID }}" + - is_connected: true + subnet: + uuid: "{{ StaticNetworkUUID }}" + disks: + - type: DISK + size_gb: 1 + bus: SCSI + - type: DISK + size_gb: 3 + bus: PCI + - type: CDROM + bus: SATA + size_gb: 1 + - type: CDROM + bus: IDE + size_gb: 1 + boot_config: + boot_type: UEFI + boot_order: + - DISK + - CDROM + - NETWORK + vcpus: 2 + cores_per_vcpu: 2 + memory_gb: 2 + register: result + ignore_errors: true + + - name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with managed and unmanaged network ' + success_msg: ' VM with managed and unmanaged network created successfully ' + + - set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + + + ######################################################################################### + - name: Delete all Created VM + + ntnx_vms: + state: absent + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + vm_uuid: '{{ item }}' + register: result + loop: '{{ todelete }}' + + + + \ No newline at end of file diff --git a/tests/integration/targets/nutanix_vms/tasks/delete.yml b/tests/integration/targets/nutanix_vms/tasks/delete.yml index 9d79f99c8..a91d30e35 100644 --- a/tests/integration/targets/nutanix_vms/tasks/delete.yml +++ b/tests/integration/targets/nutanix_vms/tasks/delete.yml @@ -1,36 +1,40 @@ --- -- name: create VM with Minimum Requirment - nutanix_vms: - state: present - name: "MinVm" - auth: - credentials: "{{credentials}}" - url: "{{config.ip_address}}:{{config.port}}" - cluster: - cluster_uuid: "{{cluster.uuid}}" +- name: VM with minimum requiremnts + ntnx_vms: + state: present + name: MinReqVM + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + cluster: + name: "{{ ClusterName }}" register: result - ignore_errors: True - -- name: assert when status not complete + ignore_errors: true + +- name: Creation Status assert: - that: - - result.response is defined - - result.response.status.state == 'SUCCEEDED' - fail_msg: "Unable to delete Vm" - success_msg: "VM Created successfully" + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + fail_msg: ' Unable to create VM with minimum requiremnts ' + success_msg: ' VM with minimum requiremnts created successfully ' + - name: delete vm - nutanix_vms: - uuid: '{{ result["response"]["metadata"]["uuid"] }}' + ntnx_vms: + vm_uuid: '{{ result["response"]["metadata"]["uuid"] }}' state: absent - auth: - credentials: "{{credentials}}" - url: "{{config.ip_address}}:{{config.port}}" + nutanix_host: "{{ IP }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False register: result + - name: assert when status not complete assert: that: - result.response is defined - - result.response.status.state == 'SUCCEEDED' + - result.response.status == 'SUCCEEDED' fail_msg: "Unable to delete Vm" success_msg: "VM Deleted successfully" diff --git a/tests/integration/targets/nutanix_vms/vars/main.yml b/tests/integration/targets/nutanix_vms/vars/main.yml index 089ac36b5..ae12a0060 100644 --- a/tests/integration/targets/nutanix_vms/vars/main.yml +++ b/tests/integration/targets/nutanix_vms/vars/main.yml @@ -1,20 +1,16 @@ --- -credentials: - username: admin - password: Nutanix.123 -config: - ip_address: 10.44.76.190 - port: 9440 - -cluster: - uuid: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" -networks: - static: - name: "static_subnet" - uuid: "72c5057d-93f7-4389-a01a-2c2f42eae3ef" - ip: "10.30.30.75" - dhcp: - uuid: "9343da22-886f-4055-88d2-3a8278c68700" -storage_config: - uuid: "4446ca0b-7846-4a6f-b00a-386736432121" +IP: 10.44.76.190 +username: "admin" +password: "Nutanix.123" +CentOS: "CentOS-7-cloudinit" +Ubuntu: "Ubuntu1404" +ClusterName: "auto_cluster_prod_1aa888141361" +ClusterUUID: "0005d578-2faf-9fb6-3c07-ac1f6b6f9780" +NetworkName: "vlan.800" +dhcpUUID: "9343da22-886f-4055-88d2-3a8278c68700" +StaticNetworkName: "static_subnet" +StaticNetworkUUID: "72c5057d-93f7-4389-a01a-2c2f42eae3ef" +StaticNetworkIP: 10.30.30.75 +StorageConfigUUID: "98b0fa80-a74b-4dac-b1b9-0fe90b2f5526" +StorageContainerName: "SelfServiceContainer" todelete: [] \ No newline at end of file From eaba8aff5a8ccaf0ec08675873572127b51eb986 Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Tue, 1 Feb 2022 06:28:07 +0000 Subject: [PATCH 10/15] black fix --- nutanix-ncp-1.0.0.tar.gz | Bin 0 -> 35117 bytes plugins/module_utils/prism/vms.py | 13 +++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 nutanix-ncp-1.0.0.tar.gz diff --git a/nutanix-ncp-1.0.0.tar.gz b/nutanix-ncp-1.0.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8118564a6496bcb6652cb9ba9958b30c2db7fbf2 GIT binary patch literal 35117 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 Date: Tue, 1 Feb 2022 07:59:55 +0000 Subject: [PATCH 11/15] fix --- plugins/module_utils/entity.py | 2 +- plugins/module_utils/prism/groups.py | 2 +- plugins/module_utils/prism/vms.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index fe0edd928..3b6fe298d 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -68,7 +68,7 @@ def list(self, data=None, endpoint=None, use_base_url=False, timeout=30): def get_uuid(self, name): data = {"filter": "name=={0}".format(name), "length": 1} - resp, tmp = self.list(data) + resp, status = self.list(data) if resp and resp.get("entities"): return resp["entities"][0]["metadata"]["uuid"] return None diff --git a/plugins/module_utils/prism/groups.py b/plugins/module_utils/prism/groups.py index dc36f4ab3..5b876db59 100644 --- a/plugins/module_utils/prism/groups.py +++ b/plugins/module_utils/prism/groups.py @@ -14,7 +14,7 @@ def __init__(self, module): def get_uuid(self, entity_type, filter): data = {"entity_type": entity_type, "filter_criteria": filter} - resp, tmp = self.list(data, use_base_url=True) + resp, status = self.list(data, use_base_url=True) if resp.get("group_results"): return resp["group_results"][0]["entity_results"][0]["entity_id"] return None diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 7191d7841..9e4585720 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -41,7 +41,7 @@ def get_spec(self): for ansible_param, ansible_value in self.module.params.items(): build_spec_method = self.build_spec_methods.get(ansible_param) if build_spec_method and ansible_value: - tmp, error = build_spec_method(spec, ansible_value) + spec, error = build_spec_method(spec, ansible_value) if error: return None, error return spec, None From 2e6cd57831af577005b10a43c2db8c54dbd9d45a Mon Sep 17 00:00:00 2001 From: alaa-bish Date: Tue, 1 Feb 2022 08:30:17 +0000 Subject: [PATCH 12/15] Edit integration test --- .../targets/nutanix_vms/tasks/create.yml | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/tests/integration/targets/nutanix_vms/tasks/create.yml b/tests/integration/targets/nutanix_vms/tasks/create.yml index 7d1001dbf..c08214a1f 100644 --- a/tests/integration/targets/nutanix_vms/tasks/create.yml +++ b/tests/integration/targets/nutanix_vms/tasks/create.yml @@ -1,7 +1,7 @@ - - name: Add Script to attached file test.yml + - name: Create Cloud init Script file copy: - dest: "test.yml" + dest: "cloud_init.yml" content: | #cloud-config chpasswd: @@ -43,7 +43,7 @@ memory_gb: 1 guest_customization: type: "cloud_init" - script_path: "./test.yml" + script_path: "./cloud_init.yml" is_overridable: True register: result @@ -58,7 +58,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - + when: result.response.status.state == 'COMPLETE' ######################################################################################### @@ -83,9 +83,10 @@ bus: "SCSI" guest_customization: type: "cloud_init" - script_path: "./test.yml" + script_path: "./cloud_init.yml" is_overridable: True register: result + ignore_errors: True - name: Creation Status assert: that: @@ -97,7 +98,8 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - + when: result.response.status.state == 'COMPLETE' + ################################################################################# @@ -131,7 +133,8 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - + when: result.response.status.state == 'COMPLETE' + ######################################################################################## - name: VM with Cluster, different Disks, Memory size @@ -165,6 +168,8 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' + ##################################################################################### @@ -199,7 +204,8 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - + when: result.response.status.state == 'COMPLETE' + #################################################################################### @@ -252,6 +258,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' ################################################################################################## @@ -285,6 +292,8 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' + ################################################################################################### @@ -313,7 +322,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' - + when: result.response.status.state == 'COMPLETE' ################################################################################################## @@ -355,6 +364,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' ################################################################################################# @@ -411,6 +421,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' ###################################################################################### @@ -467,6 +478,7 @@ - set_fact: todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + when: result.response.status.state == 'COMPLETE' ######################################################################################### From e6fb8e991b2d38d30a06d874e8ac3afa4fd523eb Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Tue, 1 Feb 2022 16:26:22 +0400 Subject: [PATCH 13/15] clear device indexes calculating --- plugins/module_utils/prism/vms.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 9e4585720..0b81ece50 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -182,30 +182,22 @@ def _build_spec_networks(self, payload, networks): def _build_spec_disks(self, payload, vdisks): disks = [] - scsi_index = sata_index = pci_index = ide_index = 0 + device_indexes = {} for vdisk in vdisks: disk = self._get_default_disk_spec() - if "type" in vdisk: + if vdisk.get("type"): 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 + if vdisk.get("bus"): + if device_indexes.get(vdisk["bus"]): + device_indexes[vdisk["bus"]] += 1 + else: + device_indexes[vdisk["bus"]] = 0 disk["device_properties"]["disk_address"]["adapter_type"] = vdisk["bus"] - disk["device_properties"]["disk_address"]["device_index"] = device_index + disk["device_properties"]["disk_address"]["device_index"] = device_indexes[vdisk["bus"]] if vdisk.get("empty_cdrom"): disk.pop("disk_size_bytes") From 2c6f93f22a2d805f811f38b64c8157a4084f297d Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Tue, 1 Feb 2022 16:34:53 +0400 Subject: [PATCH 14/15] clear device indexes calculating --- plugins/module_utils/prism/vms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index 0b81ece50..e3f84881a 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -197,7 +197,9 @@ def _build_spec_disks(self, payload, vdisks): device_indexes[vdisk["bus"]] = 0 disk["device_properties"]["disk_address"]["adapter_type"] = vdisk["bus"] - disk["device_properties"]["disk_address"]["device_index"] = device_indexes[vdisk["bus"]] + disk["device_properties"]["disk_address"][ + "device_index" + ] = device_indexes[vdisk["bus"]] if vdisk.get("empty_cdrom"): disk.pop("disk_size_bytes") From 9efa6a70c4298800d6192768218a97fa6f23d435 Mon Sep 17 00:00:00 2001 From: Gevorg-Khachatryaan Date: Tue, 1 Feb 2022 16:36:47 +0400 Subject: [PATCH 15/15] revert lost changes --- galaxy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/galaxy.yml b/galaxy.yml index d43b4ea70..24f3eb21d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -6,6 +6,7 @@ authors: - "Balu George (@balugeorge)" - "Sarath Kumar K (@kumarsarath588)" - "Prem Karat (@premkarat)" + - "Gevorg Khachatryan (@Gevorg-Khachatryan-97)" description: Ansible collection for v3 Nutanix APIs https://www.nutanix.dev/api-reference-v3/ license_file: 'LICENSE' tags: [nutanix, prism, ahv]