#!/usr/bin/python # # Copyright (C) 2018 Huawei # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations ############################################################################### # Documentation ############################################################################### DOCUMENTATION = r""" module: hwc_network_vpc description: - Represents an vpc resource. short_description: Creates a Huawei Cloud VPC author: Huawei Inc. (@huaweicloud) requirements: - requests >= 2.18.4 - keystoneauth1 >= 3.6.0 attributes: check_mode: support: full diff_mode: support: none options: state: description: - Whether the given object should exist in VPC. type: str choices: ['present', 'absent'] default: 'present' timeouts: description: - The timeouts for each operations. type: dict default: {} suboptions: create: description: - The timeout for create operation. type: str default: '15m' update: description: - The timeout for update operation. type: str default: '15m' delete: description: - The timeout for delete operation. type: str default: '15m' name: description: - The name of vpc. type: str required: true cidr: description: - The range of available subnets in the VPC. type: str required: true extends_documentation_fragment: - community.general.hwc - community.general.attributes """ EXAMPLES = r""" - name: Create a vpc community.general.hwc_network_vpc: identity_endpoint: "{{ identity_endpoint }}" user: "{{ user }}" password: "{{ password }}" domain: "{{ domain }}" project: "{{ project }}" region: "{{ region }}" name: "vpc_1" cidr: "192.168.100.0/24" state: present """ RETURN = r""" id: description: - The ID of VPC. type: str returned: success name: description: - The name of VPC. type: str returned: success cidr: description: - The range of available subnets in the VPC. type: str returned: success status: description: - The status of VPC. type: str returned: success routes: description: - The route information. type: complex returned: success contains: destination: description: - The destination network segment of a route. type: str returned: success next_hop: description: - The next hop of a route. If the route type is peering, it provides VPC peering connection ID. type: str returned: success enable_shared_snat: description: - Show whether the shared SNAT is enabled. type: bool returned: success """ ############################################################################### # Imports ############################################################################### from ansible_collections.community.general.plugins.module_utils.hwc_utils import ( Config, HwcClientException, HwcClientException404, HwcModule, are_different_dicts, is_empty_value, wait_to_finish, get_region, build_path, navigate_value, ) import re ############################################################################### # Main ############################################################################### def main(): """Main function""" module = HwcModule( argument_spec=dict( state=dict(default="present", choices=["present", "absent"], type="str"), timeouts=dict( type="dict", options=dict( create=dict(default="15m", type="str"), update=dict(default="15m", type="str"), delete=dict(default="15m", type="str"), ), default=dict(), ), name=dict(required=True, type="str"), cidr=dict(required=True, type="str"), ), supports_check_mode=True, ) config = Config(module, "vpc") state = module.params["state"] if (not module.params.get("id")) and module.params.get("name"): module.params["id"] = get_id_by_name(config) fetch = None link = self_link(module) # the link will include Nones if required format parameters are missed if not re.search("/None/|/None$", link): client = config.client(get_region(module), "vpc", "project") fetch = fetch_resource(module, client, link) if fetch: fetch = fetch.get("vpc") changed = False if fetch: if state == "present": expect = _get_editable_properties(module) current_state = response_to_hash(module, fetch) current = {"cidr": current_state["cidr"]} if are_different_dicts(expect, current): if not module.check_mode: fetch = update(config, self_link(module)) fetch = response_to_hash(module, fetch.get("vpc")) changed = True else: fetch = current_state else: if not module.check_mode: delete(config, self_link(module)) fetch = {} changed = True else: if state == "present": if not module.check_mode: fetch = create(config, "vpcs") fetch = response_to_hash(module, fetch.get("vpc")) changed = True else: fetch = {} fetch.update({"changed": changed}) module.exit_json(**fetch) def create(config, link): module = config.module client = config.client(get_region(module), "vpc", "project") r = None try: r = client.post(link, resource_to_create(module)) except HwcClientException as ex: msg = f"module(hwc_network_vpc): error creating resource, error: {ex}" module.fail_json(msg=msg) wait_done = wait_for_operation(config, "create", r) v = "" try: v = navigate_value(wait_done, ["vpc", "id"]) except Exception as ex: module.fail_json(msg=str(ex)) url = build_path(module, "vpcs/{op_id}", {"op_id": v}) return fetch_resource(module, client, url) def update(config, link): module = config.module client = config.client(get_region(module), "vpc", "project") r = None try: r = client.put(link, resource_to_update(module)) except HwcClientException as ex: msg = f"module(hwc_network_vpc): error updating resource, error: {ex}" module.fail_json(msg=msg) wait_for_operation(config, "update", r) return fetch_resource(module, client, link) def delete(config, link): module = config.module client = config.client(get_region(module), "vpc", "project") try: client.delete(link) except HwcClientException as ex: msg = f"module(hwc_network_vpc): error deleting resource, error: {ex}" module.fail_json(msg=msg) wait_for_delete(module, client, link) def fetch_resource(module, client, link): try: return client.get(link) except HwcClientException as ex: msg = f"module(hwc_network_vpc): error fetching resource, error: {ex}" module.fail_json(msg=msg) def get_id_by_name(config): module = config.module client = config.client(get_region(module), "vpc", "project") name = module.params.get("name") link = "vpcs" query_link = "?marker={marker}&limit=10" link += query_link not_format_keys = re.findall("={marker}", link) none_values = re.findall("=None", link) if not (not_format_keys or none_values): r = None try: r = client.get(link) except Exception: pass if r is None: return None r = r.get("vpcs", []) ids = [i.get("id") for i in r if i.get("name", "") == name] if not ids: return None elif len(ids) == 1: return ids[0] else: module.fail_json(msg="Multiple resources with same name are found.") elif none_values: module.fail_json(msg="Can not find id by name because url includes None.") else: p = {"marker": ""} ids = set() while True: r = None try: r = client.get(link.format(**p)) except Exception: pass if r is None: break r = r.get("vpcs", []) if r == []: break for i in r: if i.get("name") == name: ids.add(i.get("id")) if len(ids) >= 2: module.fail_json(msg="Multiple resources with same name are found.") p["marker"] = r[-1].get("id") return ids.pop() if ids else None def self_link(module): return build_path(module, "vpcs/{id}") def resource_to_create(module): params = dict() v = module.params.get("cidr") if not is_empty_value(v): params["cidr"] = v v = module.params.get("name") if not is_empty_value(v): params["name"] = v if not params: return params params = {"vpc": params} return params def resource_to_update(module): params = dict() v = module.params.get("cidr") if not is_empty_value(v): params["cidr"] = v if not params: return params params = {"vpc": params} return params def _get_editable_properties(module): return { "cidr": module.params.get("cidr"), } def response_to_hash(module, response): """Remove unnecessary properties from the response. This is for doing comparisons with Ansible's current parameters. """ return { "id": response.get("id"), "name": response.get("name"), "cidr": response.get("cidr"), "status": response.get("status"), "routes": VpcRoutesArray(response.get("routes", []), module).from_response(), "enable_shared_snat": response.get("enable_shared_snat"), } def wait_for_operation(config, op_type, op_result): module = config.module op_id = "" try: op_id = navigate_value(op_result, ["vpc", "id"]) except Exception as ex: module.fail_json(msg=str(ex)) url = build_path(module, "vpcs/{op_id}", {"op_id": op_id}) timeout = 60 * int(module.params["timeouts"][op_type].rstrip("m")) states = { "create": { "allowed": ["CREATING", "DONW", "OK"], "complete": ["OK"], }, "update": { "allowed": ["PENDING_UPDATE", "DONW", "OK"], "complete": ["OK"], }, } return wait_for_completion(url, timeout, states[op_type]["allowed"], states[op_type]["complete"], config) def wait_for_completion(op_uri, timeout, allowed_states, complete_states, config): module = config.module client = config.client(get_region(module), "vpc", "project") def _refresh_status(): r = None try: r = fetch_resource(module, client, op_uri) except Exception: return None, "" status = "" try: status = navigate_value(r, ["vpc", "status"]) except Exception: return None, "" return r, status try: return wait_to_finish(complete_states, allowed_states, _refresh_status, timeout) except Exception as ex: module.fail_json(msg=str(ex)) def wait_for_delete(module, client, link): def _refresh_status(): try: client.get(link) except HwcClientException404: return True, "Done" except Exception: return None, "" return True, "Pending" timeout = 60 * int(module.params["timeouts"]["delete"].rstrip("m")) try: return wait_to_finish(["Done"], ["Pending"], _refresh_status, timeout) except Exception as ex: module.fail_json(msg=str(ex)) class VpcRoutesArray: def __init__(self, request, module): self.module = module if request: self.request = request else: self.request = [] def to_request(self): items = [] for item in self.request: items.append(self._request_for_item(item)) return items def from_response(self): items = [] for item in self.request: items.append(self._response_from_item(item)) return items def _request_for_item(self, item): return {"destination": item.get("destination"), "nexthop": item.get("next_hop")} def _response_from_item(self, item): return {"destination": item.get("destination"), "next_hop": item.get("nexthop")} if __name__ == "__main__": main()