diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 84ce7df3190d7df29df57174d5b3d6f6adbf5c48..faea14f28c21e6203994aa624d0fa6ed293d8046 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 08f54c1b691a00a043c42abdd05d860044aaa77b..2cf73d2773a8d1411338e7b79f8b2cfe97de93cf 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 91c1251538fec86151b5582b5ec4b44a40aa682b..6c2d3f901e6825aa47e3f9357e42753c692431d7 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?