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,841 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ load_acl - Unified automatic ACL loader.
5
+
6
+ By default, ACLs will be loaded on all the devices they apply to (using
7
+ acls.db/autoacls). With ``-f``, that list will be used instead. With ``-Q``,
8
+ the load queue list will be used instead. For example, ``load_acl -Q 145``
9
+ will load on all the devices 145 is queued for. ``load_acl -Q`` with no
10
+ ACLs listed will load everything in the queue. ``load_acl --auto`` will
11
+ automatically load eligible devices from the queue and email results.
12
+ """
13
+
14
+ __version__ = "1.9.2"
15
+
16
+ # Dist imports
17
+ import curses
18
+ import datetime
19
+ import fnmatch
20
+ import logging
21
+ import os
22
+ import re
23
+ import sys
24
+ import tempfile
25
+ import time
26
+ from collections import defaultdict
27
+ from optparse import OptionParser
28
+ from xml.etree.ElementTree import Element, SubElement
29
+
30
+ import pytz
31
+ from twisted.internet import defer, reactor, task
32
+ from twisted.python import log
33
+
34
+ # Trigger imports
35
+ from trigger import exceptions
36
+ from trigger.acl.queue import Queue
37
+ from trigger.acl.tools import get_bulk_acls, process_bulk_loads
38
+ from trigger.conf import settings
39
+ from trigger.netdevices import NetDevices
40
+ from trigger.tacacsrc import Tacacsrc
41
+ from trigger.utils.cli import NullDevice, min_sec, pretty_time, print_severed_head
42
+ from trigger.utils.notifications import send_email, send_notification
43
+
44
+ # Globals
45
+ # Pull in these functions from settings
46
+ get_current_oncall = settings.GET_CURRENT_ONCALL
47
+ create_cm_ticket = settings.CREATE_CM_TICKET
48
+ stage_acls = settings.STAGE_ACLS
49
+ get_tftp_source = settings.GET_TFTP_SOURCE # (dev)
50
+
51
+ # Our global NetDevices object!
52
+ nd = NetDevices() # production_only=False) #should be added with a flag
53
+
54
+ # We don't want queue interaction messages to mess up curses display
55
+ queue = Queue(verbose=False)
56
+
57
+ # For displaying acls that were filtered during get_work()
58
+ filtered_acls = set()
59
+
60
+ # Used to keep track of the output of the curses status board.
61
+ output_cache = {}
62
+
63
+
64
+ def draw_screen(s, work, active, failures, start_qlen, start_time):
65
+ """
66
+ Curses-based status board for displaying progress during interactive
67
+ mode.
68
+
69
+ :param work:
70
+ The work dictionary (device to acls)
71
+
72
+ :param active:
73
+ Dictionary mapping running devs to human-readable status
74
+
75
+ :param failures:
76
+ Dictionary of failures
77
+
78
+ :param start_qlen:
79
+ The length of the queue at start (to calculate progress)
80
+
81
+ :param start_time:
82
+ The epoch time at startup (to calculate progress)
83
+ """
84
+ global output_cache
85
+
86
+ if not s:
87
+ # this is stuff if we don't have a ncurses handle.
88
+ for device, status in active.items():
89
+ if device in output_cache:
90
+ if output_cache[device] != status:
91
+ log.msg(f"{device}: {status}")
92
+ output_cache[device] = status
93
+ else:
94
+ log.msg(f"{device}: {status}")
95
+ output_cache[device] = status
96
+ return
97
+
98
+ s.erase()
99
+
100
+ # DO NOT cache the result of s.getmaxyx(), or you cause race conditions
101
+ # which can create exceptions when the window is resized.
102
+ def maxx():
103
+ y, x = s.getmaxyx()
104
+ return x
105
+
106
+ def maxy():
107
+ y, x = s.getmaxyx()
108
+ return y
109
+
110
+ # Display progress bar at top (#/devices, elapsed time)
111
+ s.addstr(0, 0, "load_acl"[: maxx()], curses.A_BOLD)
112
+ progress = " %d/%d devices" % (start_qlen - len(work) - len(active), start_qlen)
113
+ s.addstr(0, maxx() - len(progress), progress)
114
+
115
+ doneness = 1 - float(len(work) + len(active)) / start_qlen
116
+ elapsed = time.time() - start_time
117
+ elapsed_str = min_sec(elapsed)
118
+
119
+ # Update status
120
+ if doneness == 0:
121
+ remaining_str = " "
122
+ elif doneness == 1:
123
+ remaining_str = "done"
124
+ else:
125
+ remaining_str = min_sec(elapsed / doneness - elapsed)
126
+ max_line = int(maxx() - len(remaining_str) - len(elapsed_str) - 2)
127
+
128
+ s.addstr(1, 0, elapsed_str)
129
+ s.addstr(1, maxx() - len(remaining_str), remaining_str)
130
+ s.hline(1, len(elapsed_str) + 1, curses.ACS_HLINE, int(max_line * doneness))
131
+
132
+ # If we get failures, report them
133
+ if failures:
134
+ count, plural = len(failures), (len(failures) > 1 and "s" or "")
135
+ s.addstr(
136
+ 2,
137
+ 0,
138
+ " %d failure%s, will report at end " % (count, plural),
139
+ curses.A_STANDOUT,
140
+ )
141
+
142
+ # Update device name
143
+ for y, (dev, status) in zip(range(3, maxy()), active.items()):
144
+ s.addstr(y, 0, (f"{dev}: {status}")[: maxx()], curses.A_BOLD)
145
+ for y, (dev, acls) in zip(range(3 + len(active), maxy()), work.items()):
146
+ s.addstr(y, 0, ("{}: {}".format(dev, " ".join(acls)))[: maxx()])
147
+
148
+ s.move(maxy() - 1, maxx() - 1)
149
+ s.refresh()
150
+
151
+
152
+ def parse_args(argv):
153
+ """
154
+ Parses the args and returns opts, args back to caller. Defaults to
155
+ ``sys.argv``, but Optinally takes a custom one if you so desire.
156
+
157
+ :param argv:
158
+ A list of opts/args to use over sys.argv
159
+ """
160
+
161
+ def comma_cb(option, opt_str, value, parser):
162
+ """OptionParser callback to handle comma-separated arguments."""
163
+ values = value.split(",")
164
+ try:
165
+ getattr(parser.values, option.dest).extend(values)
166
+ except AttributeError:
167
+ setattr(parser.values, option.dest, values)
168
+
169
+ parser = OptionParser(usage="%prog [options] [acls]", description=__doc__.lstrip())
170
+ parser.add_option("-f", "--file", help="specify explicit list of devices")
171
+ parser.add_option(
172
+ "-Q",
173
+ "--queue",
174
+ action="store_true",
175
+ help="load ACLs from integrated load queue",
176
+ )
177
+ parser.add_option(
178
+ "-q",
179
+ "--quiet",
180
+ action="store_true",
181
+ help="suppress all standard output; errors/warnings still display",
182
+ )
183
+ parser.add_option(
184
+ "--exclude",
185
+ "--except",
186
+ type="string",
187
+ action="callback",
188
+ callback=comma_cb,
189
+ dest="exclude",
190
+ default=[],
191
+ help="skip over ACLs or devices; shell-type patterns "
192
+ '(e.g., "iwg?-[md]*") can be used for devices; for '
193
+ "multiple excludes, use commas or give this option "
194
+ "more than once",
195
+ )
196
+ parser.add_option(
197
+ "-j",
198
+ "--jobs",
199
+ type="int",
200
+ default=5,
201
+ help="maximum simultaneous connections (default 5)",
202
+ )
203
+ # Booleans below
204
+ parser.add_option(
205
+ "-e",
206
+ "--escalation",
207
+ "--escalated",
208
+ action="store_true",
209
+ help="load escalated ACLs from integrated load queue",
210
+ )
211
+ parser.add_option(
212
+ "--severed-head", action="store_true", help="display severed head"
213
+ )
214
+ parser.add_option(
215
+ "--no-db", action="store_true", help="disable database access (for outages)"
216
+ )
217
+ parser.add_option(
218
+ "--bouncy", action="store_true", help="load out of bounce (override checks)"
219
+ )
220
+ parser.add_option(
221
+ "--no-vip", action="store_true", help="TFTP from green address, not blue VIP"
222
+ )
223
+ parser.add_option(
224
+ "--bulk",
225
+ action="store_true",
226
+ help="force all loads to be treated as bulk, restricting "
227
+ "the amount of devices that will be loaded per "
228
+ "execution of load_acl.",
229
+ )
230
+ parser.add_option(
231
+ "--no-cm", action="store_true", help="do not open up a CM ticket for this load"
232
+ )
233
+ parser.add_option(
234
+ "--no-curses",
235
+ action="store_true",
236
+ help="do not use ncurses output; output everything line-by-line in a log format",
237
+ )
238
+ parser.add_option(
239
+ "--auto",
240
+ action="store_true",
241
+ help="automatically proceed with loads; for use with cron; assumes -q",
242
+ )
243
+
244
+ opts, args = parser.parse_args(argv)
245
+
246
+ if opts.escalation:
247
+ opts.queue = True
248
+ if opts.queue and opts.no_db:
249
+ parser.error("Can't check load queue without database access")
250
+ if opts.queue and opts.file:
251
+ parser.error("Can't get ACL load plan from both queue and file")
252
+ if len(args) == 1 and not opts.file and not opts.queue and not opts.auto:
253
+ parser.print_help()
254
+ if opts.auto:
255
+ opts.quiet = True
256
+ if opts.quiet:
257
+ sys.stdout = NullDevice()
258
+ if opts.bouncy:
259
+ opts.jobs = 1
260
+ print("Bouncy enabled, disabling multiple jobs.")
261
+ log.msg("Bouncy enabled, disabling multiple jobs.")
262
+
263
+ return opts, args
264
+
265
+
266
+ def debug_fakeout():
267
+ """Used for debug, but this method is rarely used."""
268
+ return os.getenv("DEBUG_FAKEOUT") is not None
269
+
270
+
271
+ def get_work(opts, args):
272
+ """
273
+ Determine the set of devices to load on, and what ACLs to load on
274
+ each. Processes extra CLI arguments to modify the work queue. Return a
275
+ dictionary of ``{nodeName: set(acls)}``.
276
+
277
+ :param opts:
278
+ A dictionary-like object of CLI options
279
+
280
+ :param args:
281
+ A list of CLI arguments
282
+ """
283
+ # removing acl. assumption from files
284
+ aclargs = set(
285
+ args[1:]
286
+ ) # set([x.startswith('acl.') and x[4:] or x for x in args[1:]])
287
+
288
+ work = {}
289
+ bulk_acls = get_bulk_acls()
290
+
291
+ def add_work(dev_name, acls):
292
+ """
293
+ A closure for the purpose of adding/updating ACLS for a given device.
294
+ """
295
+ try:
296
+ dev = nd[dev_name]
297
+ except KeyError:
298
+ sys.stderr.write(f"WARNING: device {dev_name} not found")
299
+ return
300
+ try:
301
+ work[dev] |= set(acls)
302
+ except KeyError:
303
+ work[dev] = set(acls)
304
+
305
+ # Get the initial list, from whatever source.
306
+ if opts.file:
307
+ for line in open(opts.file):
308
+ if len(line) == 0 or line[0].isspace():
309
+ # Lines with leading whitespace are wrapped pasted "acl" output
310
+ continue
311
+ a = line.rstrip().split()
312
+ try:
313
+ if len(a) == 1:
314
+ add_work(a[0], aclargs)
315
+ elif aclargs:
316
+ add_work(a[0], set(a[1:]) & aclargs)
317
+ else:
318
+ add_work(a[0], a[1:])
319
+ except KeyError as e:
320
+ sys.stderr.write(f"Unknown router: {e}")
321
+ log.err(f"Unknown router: {e}")
322
+ sys.exit(1)
323
+ elif opts.queue:
324
+ all_sql_data = queue.list()
325
+
326
+ # First check to make sure our AUTOLOAD_FILTER_THRESH are under control
327
+ # if they are not add them to the AUTOLOAD_BLACKLIST.
328
+ # Next check if acls are bulk acls and process them accordingly.
329
+ thresh_counts = defaultdict(int)
330
+ defaultdict(int)
331
+
332
+ for router, acl in all_sql_data:
333
+ if acl in settings.AUTOLOAD_FILTER_THRESH:
334
+ thresh_counts[acl] += 1
335
+ if thresh_counts[acl] >= settings.AUTOLOAD_FILTER_THRESH[acl]:
336
+ print("adding", router, acl, " to AUTOLOAD_BLACKLIST")
337
+ log.msg(f"Adding {acl} to AUTOLOAD_BLACKLIST")
338
+ settings.AUTOLOAD_BLACKLIST.append(acl)
339
+
340
+ for router, acl in all_sql_data:
341
+ if not aclargs or acl in aclargs:
342
+ if opts.auto:
343
+ ## check autoload blacklist
344
+ if acl not in settings.AUTOLOAD_BLACKLIST:
345
+ add_work(router, [acl])
346
+ else:
347
+ # filtered_acls = True
348
+ filtered_acls.add(acl)
349
+ else:
350
+ add_work(router, [acl])
351
+ else:
352
+ found = set()
353
+ for dev in nd.all():
354
+ intersection = dev.acls & aclargs
355
+ if len(intersection):
356
+ add_work(dev.nodeName, intersection)
357
+ found |= intersection
358
+ not_found = list(aclargs - found)
359
+ if not_found:
360
+ not_found.sort()
361
+ sys.stderr.write("No devices found for {}\n".format(", ".join(not_found)))
362
+ sys.exit(1)
363
+
364
+ # Process --bulk. Only if not --bouncy.
365
+ if not opts.bouncy:
366
+ work = process_bulk_loads(work, bulk_acls, force_bulk=opts.bulk)
367
+
368
+ # Process --exclude.
369
+ if opts.exclude:
370
+ # print 'stuff'
371
+ exclude = set(opts.exclude)
372
+ for dev in work.keys():
373
+ for ex in exclude:
374
+ if fnmatch.fnmatchcase(dev.nodeName, ex) or dev.nodeName.startswith(
375
+ ex + "."
376
+ ):
377
+ del work[dev]
378
+ break
379
+ for dev, acls in work.items():
380
+ acls -= exclude
381
+ if len(acls) == 0:
382
+ del work[dev]
383
+
384
+ # Check bounce windows, and filter or warn.
385
+ now = datetime.datetime.now(tz=pytz.UTC)
386
+ next_ok = dict([(dev, dev.next_ok("load-acl", now)) for dev in work])
387
+ bouncy_devs = [dev for dev, when in next_ok.items() if when > now]
388
+ if bouncy_devs:
389
+ bouncy_devs.sort()
390
+ print()
391
+ if opts.bouncy:
392
+ for dev in bouncy_devs:
393
+ dev_acls = ", ".join(work[dev])
394
+ print(f"Loading {dev_acls} OUT OF BOUNCE on {dev}")
395
+ log.msg(f"Loading {dev_acls} OUT OF BOUNCE on {dev}")
396
+ else:
397
+ for dev in bouncy_devs:
398
+ dev_acls = ", ".join(work[dev])
399
+ print(
400
+ "Skipping {} on {} (window starts at {})".format(
401
+ dev_acls, dev.nodeName.split(".")[0], pretty_time(next_ok[dev])
402
+ )
403
+ )
404
+ log.msg(
405
+ "Skipping {} on {} (window starts at {})".format(
406
+ dev_acls, dev.nodeName.split(".")[0], pretty_time(next_ok[dev])
407
+ )
408
+ )
409
+ del work[dev]
410
+ print("\nUse --bouncy to forcefully load on these devices anyway.")
411
+ print
412
+
413
+ # Display filtered acls
414
+ for a in filtered_acls:
415
+ print(f"{a} is in AUTOLOAD_BLACKLIST; not added to work queue.")
416
+ log.msg(f"{a} is in AUTOLOAD_BLACKLIST; not added to work queue.")
417
+
418
+ return work
419
+
420
+
421
+ def junoscript_cmds(acls_content, tftp_paths, dev):
422
+ """
423
+ Return a list of Junoscript commands to load the given ACLs, and a
424
+ matching list of tuples (acls remaining, human-readable status message).
425
+
426
+ :param acls:
427
+ A collection of ACL names
428
+
429
+ :param dev:
430
+ A Juniper `~trigger.netdevices.NetDevice` object
431
+ """
432
+ xml = [Element("lock-configuration")]
433
+ status = ["locking configuration"]
434
+
435
+ for i, acl_content in enumerate(acls_content):
436
+ lc = Element("load-configuration", action="replace", format="text")
437
+ body = SubElement(lc, "configuration-text")
438
+ body.text = acl_content
439
+ xml.append(lc)
440
+ status.append("loading ACL " + tftp_paths[i]) # acl)
441
+
442
+ # Add the proper commit command
443
+ xml.extend(dev.commit_commands)
444
+ status.append("committing for " + ",".join(tftp_paths)) # acls))
445
+ status.append("done for" + ",".join(tftp_paths))
446
+
447
+ if debug_fakeout():
448
+ xml = [Element("get-software-information")] * (len(status) - 1)
449
+
450
+ return xml, status
451
+
452
+
453
+ def ioslike_cmds(tftp_paths, dev, opts): # , nonce):
454
+ """
455
+ Return a list of IOS-like commands to load the given ACLs, and a matching
456
+ list of tuples (acls remaining, human-readable status message).
457
+
458
+ :param acls:
459
+ A collection of ACL names
460
+
461
+ :param dev:
462
+ An IOS-like `~trigger.netdevices.NetDevice` object
463
+
464
+ :param nonce:
465
+ A nonce to use when staging the ACL file for TFTP
466
+ """
467
+ template_base = {
468
+ "arista": "copy tftp://%s/%s system:/running-config\n",
469
+ "cisco": "copy tftp://%s/%s system:/running-config\n",
470
+ "dell": "copy tftp://%s/%s running-config\n",
471
+ "brocade": "copy tftp run %s %s\n",
472
+ "foundry": "copy tftp run %s %s\n",
473
+ "force10": "copy tftp://%s/%s running-config\n",
474
+ }
475
+
476
+ template = template_base[dev.vendor.name]
477
+ cmds = [
478
+ template % (get_tftp_source(dev=dev, no_vip=opts.no_vip), path)
479
+ for path in tftp_paths
480
+ ]
481
+ status = ["loading ACL " + path for path in tftp_paths] # this will print more info
482
+
483
+ # Add the proper write mem command
484
+ cmds.extend(dev.commit_commands)
485
+ status.append("saving config for " + ",".join(tftp_paths)) # acls))
486
+ status.append("done for " + ",".join(tftp_paths)) # acls))
487
+
488
+ if debug_fakeout():
489
+ cmds = ["show ver"] * (len(status) - 1)
490
+
491
+ return cmds, status
492
+
493
+
494
+ def group(dev):
495
+ """
496
+ Helper for select_next_device(). Uses name heuristics to guess whether
497
+ devices are "together". Based loosely upon naming convention that is not
498
+ the "strictest". Expect to need to tweak this.!
499
+
500
+ :param dev:
501
+ The `~trigger.netdevices.NetDevice` object to try to group
502
+ """
503
+ # TODO(jathan): Make this pattern configurable globally.
504
+ trimmer = re.compile("[0-9]*[a-z]+") # allow for e.g. "36bit1"
505
+
506
+ # Try to match on nodeName, and if we don't (such as if it's an IP
507
+ # address), just skip grouping.
508
+ match = trimmer.match(dev.nodeName)
509
+ if not match or not dev.site:
510
+ return dev.nodeName
511
+
512
+ group_key = match.group()
513
+
514
+ # FIXME(jathan): This is some hard-coded AOL-specific legacy stuff that
515
+ # will probably work for most environments, but it's awfully presumptuous.
516
+ if len(group_key) >= 4 and group_key[-1] not in ("i", "e"):
517
+ group_key = group_key[:-1] + "X"
518
+
519
+ return (dev.site, group_key)
520
+
521
+
522
+ def select_next_device(work, active):
523
+ """
524
+ Select another device for the active queue. Don't select a device
525
+ if there is another of that "group" already there.
526
+
527
+ :param work:
528
+ The work dictionary (device to acls)
529
+
530
+ :param active:
531
+ Dictionary mapping running devs to human-readable status
532
+ """
533
+ active_groups = set([group(dev) for dev in active.keys()])
534
+ for dev in work.keys():
535
+ if group(dev) not in active_groups:
536
+ return dev
537
+ return None
538
+
539
+
540
+ def clear_load_queue(dev, acls):
541
+ """Logical wrapper around queue.complete(dev, acls)"""
542
+ if debug_fakeout():
543
+ return
544
+ queue.complete(dev, acls)
545
+
546
+
547
+ def activate(work, active, failures, jobs, redraw, opts):
548
+ """
549
+ Refill the active work queue based on number of current active jobs.
550
+
551
+ :param work:
552
+ The work dictionary (device to acls)
553
+
554
+ :param active:
555
+ Dictionary mapping running devs to human-readable status
556
+
557
+ :param failures:
558
+ Dictionary of failures
559
+
560
+ :param jobs:
561
+ The max number of jobs for active queue
562
+
563
+ :param redraw:
564
+ The redraw closure passed along from the caller
565
+ """
566
+ if not active and not work:
567
+ if reactor.running:
568
+ reactor.stop()
569
+
570
+ while work and len(active) < jobs:
571
+ dev = select_next_device(work, active)
572
+ if not dev:
573
+ break
574
+ acls = work[dev]
575
+ del work[dev]
576
+
577
+ sanitize_acl = dev.vendor == "brocade"
578
+
579
+ # Closures galore! Careful; you need to explicitly save current
580
+ # values (using the default argument trick) 'dev', 'acls', and
581
+ # 'status', because they vary through this loop.
582
+ def update_board(results, dev, status):
583
+ try:
584
+ active[dev] = status[len(results)]
585
+ except IndexError:
586
+ pass
587
+
588
+ def complete(results, dev, acls):
589
+ if queue:
590
+ clear_load_queue(dev, acls)
591
+
592
+ def eb(reason, dev):
593
+ log.msg("GOT ERRBACK", reason)
594
+ failures[dev] = reason
595
+
596
+ def move_on(x, dev):
597
+ del active[dev]
598
+ activate(work, active, failures, jobs, redraw, opts)
599
+
600
+ def stage_acls_cb(unused, dev, acls, log, sanitize_acl):
601
+ # Wrapper for stage_acls; result is unused
602
+ active[dev] = "staging acls"
603
+ return stage_acls(acls, log, sanitize_acl)
604
+
605
+ def check_failure(result, dev):
606
+ (acl_contents, tftp_paths, fails) = result
607
+
608
+ if fails:
609
+ log.msg("STAGING FAILED:", fails)
610
+ raise exceptions.ACLStagingFailed(fails)
611
+
612
+ active[dev] = "connecting"
613
+ if dev.vendor == "juniper":
614
+ cmds, status = junoscript_cmds(acl_contents, tftp_paths, dev)
615
+ else:
616
+ cmds, status = ioslike_cmds(tftp_paths, dev, opts)
617
+
618
+ return (cmds, status)
619
+
620
+ # Stage the acls
621
+ handled_first = defer.Deferred()
622
+ handled_first.addCallback(stage_acls_cb, dev, acls, log, sanitize_acl)
623
+ handled_first.addBoth(check_failure, dev)
624
+ handled_first.addErrback(eb, dev)
625
+ handled_first.addErrback(move_on, dev)
626
+
627
+ # Start staging
628
+ reactor.callWhenRunning(handled_first.callback, None)
629
+
630
+ def chain(result, dev, acls):
631
+ (cmds, status) = result
632
+
633
+ # Lambda function to call update_board() with proper args
634
+ def incremental(x):
635
+ return update_board(x, dev, status)
636
+
637
+ if dev.vendor in ("brocade", "foundry"):
638
+ handled_second = dev.execute(
639
+ cmds, incremental=incremental, command_interval=1
640
+ )
641
+ else:
642
+ handled_second = dev.execute(cmds, incremental=incremental)
643
+ handled_second.addCallback(complete, dev, acls)
644
+ handled_second.addErrback(eb, dev)
645
+ handled_second.addBoth(move_on, dev)
646
+
647
+ # Try to actually push the changes after staging completes successfully
648
+ handled_first.addCallback(chain, dev, acls)
649
+
650
+ redraw()
651
+
652
+
653
+ def run(stdscr, work, jobs, failures, opts):
654
+ """
655
+ Runs the show. Starts the curses status board & starts the reactor loop.
656
+
657
+ :param stdscr:
658
+ The starting curses screen (usually None)
659
+
660
+ :param work:
661
+ The work dictionary (device to acls)
662
+
663
+ :param jobs:
664
+ The max number of jobs for active queue
665
+
666
+ :param failures:
667
+ Dictionary of failures
668
+ """
669
+ # Dictionary of currently running devs -> human-readable status
670
+ active = {}
671
+
672
+ start_qlen = len(work)
673
+ start_time = time.time()
674
+
675
+ def redraw():
676
+ """A closure to redraw the screen with current environment"""
677
+ draw_screen(stdscr, work, active, failures, start_qlen, start_time)
678
+
679
+ activate(work, active, failures, jobs, redraw, opts)
680
+
681
+ # Make sure the screen is updated regularly even when nothing happens.
682
+ drawloop = task.LoopingCall(redraw)
683
+ drawloop.start(0.25)
684
+
685
+ reactor.run()
686
+
687
+
688
+ def main():
689
+ """The Main Event."""
690
+ global opts
691
+ opts, args = parse_args(sys.argv)
692
+
693
+ if opts.severed_head:
694
+ print_severed_head()
695
+ sys.exit(0)
696
+ if opts.auto:
697
+ opts.no_curses = True
698
+ opts.queue = True
699
+
700
+ global queue
701
+ if opts.no_db:
702
+ queue = None
703
+
704
+ if (not opts.auto) or (not opts.quiet):
705
+ print("Logging to", tmpfile)
706
+
707
+ # Where the magic happens
708
+ work = get_work(opts, args)
709
+
710
+ if not work:
711
+ if not opts.auto:
712
+ print("Nothing to load.")
713
+ log.msg("Nothing to load.")
714
+ sys.exit(0)
715
+
716
+ print("You are about to perform the following loads:")
717
+ print("")
718
+ devs = work.items()
719
+ devs.sort()
720
+ for dev, acls in devs:
721
+ acls = list(work[dev])
722
+ acls.sort()
723
+ print("%-32s %s" % (dev, " ".join(acls)))
724
+ acl_count = len(acls)
725
+ print("")
726
+ if debug_fakeout():
727
+ print("DEBUG FAKEOUT ENABLED")
728
+ failures = {}
729
+ run(None, work, opts.jobs, failures, opts)
730
+ sys.exit(1)
731
+
732
+ if not opts.auto:
733
+ if opts.bouncy:
734
+ print(
735
+ "NOTE: Parallel jobs disabled for out of bounce loads, this will take longer than usual."
736
+ )
737
+ print()
738
+
739
+ confirm = input("Are you sure you want to proceed? ")
740
+ if not confirm.lower().startswith("y"):
741
+ print("LOAD CANCELLED")
742
+ log.msg("LOAD CANCELLED")
743
+ sys.exit(1)
744
+ print("")
745
+ # Don't let the credential prompts get hidden behind curses
746
+ Tacacsrc()
747
+ else:
748
+ log.msg("Auto option thrown, checking if credential file exists")
749
+ tacacsrc_file = settings.TACACSRC
750
+ if not os.path.exists(tacacsrc_file):
751
+ log.msg(f"No {tacacsrc_file} file exists and auto option enabled.")
752
+ sys.exit(1)
753
+ log.msg(f"Credential file {tacacsrc_file} exists, moving on")
754
+
755
+ cm_ticketnum = 0
756
+ if not opts.no_cm and not debug_fakeout():
757
+ oncall = get_current_oncall()
758
+ if not oncall:
759
+ if opts.auto:
760
+ send_notification(
761
+ "LOAD_ACL FAILURE", "Unable to get current ON-CALL information!"
762
+ )
763
+ log.err("Unable to get on-call information!", logLevel=logging.CRITICAL)
764
+ sys.exit(1)
765
+
766
+ print("\nSubmitting CM ticket...")
767
+ # catch failures to create a ticket
768
+ try:
769
+ cm_ticketnum = create_cm_ticket(work, oncall)
770
+ except:
771
+ # create_cm_ticket is user defined
772
+ # so we don't know what exceptions can be returned
773
+ cm_ticketnum = None
774
+ if not cm_ticketnum:
775
+ es = "Unable to create CM ticket!"
776
+ if opts.auto:
777
+ send_notification("LOAD_ACL FAILURE", es)
778
+ log.err(es, logLevel=logging.CRITICAL)
779
+ sys.exit(es)
780
+
781
+ cm_msg = f"Created CM ticket #{cm_ticketnum}"
782
+ print(cm_msg)
783
+ log.msg(cm_msg)
784
+
785
+ start = time.time()
786
+ # Dicionary of failures and their causes
787
+ failures = {}
788
+
789
+ # Don't use curses.wrapper(), because that initializes colors which
790
+ # means that we won't be using the user's chosen colors. Default in
791
+ # an xterm is ugly gray on black, not black on white. We can't even
792
+ # fix it since white background becomes unavailable.
793
+ stdscr = None
794
+ try:
795
+ if not opts.no_curses:
796
+ stdscr = curses.initscr()
797
+ stdscr.idlok(1)
798
+ stdscr.scrollok(0)
799
+ curses.noecho()
800
+ run(stdscr, work, opts.jobs, failures, opts)
801
+ finally:
802
+ if not opts.no_curses:
803
+ curses.echo()
804
+ curses.endwin()
805
+
806
+ failed_count = 0
807
+ for dev, reason in failures.items():
808
+ failed_count += 1
809
+ log.err(f"LOAD FAILED ON {dev}: {str(reason)}")
810
+ sys.stderr.write(f"LOAD FAILED ON {dev}: {str(reason)}")
811
+
812
+ if failures and not opts.auto:
813
+ print_severed_head()
814
+
815
+ if opts.auto:
816
+ if failed_count:
817
+ send_notification(
818
+ "LOAD_ACL FAILURE",
819
+ "%d ACLS failed to load! See logfile: %s on jumphost."
820
+ % (failed_count, tmpfile),
821
+ )
822
+ else:
823
+ send_email(
824
+ settings.SUCCESS_EMAILS,
825
+ "LOAD ACL SUCCESS!",
826
+ "%d acls loaded successfully! see log file: %s" % (acl_count, tmpfile),
827
+ settings.EMAIL_SENDER,
828
+ )
829
+
830
+ log.msg("%d failures" % failed_count)
831
+ log.msg(f"Elapsed time: {min_sec(time.time() - start)}")
832
+
833
+
834
+ if __name__ == "__main__":
835
+ tmpfile = tempfile.mktemp() + "_load_acl"
836
+ log.startLogging(open(tmpfile, "a"), setStdout=False)
837
+ log.msg(
838
+ 'User %s (uid:%d) executed "%s"'
839
+ % (os.environ["LOGNAME"], os.getuid(), " ".join(sys.argv))
840
+ )
841
+ main()