From be348c78498521d20c8e9e21d5e78484314a8f17 Mon Sep 17 00:00:00 2001 From: Antonin Rykalsky Date: Wed, 20 Mar 2024 14:07:54 +0100 Subject: [PATCH] feat: metal virtual circuit --- Makefile | 3 + README.md | 2 + docs/modules/metal_virtual_circuit.md | 100 ++++ docs/modules/metal_virtual_circuit_info.md | 59 +++ plugins/module_utils/metal/api_routes.py | 50 +- plugins/module_utils/metal/metal_api.py | 23 +- plugins/modules/metal_virtual_circuit.py | 440 ++++++++++++++++++ plugins/modules/metal_virtual_circuit_info.py | 128 +++++ .../metal_virtual_circuit/tasks/main.yml | 199 ++++++++ 9 files changed, 1002 insertions(+), 2 deletions(-) create mode 100644 docs/modules/metal_virtual_circuit.md create mode 100644 docs/modules/metal_virtual_circuit_info.md create mode 100644 plugins/modules/metal_virtual_circuit.py create mode 100644 plugins/modules/metal_virtual_circuit_info.py create mode 100644 tests/integration/targets/metal_virtual_circuit/tasks/main.yml diff --git a/Makefile b/Makefile index e78a2f6..588de8d 100644 --- a/Makefile +++ b/Makefile @@ -74,4 +74,7 @@ endif ifneq ("${METAL_HARDWARE_RESERVATION_PROJECT_ID}", "") echo "metal_hardware_reservation_project_id: ${METAL_HARDWARE_RESERVATION_PROJECT_ID}" >> $(INTEGRATION_CONFIG) endif +ifneq ("${ANSIBLE_ACC_METAL_DEDICATED_CONNECTION_ID}", "") + echo "ansible_acc_metal_dedicated_connection_id: ${ANSIBLE_ACC_METAL_DEDICATED_CONNECTION_ID}" >> $(INTEGRATION_CONFIG) +endif diff --git a/README.md b/README.md index 0a1d001..e1d0bf8 100755 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Name | Description | [equinix.cloud.metal_project_ssh_key](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_project_ssh_key.md)|Manage a project ssh key in Equinix Metal| [equinix.cloud.metal_reserved_ip_block](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_reserved_ip_block.md)|Create/delete blocks of reserved IP addresses in a project.| [equinix.cloud.metal_ssh_key](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_ssh_key.md)|Manage personal SSH keys in Equinix Metal| +[equinix.cloud.metal_virtual_circuit](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_virtual_circuit.md)|Manage a Virtual Circuit in Equinix Metal| [equinix.cloud.metal_vlan](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_vlan.md)|Manage a VLAN resource in Equinix Metal| [equinix.cloud.metal_vrf](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_vrf.md)|Manage a VRF resource in Equinix Metal| @@ -60,6 +61,7 @@ Name | Description | [equinix.cloud.metal_project_ssh_key_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_project_ssh_key_info.md)|Gather project SSH keys.| [equinix.cloud.metal_reserved_ip_block_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_reserved_ip_block_info.md)|Gather list of reserved IP blocks| [equinix.cloud.metal_ssh_key_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_ssh_key_info.md)|Gather personal SSH keys| +[equinix.cloud.metal_virtual_circuit_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_virtual_circuit_info.md)|Gather information about Equinix Metal Virtual Circuits| [equinix.cloud.metal_vlan_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_vlan_info.md)|Gather VLANs.| [equinix.cloud.metal_vrf_info](https://github.com/equinix-labs/ansible-collection-equinix/blob/v0.5.0/docs/modules/metal_vrf_info.md)|Gather VRFs| diff --git a/docs/modules/metal_virtual_circuit.md b/docs/modules/metal_virtual_circuit.md new file mode 100644 index 0000000..4016e5a --- /dev/null +++ b/docs/modules/metal_virtual_circuit.md @@ -0,0 +1,100 @@ +# metal_virtual_circuit + +Manage a Virtual Circuit in Equinix Metal. You can use *id* or *name* to lookup the resource. If you want to create new resource, you must provide *name*. + + +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Examples + +```yaml +- name: create first VRF virtual circuit for test + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit: + connection_id: "52373d96-ac4e-496c-8721-f7ef18a01331" + port_id: "52373d96-ac4e-496c-8721-f7ef18a01331" + name: "test_virtual_circuit" + nni_vlan: 1056 + peer_asn: 66000 + project_id: "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6" + subnet: "192.168.151.126/31" + vrf: "029c4219-04b7-4992-9fef-29ea7e2378a5" + +``` + + + + + + + + + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `id` |
`str`
|
Optional
| UUID of the Virtual Circuit. | +| `name` |
`str`
|
Optional
| Name of the Virtual Circuit resource. **(Updatable)** | +| `connection_id` |
`str`
|
Optional
| UUID of Connection where the VC is scoped to. | +| `project_id` |
`str`
|
Optional
| UUID of the Project where the VC is scoped to. | +| `port_id` |
`str`
|
Optional
| UUID of the Connection Port where the VC is scoped to. | +| `nni_vlan` |
`int`
|
Optional
| Equinix Metal network-to-network VLAN ID. | +| `vlan_id` |
`str`
|
Optional
| UUID of the VLAN to associate. | +| `vnid` |
`str`
|
Optional
| VNID VLAN parameter, see the documentation for Equinix Fabric. | +| `description` |
`str`
|
Optional
| Description for the Virtual Circuit resource. | +| `tags` |
`str`
|
Optional
| Tags for the Virtual Circuit resource. | +| `speed` |
`str`
|
Optional
| Speed of the Virtual Circuit resource. | +| `vrf` |
`str`
|
Optional
| UUID of the VRF to associate. | +| `peer_asn` |
`int`
|
Optional
| The BGP ASN of the peer. The same ASN may be the used across several VCs, but it cannot be the same as the local_asn of the VRF. | +| `subnet` |
`str`
|
Optional
| A subnet from one of the IP blocks associated with the VRF that we will help create an IP reservation for. Can only be either a /30 or /31. For a /31 block, it will only have two IP addresses, which will be used for the metal_ip and customer_ip. For a /30 block, it will have four IP addresses, but the first and last IP addresses are not usable. We will default to the first usable IP address for the metal_ip. | +| `metal_ip` |
`str`
|
Optional
| The Metal IP address for the SVI (Switch Virtual Interface) of the VirtualCircuit. Will default to the first usable IP in the subnet. | +| `customer_ip` |
`str`
|
Optional
| The Customer IP address which the CSR switch will peer with. Will default to the other usable IP in the subnet. | +| `md5` |
`str`
|
Optional
| The password that can be set for the VRF BGP peer | +| `timeout` |
`int`
|
Optional
| Timeout in seconds for Virtual Circuit to get to "ready" state **(Default: `15`)** | + + + + + + +## Return Values + + + +### Sample Response for metal_virtual_circuit +```json +{ + "changed": false, + "customer_ip": "192.168.151.127", + "id": "84f35a2f-1e0c-43ee-bd94-87aec0c5ffec", + "metal_ip": "192.168.151.126", + "name": "test_virtual_circuit", + "nni_vlan": 1056, + "peer_asn": 66000, + "port": { + "href": "/metal/v1/connections/52373d96-ac4e-496c-8721-f7ef18a01331/ports/52373d96-ac4e-496c-8721-f7ef18a01331", + "id": "4632fb7b-b1cf-48bc-8f20-a69b0a91d326" + }, + "project": { + "href": "/metal/v1/projects/11e047e1-f51a-49c6-b5b2-1c7bfa4391e6", + "id": "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6" + }, + "project_id": "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6", + "status": "active", + "subnet": "192.168.151.126/31", + "tags": [], + "type": "vrf", + "vrf": { + "bill": false, + "href": "/metal/v1/vrfs/029c4219-04b7-4992-9fef-29ea7e2378a5", + "id": "029c4219-04b7-4992-9fef-29ea7e2378a5" + } +} +``` + + diff --git a/docs/modules/metal_virtual_circuit_info.md b/docs/modules/metal_virtual_circuit_info.md new file mode 100644 index 0000000..4bf467e --- /dev/null +++ b/docs/modules/metal_virtual_circuit_info.md @@ -0,0 +1,59 @@ +# metal_virtual_circuit_info + +Gather information about Equinix Metal Virtual Circuits + + +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Examples + +```yaml +- name: Gather information about all projects in an organization + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit_info: + organization_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7 + +``` + + + + + + + + + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `connection_id` |
`str`
|
Optional
| ID of the virtual circuit resource | +| `organization_id` |
`str`
|
Optional
| ID of the organisation to which the virtual circuit belongs | + + + + + + +## Return Values + + + +### Sample Response for resources +```json +{ + "backend_transfer_enabled": false, + "customdata": {}, + "description": "", + "id": "31d3ae8b-bd5a-41f3-a420-055211345cc7", + "name": "ansible-integration-test-project-csle6t2y-project2", + "organization_id": "70c2f878-9f32-452e-8c69-ab15480e1d99", + "payment_method_id": "845b45a3-c565-47e5-b9b6-a86204a73d29" +} +``` + + diff --git a/plugins/module_utils/metal/api_routes.py b/plugins/module_utils/metal/api_routes.py index e7525a7..6260fff 100644 --- a/plugins/module_utils/metal/api_routes.py +++ b/plugins/module_utils/metal/api_routes.py @@ -78,6 +78,12 @@ def get_routes(mpc): ("metal_plan", action.GET): spec_types.Specs( equinix_metal.PlansApi(mpc).find_plans_by_project, ), + ('metal_virtual_circuit', action.GET): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).get_virtual_circuit, + ), + ('metal_virtual_circuit_vrf', action.GET): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).get_virtual_circuit, + ), # LISTERS ('metal_project_device', action.LIST): spec_types.Specs( @@ -161,6 +167,18 @@ def get_routes(mpc): equinix_metal.PlansApi(mpc).find_plans, {'category': 'category', 'type': 'type', 'slug': 'slug', 'include': 'include', 'exclude': 'exclude'}, ), + ('metal_virtual_circuit', action.LIST): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).list_interconnection_virtual_circuits, + {'connection_id': 'connection_id'}, + ), + ('metal_virtual_circuit_vrf', action.LIST): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).list_interconnection_virtual_circuits, + {'connection_id': 'connection_id'}, + ), + ('metal_port_virtual_circuit', action.LIST): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).list_interconnection_port_virtual_circuits, + {'connection_id': 'connection_id', 'port_id': 'port_id'}, + ), # DELETERS ('metal_device', action.DELETE): spec_types.Specs( @@ -194,7 +212,12 @@ def get_routes(mpc): ('metal_bgp_session', action.DELETE): spec_types.Specs( equinix_metal.BGPApi(mpc).delete_bgp_session, ), - + ('metal_virtual_circuit', action.DELETE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).delete_virtual_circuit, + ), + ('metal_virtual_circuit_vrf', action.DELETE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).delete_virtual_circuit, + ), # CREATORS ('metal_device', action.CREATE): spec_types.Specs( @@ -284,6 +307,19 @@ def get_routes(mpc): {'id': 'project_id'}, equinix_metal.BgpConfigRequestInput, ), + ('metal_virtual_circuit', action.CREATE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).create_interconnection_port_virtual_circuit, + {'connection_id': 'connection_id', 'port_id': 'port_id'}, + equinix_metal.VlanVirtualCircuitCreateInput, + equinix_metal.VirtualCircuitCreateInput, + ), + ('metal_virtual_circuit_vrf', action.CREATE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).create_interconnection_port_virtual_circuit, + {'connection_id': 'connection_id', 'port_id': 'port_id'}, + equinix_metal.VrfVirtualCircuitCreateInput, + equinix_metal.VirtualCircuitCreateInput, + ), + # UPDATERS ('metal_device', action.UPDATE): spec_types.Specs( @@ -326,4 +362,16 @@ def get_routes(mpc): {}, equinix_metal.BGPSessionInput, ), + ('metal_virtual_circuit', action.UPDATE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).update_virtual_circuit, + {}, + equinix_metal.VlanVirtualCircuitUpdateInput, + equinix_metal.VirtualCircuitUpdateInput, + ), + ('metal_virtual_circuit_vrf', action.UPDATE): spec_types.Specs( + equinix_metal.InterconnectionsApi(mpc).update_virtual_circuit, + {}, + equinix_metal.VrfVirtualCircuitUpdateInput, + equinix_metal.VirtualCircuitUpdateInput, + ), } diff --git a/plugins/module_utils/metal/metal_api.py b/plugins/module_utils/metal/metal_api.py index bd5a2b4..8a82d23 100644 --- a/plugins/module_utils/metal/metal_api.py +++ b/plugins/module_utils/metal/metal_api.py @@ -131,7 +131,7 @@ def extract_ids_from_projects_hrefs(resource: dict): 'name': 'name', 'metro': 'metro', 'project_id': 'project.id', - 'description': 'description', + 'description': optional_str('description'), 'local_asn': 'local_asn', 'ip_ranges': 'ip_ranges', } @@ -152,6 +152,7 @@ def extract_ids_from_projects_hrefs(resource: dict): 'bgp_sessions', # metal_bgp_session 'sessions', # metal_bgp_session_info 'plans', + 'virtual_circuits', ] @@ -289,6 +290,23 @@ def private_ipv4_subnet_size(resource: dict): 'available_in_metros': 'available_in_metros', } +METAL_VIRTUAL_CIRCUIT_RESPONSE_ATTRIBUTE_MAP = { + 'id': 'id', + 'name': 'name', + 'customer_ip': 'customer_ip', + 'metal_ip': 'metal_ip', + 'nni_vlan': 'nni_vlan', + 'peer_asn': 'peer_asn', + 'port': 'port', + 'project': 'project', + 'status': 'status', + 'subnet': optional('subnet'), + 'tags': 'tags', + 'type': 'type', + 'vrf': 'vrf', + 'project_id': 'project.id', +} + def get_attribute_mapper(resource_type): """ @@ -309,6 +327,7 @@ def get_attribute_mapper(resource_type): bgp_resources = {'metal_bgp_session', 'metal_bgp_session_by_project'} project_bgp_config_resources = {'metal_project_bgp_config'} plan_resources = set(["metal_plan"]) + virtual_circuit_resources = set(["metal_virtual_circuit", "metal_virtual_circuit_vrf"]) if resource_type in device_resources: return METAL_DEVICE_RESPONSE_ATTRIBUTE_MAP elif resource_type in project_resources: @@ -341,6 +360,8 @@ def get_attribute_mapper(resource_type): return METAL_PROJECT_BGP_CONFIG_RESPONSE_ATTRIBUTE_MAP elif resource_type in plan_resources: return METAL_PLAN_RESPONSE_ATTRIBUTE_MAP + elif resource_type in virtual_circuit_resources: + return METAL_VIRTUAL_CIRCUIT_RESPONSE_ATTRIBUTE_MAP else: raise NotImplementedError("No mapper for resource type %s" % resource_type) diff --git a/plugins/modules/metal_virtual_circuit.py b/plugins/modules/metal_virtual_circuit.py new file mode 100644 index 0000000..5c012fd --- /dev/null +++ b/plugins/modules/metal_virtual_circuit.py @@ -0,0 +1,440 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from equinix_metal.exceptions import NotFoundException + +# 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 a Virtual Circuit in Equinix Metal. You can use *id* or *name* + to lookup the resource. If you want to create new resource, you must provide *name*. +module: metal_virtual_circuit +notes: [] +options: + connection_id: + description: + - UUID of Connection where the VC is scoped to. + required: false + type: str + customer_ip: + description: + - The Customer IP address which the CSR switch will peer with. + - Will default to the other usable IP in the subnet. + required: false + type: str + description: + description: + - Description for the Virtual Circuit resource. + required: false + type: str + id: + description: + - UUID of the Virtual Circuit. + required: false + type: str + md5: + description: + - The password that can be set for the VRF BGP peer + required: false + type: str + metal_ip: + description: + - The Metal IP address for the SVI (Switch Virtual Interface) of the VirtualCircuit. + - Will default to the first usable IP in the subnet. + required: false + type: str + name: + description: + - Name of the Virtual Circuit resource. + required: false + type: str + nni_vlan: + description: + - Equinix Metal network-to-network VLAN ID. + required: false + type: int + peer_asn: + description: + - The BGP ASN of the peer. + - The same ASN may be the used across several VCs, but it cannot be the same as + the local_asn of the VRF. + required: false + type: int + port_id: + description: + - UUID of the Connection Port where the VC is scoped to. + required: false + type: str + project_id: + description: + - UUID of the Project where the VC is scoped to. + required: false + type: str + speed: + description: + - Speed of the Virtual Circuit resource. + required: false + type: str + subnet: + description: + - A subnet from one of the IP blocks associated with the VRF that we will help + create an IP reservation for. + - Can only be either a /30 or /31. + - For a /31 block, it will only have two IP addresses, which will be used for + the metal_ip and customer_ip. + - For a /30 block, it will have four IP addresses, but the first and last IP addresses + are not usable. + - We will default to the first usable IP address for the metal_ip. + required: false + type: str + tags: + description: + - Tags for the Virtual Circuit resource. + required: false + type: str + timeout: + default: 15 + description: + - Timeout in seconds for Virtual Circuit to get to "ready" state + required: false + type: int + vlan_id: + description: + - UUID of the VLAN to associate. + required: false + type: str + vnid: + description: + - VNID VLAN parameter, see the documentation for Equinix Fabric. + required: false + type: str + vrf: + description: + - UUID of the VRF to associate. + required: false + type: str +requirements: null +short_description: Manage a Virtual Circuit in Equinix Metal +''' +EXAMPLES = ''' +- name: create first VRF virtual circuit for test + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit: + connection_id: 52373d96-ac4e-496c-8721-f7ef18a01331 + port_id: 52373d96-ac4e-496c-8721-f7ef18a01331 + name: test_virtual_circuit + nni_vlan: 1056 + peer_asn: 66000 + project_id: 11e047e1-f51a-49c6-b5b2-1c7bfa4391e6 + subnet: 192.168.151.126/31 + vrf: 029c4219-04b7-4992-9fef-29ea7e2378a5 +''' +RETURN = ''' +metal_virtual_circuit: + description: The module object + returned: always + sample: + - changed: false + customer_ip: 192.168.151.127 + id: 84f35a2f-1e0c-43ee-bd94-87aec0c5ffec + metal_ip: 192.168.151.126 + name: test_virtual_circuit + nni_vlan: 1056 + peer_asn: 66000 + port: + href: /metal/v1/connections/52373d96-ac4e-496c-8721-f7ef18a01331/ports/52373d96-ac4e-496c-8721-f7ef18a01331 + id: 4632fb7b-b1cf-48bc-8f20-a69b0a91d326 + project: + href: /metal/v1/projects/11e047e1-f51a-49c6-b5b2-1c7bfa4391e6 + id: 11e047e1-f51a-49c6-b5b2-1c7bfa4391e6 + project_id: 11e047e1-f51a-49c6-b5b2-1c7bfa4391e6 + status: active + subnet: 192.168.151.126/31 + tags: [] + type: vrf + vrf: + bill: false + href: /metal/v1/vrfs/029c4219-04b7-4992-9fef-29ea7e2378a5 + id: 029c4219-04b7-4992-9fef-29ea7e2378a5 + 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_spec = dict( + id=SpecField( + type=FieldType.string, + description=['UUID of the Virtual Circuit.'], + ), + name=SpecField( + type=FieldType.string, + description=['Name of the Virtual Circuit resource.'], + editable=True, + ), + connection_id=SpecField( + type=FieldType.string, + description=[ + 'UUID of Connection where the VC is scoped to.' + ], + ), + project_id=SpecField( + type=FieldType.string, + description=[ + 'UUID of the Project where the VC is scoped to.' + ], + ), + port_id=SpecField( + type=FieldType.string, + description=[ + 'UUID of the Connection Port where the VC is scoped to.' + ], + ), + nni_vlan=SpecField( + type=FieldType.integer, + description=[ + 'Equinix Metal network-to-network VLAN ID.' + ], + ), + vlan_id=SpecField( + type=FieldType.string, + description=[ + 'UUID of the VLAN to associate.' + ], + ), + vnid=SpecField( + type=FieldType.string, + description=[ + 'VNID VLAN parameter, see the documentation for Equinix Fabric.' + ], + ), + description=SpecField( + type=FieldType.string, + description=[ + 'Description for the Virtual Circuit resource.' + ], + ), + tags=SpecField( + type=FieldType.string, + description=[ + 'Tags for the Virtual Circuit resource.' + ], + ), + speed=SpecField( + type=FieldType.string, + description=[ + 'Speed of the Virtual Circuit resource.' + ], + ), + vrf=SpecField( + type=FieldType.string, + description=[ + 'UUID of the VRF to associate.' + ], + ), + peer_asn=SpecField( + type=FieldType.integer, + description=[ + 'The BGP ASN of the peer.', + 'The same ASN may be the used across several VCs, but it cannot be the same as the local_asn of the VRF.' + ], + ), + subnet=SpecField( + type=FieldType.string, + description=[ + 'A subnet from one of the IP blocks associated with the VRF that we will help create an IP reservation for.', + 'Can only be either a /30 or /31.', + 'For a /31 block, it will only have two IP addresses, which will be used for the metal_ip and customer_ip.', + 'For a /30 block, it will have four IP addresses, but the first and last IP addresses are not usable.', + 'We will default to the first usable IP address for the metal_ip.', + ], + ), + metal_ip=SpecField( + type=FieldType.string, + description=[ + 'The Metal IP address for the SVI (Switch Virtual Interface) of the VirtualCircuit.', + 'Will default to the first usable IP in the subnet.' + ], + ), + customer_ip=SpecField( + type=FieldType.string, + description=[ + 'The Customer IP address which the CSR switch will peer with.', + 'Will default to the other usable IP in the subnet.' + ], + ), + md5=SpecField( + type=FieldType.string, + description=[ + 'The password that can be set for the VRF BGP peer' + ], + ), + timeout=SpecField( + type=FieldType.integer, + description=[ + 'Timeout in seconds for Virtual Circuit to get to "ready" state' + ], + default=15, + ), +) + +specdoc_examples = [ + ''' +- name: create first VRF virtual circuit for test + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit: + connection_id: "52373d96-ac4e-496c-8721-f7ef18a01331" + port_id: "52373d96-ac4e-496c-8721-f7ef18a01331" + name: "test_virtual_circuit" + nni_vlan: 1056 + peer_asn: 66000 + project_id: "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6" + subnet: "192.168.151.126/31" + vrf: "029c4219-04b7-4992-9fef-29ea7e2378a5" +''', +] + +return_values = [ + { + "changed": False, + "customer_ip": "192.168.151.127", + "id": "84f35a2f-1e0c-43ee-bd94-87aec0c5ffec", + "metal_ip": "192.168.151.126", + "name": "test_virtual_circuit", + "nni_vlan": 1056, + "peer_asn": 66000, + "port": { + "href": "/metal/v1/connections/52373d96-ac4e-496c-8721-f7ef18a01331/ports/52373d96-ac4e-496c-8721-f7ef18a01331", + "id": "4632fb7b-b1cf-48bc-8f20-a69b0a91d326" + }, + "project": { + "href": "/metal/v1/projects/11e047e1-f51a-49c6-b5b2-1c7bfa4391e6", + "id": "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6" + }, + "project_id": "11e047e1-f51a-49c6-b5b2-1c7bfa4391e6", + "status": "active", + "subnet": "192.168.151.126/31", + "tags": [], + "type": "vrf", + "vrf": { + "bill": False, + "href": "/metal/v1/vrfs/029c4219-04b7-4992-9fef-29ea7e2378a5", + "id": "029c4219-04b7-4992-9fef-29ea7e2378a5" + } + } +] + +MUTABLE_ATTRIBUTES = [ + k for k, v in module_spec.items() if v.editable +] + +SPECDOC_META = getSpecDocMeta( + short_description='Manage a Virtual Circuit in Equinix Metal', + description=( + 'Manage a Virtual Circuit in Equinix Metal. ' + 'You can use *id* or *name* to lookup the resource. ' + 'If you want to create new resource, you must provide *name*.' + ), + examples=specdoc_examples, + options=module_spec, + return_values={ + "metal_virtual_circuit": SpecReturnValue( + description='The module object', + type=FieldType.dict, + sample=return_values, + ), + }, +) + + +def main(): + module = EquinixModule( + argument_spec=SPECDOC_META.ansible_spec, + ) + + state = module.params.get("state") + changed = False + module_route = 'metal_virtual_circuit_vrf' if module.params.get('vrf', False) else 'metal_virtual_circuit' + + try: + module.params_syntax_check() + fetched = None + if module.params.get("id"): + tolerate_not_found = state == "absent" + fetched = module.get_by_id(module_route, tolerate_not_found) + else: + fetched = module.get_one_from_list( + module_route, + ["name"], + ) + + except NotFoundException as e: + pass + + except Exception as e: + tb = traceback.format_exc() + module.fail_json(msg="Error in metal_virtual_circuit: {0}".format(to_native(e)), + exception=tb) + + try: + if fetched: + module.params['id'] = fetched['id'] + if state == "present": + diff = get_diff(module.params, fetched, MUTABLE_ATTRIBUTES) + if diff: + fetched = module.update_by_id(diff, module_route) + changed = True + + else: + module.delete_by_id(module_route) + + module.wait_for_resource_removal( + module_route, + timeout=module.params.get("timeout"), + ) + changed = True + else: + if state == "present": + fetched = module.create(module_route) + 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="Error in metal_virtual_circuit: {0}".format(to_native(e)), + exception=tb) + + fetched.update({'changed': changed}) + module.exit_json(**fetched) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/metal_virtual_circuit_info.py b/plugins/modules/metal_virtual_circuit_info.py new file mode 100644 index 0000000..90e5cab --- /dev/null +++ b/plugins/modules/metal_virtual_circuit_info.py @@ -0,0 +1,128 @@ +#!/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 METAL_PROJECT_ARGS are generated by +# ansible_specdoc. Do not edit them directly. + +DOCUMENTATION = ''' +author: Equinix DevRel Team (@equinix) +description: Gather information about Equinix Metal Virtual Circuits +module: metal_virtual_circuit_info +notes: [] +options: + connection_id: + description: + - ID of the virtual circuit resource + required: false + type: str + organization_id: + description: + - ID of the organisation to which the virtual circuit belongs + required: false + type: str +requirements: null +short_description: Gather information about Equinix Metal Virtual Circuits +''' +EXAMPLES = ''' +- name: Gather information about all projects in an organization + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit_info: + organization_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7 +''' +RETURN = ''' +resources: + description: Found resources + returned: always + sample: + - backend_transfer_enabled: false + customdata: {} + description: '' + id: 31d3ae8b-bd5a-41f3-a420-055211345cc7 + name: ansible-integration-test-project-csle6t2y-project2 + organization_id: 70c2f878-9f32-452e-8c69-ab15480e1d99 + payment_method_id: 845b45a3-c565-47e5-b9b6-a86204a73d29 + 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( + connection_id=SpecField( + type=FieldType.string, + description=['ID of the virtual circuit resource'], + ), + organization_id=SpecField( + type=FieldType.string, + description=["ID of the organisation to which the virtual circuit belongs"], + ), +) + +specdoc_examples = [''' +- name: Gather information about all projects in an organization + hosts: localhost + tasks: + - equinix.cloud.metal_virtual_circuit_info: + organization_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7 +'''] + +return_values = [ + { + "backend_transfer_enabled": False, + "customdata": {}, + "description": "", + "id": "31d3ae8b-bd5a-41f3-a420-055211345cc7", + "name": "ansible-integration-test-project-csle6t2y-project2", + "organization_id": "70c2f878-9f32-452e-8c69-ab15480e1d99", + "payment_method_id": "845b45a3-c565-47e5-b9b6-a86204a73d29" + } +] + +SPECDOC_META = getSpecDocMeta( + short_description="Gather information about Equinix Metal Virtual Circuits", + description=( + 'Gather information about Equinix Metal Virtual Circuits' + ), + examples=specdoc_examples, + options=module_spec, + return_values={ + "resources": SpecReturnValue( + description='Found resources', + type=FieldType.dict, + sample=return_values, + ), + }, +) + + +def main(): + module = EquinixModule( + argument_spec=SPECDOC_META.ansible_spec, + is_info=True, + ) + try: + module.params_syntax_check() + if module.params.get('connection_id'): + return_value = {'resources': module.get_list("metal_virtual_circuit")} + else: + module.fail_json(msg="missing connection_id parameter") + + 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_virtual_circuit/tasks/main.yml b/tests/integration/targets/metal_virtual_circuit/tasks/main.yml new file mode 100644 index 0000000..add809f --- /dev/null +++ b/tests/integration/targets/metal_virtual_circuit/tasks/main.yml @@ -0,0 +1,199 @@ +# https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/equinix_metal_virtual_circuit +- name: metal_virtual_circuit + module_defaults: + equinix.cloud.metal_virtual_circuit: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_virtual_circuit_info: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_project: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_connection: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_project_info: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_vrf: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_vrf_info: + metal_api_token: '{{ metal_api_token }}' + equinix.cloud.metal_organization_info: + metal_api_token: '{{ metal_api_token }}' + block: + - set_fact: + test_resource_name_prefix: 'ansible-integration-test-virtualcircuit' + - set_fact: + unique_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=8') }}" + - set_fact: + test_prefix: "{{ test_resource_name_prefix }}-{{ unique_id }}" + - set_fact: + test_circuit_name: "test_virtual_circuit" + test_nni_vlan: 1056 + test_peer_asn: 66000 + test_subnet: "192.168.151.126/31" + + + - name: create project for test + equinix.cloud.metal_project: + name: "{{ test_prefix }}-project" + backend_transfer_enabled: true + register: test_project + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: get connection + equinix.cloud.metal_connection: + id: "{{ ansible_acc_metal_dedicated_connection_id }}" + organization_id: "{{ test_project.organization_id }}" + register: test_connection + when: ansible_acc_metal_dedicated_connection_id is defined + + - debug: + var: test_connection + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: create test VRF + equinix.cloud.metal_vrf: + name: "{{ test_prefix }}" + project_id: "{{ test_project.id }}" + metro: "{{ test_connection.metro.code }}" + description: "Test VRF with ASN 65000" + local_asn: 65000 + ip_ranges: + - "192.168.151.0/25" + - "192.168.222.0/25" + register: test_vrf + when: ansible_acc_metal_dedicated_connection_id is defined + + - debug: + var: test_vrf + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: Extract the port ID with status 'active' from test_connection + set_fact: + test_active_ports: "{{ test_connection.ports | selectattr('status', 'equalto', 'active') }}" + when: ansible_acc_metal_dedicated_connection_id is defined + + - debug: + var: test_active_ports + when: ansible_acc_metal_dedicated_connection_id is defined + + - fail: + msg: "There is no port with status=active" + when: + - ansible_acc_metal_dedicated_connection_id is defined + - test_active_ports | length == 0 + + - name: create first VRF virtual circuit for test + equinix.cloud.metal_virtual_circuit: + connection_id: "{{ ansible_acc_metal_dedicated_connection_id }}" + port_id: "{{ test_active_ports[0].id }}" + name: "{{ test_circuit_name }}" + nni_vlan: "{{ test_nni_vlan }}" + peer_asn: "{{ test_peer_asn }}" + project_id: "{{ test_project.id }}" + subnet: "{{ test_subnet }}" + vrf: "{{ test_vrf.id }}" + register: first_virtual_circuit + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: create first VRF virtual circuit for test (idempotence test) + equinix.cloud.metal_virtual_circuit: + connection_id: "{{ ansible_acc_metal_dedicated_connection_id }}" + port_id: "{{ test_active_ports[0].id }}" + name: "{{ test_circuit_name }}" + nni_vlan: "{{ test_nni_vlan }}" + peer_asn: "{{ test_peer_asn }}" + project_id: "{{ test_project.id }}" + subnet: "{{ test_subnet }}" + vrf: "{{ test_vrf.id }}" + register: first_virtual_circuit_idempotence + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: create first VRF virtual circuit for test (edit) + equinix.cloud.metal_virtual_circuit: + id: "{{ first_virtual_circuit.id }}" + name: "{{ test_circuit_name }}-changed" + register: first_virtual_circuit_changed + when: ansible_acc_metal_dedicated_connection_id is defined + + - set_fact: + expected_change_name: "{{ test_circuit_name }}-changed" + + - name: assert virtual circuits + assert: + that: + - "first_virtual_circuit.changed == true" + - "first_virtual_circuit_idempotence.changed == false" + - "first_virtual_circuit_changed.changed == true" + - "first_virtual_circuit_changed.name == expected_change_name" + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: fetch virtual circuit + equinix.cloud.metal_virtual_circuit: + id: "{{ first_virtual_circuit.id }}" + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: fetch virtual circuit by name + equinix.cloud.metal_virtual_circuit: + name: "{{ expected_change_name }}" + connection_id: "{{ ansible_acc_metal_dedicated_connection_id }}" + when: ansible_acc_metal_dedicated_connection_id is defined + + - debug: + var: first_virtual_circuit + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: list test VCs + equinix.cloud.metal_virtual_circuit_info: + connection_id: "{{ ansible_acc_metal_dedicated_connection_id }}" + register: test_circuits_listed + when: ansible_acc_metal_dedicated_connection_id is defined + + - debug: + var: test_circuits_listed + when: ansible_acc_metal_dedicated_connection_id is defined + + always: + - name: Announce teardown start + debug: + msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" + + - name: list test VCs + equinix.cloud.metal_virtual_circuit_info: + connection_id: "{{ ansible_acc_metal_dedicated_connection_id }}" + register: test_circuits_listed + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: delete test VCs + equinix.cloud.metal_virtual_circuit: + id: "{{ item.id }}" + state: absent + loop: "{{ test_circuits_listed.resources }}" + ignore_errors: yes + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: get VRF info + equinix.cloud.metal_vrf_info: + project_id: "{{ test_project.id }}" + register: vrf_list + + - debug: + var: vrf_list + + - name: delete test VRF + equinix.cloud.metal_vrf: + id: "{{ item.id }}" + state: absent + loop: "{{ vrf_list.resources }}" + + - name: list test projects + equinix.cloud.metal_project_info: + name: "{{ test_prefix }}" + register: test_projects_listed + when: ansible_acc_metal_dedicated_connection_id is defined + + - name: delete test projects + equinix.cloud.metal_project: + id: "{{ item.id }}" + state: absent + loop: "{{ test_projects_listed.resources }}" + ignore_errors: yes + when: ansible_acc_metal_dedicated_connection_id is defined