Skip to content
This repository has been archived by the owner on Sep 17, 2019. It is now read-only.

Added recorder and an example (to be removed) #296

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion napalm_base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,7 +47,8 @@

__all__ = [
'get_network_driver', # export the function
'NetworkDriver' # also export the base class
'NetworkDriver', # also export the base class
'recorder',
]


Expand Down
123 changes: 123 additions & 0 deletions napalm_base/recorder.py
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ jtextfsm
jinja2
netaddr
pyYAML
camel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are anyway adding another dependency for serialization, why not use something widely adopted like msgpack?

Whatever would be the extra package, I dislike very much the idea of having a dependency for a feature I know for sure I'll never use.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"MessagePack is an efficient binary serialization format. It's like JSON. but fast and small."

So a few reasons:

  1. We don't want binary, we want something secure that humans can look at.
  2. Camel is is a tiny library that builds on top of pyyaml, which we already have.
  3. msgpack is huge and include even more C extensions.

92 changes: 92 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -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"]))
31 changes: 31 additions & 0 deletions test_recorder/metadata.yaml
Original file line number Diff line number Diff line change
@@ -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==
7 changes: 7 additions & 0 deletions test_recorder/run_commands.1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- output: 'Thu Aug 17 10:57:20 2017

Timezone: UTC

Clock source: local

'
Loading