diff --git a/README.md b/README.md index c3ca647..39c586b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Name | Description | [equinix.cloud.metal_project](./docs/modules/metal_project.md)|Manage Projects in Equinix Metal| [equinix.cloud.metal_reserved_ip_block](./docs/modules/metal_reserved_ip_block.md)|Create/delete blocks of reserved IP addresses in a project.| [equinix.cloud.metal_ssh_key](./docs/modules/metal_ssh_key.md)|Manage personal SSH keys in Equinix Metal| +[equinix.cloud.metal_vlan](./docs/modules/metal_vlan.md)|Manage a VLAN resource in Equinix Metal| ### Info Modules @@ -46,6 +47,7 @@ Name | Description | [equinix.cloud.metal_project_info](./docs/modules/metal_project_info.md)|Gather information about Equinix Metal projects| [equinix.cloud.metal_reserved_ip_block_info](./docs/modules/metal_reserved_ip_block_info.md)|Gather list of reserved IP blocks| [equinix.cloud.metal_ssh_key_info](./docs/modules/metal_ssh_key_info.md)|Gather personal SSH keys| +[equinix.cloud.metal_vlan_info](./docs/modules/metal_vlan_info.md)|Gather VLANs.| ### Inventory Plugins @@ -142,4 +144,4 @@ Verify that new version of [equinix.cloud](https://galaxy.ansible.com/equinix/cl GNU General Public License v3.0. -See [COPYING](COPYING) to see the full text. +See [COPYING](COPYING) to see the full text. \ No newline at end of file diff --git a/docs/modules/metal_organization.md b/docs/modules/metal_organization.md index a0ca13d..66ee6fe 100644 --- a/docs/modules/metal_organization.md +++ b/docs/modules/metal_organization.md @@ -2,7 +2,7 @@ Lookup a single organization by ID in Equinix Metal. -This resource only fetches a single organization resource ID. +This resource only fetches a single organization by resource ID. It doesn't allow to create or update organizations. diff --git a/docs/modules/metal_vlan.md b/docs/modules/metal_vlan.md new file mode 100644 index 0000000..08f3853 --- /dev/null +++ b/docs/modules/metal_vlan.md @@ -0,0 +1,66 @@ +# metal_vlan + +Manage the VLAN in Equinix Metal. You can use *id* or *vxlan* to lookup the resource. If you want to create new resource, you must provide *metro*. + + +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Examples + +```yaml +- name: Create new VLAN + hosts: localhost + tasks: + - equinix.cloud.metal_vlan: + description: "This is my new VLAN." + metro: "se" + vxlan: 1234 + project_id: "778h50f7-75b6-4271-bc64-632b80f87de2" + +``` + + + + + + + + + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `id` |
`str`
|
Optional
| ID of parent project" | +| `project_id` |
`str`
|
Optional
| ID of parent project" | +| `description` |
`str`
|
Optional
| Description of the VLAN **(Updatable)** | +| `metro` |
`str`
|
Optional
| Metro in which to create the VLAN **(Updatable)** | +| `vxlan` |
`int`
|
Optional
| VLAN ID, must be unique in metro **(Updatable)** | + + + + + + +## Return Values + +- `metal_vlan` - The module object + + - Sample Response: + ```json + + { + "changed": false, + "id": "7624f0f7-75b6-4271-bc64-632b80f87de2", + "description": "This is my new VLAN.", + "metro": "se", + "vxlan": 1234, + "project_id": "778h50f7-75b6-4271-bc64-632b80f87de2" + } + + ``` + + diff --git a/docs/modules/metal_vlan_info.md b/docs/modules/metal_vlan_info.md new file mode 100644 index 0000000..7be8dca --- /dev/null +++ b/docs/modules/metal_vlan_info.md @@ -0,0 +1,56 @@ +# metal_vlan_info + +Gather information about Equinix Metal VLAN resources + + +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Examples + +```yaml +- name: list vlans + equinix.cloud.metal_vlan_info: + register: listed_vlan + +``` + + + + + + + + + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `project_id` |
`str`
|
Optional
| Filter vlans by Project UUID. | + + + + + + +## Return Values + +- `resources` - Found resources + + - Sample Response: + ```json + + [ + { + "vxlan": 1234, + "metro": "se", + "id": "845b45a3-c565-47e5-b9b6-a86204a73d29", + "description": "My VLAN." + } + ] + ``` + + diff --git a/plugins/module_utils/metal/api_routes.py b/plugins/module_utils/metal/api_routes.py index 0a64b62..1b1d1cb 100644 --- a/plugins/module_utils/metal/api_routes.py +++ b/plugins/module_utils/metal/api_routes.py @@ -56,6 +56,9 @@ def get_routes(mpc): ("metal_organization", action.GET): spec_types.Specs( equinix_metal.OrganizationsApi(mpc).find_organization_by_id, ), + ("metal_vlan", action.GET): spec_types.Specs( + equinix_metal.VLANsApi(mpc).get_virtual_network, + ), # LISTERS ('metal_project_device', action.LIST): spec_types.Specs( @@ -102,6 +105,10 @@ def get_routes(mpc): equinix_metal.HardwareReservationsApi(mpc).find_project_hardware_reservations, {'id': 'project_id'}, ), + ('metal_vlan', action.LIST): spec_types.Specs( + equinix_metal.VLANsApi(mpc).find_virtual_networks, + {'id': 'project_id'}, + ), # DELETERS ('metal_device', action.DELETE): spec_types.Specs( @@ -119,6 +126,9 @@ def get_routes(mpc): ('metal_ssh_key', action.DELETE): spec_types.Specs( equinix_metal.SSHKeysApi(mpc).delete_ssh_key, ), + ('metal_vlan', action.DELETE): spec_types.Specs( + equinix_metal.VLANsApi(mpc).delete_virtual_network, + ), # CREATORS @@ -152,6 +162,11 @@ def get_routes(mpc): {}, equinix_metal.SSHKeyCreateInput, ), + ('metal_vlan', action.CREATE): spec_types.Specs( + equinix_metal.VLANsApi(mpc).create_virtual_network, + {'id': 'project_id'}, + equinix_metal.VirtualNetworkCreateInput, + ), # UPDATERS ('metal_device', action.UPDATE): spec_types.Specs( diff --git a/plugins/module_utils/metal/metal_api.py b/plugins/module_utils/metal/metal_api.py index 2e4bb0a..c43b296 100644 --- a/plugins/module_utils/metal/metal_api.py +++ b/plugins/module_utils/metal/metal_api.py @@ -137,6 +137,7 @@ def extract_ids_from_projects_hrefs(resource: dict): 'operating_systems', 'hardware_reservations', 'organizations', + 'virtual_networks' ] @@ -191,6 +192,14 @@ def get_assignment_address(resource: dict): } +VLAN_RESPONSE_ATTRIBUTE_MAP = { + "id": "id", + "description": optional_str('description'), + "metro": "metro", + "vxlan": "vxlan", +} + + def get_attribute_mapper(resource_type): """ Returns attribute mapper for the given resource type. @@ -201,6 +210,7 @@ def get_attribute_mapper(resource_type): ip_assignment_resources = set(['metal_ip_assignment']) ssh_key_resources = set(['metal_ssh_key', 'metal_project_ssh_key']) hardware_reservation_resources = set(['metal_project_hardware_reservation', 'metal_hardware_reservation']) + vlan_resources = set(["metal_vlan"]) if resource_type in device_resources: return METAL_DEVICE_RESPONSE_ATTRIBUTE_MAP elif resource_type in project_resources: @@ -219,6 +229,8 @@ def get_attribute_mapper(resource_type): return METAL_HARDWARE_RESERVATION_RESPONSE_ATTRIBUTE_MAP elif resource_type == 'metal_organization': return METAL_ORGANIZATION_RESPONSE_ATTRIBUTE_MAP + elif resource_type in vlan_resources: + return VLAN_RESPONSE_ATTRIBUTE_MAP else: raise NotImplementedError("No mapper for resource type %s" % resource_type) @@ -235,7 +247,7 @@ def call(resource_type, action, equinix_metal_client, params={}): call = api_routes.build_api_call(conf, params) response = call.do() # uncomment to check response in /tmp/q - #import q; q(response) + # import q; q(response) if action == action.DELETE: return None attribute_mapper = get_attribute_mapper(resource_type) diff --git a/plugins/modules/metal_organization.py b/plugins/modules/metal_organization.py index 609e7a5..b72d5dc 100644 --- a/plugins/modules/metal_organization.py +++ b/plugins/modules/metal_organization.py @@ -10,7 +10,7 @@ author: Equinix DevRel Team (@equinix) description: !!python/tuple - 'Lookup a single organization by ID in Equinix Metal. ' -- 'This resource only fetches a single organization resource ID. ' +- 'This resource only fetches a single organization by resource ID. ' - It doesn't allow to create or update organizations. module: metal_organization notes: [] diff --git a/plugins/modules/metal_vlan.py b/plugins/modules/metal_vlan.py new file mode 100644 index 0000000..852cefc --- /dev/null +++ b/plugins/modules/metal_vlan.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# DOCUMENTATION, EXAMPLES, and RETURN are generated by +# ansible_specdoc. Do not edit them directly. + +DOCUMENTATION = ''' +author: Equinix DevRel Team (@equinix) +description: Manage the VLAN in Equinix Metal. You can use *id* or *vxlan* to lookup + the resource. If you want to create new resource, you must provide *metro*. +module: metal_vlan +notes: [] +options: + description: + description: + - Description of the VLAN + required: false + type: str + id: + description: + - ID of parent project + required: false + type: str + metro: + description: + - Metro in which to create the VLAN + required: false + type: str + project_id: + description: + - ID of parent project + required: false + type: str + vxlan: + description: + - VLAN ID, must be unique in metro + required: false + type: int +requirements: null +short_description: Manage a VLAN resource in Equinix Metal +''' +EXAMPLES = ''' +- name: Create new VLAN + hosts: localhost + tasks: + - equinix.cloud.metal_vlan: + description: This is my new VLAN. + metro: se + vxlan: 1234 + project_id: 778h50f7-75b6-4271-bc64-632b80f87de2 +''' +RETURN = ''' +metal_vlan: + description: The module object + returned: always + sample: + - "\n{\n \"changed\": false,\n \"id\": \"7624f0f7-75b6-4271-bc64-632b80f87de2\"\ + ,\n \"description\": \"This is my new VLAN.\",\n \"metro\": \"se\",\n \"vxlan\"\ + : 1234,\n \"project_id\": \"778h50f7-75b6-4271-bc64-632b80f87de2\"\n}\n" + type: dict +''' + +# End of generated documentation + +# This is a template for a new module. It is not meant to be used as is. +# It is meant to be copied and modified to create a new module. +# Replace all occurrences of "metal_resource" with the name of the new +# module, for example "metal_vlan". + + +from ansible.module_utils._text import to_native +from ansible_specdoc.objects import ( + SpecField, + FieldType, + SpecReturnValue, +) +import traceback + +from ansible_collections.equinix.cloud.plugins.module_utils.equinix import ( + EquinixModule, + get_diff, + getSpecDocMeta, +) + +MODULE_NAME = "metal_vlan" + +module_spec = dict( + id=SpecField( + type=FieldType.string, + description=['UUID of vlan"'], + ), + project_id=SpecField( + type=FieldType.string, + description=['ID of parent project"'], + ), + description=SpecField( + type=FieldType.string, + description=["Description of the VLAN"], + ), + metro=SpecField( + type=FieldType.string, + description=["Metro in which to create the VLAN"], + ), + vxlan=SpecField( + type=FieldType.integer, + description=["VLAN ID, must be unique in metro"], + ), +) + + +specdoc_examples = [ + """ +- name: Create new VLAN + hosts: localhost + tasks: + - equinix.cloud.metal_vlan: + description: "This is my new VLAN." + metro: "se" + vxlan: 1234 + project_id: "778h50f7-75b6-4271-bc64-632b80f87de2" +""", +] + +result_sample = [ + """ +{ + "changed": false, + "id": "7624f0f7-75b6-4271-bc64-632b80f87de2", + "description": "This is my new VLAN.", + "metro": "se", + "vxlan": 1234, + "project_id": "778h50f7-75b6-4271-bc64-632b80f87de2" +} +""" +] + +MUTABLE_ATTRIBUTES = [k for k, v in module_spec.items() if v.editable] + +SPECDOC_META = getSpecDocMeta( + short_description="Manage a VLAN resource in Equinix Metal", + description=( + "Manage the VLAN in Equinix Metal. " + "You can use *id* or *vxlan* to lookup the resource. " + "If you want to create new resource, you must provide *metro*." + ), + examples=specdoc_examples, + options=module_spec, + return_values={ + "metal_vlan": SpecReturnValue( + description="The module object", + type=FieldType.dict, + sample=result_sample, + ), + }, +) + + +def main(): + module = EquinixModule( + argument_spec=SPECDOC_META.ansible_spec, + ) + + state = module.params.get("state") + changed = False + try: + module.params_syntax_check() + if module.params.get("id"): + tolerate_not_found = state == "absent" + fetched = module.get_by_id(MODULE_NAME, tolerate_not_found) + else: + fetched = module.get_one_from_list( + MODULE_NAME, + ["vxlan"], + ) + + if fetched: + module.params["id"] = fetched["id"] + if state == "present": + diff = get_diff(module.params, fetched, MUTABLE_ATTRIBUTES) + if diff: + module.fail_json(msg="Resource metal_vlan is not mutable.") + + else: + module.delete_by_id(MODULE_NAME) + changed = True + else: + if state == "present": + fetched = module.create(MODULE_NAME) + if "id" not in fetched: + module.fail_json(msg="UUID not found in resource creation response") + changed = True + else: + fetched = {} + except Exception as e: + tb = traceback.format_exc() + module.fail_json(msg=f"Error in {MODULE_NAME}: {to_native(e)}", exception=tb) + + fetched.update({"changed": changed}) + module.exit_json(**fetched) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/metal_vlan_info.py b/plugins/modules/metal_vlan_info.py new file mode 100644 index 0000000..c911811 --- /dev/null +++ b/plugins/modules/metal_vlan_info.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# DOCUMENTATION, EXAMPLES, and RETURN are generated by +# ansible_specdoc. Do not edit them directly. + +DOCUMENTATION = ''' +author: Equinix DevRel Team (@equinix) +description: Gather information about Equinix Metal VLAN resources +module: metal_vlan_info +notes: [] +options: + project_id: + description: + - Filter vlans by Project UUID. + required: false + type: str +requirements: null +short_description: Gather VLANs. +''' +EXAMPLES = ''' +- name: list vlans + equinix.cloud.metal_vlan_info: null + register: listed_vlan +''' +RETURN = ''' +resources: + description: Found resources + returned: always + sample: + - "\n[\n {\n \"vxlan\": 1234,\n \"metro\": \"se\",\n \"id\": \"845b45a3-c565-47e5-b9b6-a86204a73d29\"\ + ,\n \"description\": \"My VLAN.\"\n }\n]" + type: dict +''' + +# End + +from ansible.module_utils._text import to_native +from ansible_specdoc.objects import SpecField, FieldType, SpecReturnValue +import traceback + +from ansible_collections.equinix.cloud.plugins.module_utils.equinix import ( + EquinixModule, + getSpecDocMeta, +) + +module_spec = dict( + project_id=SpecField( + type=FieldType.string, + description=['Filter vlans by Project UUID.'], + required=False, + ), +) + +specdoc_examples = [''' +- name: list vlans + equinix.cloud.metal_vlan_info: + register: listed_vlan +''', +] + +result_sample = [''' +[ + { + "vxlan": 1234, + "metro": "se", + "id": "845b45a3-c565-47e5-b9b6-a86204a73d29", + "description": "My VLAN" + } +]''', +] + +SPECDOC_META = getSpecDocMeta( + short_description="Gather VLANs.", + description=( + 'Gather information about Equinix Metal VLAN resources' + ), + examples=specdoc_examples, + options=module_spec, + return_values={ + "resources": SpecReturnValue( + description='Found resources', + type=FieldType.dict, + sample=result_sample, + ), + }, +) + + +def main(): + module = EquinixModule( + argument_spec=SPECDOC_META.ansible_spec, + is_info=True, + ) + try: + module.params_syntax_check() + return_value = {'resources': module.get_list("metal_vlan")} + except Exception as e: + tr = traceback.format_exc() + module.fail_json(msg=to_native(e), exception=tr) + module.exit_json(**return_value) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/metal_vlan/tasks/main.yml b/tests/integration/targets/metal_vlan/tasks/main.yml new file mode 100644 index 0000000..e894407 --- /dev/null +++ b/tests/integration/targets/metal_vlan/tasks/main.yml @@ -0,0 +1,87 @@ +- name: metal_vlan + module_defaults: + equinix.cloud.metal_vlan: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_vlan_info: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_project: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_project_info: + metal_api_token: '{{ metal_api_token }}' + metal_ua_prefix: '{{ metal_ua_prefix }}' + block: + - set_fact: + test_resource_name_prefix: 'ansible-integration-test-vlan' + - set_fact: + unique_id: "{{ lookup('community.general.random_string', upper=false, numbers=false, special=false) }}" + - set_fact: + test_prefix: "{{ test_resource_name_prefix }}-{{ unique_id }}" + - set_fact: + test_metro: 'am' + - set_fact: + test_description: 'My new VLAN' + - set_fact: + test_vxlan: 123 + + - name: create project for test + equinix.cloud.metal_project: + name: "{{ test_prefix }}-project" + register: project + + - assert: + that: + - project.name == '{{ test_prefix }}-project' + + - name: create first vlan for test + equinix.cloud.metal_vlan: + project_id: "{{ project.id }}" + metro: "{{ test_metro }}" + description: "{{ test_description }}" + vxlan: "{{ test_vxlan }}" + register: first_vlan + + - assert: + that: + - first_vlan.vxlan == {{ test_vxlan }} + + - name: get vlan info + equinix.cloud.metal_vlan_info: + project_id: "{{ project.id }}" + register: vlan_info_listed + + - assert: + that: + - "vlan_info_listed.resources | length == 1" + - vlan_info_listed.resources[0].id == "{{ first_vlan.id }}" + + - name: fetch existing vlan + equinix.cloud.metal_vlan: + id: "{{ first_vlan.id }}" + register: first_vlan_fetched + + - name: delete vlan + equinix.cloud.metal_vlan: + id: "{{ first_vlan.id }}" + state: absent + + - name: delete vlan again to check indempotence + equinix.cloud.metal_vlan: + id: "{{ first_vlan.id }}" + state: absent + + always: + - name: Announce teardown start + debug: + msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" + + - name: list test projects + equinix.cloud.metal_project_info: + name: "{{ test_prefix }}" + register: test_projects_listed + + - name: delete test projects + equinix.cloud.metal_project: + id: "{{ item.id }}" + state: absent + loop: "{{ test_projects_listed.resources }}" + ignore_errors: yes \ No newline at end of file