From 1c5f45a35109441411e50d5dc96d555ae110127f Mon Sep 17 00:00:00 2001 From: Ubuntu <jorge.sasiain@ehu.eus> Date: Mon, 15 May 2023 10:41:19 +0000 Subject: [PATCH] NAT-185: combine IPv4/IPv6/DNS in host record and update tests --- gso/services/_ipam.py | 292 ++++++++++++++++++++---------------------- gso/services/ipam.py | 12 +- test/test_ipam.py | 31 ++++- 3 files changed, 173 insertions(+), 162 deletions(-) diff --git a/gso/services/_ipam.py b/gso/services/_ipam.py index 2707a53d..a13ccad8 100644 --- a/gso/services/_ipam.py +++ b/gso/services/_ipam.py @@ -109,39 +109,6 @@ def _find_networks(network_container=None, network=None, ip_version=4): return r.json() -def _get_network_capacity(network=None): - """ - Get utilization of a IPv4 network in a fraction of 1000. - """ - oss = settings.load_oss_params() - assert oss.IPAM.INFOBLOX - infoblox_params = oss.IPAM.INFOBLOX - - ip_version = _ip_network_version(network) - assert ip_version == 4, "Utilization is only available for IPv4 networks." - params = { - 'network': network, - '_return_fields': 'network,total_hosts,utilization' - } - - r = requests.get( - f'{_wapi(infoblox_params)}/network', - params=params, - auth=HTTPBasicAuth(infoblox_params.username, - infoblox_params.password), - verify=False - ) - # Utilization info takes several minutes to converge. - # The IPAM utilization bar in the GUI as well. Why? - assert r.status_code >= 200 and r.status_code < 300, \ - f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" - capacity_info = r.json() - assert len(capacity_info) == 1, "Requested IPv4 network doesn't exist." - assert 'utilization' in capacity_info[0] - utilization = capacity_info[0]['utilization'] - return utilization - - def _allocate_network( infoblox_params: settings.InfoBloxParams, network_params: Union[settings.V4NetworkParams, settings.V6NetworkParams], @@ -254,42 +221,64 @@ def _find_next_available_ip(infoblox_params, network_ref): return received_ip[0] -def _allocate_host(hostname=None, addr=None, network=None, extattrs={} - ) -> Union[V4HostAddress, V6HostAddress]: +def _allocate_host(hostname=None, + addrs=None, + networks=None, + extattrs={} + ) -> HostAddresses: """ - If network is not None, allocate host in that network. - Otherwise if addr is not None, allocate host with that address. + If networks is not None, allocate host in those networks. + Otherwise if addrs is not None, allocate host with those addresses. hostname parameter must be full name including domain name. """ # TODO: should hostnames be unique # (i.e. fail if hostname already exists in this domain/service)? - assert addr or network, \ - "You must specify either the host address or the network CIDR." + assert addrs or networks, \ + "You must specify either the host addresses or the networks CIDR." oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX infoblox_params = oss.IPAM.INFOBLOX - if network: - ip_version = _ip_network_version(network) - # Find the next available IP address in the network - network_info = _find_networks(network=network, ip_version=ip_version) + if networks: + ipv4_network = networks[0] + ipv6_network = networks[1] + assert _ip_network_version(ipv4_network) == 4 + assert _ip_network_version(ipv6_network) == 6 + + # Find the next available IP address in each network + network_info = _find_networks(network=ipv4_network, ip_version=4) + assert len(network_info) == 1, \ + "IPv4 Network does not exist. Create it first." + assert '_ref' in network_info[0] + ipv4_addr = _find_next_available_ip(infoblox_params, + network_info[0]["_ref"]) + + network_info = _find_networks(network=ipv6_network, ip_version=6) assert len(network_info) == 1, \ - "Network does not exist. Create it first." + "IPv6 Network does not exist. Create it first." assert '_ref' in network_info[0] - addr = _find_next_available_ip(infoblox_params, - network_info[0]["_ref"]) + ipv6_addr = _find_next_available_ip(infoblox_params, + network_info[0]["_ref"]) else: - ip_version = _ip_addr_version(addr) + ipv4_addr = addrs[0] + ipv6_addr = addrs[1] + assert _ip_addr_version(ipv4_addr) == 4 + assert _ip_addr_version(ipv6_addr) == 6 ip_req_payload = { - f"ipv{ip_version}addrs": [ + "ipv4addrs": [ { - f"ipv{ip_version}addr": addr + "ipv4addr": ipv4_addr + } + ], + "ipv6addrs": [ + { + "ipv6addr": ipv6_addr } ], "name": hostname, - "configure_for_dns": False, + "configure_for_dns": True, "view": "default", "extattrs": extattrs } @@ -306,31 +295,8 @@ def _allocate_host(hostname=None, addr=None, network=None, extattrs={} assert isinstance(r.json(), str) assert r.json().startswith("record:host/") - dns_req_payload = { - f"ipv{ip_version}addr": addr, - "name": hostname, - "view": "default", - "extattrs": extattrs - } - - endpoint = 'record:a' if ip_version == 4 else 'record:aaaa' - - r = requests.post( - f'{_wapi(infoblox_params)}/{endpoint}', - json=dns_req_payload, - auth=HTTPBasicAuth(infoblox_params.username, - infoblox_params.password), - verify=False - ) - assert r.status_code >= 200 and r.status_code < 300, \ - f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" - assert isinstance(r.json(), str) - assert r.json().startswith(f"{endpoint}/") - - if ip_version == 4: - return V4HostAddress(v4=addr) - else: - return V6HostAddress(v6=addr) + return HostAddresses(v4=ipaddress.ip_address(ipv4_addr), + v6=ipaddress.ip_address(ipv6_addr)) def allocate_service_host(hostname=None, @@ -340,19 +306,16 @@ def allocate_service_host(hostname=None, extattrs={} ) -> HostAddresses: """ - Allocate host with both IPv4 and IPv6 address (and respective DNS - records). - The domain name is also taken from the service type and appended to - specified hostname. - If service_networks is provided, that one is used. - If service_networks is not provided, and host_addresses is provided, + Allocate host record with both IPv4 and IPv6 address, and respective DNS + A and AAAA records. + - If service_networks is provided, that one is used. + - 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. + - If neither is not provided, new ipv4 and ipv6 networks are created and + those are used. Note that in this case extattrs is for the hosts and not + for the networks. + The domain name is taken from the service type and appended to the + specified hostname. """ oss = settings.load_oss_params() assert oss.IPAM @@ -364,74 +327,59 @@ def allocate_service_host(hostname=None, 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_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: - assert 'network' in ipv4_network_info - capacity = _get_network_capacity(ipv4_network_info["network"]) - if capacity < 1000: - first_nonfull_ipv4_network = ipv4_network_info["network"] - break - # Create a new network if the existing networks in the container for - # the service type are all full. - if not first_nonfull_ipv4_network: - first_nonfull_ipv4_network = str(allocate_service_ipv4_network( - service_type=service_type).v4) - assert first_nonfull_ipv4_network, \ - "No available IPv4 addresses for this service type." - v4_host = _allocate_host(hostname=hostname+domain_name, - network=first_nonfull_ipv4_network, - extattrs=extattrs) + # IPv4 + ipv4_network = str(allocate_service_ipv4_network( + service_type=service_type).v4) + assert ipv4_network, \ + "No available space for IPv4 networks for this service type." + + # IPv6 + ipv6_network = str(allocate_service_ipv6_network( + service_type=service_type).v6) + assert ipv6_network, \ + "No available space for IPv6 networks for this service type." + + network_tuple = (ipv4_network, ipv6_network) + host = _allocate_host(hostname=hostname+domain_name, + networks=network_tuple, + extattrs=extattrs) + elif service_networks: - network = service_networks.v4 - assert any(network.subnet_of(ipv4_container) - for ipv4_container in ipv4_containers) - v4_host = _allocate_host(hostname=hostname+domain_name, - network=str(network), - extattrs=extattrs) - elif host_addresses: - addr = host_addresses.v4 - assert any(addr in ipv4_container + # IPv4 + ipv4_network = service_networks.v4 + assert any(ipv4_network.subnet_of(ipv4_container) for ipv4_container in ipv4_containers) - v4_host = _allocate_host(hostname=hostname+domain_name, - addr=str(addr), - extattrs=extattrs) - # 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_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'], - extattrs=extattrs) - elif service_networks: - network = service_networks.v6 - assert any(network.subnet_of(ipv6_container) + # IPv6 + ipv6_network = service_networks.v6 + assert any(ipv6_network.subnet_of(ipv6_container) for ipv6_container in ipv6_containers) - v6_host = _allocate_host(hostname=hostname+domain_name, - network=str(network), - extattrs=extattrs) + + host = _allocate_host( + hostname=hostname+domain_name, + networks=(str(ipv4_network), str(ipv6_network)), + extattrs=extattrs + ) + elif host_addresses: - addr = host_addresses.v6 - assert any(addr in ipv6_container + # IPv4 + ipv4_addr = host_addresses.v4 + assert any(ipv4_addr in ipv4_container + for ipv4_container in ipv4_containers) + + # IPv6 + ipv6_addr = host_addresses.v6 + assert any(ipv6_addr in ipv6_container for ipv6_container in ipv6_containers) - v6_host = _allocate_host(hostname=hostname+domain_name, - addr=str(addr), - extattrs=extattrs) - return HostAddresses(v4=v4_host.v4, v6=v6_host.v6) + host = _allocate_host( + hostname=hostname+domain_name, + addrs=(str(ipv4_addr), str(ipv6_addr)), + extattrs=extattrs + ) + + return host """ @@ -462,6 +410,39 @@ def _find_containers(network=None, ip_version=4): return r.json() +def _get_network_capacity(network=None): + """ + Get utilization of a IPv4 network in a fraction of 1000. + """ + oss = settings.load_oss_params() + assert oss.IPAM.INFOBLOX + infoblox_params = oss.IPAM.INFOBLOX + + ip_version = _ip_network_version(network) + assert ip_version == 4, "Utilization is only available for IPv4 networks." + params = { + 'network': network, + '_return_fields': 'network,total_hosts,utilization' + } + + r = requests.get( + f'{_wapi(infoblox_params)}/network', + params=params, + auth=HTTPBasicAuth(infoblox_params.username, + infoblox_params.password), + verify=False + ) + # Utilization info takes several minutes to converge. + # The IPAM utilization bar in the GUI as well. Why? + assert r.status_code >= 200 and r.status_code < 300, \ + f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" + capacity_info = r.json() + assert len(capacity_info) == 1, "Requested IPv4 network doesn't exist." + assert 'utilization' in capacity_info[0] + utilization = capacity_info[0]['utilization'] + return utilization + + def _delete_network(network) -> Union[V4ServiceNetwork, V6ServiceNetwork]: """ Delete IPv4 or IPv6 network by CIDR. @@ -642,15 +623,22 @@ if __name__ == '__main__': elif choice == '6': hostname = input("Enter host name (full name w/ domain name): ") - addr = input("Enter IP address to allocate: ") - alloc_ip = _allocate_host(hostname=hostname, addr=addr) + addrv4 = input("Enter IPv4 address to allocate: ") + addrv6 = input("Enter IPv6 address to allocate: ") + alloc_ip = _allocate_host(hostname=hostname, + addrs=(addrv4,addrv6)) print(json.dumps(str(alloc_ip), indent=2)) elif choice == '7': hostname = input("Enter host name (full name w/ domain name): ") - network = input( - "Enter existing network to allocate from (CIDR notation): ") - alloc_ip = _allocate_host(hostname=hostname, network=network) + networkv4 = input( + "Enter existing ipv4 network to use (CIDR notation): ") + networkv6 = input( + "Enter existing ipv6 network to use (CIDR notation): ") + alloc_ip = _allocate_host( + hostname=hostname, + networks=(networkv4, networkv6) + ) print(json.dumps(str(alloc_ip), indent=2)) elif choice == '8': diff --git a/gso/services/ipam.py b/gso/services/ipam.py index c41a1239..d794dd43 100644 --- a/gso/services/ipam.py +++ b/gso/services/ipam.py @@ -55,7 +55,6 @@ def new_service_host(hostname, extattrs=extattrs) -''' if __name__ == '__main__': # sample call flow to allocate two loopback interfaces and a trunk service # new_service_host can be called passing networks or addresses @@ -75,11 +74,9 @@ if __name__ == '__main__': ) lo1_v4_host_address = lo1_service_networks.v4.network_address lo1_v6_host_address = lo1_service_networks.v6.network_address - print(lo1_v4_host_address) - print(lo1_v6_host_address) lo1_host_addresses = HostAddresses(v4=lo1_v4_host_address, v6=lo1_v6_host_address) - new_service_host(hostname=hostname_A, + new_service_host(hostname=hostname_A+"_LO", service_type='LO', host_addresses=lo1_host_addresses) @@ -94,7 +91,7 @@ if __name__ == '__main__': new_service_networks(service_type='LO', comment="Network for h2 LO", extattrs=lo2_network_extattrs) - new_service_host(hostname=hostname_B, + new_service_host(hostname=hostname_B+"_LO", service_type='LO', service_networks=lo2_service_networks, extattrs=lo2_host_extattrs) @@ -104,10 +101,9 @@ if __name__ == '__main__': service_type='TRUNK', comment="Network for h1-h2 TRUNK" ) - new_service_host(hostname=hostname_A, + new_service_host(hostname=hostname_A+"_TRUNK", service_type='TRUNK', service_networks=trunk12_service_networks) - new_service_host(hostname=hostname_B, + new_service_host(hostname=hostname_B+"_TRUNK", service_type='TRUNK', service_networks=trunk12_service_networks) -''' diff --git a/test/test_ipam.py b/test/test_ipam.py index 27781483..0ea33535 100644 --- a/test/test_ipam.py +++ b/test/test_ipam.py @@ -81,16 +81,34 @@ def test_new_service_host(data_config_filename): responses.add( method=responses.POST, - url=re.compile(r'.*/wapi.*/network.*/.*?_function=next_available_ip&num=1.*'), # noqa: E501 + url=re.compile(r'.*/wapi.*/network.*/.*?_function=next_available_ip&num=1$'), # noqa: E501 json={'ips': ['10.255.255.20']} ) responses.add( method=responses.POST, - url=re.compile(r'.*/wapi.*/ipv6network.*/.*?_function=next_available_ip&num=1.*'), # noqa: E501 + url=re.compile(r'.*/wapi.*/ipv6network.*/.*?_function=next_available_ip&num=1$'), # noqa: E501 json={'ips': ['dead:beef::18']} ) + responses.add( + method=responses.POST, + url=re.compile(r'.*/wapi.*/network.*_return_fields.*'), + json={ + '_ref': 'network/ZG5zLm5ldHdvcmskMTAuMjU1LjI1NS4yMC8zMi8w:10.255.255.20/32/default', # noqa: E501 + 'network': '10.255.255.20/32' + } + ) + + responses.add( + method=responses.POST, + url=re.compile(r'.*/wapi.*/ipv6network.*_return_fields.*'), + json={ + '_ref': 'ipv6network/ZG5zLm5ldHdvcmskZGVhZDpiZWVmOjoxOC8xMjgvMA:dead%3Abeef%3A%3A18/128/default', # noqa: E501 + 'network': 'dead:beef::18/128' + } + ) + service_hosts = ipam.new_service_host( hostname='test', service_type='LO', @@ -116,3 +134,12 @@ def test_new_service_host(data_config_filename): v4=ipaddress.ip_address('10.255.255.20'), v6=ipaddress.ip_address('dead:beef::18') ) + + service_hosts = ipam.new_service_host( + hostname='test', + service_type='LO' + ) + assert service_hosts == ipam.HostAddresses( + v4=ipaddress.ip_address('10.255.255.20'), + v6=ipaddress.ip_address('dead:beef::18') + ) -- GitLab