From 0288770e51f14e44dcca9ddb45d87cfd71094768 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 24 Mar 2016 15:26:42 -0600 Subject: [PATCH 01/23] fixes #91 include dot and hyphen in regex match for domain-id --- pyeapi/api/mlag.py | 2 +- test/system/test_api_mlag.py | 2 +- test/unit/test_api_mlag.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyeapi/api/mlag.py b/pyeapi/api/mlag.py index f958f01..7586a92 100644 --- a/pyeapi/api/mlag.py +++ b/pyeapi/api/mlag.py @@ -116,7 +116,7 @@ def _parse_domain_id(self, config): dict: A dict object that is intended to be merged into the resource dict """ - match = re.search(r'domain-id (\w+)', config) + match = re.search(r'domain-id ([\w\.\-]+)', config) value = match.group(1) if match else None return dict(domain_id=value) diff --git a/test/system/test_api_mlag.py b/test/system/test_api_mlag.py index e7a75c7..f89840b 100644 --- a/test/system/test_api_mlag.py +++ b/test/system/test_api_mlag.py @@ -55,7 +55,7 @@ def test_set_domain_id_with_value(self): dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no domain-id', api.get_block('mlag configuration')) - result = dut.api('mlag').set_domain_id('test') + result = dut.api('mlag').set_domain_id('test.dom-id') self.assertTrue(result) self.assertIn('domain-id test', api.get_block('mlag configuration')) diff --git a/test/unit/test_api_mlag.py b/test/unit/test_api_mlag.py index 222b984..b13ba08 100644 --- a/test/unit/test_api_mlag.py +++ b/test/unit/test_api_mlag.py @@ -66,8 +66,8 @@ def test_set_domain_id(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': - cmds.append('domain-id test') - func = function('set_domain_id', 'test') + cmds.append('domain-id test.dom-id') + func = function('set_domain_id', 'test.dom-id') elif state == 'negate': cmds.append('no domain-id') func = function('set_domain_id', value='test', disable=True) From 10e5c5faca716070a1dfbe97b36987c61be80785 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 28 Mar 2016 11:37:42 -0600 Subject: [PATCH 02/23] Update to take any characters as domain id Update test cases --- pyeapi/api/mlag.py | 2 +- test/system/test_api_mlag.py | 7 ++++--- test/unit/test_api_mlag.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyeapi/api/mlag.py b/pyeapi/api/mlag.py index 7586a92..6827615 100644 --- a/pyeapi/api/mlag.py +++ b/pyeapi/api/mlag.py @@ -116,7 +116,7 @@ def _parse_domain_id(self, config): dict: A dict object that is intended to be merged into the resource dict """ - match = re.search(r'domain-id ([\w\.\-]+)', config) + match = re.search(r'domain-id (.+)', config) value = match.group(1) if match else None return dict(domain_id=value) diff --git a/test/system/test_api_mlag.py b/test/system/test_api_mlag.py index f89840b..7c395c7 100644 --- a/test/system/test_api_mlag.py +++ b/test/system/test_api_mlag.py @@ -55,9 +55,10 @@ def test_set_domain_id_with_value(self): dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no domain-id', api.get_block('mlag configuration')) - result = dut.api('mlag').set_domain_id('test.dom-id') - self.assertTrue(result) - self.assertIn('domain-id test', api.get_block('mlag configuration')) + for domid in ['test_domain_id', 'test.dom-id', 'test domain id']: + result = dut.api('mlag').set_domain_id(domid) + self.assertTrue(result) + self.assertIn('domain-id %s' % domid, api.get_block('mlag configuration')) def test_set_domain_id_with_no_value(self): for dut in self.duts: diff --git a/test/unit/test_api_mlag.py b/test/unit/test_api_mlag.py index b13ba08..aa071b6 100644 --- a/test/unit/test_api_mlag.py +++ b/test/unit/test_api_mlag.py @@ -66,8 +66,8 @@ def test_set_domain_id(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': - cmds.append('domain-id test.dom-id') - func = function('set_domain_id', 'test.dom-id') + cmds.append('domain-id test.dom-id string') + func = function('set_domain_id', 'test.dom-id string') elif state == 'negate': cmds.append('no domain-id') func = function('set_domain_id', value='test', disable=True) From 634131e11bf9ba8b68681c7b045810d6ec549bd3 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 28 Mar 2016 11:45:43 -0600 Subject: [PATCH 03/23] Add end of string to regex --- pyeapi/api/mlag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/api/mlag.py b/pyeapi/api/mlag.py index 6827615..6c523d8 100644 --- a/pyeapi/api/mlag.py +++ b/pyeapi/api/mlag.py @@ -116,7 +116,7 @@ def _parse_domain_id(self, config): dict: A dict object that is intended to be merged into the resource dict """ - match = re.search(r'domain-id (.+)', config) + match = re.search(r'domain-id (.+)$', config) value = match.group(1) if match else None return dict(domain_id=value) From 2189b986b8510d5527cb54eee561806b4d9518bc Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 31 Mar 2016 16:01:55 -0600 Subject: [PATCH 04/23] use logger instead of syslog to make more universal --- pyeapi/client.py | 26 +++++++++++++++++++++++--- pyeapi/utils.py | 11 ----------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 6223197..9cbb080 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -91,6 +91,7 @@ """ import os +import sys import logging import re @@ -104,13 +105,30 @@ from ConfigParser import SafeConfigParser from ConfigParser import Error as SafeConfigParserError -from pyeapi.utils import load_module, make_iterable, syslog_warning +from pyeapi.utils import load_module, make_iterable from pyeapi.eapilib import HttpEapiConnection, HttpsEapiConnection from pyeapi.eapilib import SocketEapiConnection, HttpLocalEapiConnection from pyeapi.eapilib import CommandError +if sys.platform == "darwin": + # Mac OS syslog + address = '/var/run/syslog' +elif sys.platform == "win32": + # Windows write to localhost, port 514 + # This is the default SysLogHandler address, but needs to be + # specified since we are using a variable. + address = ('localhost', 514) +else: + # Most *nix syslog + address = '/dev/log' + LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) +handler = logging.handlers.SysLogHandler(address=address) +formatter = logging.Formatter('pyeapi.%(module)s.%(funcName)s: %(message)s') +handler.setFormatter(formatter) +LOGGER.addHandler(handler) CONFIG_SEARCH_PATH = ['~/.eapi.conf', '/mnt/flash/eapi.conf'] @@ -193,12 +211,14 @@ def read(self, filename): Args: filename (str): The full path to the file to load """ + try: SafeConfigParser.read(self, filename) except SafeConfigParserError as exc: # Ignore file and syslog a message on SafeConfigParser errors - syslog_warning("%s: parsing error in eapi conf file: %s" % - (type(exc).__name__, filename)) + msg = ("%s: parsing error in eapi conf file: %s" % + (type(exc).__name__, filename)) + LOGGER.debug(msg) self._add_default_connection() diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 9af4841..93c72c2 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -150,17 +150,6 @@ def debug(text): if islocalconnection(): _LOGGER.debug(text) -def syslog_warning(text): - """Print text to syslog at warning level - - Args: - text (str): The string object to print to syslog - - """ - - syslog.openlog("pyeapi") - syslog.syslog(syslog.LOG_WARNING, text) - def make_iterable(value): """Converts the supplied value to a list object From 0467d1e5b525ed8e45839b9f7aacb1cc30dd09e3 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 31 Mar 2016 16:13:57 -0600 Subject: [PATCH 05/23] update default syslog path --- pyeapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 9cbb080..7a52486 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -121,7 +121,7 @@ address = ('localhost', 514) else: # Most *nix syslog - address = '/dev/log' + address = '/var/log/syslog' LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) From 0da437875d287ba56e877955a2e7e89aa292def3 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 31 Mar 2016 16:28:21 -0600 Subject: [PATCH 06/23] test --- pyeapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 7a52486..4968073 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -125,7 +125,7 @@ LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) -handler = logging.handlers.SysLogHandler(address=address) +handler = logging.handlers.SysLogHandler() formatter = logging.Formatter('pyeapi.%(module)s.%(funcName)s: %(message)s') handler.setFormatter(formatter) LOGGER.addHandler(handler) From e0e8097b756c29d5fb4af680c4dc1813416e9ea0 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 1 Apr 2016 13:09:06 -0600 Subject: [PATCH 07/23] fixes to send debug to syslog --- pyeapi/client.py | 23 ++--------------------- pyeapi/eapilib.py | 2 +- pyeapi/utils.py | 28 ++++++++++++++++++++-------- test/unit/test_utils.py | 2 +- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 4968073..8b1264d 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -105,31 +105,12 @@ from ConfigParser import SafeConfigParser from ConfigParser import Error as SafeConfigParserError -from pyeapi.utils import load_module, make_iterable +from pyeapi.utils import load_module, make_iterable, debug from pyeapi.eapilib import HttpEapiConnection, HttpsEapiConnection from pyeapi.eapilib import SocketEapiConnection, HttpLocalEapiConnection from pyeapi.eapilib import CommandError -if sys.platform == "darwin": - # Mac OS syslog - address = '/var/run/syslog' -elif sys.platform == "win32": - # Windows write to localhost, port 514 - # This is the default SysLogHandler address, but needs to be - # specified since we are using a variable. - address = ('localhost', 514) -else: - # Most *nix syslog - address = '/var/log/syslog' - -LOGGER = logging.getLogger(__name__) -LOGGER.setLevel(logging.DEBUG) -handler = logging.handlers.SysLogHandler() -formatter = logging.Formatter('pyeapi.%(module)s.%(funcName)s: %(message)s') -handler.setFormatter(formatter) -LOGGER.addHandler(handler) - CONFIG_SEARCH_PATH = ['~/.eapi.conf', '/mnt/flash/eapi.conf'] TRANSPORTS = { @@ -218,7 +199,7 @@ def read(self, filename): # Ignore file and syslog a message on SafeConfigParser errors msg = ("%s: parsing error in eapi conf file: %s" % (type(exc).__name__, filename)) - LOGGER.debug(msg) + debug(msg) self._add_default_connection() diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 267ecfa..26b867e 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -389,7 +389,7 @@ def send(self, data): # For Python 3.x - decode bytes into string response_content = response_content.decode() decoded = json.loads(response_content) - debug('eapi_response: %s' % decoded) + _LOGGER.debug('eapi_response: %s' % decoded) if 'error' in decoded: (code, msg, err, out) = self._parse_error_message(decoded) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 93c72c2..82fd140 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -32,9 +32,9 @@ import os import sys import imp +import inspect import logging import logging.handlers -import syslog import collections from itertools import tee @@ -47,10 +47,20 @@ from itertools import izip_longest as zip_longest _LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) -_syslog_handler = logging.handlers.SysLogHandler() +# Create a handler to log messages to syslog +if sys.platform == "darwin": + _syslog_handler = logging.handlers.SysLogHandler(address='/var/run/syslog') +else: + _syslog_handler = logging.handlers.SysLogHandler() _LOGGER.addHandler(_syslog_handler) -_LOGGER.setLevel(logging.INFO) + +# Create a handler to log messages to stderr +_stderr_formatter = logging.Formatter('\n\n******** LOG NOTE ********\n%(message)s\n') +_stderr_handler = logging.StreamHandler() +_stderr_handler.setFormatter(_stderr_formatter) +_LOGGER.addHandler(_stderr_handler) def import_module(name): """ Imports a module into the current runtime environment @@ -140,15 +150,17 @@ def islocalconnection(): return os.path.exists('/etc/Eos-release') def debug(text): - """Prints text to syslog when on a local connection + """Log a message to syslog and stderr Args: - text (str): The string object to print to syslog + text (str): The string object to print """ - - if islocalconnection(): - _LOGGER.debug(text) + frame = inspect.currentframe().f_back + module = frame.f_globals['__name__'] + func = frame.f_code.co_name + msg = "%s.%s: %s" % (module, func, text) + _LOGGER.debug(msg) def make_iterable(value): """Converts the supplied value to a list object diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py index bc582b7..e52b7ed 100644 --- a/test/unit/test_utils.py +++ b/test/unit/test_utils.py @@ -76,4 +76,4 @@ def test_collapse_mixed(self): def test_debug(self, mock_logger): pyeapi.utils.islocalconnection = Mock(return_value=True) pyeapi.utils.debug('test') - mock_logger.debug.assert_called_with('test') + mock_logger.debug.assert_called_with('test_utils.test_debug: test') From 1c778ed6768696d7f842718477fd44ecbf4486ba Mon Sep 17 00:00:00 2001 From: Brian Goldberg Date: Thu, 23 Jun 2016 13:13:10 -0400 Subject: [PATCH 08/23] Added ospf scripts --- pyeapi/api/ospf.py | 172 ++++++++++++++++++++++++++++++ test/fixtures/running_config.ospf | 28 +++++ test/unit/test_api_ospf.py | 94 ++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 pyeapi/api/ospf.py create mode 100644 test/fixtures/running_config.ospf create mode 100644 test/unit/test_api_ospf.py diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py new file mode 100644 index 0000000..0abd725 --- /dev/null +++ b/pyeapi/api/ospf.py @@ -0,0 +1,172 @@ +# +# Copyright (c) 2014, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +''' +API module for Ospf +''' + +import re + +from collections import namedtuple + +import netaddr + +from pyeapi.api import Entity, EntityCollection +from pyeapi.utils import make_iterable + + +class Ospf(Entity): + # The Ospf class implements global Ospf router configuration + + def __init__(self, *args, **kwargs): + super(Ospf, self).__init__(*args, **kwargs) + pass + + def get(self): + # Returns the OSPF routing configuration as a dict object + config = self.get_block('^router ospf .*') + if not config: + return None + + response = dict() + response.update(self._parse_router_id(config)) + response.update(self._parse_networks(config)) + response.update(self._parse_ospf_process_id(config)) + response.update(self._parse_redistribution(config)) + response.update(self._parse_shutdown(config)) + + return response + + def _parse_ospf_process_id(self, config): + match = re.search(r'^router ospf (\d+)', config) + return dict(ospf_process_id=int(match.group(1))) + + def _parse_router_id(self, config): + match = re.search(r'router-id ([^\s]+)', config) + value = match.group(1) if match else None + return dict(router_id=value) + + def _parse_networks(self, config): + networks = list() + regexp = r'network (.+)/(\d+) area (\d+\.\d+\.\d+\.\d+)' + matches = re.findall(regexp, config) + for (network, netmask, area) in matches: + networks.append(dict(network=network, netmask=netmask, area=area)) + return dict(networks=networks) + + def _parse_redistribution(self, config): + redistributions = list() + regexp = r'redistribute .*' + matches = re.findall(regexp, config) + for line in matches: + ospf_redist = line.split() + if len(ospf_redist) == 2: + # simple redist: eg 'redistribute bgp' + protocol = ospf_redist[1] + redistributions.append(dict(protocol=protocol)) + if len(ospf_redist) == 4: + # complex redist eg 'redistribute bgp route-map NYSE-RP-MAP' + protocol = ospf_redist[1] + route_map_name = ospf_redist[3] + redistributions.append(dict(protocol=protocol, route_map=route_map_name)) + return dict(redistributions=redistributions) + + def _parse_shutdown(self, config): + value = 'no shutdown' in config + return dict(shutdown=not value) + + def set_shutdown(self): + cmd = 'shutdown' + return self.configure_ospf(cmd) + + def set_no_shutdown(self): + cmd = 'no shutdown' + return self.configure_ospf(cmd) + + def delete(self): + config = self.get() + if not config: + return True + command = 'no router ospf {}'.format(config['ospf_process_id']) + return self.configure(command) + + def create(self, ospf_process_id): + value = int(ospf_process_id) + if not 0 < value < 65536: + raise ValueError('ospf as must be between 1 and 65535') + command = 'router ospf {}'.format(ospf_process_id) + return self.configure(command) + + def configure_ospf(self, cmd): + config = self.get() + cmds = ['router ospf {}'.format(config['ospf_process_id'])] + cmds.extend(make_iterable(cmd)) + return super(Ospf, self).configure(cmds) + + def set_router_id(self, value=None, default=False, disable=False): + cmd = self.command_builder('router-id', value=value, default=default, disable=disable) + return self.configure_ospf(cmd) + + def add_network(self, network, netmask, area=0): + if network == '' or netmask == '': + raise ValueError('network and mask values ' + 'may not be empty') + cmd = 'network {}/{} area {}'.format(network, netmask, area) + return self.configure_ospf(cmd) + + def remove_network(self, network, netmask, area=0): + if network == '' or netmask == '': + raise ValueError('network and mask values ' + 'may not be empty') + cmd = 'no network {}/{} area {}'.format(network, netmask, area) + return self.configure_ospf(cmd) + + def add_redistribution(self, protocol, route_map_name=None): + protocols = ['bgp', 'rip', 'static', 'connected'] + if protocol not in protocols: + raise ValueError('redistributed protocol must be' + 'bgp, connected, rip or static') + if route_map_name is None: + cmd = 'redistribute {}'.format(protocol) + else: + cmd = 'redistribute {} route-map {}'.format(protocol, route_map_name) + return self.configure_ospf(cmd) + + def remove_redistribution(self, protocol): + protocols = ['bgp', 'rip', 'static', 'connected'] + if protocol not in protocols: + raise ValueError('redistributed protocol must be' + 'bgp, connected, rip or static') + cmd = 'no redistribute {}'.format(protocol) + return self.configure_ospf(cmd) + +def instance(api): + return Ospf(api) diff --git a/test/fixtures/running_config.ospf b/test/fixtures/running_config.ospf new file mode 100644 index 0000000..c928278 --- /dev/null +++ b/test/fixtures/running_config.ospf @@ -0,0 +1,28 @@ +! +ip routing +! +router ospf 65000 + router-id 1.1.1.1 + no bfd all-interfaces + distance ospf intra-area 110 + distance ospf external 110 + distance ospf inter-area 110 + redistribute bgp route-map RM-IN + redistribute bgp route-map RM-OUT + area 0.0.0.0 default-cost 10 + network 172.16.10.0/24 area 0.0.0.0 + network 172.17.0.0/16 area 0.0.0.0 + max-lsa 12000 75 ignore-time 5 ignore-count 5 reset-time 5 + adjacency exchange-start threshold 20 + log-adjacency-changes + timers throttle spf 0 5000 5000 + timers lsa arrival 1000 + timers throttle lsa all 1000 5000 5000 + no timers out-delay + maximum-paths 128 + no timers pacing flood + no max-metric router-lsa + point-to-point routes + no graceful-restart +! +! diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py new file mode 100644 index 0000000..0c68aa7 --- /dev/null +++ b/test/unit/test_api_ospf.py @@ -0,0 +1,94 @@ +import sys +import os +import unittest + +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from testlib import get_fixture, function +from testlib import EapiConfigUnitTest + +import pyeapi.api.ospf + + +class TestApiOspf(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiOspf, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.ospf.instance(None) + self.config = open(get_fixture('running_config.ospf')).read() + + def test_get(self): + result = self.instance.get() + keys = ['networks', 'ospf_process_id', 'redistributions', 'router_id', 'shutdown'] + self.assertEqual(sorted(keys), sorted(result.keys())) + + def test_create(self): + for ospf_id in ['65000', 65000]: + func = function('create', ospf_id) + cmds = 'router ospf {}'.format(ospf_id) + self.eapi_positive_config_test(func, cmds) + + def test_create_invalid_id(self): + for ospf_id in ['66000', 66000]: + with self.assertRaises(ValueError): + self.instance.create(ospf_id) + + def test_delete(self): + func = function('delete') + cmds = 'no router ospf 65000' + self.eapi_positive_config_test(func, cmds) + + def test_add_network(self): + func = function('add_network', '172.16.10.0', '24', '0') + cmds = ['router ospf 65000', 'network 172.16.10.0/24 area 0'] + self.eapi_positive_config_test(func, cmds) + + func = function('add_network', '', '24', '0') + self.eapi_exception_config_test(func, ValueError) + + func = function('add_network', '172.16.10.0', '', '0') + self.eapi_exception_config_test(func, ValueError) + + def test_remove_network(self): + func = function('remove_network', '172.16.10.0', '24', '0') + cmds = ['router ospf 65000', 'no network 172.16.10.0/24 area 0'] + self.eapi_positive_config_test(func, cmds) + + func = function('remove_network', '', '24', '0') + self.eapi_exception_config_test(func, ValueError) + + func = function('remove_network', '172.16.10.0', '', '0') + self.eapi_exception_config_test(func, ValueError) + + def test_set_router_id(self): + for state in ['config', 'negate', 'default']: + rid = '1.1.1.1' + if state == 'config': + cmds = ['router ospf 65000', 'router-id 1.1.1.1'] + func = function('set_router_id', rid) + elif state == 'negate': + cmds = ['router ospf 65000', 'no router-id'] + func = function('set_router_id') + elif state == 'default': + cmds = ['router ospf 65000', 'default router-id'] + func = function('set_router_id', rid, True) + self.eapi_positive_config_test(func, cmds) + + cmds = ['router ospf 65000', 'no router-id'] + func = function('set_router_id') + self.eapi_positive_config_test(func, cmds) + + def test_set_shutdown(self): + for state in ['config', 'negate', 'default']: + if state == 'config': + cmds = ['router ospf 65000', 'shutdown'] + func = function('set_shutdown') + elif state == 'negate': + cmds = ['router ospf 65000', 'no shutdown'] + func = function('set_no_shutdown') + self.eapi_positive_config_test(func, cmds) + + +if __name__ == '__main__': + unittest.main() + From af8688f146dc9f481e933364caede3ef54ace5b3 Mon Sep 17 00:00:00 2001 From: Brian Goldberg Date: Thu, 28 Jul 2016 17:20:20 -0400 Subject: [PATCH 09/23] added recursive_include test *.ospf to MANIFEST.in --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index df2bb8e..70e6dc3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ recursive-include test *.py recursive-include test *.text recursive-include test *.vxlan recursive-include test *.bgp +recursive-include test *.ospf recursive-include test *.routemaps recursive-include test *.varp recursive-include test *.varp_null From e246ad722633d59c0507ce3ef7d8ed0924f10aa9 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Tue, 6 Sep 2016 23:21:08 -0400 Subject: [PATCH 10/23] start of ospf system tests --- pyeapi/api/ospf.py | 4 -- test/fixtures/dut.conf | 4 +- test/system/test_ospf_api.py | 132 +++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 test/system/test_ospf_api.py diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py index 0abd725..eae1270 100644 --- a/pyeapi/api/ospf.py +++ b/pyeapi/api/ospf.py @@ -35,10 +35,6 @@ import re -from collections import namedtuple - -import netaddr - from pyeapi.api import Entity, EntityCollection from pyeapi.utils import make_iterable diff --git a/test/fixtures/dut.conf b/test/fixtures/dut.conf index 036a7d0..a40f0bf 100644 --- a/test/fixtures/dut.conf +++ b/test/fixtures/dut.conf @@ -1,5 +1,5 @@ [connection:veos01] -host: 192.168.1.16 +host: 172.16.130.21 username: eapi password: password -transport: http +transport: https diff --git a/test/system/test_ospf_api.py b/test/system/test_ospf_api.py new file mode 100644 index 0000000..a04cec4 --- /dev/null +++ b/test/system/test_ospf_api.py @@ -0,0 +1,132 @@ +# +# Copyright (c) 2016, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +import os +import unittest + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from random import randint +from systestlib import DutSystemTest +def clear_ospf_config(dut, id=None): + if id is None: + try: + id = int(dut.get_config(params="section ospf")[0].split()[2]) + dut.config(['no router ospf %d' % id]) + except IndexError: + '''No OSPF configured''' + pass + +class TestApiOspf(DutSystemTest): + def test_get(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", + "redistribute bgp"]) + ospf_response = dut.api('ospf').get() + config = dict(router_id="1.1.1.1", ospf_process_id=1, + networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], + redistributions=[dict(protocol="bgp")], shutdown=False) + self.assertEqual(ospf_response, config) + + def test_shutdown(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1", "network 1.1.1.1/32 area 0"]) + ospf = dut.api('ospf') + response = ospf.set_shutdown() + self.assertTrue(response) + self.assertIn('shutdown', ospf.get_block("router ospf 1")) + + def test_no_shutown(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", + "shutdown"]) + ospf = dut.api('ospf') + response = ospf.set_no_shutdown() + self.assertTrue(response) + self.assertIn('no shutdown', ospf.get_block("router ospf 10")) + + def test_delete(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 10"]) + ospf = dut.api("ospf") + response = ospf.delete() + self.assertTrue(response) + self.assertEqual(None, ospf.get_block("router ospf")) + + def test_create_valid_id(self): + for dut in self.duts: + clear_ospf_config(dut) + id = randint(1,65536) + ospf = dut.api("ospf") + response = ospf.create(id) + self.assertTrue(response) + self.assertIn("router ospf {}".format(id), dut.get_config()) + + def test_create_invalid_id(self): + for dut in self.duts: + clear_ospf_config(dut) + id = randint(70000, 100000) + with self.assertRaises(ValueError): + dut.api("ospf").create(id) + + def test_configure_ospf(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + response = ospf.configure_ospf("router-id 1.1.1.1") + self.assertTrue(response) + self.assertIn("router-id 1.1.1.1", ospf.get_block("router ospf 1")) + def test_set_router_id(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + response = ospf.set_router_id(randint(1, 65536)) + self.assertFalse(response) + response = ospf.set_router_id("2.2.2.2") + self.assertTrue(response) + self.assertIn("router-id 2.2.2.2", ospf.get_block("router ospf 1")) + response = ospf.set_router_id(default=True) + self.assertTrue(response) + self.assertIn("no router-id", ospf.get_block("router ospf 1")) + response = ospf.set_router_id(disable=True) + self.assertTrue(response) + self.assertIn("no router-id", ospf.get_block("router ospf 1")) + + + + From 7c3849da532fdf7542e59e331f3fb685dd3ef099 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 18:49:53 -0400 Subject: [PATCH 11/23] added to unittest code coverage --- test/unit/test_api_ospf.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index 0c68aa7..9b55e76 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -16,6 +16,7 @@ def __init__(self, *args, **kwargs): super(TestApiOspf, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ospf.instance(None) self.config = open(get_fixture('running_config.ospf')).read() + def test_get(self): result = self.instance.get() @@ -88,6 +89,50 @@ def test_set_shutdown(self): func = function('set_no_shutdown') self.eapi_positive_config_test(func, cmds) + def test_add_redistribution_no_route_map(self): + for protocol in ['bgp', 'rip', 'static', 'connected', 'no-proto']: + cmds = ['router ospf 65000', 'redistribute {}'.format(protocol)] + func = function('add_redistribution', protocol) + if protocol != 'no-proto': + self.eapi_positive_config_test(func, cmds) + else: + self.eapi_exception_config_test(func, ValueError) + + def test_add_redistribution_with_route_map(self): + for protocol in ['bgp', 'rip', 'static', 'connected']: + cmds = ['router ospf 65000', 'redistribute {} route-map test'.format(protocol)] + func = function('add_redistribution', protocol, 'test') + if protocol != 'no-proto': + self.eapi_positive_config_test(func, cmds) + else: + self.eapi_exception_config_test(func, ValueError) + + + def test_delete_redistribution_no_route_map(self): + for protocol in ['bgp', 'rip', 'static', 'connected', 'no-proto']: + cmds = ['router ospf 65000', 'no redistribute {}'.format(protocol)] + func = function('remove_redistribution', protocol) + if protocol != 'no-proto': + self.eapi_positive_config_test(func, cmds) + else: + self.eapi_exception_config_test(func, ValueError) + + +class TestApiNegOspf(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiNegOspf, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.ospf.instance(None) + self.config = open(get_fixture('running_config.bgp')).read() + + def test_no_get(self): + result = self.instance.get() + self.assertEqual(None, result) + + def test_no_delete(self): + result = self.instance.delete() + self.assertTrue(result) + if __name__ == '__main__': unittest.main() From 369e18b82bb51be50ca0183a94b07351c47a0d89 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 18:53:08 -0400 Subject: [PATCH 12/23] finishing system tests and file rename --- test/system/test_api_ospf.py | 186 +++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 test/system/test_api_ospf.py diff --git a/test/system/test_api_ospf.py b/test/system/test_api_ospf.py new file mode 100644 index 0000000..2fe3cf7 --- /dev/null +++ b/test/system/test_api_ospf.py @@ -0,0 +1,186 @@ +# +# Copyright (c) 2016, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +import os +import unittest + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from random import randint +from systestlib import DutSystemTest +def clear_ospf_config(dut, id=None): + if id is None: + try: + id = int(dut.get_config(params="section ospf")[0].split()[2]) + dut.config(['no router ospf %d' % id]) + except IndexError: + '''No OSPF configured''' + pass + +class TestApiOspf(DutSystemTest): + def test_get(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", + "redistribute bgp"]) + ospf_response = dut.api('ospf').get() + config = dict(router_id="1.1.1.1", ospf_process_id=1, + networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], + redistributions=[dict(protocol="bgp")], shutdown=False) + self.assertEqual(ospf_response, config) + + def test_shutdown(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1", "network 1.1.1.1/32 area 0"]) + ospf = dut.api('ospf') + response = ospf.set_shutdown() + self.assertTrue(response) + self.assertIn('shutdown', ospf.get_block("router ospf 1")) + + def test_no_shutown(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", + "shutdown"]) + ospf = dut.api('ospf') + response = ospf.set_no_shutdown() + self.assertTrue(response) + self.assertIn('no shutdown', ospf.get_block("router ospf 10")) + + def test_delete(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 10"]) + ospf = dut.api("ospf") + response = ospf.delete() + self.assertTrue(response) + self.assertEqual(None, ospf.get_block("router ospf")) + + def test_create_valid_id(self): + for dut in self.duts: + clear_ospf_config(dut) + id = randint(1,65536) + ospf = dut.api("ospf") + response = ospf.create(id) + self.assertTrue(response) + self.assertIn("router ospf {}".format(id), dut.get_config()) + + def test_create_invalid_id(self): + for dut in self.duts: + clear_ospf_config(dut) + id = randint(70000, 100000) + with self.assertRaises(ValueError): + dut.api("ospf").create(id) + + def test_configure_ospf(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + response = ospf.configure_ospf("router-id 1.1.1.1") + self.assertTrue(response) + self.assertIn("router-id 1.1.1.1", ospf.get_block("router ospf 1")) + + def test_set_router_id(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + response = ospf.set_router_id(randint(1, 65536)) + self.assertFalse(response) + response = ospf.set_router_id("2.2.2.2") + self.assertTrue(response) + self.assertIn("router-id 2.2.2.2", ospf.get_block("router ospf 1")) + response = ospf.set_router_id(default=True) + self.assertTrue(response) + self.assertIn("no router-id", ospf.get_block("router ospf 1")) + response = ospf.set_router_id(disable=True) + self.assertTrue(response) + self.assertIn("no router-id", ospf.get_block("router ospf 1")) + + def test_add_network(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + response = ospf.add_network("2.2.2.0", "24", 1234) + self.assertTrue(response) + self.assertIn("network 2.2.2.0/24 area 0.0.4.210", ospf.get_block("router ospf 1")) + response = ospf.add_network("10.10.10.0", "24") + self.assertTrue(response) + self.assertIn("network 10.10.10.0/24 area 0.0.0.0", ospf.get_block("router ospf 1")) + + def test_remove_network(self): + for dut in self.duts: + clear_ospf_config(dut) + ospf_config = ["router ospf 1", "network 2.2.2.0/24 area 0.0.0.0", + "network 3.3.3.1/32 area 1.1.1.1"] + dut.config(ospf_config) + ospf = dut.api("ospf") + response = ospf.remove_network("2.2.2.0", "24") + self.assertTrue(response) + response = ospf.remove_network("3.3.3.1", "32", "1.1.1.1") + self.assertTrue(response) + for config in ospf_config: + if "router ospf" not in config: + self.assertNotIn(config, ospf.get_block("router ospf 1")) + + def test_add_redistribution(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1"]) + ospf = dut.api("ospf") + protos = ['bgp', 'rip', 'static', 'connected'] + for proto in protos: + if randint(1,10) % 2 == 0: + response = ospf.add_redistribution(proto, 'test') + else: + response = ospf.add_redistribution(proto) + self.assertTrue(response) + for proto in protos: + self.assertIn("redistribute {}".format(proto), ospf.get_block("router ospf 1")) + with self.assertRaises(ValueError): + ospf.add_redistribution("NOT VALID") + + def test_remove_redistribution(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 1", "redistribute bgp", "redistribute static route-map test"]) + ospf = dut.api("ospf") + response = ospf.remove_redistribution('bgp') + self.assertTrue(response) + response = ospf.remove_redistribution('static') + self.assertTrue(response) + self.assertNotIn("redistribute", ospf.get_block("router ospf 1")) + + From 2f30a6e06f69000f70a24995ea089d122f0201a7 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 18:53:58 -0400 Subject: [PATCH 13/23] adding redistribution to config --- test/fixtures/running_config.ospf | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/running_config.ospf b/test/fixtures/running_config.ospf index c928278..92f03b6 100644 --- a/test/fixtures/running_config.ospf +++ b/test/fixtures/running_config.ospf @@ -9,6 +9,7 @@ router ospf 65000 distance ospf inter-area 110 redistribute bgp route-map RM-IN redistribute bgp route-map RM-OUT + redistribute static area 0.0.0.0 default-cost 10 network 172.16.10.0/24 area 0.0.0.0 network 172.17.0.0/16 area 0.0.0.0 From 2666e44a55d8199f2b860d5446d96a0f6fcbe7c5 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 18:56:29 -0400 Subject: [PATCH 14/23] deleting file with wrong name --- test/system/test_ospf_api.py | 132 ----------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 test/system/test_ospf_api.py diff --git a/test/system/test_ospf_api.py b/test/system/test_ospf_api.py deleted file mode 100644 index a04cec4..0000000 --- a/test/system/test_ospf_api.py +++ /dev/null @@ -1,132 +0,0 @@ -# -# Copyright (c) 2016, Arista Networks, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# Neither the name of Arista Networks nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -import os -import unittest - -import sys -sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) - -from random import randint -from systestlib import DutSystemTest -def clear_ospf_config(dut, id=None): - if id is None: - try: - id = int(dut.get_config(params="section ospf")[0].split()[2]) - dut.config(['no router ospf %d' % id]) - except IndexError: - '''No OSPF configured''' - pass - -class TestApiOspf(DutSystemTest): - def test_get(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", - "redistribute bgp"]) - ospf_response = dut.api('ospf').get() - config = dict(router_id="1.1.1.1", ospf_process_id=1, - networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], - redistributions=[dict(protocol="bgp")], shutdown=False) - self.assertEqual(ospf_response, config) - - def test_shutdown(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 1", "network 1.1.1.1/32 area 0"]) - ospf = dut.api('ospf') - response = ospf.set_shutdown() - self.assertTrue(response) - self.assertIn('shutdown', ospf.get_block("router ospf 1")) - - def test_no_shutown(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", - "shutdown"]) - ospf = dut.api('ospf') - response = ospf.set_no_shutdown() - self.assertTrue(response) - self.assertIn('no shutdown', ospf.get_block("router ospf 10")) - - def test_delete(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 10"]) - ospf = dut.api("ospf") - response = ospf.delete() - self.assertTrue(response) - self.assertEqual(None, ospf.get_block("router ospf")) - - def test_create_valid_id(self): - for dut in self.duts: - clear_ospf_config(dut) - id = randint(1,65536) - ospf = dut.api("ospf") - response = ospf.create(id) - self.assertTrue(response) - self.assertIn("router ospf {}".format(id), dut.get_config()) - - def test_create_invalid_id(self): - for dut in self.duts: - clear_ospf_config(dut) - id = randint(70000, 100000) - with self.assertRaises(ValueError): - dut.api("ospf").create(id) - - def test_configure_ospf(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 1"]) - ospf = dut.api("ospf") - response = ospf.configure_ospf("router-id 1.1.1.1") - self.assertTrue(response) - self.assertIn("router-id 1.1.1.1", ospf.get_block("router ospf 1")) - def test_set_router_id(self): - for dut in self.duts: - clear_ospf_config(dut) - dut.config(["router ospf 1"]) - ospf = dut.api("ospf") - response = ospf.set_router_id(randint(1, 65536)) - self.assertFalse(response) - response = ospf.set_router_id("2.2.2.2") - self.assertTrue(response) - self.assertIn("router-id 2.2.2.2", ospf.get_block("router ospf 1")) - response = ospf.set_router_id(default=True) - self.assertTrue(response) - self.assertIn("no router-id", ospf.get_block("router ospf 1")) - response = ospf.set_router_id(disable=True) - self.assertTrue(response) - self.assertIn("no router-id", ospf.get_block("router ospf 1")) - - - - From bf871239072df0291c6e5739d225645dddf67b20 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 21:45:05 -0400 Subject: [PATCH 15/23] fixing flake8 issues --- Makefile | 1 + pyeapi/api/ospf.py | 22 +++++++++++----------- test/system/test_api_ospf.py | 23 +++++++++++------------ test/unit/test_api_ospf.py | 2 -- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 9829dcb..d5d39f9 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ # make unittest -- runs the unit tests # make systest -- runs the system tests # make clean -- clean distutils +# make coverage_report -- code coverage report # ######################################################## # variable section diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py index eae1270..5dea36a 100644 --- a/pyeapi/api/ospf.py +++ b/pyeapi/api/ospf.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, Arista Networks, Inc. +# Copyright (c) 2016, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -32,13 +32,10 @@ ''' API module for Ospf ''' - import re - -from pyeapi.api import Entity, EntityCollection +from pyeapi.api import Entity from pyeapi.utils import make_iterable - class Ospf(Entity): # The Ospf class implements global Ospf router configuration @@ -51,7 +48,7 @@ def get(self): config = self.get_block('^router ospf .*') if not config: return None - + response = dict() response.update(self._parse_router_id(config)) response.update(self._parse_networks(config)) @@ -92,7 +89,8 @@ def _parse_redistribution(self, config): # complex redist eg 'redistribute bgp route-map NYSE-RP-MAP' protocol = ospf_redist[1] route_map_name = ospf_redist[3] - redistributions.append(dict(protocol=protocol, route_map=route_map_name)) + redistributions.append(dict(protocol=protocol, + route_map=route_map_name)) return dict(redistributions=redistributions) def _parse_shutdown(self, config): @@ -128,9 +126,10 @@ def configure_ospf(self, cmd): return super(Ospf, self).configure(cmds) def set_router_id(self, value=None, default=False, disable=False): - cmd = self.command_builder('router-id', value=value, default=default, disable=disable) + cmd = self.command_builder('router-id', value=value, + default=default, disable=disable) return self.configure_ospf(cmd) - + def add_network(self, network, netmask, area=0): if network == '' or netmask == '': raise ValueError('network and mask values ' @@ -153,7 +152,8 @@ def add_redistribution(self, protocol, route_map_name=None): if route_map_name is None: cmd = 'redistribute {}'.format(protocol) else: - cmd = 'redistribute {} route-map {}'.format(protocol, route_map_name) + cmd = 'redistribute {} route-map {}'.format(protocol, + route_map_name) return self.configure_ospf(cmd) def remove_redistribution(self, protocol): @@ -161,7 +161,7 @@ def remove_redistribution(self, protocol): if protocol not in protocols: raise ValueError('redistributed protocol must be' 'bgp, connected, rip or static') - cmd = 'no redistribute {}'.format(protocol) + cmd = 'no redistribute {}'.format(protocol) return self.configure_ospf(cmd) def instance(api): diff --git a/test/system/test_api_ospf.py b/test/system/test_api_ospf.py index 2fe3cf7..bed21cd 100644 --- a/test/system/test_api_ospf.py +++ b/test/system/test_api_ospf.py @@ -30,13 +30,12 @@ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os -import unittest - import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from random import randint from systestlib import DutSystemTest + def clear_ospf_config(dut, id=None): if id is None: try: @@ -50,14 +49,14 @@ class TestApiOspf(DutSystemTest): def test_get(self): for dut in self.duts: clear_ospf_config(dut) - dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", + dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() - config = dict(router_id="1.1.1.1", ospf_process_id=1, + config = dict(router_id="1.1.1.1", ospf_process_id=1, networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], redistributions=[dict(protocol="bgp")], shutdown=False) self.assertEqual(ospf_response, config) - + def test_shutdown(self): for dut in self.duts: clear_ospf_config(dut) @@ -70,7 +69,7 @@ def test_shutdown(self): def test_no_shutown(self): for dut in self.duts: clear_ospf_config(dut) - dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", + dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", "shutdown"]) ospf = dut.api('ospf') response = ospf.set_no_shutdown() @@ -89,7 +88,7 @@ def test_delete(self): def test_create_valid_id(self): for dut in self.duts: clear_ospf_config(dut) - id = randint(1,65536) + id = randint(1, 65536) ospf = dut.api("ospf") response = ospf.create(id) self.assertTrue(response) @@ -127,7 +126,7 @@ def test_set_router_id(self): response = ospf.set_router_id(disable=True) self.assertTrue(response) self.assertIn("no router-id", ospf.get_block("router ospf 1")) - + def test_add_network(self): for dut in self.duts: clear_ospf_config(dut) @@ -139,7 +138,7 @@ def test_add_network(self): response = ospf.add_network("10.10.10.0", "24") self.assertTrue(response) self.assertIn("network 10.10.10.0/24 area 0.0.0.0", ospf.get_block("router ospf 1")) - + def test_remove_network(self): for dut in self.duts: clear_ospf_config(dut) @@ -153,7 +152,7 @@ def test_remove_network(self): self.assertTrue(response) for config in ospf_config: if "router ospf" not in config: - self.assertNotIn(config, ospf.get_block("router ospf 1")) + self.assertNotIn(config, ospf.get_block("router ospf 1")) def test_add_redistribution(self): for dut in self.duts: @@ -162,11 +161,11 @@ def test_add_redistribution(self): ospf = dut.api("ospf") protos = ['bgp', 'rip', 'static', 'connected'] for proto in protos: - if randint(1,10) % 2 == 0: + if randint(1, 10) % 2 == 0: response = ospf.add_redistribution(proto, 'test') else: response = ospf.add_redistribution(proto) - self.assertTrue(response) + self.assertTrue(response) for proto in protos: self.assertIn("redistribute {}".format(proto), ospf.get_block("router ospf 1")) with self.assertRaises(ValueError): diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index 9b55e76..cd2844c 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -16,7 +16,6 @@ def __init__(self, *args, **kwargs): super(TestApiOspf, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ospf.instance(None) self.config = open(get_fixture('running_config.ospf')).read() - def test_get(self): result = self.instance.get() @@ -132,7 +131,6 @@ def test_no_get(self): def test_no_delete(self): result = self.instance.delete() self.assertTrue(result) - if __name__ == '__main__': unittest.main() From bb9e132708026ca441ce5b8c28a5e3f179f33003 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Wed, 7 Sep 2016 22:51:30 -0400 Subject: [PATCH 16/23] fixing docstrings and reverting dut.conf --- pyeapi/api/ospf.py | 180 +++++++++++++++++++++++++++++++++++++++-- test/fixtures/dut.conf | 4 +- 2 files changed, 177 insertions(+), 7 deletions(-) diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py index 5dea36a..28c1325 100644 --- a/pyeapi/api/ospf.py +++ b/pyeapi/api/ospf.py @@ -29,22 +29,43 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -''' -API module for Ospf -''' +"""Module for working with OSPF configuration in EOS + +This module provides an API for creating/modifying/deleting +OSPF configurations + +""" + import re from pyeapi.api import Entity from pyeapi.utils import make_iterable class Ospf(Entity): - # The Ospf class implements global Ospf router configuration + """ The Ospf class implements global Ospf router configuration + """ def __init__(self, *args, **kwargs): super(Ospf, self).__init__(*args, **kwargs) pass def get(self): - # Returns the OSPF routing configuration as a dict object + """Returns the OSPF routing configuration + + Args: + None + Returns: + dict: + keys: router_id (int): OSPF router-id + networks (dict): All networks that + are advertised in OSPF + ospf_process_id (int): OSPF proc id + redistribution (dict): All protocols that + are configured to be + redistributed in OSPF + shutdown (bool): Gives the current shutdown + off the process + """ + config = self.get_block('^router ospf .*') if not config: return None @@ -59,15 +80,41 @@ def get(self): return response def _parse_ospf_process_id(self, config): + """Parses config file for the OSPF proc ID + + Args: + config(str): Running configuration + Returns: + dict: key: ospf_process_id (int) + """ match = re.search(r'^router ospf (\d+)', config) return dict(ospf_process_id=int(match.group(1))) def _parse_router_id(self, config): + """Parses config file for the OSPF router ID + + Args: + config(str): Running configuration + Returns: + dict: key: router_id (str) + """ match = re.search(r'router-id ([^\s]+)', config) value = match.group(1) if match else None return dict(router_id=value) def _parse_networks(self, config): + """Parses config file for the networks advertised + by the OSPF process + + Args: + config(str): Running configuration + Returns: + list: dict: + keys: network (str) + netmask (str) + area (str) + """ + networks = list() regexp = r'network (.+)/(\d+) area (\d+\.\d+\.\d+\.\d+)' matches = re.findall(regexp, config) @@ -76,6 +123,15 @@ def _parse_networks(self, config): return dict(networks=networks) def _parse_redistribution(self, config): + """Parses config file for the OSPF router ID + + Args: + config (str): Running configuration + Returns: + list: dict: + keys: protocol (str) + route-map (optional) (str) + """ redistributions = list() regexp = r'redistribute .*' matches = re.findall(regexp, config) @@ -94,18 +150,50 @@ def _parse_redistribution(self, config): return dict(redistributions=redistributions) def _parse_shutdown(self, config): + """Parses config file for the OSPF router ID + + Args: + config(str): Running configuration + Returns: + dict: key: shutdown (bool) + """ + value = 'no shutdown' in config return dict(shutdown=not value) def set_shutdown(self): + """Shutdowns the OSPF process + + Args: + None + Returns: + bool: True if the commands are completed successfully + """ + cmd = 'shutdown' return self.configure_ospf(cmd) def set_no_shutdown(self): + """Removes the shutdown property from the OSPF process + + Args: + None + Returns: + bool: True if the commands are completed successfully + """ + + cmd = 'no shutdown' return self.configure_ospf(cmd) def delete(self): + """Removes the entire ospf process from the running configuration + + Args: + None + Returns: + bool: True if the command completed succssfully + """ config = self.get() if not config: return True @@ -113,6 +201,16 @@ def delete(self): return self.configure(command) def create(self, ospf_process_id): + """Creates a OSPF process in the default VRF + + Args: + ospf_process_id (str): The OSPF proccess Id value + Returns: + bool: True if the command completed successfully + Exception: + ValueError: If the ospf_process_id passed in less + than 0 or greater than 65536 + """ value = int(ospf_process_id) if not 0 < value < 65536: raise ValueError('ospf as must be between 1 and 65535') @@ -120,17 +218,47 @@ def create(self, ospf_process_id): return self.configure(command) def configure_ospf(self, cmd): + """Allows for a list of OSPF subcommands to be configured" + + Args: + cmd: (list or str): Subcommand to be entered + Returns: + bool: True if all the commands completed successfully + """ config = self.get() cmds = ['router ospf {}'.format(config['ospf_process_id'])] cmds.extend(make_iterable(cmd)) return super(Ospf, self).configure(cmds) def set_router_id(self, value=None, default=False, disable=False): + """Controls the router id property for the OSPF Proccess + + Args: + value (str): The router-id value + default (bool): Controls the use of the default keyword + disable (bool): Controls the use of the no keyword + Returns: + bool: True if the commands are completed successfully + """ cmd = self.command_builder('router-id', value=value, default=default, disable=disable) return self.configure_ospf(cmd) def add_network(self, network, netmask, area=0): + """Adds a network to be advertised by OSPF + + Args: + network (str): The network to be advertised in dotted decimal + notation + netmask (str): The netmask to configure + area (str): The area the network belongs to. + By default this value is 0 + Returns: + bool: True if the command completes successfully + Exception: + ValueError: This will get raised if network or netmask + are not passed to the method + """ if network == '' or netmask == '': raise ValueError('network and mask values ' 'may not be empty') @@ -138,6 +266,21 @@ def add_network(self, network, netmask, area=0): return self.configure_ospf(cmd) def remove_network(self, network, netmask, area=0): + """Removes a network advertisment by OSPF + + Args: + network (str): The network to be removed in dotted decimal + notation + netmask (str): The netmask to configure + area (str): The area the network belongs to. + By default this value is 0 + Returns: + bool: True if the command completes successfully + Exception: + ValueError: This will get raised if network or netmask + are not passed to the method + """ + if network == '' or netmask == '': raise ValueError('network and mask values ' 'may not be empty') @@ -145,6 +288,18 @@ def remove_network(self, network, netmask, area=0): return self.configure_ospf(cmd) def add_redistribution(self, protocol, route_map_name=None): + """Adds a protocol redistribution to OSPF + + Args: + protocol (str): protocol to redistribute + route_map_name (str): route-map to be used to + filter the protocols + Returns: + bool: True if the command completes successfully + Exception: + ValueError: This will be raised if the protocol pass is not one + of the following: [rip, bgp, static, connected] + """ protocols = ['bgp', 'rip', 'static', 'connected'] if protocol not in protocols: raise ValueError('redistributed protocol must be' @@ -157,6 +312,19 @@ def add_redistribution(self, protocol, route_map_name=None): return self.configure_ospf(cmd) def remove_redistribution(self, protocol): + """Removes a protocol redistribution to OSPF + + Args: + protocol (str): protocol to redistribute + route_map_name (str): route-map to be used to + filter the protocols + Returns: + bool: True if the command completes successfully + Exception: + ValueError: This will be raised if the protocol pass is not one + of the following: [rip, bgp, static, connected] + """ + protocols = ['bgp', 'rip', 'static', 'connected'] if protocol not in protocols: raise ValueError('redistributed protocol must be' @@ -165,4 +333,6 @@ def remove_redistribution(self, protocol): return self.configure_ospf(cmd) def instance(api): + """Returns an instance of Ospf + """ return Ospf(api) diff --git a/test/fixtures/dut.conf b/test/fixtures/dut.conf index a40f0bf..036a7d0 100644 --- a/test/fixtures/dut.conf +++ b/test/fixtures/dut.conf @@ -1,5 +1,5 @@ [connection:veos01] -host: 172.16.130.21 +host: 192.168.1.16 username: eapi password: password -transport: https +transport: http From bde6d995fb0c05dc5985f94938be3d89e53ad2ed Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Thu, 8 Sep 2016 00:07:32 -0400 Subject: [PATCH 17/23] adding a send_enable flag to the enable() method --- pyeapi/client.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 8b1264d..ab6fb0f 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -560,7 +560,8 @@ def section(self, regex, config='running_config'): block_end = line_end + block_end return config[block_start:block_end] - def enable(self, commands, encoding='json', strict=False): + def enable(self, commands, encoding='json', strict=False, + send_enable=True): """Sends the array of commands to the node in enable mode This method will send the commands to the node and evaluate @@ -576,6 +577,8 @@ def enable(self, commands, encoding='json', strict=False): strict (bool): If False, this method will attempt to run a command with text encoding if JSON encoding fails + send_enable (bool): If True the enable command will be + prepended to the command list automatically. Returns: A dict object that includes the response for each command along @@ -604,7 +607,7 @@ def enable(self, commands, encoding='json', strict=False): # there in error and both are now present to avoid breaking # existing scripts. 'response' will be removed in a future release. if strict: - responses = self.run_commands(commands, encoding) + responses = self.run_commands(commands, encoding, send_enable) for index, response in enumerate(responses): results.append(dict(command=commands[index], result=response, @@ -613,13 +616,13 @@ def enable(self, commands, encoding='json', strict=False): else: for command in commands: try: - resp = self.run_commands(command, encoding) + resp = self.run_commands(command, encoding, send_enable) results.append(dict(command=command, result=resp[0], encoding=encoding)) except CommandError as exc: if exc.error_code == 1003: - resp = self.run_commands(command, 'text') + resp = self.run_commands(command, 'text', send_enable) results.append(dict(command=command, result=resp[0], encoding='text')) @@ -627,7 +630,7 @@ def enable(self, commands, encoding='json', strict=False): raise return results - def run_commands(self, commands, encoding='json'): + def run_commands(self, commands, encoding='json', send_enable=True): """Sends the commands over the transport to the device This method sends the commands to the device using the nodes @@ -639,6 +642,8 @@ def run_commands(self, commands, encoding='json'): device using the transport encoding (str): The encoding method to use for the request and excpected response. + send_enable (bool): If True the enable command will be + prepended to the command list automatically. Returns: This method will return the raw response from the connection @@ -658,15 +663,17 @@ def run_commands(self, commands, encoding='json'): 'input': '%s\n' % (c.split('MULTILINE:')[1].strip())} if 'MULTILINE:' in c else c for c in commands] - if self._enablepwd: - commands.insert(0, {'cmd': 'enable', 'input': self._enablepwd}) - else: - commands.insert(0, 'enable') + if send_enable: + if self._enablepwd: + commands.insert(0, {'cmd': 'enable', 'input': self._enablepwd}) + else: + commands.insert(0, 'enable') response = self._connection.execute(commands, encoding) - # pop enable command from the response - response['result'].pop(0) + # pop enable command from the response only if we sent enable + if send_enable: + response['result'].pop(0) return response['result'] From 260936b8ecc2f6093c6c945298048bf06191ed3c Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Thu, 8 Sep 2016 00:08:01 -0400 Subject: [PATCH 18/23] test cases for new send_enable flag --- test/system/test_client.py | 43 ++++++++++++++++++++++++++++++++------ test/unit/test_client.py | 12 +++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/test/system/test_client.py b/test/system/test_client.py index 2230a1f..1f794ec 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -38,7 +38,7 @@ from testlib import random_int, random_string, get_fixture import pyeapi.client - +import pyeapi.eapilib class TestClient(unittest.TestCase): @@ -63,6 +63,18 @@ def test_enable_single_command(self): self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) + def test_no_enable_single_command(self): + for dut in self.duts: + result = dut.run_commands('show version', 'json', send_enable=False) + self.assertIsInstance(result, list, 'dut=%s' % dut) + self.assertEqual(len(result), 1, 'dut=%s' % dut) + + def test_no_enable_single_command_no_auth(self): + for dut in self.duts: + dut.run_commands('disable') + with self.assertRaises(pyeapi.eapilib.CommandError): + dut.run_commands('show running-config', 'json', send_enable=False) + def test_enable_multiple_commands(self): for dut in self.duts: commands = list() @@ -164,29 +176,48 @@ def test_exception_trace(self): % (1002, 'invalid command', 'Invalid input \(at token \d+: \'.*\'\)'))) # Send a command that cannot be run through the api + # note the command for reload looks to change in new EOS + # in 4.15 the reload now is replaced with 'force' if you are + # testing some DUT running older code and this test fails + # change the error message to the following: + # To reload the machine over the API, please use 'reload now' instead cases.append(('reload', rfmt % (1004, 'incompatible command', 'Command not permitted via API access. To reload ' - 'the machine over the API, please use \'reload ' - 'now\' instead.'))) + 'the machine over the API, please include the' + ' `force` keyword.'))) # Send a continuous command that requires a break cases.append(('watch 10 show int e1 count rates', rfmt % (1000, 'could not run command', 'init error \(cbreak\(\) returned ERR\)'))) + # Send a command that has insufficient priv + cases.append(('show running-config', rfmt + % (1002, 'invalid command', + 'Invalid input (privileged mode required)'))) + for dut in self.duts: for (cmd, regex) in cases: try: # Insert the error in list of valid commands - dut.enable(['show version', cmd, 'show hostname'], - strict=True) + if cmd != "show running-config": + dut.enable(['show version', cmd, 'show hostname'], + strict=True) + else: + dut.enable(['disable', 'show version', cmd], + strict=True, send_enable=False) + self.fail('A CommandError should have been raised') except pyeapi.eapilib.CommandError as exc: # Validate the properties of the exception - self.assertEqual(len(exc.trace), 4) + if cmd != 'show running-config': + self.assertEqual(len(exc.trace), 4) + else: + self.assertEqual(len(exc.trace), 3) self.assertIsNotNone(exc.command_error) self.assertIsNotNone(exc.output) self.assertIsNotNone(exc.commands) + print regex self.assertRegexpMatches(exc.message, regex) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index d19e658..57b1f3e 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -61,6 +61,18 @@ def test_enable_with_single_command(self): self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) + def test_no_enable_with_single_command(self): + command = random_string() + response = [command] + + self.connection.execute.return_value = {'result': list(response)} + result = self.node.enable(command, send_enable=False) + + self.connection.execute.assert_called_once_with(response, 'json') + self.assertEqual(command, result[0]['result']) + + + def test_enable_with_multiple_commands(self): commands = list() for i in range(0, random_int(2, 5)): From 44bd38e819219b496b5dc3ae7d7dd1128513dba0 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Thu, 8 Sep 2016 00:17:27 -0400 Subject: [PATCH 19/23] left a debug statement in --- test/system/test_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/system/test_client.py b/test/system/test_client.py index 1f794ec..d3d470d 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -217,7 +217,6 @@ def test_exception_trace(self): self.assertIsNotNone(exc.command_error) self.assertIsNotNone(exc.output) self.assertIsNotNone(exc.commands) - print regex self.assertRegexpMatches(exc.message, regex) From ae1582048e8c42685517dfa626f2561f246d3658 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Sep 2016 08:43:11 -0700 Subject: [PATCH 20/23] Update coverage report config --- .coveragerc | 3 +++ Makefile | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 8726c6c..deafaca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ branch = True omit = *mock* *test* *netaddr* + *site-packages* [report] # Regexes for lines to exclude from consideration @@ -23,6 +24,8 @@ exclude_lines = if 0: if __name__ == .__main__.: +show_missing = True + ignore_errors = True [html] diff --git a/Makefile b/Makefile index d5d39f9..7cbd456 100644 --- a/Makefile +++ b/Makefile @@ -68,4 +68,4 @@ systest: clean $(COVERAGE) run -m unittest discover test/system -v coverage_report: - $(COVERAGE) report -m + $(COVERAGE) report --rcfile=".coveragerc" From 17c1b7cb82ecfa04e70977aa2262397a570b40c7 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Sep 2016 14:07:41 -0700 Subject: [PATCH 21/23] fix systest case to accommodate EOS error text --- test/system/test_client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/system/test_client.py b/test/system/test_client.py index d3d470d..58da65d 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -183,9 +183,7 @@ def test_exception_trace(self): # To reload the machine over the API, please use 'reload now' instead cases.append(('reload', rfmt % (1004, 'incompatible command', - 'Command not permitted via API access. To reload ' - 'the machine over the API, please include the' - ' `force` keyword.'))) + 'Command not permitted via API access..*'))) # Send a continuous command that requires a break cases.append(('watch 10 show int e1 count rates', rfmt % (1000, 'could not run command', @@ -193,7 +191,7 @@ def test_exception_trace(self): # Send a command that has insufficient priv cases.append(('show running-config', rfmt % (1002, 'invalid command', - 'Invalid input (privileged mode required)'))) + 'Invalid input \(privileged mode required\)'))) for dut in self.duts: From 48cce5df8d8369899049c7fd2189f3a88cd02de1 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Sep 2016 14:54:21 -0700 Subject: [PATCH 22/23] Release 0.7.0 - Adds better platform support (Windows) - Adds enhancement to Node enable() method - Add OSPF API --- LICENSE | 2 +- VERSION | 2 +- docs/release-notes-0.7.0.rst | 27 +++++++++++++++++++++++++++ docs/release-notes.rst | 29 +++++++++++++++-------------- pyeapi/__init__.py | 2 +- 5 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 docs/release-notes-0.7.0.rst diff --git a/LICENSE b/LICENSE index 60a2b6a..7370f69 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014, Arista Networks EOS+ +Copyright (c) 2016, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/VERSION b/VERSION index 6563189..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +0.7.0 diff --git a/docs/release-notes-0.7.0.rst b/docs/release-notes-0.7.0.rst new file mode 100644 index 0000000..0b99459 --- /dev/null +++ b/docs/release-notes-0.7.0.rst @@ -0,0 +1,27 @@ +###### +v0.7.0 +###### + +2016-09-08 + +New Modules +^^^^^^^^^^^ + +* Add OSPF API (`95 `_) [`brigoldberg `_] + Big thanks for the community support! + +Enhancements +^^^^^^^^^^^^ + +* Enhance Node enable() method (`100 `_) [`dathelen `_] + This enhancement adds a send_enable flag to the enable and run_commands Node methods. By default the enable command will be sent, however you can now run commands without prepending the enable. +* Finish OSPF API (`99 `_) [`dathelen `_] + Create system tests and add unit tests to increase code coverage. +* Add Cross-Platform Support for pyeapi (`94 `_) [`grybak-arista `_] + Use logging instead of syslog for better cross-platform compatibility. This enhancement provides support for Windows users. + +Fixed +^^^^^ + +* Allow dot and hyphen in mlag domain-id (`91 `_) + Include handling any character in domain-id string, including dot, hyphen, and space diff --git a/docs/release-notes.rst b/docs/release-notes.rst index b406f3a..c9a806c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -6,18 +6,19 @@ Release Notes :maxdepth: 2 :titlesonly: - release-notes-0.1.0.rst - release-notes-0.1.1.rst - release-notes-0.2.0.rst - release-notes-0.2.1.rst - release-notes-0.2.2.rst - release-notes-0.2.3.rst - release-notes-0.2.4.rst - release-notes-0.3.0.rst - release-notes-0.3.1.rst - release-notes-0.3.2.rst - release-notes-0.3.3.rst - release-notes-0.4.0.rst - release-notes-0.5.0.rst - release-notes-0.6.0.rst + release-notes-0.7.0.rst release-notes-0.6.1.rst + release-notes-0.6.0.rst + release-notes-0.5.0.rst + release-notes-0.4.0.rst + release-notes-0.3.3.rst + release-notes-0.3.2.rst + release-notes-0.3.1.rst + release-notes-0.3.0.rst + release-notes-0.2.4.rst + release-notes-0.2.3.rst + release-notes-0.2.2.rst + release-notes-0.2.1.rst + release-notes-0.2.0.rst + release-notes-0.1.1.rst + release-notes-0.1.0.rst diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index e646e7e..2d0d25f 100644 --- a/pyeapi/__init__.py +++ b/pyeapi/__init__.py @@ -29,7 +29,7 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -__version__ = 'develop' +__version__ = '0.7.0' __author__ = 'Arista EOS+' From 4b689fe57c3f262074c8a9e96f9eebdbdca3846f Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Sep 2016 15:06:58 -0700 Subject: [PATCH 23/23] formatting change --- docs/release-notes-0.7.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes-0.7.0.rst b/docs/release-notes-0.7.0.rst index 0b99459..6970888 100644 --- a/docs/release-notes-0.7.0.rst +++ b/docs/release-notes-0.7.0.rst @@ -24,4 +24,4 @@ Fixed ^^^^^ * Allow dot and hyphen in mlag domain-id (`91 `_) - Include handling any character in domain-id string, including dot, hyphen, and space + Include handling any character in domain-id string, including dot, hyphen, and space.