diff --git a/napalm_base/__init__.py b/napalm_base/__init__.py index bce3d114..57b05b89 100644 --- a/napalm_base/__init__.py +++ b/napalm_base/__init__.py @@ -37,6 +37,7 @@ from napalm_base.exceptions import ModuleImportError from napalm_base.mock import MockDriver from napalm_base.utils import py23_compat +from napalm_base import recorder try: __version__ = pkg_resources.get_distribution('napalm-base').version @@ -46,7 +47,8 @@ __all__ = [ 'get_network_driver', # export the function - 'NetworkDriver' # also export the base class + 'NetworkDriver', # also export the base class + 'recorder', ] diff --git a/napalm_base/recorder.py b/napalm_base/recorder.py new file mode 100644 index 00000000..928b45e8 --- /dev/null +++ b/napalm_base/recorder.py @@ -0,0 +1,123 @@ +from functools import wraps + +from collections import defaultdict + +from datetime import datetime + +from napalm_base.utils import py23_compat + +import pip +import logging +import os + +from camel import Camel, CamelRegistry + +logger = logging.getLogger("napalm-base") + +camel_registry = CamelRegistry() +camel = Camel([camel_registry]) + + +try: + from pyeapi.eapilib import CommandError as pyeapiCommandError + + @camel_registry.dumper(pyeapiCommandError, 'pyeapiCommandError', version=1) + def _dump_pyeapiCommandError(e): + return { + "code": e.error_code, + "message": e.error_text, + "kwargs": { + "command_error": e.command_error, + "commands": e.commands, + "output": e.output, + }, + } + + @camel_registry.loader('pyeapiCommandError', version=1) + def _load_pyeapiCommandError(data, version): + return pyeapiCommandError(data["code"], data["message"], **data["kwargs"]) +except Exception: + # If we can't import pyeapi there is no point on adding serializer/deserializer + pass + + +# This is written as a decorator so it can be used independently +def recorder(cls): + def real_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if cls.mode == "pass": + return func(*args, **kwargs) + + cls.current_count = cls.calls[func.__name__] + cls.calls[func.__name__] += 1 + + if cls.mode == "record": + return record(cls, cls.record_exceptions, func, *args, **kwargs) + elif cls.mode == "replay": + return replay(cls, func, *args, **kwargs) + + return wrapper + return real_decorator + + +def record(cls, exception_valid, func, *args, **kwargs): + logger.debug("Recording {}".format(func.__name__)) + + try: + r = func(*args, **kwargs) + raised_exception = False + except Exception as e: + if not exception_valid: + raise e + raised_exception = True + r = e + + filename = "{}.{}.yaml".format(func.__name__, cls.current_count) + with open(os.path.join(cls.path, filename), 'w') as f: + f.write(camel.dump(r)) + + if raised_exception: + raise r + else: + return r + + +def replay(cls, func, *args, **kwargs): + logger.debug("Replaying {}".format(func.__name__)) + filename = "{}.{}.yaml".format(func.__name__, cls.current_count) + with open(os.path.join(cls.path, filename), 'r') as f: + r = camel.load(py23_compat.text_type(f.read())) + + if isinstance(r, Exception): + raise r + return r + + +class Recorder(object): + + def __init__(self, cls, recorder_options, *args, **kwargs): + self.cls = cls + + self.mode = recorder_options.get("mode", "pass") + self.path = recorder_options.get("path", "") + self.record_exceptions = recorder_options.get("record_exceptions", True) + + self.device = cls(*args, **kwargs) + self.calls = defaultdict(lambda: 1) + + if self.mode == "record": + self.stamp_metadata() + + def stamp_metadata(self): + dt = datetime.now() + + installed_packages = pip.get_installed_distributions() + napalm_packages = sorted(["{}=={}".format(i.key, i.version) + for i in installed_packages if i.key.startswith("napalm")]) + + with open("{}/metadata.yaml".format(self.path), "w") as f: + f.write(camel.dump({"date": dt, "napalm_version": napalm_packages})) + + def __getattr__(self, attr): + return recorder(self)(self.device.__getattribute__(attr)) diff --git a/requirements.txt b/requirements.txt index 5760f0d9..307dbdf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ jtextfsm jinja2 netaddr pyYAML +camel diff --git a/test.py b/test.py new file mode 100644 index 00000000..e1215017 --- /dev/null +++ b/test.py @@ -0,0 +1,92 @@ +from napalm_base import get_network_driver + +import pprint + +import logging +import sys + +logger = logging.getLogger("napalm-base") + + +def config_logging(level=logging.DEBUG, stream=sys.stdout): + logger.setLevel(level) + ch = logging.StreamHandler(stream) + formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') + ch.setFormatter(formatter) + logger.addHandler(ch) + + +config_logging() + + +########################################################################## +# By default the recorder is set in mode "pass" which doesn't do a thing +########################################################################## +eos_configuration = { + 'hostname': '127.0.0.1', + 'username': 'vagrant', + 'password': 'vagrant', + 'optional_args': {'port': 12443} +} + +# eos = get_network_driver("eos") +# d = eos(**eos_configuration) + +# d.open() +# pprint.pprint(d.get_facts()) +# pprint.pprint(d.get_interfaces()) + + +########################################################################## +# In recording mode it will capture all the interactions between the drivers +# and the underlying library and store them into a file. +########################################################################## + +eos_configuration = { + 'hostname': '127.0.0.1', + 'username': 'vagrant', + 'password': 'vagrant', + 'optional_args': {'port': 12443, + 'recorder_mode': "record", + 'recorder_path': "./test_recorder"} +} + +eos = get_network_driver("eos") +d = eos(**eos_configuration) + +d.open() +pprint.pprint(d.get_facts()) +pprint.pprint(d.get_interfaces()) +pprint.pprint(d.cli(["show version"])) + +# pprint.pprint(d.cli(["wrong command"])) +try: + pprint.pprint(d.cli(["wrong command"])) +except Exception as e: + print("Recording exception") + print(e) + + +########################################################################## +# In replaying mode it will capture all the interactions between the drivers +# and the underlying library and instead of caling it it will return +# the results of a previous run +########################################################################## + +eos_configuration = { + 'hostname': '127.0.0.1', + 'username': 'fake', + 'password': 'wrong', + 'optional_args': {'port': 123, + 'recorder_mode': "replay", + 'recorder_path': "./test_recorder"} +} + +eos = get_network_driver("eos") +d = eos(**eos_configuration) + +d.open() +pprint.pprint(d.get_facts()) +pprint.pprint(d.get_interfaces()) +pprint.pprint(d.cli(["show version"])) +pprint.pprint(d.cli(["wrong command"])) diff --git a/test_recorder/metadata.yaml b/test_recorder/metadata.yaml new file mode 100644 index 00000000..d6bb1078 --- /dev/null +++ b/test_recorder/metadata.yaml @@ -0,0 +1,31 @@ +? !!binary | + ZGF0ZQ== +: 2017-08-17 12:57:21.126842 +? !!binary | + bmFwYWxtX3ZlcnNpb24= +: - !!binary | + bmFwYWxtLWFuc2libGU9PTAuNy4w + - !!binary | + bmFwYWxtLWJhc2U9PTAuMjQuMw== + - !!binary | + bmFwYWxtLWVvcz09MC42LjA= + - !!binary | + bmFwYWxtLWZvcnRpb3M9PTAuNC4w + - !!binary | + bmFwYWxtLWlvcz09MC43LjA= + - !!binary | + bmFwYWxtLWlvc3hyPT0wLjUuNA== + - !!binary | + bmFwYWxtLWp1bm9zPT0wLjEyLjA= + - !!binary | + bmFwYWxtLW54b3M9PTAuNi4w + - !!binary | + bmFwYWxtLXBhbm9zPT0wLjQuMA== + - !!binary | + bmFwYWxtLXBsdXJpYnVzPT0wLjUuMQ== + - !!binary | + bmFwYWxtLXJvcz09MC4yLjI= + - !!binary | + bmFwYWxtLXZ5b3M9PTAuMS4z + - !!binary | + bmFwYWxtPT0xLjIuMA== diff --git a/test_recorder/run_commands.1.yaml b/test_recorder/run_commands.1.yaml new file mode 100644 index 00000000..fdd8738d --- /dev/null +++ b/test_recorder/run_commands.1.yaml @@ -0,0 +1,7 @@ +- output: 'Thu Aug 17 10:57:20 2017 + + Timezone: UTC + + Clock source: local + + ' diff --git a/test_recorder/run_commands.2.yaml b/test_recorder/run_commands.2.yaml new file mode 100644 index 00000000..07488248 --- /dev/null +++ b/test_recorder/run_commands.2.yaml @@ -0,0 +1,171 @@ +- architecture: i386 + bootupTimestamp: 1502905826.22 + hardwareRevision: '' + internalBuildId: 8404cfa4-04c4-4008-838b-faf3f77ef6b8 + internalVersion: 4.15.2.1F-2759627.41521F + memFree: 118060 + memTotal: 1897596 + modelName: vEOS + serialNumber: '' + systemMacAddress: 08:00:27:16:f6:e6 + version: 4.15.2.1F +- fqdn: localhost + hostname: localhost +- interfaces: + Ethernet1: + autoNegotiate: unknown + bandwidth: 0 + burnedInAddress: 08:00:27:31:5b:5e + description: '' + duplex: duplexFull + forwardingModel: bridged + hardware: ethernet + interfaceAddress: [] + interfaceCounters: + counterRefreshTime: 1502967440.667403 + inBroadcastPkts: 0 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 0 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + linkStatusChanges: 1 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 32270 + outOctets: 4080170 + outUcastPkts: 0 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 0.0 + inPktsRate: 0.0 + outBitsRate: 0.0 + outPktsRate: 0.0 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905876.9322135 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 9214 + name: Ethernet1 + physicalAddress: 08:00:27:31:5b:5e + Ethernet2: + autoNegotiate: unknown + bandwidth: 0 + burnedInAddress: 08:00:27:35:cc:32 + description: '' + duplex: duplexFull + forwardingModel: bridged + hardware: ethernet + interfaceAddress: [] + interfaceCounters: + counterRefreshTime: 1502967440.664387 + inBroadcastPkts: 0 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 0 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + lastClear: 1502905826.2135878 + linkStatusChanges: 1 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 32270 + outOctets: 4080170 + outUcastPkts: 0 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 0.0 + inPktsRate: 0.0 + outBitsRate: 0.0 + outPktsRate: 0.0 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905876.9323654 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 9214 + name: Ethernet2 + physicalAddress: 08:00:27:35:cc:32 + Management1: + autoNegotiate: success + bandwidth: 1000000000 + burnedInAddress: 08:00:27:7d:44:c1 + description: '' + duplex: duplexFull + forwardingModel: routed + hardware: ethernet + interfaceAddress: + - broadcastAddress: 255.255.255.255 + primaryIp: + address: 10.0.2.15 + maskLen: 24 + secondaryIps: {} + secondaryIpsOrderedList: [] + virtualIp: + address: 0.0.0.0 + maskLen: 0 + interfaceCounters: + counterRefreshTime: 1502967440.660936 + inBroadcastPkts: 4 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 644221 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + linkStatusChanges: 5 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 0 + outOctets: 1485127 + outUcastPkts: 4893 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 226.85441155883467 + inPktsRate: 1.011684899724945e-91 + outBitsRate: 450.912565364304 + outPktsRate: 0.13201541432747763 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905890.7845936 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 1500 + name: Management1 + physicalAddress: 08:00:27:7d:44:c1 diff --git a/test_recorder/run_commands.3.yaml b/test_recorder/run_commands.3.yaml new file mode 100644 index 00000000..056977e7 --- /dev/null +++ b/test_recorder/run_commands.3.yaml @@ -0,0 +1,158 @@ +- interfaces: + Ethernet1: + autoNegotiate: unknown + bandwidth: 0 + burnedInAddress: 08:00:27:31:5b:5e + description: '' + duplex: duplexFull + forwardingModel: bridged + hardware: ethernet + interfaceAddress: [] + interfaceCounters: + counterRefreshTime: 1502967440.707486 + inBroadcastPkts: 0 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 0 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + linkStatusChanges: 1 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 32270 + outOctets: 4080170 + outUcastPkts: 0 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 0.0 + inPktsRate: 0.0 + outBitsRate: 0.0 + outPktsRate: 0.0 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905876.9322298 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 9214 + name: Ethernet1 + physicalAddress: 08:00:27:31:5b:5e + Ethernet2: + autoNegotiate: unknown + bandwidth: 0 + burnedInAddress: 08:00:27:35:cc:32 + description: '' + duplex: duplexFull + forwardingModel: bridged + hardware: ethernet + interfaceAddress: [] + interfaceCounters: + counterRefreshTime: 1502967440.704994 + inBroadcastPkts: 0 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 0 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + lastClear: 1502905826.213603 + linkStatusChanges: 1 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 32270 + outOctets: 4080170 + outUcastPkts: 0 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 0.0 + inPktsRate: 0.0 + outBitsRate: 0.0 + outPktsRate: 0.0 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905876.9323828 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 9214 + name: Ethernet2 + physicalAddress: 08:00:27:35:cc:32 + Management1: + autoNegotiate: success + bandwidth: 1000000000 + burnedInAddress: 08:00:27:7d:44:c1 + description: '' + duplex: duplexFull + forwardingModel: routed + hardware: ethernet + interfaceAddress: + - broadcastAddress: 255.255.255.255 + primaryIp: + address: 10.0.2.15 + maskLen: 24 + secondaryIps: {} + secondaryIpsOrderedList: [] + virtualIp: + address: 0.0.0.0 + maskLen: 0 + interfaceCounters: + counterRefreshTime: 1502967440.702346 + inBroadcastPkts: 4 + inDiscards: 0 + inMulticastPkts: 0 + inOctets: 644221 + inUcastPkts: 0 + inputErrorsDetail: + alignmentErrors: 0 + fcsErrors: 0 + giantFrames: 0 + runtFrames: 0 + rxPause: 0 + symbolErrors: 0 + linkStatusChanges: 5 + outBroadcastPkts: 0 + outDiscards: 0 + outMulticastPkts: 0 + outOctets: 1485127 + outUcastPkts: 4893 + outputErrorsDetail: + collisions: 0 + deferredTransmissions: 0 + lateCollisions: 0 + txPause: 0 + totalInErrors: 0 + totalOutErrors: 0 + interfaceStatistics: + inBitsRate: 226.85441155883467 + inPktsRate: 1.011684899724945e-91 + outBitsRate: 450.912565364304 + outPktsRate: 0.13201541432747763 + updateInterval: 300.0 + interfaceStatus: connected + lastStatusChangeTimestamp: 1502905890.7846065 + lineProtocolStatus: up + loopbackMode: loopbackNone + mtu: 1500 + name: Management1 + physicalAddress: 08:00:27:7d:44:c1 diff --git a/test_recorder/run_commands.4.yaml b/test_recorder/run_commands.4.yaml new file mode 100644 index 00000000..bc7745ca --- /dev/null +++ b/test_recorder/run_commands.4.yaml @@ -0,0 +1,6 @@ +- output: "Arista vEOS\nHardware version: \nSerial number: \nSystem MAC address:\ + \ 0800.2716.f6e6\n\nSoftware image version: 4.15.2.1F\nArchitecture: \ + \ i386\nInternal build version: 4.15.2.1F-2759627.41521F\nInternal build ID:\ + \ 8404cfa4-04c4-4008-838b-faf3f77ef6b8\n\nUptime: 17 hours\ + \ and 6 minutes\nTotal memory: 1897596 kB\nFree memory: 118060\ + \ kB\n\n" diff --git a/test_recorder/run_commands.5.yaml b/test_recorder/run_commands.5.yaml new file mode 100644 index 00000000..4e803a98 --- /dev/null +++ b/test_recorder/run_commands.5.yaml @@ -0,0 +1,26 @@ +!pyeapiCommandError;1 +? !!binary | + Y29kZQ== +: 1002 +? !!binary | + a3dhcmdz +: ? !!binary | + Y29tbWFuZF9lcnJvcg== + : 'Invalid input (at token 0: ''wrong'')' + ? !!binary | + Y29tbWFuZHM= + : - !!binary | + ZW5hYmxl + - !!binary | + d3JvbmcgY29tbWFuZA== + ? !!binary | + b3V0cHV0 + : - output: '' + - errors: + - 'Invalid input (at token 0: ''wrong'')' + output: '% Invalid input (at token 0: ''wrong'') + + ' +? !!binary | + bWVzc2FnZQ== +: 'CLI command 2 of 2 ''wrong command'' failed: invalid command'