Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/vpc Ansible module for VPCs #84

Merged
merged 19 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
run: |
cd /home/${USER}/.ansible/collections/ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
ansible-test units --docker default --python ${{ matrix.python-version }} --coverage -v
ansible-test coverage report --include */plugins/* --omit */utils.py* > coverage.txt
ansible-test coverage report --include */plugins/* --omit */utils.py,_fetch_url* > coverage.txt
- name: Code Coverage Check
run: |
cd /home/${USER}/.ansible/collections/ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
Expand Down
17 changes: 17 additions & 0 deletions plugins/module_utils/prism/subnets.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,20 @@ def _get_default_dhcp_spec(self):
"tftp_server_name": "",
}
)


# Helper functions


def get_subnet_uuid(config, module):
if "name" in config or "subnet_name" in config:
subnet = Subnet(module)
name = config.get("name") or config.get("subnet_name")
uuid = subnet.get_uuid(name)
if not uuid:
error = "Subnet {0} not found.".format(name)
return None, error
elif "uuid" in config or "subnet_uuid" in config:
uuid = config.get("uuid") or config.get("subnet_uuid")

return uuid, None
60 changes: 55 additions & 5 deletions plugins/module_utils/prism/vpcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,69 @@ def __init__(self, module):
resource_type = "/vpcs"
super(Vpc, self).__init__(module, resource_type=resource_type)
self.build_spec_methods = {
# TODO. This is a Map of
# ansible attirbute and corresponding API spec generation method
# Example: method name should start with _build_spec_<method_name>
# name: _build_spec_name
"name": self._build_spec_name,
"external_subnets": self._build_spec_external_subnet,
"routable_ips": self._build_spec_routable_ips,
"dns_servers": self._build_dns_servers,
}

def _get_default_spec(self):
return deepcopy(
{
# TODO: Default API spec
"api_version": "3.1.0",
"metadata": {"kind": "vpc", "categories": {}},
"spec": {
"name": None,
"resources": {
"common_domain_name_server_ip_list": [],
"external_subnet_list": [],
"externally_routable_prefix_list": [],
},
},
}
)

def _build_spec_name(self, payload, name):
payload["spec"]["name"] = name
return payload, None

def _build_spec_external_subnet(self, payload, subnets):
from .subnets import get_subnet_uuid

external_subnets = []
for subnet in subnets:
uuid, error = get_subnet_uuid(subnet, self.module)
if error:
return None, error
subnet_ref_spec = self._get_external_subnet_ref_spec(uuid)
external_subnets.append(subnet_ref_spec)

payload["spec"]["resources"]["external_subnet_list"] = external_subnets
return payload, None

def _build_spec_routable_ips(self, payload, ips):
routable_ips = []
for ip in ips:
routable_ip_ref_spec = self._get_routable_ip_spec(
ip["network_ip"], ip["network_prefix"]
)
routable_ips.append(routable_ip_ref_spec)

payload["spec"]["resources"]["externally_routable_prefix_list"] = routable_ips
return payload, None

def _build_dns_servers(self, payload, dns_servers):
payload["spec"]["resources"]["common_domain_name_server_ip_list"] = [
{"ip": i} for i in dns_servers
]
return payload, None

def _get_external_subnet_ref_spec(self, uuid):
return deepcopy({"external_subnet_reference": {"kind": "subnet", "uuid": uuid}})

def _get_routable_ip_spec(self, ip, prefix):
return deepcopy({"ip": ip, "prefix_length": prefix})


# Helper functions

Expand Down
230 changes: 230 additions & 0 deletions plugins/modules/ntnx_vpcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/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_vpcs
short_description: vpcs module which suports vpc CRUD operations
version_added: 1.0.0
description: 'Create, Update, Delete vpcs'
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 vpc
- If C(state) is set to C(present) then vpc is created.
- >-
If C(state) is set to C(absent) and if the vpc exists, then
vpc is removed.
choices:
- present
- absent
type: str
default: present
wait:
description: Wait for vpc CRUD operation to complete.
type: bool
required: false
default: True
name:
description: vpc Name
type: str
vpc_uuid:
description: vpc uuid
type: str
dns_servers:
description: List of DNS servers IPs
type: list
elements: str
routable_ips:
description: Address space within the VPC which can talk externally without NAT. These are in effect when No-NAT External subnet is used.
type: list
elements: dict
suboptions:
network_ip:
description: ip address
type: str
network_prefix:
description: Subnet ip address prefix length
type: int
external_subnets:
description: A subnet with external connectivity
type: list
elements: dict
suboptions:
subnet_uuid:
description: Subnet UUID
type: str
subnet_name:
description: Subnet Name
type: str
author:
- Prem Karat (@premkarat)
- Gevorg Khachatryan (@Gevorg-Khachatryan-97)
- Alaa Bishtawi (@alaa-bish)
- Dina AbuHijleh (@dina-abuhijleh)
"""

EXAMPLES = r"""
# TODO
"""

RETURN = r"""
# TODO
"""

from ..module_utils.base_module import BaseModule # noqa: E402
from ..module_utils.prism.tasks import Task # noqa: E402
from ..module_utils.prism.vpcs import Vpc # noqa: E402
from ..module_utils.utils import remove_param_with_none_value # noqa: E402


def get_module_spec():
external_subnets_spec = dict(
subnet_name=dict(type="str"), subnet_uuid=dict(type="str")
)
routable_ips_spec = dict(
network_ip=dict(type="str"), network_prefix=dict(type="int")
)
module_args = dict(
name=dict(type="str"),
vpc_uuid=dict(type="str"),
external_subnets=dict(
type="list",
elements="dict",
options=external_subnets_spec,
mutually_exclusive=[("subnet_name", "subnet_uuid")],
),
routable_ips=dict(type="list", elements="dict", options=routable_ips_spec),
dns_servers=dict(type="list", elements="str"),
)

return module_args


def create_vpc(module, result):
vpc = Vpc(module)
spec, error = vpc.get_spec()
if error:
result["error"] = error
module.fail_json(msg="Failed generating vpc spec", **result)

if module.check_mode:
result["response"] = spec
return

resp, status = vpc.create(spec)
if status["error"]:
result["error"] = status["error"]
result["response"] = resp
module.fail_json(msg="Failed creating vpc", **result)

vpc_uuid = resp["metadata"]["uuid"]
result["changed"] = True
result["response"] = resp
result["vpc_uuid"] = vpc_uuid
result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"]

if module.params.get("wait"):
wait_for_task_completion(module, result)
resp, tmp = vpc.read(vpc_uuid)
result["response"] = resp


def delete_vpc(module, result):
vpc_uuid = module.params["vpc_uuid"]
if not vpc_uuid:
result["error"] = "Missing parameter vpc_uuid in playbook"
module.fail_json(msg="Failed deleting vpc", **result)

vpc = Vpc(module)
resp, status = vpc.delete(vpc_uuid)
if status["error"]:
result["error"] = status["error"]
result["response"] = resp
module.fail_json(msg="Failed deleting vpc", **result)

result["changed"] = True
result["response"] = resp
result["vpc_uuid"] = vpc_uuid
result["task_uuid"] = resp["status"]["execution_context"]["task_uuid"]

if module.params.get("wait"):
wait_for_task_completion(module, result)


def wait_for_task_completion(module, result):
task = Task(module)
task_uuid = result["task_uuid"]
resp, status = task.wait_for_completion(task_uuid)
result["response"] = resp
if status["error"]:
result["error"] = status["error"]
result["response"] = resp
module.fail_json(msg="Failed creating vpc", **result)


def run_module():
module = BaseModule(
argument_spec=get_module_spec(),
supports_check_mode=True,
required_if=[
("state", "present", ("name",)),
("state", "absent", ("vpc_uuid",)),
],
)
remove_param_with_none_value(module.params)
result = {
"changed": False,
"error": None,
"response": None,
"vpc_uuid": None,
"task_uuid": None,
}
state = module.params["state"]
if state == "present":
create_vpc(module, result)
elif state == "absent":
delete_vpc(module, result)

module.exit_json(**result)


def main():
run_module()


if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions scripts/create_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,14 @@ def _get_default_spec(self):


def create_module(name):
with open("plugins/modules/ntnx_{0}s.py".format(name), "w") as f:
with open("plugins/modules/ntnx_{0}s.py".format(name), "wb") as f:
f.write(
module_content.replace("object", name.lower()).replace(
"Object", name.capitalize()
)
)

with open("plugins/module_utils/prism/{0}s.py".format(name), "w") as f:
with open("plugins/module_utils/prism/{0}s.py".format(name), "wb") as f:
f.write(
object_content.replace("object", name.lower()).replace(
"Object", name.capitalize()
Expand Down
Loading