trigger 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. trigger/__init__.py +7 -0
  2. trigger/acl/__init__.py +32 -0
  3. trigger/acl/autoacl.py +70 -0
  4. trigger/acl/db.py +324 -0
  5. trigger/acl/dicts.py +357 -0
  6. trigger/acl/grammar.py +112 -0
  7. trigger/acl/ios.py +222 -0
  8. trigger/acl/junos.py +422 -0
  9. trigger/acl/models.py +118 -0
  10. trigger/acl/parser.py +168 -0
  11. trigger/acl/queue.py +296 -0
  12. trigger/acl/support.py +1431 -0
  13. trigger/acl/tools.py +746 -0
  14. trigger/bin/__init__.py +0 -0
  15. trigger/bin/acl.py +233 -0
  16. trigger/bin/acl_script.py +574 -0
  17. trigger/bin/aclconv.py +82 -0
  18. trigger/bin/check_access.py +93 -0
  19. trigger/bin/check_syntax.py +66 -0
  20. trigger/bin/fe.py +197 -0
  21. trigger/bin/find_access.py +191 -0
  22. trigger/bin/gnng.py +434 -0
  23. trigger/bin/gong.py +86 -0
  24. trigger/bin/load_acl.py +841 -0
  25. trigger/bin/load_config.py +18 -0
  26. trigger/bin/netdev.py +317 -0
  27. trigger/bin/optimizer.py +638 -0
  28. trigger/bin/run_cmds.py +18 -0
  29. trigger/changemgmt/__init__.py +352 -0
  30. trigger/changemgmt/bounce.py +57 -0
  31. trigger/cmds.py +1217 -0
  32. trigger/conf/__init__.py +94 -0
  33. trigger/conf/global_settings.py +674 -0
  34. trigger/contrib/__init__.py +7 -0
  35. trigger/exceptions.py +307 -0
  36. trigger/gorc.py +172 -0
  37. trigger/netdevices/__init__.py +1288 -0
  38. trigger/netdevices/loader.py +174 -0
  39. trigger/netscreen.py +1030 -0
  40. trigger/packages/__init__.py +6 -0
  41. trigger/packages/peewee.py +8084 -0
  42. trigger/rancid.py +463 -0
  43. trigger/tacacsrc.py +584 -0
  44. trigger/twister.py +2203 -0
  45. trigger/twister2.py +745 -0
  46. trigger/utils/__init__.py +88 -0
  47. trigger/utils/cli.py +349 -0
  48. trigger/utils/importlib.py +77 -0
  49. trigger/utils/network.py +157 -0
  50. trigger/utils/rcs.py +178 -0
  51. trigger/utils/templates.py +81 -0
  52. trigger/utils/url.py +78 -0
  53. trigger/utils/xmltodict.py +298 -0
  54. trigger-2.0.0.dist-info/METADATA +146 -0
  55. trigger-2.0.0.dist-info/RECORD +61 -0
  56. trigger-2.0.0.dist-info/WHEEL +5 -0
  57. trigger-2.0.0.dist-info/entry_points.txt +15 -0
  58. trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
  59. trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
  60. trigger-2.0.0.dist-info/top_level.txt +2 -0
  61. twisted/plugins/trigger_xmlrpc.py +124 -0
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ acl_script - CLI interface to simplify complex modification to access-lists.
5
+ """
6
+
7
+
8
+ # TODO (jathan): Have this import from trigger.acl.utils.AclScript, because
9
+ # much of the code is copypasta.
10
+
11
+ __version__ = "1.2"
12
+
13
+
14
+ import os
15
+ import re
16
+ import sys
17
+ from optparse import OptionParser
18
+
19
+ import trigger.acl.tools as acl_tools
20
+ from trigger.acl.parser import TIP, Comment, parse
21
+ from trigger.utils.cli import yesno
22
+ from trigger.utils.rcs import RCS
23
+
24
+
25
+ def parse_args(argv):
26
+ notes = ""
27
+ parser = OptionParser(
28
+ usage="%prog [options]",
29
+ add_help_option=0,
30
+ description="""ACL modify/generator from the commandline.""",
31
+ )
32
+ parser.add_option("-h", "--help", action="store_false")
33
+ parser.add_option(
34
+ "-a", "--acl", action="append", type="string", help="specify the acl file"
35
+ )
36
+ parser.add_option(
37
+ "-n", "--no-changes", action="store_true", help="don't make the changes"
38
+ )
39
+ parser.add_option(
40
+ "--show-mods",
41
+ action="store_true",
42
+ help="show modifications being made in a simple format.",
43
+ )
44
+ parser.add_option(
45
+ "--no-worklog", action="store_true", help="don't make a worklog entry"
46
+ )
47
+ parser.add_option(
48
+ "-N",
49
+ "--no-input",
50
+ action="store_true",
51
+ help="require no input (good for scripts)",
52
+ )
53
+ parser.add_option(
54
+ "-s",
55
+ "--source-address",
56
+ action="append",
57
+ type="string",
58
+ default=[],
59
+ help="define a source address",
60
+ )
61
+ parser.add_option(
62
+ "-d",
63
+ "--destination-address",
64
+ action="append",
65
+ type="string",
66
+ default=[],
67
+ help="define a destination address",
68
+ )
69
+ parser.add_option(
70
+ "--destination-address-from-file",
71
+ action="append",
72
+ type="string",
73
+ default=[],
74
+ help="read a set of destination-addresses from a file",
75
+ )
76
+ parser.add_option(
77
+ "--source-address-from-file",
78
+ default=[],
79
+ action="append",
80
+ type="string",
81
+ help="read a set of source-addresses from a file",
82
+ )
83
+ parser.add_option(
84
+ "--protocol",
85
+ default=[],
86
+ action="append",
87
+ type="string",
88
+ help="define a protocol",
89
+ )
90
+ parser.add_option(
91
+ "-p",
92
+ "--source-port",
93
+ default=[],
94
+ action="append",
95
+ type="int",
96
+ help="define a source-port",
97
+ )
98
+ parser.add_option(
99
+ "",
100
+ "--source-port-range",
101
+ default=[],
102
+ action="append",
103
+ type="string",
104
+ nargs=2,
105
+ help="define a source-port range",
106
+ )
107
+ parser.add_option(
108
+ "",
109
+ "--destination-port-range",
110
+ default=[],
111
+ action="append",
112
+ type="string",
113
+ nargs=2,
114
+ help="define a destination-port range",
115
+ )
116
+ parser.add_option(
117
+ "-P",
118
+ "--destination-port",
119
+ default=[],
120
+ action="append",
121
+ type="int",
122
+ help="define a destination port",
123
+ )
124
+ parser.add_option(
125
+ "-t",
126
+ "--modify-specific-term",
127
+ default=[],
128
+ action="append",
129
+ type="string",
130
+ help="""When modifying a JUNOS type ACL, you may specify this option one or more times
131
+ to define a specific JUNOS term you want to modify. This takes one argument
132
+ which should be the name of term.
133
+ """,
134
+ )
135
+ parser.add_option(
136
+ "-c",
137
+ "--modify-between-comments",
138
+ action="append",
139
+ type="string",
140
+ nargs=2,
141
+ help="""When modifying a IOS type ACL, you may specify this option one or more times
142
+ to define a specific AREA of the ACL you want to modify. You must have at least
143
+ 2 comments defined in the ACL prior to running. This requires two arguments, the
144
+ start comment, and the end comment. Your modifications will be done between the
145
+ two.
146
+ """,
147
+ )
148
+ parser.add_option(
149
+ "-C",
150
+ "--comment",
151
+ type="string",
152
+ help="Add a comment when making the modification.",
153
+ )
154
+ parser.add_option(
155
+ "--insert-defined",
156
+ default=False,
157
+ action="store_true",
158
+ help="""This option works differently based on the type of ACL we are modifying. The
159
+ one similar characteristic is that this will never remove any access already defined, just
160
+ append.
161
+ """,
162
+ )
163
+
164
+ notes += """
165
+ NOTE when using --insert-defined:
166
+ On a JUNOS type acl using --insert-defined, this will only replace parts of the
167
+ term that have been specified on the command-line. This may sound confusing
168
+ but this example should clear things up.
169
+
170
+ Take the following term:
171
+ term sr31337 {
172
+ from {
173
+ source-address {
174
+ 10.0.0.0/8;
175
+ 11.0.0.0/8;
176
+ }
177
+ destination-address {
178
+ 192.168.0.1/32;
179
+ }
180
+ destination-port 80;
181
+ protocol tcp;
182
+ }
183
+ then {
184
+ accept;
185
+ count sr31337;
186
+ }
187
+ }
188
+
189
+ If you run this with the following arguments:
190
+ --modify-specific-term sr31337 --source-address 5.5.5.5/32 --destination-port 81 --insert-defined
191
+ The following term will be generated:
192
+ term sr31337 {
193
+ from {
194
+ source-address {
195
+ 5.5.5.5/32;
196
+ 10.0.0.0/8;
197
+ 11.0.0.0/8;
198
+ }
199
+ destination-address {
200
+ 192.168.0.1/32;
201
+ }
202
+ destination-port 80-81;
203
+ protocol tcp;
204
+ }
205
+ then {
206
+ accept;
207
+ count sr31337;
208
+ }
209
+ }
210
+
211
+ As you can see 5.5.5.5/32 was added to the source-address portion, and 81
212
+ was added as a destination-port. Notice that all other fields were left
213
+ alone.
214
+
215
+ On a IOS type acl using --insert-defined, this will only add access where
216
+ it is needed. Take the following example:
217
+
218
+ !!! I AM L33T
219
+ access-list 101 permit udp host 192.168.0.1 host 192.168.1.1 eq 80
220
+ access-list 101 permit ip host 192.168.0.5 host 192.168.1.10
221
+ access-list 101 permit tcp host 192.168.0.6 host 192.168.1.11 eq 22
222
+ !!! I AM NOT L33T
223
+
224
+ If you run this with the following arguments:
225
+ --modify-between-comments "I AM L33T" "I AM NOT L33T" \\
226
+ --source-address 192.168.0.5 \\
227
+ --destination-address 192.168.1.10 \\
228
+ --destination-address 192.168.1.11 \\
229
+ --protocol tcp \\
230
+ --destination-port 80 \\
231
+ --insert-defined
232
+ The following will be generated:
233
+ !!! I AM L33T
234
+ access-list 101 permit udp host 192.168.0.1 host 192.168.1.1 eq 80
235
+ access-list 101 permit ip host 192.168.0.5 host 192.168.1.10
236
+ access-list 101 permit tcp host 192.168.0.6 host 192.168.1.11 eq 22
237
+ access-list 101 permit tcp host 192.168.0.5 host 192.168.1.11 eq 80
238
+ !!! I AM NOT L33T
239
+
240
+ As you can see the last line was added, take note that the
241
+ 192.168.0.5->192.168.1.10:80 access was not added because it
242
+ was already allowed.
243
+ """
244
+
245
+ parser.add_option(
246
+ "--replace-defined",
247
+ default=False,
248
+ action="store_true",
249
+ help="""This option works differently based on the type of ACL we are modifying. The
250
+ one similar characteristic is that access can be removed, since this replaces
251
+ whole sets of defined data.
252
+ """,
253
+ )
254
+ notes += """
255
+
256
+ NOTE when using --replace-defined:
257
+ When modifying a JUNOS term you only replace parts of the term that are
258
+ specified on the commandline.
259
+
260
+ Take the following as an example:
261
+ term sr31337 {
262
+ from {
263
+ source-address {
264
+ 10.0.0.0/8;
265
+ 11.0.0.0/8;
266
+ }
267
+ destination-address {
268
+ 192.168.0.1/32;
269
+ }
270
+ destination-port 80;
271
+ protocol tcp;
272
+ }
273
+ then {
274
+ accept;
275
+ count sr31337;
276
+ }
277
+ }
278
+ If you run this with the following arguments:
279
+ --modify-specific-term sr31337 --source-address 5.5.5.5 --replace-defined
280
+ The following is generated:
281
+ term sr31337 {
282
+ from {
283
+ source-address {
284
+ 5.5.5.5/32;
285
+ }
286
+ destination-address {
287
+ 192.168.0.1/32;
288
+ }
289
+ destination-port 80;
290
+ protocol tcp;
291
+ }
292
+ then {
293
+ accept;
294
+ count sr31337;
295
+ }
296
+ }
297
+
298
+ As you can see only the source-address portion of the term was replaced.
299
+ """
300
+ opts, args = parser.parse_args(argv)
301
+ # print 'OPTS:', opts
302
+ # print 'ARGS:', args
303
+
304
+ for i in opts.source_port_range:
305
+ opts.source_port.append(i)
306
+ for i in opts.destination_port_range:
307
+ opts.destination_port.append(i)
308
+
309
+ cnt = 0
310
+ for i in opts.source_address:
311
+ opts.source_address[cnt] = TIP(i)
312
+ cnt += 1
313
+
314
+ cnt = 0
315
+ for i in opts.destination_address:
316
+ opts.destination_address[cnt] = TIP(i)
317
+ cnt += 1
318
+ for i in opts.destination_address_from_file:
319
+ f = open(i)
320
+ line = f.readline()
321
+ while line:
322
+ line = line.rstrip()
323
+ opts.destination_address.append(TIP(line))
324
+ line = f.readline()
325
+ for i in opts.source_address_from_file:
326
+ f = open(i)
327
+ line = f.readline()
328
+ while line:
329
+ line = line.rstrip()
330
+ opts.source_address.append(TIP(line))
331
+ line = f.readline()
332
+
333
+ if len(argv) == 1 or not opts.acl or opts.help:
334
+ parser.print_help()
335
+ print(notes)
336
+ sys.exit(2)
337
+
338
+ if not opts.replace_defined or opts.insert_defined:
339
+ opts.insert_defined = True
340
+
341
+ return opts, args
342
+
343
+
344
+ def log_term(term, msg="ADDING"):
345
+ print(f">>{msg}<<", end=" ")
346
+ for k, v in term.match.items():
347
+ for x in v:
348
+ print(f"KEY:{k} VAL:{x},", end=" ")
349
+ print("")
350
+
351
+
352
+ def wedge_acl(acl, new_term, between, opts):
353
+ if not between:
354
+ # ugg, don't deal with this yet.
355
+ return
356
+ elif isinstance(between, tuple):
357
+ found_start = False
358
+ found_end = False
359
+ start_offset = 0
360
+ end_offset = 0
361
+ offset = 0
362
+ for term in acl.terms:
363
+ for comment in term.comments:
364
+ if found_start and between[1] in comment:
365
+ end_offset = offset
366
+ found_end = True
367
+ break
368
+ elif not found_start and between[0] in comment:
369
+ start_offset = offset
370
+ found_start = True
371
+ if found_start and found_end:
372
+ break
373
+ offset += 1
374
+ if found_start and found_end:
375
+ # everthing before the start of the modify
376
+ head_terms = acl.terms[0:start_offset]
377
+ # everything after the modify portion.
378
+ footer_terms = acl.terms[end_offset:]
379
+ # the terms to modify
380
+ terms = acl.terms[start_offset:end_offset]
381
+ # create a new term and add the head to it
382
+ create_term = head_terms
383
+ if opts.replace_defined:
384
+ # don't forget about the comment
385
+ # or else future edits will be gone.
386
+ new_term.comments = acl.terms[start_offset].comments
387
+ create_term.append(new_term)
388
+ if opts.show_mods:
389
+ # log the added
390
+ log_term(new_term)
391
+ # log the removed
392
+ for term in terms:
393
+ log_term(term, "REMOVING")
394
+ elif opts.insert_defined:
395
+ to_insert = acl_tools.create_access(terms, new_term)
396
+ # put all the other stuff in here.
397
+ for term in terms:
398
+ create_term.append(term)
399
+ # just insert the new entries at the bottom...
400
+ if to_insert:
401
+ to_insert[0].comments.append(
402
+ Comment("Added by acl_script on <date>")
403
+ )
404
+ for toins in to_insert:
405
+ if opts.show_mods:
406
+ log_term(toins)
407
+ create_term.append(toins)
408
+ # add the end
409
+ for term in footer_terms:
410
+ create_term.append(term)
411
+ # overwrite the original list.
412
+ acl.terms = create_term
413
+ elif isinstance(between, str):
414
+ # find a specific term to modify
415
+ assert acl.format == "junos"
416
+ for term in acl.terms:
417
+ if term.name == between:
418
+ if opts.replace_defined:
419
+ # for every part of new_term that is defined
420
+ # we replace that portion in this term.
421
+ for k, v in new_term.match.items():
422
+ if opts.show_mods:
423
+ removing = term.match.get(k, [])
424
+ for x in removing:
425
+ print(f">>REMOVING<< KEY:{k} VAL:{x},TERM:{term.name}")
426
+ for x in v:
427
+ print(f">>ADDING<< KEY:{k} VAL:{x},TERM:{term.name}")
428
+ term.match[k] = v
429
+
430
+ elif opts.insert_defined:
431
+ # for every part of new_term that is defined
432
+ # we just insert if not found in the section
433
+ # of this term.
434
+ for k, v in new_term.match.items():
435
+ if not term.match.has_key(k):
436
+ if opts.show_mods:
437
+ for x in v:
438
+ print(
439
+ f">>ADDING<< KEY:{k} VAL:{x}, TERM:{term.name}"
440
+ )
441
+ term.match[k] = v
442
+ else:
443
+ if opts.comment:
444
+ for comment in term.comments:
445
+ if opts.comment == comment.data:
446
+ break
447
+ else:
448
+ term.comments.append(Comment(opts.comment))
449
+ for x in v:
450
+ print(x)
451
+ if x not in term.match[k]:
452
+ if opts.show_mods:
453
+ print(
454
+ f">>ADDING<< KEY:{k} VAL:{x}, TERM:{term.name}"
455
+ )
456
+ term.match[k].append(x)
457
+ break
458
+
459
+
460
+ def main():
461
+ """Main entry point for the CLI tool."""
462
+ opts, args = parse_args(sys.argv)
463
+ for acl_file in opts.acl:
464
+ rcs = RCS(acl_file, create=False)
465
+ rcs.lock_loop()
466
+
467
+ try:
468
+ with open(acl_file) as f:
469
+ acl = parse(f)
470
+ # TODO (jathan): Improve this naked except
471
+ except Exception:
472
+ print(f">>ERROR<< Could not parse acl file {acl_file}!")
473
+ rcs.unlock()
474
+ sys.exit(2)
475
+
476
+ short_acl = acl_file
477
+ r = re.compile(r"\/(acl\..*?)$")
478
+ ar = r.findall(acl_file) # opts.acl)
479
+ if ar:
480
+ short_acl = ar[0]
481
+
482
+ term = acl_tools.create_trigger_term(
483
+ opts.source_address,
484
+ opts.destination_address,
485
+ opts.source_port,
486
+ opts.destination_port,
487
+ opts.protocol,
488
+ )
489
+
490
+ # Comments are not integrated and handled differently
491
+ if opts.comment:
492
+ term.comments.append(opts.comment)
493
+
494
+ if opts.modify_between_comments and not acl.format.startswith("ios"):
495
+ print("--modify-between-comments should only be used for ios like acls")
496
+ rcs.unlock()
497
+ sys.exit(1)
498
+
499
+ if opts.modify_specific_term and acl.format != "junos":
500
+ print("--modify-specific-term should only be used on junos like acls")
501
+ rcs.unlock()
502
+ sys.exit(1)
503
+
504
+ if opts.modify_between_comments:
505
+ for d in opts.modify_between_comments:
506
+ wedge_acl(acl, term, d, opts)
507
+ elif opts.modify_specific_term:
508
+ for d in opts.modify_specific_term:
509
+ wedge_acl(acl, term, d, opts)
510
+
511
+ tempfile = acl_tools.write_tmpacl(acl, process_name="_acl_script")
512
+ diff = acl_tools.diff_files(acl_file, tempfile)
513
+
514
+ if not diff:
515
+ print(f"No changes made to {acl_file}")
516
+ rcs.unlock()
517
+ os.remove(tempfile)
518
+ # sys.exit(0)
519
+ continue
520
+
521
+ prestr = ""
522
+ if opts.no_input:
523
+ prestr = ">>DIFF<< "
524
+
525
+ print(f'{prestr}"{acl_file}"') # opts.acl)
526
+ print(f"{prestr}BEGINNING OF CHANGES ===========")
527
+ for l in diff.split("\n"):
528
+ print(f"{prestr}{l}")
529
+ print(f"{prestr}END OF CHANGES =================")
530
+
531
+ if not opts.no_input:
532
+ if not yesno("Do you want to save changes?"):
533
+ rcs.unlock()
534
+ os.remove(tempfile)
535
+ # sys.exit(1)
536
+ continue
537
+
538
+ if not opts.no_changes:
539
+ import shutil
540
+
541
+ shutil.copy(tempfile, acl_file) # opts.acl)
542
+
543
+ # rcs.checkin() #message=opts.comment)
544
+ pats = (re.compile(r"^\+.*!+(.*)"), re.compile(r"^\+.*/\*(.*)\*/"))
545
+ logstr = "From acl_script\n"
546
+ for line in diff.split("\n"):
547
+ for pat in pats:
548
+ m = pat.match(line)
549
+ if m:
550
+ msg = m.group(1).strip()
551
+ if msg:
552
+ logstr += m.group(1).strip() + "\n"
553
+ break
554
+ if logstr:
555
+ print("Autodetected log message:")
556
+ print(logstr)
557
+ print("")
558
+ else:
559
+ logstr = ""
560
+ # TODO (jathan): Replace this with rcs.checkin()
561
+ os.spawnlp(
562
+ os.P_WAIT, "ci", "ci", "-u", "-m" + logstr, acl_file
563
+ ) # old_file)
564
+ # os.remove(tmpfile)
565
+
566
+ if not opts.no_worklog:
567
+ acl_tools.worklog(short_acl, diff, log_string="updated by acl_script")
568
+
569
+ rcs.unlock()
570
+ os.remove(tempfile)
571
+
572
+
573
+ if __name__ == "__main__":
574
+ main()
trigger/bin/aclconv.py ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ aclconv - Converts an ACL, or a list of ACLs, from one format to another.
5
+ """
6
+
7
+ __version__ = "1.11"
8
+
9
+ import optparse
10
+ import sys
11
+
12
+ from trigger import acl
13
+
14
+ formats = {
15
+ "ios": ("i", "IOS old-school"),
16
+ "ios_named": ("o", "IOS named"),
17
+ "junos": ("j", "JunOS"),
18
+ "iosxr": ("x", "IOS XR"),
19
+ }
20
+
21
+
22
+ def main():
23
+ """Main entry point for the CLI tool."""
24
+ optp = optparse.OptionParser(
25
+ description="""\
26
+ Convert an ACL on stdin, or a list of ACLs, from one format to another.
27
+
28
+ Input format is determined automatically. Output format can be given
29
+ with -f or with one of -i/-o/-j/-x.
30
+
31
+ The name of the output ACL is determined automatically, or it can be
32
+ specified with -n."""
33
+ )
34
+ optp.add_option("-f", "--format", choices=list(formats.keys()))
35
+ for format, t in formats.items():
36
+ optp.add_option(
37
+ "-" + t[0],
38
+ "--" + format.replace("_", "-"),
39
+ action="store_const",
40
+ const=format,
41
+ dest="format",
42
+ help=f"Use {t[1]} ACL output format",
43
+ )
44
+ optp.add_option("-n", "--name", dest="aclname")
45
+ (opts, files) = optp.parse_args()
46
+
47
+ if opts.format is None:
48
+ optp.print_help()
49
+ sys.exit(1)
50
+
51
+ if not files or files == ["-"]:
52
+ files = ["/dev/fd/0"]
53
+
54
+ for fname in files:
55
+ with open(fname) as f:
56
+ a = acl.parse(f)
57
+
58
+ if opts.aclname is not None:
59
+ a.name = opts.aclname
60
+ elif opts.format == "ios" and (a.name is None or not a.name.isdigit()):
61
+ a.name = "100"
62
+ elif opts.format != "ios" and not a.name.endswith(formats[opts.format][0]):
63
+ a.name += formats[opts.format][0]
64
+
65
+ if opts.format.startswith("ios"):
66
+ for t in a.terms:
67
+ if t.action == ("discard",):
68
+ t.action = "reject"
69
+ if opts.format == "junos":
70
+ a.name_terms()
71
+ for t in a.terms:
72
+ if "count" not in t.modifiers:
73
+ t.modifiers["count"] = t.name
74
+ if opts.format == "iosxr":
75
+ for t in a.terms:
76
+ t.name = None
77
+
78
+ print("\n".join(a.output(opts.format, replace=True)))
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()