trigger 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. trigger/__init__.py +7 -0
  2. trigger/acl/__init__.py +32 -0
  3. trigger/acl/autoacl.py +70 -0
  4. trigger/acl/db.py +324 -0
  5. trigger/acl/dicts.py +357 -0
  6. trigger/acl/grammar.py +112 -0
  7. trigger/acl/ios.py +222 -0
  8. trigger/acl/junos.py +422 -0
  9. trigger/acl/models.py +118 -0
  10. trigger/acl/parser.py +168 -0
  11. trigger/acl/queue.py +296 -0
  12. trigger/acl/support.py +1431 -0
  13. trigger/acl/tools.py +746 -0
  14. trigger/bin/__init__.py +0 -0
  15. trigger/bin/acl.py +233 -0
  16. trigger/bin/acl_script.py +574 -0
  17. trigger/bin/aclconv.py +82 -0
  18. trigger/bin/check_access.py +93 -0
  19. trigger/bin/check_syntax.py +66 -0
  20. trigger/bin/fe.py +197 -0
  21. trigger/bin/find_access.py +191 -0
  22. trigger/bin/gnng.py +434 -0
  23. trigger/bin/gong.py +86 -0
  24. trigger/bin/load_acl.py +841 -0
  25. trigger/bin/load_config.py +18 -0
  26. trigger/bin/netdev.py +317 -0
  27. trigger/bin/optimizer.py +638 -0
  28. trigger/bin/run_cmds.py +18 -0
  29. trigger/changemgmt/__init__.py +352 -0
  30. trigger/changemgmt/bounce.py +57 -0
  31. trigger/cmds.py +1217 -0
  32. trigger/conf/__init__.py +94 -0
  33. trigger/conf/global_settings.py +674 -0
  34. trigger/contrib/__init__.py +7 -0
  35. trigger/exceptions.py +307 -0
  36. trigger/gorc.py +172 -0
  37. trigger/netdevices/__init__.py +1288 -0
  38. trigger/netdevices/loader.py +174 -0
  39. trigger/netscreen.py +1030 -0
  40. trigger/packages/__init__.py +6 -0
  41. trigger/packages/peewee.py +8084 -0
  42. trigger/rancid.py +463 -0
  43. trigger/tacacsrc.py +584 -0
  44. trigger/twister.py +2203 -0
  45. trigger/twister2.py +745 -0
  46. trigger/utils/__init__.py +88 -0
  47. trigger/utils/cli.py +349 -0
  48. trigger/utils/importlib.py +77 -0
  49. trigger/utils/network.py +157 -0
  50. trigger/utils/rcs.py +178 -0
  51. trigger/utils/templates.py +81 -0
  52. trigger/utils/url.py +78 -0
  53. trigger/utils/xmltodict.py +298 -0
  54. trigger-2.0.0.dist-info/METADATA +146 -0
  55. trigger-2.0.0.dist-info/RECORD +61 -0
  56. trigger-2.0.0.dist-info/WHEEL +5 -0
  57. trigger-2.0.0.dist-info/entry_points.txt +15 -0
  58. trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
  59. trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
  60. trigger-2.0.0.dist-info/top_level.txt +2 -0
  61. twisted/plugins/trigger_xmlrpc.py +124 -0
@@ -0,0 +1,1288 @@
1
+ """
2
+ The heart and soul of Trigger, NetDevices is an abstract interface to network
3
+ device metadata and ACL associations.
4
+
5
+ Parses :setting:`NETDEVICES_SOURCE` and makes available a dictionary of
6
+ `~trigger.netdevices.NetDevice` objects, which is keyed by the FQDN of every
7
+ network device.
8
+
9
+ Other interfaces are non-public.
10
+
11
+ Example::
12
+
13
+ >>> from trigger.netdevices import NetDevices
14
+ >>> nd = NetDevices()
15
+ >>> dev = nd['test1-abc.net.aol.com']
16
+ >>> dev.vendor, dev.make
17
+ (<Vendor: Juniper>, 'MX960-BASE-AC')
18
+ >>> dev.bounce.next_ok('green')
19
+ datetime.datetime(2010, 4, 9, 9, 0, tzinfo=<UTC>)
20
+
21
+ """
22
+
23
+ # Imports
24
+ import copy
25
+ import itertools
26
+ import os
27
+ import re
28
+ import sys
29
+ import time
30
+ import xml.etree.ElementTree as ET
31
+ from collections.abc import MutableMapping
32
+
33
+ from crochet import run_in_reactor, setup, wait_for
34
+ from twisted.internet import defer, reactor
35
+ from twisted.internet.protocol import Factory
36
+ from twisted.python import log
37
+
38
+ from trigger import changemgmt, exceptions, rancid
39
+ from trigger.conf import settings
40
+ from trigger.utils import network, parse_node_port
41
+ from trigger.utils.url import parse_url
42
+
43
+ from . import loader
44
+
45
+ try:
46
+ from trigger.acl.db import AclsDB
47
+ except ImportError:
48
+ log.msg("ACLs database could not be loaded; Loading without ACL support")
49
+ settings.WITH_ACLS = False
50
+
51
+
52
+ # Constants
53
+ JUNIPER_COMMIT = ET.Element("commit-configuration")
54
+ JUNIPER_COMMIT_FULL = copy.copy(JUNIPER_COMMIT)
55
+ ET.SubElement(JUNIPER_COMMIT_FULL, "full")
56
+
57
+
58
+ # Exports
59
+ __all__ = ["device_match", "NetDevice", "NetDevices", "Vendor"]
60
+
61
+
62
+ # Functions
63
+ def _munge_source_data(data_source=settings.NETDEVICES_SOURCE):
64
+ """
65
+ Read the source data in the specified format, parse it, and return a
66
+
67
+ :param data_source:
68
+ Absolute path to source data file
69
+ """
70
+ log.msg("LOADING FROM: ", data_source)
71
+ kwargs = parse_url(data_source)
72
+ path = kwargs.pop("path")
73
+ return loader.load_metadata(path, **kwargs)
74
+
75
+
76
+ def _populate(netdevices, data_source, production_only, with_acls):
77
+ """
78
+ Populates the NetDevices with NetDevice objects.
79
+
80
+ Abstracted from within NetDevices to prevent accidental repopulation of NetDevice
81
+ objects.
82
+ """
83
+ # start = time.time()
84
+ loader, device_data = _munge_source_data(data_source=data_source)
85
+ netdevices.set_loader(loader)
86
+
87
+ # Populate AclsDB if `with_acls` is set
88
+ if with_acls:
89
+ log.msg("NetDevices ACL associations: ENABLED")
90
+ aclsdb = AclsDB()
91
+ else:
92
+ log.msg("NetDevices ACL associations: DISABLED")
93
+ aclsdb = None
94
+
95
+ # Populate `netdevices` dictionary with `NetDevice` objects!
96
+ for obj in device_data:
97
+ # Don't process it if it's already a NetDevice
98
+ if isinstance(obj, NetDevice):
99
+ dev = obj
100
+ else:
101
+ dev = NetDevice(data=obj, with_acls=aclsdb)
102
+
103
+ # Only return devices with adminStatus of 'PRODUCTION' unless
104
+ # `production_only` is True
105
+ if dev.adminStatus.upper() != "PRODUCTION" and production_only:
106
+ log.msg(f"[{dev.nodeName}] Skipping: adminStatus not PRODUCTION")
107
+ continue
108
+
109
+ # These checks should be done on generation of netdevices.xml.
110
+ # Skip empty nodenames
111
+ if dev.nodeName is None:
112
+ continue
113
+
114
+ # Add to dict
115
+ netdevices.add_device(dev)
116
+
117
+ # end = time.time()
118
+ # print 'Took %f seconds' % (end - start)
119
+
120
+
121
+ def device_match(name, production_only=True):
122
+ """
123
+ Return a matching :class:`~trigger.netdevices.NetDevice` object based on
124
+ partial name. Return `None` if no match or if multiple matches is
125
+ cancelled::
126
+
127
+ >>> device_match('test')
128
+ 2 possible matches found for 'test':
129
+ [ 1] test1-abc.net.aol.com
130
+ [ 2] test2-abc.net.aol.com
131
+ [ 0] Exit
132
+
133
+ Enter a device number: 2
134
+ <NetDevice: test2-abc.net.aol.com>
135
+
136
+ If there is only a single match, that device object is returned without
137
+ a prompt::
138
+
139
+ >>> device_match('fw')
140
+ Matched 'fw1-xyz.net.aol.com'.
141
+ <NetDevice: fw1-xyz.net.aol.com>
142
+ """
143
+ match = None
144
+ nd = NetDevices(production_only)
145
+ try:
146
+ match = nd.find(name)
147
+ except KeyError:
148
+ matches = nd.search(name)
149
+ if matches:
150
+ if len(matches) == 1:
151
+ single = matches[0]
152
+ print(f"Matched '{single}'.")
153
+ return single
154
+
155
+ print("%d possible matches found for '%s':" % (len(matches), name))
156
+
157
+ matches.sort()
158
+ for num, shortname in enumerate(matches):
159
+ print(f" [{str(num + 1).rjust(2)}] {shortname}")
160
+ print(" [ 0] Exit\n")
161
+
162
+ choice = int(input("Enter a device number: ")) - 1
163
+ match = None if choice < 0 else matches[choice]
164
+ log.msg(f"Choice: {choice}")
165
+ log.msg(f"You chose: {match}")
166
+ else:
167
+ print(f"No matches for '{name}'.")
168
+
169
+ return match
170
+
171
+
172
+ # Classes
173
+ class NetDevice:
174
+ """
175
+ An object that represents a distinct network device and its metadata.
176
+
177
+ Almost all of the attributes are populated by
178
+ `~trigger.netdevices._populate()` and are mostly dependent upon the source
179
+ data. This is prone to implementation problems and should be revisited in
180
+ the long-run as there are certain fields that are baked into the core
181
+ functionality of Trigger.
182
+
183
+ Users usually won't create these objects directly! Rely instead upon
184
+ `~trigger.netdevice.NetDevices` to do this for you.
185
+ """
186
+
187
+ def __init__(self, data=None, with_acls=None):
188
+ # Here comes all of the bare minimum set of attributes a NetDevice
189
+ # object needs for basic functionality within the existing suite.
190
+
191
+ # Hostname
192
+ self.nodeName = None
193
+ self.nodePort = None
194
+
195
+ # Hardware Info
196
+ self.deviceType = None
197
+ self.make = None
198
+ self.manufacturer = settings.FALLBACK_MANUFACTURER
199
+ self.vendor = None
200
+ self.model = None
201
+ self.serialNumber = None
202
+
203
+ # Administrivia
204
+ self.adminStatus = settings.DEFAULT_ADMIN_STATUS
205
+ self.assetID = None
206
+ self.budgetCode = None
207
+ self.budgetName = None
208
+ self.enablePW = None
209
+ self.owningTeam = None
210
+ self.owner = None
211
+ self.onCallName = None
212
+ self.operationStatus = None
213
+ self.lastUpdate = None
214
+ self.lifecycleStatus = None
215
+ self.projectName = None
216
+
217
+ # Location
218
+ self.site = None
219
+ self.room = None
220
+ self.coordinate = None
221
+
222
+ # If `data` has been passed, use it to update our attributes
223
+ if data is not None:
224
+ self._populate_data(data)
225
+
226
+ # Set node remote port based on "hostname:port" as nodeName
227
+ self._set_node_port()
228
+
229
+ # Cleanup the attributes (strip whitespace, lowercase values, etc.)
230
+ self._cleanup_attributes()
231
+
232
+ # Map the manufacturer name to a Vendor object that has extra sauce
233
+ if self.manufacturer is not None:
234
+ self.vendor = vendor_factory(self.manufacturer)
235
+
236
+ # Use the vendor to populate the deviceType if it's not set already
237
+ if self.deviceType is None:
238
+ self._populate_deviceType()
239
+
240
+ # ACLs (defaults to empty sets)
241
+ self.explicit_acls = self.implicit_acls = self.acls = self.bulk_acls = set()
242
+ if with_acls:
243
+ log.msg(f"[{self.nodeName}] Populating ACLs")
244
+ self._populate_acls(aclsdb=with_acls)
245
+
246
+ # Bind the correct execute/connect methods based on deviceType
247
+ self._bind_dynamic_methods()
248
+
249
+ # Set the correct command(s) to run on startup based on deviceType
250
+ self.startup_commands = self._set_startup_commands()
251
+
252
+ # Assign the configuration commit commands (e.g. 'write memory')
253
+ self.commit_commands = self._set_commit_commands()
254
+
255
+ # Determine whether we require an async pty SSH channel
256
+ self.requires_async_pty = self._set_requires_async_pty()
257
+
258
+ # Set the correct line-ending per vendor
259
+ self.delimiter = self._set_delimiter()
260
+
261
+ # Set initial endpoint state
262
+ self.factories = {}
263
+ self._connected = False
264
+ self._endpoint = None
265
+
266
+ def _populate_data(self, data):
267
+ """
268
+ Populate the custom attribute data
269
+
270
+ :param data:
271
+ An iterable of key/value pairs
272
+ """
273
+ self.__dict__.update(data) # Better hope this is a dict!
274
+
275
+ def _cleanup_attributes(self):
276
+ """Perform various cleanup actions. Abstracted for customization."""
277
+ # Lowercase the nodeName for completeness.
278
+ if self.nodeName is not None:
279
+ self.nodeName = self.nodeName.lower()
280
+
281
+ if self.deviceType is not None:
282
+ self.deviceType = self.deviceType.upper()
283
+
284
+ # Make sure the password is bytes not unicode
285
+ if self.enablePW is not None:
286
+ self.enablePW = str(self.enablePW)
287
+
288
+ # Cleanup whitespace from owning team
289
+ if self.owningTeam is not None:
290
+ self.owningTeam = self.owningTeam.strip()
291
+
292
+ # Map deviceStatus to adminStatus when data source is RANCID
293
+ if hasattr(self, "deviceStatus"):
294
+ STATUS_MAP = {
295
+ "up": "PRODUCTION",
296
+ "down": "NON-PRODUCTION",
297
+ }
298
+ self.adminStatus = STATUS_MAP.get(self.deviceStatus, STATUS_MAP["up"])
299
+
300
+ def _set_node_port(self):
301
+ """Set the freakin' TCP port"""
302
+ # If nodename is set, try to parse out a nodePort
303
+ if self.nodeName is not None:
304
+ nodeport_info = parse_node_port(self.nodeName)
305
+ nodeName, nodePort = nodeport_info
306
+
307
+ # If the nodeName differs, use it to replace the one we parsed
308
+ if nodeName != self.nodeName:
309
+ self.nodeName = nodeName
310
+
311
+ # If the port isn't set, set it
312
+ if nodePort is not None:
313
+ self.nodePort = nodePort
314
+ return None
315
+
316
+ # Make sure the port is an integer if it's not None
317
+ if self.nodePort is not None and isinstance(self.nodePort, str):
318
+ self.nodePort = int(self.nodePort)
319
+
320
+ def _populate_deviceType(self):
321
+ """Try to make a guess what the device type is"""
322
+ self.deviceType = settings.DEFAULT_TYPES.get(
323
+ self.vendor.name, settings.FALLBACK_TYPE
324
+ )
325
+
326
+ def _set_requires_async_pty(self):
327
+ """
328
+ Set whether a device requires an async pty (see:
329
+ `~trigger.twister.TriggerSSHAsyncPtyChannel`).
330
+ """
331
+ RULES = (
332
+ self.vendor in ("a10", "arista", "aruba", "cisco", "cumulus", "force10"),
333
+ self.is_brocade_vdx(),
334
+ )
335
+ return any(RULES)
336
+
337
+ def _set_delimiter(self):
338
+ """
339
+ Set the delimiter to use for line-endings.
340
+ """
341
+ default = "\n"
342
+ delimiter_map = {
343
+ "force10": "\r\n",
344
+ }
345
+ delimiter = delimiter_map.get(self.vendor.name, default)
346
+ return delimiter
347
+
348
+ def _set_startup_commands(self):
349
+ """
350
+ Set the commands to run at startup. For now they are just ones to
351
+ disable pagination.
352
+ """
353
+
354
+ def get_vendor_name():
355
+ """Return the vendor name for startup commands lookup."""
356
+ if self.is_brocade_vdx():
357
+ return "brocade_vdx"
358
+ elif self.is_cisco_asa():
359
+ return "cisco_asa"
360
+ elif self.is_netscreen():
361
+ return "netscreen"
362
+ else:
363
+ return self.vendor.name
364
+
365
+ paging_map = settings.STARTUP_COMMANDS_MAP
366
+ cmds = paging_map.get(get_vendor_name())
367
+
368
+ if cmds is not None:
369
+ return cmds
370
+
371
+ return []
372
+
373
+ def _set_commit_commands(self):
374
+ """
375
+ Return the proper "commit" command. (e.g. write mem, etc.)
376
+ """
377
+ if self.is_ioslike():
378
+ return self._ioslike_commit()
379
+ elif self.is_netscaler() or self.is_netscreen():
380
+ return ["save config"]
381
+ elif self.vendor == "juniper":
382
+ return self._juniper_commit()
383
+ elif self.vendor == "paloalto":
384
+ return ["commit"]
385
+ elif self.vendor == "pica8":
386
+ return ["commit"]
387
+ elif self.vendor == "mrv":
388
+ return ["save configuration flash"]
389
+ elif self.vendor == "f5":
390
+ return ["save sys config"]
391
+ else:
392
+ return []
393
+
394
+ def _ioslike_commit(self):
395
+ """
396
+ Return proper 'write memory' command for IOS-like devices.
397
+ """
398
+ if self.is_brocade_vdx() or self.vendor == "dell":
399
+ return ["copy running-config startup-config", "y"]
400
+ elif self.is_cisco_nexus():
401
+ return ["copy running-config startup-config"]
402
+ else:
403
+ return ["write memory"]
404
+
405
+ def _juniper_commit(self, fields=settings.JUNIPER_FULL_COMMIT_FIELDS):
406
+ """
407
+ Return proper ``commit-configuration`` element for a Juniper
408
+ device.
409
+ """
410
+ default = [JUNIPER_COMMIT]
411
+ if not fields:
412
+ return default
413
+
414
+ # Either it's a normal "commit-configuration"
415
+ for attr, val in fields.items():
416
+ if not getattr(self, attr) == val:
417
+ return default
418
+
419
+ # Or it's a "commit-configuration full"
420
+ return [JUNIPER_COMMIT_FULL]
421
+
422
+ def _bind_dynamic_methods(self):
423
+ """
424
+ Bind dynamic methods to the instance. Currently does these:
425
+
426
+ + Dynamically bind ~trigger.twister.excute` to .execute()
427
+ + Dynamically bind ~trigger.twister.connect` to .connect()
428
+
429
+ Note that these both rely on the value of the ``vendor`` attribute.
430
+ """
431
+ from trigger import twister
432
+
433
+ self.execute = twister.execute.__get__(self, self.__class__)
434
+ self.connect = twister.connect.__get__(self, self.__class__)
435
+
436
+ def _populate_acls(self, aclsdb=None):
437
+ """
438
+ Populate the associated ACLs for this device.
439
+
440
+ :param aclsdb:
441
+ An `~trigger.acl.db.AclsDB` object.
442
+ """
443
+ if not aclsdb:
444
+ return None
445
+
446
+ acls_dict = aclsdb.get_acl_dict(self)
447
+ self.explicit_acls = acls_dict["explicit"]
448
+ self.implicit_acls = acls_dict["implicit"]
449
+ self.acls = acls_dict["all"]
450
+
451
+ def __str__(self):
452
+ return self.nodeName
453
+
454
+ def __repr__(self):
455
+ return f"<NetDevice: {self.nodeName}>"
456
+
457
+ def __eq__(self, other):
458
+ """Compare NetDevice objects by nodeName."""
459
+ if not isinstance(other, NetDevice):
460
+ return NotImplemented
461
+ return self.nodeName == other.nodeName
462
+
463
+ def __ne__(self, other):
464
+ result = self.__eq__(other)
465
+ if result is NotImplemented:
466
+ return NotImplemented
467
+ return not result
468
+
469
+ def __lt__(self, other):
470
+ if not isinstance(other, NetDevice):
471
+ return NotImplemented
472
+ return self.nodeName < other.nodeName
473
+
474
+ def __le__(self, other):
475
+ if not isinstance(other, NetDevice):
476
+ return NotImplemented
477
+ return self.nodeName <= other.nodeName
478
+
479
+ def __gt__(self, other):
480
+ if not isinstance(other, NetDevice):
481
+ return NotImplemented
482
+ return self.nodeName > other.nodeName
483
+
484
+ def __ge__(self, other):
485
+ if not isinstance(other, NetDevice):
486
+ return NotImplemented
487
+ return self.nodeName >= other.nodeName
488
+
489
+ @property
490
+ def bounce(self):
491
+ return changemgmt.bounce(self)
492
+
493
+ @property
494
+ def shortName(self):
495
+ return self.nodeName.split(".", 1)[0]
496
+
497
+ @property
498
+ def os(self):
499
+ vendor_mapping = settings.TEXTFSM_VENDOR_MAPPINGS
500
+ try:
501
+ oss = vendor_mapping[self.vendor]
502
+ if self.operatingSystem.lower() in oss:
503
+ return f"{self.vendor}_{self.operatingSystem.lower()}"
504
+ except:
505
+ log.msg("""Unable to find template for given device.
506
+ Check to see if your netdevices object has the 'platform' key.
507
+ Otherwise template does not exist.""")
508
+ return None
509
+
510
+ def _get_endpoint(self, *args):
511
+ """Private method used for generating an endpoint for `~trigger.netdevices.NetDevice`."""
512
+ from trigger.twister2 import (
513
+ IoslikeSendExpect,
514
+ TriggerEndpointClientFactory,
515
+ generate_endpoint,
516
+ )
517
+
518
+ endpoint = generate_endpoint(self).wait()
519
+
520
+ factory = TriggerEndpointClientFactory()
521
+ factory.protocol = IoslikeSendExpect
522
+
523
+ self.factories["base"] = factory
524
+
525
+ # FIXME(jathan): prompt_pattern could move back to protocol?
526
+ prompt = re.compile(settings.IOSLIKE_PROMPT_PAT)
527
+ proto = endpoint.connect(factory, prompt_pattern=prompt)
528
+ self._proto = proto # Track this for later, too.
529
+
530
+ return proto
531
+
532
+ def open(self):
533
+ """Open new session with `~trigger.netdevices.NetDevice`.
534
+
535
+ Example:
536
+ >>> nd = NetDevices()
537
+ >>> dev = nd.find('arista-sw1.demo.local')
538
+ >>> dev.open()
539
+
540
+ """
541
+
542
+ def inject_net_device_into_protocol(proto):
543
+ """Now we're only injecting connection for use later."""
544
+ self._conn = proto.transport.conn
545
+ return proto
546
+
547
+ self._endpoint = self._get_endpoint()
548
+
549
+ if self._endpoint is None:
550
+ raise ValueError("Endpoint has not been instantiated.")
551
+
552
+ self.d = self._endpoint.addCallback(inject_net_device_into_protocol)
553
+
554
+ self._connected = True
555
+ return self._connected
556
+
557
+ def close(self):
558
+ """Close an open `~trigger.netdevices.NetDevice` object."""
559
+
560
+ def disconnect(proto):
561
+ proto.transport.loseConnection()
562
+ return proto
563
+
564
+ if self._endpoint is None:
565
+ raise ValueError("Endpoint has not been instantiated.")
566
+
567
+ self._endpoint.addCallback(disconnect)
568
+
569
+ self._connected = False
570
+ return
571
+
572
+ def __enter__(self):
573
+ self.open()
574
+ return self
575
+
576
+ def __exit__(self, exc_type, exc_value, traceback):
577
+ self.close()
578
+
579
+ def get_results(self):
580
+ self._results = []
581
+ while len(self._results) != len(self.commands):
582
+ pass
583
+ return self._results
584
+
585
+ def run_channeled_commands(self, commands, on_error=None):
586
+ """Public method for scheduling commands onto device.
587
+
588
+ This variant allows for efficient multiplexing of commands across multiple vty
589
+ lines where supported ie Arista and Cumulus.
590
+
591
+ :param commands: List containing commands to schedule onto device loop.
592
+ :type commands: list
593
+ :param on_error: Error handler
594
+ :type on_error: func
595
+
596
+ :Example:
597
+ >>> ...
598
+ >>> dev.open()
599
+ >>> dev.run_channeled_commands(['show ip int brief', 'show version'], on_error=lambda x: handle(x))
600
+
601
+ """
602
+ from trigger.twister2 import (
603
+ IoslikeSendExpect,
604
+ TriggerEndpointClientFactory,
605
+ TriggerSSHShellClientEndpointBase,
606
+ )
607
+
608
+ if on_error is None:
609
+
610
+ def on_error(x):
611
+ return x
612
+
613
+ factory = TriggerEndpointClientFactory()
614
+ factory.protocol = IoslikeSendExpect
615
+ self.factories["channeled"] = factory
616
+
617
+ # Here's where we're using self._connect injected on .open()
618
+ ep = TriggerSSHShellClientEndpointBase.existingConnection(self._conn)
619
+ prompt = re.compile(settings.IOSLIKE_PROMPT_PAT)
620
+ proto = ep.connect(factory, prompt_pattern=prompt)
621
+
622
+ d = defer.Deferred()
623
+
624
+ def inject_commands_into_protocol(proto):
625
+ result = proto.add_commands(commands, on_error)
626
+ result.addCallback(lambda results: d.callback(results))
627
+ result.addBoth(on_error)
628
+ return proto
629
+
630
+ proto = proto.addCallbacks(inject_commands_into_protocol)
631
+
632
+ return d
633
+
634
+ def run_commands(self, commands, on_error=None):
635
+ """Public method for scheduling commands onto device.
636
+
637
+ Default implementation that schedules commands onto a Device loop.
638
+ This implementation ensures commands are executed sequentially.
639
+
640
+ :param commands: List containing commands to schedule onto device loop.
641
+ :type commands: list
642
+ :param on_error: Error handler
643
+ :type on_error: func
644
+
645
+ :Example:
646
+ >>> ...
647
+ >>> dev.open()
648
+ >>> dev.run_commands(['show ip int brief', 'show version'], on_error=lambda x: handle(x))
649
+
650
+ """
651
+ from trigger.twister2 import (
652
+ IoslikeSendExpect,
653
+ TriggerEndpointClientFactory,
654
+ TriggerSSHShellClientEndpointBase,
655
+ )
656
+
657
+ if on_error is None:
658
+
659
+ def on_error(x):
660
+ return x
661
+
662
+ factory = TriggerEndpointClientFactory()
663
+ factory.protocol = IoslikeSendExpect
664
+
665
+ proto = self._proto
666
+
667
+ d = defer.Deferred()
668
+
669
+ def inject_commands_into_protocol(proto):
670
+ result = proto.add_commands(commands, on_error)
671
+ result.addCallback(lambda results: d.callback(results))
672
+ result.addBoth(on_error)
673
+ return proto
674
+
675
+ proto = proto.addCallbacks(inject_commands_into_protocol)
676
+
677
+ return d
678
+
679
+ @property
680
+ def connected(self):
681
+ return self._connected
682
+
683
+ def allowable(self, action, when=None):
684
+ """
685
+ Return whether it's okay to perform the specified ``action``.
686
+
687
+ False means a bounce window conflict. For now ``'load-acl'`` is the
688
+ only valid action and moratorium status is not checked.
689
+
690
+ :param action:
691
+ The action to check.
692
+
693
+ :param when:
694
+ A datetime object.
695
+ """
696
+ assert action == "load-acl"
697
+ return self.bounce.status(when) == changemgmt.BounceStatus("green")
698
+
699
+ def next_ok(self, action, when=None):
700
+ """
701
+ Return the next time at or after the specified time (default now)
702
+ that it will be ok to perform the specified action.
703
+
704
+ :param action:
705
+ The action to check.
706
+
707
+ :param when:
708
+ A datetime object.
709
+ """
710
+ assert action == "load-acl"
711
+ return self.bounce.next_ok(changemgmt.BounceStatus("green"), when)
712
+
713
+ def is_router(self):
714
+ """Am I a router?"""
715
+ return self.deviceType == "ROUTER"
716
+
717
+ def is_switch(self):
718
+ """Am I a switch?"""
719
+ return self.deviceType == "SWITCH"
720
+
721
+ def is_firewall(self):
722
+ """Am I a firewall?"""
723
+ return self.deviceType == "FIREWALL"
724
+
725
+ def is_netscaler(self):
726
+ """Am I a NetScaler?"""
727
+ return all([self.is_switch(), self.vendor == "citrix"])
728
+
729
+ def is_pica8(self):
730
+ """Am I a Pica8?"""
731
+ ## This is only really needed because pica8
732
+ ## doesn't have a global command to disable paging
733
+ ## so we need to do some special magic.
734
+ return all([self.vendor == "pica8"])
735
+
736
+ def is_netscreen(self):
737
+ """Am I a NetScreen running ScreenOS?"""
738
+ # Are we even a firewall?
739
+ if not self.is_firewall():
740
+ return False
741
+
742
+ # If vendor or make is netscreen, automatically True
743
+ make_netscreen = self.make is not None and self.make.lower() == "netscreen"
744
+ if self.vendor == "netscreen" or make_netscreen:
745
+ return True
746
+
747
+ # Final check: Are we made by Juniper and an SSG? This requires that
748
+ # make or model is populated and has the word 'ssg' in it. This still
749
+ # fails if it's an SSG running JunOS, but this is not an edge case we
750
+ # can easily support at this time.
751
+ is_ssg = (self.model is not None and "ssg" in self.model.lower()) or (
752
+ self.make is not None and "ssg" in self.make.lower()
753
+ )
754
+ return self.vendor == "juniper" and is_ssg
755
+
756
+ def is_ioslike(self):
757
+ """
758
+ Am I an IOS-like device (as determined by :setting:`IOSLIKE_VENDORS`)?
759
+ """
760
+ return self.vendor in settings.IOSLIKE_VENDORS
761
+
762
+ def is_cumulus(self):
763
+ """
764
+ Am I running Cumulus?
765
+ """
766
+ return self.vendor == "cumulus"
767
+
768
+ def is_brocade_vdx(self):
769
+ """
770
+ Am I a Brocade VDX switch?
771
+
772
+ This is used to account for the disparity between the Brocade FCX
773
+ switches (which behave like Foundry devices) and the Brocade VDX
774
+ switches (which behave differently from classic Foundry devices).
775
+ """
776
+ if hasattr(self, "_is_brocade_vdx"):
777
+ return self._is_brocade_vdx
778
+
779
+ if not (self.vendor == "brocade" and self.is_switch()):
780
+ self._is_brocade_vdx = False
781
+ return False
782
+
783
+ if self.make is not None:
784
+ self._is_brocade_vdx = "vdx" in self.make.lower()
785
+ return self._is_brocade_vdx
786
+
787
+ def is_cisco_asa(self):
788
+ """
789
+ Am I a Cisco ASA Firewall?
790
+
791
+ This is used to account for slight differences in the commands that
792
+ may be used between Cisco's ASA and IOS platforms. Cisco ASA is still
793
+ very IOS-like, but there are still several gotcha's between the
794
+ platforms.
795
+
796
+ Will return True if vendor is Cisco and platform is Firewall. This
797
+ is to allow operability if using .csv NetDevices and pretty safe to
798
+ assume considering ASA (was PIX) are Cisco's flagship(if not only)
799
+ Firewalls.
800
+ """
801
+ if hasattr(self, "_is_cisco_asa"):
802
+ return self._is_cisco_asa
803
+
804
+ if not (self.vendor == "cisco" and self.is_firewall()):
805
+ self._is_cisco_asa = False
806
+ return False
807
+
808
+ if self.make is not None:
809
+ self._is_cisco_asa = "asa" in self.make.lower()
810
+
811
+ self._is_cisco_asa = self.vendor == "cisco" and self.is_firewall()
812
+
813
+ return self._is_cisco_asa
814
+
815
+ def is_cisco_nexus(self):
816
+ """
817
+ Am I a Cisco Nexus device?
818
+ """
819
+ words = (self.make, self.model)
820
+ patterns = ("n.k", "nexus") # Patterns to match
821
+ pairs = itertools.product(patterns, words)
822
+
823
+ for pat, word in pairs:
824
+ if word and re.search(pat, word.lower()):
825
+ return True
826
+ return False
827
+
828
+ def _ssh_enabled(self, disabled_mapping):
829
+ """Check whether vendor/type is enabled against the given mapping."""
830
+ disabled_types = disabled_mapping.get(self.vendor.name, [])
831
+ return self.deviceType not in disabled_types
832
+
833
+ def has_ssh(self):
834
+ """Am I even listening on SSH?"""
835
+ return network.test_ssh(self.nodeName)
836
+
837
+ def _can_ssh(self, method):
838
+ """
839
+ Am I enabled to use SSH for the given method in Trigger settings, and
840
+ if so do I even have SSH?
841
+
842
+ :param method: One of ('pty', 'async')
843
+ """
844
+ METHOD_MAP = {
845
+ "pty": settings.SSH_PTY_DISABLED,
846
+ "async": settings.SSH_ASYNC_DISABLED,
847
+ }
848
+ assert method in METHOD_MAP
849
+ method_enabled = self._ssh_enabled(METHOD_MAP[method])
850
+
851
+ return method_enabled and self.has_ssh()
852
+
853
+ def can_ssh_async(self):
854
+ """Am I enabled to use SSH async?"""
855
+ return self._can_ssh("async")
856
+
857
+ def can_ssh_pty(self):
858
+ """Am I enabled to use SSH pty?"""
859
+ return self._can_ssh("pty")
860
+
861
+ def is_reachable(self):
862
+ """Do I respond to a ping?"""
863
+ return network.ping(self.nodeName)
864
+
865
+ def dump(self):
866
+ """Prints details for a device."""
867
+ dev = self
868
+ # Python 3: print is a function, not a statement
869
+ print()
870
+ print("\tHostname: ", dev.nodeName)
871
+ print("\tOwning Org.: ", dev.owner)
872
+ print("\tOwning Team: ", dev.owningTeam)
873
+ print("\tOnCall Team: ", dev.onCallName)
874
+ print()
875
+ print("\tVendor: ", f"{dev.vendor.title} ({dev.manufacturer})")
876
+ # print '\tManufacturer: ', dev.manufacturer
877
+ print("\tMake: ", dev.make)
878
+ print("\tModel: ", dev.model)
879
+ print("\tType: ", dev.deviceType)
880
+ print("\tLocation: ", dev.site, dev.room, dev.coordinate)
881
+ print()
882
+ print("\tProject: ", dev.projectName)
883
+ print("\tSerial: ", dev.serialNumber)
884
+ print("\tAsset Tag: ", dev.assetID)
885
+ print("\tBudget Code: ", f"{dev.budgetCode} ({dev.budgetName})")
886
+ print()
887
+ print("\tAdmin Status: ", dev.adminStatus)
888
+ print("\tLifecycle Status: ", dev.lifecycleStatus)
889
+ print("\tOperation Status: ", dev.operationStatus)
890
+ print("\tLast Updated: ", dev.lastUpdate)
891
+ print()
892
+
893
+
894
+ class Vendor:
895
+ """
896
+ Map a manufacturer name to Trigger's canonical name.
897
+
898
+ Given a manufacturer name like 'CISCO SYSTEMS', this will attempt to map it
899
+ to the canonical vendor name specified in ``settings.VENDOR_MAP``. If this
900
+ can't be done, attempt to split the name up ('CISCO, 'SYSTEMS') and see if
901
+ any of the words map. An exception is raised as a last resort.
902
+
903
+ This exposes a normalized name that can be used in the event of a
904
+ multi-word canonical name.
905
+ """
906
+
907
+ def __init__(self, manufacturer=None):
908
+ """
909
+ :param manufacturer:
910
+ The literal or "internal" name for a vendor that is to be mapped to
911
+ its canonical name.
912
+ """
913
+ if manufacturer is None:
914
+ raise SyntaxError("You must specify a `manufacturer` name")
915
+
916
+ self.manufacturer = manufacturer
917
+ self.name = self.determine_vendor(manufacturer)
918
+ self.title = self.name.title()
919
+ self.prompt_pattern = self._get_prompt_pattern(self.name)
920
+
921
+ def determine_vendor(self, manufacturer):
922
+ """Try to turn the provided vendor name into the cname."""
923
+ vendor = settings.VENDOR_MAP.get(manufacturer)
924
+ if vendor is None:
925
+ mparts = [w for w in manufacturer.lower().split()]
926
+ for word in mparts:
927
+ if word in settings.SUPPORTED_VENDORS:
928
+ vendor = word
929
+ break
930
+ else:
931
+ # Safe fallback to first word
932
+ vendor = mparts[0]
933
+
934
+ return vendor
935
+
936
+ def _get_prompt_pattern(self, vendor, prompt_patterns=None):
937
+ """
938
+ Map the vendor name to the appropriate ``prompt_pattern`` defined in
939
+ :setting:`PROMPT_PATTERNS`.
940
+ """
941
+ if prompt_patterns is None:
942
+ prompt_patterns = settings.PROMPT_PATTERNS
943
+
944
+ # Try to get it by vendor
945
+ pat = prompt_patterns.get(vendor)
946
+ if pat is not None:
947
+ return pat
948
+
949
+ # Try to map it by IOS-like vendors...
950
+ if vendor in settings.IOSLIKE_VENDORS:
951
+ return settings.IOSLIKE_PROMPT_PAT
952
+
953
+ # Or fall back to the default
954
+ return settings.DEFAULT_PROMPT_PAT
955
+
956
+ @property
957
+ def normalized(self):
958
+ """Return the normalized name for the vendor."""
959
+ return self.name.replace(" ", "_").lower()
960
+
961
+ def __str__(self):
962
+ return self.name
963
+
964
+ def __repr__(self):
965
+ return f"<{self.__class__.__name__}: {self.title}>"
966
+
967
+ def __eq__(self, other):
968
+ return self.name.__eq__(Vendor(str(other)).name)
969
+
970
+ def __contains__(self, other):
971
+ return self.name.__contains__(Vendor(str(other)).name)
972
+
973
+ def __hash__(self):
974
+ return hash(self.name)
975
+
976
+ def lower(self):
977
+ return self.normalized
978
+
979
+
980
+ _vendor_registry = {}
981
+
982
+
983
+ def vendor_factory(vendor_name):
984
+ """
985
+ Given a full name of a vendor, retrieve or create the canonical
986
+ `~trigger.netdevices.Vendor` object.
987
+
988
+ Vendor instances are cached to improve startup speed.
989
+
990
+ :param vendor_name:
991
+ The vendor's full manufacturer name (e.g. 'CISCO SYSTEMS')
992
+ """
993
+ return _vendor_registry.setdefault(vendor_name, Vendor(vendor_name))
994
+
995
+
996
+ class NetDevices(MutableMapping):
997
+ """
998
+ Returns an immutable Singleton dictionary of
999
+ `~trigger.netdevices.NetDevice` objects.
1000
+
1001
+ By default it will only return devices for which
1002
+ ``adminStatus=='PRODUCTION'``.
1003
+
1004
+ There are hardly any use cases where ``NON-PRODUCTION`` devices are needed,
1005
+ and it can cause real bugs of two sorts:
1006
+
1007
+ 1. trying to contact unreachable devices and reporting spurious failures,
1008
+ 2. hot spares with the same ``nodeName``.
1009
+
1010
+ You may override this by passing ``production_only=False``.
1011
+ """
1012
+
1013
+ _Singleton = None
1014
+
1015
+ class _actual:
1016
+ """
1017
+ This is the real class that stays active upon instantiation. All
1018
+ attributes are inherited by NetDevices from this object. This means you
1019
+ do NOT reference ``_actual`` itself, and instead call the methods from
1020
+ the parent object.
1021
+
1022
+ Right::
1023
+
1024
+ >>> nd = NetDevices()
1025
+ >>> nd.search('fw')
1026
+ [<NetDevice: fw1-xyz.net.aol.com>]
1027
+
1028
+ Wrong::
1029
+
1030
+ >>> nd._actual.search('fw')
1031
+ Traceback (most recent call last):
1032
+ File "<stdin>", line 1, in <module>
1033
+ TypeError: unbound method match() must be called with _actual
1034
+ instance as first argument (got str instance instead)
1035
+ """
1036
+
1037
+ def __init__(self, production_only=True, with_acls=None):
1038
+ self.loader = None
1039
+ self.__dict = {}
1040
+
1041
+ _populate(
1042
+ netdevices=self,
1043
+ data_source=settings.NETDEVICES_SOURCE,
1044
+ production_only=production_only,
1045
+ with_acls=with_acls,
1046
+ )
1047
+
1048
+ def set_loader(self, loader):
1049
+ """
1050
+ Set the NetDevices loader and initialize internal dictionary.
1051
+
1052
+ :param loader:
1053
+ A `~trigger.netdevices.loader.BaseLoader` plugin instance
1054
+ """
1055
+ self.loader = loader
1056
+
1057
+ if hasattr(loader, "_dict"):
1058
+ log.msg("Installing NetDevices._dict from loader plugin!")
1059
+ else:
1060
+ log.msg("Installing NetDevice._dict internally!")
1061
+
1062
+ @property
1063
+ def _dict(self):
1064
+ """
1065
+ If the loader has an inner _dict, store objects on that instead.
1066
+ """
1067
+ if hasattr(self.loader, "_dict"):
1068
+ return self.loader._dict
1069
+ else:
1070
+ return self.__dict
1071
+
1072
+ def add_device(self, device):
1073
+ """
1074
+ Add a device object to the store.
1075
+
1076
+ :param device:
1077
+ `~trigger.netdevices.NetDevice` object
1078
+ """
1079
+ self._dict[device.nodeName] = device
1080
+
1081
+ def __getitem__(self, key):
1082
+ return self._dict[key]
1083
+
1084
+ def __contains__(self, item):
1085
+ return item in self._dict
1086
+
1087
+ def keys(self):
1088
+ return self._dict.keys()
1089
+
1090
+ def values(self):
1091
+ return self._dict.values()
1092
+
1093
+ def find(self, key):
1094
+ """
1095
+ Return either the exact nodename, or a unique dot-delimited
1096
+ prefix. For example, if there is a node 'test1-abc.net.aol.com',
1097
+ then any of find('test1-abc') or find('test1-abc.net') or
1098
+ find('test1-abc.net.aol.com') will match, but not find('test1').
1099
+
1100
+ This method can be overloaded in NetDevices loader plugins to
1101
+ customize the behavior as dictated by the plugin.
1102
+
1103
+ :param string key: Hostname prefix to find.
1104
+ :returns: NetDevice object
1105
+ """
1106
+ key = key.lower()
1107
+
1108
+ # Try to use the loader plugin first.
1109
+ if hasattr(self.loader, "find"):
1110
+ return self.loader.find(key)
1111
+
1112
+ # Or if there's a key, return that.
1113
+ elif key in self:
1114
+ return self[key]
1115
+
1116
+ matches = [x for x in self.keys() if x.startswith(key + ".")]
1117
+
1118
+ if matches:
1119
+ return self[matches[0]]
1120
+ raise KeyError(key)
1121
+
1122
+ def all(self):
1123
+ """
1124
+ Returns all NetDevice objects.
1125
+
1126
+ This method can be overloaded in NetDevices loader plugins to
1127
+ customize the behavior as dictated by the plugin.
1128
+ """
1129
+ if hasattr(self.loader, "all"):
1130
+ return self.loader.all()
1131
+ # Python 3: dict.values() returns a view, convert to list
1132
+ return list(self.values())
1133
+
1134
+ def search(self, token, field="nodeName"):
1135
+ """
1136
+ Returns a list of NetDevice objects where other is in
1137
+ ``dev.nodeName``. The getattr call in the search will allow a
1138
+ ``AttributeError`` from a bogus field lookup so that you
1139
+ don't get an empty list thinking you performed a legit query.
1140
+
1141
+ For example, this::
1142
+
1143
+ >>> field = 'bacon'
1144
+ >>> [x for x in nd.all() if 'ash' in getattr(x, field)]
1145
+ Traceback (most recent call last):
1146
+ File "<stdin>", line 1, in <module>
1147
+ AttributeError: 'NetDevice' object has no attribute 'bacon'
1148
+
1149
+ Is better than this::
1150
+
1151
+ >>> [x for x in nd.all() if 'ash' in getattr(x, field, '')]
1152
+ []
1153
+
1154
+ Because then you know that 'bacon' isn't a field you can search on.
1155
+
1156
+ :param string token: Token to search match on in @field
1157
+ :param string field: The field to match on when searching
1158
+ :returns: List of NetDevice objects
1159
+ """
1160
+ # We could actually just make this call match() to make this
1161
+ # case-insensitive as well. But we won't yet because of possible
1162
+ # implications in outside dependencies.
1163
+ # return self.match(**{field:token})
1164
+
1165
+ return [x for x in self.all() if token in getattr(x, field)]
1166
+
1167
+ def match(self, **kwargs):
1168
+ """
1169
+ Attempt to match values to all keys in @kwargs by dynamically
1170
+ building a list comprehension. Will throw errors if the keys don't
1171
+ match legit NetDevice attributes.
1172
+
1173
+ Keys and values are case IN-senstitive. Matches against non-string
1174
+ values will FAIL.
1175
+
1176
+ This method can be overloaded in NetDevices loader plugins to
1177
+ customize the behavior as dictated by the plugin. If
1178
+ ``skip_loader=True`` the built-in method will be used instead.
1179
+
1180
+ Example by reference::
1181
+
1182
+ >>> nd = NetDevices()
1183
+ >>> myargs = {'onCallName':'Data Center', 'model':'FCSLB'}
1184
+ >>> mydevices = nd(**myargs)
1185
+
1186
+ Example by keyword arguments::
1187
+
1188
+ >>> mydevices = nd(oncallname='data center', model='fcslb')
1189
+
1190
+ :returns: List of NetDevice objects
1191
+ """
1192
+ skip_loader = kwargs.pop("skip_loader", False)
1193
+ if skip_loader:
1194
+ log.msg("Skipping loader.match()")
1195
+
1196
+ if not skip_loader and hasattr(self.loader, "match"):
1197
+ log.msg("Calling loader.match()")
1198
+ return self.loader.match(**kwargs)
1199
+
1200
+ all_field_names = getattr(self, "_all_field_names", {})
1201
+ devices = self.all()
1202
+
1203
+ # Cache the field names the first time .match() is called.
1204
+ if not all_field_names:
1205
+ # Merge in field_names from every NetDevice
1206
+ for dev in devices:
1207
+ dev_fields = ((f.lower(), f) for f in dev.__dict__)
1208
+ all_field_names.update(dev_fields)
1209
+ self._all_field_names = all_field_names
1210
+
1211
+ def map_attr(attr):
1212
+ """Helper function for lower-to-regular attribute mapping."""
1213
+ return self._all_field_names[attr.lower()]
1214
+
1215
+ # Use list comp. to keep filtering out the devices.
1216
+ for attr, val in kwargs.items():
1217
+ attr = map_attr(attr)
1218
+ val = str(val).lower()
1219
+ devices = [
1220
+ d for d in devices if (val in str(getattr(d, attr, "")).lower())
1221
+ ]
1222
+
1223
+ return devices
1224
+
1225
+ def get_devices_by_type(self, devtype):
1226
+ """
1227
+ Returns a list of NetDevice objects with deviceType matching type.
1228
+
1229
+ Known deviceTypes: ['FIREWALL', 'ROUTER', 'SWITCH']
1230
+ """
1231
+ return [x for x in self.values() if x.deviceType == devtype]
1232
+
1233
+ def list_switches(self):
1234
+ """Returns a list of NetDevice objects with deviceType of SWITCH"""
1235
+ return self.get_devices_by_type("SWITCH")
1236
+
1237
+ def list_routers(self):
1238
+ """Returns a list of NetDevice objects with deviceType of ROUTER"""
1239
+ return self.get_devices_by_type("ROUTER")
1240
+
1241
+ def list_firewalls(self):
1242
+ """Returns a list of NetDevice objects with deviceType of FIREWALL"""
1243
+ return self.get_devices_by_type("FIREWALL")
1244
+
1245
+ def __init__(self, production_only=True, with_acls=None):
1246
+ """
1247
+ :param production_only:
1248
+ Whether to require devices to have ``adminStatus=='PRODUCTION'``.
1249
+
1250
+ :param with_acls:
1251
+ Whether to load ACL associations (requires Redis). Defaults to whatever
1252
+ is specified in settings.WITH_ACLS
1253
+ """
1254
+ if with_acls is None:
1255
+ with_acls = settings.WITH_ACLS
1256
+ classobj = self.__class__
1257
+ if classobj._Singleton is None:
1258
+ classobj._Singleton = classobj._actual(
1259
+ production_only=production_only, with_acls=with_acls
1260
+ )
1261
+
1262
+ def __getattr__(self, attr):
1263
+ return getattr(self.__class__._Singleton, attr)
1264
+
1265
+ def __setattr__(self, attr, value):
1266
+ return setattr(self.__class__._Singleton, attr, value)
1267
+
1268
+ # MutableMapping abstract methods - delegate to singleton
1269
+ def __getitem__(self, key):
1270
+ return self.__class__._Singleton[key]
1271
+
1272
+ def __setitem__(self, key, value):
1273
+ self.__class__._Singleton._dict[key] = value
1274
+
1275
+ def __delitem__(self, key):
1276
+ del self.__class__._Singleton._dict[key]
1277
+
1278
+ def __iter__(self):
1279
+ return iter(self.__class__._Singleton._dict)
1280
+
1281
+ def __len__(self):
1282
+ return len(self.__class__._Singleton._dict)
1283
+
1284
+ def reload(self, **kwargs):
1285
+ """Reload NetDevices metadata."""
1286
+ log.msg("Reloading NetDevices.")
1287
+ classobj = self.__class__
1288
+ classobj._Singleton = classobj._actual(**kwargs)