diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..56a31925 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,7 @@ +### The NAPALM Project has reunified! + +Please submit all NAPALM issues to: +https://github.com/napalm-automation/napalm/issues + + +### DO NOT submit any issues to this repository. diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..d5848f97 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,7 @@ +### The NAPALM Project has reunified! + +Please submit all NAPALM pull requests to: +https://github.com/napalm-automation/napalm/pulls + + +### DO NOT submit any pull requests to this repository. diff --git a/.travis.yml b/.travis.yml index 964e98ad..80d9f2df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,11 @@ python: - 2.7 - 3.4 - 3.5 + - 3.6 install: - - pip install . - - pip install -r requirements-dev.txt - - pip install -r requirements.txt - - pip install -r test/unit/requirements.txt - + - pip install tox-travis + - pip install coveralls deploy: provider: pypi user: dbarroso @@ -21,8 +19,7 @@ deploy: branch: master script: - - nosetests ./test/unit/TestGetNetworkDriver.py - - nosetests ./test/unit/TestHelpers.py - - nosetests ./test/unit/TestNapalmTestFramework.py - - py.test test/unit/validate - - pylama . + - tox +after_success: + - coveralls + - if [ $TRAVIS_TAG ]; then curl -X POST https://readthedocs.org/build/napalm; fi diff --git a/README.md b/README.md index 11f0e325..7b26f503 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![PyPI](https://img.shields.io/pypi/v/napalm-base.svg)](https://pypi.python.org/pypi/napalm-base) -[![PyPI](https://img.shields.io/pypi/dm/napalm-base.svg)](https://pypi.python.org/pypi/napalm-base) [![Build Status](https://travis-ci.org/napalm-automation/napalm-base.svg?branch=master)](https://travis-ci.org/napalm-automation/napalm-base) diff --git a/napalm_base/__init__.py b/napalm_base/__init__.py index bce3d114..6a82fb41 100644 --- a/napalm_base/__init__.py +++ b/napalm_base/__init__.py @@ -20,8 +20,6 @@ # Python std lib import sys -import inspect -import importlib import pkg_resources # Verify Python Version that is running @@ -32,11 +30,16 @@ except AttributeError: raise RuntimeError('NAPALM requires Python 2.7 or Python3') -# NAPALM base -from napalm_base.base import NetworkDriver -from napalm_base.exceptions import ModuleImportError -from napalm_base.mock import MockDriver -from napalm_base.utils import py23_compat +# Try to import napalm +try: + import napalm + HAS_NAPALM = True + try: + NAPALM_MAJOR = int(napalm.__version__.split('.')[0]) + except AttributeError: + NAPALM_MAJOR = 0 +except ImportError: + HAS_NAPALM = False try: __version__ = pkg_resources.get_distribution('napalm-base').version @@ -44,71 +47,89 @@ __version__ = "Not installed" -__all__ = [ - 'get_network_driver', # export the function - 'NetworkDriver' # also export the base class -] - +if HAS_NAPALM and NAPALM_MAJOR >= 2: + # If napalm >= 2.0.0 is installed, then import get_network_driver + from napalm import get_network_driver + from napalm.base import NetworkDriver +else: + # Import std lib + import inspect + import importlib + + # Import local modules + from napalm_base.exceptions import ModuleImportError + from napalm_base.mock import MockDriver + from napalm_base.utils import py23_compat + from napalm_base.base import NetworkDriver + + def get_network_driver(module_name, prepend=True): + """ + Searches for a class derived form the base NAPALM class NetworkDriver in a specific library. + The library name must repect the following pattern: napalm_[DEVICE_OS]. + NAPALM community supports a list of devices and provides the corresponding libraries; for + full reference please refer to the `Supported Network Operation Systems`_ paragraph on + `Read the Docs`_. + + .. _`Supported Network Operation Systems`: \ + http://napalm.readthedocs.io/en/latest/#supported-network-operating-systems + .. _`Read the Docs`: \ + http://napalm.readthedocs.io/ + + module_name + The name of the device operating system or the name of the library. + + :return: The first class derived from NetworkDriver, found in the library. + + :raise ModuleImportError: When the library is not installed or a derived class from \ + NetworkDriver was not found. + + Example: + + .. code-block:: python + + >>> get_network_driver('junos') + + >>> get_network_driver('IOS-XR') + + >>> get_network_driver('napalm_eos') + + >>> get_network_driver('wrong') + napalm_base.exceptions.ModuleImportError: Cannot import "napalm_wrong". Is the library \ + installed? + """ + if module_name == "mock": + return MockDriver + + if not (isinstance(module_name, py23_compat.string_types) and len(module_name) > 0): + raise ModuleImportError('Please provide a valid driver name.') + + try: + # Only lowercase allowed + module_name = module_name.lower() + # Try to not raise error when users requests IOS-XR for e.g. + module_install_name = module_name.replace('-', '') + # Can also request using napalm_[SOMETHING] + if 'napalm_' not in module_install_name and prepend is True: + module_install_name = 'napalm_{name}'.format(name=module_install_name) + module = importlib.import_module(module_install_name) + except ImportError: + raise ModuleImportError( + 'Cannot import "{install_name}". Is the library installed?'.format( + install_name=module_install_name + ) + ) -def get_network_driver(module_name, prepend=True): - """ - Searches for a class derived form the base NAPALM class NetworkDriver in a specific library. - The library name must repect the following pattern: napalm_[DEVICE_OS]. - NAPALM community supports a list of devices and provides the corresponding libraries; for - full reference please refer to the `Supported Network Operation Systems`_ paragraph on - `Read the Docs`_. - - .. _`Supported Network Operation Systems`: \ - http://napalm.readthedocs.io/en/latest/#supported-network-operating-systems - .. _`Read the Docs`: \ - http://napalm.readthedocs.io/ - - :param module_name: the name of the device operating system or the name of the library. - :return: the first class derived from NetworkDriver, found in the library. - :raise ModuleImportError: when the library is not installed or a derived class from \ - NetworkDriver was not found. - - Example:: - - .. code-block:: python - - >>> get_network_driver('junos') - - >>> get_network_driver('IOS-XR') - - >>> get_network_driver('napalm_eos') - - >>> get_network_driver('wrong') - napalm_base.exceptions.ModuleImportError: Cannot import "napalm_wrong". Is the library \ - installed? - """ - if module_name == "mock": - return MockDriver - - if not (isinstance(module_name, py23_compat.string_types) and len(module_name) > 0): - raise ModuleImportError('Please provide a valid driver name.') + for name, obj in inspect.getmembers(module): + if inspect.isclass(obj) and issubclass(obj, NetworkDriver): + return obj - try: - # Only lowercase allowed - module_name = module_name.lower() - # Try to not raise error when users requests IOS-XR for e.g. - module_install_name = module_name.replace('-', '') - # Can also request using napalm_[SOMETHING] - if 'napalm_' not in module_install_name and prepend is True: - module_install_name = 'napalm_{name}'.format(name=module_install_name) - module = importlib.import_module(module_install_name) - except ImportError: + # looks like you don't have any Driver class in your module... raise ModuleImportError( - 'Cannot import "{install_name}". Is the library installed?'.format( - install_name=module_install_name - ) - ) + 'No class inheriting "napalm_base.base.NetworkDriver" found in "{install_name}".' + .format(install_name=module_install_name)) - for name, obj in inspect.getmembers(module): - if inspect.isclass(obj) and issubclass(obj, NetworkDriver): - return obj - # looks like you don't have any Driver class in your module... - raise ModuleImportError( - 'No class inheriting "napalm_base.base.NetworkDriver" found in "{install_name}".' - .format(install_name=module_install_name)) +__all__ = [ + 'get_network_driver', # export the function + 'NetworkDriver' # also export the base class +] diff --git a/napalm_base/base.py b/napalm_base/base.py index a3081b9f..17ac19d6 100644 --- a/napalm_base/base.py +++ b/napalm_base/base.py @@ -16,9 +16,6 @@ from __future__ import print_function from __future__ import unicode_literals -# std libs -import sys - # local modules import napalm_base.exceptions import napalm_base.helpers @@ -29,7 +26,6 @@ class NetworkDriver(object): - def __init__(self, hostname, username, password, timeout=60, optional_args=None): """ This is the base class you have to inherit from when writing your own Network Driver to @@ -47,17 +43,19 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) raise NotImplementedError def __enter__(self): - try: - self.open() - except Exception: # noqa - exc_info = sys.exc_info() - return self.__raise_clean_exception(exc_info[0], exc_info[1], exc_info[2]) + self.open() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.close() - if exc_type is not None: - return self.__raise_clean_exception(exc_type, exc_value, exc_traceback) + if exc_type is not None and ( + exc_type.__name__ not in dir(napalm_base.exceptions) and + exc_type.__name__ not in __builtins__.keys()): + epilog = ("NAPALM didn't catch this exception. Please, fill a bugfix on " + "https://github.com/napalm-automation/napalm/issues\n" + "Don't forget to include this traceback.") + print(epilog) + return False def __del__(self): """ @@ -71,27 +69,6 @@ def __del__(self): except Exception: pass - @staticmethod - def __raise_clean_exception(exc_type, exc_value, exc_traceback): - """ - This method is going to check if the exception exc_type is part of the builtins exceptions - or part of the napalm exceptions. If it is not, it will print a message on the screen - giving instructions to fill a bug. - - Finally it will raise the original exception. - - :param exc_type: Exception class. - :param exc_value: Exception object. - :param exc_traceback: Traceback. - """ - if (exc_type.__name__ not in dir(napalm_base.exceptions) and - exc_type.__name__ not in __builtins__.keys()): - epilog = ("NAPALM didn't catch this exception. Please, fill a bugfix on " - "https://github.com/napalm-automation/napalm/issues\n" - "Don't forget to include this traceback.") - print(epilog) - return False - def open(self): """ Opens a connection to the device. @@ -114,6 +91,30 @@ def is_alive(self): """ raise NotImplementedError + def pre_connection_tests(self): + """ + This is a helper function used by the cli tool cl_napalm_show_tech. Drivers + can override this method to do some tests, show information, enable debugging, etc. + before a connection with the device is attempted. + """ + raise NotImplementedError + + def connection_tests(self): + """ + This is a helper function used by the cli tool cl_napalm_show_tech. Drivers + can override this method to do some tests, show information, enable debugging, etc. + before a connection with the device has been successful. + """ + raise NotImplementedError + + def post_connection_tests(self): + """ + This is a helper function used by the cli tool cl_napalm_show_tech. Drivers + can override this method to do some tests, show information, enable debugging, etc. + after a connection with the device has been closed successfully. + """ + raise NotImplementedError + def load_template(self, template_name, template_source=None, template_path=None, **template_vars): """ @@ -1537,11 +1538,48 @@ def get_firewall_policies(self): """ raise NotImplementedError - def compliance_report(self, validation_file='validate.yml'): + def get_ipv6_neighbors_table(self): + """ + Get IPv6 neighbors table information. + + Return a list of dictionaries having the following set of keys: + * interface (string) + * mac (string) + * ip (string) + * age (float) in seconds + * state (string) + + For example:: + [ + { + 'interface' : 'MgmtEth0/RSP0/CPU0/0', + 'mac' : '5c:5e:ab:da:3c:f0', + 'ip' : '2001:db8:1:1::1', + 'age' : 1454496274.84, + 'state' : 'REACH' + }, + { + 'interface': 'MgmtEth0/RSP0/CPU0/0', + 'mac' : '66:0e:94:96:e0:ff', + 'ip' : '2001:db8:1:1::2', + 'age' : 1435641582.49, + 'state' : 'STALE' + } + ] + """ + raise NotImplementedError + + def compliance_report(self, validation_file=None, validation_source=None): """ Return a compliance report. Verify that the device complies with the given validation file and writes a compliance - report file. See https://napalm.readthedocs.io/en/latest/validate.html. + report file. See https://napalm.readthedocs.io/en/latest/validate/index.html. + + :param validation_file: Path to the file containing compliance definition. Default is None. + :param validation_source: Dictionary containing compliance rules. + :raise ValidationException: File is not valid. + :raise NotImplementedError: Method not implemented. """ - return validate.compliance_report(self, validation_file=validation_file) + return validate.compliance_report(self, validation_file=validation_file, + validation_source=validation_source) diff --git a/napalm_base/clitools/cl_napalm.py b/napalm_base/clitools/cl_napalm.py new file mode 100755 index 00000000..9ca4682e --- /dev/null +++ b/napalm_base/clitools/cl_napalm.py @@ -0,0 +1,289 @@ +# Python3 support +from __future__ import print_function +from __future__ import unicode_literals + +# import helpers +from napalm_base import get_network_driver +from napalm_base.clitools import helpers + +# stdlib +import pip +import json +import logging +import argparse +import getpass +from functools import wraps + + +def debugging(name): + def real_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + censor_parameters = ["password"] + censored_kwargs = {k: v if k not in censor_parameters else "*******" + for k, v in kwargs.items()} + logger.debug("{} - Calling with args: {}, {}".format(name, args, censored_kwargs)) + try: + r = func(*args, **kwargs) + logger.debug("{} - Successful".format(name)) + return r + except NotImplementedError: + if name not in ["pre_connection_tests", "connection_tests", + "post_connection_tests"]: + logger.debug("{} - Not implemented".format(name)) + except Exception as e: + logger.error("{} - Failed: {}".format(name, e)) + print("\n================= Traceback =================\n") + raise + return wrapper + return real_decorator + + +logger = logging.getLogger('napalm') + + +def build_help(): + parser = argparse.ArgumentParser( + description='Command line tool to handle configuration on devices using NAPALM.' + 'The script will print the diff on the screen', + epilog='Automate all the things!!!' + ) + parser.add_argument( + dest='hostname', + action='store', + help='Host where you want to deploy the configuration.' + ) + parser.add_argument( + '--user', '-u', + dest='user', + action='store', + default=getpass.getuser(), + help='User for authenticating to the host. Default: user running the script.' + ) + parser.add_argument( + '--password', '-p', + dest='password', + action='store', + help='Password for authenticating to the host.' + 'If you do not provide a password in the CLI you will be prompted.', + ) + parser.add_argument( + '--vendor', '-v', + dest='vendor', + action='store', + required=True, + help='Host Operating System.' + ) + parser.add_argument( + '--optional_args', '-o', + dest='optional_args', + action='store', + help='String with comma separated key=value pairs passed via optional_args to the driver.', + ) + parser.add_argument( + '--debug', + dest='debug', + action='store_true', + help='Enables debug mode; more verbosity.' + ) + subparser = parser.add_subparsers(title='actions') + + config = subparser.add_parser('configure', help='Perform a configuration operation') + config.set_defaults(which='config') + config.add_argument( + dest='config_file', + action='store', + help='File containing the configuration you want to deploy.' + ) + config.add_argument( + '--strategy', '-s', + dest='strategy', + action='store', + choices=['replace', 'merge'], + default='replace', + help='Strategy to use to deploy configuration. Default: replace.' + ) + config.add_argument( + '--dry-run', '-d', + dest='dry_run', + action='store_true', + default=None, + help='Only returns diff, it does not deploy the configuration.', + ) + + call = subparser.add_parser('call', help='Call a napalm method') + call.set_defaults(which='call') + call.add_argument( + dest='method', + action='store', + help='Run this method' + ) + call.add_argument( + '--method-kwargs', '-k', + dest='method_kwargs', + action='store', + help='kwargs to pass to the method. For example: "destination=1.1.1.1,protocol=bgp"' + ) + + validate = subparser.add_parser('validate', help='Validate configuration/state') + validate.set_defaults(which='validate') + validate.add_argument( + dest='validation_file', + action='store', + help='Validation file containing resources derised states' + ) + args = parser.parse_args() + + if args.password is None: + password = getpass.getpass('Enter password: ') + setattr(args, 'password', password) + + return args + + +def check_installed_packages(): + logger.debug("Gathering napalm packages") + installed_packages = pip.get_installed_distributions() + napalm_packages = sorted(["{}=={}".format(i.key, i.version) + for i in installed_packages if i.key.startswith("napalm")]) + for n in napalm_packages: + logger.debug(n) + + +@debugging("get_network_driver") +def call_get_network_driver(vendor): + return get_network_driver(vendor) + + +@debugging("__init__") +def call_instantiating_object(driver, *args, **kwargs): + return driver(*args, **kwargs) + + +@debugging("pre_connection_tests") +def call_pre_connection(driver): + driver.pre_connection_tests() + + +@debugging("connection_tests") +def call_connection(device): + device.connection_tests() + + +@debugging("post_connection_tests") +def call_post_connection(device): + device.post_connection_tests() + + +@debugging("get_facts") +def call_facts(device): + facts = device.get_facts() + logger.debug("Gathered facts:\n{}".format(json.dumps(facts, indent=4))) + print(json.dumps(facts, indent=4)) + + +@debugging("close") +def call_close(device): + return device.close() + + +@debugging("open") +def call_open_device(device): + return device.open() + + +@debugging("load_replace_candidate") +def call_load_replace_candidate(device, *args, **kwargs): + return device.load_replace_candidate(*args, **kwargs) + + +@debugging("load_merge_candidate") +def call_load_merge_candidate(device, *args, **kwargs): + return device.load_merge_candidate(*args, **kwargs) + + +@debugging("compare_config") +def call_compare_config(device, *args, **kwargs): + diff = device.compare_config(*args, **kwargs) + logger.debug("Gathered diff:") + print(diff) + return diff + + +@debugging("commit_config") +def call_commit_config(device, *args, **kwargs): + return device.commit_config(*args, **kwargs) + + +def configuration_change(device, config_file, strategy, dry_run): + if strategy == 'replace': + strategy_method = call_load_replace_candidate + elif strategy == 'merge': + strategy_method = call_load_merge_candidate + + strategy_method(device, filename=config_file) + + diff = call_compare_config(device) + + if not dry_run: + call_commit_config(device) + return diff + + +@debugging("method") +def call_getter(device, method, **kwargs): + logger.debug("{} - Attempting to resolve method".format(method)) + func = getattr(device, method) + logger.debug("{} - Attempting to call method with kwargs: {}".format(method, kwargs)) + r = func(**kwargs) + logger.debug("{} - Response".format(method)) + print(json.dumps(r, indent=4)) + + +@debugging("compliance_report") +def call_compliance_report(device, validation_file): + result = device.compliance_report(validation_file) + print(json.dumps(result, indent=4)) + return result + + +def run_tests(args): + driver = call_get_network_driver(args.vendor) + optional_args = helpers.parse_optional_args(args.optional_args) + + device = call_instantiating_object(driver, args.hostname, args.user, password=args.password, + timeout=60, optional_args=optional_args) + + if args.debug: + call_pre_connection(device) + + call_open_device(device) + + if args.debug: + call_connection(device) + call_facts(device) + + if args.which == 'call': + method_kwargs = helpers.parse_optional_args(args.method_kwargs) + call_getter(device, args.method, **method_kwargs) + elif args.which == 'config': + configuration_change(device, args.config_file, args.strategy, args.dry_run) + elif args.which == 'validate': + call_compliance_report(device, args.validation_file) + + call_close(device) + + if args.debug: + call_post_connection(device) + + +def main(): + args = build_help() + helpers.configure_logging(logger, debug=args.debug) + logger.debug("Starting napalm's debugging tool") + check_installed_packages() + run_tests(args) + + +if __name__ == '__main__': + main() diff --git a/napalm_base/clitools/cl_napalm_configure.py b/napalm_base/clitools/cl_napalm_configure.py index fce6b326..3924fc43 100644 --- a/napalm_base/clitools/cl_napalm_configure.py +++ b/napalm_base/clitools/cl_napalm_configure.py @@ -15,10 +15,13 @@ from napalm_base.clitools.helpers import build_help from napalm_base.clitools.helpers import configure_logging from napalm_base.clitools.helpers import parse_optional_args +from napalm_base.clitools.helpers import warning import sys import logging + logger = logging.getLogger('cl-napalm-config.py') +warning() def run(vendor, hostname, user, password, strategy, optional_args, config_file, dry_run): diff --git a/napalm_base/clitools/cl_napalm_test.py b/napalm_base/clitools/cl_napalm_test.py index 5f934c19..7bbe6820 100644 --- a/napalm_base/clitools/cl_napalm_test.py +++ b/napalm_base/clitools/cl_napalm_test.py @@ -14,11 +14,14 @@ from napalm_base.clitools.helpers import build_help from napalm_base.clitools.helpers import configure_logging from napalm_base.clitools.helpers import parse_optional_args +from napalm_base.clitools.helpers import warning # stdlib import sys import logging + logger = logging.getLogger('cl_napalm_test.py') +warning() def main(): diff --git a/napalm_base/clitools/cl_napalm_validate.py b/napalm_base/clitools/cl_napalm_validate.py index 69b55a03..825d79d7 100755 --- a/napalm_base/clitools/cl_napalm_validate.py +++ b/napalm_base/clitools/cl_napalm_validate.py @@ -15,12 +15,15 @@ from napalm_base.clitools.helpers import build_help from napalm_base.clitools.helpers import configure_logging from napalm_base.clitools.helpers import parse_optional_args +from napalm_base.clitools.helpers import warning # stdlib import sys import json import logging + logger = logging.getLogger('cl_napalm_validate.py') +warning() def main(): diff --git a/napalm_base/clitools/helpers.py b/napalm_base/clitools/helpers.py index 329884be..d3020e57 100644 --- a/napalm_base/clitools/helpers.py +++ b/napalm_base/clitools/helpers.py @@ -10,13 +10,21 @@ from __future__ import unicode_literals # stdlib +import ast import sys import logging import getpass import argparse +import warnings -def build_help(connect_test=False, validate=False, configure=False): +def warning(): + warnings.simplefilter('always', DeprecationWarning) + warnings.warn("This tool has been deprecated, please use `napalm` instead\n", + DeprecationWarning) + + +def build_help(connect_test=False, validate=False, configure=False, napalm_cli=False): parser = argparse.ArgumentParser( description='Command line tool to handle configuration on devices using NAPALM.' 'The script will print the diff on the screen', @@ -60,6 +68,7 @@ def build_help(connect_test=False, validate=False, configure=False): action='store_true', help='Enables debug mode; more verbosity.' ) + if configure: parser.add_argument( '--strategy', '-s', @@ -111,7 +120,7 @@ def configure_logging(logger, debug): def parse_optional_args(optional_args): - if optional_args is not None: - return {x.split('=')[0]: x.split('=')[1] for x in optional_args.replace(' ', '').split(',')} + return {x.split('=')[0]: ast.literal_eval(x.split('=')[1]) + for x in optional_args.split(',')} return {} diff --git a/napalm_base/helpers.py b/napalm_base/helpers.py index a5122179..8df17ab4 100644 --- a/napalm_base/helpers.py +++ b/napalm_base/helpers.py @@ -37,29 +37,28 @@ class _MACFormat(mac_unix): def load_template(cls, template_name, template_source=None, template_path=None, openconfig=False, **template_vars): try: + search_path = [] if isinstance(template_source, py23_compat.string_types): template = jinja2.Template(template_source) else: - current_dir = os.path.dirname(os.path.abspath(sys.modules[cls.__module__].__file__)) - if (isinstance(template_path, py23_compat.string_types) and - os.path.isdir(template_path) and os.path.isabs(template_path)): - current_dir = os.path.join(template_path, cls.__module__.split('.')[-1]) - # append driver name at the end of the custom path + if template_path is not None: + if (isinstance(template_path, py23_compat.string_types) and + os.path.isdir(template_path) and os.path.isabs(template_path)): + # append driver name at the end of the custom path + search_path.append(os.path.join(template_path, cls.__module__.split('.')[-1])) + else: + raise IOError("Template path does not exist: {}".format(template_path)) + else: + # Search modules for template paths + search_path = [os.path.dirname(os.path.abspath(sys.modules[c.__module__].__file__)) + for c in cls.__class__.mro() if c is not object] if openconfig: - template_dir_path = '{current_dir}/oc_templates'.format(current_dir=current_dir) + search_path = ['{}/oc_templates'.format(s) for s in search_path] else: - template_dir_path = '{current_dir}/templates'.format(current_dir=current_dir) - - if not os.path.isdir(template_dir_path): - raise napalm_base.exceptions.DriverTemplateNotImplemented( - '''Config template dir does not exist: {path}. - Please create it and add driver-specific templates.'''.format( - path=template_dir_path - ) - ) + search_path = ['{}/templates'.format(s) for s in search_path] - loader = jinja2.FileSystemLoader(template_dir_path) + loader = jinja2.FileSystemLoader(search_path) environment = jinja2.Environment(loader=loader) for filter_name, filter_function in CustomJinjaFilters.filters().items(): @@ -71,9 +70,9 @@ def load_template(cls, template_name, template_source=None, template_path=None, configuration = template.render(**template_vars) except jinja2.exceptions.TemplateNotFound: raise napalm_base.exceptions.TemplateNotImplemented( - "Config template {template_name}.j2 is not defined under {path}".format( + "Config template {template_name}.j2 not found in search path: {sp}".format( template_name=template_name, - path=template_dir_path + sp=search_path ) ) except (jinja2.exceptions.UndefinedError, jinja2.exceptions.TemplateSyntaxError) as jinjaerr: diff --git a/napalm_base/mock.py b/napalm_base/mock.py index 18f08a42..74fe0d2f 100644 --- a/napalm_base/mock.py +++ b/napalm_base/mock.py @@ -88,7 +88,7 @@ def __init__(self, parent, profile): def run_commands(self, commands): """Only useful for EOS""" if "eos" in self.profile: - return self.parent.cli(commands).values()[0] + return list(self.parent.cli(commands).values())[0] else: raise AttributeError("MockedDriver instance has not attribute '_rpc'") @@ -106,6 +106,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) self.password = password self.path = optional_args["path"] self.profile = optional_args.get("profile", []) + self.fail_on_open = optional_args.get("fail_on_open", False) self.opened = False self.calls = {} @@ -126,6 +127,8 @@ def _raise_if_closed(self): raise napalm_base.exceptions.ConnectionClosedException("connection closed") def open(self): + if self.fail_on_open: + raise napalm_base.exceptions.ConnectionException("You told me to do this") self.opened = True def close(self): @@ -185,10 +188,7 @@ def discard_config(self): def _rpc(self, get): """This one is only useful for junos.""" - if "junos" in self.profile: - return self.cli([get]).values()[0] - else: - raise AttributeError("MockedDriver instance has not attribute '_rpc'") + return list(self.cli([get]).values())[0] def __getattribute__(self, name): if is_mocked_method(name): diff --git a/napalm_base/test/base.py b/napalm_base/test/base.py index 0fc17a67..697021c2 100644 --- a/napalm_base/test/base.py +++ b/napalm_base/test/base.py @@ -579,3 +579,15 @@ def test_get_network_instances(self): self._test_model(models.network_instance_interfaces, network_instance['interfaces']) self.assertTrue(result) + + def test_get_ipv6_neighbors_table(self): + try: + get_ipv6_neighbors_table = self.device.get_ipv6_neighbors_table() + except NotImplementedError: + raise SkipTest() + result = len(get_ipv6_neighbors_table) > 0 + + for entry in get_ipv6_neighbors_table: + result = result and self._test_model(models.ipv6_neighbor, entry) + + self.assertTrue(result) diff --git a/napalm_base/test/getters.py b/napalm_base/test/getters.py index db6ebdb7..43552390 100644 --- a/napalm_base/test/getters.py +++ b/napalm_base/test/getters.py @@ -288,6 +288,17 @@ def test_get_arp_table(self, test_case): return get_arp_table + @wrap_test_cases + def test_get_ipv6_neighbors_table(self, test_case): + """Test get_ipv6_neighbors_table.""" + get_ipv6_neighbors_table = self.device.get_ipv6_neighbors_table() + assert len(get_ipv6_neighbors_table) > 0 + + for entry in get_ipv6_neighbors_table: + assert helpers.test_model(models.ipv6_neighbor, entry) + + return get_ipv6_neighbors_table + @wrap_test_cases def test_get_ntp_peers(self, test_case): """Test get_ntp_peers.""" diff --git a/napalm_base/test/helpers.py b/napalm_base/test/helpers.py index e33ce6c5..ca5d6a73 100644 --- a/napalm_base/test/helpers.py +++ b/napalm_base/test/helpers.py @@ -16,11 +16,14 @@ def test_model(model, data): correct_class = True for key, instance_class in model.items(): - correct_class = isinstance(data[key], instance_class) and correct_class - # Properly handle PY2 long - if py23_compat.PY2: - if isinstance(data[key], long) and isinstance(1, instance_class): # noqa - correct_class = True and correct_class + if py23_compat.PY2 and isinstance(data[key], long): # noqa + # Properly handle PY2 long + correct_class = (isinstance(data[key], long) and # noqa + isinstance(1, instance_class) and + correct_class) + else: + correct_class = isinstance(data[key], instance_class) and correct_class + if not correct_class: print("key: {}\nmodel_class: {}\ndata_class: {}".format( key, instance_class, data[key].__class__)) diff --git a/napalm_base/test/models.py b/napalm_base/test/models.py index 37bd84ff..ce7b5a66 100644 --- a/napalm_base/test/models.py +++ b/napalm_base/test/models.py @@ -174,6 +174,14 @@ 'age': float } +ipv6_neighbor = { + 'interface': text_type, + 'mac': text_type, + 'ip': text_type, + 'age': float, + 'state': text_type +} + ntp_peer = { # will populate it in the future wit potential keys } diff --git a/napalm_base/validate.py b/napalm_base/validate.py index ccaf1914..7f46fe5b 100644 --- a/napalm_base/validate.py +++ b/napalm_base/validate.py @@ -92,6 +92,7 @@ def _compare_getter_dict(src, dst, mode): complies = intermediate_result nested = False if not complies: + result["present"][key]["expected_value"] = src_element result["present"][key]["actual_value"] = dst_element if not complies: @@ -123,22 +124,27 @@ def _compare_getter(src, dst): return _compare_getter_list(src['list'], dst, mode) return _compare_getter_dict(src, dst, mode) - else: - if isinstance(src, py23_compat.string_types): - if src.startswith('<') or src.startswith('>'): - cmp_result = compare_numeric(src, dst) - return cmp_result - else: - m = re.search(src, py23_compat.text_type(dst)) - return m is not None - elif(type(src) == type(dst) == list): - pairs = zip(src, dst) - diff_lists = [[(k, x[k], y[k]) - for k in x if not re.search(x[k], y[k])] - for x, y in pairs if x != y] - return empty_tree(diff_lists) + + elif isinstance(src, py23_compat.string_types): + if src.startswith('<') or src.startswith('>'): + cmp_result = compare_numeric(src, dst) + return cmp_result else: - return src == dst + m = re.search(src, py23_compat.text_type(dst)) + if m: + return bool(m) + else: + return src == dst + + elif(type(src) == type(dst) == list): + pairs = zip(src, dst) + diff_lists = [[(k, x[k], y[k]) + for k in x if not re.search(x[k], y[k])] + for x, y in pairs if x != y] + return empty_tree(diff_lists) + + else: + return src == dst def compare_numeric(src_num, dst_num): @@ -169,9 +175,10 @@ def empty_tree(input_list): return True -def compliance_report(cls, validation_file=None): +def compliance_report(cls, validation_file=None, validation_source=None): report = {} - validation_source = _get_validation_file(validation_file) + if validation_file: + validation_source = _get_validation_file(validation_file) for validation_check in validation_source: for getter, expected_results in validation_check.items(): diff --git a/requirements-dev.txt b/requirements-dev.txt index 565c2e07..5663f897 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,20 @@ +coveralls pytest pytest-cov pytest-json pytest-pythonpath pylama flake8-import-order -nose +ddt +lxml +napalm-eos +napalm-fortios +napalm-ios +napalm-iosxr +napalm-junos +napalm-nxos +napalm-panos +napalm-pluribus +napalm-ros +napalm-vyos -r requirements.txt diff --git a/setup.cfg b/setup.cfg index e240d19e..bf8e0ccf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,37 @@ [pylama] linters = mccabe,pep8,pyflakes ignore = D203,C901 +skip = .tox/* [pylama:pep8] max_line_length = 100 -[pytest] -addopts = --cov=./ -vs +[tool:pytest] +norecursedirs = + .git + .tox + .env + dist + build + south_migraitons + migrations + napalm_base +python_files = + test_*.py + *_test.py + tests.py +addopts = + --cov=napalm_base + --cov-report term-missing + -vs + --pylama json_report = report.json jsonapi = true [coverage:run] include = - napalm_base/* + napalm_base/* + +[coverage:report] +omit = + napalm_base/test/* \ No newline at end of file diff --git a/setup.py b/setup.py index c19636b7..7a12bc66 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="napalm-base", - version='0.24.3', + version='1.0.0', packages=find_packages(), author="David Barroso, Kirk Byers, Mircea Ulinic", author_email="dbarrosop@dravetech.com, ping@mirceaulinic.net, ktbyers@twb-tech.com", @@ -35,7 +35,8 @@ 'console_scripts': [ 'cl_napalm_configure=napalm_base.clitools.cl_napalm_configure:main', 'cl_napalm_test=napalm_base.clitools.cl_napalm_test:main', - 'cl_napalm_validate=napalm_base.clitools.cl_napalm_validate:main' + 'cl_napalm_validate=napalm_base.clitools.cl_napalm_validate:main', + 'napalm=napalm_base.clitools.cl_napalm:main', ], } ) diff --git a/test/custom/path/base/__a_very_nice_template__.j2 b/test/custom/path/base/test_helpers/templates/__a_very_nice_template__.j2 similarity index 100% rename from test/custom/path/base/__a_very_nice_template__.j2 rename to test/custom/path/base/test_helpers/templates/__a_very_nice_template__.j2 diff --git a/test/unit/requirements.txt b/test/unit/requirements.txt index 67d5d5fb..9b6b2778 100644 --- a/test/unit/requirements.txt +++ b/test/unit/requirements.txt @@ -9,4 +9,4 @@ napalm-nxos napalm-panos napalm-pluribus napalm-ros -napalm-vyos +napalm-vyos \ No newline at end of file diff --git a/test/unit/TestGetNetworkDriver.py b/test/unit/test_get_network_driver.py similarity index 100% rename from test/unit/TestGetNetworkDriver.py rename to test/unit/test_get_network_driver.py diff --git a/test/unit/TestHelpers.py b/test/unit/test_helpers.py similarity index 94% rename from test/unit/TestHelpers.py rename to test/unit/test_helpers.py index d17eeff6..0303ea0b 100644 --- a/test/unit/TestHelpers.py +++ b/test/unit/test_helpers.py @@ -59,12 +59,14 @@ def test_load_template(self): * check if can load empty template * check if raises TemplateRenderException when template is not correctly formatted * check if can load correct template - * check if can load correct template even if wrong custom path specified + * check if raises IOError if invalid path is specified * check if raises TemplateNotImplemented when trying to use inexisting template in custom path * check if can load correct template from custom path * check if template passed as string can be loaded + * check that the search path setup by MRO is correct when loading an incorrecet template """ + self.assertTrue(HAS_JINJA) # firstly check if jinja2 is installed _NTP_PEERS_LIST = [ '172.17.17.1', @@ -94,14 +96,16 @@ def test_load_template(self): '__a_very_nice_template__', **_TEMPLATE_VARS)) - self.assertTrue(napalm_base.helpers.load_template(self.network_driver, - '__a_very_nice_template__', - template_path='/this/path/does/not/exist', - **_TEMPLATE_VARS)) + self.assertRaises(IOError, + napalm_base.helpers.load_template, + self.network_driver, + '__a_very_nice_template__', + template_path='/this/path/does/not/exist', + **_TEMPLATE_VARS) install_dir = os.path.dirname( os.path.abspath(sys.modules[self.network_driver.__module__].__file__)) - custom_path = os.path.join(install_dir, 'test/custom/path/base') + custom_path = os.path.join(install_dir, '../custom/path/base') self.assertRaises(napalm_base.exceptions.TemplateNotImplemented, napalm_base.helpers.load_template, @@ -121,6 +125,13 @@ def test_load_template(self): '_this_still_needs_a_name', template_source=template_source, **_TEMPLATE_VARS)) + self.assertRaisesRegexp(napalm_base.exceptions.TemplateNotImplemented, + "path.*napalm-base/test/unit/templates'" + + ",.*napalm-base/napalm_base/templates']", + napalm_base.helpers.load_template, + self.network_driver, + '__this_template_does_not_exist__', + **_TEMPLATE_VARS) def test_textfsm_extractor(self): """ diff --git a/test/unit/TestMockDriver.py b/test/unit/test_mock_driver.py similarity index 75% rename from test/unit/TestMockDriver.py rename to test/unit/test_mock_driver.py index 9368984c..5475e8b9 100644 --- a/test/unit/TestMockDriver.py +++ b/test/unit/test_mock_driver.py @@ -10,6 +10,7 @@ # NAPALM base from napalm_base import get_network_driver import napalm_base.exceptions +from napalm_base.utils import py23_compat import pytest @@ -24,6 +25,11 @@ "path": os.path.join(BASE_PATH, "test_mock_driver"), "profile": ["eos"], } +fail_args = { + "path": os.path.join(BASE_PATH, "test_mock_driver"), + "profile": ["eos"], + "fail_on_open": True, +} class TestMockDriver(object): @@ -39,12 +45,19 @@ def test_basic(self): with pytest.raises(napalm_base.exceptions.ConnectionClosedException) as excinfo: d.get_facts() - assert "connection closed" in excinfo.value + assert "connection closed" in py23_compat.text_type(excinfo.value) def test_context_manager(self): - with driver("blah", "bleh", "blih", optional_args=optional_args) as d: + with pytest.raises(napalm_base.exceptions.ConnectionException) as e, \ + driver("blah", "bleh", "blih", optional_args=fail_args) as d: + pass + assert "You told me to do this" in py23_compat.text_type(e.value) + with pytest.raises(AttributeError) as e, \ + driver("blah", "bleh", "blih", optional_args=optional_args) as d: assert d.is_alive() == {u'is_alive': True} + d.__fake_call() assert d.is_alive() == {u'is_alive': False} + assert "object has no attribute" in py23_compat.text_type(e.value) def test_mocking_getters(self): d = driver("blah", "bleh", "blih", optional_args=optional_args) @@ -60,12 +73,12 @@ def test_not_mocking_getters(self): with pytest.raises(NotImplementedError) as excinfo: d.get_route_to() expected = "You can provide mocked data in {}/get_route_to.1".format(optional_args["path"]) - assert expected in excinfo.value + assert expected in py23_compat.text_type(excinfo.value) with pytest.raises(NotImplementedError) as excinfo: d.get_route_to() expected = "You can provide mocked data in {}/get_route_to.2".format(optional_args["path"]) - assert expected in excinfo.value + assert expected in py23_compat.text_type(excinfo.value) d.close() @@ -75,15 +88,18 @@ def test_arguments(self): with pytest.raises(TypeError) as excinfo: d.get_route_to(1, 2, 3) - assert "get_route_to: expected at most 3 arguments, got 4" in excinfo.value + assert "get_route_to: expected at most 3 arguments, got 4" in py23_compat.text_type( + excinfo.value) with pytest.raises(TypeError) as excinfo: d.get_route_to(1, 1, protocol=2) - assert "get_route_to: expected at most 3 arguments, got 3" in excinfo.value + assert "get_route_to: expected at most 3 arguments, got 3" in py23_compat.text_type( + excinfo.value) with pytest.raises(TypeError) as excinfo: d.get_route_to(proto=2) - assert "get_route_to got an unexpected keyword argument 'proto'" in excinfo.value + assert "get_route_to got an unexpected keyword argument 'proto'" in py23_compat.text_type( + excinfo.value) d.close() @@ -93,15 +109,16 @@ def test_mock_error(self): with pytest.raises(KeyError) as excinfo: d.get_bgp_neighbors() - assert "Something" in excinfo.value + assert "Something" in py23_compat.text_type(excinfo.value) with pytest.raises(napalm_base.exceptions.ConnectionClosedException) as excinfo: d.get_bgp_neighbors() - assert "Something" in excinfo.value + assert "Something" in py23_compat.text_type(excinfo.value) with pytest.raises(TypeError) as excinfo: d.get_bgp_neighbors() - assert "Couldn't resolve exception NoIdeaException" in excinfo.value + assert "Couldn't resolve exception NoIdeaException" in py23_compat.text_type( + excinfo.value) d.close() diff --git a/test/unit/TestNapalmTestFramework.py b/test/unit/test_napalm_test_framework.py similarity index 100% rename from test/unit/TestNapalmTestFramework.py rename to test/unit/test_napalm_test_framework.py diff --git a/test/unit/validate/mocked_data/non_strict_fail/report.yml b/test/unit/validate/mocked_data/non_strict_fail/report.yml index 5d381c71..179c4e85 100644 --- a/test/unit/validate/mocked_data/non_strict_fail/report.yml +++ b/test/unit/validate/mocked_data/non_strict_fail/report.yml @@ -42,7 +42,7 @@ get_bgp_neighbors: extra: [] missing: [] present: - sent_prefixes: {actual_value: 2, complies: false, + sent_prefixes: {actual_value: 2, expected_value: 6, complies: false, nested: false} nested: true nested: true @@ -50,7 +50,7 @@ get_bgp_neighbors: nested: true 192.0.2.3: {complies: true, nested: true} nested: true - router_id: {actual_value: 192.0.2.2, complies: false, nested: false} + router_id: {actual_value: 192.0.2.2, expected_value: 192.6.6.6, complies: false, nested: false} nested: true get_facts: complies: false @@ -110,6 +110,7 @@ get_environment: present: '%usage': actual_value: 100.0 + expected_value: <20.0 complies: False nested: False nested: True @@ -123,6 +124,7 @@ get_environment: present: available_ram: actual_value: 90.0 + expected_value: <20.0 complies: False nested: False - nested: True \ No newline at end of file + nested: True diff --git a/test/unit/validate/mocked_data/simple_fail/get_facts.json b/test/unit/validate/mocked_data/simple_fail/get_facts.json new file mode 100644 index 00000000..e55a07e1 --- /dev/null +++ b/test/unit/validate/mocked_data/simple_fail/get_facts.json @@ -0,0 +1,10 @@ +{ + "os_version": "7.0(3)I2(2d)", + "uptime": 16676160, + "interface_list": ["Vlan5", "Vlan100", "Vlan40", "Vlan41", "GigabitEthernet0/1", "GigabitEthernet0/2", "GigabitEthernet0/3", "GigabitEthernet0/4", "GigabitEthernet0/5", "GigabitEthernet0/6", "GigabitEthernet0/7", "GigabitEthernet0/8", "Port-channel1"], + "vendor": "Cisco", + "serial_number": "FOC1308V5NB", + "model": "WS-C2960G-8TC-L", + "hostname": "n9k2", + "fqdn": "NS2903-ASW-01.int.ogenstad.com" +} diff --git a/test/unit/validate/mocked_data/simple_fail/report.yml b/test/unit/validate/mocked_data/simple_fail/report.yml new file mode 100644 index 00000000..6e43ed77 --- /dev/null +++ b/test/unit/validate/mocked_data/simple_fail/report.yml @@ -0,0 +1,13 @@ +--- +complies: false +skipped: [] +get_facts: + complies: false + extra: [] + missing: [] + present: + hostname: + complies: false + actual_value: n9k2 + expected_value: my_hostname + nested: false diff --git a/test/unit/validate/mocked_data/simple_fail/validate.yml b/test/unit/validate/mocked_data/simple_fail/validate.yml new file mode 100644 index 00000000..d2c2573a --- /dev/null +++ b/test/unit/validate/mocked_data/simple_fail/validate.yml @@ -0,0 +1,3 @@ +--- +- get_facts: + hostname: my_hostname diff --git a/test/unit/validate/mocked_data/strict_fail/report.yml b/test/unit/validate/mocked_data/strict_fail/report.yml index d5c6300d..af9e2b7b 100644 --- a/test/unit/validate/mocked_data/strict_fail/report.yml +++ b/test/unit/validate/mocked_data/strict_fail/report.yml @@ -42,7 +42,7 @@ get_bgp_neighbors: extra: [] missing: [] present: - sent_prefixes: {actual_value: 0, complies: false, + sent_prefixes: {actual_value: 0, expected_value: "^[1-9]", complies: false, nested: false} nested: true ipv6: {complies: true, nested: true} @@ -90,7 +90,7 @@ ping: extra: [] missing: [] present: - packet_loss: {actual_value: 1, complies: false, nested: false} + packet_loss: {actual_value: 1, expected_value: 0, complies: false, nested: false} nested: true get_route_to: diff --git a/test/unit/validate/test_unit.py b/test/unit/validate/test_unit.py index 09123c6e..9df339ca 100644 --- a/test/unit/validate/test_unit.py +++ b/test/unit/validate/test_unit.py @@ -95,7 +95,8 @@ {u'complies': False, u'extra': [], u'missing': [], - u'present': {'a': {u'actual_value': 2, u'complies': False, u'nested': False}, + u'present': {'a': {u'actual_value': 2, u'expected_value': 1, + u'complies': False, u'nested': False}, 'b': {u'complies': True, u'nested': False}, 'c': {u'complies': True, u'nested': False}}} ), @@ -105,7 +106,8 @@ {u'complies': False, u'extra': [], u'missing': ['a'], - u'present': {'b': {u'actual_value': 1, u'complies': False, u'nested': False}, + u'present': {'b': {u'actual_value': 1, u'expected_value': 2, + u'complies': False, u'nested': False}, 'c': {u'complies': True, u'nested': False}}} ), ( @@ -140,6 +142,7 @@ u'extra': [], u'missing': [], u'present': {'A': {u'actual_value': 1, + u'expected_value': 3, u'complies': False, u'nested': False}, 'B': {u'complies': True, @@ -159,6 +162,7 @@ u'extra': [], u'missing': ['B'], u'present': {'A': {u'actual_value': 1, + u'expected_value': 3, u'complies': False, u'nested': False}}}, u'nested': True}}} @@ -179,7 +183,8 @@ {u'complies': False, u'extra': [], u'missing': [], - u'present': {'a': {u'actual_value': 2, u'complies': False, u'nested': False}, + u'present': {'a': {u'actual_value': 2, u'expected_value': 1, + u'complies': False, u'nested': False}, 'b': {u'complies': True, u'nested': False}, 'c': {u'complies': True, u'nested': False}}} ), @@ -189,7 +194,8 @@ {u'complies': False, u'extra': [], u'missing': ['a'], - u'present': {'b': {u'actual_value': 1, u'complies': False, u'nested': False}, + u'present': {'b': {u'actual_value': 1, u'expected_value': 2, + u'complies': False, u'nested': False}, 'c': {u'complies': True, u'nested': False}}} ), ( @@ -224,6 +230,7 @@ u'extra': [], u'missing': [], u'present': {'A': {u'actual_value': 1, + u'expected_value': 3, u'complies': False, u'nested': False}, 'B': {u'complies': True, @@ -243,6 +250,7 @@ u'extra': ['C'], u'missing': ['B'], u'present': {'A': {u'actual_value': 1, + u'expected_value': 3, u'complies': False, u'nested': False}}}, u'nested': True}}} @@ -260,6 +268,7 @@ u'extra': ['C'], u'missing': ['B'], u'present': {'A': {u'actual_value': 1, + u'expected_value': 3, u'complies': False, u'nested': False}}}, u'nested': True}}} diff --git a/test/unit/validate/test_validate.py b/test/unit/validate/test_validate.py index 9e831fbe..3166e7c1 100644 --- a/test/unit/validate/test_validate.py +++ b/test/unit/validate/test_validate.py @@ -28,6 +28,16 @@ def _read_yaml(filename): class TestValidate: """Wraps tests.""" + def test_simple_fail(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "simple_fail") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + actual_report = device.compliance_report(os.path.join(mocked_data, "validate.yml")) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_non_strict_pass(self): """A simple test.""" mocked_data = os.path.join(BASEPATH, "mocked_data", "non_strict_pass") @@ -38,6 +48,17 @@ def test_non_strict_pass(self): assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_non_strict_pass_from_source(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "non_strict_pass") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + source = _read_yaml(os.path.join(mocked_data, "validate.yml")) + actual_report = device.compliance_report(validation_source=source) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_non_strict_fail(self): """A simple test.""" mocked_data = os.path.join(BASEPATH, "mocked_data", "non_strict_fail") @@ -48,6 +69,17 @@ def test_non_strict_fail(self): assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_non_strict_fail_from_source(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "non_strict_fail") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + source = _read_yaml(os.path.join(mocked_data, "validate.yml")) + actual_report = device.compliance_report(validation_source=source) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_fail(self): """A simple test.""" mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_fail") @@ -58,6 +90,17 @@ def test_strict_fail(self): assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_fail_from_source(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_fail") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + source = _read_yaml(os.path.join(mocked_data, "validate.yml")) + actual_report = device.compliance_report(validation_source=source) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_pass(self): """A simple test.""" mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_pass") @@ -68,6 +111,17 @@ def test_strict_pass(self): assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_pass_from_source(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_pass") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + source = _read_yaml(os.path.join(mocked_data, "validate.yml")) + actual_report = device.compliance_report(validation_source=source) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_pass_skip(self): """A simple test.""" mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_pass_skip") @@ -78,6 +132,17 @@ def test_strict_pass_skip(self): assert expected_report == actual_report, yaml.safe_dump(actual_report) + def test_strict_pass_skip_from_source(self): + """A simple test.""" + mocked_data = os.path.join(BASEPATH, "mocked_data", "strict_pass_skip") + expected_report = _read_yaml(os.path.join(mocked_data, "report.yml")) + + device = FakeDriver(mocked_data) + source = _read_yaml(os.path.join(mocked_data, "validate.yml")) + actual_report = device.compliance_report(validation_source=source) + + assert expected_report == actual_report, yaml.safe_dump(actual_report) + class FakeDriver(NetworkDriver): """This is a fake NetworkDriver.""" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..b7e6bd0b --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py27,py34,py35,py36 + +[testenv] +deps = -rrequirements-dev.txt +commands = + py.test