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/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
|