Skip to content

Commit 0f6d153

Browse files
authored
[B] Set node attributes from device/all groups (ipspace#2745)
The device/all groups are not allowed to have static members. It was thus impossible to set node attributes in a device/all group. This change adds dynamic calculation of group members for device/all groups. Making that work reliably required several other changes: * The 'set node device/module' function has to apply group values in two passes. It has to apply group.device in the first pass and group.module in the second pass because changing node.device could change group membership for a device group. * Computing members of a BGP autogroup now takes into account whether the global BGP module will be propagated into a node during the module initialization. * The 'should we propagate modules' check was refactored into a separate function because it's used during module initialization as well as during BGP autogroup creation. * node.device could be unspecified when BGP autogroup function calls the 'propagate_global_modules' check, but we know the only possible value that could be applied later on is the default device (because group-based devices were already set).
1 parent 3a9cb62 commit 0f6d153

File tree

5 files changed

+105
-24
lines changed

5 files changed

+105
-24
lines changed

netsim/augment/groups.py

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@
1515
from ..data import get_box
1616
from ..data.types import must_be_dict, must_be_id, must_be_list, transform_asdot
1717
from ..data.validate import get_object_attributes, validate_attributes
18-
from ..modules import bgp
18+
from ..modules import bgp, propagate_global_modules
1919
from ..utils import log
2020

21+
'''
22+
Return computed members of special-purpose node group ('all' or device)
23+
'''
24+
def special_node_group_members(group: str, topology: Box) -> typing.Optional[list]:
25+
if group == 'all':
26+
return list(topology.nodes)
27+
28+
if group in topology.defaults.devices:
29+
return [ node for node,ndata in topology.nodes.items() if ndata.get('device',None) == group ]
30+
31+
return None
32+
2133
'''
2234
Return members of the specified group. Recurse through child groups if needed
2335
'''
@@ -37,10 +49,16 @@ def group_members(topology: Box, group: str, grp_type: str = 'node', count: int
3749
header=True)
3850

3951
gdata = topology.groups[group]
40-
if gdata.get('type','node') != grp_type:
52+
g_type = gdata.get('type','node')
53+
if g_type != grp_type:
4154
return []
4255

43-
for m in gdata.members:
56+
if g_type == 'node':
57+
sg_members = special_node_group_members(group,topology)
58+
if not sg_members is None:
59+
return sg_members
60+
61+
for m in gdata.get('members',[]):
4462
m_ns = grp_type + 's'
4563
if m in topology[m_ns]:
4664
members = members + [ m ]
@@ -236,6 +254,11 @@ def check_group_data_structure(
236254
more_hints='Device names or "all" cannot be used as names for user-defined groups',
237255
category=log.IncorrectValue,
238256
module='groups')
257+
if grp in topology.defaults.devices and 'device' in gdata:
258+
log.error(
259+
text=f'Trying to change the device to {gdata.device} in device group {grp} is ridiculous',
260+
category=log.IncorrectAttr,
261+
module='groups')
239262

240263
must_be_list(gdata,'module',gpath,create_empty=False,module='groups',valid_values=sorted(list_of_modules))
241264

@@ -452,39 +475,64 @@ def reverse_topsort(topology: Box) -> list:
452475
Copy group-level module or device setting into node data
453476
'''
454477
def copy_group_device_module(topology: Box) -> None:
455-
for grp in reverse_topsort(topology):
478+
sorted_groups = reverse_topsort(topology)
479+
for grp in sorted_groups: # First, set the device type
456480
gdata = topology.groups[grp]
457-
if not 'device' in gdata and not 'module' in gdata:
458-
continue # This group is not interesting, move on
481+
if not 'device' in gdata:
482+
continue # This group is not interesting, move on
459483

460484
if log.debug_active('groups'):
461-
print(f'Setting device/module for group {grp}')
485+
print(f'Setting device for group {grp}: {gdata.device}')
462486
g_members = group_members(topology,grp)
463487
if not g_members and not gdata.get('_default_group',False):
464488
log.error(
465-
f'Cannot use "module" or "device" attribute on in group {grp} that has no direct or indirect members',
489+
f'Cannot use "device" attribute in group {grp} that has no direct or indirect members',
466490
log.IncorrectValue,
467491
'groups')
468492
continue
469493

470-
for name in g_members: # Iterate over group members
471-
if not name in topology.nodes: # Member not a node? Move on...
494+
for name in g_members: # Iterate over group members
495+
if not name in topology.nodes: # Member not a node? Weird, move on...
472496
continue
473497

474498
ndata = topology.nodes[name]
475-
if 'device' in gdata and not 'device' in ndata: # Copy device from group data to node data
499+
if 'device' not in ndata: # Copy device from group data to node data
476500
ndata.device = gdata.device
477501
if log.debug_active('groups'):
478502
print(f'... setting {name}.device to {gdata.device}')
479503

480-
if 'module' in gdata: # Merge group modules with device modules
481-
ndata.module = ndata.module or [] # Make sure node.module is a list
482-
for m in gdata.module: # Now iterate over group modules
483-
if not m in ndata.module: # ... and add missing modules to nodes
484-
ndata.module.append(m)
504+
for grp in sorted_groups: # Next, augment device modules
505+
gdata = topology.groups[grp] # We have to do this after augmenting devices
506+
# ... because modules could be specified on a device group
507+
if 'module' not in gdata: # No module specified on this group
508+
continue # ... move on
485509

486-
if log.debug_active('groups'):
487-
print(f'... adding module {gdata.module} to {name}. Node modules now {ndata.module}')
510+
if log.debug_active('groups'):
511+
print(f'Setting module for group {grp}: {gdata.module}')
512+
g_members = group_members(topology,grp)
513+
if not g_members and not gdata.get('_default_group',False):
514+
log.error(
515+
f'Cannot use "module" attribute in group {grp} that has no direct or indirect members',
516+
log.IncorrectValue,
517+
'groups')
518+
continue
519+
520+
for name in g_members: # Iterate over group members
521+
if not name in topology.nodes: # Member not a node? Move on...
522+
continue
523+
524+
ndata = topology.nodes[name] # Get node data
525+
if 'module' not in ndata: # Group can set module(s) to empty, so we have to
526+
ndata.module = [] # ... start with an empty list
527+
528+
if not isinstance(ndata.module,list): # We'll let someone else deal with incorrect
529+
continue # ... ndata.module data type
530+
531+
for m in gdata.module: # ... iterate over group modules
532+
data.append_to_list(ndata,'module',m) # ... and append group modules to node.module list
533+
534+
if log.debug_active('groups'):
535+
print(f'... adding module {gdata.module} to {name}. Node modules now {ndata.module}')
488536

489537
'''
490538
Copy node data from group into group members
@@ -587,7 +635,12 @@ def create_bgp_autogroups(topology: Box) -> None:
587635
'groups')
588636

589637
for n_name,n_data in topology.nodes.items(): # Now iterate over nodes
590-
n_module = n_data.get('module',g_module) # Get node or global module (global modules haven't been propagated yet)
638+
#
639+
# Get node or global module (global modules haven't been propagated yet) taking into account
640+
# whether the global modules will be propagated into the node
641+
#
642+
g_propagate = propagate_global_modules(n_data,topology)
643+
n_module = n_data.get('module',g_module if g_propagate else [])
591644
if not isinstance(n_module,list): # Node list of modules is insane, someone else will complain
592645
continue
593646

netsim/modules/__init__.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,28 @@ def remove_module(node: Box, module: str, extra: list = []) -> None:
141141

142142
node.module = [ m for m in node.module if m != module ] # Remove module from the list of node modules
143143

144+
"""
145+
Should we propagate global modules to a node?
146+
147+
This function is called during module initialization, but also very early in the
148+
transformation process during BGP group creation, so we cannot assume that
149+
node.device is set
150+
"""
151+
def propagate_global_modules(n: Box, topology: Box) -> bool:
152+
if 'device' not in n: # Device might not be set yet
153+
if 'device' not in topology.defaults: # Do we have a default device?
154+
return False # ... we'll have a problem anyway, so we can return whatever
155+
156+
n = n + {'device': topology.defaults.device} # Assume default device
157+
158+
if n.device not in topology.defaults.devices: # Have to do one more check for valid device type
159+
return False # ... because we cannot assume it has been checked
160+
161+
daemon = devices.get_device_attribute(n,'daemon',topology.defaults) or False
162+
is_host = devices.get_device_attribute(n,'role',topology.defaults) == 'host' or n.get('role') == 'host'
163+
164+
return not is_host or daemon
165+
144166
# Set default list of modules for nodes without specific module list
145167
#
146168
def augment_node_module(topology: Box) -> None:
@@ -160,9 +182,7 @@ def augment_node_module(topology: Box) -> None:
160182
#
161183
# non-host nodes are nodes that do not have 'role' set to 'host' or have 'daemon' set to True
162184
#
163-
daemon = devices.get_device_attribute(n,'daemon',topology.defaults)
164-
is_host = devices.get_device_attribute(n,'role',topology.defaults) == 'host' or n.get('role') == 'host'
165-
if g_module and (not is_host or daemon):
185+
if g_module and propagate_global_modules(n,topology):
166186
n.module = g_module
167187

168188
# Check whether the modules defined on individual nodes are valid module names
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
IncorrectValue in groups: Cannot use "module" or "device" attribute on in group g2 that has no direct or indirect members
1+
IncorrectValue in groups: Cannot use "device" attribute in group g2 that has no direct or indirect members
22
Fatal error in netlab: Cannot proceed beyond this point due to errors, exiting

tests/topology/expected/groups-hierarchy.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ groups:
1111
as65000:
1212
members:
1313
- e
14+
eos:
15+
members: []
16+
module:
17+
- dhcp
1418
g1:
1519
members:
1620
- a
@@ -62,6 +66,7 @@ links:
6266
ipv4: 172.16.0.0/24
6367
type: lan
6468
module:
69+
- dhcp
6570
- ospf
6671
- bgp
6772
name: input
@@ -157,7 +162,8 @@ nodes:
157162
ifname: Management1
158163
ipv4: 192.168.121.103
159164
mac: ca:fe:00:03:00:00
160-
module: []
165+
module:
166+
- dhcp
161167
name: c
162168
role: router
163169
d:

tests/topology/input/groups-hierarchy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ groups:
3030
device: iosv
3131
ospf.area: 51
3232
bgp.as: 65000
33+
eos:
34+
module: [ dhcp ]
3335

3436
links:
3537
- a-e-d

0 commit comments

Comments
 (0)