-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsymd.py
More file actions
executable file
·556 lines (494 loc) · 19.5 KB
/
symd.py
File metadata and controls
executable file
·556 lines (494 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
#!/usr/bin/env python
##############################################################################
# Copyright The IETF Trust 2019, All Rights Reserved
# Copyright (c) 2015 Cisco Systems All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License v1.0 which accompanies this distribution,
# and is available at http://www.eclipse.org/legal/epl-v10.html
##############################################################################
from __future__ import print_function # Must be at the beginning of the file
import argparse
import re
import sys
import matplotlib.pyplot as plt
import networkx as nx
from utility.utility import list_files_by_extensions
__author__ = 'Jan Medved, Eric Vyncke'
__copyright__ = 'Copyright(c) 2015, Cisco Systems, Inc., Copyright The IETF Trust 2019, All Rights Reserved'
__license__ = 'Eclipse Public License v1.0'
__email__ = 'jmedved@cisco.com, evyncke@cisco.com'
G = nx.DiGraph()
# Regular expressions for parsing yang files; we are only interested in
# the 'module', 'import' and 'revision' statements
MODULE_STATEMENT = re.compile(r'''^[ \t]*(sub)?module +(["'])?([-A-Za-z0-9]*(@[0-9-]*)?)(["'])?.*$''') # noqa: Q001
IMPORT_STATEMENT = re.compile(
r'''^[ \t]*import[\s]*([-A-Za-z0-9]*)?[\s]*\{([\s]*prefix[\s]*[\S]*;[\s]*})?.*$''', # noqa: Q001
)
INCLUDE_STATEMENT = re.compile(r'''^[ \t]*include[\s]*([-A-Za-z0-9]*)?[\s]*\{.*$''') # noqa: Q001
REVISION_STATEMENT = re.compile(r'''^[ \t]*revision[\s]*(['"])?([-0-9]*)?(['"])?[\s]*\{.*$''') # noqa: Q001
# Node Attribute Types
# All those attributes do not fare well in network 2.*
TAG_ATTR = 'tag'
IMPORT_ATTR = 'imports'
TYPE_ATTR = 'type'
REV_ATTR = 'revision'
# Tags
RFC_TAG = 'rfc'
DRAFT_TAG = 'draft'
UNKNOWN_TAG = 'unknown'
def warning(s):
"""
Prints out a warning message to stderr.
:param s: The warning string to print
:return: None
"""
print('WARNING: %s' % s, file=sys.stderr)
def error(s):
"""
Prints out an error message to stderr.
:param s: The error string to print
:return: None
"""
print('ERROR: %s' % s, file=sys.stderr)
def get_local_yang_files(local_repos: list[str], recurse: bool = False) -> list[str]:
"""
Gets the list of all yang module files in the specified local repositories
:param local_repos: List of local repositories, i.e. directories where yang modules may be located
:return: list of all *.yang files in the local repositories
"""
yfs = []
for repo in local_repos:
yfs.extend(
list_files_by_extensions(
repo,
('yang',),
return_full_paths=True,
recursive=recurse,
follow_links=True,
),
)
return yfs
def parse_yang_module(lines):
"""
Parses a yang module; look for the 'module', 'import'/'include' and
'revision' statements
:param lines: Pre-parsed yang files as a list of lines
:return: module name, module type (module or sub-module), list of
imports and list of revisions
"""
module = None
mod_type = None
revisions = []
imports = []
for line in lines:
match = MODULE_STATEMENT.match(line)
if match:
module = match.groups()[2]
if match.groups()[0] == 'sub':
mod_type = 'sub'
else:
mod_type = 'mod'
match = IMPORT_STATEMENT.match(line)
if match:
imports.append(match.groups()[0])
match = INCLUDE_STATEMENT.match(line)
if match:
imports.append(match.groups()[0])
match = REVISION_STATEMENT.match(line)
if match:
revisions.append(match.groups()[1])
return module, mod_type, imports, revisions
def get_yang_modules(yfiles, tag):
"""
Creates a list of yang modules from the specified yang files and stores
them as nodes in a Networkx directed graph. This function also stores
node attributes (list of imports, tag, revision, ...) for each module
in the NetworkX data structures. The function uses the global variable
G (directed network graph of yang model dependencies)
:param yfiles: List of files containing yang modules
:param tag: Tag - RFC or draft for now
:return: None; resulting nodes are stored in G.
"""
for yf in yfiles:
try:
with open(yf, encoding='latin-1', errors='ignore') as yfd:
name, mod_type, imports, revisions = parse_yang_module(yfd.readlines())
if len(revisions) > 0:
rev = max(revisions)
else:
error("No revision specified for module '%s', file '%s'" % (name, yf))
rev = None
# IF we already have a module with a lower revision, replace it now
try:
en = G.nodes[name]
en_rev = en['revision']
if en_rev:
if rev:
if rev > en_rev:
warning("Replacing revision for module '%s' ('%s' -> '%s')" % (name, en_rev, rev))
G.nodes[name]['revision'] = rev
G.nodes[name]['imports'] = imports
else:
if rev:
warning("Replacing revision for module '%s' ('%s' -> '%s')" % (name, en_rev, rev))
G.nodes[name]['revision'] = rev
G.nodes[name]['imports'] = imports
except KeyError:
G.add_node(name, type=mod_type, tag=tag, imports=imports, revision=rev)
except IOError as ioe:
print(ioe)
def prune_graph_nodes(graph, tag):
"""
Filers graph nodes to only nodes that are tagged with the specified tag
:param graph: Original graph to prune
:param tag: Tag for nodes of interest
:return: List of nodes tagged with the specified tag
"""
node_list = []
for node_name in graph.nodes():
try:
if graph.nodes[node_name]['tag'] == tag:
# if graph.nodes[node_name]['attr_dict'][TAG_ATTR] == tag:
node_list.append(node_name)
except KeyError:
pass
return node_list
def get_module_dependencies():
"""
Creates the dependencies between modules (i.e. the edges) in the NetworkX
directed graph created by 'get_yang_modules()'
This function uses the global variable G (directed network graph of yang
modules)
:return: None
"""
# TODO should probably avoid calling this function in all functions...
attr_dict = nx.get_node_attributes(G, 'imports')
for node_name in G.nodes():
for imp in attr_dict[node_name]:
if imp in G:
G.add_edge(node_name, imp)
else:
error("Module '%s': imports unknown module '%s'" % (node_name, imp))
def get_unknown_modules():
unknown_nodes = []
# Next line is added
attr_dict = nx.get_node_attributes(G, 'imports')
for node_name in G.nodes():
for imp in attr_dict[node_name]:
if imp not in G:
unknown_nodes.append(imp)
warning("Module '%s': imports module '%s' that was not scanned" % (node_name, imp))
for un in unknown_nodes:
G.add_node(un, type='module', tag=UNKNOWN_TAG, imports=[], revision=None)
def print_impacting_modules(single_node=None):
"""
For each module, print a list of modules that the module is depending on,
i.e. modules whose change can potentially impact the module. The function
shows all levels of dependency, not just the immediately imported
modules.
:return:
"""
print('\n===Impacting Modules===')
for node_name in G.nodes():
if single_node and (node_name != single_node):
continue
descendants = nx.descendants(G, node_name)
print(augment_format_string(node_name, '\n%s:') % node_name)
for d in descendants:
print(augment_format_string(d, ' %s') % d)
def augment_format_string(node_name, fmts):
"""
Depending on the tag for the specified node, this function will add
a marker to the specified format string. Tags can currently be 'rfc'
or 'draft', the marker is '*' (asterisk)
:param node_name: Node name to query
:param fmts: format string to augment
:return: Augmented format string
"""
module_tag = G.nodes[node_name]['tag']
# module_tag = G.nodes[node_name]['attr_dict'][TAG_ATTR]
if module_tag == RFC_TAG:
return fmts + ' *'
if module_tag == UNKNOWN_TAG:
return fmts + ' (?)'
return fmts
def print_impacted_modules(single_node=None):
"""
For each module, print a list of modules that depend on the module, i.e.
modules that would be impacted by a change in this module. The function
shows all levels of dependency, not just the immediately impacted
modules.
:return:
"""
print('\n===Impacted Modules===')
for node_name in G.nodes():
if single_node and (node_name != single_node):
continue
ancestors = nx.ancestors(G, node_name)
if len(ancestors) > 0:
print(augment_format_string(node_name, '\n%s:') % node_name)
for a in ancestors:
print(augment_format_string(a, ' %s') % a)
def get_subgraph_for_node(node_name):
"""
Prints the dependency graph for only the specified node_name (a full dependency
graph can be difficult to read).
:param node_name: Node for which to print the sub-graph
:return:
"""
ancestors = nx.ancestors(G, node_name)
ancestors.add(node_name)
return nx.subgraph(G, ancestors)
def print_dependents(graph, preamble_list, imports):
"""
Print the immediate dependencies (imports/includes), and for each
immediate dependency print its dependencies
:param graph: Dictionary containing the subgraph of dependencies that
we are about to print
:param preamble_list: Preamble list, list of string to print out before each
dependency (Provides the offset for higher order dependencies)
:param imports: List of immediate imports/includes
:return:
"""
# Create the preamble string for the current level
preamble = ''
for preamble_string in preamble_list:
preamble += preamble_string
# Print a newline for the current level
print(preamble + ' |')
for i in range(len(imports)):
print(augment_format_string(imports[i], preamble + ' +--> %s') % imports[i])
# Determine if a dependency has dependencies on its own; if yes,
# print them out before moving onto the next dependency
try:
imp_imports = graph[imports[i]]
if i < (len(imports) - 1):
preamble_list.append(' | ')
else:
preamble_list.append(' ')
print_dependents(graph, preamble_list, imp_imports)
preamble_list.pop(-1)
# Only print a newline if we're NOT the last processed module
if i < (len(imports) - 1):
print(preamble + ' |')
except KeyError:
pass
def print_dependency_tree():
"""
For each module, print the dependency tree for imported modules
:return: None
"""
print('\n=== Module Dependency Trees ===')
for node_name in G.nodes():
if G.nodes[node_name]['tag'] != UNKNOWN_TAG:
# if G.nodes[node_name]['attr_dict'][TAG_ATTR] != UNKNOWN_TAG:
dg = nx.dfs_successors(G, node_name)
plist = []
print(augment_format_string(node_name, '\n%s:') % node_name)
if len(dg):
imports = dg[node_name]
print_dependents(dg, plist, imports)
def prune_standalone_nodes():
"""
Remove from the module dependency graph all modules that do not have any
dependencies (i.e they neither import/include any modules nor are they
imported/included by any modules)
:return: the connected module dependency graph
"""
ng = nx.DiGraph(G)
for node_name in G.nodes():
ancestors = nx.ancestors(G, node_name)
descendants = nx.descendants(G, node_name)
if len(ancestors) == 0 and len(descendants) == 0:
ng.remove_node(node_name)
return ng
def get_dependent_modules():
print('\n===Dependent Modules===')
for node_name in G.nodes():
dependents = tuple(nx.bfs_predecessors(G, node_name))
if len(dependents):
print(dependents)
def init(rfc_repos, draft_repos, recurse=False):
"""
Initialize the dependency graph
:param rfc_repos: List of local repositories for yang modules defined in
IETF RFCs
:param draft_repos: List of local repositories for yang modules defined in
IETF drafts
:return: None
"""
rfc_yang_files = get_local_yang_files(rfc_repos, recurse)
print("\n*** Scanning %d RFC yang module files for 'import' and 'revision' statements..." % len(rfc_yang_files))
get_yang_modules(rfc_yang_files, RFC_TAG)
num_rfc_modules = len(G.nodes())
print('\n*** Found %d RFC yang modules.' % num_rfc_modules)
draft_yang_files = get_local_yang_files(draft_repos, recurse)
print("\n*** Scanning %d draft yang module files for 'import' and 'revision' statements..." % len(draft_yang_files))
get_yang_modules(draft_yang_files, DRAFT_TAG)
num_draft_modules = len(G.nodes()) - num_rfc_modules
print('\n*** Found %d draft yang modules.' % num_draft_modules)
print('\n*** Analyzing imports...')
get_unknown_modules()
num_unknown_modules = len(G.nodes()) - (num_rfc_modules + num_draft_modules)
print('\n*** Found %d imported/included yang modules that were scanned.' % num_unknown_modules)
print('\n*** Creating module dependencies...')
get_module_dependencies()
print('\nInitialization finished.\n')
def plot_module_dependency_graph(graph):
# def plot_module_dependency_graph(graph, node):
"""
Plot a graph of specified yang modules. this function is used to plot
both the full dependency graph of all yang modules in the DB, or a
subgraph of dependencies for a specified module
:param graph: Graph to be plotted
:return: None
"""
# fixed_positions = {node: [0.5, 0.5] }
# print(fixed_positions)
pos = nx.spring_layout(graph, iterations=50, center=[0.5, 0.5], weight=2, k=0.6)
# EVY pos = nx.spring_layout(graph, iterations=2000, threshold=1e-5, fixed=fixed_positions, k=k, center=[0.5, 0.5])
# pos = nx.spring_layout(graph, iterations=2000, threshold=1e-6)
print(pos)
# Draw RFC nodes (yang modules) in red
nx.draw_networkx_nodes(
graph,
pos=pos,
nodelist=prune_graph_nodes(graph, RFC_TAG),
node_size=200,
node_shape='s',
node_color='red',
alpha=0.5,
linewidths=0.25,
label='RFC',
)
# Draw draft nodes (yang modules) in green
nx.draw_networkx_nodes(
graph,
pos=pos,
nodelist=prune_graph_nodes(graph, DRAFT_TAG),
node_size=150,
node_shape='o',
node_color='green',
alpha=0.5,
linewidths=0.25,
label='Draft',
)
# Draw unknown nodes (yang modules) in green
nx.draw_networkx_nodes(
graph,
pos=pos,
nodelist=prune_graph_nodes(graph, UNKNOWN_TAG),
node_size=200,
node_shape='^',
node_color='orange',
alpha=1.0,
linewidths=0.25,
label='Unknown',
)
# Draw edges in light gray (fairly transparent)
nx.draw_networkx_edges(graph, pos=pos, alpha=0.25, arrows=False)
# Draw labels on nodes (modules)
nx.draw_networkx_labels(graph, pos=pos, font_size=14, font_weight='normal', alpha=1.0)
##############################################################################
# symd - Show Yang Module Dependencies.
#
# A program to analyze and show dependencies between yang modules
##############################################################################
if __name__ == '__main__':
# Set matplotlib into non-interactive mode
plt.interactive(False)
parser = argparse.ArgumentParser(description='Show the dependency graph for a set of yang models.')
parser.add_argument(
'--draft-repos',
help='List of local directories where models defined in IETF drafts are located.',
type=str,
nargs='+',
default=['./'],
)
parser.add_argument(
'--rfc-repos',
help='List of local directories where models defined in IETF RFC are located.',
type=str,
nargs='+',
default=['./'],
)
parser.add_argument(
'-r',
'--recurse',
help='Recurse into directories specified to find yang models',
action='store_true',
default=False,
)
g = parser.add_mutually_exclusive_group()
g.add_argument('--graph', help='Plot the overall dependency graph.', action='store_true', default=False)
g.add_argument(
'--sub-graphs',
help='Plot the dependency graphs for the specified modules.',
type=str,
nargs='+',
default=[],
)
g.add_argument(
'--impact-analysis',
help='For each scanned yang module, print the impacting and impacted modules.',
action='store_true',
default=False,
)
g.add_argument(
'--single-impact-analysis',
help='For a single yang module, print the impacting and impacted modules',
type=str,
)
g.add_argument(
'--dependency-tree',
help='For each scanned yang module, print its dependency tree to stdout '
'(i.e. show all the modules that it depends on).',
action='store_true',
default=False,
)
g.add_argument(
'--single-dependency-tree',
help='For a single yang module, print its dependency tree to stdout '
'(i.e. show all the modules that it depends on).',
type=str,
)
args = parser.parse_args()
init(args.rfc_repos, args.draft_repos, recurse=args.recurse)
if args.dependency_tree:
print_dependency_tree()
if args.single_dependency_tree:
print_dependency_tree()
if args.impact_analysis:
print_impacting_modules()
print_impacted_modules()
if args.single_impact_analysis:
print_impacting_modules(single_node=args.single_impact_analysis)
print_impacted_modules(single_node=args.single_impact_analysis)
plot_num = 1
if args.graph:
# Set matplotlib into non-interactive mode
plt.interactive(False)
ng = prune_standalone_nodes()
plt.figure(plot_num, figsize=(40, 40))
plot_num += 1
print('Plotting the overall dependency graph...')
plot_module_dependency_graph(ng)
plt.savefig('modules.png')
print(' Done.')
for node in args.sub_graphs:
# Set matplotlib into non-interactive mode
plt.interactive(False)
plt.figure(plot_num, figsize=(20, 20))
# plt.figure(plot_num, figsize=(40, 40))
plot_num += 1
print(f'Plotting graph for module \'{node}\'...')
try:
# EVY: do we need this extract argument ???
# plot_module_dependency_graph(get_subgraph_for_node(node), node)
plot_module_dependency_graph(get_subgraph_for_node(node))
plt.savefig('%s.png' % node)
print(' Done.')
except nx.NetworkXError as e:
print(' %s' % e)
print('\n')