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
trigger/cmds.py ADDED
@@ -0,0 +1,1217 @@
1
+ """
2
+ This module abstracts the asynchronous execution of commands on multiple
3
+ network devices. It allows for integrated parsing and event-handling of return
4
+ data for rapid integration to existing or newly-created tools.
5
+
6
+ The `~trigger.cmds.Commando` class is designed to be extended but can still be
7
+ used as-is to execute commands and return the results as-is.
8
+ """
9
+
10
+ __author__ = "Jathan McCollum, Eileen Tschetter, Mark Thomas"
11
+ __maintainer__ = "Jathan McCollum"
12
+ __email__ = "jathan@gmail.com"
13
+ __copyright__ = "Copyright 2009-2013, AOL Inc.; 2014 Salesforce.com"
14
+ __version__ = "2.7"
15
+
16
+
17
+ # Imports
18
+ import collections
19
+ import itertools
20
+ from xml.etree.ElementTree import Element, SubElement
21
+
22
+ from IPy import IP
23
+ from twisted.internet import defer, task
24
+ from twisted.python import log
25
+
26
+ from trigger import exceptions
27
+ from trigger.conf import settings
28
+ from trigger.netdevices import NetDevices
29
+ from trigger.utils.templates import (
30
+ get_textfsm_object,
31
+ load_cmd_template,
32
+ )
33
+
34
+ # Exports
35
+ __all__ = ("Commando", "ReactorlessCommando", "NetACLInfo")
36
+
37
+
38
+ # Default timeout in seconds for commands to return a result
39
+ DEFAULT_TIMEOUT = 30
40
+
41
+
42
+ # Classes
43
+ class Commando:
44
+ """
45
+ Execute commands asynchronously on multiple network devices.
46
+
47
+ This class is designed to be extended but can still be used as-is to execute
48
+ commands and return the results as-is.
49
+
50
+ At the bare minimum you must specify a list of ``devices`` to interact with.
51
+ You may optionally specify a list of ``commands`` to execute on those
52
+ devices, but doing so will execute the same commands on every device
53
+ regardless of platform.
54
+
55
+ If ``commands`` are not specified, they will be expected to be emitted by
56
+ the ``generate`` method for a given platform. Otherwise no commands will be
57
+ executed.
58
+
59
+ If you wish to customize the commands executed by device, you must define a
60
+ ``to_{vendor_name}`` method containing your custom logic.
61
+
62
+ If you wish to customize what is done with command results returned from a
63
+ device, you must define a ``from_{vendor_name}`` method containing your
64
+ custom logic.
65
+
66
+ :param devices:
67
+ A list of device hostnames or `~trigger.netdevices.NetDevice` objects
68
+
69
+ :param commands:
70
+ (Optional) A list of commands to execute on the ``devices``.
71
+
72
+ :param creds:
73
+ (Optional) A 3-tuple of (username, password, realm). If only (username,
74
+ password) are provided, realm will be populated from
75
+ :setting:`DEFAULT_REALM`. If unset it will fetch from ``.tacacsrc``.
76
+
77
+ :param incremental:
78
+ (Optional) A callback that will be called with an empty sequence upon
79
+ connection and then called every time a result comes back from the
80
+ device, with the list of all results.
81
+
82
+ :param max_conns:
83
+ (Optional) The maximum number of simultaneous connections to keep open.
84
+
85
+ :param verbose:
86
+ (Optional) Whether or not to display informational messages to the
87
+ console.
88
+
89
+ :param timeout:
90
+ (Optional) Time in seconds to wait for each command executed to return a
91
+ result. Set to ``None`` to disable timeout (not recommended).
92
+
93
+ :param production_only:
94
+ (Optional) If set, includes all devices instead of excluding any devices
95
+ where ``adminStatus`` is not set to ``PRODUCTION``.
96
+
97
+ :param allow_fallback:
98
+ If set (default), allow fallback to base parse/generate methods when
99
+ they are not customized in a subclass, otherwise an exception is raised
100
+ when a method is called that has not been explicitly defined.
101
+
102
+ :param with_errors:
103
+ (Optional) Return exceptions as results instead of raising them. The
104
+ default is to always return them.
105
+
106
+ :param force_cli:
107
+ (Optional) Juniper only. If set, sends commands using CLI instead of
108
+ Junoscript.
109
+
110
+ :param with_acls:
111
+ Whether to load ACL associations (requires Redis). Defaults to whatever
112
+ is specified in settings.WITH_ACLS
113
+
114
+ :param command_interval:
115
+ (Optional) Amount of time in seconds to wait between sending commands.
116
+
117
+ :param stop_reactor:
118
+ Whether to stop the reactor loop when all results have returned.
119
+ (Default: ``True``)
120
+ """
121
+
122
+ # Defaults to all supported vendors
123
+ vendors = settings.SUPPORTED_VENDORS
124
+
125
+ # Defaults to all supported platforms
126
+ platforms = settings.SUPPORTED_PLATFORMS
127
+
128
+ # The commands to run (defaults to [])
129
+ commands = None
130
+
131
+ # The timeout for commands to return results. We are setting this to 0
132
+ # so that if it's not overloaded in a subclass, the timeout value passed to
133
+ # the constructor will be preferred, especially if it is set to ``None``
134
+ # which Twisted uses to disable timeouts completely.
135
+ timeout = 0
136
+
137
+ # How results are stored (defaults to {})
138
+ results = None
139
+
140
+ # How parsed results are stored (defaults to {})
141
+ parsed_results = None
142
+
143
+ # How errors are stored (defaults to {})
144
+ errors = None
145
+
146
+ # Whether to stop the reactor when all results have returned.
147
+ stop_reactor = None
148
+
149
+ def __init__(
150
+ self,
151
+ devices=None,
152
+ commands=None,
153
+ creds=None,
154
+ incremental=None,
155
+ max_conns=10,
156
+ verbose=False,
157
+ timeout=DEFAULT_TIMEOUT,
158
+ production_only=True,
159
+ allow_fallback=True,
160
+ with_errors=True,
161
+ force_cli=False,
162
+ with_acls=False,
163
+ command_interval=0,
164
+ stop_reactor=True,
165
+ ):
166
+ if devices is None:
167
+ raise exceptions.ImproperlyConfigured(
168
+ "You must specify some `devices` to interact with!"
169
+ )
170
+
171
+ self.devices = devices
172
+ self.commands = self.commands or (commands or []) # Always fallback to []
173
+ self.creds = creds
174
+ self.incremental = incremental
175
+ self.max_conns = max_conns
176
+ self.verbose = verbose
177
+ self.timeout = timeout if timeout != self.timeout else self.timeout
178
+ self.nd = NetDevices(production_only=production_only, with_acls=with_acls)
179
+ self.allow_fallback = allow_fallback
180
+ self.with_errors = with_errors
181
+ self.force_cli = force_cli
182
+ self.command_interval = command_interval
183
+ self.stop_reactor = self.stop_reactor or stop_reactor
184
+ self.curr_conns = 0
185
+ self.jobs = []
186
+
187
+ # Always fallback to {} for these
188
+ self.errors = self.errors if self.errors is not None else {}
189
+ self.results = self.results if self.results is not None else {}
190
+ self.parsed_results = (
191
+ self.parsed_results if self.parsed_results is not None else {}
192
+ )
193
+
194
+ # self.deferrals = []
195
+ self.supported_platforms = self._validate_platforms()
196
+ self._setup_jobs()
197
+
198
+ def _validate_platforms(self):
199
+ """
200
+ Determine the set of supported platforms for this instance by making
201
+ sure the specified vendors/platforms for the class match up.
202
+ """
203
+ supported_platforms = {}
204
+ for vendor in self.vendors:
205
+ if vendor in self.platforms:
206
+ types = self.platforms[vendor]
207
+ if not types:
208
+ raise exceptions.MissingPlatform(
209
+ f"No platforms specified for {vendor!r}"
210
+ )
211
+ else:
212
+ # self.supported_platforms[vendor] = types
213
+ supported_platforms[vendor] = types
214
+ else:
215
+ raise exceptions.ImproperlyConfigured(
216
+ f"Platforms for vendor {vendor!r} not found. Please provide it at either the class level or using the arguments."
217
+ )
218
+
219
+ return supported_platforms
220
+
221
+ def _decrement_connections(self, data=None):
222
+ """
223
+ Self-explanatory. Called by _add_worker() as both callback/errback
224
+ so we can accurately refill the jobs queue, which relies on the
225
+ current connection count.
226
+ """
227
+ self.curr_conns -= 1
228
+ return data
229
+
230
+ def _increment_connections(self, data=None):
231
+ """Increment connection count."""
232
+ self.curr_conns += 1
233
+ return True
234
+
235
+ def _setup_jobs(self):
236
+ """
237
+ "Maps device hostnames to `~trigger.netdevices.NetDevice` objects and
238
+ populates the job queue.
239
+ """
240
+ for dev in self.devices:
241
+ log.msg("Adding", dev)
242
+ if self.verbose:
243
+ print("Adding", dev)
244
+
245
+ # Make sure that devices are actually in netdevices and keep going
246
+ try:
247
+ devobj = self.nd.find(str(dev))
248
+ except KeyError:
249
+ msg = f"Device not found in NetDevices: {dev}"
250
+ log.err(msg)
251
+ if self.verbose:
252
+ print("ERROR:", msg)
253
+
254
+ # Track the errors and keep moving
255
+ self.store_error(dev, msg)
256
+ continue
257
+
258
+ # We only want to add devices for which we've enabled support in
259
+ # this class
260
+ if devobj.vendor not in self.vendors:
261
+ raise exceptions.UnsupportedVendor(
262
+ f"The vendor '{devobj.vendor}' is not specified in ``vendors``. Could not add {devobj} to job queue. Please check the attribute in the class object."
263
+ )
264
+
265
+ self.jobs.append(devobj)
266
+
267
+ def select_next_device(self, jobs=None):
268
+ """
269
+ Select another device for the active queue.
270
+
271
+ Currently only returns the next device in the job queue. This is
272
+ abstracted out so that this behavior may be customized, such as for
273
+ future support for incremental callbacks.
274
+
275
+ If a device is determined to be invalid, you must return ``None``.
276
+
277
+ :param jobs:
278
+ (Optional) The jobs queue. If not set, uses ``self.jobs``.
279
+
280
+ :returns:
281
+ A `~trigger.netdevices.NetDevice` object or ``None``.
282
+ """
283
+ if jobs is None:
284
+ jobs = self.jobs
285
+
286
+ return jobs.pop()
287
+
288
+ def _add_worker(self):
289
+ """
290
+ Adds devices to the work queue to keep it populated with the maximum
291
+ connections as specified by ``max_conns``.
292
+ """
293
+ while self.jobs and self.curr_conns < self.max_conns:
294
+ device = self.select_next_device()
295
+ if device is None:
296
+ log.msg("No device returned when adding worker. Moving on.")
297
+ continue
298
+
299
+ self._increment_connections()
300
+ log.msg("connections:", self.curr_conns)
301
+ log.msg("Adding work to queue...")
302
+ if self.verbose:
303
+ print("connections:", self.curr_conns)
304
+ print("Adding work to queue...")
305
+
306
+ # Setup the async Deferred object with a timeout and error printing.
307
+ commands = self.generate(device)
308
+ async_result = device.execute(
309
+ commands,
310
+ creds=self.creds,
311
+ incremental=self.incremental,
312
+ timeout=self.timeout,
313
+ with_errors=self.with_errors,
314
+ force_cli=self.force_cli,
315
+ command_interval=self.command_interval,
316
+ )
317
+
318
+ # Add the template parser callback for great justice!
319
+ async_result.addCallback(self.parse_template, device, commands)
320
+
321
+ # Add the parser callback for even greater justice!
322
+ async_result.addCallback(self.parse, device, commands)
323
+
324
+ # If parse fails, still decrement and track the error
325
+ async_result.addErrback(self.errback, device)
326
+
327
+ # Make sure any further uncaught errors get logged
328
+ async_result.addErrback(log.err)
329
+
330
+ # Here we addBoth to continue on after pass/fail, decrement the
331
+ # connections and move on.
332
+ async_result.addBoth(self._decrement_connections)
333
+ async_result.addBoth(lambda x: self._add_worker())
334
+
335
+ # Do this once we've exhausted the job queue
336
+ else:
337
+ if not self.curr_conns and self.reactor_running:
338
+ self._stop()
339
+ elif not self.jobs and not self.reactor_running:
340
+ log.msg("No work left.")
341
+ if self.verbose:
342
+ print("No work left.")
343
+
344
+ def _lookup_method(self, device, method):
345
+ """
346
+ Base lookup method. Looks up stuff by device manufacturer like:
347
+
348
+ from_juniper
349
+ to_foundry
350
+
351
+ and defaults to ``self.from_base`` and ``self.to_base`` methods if
352
+ customized methods not found.
353
+
354
+ :param device:
355
+ A `~trigger.netdevices.NetDevice` object
356
+
357
+ :param method:
358
+ One of 'generate', 'parse'
359
+ """
360
+ METHOD_MAP = {
361
+ "generate": "to_%s",
362
+ "parse": "from_%s",
363
+ }
364
+ assert method in METHOD_MAP
365
+
366
+ desired_method = None
367
+
368
+ # Select the desired vendor name.
369
+ desired_vendor = device.vendor.name
370
+
371
+ # Workaround until we implement device drivers
372
+ if device.is_netscreen():
373
+ desired_vendor = "netscreen"
374
+
375
+ vendor_types = self.platforms.get(desired_vendor)
376
+ method_name = METHOD_MAP[method] % desired_vendor # => 'to_cisco'
377
+ device_type = device.deviceType
378
+
379
+ if device_type in vendor_types:
380
+ if hasattr(self, method_name):
381
+ log.msg(f"[{device}] Found {method!r} method: {method_name}")
382
+ desired_method = method_name
383
+ else:
384
+ log.msg(f"[{device}] Did not find {method!r} method: {method_name}")
385
+ else:
386
+ raise exceptions.UnsupportedDeviceType(
387
+ f"Device {device.nodeName!r} has an invalid type {device_type!r} for vendor {desired_vendor!r}. Must be "
388
+ f"one of {vendor_types!r}."
389
+ )
390
+
391
+ if desired_method is None:
392
+ if self.allow_fallback:
393
+ desired_method = METHOD_MAP[method] % "base"
394
+ log.msg(
395
+ f"[{device}] Fallback enabled. Using base method: {desired_method!r}"
396
+ )
397
+ else:
398
+ raise exceptions.UnsupportedVendor(
399
+ f"The vendor {device.vendor.name!r} had no available {method} method. Please check "
400
+ "your `vendors` and `platforms` attributes in your class "
401
+ "object."
402
+ )
403
+
404
+ func = getattr(self, desired_method)
405
+ return func
406
+
407
+ def generate(self, device, commands=None, extra=None):
408
+ """
409
+ Generate commands to be run on a device. If you don't provide
410
+ ``commands`` to the class constructor, this will return an empty list.
411
+
412
+ Define a 'to_{vendor_name}' method to customize the behavior for each
413
+ platform.
414
+
415
+ :param device:
416
+ NetDevice object
417
+ :type device:
418
+ `~trigger.netdevices.NetDevice`
419
+
420
+ :param commands:
421
+ (Optional) A list of commands to execute on the device. If not
422
+ specified in they will be inherited from commands passed to the
423
+ class constructor.
424
+ :type commands:
425
+ list
426
+
427
+ :param extra:
428
+ (Optional) A dictionary of extra data to send to the generate
429
+ method for the device.
430
+ """
431
+ if commands is None:
432
+ commands = self.commands
433
+ if extra is None:
434
+ extra = {}
435
+
436
+ func = self._lookup_method(device, method="generate")
437
+ return func(device, commands, extra)
438
+
439
+ def parse_template(self, results, device, commands=None):
440
+ """
441
+ Generator function that processes unstructured CLI data and yields either
442
+ a TextFSM based object or generic raw output.
443
+
444
+ :param results:
445
+ The unstructured "raw" CLI data from device.
446
+ :type results:
447
+ str
448
+ :param device:
449
+ NetDevice object
450
+ :type device:
451
+ `~trigger.netdevices.NetDevice`
452
+ """
453
+
454
+ device_type = device.os
455
+ ret = []
456
+
457
+ for idx, command in enumerate(commands):
458
+ if device_type:
459
+ try:
460
+ re_table = load_cmd_template(command, dev_type=device_type)
461
+ fsm = get_textfsm_object(re_table, results[idx])
462
+ self.append_parsed_results(
463
+ device, self.map_parsed_results(command, fsm)
464
+ )
465
+ except:
466
+ log.msg(
467
+ "Unable to load TextFSM template, just updating with unstructured output"
468
+ )
469
+
470
+ ret.append(results[idx])
471
+
472
+ return ret
473
+
474
+ def parse(self, results, device, commands=None):
475
+ """
476
+ Parse output from a device. Calls to ``self._lookup_method`` to find
477
+ specific ``from`` method.
478
+
479
+ Define a 'from_{vendor_name}' method to customize the behavior for each
480
+ platform.
481
+
482
+ :param results:
483
+ The results of the commands executed on the device
484
+ :type results:
485
+ list
486
+
487
+ :param device:
488
+ Device object
489
+ :type device:
490
+ `~trigger.netdevices.NetDevice`
491
+
492
+ :param commands:
493
+ (Optional) A list of commands to execute on the device. If not
494
+ specified in they will be inherited from commands passed to the
495
+ class constructor.
496
+ :type commands:
497
+ list
498
+ """
499
+ func = self._lookup_method(device, method="parse")
500
+ return func(results, device, commands)
501
+
502
+ def errback(self, failure, device):
503
+ """
504
+ The default errback. Overload for custom behavior but make sure it
505
+ always decrements the connections.
506
+
507
+ :param failure:
508
+ Usually a Twisted ``Failure`` instance.
509
+
510
+ :param device:
511
+ A `~trigger.netdevices.NetDevice` object
512
+ """
513
+ failure.trap(Exception)
514
+ self.store_error(device, failure)
515
+ # self._decrement_connections(failure)
516
+ return failure
517
+
518
+ def store_error(self, device, error):
519
+ """
520
+ A simple method for storing an error called by all default
521
+ parse/generate methods.
522
+
523
+ If you want to customize the default method for storing results,
524
+ overload this in your subclass.
525
+
526
+ :param device:
527
+ A `~trigger.netdevices.NetDevice` object
528
+
529
+ :param error:
530
+ The error to store. Anything you want really, but usually a Twisted
531
+ ``Failure`` instance.
532
+ """
533
+ devname = str(device)
534
+ self.errors[devname] = error
535
+ return True
536
+
537
+ def append_parsed_results(self, device, results):
538
+ """
539
+ A simple method for appending results called by template parser
540
+ method.
541
+
542
+ If you want to customize the default method for storing parsed
543
+ results, overload this in your subclass.
544
+
545
+ :param device:
546
+ A `~trigger.netdevices.NetDevice` object
547
+
548
+ :param results:
549
+ The results to store. Anything you want really.
550
+ """
551
+ devname = str(device)
552
+ log.msg(f"Appending results for {devname!r}: {results!r}")
553
+ if self.parsed_results.get(devname):
554
+ self.parsed_results[devname].update(results)
555
+ else:
556
+ self.parsed_results[devname] = results
557
+ return True
558
+
559
+ def store_results(self, device, results):
560
+ """
561
+ A simple method for storing results called by all default
562
+ parse/generate methods.
563
+
564
+ If you want to customize the default method for storing results,
565
+ overload this in your subclass.
566
+
567
+ :param device:
568
+ A `~trigger.netdevices.NetDevice` object
569
+
570
+ :param results:
571
+ The results to store. Anything you want really.
572
+ """
573
+ devname = str(device)
574
+ log.msg(f"Storing results for {devname!r}: {results!r}")
575
+ self.results[devname] = results
576
+ return True
577
+
578
+ def map_parsed_results(self, command=None, fsm=None):
579
+ """Return a dict of ``{command: fsm, ...}``"""
580
+ if fsm is None:
581
+ fsm = {}
582
+
583
+ return {command: fsm}
584
+
585
+ def map_results(self, commands=None, results=None):
586
+ """Return a dict of ``{command: result, ...}``"""
587
+ if commands is None:
588
+ commands = self.commands
589
+ if results is None:
590
+ results = []
591
+
592
+ # Python 3: izip_longest renamed to zip_longest
593
+ return dict(itertools.zip_longest(commands, results))
594
+
595
+ @property
596
+ def reactor_running(self):
597
+ """Return whether reactor event loop is running or not"""
598
+ from twisted.internet import reactor
599
+
600
+ log.msg(f"Reactor running? {reactor.running}")
601
+ return reactor.running
602
+
603
+ def _stop(self):
604
+ """Stop the reactor event loop"""
605
+
606
+ if self.stop_reactor:
607
+ log.msg("Stop reactor enabled: stopping reactor...")
608
+ from twisted.internet import reactor
609
+
610
+ if reactor.running:
611
+ reactor.stop()
612
+ else:
613
+ log.msg("stopping reactor... except not really.")
614
+ if self.verbose:
615
+ print("stopping reactor... except not really.")
616
+
617
+ def _start(self):
618
+ """Start the reactor event loop"""
619
+ log.msg("starting reactor. maybe.")
620
+ if self.verbose:
621
+ print("starting reactor. maybe.")
622
+
623
+ if self.curr_conns:
624
+ from twisted.internet import reactor
625
+
626
+ if not reactor.running:
627
+ reactor.run()
628
+ else:
629
+ msg = "Won't start reactor with no work to do!"
630
+ log.msg(msg)
631
+ if self.verbose:
632
+ print(msg)
633
+
634
+ def run(self):
635
+ """
636
+ Nothing happens until you execute this to perform the actual work.
637
+ """
638
+ self._add_worker()
639
+ self._start()
640
+
641
+ # =======================================
642
+ # Base generate (to_)/parse (from_) methods
643
+ # =======================================
644
+ def to_base(self, device, commands=None, extra=None):
645
+ commands = commands or self.commands
646
+ log.msg(f"Sending {commands!r} to {device}")
647
+ return commands
648
+
649
+ def from_base(self, results, device, commands=None):
650
+ commands = commands or self.commands
651
+ log.msg(f"Received {results!r} from {device}")
652
+ self.store_results(device, self.map_results(commands, results))
653
+
654
+ # =======================================
655
+ # Vendor-specific generate (to_)/parse (from_) methods
656
+ # =======================================
657
+ def to_juniper(self, device, commands=None, extra=None):
658
+ """
659
+ This just creates a series of ``<command>foo</command>`` elements to
660
+ pass along to execute_junoscript()"""
661
+ commands = commands or self.commands
662
+
663
+ # If we've set force_cli, use to_base() instead
664
+ if self.force_cli:
665
+ return self.to_base(device, commands, extra)
666
+
667
+ ret = []
668
+ for command in commands:
669
+ cmd = Element("command")
670
+ cmd.text = command
671
+ ret.append(cmd)
672
+
673
+ return ret
674
+
675
+
676
+ class ReactorlessCommando(Commando):
677
+ """
678
+ A reactor-less Commando subclass.
679
+
680
+ This allows multiple instances to coexist, with the side-effect that you
681
+ have to manage the reactor start/stop manually.
682
+
683
+ An example of how this could be used::
684
+
685
+ from twisted.internet import defer, reactor
686
+
687
+ devices = ['dev1', 'dev2']
688
+
689
+ # Our Commando instances. This is an example to show we have two instances
690
+ # co-existing under the same reactor.
691
+ c1 = ShowClock(devices)
692
+ c2 = ShowUsers(devices)
693
+ instances = [c1, c2]
694
+
695
+ # Call the run method for each instance to get a list of Deferred task objects.
696
+ deferreds = []
697
+ for i in instances:
698
+ deferreds.append(i.run())
699
+
700
+ # Here we use a DeferredList to track a list of Deferred tasks that only
701
+ # returns once they've all completed.
702
+ d = defer.DeferredList(deferreds)
703
+
704
+ # Once every task has returned a result, stop the reactor
705
+ d.addBoth(lambda _: reactor.stop())
706
+
707
+ # And... finally, start the reactor to kick things off.
708
+ reactor.run()
709
+
710
+ # Inspect your results
711
+ print(d.result)
712
+ """
713
+
714
+ def _start(self):
715
+ """Initializes ``all_done`` instead of starting the reactor"""
716
+ log.msg("._start() called")
717
+ self.all_done = False
718
+
719
+ def _stop(self):
720
+ """Sets ``all_done`` to True instead of stopping the reactor"""
721
+ log.msg("._stop() called")
722
+ self.all_done = True
723
+
724
+ def run(self):
725
+ """
726
+ We've overloaded the run method to return a Deferred task object.
727
+ """
728
+ log.msg(".run() called")
729
+
730
+ # This is the default behavior
731
+ super().run()
732
+
733
+ # Setup a deferred to hold the delayed result and not return it until
734
+ # it's done. This object will be populated with the value of the
735
+ # results once all commands have been executed on all devices.
736
+ d = defer.Deferred()
737
+
738
+ # Add monitor_result as a callback
739
+ from twisted.internet import reactor
740
+
741
+ d.addCallback(self.monitor_result, reactor)
742
+
743
+ # Tell the reactor to call the callback above when it starts
744
+ reactor.callWhenRunning(d.callback, reactor)
745
+
746
+ return d
747
+
748
+ def monitor_result(self, result, reactor):
749
+ """
750
+ Loop periodically or until the factory stops to check if we're
751
+ ``all_done`` and then return the results.
752
+ """
753
+ # Once we're done, return the results
754
+ if self.all_done:
755
+ return self.results
756
+
757
+ # Otherwise tell the reactor to call me again after 0.5 seconds.
758
+ return task.deferLater(reactor, 0.5, self.monitor_result, result, reactor)
759
+
760
+
761
+ class NetACLInfo(Commando):
762
+ """
763
+ Class to fetch and parse interface information. Exposes a config
764
+ attribute which is a dictionary of devices passed to the constructor and
765
+ their interface information.
766
+
767
+ Each device is a dictionary of interfaces. Each interface field will
768
+ default to an empty list if not populated after parsing. Below is a
769
+ skeleton of the basic config, with expected fields::
770
+
771
+ config {
772
+ 'device1': {
773
+ 'interface1': {
774
+ 'acl_in': [],
775
+ 'acl_out': [],
776
+ 'addr': [],
777
+ 'description': [],
778
+ 'subnets': [],
779
+ }
780
+ }
781
+ }
782
+
783
+ Interface field descriptions:
784
+
785
+ :addr:
786
+ List of ``IPy.IP`` objects of interface addresses
787
+
788
+ :acl_in:
789
+ List of inbound ACL names
790
+
791
+ :acl_out:
792
+ List of outbound ACL names
793
+
794
+ :description:
795
+ List of interface description(s)
796
+
797
+ :subnets:
798
+ List of ``IPy.IP`` objects of interface networks/CIDRs
799
+
800
+ Example::
801
+
802
+ >>> n = NetACLInfo(devices=['jm10-cc101-lab.lab.aol.net'])
803
+ >>> n.run()
804
+ Fetching jm10-cc101-lab.lab.aol.net
805
+ >>> n.config.keys()
806
+ [<NetDevice: jm10-cc101-lab.lab.aol.net>]
807
+ >>> dev = n.config.keys()[0]
808
+ >>> n.config[dev].keys()
809
+ ['lo0.0', 'ge-0/0/0.0', 'ge-0/2/0.0', 'ge-0/1/0.0', 'fxp0.0']
810
+ >>> n.config[dev]['lo0.0'].keys()
811
+ ['acl_in', 'subnets', 'addr', 'acl_out', 'description']
812
+ >>> lo0 = n.config[dev]['lo0.0']
813
+ >>> lo0['acl_in']; lo0['addr']
814
+ ['abc123']
815
+ [IP('66.185.128.160')]
816
+
817
+ This accepts all arguments from the `~trigger.cmds.Commando` parent class,
818
+ as well as this one extra:
819
+
820
+ :param skip_disabled:
821
+ Whether to include interface names without any information. (Default:
822
+ ``True``)
823
+ """
824
+
825
+ def __init__(self, **args):
826
+ try:
827
+ import pyparsing as pp
828
+ except ImportError:
829
+ raise RuntimeError(
830
+ "You must install ``pyparsing==1.5.7`` to use NetACLInfo"
831
+ )
832
+ self.config = {}
833
+ self.skip_disabled = args.pop("skip_disabled", True)
834
+ super().__init__(**args)
835
+
836
+ def IPsubnet(self, addr):
837
+ """Given '172.20.1.4/24', return IP('172.20.1.0/24')."""
838
+ return IP(addr, make_net=True)
839
+
840
+ def IPhost(self, addr):
841
+ """Given '172.20.1.4/24', return IP('172.20.1.4/32')."""
842
+ return IP(addr[: addr.index("/")]) # Only keep before "/"
843
+
844
+ # =======================================
845
+ # Vendor-specific generate (to_)/parse (from_) methods
846
+ # =======================================
847
+
848
+ def to_cisco(self, dev, commands=None, extra=None):
849
+ """This is the "show me all interface information" command we pass to
850
+ IOS devices"""
851
+ if dev.is_cisco_asa():
852
+ return [
853
+ "show running-config | include ^(interface | ip address | nameif | description |access-group|!)"
854
+ ]
855
+ elif dev.is_cisco_nexus():
856
+ return [
857
+ 'show running-config | include "^(interface | ip address | ip access-group | description |!)"'
858
+ ]
859
+ else:
860
+ return [
861
+ "show configuration | include ^(interface | ip address | ip access-group | description|!)"
862
+ ]
863
+
864
+ def to_arista(self, dev, commands=None, extra=None):
865
+ """
866
+ Similar to IOS, but:
867
+
868
+ + Arista has no "show conf" so we have to do "show run"
869
+ + The regex used in the CLI for Arista is more "precise" so we have
870
+ to change the pattern a little bit compared to the on in
871
+ generate_ios_cmd
872
+
873
+ """
874
+ return [
875
+ "show running-config | include (^interface | ip address | ip access-group | description |!)"
876
+ ]
877
+
878
+ def to_force10(self, dev, commands=None, extra=None):
879
+ """
880
+ Similar to IOS, but:
881
+ + You only get the "grep" ("include" equivalent) when using "show
882
+ run".
883
+ + The regex must be quoted.
884
+ """
885
+ return [
886
+ 'show running-config | grep "^(interface | ip address | ip access-group | description|!)"'
887
+ ]
888
+
889
+ # Other IOS-like vendors are Cisco-enough
890
+ to_brocade = to_cisco
891
+ to_foundry = to_cisco
892
+
893
+ def from_cisco(self, data, device, commands=None):
894
+ """Parse IOS config based on EBNF grammar"""
895
+ self.results[device.nodeName] = data # "MY OWN IOS DATA"
896
+ alld = data[0]
897
+
898
+ log.msg("Parsing interface data (%d bytes)" % len(alld))
899
+ if not device.is_cisco_asa():
900
+ self.config[device] = _parse_ios_interfaces(
901
+ alld, skip_disabled=self.skip_disabled
902
+ )
903
+ else:
904
+ self.config[device] = {
905
+ "unsupported": "ASA ACL parsing unsupported this release"
906
+ }
907
+
908
+ return True
909
+
910
+ # Other IOS-like vendors are Cisco-enough
911
+ from_arista = from_cisco
912
+ from_brocade = from_cisco
913
+ from_foundry = from_cisco
914
+ from_force10 = from_cisco
915
+
916
+ def to_juniper(self, dev, commands=None, extra=None):
917
+ """Generates an etree.Element object suitable for use with
918
+ JunoScript"""
919
+ cmd = Element("get-configuration", database="committed", inherit="inherit")
920
+
921
+ SubElement(SubElement(cmd, "configuration"), "interfaces")
922
+
923
+ self.commands = [cmd]
924
+ return self.commands
925
+
926
+ def __children_with_namespace(self, ns):
927
+ return lambda elt, tag: elt.findall("./" + ns + tag)
928
+
929
+ def from_juniper(self, data, device, commands=None):
930
+ """Do all the magic to parse Junos interfaces"""
931
+ self.results[device.nodeName] = data # "MY OWN JUNOS DATA"
932
+
933
+ ns = "{http://xml.juniper.net/xnm/1.1/xnm}"
934
+ children = self.__children_with_namespace(ns)
935
+
936
+ xml = data[0]
937
+ dta = {}
938
+ for interface in xml.getiterator(ns + "interface"):
939
+ basename = children(interface, "name")[0].text
940
+
941
+ description = interface.find(ns + "description")
942
+ desctext = []
943
+ if description is not None:
944
+ desctext.append(description.text)
945
+
946
+ for unit in children(interface, "unit"):
947
+ ifname = basename + "." + children(unit, "name")[0].text
948
+ dta[ifname] = {}
949
+ dta[ifname]["addr"] = []
950
+ dta[ifname]["subnets"] = []
951
+ dta[ifname]["description"] = desctext
952
+ dta[ifname]["acl_in"] = []
953
+ dta[ifname]["acl_out"] = []
954
+
955
+ # Iterating the "family/inet" tree. Seems ugly.
956
+ for family in children(unit, "family"):
957
+ for family2 in family:
958
+ if family2.tag != ns + "inet":
959
+ continue
960
+ for inout in "in", "out":
961
+ dta[ifname][f"acl_{inout}"] = []
962
+
963
+ # Try basic 'filter/xput'...
964
+ acl = family2.find(f"{ns}filter/{ns}{inout}put")
965
+
966
+ # Junos 9.x changes to 'filter/xput/filter-name'
967
+ if acl is not None and " " in acl.text:
968
+ acl = family2.find(
969
+ f"{ns}filter/{ns}{inout}put/{ns}filter-name"
970
+ )
971
+
972
+ # Pushes text as variable name. Must be a better way to do this?
973
+ if acl is not None:
974
+ acl = acl.text
975
+
976
+ # If we couldn't match a single acl, try 'filter/xput-list'
977
+ if not acl:
978
+ # print 'trying filter list..'
979
+ acl = [
980
+ i.text
981
+ for i in family2.findall(
982
+ f"{ns}filter/{ns}{inout}put-list"
983
+ )
984
+ ]
985
+ # if acl: print 'got filter list'
986
+
987
+ # Otherwise, making single acl into a list
988
+ else:
989
+ acl = [acl]
990
+
991
+ # Append acl list to dict
992
+ if acl:
993
+ dta[ifname][f"acl_{inout}"].extend(acl)
994
+
995
+ for node in family2.findall(f"{ns}address/{ns}name"):
996
+ ip = node.text
997
+ dta[ifname]["subnets"].append(self.IPsubnet(ip))
998
+ dta[ifname]["addr"].append(self.IPhost(ip))
999
+
1000
+ self.config[device] = dta
1001
+ return True
1002
+
1003
+
1004
+ def _parse_ios_interfaces(
1005
+ data, acls_as_list=True, auto_cleanup=True, skip_disabled=True
1006
+ ):
1007
+ """
1008
+ Walks through a IOS interface config and returns a dict of parts.
1009
+
1010
+ Intended for use by `~trigger.cmds.NetACLInfo.ios_parse()` but was written
1011
+ to be portable.
1012
+
1013
+ :param acls_as_list:
1014
+ Whether you want acl names as strings instead of list members, e.g.
1015
+
1016
+ :param auto_cleanup:
1017
+ Whether you want to pass results through cleanup_results(). Default: ``True``)
1018
+ "ABC123" vs. ['ABC123']. (Default: ``True``)
1019
+
1020
+ :param skip_disabled:
1021
+ Whether to skip disabled interfaces. (Default: ``True``)
1022
+ """
1023
+ import pyparsing as pp
1024
+
1025
+ # Setup
1026
+ bang = pp.Literal("!").suppress()
1027
+ anychar = pp.Word(pp.printables)
1028
+ pp.Word("".join([x for x in pp.printables if x != "!"]) + "\n\r\t ")
1029
+ bang + pp.restOfLine.suppress()
1030
+
1031
+ # weird things to ignore in foundries
1032
+ pp.Literal("aaa").suppress() + pp.restOfLine.suppress()
1033
+ pp.Literal("module").suppress() + pp.restOfLine.suppress()
1034
+ pp.Literal("Startup").suppress() + pp.restOfLine.suppress()
1035
+ pp.Literal("ver") + anychar # + pp.restOfLine.suppress()
1036
+ # using SkipTO instead now
1037
+
1038
+ # foundry example:
1039
+ # telnet@olse1-dc5#show configuration | include ^(interface | ip address | ip access-group | description|!)
1040
+ #!
1041
+ # Startup-config data location is flash memory
1042
+ #!
1043
+ # Startup configuration:
1044
+ #!
1045
+ # ver 07.5.05hT53
1046
+ #!
1047
+ # module 1 bi-0-port-m4-management-module
1048
+ # module 2 bi-8-port-gig-module
1049
+
1050
+ # there is a lot more that foundry is including in the output that should be ignored
1051
+
1052
+ interface_keyword = pp.Keyword("interface")
1053
+ unwanted = pp.SkipTo(interface_keyword, include=False).suppress()
1054
+
1055
+ # unwanted = pp.ZeroOrMore(bang ^ comment ^ aaa_line ^ module_line ^ startup_line ^ ver_line)
1056
+
1057
+ octet = pp.Word(pp.nums, max=3)
1058
+ ipaddr = pp.Combine(octet + "." + octet + "." + octet + "." + octet)
1059
+ address = ipaddr
1060
+ netmask = ipaddr
1061
+ cidr = pp.Literal("/").suppress() + pp.Word(pp.nums, max=2)
1062
+
1063
+ # Description
1064
+ desc_keyword = pp.Keyword("description")
1065
+ description = pp.Dict(pp.Group(desc_keyword + pp.Group(pp.restOfLine)))
1066
+
1067
+ # Addresses
1068
+ # cisco example:
1069
+ # ip address 172.29.188.27 255.255.255.224 secondary
1070
+ #
1071
+ # foundry example:
1072
+ # ip address 10.62.161.187/26
1073
+
1074
+ ipaddr_keyword = pp.Keyword("ip address").suppress()
1075
+ secondary = pp.Literal("secondary").suppress()
1076
+
1077
+ # foundry matches on cidr and cisco matches on netmask
1078
+ # netmask converted to cidr in cleanup
1079
+ ip_tuple = pp.Group(address + (cidr ^ netmask)).setResultsName(
1080
+ "addr", listAllMatches=True
1081
+ )
1082
+ negotiated = pp.Literal("negotiated") # Seen on Cisco 886
1083
+ ip_address = ipaddr_keyword + (negotiated ^ ip_tuple) + pp.Optional(secondary)
1084
+
1085
+ addrs = pp.ZeroOrMore(ip_address)
1086
+
1087
+ # ACLs
1088
+ acl_keyword = pp.Keyword("ip access-group").suppress()
1089
+
1090
+ # acl_name to be [''] or '' depending on acls_as_list
1091
+ acl_name = pp.Group(anychar) if acls_as_list else anychar
1092
+ direction = pp.oneOf("in out").suppress()
1093
+ acl_in = acl_keyword + pp.FollowedBy(acl_name + pp.Literal("in"))
1094
+ acl_in.setParseAction(pp.replaceWith("acl_in"))
1095
+ acl_out = acl_keyword + pp.FollowedBy(acl_name + pp.Literal("out"))
1096
+ acl_out.setParseAction(pp.replaceWith("acl_out"))
1097
+
1098
+ acl = pp.Dict(pp.Group((acl_in ^ acl_out) + acl_name)) + direction
1099
+ acls = pp.ZeroOrMore(acl)
1100
+
1101
+ # Interfaces
1102
+ iface_keyword = pp.Keyword("interface").suppress()
1103
+ foundry_awesome = pp.Literal(" ").suppress() + anychar
1104
+ # foundry exmaple:
1105
+ #!
1106
+ # interface ethernet 6/6
1107
+ # ip access-group 126 in
1108
+ # ip address 172.18.48.187 255.255.255.255
1109
+
1110
+ # cisco example:
1111
+ #!
1112
+ # interface Port-channel1
1113
+ # description gear1-mtc : AE1 : iwslbfa1-mtc-sw0 : : 1x1000 : 172.20.166.0/24 : : :
1114
+ # ip address 172.20.166.251 255.255.255.0
1115
+
1116
+ interface = pp.Combine(anychar + pp.Optional(foundry_awesome))
1117
+
1118
+ iface_body = (
1119
+ pp.Optional(description)
1120
+ + pp.Optional(acls)
1121
+ + pp.Optional(addrs)
1122
+ + pp.Optional(acls)
1123
+ )
1124
+ # foundry's body is acl then ip and cisco's is ip then acl
1125
+
1126
+ iface_info = (
1127
+ pp.Optional(unwanted)
1128
+ + iface_keyword
1129
+ + pp.Dict(pp.Group(interface + iface_body))
1130
+ + pp.Optional(pp.SkipTo(bang))
1131
+ )
1132
+
1133
+ interfaces = pp.Dict(pp.ZeroOrMore(iface_info))
1134
+
1135
+ # This is where the parsing is actually happening
1136
+ try:
1137
+ results = interfaces.parseString(data)
1138
+ except: # (ParseException, ParseFatalException, RecursiveGrammarException):
1139
+ results = {}
1140
+
1141
+ if auto_cleanup:
1142
+ return _cleanup_interface_results(results, skip_disabled=skip_disabled)
1143
+ return results
1144
+
1145
+
1146
+ def _cleanup_interface_results(results, skip_disabled=True):
1147
+ """
1148
+ Takes ParseResults dictionary-like object and returns an actual dict of
1149
+ populated interface details. The following is performed:
1150
+
1151
+ * Ensures all expected fields are populated
1152
+ * Down/un-addressed interfaces are skipped
1153
+ * Bare IP/CIDR addresses are converted to IPy.IP objects
1154
+
1155
+ :param results:
1156
+ Interface results to parse
1157
+
1158
+ :param skip_disabled:
1159
+ Whether to skip disabled interfaces. (Default: ``True``)
1160
+ """
1161
+ interfaces = sorted(results.keys())
1162
+ newdict = {}
1163
+ for interface in interfaces:
1164
+ iface_info = results[interface]
1165
+
1166
+ # Maybe skip down interfaces
1167
+ if "addr" not in iface_info and skip_disabled:
1168
+ continue
1169
+
1170
+ # Ensure we have a dict to work with.
1171
+ if not iface_info:
1172
+ iface_info = collections.defaultdict(list)
1173
+
1174
+ newdict[interface] = {}
1175
+ new_int = newdict[interface]
1176
+
1177
+ new_int["addr"] = _make_ipy(iface_info.get("addr", []))
1178
+ new_int["subnets"] = _make_cidrs(
1179
+ iface_info.get("subnets", iface_info.get("addr", []))
1180
+ )
1181
+ new_int["acl_in"] = list(iface_info.get("acl_in", []))
1182
+ new_int["acl_out"] = list(iface_info.get("acl_out", []))
1183
+ new_int["description"] = list(iface_info.get("description", []))
1184
+
1185
+ return newdict
1186
+
1187
+
1188
+ def _make_ipy(nets):
1189
+ """Given a list of 2-tuples of (address, netmask), returns a list of
1190
+ IP address objects"""
1191
+ return [IP(addr) for addr, mask in nets]
1192
+
1193
+
1194
+ def _make_cidrs(nets):
1195
+ """Given a list of 2-tuples of (address, netmask), returns a list CIDR
1196
+ blocks"""
1197
+ return [IP(addr).make_net(mask) for addr, mask in nets]
1198
+
1199
+
1200
+ def _dump_interfaces(idict):
1201
+ """Prints a dict of parsed interface results info for use in debugging"""
1202
+ for name, info in idict.items():
1203
+ print(">>>", name)
1204
+ print(
1205
+ "\t",
1206
+ )
1207
+ if idict[name]:
1208
+ if hasattr(info, "keys"):
1209
+ keys = info.keys()
1210
+ print(keys)
1211
+ for key in keys:
1212
+ print("\t", key, ":", info[key])
1213
+ else:
1214
+ print(str(info))
1215
+ else:
1216
+ print("might be shutdown")
1217
+ print