From f4174ea138fce27ba385e83bf3cd1ae6f6fdfd27 Mon Sep 17 00:00:00 2001 From: Pradeepsingh Bhati Date: Mon, 4 Jul 2022 23:46:46 +0530 Subject: [PATCH] Release v1.3 features (#220) * security rules basic structure #207 * security rules spec * security rules create and delete functionality * Fix sanity and doc * security rules update functionality * security rules info module * add integraioson test * security rules spec fixes * security rules spec fixes * security rules spec fixes * security rules spec fixes, rule update functionality * security rules requirements fixes * fix sanity * security rules requirements and spec generation fixes * security rules requirements and spec generation fixes * Add integration test * sanity fix * Add integration tests * security rules requirements and spec generation fixes * sanity fix * Add integration test * ad_rule -> vdi_rule * update protocols spec, ability to delete rule * update protocols spec * updates for protocols spec * updates for protocols spec * Fix integration test * updates for isolation rule * updates for app, ad quarantine rules * fix integraton * Nutanix Image Module (#211) * Image ansible module for creating images in PC * Image placement policy module and its info module (#214) * Image placement policy module * Add functionality to deattach all the categories from vm using flag remove_categories (#216) * Add functionality to remove all categories from vm * Update docs * Vm example minor fix * list functionality for address/service groups * black and isort fixes * black and isort fixes * black and isort fixes * black and isort fixes * update requirements * fixes * ADGroup fixes * ADGroup fixes * remove default values for fields which are updatable * Doc fix * allow all updates * allow all updates * Common spec for categories mapping * Formatting * Formatting changes * formatting * formatting * flake8 changes * Sanity fix * config changes * Update categories in tests * categories mapping * categories mapping optimizations * default_internal_policy fixes * default_internal_policy fixes * fixes for target categories updating * Fix tests * fixes for rules description * remove categories mapping and prject reference * Fix tests * fix integration * flake8 fix * black and isort fixes * black and isort fixes * examples fixes * isolation rule docs fixes * fix sanity * galaxy file changes * sanity fix * docs fixes * vm's create ova and clone example fixes * fix sanity * fix sanity * doc fixes * doc fixes * Setup config changes * fix test * Security info module fix * doc fix * Lint fixes * setup config changes * quarantine rule uuid changes * sanity fix * Update and info module for static routes (#221) Static routes module and info module * UUID changes * Changes for sanity and config * sanity fix * static routes minor formatting * setup config changes * formatting * Fix bug dynamic inventory bug due to load_params * changelog and readme updates * Docs changes * Change log changes * Update docs * Docs update * Minor fix Co-authored-by: Gevorg-Khachatryaan Co-authored-by: alaa-bish Co-authored-by: Gevorg Khachatryan <95351366+Gevorg-Khachatryan-97@users.noreply.github.com> Co-authored-by: Yannick Struyf --- CHANGELOG.md | 14 + README.md | 27 +- examples/vm.yml | 3 +- examples/vm_operations.yml | 14 +- galaxy.yml | 2 +- meta/runtime.yml | 14 +- plugins/inventory/ntnx_prism_vm_inventory.py | 3 +- plugins/module_utils/entity.py | 17 +- plugins/module_utils/prism/address_groups.py | 43 + .../prism/image_placement_policy.py | 75 + plugins/module_utils/prism/images.py | 111 +- plugins/module_utils/prism/prism.py | 6 +- plugins/module_utils/prism/security_rules.py | 240 +++ plugins/module_utils/prism/service_groups.py | 43 + .../prism/spec/categories_mapping.py | 41 + plugins/module_utils/prism/static_routes.py | 93 ++ plugins/module_utils/prism/users.py | 39 + plugins/module_utils/prism/vms.py | 14 +- plugins/module_utils/prism/vpn_connections.py | 33 + plugins/module_utils/utils.py | 2 + plugins/modules/ntnx_floating_ips_info.py | 4 +- .../ntnx_image_placement_policies_info.py | 289 ++++ .../modules/ntnx_image_placement_policy.py | 432 ++++++ plugins/modules/ntnx_images.py | 509 +++++++ plugins/modules/ntnx_images_info.py | 231 +++ plugins/modules/ntnx_pbrs_info.py | 4 +- plugins/modules/ntnx_security_rules.py | 1327 +++++++++++++++++ plugins/modules/ntnx_security_rules_info.py | 232 +++ plugins/modules/ntnx_static_routes.py | 390 +++++ plugins/modules/ntnx_static_routes_info.py | 205 +++ plugins/modules/ntnx_subnets_info.py | 4 +- plugins/modules/ntnx_vms.py | 11 + plugins/modules/ntnx_vms_info.py | 4 +- plugins/modules/ntnx_vpcs_info.py | 4 +- .../meta/main.yml | 2 + .../tasks/info.yml | 107 ++ .../tasks/main.yml | 9 + .../ntnx_image_placement_policy/meta/main.yml | 2 + .../tasks/create.yml | 127 ++ .../tasks/delete.yml | 57 + .../tasks/main.yml | 11 + .../tasks/update.yml | 130 ++ .../targets/ntnx_images/meta/main.yml | 2 + .../targets/ntnx_images/tasks/create.yml | 147 ++ .../targets/ntnx_images/tasks/delete.yml | 53 + .../targets/ntnx_images/tasks/main.yml | 11 + .../targets/ntnx_images/tasks/update.yml | 124 ++ .../targets/ntnx_images_info/meta/main.yml | 2 + .../targets/ntnx_images_info/tasks/info.yml | 50 + .../targets/ntnx_images_info/tasks/main.yml | 9 + .../targets/ntnx_security_rules/meta/main.yml | 2 + .../ntnx_security_rules/tasks/app_rule.yml | 176 +++ .../tasks/isolation_rule.yml | 133 ++ .../ntnx_security_rules/tasks/main.yml | 12 + .../tasks/quarantine_rule.yml | 35 + .../targets/ntnx_security_rules/tasks/vdi.yml | 102 ++ .../targets/ntnx_security_rules/vars/main.yml | 1 + .../ntnx_security_rules_info/meta/main.yml | 2 + .../tasks/get_security_rules.yml | 95 ++ .../ntnx_security_rules_info/tasks/main.yml | 9 + .../targets/ntnx_static_routes/aliases | 0 .../targets/ntnx_static_routes/meta/main.yml | 2 + .../ntnx_static_routes/tasks/create.yml | 134 ++ .../targets/ntnx_static_routes/tasks/main.yml | 9 + .../targets/ntnx_static_routes_info/aliases | 0 .../ntnx_static_routes_info/meta/main.yml | 2 + .../ntnx_static_routes_info/tasks/info.yml | 71 + .../ntnx_static_routes_info/tasks/main.yml | 9 + .../targets/ntnx_vms_clone/vars/main.yml | 6 +- .../targets/nutanix_vms/tasks/create.yml | 6 +- .../targets/nutanix_vms/tasks/vm_update.yml | 49 + .../targets/nutanix_vms/vars/main.yml | 6 +- .../targets/prepare_env/tasks/cleanup.yml | 5 + .../targets/prepare_env/tasks/prepare_env.yml | 5 + .../targets/prepare_env/vars/main.yml | 14 +- 75 files changed, 6147 insertions(+), 61 deletions(-) create mode 100644 plugins/module_utils/prism/address_groups.py create mode 100644 plugins/module_utils/prism/image_placement_policy.py create mode 100644 plugins/module_utils/prism/security_rules.py create mode 100644 plugins/module_utils/prism/service_groups.py create mode 100644 plugins/module_utils/prism/spec/categories_mapping.py create mode 100644 plugins/module_utils/prism/static_routes.py create mode 100644 plugins/module_utils/prism/users.py create mode 100644 plugins/module_utils/prism/vpn_connections.py create mode 100644 plugins/modules/ntnx_image_placement_policies_info.py create mode 100644 plugins/modules/ntnx_image_placement_policy.py create mode 100644 plugins/modules/ntnx_images.py create mode 100644 plugins/modules/ntnx_images_info.py create mode 100644 plugins/modules/ntnx_security_rules.py create mode 100644 plugins/modules/ntnx_security_rules_info.py create mode 100644 plugins/modules/ntnx_static_routes.py create mode 100644 plugins/modules/ntnx_static_routes_info.py create mode 100644 tests/integration/targets/ntnx_image_placement_policies_info/meta/main.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policies_info/tasks/info.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policies_info/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policy/meta/main.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policy/tasks/create.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policy/tasks/delete.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policy/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_image_placement_policy/tasks/update.yml create mode 100644 tests/integration/targets/ntnx_images/meta/main.yml create mode 100644 tests/integration/targets/ntnx_images/tasks/create.yml create mode 100644 tests/integration/targets/ntnx_images/tasks/delete.yml create mode 100644 tests/integration/targets/ntnx_images/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_images/tasks/update.yml create mode 100644 tests/integration/targets/ntnx_images_info/meta/main.yml create mode 100644 tests/integration/targets/ntnx_images_info/tasks/info.yml create mode 100644 tests/integration/targets/ntnx_images_info/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_security_rules/meta/main.yml create mode 100644 tests/integration/targets/ntnx_security_rules/tasks/app_rule.yml create mode 100644 tests/integration/targets/ntnx_security_rules/tasks/isolation_rule.yml create mode 100644 tests/integration/targets/ntnx_security_rules/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_security_rules/tasks/quarantine_rule.yml create mode 100644 tests/integration/targets/ntnx_security_rules/tasks/vdi.yml create mode 100644 tests/integration/targets/ntnx_security_rules/vars/main.yml create mode 100644 tests/integration/targets/ntnx_security_rules_info/meta/main.yml create mode 100644 tests/integration/targets/ntnx_security_rules_info/tasks/get_security_rules.yml create mode 100644 tests/integration/targets/ntnx_security_rules_info/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_static_routes/aliases create mode 100644 tests/integration/targets/ntnx_static_routes/meta/main.yml create mode 100644 tests/integration/targets/ntnx_static_routes/tasks/create.yml create mode 100644 tests/integration/targets/ntnx_static_routes/tasks/main.yml create mode 100644 tests/integration/targets/ntnx_static_routes_info/aliases create mode 100644 tests/integration/targets/ntnx_static_routes_info/meta/main.yml create mode 100644 tests/integration/targets/ntnx_static_routes_info/tasks/info.yml create mode 100644 tests/integration/targets/ntnx_static_routes_info/tasks/main.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d667c8ca7..8fb890f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v1.3.0 (4 July 2022) + +**Features** + +**Prism Central** +- Ansible module for Image Management +- Ansible info module for Image Management +- Ansible module for Image Placement Policy +- Ansible info module for Image Placement Policies +- Ansible module for Network Security Rules +- Ansible info module for Network Security Rules +- Ansible module for VPC Static Routes +- Ansible info module for VPC Static Routes + ## v1.2.0 (3 June 2022) **Features** diff --git a/README.md b/README.md index bfce75d12..d77a41df4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ It is designed keeping simplicity as the core value. Hence it is ## Prism Cenral > For the 1.1.0 release of the ansible plugin it will have N-2 compatibility with the Prism Central APIs. This release was tested against Prism Central versions pc2022.1.0.2, pc.2021.9.0.5 and pc.2021.8.0.1. > For the 1.2.0 release of the ansible plugin it will have N-2 compatibility with the Prism Central APIs. This release was tested against Prism Central versions pc.2022.4, pc2022.1.0.2 and pc.2021.9.0.5. +> For the 1.3.0 release of the ansible plugin it will have N-2 compatibility with the Prism Central APIs. This release was tested against Prism Central versions pc.2022.4, pc2022.1.0.2 and pc.2021.9.0.4. Static routes module (ntnx_static_routes) is only supported for PC versions >= pc.2022.1. Prism Central based examples: https://github.com/nutanix/nutanix.ansible/tree/main/examples/ @@ -98,24 +99,32 @@ ansible-playbook examples/iaas/iaas.yml | Name | Description | | --- | --- | +| ntnx_floating_ips | Create or delete a Floating Ip. | +| ntnx_floating_ips_info | List existing Floating_Ips. | +| ntnx_images | Create, update or delete a image. | +| ntnx_images_info | List existing images. | +| ntnx_image_placement_policy | Create, update or delete a image placement policy. | +| ntnx_image_placement_policies_info | List existing image placement policies. | +| ntnx_pbrs | Create or delete a PBR. | +| ntnx_pbrs_info | List existing PBRs. | +| ntnx_security_rules | Create, update or delete a Security Rule. | +| ntnx_security_rules_info | List existing Security Rules. | +| ntnx_static_routes | Update static routes of a vpc. | +| ntnx_static_routes_info | List existing static routes of a vpc. | +| ntnx_subnets | Create or delete a Subnet. | +| ntnx_subnets_info | List existing Subnets. | | ntnx_vms | Create or delete a VM. | | ntnx_vms_clone | Clone VM. | | ntnx_vms_ova | Create OVA image from VM. | | ntnx_vms_info | List existing VMs. | | ntnx_vpcs | Create or delete a VPC. | | ntnx_vpcs_info | List existing VPCs. | -| ntnx_subnets | Create or delete a Subnet. | -| ntnx_subnets_info | List existing Subnets. | -| ntnx_floating_ips | Create or delete a Floating Ip. | -| ntnx_floating_ips_info | List existing Floating_Ips. | -| ntnx_pbrs | Create or delete a PBR. | -| ntnx_pbrs_info | List existing PBRs. | | ntnx_foundation | Image nodes and create new cluster. | -| ntnx_foundation_bmc_ipmi_config | Configure IPMI IP address on BMC of nodes. | -| ntnx_foundation_image_upload | Upload hypervisor or AOS image to Foundation VM. | | ntnx_foundation_aos_packages_info | List the AOS packages uploaded to Foundation. | +| ntnx_foundation_bmc_ipmi_config | Configure IPMI IP address on BMC of nodes. | | ntnx_foundation_discover_nodes_info | List the nodes discovered by Foundation. | | ntnx_foundation_hypervisor_images_info | List the hypervisor images uploaded to Foundation. | +| ntnx_foundation_image_upload | Upload hypervisor or AOS image to Foundation VM. | | ntnx_foundation_node_network_info | Get node network information discovered by Foundation. | | ntnx_foundation_central | Create a cluster out of nodes registered with Foundation Central. | | ntnx_foundation_central_api_keys | Create a new api key which will be used by remote nodes to authenticate with Foundation Central. | @@ -127,7 +136,7 @@ ansible-playbook examples/iaas/iaas.yml | Name | Description | | --- | --- | -| ntnx_vms_inventory | Nutanix VMs inventory source | +| ntnx_prism_vm_inventory | Nutanix VMs inventory source | # Module documentation and examples ``` diff --git a/examples/vm.yml b/examples/vm.yml index 43850b078..87197f302 100644 --- a/examples/vm.yml +++ b/examples/vm.yml @@ -47,7 +47,7 @@ is_overridable: True register: output - - name: output of list Subnets + - name: output of vm created debug: msg: '{{ output }}' @@ -55,4 +55,3 @@ ntnx_vms: state: absent vm_uuid: "{{output.vm_uuid}}" - register: output diff --git a/examples/vm_operations.yml b/examples/vm_operations.yml index 2eaa5d67b..ac5d1aa94 100644 --- a/examples/vm_operations.yml +++ b/examples/vm_operations.yml @@ -27,21 +27,19 @@ ignore_errors: true - name: create_ova_image while vm is on - ntnx_vms: + ntnx_vms_ova: state: present - vm_uuid: "{{ vm_uuid }}" - operation: create_ova_image - ova_name: integration_test_VMDK_ova - ova_file_format: VMDK + src_vm_uuid: "{{ vm_uuid }}" + name: integration_test_VMDK_ova + file_format: VMDK wait: true register: result ignore_errors: true - name: clone vm while it's off also add network and script - ntnx_vms: + ntnx_vms_clone: state: present - vm_uuid: "{{ vm_uuid }}" - operation: clone + src_vm_uuid: "{{ vm_uuid }}" networks: - is_connected: true subnet: diff --git a/galaxy.yml b/galaxy.yml index 7852cf54f..816b8afbc 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: "nutanix" name: "ncp" -version: "1.2.0" +version: "1.3.0" readme: "README.md" authors: - "Abhishek Chaudhary (@abhimutant)" diff --git a/meta/runtime.yml b/meta/runtime.yml index 677a289ed..ac591d208 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -3,17 +3,25 @@ requires_ansible: '>=2.11.6' action_groups: ntnx: - ntnx_floating_ips + - ntnx_images + - ntnx_image_placement_policy - ntnx_pbrs - - ntnx_vms - - ntnx_vpcs + - ntnx_security_rules + - ntnx_static_routes - ntnx_subnets - ntnx_vms_ova - ntnx_vms_clone + - ntnx_vms + - ntnx_vpcs - ntnx_floating_ips_info + - ntnx_images_info + - ntnx_image_placement_policies_info - ntnx_pbrs_info + - ntnx_security_rules_info + - ntnx_static_routes_info + - ntnx_subnets_info - ntnx_vms_info - ntnx_vpcs_info - - ntnx_subnets_info - ntnx_foundation_aos_packages_info - ntnx_foundation_bmc_ipmi_config - ntnx_foundation_discover_nodes_info diff --git a/plugins/inventory/ntnx_prism_vm_inventory.py b/plugins/inventory/ntnx_prism_vm_inventory.py index 456880339..43c0ba216 100644 --- a/plugins/inventory/ntnx_prism_vm_inventory.py +++ b/plugins/inventory/ntnx_prism_vm_inventory.py @@ -82,6 +82,7 @@ def __init__(self, host, port, username, password, validate_certs=False): "nutanix_username": username, "nutanix_password": password, "validate_certs": validate_certs, + "load_params_without_defaults": False, } def jsonify(self, data): @@ -150,7 +151,7 @@ def parse(self, inventory, loader, path, cache=True): 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": + if endpoint["type"] in ["ASSIGNED", "LEARNED"]: vm_ip = endpoint["ip"] nic_count += 1 continue diff --git a/plugins/module_utils/entity.py b/plugins/module_utils/entity.py index f53185c7f..45006484e 100644 --- a/plugins/module_utils/entity.py +++ b/plugins/module_utils/entity.py @@ -114,6 +114,7 @@ def upload( self, source, endpoint=None, + method="POST", query=None, raise_error=True, no_response=False, @@ -125,7 +126,7 @@ def upload( return self._upload_file( url, source, - method="POST", + method=method, raise_error=raise_error, no_response=no_response, timeout=timeout, @@ -353,13 +354,15 @@ def _fetch_url( def _upload_file( self, url, source, method, raise_error=True, no_response=False, timeout=30 ): - + file_chunks_iterator = FileChunksIterator(source) + headers = copy.deepcopy(self.headers) + headers["Content-Length"] = file_chunks_iterator.length resp, info = fetch_url( self.module, url, - data=FileChunksIterator(source), + data=file_chunks_iterator, method=method, - headers=self.headers, + headers=headers, cookies=self.cookies, timeout=timeout, ) @@ -372,6 +375,12 @@ def _upload_file( resp_json = None if not raise_error: + # Add error details and status details if any + if not resp_json: + resp_json = {} + if status_code >= 300: + resp_json["error"] = body + resp_json["status_code"] = status_code return resp_json if status_code >= 300: diff --git a/plugins/module_utils/prism/address_groups.py b/plugins/module_utils/prism/address_groups.py new file mode 100644 index 000000000..cbcff71cf --- /dev/null +++ b/plugins/module_utils/prism/address_groups.py @@ -0,0 +1,43 @@ +# 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 + + +class AddressGroup(Prism): + def __init__(self, module): + resource_type = "/address_groups" + super(AddressGroup, self).__init__(module, resource_type=resource_type) + + def get_uuid(self, value, key="name", raise_error=True, no_response=False): + data = {"filter": "{0}=={1}".format(key, value), "length": 1} + resp = self.list(data, raise_error=raise_error, no_response=no_response) + entities = resp.get("entities") if resp else None + if entities: + for entity in entities: + if entity["address_group"]["name"] == value: + return entity["uuid"] + return None + + +# Helper functions + + +def get_address_uuid(config, module): + if "name" in config: + address_group = AddressGroup(module) + name = config["name"] + uuid = address_group.get_uuid(name) + if not uuid: + error = "Address {0} not found.".format(name) + return None, error + elif "uuid" in config: + uuid = config["uuid"] + else: + error = "Config {0} doesn't have name or uuid key".format(config) + return None, error + + return uuid, None diff --git a/plugins/module_utils/prism/image_placement_policy.py b/plugins/module_utils/prism/image_placement_policy.py new file mode 100644 index 000000000..51aa4e8f5 --- /dev/null +++ b/plugins/module_utils/prism/image_placement_policy.py @@ -0,0 +1,75 @@ +# 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 + +from .prism import Prism +from .spec.categories_mapping import CategoriesMapping + +__metaclass__ = type + + +class ImagePlacementPolicy(Prism): + def __init__(self, module): + resource_type = "/images/placement_policies" + super(ImagePlacementPolicy, self).__init__(module, resource_type=resource_type) + self.build_spec_methods = { + "name": self._build_spec_name, + "desc": self._build_spec_desc, + "categories": CategoriesMapping.build_categories_mapping_spec, + "remove_categories": CategoriesMapping.build_remove_all_categories_spec, + "placement_type": self._build_spec_placement_type, + "image_categories": self._build_spec_image_categories, + "cluster_categories": self._build_spec_cluster_categories, + } + + def _get_default_spec(self): + return deepcopy( + { + "api_version": "3.1.0", + "metadata": { + "kind": "image_placement_policy", + }, + "spec": { + "name": None, + "resources": { + "image_entity_filter": { + "params": {}, + "type": "CATEGORIES_MATCH_ANY", + }, + "cluster_entity_filter": { + "params": {}, + "type": "CATEGORIES_MATCH_ANY", + }, + }, + }, + } + ) + + def _build_spec_name(self, payload, name): + payload["spec"]["name"] = name + return payload, None + + def _build_spec_desc(self, payload, desc): + payload["spec"]["description"] = desc + return payload, None + + def _build_spec_placement_type(self, payload, type): + if type == "hard": + payload["spec"]["resources"]["placement_type"] = "EXACTLY" + else: + payload["spec"]["resources"]["placement_type"] = "AT_LEAST" + return payload, None + + def _build_spec_image_categories(self, payload, category_mappings): + payload["spec"]["resources"]["image_entity_filter"][ + "params" + ] = category_mappings + return payload, None + + def _build_spec_cluster_categories(self, payload, category_mappings): + payload["spec"]["resources"]["cluster_entity_filter"][ + "params" + ] = category_mappings + return payload, None diff --git a/plugins/module_utils/prism/images.py b/plugins/module_utils/prism/images.py index 398086d78..43c0bff4a 100644 --- a/plugins/module_utils/prism/images.py +++ b/plugins/module_utils/prism/images.py @@ -2,15 +2,120 @@ # 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 +from .clusters import Cluster from .prism import Prism +from .spec.categories_mapping import CategoriesMapping + +__metaclass__ = type class Image(Prism): - def __init__(self, module): + def __init__(self, module, upload_image=False): + additional_headers = None + if upload_image: + additional_headers = { + "Content-Type": "application/octet-stream", + "Accept": "application/json", + } + + # add checksum headers if given incase of local upload + checksum = module.params.get("checksum") + if checksum and module.params.get("source_path"): + additional_headers["X-Nutanix-Checksum-Type"] = checksum[ + "checksum_algorithm" + ] + additional_headers["X-Nutanix-Checksum-Bytes"] = checksum[ + "checksum_value" + ] + resource_type = "/images" - super(Image, self).__init__(module, resource_type=resource_type) + super(Image, self).__init__( + module, resource_type=resource_type, additional_headers=additional_headers + ) + self.build_spec_methods = { + "name": self._build_spec_name, + "desc": self._build_spec_desc, + "remove_categories": CategoriesMapping.build_remove_all_categories_spec, + "categories": CategoriesMapping.build_categories_mapping_spec, + "source_uri": self._build_spec_source_uri, + "checksum": self._build_spec_checksum, + "image_type": self._build_spec_image_type, + "clusters": self._build_spec_clusters, + "version": self._build_spec_version, + } + + def upload_image(self, image_uuid, source_path, timeout=600, raise_error=True): + endpoint = "{0}/file".format(image_uuid) + return self.upload( + source_path, + endpoint=endpoint, + method="PUT", + timeout=timeout, + no_response=True, + raise_error=raise_error, + ) + + def _get_default_spec(self): + return deepcopy( + { + "api_version": "3.1.0", + "metadata": { + "kind": "image", + }, + "spec": { + "name": None, + "resources": { + "architecture": "X86_64", + }, + }, + } + ) + + def _build_spec_name(self, payload, name): + payload["spec"]["name"] = name + return payload, None + + def _build_spec_desc(self, payload, desc): + payload["spec"]["description"] = desc + return payload, None + + def _build_spec_source_uri(self, payload, source_uri): + payload["spec"]["resources"]["source_uri"] = source_uri + return payload, None + + def _build_spec_checksum(self, payload, checksum): + if self.module.params.get("source_uri"): + payload["spec"]["resources"]["checksum"] = checksum + return payload, None + + def _build_spec_image_type(self, payload, image_type): + payload["spec"]["resources"]["image_type"] = image_type + return payload, None + + def _build_spec_clusters(self, payload, clusters): + cluster_references = [] + for cluster_ref in clusters: + if "name" in cluster_ref: + cluster = Cluster(self.module) + name = cluster_ref["name"] + uuid = cluster.get_uuid(name) + if not uuid: + error = "Cluster {0} not found.".format(name) + return None, error + + elif "uuid" in cluster_ref: + uuid = cluster_ref["uuid"] + + cluster_references.append({"uuid": uuid, "kind": "cluster"}) + + payload["spec"]["resources"]["initial_placement_ref_list"] = cluster_references + return payload, None + + def _build_spec_version(self, payload, version): + payload["spec"]["resources"]["version"] = version + return payload, None def get_image_uuid(config, module): diff --git a/plugins/module_utils/prism/prism.py b/plugins/module_utils/prism/prism.py index fc1892bf5..8e9d96e7e 100644 --- a/plugins/module_utils/prism/prism.py +++ b/plugins/module_utils/prism/prism.py @@ -8,6 +8,8 @@ class Prism(Entity): __BASEURL__ = "/api/nutanix/v3" - def __init__(self, module, resource_type): + def __init__(self, module, resource_type, additional_headers=None): resource_type = self.__BASEURL__ + resource_type - super(Prism, self).__init__(module, resource_type) + super(Prism, self).__init__( + module, resource_type, additional_headers=additional_headers + ) diff --git a/plugins/module_utils/prism/security_rules.py b/plugins/module_utils/prism/security_rules.py new file mode 100644 index 000000000..c8a7d391a --- /dev/null +++ b/plugins/module_utils/prism/security_rules.py @@ -0,0 +1,240 @@ +# 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 + +from .address_groups import get_address_uuid +from .prism import Prism +from .service_groups import get_service_uuid + +__metaclass__ = type + + +class SecurityRule(Prism): + def __init__(self, module): + resource_type = "/network_security_rules" + super(SecurityRule, self).__init__(module, resource_type=resource_type) + self.build_spec_methods = { + "name": self._build_spec_name, + "desc": self._build_spec_desc, + "allow_ipv6_traffic": self._build_allow_ipv6_traffic, + "is_policy_hitlog_enabled": self._build_is_policy_hitlog_enabled, + "vdi_rule": self._build_vdi_rule, + "app_rule": self._build_app_rule, + "isolation_rule": self._build_isolation_rule, + "quarantine_rule": self._build_quarantine_rule, + } + + def _get_default_spec(self): + return deepcopy( + { + "api_version": "3.1.0", + "metadata": {"kind": "network_security_rule"}, + "spec": { + "name": None, + "resources": {"is_policy_hitlog_enabled": False}, + }, + } + ) + + def _build_spec_name(self, payload, value): + payload["spec"]["name"] = value + return payload, None + + def _build_spec_desc(self, payload, value): + payload["spec"]["description"] = value + return payload, None + + def _build_allow_ipv6_traffic(self, payload, value): + payload["spec"]["resources"]["allow_ipv6_traffic"] = value + return payload, None + + def _build_is_policy_hitlog_enabled(self, payload, value): + payload["spec"]["resources"]["is_policy_hitlog_enabled"] = value + return payload, None + + def _build_vdi_rule(self, payload, value): + ad_rule = payload["spec"]["resources"].get("ad_rule", {}) + payload["spec"]["resources"]["ad_rule"] = self._build_spec_rule(ad_rule, value) + return payload, None + + def _build_app_rule(self, payload, value): + app_rule = payload["spec"]["resources"].get("app_rule", {}) + payload["spec"]["resources"]["app_rule"] = self._build_spec_rule( + app_rule, value + ) + return payload, None + + def _build_isolation_rule(self, payload, value): + isolation_rule = payload["spec"]["resources"].get("isolation_rule", {}) + if not isolation_rule.get("first_entity_filter") and not isolation_rule.get( + "second_entity_filter" + ): + if value.get("isolate_category"): + isolation_rule["first_entity_filter"] = self._get_default_filter_spec() + isolation_rule["first_entity_filter"]["params"] = value[ + "isolate_category" + ] + + if value.get("from_category"): + isolation_rule["second_entity_filter"] = self._get_default_filter_spec() + isolation_rule["second_entity_filter"]["params"] = value[ + "from_category" + ] + if value.get("subset_category"): + category_key = next(iter(value["subset_category"])) + category_value = value["subset_category"][category_key] + for category in isolation_rule.values(): + if category_key in category["params"]: + category["params"][category_key].extend(category_value) + else: + category["params"].update(value["subset_category"]) + + if value.get("policy_mode"): + isolation_rule["action"] = value["policy_mode"] + payload["spec"]["resources"]["isolation_rule"] = isolation_rule + return payload, None + + def _build_quarantine_rule(self, payload, value): + if payload["spec"]["resources"].get("quarantine_rule"): + quarantine_rule = payload["spec"]["resources"]["quarantine_rule"] + payload["spec"]["resources"]["quarantine_rule"] = self._build_spec_rule( + quarantine_rule, value + ) + return payload, None + + def _build_spec_rule(self, payload, value): + rule = payload + + if value.get("target_group"): + target_group = {} + params = {} + categories = value["target_group"].get("categories", {}) + if categories.get("adgroup"): + params["ADGroup"] = [categories["adgroup"]] + if value["target_group"].get("default_internal_policy"): + target_group["default_internal_policy"] = value["target_group"][ + "default_internal_policy" + ] + if categories.get("apptype"): + params["AppType"] = [categories["apptype"]] + if categories.get("apptier"): + params["AppTier"] = [categories.get("apptier")] + if value["target_group"].get("default_internal_policy"): + target_group["default_internal_policy"] = value["target_group"][ + "default_internal_policy" + ] + if categories.get("apptype_filter_by_category"): + params.update(**categories["apptype_filter_by_category"]) + + target_group["filter"] = ( + payload.get("target_group", {}).get("filter") + or self._get_default_filter_spec() + ) + if params: + target_group["filter"]["params"] = params + target_group["peer_specification_type"] = "FILTER" + payload["target_group"] = target_group + + if value.get("inbounds"): + rule["inbound_allow_list"] = self._generate_bound_spec( + rule.get("inbound_allow_list", []), value["inbounds"] + ) + elif value.get("allow_all_inbounds"): + rule["inbound_allow_list"] = [{"peer_specification_type": "ALL"}] + if value.get("outbounds"): + rule["outbound_allow_list"] = self._generate_bound_spec( + rule.get("outbound_allow_list", []), value["outbounds"] + ) + elif value.get("allow_all_outbounds"): + rule["outbound_allow_list"] = [{"peer_specification_type": "ALL"}] + if value.get("policy_mode"): + rule["action"] = value["policy_mode"] + return rule + + def _generate_bound_spec(self, payload, list_of_rules): + for rule in list_of_rules: + if rule.get("rule_id"): + rule_spec = self._filter_by_uuid(rule["rule_id"], payload) + if rule.get("state") == "absent": + payload.remove(rule_spec) + continue + else: + rule_spec = {} + if rule.get("categories"): + rule_spec["filter"] = self._get_default_filter_spec() + rule_spec["filter"]["params"] = rule["categories"] + rule_spec["peer_specification_type"] = "FILTER" + elif rule.get("ip_subnet"): + rule_spec["ip_subnet"] = rule["ip_subnet"] + rule_spec["peer_specification_type"] = "IP_SUBNET" + elif rule.get("address"): + address_group = rule["address"] + + if address_group.get("uuid"): + address_group["kind"] = "address_group" + rule_spec["address_group_inclusion_list"] = [address_group] + elif address_group.get("name"): + uuid, error = get_address_uuid(address_group, self.module) + if error: + self.module.fail_json( + msg="Failed generating Security Rule Spec", + error="Entity {0} not found.".format(address_group["name"]), + ) + + address_group["kind"] = "address_group" + address_group["uuid"] = uuid + rule_spec["address_group_inclusion_list"] = [address_group] + + rule_spec["peer_specification_type"] = "IP_SUBNET" + + if rule.get("protocol"): + self._generate_protocol_spec(rule_spec, rule["protocol"]) + if rule.get("description"): + rule_spec["description"] = rule["description"] + if not rule_spec.get("rule_id"): + payload.append(rule_spec) + return payload + + def _generate_protocol_spec(self, payload, config): + if config.get("tcp"): + payload["protocol"] = "TCP" + payload["tcp_port_range_list"] = config["tcp"] + elif config.get("udp"): + payload["protocol"] = "UDP" + payload["udp_port_range_list"] = config["udp"] + elif config.get("icmp"): + payload["protocol"] = "ICMP" + payload["icmp_type_code_list"] = config["icmp"] + elif config.get("service"): + service = config["service"] + + if service.get("uuid"): + service["kind"] = "service_group" + payload["service_group_list"] = [service] + elif service.get("name"): + uuid, error = get_service_uuid(service, self.module) + if error: + self.module.fail_json( + msg="Failed generating Security Rule Spec", + error="Entity {0} not found.".format(service["name"]), + ) + + service["kind"] = "service_group" + service["uuid"] = uuid + payload["service_group_list"] = [service] + + def _get_default_filter_spec(self): + return deepcopy( + {"type": "CATEGORIES_MATCH_ALL", "kind_list": ["vm"], "params": {}} + ) + + def _filter_by_uuid(self, uuid, items_list): + try: + return next(filter(lambda d: d.get("rule_id") == uuid, items_list)) + except BaseException: + self.module.fail_json( + msg="Failed generating VM Spec", + error="Entity {0} not found.".format(uuid), + ) diff --git a/plugins/module_utils/prism/service_groups.py b/plugins/module_utils/prism/service_groups.py new file mode 100644 index 000000000..99f39cacd --- /dev/null +++ b/plugins/module_utils/prism/service_groups.py @@ -0,0 +1,43 @@ +# 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 + + +class ServiceGroup(Prism): + def __init__(self, module): + resource_type = "/service_groups" + super(ServiceGroup, self).__init__(module, resource_type=resource_type) + + def get_uuid(self, value, key="name", raise_error=True, no_response=False): + data = {"filter": "{0}=={1}".format(key, value), "length": 1} + resp = self.list(data, raise_error=raise_error, no_response=no_response) + entities = resp.get("entities") if resp else None + if entities: + for entity in entities: + if entity["service_group"]["name"] == value: + return entity["uuid"] + return None + + +# Helper functions + + +def get_service_uuid(config, module): + if "name" in config: + service_group = ServiceGroup(module) + name = config["name"] + uuid = service_group.get_uuid(name) + if not uuid: + error = "Service {0} not found.".format(name) + return None, error + elif "uuid" in config: + uuid = config["uuid"] + else: + error = "Config {0} doesn't have name or uuid key".format(config) + return None, error + + return uuid, None diff --git a/plugins/module_utils/prism/spec/categories_mapping.py b/plugins/module_utils/prism/spec/categories_mapping.py new file mode 100644 index 000000000..241fd6fbf --- /dev/null +++ b/plugins/module_utils/prism/spec/categories_mapping.py @@ -0,0 +1,41 @@ +# 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 + +__metaclass__ = type + + +class CategoriesMapping: + """Categories mapping related helpers and spec""" + + @staticmethod + def build_categories_mapping_spec(payload, categories_mapping): + """ + This routine overrides categories mapping in a pc v3 api input payload + Args: + payload(dict): api payload + categories(dict): categories to override in payload + """ + if not payload.get("metadata"): + error = "metadata missing in payload for building categories mapping spec" + return None, error + if payload["metadata"].get("categories_mapping") != categories_mapping: + payload["metadata"]["use_categories_mapping"] = True + payload["metadata"]["categories_mapping"] = categories_mapping + return payload, None + + @staticmethod + def build_remove_all_categories_spec(payload, remove_categories): + """ + This routine removes all categories from pc v3 api input payload + Args: + payload(dict): api payload + remove_categories(dict): flag to remove all categories from payload + """ + if not payload.get("metadata"): + error = "metadata missing in payload for removing all categories" + return None, error + if remove_categories and payload["metadata"].get("categories_mapping"): + payload["metadata"]["use_categories_mapping"] = True + payload["metadata"]["categories_mapping"] = {} + return payload, None diff --git a/plugins/module_utils/prism/static_routes.py b/plugins/module_utils/prism/static_routes.py new file mode 100644 index 000000000..9c2369c1e --- /dev/null +++ b/plugins/module_utils/prism/static_routes.py @@ -0,0 +1,93 @@ +# 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 + +from .subnets import get_subnet_uuid +from .vpcs import Vpc +from .vpn_connections import get_vpn_connection_uuid + + +class StaticRoutes(Vpc): + default_route_dest = "0.0.0.0/0" + route_tables_endpoint = "route_tables" + + def __init__(self, module): + super(StaticRoutes, self).__init__(module) + self.build_spec_methods = { + "static_routes": self._build_spec_static_routes, + "remove_all_routes": self._build_spec_remove_all_routes, + } + + def update_static_routes(self, data, vpc_uuid): + return self.update( + data=data, uuid=vpc_uuid, endpoint=self.route_tables_endpoint + ) + + def get_static_routes(self, vpc_uuid): + return self.read(uuid=vpc_uuid, endpoint=self.route_tables_endpoint) + + def _get_default_spec(self): + return deepcopy( + { + "metadata": {"kind": "vpc_route_table"}, + "spec": { + "resources": { + "static_routes_list": [], + "default_route_nexthop": None, + } + }, + } + ) + + def _build_default_route_spec(self, payload, next_hop): + if payload["spec"]["resources"].get("default_route_nexthop"): + error = "More than one default routes are not allowed" + return None, error + payload["spec"]["resources"]["default_route_nexthop"] = next_hop + return payload, None + + def _build_spec_static_routes(self, payload, inp_static_routes): + # since static route list has to be overriden + if payload["spec"]["resources"].get("default_route_nexthop"): + payload["spec"]["resources"].pop("default_route_nexthop") + static_routes_list = [] + for route in inp_static_routes: + next_hop = {} + if route["next_hop"].get("external_subnet_ref"): + subnet_ref = route["next_hop"]["external_subnet_ref"] + uuid, err = get_subnet_uuid(subnet_ref, self.module) + if err: + return None, err + next_hop["external_subnet_reference"] = {"kind": "subnet", "uuid": uuid} + elif route["next_hop"].get("vpn_connection_ref"): + vpn_ref = route["next_hop"]["vpn_connection_ref"] + uuid, err = get_vpn_connection_uuid(self.module, vpn_ref) + if err: + return None, err + next_hop["vpn_connection_reference"] = { + "kind": "vpn_connection", + "uuid": uuid, + } + + if route["destination"] == self.default_route_dest: + default_spec, err = self._build_default_route_spec(payload, next_hop) + if err: + return None, err + else: + static_routes_list.append( + {"nexthop": next_hop, "destination": route["destination"]} + ) + + payload["spec"]["resources"]["static_routes_list"] = static_routes_list + return payload, None + + def _build_spec_remove_all_routes(self, payload, remove_all_routes): + if remove_all_routes: + if payload["spec"]["resources"].get("default_route_nexthop"): + payload["spec"]["resources"].pop("default_route_nexthop") + payload["spec"]["resources"]["static_routes_list"] = [] + return payload, None diff --git a/plugins/module_utils/prism/users.py b/plugins/module_utils/prism/users.py new file mode 100644 index 000000000..f67f1f879 --- /dev/null +++ b/plugins/module_utils/prism/users.py @@ -0,0 +1,39 @@ +# 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 + + +class Users(Prism): + def __init__(self, module): + resource_type = "/users" + super(Users, self).__init__(module, resource_type=resource_type) + + def get_uuid(self, value, key="username", raise_error=True, no_response=False): + data = {"filter": "{0}=={1}".format(key, value), "length": 1} + resp = self.list(data, raise_error=raise_error, no_response=no_response) + entities = resp.get("entities") if resp else None + if entities: + for entity in entities: + if entity["status"]["name"] == value: + return entity["metadata"]["uuid"] + return None + + +def get_user_uuid(config, module): + if "name" in config: + users = Users(module) + name = config["name"] + uuid = users.get_uuid(name) + if not uuid: + + error = "User {0} not found.".format(name) + return None, error + + elif "uuid" in config: + uuid = config["uuid"] + + return uuid diff --git a/plugins/module_utils/prism/vms.py b/plugins/module_utils/prism/vms.py index a703628c8..c69af6c83 100644 --- a/plugins/module_utils/prism/vms.py +++ b/plugins/module_utils/prism/vms.py @@ -15,6 +15,7 @@ from .images import get_image_uuid from .prism import Prism from .projects import Project +from .spec.categories_mapping import CategoriesMapping from .subnets import Subnet @@ -22,7 +23,10 @@ class VM(Prism): def __init__(self, module): resource_type = "/vms" super(VM, self).__init__(module, resource_type=resource_type) - self.params_without_defaults = _load_params() + if self.module.params.get("load_params_without_defaults", True): + self.params_without_defaults = _load_params() + else: + self.params_without_defaults = self.module.params self.require_vm_restart = False self.build_spec_methods = { "name": self._build_spec_name, @@ -37,7 +41,8 @@ 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": CategoriesMapping.build_categories_mapping_spec, + "remove_categories": CategoriesMapping.build_remove_all_categories_spec, } @staticmethod @@ -323,11 +328,6 @@ def _build_spec_timezone(self, payload, value): payload["spec"]["resources"]["hardware_clock_timezone"] = value return payload, None - def _build_spec_categories(self, payload, value): - payload["metadata"]["categories_mapping"] = value - payload["metadata"]["use_categories_mapping"] = True - return payload, None - def _check_and_set_require_vm_restart(self, current_value, new_value): if new_value < current_value: self.require_vm_restart = True diff --git a/plugins/module_utils/prism/vpn_connections.py b/plugins/module_utils/prism/vpn_connections.py new file mode 100644 index 000000000..c382e07ce --- /dev/null +++ b/plugins/module_utils/prism/vpn_connections.py @@ -0,0 +1,33 @@ +# 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 + + +class VpnConnection(Prism): + def __init__(self, module): + resource_type = "/vpn_connections" + super(VpnConnection, self).__init__(module, resource_type=resource_type) + + +# Helper functions + + +def get_vpn_connection_uuid(module, config): + if "name" in config: + vpn_obj = VpnConnection(module) + name = config.get("name") + uuid = vpn_obj.get_uuid(name) + if not uuid: + error = "VPN connection {0} not found.".format(name) + return None, error + elif "uuid" in config: + uuid = config.get("uuid") + else: + error = "Config {0} doesn't have name or uuid key".format(config) + None, error + + return uuid, None diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py index 18cb2cf6f..0d481bbb0 100644 --- a/plugins/module_utils/utils.py +++ b/plugins/module_utils/utils.py @@ -38,10 +38,12 @@ def check_for_idempotency(spec, resp, **kwargs): if spec == resp: if ( state == "present" + # only for VMs or ( state in ["soft_shutdown", "hard_poweroff", "power_off"] and resp["spec"]["resources"]["power_state"] == "OFF" ) + # only for VMs or ( state == "power_on" and resp["spec"]["resources"]["power_state"] == "ON" ) diff --git a/plugins/modules/ntnx_floating_ips_info.py b/plugins/modules/ntnx_floating_ips_info.py index a09311a24..4421db48d 100644 --- a/plugins/modules/ntnx_floating_ips_info.py +++ b/plugins/modules/ntnx_floating_ips_info.py @@ -150,7 +150,7 @@ def get_fip(module, result): result["response"] = resp -def list_fip(module, result): +def get_fips(module, result): floating_ip = FloatingIP(module) spec, error = floating_ip.get_info_spec() @@ -170,7 +170,7 @@ def run_module(): if module.params.get("fip_uuid"): get_fip(module, result) else: - list_fip(module, result) + get_fips(module, result) module.exit_json(**result) diff --git a/plugins/modules/ntnx_image_placement_policies_info.py b/plugins/modules/ntnx_image_placement_policies_info.py new file mode 100644 index 000000000..4e0644c95 --- /dev/null +++ b/plugins/modules/ntnx_image_placement_policies_info.py @@ -0,0 +1,289 @@ +#!/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: ntnx_image_placement_policies_info +short_description: image placement policies info module +version_added: 1.3.0 +description: 'Get image placement policy info' +options: + kind: + description: + - The kind name + type: str + default: image_placement_policy + policy_uuid: + description: + - policy UUID + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" +EXAMPLES = r""" + - name: Get image placement policy using policy_uuid + ntnx_image_placement_policies_info: + policy_uuid: "" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + filter: + - name: test_policy + register: result + + - name: List image placement policies using name filter criteria + ntnx_image_placement_policies_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + filter: + - name: test_policy + register: result + + - name: List image placement policies using length, offset + ntnx_image_placement_policies_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + length: 2 + offset: 1 + register: result +""" +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: Metadata for image placement policies list output + returned: always + type: dict + sample: { + "filter": "", + "kind": "image_placement_policy", + "length": 2, + "offset": 0, + "total_matches": 2 + } +entities: + description: image placement policies intent response + returned: always + type: list + sample: [ + { + "metadata": { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-06-15T22:04:26Z", + "kind": "image_placement_policy", + "last_update_time": "2022-06-15T22:04:28Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 0, + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec": { + "name": "test_policy_1", + "resources": { + "cluster_entity_filter": { + "params": { + "AppTier": [ + "Default" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + } + }, + "status": { + "description": "", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "test_policy_1", + "resources": { + "cluster_entity_filter": { + "params": { + "AppTier": [ + "Default" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + }, + "state": "COMPLETE" + } + }, + { + "metadata": { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-06-15T22:04:33Z", + "kind": "image_placement_policy", + "last_update_time": "2022-06-15T22:04:35Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 0, + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec": { + "name": "test_policy_2", + "resources": { + "cluster_entity_filter": { + "params": { + "AppTier": [ + "Default" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + } + }, + "status": { + "description": "", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "test_policy_2", + "resources": { + "cluster_entity_filter": { + "params": { + "AppTier": [ + "Default" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + }, + "state": "COMPLETE" + } + }, + ] +""" + + +from ..module_utils.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.prism.image_placement_policy import ( # noqa: E402 + ImagePlacementPolicy, +) +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + + module_args = dict( + policy_uuid=dict(type="str"), + kind=dict(type="str", default="image_placement_policy"), + sort_order=dict(type="str"), + sort_attribute=dict(type="str"), + ) + + return module_args + + +def get_placement_policy(module, result): + policy_obj = ImagePlacementPolicy(module) + uuid = module.params.get("policy_uuid") + resp = policy_obj.read(uuid) + result["response"] = resp + + +def get_placement_policies(module, result): + policy_obj = ImagePlacementPolicy(module) + spec, err = policy_obj.get_info_spec() + if err: + result["error"] = err + module.fail_json( + msg="Failed generating Image Placement Policies info Spec", **result + ) + resp = policy_obj.list(spec) + result["response"] = resp + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + required_together=[("sort_order", "sort_attribute")], + mutually_exclusive=[ + ("policy_uuid", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + if module.params.get("policy_uuid"): + get_placement_policy(module, result) + else: + get_placement_policies(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_image_placement_policy.py b/plugins/modules/ntnx_image_placement_policy.py new file mode 100644 index 000000000..9f87fb9ae --- /dev/null +++ b/plugins/modules/ntnx_image_placement_policy.py @@ -0,0 +1,432 @@ +#!/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: ntnx_image_placement_policy +short_description: image placement policy module which supports Create, update and delete operations +version_added: 1.3.0 +description: "Create, Update, Delete image placement policy" +options: + state: + description: + - Specify state + - If C(state) is set to C(present) then the operation will be create the item. + - if C(state) is set to C(present) and C(policy_uuid) is given then it will update that image placement policy. + - if C(state) is set to C(present) then C(image_uuid) or one of C(name), C(image_categories), C(cluster_categories) needs to be set. + - >- + If C(state) is set to C(absent) and if the item exists, then + item is removed. + choices: + - present + - absent + type: str + default: present + wait: + description: Wait for the CRUD operation to complete. + type: bool + required: false + default: True + name: + description: + - policy name + - allowed in update + required: false + type: str + policy_uuid: + description: + - image placement policy of existig uuid + - required only when updating or deleting + type: str + required: false + desc: + description: + - A description for policy + - allowed in update + required: false + type: str + placement_type: + description: + - placement type of the policy. + - allowed in update + type: str + required: false + choices: + - hard + - soft + image_categories: + description: + - categories for images which needs to be affected by this policy + - allowed in update + - this field cannot be empty + type: dict + required: false + cluster_categories: + description: + - categories for clusters which needs to be affected by this policy + - allowed in update + - this field cannot be empty + type: dict + required: false + categories: + description: + - Categories for the policy. This allows setting up multiple values from a single key. + - In update, it will override he existing categories attached to policy + - Mutually exclusive with C(remove_categories) + required: false + type: dict + remove_categories: + description: + - When set will remove all categories attached to the policy. + - Mutually exclusive ith C(categories) + - It doesnot remove C(image_categories) or C(cluster_categories) + required: false + type: bool + default: false +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" + +EXAMPLES = r""" +- name: Create image placement policy with minimal spec + ntnx_image_placement_policy: + state: "present" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "test_policy_1" + placement_type: soft + image_categories: + AppFamily: + - Backup + cluster_categories: + AppTier: + - Default + register: result + +- name: Create image placement policy with all specs and hard type + ntnx_image_placement_policy: + state: "present" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "test_policy_2" + desc: "test_policy_2_desc" + placement_type: hard + categories: + Environment: + - "Dev" + AppType: + - "Default" + image_categories: + AppFamily: + - Backup + - Networking + cluster_categories: + AppTier: + - Default + register: result + +- name: Update image placement policy + ntnx_image_placement_policy: + state: "present" + policy_uuid: "" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + name: "test_policy_2-uodated" + desc: "test_policy_2_desc-updated" + placement_type: hard + categories: + Environment: + - "Dev" + image_categories: + AppFamily: + - Backup + cluster_categories: + AppTier: + - Default + register: result + +- name: Remove all categories attached to policy + ntnx_image_placement_policy: + state: "present" + policy_uuid: "" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + remove_categories: True + register: result + +- name: Delete image placement policy + ntnx_image_placement_policy: + state: "absent" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + policy_uuid: "" + register: result +""" + +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: The image placement policy type kind metadata + returned: always + type: dict + sample: { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-06-16T08:03:59Z", + "kind": "image_placement_policy", + "last_update_time": "2022-06-16T08:04:01Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 0, + "uuid": "00000000-0000-0000-0000-000000000000" + } +spec: + description: An intentful representation of a image placement policy spec + returned: always + type: dict + sample: { + "description": "check123233", + "name": "test_policy_1", + "resources": { + "cluster_entity_filter": { + "params": { + "AppFamily": [ + "Networking" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup", + "Databases" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + } + } +status: + description: An intentful representation of a image placement policy status + returned: always + type: dict + sample: { + "description": "check123233", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "test_policy_1", + "resources": { + "cluster_entity_filter": { + "params": { + "AppFamily": [ + "Networking" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "image_entity_filter": { + "params": { + "AppFamily": [ + "Backup", + "Databases" + ] + }, + "type": "CATEGORIES_MATCH_ANY" + }, + "placement_type": "AT_LEAST" + }, + "state": "COMPLETE" + } +policy_uuid: + description: The created image placement policy uuid + returned: always + type: str + sample: "00000000-0000-0000-0000-000000000000" +""" + +from ..module_utils import utils # noqa: E402 +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.image_placement_policy import ( # noqa: E402 + ImagePlacementPolicy, +) +from ..module_utils.prism.tasks import Task # noqa: E402 + + +def get_module_spec(): + module_args = dict( + name=dict(type="str", required=False), + desc=dict(type="str", required=False), + remove_categories=dict(type="bool", required=False, default=False), + placement_type=dict(type="str", choices=["hard", "soft"], required=False), + image_categories=dict(type="dict", required=False), + cluster_categories=dict(type="dict", required=False), + categories=dict(type="dict", required=False), + policy_uuid=dict(type="str", required=False), + ) + return module_args + + +def create_policy(module, result): + policy_obj = ImagePlacementPolicy(module) + spec, error = policy_obj.get_spec() + if error: + result["error"] = error + module.fail_json( + msg="Failed generating create Image Placement Policies Spec", **result + ) + if module.check_mode: + result["response"] = spec + return + + # create image placement policies + resp = policy_obj.create(spec) + policy_uuid = resp["metadata"]["uuid"] + task_uuid = resp["status"]["execution_context"]["task_uuid"] + result["policy_uuid"] = policy_uuid + result["changed"] = True + + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + resp = policy_obj.read(policy_uuid) + + result["response"] = resp + + +def update_policy(module, result): + policy_obj = ImagePlacementPolicy(module) + policy_uuid = module.params.get("policy_uuid") + if not policy_uuid: + result["error"] = "Missing parameter policy_uuid in task" + module.fail_json(msg="Failed updating image placement policy", **result) + + # read the current state of policy + resp = policy_obj.read(policy_uuid) + utils.strip_extra_attrs_from_status(resp["status"], resp["spec"]) + resp["spec"] = resp.pop("status") + + # new spec for updating policy + update_spec, error = policy_obj.get_spec(resp) + if error: + result["error"] = error + module.fail_json( + msg="Failed generating Image Placement Policy update spec", **result + ) + + # check for idempotency using update spec and current spec + if resp == update_spec: + result["skipped"] = True + module.exit_json( + msg="Nothing to change. Refer docs to check for fields which can be updated" + ) + + result["policy_uuid"] = policy_uuid + if module.check_mode: + result["response"] = update_spec + return + + # update policy + resp = policy_obj.update(update_spec, uuid=policy_uuid) + policy_uuid = resp["metadata"]["uuid"] + task_uuid = resp["status"]["execution_context"]["task_uuid"] + result["policy_uuid"] = policy_uuid + + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + resp = policy_obj.read(policy_uuid) + + result["changed"] = True + result["response"] = resp + + +def delete_policy(module, result): + policy_uuid = module.params["policy_uuid"] + if not policy_uuid: + result["error"] = "Missing parameter policy_uuid in task" + module.fail_json(msg="Failed deleting Image placement policy", **result) + + policy_obj = ImagePlacementPolicy(module) + resp = policy_obj.delete(policy_uuid) + result["response"] = resp + result["changed"] = True + task_uuid = resp["status"]["execution_context"]["task_uuid"] + + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + + +def run_module(): + required_one_of_list = [ + ("policy_uuid", "name"), + ("policy_uuid", "image_categories"), + ("policy_uuid", "cluster_categories"), + ] + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_one_of=required_one_of_list, + mutually_exclusive=[ + ("categories", "remove_categories"), + ], + required_if=[("state", "absent", ("policy_uuid",))], + ) + result = { + "changed": False, + "error": None, + "response": None, + "policy_uuid": None, + } + utils.remove_param_with_none_value(module.params) + state = module.params["state"] + if state == "present": + if module.params.get("policy_uuid"): + update_policy(module, result) + else: + create_policy(module, result) + elif state == "absent": + delete_policy(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_images.py b/plugins/modules/ntnx_images.py new file mode 100644 index 000000000..fe22e6f40 --- /dev/null +++ b/plugins/modules/ntnx_images.py @@ -0,0 +1,509 @@ +#!/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: ntnx_images +short_description: images module which supports pc images management CRUD operations +version_added: 1.3.0 +description: "Create, Update, Delete images" +options: + state: + description: + - Specify state + - If C(state) is set to C(present) then the operation will be create the item. + - if C(state) is set to C(present) and C(image_uuid) is given then it will update that image. + - if C(state) is set to C(present) then C(image_uuid), C(source_uri) and C(source_path) are mutually exclusive. + - if C(state) is set to C(present) then C(image_uuid) or C(name) needs to be set. + - >- + If C(state) is set to C(absent) and if the item exists, then + item is removed. + choices: + - present + - absent + type: str + default: present + wait: + description: Wait for the CRUD operation to complete. + type: bool + required: false + default: True + name: + description: Image name + required: false + type: str + image_uuid: + description: Image uuid + type: str + required: false + desc: + description: A description for image + required: false + type: str + source_uri: + description: + - Source URL for image + - Mutually exclusive with C(source_path) + required: false + type: str + source_path: + description: + - local image path + - Mutually exclusive with C(source_uri) + required: false + type: str + categories: + description: + - Categories for the image. This allows setting up multiple values from a single key. + - this will override existing categories with mentioned during update + - mutually_exclusive with C(remove_categories) + required: false + type: dict + remove_categories: + description: + - set this flag to remove dettach all categories attached to image + - mutually_exclusive with C(categories) + type: bool + required: false + default: false + image_type: + description: The type of image. + required: false + type: str + choices: + - ISO_IMAGE + - DISK_IMAGE + version: + description: The image version, which is composed of a product name and product version. + required: false + type: dict + suboptions: + product_name: + description: Name of the producer/distribution of the image. For example windows or red hat. <= 64 characters. + type: str + required: true + product_version: + description: Version string for the disk image. <= 64 characters + type: str + required: true + clusters: + description: Name or UUID of the cluster on which the image will be placed + type: list + elements: dict + required: false + suboptions: + name: + description: + - Cluster Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Cluster UUID + - Mutually exclusive with C(name) + type: str + checksum: + description: Image checksum + type: dict + required: false + suboptions: + checksum_algorithm: + description: checksum algorithm + choices: + - SHA_1 + - SHA_256 + type: str + required: true + checksum_value: + description: checksum value + type: str + required: true +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" + +EXAMPLES = r""" + - name: create image from local workstation + ntnx_images: + state: "present" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + source_path: "/Users/ubuntu/Downloads/alpine-virt-3.8.1-x86_64.iso" + clusters: + - name: "temp_cluster" + categories: + AppFamily: + - Backup + checksum: + checksum_algorithm: SHA_1 + checksum_value: 44610efd741a3ab4a548a81ea94869bb8b692977 + name: "ansible-test-with-categories-mapping" + desc: "description" + image_type: "ISO_IMAGE" + version: + product_name: "test" + product_version: "1.2.0" + wait: true + + - name: create image from with source as remote server file location + ntnx_images: + state: "present" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + source_uri: "https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img" + clusters: + - name: "temp_cluster" + categories: + AppFamily: + - Backup + checksum: + checksum_algorithm: SHA_1 + checksum_value: 44610efd741a3ab4a548a81ea94869bb8b692977 + name: "ansible-test-with-categories-mapping" + desc: "description" + image_type: "DISK_IMAGE" + version: + product_name: "test" + product_version: "1.2.0" + wait: true + + - name: override categories of existing image + ntnx_images: + state: "present" + image_uuid: "" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + categories: + AppTier: + - Default + AppFamily: + - Backup + wait: true + + - name: dettach all categories from existing image + ntnx_images: + state: "present" + image_uuid: "00000000-0000-0000-0000-000000000000" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + remove_categories: true + wait: true + + - name: delete existing image + ntnx_images: + state: "absent" + image_uuid: "00000000-0000-0000-0000-000000000000" + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + wait: true +""" + +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: The image kind metadata + returned: always + type: dict + sample: { + "categories": { + "AppFamily": "Backup" + }, + "categories_mapping": { + "AppFamily": [ + "Backup" + ] + }, + "creation_time": "2022-06-09T10:13:38Z", + "kind": "image", + "last_update_time": "2022-06-09T10:37:14Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 14, + "uuid": "00000000-0000-0000-0000-000000000000" + } +spec: + description: An intentful representation of a image spec + returned: always + type: dict + sample: { + "description": "check123", + "name": "update_name", + "resources": { + "architecture": "X86_64", + "image_type": "ISO_IMAGE", + "source_uri": "http://dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-virt-3.8.1-x86_64.iso", + "version": { + "product_name": "test", + "product_version": "1.2.0" + } + } + } +status: + description: An intentful representation of a image status + returned: always + type: dict + sample: { + "description": "check123", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "update_name", + "resources": { + "architecture": "X86_64", + "current_cluster_reference_list": [ + { + "kind": "cluster", + "uuid": "00000000-0000-0000-0000-000000000000" + } + ], + "image_type": "ISO_IMAGE", + "retrieval_uri_list": [ + "" + ], + "size_bytes": 33554432, + "source_uri": "http://dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-virt-3.8.1-x86_64.iso", + "version": { + "product_name": "test", + "product_version": "1.2.0" + } + }, + "state": "COMPLETE" + } +image_uuid: + description: The created image uuid + returned: always + type: str + sample: "00000000-0000-0000-0000-000000000000" +""" + +from ..module_utils import utils # noqa: E402 +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.images import Image # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 + + +def get_module_spec(): + mutually_exclusive = [("name", "uuid")] + entity_by_spec = dict(name=dict(type="str"), uuid=dict(type="str")) + version = dict( + product_version=dict(type="str", required=True), + product_name=dict(type="str", required=True), + ) + checksum = dict( + checksum_algorithm=dict( + type="str", required=True, choices=["SHA_1", "SHA_256"] + ), + checksum_value=dict(type="str", required=True), + ) + module_args = dict( + name=dict(type="str", required=False), + desc=dict(type="str", required=False), + source_uri=dict(type="str", required=False), + source_path=dict(type="str", required=False), + remove_categories=dict(type="bool", required=False, default=False), + categories=dict(type="dict", required=False), + image_type=dict( + type="str", + required=False, + choices=["DISK_IMAGE", "ISO_IMAGE"], + ), + version=dict(type="dict", options=version, required=False), + clusters=dict( + type="list", + elements="dict", + mutually_exclusive=mutually_exclusive, + options=entity_by_spec, + required=False, + ), + checksum=dict(type="dict", options=checksum, required=False), + image_uuid=dict(type="str", required=False), + ) + return module_args + + +def create_image(module, result): + image = Image(module) + spec, error = image.get_spec() + if error: + result["error"] = error + module.fail_json(msg="Failed generating create Image Spec", **result) + if module.check_mode: + result["response"] = spec + return + + # create image + resp = image.create(spec) + image_uuid = resp["metadata"]["uuid"] + task_uuid = resp["status"]["execution_context"]["task_uuid"] + result["image_uuid"] = image_uuid + result["changed"] = True + + # upload image if source_path is given + source_path = module.params.get("source_path", "") + task = Task(module) + + if source_path: + # wait for image create to finish + task.wait_for_completion(task_uuid) + + # upload image contents + timeout = module.params.get("timeout", 600) + image_upload_obj = Image(module, upload_image=True) + resp = image_upload_obj.upload_image( + image_uuid, source_path, timeout, raise_error=False + ) + error = resp.get("error") + if error: + # delete the image metadata from PC + image_upload_obj.delete(image_uuid) + task.wait_for_completion(task_uuid) + result["error"] = error + result["changed"] = False + result["response"] = None + module.fail_json(msg="Failed uploading image contents", **result) + resp = image_upload_obj.read(image_uuid) + + elif module.params.get("wait"): + task.wait_for_completion(task_uuid) + # get the image + resp = image.read(image_uuid) + + result["response"] = resp + + +def update_image(module, result): + image = Image(module) + image_uuid = module.params.get("image_uuid") + if not image_uuid: + result["error"] = "Missing parameter image_uuid in playbook" + module.fail_json(msg="Failed updating image", **result) + result["image_uuid"] = image_uuid + + # read the current state of image + resp = image.read(image_uuid) + utils.strip_extra_attrs_from_status(resp["status"], resp["spec"]) + resp["spec"] = resp.pop("status") + + # new spec for updating image + update_spec, error = image.get_spec(resp) + if error: + result["error"] = error + module.fail_json(msg="Failed generating Image update spec", **result) + + # check for idempotency + if resp == update_spec: + result["skipped"] = True + module.exit_json( + msg="Nothing to change. Refer docs to check for fields which can be updated" + ) + + if module.check_mode: + result["response"] = update_spec + return + + # update image + resp = image.update(update_spec, uuid=image_uuid) + task_uuid = resp["status"]["execution_context"]["task_uuid"] + + # wait for image update to finish + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + # get the image + resp = image.read(image_uuid) + + result["changed"] = True + result["response"] = resp + + +def delete_image(module, result): + uuid = module.params["image_uuid"] + if not uuid: + result["error"] = "Missing parameter image_uuid" + module.fail_json(msg="Failed deleting Image", **result) + + image = Image(module) + resp = image.delete(uuid) + result["response"] = resp + result["changed"] = True + task_uuid = resp["status"]["execution_context"]["task_uuid"] + + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + + +def run_module(): + # mutually_exclusive_list have params which are not allowed together + # we cannot update source_uri, source_path, checksum and clusters. + mutually_exclusive_list = [ + ("image_uuid", "source_uri", "source_path"), + ("image_uuid", "checksum"), + ("image_uuid", "clusters"), + ("categories", "remove_categories"), + ] + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_if=[ + ("state", "present", ("source_uri", "source_path", "image_uuid"), True), + ("state", "present", ("name", "image_uuid"), True), + ("state", "absent", ("image_uuid",)), + ], + mutually_exclusive=mutually_exclusive_list, + ) + utils.remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "image_uuid": None, + } + state = module.params["state"] + if state == "present": + if module.params.get("image_uuid"): + update_image(module, result) + else: + create_image(module, result) + elif state == "absent": + delete_image(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_images_info.py b/plugins/modules/ntnx_images_info.py new file mode 100644 index 000000000..1f456eb0e --- /dev/null +++ b/plugins/modules/ntnx_images_info.py @@ -0,0 +1,231 @@ +#!/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: ntnx_images_info +short_description: images info module +version_added: 1.3.0 +description: 'Get images info' +options: + kind: + description: + - The kind name + type: str + default: image + image_uuid: + description: + - image UUID + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" +EXAMPLES = r""" + - name: List images using name filter criteria + ntnx_images_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + filter: + - name: Ubuntu + register: result +""" +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: Metadata for images list output + returned: always + type: dict + sample: { + "filter": "name==Ubuntu", + "kind": "image", + "length": 2, + "offset": 0, + "total_matches": 2 + } +entities: + description: images intent response + returned: always + type: list + sample: [ + { + "metadata": { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-06-09T12:44:17Z", + "kind": "image", + "last_update_time": "2022-06-09T12:44:21Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 1, + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec": { + "description": "adasdasdsad", + "name": "Ubuntu", + "resources": { + "architecture": "X86_64", + "image_type": "DISK_IMAGE", + "source_uri": "" + } + }, + "status": { + "description": "adasdasdsad", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "Ubuntu", + "resources": { + "architecture": "X86_64", + "current_cluster_reference_list": [ + { + "kind": "cluster", + "uuid": "00000000-0000-0000-0000-000000000000" + } + ], + "image_type": "DISK_IMAGE", + "retrieval_uri_list": [ + "" + ], + "size_bytes": 33554432, + "source_uri": "" + }, + "state": "COMPLETE" + } + }, + { + "metadata": { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-06-09T12:44:28Z", + "kind": "image", + "last_update_time": "2022-06-09T12:44:33Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 1, + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec": { + "description": "adasdasd", + "name": "Ubuntu", + "resources": { + "architecture": "X86_64", + "image_type": "DISK_IMAGE", + "source_uri": "" + } + }, + "status": { + "description": "adasdasd", + "execution_context": { + "task_uuid": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "name": "Ubuntu", + "resources": { + "architecture": "X86_64", + "current_cluster_reference_list": [ + { + "kind": "cluster", + "uuid": "00000000-0000-0000-0000-000000000000" + } + ], + "image_type": "DISK_IMAGE", + "retrieval_uri_list": [ + "" + ], + "size_bytes": 33554432, + "source_uri": "" + }, + "state": "COMPLETE" + } + } + ] + +""" + + +from ..module_utils.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.prism.images import Image # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + + module_args = dict( + image_uuid=dict(type="str"), + kind=dict(type="str", default="image"), + sort_order=dict(type="str"), + sort_attribute=dict(type="str"), + ) + + return module_args + + +def get_image(module, result): + image = Image(module) + uuid = module.params.get("image_uuid") + resp = image.read(uuid) + result["response"] = resp + + +def get_images(module, result): + image = Image(module) + spec, err = image.get_info_spec() + if err: + result["error"] = err + module.fail_json(msg="Failed generating Image info Spec", **result) + resp = image.list(spec) + result["response"] = resp + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + required_together=[("sort_order", "sort_attribute")], + mutually_exclusive=[ + ("image_uuid", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + if module.params.get("image_uuid"): + get_image(module, result) + else: + get_images(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pbrs_info.py b/plugins/modules/ntnx_pbrs_info.py index cebe16203..de5110380 100644 --- a/plugins/modules/ntnx_pbrs_info.py +++ b/plugins/modules/ntnx_pbrs_info.py @@ -173,7 +173,7 @@ def get_pbr(module, result): result["response"] = resp -def list_pbr(module, result): +def get_pbrs(module, result): pbr = Pbr(module) spec, error = pbr.get_info_spec() @@ -193,7 +193,7 @@ def run_module(): if module.params.get("pbr_uuid"): get_pbr(module, result) else: - list_pbr(module, result) + get_pbrs(module, result) module.exit_json(**result) diff --git a/plugins/modules/ntnx_security_rules.py b/plugins/modules/ntnx_security_rules.py new file mode 100644 index 000000000..c8b024f65 --- /dev/null +++ b/plugins/modules/ntnx_security_rules.py @@ -0,0 +1,1327 @@ +#!/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: ntnx_security_rules +short_description: security_rule module which suports security_rule CRUD operations +version_added: 1.3.0 +description: 'Create, Update, Delete security_rule' +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 + state: + description: + - Specify state of security_rule + - If C(state) is set to C(present) then security_rule is created. + - >- + If C(state) is set to C(absent) and if the security_rule exists, then + security_rule is removed. + choices: + - present + - absent + type: str + default: present + wait: + description: Wait for security_rule CRUD operation to complete. + type: bool + required: false + default: true + name: + description: security_rule Name + required: false + type: str + security_rule_uuid: + description: security_rule UUID + type: str + allow_ipv6_traffic: + description: Allow traffic from ipv6 + type: bool + policy_hitlog: + description: Allow policy hitlog + type: bool + vdi_rule: + description: >- + These rules are used for quarantining suspected VMs. Target group is a + required attribute. Empty inbounds will not allow anything into + target group. Empty outbounds will allow everything from target + group. + type: dict + suboptions: + policy_mode: + description: Type of deployment of the rule. + type: str + choices: + - MONITOR + - APPLY + target_group: + description: Target Group + type: dict + suboptions: + default_internal_policy: + description: Default policy for communication within target group. + type: str + choices: ["ALLOW_ALL", "DENY_ALL"] + categories: + type: dict + description: A category's key and values + suboptions: + apptype: + description: A category value. + type: str + apptype_filter_by_category: + description: A category key and value. + type: dict + apptier: + description: A category value. + type: str + adgroup: + description: + - A category value. + - Mutually exclusive with c(apptype). + type: str + allow_all_inbounds: + description: Allow all inbounds + type: bool + allow_all_outbounds: + description: Allow all outbounds + type: bool + inbounds: + description: Array of inbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + + outbounds: + description: Array of Outbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + app_rule: + description: >- + These rules are used for quarantining suspected VMs. Target group is a + required attribute. Empty inbounds will not allow anything into + target group. Empty outbounds will allow everything from target + group. + type: dict + suboptions: + policy_mode: + description: Type of deployment of the rule. + type: str + choices: + - MONITOR + - APPLY + target_group: + description: Target Group + type: dict + suboptions: + default_internal_policy: + description: Default policy for communication within target group. + type: str + choices: ["ALLOW_ALL", "DENY_ALL"] + categories: + type: dict + description: A category's key and values + suboptions: + apptype: + description: A category value. + type: str + apptype_filter_by_category: + description: A category key and value. + type: dict + apptier: + description: A category value. + type: str + adgroup: + description: + - A category value. + - Mutually exclusive with c(apptype). + type: str + allow_all_inbounds: + description: Allow all inbounds + type: bool + allow_all_outbounds: + description: Allow all outbounds + type: bool + inbounds: + description: Array of inbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + + outbounds: + description: Array of Outbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + quarantine_rule: + description: >- + These rules are used for quarantining suspected VMs. Target group is a + required attribute. Empty inbounds will not allow anything into + target group. Empty outbounds will allow everything from target + group. + type: dict + suboptions: + policy_mode: + description: Type of deployment of the rule. + type: str + choices: + - MONITOR + - APPLY + target_group: + description: Target Group + type: dict + suboptions: + default_internal_policy: + description: Default policy for communication within target group. + type: str + choices: ["ALLOW_ALL", "DENY_ALL"] + categories: + type: dict + description: A category's key and values + suboptions: + apptype: + description: A category value. + type: str + apptype_filter_by_category: + description: A category key and value. + type: dict + apptier: + description: A category value. + type: str + adgroup: + description: + - A category value. + - Mutually exclusive with c(apptype). + type: str + allow_all_inbounds: + description: Allow all inbounds + type: bool + allow_all_outbounds: + description: Allow all outbounds + type: bool + inbounds: + description: Array of inbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + outbounds: + description: Array of Outbound Network rule + type: list + elements: dict + suboptions: + state: + description: + - Item's state to delete it + type: str + choices: + - absent + categories: + type: dict + description: A category's key and values + description: + type: str + description: >- + Description for network security rule that is for inbound or + outbound + rule_id: + type: int + description: >- + Unique identifier for inbound or outbound rule. This is system + generated and used internally. User should not set this field + while creating a new rule or should not modify it while updating + the existing rule. + protocol: + type: dict + description: >- + Select a protocol to allow. Multiple protocols can be allowed by + repeating network_rule object. If a protocol is not configured in + the network_rule object then it is allowed. + suboptions: + tcp: + description: List of TCP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + udp: + description: List of UDP ports that are allowed by this rule. + type: list + elements: dict + suboptions: + start_port: + description: start_port + type: int + end_port: + description: end_port + type: int + icmp: + description: List of ICMP types and codes allowed by this rule. + elements: dict + type: list + suboptions: + code: + description: ICMP code + type: int + type: + description: ICMP type + type: int + service: + type: dict + description: A service groups associated with this rule. + suboptions: + uuid: + type: str + description: + - Service group uuid. + - Mutually exclusive with c(name). + name: + description: + - Service group name. + - Mutually exclusive with c(uuid). + type: str + address: + type: dict + description: Address groups that is allowed access by this rule + suboptions: + uuid: + type: str + description: + - Address group uuid. + - Mutually exclusive with c(name). + name: + description: + - Address group name. + - Mutually exclusive with c(uuid). + type: str + ip_subnet: + description: IP subnet provided as an address and prefix_length length. + type: dict + suboptions: + ip: + type: str + description: IPV4 address. + prefix_length: + description: prefix length + type: int + + isolation_rule: + description: These rules are used for environmental isolation. + type: dict + suboptions: + policy_mode: + description: Type of deployment of the rule. + type: str + choices: + - MONITOR + - APPLY + isolate_category: + description: + - A category key and value. + - Need to provide only one. + type: dict + from_category: + description: + - A category key and value. + - Need to provide only one. + type: dict + subset_category: + type: dict + description: + - A category key and value. + - Need to provide only one. +author: + - Prem Karat (@premkarat) + - Gevorg Khachatryan (@Gevorg-Khachatryan-97) + - Alaa Bishtawi (@alaa-bish) +""" + +EXAMPLES = r""" +- name: create app security rule + ntnx_security_rules: + name: test_app_rule + app_rule: + target_group: + categories: + apptype: Apache_Spark + default_internal_policy: DENY_ALL + inbound: + - categories: + AppFamily: + - Databases + - DevOps + icmp: + - code: 1 + type: 1 + - categories: + AppFamily: + - Databases + - DevOps + tcp: + - start_port: 22 + end_port: 80 + - categories: + AppFamily: + - Databases + - DevOps + udp: + - start_port: 82 + end_port: 8080 + - ip_subnet: + prefix_length: 24 + ip: 192.168.1.1 + description: test description + protocol: ALL + outbound: + - categories: + AppFamily: + - Databases + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog:: true + register: result +- name: update app security rule with outbound list + ntnx_security_rules: + security_rule_uuid: '{{ result.response.metadata.uuid }}' + app_rule: + policy_mode: APPLY + outbound: + - icmp: + - code: 1 + type: 1 + categories: + AppFamily: + - Databases + - DevOps + register: result +- name: update quarantine_rule by adding inbound and outbound list + ntnx_security_rules: + security_rule_uuid: '{{quarantine_rule_uuid}}' + quarantine_rule: + inbound: + - categories: + AppFamily: + - Databases + - DevOps + outbound: + - categories: + AppFamily: + - Databases + - DevOps + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog:: true + register: result +""" + +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: '3.1' +metadata: + description: Metadata for security_rule output + returned: always + type: dict + sample: + categories: {} + categories_mapping: {} + creation_time: '2022-06-15T11:59:38Z' + kind: network_security_rule + last_update_time: '2022-06-15T11:59:41Z' + owner_reference: + kind: user + name: admin + uuid: 00000000-0000-0000-0000-000000000000 + spec_hash: '00000000000000000000000000000000000000000000000000' + spec_version: 0 + uuid: c340bc98-170b-4ead-a86c-861b023cc8ff +spec: + description: An intentful representation of a subnet spec + returned: always + type: dict + sample: + name: test_app_rule + resources: + allow_ipv6_traffic: true + app_rule: + action: MONITOR + inbound_allow_list: + - description: test description + filter: + kind_list: + - vm + params: + AppFamily: + - Databases + - DevOps + type: CATEGORIES_MATCH_ALL + icmp_type_code_list: + - code: 1 + type: 1 + ip_subnet: + ip: 192.168.1.1 + prefix_length: 24 + peer_specification_type: FILTER + protocol: ALL + rule_id: 1 + tcp_port_range_list: + - end_port: 80 + start_port: 22 + udp_port_range_list: + - end_port: 8080 + start_port: 82 + outbound_allow_list: + - filter: + kind_list: + - vm + params: + AppFamily: + - Databases + - DevOps + type: CATEGORIES_MATCH_ALL + peer_specification_type: FILTER + rule_id: 1 + target_group: + default_internal_policy: DENY_ALL + filter: + kind_list: + - vm + params: + AppType: + - Apache_Spark + type: CATEGORIES_MATCH_ALL + peer_specification_type: FILTER + policy_hitlog:: true +status: + description: An intentful representation of a subnet status + returned: always + type: dict + sample: + execution_context: + task_uuid: + - ac7ae2c4-acbe-4ab7-b0b3-faca16395429 + name: test_app_rule + resources: + allow_ipv6_traffic: true + app_rule: + action: MONITOR + inbound_allow_list: + - description: test description + filter: + kind_list: + - vm + params: + AppFamily: + - Databases + - DevOps + type: CATEGORIES_MATCH_ALL + peer_specification_type: FILTER + protocol: ALL + rule_id: 1 + outbound_allow_list: + - filter: + kind_list: + - vm + params: + AppFamily: + - Databases + - DevOps + type: CATEGORIES_MATCH_ALL + peer_specification_type: FILTER + rule_id: 1 + target_group: + default_internal_policy: DENY_ALL + filter: + kind_list: + - vm + params: + AppType: + - Apache_Spark + type: CATEGORIES_MATCH_ALL + peer_specification_type: FILTER + policy_hitlog:: true + state: COMPLETE +security_rule_uuid: + description: The created security rule uuid + returned: always + type: str + sample: 00000000000-0000-0000-0000-00000000000 +task_uuid: + description: The task uuid for the creation + returned: always + type: str + sample: 00000000000-0000-0000-0000-00000000000 +""" + + +from ..module_utils import utils # noqa: E402 +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.security_rules import SecurityRule # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + group_spec = dict(uuid=dict(type="str"), name=dict(type="str")) + + tcp_and_udp_spec = dict(start_port=dict(type="int"), end_port=dict(type="int")) + + network_spec = dict(ip=dict(type="str"), prefix_length=dict(type="int")) + + icmp_spec = dict(code=dict(type="int"), type=dict(type="int")) + + categories_spec = dict( + apptype=dict(type="str"), + apptype_filter_by_category=dict(type="dict"), + apptier=dict(type="str"), + adgroup=dict(type="str"), + ) + + protocol_spec = dict( + tcp=dict(type="list", elements="dict", options=tcp_and_udp_spec), + udp=dict(type="list", elements="dict", options=tcp_and_udp_spec), + icmp=dict( + type="list", + elements="dict", + options=icmp_spec, + required_by={"code": "type"}, + ), + service=dict(type="dict", options=group_spec), + ) + + target_spec = dict( + categories=dict( + type="dict", + options=categories_spec, + mutually_exclusive=[("apptype", "adgroup")], + ), + default_internal_policy=dict(type="str", choices=["ALLOW_ALL", "DENY_ALL"]), + ) + + whitelisted_traffic = dict( + categories=dict(type="dict"), + address=dict(type="dict", options=group_spec), + ip_subnet=dict(type="dict", options=network_spec), + description=dict(type="str"), + rule_id=dict(type="int"), + state=dict(type="str", choices=["absent"]), + protocol=dict( + type="dict", + options=protocol_spec, + apply_defaults=True, + mutually_exclusive=[("tcp", "udp", "icmp", "service")], + ), + ) + rule_spec = dict( + target_group=dict(type="dict", options=target_spec), + inbounds=dict( + type="list", + elements="dict", + options=whitelisted_traffic, + mutually_exclusive=[("address", "categories", "ip_subnet")], + ), + allow_all_inbounds=dict(type="bool"), + outbounds=dict( + type="list", + elements="dict", + options=whitelisted_traffic, + mutually_exclusive=[("address", "categories", "ip_subnet")], + ), + allow_all_outbounds=dict(type="bool"), + policy_mode=dict(type="str", choices=["MONITOR", "APPLY"]), + ) + + isolation_rule_spec = dict( + isolate_category=dict(type="dict"), + from_category=dict(type="dict"), + subset_category=dict(type="dict"), + policy_mode=dict(type="str", choices=["MONITOR", "APPLY"]), + ) + module_args = dict( + name=dict(type="str"), + security_rule_uuid=dict(type="str"), + allow_ipv6_traffic=dict(type="bool"), + policy_hitlog=dict(type="bool"), + vdi_rule=dict( + type="dict", + options=rule_spec, + mutually_exclusive=[ + ("inbounds", "allow_all_inbounds"), + ("outbounds", "allow_all_outbounds"), + ], + ), + app_rule=dict( + type="dict", + options=rule_spec, + mutually_exclusive=[ + ("inbounds", "allow_all_inbounds"), + ("outbounds", "allow_all_outbounds"), + ], + ), + isolation_rule=dict(type="dict", options=isolation_rule_spec), + quarantine_rule=dict( + type="dict", + options=rule_spec, + mutually_exclusive=[ + ("inbounds", "allow_all_inbounds"), + ("outbounds", "allow_all_outbounds"), + ], + ), + ) + + return module_args + + +def create_security_rule(module, result): + security_rule = SecurityRule(module) + spec, error = security_rule.get_spec() + if error: + result["error"] = error + module.fail_json(msg="Failed generating security_rule spec", **result) + + if module.check_mode: + result["response"] = spec + return + + resp = security_rule.create(spec) + security_rule_uuid = resp["metadata"]["uuid"] + result["changed"] = True + result["response"] = resp + result["security_rule_uuid"] = security_rule_uuid + result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"] + + if module.params.get("wait"): + wait_for_task_completion(module, result) + resp = security_rule.read(security_rule_uuid) + result["response"] = resp + + +def update_security_rule(module, result): + security_rule_uuid = module.params["security_rule_uuid"] + state = module.params.get("state") + + security_rule = SecurityRule(module) + resp = security_rule.read(security_rule_uuid) + result["response"] = resp + utils.strip_extra_attrs_from_status(resp["status"], resp["spec"]) + resp.pop("status") + + spec, error = security_rule.get_spec(resp) + + if error: + result["error"] = error + module.fail_json(msg="Failed generating security_rule spec", **result) + + if module.check_mode: + result["response"] = spec + return + + if utils.check_for_idempotency(spec, resp, state=state): + result["skipped"] = True + module.exit_json(msg="Nothing to change") + + resp = security_rule.update(spec, security_rule_uuid) + security_rule_uuid = resp["metadata"]["uuid"] + result["changed"] = True + result["response"] = resp + result["security_rule_uuid"] = security_rule_uuid + result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"] + + if module.params.get("wait"): + wait_for_task_completion(module, result) + resp = security_rule.read(security_rule_uuid) + result["response"] = resp + + +def delete_security_rule(module, result): + security_rule_uuid = module.params["security_rule_uuid"] + if not security_rule_uuid: + result["error"] = "Missing parameter security_rule_uuid in playbook" + module.fail_json(msg="Failed deleting security_rule", **result) + + security_rule = SecurityRule(module) + resp = security_rule.delete(security_rule_uuid) + result["changed"] = True + result["response"] = resp + result["security_rule_uuid"] = security_rule_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 = task.wait_for_completion(task_uuid) + result["response"] = resp + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + mutually_exclusive=[ + ("vdi_rule", "app_rule", "isolation_rule", "quarantine_rule"), + ("allow_ipv6_traffic", "isolation_rule"), + ], + required_by={"quarantine_rule": "security_rule_uuid"}, + required_one_of=[("security_rule_uuid", "name")], + ) + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "security_rule_uuid": None, + "task_uuid": None, + } + state = module.params["state"] + if state == "absent": + delete_security_rule(module, result) + elif module.params.get("security_rule_uuid"): + update_security_rule(module, result) + else: + create_security_rule(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_security_rules_info.py b/plugins/modules/ntnx_security_rules_info.py new file mode 100644 index 000000000..96b6345cc --- /dev/null +++ b/plugins/modules/ntnx_security_rules_info.py @@ -0,0 +1,232 @@ +#!/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: ntnx_security_rules_info +short_description: security_rule info module +version_added: 1.3.0 +description: 'Get security_rule info' +options: + kind: + description: + - The kind name + type: str + default: network_security_rule + security_rule_uuid: + description: + - security_rule UUID + type: str + sort_order: + description: + - The sort order in which results are returned + type: str + choices: + - ASCENDING + - DESCENDING +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info +author: + - Prem Karat (@premkarat) + - Gevorg Khachatryan (@Gevorg-Khachatryan-97) + - Alaa Bishtawi (@alaa-bish) +""" +EXAMPLES = r""" + - name: List security_rule using name filter criteria + ntnx_security_rules_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + filter: "name=={{ security_rule.name }}" + kind: security_rule + register: result + + - name: List security_rule using length, offset, sort order and name sort attribute + ntnx_security_rules_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + length: 1 + offset: 1 + sort_order: "ASCENDING" + sort_attribute: "name" + register: result +""" +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: Metadata for security_rule_info list output + returned: always + type: dict + sample: { + "metadata": { + "kind": "security_rule", + "length": 1, + "offset": 2, + "sort_attribute": "name", + "sort_order": "DESCENDING", + "total_matches": 3 + } } +entities: + description: security_rule intent response + returned: always + type: list + sample: { + "entities": [ + { + "metadata": { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-07-02T07:31:20Z", + "kind": "network_security_rule", + "last_update_time": "2022-07-02T07:31:22Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 0, + "uuid": "734a569e-f43b-4b33-b71b-e3b5f1970a37" + }, + "spec": { + "name": "isolation_test_rule", + "resources": { + "is_policy_hitlog_enabled": false, + "isolation_rule": { + "action": "MONITOR", + "first_entity_filter": { + "kind_list": [ + "vm" + ], + "params": { + "Environment": [ + "Dev" + ] + }, + "type": "CATEGORIES_MATCH_ALL" + }, + "second_entity_filter": { + "kind_list": [ + "vm" + ], + "params": { + "Environment": [ + "Production" + ] + }, + "type": "CATEGORIES_MATCH_ALL" + } + } + } + }, + "status": { + "description": null, + "execution_context": { + "task_uuid": [ + "2b88bf1d-ed24-4bc0-a0eb-baecc4cae71f" + ] + }, + "name": "isolation_test_rule", + "resources": { + "isolation_rule": { + "action": "MONITOR", + "first_entity_filter": { + "kind_list": [ + "vm" + ], + "params": { + "Environment": [ + "Dev" + ] + }, + "type": "CATEGORIES_MATCH_ALL" + }, + "second_entity_filter": { + "kind_list": [ + "vm" + ], + "params": { + "Environment": [ + "Production" + ] + }, + "type": "CATEGORIES_MATCH_ALL" + } + } + }, + "state": "COMPLETE" + } + } + ] + } +""" + +from ..module_utils.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.prism.security_rules import SecurityRule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + + module_args = dict( + security_rule_uuid=dict(type="str"), + kind=dict(type="str", default="network_security_rule"), + sort_order=dict(type="str", choices=["ASCENDING", "DESCENDING"]), + sort_attribute=dict(type="str"), + ) + + return module_args + + +def get_security_rule(module, result): + security_rule = SecurityRule(module) + security_rule_uuid = module.params.get("security_rule_uuid") + resp = security_rule.read(security_rule_uuid) + + result["response"] = resp + + +def list_security_rule(module, result): + security_rule = SecurityRule(module) + spec, error = security_rule.get_info_spec() + + resp = security_rule.list(spec) + + result["response"] = resp + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + required_together=[("sort_order", "sort_attribute")], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + if module.params.get("security_rule_uuid"): + get_security_rule(module, result) + else: + list_security_rule(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_static_routes.py b/plugins/modules/ntnx_static_routes.py new file mode 100644 index 000000000..ba29c3fc7 --- /dev/null +++ b/plugins/modules/ntnx_static_routes.py @@ -0,0 +1,390 @@ +#!/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: ntnx_static_routes +short_description: module for create/update static routes of vpc +version_added: 1.0.0 +description: "Create/Update static routes of vpc" +options: + wait: + description: Wait for the create/update operations to complete. + type: bool + required: false + default: True + vpc_uuid: + description: vpc uuid whose static routes has to be created/updated + required: true + type: str + remove_all_routes: + description: + - set this flag to remove all static routes + - this will only remove all static routes except local and dynamic routes + - mutually_exclusive with C(static_routes) + type: bool + required: false + default: false + static_routes: + description: + - list of static routes to be overriden in vpc. + - mutually exclusive with C(remove_all_routes) + - required incase remove_all_categories is not given + - default static route can be mentioned in this with destination - 0.0.0.0/0 + - Only one default static route is allowed + required: false + type: list + elements: dict + suboptions: + destination: + description: + - destination prefix eg. 10.2.3.0/24 + - for defaut static route give 0.0.0.0/0 + required: true + type: str + next_hop: + description: + - info about next hop in static route + type: dict + required: true + suboptions: + vpn_connection_ref: + description: + - vpn connection reference + - mutually exclusive with C(external_subnet_ref) + type: dict + required: false + suboptions: + name: + description: + - vpn connection Name + - Mutually exclusive with C(uuid) + type: str + required: false + uuid: + description: + - vpn connection UUID + - Mutually exclusive with C(name) + type: str + required: false + external_subnet_ref: + description: + - external subnet reference + - mutually exclusive with C(vpn_connection_ref) + type: dict + required: false + suboptions: + name: + description: + - subnet connection Name + - Mutually exclusive with C(uuid) + type: str + required: false + uuid: + description: + - subnet connection UUID + - Mutually exclusive with C(name) + type: str + required: false +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" +EXAMPLES = r""" +- name: create static routes and default static routes with external nat subnet + ntnx_static_routes: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + - destination: "10.2.2.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + - destination: "10.2.3.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + - destination: "10.2.4.0/24" + next_hop: + vpn_connection_ref: + uuid: "{{ vpn.uuid }}" + register: result + +- name: remove all routes excluding dynamic and local routes + ntnx_static_routes: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + vpc_uuid: "{{ vpc.uuid }}" + remove_all_routes: true + register: result +""" + +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: The vpc_route_table kind metadata + returned: always + type: dict + sample: { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-07-01T08:20:39Z", + "kind": "vpc_route_table", + "last_update_time": "2022-07-01T10:30:41Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 8, + "uuid": "528323ee-7c89-sb65-68a7-a66c0c4fc9d5" + } +spec: + description: An intentful representation of a vpc static routes spec + returned: always + type: dict + sample: { + "name": "Route Table for vpc", + "resources": { + "default_route_nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-a1a9-43ca-a11a-cbac200044b7" + } + }, + "static_routes_list": [ + { + "destination": "10.2.2.0/24", + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-a1a9-43ca-c11a-9bac200044b7" + } + } + }, + { + "destination": "10.2.3.0/24", + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-d1a9-43ca-a11a-9bac200044b7" + } + } + } + ] + } + } +status: + description: An intentful representation of a vpc static routes status + returned: always + type: dict + sample: { + "execution_context": { + "task_uuid": [ + "bc6bdb00-18b3-sdab-8b03-701db6856e7f" + ] + }, + "resources": { + "default_route": { + "destination": "0.0.0.0/0", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "ace7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + }, + "dynamic_routes_list": [], + "local_routes_list": [ + { + "destination": "xx.xx.xx.xx/24", + "is_active": true, + "nexthop": { + "local_subnet_reference": { + "kind": "subnet", + "name": "integration_test_overlay_subnet", + "uuid": "974234b1-3fd1-4525-adeb-bce069696d2e" + } + }, + "priority": 65534 + } + ], + "static_routes_list": [ + { + "destination": "10.2.2.0/24", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "ave7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + }, + { + "destination": "10.2.3.0/24", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "cce7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + } + ] + }, + "state": "COMPLETE" + } +vpc_uuid: + description: vpc uuid + returned: always + type: str + sample: "00000000-0000-0000-0000-000000000000" +""" + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.static_routes import StaticRoutes # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 +from ..module_utils.utils import ( # noqa: E402 + remove_param_with_none_value, + strip_extra_attrs_from_status, +) + + +def get_module_spec(): + mutually_exclusive = [("name", "uuid")] + + entity_by_spec = dict(name=dict(type="str"), uuid=dict(type="str")) + + nexthop_spec = dict( + external_subnet_ref=dict( + type="dict", + options=entity_by_spec, + mutually_exclusive=mutually_exclusive, + required=False, + ), + vpn_connection_ref=dict( + type="dict", + options=entity_by_spec, + mutually_exclusive=mutually_exclusive, + required=False, + ), + ) + static_route_spec = dict( + destination=dict(type="str", required=True), + next_hop=dict( + type="dict", + options=nexthop_spec, + required=True, + mutually_exclusive=[("external_subnet_ref", "vpn_connection_ref")], + ), + ) + module_args = dict( + vpc_uuid=dict(type="str", required=True), + static_routes=dict( + type="list", elements="dict", options=static_route_spec, required=False + ), + remove_all_routes=dict(type="bool", required=False, default=False), + ) + return module_args + + +def update_static_routes(module, result): + static_routes = StaticRoutes(module) + vpc_uuid = module.params["vpc_uuid"] + curr_routes = static_routes.get_static_routes(vpc_uuid) + result["response"] = curr_routes + result["vpc_uuid"] = vpc_uuid + + # status and spec have field name different schema for default static routes + if curr_routes["status"]["resources"].get("default_route"): + curr_routes["status"]["resources"]["default_route_nexthop"] = curr_routes[ + "status" + ]["resources"]["default_route"]["nexthop"] + + strip_extra_attrs_from_status(curr_routes["status"], curr_routes["spec"]) + curr_routes["spec"] = curr_routes.pop("status") + + # new spec for updating static routes + update_spec, err = static_routes.get_spec(curr_routes) + if err: + result["error"] = err + module.fail_json(msg="Failed generating static routes update spec", **result) + + if update_spec == curr_routes: + result["skipped"] = True + module.exit_json(msg="Nothing to update") + + if module.check_mode: + result["response"] = update_spec + result["params"] = module.params + result["current_spec"] = curr_routes + return + + # update static routes + resp = static_routes.update_static_routes(update_spec, vpc_uuid) + task_uuid = resp["status"]["execution_context"]["task_uuid"] + + # wait for static routes update to finish + if module.params.get("wait"): + task = Task(module) + task.wait_for_completion(task_uuid) + # get the current static routes + resp = static_routes.get_static_routes(vpc_uuid) + + result["changed"] = True + result["response"] = resp + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_one_of=[("static_routes", "remove_all_routes")], + mutually_exclusive=[("static_routes", "remove_all_routes")], + ) + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "vpc_uuid": None, + } + update_static_routes(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_static_routes_info.py b/plugins/modules/ntnx_static_routes_info.py new file mode 100644 index 000000000..db935ad73 --- /dev/null +++ b/plugins/modules/ntnx_static_routes_info.py @@ -0,0 +1,205 @@ +#!/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: ntnx_static_routes_info +short_description: static routes info module +version_added: 1.0.0 +description: 'Get static routes info for a vpc' +options: + vpc_uuid: + description: + - vpc UUID whose static routes needs to be fetched + type: str + required: true +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations +author: + - Prem Karat (@premkarat) + - Pradeepsingh Bhati (@bhati-pradeep) +""" +EXAMPLES = r""" +- name: get all static routes for a vpc + ntnx_static_routes_info: + vpc_uuid: "{{ vpc.uuid }}" + register: result +""" +RETURN = r""" +api_version: + description: API Version of the Nutanix v3 API framework. + returned: always + type: str + sample: "3.1" +metadata: + description: The vpc_route_table kind metadata + returned: always + type: dict + sample: { + "categories": {}, + "categories_mapping": {}, + "creation_time": "2022-07-01T08:20:39Z", + "kind": "vpc_route_table", + "last_update_time": "2022-07-01T10:30:41Z", + "owner_reference": { + "kind": "user", + "name": "admin", + "uuid": "00000000-0000-0000-0000-000000000000" + }, + "spec_hash": "00000000000000000000000000000000000000000000000000", + "spec_version": 8, + "uuid": "528323ee-7c89-sb65-68a7-a66c0c4fc9d5" + } +spec: + description: An intentful representation of a vpc static routes spec + returned: always + type: dict + sample: { + "name": "Route Table for vpc", + "resources": { + "default_route_nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-a1a9-43ca-a11a-cbac200044b7" + } + }, + "static_routes_list": [ + { + "destination": "10.2.2.0/24", + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-a1a9-43ca-c11a-9bac200044b7" + } + } + }, + { + "destination": "10.2.3.0/24", + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "uuid": "ace7f19a-d1a9-43ca-a11a-9bac200044b7" + } + } + } + ] + } + } +status: + description: An intentful representation of a vpc static routes status + returned: always + type: dict + sample: { + "execution_context": { + "task_uuid": [ + "bc6bdb00-18b3-sdab-8b03-701db6856e7f" + ] + }, + "resources": { + "default_route": { + "destination": "0.0.0.0/0", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "ace7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + }, + "dynamic_routes_list": [], + "local_routes_list": [ + { + "destination": "xx.xx.xx.xx/24", + "is_active": true, + "nexthop": { + "local_subnet_reference": { + "kind": "subnet", + "name": "integration_test_overlay_subnet", + "uuid": "974234b1-3fd1-4525-adeb-bce069696d2e" + } + }, + "priority": 65534 + } + ], + "static_routes_list": [ + { + "destination": "10.2.2.0/24", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "ave7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + }, + { + "destination": "10.2.3.0/24", + "is_active": true, + "nexthop": { + "external_subnet_reference": { + "kind": "subnet", + "name": "no-nat", + "uuid": "cce7f19a-a1a9-43ca-a11a-9bac200044b7" + } + }, + "priority": 23455 + } + ] + }, + "state": "COMPLETE" + } +vpc_uuid: + description: vpc uuid + returned: always + type: str + sample: "00000000-0000-0000-0000-000000000000" +""" + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.static_routes import StaticRoutes # noqa: E402 + + +def get_module_spec(): + + module_args = dict( + vpc_uuid=dict(type="str", required=True), + ) + + return module_args + + +def get_static_routes(module, result): + vpc_uuid = module.params["vpc_uuid"] + static_routes = StaticRoutes(module) + result["response"] = static_routes.get_static_routes(vpc_uuid) + result["vpc_uuid"] = vpc_uuid + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + ) + result = {"changed": False, "error": None, "response": None, "vpc_uuid": None} + + get_static_routes(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_subnets_info.py b/plugins/modules/ntnx_subnets_info.py index a14d1854b..f0662d451 100644 --- a/plugins/modules/ntnx_subnets_info.py +++ b/plugins/modules/ntnx_subnets_info.py @@ -208,7 +208,7 @@ def get_subnet(module, result): result["response"] = resp -def list_subnet(module, result): +def get_subnets(module, result): subnet = Subnet(module) spec, error = subnet.get_info_spec() @@ -228,7 +228,7 @@ def run_module(): if module.params.get("subnet_uuid"): get_subnet(module, result) else: - list_subnet(module, result) + get_subnets(module, result) module.exit_json(**result) diff --git a/plugins/modules/ntnx_vms.py b/plugins/modules/ntnx_vms.py index 7664f398c..49d077c1e 100644 --- a/plugins/modules/ntnx_vms.py +++ b/plugins/modules/ntnx_vms.py @@ -46,6 +46,13 @@ vm_uuid: description: VM UUID type: str + remove_categories: + description: + - When set will remove all categories attached to the vm. + - Mutually exclusive ith C(categories) + required: false + type: bool + default: false disks: description: - List of disks attached to the VM @@ -778,6 +785,7 @@ def get_module_spec(): default="present", ), desc=dict(type="str"), + remove_categories=dict(type="bool", required=False, default=False), disks=dict( type="list", elements="dict", @@ -962,6 +970,9 @@ def run_module(): module = BaseModule( argument_spec=get_module_spec(), supports_check_mode=True, + mutually_exclusive=[ + ("categories", "remove_categories"), + ], required_if=[("vm_uuid", None, ("name",)), ("state", "absent", ("vm_uuid",))], ) utils.remove_param_with_none_value(module.params) diff --git a/plugins/modules/ntnx_vms_info.py b/plugins/modules/ntnx_vms_info.py index 8ea794441..07f2f9f12 100644 --- a/plugins/modules/ntnx_vms_info.py +++ b/plugins/modules/ntnx_vms_info.py @@ -273,7 +273,7 @@ def get_vm(module, result): result["response"] = resp -def list_vm(module, result): +def get_vms(module, result): vm = VM(module) spec, error = vm.get_info_spec() @@ -293,7 +293,7 @@ def run_module(): if module.params.get("vm_uuid"): get_vm(module, result) else: - list_vm(module, result) + get_vms(module, result) module.exit_json(**result) diff --git a/plugins/modules/ntnx_vpcs_info.py b/plugins/modules/ntnx_vpcs_info.py index 214e0ab47..5a9c75e6c 100644 --- a/plugins/modules/ntnx_vpcs_info.py +++ b/plugins/modules/ntnx_vpcs_info.py @@ -172,7 +172,7 @@ def get_vpc(module, result): result["response"] = resp -def list_vpc(module, result): +def get_vpcs(module, result): vpc = Vpc(module) spec, error = vpc.get_info_spec() @@ -192,7 +192,7 @@ def run_module(): if module.params.get("vpc_uuid"): get_vpc(module, result) else: - list_vpc(module, result) + get_vpcs(module, result) module.exit_json(**result) diff --git a/tests/integration/targets/ntnx_image_placement_policies_info/meta/main.yml b/tests/integration/targets/ntnx_image_placement_policies_info/meta/main.yml new file mode 100644 index 000000000..23b0fb268 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policies_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policies_info/tasks/info.yml b/tests/integration/targets/ntnx_image_placement_policies_info/tasks/info.yml new file mode 100644 index 000000000..e7f18c567 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policies_info/tasks/info.yml @@ -0,0 +1,107 @@ +--- +- debug: + msg: "start ntnx_image_placement_policies_info tests" + +- name: create image placement policy + ntnx_image_placement_policy: + placement_type: soft + name: "test_policy_1" + image_categories: + AnalyticsExclusions: + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: policy_1 + +- name: create image placement policy + ntnx_image_placement_policy: + placement_type: soft + name: "test_policy_2" + image_categories: + AnalyticsExclusions: + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: policy_2 + +- set_fact: + todelete: '{{ todelete + [ policy_1["response"]["metadata"]["uuid"] ] }}' + +- set_fact: + todelete: '{{ todelete + [ policy_2["response"]["metadata"]["uuid"] ] }}' + +################################################################### +- name: test getting all image placement policies + ntnx_image_placement_policies_info: + register: result + +- name: check listing status + assert: + that: + - result.response is defined + - result.changed == false + - result.response.metadata.total_matches > 0 + fail_msg: "Unable to list all image placement policies" + success_msg: "Image placement policies listed successfully" +################################################################ + +- name: test getting particular image placement policy using filter + ntnx_image_placement_policies_info: + filter: + name: "{{ policy_1.response.status.name }}" + length: 1 + offset: 0 + register: result + +- name: check listing status + assert: + that: + - result.response is defined + - result.response.entities[0].status.name == '{{ policy_1.response.status.name }}' + fail_msg: "Unable to get particular image placement policy" + success_msg: "Image placement policy info obtained successfully using filter" + +################################################################ + +- name: test getting particular image placement policy using uuid + ntnx_image_placement_policies_info: + policy_uuid: "{{ policy_2.response.metadata.uuid }}" + register: result + +- name: check listing status + assert: + that: + - result.response is defined + - result.response.status.name == '{{ policy_2.response.status.name }}' + fail_msg: "Unable to get particular image placement policy" + success_msg: "Image placement policy info obtained successfully using uuid" + +############################################################# + +- name: List image placement policy using ascending name sorting + ntnx_image_placement_policies_info: + sort_order: "DESCENDING" + sort_attribute: "name" + register: result + +- name: Listing Status + assert: + that: + - result.response is defined + fail_msg: "Unable to list policies as per names" + success_msg: "Image placement policy listed successfully" + +##################################################CLEANUP####################################################### + +- name: cleanup created entities + ntnx_image_placement_policy: + state: absent + policy_uuid: "{{ item }}" + register: result + loop: "{{ todelete }}" + ignore_errors: True + +- set_fact: + todelete: [] \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policies_info/tasks/main.yml b/tests/integration/targets/ntnx_image_placement_policies_info/tasks/main.yml new file mode 100644 index 000000000..3364b30c6 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policies_info/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "info.yml" diff --git a/tests/integration/targets/ntnx_image_placement_policy/meta/main.yml b/tests/integration/targets/ntnx_image_placement_policy/meta/main.yml new file mode 100644 index 000000000..23b0fb268 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policy/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policy/tasks/create.yml b/tests/integration/targets/ntnx_image_placement_policy/tasks/create.yml new file mode 100644 index 000000000..5c18acc65 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policy/tasks/create.yml @@ -0,0 +1,127 @@ +--- +- debug: + msg: start ntnx_image_placement_policy create tests + +- name: Create image placement policy with minimal spec + ntnx_image_placement_policy: + name: "test_policy_1" + placement_type: soft + image_categories: + AnalyticsExclusions: + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == true + - result.response.status.state == 'COMPLETE' + - result.response.status.name == 'test_policy_1' + - result.response.status.resources.cluster_entity_filter.params["AppTier"][0] == 'Default' + - result.response.status.resources.image_entity_filter.params["AnalyticsExclusions"][0] == 'AnomalyDetection' + - result.response.status.resources.placement_type == 'AT_LEAST' + fail_msg: "Unable to create image placement policy with minimal spec" + success_msg: "Image placement policy with minimal spec created successfully" + +- set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + +################################################################################################################ + +- name: Create image placement policy with all specs and hard type + ntnx_image_placement_policy: + name: "test_policy_2" + desc: "test_policy_2_desc" + placement_type: hard + categories: + Environment: + - "Dev" + AppType: + - "Default" + image_categories: + AnalyticsExclusions: + - EfficiencyMeasurement + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.response.status.description == 'test_policy_2_desc' + - result.response.status.resources.placement_type == 'EXACTLY' + fail_msg: "Unable to create image placement policy with all specifications" + success_msg: "Image placement policy with all specifications created successfully" + +- set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + +################################################################################################################ + +- name: Create image placement policy in check mode + check_mode: yes + ntnx_image_placement_policy: + name: "test_policy_3" + desc: "test_policy_3_desc" + placement_type: hard + image_categories: + AnalyticsExclusions: + - EfficiencyMeasurement + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == false + fail_msg: "Unable to use check mode in image placement policy" + success_msg: "Image placement policy in check mode runs successfully" + +################################################################################################################ + +- name: Create image placement policy with incorrect category + ntnx_image_placement_policy: + name: "test_policy_4" + image_categories: + Wrong: + - Category + cluster_categories: + AppTier: + - Default + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == true + fail_msg: "Image placement policy didn't failed with incorrect category" + success_msg: "Image placement policy with incorrect category errored out successfully" + +##################################################CLEANUP####################################################### + +- name: cleanup created entities + ntnx_image_placement_policy: + state: absent + policy_uuid: "{{ item }}" + register: result + loop: "{{ todelete }}" + ignore_errors: true + +- set_fact: + todelete: [] \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policy/tasks/delete.yml b/tests/integration/targets/ntnx_image_placement_policy/tasks/delete.yml new file mode 100644 index 000000000..871de54e2 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policy/tasks/delete.yml @@ -0,0 +1,57 @@ +--- +- debug: + msg: start ntnx_image_placement_policy delete tests + +- name: Create image placement policy with minimal spec for delete tests + ntnx_image_placement_policy: + name: "test_policy_5" + placement_type: soft + image_categories: + AnalyticsExclusions: + - AnomalyDetection + cluster_categories: + AppTier: + - Default + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == true + - result.response.status.state == 'COMPLETE' + fail_msg: "Unable to create image placement policy with minimal spec" + success_msg: "Image placement policy with minimal spec created successfully" + +- name: Delete image placement policy created above + ntnx_image_placement_policy: + state: absent + policy_uuid: "{{ result.response.metadata.uuid }}" + register: result + +- name: Delete Status + assert: + that: + - result.response is defined + - result.changed == true + - result.failed == false + fail_msg: "Unable to delete image placement policy" + success_msg: "Image placement policy deleted successfully" + +##################################################################################################### + +- name: Delete image placement policy with incorrect uuid + ntnx_image_placement_policy: + state: absent + policy_uuid: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + register: result + ignore_errors: true + +- name: Delete Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == true + fail_msg: "delete of image placement policy with incorrect uuid didn't failed" + success_msg: "Image placement policy delete with incorrect uuid failed successfully" \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policy/tasks/main.yml b/tests/integration/targets/ntnx_image_placement_policy/tasks/main.yml new file mode 100644 index 000000000..b5e00ad70 --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policy/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "create.yml" + - import_tasks: "update.yml" + - import_tasks: "delete.yml" \ No newline at end of file diff --git a/tests/integration/targets/ntnx_image_placement_policy/tasks/update.yml b/tests/integration/targets/ntnx_image_placement_policy/tasks/update.yml new file mode 100644 index 000000000..cca46377f --- /dev/null +++ b/tests/integration/targets/ntnx_image_placement_policy/tasks/update.yml @@ -0,0 +1,130 @@ +--- +- debug: + msg: start ntnx_image_placement_policy update tests + +- name: Create image placement policy with minimal spec for update tests + ntnx_image_placement_policy: + name: "test_policy_6" + placement_type: soft + image_categories: + AnalyticsExclusions: + - EfficiencyMeasurement + cluster_categories: + AppTier: + - Default + categories: + Environment: + - "Dev" + AppType: + - "Default" + register: setup_policy + +- name: Creation Status + assert: + that: + - setup_policy.response is defined + - setup_policy.changed == true + - setup_policy.response.status.state == 'COMPLETE' + fail_msg: "Unable to create image placement policy with minimal spec" + success_msg: "Image placement policy with minimal spec created successfully" + +- set_fact: + todelete: '{{ todelete + [ setup_policy["response"]["metadata"]["uuid"] ] }}' + +############################################################################################# + +- name: test idempotency by definig same spec as before + ntnx_image_placement_policy: + state: present + policy_uuid: "{{ setup_policy.response.metadata.uuid }}" + placement_type: "soft" + image_categories: + AnalyticsExclusions: + - EfficiencyMeasurement + cluster_categories: + AppTier: + - Default + categories: + Environment: + - "Dev" + AppType: + - "Default" + register: result + +- name: Update Status + assert: + that: + - result.changed == false + - "'Nothing to change' in result.msg" + fail_msg: "Image placement policy got updated" + success_msg: "Image placement policy update skipped succesfully due to no changes in spec" + +##################################################################################################### + +- name: update all specs + ntnx_image_placement_policy: + state: present + policy_uuid: "{{ setup_policy.response.metadata.uuid }}" + placement_type: "hard" + name: "test_policy_6_updated" + desc: "test_policy_6_desc_updated" + image_categories: + AppTier: + - Default + cluster_categories: + AnalyticsExclusions: + - EfficiencyMeasurement + categories: + Environment: + - "Production" + AppType: + - "Default" + register: result + +- name: Update Status + assert: + that: + - result.response is defined + - result.changed == true + - result.response.status.state == 'COMPLETE' + - result.response.status.name == 'test_policy_6_updated' + - result.response.status.description == 'test_policy_6_desc_updated' + - result.response.status.resources.image_entity_filter.params["AppTier"][0] == 'Default' + - result.response.status.resources.cluster_entity_filter.params["AnalyticsExclusions"][0] == 'EfficiencyMeasurement' + - result.response.status.resources.placement_type == 'EXACTLY' + - result.response.metadata.categories_mapping["Environment"] == ['Production'] + - result.response.metadata.categories_mapping["AppType"] == ['Default'] + fail_msg: "Unable to update image placement policy" + success_msg: "Image placement policy updated successfully" + +##################################################################################################### + +- name: remove attached categories to the policy + ntnx_image_placement_policy: + state: present + policy_uuid: "{{ setup_policy.response.metadata.uuid }}" + remove_categories: true + register: result + +- name: Update Status + assert: + that: + - result.response is defined + - result.changed == true + - result.response.status.state == 'COMPLETE' + - result.response.metadata.categories_mapping == {} + fail_msg: "Unable to remove categories from image placement policy" + success_msg: "Image placement policy updated successfully" + +##################################################CLEANUP####################################################### + +- name: cleanup created entities + ntnx_image_placement_policy: + state: absent + policy_uuid: "{{ item }}" + register: result + loop: "{{ todelete }}" + ignore_errors: True + +- set_fact: + todelete: [] \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images/meta/main.yml b/tests/integration/targets/ntnx_images/meta/main.yml new file mode 100644 index 000000000..23b0fb268 --- /dev/null +++ b/tests/integration/targets/ntnx_images/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images/tasks/create.yml b/tests/integration/targets/ntnx_images/tasks/create.yml new file mode 100644 index 000000000..856d9e86c --- /dev/null +++ b/tests/integration/targets/ntnx_images/tasks/create.yml @@ -0,0 +1,147 @@ +--- +- debug: + msg: start ntnx_image create tests + +- name: Create image with minimal spec and source uri + ntnx_images: + state: present + image_type: DISK_IMAGE + source_uri: "{{ disk_image.url }}" + name: integration-test-image-with-uri + wait: true + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.response.status.resources.image_type == "DISK_IMAGE" + - result.response.status.name == 'integration-test-image-with-uri' + - result.response.status.resources.source_uri == "{{ disk_image.url }}" + fail_msg: "Unable to create image using source_uri" + success_msg: "Image with given source_uri created successfully" + +- set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + +################################################################################## + +- name: Create image with all spec and using local image upload + ntnx_images: + state: present + name: integration-test-image-with-upload + desc: "uploaded image during integration test" + source_path: "{{ disk_image.dest }}" + image_type: "ISO_IMAGE" + categories: + Environment: + - "Dev" + AppType: + - "Default" + version: + product_name: "test" + product_version: "1.2.0" + clusters: + - name: "{{ cluster.name }}" + checksum: + checksum_algorithm: "SHA_1" + checksum_value: "{{ disk_image.checksum }}" + wait: true + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.response.status.resources.image_type == "ISO_IMAGE" + - result.response.metadata.categories_mapping['AppType'] == ['Default'] + - result.response.metadata.categories_mapping['Environment'] == ['Dev'] + - result.response.status.resources.initial_placement_ref_list[0]['uuid'] == "{{ cluster.uuid }}" + - result.response.status.resources.checksum.checksum_value == "{{ disk_image.checksum }}" + - result.response.status.resources.version.product_name == "test" + - result.response.status.resources.version.product_version == "1.2.0" + fail_msg: "Unable to create image and upload local image with all specification" + success_msg: "Image with given local path created successfully" + +- set_fact: + todelete: '{{ todelete + [ result["response"]["metadata"]["uuid"] ] }}' + +############################################################################################### + +- name: Verify check mode for ntnx_image + check_mode: yes + ntnx_images: + state: present + name: integration-test-image + desc: "uploaded image during integration test" + source_uri: "{{ disk_image.url }}" + image_type: "DISK_IMAGE" + categories: + Environment: + - "Dev" + AppType: + - "Default" + version: + product_name: "test" + product_version: "1.2.0" + clusters: + - name: "{{ cluster.name }}" + checksum: + checksum_algorithm: "SHA_1" + checksum_value: "{{ disk_image.checksum }}" + wait: true + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == false + - result.response.metadata is defined + - result.response.spec.name == 'integration-test-image' + - result.response.spec.resources.source_uri == "{{ disk_image.url }}" + - result.response.metadata.categories_mapping['AppType'] == ['Default'] + - result.response.metadata.categories_mapping['Environment'] == ['Dev'] + - result.response.spec.resources.initial_placement_ref_list[0]['uuid'] == "{{ cluster.uuid }}" + - result.response.spec.resources.checksum.checksum_value == "{{ disk_image.checksum }}" + - result.response.spec.resources.version.product_name == "test" + - result.response.spec.resources.version.product_version == "1.2.0" + success_msg: "Success: check mode spec returned as expected" + fail_msg: "Check mode for ntnx_image failed" + +############################################################################################## + +- name: "Negative scenario when create image with invalid source uri" + ntnx_images: + state: present + source_uri: "www.google.com" + image_type: "DISK_IMAGE" + name: integration-test-image-with-uri + wait: true + register: result + ignore_errors: True + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == True + - result.status_code == "500" + - "'INTERNAL_ERROR' in result.error" + success_msg: "Success: returned error as expected" + fail_msg: "Image create didn't failed for invalid source uri" + +################################################################################################### +- name: cleanup created entities + ntnx_images: + state: absent + image_uuid: "{{ item }}" + register: result + loop: "{{ todelete }}" + ignore_errors: True + +- set_fact: + todelete: [] \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images/tasks/delete.yml b/tests/integration/targets/ntnx_images/tasks/delete.yml new file mode 100644 index 000000000..0f9d360be --- /dev/null +++ b/tests/integration/targets/ntnx_images/tasks/delete.yml @@ -0,0 +1,53 @@ +--- +- debug: + msg: start ntnx_image delete tests + +- name: Create image with minimal spec and source uri for delete tests + ntnx_images: + state: present + image_type: DISK_IMAGE + source_uri: "{{ disk_image.url }}" + name: integration-test-image-with-uri + wait: true + register: result + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == true + - result.response.status.state == 'COMPLETE' + fail_msg: "Unable to create image with minimal spec for delete tests" + success_msg: "Image with minimal spec created successfully" + +- name: Delete image created above + ntnx_images: + state: absent + image_uuid: "{{ result.response.metadata.uuid }}" + register: result + +- name: Delete Status + assert: + that: + - result.response is defined + - result.changed == true + - result.failed == false + fail_msg: "Unable to delete image" + success_msg: "Image deleted successfully" + +##################################################################################################### + +- name: "Negative scenario when delete image with incorrect uuid" + ntnx_images: + state: absent + image_uuid: abcd + register: result + ignore_errors: True + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == True + success_msg: "Success: delete failed as expected" + fail_msg: "Fail: Delete vm didn't returned error for incorrect uuid" \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images/tasks/main.yml b/tests/integration/targets/ntnx_images/tasks/main.yml new file mode 100644 index 000000000..b5e00ad70 --- /dev/null +++ b/tests/integration/targets/ntnx_images/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "create.yml" + - import_tasks: "update.yml" + - import_tasks: "delete.yml" \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images/tasks/update.yml b/tests/integration/targets/ntnx_images/tasks/update.yml new file mode 100644 index 000000000..79484eaaa --- /dev/null +++ b/tests/integration/targets/ntnx_images/tasks/update.yml @@ -0,0 +1,124 @@ +--- +- debug: + msg: start ntnx_image create tests + +- name: Create image for update tests + ntnx_images: + state: present + name: integration-test-image-update + desc: "image for integration" + source_uri: "{{ disk_image.url }}" + image_type: "DISK_IMAGE" + categories: + Environment: + - "Dev" + AppType: + - "Default" + version: + product_name: "test" + product_version: "1.2.0" + clusters: + - name: "{{ cluster.name }}" + checksum: + checksum_algorithm: "SHA_1" + checksum_value: "{{ disk_image.checksum }}" + wait: true + + register: setup_image + +- name: Creation Status + assert: + that: + - setup_image.response is defined + - setup_image.response.status.state == 'COMPLETE' + fail_msg: "Unable to create image using source_uri for update tests" + success_msg: "Image with given source_uri created successfully for update tests" + +- set_fact: + todelete: '{{ todelete + [ setup_image["response"]["metadata"]["uuid"] ] }}' + +############################################# UPDATE TESTS ######################################## + +- name: check idempotency + ntnx_images: + state: present + image_uuid: "{{ setup_image.image_uuid }}" + name: integration-test-image-update + desc: "image for integration" + categories: + Environment: + - "Dev" + AppType: + - "Default" + image_type: "DISK_IMAGE" + register: result + +- name: check idempotency status + assert: + that: + - result.changed == false + - result.failed == false + - "'Nothing to change' in result.msg" + fail_msg: "Image got updated" + success_msg: "Image update skipped succesfully due to no changes in spec" + +######################################################################################## + +- name: Update name, desc and categories + ntnx_images: + state: present + image_uuid: "{{ setup_image.image_uuid }}" + name: integration-test-image-after-update + desc: "description after update" + categories: + Environment: + - "Production" + AppType: + - "Default" + image_type: "ISO_IMAGE" + register: result + +- name: Update Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.response.status.name == 'integration-test-image-after-update' + - result.response.status.description == 'description after update' + - result.response.status.resources.image_type == 'ISO_IMAGE' + - result.response.metadata.categories_mapping['Environment'] == ['Production'] + - result.response.metadata.categories_mapping['AppType'] == ['Default'] + fail_msg: "Unable to update image" + success_msg: "Image with given update spec updated successfully" + +######################################################################################## + +- name: test removal of all categories from image + ntnx_images: + state: present + image_uuid: "{{ setup_image.image_uuid }}" + remove_categories: true + register: result + + +- name: Update Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.response.metadata.categories_mapping == {} + fail_msg: "Unable to remove all categories from image" + success_msg: "All categories attached to image removed successfully" + +########################################### Cleanup ################################################### + +- name: cleanup created entities + ntnx_images: + state: absent + image_uuid: "{{ item }}" + register: result + loop: "{{ todelete }}" + ignore_errors: True + +- set_fact: + todelete: [] \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images_info/meta/main.yml b/tests/integration/targets/ntnx_images_info/meta/main.yml new file mode 100644 index 000000000..23b0fb268 --- /dev/null +++ b/tests/integration/targets/ntnx_images_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images_info/tasks/info.yml b/tests/integration/targets/ntnx_images_info/tasks/info.yml new file mode 100644 index 000000000..aa21addcc --- /dev/null +++ b/tests/integration/targets/ntnx_images_info/tasks/info.yml @@ -0,0 +1,50 @@ +--- +- debug: + msg: "start ntnx_images_info tests" + +- name: test getting all images + ntnx_images_info: + register: result + +- name: check listing status + assert: + that: + - result.response is defined + fail_msg: "Unable to list all images" + success_msg: "Images listed successfully" +################################################################ + +- name: test getting particular image using filter + ntnx_images_info: + filter: + name: "{{ disk_image.centos }}" + register: result + +- name: check listing status + assert: + that: + - result.response is defined + - result.response.entities[0].status.name == '{{ disk_image.centos }}' + fail_msg: "Unable to get particular image" + success_msg: "Image info obtained successfully" + +################################################################ + +- name: getting image info for test + ntnx_images_info: + filter: + name: "{{ disk_image.centos }}" + register: result + +- name: test getting particular image using uuid + ntnx_images_info: + image_uuid: '{{ result.response.entities[0].metadata.uuid }}' + register: result + +- name: check listing status + assert: + that: + - result.response is defined + - result.response.status.name == '{{ disk_image.centos }}' + fail_msg: "Unable to get particular image" + success_msg: "Image info obtained successfully" \ No newline at end of file diff --git a/tests/integration/targets/ntnx_images_info/tasks/main.yml b/tests/integration/targets/ntnx_images_info/tasks/main.yml new file mode 100644 index 000000000..3364b30c6 --- /dev/null +++ b/tests/integration/targets/ntnx_images_info/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "info.yml" diff --git a/tests/integration/targets/ntnx_security_rules/meta/main.yml b/tests/integration/targets/ntnx_security_rules/meta/main.yml new file mode 100644 index 000000000..e4f447d3a --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env diff --git a/tests/integration/targets/ntnx_security_rules/tasks/app_rule.yml b/tests/integration/targets/ntnx_security_rules/tasks/app_rule.yml new file mode 100644 index 000000000..07191d468 --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/tasks/app_rule.yml @@ -0,0 +1,176 @@ +- name: create app security rule with inbound and outbound list + ntnx_security_rules: + name: test_app_rule + app_rule: + target_group: + categories: + apptype: Apache_Spark + apptype_filter_by_category: + AppFamily: + - Backup + apptier: Default + default_internal_policy: DENY_ALL + + inbounds: + - + categories: + AppFamily: + - Databases + - DevOps + description: test description + protocol: + tcp: + - start_port: 22 + end_port: 80 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + icmp: + - code: 1 + type: 1 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + udp: + - start_port: 82 + end_port: 8080 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + service: + name: 6a44 + - + ip_subnet: + prefix_length: 24 + ip: 192.168.1.0 + description: test description + - + address: + name: dest + outbounds: + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + icmp: + - code: 1 + type: 1 + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog: true + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status.state == 'COMPLETE' + - result.response.spec.name=="test_app_rule" + fail_msg: ' fail: unable to create app security rule with inbound and outbound list' + success_msg: 'pass: create app security rule with inbound and outbound list successfully' + +- name: update app security rule by adding to outbound list and remove tule from inbound list + ntnx_security_rules: + security_rule_uuid: '{{ result.response.metadata.uuid }}' + app_rule: + policy_mode: APPLY + inbounds: + - + rule_id: "{{result.response.spec.resources.app_rule.inbound_allow_list.0.rule_id}}" + state: absent + outbounds: + - + protocol: + icmp: + - code: 1 + type: 1 + categories: + AppFamily: + - Databases + - DevOps + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status.state == 'COMPLETE' + - result.response.spec.resources.app_rule.action == "APPLY" + - result.response.spec.resources.app_rule.outbound_allow_list.0.icmp_type_code_list is defined + fail_msg: ' fail: unable to update app security rule with outbound list ' + success_msg: 'pass :update app security rule with outbound list successfully' + +- name: delete app security rule + ntnx_security_rules: + state: absent + security_rule_uuid: '{{ result.response.metadata.uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status == 'SUCCEEDED' + fail_msg: ' fail: unable to delete app security rule ' + success_msg: 'pass : delete app security rule successfully' +- name: create app security rule with allow all inbound and outbound list + ntnx_security_rules: + name: test_app_rule + app_rule: + target_group: + categories: + apptype: Apache_Spark + apptype_filter_by_category: + AppFamily: + - Backup + apptier: Default + default_internal_policy: DENY_ALL + allow_all_outbounds: true + allow_all_inbounds: true + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog: true + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status.state == 'COMPLETE' + - result.response.spec.name=="test_app_rule" + fail_msg: ' fail: unable to create app security rule with allow all inbound and outbound list' + success_msg: 'pass: create app security rule with allow all inbound and outbound list successfully' +- name: delete app security rule + ntnx_security_rules: + state: absent + security_rule_uuid: '{{ result.response.metadata.uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status == 'SUCCEEDED' + fail_msg: ' fail: unable to delete app security rule ' + success_msg: 'pass : delete app security rule successfully' \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules/tasks/isolation_rule.yml b/tests/integration/targets/ntnx_security_rules/tasks/isolation_rule.yml new file mode 100644 index 000000000..ee6bda0db --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/tasks/isolation_rule.yml @@ -0,0 +1,133 @@ +- name: >- + create isolation security rule with first_entity_filter and + second_entity_filter with check mode + ntnx_security_rules: + name: test_isolation_rule + isolation_rule: + isolate_category: + Environment: + - Dev + from_category: + Environment: + - Production + subset_category: + Environment: + - Staging + policy_mode: MONITOR + register: result + ignore_errors: true + check_mode: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.changed == false + - result.response.spec.name=="test_isolation_rule" + - result.security_rule_uuid is none + fail_msg: ' fail: unable to create isolation security rule with first_entity_filter and second_entity_filter with check mode ' + success_msg: >- + pass: create isolation security rule with first_entity_filter and + second_entity_filter successfully with check mode +- name: >- + create isolation security rule with first_entity_filter and + second_entity_filter + ntnx_security_rules: + name: test_isolation_rule + isolation_rule: + isolate_category: + Environment: + - Dev + from_category: + Environment: + - Production + subset_category: + Environment: + - Staging + policy_mode: MONITOR + policy_hitlog: true + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.spec.name=="test_isolation_rule" + - result.response.status.state == 'COMPLETE' + fail_msg: ' fail: unable to create isolation security rule with first_entity_filter and second_entity_filter' + success_msg: >- + pass: create isolation security rule with first_entity_filter and + second_entity_filter successfully + +- name: update isoloation security rule action with check_mode + ntnx_security_rules: + security_rule_uuid: '{{ result.response.metadata.uuid }}' + isolation_rule: + policy_mode: APPLY + register: output + ignore_errors: true + check_mode: true + +- name: Creation Status + assert: + that: + - output.response is defined + - output.failed == false + - output.changed == false + - output.response.spec.name=="test_isolation_rule" + - output.security_rule_uuid is none + fail_msg: ' fail: unable to update isoloation security rule action with check_mode' + success_msg: >- + pass: update isoloation security rule action with check_mode successfully + +- name: update isoloation security rule action + ntnx_security_rules: + security_rule_uuid: '{{ result.security_rule_uuid}}' + isolation_rule: + policy_mode: APPLY + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.changed == true + - result.response.status.state == 'COMPLETE' + - result.response.spec.resources.isolation_rule.action == "APPLY" + fail_msg: ' fail: unable to update isolation rule action ' + success_msg: 'pass : update isolation rule action successfully' +- name: update isoloation security with same values + ntnx_security_rules: + security_rule_uuid: '{{result.security_rule_uuid}}' + isolation_rule: + policy_mode: APPLY + register: output + ignore_errors: true +- name: Creation Status + assert: + that: + - output.failed == false + - output.changed == false + - output.msg == "Nothing to change" + fail_msg: ' fail: unable to update isolation rule action ' + success_msg: 'pass : update isolation rule action successfully' +- name: delete isolation rule + ntnx_security_rules: + state: absent + security_rule_uuid: '{{ result.security_rule_uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status == 'SUCCEEDED' + fail_msg: ' fail: unable to delete isolation security rule ' + success_msg: 'pass : delete isolation security rule successfully' \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules/tasks/main.yml b/tests/integration/targets/ntnx_security_rules/tasks/main.yml new file mode 100644 index 000000000..172cfd461 --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "app_rule.yml" + - import_tasks: "isolation_rule.yml" + - import_tasks: "quarantine_rule.yml" + - import_tasks: "vdi.yml" diff --git a/tests/integration/targets/ntnx_security_rules/tasks/quarantine_rule.yml b/tests/integration/targets/ntnx_security_rules/tasks/quarantine_rule.yml new file mode 100644 index 000000000..badaadfae --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/tasks/quarantine_rule.yml @@ -0,0 +1,35 @@ + - name: update quarantine_rule by adding inbound and outbound list + ntnx_security_rules: + security_rule_uuid: "{{quarantine_rule_uuid}}" + quarantine_rule: + target_group: + default_internal_policy: DENY_ALL + inbounds: + - + categories: + AppFamily: + - Databases + - DevOps + outbounds: + - + categories: + AppFamily: + - Databases + - DevOps + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog: true + register: result + ignore_errors: true + + - name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status.state == 'COMPLETE' + - result.response.spec.resources.quarantine_rule.action == "MONITOR" + fail_msg: ' fail: unable to update quarantine_rule by adding inbound and outbound list ' + success_msg: >- + pass: update quarantine_rule by adding inbound and outbound list + succesfully \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules/tasks/vdi.yml b/tests/integration/targets/ntnx_security_rules/tasks/vdi.yml new file mode 100644 index 000000000..5fa19d511 --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/tasks/vdi.yml @@ -0,0 +1,102 @@ +- name: create ad security rule with inbound and outbound list + ntnx_security_rules: + name: VDI Policy + vdi_rule: + target_group: + categories: + adgroup: "$Default" + default_internal_policy: DENY_ALL + allow_all_outbounds: true + inbounds: + - + categories: + AppFamily: + - Databases + - DevOps + description: test description + protocol: + tcp: + - start_port: 22 + end_port: 80 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + icmp: + - code: 1 + type: 1 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + udp: + - start_port: 82 + end_port: 8080 + - + categories: + AppFamily: + - Databases + - DevOps + protocol: + service: + name: 6a44 + - + address: + name: dest + policy_mode: MONITOR + allow_ipv6_traffic: true + policy_hitlog: true + register: result + ignore_errors: true +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.spec.name=="VDI Policy" + - result.response.status.state == 'COMPLETE' + fail_msg: ' fail: unable create ad security rule with inbound and outbound list' + success_msg: >- + pass: create ad security rule with inbound and outbound list finished successfully +- name: update VDI security rule action + ntnx_security_rules: + security_rule_uuid: '{{ result.response.metadata.uuid }}' + vdi_rule: + inbounds: + - + ip_subnet: + prefix_length: 8 + ip: 10.0.0.0 + description: test description + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.changed == true + - result.response.status.state == 'COMPLETE' + fail_msg: ' fail: unable to update vdi_rule ' + success_msg: 'pass : update vdi_rule successfully' + +- name: delete vdi_rule rule + ntnx_security_rules: + state: absent + security_rule_uuid: '{{ result.response.metadata.uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status == 'SUCCEEDED' + fail_msg: ' fail: unable to delete vdi_rule security rule ' + success_msg: 'pass : delete vdi_rule security rule successfully' \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules/vars/main.yml b/tests/integration/targets/ntnx_security_rules/vars/main.yml new file mode 100644 index 000000000..579189152 --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules/vars/main.yml @@ -0,0 +1 @@ + quarantine_rule_uuid: 8c520823-8ce2-4b0c-ba18-d2ec7bbd86bc \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules_info/meta/main.yml b/tests/integration/targets/ntnx_security_rules_info/meta/main.yml new file mode 100644 index 000000000..e4f447d3a --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env diff --git a/tests/integration/targets/ntnx_security_rules_info/tasks/get_security_rules.yml b/tests/integration/targets/ntnx_security_rules_info/tasks/get_security_rules.yml new file mode 100644 index 000000000..83770606f --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules_info/tasks/get_security_rules.yml @@ -0,0 +1,95 @@ +- debug: + msg: Start testing ntnx_security_rules_info +################################### +- name: Create isolation_rule for testing + ntnx_security_rules: + name: isolation_test_rule + isolation_rule: + isolate_category: + Environment: + - Dev + from_category: + Environment: + - Production + policy_mode: MONITOR + register: first_rule + ignore_errors: true + +- name: Creation Status + assert: + that: + - first_rule.response is defined + - first_rule.failed == false + - first_rule.response.status.state == 'COMPLETE' + - first_rule.response.spec.name=="isolation_test_rule" + fail_msg: ' fail: Unable to create isolation_rule for testing ' + success_msg: 'pass: isolation_rule for testing created successfully ' +################################### +- name: getting all security rules + ntnx_security_rules_info: + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == false + - result.response.metadata.kind == "network_security_rule" + - result.response.metadata.total_matches > 0 + fail_msg: ' fail: unable to get security rules ' + success_msg: 'pass: get all security rules successfully ' +################################### +- name: getting particlar security rule using security_rule_uuid + ntnx_security_rules_info: + security_rule_uuid: '{{ first_rule.response.metadata.uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == false + - result.response.status.state == 'COMPLETE' + - first_rule.response.metadata.uuid == result.response.metadata.uuid + fail_msg: ' fail : unable to get particlar security rule using security_rule_uuid' + success_msg: 'pass: getting security rule using security_rule_uuid succesfuly' +################################### +- name: getting all security rules sorted + ntnx_security_rules_info: + sort_order: ASCENDING + sort_attribute: Name + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.changed == false + - result.failed == false + - result.response.metadata.kind == "network_security_rule" + - result.response.metadata.sort_order == "ASCENDING" + - result.response.metadata.sort_attribute == "Name" + fail_msg: ' fail: unable to get all security rules sorted' + success_msg: 'pass: getting all security rules sorted successfully ' +################################### +- name: delete security rule + ntnx_security_rules: + state: absent + security_rule_uuid: '{{ first_rule.response.metadata.uuid }}' + register: result + ignore_errors: true + +- name: Creation Status + assert: + that: + - result.response is defined + - result.failed == false + - result.response.status == 'SUCCEEDED' + fail_msg: ' fail: unable to delete secutiry rule ' + success_msg: 'pass: security rule deleted succesfully ' +################################### \ No newline at end of file diff --git a/tests/integration/targets/ntnx_security_rules_info/tasks/main.yml b/tests/integration/targets/ntnx_security_rules_info/tasks/main.yml new file mode 100644 index 000000000..cc243432b --- /dev/null +++ b/tests/integration/targets/ntnx_security_rules_info/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "get_security_rules.yml" diff --git a/tests/integration/targets/ntnx_static_routes/aliases b/tests/integration/targets/ntnx_static_routes/aliases new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/ntnx_static_routes/meta/main.yml b/tests/integration/targets/ntnx_static_routes/meta/main.yml new file mode 100644 index 000000000..e4f447d3a --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env diff --git a/tests/integration/targets/ntnx_static_routes/tasks/create.yml b/tests/integration/targets/ntnx_static_routes/tasks/create.yml new file mode 100644 index 000000000..5d3bbfb65 --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes/tasks/create.yml @@ -0,0 +1,134 @@ +- debug: + msg: Start testing static routes update tests + +- name: Add default static route and external nat static route to the vpc route table + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + - destination: "10.2.2.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + - destination: "10.2.3.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + register: result + +- name: Update static routes list Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.changed == true + - result.response.status.resources.static_routes_list[0]["is_active"] == true + - result.response.status.resources.static_routes_list[0]["destination"] == "10.2.2.0/24" + - result.response.status.resources.static_routes_list[0]["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + - result.response.status.resources.static_routes_list[1]["is_active"] == true + - result.response.status.resources.static_routes_list[1]["destination"] == "10.2.3.0/24" + - result.response.status.resources.static_routes_list[1]["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + - result.response.status.resources.default_route["is_active"] == true + - result.response.status.resources.default_route["destination"] == "0.0.0.0/0" + - result.response.status.resources.default_route["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + + fail_msg: 'Fail: Unable to update static routes of vpc' + success_msg: 'Succes: static routes updated succesfully' + +########################################################################################################### + +- name: Idempotency check + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + - destination: "10.2.2.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + - destination: "10.2.3.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + register: result + +- name: check idempotency status + assert: + that: + - result.changed == false + - result.failed == false + - "'Nothing to update' in result.msg" + fail_msg: "Static routes" + success_msg: "Static routes update skipped succesfully due to no changes in spec" + +########################################################################################################### + +- name: Override existing static routes + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "10.2.4.0/24" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + register: result + +- name: Update static routes list Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.changed == true + - result.response.status.resources.static_routes_list[0]["is_active"] == true + - result.response.status.resources.static_routes_list[0]["destination"] == "10.2.4.0/24" + - result.response.status.resources.static_routes_list[0]["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + fail_msg: "Static routes overriding failed" + success_msg: "Static routes overriden successfully" + +########################################################################################################### + +- name: Netgative scenario of cretaing multiple default routes + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + register: result + ignore_errors: true + +- name: Update static routes list Status + assert: + that: + - result.changed == false + - result.failed == true + fail_msg: "Static routes updated successfully" + success_msg: "Static routes update failed successfully" + +########################################################################################################### +- name: remove all routes + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + remove_all_routes: true + register: result + +- name: Remove all routes status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.changed == true + - result.response.status.resources.static_routes_list == [] + fail_msg: "Static routes remove failed" + success_msg: "Static routes removed successfully" diff --git a/tests/integration/targets/ntnx_static_routes/tasks/main.yml b/tests/integration/targets/ntnx_static_routes/tasks/main.yml new file mode 100644 index 000000000..b19cfc1ec --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "create.yml" diff --git a/tests/integration/targets/ntnx_static_routes_info/aliases b/tests/integration/targets/ntnx_static_routes_info/aliases new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/ntnx_static_routes_info/meta/main.yml b/tests/integration/targets/ntnx_static_routes_info/meta/main.yml new file mode 100644 index 000000000..e4f447d3a --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env diff --git a/tests/integration/targets/ntnx_static_routes_info/tasks/info.yml b/tests/integration/targets/ntnx_static_routes_info/tasks/info.yml new file mode 100644 index 000000000..8eda760ab --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes_info/tasks/info.yml @@ -0,0 +1,71 @@ +- debug: + msg: Start testing static routes info tests + +- name: create new static routes + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + static_routes: + - destination: "0.0.0.0/0" + next_hop: + external_subnet_ref: + name: "{{ external_nat_subnet.name }}" + - destination: "10.2.2.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + - destination: "10.2.3.0/24" + next_hop: + external_subnet_ref: + uuid: "{{ external_nat_subnet.uuid }}" + register: result + +- name: Update static routes list Status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.changed == true + fail_msg: 'Fail: Unable to update static routes of vpc' + success_msg: 'Succes: static routes updated succesfully' + +########################################################################################################### + +- name: get all static routes + ntnx_static_routes_info: + vpc_uuid: "{{ vpc.uuid }}" + register: result + +- name: check info module response + assert: + that: + - result.response is defined + - result.changed == false + - result.response.status.resources.static_routes_list[0]["is_active"] == true + - result.response.status.resources.static_routes_list[0]["destination"] == "10.2.2.0/24" + - result.response.status.resources.static_routes_list[0]["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + - result.response.status.resources.static_routes_list[1]["is_active"] == true + - result.response.status.resources.static_routes_list[1]["destination"] == "10.2.3.0/24" + - result.response.status.resources.static_routes_list[1]["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + - result.response.status.resources.default_route["is_active"] == true + - result.response.status.resources.default_route["destination"] == "0.0.0.0/0" + - result.response.status.resources.default_route["nexthop"]["external_subnet_reference"]["name"] == "{{ external_nat_subnet.name }}" + + fail_msg: 'Fail: Unable to get static routes for vpc' + success_msg: 'Succes' + +########################################################################################################### + +- name: remove all routes for cleanup + ntnx_static_routes: + vpc_uuid: "{{ vpc.uuid }}" + remove_all_routes: true + register: result + ignore_errors: true + +- name: Remove all routes status + assert: + that: + - result.response is defined + - result.response.status.state == 'COMPLETE' + - result.changed == true + - result.response.status.resources.static_routes_list == [] diff --git a/tests/integration/targets/ntnx_static_routes_info/tasks/main.yml b/tests/integration/targets/ntnx_static_routes_info/tasks/main.yml new file mode 100644 index 000000000..3364b30c6 --- /dev/null +++ b/tests/integration/targets/ntnx_static_routes_info/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - import_tasks: "info.yml" diff --git a/tests/integration/targets/ntnx_vms_clone/vars/main.yml b/tests/integration/targets/ntnx_vms_clone/vars/main.yml index 305e19156..83f47dfce 100644 --- a/tests/integration/targets/ntnx_vms_clone/vars/main.yml +++ b/tests/integration/targets/ntnx_vms_clone/vars/main.yml @@ -3,13 +3,13 @@ centos: "CentOS-7-cloudinit" ubuntu: "Ubuntu-20.04.4" storage_container: name: SelfServiceContainer - uuid: d4fd45e9-af30-4eb1-b4a6-9c0f66fe7363 + uuid: 701e50d4-dfe5-4294-8c5d-3b7054230b36 network: dhcp: name: vlan.154 - uuid: 9dd38e9c-0c76-44d6-85ec-82cceb76a9ea + uuid: f1b7a142-ed69-4077-9385-ee34dd6a3532 static: ip: 10.30.30.75 project: name: integration_test_project - uuid: 5cda2e10-c62c-4e01-9efb-76d768a732e0 + uuid: 4e42ef74-7176-40f1-8e10-f6aa422e896f \ No newline at end of file diff --git a/tests/integration/targets/nutanix_vms/tasks/create.yml b/tests/integration/targets/nutanix_vms/tasks/create.yml index 049c04e4a..c48f033d4 100644 --- a/tests/integration/targets/nutanix_vms/tasks/create.yml +++ b/tests/integration/targets/nutanix_vms/tasks/create.yml @@ -51,7 +51,9 @@ desc: "VM with cluster, network, category, disk with Ubuntu image, guest customization " categories: AppType: - - "Apache_Spark" + - Default + Environment: + - Dev cluster: name: "{{ cluster.name }}" networks: @@ -78,6 +80,8 @@ that: - result.response is defined - result.response.status.state == 'COMPLETE' + - result.response.metadata.categories_mapping["AppType"] == ["Default"] + - result.response.metadata.categories_mapping["Environment"] == ["Dev"] fail_msg: 'Unable to Create VM with Ubuntu image and different specifications ' success_msg: 'VM with Ubuntu image and different specifications created successfully ' diff --git a/tests/integration/targets/nutanix_vms/tasks/vm_update.yml b/tests/integration/targets/nutanix_vms/tasks/vm_update.yml index 6b3910506..d8ee4048f 100644 --- a/tests/integration/targets/nutanix_vms/tasks/vm_update.yml +++ b/tests/integration/targets/nutanix_vms/tasks/vm_update.yml @@ -6,6 +6,9 @@ name: update vm cluster: name: "{{ cluster.name }}" + categories: + Environment: + - Production vcpus: 5 cores_per_vcpu: 5 memory_gb: 5 @@ -82,7 +85,53 @@ - result.response.status.state == "COMPLETE" fail_msg: ' Unable to update vm by increase values for corespervcpu with force_power_off' success_msg: ' VM updated successfully by increase values for corespervcpu with force_power_off ' + #################################################################### +- debug: + msg: Start update tests for vm categories + +- name: update categories + ntnx_vms: + vm_uuid: "{{ result.vm_uuid }}" + categories: + Environment: + - Dev + AppType: + - Default + register: result + ignore_errors: true + +- name: Assert categories Status + assert: + that: + - result.response is defined + - result.vm_uuid + - result.task_uuid + - result.response.status.state == "COMPLETE" + - result.response.metadata.categories_mapping["Environment"] == ["Dev"] + - result.response.metadata.categories_mapping["AppType"] == ["Default"] + fail_msg: ' Unable to update categories attached to vm' + success_msg: ' VM categories updated successfully ' + +- name: remove all categoies attached to vm + ntnx_vms: + vm_uuid: "{{ result.vm_uuid }}" + remove_categories: true + register: result + ignore_errors: true + +- name: Assert categories Status + assert: + that: + - result.response is defined + - result.vm_uuid + - result.task_uuid + - result.response.status.state == "COMPLETE" + - result.response.metadata.categories_mapping == {} + fail_msg: ' Unable to remove all categories attached to vm' + success_msg: ' All VM categories removed successfully ' + +################################################################### - debug: msg: Start update tests for disks ##### CRUD opperation for SCSI disks diff --git a/tests/integration/targets/nutanix_vms/vars/main.yml b/tests/integration/targets/nutanix_vms/vars/main.yml index a62a430fb..83f47dfce 100644 --- a/tests/integration/targets/nutanix_vms/vars/main.yml +++ b/tests/integration/targets/nutanix_vms/vars/main.yml @@ -3,13 +3,13 @@ centos: "CentOS-7-cloudinit" ubuntu: "Ubuntu-20.04.4" storage_container: name: SelfServiceContainer - uuid: d4fd45e9-af30-4eb1-b4a6-9c0f66fe7363 + uuid: 701e50d4-dfe5-4294-8c5d-3b7054230b36 network: dhcp: name: vlan.154 - uuid: 9dd38e9c-0c76-44d6-85ec-82cceb76a9ea + uuid: f1b7a142-ed69-4077-9385-ee34dd6a3532 static: ip: 10.30.30.75 project: name: integration_test_project - uuid: 5cda2e10-c62c-4e01-9efb-76d768a732e0 \ No newline at end of file + uuid: 4e42ef74-7176-40f1-8e10-f6aa422e896f \ No newline at end of file diff --git a/tests/integration/targets/prepare_env/tasks/cleanup.yml b/tests/integration/targets/prepare_env/tasks/cleanup.yml index 259a4e422..d997296db 100644 --- a/tests/integration/targets/prepare_env/tasks/cleanup.yml +++ b/tests/integration/targets/prepare_env/tasks/cleanup.yml @@ -44,3 +44,8 @@ loop: - "{{external_nat_subnet.uuid}}" - "{{static.uuid}}" + + - name: Delete downloaded disk file + file: + path: "{{ disk_image.dest }}" + state: absent \ No newline at end of file diff --git a/tests/integration/targets/prepare_env/tasks/prepare_env.yml b/tests/integration/targets/prepare_env/tasks/prepare_env.yml index 1a4dc2825..c2b90cc03 100644 --- a/tests/integration/targets/prepare_env/tasks/prepare_env.yml +++ b/tests/integration/targets/prepare_env/tasks/prepare_env.yml @@ -158,3 +158,8 @@ vm: name: "{{vm_name}}" uuid: "{{result.vm_uuid}}" + + - name: Downloading disk image for image related tests + get_url: + url: "{{ disk_image.url }}" + dest: "{{ disk_image.dest }}" diff --git a/tests/integration/targets/prepare_env/vars/main.yml b/tests/integration/targets/prepare_env/vars/main.yml index 4bac434da..534aca61f 100644 --- a/tests/integration/targets/prepare_env/vars/main.yml +++ b/tests/integration/targets/prepare_env/vars/main.yml @@ -1,13 +1,19 @@ validate_certs: False todelete: [] +disk_image: + url: https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1602.qcow2 + dest: /home/ubuntu/.ansible/collections/ansible_collections/nutanix/ncp/tests/integration/targets/prepare_env/tasks/centosO7-1.qcow2 + checksum: b0e24d81219fe39bfe16d9b10c91e66315333100 + centos: "CentOS-7-cloudinit" + cluster: - name: auto_cluster_prod_1a680af8db60 - uuid: 0005e07c-aca2-a24f-3507-ac1f6b60292f + name: auto_cluster_prod_1af28270d50d + uuid: 0005e2f5-ba05-d75a-1fc3-ac1f6b6029c1 virtual_switch: name: vs0 - uuid: b71adeea-c7a8-4bf2-80d0-3b127a31bb80 + uuid: 32792030-2db7-4973-920d-36eaf4902c6c external_nat_subnets: name: integration_test_Ext-Nat vlan_id: 103 @@ -29,4 +35,4 @@ overlay_subnet: network_ip: 192.168.1.0 network_prefix: 24 gateway_ip: 192.168.1.1 - private_ip: 192.168.1.13 + private_ip: 192.168.1.13 \ No newline at end of file