diff --git a/README.md b/README.md
index fd96545..61f9719 100755
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Name | Description |
--- | ------------ |
[equinix.cloud.metal_connection](./docs/modules/metal_connection.md)|Manage an Interconnection in Equinix Metal|
[equinix.cloud.metal_device](./docs/modules/metal_device.md)|Create, update, or delete Equinix Metal devices|
+[equinix.cloud.metal_gateway](./docs/modules/metal_gateway.md)|Manage Metal Gateway in Equinix Metal|
[equinix.cloud.metal_hardware_reservation](./docs/modules/metal_hardware_reservation.md)|Lookup a single hardware_reservation by ID in Equinix Metal|
[equinix.cloud.metal_ip_assignment](./docs/modules/metal_ip_assignment.md)|Manage Equinix Metal IP assignments|
[equinix.cloud.metal_organization](./docs/modules/metal_organization.md)|Lookup a single organization by ID in Equinix Metal|
@@ -45,6 +46,7 @@ Name | Description |
[equinix.cloud.metal_available_ips_info](./docs/modules/metal_available_ips_info.md)|Get list of avialable IP addresses from a reserved IP block|
[equinix.cloud.metal_connection_info](./docs/modules/metal_connection_info.md)|Gather information about Interconnections|
[equinix.cloud.metal_device_info](./docs/modules/metal_device_info.md)|Select list of Equinix Metal devices|
+[equinix.cloud.metal_gateway_info](./docs/modules/metal_gateway_info.md)|Gather information about Metal Gateways|
[equinix.cloud.metal_hardware_reservation_info](./docs/modules/metal_hardware_reservation_info.md)|Gather information about Equinix Metal hardware_reservations|
[equinix.cloud.metal_ip_assignment_info](./docs/modules/metal_ip_assignment_info.md)|Gather IP address assignments for a device|
[equinix.cloud.metal_metro_info](./docs/modules/metal_metro_info.md)|Gather information about Equinix Metal metros|
diff --git a/docs/modules/metal_gateway.md b/docs/modules/metal_gateway.md
new file mode 100644
index 0000000..5836d22
--- /dev/null
+++ b/docs/modules/metal_gateway.md
@@ -0,0 +1,112 @@
+# metal_gateway
+
+Manage Metal Gateway in Equinix Metal. You can use *id* or *ip_reservation_id* to lookup a Gateway. If you want to create new resource, you must provide *project_id*, *virtual_network_id* and either *ip_reservation_id* or *private_ipv4_subnet_size*.
+
+
+- [Examples](#examples)
+- [Parameters](#parameters)
+- [Return Values](#return-values)
+
+## Examples
+
+```yaml
+- name: Create new gateway with existing IP reservation
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: "a4cc87f9-e00f-48c2-9460-74aa60beb6b0"
+ ip_reservation_id: "83b5503c-7b7f-4883-9509-b6b728b41491"
+ virtual_network_id: "eef49903-7a09-4ca1-af67-4087c29ab5b6"
+
+```
+
+```yaml
+- name: Create new gateway with new private /29 subnet
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ private_ipv4_subnet_size: 8
+
+```
+
+```yaml
+- name: Lookup a gateway by ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ id: "eef49903-7a09-4ca1-af67-4087c29ab5b6"
+ register: gateway
+
+```
+
+```yaml
+- name: Lookup a gateway by IP reservation ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ ip_reservation_id: "a4cc87f9-e00f-48c2-9460-74aa60beb6b0"
+ register: gateway
+
+```
+
+
+
+
+
+
+
+
+
+
+## Parameters
+
+| Field | Type | Required | Description |
+|-----------|------|----------|------------------------------------------------------------------------------|
+| `id` |
`str` | Optional | UUID of the gateway. |
+| `project_id` | `str` | Optional | UUID of the project where the gateway is scoped to. |
+| `ip_reservation_id` | `str` | Optional | UUID of Public Reservation to associate with the gateway, the reservation must be in the same metro as the VLAN, conflicts with private_ipv4_subnet_size. |
+| `private_ipv4_subnet_size` | `int` | Optional | Size of the private IPv4 subnet to create for this metal gateway, must be one of 8, 16, 32, 64, 128. Conflicts with ip_reservation_id. |
+| `virtual_network_id` | `str` | Optional | UUID of the VLAN where the gateway is scoped to. |
+| `timeout` | `int` | Optional | Timeout in seconds for gateway to get to "ready" state, and for gateway to be removed **(Default: `10`)** |
+
+
+
+
+
+
+## Return Values
+
+- `metal_gateway` - The module object
+
+ - Sample Response:
+ ```json
+
+ {
+
+ "changed": true,
+ "id": "1f4d30da-4041-406d-8d94-6ce929340d98",
+ "ip_reservation_id": "fa017281-b10e-4b22-b449-35a93fb88d85",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": null,
+ "project_id": "2e85a66a-ea6a-4e33-8029-cc5ab9a0bc91",
+ "virtual_network_id": "4a06c542-e47c-4e3c-ab85-bfc3cba4004d"
+ }
+
+ ```
+ ```json
+
+ {
+ "changed": true,
+ "id": "be809e36-42a0-4a3b-982c-8f4487b9b9fc",
+ "ip_reservation_id": "e5c4be29-e238-431a-8c5f-f44f30fd5098",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "0491c16b-376d-4842-89d2-da3efead4991",
+ "virtual_network_id": "f46ab2c8-1332-4f87-91e9-f3a6a81d9769"
+ }
+
+ ```
+
+
diff --git a/docs/modules/metal_gateway_info.md b/docs/modules/metal_gateway_info.md
new file mode 100644
index 0000000..598fe34
--- /dev/null
+++ b/docs/modules/metal_gateway_info.md
@@ -0,0 +1,68 @@
+# metal_gateway_info
+
+Gather information about Metal Gateways
+
+
+- [Examples](#examples)
+- [Parameters](#parameters)
+- [Return Values](#return-values)
+
+## Examples
+
+```yaml
+- name: Gather information about all gateways in a project
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway_info:
+ project_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7
+
+```
+
+
+
+
+
+
+
+
+
+
+## Parameters
+
+| Field | Type | Required | Description |
+|-----------|------|----------|------------------------------------------------------------------------------|
+| `project_id` | `str` | Optional | UUID of parent project the gateway is scoped to. |
+
+
+
+
+
+
+## Return Values
+
+- `resources` - Found Metal Gateways
+
+ - Sample Response:
+ ```json
+
+ [
+ {
+ "id": "771c9418-7c60-4a45-8fa6-3a002132331d",
+ "ip_reservation_id": "d45c9629-3aab-4a7b-af5d-4ca50041e311",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "f7a35065-2e41-4747-b3d1-400af0a3e0e8",
+ "virtual_network_id": "898972b3-7eb9-4ca2-b803-7b5d339bbea7"
+ },
+ {
+ "id": "b66eb02d-c4bb-4ae8-a22e-0f7934da971e",
+ "ip_reservation_id": "6282982a-e6de-4f4d-b230-2ae27e90778c",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "f7a35065-2e41-4747-b3d1-400af0a3e0e8",
+ "virtual_network_id": "898972b3-7eb9-4ca2-b803-7b5d339bbea7"
+ }
+ ]
+ ```
+
+
diff --git a/plugins/module_utils/equinix.py b/plugins/module_utils/equinix.py
index 9f7f370..709113d 100644
--- a/plugins/module_utils/equinix.py
+++ b/plugins/module_utils/equinix.py
@@ -171,11 +171,16 @@ def update_by_id(self, update_dict: dict, resource_type: str):
update_dict['id'] = specified_id
return self._metal_api_call(resource_type, action.UPDATE, update_dict)
- def wait_for_resource_condition(self, resource_type: str,
- attribute: str, target_value: str, timeout: int):
+ def _get_id_safe(self):
specified_id = self.params.get('id')
if specified_id is None:
- raise Exception('no id in module when waiting for condition, this is a module bug')
+ raise Exception('no id in module when about to poll for a condition, this is a module bug')
+ return specified_id
+
+
+ def wait_for_resource_condition(self, resource_type: str,
+ attribute: str, target_value: str, timeout: int):
+ specified_id = self._get_id_safe()
stop_time = time.time() + timeout
while time.time() < stop_time:
result = self._metal_api_call(resource_type, action.GET, self.params.copy())
@@ -184,6 +189,17 @@ def wait_for_resource_condition(self, resource_type: str,
time.sleep(5)
raise Exception(f'wait for {resource_type} {specified_id} {attribute} {target_value} timed out')
+ def wait_for_resource_removal(self, resource_type: str, timeout: int):
+ specified_id = self._get_id_safe()
+ stop_time = time.time() + timeout
+ while time.time() < stop_time:
+ try:
+ self._metal_api_call(resource_type, action.GET, self.params.copy())
+ except metal_client.NotFoundException:
+ return
+ time.sleep(5)
+ raise Exception(f'wait for {resource_type} {specified_id} removal timed out')
+
def get_hardware_reservation(self):
params = {'id': self.params['hardware_reservation_id']}
return self._metal_api_call('metal_hardware_reservation', action.GET, params)
diff --git a/plugins/module_utils/metal/api_routes.py b/plugins/module_utils/metal/api_routes.py
index 4fbdae2..172b7c1 100644
--- a/plugins/module_utils/metal/api_routes.py
+++ b/plugins/module_utils/metal/api_routes.py
@@ -65,6 +65,10 @@ def get_routes(mpc):
('metal_vrf', action.GET): spec_types.Specs(
equinix_metal.VRFsApi(mpc).find_vrf_by_id,
),
+ ("metal_gateway", action.GET): spec_types.Specs(
+ equinix_metal.MetalGatewaysApi(mpc).find_metal_gateway_by_id,
+ extra_kwargs={"include": ["ip_reservation"]},
+ ),
# LISTERS
('metal_project_device', action.LIST): spec_types.Specs(
@@ -131,6 +135,11 @@ def get_routes(mpc):
equinix_metal.VRFsApi(mpc).find_vrfs,
{'id': 'project_id'},
),
+ ('metal_gateway', action.LIST): spec_types.Specs(
+ equinix_metal.MetalGatewaysApi(mpc).find_metal_gateways_by_project,
+ {'project_id': 'project_id'},
+ extra_kwargs={"include": ["ip_reservation"]},
+ ),
# DELETERS
('metal_device', action.DELETE): spec_types.Specs(
@@ -157,6 +166,10 @@ def get_routes(mpc):
('metal_vrf', action.DELETE): spec_types.Specs(
equinix_metal.VRFsApi(mpc).delete_vrf,
),
+ ('metal_gateway', action.DELETE): spec_types.Specs(
+ equinix_metal.MetalGatewaysApi(mpc).delete_metal_gateway,
+ extra_kwargs={"include": ["ip_reservation"]},
+ ),
# CREATORS
('metal_device', action.CREATE): spec_types.Specs(
@@ -228,6 +241,14 @@ def get_routes(mpc):
{'id': 'project_id'},
equinix_metal.VrfCreateInput,
),
+ ('metal_gateway', action.CREATE): spec_types.Specs(
+ equinix_metal.MetalGatewaysApi(mpc).create_metal_gateway,
+ {'project_id': 'project_id'},
+ equinix_metal.MetalGatewayCreateInput,
+ equinix_metal.CreateMetalGatewayRequest,
+ {"include": ["ip_reservation"]},
+ ),
+
# 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 c9959f7..d6a6615 100644
--- a/plugins/module_utils/metal/metal_api.py
+++ b/plugins/module_utils/metal/metal_api.py
@@ -15,6 +15,8 @@
api_routes,
)
+def ip_count_from_mask(mask: int):
+ return 2 ** (32 - mask)
def optional(key: str):
return lambda resource: utils.dict_get(resource, key)
@@ -39,9 +41,8 @@ def optional_bool(key: str):
def optional_float(key: str):
return lambda resource: resource.get(key, 0.0)
-
def cidr_to_quantity(key: str):
- return lambda resource: 2 ** (32 - resource.get(key))
+ return lambda resource: ip_count_from_mask(resource.get(key))
def ip_address_getter(resource: dict):
@@ -147,6 +148,7 @@ def extract_ids_from_projects_hrefs(resource: dict):
'virtual_networks',
'interconnections',
'vrfs',
+ 'metal_gateways',
]
@@ -223,6 +225,34 @@ def get_assignment_address(resource: dict):
'status': 'status',
}
+def private_ipv4_subnet_size(resource: dict):
+ """
+ Computes the private_ipv4_subnet_size from the ip_reservation.cidr field in the API response.
+ Returns None if the ip_reservation is public.
+ """
+ ip_reservation = resource.get('ip_reservation')
+ if ip_reservation is None:
+ raise Exception("ip_reservation is not present in API response")
+ is_public = ip_reservation.get('public')
+ if is_public is None:
+ raise Exception("'public' is not present in ip_reservation field in API response (maybe we need to explicitly include ip_reservation in request kwargs?)")
+ if is_public:
+ return None
+ cidr = ip_reservation.get('cidr')
+ if cidr is None:
+ raise Exception("'cidr' is not present in ip_reservation field in API response (maybe we need to explicitly include ip_reservation through request kwargs?)")
+ return ip_count_from_mask(cidr)
+
+
+METAL_GATEWAY_RESPONSE_ATTRIBUTE_MAP = {
+ 'id': 'id',
+ 'ip_reservation_id': 'ip_reservation.id',
+ 'private_ipv4_subnet_size': private_ipv4_subnet_size,
+ 'virtual_network_id': 'virtual_network.id',
+ 'project_id': 'project.id',
+ 'metal_state': optional_str('state'),
+}
+
def get_attribute_mapper(resource_type):
"""
@@ -239,6 +269,7 @@ def get_attribute_mapper(resource_type):
'metal_connection_project_dedicated', 'metal_connection_organization_dedicated',
'metal_connection_project_vlanfabric', 'metal_connection_project_vrf'])
vrf_resources = set(['metal_vrf'])
+ gateway_resources = set(["metal_gateway", "metal_gateway_vrf"])
if resource_type in device_resources:
return METAL_DEVICE_RESPONSE_ATTRIBUTE_MAP
elif resource_type in project_resources:
@@ -263,6 +294,8 @@ def get_attribute_mapper(resource_type):
return VLAN_RESPONSE_ATTRIBUTE_MAP
elif resource_type in vrf_resources:
return METAL_VRF_RESPONSE_ATTRIBUTE_MAP
+ elif resource_type in gateway_resources:
+ return METAL_GATEWAY_RESPONSE_ATTRIBUTE_MAP
else:
raise NotImplementedError("No mapper for resource type %s" % resource_type)
@@ -278,8 +311,6 @@ 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)
if action == action.DELETE:
return None
attribute_mapper = get_attribute_mapper(resource_type)
diff --git a/plugins/module_utils/metal/spec_types.py b/plugins/module_utils/metal/spec_types.py
index acdca5e..806fe2f 100644
--- a/plugins/module_utils/metal/spec_types.py
+++ b/plugins/module_utils/metal/spec_types.py
@@ -24,11 +24,13 @@ def __init__(self,
named_args_mapping: Optional[Dict[str, str]] = None,
request_model_class: Optional[Callable] = None,
request_superclass: Optional[Callable] = None,
+ extra_kwargs: Optional[Dict] = None,
):
self.func = func
self.named_args_mapping = named_args_mapping
self.request_model_class = request_model_class
self.request_superclass = request_superclass
+ self.extra_kwargs = extra_kwargs
if self.request_model_class is not None:
if not inspect.isclass(request_model_class):
raise ValueError('request_model_class must be a class, is {-1}'.format(type(request_model_class)))
@@ -58,6 +60,8 @@ def __init__(self,
param_names = set(inspect.signature(conf.func).parameters.keys())
self.sdk_kwargs = {}
+ if conf.extra_kwargs is not None:
+ self.sdk_kwargs.update(conf.extra_kwargs)
arg_mapping = self.conf.named_args_mapping or {}
for param_name in param_names:
lookup_name = param_name
diff --git a/plugins/modules/metal_gateway.py b/plugins/modules/metal_gateway.py
new file mode 100644
index 0000000..45723e1
--- /dev/null
+++ b/plugins/modules/metal_gateway.py
@@ -0,0 +1,328 @@
+#!/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 Metal Gateway in Equinix Metal. You can use *id* or *ip_reservation_id*
+ to lookup a Gateway. If you want to create new resource, you must provide *project_id*,
+ *virtual_network_id* and either *ip_reservation_id* or *private_ipv4_subnet_size*.
+module: metal_gateway
+notes: []
+options:
+ id:
+ description:
+ - UUID of the gateway.
+ required: false
+ type: str
+ ip_reservation_id:
+ description:
+ - UUID of Public Reservation to associate with the gateway, the reservation must
+ be in the same metro as the VLAN, conflicts with private_ipv4_subnet_size.
+ required: false
+ type: str
+ private_ipv4_subnet_size:
+ description:
+ - Size of the private IPv4 subnet to create for this metal gateway, must be one
+ of 8, 16, 32, 64, 128. Conflicts with ip_reservation_id.
+ required: false
+ type: int
+ project_id:
+ description:
+ - UUID of the project where the gateway is scoped to.
+ required: false
+ type: str
+ timeout:
+ default: 10
+ description:
+ - Timeout in seconds for gateway to get to "ready" state, and for gateway to be
+ removed
+ required: false
+ type: int
+ virtual_network_id:
+ description:
+ - UUID of the VLAN where the gateway is scoped to.
+ required: false
+ type: str
+requirements: null
+short_description: Manage Metal Gateway in Equinix Metal
+'''
+EXAMPLES = '''
+- name: Create new gateway with existing IP reservation
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: a4cc87f9-e00f-48c2-9460-74aa60beb6b0
+ ip_reservation_id: 83b5503c-7b7f-4883-9509-b6b728b41491
+ virtual_network_id: eef49903-7a09-4ca1-af67-4087c29ab5b6
+- name: Create new gateway with new private /29 subnet
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: '{{ project.id }}'
+ virtual_network_id: '{{ vlan.id }}'
+ private_ipv4_subnet_size: 8
+- name: Lookup a gateway by ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ id: eef49903-7a09-4ca1-af67-4087c29ab5b6
+ register: gateway
+- name: Lookup a gateway by IP reservation ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ ip_reservation_id: a4cc87f9-e00f-48c2-9460-74aa60beb6b0
+ register: gateway
+'''
+RETURN = '''
+metal_gateway:
+ description: The module object
+ returned: always
+ sample:
+ - "\n{\n \n \"changed\": true,\n \"id\": \"1f4d30da-4041-406d-8d94-6ce929340d98\"\
+ ,\n \"ip_reservation_id\": \"fa017281-b10e-4b22-b449-35a93fb88d85\",\n \"metal_state\"\
+ : \"ready\",\n \"private_ipv4_subnet_size\": null,\n \"project_id\": \"2e85a66a-ea6a-4e33-8029-cc5ab9a0bc91\"\
+ ,\n \"virtual_network_id\": \"4a06c542-e47c-4e3c-ab85-bfc3cba4004d\"\n}\n"
+ - "\n{\n \"changed\": true,\n \"id\": \"be809e36-42a0-4a3b-982c-8f4487b9b9fc\"\
+ ,\n \"ip_reservation_id\": \"e5c4be29-e238-431a-8c5f-f44f30fd5098\",\n \"metal_state\"\
+ : \"ready\",\n \"private_ipv4_subnet_size\": 8,\n \"project_id\": \"0491c16b-376d-4842-89d2-da3efead4991\"\
+ ,\n \"virtual_network_id\": \"f46ab2c8-1332-4f87-91e9-f3a6a81d9769\"\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_gateway"
+
+module_spec = dict(
+ id=SpecField(
+ type=FieldType.string,
+ description=['UUID of the gateway.'],
+ ),
+ project_id=SpecField(
+ type=FieldType.string,
+ description=['UUID of the project where the gateway is scoped to.'],
+ ),
+ # in order to support VRF, we should do antoher parameter
+ # vrf_ip_reservation_id which will use VRF
+ ip_reservation_id=SpecField(
+ type=FieldType.string,
+ conflicts_with=["private_ipv4_subnet_size"],
+ description=['UUID of Public Reservation to associate with the gateway, the reservation must be in the same metro as the VLAN, conflicts with private_ipv4_subnet_size.'],
+ ),
+ private_ipv4_subnet_size=SpecField(
+ type=FieldType.integer,
+ conflicts_with=["ip_reservation_id"],
+ description=['Size of the private IPv4 subnet to create for this metal gateway, must be one of 8, 16, 32, 64, 128. Conflicts with ip_reservation_id.'],
+ ),
+ virtual_network_id=SpecField(
+ type=FieldType.string,
+ description=['UUID of the VLAN where the gateway is scoped to.'],
+ ),
+ timeout=SpecField(
+ type=FieldType.integer,
+ description=['Timeout in seconds for gateway to get to "ready" state, and for gateway to be removed'],
+ default=10,
+ ),
+)
+
+
+specdoc_examples = [
+'''
+- name: Create new gateway with existing IP reservation
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: "a4cc87f9-e00f-48c2-9460-74aa60beb6b0"
+ ip_reservation_id: "83b5503c-7b7f-4883-9509-b6b728b41491"
+ virtual_network_id: "eef49903-7a09-4ca1-af67-4087c29ab5b6"
+''',
+'''
+- name: Create new gateway with new private /29 subnet
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ private_ipv4_subnet_size: 8
+''',
+'''
+- name: Lookup a gateway by ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ id: "eef49903-7a09-4ca1-af67-4087c29ab5b6"
+ register: gateway
+''',
+'''
+- name: Lookup a gateway by IP reservation ID
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway:
+ ip_reservation_id: "a4cc87f9-e00f-48c2-9460-74aa60beb6b0"
+ register: gateway
+''',
+]
+
+result_sample = ['''
+{
+
+ "changed": true,
+ "id": "1f4d30da-4041-406d-8d94-6ce929340d98",
+ "ip_reservation_id": "fa017281-b10e-4b22-b449-35a93fb88d85",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": null,
+ "project_id": "2e85a66a-ea6a-4e33-8029-cc5ab9a0bc91",
+ "virtual_network_id": "4a06c542-e47c-4e3c-ab85-bfc3cba4004d"
+}
+''',
+'''
+{
+ "changed": true,
+ "id": "be809e36-42a0-4a3b-982c-8f4487b9b9fc",
+ "ip_reservation_id": "e5c4be29-e238-431a-8c5f-f44f30fd5098",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "0491c16b-376d-4842-89d2-da3efead4991",
+ "virtual_network_id": "f46ab2c8-1332-4f87-91e9-f3a6a81d9769"
+}
+''',
+]
+
+MUTABLE_ATTRIBUTES = [
+ k for k, v in module_spec.items() if v.editable
+]
+
+SPECDOC_META = getSpecDocMeta(
+ short_description='Manage Metal Gateway in Equinix Metal',
+ description=(
+ 'Manage Metal Gateway in Equinix Metal. '
+ 'You can use *id* or *ip_reservation_id* to lookup a Gateway. '
+ 'If you want to create new resource, you must provide *project_id*, *virtual_network_id* and either *ip_reservation_id* or *private_ipv4_subnet_size*.'
+ ),
+ examples=specdoc_examples,
+ options=module_spec,
+ return_values={
+ "metal_gateway": SpecReturnValue(
+ description='The module object',
+ type=FieldType.dict,
+ sample=result_sample,
+ ),
+ },
+)
+
+
+def main():
+ # In case you ever want to try to understand the dependencies between module options:
+ # https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#argument-spec-dependencies
+ module = EquinixModule(
+ argument_spec=SPECDOC_META.ansible_spec,
+ # we either create new gateway with ip_reservation_id or with private_ipv4_subnet_size
+ mutually_exclusive=[("private_ipv4_subnet_size", "ip_reservation_id")],
+ required_one_of=[
+ # we either create new gateway or lookup existing one by id or
+ # by ip_reservation_id
+ ("id", "project_id", "ip_reservation_id"),
+ ],
+ required_by=dict(
+ # if we create new gateway in a project we need to provide virtual_network_id
+ project_id=["virtual_network_id"],
+ # if we lookup existing gateway, we need to provide ip_reservation_id
+ ip_reservation_id=["virtual_network_id","project_id"],
+ ),
+ # I think we still need to check that:
+ # - if we create new gateway in a project, either of ip_reservation_id or private_ipv4_subnet_size must be provided
+ )
+
+
+ state = module.params.get("state")
+ changed = False
+ fetched = 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 = None
+
+ # If user supplied ip_reservation_id, we need to check if
+ # there's already a gateway with the same reservation ID
+ if module.params.get("ip_reservation_id") is not None:
+ fetched = module.get_one_from_list(
+ MODULE_NAME,
+ ['ip_reservation_id'],
+ )
+
+ if fetched:
+ module.params['id'] = fetched['id']
+ if state == "present":
+ diff = get_diff(module.params, fetched, MUTABLE_ATTRIBUTES)
+ if diff:
+ module.fail_json(msg="Metal_gateway isn't mutable.")
+
+ else:
+ module.delete_by_id(MODULE_NAME)
+ # We wait for removal because Terraform resource for Gateway
+ # also waits for removal.
+ module.wait_for_resource_removal(
+ "metal_gateway",
+ timeout=module.params.get("timeout"),
+ )
+ changed = True
+ else:
+ if state == "present":
+ # if not any((module.params.get("private_ipv4_subnet_size"), module.params.get("ip_reservation_id"))):
+ # module.fail_json(msg="You must set either ip_reservation_id or private_ipv4_subnet_size!")
+ # todo remove
+ # module.params.pop("ip_reservation_id")
+ fetched = module.create(MODULE_NAME)
+ if 'id' not in fetched:
+ module.fail_json(msg="UUID not found in gateway creation response")
+ module.params['id'] = fetched['id']
+ seconds = module.params.get("timeout")
+ fetched = module.wait_for_resource_condition(
+ "metal_gateway",
+ "metal_state",
+ "ready",
+ timeout=seconds)
+ changed = True
+ else:
+ fetched = {}
+ except Exception as e:
+ tb = traceback.format_exc()
+ module.fail_json(msg=f"Error in metal_gateway: {to_native(e)}",
+ exception=tb)
+
+ fetched.update({'changed': changed})
+ module.exit_json(**fetched)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plugins/modules/metal_gateway_info.py b/plugins/modules/metal_gateway_info.py
new file mode 100644
index 0000000..2ee22e1
--- /dev/null
+++ b/plugins/modules/metal_gateway_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 RETURN are generated by
+# ansible_specdoc. Do not edit them directly.
+
+DOCUMENTATION = '''
+author: Equinix DevRel Team (@equinix)
+description: Gather information about Metal Gateways
+module: metal_gateway_info
+notes: []
+options:
+ project_id:
+ description:
+ - UUID of parent project the gateway is scoped to.
+ required: false
+ type: str
+requirements: null
+short_description: Gather information about Metal Gateways
+'''
+EXAMPLES = '''
+- name: Gather information about all gateways in a project
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway_info:
+ project_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7
+'''
+RETURN = '''
+resources:
+ description: Found Metal Gateways
+ returned: always
+ sample:
+ - "\n[\n {\n \"id\": \"771c9418-7c60-4a45-8fa6-3a002132331d\",\n \
+ \ \"ip_reservation_id\": \"d45c9629-3aab-4a7b-af5d-4ca50041e311\",\n \
+ \ \"metal_state\": \"ready\",\n \"private_ipv4_subnet_size\": 8,\n \
+ \ \"project_id\": \"f7a35065-2e41-4747-b3d1-400af0a3e0e8\",\n \"virtual_network_id\"\
+ : \"898972b3-7eb9-4ca2-b803-7b5d339bbea7\"\n },\n {\n \"id\": \"\
+ b66eb02d-c4bb-4ae8-a22e-0f7934da971e\",\n \"ip_reservation_id\": \"6282982a-e6de-4f4d-b230-2ae27e90778c\"\
+ ,\n \"metal_state\": \"ready\",\n \"private_ipv4_subnet_size\":\
+ \ 8,\n \"project_id\": \"f7a35065-2e41-4747-b3d1-400af0a3e0e8\",\n \
+ \ \"virtual_network_id\": \"898972b3-7eb9-4ca2-b803-7b5d339bbea7\"\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=['UUID of parent project the gateway is scoped to.'],
+ ),
+)
+
+specdoc_examples = ['''
+- name: Gather information about all gateways in a project
+ hosts: localhost
+ tasks:
+ - equinix.cloud.metal_gateway_info:
+ project_id: 2a5122b9-c323-4d5c-b53c-9ad3f54273e7
+''',
+]
+
+result_sample = ['''
+[
+ {
+ "id": "771c9418-7c60-4a45-8fa6-3a002132331d",
+ "ip_reservation_id": "d45c9629-3aab-4a7b-af5d-4ca50041e311",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "f7a35065-2e41-4747-b3d1-400af0a3e0e8",
+ "virtual_network_id": "898972b3-7eb9-4ca2-b803-7b5d339bbea7"
+ },
+ {
+ "id": "b66eb02d-c4bb-4ae8-a22e-0f7934da971e",
+ "ip_reservation_id": "6282982a-e6de-4f4d-b230-2ae27e90778c",
+ "metal_state": "ready",
+ "private_ipv4_subnet_size": 8,
+ "project_id": "f7a35065-2e41-4747-b3d1-400af0a3e0e8",
+ "virtual_network_id": "898972b3-7eb9-4ca2-b803-7b5d339bbea7"
+ }
+]''',
+]
+
+SPECDOC_META = getSpecDocMeta(
+ short_description="Gather information about Metal Gateways",
+ description=(
+ 'Gather information about Metal Gateways'
+ ),
+ examples=specdoc_examples,
+ options=module_spec,
+ return_values={
+ "resources": SpecReturnValue(
+ description='Found Metal Gateways',
+ 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_gateway")}
+ 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_gateway/tasks/main.yml b/tests/integration/targets/metal_gateway/tasks/main.yml
new file mode 100644
index 0000000..c76af82
--- /dev/null
+++ b/tests/integration/targets/metal_gateway/tasks/main.yml
@@ -0,0 +1,97 @@
+- name: metal_gateway
+ module_defaults:
+ equinix.cloud.metal_gateway:
+ metal_api_token: '{{ metal_api_token }}'
+ equinix.cloud.metal_gateway_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 }}'
+ equinix.cloud.metal_vlan:
+ metal_api_token: '{{ metal_api_token }}'
+ block:
+ - set_fact:
+ test_resource_name_prefix: 'ansible-integration-test-gateway'
+ - 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_vxlan: 123
+
+ - name: create project for test
+ equinix.cloud.metal_project:
+ name: "{{ test_prefix }}-project"
+ register: project
+
+ - name: create vlan for test
+ equinix.cloud.metal_vlan:
+ project_id: "{{ project.id }}"
+ vxlan: "{{ test_vxlan }}"
+ metro: "{{ metal_test_metro }}"
+ register: vlan
+
+ - name: create gateway for test
+ equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ private_ipv4_subnet_size: 8
+ register: gateway
+
+ # We can't check idempotent calls for metal_gateway from private IP range.
+ # There can be many of those.
+
+ - name: create another gateway for test
+ equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ private_ipv4_subnet_size: 8
+ register: gateway_2
+
+ - name: fetch gateway
+ equinix.cloud.metal_gateway:
+ id: "{{ gateway.id }}"
+ register: fetched_gateway
+
+ - assert:
+ that:
+ - fetched_gateway.id == "{{ gateway.id }}"
+
+ - name: list gateways
+ equinix.cloud.metal_gateway_info:
+ project_id: "{{ project.id }}"
+ register: listed_gateways
+
+
+ - assert:
+ that:
+ - "listed_gateways.resources | length == 2"
+
+ - name: delete gateways
+ equinix.cloud.metal_gateway:
+ id: "{{ item.id }}"
+ state: absent
+ loop: "{{ listed_gateways.resources }}"
+
+ - name: delete test vlan
+ equinix.cloud.metal_vlan:
+ id: "{{ 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
diff --git a/tests/integration/targets/metal_gateway_ip_reservation/tasks/main.yml b/tests/integration/targets/metal_gateway_ip_reservation/tasks/main.yml
new file mode 100644
index 0000000..953e52d
--- /dev/null
+++ b/tests/integration/targets/metal_gateway_ip_reservation/tasks/main.yml
@@ -0,0 +1,106 @@
+- name: metal_gateway_ip_reservation
+ module_defaults:
+ equinix.cloud.metal_gateway:
+ metal_api_token: '{{ metal_api_token }}'
+ equinix.cloud.metal_gateway_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 }}'
+ equinix.cloud.metal_vlan:
+ metal_api_token: '{{ metal_api_token }}'
+ equinix.cloud.metal_reserved_ip_block:
+ metal_api_token: '{{ metal_api_token }}'
+ block:
+ - set_fact:
+ test_resource_name_prefix: 'ansible-integration-test-gateway-ip-res'
+ - 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_vxlan: 123
+
+ - name: create project for test
+ equinix.cloud.metal_project:
+ name: "{{ test_prefix }}-project"
+ register: project
+
+ - name: create vlan for test
+ equinix.cloud.metal_vlan:
+ project_id: "{{ project.id }}"
+ vxlan: "{{ test_vxlan }}"
+ metro: "{{ metal_test_metro }}"
+ register: vlan
+
+ # 8 is the minimum amount of IPs we can use for Metal Gateway
+ - name: request 8 public IPs for test
+ equinix.cloud.metal_reserved_ip_block:
+ type: "public_ipv4"
+ metro: "{{ metal_test_metro }}"
+ quantity: 8
+ project_id: "{{ project.id }}"
+ register: ip_reservation
+
+ - name: create gateway for test
+ equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ ip_reservation_id: "{{ ip_reservation.id }}"
+ register: gateway
+
+ - name: check idempotency of metal_gateway module when using existing ip_reservation
+ equinix.cloud.metal_gateway:
+ project_id: "{{ project.id }}"
+ virtual_network_id: "{{ vlan.id }}"
+ ip_reservation_id: "{{ ip_reservation.id }}"
+ register: gateway_again
+
+ - assert:
+ that:
+ - gateway_again.id == "{{ gateway.id }}"
+ - gateway_again.changed == false
+
+ - name: fetch gateway by id
+ equinix.cloud.metal_gateway:
+ id: "{{ gateway.id }}"
+ register: fetched_gateway
+
+ - assert:
+ that:
+ - fetched_gateway.id == "{{ gateway.id }}"
+ - fetched_gateway.ip_reservation_id == "{{ ip_reservation.id }}"
+ - fetched_gateway.virtual_network_id == "{{ vlan.id }}"
+
+ - name: delete gateway
+ equinix.cloud.metal_gateway:
+ id: "{{ gateway.id }}"
+ state: absent
+
+ - name: delete ip reservation
+ equinix.cloud.metal_reserved_ip_block:
+ id: "{{ ip_reservation.id }}"
+ state: absent
+
+ - name: delete test vlan
+ equinix.cloud.metal_vlan:
+ id: "{{ 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