From 7e8a8f18cda5b65f153cdba247d370e383afcad1 Mon Sep 17 00:00:00 2001 From: Ubuntu <jorge.sasiain@ehu.eus> Date: Wed, 3 May 2023 17:14:47 +0000 Subject: [PATCH] NAT-152: support list of containers oer service --- gso/oss-params-example.json | 12 ++--- gso/services/_ipam.py | 103 ++++++++++++++++-------------------- gso/settings.py | 4 +- 3 files changed, 55 insertions(+), 64 deletions(-) diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 84ce7df3..faea14f2 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -9,18 +9,18 @@ "password": "robot-user-password" }, "LO": { - "V4": {"container": "1.1.0.0/24", "mask": 31}, - "V6": {"container": "dead:beef::/64", "mask": 126}, + "V4": {"containers": ["1.1.0.0/24"], "mask": 32}, + "V6": {"containers": ["dead:beef::/64"], "mask": 128}, "domain_name": ".lo" }, "TRUNK": { - "V4": {"container": "1.1.1.0/24", "mask": 31}, - "V6": {"container": "dead:beef::/64", "mask": 126}, + "V4": {"containers": ["1.1.1.0/24"], "mask": 31}, + "V6": {"containers": ["dead:beef::/64"], "mask": 126}, "domain_name": ".trunk" }, "GEANT_IP": { - "V4": {"container": "1.1.2.0/24", "mask": 31}, - "V6": {"container": "dead:beef::/64", "mask": 126}, + "V4": {"containers": ["1.1.2.0/24"], "mask": 31}, + "V6": {"containers": ["dead:beef::/64"], "mask": 126}, "domain_name": ".geantip" } } diff --git a/gso/services/_ipam.py b/gso/services/_ipam.py index 08f54c1b..2cf73d27 100644 --- a/gso/services/_ipam.py +++ b/gso/services/_ipam.py @@ -1,10 +1,11 @@ -import requests -from requests.auth import HTTPBasicAuth -import json import ipaddress +import json +import random +import requests +from enum import Enum from pydantic import BaseSettings +from requests.auth import HTTPBasicAuth from typing import Union -import random from gso import settings @@ -35,12 +36,19 @@ class HostAddresses(BaseSettings): v6: ipaddress.IPv6Address +class IPAMErrors(Enum): +# HTTP error code, match in error message + CONTAINER_FULL = 400, "Can not find requested number of networks" + # TODO: remove this! # lab infoblox cert is not valid for the ipv4 address # ... disable warnings for now requests.packages.urllib3.disable_warnings() +def _match_error_code(response, error_code): + return response.status_code == error_code.value[0] and error_code.value[1] in response.text + def _wapi(infoblox_params: settings.InfoBloxParams): return (f'https://{infoblox_params.host}' f'/wapi/{infoblox_params.wapi_version}') @@ -68,7 +76,7 @@ def _ip_network_version(network): def _find_networks(network_container=None, network=None, ip_version=4): """ If network_container is not None, find all networks within the specified container. - Otherwise if network is not None, find the specified network. + Otherwise, if network is not None, find the specified network. Otherwise find all networks. """ assert ip_version in [4, 6] @@ -121,46 +129,40 @@ def _get_network_capacity(network=None): def _allocate_network(infoblox_params: settings.InfoBloxParams, network_params: Union[settings.V4NetworkParams, settings.V6NetworkParams], ip_version=4) -> Union[V4ServiceNetwork, V6ServiceNetwork]: assert ip_version in [4, 6] endpoint = 'network' if ip_version == 4 else 'ipv6network' + ip_container = 'networkcontainer' if ip_version == 4 else 'ipv6networkcontainer' - ipv4_req_payload = { + req_payload = { "network": { "_object_function": "next_available_network", "_parameters": { "cidr": network_params.mask }, - "_object": "networkcontainer", + "_object": ip_container, "_object_parameters": { - "network": str(network_params.container) + "network": str(network_params.containers[0]) }, "_result_field": "networks", # only return in the response the allocated network, not all available } } - ipv6_req_payload = { - "network": { - "_object_function": "next_available_network", - "_parameters": { - "cidr": network_params.mask - }, - "_object": "ipv6networkcontainer", - "_object_parameters": { - "network": str(network_params.container) - }, - "_result_field": "networks", # only return in the response the allocated network, not all available - } - } - - req_payload = ipv4_req_payload if ip_version == 4 else ipv6_req_payload + container_index = 0 + while True: + r = requests.post( + f'{_wapi(infoblox_params)}/{endpoint}', + params={'_return_fields': 'network'}, + json=req_payload, + auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), + headers={'content-type': "application/json"}, + verify=False + ) + if not _match_error_code(response=r, error_code=IPAMErrors.CONTAINER_FULL): + break + # Container full: try with next valid container for the service (if any) + container_index += 1 + if len(network_params.containers) < (container_index + 1): + break + req_payload["network"]["_object_parameters"]["network"] = str(network_params.containers[container_index]) - r = requests.post( - f'{_wapi(infoblox_params)}/{endpoint}', - params={'_return_fields': 'network'}, - json=req_payload, - auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - headers={'content-type': "application/json"}, - verify=False - ) - # TODO: handle no more available space for networks in the container assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" assert 'network' in r.json() @@ -226,10 +228,10 @@ def _allocate_host(hostname=None, addr=None, network=None) -> Union[V4HostAddres else: ip_version = _ip_addr_version(addr) - ipv4_req_payload = { - "ipv4addrs": [ + ip_req_payload = { + f"ipv{ip_version}addrs": [ { - "ipv4addr": addr + f"ipv{ip_version}addr": addr } ], "name": hostname, @@ -237,19 +239,6 @@ def _allocate_host(hostname=None, addr=None, network=None) -> Union[V4HostAddres "view": "default" } - ipv6_req_payload = { - "ipv6addrs": [ - { - "ipv6addr": addr - } - ], - "name": hostname, - "configure_for_dns": False, - "view": "default" - } - - ip_req_payload = ipv4_req_payload if ip_version == 4 else ipv6_req_payload - r = requests.post( f'{_wapi(infoblox_params)}/record:host', json=ip_req_payload, @@ -298,19 +287,20 @@ def allocate_service_host(hostname=None, service_type=None, service_networks: Se If service_networks is not provided, and host_addresses is provided, those specific addresses are used. If neither is not provided, the first network with available space for this service type is used. Note that if WFO will always specify the network/addresses after creating it, this mode won't be needed. + Currently this mode doesn't look further than the first container, so if needed, this will need to be updated. """ oss = settings.load_oss_params() assert oss.IPAM ipam_params = oss.IPAM assert hasattr(ipam_params, service_type) and service_type != 'INFOBLOX', "Invalid service type." - ipv4_container = getattr(ipam_params, service_type).V4.container - ipv6_container = getattr(ipam_params, service_type).V6.container + ipv4_containers = getattr(ipam_params, service_type).V4.containers + ipv6_containers = getattr(ipam_params, service_type).V6.containers domain_name = getattr(ipam_params, service_type).domain_name # IPv4 if not service_networks and not host_addresses: - ipv4_networks_info = _find_networks(network_container=str(ipv4_container), ip_version=4) + ipv4_networks_info = _find_networks(network_container=str(ipv4_containers[0]), ip_version=4) assert len(ipv4_networks_info) >= 1, "No IPv4 network exists in the container for this service type." first_nonfull_ipv4_network = None for ipv4_network_info in ipv4_networks_info: @@ -326,29 +316,30 @@ def allocate_service_host(hostname=None, service_type=None, service_networks: Se v4_host = _allocate_host(hostname=hostname+domain_name, network=first_nonfull_ipv4_network) elif service_networks: network = service_networks.v4 - assert network.subnet_of(ipv4_container) + assert any(network.subnet_of(ipv4_container) for ipv4_container in ipv4_containers) v4_host = _allocate_host(hostname=hostname+domain_name, network=str(network)) elif host_addresses: addr = host_addresses.v4 - assert addr in ipv4_container + assert any(addr in ipv4_container for ipv4_container in ipv4_containers) v4_host = _allocate_host(hostname=hostname+domain_name, addr=str(addr)) + # IPv6 if not service_networks and not host_addresses: # ipv6 does not support capacity fetching (not even the GUI displays it). # Maybe it's assumed that there is always available space? - ipv6_networks_info = _find_networks(network_container=str(ipv6_container), ip_version=6) + ipv6_networks_info = _find_networks(network_container=str(ipv6_containers[0]), ip_version=6) assert len(ipv6_networks_info) >= 1, "No IPv6 network exists in the container for this service type." assert 'network' in ipv6_networks_info[0] # TODO: if "no available IP" error, create a new network? v6_host = _allocate_host(hostname=hostname+domain_name, network=ipv6_networks_info[0]['network']) elif service_networks: network = service_networks.v6 - assert network.subnet_of(ipv6_container) + assert any(network.subnet_of(ipv6_container) for ipv6_container in ipv6_containers) v6_host = _allocate_host(hostname=hostname+domain_name, network=str(network)) elif host_addresses: addr = host_addresses.v6 - assert addr in ipv6_container + assert any(addr in ipv6_container for ipv6_container in ipv6_containers) v6_host = _allocate_host(hostname=hostname+domain_name, addr=str(addr)) return HostAddresses(v4=v4_host.v4,v6=v6_host.v6) diff --git a/gso/settings.py b/gso/settings.py index 91c12515..6c2d3f90 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -13,12 +13,12 @@ class InfoBloxParams(BaseSettings): class V4NetworkParams(BaseSettings): - container: ipaddress.IPv4Network + containers: list[ipaddress.IPv4Network] mask: int # TODO: validation on mask? class V6NetworkParams(BaseSettings): - container: ipaddress.IPv6Network + containers: list[ipaddress.IPv6Network] mask: int # TODO: validation on mask? -- GitLab