diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 4c9f4c580f3bc23e0f5a2514125cfa7f7c2e4d86..73b7a6302bb3bd971af17b051d64f10888797fca 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -8,13 +8,20 @@ "username": "robot-user", "password": "robot-user-password" }, - "TRUNK": { + "LO": { "V4": {"container": "1.1.0.0/24", "mask": 31}, - "V6": {"container": "dead:beef::/64", "mask": 96} + "V6": {"container": "dead:beef::/64", "mask": 126}, + "domain_name": ".lo" + }, + "TRUNK": { + "V4": {"container": "1.1.1.0/24", "mask": 31}, + "V6": {"container": "dead:beef::/64", "mask": 126}, + "domain_name": ".trunk" }, "GEANT_IP": { - "V4": {"container": "1.1.8.0/24", "mask": 31}, - "V6": {"container": "dead:beef::/64", "mask": 96} + "V4": {"container": "1.1.2.0/24", "mask": 31}, + "V6": {"container": "dead:beef::/64", "mask": 126}, + "domain_name": ".geantip" } } } \ No newline at end of file diff --git a/gso/services/_ipam.py b/gso/services/_ipam.py index 58715aedc705e5e5fef538ed3228dbb5a26a1e76..1c9f7d4c1e667694b78a8a1ee4bb7ed36fc14fde 100644 --- a/gso/services/_ipam.py +++ b/gso/services/_ipam.py @@ -66,6 +66,10 @@ def _ip_network_version(network): return ip_version def find_containers(network=None, ip_version=4): + """ + If network is not None, find the container that contains specified network. + Otherwise find all containers. + """ assert ip_version in [4, 6] oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX @@ -75,24 +79,62 @@ def find_containers(network=None, ip_version=4): f'{_wapi(infoblox_params)}/{endpoint}', params={'network': network} if network else None, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" return r.json() -def find_networks(network=None, ip_version=4): +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 find all networks. + """ assert ip_version in [4, 6] oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX infoblox_params = oss.IPAM.INFOBLOX endpoint = 'network' if ip_version == 4 else 'ipv6network' + params = None + if network_container: + params={'network_container': network_container} + elif network: + params={'network': network} r = requests.get( f'{_wapi(infoblox_params)}/{endpoint}', - params={'network': network} if network else None, + params=params, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" 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 + ) + 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], ip_version=4): assert ip_version in [4, 6] endpoint = 'network' if ip_version == 4 else 'ipv6network' @@ -133,7 +175,8 @@ def _allocate_network(infoblox_params: settings.InfoBloxParams, network_params: json=req_payload, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), headers={'content-type': "application/json"}, - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" allocated_network = r.json()['network'] @@ -143,6 +186,9 @@ def _allocate_network(infoblox_params: settings.InfoBloxParams, network_params: return V6ServiceNetwork(v6=allocated_network) def allocate_service_ipv4_network(service_type): + """ + Allocate IPv4 network within the container of the specified service type. + """ oss = settings.load_oss_params() assert oss.IPAM ipam_params = oss.IPAM @@ -150,6 +196,9 @@ def allocate_service_ipv4_network(service_type): return _allocate_network(ipam_params.INFOBLOX, getattr(ipam_params, service_type).V4, 4) def allocate_service_ipv6_network(service_type): + """ + Allocate IPv6 network within the container of the specified service type. + """ oss = settings.load_oss_params() assert oss.IPAM ipam_params = oss.IPAM @@ -157,6 +206,9 @@ def allocate_service_ipv6_network(service_type): return _allocate_network(ipam_params.INFOBLOX, getattr(ipam_params, service_type).V6, 6) def delete_network(network): + """ + Delete IPv4 or IPv6 network by CIDR. + """ oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX infoblox_params = oss.IPAM.INFOBLOX @@ -170,7 +222,8 @@ def delete_network(network): r = requests.delete( f'{_wapi(infoblox_params)}/{network_info[0]["_ref"]}', auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" # Extract ipv4/ipv6 address from the network reference obtained in the response @@ -185,14 +238,20 @@ def _find_next_available_ip(infoblox_params, network_ref): r = requests.post( f'{_wapi(infoblox_params)}/{network_ref}?_function=next_available_ip&num=1', auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + 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 'ips' in r.json() received_ip = r.json()['ips'] assert len(received_ip) == 1 return received_ip[0] -def allocate_host(mac=None, hostname=None, addr=None, network=None, service_type=None): +def allocate_host(hostname=None, addr=None, network=None): + """ + If network is not None, allocate host in that network. + Otherwise if addr is not None, allocate host with that address. + """ + assert addr or network, "You must specify either the host address or the network CIDR." oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX infoblox_params = oss.IPAM.INFOBLOX @@ -208,11 +267,12 @@ def allocate_host(mac=None, hostname=None, addr=None, network=None, service_type else: ip_version = _ip_addr_version(addr) + # TODO: use same request for both IP and DNS records + ipv4_req_payload = { "ipv4addrs": [ { - "ipv4addr": addr, - "mac": mac + "ipv4addr": addr } ], "name": hostname, @@ -237,7 +297,8 @@ def allocate_host(mac=None, hostname=None, addr=None, network=None, service_type f'{_wapi(infoblox_params)}/record:host', json=ip_req_payload, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + 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("record:host/") @@ -261,7 +322,8 @@ def allocate_host(mac=None, hostname=None, addr=None, network=None, service_type f'{_wapi(infoblox_params)}/{endpoint}', json=dns_req_payload, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + 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}/") @@ -271,7 +333,49 @@ def allocate_host(mac=None, hostname=None, addr=None, network=None, service_type else: return V6HostAddress(v6=addr) +def allocate_host_by_service_type(hostname=None, service_type=None): + """ + Allocate host with both IPv4 and IPv6 address (and respective DNS records). + The first network with available space for this service type is used. + The domain name is also taken from the service type and appended to specified hostname. + """ + oss = settings.load_oss_params() + assert oss.IPAM + ipam_params = oss.IPAM + assert ipam_params.INFOBLOX + infoblox_params = ipam_params.INFOBLOX + + 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 + domain_name = getattr(ipam_params, service_type).domain_name + + ipv4_networks_info = find_networks(network_container=str(ipv4_container), 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 + # TODO: create a new network if available space in the container. + 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) + + # 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) + assert len(ipv6_networks_info) >= 1, "No IPv6 network exists in the container for this service type." + assert 'network' in ipv6_networks_info[0] + v6_host = allocate_host(hostname=hostname+domain_name, network=ipv6_networks_info[0]['network']) + + return HostAddresses(v4=v4_host,v6=v6_host) + def delete_host_by_ip(addr): + """ + Delete IPv4 or IPv6 host by its address. + """ oss = settings.load_oss_params() assert oss.IPAM.INFOBLOX infoblox_params = oss.IPAM.INFOBLOX @@ -284,7 +388,8 @@ def delete_host_by_ip(addr): f'{_wapi(infoblox_params)}/record:host', params={ip_param: addr}, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) host_data = r.json() assert len(host_data) == 1, "Host does not exist." assert '_ref' in host_data[0] @@ -294,7 +399,8 @@ def delete_host_by_ip(addr): r = requests.delete( f'{_wapi(infoblox_params)}/{host_ref}', auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" # Also find and delete the associated dns a/aaaa record @@ -304,7 +410,8 @@ def delete_host_by_ip(addr): f'{_wapi(infoblox_params)}/{endpoint}', params={ip_param: addr}, auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) dns_data = r.json() assert len(dns_data) == 1, "DNS record does not exist." assert '_ref' in dns_data[0] @@ -313,7 +420,8 @@ def delete_host_by_ip(addr): r = requests.delete( f'{_wapi(infoblox_params)}/{dns_ref}', auth=HTTPBasicAuth(infoblox_params.username, infoblox_params.password), - verify=False) + verify=False + ) assert r.status_code >= 200 and r.status_code < 300, f"HTTP error {r.status_code}: {r.reason}\n\n{r.text}" if ip_version == 4: @@ -325,13 +433,14 @@ if __name__ == '__main__': while True: print("1. Find all containers") print("2. Find all networks") - print("3. Create new network") - print("4. Delete network") - print("5. Allocate host by IP") - print("6. Allocate host by network CIDR") - print("7. Allocate host by service type") - print("8. Delete host by IP") - print("9. Exit") + print("3. Get network capacity") + print("4. Create new network") + print("5. Delete network") + print("6. Allocate host by IP") + print("7. Allocate host by network CIDR") + print("8. Allocate host by service type") + print("9. Delete host by IP") + print("10. Exit") choice = input("Enter your choice: ") @@ -342,10 +451,15 @@ if __name__ == '__main__': elif choice == '2': ip_version = int(input("Enter IP version (4 or 6): ")) - networks = find_networks(ip_version=ip_version) + networks = find_networks(ip_version=ip_version) #, network_container="10.255.255.0/24" print(json.dumps(networks, indent=2)) elif choice == '3': + network = input("Enter network (in CIDR notation): ") + network_capacity = get_network_capacity(network=network) + print(json.dumps(network_capacity, indent=2)) + + elif choice == '4': service_type = input("Enter service type: ") ip_version = int(input("Enter IP version (4 or 6): ")) if ip_version == 4: @@ -357,40 +471,35 @@ if __name__ == '__main__': continue print(json.dumps(str(new_network), indent=2)) - elif choice == '4': + elif choice == '5': network = input("Enter network to delete (in CIDR notation): ") deleted_network = delete_network(network=network) print(json.dumps(str(deleted_network), indent=2)) - elif choice == '5': + elif choice == '6': hostname = input("Enter host name: ") addr = input("Enter IP address to allocate: ") - mac = ":".join(["{:02x}".format(random.randint(0x00, 0xff)) for i in range(6)]) #input("Enter MAC address (you can leave empty if IPv6): ") - alloc_ip = allocate_host(hostname=hostname, addr=addr, mac=mac) + alloc_ip = allocate_host(hostname=hostname, addr=addr) print(json.dumps(str(alloc_ip), indent=2)) - elif choice == '6': + elif choice == '7': hostname = input("Enter host name: ") network = input("Enter an existing network to allocate from (in CIDR notation): ") - mac = ":".join(["{:02x}".format(random.randint(0x00, 0xff)) for i in range(6)]) #input("Enter MAC address (you can leave empty if IPv6): ") - alloc_ip = allocate_host(hostname=hostname, network=network, mac=mac) + alloc_ip = allocate_host(hostname=hostname, network=network) print(json.dumps(str(alloc_ip), indent=2)) - elif choice == '7': - print("Not implemented.") - continue + elif choice == '8': hostname = input("Enter host name: ") service_type = input("Enter service type: ") - mac = ":".join(["{:02x}".format(random.randint(0x00, 0xff)) for i in range(6)]) #input("Enter MAC address (you can leave empty if IPv6): ") - alloc_ip = allocate_host(hostname=hostname, service_type=service_type, mac=mac) + alloc_ip = allocate_host_by_service_type(hostname=hostname, service_type=service_type) print(json.dumps(str(alloc_ip), indent=2)) - elif choice == '8': + elif choice == '9': addr = input("Enter IP address of host to delete: ") deleted_host = delete_host_by_ip(addr=addr) print(json.dumps(str(deleted_host), indent=2)) - elif choice == '9': + elif choice == '10': print("Exiting...") break diff --git a/gso/services/ipam.py b/gso/services/ipam.py index bea4dee9afd76cf94c3a2cc26574fb53eaad9d8d..14c80ce782c1920e9e3ca3ac5b85cf7e3468fe57 100644 --- a/gso/services/ipam.py +++ b/gso/services/ipam.py @@ -39,14 +39,10 @@ def new_service_networks(service_type) -> ServiceNetworks: v6=v6_service_network) -def new_device_lo_address() -> HostAddresses: - oss = settings.load_oss_params() - assert oss.IPAM.INFOBLOX - # TODO: load from ipam - return HostAddresses( - v4=ipaddress.IPv4Address('10.10.10.10'), - v6=ipaddress.IPv6Address('fc00:798:aa:1::10')) +def new_device_lo_address(hostname) -> HostAddresses: + return _ipam.allocate_host_by_service_type(hostname=hostname, service_type='LO') if __name__ == '__main__': - new_service_networks('TRUNK') \ No newline at end of file + #new_service_networks('TRUNK') + new_device_lo_address('newhost1') \ No newline at end of file diff --git a/gso/settings.py b/gso/settings.py index 0846e5f2ae2f983f7216666df34a8388e0697853..91c1251538fec86151b5582b5ec4b44a40aa682b 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -25,10 +25,12 @@ class V6NetworkParams(BaseSettings): class ServiceNetworkParams(BaseSettings): V4: V4NetworkParams V6: V6NetworkParams + domain_name: str class IPAMParams(BaseSettings): INFOBLOX: InfoBloxParams + LO: ServiceNetworkParams TRUNK: ServiceNetworkParams GEANT_IP: ServiceNetworkParams