Skip to content

Commit

Permalink
Merge pull request #144 from equinix-labs/feat/57-metal-gateway
Browse files Browse the repository at this point in the history
feat: add modules metal_gateway and metal_gateway_info
  • Loading branch information
t0mk authored Feb 7, 2024
2 parents 712e076 + b6e2520 commit 45d6699
Show file tree
Hide file tree
Showing 11 changed files with 920 additions and 7 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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|
Expand Down
112 changes: 112 additions & 0 deletions docs/modules/metal_gateway.md
Original file line number Diff line number Diff line change
@@ -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` | <center>`str`</center> | <center>Optional</center> | UUID of the gateway. |
| `project_id` | <center>`str`</center> | <center>Optional</center> | UUID of the project where the gateway is scoped to. |
| `ip_reservation_id` | <center>`str`</center> | <center>Optional</center> | 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` | <center>`int`</center> | <center>Optional</center> | 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` | <center>`str`</center> | <center>Optional</center> | UUID of the VLAN where the gateway is scoped to. |
| `timeout` | <center>`int`</center> | <center>Optional</center> | 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"
}

```


68 changes: 68 additions & 0 deletions docs/modules/metal_gateway_info.md
Original file line number Diff line number Diff line change
@@ -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` | <center>`str`</center> | <center>Optional</center> | 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"
}
]
```


22 changes: 19 additions & 3 deletions plugins/module_utils/equinix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions plugins/module_utils/metal/api_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
39 changes: 35 additions & 4 deletions plugins/module_utils/metal/metal_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -147,6 +148,7 @@ def extract_ids_from_projects_hrefs(resource: dict):
'virtual_networks',
'interconnections',
'vrfs',
'metal_gateways',
]


Expand Down Expand Up @@ -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):
"""
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions plugins/module_utils/metal/spec_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 45d6699

Please sign in to comment.