55'''
66
77import copy
8+ import fnmatch
89import re
910import 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+
106186def 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
170249def validate_group_data (
171250 topology : Box ,
0 commit comments