From 1c9f8964778b49729a7f39b416b5f4393ff5d2eb Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 08:46:17 -0400 Subject: [PATCH 1/8] Add routemap support --- pyeapi/api/routemaps.py | 301 +++++++++++++++++++++++++ test/fixtures/running_config.routemaps | 11 + test/system/test_api_routemaps.py | 262 +++++++++++++++++++++ test/unit/test_api_routemaps.py | 113 ++++++++++ 4 files changed, 687 insertions(+) create mode 100644 pyeapi/api/routemaps.py create mode 100644 test/fixtures/running_config.routemaps create mode 100644 test/system/test_api_routemaps.py create mode 100644 test/unit/test_api_routemaps.py diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py new file mode 100644 index 0000000..5739a17 --- /dev/null +++ b/pyeapi/api/routemaps.py @@ -0,0 +1,301 @@ +# +# Copyright (c) 2015, 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. +# +"""Module for working with EOS MLAG resources + +The Mlag resource provides configuration management of global and interface +MLAG settings on an EOS node. + +Parameters: + + config (dict): The global MLAG configuration values + + interfaces (dict): The configured MLAG interfaces + +Config Parameters: + domain_id (str): The domain_id parameter is parsed from the nodes + mlag configuration. The domain id is an alphanumeric string that + names the MLAG domain + + local_interface (str): The local VLAN interface used as the control + plane endpoint between MLAG peers. Valid values include any + VLAN SVI + + peer_address (str): The IP address of the MLAG peer used to send MLAG + control traffic. The peer address must be reachable from the + local interface. Valid values include any IPv4 unicast address + + peer_link (str): The physical link that connects the node to its MLAG + peer. Valid values for the peer link include layer 2 Ethernet or + Port-Channel interfaces + + shutdown (bool): The administrative state of the global MLAG process. + +Interface Parameters: + mlag_id (str): The interface mlag parameter parsed from the nodes + interface configuration. Valid values for the mlag id are in + the range of 1 to 2000 + +""" + +import re + +from pyeapi.api import Entity + + +class Routemaps(Entity): + """The Routemaps class provides management of the routemaps configuration + + The Routemaps class is derived from Entity and provides an API for working + with the nodes routemaps configuraiton. + """ + + def get(self, name, action, seqno): + return self._get_instance(name, action, seqno) + + def getall(self): + resources = dict() + routemaps_re = re.compile(r'^route-map\s(\w+)\s(\w+)\s(\d+)$', re.M) + for entry in routemaps_re.findall(self.config): + name = entry[0] + action = entry[1] + seqno = int(entry[2]) + + routemap = self.get(name, action, seqno) + if routemap: + key = (name, action, seqno) + resources[key] = routemap + return resources + + def _get_instance(self, name, action, seqno): + routemap_re = r'route-map\s%s\s%s\s%s' % (name, action, seqno) + routemap = self.get_block(routemap_re) + + if not routemap: + return None + + resource = dict(name=name, action=action, seqno=int(seqno)) + resource.update(self._parse_match_statements(routemap)) + resource.update(self._parse_set_statements(routemap)) + resource.update(self._parse_continue_statement(routemap)) + return resource + + def _parse_match_statements(self, config): + match_re = re.compile(r'^\s+match\s(.*)$', re.M) + return dict(match=match_re.findall(config)) + + def _parse_set_statements(self, config): + set_re = re.compile(r'^\s+set\s(.*)$', re.M) + return dict(set=set_re.findall(config)) + + def _parse_continue_statement(self, config): + continue_re = re.compile(r'^\s+continue\s(\d+)$', re.M) + match = continue_re.search(config) + value = int(match.group(1)) if match else None + return {'continue': value} + + def create(self, name, action, seqno): + """Creates a new routemap on the node + + Note: + This method will attempt to create the routemap regardless + if the routemap exists or not. If the routemap already exists + then this method will still return True. + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + + Returns: + True if the routemap could be created otherwise False (see Note) + + """ + if action not in ['permit', 'deny']: + raise ValueError('action must be permit or deny') + if seqno < 1 or seqno > 16777215: + raise ValueError('seqno must be an integer between 1 and 16777215') + return self.configure('route-map %s %s %s' % (name, action, seqno)) + + def delete(self, name, action, seqno): + """Deletes the routemap from the node + + Note: + This method will attempt to delete the routemap from the nodes + operational config. If the routemap does not exist then this + method will not perform any changes but still return True + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + + Returns: + True if the routemap could be deleted otherwise False (see Node) + + """ + return self.configure('no route-map %s %s %s' % (name, action, seqno)) + + def default(self, name, action, seqno): + """Defaults the routemap on the node + + Note: + This method will attempt to default the routemap from the nodes + operational config. Since routemaps do not exist by default, + the default action is essentially a negation and the result will + be the removal of the routemap clause. + If the routemap does not exist then this + method will not perform any changes but still return True + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + + Returns: + True if the routemap could be deleted otherwise False (see Node) + + """ + return self.configure('default route-map %s %s %s' + % (name, action, seqno)) + + def set_match_statements(self, name, action, seqno, statements): + """Configures the match statements that are found within the + routemap clause. Match statements found in the routemap that + are not specified in the \'statements\' list will be removed. + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + statements (list): A list of the match-related statements. Note + that the statements should omit the leading + \'match\'. + + Returns: + True if the operation succeeds otherwise False + """ + try: + current_statements = self._get_instance(name, action, seqno)['match'] + except: + current_statements = [] + + commands = list() + + # remove set statements from current routemap + for entry in set(current_statements).difference(statements): + commands.append('route-map %s %s %s' % (name, action, seqno)) + commands.append('no match %s' % entry) + + # add new set statements to the routemap + for entry in set(statements).difference(current_statements): + commands.append('route-map %s %s %s' % (name, action, seqno)) + commands.append('match %s' % entry) + + return self.configure(commands) if commands else True + + def set_set_statements(self, name, action, seqno, statements): + """Configures the set statements that are found within the + routemap clause. Set statements found in the routemap that + are not specified in the \'statements\' list will be removed. + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + statements (list): A list of the set-related statements. Note that + the statements should omit the leading \'set\'. + + Returns: + True if the operation succeeds otherwise False + """ + try: + current_statements = self._get_instance(name, action, seqno)['set'] + except: + current_statements = [] + + commands = list() + + # remove set statements from current routemap + for entry in set(current_statements).difference(statements): + commands.append('route-map %s %s %s' % (name, action, seqno)) + commands.append('no set %s' % entry) + + # add new set statements to the routemap + for entry in set(statements).difference(current_statements): + commands.append('route-map %s %s %s' % (name, action, seqno)) + commands.append('set %s' % entry) + + return self.configure(commands) if commands else True + + def set_continue(self, name, action, seqno, value=None, default=False): + """Configures the routemap \'continue\' value + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + value (integer): The value to configure for the routemap continue + default (bool): Specifies to default the routemap continue value + + Returns: + True if the operation succeeds otherwise False is returned + """ + commands = ['route-map %s %s %s' % (name, action, seqno)] + if default: + commands.append('default continue') + elif value is not None: + if value < 1 or value > 16777215: + raise ValueError('seqno must be an integer between ' + '1 and 16777215') + commands.append('continue %s' % value) + else: + commands.append('no continue') + + return self.configure(commands) + +def instance(node): + """Returns an instance of Routemaps + + Args: + node (Node): The node argument passes an instance of Node to the + resource + + Returns: + object: An instance of Routemaps + """ + return Routemaps(node) diff --git a/test/fixtures/running_config.routemaps b/test/fixtures/running_config.routemaps new file mode 100644 index 0000000..8570136 --- /dev/null +++ b/test/fixtures/running_config.routemaps @@ -0,0 +1,11 @@ +route-map TEST permit 10 + set tag 50 + match interface Ethernet1 + continue 100 +! +route-map TEST permit 20 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py new file mode 100644 index 0000000..6239e0f --- /dev/null +++ b/test/system/test_api_routemaps.py @@ -0,0 +1,262 @@ +# +# Copyright (c) 2015, 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 systestlib import DutSystemTest + + +class TestApiRoutemaps(DutSystemTest): + + def test_get(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100', + 'match tag 50']) + response = dut.api('routemaps').get('TEST', 'deny', 10) + self.assertIsNotNone(response) + + def test_get_none(self): + for dut in self.duts: + dut.config('no route-map TEST deny 10') + result = dut.api('routemaps').get('TEST', 'deny', 10) + self.assertIsNone(result) + + def test_getall(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100', + 'no route-map TEST2 permit 50', + 'route-map TEST2 permit 50', + 'match tag 50']) + result = dut.api('routemaps').getall() + self.assertIn(('TEST', 'deny', 10), result) + self.assertIn(('TEST2', 'permit', 50), result) + + def test_create(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10']) + api = dut.api('routemaps') + self.assertIsNone(api.get('TEST', 'deny', 10)) + result = dut.api('routemaps').create('TEST', 'deny', 10) + self.assertTrue(result) + self.assertIsNotNone(api.get('TEST', 'deny', 10)) + dut.config(['no route-map TEST deny 10']) + + def test_create_with_hyphen(self): + for dut in self.duts: + dut.config(['no route-map TEST-1 deny 10']) + api = dut.api('routemaps') + self.assertIsNone(api.get('TEST-1', 'deny', 10)) + result = dut.api('routemaps').create('TEST-1', 'deny', 10) + self.assertTrue(result) + self.assertIsNotNone(api.get('TEST-1', 'deny', 10)) + dut.config(['no route-map TEST-1 deny 10']) + + def test_create_with_underscore(self): + for dut in self.duts: + dut.config(['no route-map TEST_1 deny 10']) + api = dut.api('routemaps') + self.assertIsNone(api.get('TEST_1', 'deny', 10)) + result = dut.api('routemaps').create('TEST_1', 'deny', 10) + self.assertTrue(result) + self.assertIsNotNone(api.get('TEST_1', 'deny', 10)) + dut.config(['no route-map TEST_1 deny 10']) + + def test_delete(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100']) + api = dut.api('routemaps') + self.assertIsNotNone(api.get('TEST', 'deny', 10)) + result = dut.api('routemaps').delete('TEST', 'deny', 10) + self.assertTrue(result) + self.assertIsNone(api.get('TEST', 'deny', 10)) + + def test_default(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100']) + api = dut.api('routemaps') + self.assertIsNotNone(api.get('TEST', 'deny', 10)) + result = dut.api('routemaps').default('TEST', 'deny', 10) + self.assertTrue(result) + self.assertIsNone(api.get('TEST', 'deny', 10)) + + def test_set_match_statements(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10']) + api = dut.api('routemaps') + self.assertNotIn('match as 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_match_statements('TEST', 'deny', + 10, ['as 100']) + self.assertTrue(result) + self.assertIn('match as 100', + api.get_block('route-map TEST deny 10')) + + def test_update_match_statement(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'match as 100']) + api = dut.api('routemaps') + self.assertIn('match as 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_match_statements('TEST', 'deny', + 10, ['as 200']) + self.assertTrue(result) + self.assertNotIn('match as 100', + api.get_block('route-map TEST deny 10')) + self.assertIn('match as 200', + api.get_block('route-map TEST deny 10')) + + def test_remove_match_statement(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'match as 100']) + api = dut.api('routemaps') + self.assertIn('match as 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_match_statements('TEST', 'deny', + 10, ['tag 50']) + self.assertTrue(result) + self.assertNotIn('match as 100', + api.get_block('route-map TEST deny 10')) + self.assertIn('match tag 50', + api.get_block('route-map TEST deny 10')) + + def test_set_set_statements(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10']) + api = dut.api('routemaps') + self.assertNotIn('set weight 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_set_statements('TEST', 'deny', + 10, ['weight 100']) + self.assertTrue(result) + self.assertIn('set weight 100', + api.get_block('route-map TEST deny 10')) + + def test_update_set_statement(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100']) + api = dut.api('routemaps') + self.assertIn('set weight 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_set_statements('TEST', 'deny', + 10, ['weight 200']) + self.assertTrue(result) + self.assertNotIn('set weight 100', + api.get_block('route-map TEST deny 10')) + self.assertIn('set weight 200', + api.get_block('route-map TEST deny 10')) + + def test_remove_set_statement(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'set weight 100']) + api = dut.api('routemaps') + self.assertIn('set weight 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_set_statements('TEST', 'deny', + 10, ['tag 50']) + self.assertTrue(result) + self.assertNotIn('set weight 100', + api.get_block('route-map TEST deny 10')) + self.assertIn('set tag 50', + api.get_block('route-map TEST deny 10')) + + def test_set_continue(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10']) + api = dut.api('routemaps') + self.assertNotIn('continue', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_continue('TEST', 'deny', 10, 100) + self.assertTrue(result) + self.assertEqual(100, api.get('TEST', 'deny', 10)['continue']) + + def test_update_continue(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'continue 30']) + api = dut.api('routemaps') + self.assertIn('continue 30', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_continue('TEST', 'deny', 10, 60) + self.assertTrue(result) + self.assertEqual(60, api.get('TEST', 'deny', 10)['continue']) + + def test_default_continue(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'continue 100']) + api = dut.api('routemaps') + self.assertIn('continue 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_continue('TEST', 'deny', 10, + default=True) + self.assertTrue(result) + self.assertEqual(None, api.get('TEST', 'deny', 10)['continue']) + + def test_negate_continue(self): + for dut in self.duts: + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10', + 'continue 100']) + api = dut.api('routemaps') + self.assertIn('continue 100', + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_continue('TEST', 'deny', 10, + value=None) + self.assertTrue(result) + self.assertEqual(None, api.get('TEST', 'deny', 10)['continue']) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py new file mode 100644 index 0000000..5561d8f --- /dev/null +++ b/test/unit/test_api_routemaps.py @@ -0,0 +1,113 @@ +# +# Copyright (c) 2015, 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 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.routemaps + +class TestApiRoutemaps(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiRoutemaps, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.routemaps.Routemaps(None) + self.config = open(get_fixture('running_config.routemaps')).read() + + def test_instance(self): + result = pyeapi.api.routemaps.instance(None) + self.assertIsInstance(result, pyeapi.api.routemaps.Routemaps) + + def test_get(self): + result = self.instance.get('TEST', 'permit', 10) + keys = ['name', 'action', 'seqno', 'set', 'match', 'continue'] + self.assertEqual(sorted(keys), sorted(result.keys())) + + def test_get_not_configured(self): + self.assertIsNone(self.instance.get('blah', 'blah', 1)) + + def test_getall(self): + result = self.instance.getall() + self.assertIsInstance(result, dict) + + def test_routemaps_functions(self): + for name in ['create', 'delete', 'default']: + if name == 'create': + cmds = 'route-map new permit 100' + elif name == 'delete': + cmds = 'no route-map new permit 100' + elif name == 'default': + cmds = 'default route-map new permit 100' + func = function(name, 'test') + self.eapi_positive_config_test(func, cmds) + + def test_set_set_statement_clean(self): + cmds = ['route-map new permit 100', 'set weight 100'] + func = function('set_set_statements', 'new', 'permit', 10, + ['weight 100']) + self.eapi_positive_config_test(func, cmds) + + def test_set_set_statement_remove_extraneous(self): + # Review fixtures/running_config.routemaps to see the default + # running-config that is the basis for this test + cmds = ['route-map TEST permit 10', 'no set tag 50', 'set weight 100'] + func = function('set_set_statements', 'TEST', 'permit', 10, + ['weight 100']) + self.eapi_positive_config_test(func, cmds) + + def test_set_match_statement_clean(self): + cmds = ['route-map new permit 200', 'match as 100'] + func = function('set_match_statements', 'new', 'permit', 200, + ['as 100']) + self.eapi_positive_config_test(func, cmds) + + def test_set_match_statement_remove_extraneous(self): + # Review fixtures/running_config.routemaps to see the default + # running-config that is the basis for this test + cmds = ['route-map TEST permit 10', 'no match interface Ethernet1', + 'match as 1000'] + func = function('set_set_statements', 'TEST', 'permit', 10, + ['as 1000']) + self.eapi_positive_config_test(func, cmds) + + def test_set_continue(self): + cmds = ['route-map TEST permit 10', 'continue 100'] + func = function('set_continue', 'TESt', 'permit', 10, 100) + self.eapi_positive_config_test(func, cmds) + + +if __name__ == '__main__': + unittest.main() From acaeca45b613e9e1046559c8eeb0f4504219a318 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 08:58:10 -0400 Subject: [PATCH 2/8] Update docstring --- pyeapi/api/routemaps.py | 55 +++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index 5739a17..26155d3 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -29,41 +29,26 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -"""Module for working with EOS MLAG resources - -The Mlag resource provides configuration management of global and interface -MLAG settings on an EOS node. - -Parameters: - - config (dict): The global MLAG configuration values - - interfaces (dict): The configured MLAG interfaces - -Config Parameters: - domain_id (str): The domain_id parameter is parsed from the nodes - mlag configuration. The domain id is an alphanumeric string that - names the MLAG domain - - local_interface (str): The local VLAN interface used as the control - plane endpoint between MLAG peers. Valid values include any - VLAN SVI - - peer_address (str): The IP address of the MLAG peer used to send MLAG - control traffic. The peer address must be reachable from the - local interface. Valid values include any IPv4 unicast address - - peer_link (str): The physical link that connects the node to its MLAG - peer. Valid values for the peer link include layer 2 Ethernet or - Port-Channel interfaces - - shutdown (bool): The administrative state of the global MLAG process. - -Interface Parameters: - mlag_id (str): The interface mlag parameter parsed from the nodes - interface configuration. Valid values for the mlag id are in - the range of 1 to 2000 - +"""Module for working with EOS routemap resources + +The Routemap resource provides configuration management of global route-map +resources on an EOS node. It provides the following class implementations: + + * Routemaps - Configures routemaps in EOS + +Routemaps Attributes: + name (string): The name given to the routemap clause + action (choices: permit/deny): How the clause will filter the route + seqno (integer): The sequence number of this clause + set (list): The list of set statements present in this clause + match (list): The list of match statements present in this clause. + continue (integer): The next sequence number to evaluate if the criteria + in this clause are met. + +Notes: + The \'set\' and \'match\' attributes produce a list of strings with The + corresponding configuration. These strings will omit the preceeding + \'set\' or \'match\' words, respectively. """ import re From acb340f4762a6949881b77d313c182adac93db84 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 09:08:13 -0400 Subject: [PATCH 3/8] Fix typo in unittest --- test/unit/test_api_routemaps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index 5561d8f..91fa23d 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -71,7 +71,7 @@ def test_routemaps_functions(self): cmds = 'no route-map new permit 100' elif name == 'default': cmds = 'default route-map new permit 100' - func = function(name, 'test') + func = function(name, 'new', 'permit', 100) self.eapi_positive_config_test(func, cmds) def test_set_set_statement_clean(self): @@ -99,13 +99,13 @@ def test_set_match_statement_remove_extraneous(self): # running-config that is the basis for this test cmds = ['route-map TEST permit 10', 'no match interface Ethernet1', 'match as 1000'] - func = function('set_set_statements', 'TEST', 'permit', 10, + func = function('set_match_statements', 'TEST', 'permit', 10, ['as 1000']) self.eapi_positive_config_test(func, cmds) def test_set_continue(self): cmds = ['route-map TEST permit 10', 'continue 100'] - func = function('set_continue', 'TESt', 'permit', 10, 100) + func = function('set_continue', 'TEST', 'permit', 10, 100) self.eapi_positive_config_test(func, cmds) From 638e30c7206637653a526e4c1052439f308f75cc Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 09:12:36 -0400 Subject: [PATCH 4/8] Fix typo in unittest --- test/unit/test_api_routemaps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index 91fa23d..b317879 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -76,14 +76,15 @@ def test_routemaps_functions(self): def test_set_set_statement_clean(self): cmds = ['route-map new permit 100', 'set weight 100'] - func = function('set_set_statements', 'new', 'permit', 10, + func = function('set_set_statements', 'new', 'permit', 100, ['weight 100']) self.eapi_positive_config_test(func, cmds) def test_set_set_statement_remove_extraneous(self): # Review fixtures/running_config.routemaps to see the default # running-config that is the basis for this test - cmds = ['route-map TEST permit 10', 'no set tag 50', 'set weight 100'] + cmds = ['route-map TEST permit 10', 'no set tag 50', + 'route-map TEST permit 10', 'set weight 100'] func = function('set_set_statements', 'TEST', 'permit', 10, ['weight 100']) self.eapi_positive_config_test(func, cmds) @@ -98,7 +99,7 @@ def test_set_match_statement_remove_extraneous(self): # Review fixtures/running_config.routemaps to see the default # running-config that is the basis for this test cmds = ['route-map TEST permit 10', 'no match interface Ethernet1', - 'match as 1000'] + 'route-map TEST permit 10', 'match as 1000'] func = function('set_match_statements', 'TEST', 'permit', 10, ['as 1000']) self.eapi_positive_config_test(func, cmds) From dbfba32fed61b8927ac9ffb3312daa6c52215a3a Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 12:50:33 -0400 Subject: [PATCH 5/8] Add support for description --- pyeapi/api/routemaps.py | 42 +++++++++++++++++++++++++++---- test/system/test_api_routemaps.py | 15 +++++++++++ test/unit/test_api_routemaps.py | 10 ++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index 26155d3..abfb81a 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -40,15 +40,16 @@ name (string): The name given to the routemap clause action (choices: permit/deny): How the clause will filter the route seqno (integer): The sequence number of this clause + description (string): A description for the routemap clause set (list): The list of set statements present in this clause match (list): The list of match statements present in this clause. continue (integer): The next sequence number to evaluate if the criteria in this clause are met. Notes: - The \'set\' and \'match\' attributes produce a list of strings with The + The set and match attributes produce a list of strings with The corresponding configuration. These strings will omit the preceeding - \'set\' or \'match\' words, respectively. + set or match words, respectively. """ import re @@ -91,14 +92,15 @@ def _get_instance(self, name, action, seqno): resource.update(self._parse_match_statements(routemap)) resource.update(self._parse_set_statements(routemap)) resource.update(self._parse_continue_statement(routemap)) + resource.update(self._parse_description(routemap)) return resource def _parse_match_statements(self, config): - match_re = re.compile(r'^\s+match\s(.*)$', re.M) + match_re = re.compile(r'^\s+match\s(.+)$', re.M) return dict(match=match_re.findall(config)) def _parse_set_statements(self, config): - set_re = re.compile(r'^\s+set\s(.*)$', re.M) + set_re = re.compile(r'^\s+set\s(.+)$', re.M) return dict(set=set_re.findall(config)) def _parse_continue_statement(self, config): @@ -107,6 +109,12 @@ def _parse_continue_statement(self, config): value = int(match.group(1)) if match else None return {'continue': value} + def _parse_description(self, config): + desc_re = re.compile(r'^\s+description\s\'(.+)\'$', re.M) + match = desc_re.search(config) + value = match.group(1) if match else None + return dict(description=value) + def create(self, name, action, seqno): """Creates a new routemap on the node @@ -247,7 +255,7 @@ def set_set_statements(self, name, action, seqno, statements): return self.configure(commands) if commands else True def set_continue(self, name, action, seqno, value=None, default=False): - """Configures the routemap \'continue\' value + """Configures the routemap continue value Args: name (string): The full name of the routemap. @@ -273,6 +281,30 @@ def set_continue(self, name, action, seqno, value=None, default=False): return self.configure(commands) + def set_description(self, name, action, seqno, value=None, default=False): + """Configures the routemap description + + Args: + name (string): The full name of the routemap. + action (choices: permit,deny): The action to take for this routemap + clause. + seqno (integer): The sequence number for the routemap clause. + value (string): The value to configure for the routemap description + default (bool): Specifies to default the routemap continue value + + Returns: + True if the operation succeeds otherwise False is returned + """ + commands = ['route-map %s %s %s' % (name, action, seqno)] + if default: + commands.append('default description') + elif value is not None: + commands.append('description %s' % value) + else: + commands.append('no description') + + return self.configure(commands) + def instance(node): """Returns an instance of Routemaps diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py index 6239e0f..10e1677 100644 --- a/test/system/test_api_routemaps.py +++ b/test/system/test_api_routemaps.py @@ -36,6 +36,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest +from testlib import random_string class TestApiRoutemaps(DutSystemTest): @@ -119,6 +120,20 @@ def test_default(self): self.assertTrue(result) self.assertIsNone(api.get('TEST', 'deny', 10)) + def test_set_description(self): + for dut in self.duts: + text = random_string() + dut.config(['no route-map TEST deny 10', + 'route-map TEST deny 10']) + api = dut.api('routemaps') + self.assertNotIn('description %s' % text, + api.get_block('route-map TEST deny 10')) + result = dut.api('routemaps').set_description('TEST', 'deny', 10, + text) + self.assertTrue(result) + self.assertIn('description %s' % text, + api.get_block('route-map TEST deny 10')) + def test_set_match_statements(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index b317879..3c9d7a0 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -35,7 +35,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) -from testlib import get_fixture, function +from testlib import get_fixture, function, random_string from testlib import EapiConfigUnitTest import pyeapi.api.routemaps @@ -53,7 +53,8 @@ def test_instance(self): def test_get(self): result = self.instance.get('TEST', 'permit', 10) - keys = ['name', 'action', 'seqno', 'set', 'match', 'continue'] + keys = ['name', 'action', 'seqno', 'set', 'match', 'continue', + 'description'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_get_not_configured(self): @@ -109,6 +110,11 @@ def test_set_continue(self): func = function('set_continue', 'TEST', 'permit', 10, 100) self.eapi_positive_config_test(func, cmds) + def test_set_description_with_value(self): + value = random_string() + cmds = ['route-map TEST permit 10', 'description %s' % value] + func = function('set_description', 'TEST', 'permit', 10, value) + self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() From bdf275fe8a2ca030579a5d1eb7fe75ec0e9364a3 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 12:57:01 -0400 Subject: [PATCH 6/8] Modify regex --- pyeapi/api/routemaps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index abfb81a..dd009e6 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -110,7 +110,7 @@ def _parse_continue_statement(self, config): return {'continue': value} def _parse_description(self, config): - desc_re = re.compile(r'^\s+description\s\'(.+)\'$', re.M) + desc_re = re.compile(r'^\s+description\s(.+)$', re.M) match = desc_re.search(config) value = match.group(1) if match else None return dict(description=value) From 77bb81a69acaf626fc201058f07f42a36825a58e Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 13:46:30 -0400 Subject: [PATCH 7/8] Update description logic to remove old entries --- pyeapi/api/routemaps.py | 1 + test/unit/test_api_routemaps.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index dd009e6..fe064cc 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -299,6 +299,7 @@ def set_description(self, name, action, seqno, value=None, default=False): if default: commands.append('default description') elif value is not None: + commands.append('no description') commands.append('description %s' % value) else: commands.append('no description') diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index 3c9d7a0..25b1aad 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -112,7 +112,8 @@ def test_set_continue(self): def test_set_description_with_value(self): value = random_string() - cmds = ['route-map TEST permit 10', 'description %s' % value] + cmds = ['route-map TEST permit 10', 'no description', + 'description %s' % value] func = function('set_description', 'TEST', 'permit', 10, value) self.eapi_positive_config_test(func, cmds) From 1e2e6af226d7d8be0350de9a169955c977a38a37 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 13 Oct 2015 12:57:26 -0400 Subject: [PATCH 8/8] Remove value checks --- pyeapi/api/routemaps.py | 53 ++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index fe064cc..e41216f 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -38,7 +38,8 @@ Routemaps Attributes: name (string): The name given to the routemap clause - action (choices: permit/deny): How the clause will filter the route + action (string): How the clause will filter the route. Typically + permit or deny. seqno (integer): The sequence number of this clause description (string): A description for the routemap clause set (list): The list of set statements present in this clause @@ -125,18 +126,13 @@ def create(self, name, action, seqno): Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: True if the routemap could be created otherwise False (see Note) """ - if action not in ['permit', 'deny']: - raise ValueError('action must be permit or deny') - if seqno < 1 or seqno > 16777215: - raise ValueError('seqno must be an integer between 1 and 16777215') return self.configure('route-map %s %s %s' % (name, action, seqno)) def delete(self, name, action, seqno): @@ -149,8 +145,7 @@ def delete(self, name, action, seqno): Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: @@ -172,8 +167,7 @@ def default(self, name, action, seqno): Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: @@ -184,18 +178,19 @@ def default(self, name, action, seqno): % (name, action, seqno)) def set_match_statements(self, name, action, seqno, statements): - """Configures the match statements that are found within the - routemap clause. Match statements found in the routemap that - are not specified in the \'statements\' list will be removed. + """Configures the match statements within the routemap clause. + The final configuration of match statements will reflect the list + of statements passed into the statements attribute. This implies + match statements found in the routemap that are not specified in the + statements attribute will be removed. Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. statements (list): A list of the match-related statements. Note that the statements should omit the leading - \'match\'. + match. Returns: True if the operation succeeds otherwise False @@ -220,17 +215,18 @@ def set_match_statements(self, name, action, seqno, statements): return self.configure(commands) if commands else True def set_set_statements(self, name, action, seqno, statements): - """Configures the set statements that are found within the - routemap clause. Set statements found in the routemap that - are not specified in the \'statements\' list will be removed. + """Configures the set statements within the routemap clause. + The final configuration of set statements will reflect the list + of statements passed into the statements attribute. This implies + set statements found in the routemap that are not specified in the + statements attribute will be removed. Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. statements (list): A list of the set-related statements. Note that - the statements should omit the leading \'set\'. + the statements should omit the leading set. Returns: True if the operation succeeds otherwise False @@ -259,8 +255,7 @@ def set_continue(self, name, action, seqno, value=None, default=False): Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. value (integer): The value to configure for the routemap continue default (bool): Specifies to default the routemap continue value @@ -272,9 +267,8 @@ def set_continue(self, name, action, seqno, value=None, default=False): if default: commands.append('default continue') elif value is not None: - if value < 1 or value > 16777215: - raise ValueError('seqno must be an integer between ' - '1 and 16777215') + if value < 1: + raise ValueError('seqno must be a positive integer') commands.append('continue %s' % value) else: commands.append('no continue') @@ -286,8 +280,7 @@ def set_description(self, name, action, seqno, value=None, default=False): Args: name (string): The full name of the routemap. - action (choices: permit,deny): The action to take for this routemap - clause. + action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. value (string): The value to configure for the routemap description default (bool): Specifies to default the routemap continue value