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.
- trigger/__init__.py +7 -0
- trigger/acl/__init__.py +32 -0
- trigger/acl/autoacl.py +70 -0
- trigger/acl/db.py +324 -0
- trigger/acl/dicts.py +357 -0
- trigger/acl/grammar.py +112 -0
- trigger/acl/ios.py +222 -0
- trigger/acl/junos.py +422 -0
- trigger/acl/models.py +118 -0
- trigger/acl/parser.py +168 -0
- trigger/acl/queue.py +296 -0
- trigger/acl/support.py +1431 -0
- trigger/acl/tools.py +746 -0
- trigger/bin/__init__.py +0 -0
- trigger/bin/acl.py +233 -0
- trigger/bin/acl_script.py +574 -0
- trigger/bin/aclconv.py +82 -0
- trigger/bin/check_access.py +93 -0
- trigger/bin/check_syntax.py +66 -0
- trigger/bin/fe.py +197 -0
- trigger/bin/find_access.py +191 -0
- trigger/bin/gnng.py +434 -0
- trigger/bin/gong.py +86 -0
- trigger/bin/load_acl.py +841 -0
- trigger/bin/load_config.py +18 -0
- trigger/bin/netdev.py +317 -0
- trigger/bin/optimizer.py +638 -0
- trigger/bin/run_cmds.py +18 -0
- trigger/changemgmt/__init__.py +352 -0
- trigger/changemgmt/bounce.py +57 -0
- trigger/cmds.py +1217 -0
- trigger/conf/__init__.py +94 -0
- trigger/conf/global_settings.py +674 -0
- trigger/contrib/__init__.py +7 -0
- trigger/exceptions.py +307 -0
- trigger/gorc.py +172 -0
- trigger/netdevices/__init__.py +1288 -0
- trigger/netdevices/loader.py +174 -0
- trigger/netscreen.py +1030 -0
- trigger/packages/__init__.py +6 -0
- trigger/packages/peewee.py +8084 -0
- trigger/rancid.py +463 -0
- trigger/tacacsrc.py +584 -0
- trigger/twister.py +2203 -0
- trigger/twister2.py +745 -0
- trigger/utils/__init__.py +88 -0
- trigger/utils/cli.py +349 -0
- trigger/utils/importlib.py +77 -0
- trigger/utils/network.py +157 -0
- trigger/utils/rcs.py +178 -0
- trigger/utils/templates.py +81 -0
- trigger/utils/url.py +78 -0
- trigger/utils/xmltodict.py +298 -0
- trigger-2.0.0.dist-info/METADATA +146 -0
- trigger-2.0.0.dist-info/RECORD +61 -0
- trigger-2.0.0.dist-info/WHEEL +5 -0
- trigger-2.0.0.dist-info/entry_points.txt +15 -0
- trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
- trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
- trigger-2.0.0.dist-info/top_level.txt +2 -0
- twisted/plugins/trigger_xmlrpc.py +124 -0
trigger/bin/load_acl.py
ADDED
|
@@ -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()
|