Skip to content

Commit

Permalink
Add Sort List of IPs (networktocode#473)
Browse files Browse the repository at this point in the history
* feat: Add method to sort list of CIDRs
  • Loading branch information
jdrew82 authored Mar 9, 2024
1 parent a2be149 commit 62b3fc6
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/user/include_jinja_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
48 changes: 48 additions & 0 deletions netutils/ip.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Functions for working with IP addresses."""

import ipaddress
import typing as t
from operator import attrgetter
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions netutils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"paloalto_panos_brace_to_set": "config.conversion.paloalto_panos_brace_to_set",
"get_upgrade_path": "os_version.get_upgrade_path",
"hash_data": "hash.hash_data",
"get_ips_sorted": "ip.get_ips_sorted",
}


Expand Down
64 changes: 64 additions & 0 deletions tests/unit/test_ip.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test for the IP functions."""

import ipaddress
import pytest

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")

0 comments on commit 62b3fc6

Please sign in to comment.