Skip to content

Commit 232d81c

Browse files
authored
Migrate from netaddr to ipaddress (#1926)
* Most netaddr calls have been replaced with ipaddress calls, the major exception is MAC address handling which is not available in ipaddress * The 'get ip from interface' and 'get prefix from interface' functions have been moved to 'utils.routing' and most other modules use those functions * Direct calls to ipaddress functions are use only for more advanced use cases. * Error messages in augment.addressing and augment.links have been simplified and include parsing error information from ipaddress * 'get_addr_mask' and 'get_nth_ip_from_prefix' functions did almost the same thing and have been merged into a single function * Extra properties available in ipaddress make it easier to check if an address is a network address, multicast address, or a broadcast address Note: * Sometimes we still cheat and merge IP address string with a prefix length string to get a full prefix. There's no good way to get that done with ipaddress functions (you cannot change the prefix length in IPv4Network/IPv6Network) * Some error checking code has been removed from old modules; that functionality is now available as generic data validation
1 parent 29158ff commit 232d81c

File tree

26 files changed

+186
-199
lines changed

26 files changed

+186
-199
lines changed

Diff for: netsim/augment/addressing.py

+19-22
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import typing
5353
import types
5454

55+
import ipaddress
5556
import netaddr
5657
from box import Box
5758

@@ -65,7 +66,7 @@ def normalize_prefix(pfx: typing.Union[str,Box]) -> Box:
6566
# Normalize IP addr strings, e.g. 2001:001::/48 becomes 2001:1::/48
6667
def normalize_ip(ip:typing.Union[str,bool]) -> typing.Union[str,bool]:
6768
try:
68-
return str(netaddr.IPNetwork(ip)) if isinstance(ip,str) else ip
69+
return str(ipaddress.ip_network(ip)) if isinstance(ip,str) else ip
6970
except Exception as ex:
7071
log.error(
7172
f'Cannot parse address prefix: {ex}',
@@ -152,14 +153,12 @@ def validate_pools(addrs: Box, topology: Box) -> None:
152153
if k in pfx:
153154
if not isinstance(pfx[k],bool):
154155
try:
155-
network = netaddr.IPNetwork(pfx[k])
156-
if str(network.cidr) != pfx[k]:
157-
log.error( f"pool '{pool}' is using an invalid prefix {pfx[k]} with host bits set ({str(network.cidr)})",
158-
category=log.IncorrectValue, module='addressing')
156+
network = ipaddress.ip_network(pfx[k])
159157
addrs[pool][k+'_pfx'] = network
160-
except:
158+
except Exception as Ex:
161159
log.error(
162-
"%s prefix %s in addressing pool '%s' is invalid (%s)" % (k,pfx[k],pool,sys.exc_info()[1]),
160+
f"{k} prefix {pfx[k]} in addressing pool '{pool}' is invalid",
161+
more_data=str(Ex),
163162
category=log.IncorrectValue,
164163
module='addressing')
165164
continue
@@ -221,7 +220,7 @@ def create_pool_generators(addrs: Box, no_copy_list: list) -> Box:
221220
if "_pfx" in key:
222221
af = key.replace('_pfx','')
223222
plen = pfx['prefix'] if af == 'ipv4' else pfx.get('prefix6',64)
224-
gen[pool][af] = data.subnet(plen)
223+
gen[pool][af] = data.subnets(new_prefix=plen)
225224
if (af == 'ipv4' and plen == 32) or (af == 'ipv6' and plen >= 127) or (pool == 'loopback'):
226225
next(gen[pool][af])
227226
elif not key in no_copy_list:
@@ -239,7 +238,7 @@ def get_pool(pools: Box, pool_list: typing.List[str]) -> typing.Optional[str]:
239238
module='addressing') # pragma: no cover (impossible to get here due to built-in default pools)
240239
return None # pragma: no cover
241240

242-
def get_nth_subnet(n: int, subnet: netaddr.IPNetwork.subnet, cache_list: list) -> netaddr.IPNetwork:
241+
def get_nth_subnet(n: int, subnet: types.GeneratorType, cache_list: list) -> ipaddress._BaseNetwork:
243242
while len(cache_list) < n:
244243
cache_list.append(next(subnet))
245244
return cache_list[n-1]
@@ -253,7 +252,7 @@ def get_pool_prefix(pools: Box, p: str, n: typing.Optional[int] = None) -> Box:
253252
prefixes[af] = pools[p][af]
254253
continue
255254

256-
subnet_cache = 'cache_%s' % af
255+
subnet_cache = f'cache_{af}'
257256
if n: # Allocating a specific prefix or IP address from a subnet
258257
if not subnet_cache in pools[p]: # Set up a cache to speed up things
259258
pools[p][subnet_cache] = []
@@ -365,9 +364,13 @@ def parse_prefix(prefix: typing.Union[str,Box],path: str = 'links') -> Box:
365364
evaluate_named_prefix(topology,prefix)
366365
return topology.prefix[prefix]
367366
try:
368-
return get_box({ 'ipv4' : netaddr.IPNetwork(prefix) })
367+
return get_box({ 'ipv4' : ipaddress.IPv4Network(prefix) })
369368
except Exception as ex:
370-
log.error(f'Cannot parse {prefix} used in {path} as an IPv4 prefix',log.IncorrectValue,'addressing')
369+
log.error(
370+
f'Cannot parse {prefix} used in {path} as an IPv4 prefix',
371+
more_data=str(ex),
372+
category=log.IncorrectValue,
373+
module='addressing')
371374
return empty_box
372375

373376
if not isinstance(prefix,Box): # pragma: no cover
@@ -394,23 +397,17 @@ def parse_prefix(prefix: typing.Union[str,Box],path: str = 'links') -> Box:
394397
continue
395398

396399
try:
397-
prefix_list[af] = netaddr.IPNetwork(pfx)
400+
prefix_list[af] = ipaddress.ip_network(pfx)
398401
except Exception as ex:
399402
log.error(
400403
f'Cannot parse {af} prefix: {prefix} used in {path}',
401404
more_data= [ str(ex) ],
402405
category=log.IncorrectValue,
403406
module='addressing')
404407
return empty_box
405-
if str(prefix_list[af]) != str(prefix_list[af].cidr):
406-
log.error(
407-
f'{af} prefix used in {path} contains host bits: {prefix}',
408-
category=log.IncorrectValue,
409-
module='addressing')
410408

411409
return prefix_list
412410

413-
def get_addr_mask(pfx: netaddr.IPNetwork, host: int) -> str:
414-
host_ip = netaddr.IPNetwork(pfx[host])
415-
host_ip.prefixlen = pfx.prefixlen
416-
return str(host_ip)
411+
def get_nth_ip_from_prefix(pfx: typing.Union[str,ipaddress._BaseNetwork], host: int) -> str:
412+
net = ipaddress.ip_network(pfx) if isinstance(pfx,str) else pfx
413+
return f'{str(net[host])}/{net.prefixlen}'

Diff for: netsim/augment/links.py

+32-45
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
import typing
66

7-
import netaddr
7+
import ipaddress
88
from box import Box
99

1010
# Related modules
@@ -373,7 +373,7 @@ def set_fhrp_gateway(link: Box, pfx_list: Box, nodes: Box, link_path: str) -> No
373373
continue
374374

375375
try: # Now try to get N-th IP address on that link
376-
link.gateway[af] = get_nth_ip_from_prefix(netaddr.IPNetwork(link.prefix[af]),link.gateway.id)
376+
link.gateway[af] = addressing.get_nth_ip_from_prefix(link.prefix[af],link.gateway.id)
377377
fhrp_assigned = True
378378
except Exception as ex:
379379
log.error(
@@ -468,15 +468,15 @@ def assign_link_prefix(
468468
469469
Return 'error' if the prefix size is too small
470470
"""
471-
def get_prefix_IPAM_policy(link: Box, pfx: typing.Union[netaddr.IPNetwork,bool], ndict: Box) -> str:
471+
def get_prefix_IPAM_policy(link: Box, pfx: typing.Union[ipaddress._BaseNetwork,bool], ndict: Box) -> str:
472472
if isinstance(pfx,bool):
473473
return 'unnumbered'
474474

475475
gwid = get_gateway_id(link) or 0 # Get link gateway ID (if set) --- must be int for min to work
476476
if link.type == 'p2p' and not gwid: # P2P allocation policy cannot be used with default gateway
477-
return 'p2p' if pfx.first != pfx.last else 'error'
477+
return 'p2p' if pfx.num_addresses > 1 else 'error'
478478

479-
pfx_size = pfx.last - pfx.first + 1
479+
pfx_size = pfx.num_addresses
480480
add_extra_ip = 0
481481
subtract_reserved_ip = -2
482482

@@ -501,19 +501,6 @@ def get_prefix_IPAM_policy(link: Box, pfx: typing.Union[netaddr.IPNetwork,bool],
501501

502502
return 'error'
503503

504-
"""
505-
Get Nth IP address in a prefix returned as a nice string with a subnet mask
506-
507-
*** WARNING *** WARNING *** WARNING ***
508-
509-
Parent must catch the exception as we don't know what error text to display
510-
"""
511-
512-
def get_nth_ip_from_prefix(pfx: netaddr.IPNetwork, n_id: int) -> str:
513-
node_addr = netaddr.IPNetwork(pfx[n_id])
514-
node_addr.prefixlen = pfx.prefixlen
515-
return str(node_addr)
516-
517504
"""
518505
check_interface_host_bits: Check whether the IP addresses on an interface have host bits. The check is disabled
519506
for special prefixes (IPv4: /31, /32, IPv6: /127, /128)
@@ -533,27 +520,27 @@ def check_interface_host_bits(intf: Box, node: Box, link: typing.Optional[Box] =
533520
continue
534521
if not isinstance(intf[af],str): # Skip unnumbered interfaces
535522
continue
536-
pfx = netaddr.IPNetwork(intf[af])
537-
if pfx[0].is_multicast():
523+
pfx = ipaddress.ip_interface(intf[af])
524+
if pfx.ip.is_multicast:
538525
log.error(
539526
f'Interfaces cannot have multicast {af} addresses ({intf[af]} on {node.name}/{intf_name})',
540527
log.IncorrectValue,
541528
'links')
542529
OK = False
543530
continue
544531

545-
if pfx.last <= pfx.first + 1: # Are we dealing with special prefix (loopback or /31)
532+
if pfx.network.num_addresses <= 2: # Are we dealing with special prefix (loopback or /31)
546533
continue # ... then it's OK not to have host bits
547534

548-
if str(pfx) == str(pfx.cidr): # Does the IP address have host bits -- is it different from its CIDR subnet?
535+
if pfx.ip == pfx.network.network_address: # Does the IP address have host bits -- is it different from its CIDR subnet?
549536
log.error(
550537
f'Address {intf[af]} on node {node.name}/{intf_name} does not contain host bits',
551538
log.IncorrectValue,
552539
'links')
553540
OK = False
554541
continue
555542

556-
if str(pfx[-1]) == str(pfx.ip) and af == 'ipv4':
543+
if pfx.ip == pfx.network.broadcast_address:
557544
log.error(
558545
f'Address {intf[af]} on node {node.name}/{intf_name} is a subnet broadcast address',
559546
log.IncorrectValue,
@@ -566,7 +553,7 @@ def check_interface_host_bits(intf: Box, node: Box, link: typing.Optional[Box] =
566553
"""
567554
Set an interface address based on the link prefix and interface sequential number (could be node.id or counter)
568555
"""
569-
def set_interface_address(intf: Box, af: str, pfx: netaddr.IPNetwork, node_id: int) -> bool:
556+
def set_interface_address(intf: Box, af: str, pfx: ipaddress._BaseNetwork, node_id: int) -> bool:
570557
if af in intf: # Check static interface addresses
571558
if isinstance(intf[af],bool): # unnumbered or unaddressed node, leave it alone
572559
return True
@@ -575,22 +562,23 @@ def set_interface_address(intf: Box, af: str, pfx: netaddr.IPNetwork, node_id: i
575562
node_id = intf[af]
576563
elif isinstance(intf[af],str): # static address specified on the interface
577564
try:
578-
intf_pfx = netaddr.IPNetwork(intf[af]) # Try to parse the interface IP address
579-
if not '/' in intf[af]:
580-
intf_pfx.prefixlen = pfx.prefixlen # If it lacks a prefix, add link prefix
565+
if '/' not in intf[af]: # Add link prefix if needed
566+
intf[af] += f'/{pfx.prefixlen}'
567+
intf_pfx = ipaddress.ip_interface(intf[af])
581568
intf[af] = str(intf_pfx) # ... and save modified/validated interface IP address
582569
except Exception as ex:
583570
log.error(
584-
f'Cannot parse {af} address {intf.af} for node {intf.node}\n'+strings.extra_data_printout(str(ex)),
585-
log.IncorrectValue,
586-
'links')
571+
f'Cannot parse {af} address {intf.af} for node {intf.node}',
572+
more_data=str(ex),
573+
category=log.IncorrectValue,
574+
module='links')
587575
return False
588576

589577
return True # Further checks done in check_interface_host_bits
590578

591579
# No static interface address, or static address specified as relative node_id
592580
try:
593-
intf[af] = get_nth_ip_from_prefix(pfx,node_id)
581+
intf[af] = addressing.get_nth_ip_from_prefix(pfx,node_id)
594582
return True
595583
except Exception as ex:
596584
log.error(
@@ -625,8 +613,8 @@ def IPAM_unnumbered(link: Box, af: str, pfx: typing.Optional[bool], ndict: Box)
625613

626614
return OK
627615

628-
def IPAM_sequential(link: Box, af: str, pfx: netaddr.IPNetwork, ndict: Box) -> bool:
629-
start = 1 if pfx.last != pfx.first + 1 else 0
616+
def IPAM_sequential(link: Box, af: str, pfx: ipaddress._BaseNetwork, ndict: Box) -> bool:
617+
start = 1 if pfx.num_addresses > 2 else 0
630618
gwid = get_gateway_id(link)
631619
OK = True
632620
for count,intf in enumerate(link.interfaces):
@@ -636,25 +624,25 @@ def IPAM_sequential(link: Box, af: str, pfx: netaddr.IPNetwork, ndict: Box) -> b
636624

637625
return OK
638626

639-
def IPAM_p2p(link: Box, af: str, pfx: netaddr.IPNetwork, ndict: Box) -> bool:
640-
start = 1 if pfx.last != pfx.first + 1 else 0
627+
def IPAM_p2p(link: Box, af: str, pfx: ipaddress._BaseNetwork, ndict: Box) -> bool:
628+
start = 1 if pfx.num_addresses > 2 else 0
641629
OK = True
642630
for count,intf in enumerate(sorted(link.interfaces, key=lambda intf: intf.node)):
643631
OK = set_interface_address(intf,af,pfx,count+start) and OK
644632

645633
return OK
646634

647-
def IPAM_id_based(link: Box, af: str, pfx: netaddr.IPNetwork, ndict: Box) -> bool:
635+
def IPAM_id_based(link: Box, af: str, pfx: ipaddress._BaseNetwork, ndict: Box) -> bool:
648636
OK = True
649637
for intf in link.interfaces:
650638
OK = set_interface_address(intf,af,pfx,ndict[intf.node].id) and OK
651639

652640
return OK
653641

654-
def IPAM_loopback(link: Box, af: str, pfx: netaddr.IPNetwork, ndict: Box) -> bool:
642+
def IPAM_loopback(link: Box, af: str, pfx: ipaddress._BaseNetwork, ndict: Box) -> bool:
655643
for intf in link.interfaces:
656-
pfx.prefixlen = 128 if ':' in str(pfx) else 32
657-
intf[af] = str(pfx)
644+
prefixlen = 128 if af == 'ipv6' else 32
645+
intf[af] = f'{str(pfx.network_address)}/{prefixlen}'
658646

659647
return True
660648

@@ -699,14 +687,13 @@ def assign_interface_addresses(link: Box, addr_pools: Box, ndict: Box, defaults:
699687
pfx_net = pfx_list[af]
700688
else:
701689
try: # Parse the AF prefix
702-
pfx_net = netaddr.IPNetwork(pfx_list[af])
690+
pfx_net = ipaddress.ip_network(pfx_list[af])
703691
except Exception as ex: # Report an error and move on if it cannot be parsed
704692
log.error(
705-
f'Cannot parse {af} prefix {pfx_list[af]} on {link._linkname}\n' + \
706-
strings.extra_data_printout(f'{ex}') + '\n' + \
707-
strings.extra_data_printout(f'{link}'),
708-
log.IncorrectValue,
709-
'links')
693+
f'Cannot parse {af} prefix {pfx_list[af]} on {link._linkname}',
694+
more_data=[ str(ex), str(link) ],
695+
category=log.IncorrectValue,
696+
module='links')
710697
error = True
711698
continue
712699

Diff for: netsim/augment/nodes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def loopback_interface(n: Box, pools: Box, topology: Box) -> None:
360360
if prefix_list[af].prefixlen == 128:
361361
n.loopback[af] = str(prefix_list[af])
362362
else:
363-
n.loopback[af] = addressing.get_addr_mask(prefix_list[af],1)
363+
n.loopback[af] = addressing.get_nth_ip_from_prefix(prefix_list[af],1)
364364
else:
365365
n.loopback[af] = str(prefix_list[af])
366366
n.af[af] = True

Diff for: netsim/data/types.py

+8-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Data validation routines
33
#
44

5-
import typing,typing_extensions,types
5+
import typing,typing_extensions
66
import functools
77
import ipaddress
88
import netaddr
@@ -743,21 +743,14 @@ def transform_to_ipv6(value: typing.Any) -> dict:
743743
return { '_type': 'IPv4 or IPv6 prefix' }
744744

745745
try:
746-
parse = netaddr.IPNetwork(value) # now let's check if we have a valid address
746+
parse = ipaddress.ip_network(value) # now let's check if we have a valid prefix
747747
except Exception as ex:
748-
return check_named_prefix(value) or { '_value': "IPv4, IPv6, or named prefix" }
748+
return check_named_prefix(value) or { '_value': f"IPv4, IPv6, or named prefix" }
749749

750-
if parse.network != parse.ip:
751-
return { '_value': "IPv4 or IPv6 prefix without the host bits" }
752-
753-
try: # ... and finally we have to check it's a true IPv4 address
754-
parse.ipv4()
755-
if not parse.is_ipv4_mapped():
756-
return { '_valid': True, '_transform': transform_to_ipv4 }
757-
except Exception as ex:
758-
pass
759-
760-
return { '_valid': True, '_transform': transform_to_ipv6 }
750+
if isinstance(parse,ipaddress.IPv4Network):
751+
return { '_valid': True, '_transform': transform_to_ipv4 }
752+
else:
753+
return { '_valid': True, '_transform': transform_to_ipv6 }
761754

762755
@type_test()
763756
def must_be_named_pfx(value: typing.Any) -> dict:
@@ -832,9 +825,7 @@ def must_be_rd(value: typing.Any) -> dict:
832825
rdt_parsed = int(rdt)
833826
except Exception as ex:
834827
try:
835-
if '/' in rdt:
836-
return { '_value': "route distinguisher in asn:id or ip:id format" }
837-
netaddr.IPNetwork(rdt)
828+
ipaddress.IPv4Address(rdt)
838829
except Exception as ex:
839830
return { '_value': "an RD in asn:id or ip:id format where asn is an integer or ip is an IPv4 address" }
840831

Diff for: netsim/extra/ebgp.multihop/plugin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from box import Box
2-
import netaddr
2+
import ipaddress
33

44
from netsim.utils import log,routing as _bgp
55
from netsim.modules import vrf
@@ -177,7 +177,7 @@ def fix_vrf_loopbacks(ndata: Box, topology: Box) -> None:
177177
for af in ('ipv4','ipv6'): # Now copy remote VRF loopback IPv4/IPv6 address
178178
ngb.pop(af) # ... into neighbor data
179179
if af in lb: # ... removing whatever might have come from the
180-
ngb[af] = str(netaddr.IPNetwork(lb[af]).ip) # ... global loopback
180+
ngb[af] = str(ipaddress.ip_interface(lb[af]).ip) # ... global loopback
181181

182182
if '_src_vrf' in ngb: # Is out endpoint in a VRF?
183183
if not isinstance(features.bgp.multihop,Box) or features.bgp.multihop.vrf is not True:

Diff for: netsim/extra/mlag.vtep/plugin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def pre_link_transform(topology: Box) -> None:
3737
if 'vxlan' in node.module:
3838
vtep_loopback = data.get_empty_box()
3939
vtep_loopback.type = 'loopback' # Assign same static IP to both nodes
40-
vtep_loopback.interfaces = [ { 'node': node_name, 'ipv4': str(vtep_a.network)+"/32" } ]
40+
vtep_loopback.interfaces = [ { 'node': node_name, 'ipv4': str(vtep_a.network_address)+"/32" } ]
4141
vtep_loopback._linkname = f"MLAG VTEP VXLAN interface shared between {' - '.join(peers)}"
4242
vtep_loopback.vxlan.vtep = True
4343
topology.links.append(vtep_loopback)

Diff for: netsim/extra/multilab/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import typing, netaddr
1+
import typing
22
from box import Box
33
from netsim import utils,data
44

Diff for: netsim/extra/proxy-arp/plugin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import typing, netaddr
1+
import typing
22
from box import Box
33

44
from netsim import api

0 commit comments

Comments
 (0)