Skip to content

Commit ce7572d

Browse files
committed
Allow wildcard- and regular expressions in group members (ipspace#2681)
Implements ipspace#2672
1 parent 1f73b87 commit ce7572d

File tree

4 files changed

+97
-12
lines changed

4 files changed

+97
-12
lines changed

docs/groups.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ groups:
3838
members: [ d,e,f ]
3939
```
4040

41+
The elements in the **members** list can be:
42+
43+
* node/VLAN/VRF names
44+
* wildcard expressions, for example, `h*` to match all nodes starting with `h` or `h?` to match all nodes starting with `h` and having one more character in their name
45+
* regular expressions (strings starting with `~`), for example, `~h.` to match all nodes starting with `h` and having one more character in their name.
46+
4147
The custom groups are assumed to contain nodes. To create a VLAN or a VRF group, set the group **type** to **vlan** or **vrf**, for example:
4248

4349
```

netsim/augment/groups.py

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
'''
66

77
import copy
8+
import fnmatch
89
import re
910
import typing
1011

@@ -103,10 +104,89 @@ def check_group_data_sanity(
103104

104105
return True
105106

107+
def group_re_match(m_expr: str, names: list) -> list:
108+
pattern = re.compile(m_expr[1:])
109+
return [ member for member in names if pattern.fullmatch(member) ]
110+
111+
"""
112+
Expand the regular expressions or globs in the list of group members
113+
114+
Most of the function is just error checking and reporting, there's very little
115+
actual work done here
116+
"""
117+
def expand_group_members(
118+
g_members: list, # Members of the current group that have to be checked/expanded
119+
g_objects: Box, # Parent objects (nodes, VLANs, VRFs)
120+
g_list: list, # List of group names (to check hierarchical groups)
121+
g_name: str, # The name of the current group (needed for error messages)
122+
g_type: str, # Group type (node/vlan/vrf)
123+
g_prune: bool = False) -> list: # Prune non-existent group members (used in default groups)
124+
125+
members: typing.List[str] = []
126+
g_names: typing.List[str] = []
127+
for m_id in g_members:
128+
if not isinstance(m_id,str):
129+
log.error(f'Member {m_id} of {g_type} group {g_name} is not a string',log.IncorrectType,module='groups')
130+
continue
131+
132+
# Simple case: the member belongs to the group objects
133+
if m_id in g_objects or m_id in g_list:
134+
members.append(m_id)
135+
136+
# Regular expression, identified by a string starting with ~
137+
elif m_id.startswith('~'):
138+
if not g_names: # Caching group object names just in case we're dealing
139+
g_names = list(g_objects) # ... with a humongous topology
140+
try:
141+
g_match = group_re_match(m_id,g_names)
142+
except Exception as ex:
143+
log.error( # Regex matching failed for some reason
144+
f'Invalid regular expression {m_id} used in {g_type} group {g_name}',
145+
more_data=str(ex),
146+
category=log.IncorrectValue,
147+
module='groups')
148+
continue
149+
if not g_match: # ... or we had no matches
150+
log.error(
151+
f'Regular expression {m_id} used in {g_type} group {g_name} does not match anything',
152+
category=log.IncorrectType,
153+
module='groups')
154+
continue
155+
members.extend(g_match) # All good, add re-matched members
156+
continue
157+
158+
elif re.search('[\\[\\].*?!]',m_id): # Using regexp to identify a potential glob pattern
159+
if not g_names: # Again: get a list of object names when first needed
160+
g_names = list(g_objects)
161+
try:
162+
g_match = fnmatch.filter(g_names,m_id)
163+
except Exception as ex:
164+
log.error( # Regex matching failed for some reason
165+
f'Invalid wildcard expression {m_id} used in {g_type} group {g_name}',
166+
more_data=str(ex),
167+
category=log.IncorrectValue,
168+
module='groups')
169+
continue
170+
if not g_match:
171+
log.error(
172+
f'Wildcard expression {m_id} used in {g_type} group {g_name} does not match anything',
173+
category=log.IncorrectType,
174+
module='groups')
175+
continue
176+
members.extend(g_match)
177+
178+
elif not g_prune:
179+
log.error(
180+
f'Member {m_id} of group {g_name} is not a valid {g_type} or group name',
181+
category=log.IncorrectValue,
182+
module='groups')
183+
184+
return members
185+
106186
def check_group_data_structure(
107187
topology: Box,
108188
parent_path: typing.Optional[str] = '',
109-
prune_members: typing.Optional[bool] = False) -> None:
189+
prune_members: bool = False) -> None:
110190

111191
parent = topology.get(parent_path) if parent_path else topology
112192
grp_namespace = f'{parent_path} ' if parent_path else ''
@@ -116,6 +196,7 @@ def check_group_data_structure(
116196
'''
117197

118198
list_of_modules = modules.list_of_modules(topology)
199+
group_names = list(parent.groups)
119200

120201
for grp,gdata in parent.groups.items():
121202
if grp.startswith('_'): # Skip stuff starting with underscore
@@ -157,15 +238,13 @@ def check_group_data_structure(
157238
if must_be_list(gdata,'members',gpath,create_empty=False,module='groups') is None:
158239
continue
159240

160-
if prune_members:
161-
gdata.members = [ n for n in gdata.members if n in g_objects or n in parent.groups ]
162-
else:
163-
for n in gdata.members:
164-
if not n in g_objects and not n in parent.groups:
165-
log.error(
166-
text=f'Member {n} of {grp_namespace}group {grp} is not a valid {g_type} or group name',
167-
category=log.IncorrectValue,
168-
module='groups')
241+
gdata.members = expand_group_members(
242+
g_members=gdata.members,
243+
g_objects=g_objects,
244+
g_list=group_names,
245+
g_name=grp,
246+
g_type=g_type,
247+
g_prune=prune_members)
169248

170249
def validate_group_data(
171250
topology: Box,

tests/topology/input/group-data-vlan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module: [vlan, ospf]
44

55
groups:
66
g1:
7-
members: [r1, r2]
7+
members: [ '~r[12]' ]
88
vlans:
99
red:
1010
ospf.cost: 10

tests/topology/input/group-data-vrf.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ defaults.groups:
1818

1919
groups:
2020
g1:
21-
members: [r1, r2]
21+
members: r*
2222
provider: clab # Regression test for 2219
2323
vrf.loopback: true
2424

0 commit comments

Comments
 (0)