diff --git a/docs/user/include_jinja_list.md b/docs/user/include_jinja_list.md index 7c67d29a..11845433 100644 --- a/docs/user/include_jinja_list.md +++ b/docs/user/include_jinja_list.md @@ -34,6 +34,7 @@ | get_all_host | netutils.ip.get_all_host | | get_broadcast_address | netutils.ip.get_broadcast_address | | get_first_usable | netutils.ip.get_first_usable | +| get_ips_sorted | netutils.ip.get_ips_sorted | | get_peer_ip | netutils.ip.get_peer_ip | | get_range_ips | netutils.ip.get_range_ips | | get_usable_range | netutils.ip.get_usable_range | diff --git a/netutils/ip.py b/netutils/ip.py index 7926e3c5..267e7329 100644 --- a/netutils/ip.py +++ b/netutils/ip.py @@ -1,4 +1,5 @@ """Functions for working with IP addresses.""" + import ipaddress import typing as t from operator import attrgetter @@ -596,3 +597,50 @@ def get_usable_range(ip_network: str) -> str: lower_bound = str(net[1]) upper_bound = str(net[-2]) return f"{lower_bound} - {upper_bound}" + + +def get_ips_sorted(ips: t.Union[str, t.List[str]], sort_type: str = "network") -> t.List[str]: + """Given a concatenated list of CIDRs sorts them into the correct order and returns them as a list. + + Examples: + >>> from netutils.ip import get_ips_sorted + >>> get_ips_sorted("3.3.3.3,2.2.2.2,1.1.1.1") + ['1.1.1.1/32', '2.2.2.2/32', '3.3.3.3/32'] + >>> get_ips_sorted("10.0.20.0/24,10.0.20.0/23,10.0.19.0/24") + ['10.0.19.0/24', '10.0.20.0/23', '10.0.20.0/24'] + >>> get_ips_sorted("10.0.20.0/24,10.0.20.0/23,10.0.19.0/24", "interface") + ['10.0.19.0/24', '10.0.20.0/23', '10.0.20.0/24'] + >>> get_ips_sorted("10.0.20.20/24,10.0.20.1/23,10.0.19.5/24", "interface") + ['10.0.19.5/24', '10.0.20.1/23', '10.0.20.20/24'] + >>> get_ips_sorted(["10.0.20.20", "10.0.20.1", "10.0.19.5"], "address") + ['10.0.19.5', '10.0.20.1', '10.0.20.20'] + + Args: + ips (t.Union[str, t.List[str]]): Concatenated string list of CIDRs, IPAddresses, or Interfaces or list of the same strings. + sort_type (str): Whether the passed list are networks, IP addresses, or interfaces, ie "address", "interface", or "network". + + Returns: + t.List[str]: Sorted list of sort_type IPs. + """ + if sort_type not in ["address", "interface", "network"]: + raise ValueError("Invalid sort type passed. Must be `address`, `interface`, or `network`.") + if isinstance(ips, list): + ips_list = ips + elif (isinstance(ips, str) and "," not in ips) or not isinstance(ips, str): + raise ValueError("Not a concatenated list of IPs as expected.") + elif isinstance(ips, str): + ips_list = ips.replace(" ", "").split(",") + + functions: t.Dict[str, t.Callable[[t.Any], t.Any]] = { + "address": ipaddress.ip_address, + "interface": ipaddress.ip_interface, + "network": ipaddress.ip_network, + } + + try: + sorted_list = sorted(functions[sort_type](ip) for ip in ips_list) + if sort_type in ["interface", "network"]: + return [cidrs.with_prefixlen for cidrs in sorted_list] + return [str(ip) for ip in sorted_list] + except ValueError as err: + raise ValueError(f"Invalid IP of {sort_type} input: {err}") from err diff --git a/netutils/utils.py b/netutils/utils.py index 82677419..b9ff9416 100644 --- a/netutils/utils.py +++ b/netutils/utils.py @@ -91,6 +91,7 @@ "os_platform_object_builder": "platform_mapper.os_platform_object_builder", "juniper_junos_version_parser": "os_version.juniper_junos_version_parser", "hash_data": "hash.hash_data", + "get_ips_sorted": "ip.get_ips_sorted", } diff --git a/tests/unit/test_ip.py b/tests/unit/test_ip.py index f3898382..b76447e0 100644 --- a/tests/unit/test_ip.py +++ b/tests/unit/test_ip.py @@ -1,4 +1,5 @@ """Test for the IP functions.""" + import ipaddress import pytest @@ -471,6 +472,49 @@ {"sent": {"ip_network": "224.0.0.0/24"}, "received": False}, ] +SORTED_IPS = [ + { + "sent": "10.0.10.0/24,10.0.100.0/24,10.0.12.0/24,10.0.200.0/24", + "expected": ["10.0.10.0/24", "10.0.12.0/24", "10.0.100.0/24", "10.0.200.0/24"], + "sort_type": "network", + }, + { + "sent": "10.0.10.0/24, 10.0.100.0/24, 10.0.12.0/24, 10.0.200.0/24", + "expected": ["10.0.10.0/24", "10.0.12.0/24", "10.0.100.0/24", "10.0.200.0/24"], + "sort_type": "network", + }, + { + "sent": "192.168.1.1,10.1.1.2,172.16.10.1", + "expected": ["10.1.1.2", "172.16.10.1", "192.168.1.1"], + "sort_type": "address", + }, + { + "sent": "192.168.1.1/24,10.1.1.2/32,172.16.10.1/16", + "expected": ["10.1.1.2/32", "172.16.10.1/16", "192.168.1.1/24"], + "sort_type": "interface", + }, + { + "sent": "10.0.0.0/24, 10.0.0.0/16, 10.0.0.0/18", + "expected": ["10.0.0.0/16", "10.0.0.0/18", "10.0.0.0/24"], + "sort_type": "network", + }, + { + "sent": ["10.0.10.0/24", "10.0.100.0/24", "10.0.12.0/24", "10.0.200.0/24"], + "expected": ["10.0.10.0/24", "10.0.12.0/24", "10.0.100.0/24", "10.0.200.0/24"], + "sort_type": "network", + }, + { + "sent": ["192.168.1.1", "10.1.1.2", "172.16.10.1"], + "expected": ["10.1.1.2", "172.16.10.1", "192.168.1.1"], + "sort_type": "address", + }, + { + "sent": ["192.168.1.1/24", "10.1.1.2/32", "172.16.10.1/16"], + "expected": ["10.1.1.2/32", "172.16.10.1/16", "192.168.1.1/24"], + "sort_type": "interface", + }, +] + @pytest.mark.parametrize("data", IP_TO_HEX) def test_ip_to_hex(data): @@ -617,3 +661,23 @@ def test_ipaddress_network(data): @pytest.mark.parametrize("data", IS_CLASSFUL) def test_is_classful(data): assert ip.is_classful(**data["sent"]) == data["received"] + + +@pytest.mark.parametrize("data", SORTED_IPS) +def test_get_ips_sorted(data): + assert data["expected"] == ip.get_ips_sorted(data["sent"], sort_type=data["sort_type"]) + + +def test_get_ips_sorted_exception_invalid_list(): + with pytest.raises(ValueError, match="Not a concatenated list of IPs as expected."): + ip.get_ips_sorted("10.1.1.1/24 10.2.2.2/16") + + +def test_get_ips_sorted_exception_invalid_instance_type(): + with pytest.raises(ValueError, match="Not a concatenated list of IPs as expected."): + ip.get_ips_sorted({"10.1.1.1/24", "10.2.2.2/16"}) + + +def test_get_ips_sorted_invalid_sort_type(): + with pytest.raises(ValueError, match="Invalid sort type passed. Must be `address`, `interface`, or `network`."): + ip.get_ips_sorted("10.0.0.0/24,192.168.0.0/16", sort_type="wrong_type")