From a1b7241d489527fd69d4fb5135f881ebd4dbb970 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 31 Jul 2015 13:25:43 -0400 Subject: [PATCH 01/83] version bump --- VERSION | 2 +- pyeapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 1c09c74..6563189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +develop diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index f71ea74..e646e7e 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__ = '0.3.3' +__version__ = 'develop' __author__ = 'Arista EOS+' From 417021f45d1661c83f52d85ccbfca202d48861d2 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 10 Aug 2015 16:11:12 -0400 Subject: [PATCH 02/83] Fix #22 - Change users_re regex to look for sha512 in the username string --- pyeapi/api/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index ae9e67f..f7dbf3f 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -80,7 +80,7 @@ class Users(EntityCollection): users_re = re.compile(r'username ([^\s]+) privilege (\d+)' r'(?: role ([^\s]+))?' r'(?: (nopassword))?' - r'(?: secret ([0,5,7]) (.+))?', re.M) + r'(?: secret (0|5|7|sha512) (.+))?', re.M) def get(self, name): """Returns the local user configuration as a resource dict From db1a458242b05b35e25eb8762c836040a887acf3 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 13 Aug 2015 17:00:36 -0400 Subject: [PATCH 03/83] Fix peer-link parsing regex - Fix #24 - Add testcases to set port-channel for peer-link - Change stp test to not use et1-7, just et1-4 --- pyeapi/api/mlag.py | 3 +-- test/system/test_api_mlag.py | 9 +++++++++ test/system/test_api_stp.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/mlag.py b/pyeapi/api/mlag.py index 71a92ff..775821b 100644 --- a/pyeapi/api/mlag.py +++ b/pyeapi/api/mlag.py @@ -158,7 +158,7 @@ def _parse_peer_link(self, config): dict: A dict object that is intended to be merged into the resource dict """ - match = re.search(r'peer-link (\w+)', config) + match = re.search(r'peer-link (\S+)', config) value = match.group(1) if match else None return dict(peer_link=value) @@ -289,4 +289,3 @@ def instance(node): object: An instance of Mlag """ return Mlag(node) - diff --git a/test/system/test_api_mlag.py b/test/system/test_api_mlag.py index b77b65a..80775e4 100644 --- a/test/system/test_api_mlag.py +++ b/test/system/test_api_mlag.py @@ -142,6 +142,15 @@ def test_set_peer_link_with_value(self): self.assertTrue(result) self.assertIn('peer-link Ethernet1', api.get_block('mlag configuration')) + def test_set_peer_link_with_value_portchannel(self): + for dut in self.duts: + dut.config(['default mlag configuration','interface Port-Channel5']) + api = dut.api('mlag') + self.assertIn('no peer-link', api.get_block('mlag configuration')) + result = dut.api('mlag').set_peer_link('Port-Channel5') + self.assertTrue(result) + self.assertIn('peer-link Port-Channel5', api.get_block('mlag configuration')) + def test_set_peer_link_with_no_value(self): for dut in self.duts: dut.config(['mlag configuration', 'peer-link Ethernet1']) diff --git a/test/system/test_api_stp.py b/test/system/test_api_stp.py index 273d1d1..5837072 100644 --- a/test/system/test_api_stp.py +++ b/test/system/test_api_stp.py @@ -49,7 +49,7 @@ def test_get(self): def test_getall(self): for dut in self.duts: - dut.config('default interface Et1-7') + dut.config('default interface Et1-4') result = dut.api('stp').interfaces.getall() self.assertIsInstance(result, dict) From 12fab199aa98e24a755ac32806fea448d53553b5 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 26 Aug 2015 13:50:43 -0400 Subject: [PATCH 04/83] Fix #28 - Change regex to ignore lag members that start with PeerEthernet - Add missing systest to check for this --- pyeapi/api/interfaces.py | 7 ++----- test/system/test_api_interfaces.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index dc39533..ac33d90 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -587,7 +587,8 @@ def get_members(self, name): grpid = re.search(r'(\d+)', name).group() command = 'show port-channel %s all-ports' % grpid config = self.node.enable(command, 'text') - return re.findall(r'Ethernet[\d/]*', config[0]['result']['output']) + return re.findall(r'\b(?!Peer)Ethernet[\d/]*\b', + config[0]['result']['output']) def set_members(self, name, members): """Configures the array of member interfaces for the Port-Channel @@ -917,7 +918,3 @@ def remove_vlan(self, name, vid): def instance(api): return Interfaces(api) - - - - diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 3e293cf..337e760 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -163,6 +163,28 @@ def test_get_members_default(self): result = instance.get_members('Port-Channel1') self.assertEqual(result, list(), 'dut=%s' % dut) + def test_get_members_one_member(self): + for dut in self.duts: + dut.config(['no interface Port-Channel1', + 'interface Port-Channel1', + 'default interface Ethernet1', + 'interface Ethernet1', + 'channel-group 1 mode active']) + instance = dut.api('interfaces').get_instance('Port-Channel1') + result = instance.get_members('Port-Channel1') + self.assertEqual(result, ['Ethernet1'], 'dut=%s' % dut) + + def test_get_members_two_members(self): + for dut in self.duts: + dut.config(['no interface Port-Channel1', + 'interface Port-Channel1', + 'default interface Ethernet1-2', + 'interface Ethernet1-2', + 'channel-group 1 mode active']) + instance = dut.api('interfaces').get_instance('Port-Channel1') + result = instance.get_members('Port-Channel1') + self.assertEqual(result, ['Ethernet1', 'Ethernet2'], 'dut=%s' % dut) + def test_set_lacp_mode(self): for dut in self.duts: for mode in ['on', 'active', 'passive']: From 4214e68f1be9ab7a1a896def40c858b2bf72b783 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 4 Sep 2015 17:34:23 -0400 Subject: [PATCH 05/83] Add sshkey support to users API - Close #31 - Adding support for sshkeys - Fix #32 - set_privilege tests - Fix #33 - set_role test tests --- pyeapi/api/users.py | 36 ++++++++++++++++---- test/system/test_api_users.py | 64 +++++++++++++++++++++++++++++++---- test/unit/test_api_users.py | 9 +---- 3 files changed, 87 insertions(+), 22 deletions(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index f7dbf3f..1f4a7da 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -74,13 +74,18 @@ def isprivilege(value): class Users(EntityCollection): - """The Users class provides a configuration resource for local users + """The Users class provides a configuration resource for local users. + The regex used here parses the running configuration to find username + entries. There is extra logic in the regular expression to store + the username as 'user' and then creates a backreference to find a + following configuration line that might contain the users sshkey. """ - users_re = re.compile(r'username ([^\s]+) privilege (\d+)' + users_re = re.compile(r'username (?P[^\s]+) privilege (\d+)' r'(?: role ([^\s]+))?' r'(?: (nopassword))?' - r'(?: secret (0|5|7|sha512) (.+))?', re.M) + r'(?: secret (0|5|7|sha512) (.+))?' + r'.*$\n(?:username (?P=user) sshkey (.+)$)?', re.M) def get(self, name): """Returns the local user configuration as a resource dict @@ -122,13 +127,14 @@ def _parse_username(self, config): dict: A resource dict that is intended to be merged into the user resource """ - (username, priv, role, nopass, fmt, secret) = config + (username, priv, role, nopass, fmt, secret, sshkey) = config resource = dict() resource['privilege'] = priv resource['role'] = role resource['nopassword'] = nopass == 'nopassword' resource['format'] = fmt resource['secret'] = secret + resource['sshkey'] = sshkey return {username: resource} def create(self, name, nopassword=None, secret=None, encryption=None): @@ -243,7 +249,7 @@ def set_privilege(self, name, value=None): raise TypeError('priviledge value must be between 0 and 15') cmd += ' privilege %s' % value else: - cmd += ' no privilege' + cmd += ' privilege 1' return self.configure(cmd) def set_role(self, name, value=None): @@ -261,9 +267,26 @@ def set_role(self, name, value=None): if value is not None: cmd += ' role %s' % value else: - cmd += ' no role' + cmd = 'default username %s role' % name return self.configure(cmd) + def set_sshkey(self, name, value=None): + """Configures the user sshkey + + Args: + name (str): The name of the user to add the sshkey to + + value (str): The value to configure for the sshkey. + + Returns: + True if the operation was successful otherwise False + """ + cmd = 'username %s' % name + if value is not None: + cmd += ' sshkey %s' % value + else: + cmd = 'no username %s sshkey' % name + return self.configure(cmd) def instance(node): """Returns an instance of Users @@ -277,4 +300,3 @@ def instance(node): resource """ return Users(node) - diff --git a/test/system/test_api_users.py b/test/system/test_api_users.py index c1db5e5..49c3e49 100644 --- a/test/system/test_api_users.py +++ b/test/system/test_api_users.py @@ -37,15 +37,25 @@ from systestlib import DutSystemTest +TEST_SSH_KEY = ('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKL1UtBALa4CvFUsHUipNym' + 'A04qCXuAtTwNcMj84bTUzUI+q7mdzRCTLkllXeVxKuBnaTm2PW7W67K5CVpl0' + 'EVCm6IY7FS7kc4nlnD/tFvTvShy/fzYQRAdM7ZfVtegW8sMSFJzBR/T/Y/sxI' + '16Y/dQb8fC3la9T25XOrzsFrQiKRZmJGwg8d+0RLxpfMg0s/9ATwQKp6tPoLE' + '4f3dKlAgSk5eENyVLA3RsypWADHpenHPcB7sa8D38e1TS+n+EUyAdb3Yov+5E' + 'SAbgLIJLd52Xv+FyYi0c2L49ByBjcRrupp4zfXn4DNRnEG4K6GcmswHuMEGZv' + '5vjJ9OYaaaaaaa') class TestApiUsers(DutSystemTest): def test_get(self): for dut in self.duts: - dut.config(['no username test', 'username test nopassword']) + dut.config(['no username test', 'username test nopassword', + 'username test sshkey %s' % TEST_SSH_KEY]) + result = dut.api('users').get('test') values = dict(nopassword=True, privilege='1', secret='', - role='', format='') + role='', format='', + sshkey=TEST_SSH_KEY) result = self.sort_dict_by_keys(result) values = self.sort_dict_by_keys(values) @@ -85,16 +95,17 @@ def test_default(self): self.assertTrue(result) self.assertNotIn('username test nopassword', api.config) - def set_privilege_with_value(self): + def test_set_privilege_with_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') - self.assertIn('username test nopassword', api.config) + # EOS defaults to privilege 1 + self.assertIn('username test privilege 1 nopassword', api.config) result = api.set_privilege('test', 8) self.assertTrue(result) - self.assertNotIn('username test privilege 8', api.config) + self.assertIn('username test privilege 8 nopassword', api.config) - def set_privilege_with_no_value(self): + def test_set_privilege_with_no_value(self): for dut in self.duts: dut.config(['no username test', 'username test privilege 8 nopassword']) @@ -102,8 +113,47 @@ def set_privilege_with_no_value(self): self.assertIn('username test privilege 8', api.config) result = api.set_privilege('test') self.assertTrue(result) - self.assertNotIn('username test privilege 1', api.config) + self.assertIn('username test privilege 1', api.config) + + def test_set_role_with_value(self): + for dut in self.duts: + dut.config(['no username test', 'username test nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 nopassword', api.config) + result = api.set_role('test', 'network-admin') + self.assertTrue(result) + self.assertIn('username test privilege 1 role network-admin nopassword', api.config) + + def test_set_role_with_no_value(self): + for dut in self.duts: + dut.config(['no username test', + 'username test role network-admin nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 role network-admin nopassword', api.config) + result = api.set_role('test') + self.assertTrue(result) + self.assertNotIn('username test privilege 1 role network-admin nopassword', api.config) + def test_set_sshkey_with_value(self): + for dut in self.duts: + dut.config(['no username test', 'username test nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 nopassword', api.config) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', TEST_SSH_KEY) + self.assertTrue(result) + self.assertIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + + def test_set_sshkey_with_no_value(self): + for dut in self.duts: + dut.config(['no username test', + 'username test nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 nopassword', api.config) + result = api.set_sshkey('test') + self.assertTrue(result) + self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, + api.config) if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_users.py b/test/unit/test_api_users.py index 00898d0..573aa82 100644 --- a/test/unit/test_api_users.py +++ b/test/unit/test_api_users.py @@ -52,7 +52,7 @@ def test_isprivilege_returns_false(self): self.assertFalse(result) def test_get(self): - keys = ['nopassword', 'privilege', 'role', 'secret', 'format'] + keys = ['nopassword', 'privilege', 'role', 'secret', 'format', 'sshkey'] result = self.instance.get('test') self.assertEqual(sorted(keys), sorted(result.keys())) @@ -123,12 +123,5 @@ def test_set_role_negate(self): self.eapi_positive_config_test(func, cmds) - - - - - if __name__ == '__main__': unittest.main() - - From d75d5b8f3d70c72981ddd96040b01d03051b96d2 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 4 Sep 2015 18:03:49 -0400 Subject: [PATCH 06/83] Fix users unit test --- test/unit/test_api_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_api_users.py b/test/unit/test_api_users.py index 573aa82..08278aa 100644 --- a/test/unit/test_api_users.py +++ b/test/unit/test_api_users.py @@ -104,7 +104,7 @@ def test_set_privilege(self): self.eapi_positive_config_test(func, cmds) def test_set_privilege_negate(self): - cmds = 'username test no privilege' + cmds = 'username test privilege 1' func = function('set_privilege', 'test') self.eapi_positive_config_test(func, cmds) @@ -118,7 +118,7 @@ def test_set_role(self): self.eapi_positive_config_test(func, cmds) def test_set_role_negate(self): - cmds = 'username test no role' + cmds = 'default username test role' func = function('set_role', 'test') self.eapi_positive_config_test(func, cmds) From cc750286b26156805964e350c87a101e3a6dc47f Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 8 Sep 2015 09:55:31 -0400 Subject: [PATCH 07/83] Add tests for empty string and None --- pyeapi/api/users.py | 2 +- test/system/test_api_users.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index 1f4a7da..2772e2c 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -282,7 +282,7 @@ def set_sshkey(self, name, value=None): True if the operation was successful otherwise False """ cmd = 'username %s' % name - if value is not None: + if value: cmd += ' sshkey %s' % value else: cmd = 'no username %s sshkey' % name diff --git a/test/system/test_api_users.py b/test/system/test_api_users.py index 49c3e49..de46e75 100644 --- a/test/system/test_api_users.py +++ b/test/system/test_api_users.py @@ -144,6 +144,26 @@ def test_set_sshkey_with_value(self): self.assertTrue(result) self.assertIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + def test_set_sshkey_with_empty_string(self): + for dut in self.duts: + dut.config(['no username test', 'username test nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 nopassword', api.config) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', '') + self.assertTrue(result) + self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + + def test_set_sshkey_with_None(self): + for dut in self.duts: + dut.config(['no username test', 'username test nopassword']) + api = dut.api('users') + self.assertIn('username test privilege 1 nopassword', api.config) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', None) + self.assertTrue(result) + self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + def test_set_sshkey_with_no_value(self): for dut in self.duts: dut.config(['no username test', From 880f0fd42dff60aeeb52a473a08df7621034c8e2 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 16 Sep 2015 12:28:58 -0400 Subject: [PATCH 08/83] Add BGP maximum-paths support --- pyeapi/api/bgp.py | 19 +++++++++++++++++++ test/unit/test_api_bgp.py | 17 ++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 975e18e..c8396b5 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -69,6 +69,7 @@ def get(self): response = dict() response.update(self._parse_bgp_as(config)) response.update(self._parse_router_id(config)) + response.update(self._parse_max_paths(config)) response.update(self._parse_shutdown(config)) response.update(self._parse_networks(config)) @@ -85,6 +86,12 @@ def _parse_router_id(self, config): value = match.group(1) if match else None return dict(router_id=value) + def _parse_max_paths(self, config): + match = re.search(r'/maximum-paths\s+(\d+)\s+ecmp\s+(\d+)/', config) + paths = int(match.group(1)) if match else None + ecmp_paths = int(match.group(2)) if match else None + return dict(maximum_paths=paths, maximum_ecmp_paths=ecmp_paths) + def _parse_shutdown(self, config): value = 'no shutdown' in config return dict(shutdown=not value) @@ -129,6 +136,18 @@ def set_router_id(self, value=None, default=False): cmd = self.command_builder('router-id', value=value, default=default) return self.configure_bgp(cmd) + def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False): + # You cannot configure max_ecmp_path without max_paths + if default: + cmd = 'default maximum-paths' + elif max_path: + cmd = 'maximum-paths {}'.format(max_path) + if max_ecmp_path: + cmd += ' ecmp {}'.format(max_ecmp_path) + else: + cmd = 'default maximum-paths' + return self.configure_bgp(cmd) + def set_shutdown(self, value=None, default=False): cmd = self.command_builder('shutdown', value=value, default=default) return self.configure_bgp(cmd) diff --git a/test/unit/test_api_bgp.py b/test/unit/test_api_bgp.py index 4faef7b..390f8d3 100644 --- a/test/unit/test_api_bgp.py +++ b/test/unit/test_api_bgp.py @@ -49,7 +49,8 @@ def __init__(self, *args, **kwargs): def test_get(self): result = self.instance.get() - keys = ['bgp_as', 'router_id', 'shutdown', 'neighbors', 'networks'] + keys = ['bgp_as', 'router_id', 'maximum_paths', 'maximum_ecmp_paths', + 'shutdown', 'neighbors', 'networks'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_create(self): @@ -97,6 +98,18 @@ def test_set_router_id(self): func = function('set_router_id', rid, True) self.eapi_positive_config_test(func, cmds) + def test_maximum_paths(self): + for state in ['config', 'default']: + max_paths = 20 + max_ecmp_path = 20 + if state == 'config': + cmds = ['router bgp 65000'] + func = function('set_maximum_paths', max_paths, max_ecmp_path) + elif state == 'default': + cmds = ['router bgp 65000'] + func = function('set_maximum_paths', default=True) + self.eapi_positive_config_test(func, cmds) + def test_set_shutdown(self): for state in ['config', 'negate', 'default']: if state == 'config': @@ -262,5 +275,3 @@ def test_set_description(self): if __name__ == '__main__': unittest.main() - - From d90fc67bd2c23358cd1fed3404195b515fb3ad26 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 16 Sep 2015 12:34:19 -0400 Subject: [PATCH 09/83] Fix unit test --- test/unit/test_api_bgp.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/unit/test_api_bgp.py b/test/unit/test_api_bgp.py index 390f8d3..db652df 100644 --- a/test/unit/test_api_bgp.py +++ b/test/unit/test_api_bgp.py @@ -98,15 +98,26 @@ def test_set_router_id(self): func = function('set_router_id', rid, True) self.eapi_positive_config_test(func, cmds) - def test_maximum_paths(self): + def test_maximum_paths_just_max_path(self): + for state in ['config', 'default']: + max_paths = 20 + if state == 'config': + cmds = ['router bgp 65000', 'maximum-paths 20'] + func = function('set_maximum_paths', max_paths) + elif state == 'default': + cmds = ['router bgp 65000', 'default maximum-paths'] + func = function('set_maximum_paths', default=True) + self.eapi_positive_config_test(func, cmds) + + def test_maximum_paths_max_path_and_ecmp(self): for state in ['config', 'default']: max_paths = 20 max_ecmp_path = 20 if state == 'config': - cmds = ['router bgp 65000'] + cmds = ['router bgp 65000', 'maximum-paths 20 ecmp 20'] func = function('set_maximum_paths', max_paths, max_ecmp_path) elif state == 'default': - cmds = ['router bgp 65000'] + cmds = ['router bgp 65000', 'default maximum-paths'] func = function('set_maximum_paths', default=True) self.eapi_positive_config_test(func, cmds) From 10ebb1425b116e9461970ee7d3a2fbef056df0a2 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 16 Sep 2015 13:08:36 -0400 Subject: [PATCH 10/83] Fix variable type --- pyeapi/api/bgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index c8396b5..4c6f050 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -87,7 +87,7 @@ def _parse_router_id(self, config): return dict(router_id=value) def _parse_max_paths(self, config): - match = re.search(r'/maximum-paths\s+(\d+)\s+ecmp\s+(\d+)/', config) + match = re.search(r'maximum-paths\s+(\d+)\s+ecmp\s+(\d+)', config) paths = int(match.group(1)) if match else None ecmp_paths = int(match.group(2)) if match else None return dict(maximum_paths=paths, maximum_ecmp_paths=ecmp_paths) From 535de13e9100cef0c67b02f3fe4e29c7d35f53bc Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 16 Sep 2015 14:30:25 -0400 Subject: [PATCH 11/83] Add check for missing argument --- pyeapi/api/bgp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 4c6f050..a602e18 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -138,6 +138,9 @@ def set_router_id(self, value=None, default=False): def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False): # You cannot configure max_ecmp_path without max_paths + if not max_path and max_ecmp_path: + raise TypeError('Cannot use maximum_ecmp_paths without' + 'providing max_path') if default: cmd = 'default maximum-paths' elif max_path: @@ -145,7 +148,7 @@ def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False): if max_ecmp_path: cmd += ' ecmp {}'.format(max_ecmp_path) else: - cmd = 'default maximum-paths' + cmd = 'no maximum-paths' return self.configure_bgp(cmd) def set_shutdown(self, value=None, default=False): From fe4fe207b232ee8f14216751f2285ffece8efd7f Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 16 Sep 2015 14:33:17 -0400 Subject: [PATCH 12/83] Add negate case to unit test --- test/unit/test_api_bgp.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/unit/test_api_bgp.py b/test/unit/test_api_bgp.py index db652df..7ff65a6 100644 --- a/test/unit/test_api_bgp.py +++ b/test/unit/test_api_bgp.py @@ -99,23 +99,29 @@ def test_set_router_id(self): self.eapi_positive_config_test(func, cmds) def test_maximum_paths_just_max_path(self): - for state in ['config', 'default']: + for state in ['config', 'negate', 'default']: max_paths = 20 if state == 'config': cmds = ['router bgp 65000', 'maximum-paths 20'] func = function('set_maximum_paths', max_paths) + elif state == 'negate': + cmds = ['router bgp 65000', 'no maximum-paths'] + func = function('set_maximum_paths') elif state == 'default': cmds = ['router bgp 65000', 'default maximum-paths'] func = function('set_maximum_paths', default=True) self.eapi_positive_config_test(func, cmds) def test_maximum_paths_max_path_and_ecmp(self): - for state in ['config', 'default']: + for state in ['config', 'negate', 'default']: max_paths = 20 max_ecmp_path = 20 if state == 'config': cmds = ['router bgp 65000', 'maximum-paths 20 ecmp 20'] func = function('set_maximum_paths', max_paths, max_ecmp_path) + elif state == 'negate': + cmds = ['router bgp 65000', 'no maximum-paths'] + func = function('set_maximum_paths') elif state == 'default': cmds = ['router bgp 65000', 'default maximum-paths'] func = function('set_maximum_paths', default=True) From ca518324274f208652870d59b966ea65326cc8cf Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 18 Sep 2015 00:46:43 -0400 Subject: [PATCH 13/83] Fix docstring --- pyeapi/api/bgp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index a602e18..6da7f7a 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -137,9 +137,8 @@ def set_router_id(self, value=None, default=False): return self.configure_bgp(cmd) def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False): - # You cannot configure max_ecmp_path without max_paths if not max_path and max_ecmp_path: - raise TypeError('Cannot use maximum_ecmp_paths without' + raise TypeError('Cannot use maximum_ecmp_paths without ' 'providing max_path') if default: cmd = 'default maximum-paths' From c539798cc9c4ca4f3a9d5f823b16f602cb2e0e68 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 5 Oct 2015 10:07:09 -0400 Subject: [PATCH 14/83] Fix typos --- pyeapi/api/users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index 2772e2c..917cb4a 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -31,7 +31,7 @@ # """API Module for working with EOS local user resources -The Users resource provides configuraiton of local user resources for +The Users resource provides configuration of local user resources for an EOS node. Parameters: @@ -140,7 +140,7 @@ def _parse_username(self, config): def create(self, name, nopassword=None, secret=None, encryption=None): """Creates a new user on the local system. - Creating users require either a secret (password) or the nopassword + Creating users requires either a secret (password) or the nopassword keyword to be specified. Args: @@ -256,7 +256,7 @@ def set_role(self, name, value=None): """Configures the user role vale in EOS Args: - name (str): The name of the user to craete + name (str): The name of the user to create value (str): The value to configure for the user role From 9556dca8fb0ce2c84e55f0a31534516b74aeee02 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 5 Oct 2015 10:43:13 -0400 Subject: [PATCH 15/83] Fix getall method --- pyeapi/api/users.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index 917cb4a..274afa8 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -102,11 +102,7 @@ def get(self, name): return self.getall().get(name) def getall(self): - """Returns the local user configuration as a resource dict - - Args: - name (str): The username to return from the nodes global running- - config. + """Returns the all local users configuration as a resource dict Returns: dict: A resource dict object From b42e0f29bc8eb7271f2c574fb31c7796072a688b Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 5 Oct 2015 10:45:40 -0400 Subject: [PATCH 16/83] Fix getall method --- pyeapi/api/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index 274afa8..df5e67b 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -102,10 +102,10 @@ def get(self, name): return self.getall().get(name) def getall(self): - """Returns the all local users configuration as a resource dict + """Returns all local users configuration as a resource dict Returns: - dict: A resource dict object + dict: A dict of usernames with a nested resource dict object """ users = self.users_re.findall(self.config, re.M) resources = dict() From 1c9f8964778b49729a7f39b416b5f4393ff5d2eb Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 08:46:17 -0400 Subject: [PATCH 17/83] 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 18/83] 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 19/83] 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 20/83] 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 807d0981cd1f0e3b16700143d19166ec13a9f0bb Mon Sep 17 00:00:00 2001 From: David Barroso Date: Thu, 8 Oct 2015 18:15:34 +0200 Subject: [PATCH 21/83] Added support to specify timeout --- pyeapi/client.py | 8 ++------ pyeapi/eapilib.py | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index f8a775a..502b09d 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -375,7 +375,7 @@ def make_connection(transport, **kwargs): return klass(**kwargs) def connect(transport=None, host='localhost', username='admin', - password='', port=None): + password='', port=None, timeout=60): """ Creates a connection using the supplied settings This function will create a connection to an Arista EOS node using @@ -401,7 +401,7 @@ def connect(transport=None, host='localhost', username='admin', """ transport = transport or DEFAULT_TRANSPORT return make_connection(transport, host=host, username=username, - password=password, port=port) + password=password, port=port, timeout=timeout) class Node(object): @@ -745,7 +745,3 @@ def connect_to(name): port=kwargs.get('port')) node = Node(connection, **kwargs) return node - - - - diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 7145b5f..012f0b6 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -61,11 +61,11 @@ DEFAULT_UNIX_SOCKET = '/var/run/command-api.sock' -def https_connection_factory(path, host, port, context=None): +def https_connection_factory(path, host, port, context=None, timeout=60): # ignore ssl context for python versions before 2.7.9 if sys.hexversion < 34015728: - return HttpsConnection(path, host, port) - return HttpsConnection(path, host, port, context=context) + return HttpsConnection(path, host, port, timeout=timeout) + return HttpsConnection(path, host, port, context=context, timeout=timeout) class EapiError(Exception): """Base exception class for all exceptions generated by eapilib @@ -156,9 +156,10 @@ def __init__(self, connection_type, message, commands=None): class SocketConnection(HTTPConnection): - def __init__(self, path): + def __init__(self, path, timeout=60): HTTPConnection.__init__(self, 'localhost') self.path = path + self.timeout = timeout def __str__(self): return 'unix:%s' % self.path @@ -168,6 +169,7 @@ def __repr__(self): def connect(self): self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.settimeout(self.timeout) self.sock.connect(self.path) class HttpConnection(HTTPConnection): @@ -470,30 +472,30 @@ def execute(self, commands, encoding='json', **kwargs): raise class SocketEapiConnection(EapiConnection): - def __init__(self, path=None, **kwargs): + def __init__(self, path=None, timeout=60, **kwargs): super(SocketEapiConnection, self).__init__() path = path or DEFAULT_UNIX_SOCKET - self.transport = SocketConnection(path) + self.transport = SocketConnection(path, timeout) class HttpLocalEapiConnection(EapiConnection): - def __init__(self, port=None, path=None, **kwargs): + def __init__(self, port=None, path=None, timeout=60, **kwargs): super(HttpLocalEapiConnection, self).__init__() port = port or DEFAULT_HTTP_LOCAL_PORT path = path or DEFAULT_HTTP_PATH - self.transport = HttpConnection(path, 'localhost', port) + self.transport = HttpConnection(path, 'localhost', port, timeout=timeout) class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, - password=None, **kwargs): + password=None, timeout=60, **kwargs): super(HttpEapiConnection, self).__init__() port = port or DEFAULT_HTTP_PORT path = path or DEFAULT_HTTP_PATH - self.transport = HttpConnection(path, host, port) + self.transport = HttpConnection(path, host, port, timeout=timeout) self.authentication(username, password) class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, - password=None, context=None, **kwargs): + password=None, context=None, timeout=60, **kwargs): super(HttpsEapiConnection, self).__init__() port = port or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH @@ -503,7 +505,7 @@ def __init__(self, host, port=None, path=None, username=None, if context is None and not enforce_verification: context = self.disable_certificate_verification() - self.transport = https_connection_factory(path, host, port, context) + self.transport = https_connection_factory(path, host, port, context, timeout) self.authentication(username, password) def disable_certificate_verification(self): @@ -518,6 +520,3 @@ def disable_certificate_verification(self): # temporary until a proper fix is implemented. if hasattr(ssl, '_create_unverified_context'): return ssl._create_unverified_context() - - - From dbfba32fed61b8927ac9ffb3312daa6c52215a3a Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Oct 2015 12:50:33 -0400 Subject: [PATCH 22/83] 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 23/83] 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 24/83] 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 fdc12c05eabb2812a990a394d7cf5bb4cf7b3266 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 11 Oct 2015 18:31:11 +0200 Subject: [PATCH 25/83] Fixed tests to include the timeout kwarg --- test/unit/test_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index e27ccef..123f1b3 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -194,7 +194,7 @@ def test_hosts_for_tag_returns_names(self): def test_connect_types(self, connection): transports = list(pyeapi.client.TRANSPORTS.keys()) kwargs = dict(host='localhost', username='admin', password='', - port=None) + port=None, timeout=60) for transport in transports: pyeapi.client.connect(transport) @@ -250,7 +250,7 @@ def test_connect_default_type(self): with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): pyeapi.client.connect() kwargs = dict(host='localhost', username='admin', password='', - port=None) + port=None, timeout=60) transport.assert_called_once_with(**kwargs) def test_connect_to_with_config(self): @@ -260,7 +260,7 @@ def test_connect_to_with_config(self): pyeapi.client.load_config(filename=conf) pyeapi.client.connect_to('test1') kwargs = dict(host='192.168.1.16', username='eapi', - password='password', port=None) + password='password', port=None, timeout=60) transport.assert_called_once_with(**kwargs) From 1e2e6af226d7d8be0350de9a169955c977a38a37 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 13 Oct 2015 12:57:26 -0400 Subject: [PATCH 26/83] 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 From 133f8afe05ace2917b28409d93d0d75fd5c75ad3 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 13 Oct 2015 13:56:51 -0400 Subject: [PATCH 27/83] Add support for Varp API - Manage virtual-router mac address - Manage virtual-router ip addresses in Vlan interfaces --- pyeapi/api/varp.py | 213 +++++++++++++++++++++++ test/fixtures/running_config.varp | 17 ++ test/fixtures/running_config.varp_null | 19 +++ test/system/test_api_varp.py | 228 +++++++++++++++++++++++++ test/unit/test_api_varp.py | 112 ++++++++++++ 5 files changed, 589 insertions(+) create mode 100644 pyeapi/api/varp.py create mode 100644 test/fixtures/running_config.varp create mode 100644 test/fixtures/running_config.varp_null create mode 100644 test/system/test_api_varp.py create mode 100644 test/unit/test_api_varp.py diff --git a/pyeapi/api/varp.py b/pyeapi/api/varp.py new file mode 100644 index 0000000..c7bc758 --- /dev/null +++ b/pyeapi/api/varp.py @@ -0,0 +1,213 @@ +# +# 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 managing the VARP configuration in EOS + +This module provides an API for configuring VARP resources using +EOS and eAPI. + +Example: + { + "mac_address": "aaaa.bbbb.cccc", + "interfaces": { + "Vlan100": { + "addresses": [ "1.1.1.1", "2.2.2.2"] + }, + "Vlan200": [...] + } + } + +Parameters: + name (string): The interface name the configuration is in reference + to. The interface name is the full interface identifier + + address (string): The interface IP address in the form of + address/len. + + mtu (integer): The interface MTU value. The MTU value accepts + integers in the range of 68 to 65535 bytes +""" + +import re + +from pyeapi.api import EntityCollection + + +class Varp(EntityCollection): + + def __init__(self, *args, **kwargs): + super(Varp, self).__init__(*args, **kwargs) + self._interfaces = None + + @property + def interfaces(self): + if self._interfaces is not None: + return self._interfaces + self._interfaces = VarpInterfaces(self.node) + return self._interfaces + + def get(self): + """Returns the current VARP configuration + + The Varp resource returns the following: + + * mac_address (str): The virtual-router mac address + * interfaces (dict): A list of the interfaces that have a + virtual-router address configured. + + Return: + A Python dictionary object of key/value pairs that represents + the current configuration of the node. If the specified + interface does not exist then None is returned. + Example: + { + "mac_address": "aa:bb:cc:dd:ee:ff", + "interfaces": { + "Vlan100": { + "addresses": [ "1.1.1.1", "2.2.2.2"] + }, + "Vlan200": [...] + } + } + """ + resource = dict() + resource.update(self._parse_mac_address()) + resource.update(self._parse_interfaces()) + return resource + + def _parse_mac_address(self): + mac_address_re = re.compile(r'^ip\svirtual-router\smac-address\s((?:[a-f0-9]{2}:){5}[a-f0-9]{2})$', re.M) + mac = mac_address_re.search(self.config) + mac = mac.group(1) if mac else None + return dict(mac_address=mac) + + def _parse_interfaces(self): + interfaces = VarpInterfaces(self.node).getall() + return dict(interfaces=interfaces) + + def set_mac_address(self, mac_address=None, default=False): + """ Sets the virtual-router mac address + + This method will set the switch virtual-router mac address. If a + virtual-router mac address already exists it will be overwritten. + + Args: + mac_address (string): The mac address that will be assigned as + the virtual-router mac address. This should be in the format, + aa:bb:cc:dd:ee:ff. + + default (bool): Sets the virtual-router mac address to the system + default (which is to remove the configuration line). + + Returns: + True if the set operation succeeds otherwise False. + """ + if default: + commands = 'default ip virtual-router mac-address' + elif mac_address is not None: + # Check to see if mac_address matches expected format + if not re.match(r'(?:[a-f0-9]{2}:){5}[a-f0-9]{2}', mac_address): + raise ValueError('mac_address must be formatted like:' + 'aa:bb:cc:dd:ee:ff') + commands = 'ip virtual-router mac-address %s' % mac_address + else: + commands = 'no ip virtual-router mac-address' + + return self.configure(commands) + + +class VarpInterfaces(EntityCollection): + """The VarpInterfaces class helps manage interfaces with + virtual-router configuration. + """ + def get(self, name): + interface_re = r'interface\s%s' % name + config = self.get_block(interface_re) + + if not config: + return None + + resource = dict(addresses=dict()) + resource.update(self._parse_virtual_addresses(config)) + return resource + + def getall(self): + resources = dict() + interfaces_re = re.compile(r'^interface\s(Vlan\d+)$', re.M) + for name in interfaces_re.findall(self.config): + interface_detail = self.get(name) + if interface_detail: + resources[name] = interface_detail + return resources + + def set_addresses(self, name, addresses=None, default=False): + + commands = list() + commands.append('interface %s' % name) + + if default: + commands.append('default ip virtual-router address') + elif addresses is not None: + try: + current_addresses = self.get(name)['addresses'] + except: + current_addresses = [] + + # remove virtual-router addresses not present in addresses list + for entry in set(current_addresses).difference(addresses): + commands.append('no ip virtual-router address %s' % entry) + + # add new set virtual-router addresses that werent present + for entry in set(addresses).difference(current_addresses): + commands.append('ip virtual-router address %s' % entry) + else: + commands.append('no ip virtual-router address') + + return self.configure(commands) if commands else True + + def _parse_virtual_addresses(self, config): + virt_ip_re = re.compile(r'^\s+ip\svirtual-router\saddress\s(\S+)$', + re.M) + return dict(addresses=virt_ip_re.findall(config)) + + +def instance(node): + """Returns an instance of Ipinterfaces + + This method will create and return an instance of the Varp object + passing the value of node to the instance. This function is required + for the resource to be autoloaded by the Node object + + Args: + node (Node): The node argument provides an instance of Node to + the Varp instance + """ + return Varp(node) diff --git a/test/fixtures/running_config.varp b/test/fixtures/running_config.varp new file mode 100644 index 0000000..b7737cd --- /dev/null +++ b/test/fixtures/running_config.varp @@ -0,0 +1,17 @@ +ip virtual-router mac-address 00:11:22:33:44:55 + +interface Vlan4001 + ip address 1.1.1.1/24 + ip virtual-router address 1.1.1.2 +! +interface Vlan4002 + ip address 1.1.2.1/24 + ip virtual-router address 1.1.2.2 + ip virtual-router address 1.1.2.3 + ip virtual-router address 1.1.2.4 + ip virtual-router address 1.1.2.5 + ip virtual-router address 1.1.2.6 +! +interface Vlan4003 + ip address 1.1.2.1/24 +! diff --git a/test/fixtures/running_config.varp_null b/test/fixtures/running_config.varp_null new file mode 100644 index 0000000..be4d8a7 --- /dev/null +++ b/test/fixtures/running_config.varp_null @@ -0,0 +1,19 @@ +interface Vlan4001 + ip address 1.1.1.1/24 + ip virtual-router address 1.1.1.2 + ip virtual-router address 1.1.1.3 + ip virtual-router address 1.1.1.4 + ip virtual-router address 1.1.1.5 + ip virtual-router address 1.1.1.6 +! +interface Vlan4002 + ip address 1.1.2.1/24 + ip virtual-router address 1.1.2.2 + ip virtual-router address 1.1.2.3 + ip virtual-router address 1.1.2.4 + ip virtual-router address 1.1.2.5 + ip virtual-router address 1.1.2.6 +! +interface Vlan4003 + ip address 1.1.2.1/24 +! diff --git a/test/system/test_api_varp.py b/test/system/test_api_varp.py new file mode 100644 index 0000000..25ce5c8 --- /dev/null +++ b/test/system/test_api_varp.py @@ -0,0 +1,228 @@ +# +# 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 +from testlib import random_string + +VIRT_NULL = 'no ip virtual-router mac-address' +VIRT_ENTRY_A = 'ip virtual-router mac-address 00:11:22:33:44:55' +VIRT_ENTRY_B = 'ip virtual-router mac-address 00:11:22:33:44:56' +VIRT_ENTRY_C = 'ip virtual-router mac-address 00:11:22:33:44:57' +IP_CMD = 'ip virtual-router address' + +class TestApiVarp(DutSystemTest): + + def test_basic_get(self): + for dut in self.duts: + dut.config([VIRT_NULL]) + response = dut.api('varp').get() + self.assertIsNotNone(response) + + def test_get_with_value(self): + for dut in self.duts: + dut.config([VIRT_NULL, VIRT_ENTRY_A]) + response = dut.api('varp').get() + self.assertIsNotNone(response) + self.assertEqual(response['mac_address'], '00:11:22:33:44:55') + + def test_get_none(self): + for dut in self.duts: + dut.config([VIRT_NULL]) + response = dut.api('varp').get() + self.assertIsNotNone(response) + self.assertEqual(response['mac_address'], None) + + def test_set_mac_address_with_value(self): + for dut in self.duts: + dut.config([VIRT_NULL]) + api = dut.api('varp') + self.assertNotIn(VIRT_ENTRY_A, api.config) + result = dut.api('varp').set_mac_address('00:11:22:33:44:55') + self.assertTrue(result) + self.assertIn(VIRT_ENTRY_A, api.config) + + def test_change_mac_address(self): + for dut in self.duts: + dut.config([VIRT_NULL, VIRT_ENTRY_A]) + api = dut.api('varp') + self.assertIn(VIRT_ENTRY_A, api.config) + result = dut.api('varp').set_mac_address('00:11:22:33:44:56') + self.assertTrue(result) + self.assertIn(VIRT_ENTRY_B, api.config) + + def test_set_mac_address_with_bad_value(self): + for dut in self.duts: + dut.config([VIRT_NULL]) + api = dut.api('varp') + self.assertNotIn(VIRT_ENTRY_A, api.config) + + with self.assertRaises(ValueError): + dut.api('varp').set_mac_address('0011.2233.4455') + +class TestApiVarpInterfaces(DutSystemTest): + + def test_set_virtual_addr_with_values_clean(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24']) + api = dut.api('varp') + self.assertNotIn('ip virtual-router address 1.1.1.2', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + ['1.1.1.2', + '1.1.1.3']) + self.assertTrue(result) + self.assertIn('ip virtual-router address 1.1.1.2', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.3', + api.get_block('interface Vlan1000')) + + def test_set_virtual_addr_with_values_dirty(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + ['1.1.1.2', + '1.1.1.3']) + self.assertTrue(result) + self.assertIn('ip virtual-router address 1.1.1.2', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.3', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + + def test_set_virtual_addr_with_values_dirty(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + ['1.1.1.2', + '1.1.1.3']) + self.assertTrue(result) + self.assertIn('ip virtual-router address 1.1.1.2', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.3', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + + def test_default_virtual_addrs(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20', + 'ip virtual-router address 1.1.1.21']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + default=True) + self.assertTrue(result) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + + def test_negate_virtual_addrs(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20', + 'ip virtual-router address 1.1.1.21']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + addresses=None) + self.assertTrue(result) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + + def test_empty_list_virtual_addrs(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20', + 'ip virtual-router address 1.1.1.21']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000', + addresses=[]) + self.assertTrue(result) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + + def test_no_attr_virtual_addrs(self): + for dut in self.duts: + dut.config(['no interface Vlan1000', 'interface Vlan1000', + 'ip address 1.1.1.1/24', + 'ip virtual-router address 1.1.1.20', + 'ip virtual-router address 1.1.1.21']) + api = dut.api('varp') + self.assertIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + result = dut.api('varp').interfaces.set_addresses('Vlan1000') + self.assertTrue(result) + self.assertNotIn('ip virtual-router address 1.1.1.20', + api.get_block('interface Vlan1000')) + self.assertNotIn('ip virtual-router address 1.1.1.21', + api.get_block('interface Vlan1000')) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py new file mode 100644 index 0000000..d1f826c --- /dev/null +++ b/test/unit/test_api_varp.py @@ -0,0 +1,112 @@ +# +# 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, random_string +from testlib import EapiConfigUnitTest + +import pyeapi.api.varp + +class TestApiVarp(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiVarp, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.varp.Varp(None) + self.config = open(get_fixture('running_config.varp')).read() + + def test_instance(self): + result = pyeapi.api.varp.instance(None) + self.assertIsInstance(result, pyeapi.api.varp.Varp) + + def test_get(self): + result = self.instance.get() + keys = ['mac_address', 'interfaces'] + self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertIsNotNone(self.instance.get()['mac_address']) + self.assertIsNotNone(self.instance.get()['interfaces']) + + def test_set_mac_address_with_value(self): + value = 'aa:bb:cc:dd:ee:ff' + func = function('set_mac_address', mac_address=value) + cmds = 'ip virtual-router mac-address %s' % value + self.eapi_positive_config_test(func, cmds) + + def test_set_mac_address_with_positional_value(self): + value = 'aa:bb:cc:dd:ee:ff' + func = function('set_mac_address', value) + cmds = 'ip virtual-router mac-address %s' % value + self.eapi_positive_config_test(func, cmds) + + def test_set_mac_address_with_no_value(self): + func = function('set_mac_address', mac_address=None) + cmds = 'no ip virtual-router mac-address' + self.eapi_positive_config_test(func, cmds) + + def test_set_mac_address_with_default(self): + func = function('set_mac_address', default=True) + cmds = 'default ip virtual-router mac-address' + self.eapi_positive_config_test(func, cmds) + +class TestApiVarpInterfaces(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiVarpInterfaces, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.varp.VarpInterfaces(None) + self.config = open(get_fixture('running_config.varp')).read() + + def test_add_address_with_value(self): + func = function('set_addresses', 'Vlan4001', addresses=['1.1.1.4']) + cmds = ['interface Vlan4001', 'no ip virtual-router address 1.1.1.2', + 'ip virtual-router address 1.1.1.4'] + self.eapi_positive_config_test(func, cmds) + + def test_add_address_with_no_value(self): + func = function('set_addresses', 'Vlan4002') + cmds = ['interface Vlan4002', 'no ip virtual-router address'] + self.eapi_positive_config_test(func, cmds) + + def test_add_address_with_empty_list(self): + func = function('set_addresses', 'Vlan4001', addresses=[]) + cmds = ['interface Vlan4001', 'no ip virtual-router address 1.1.1.2'] + self.eapi_positive_config_test(func, cmds) + + def test_add_address_with_default(self): + func = function('set_addresses', 'Vlan4001', default=True) + cmds = ['interface Vlan4001', 'default ip virtual-router address'] + self.eapi_positive_config_test(func, cmds) + +if __name__ == '__main__': + unittest.main() From 000ffe1f06d082a4780e07a7adc485192e74a197 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Tue, 13 Oct 2015 16:52:28 -0600 Subject: [PATCH 28/83] initial file commit --- pyeapi/api/staticroute.py | 356 ++++++++++++++++++++++++++++ test/fixtures/running_config.text | 13 +- test/system/test_api_staticroute.py | 285 ++++++++++++++++++++++ test/unit/test_api_staticroute.py | 318 +++++++++++++++++++++++++ 4 files changed, 970 insertions(+), 2 deletions(-) create mode 100644 pyeapi/api/staticroute.py create mode 100644 test/system/test_api_staticroute.py create mode 100644 test/unit/test_api_staticroute.py diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py new file mode 100644 index 0000000..15cf8d6 --- /dev/null +++ b/pyeapi/api/staticroute.py @@ -0,0 +1,356 @@ +# +# 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. +# +# XXX fix the documentation below +"""Module for working with EOS static routes +XXX +The staticroute resource provides configuration management of static +route resources on an EOS node. It provides the following class +implementations: + + * StaticRoute - Configure static routes in EOS + +StaticRoute Attributes: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + +Notes: + The 'default' prefix function of the 'ip route' command, + 'default ip route ...', currently equivalent to the 'no ip route ...' + command. +""" + +import re + +from pyeapi.api import EntityCollection + +# Create the regular expression for matching ip route strings +ROUTES_RE = re.compile(r'''(?<=^ip\sroute\s) + (?P[^\s]+)\s + (?P[^\s$]+) + [\s|$]{0,1}(?P\d+\.\d+\.\d+\.\d+)? + [\s|$](?P\d+) + [\s|$]{1}(?:tag\s(?P\d+))? + [\s|$]{1}(?:name\s(?P.+))? + ''', re.X) + + +class StaticRoute(EntityCollection): + """The StaticRoute class provides a configuration instance + for working with static routes + + """ + + def __str__(self): + return 'StaticRoute' + + def get(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Check the running config for a route that matches the input + values exactly. + + This may be used as a route_exists function. + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns the matched ip route string if the route entry exists, + otherwise returns False. + """ + + # If distance is None, then distance on the switch is 1 by default + if distance is None: + distance = 1 + + # If tag is None, then tag on the switch is 0 by default + if tag is None: + tag = 0 + + # Create the match string based on the parameters received + match_str = self._build_commands(ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, + tag=tag, + route_name=route_name) + ip_route_re = re.compile(r'^%s$' % match_str, re.M) + + # Search the configuration for the match string + config = ip_route_re.search(self.config) + + # Return the matched string from the config if found, otherwise + # will return None + return config.group(0) + + def get_all(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + # XXX documentation + """Return a list of all static route strings that contain + exact matches for each of the specified parameters. If a + parameter is not specified, then static routes with or without + that parameter can be matched + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns a list of the routes on the list that match all the + specified parameters exactly, or None if no matches are found. + """ + + # Initialize the return array + match_list = [] + + # Initialize the match string with the required parameters + # and the 'ip route' label + match_str = "ip route %s %s" % (ip_dest, next_hop) + + # Each segment should match a specific value if passed in as + # a parameter, or if not specified, it should match any value + # in that place or an empty string where that toke should be. + # The name and tag negation prevents matching tag and name + # to an earlier regex which would prevent the exact match + # later in the string. + + # If next_hop_ip is specified, look for an exact match, + # otherwise allow any or no next_hop_ip to match + if next_hop_ip is not None: + match_str += " %s" % next_hop_ip + else: + match_str += "( (?!name)(?!tag)[^\s]*)?" + + # If distance is specified, look for an exact match, + # otherwise allow any or no distance to match + if distance is not None: + match_str += " %s" % distance + else: + match_str += "( (?!name)(?!tag)[^\s]*)?" + + # If tag is specified, look for an exact match, + # otherwise allow any or no tag to match + if tag is not None: + match_str += " tag %s" % tag + else: + match_str += "( (?!name)[^\s]*)?( (?!name)[^\s]*)?" + + # If route_name is specified, look for an exact match, + # otherwise allow any or no route_name to match + if route_name is not None: + match_str += " name %s" % route_name + else: + match_str += "( [^\s]*)?( [^\s]*)?" + + # Enclose the entire string in parenthesis to capture + # the whole string as well as any groups from above + match_str = "^(%s)$" % match_str + + ip_route_re = re.compile(r'%s' % match_str, re.M) + + # Add the matching full strings to the match_list, using + # the first entry when the result has other groups that + # match (when one or more parameters is unspecified), or + # using the single string when only the entire string matches. + for route in ip_route_re.findall(self.config): + if type(route).__name__ == 'str': + match_list.append(route) + else: + match_list.append(route[0]) + + return match_list + + def create(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Create a static route + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns: + True if the operation succeeds, otherwise False. + """ + + # Call _set_route with the delete flag set to True + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + + def delete(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Delete a static route + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns: + True if the operation succeeds, otherwise False. + """ + + # Call _set_route with the delete flag set to True + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name, delete=True) + + def default(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Set a static route to default (i.e. delete the matching route) + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns: + True if the operation succeeds, otherwise False. + """ + + # Call _set_route with the delete flag set to True + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name, default=True) + + def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Build the EOS command string for ip route interactions. + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + + Returns the ip route command string to be sent to the switch for + the given set of parameters. + """ + + commands = "ip route %s %s" % (ip_dest, next_hop) + + if next_hop_ip is not None: + commands += " %s" % next_hop_ip + if distance is not None: + commands += " %s" % distance + if tag is not None: + commands += " tag %s" % tag + if route_name is not None: + commands += " name %s" % route_name + + return commands + + def _set_route(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None, + delete=False, default=False): + """Configure a static route + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (int): Administrative distance for this route + tag (int): Route tag + route_name (string): Route name + delete (boolean): If true, deletes the specified route + instead of creating or setting values for the route + + Returns: + True if the operation succeeds, otherwise False. + """ + + # Build the route string based on the parameters given + commands = self._build_commands(ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, + tag=tag, + route_name=route_name) + + # Prefix with 'no' if delete is set + if delete: + commands = "no " + commands + # Or with 'default' if default is setting + else: + if default: + commands = "default " + commands + + return self.configure(commands) + + +def instance(node): + """Returns an instance of Vlans + + This method will create and return an instance of the Vlans object passing + the value of API to the object. The instance method is required for the + resource to be autoloaded by the Node object + + Args: + node (Node): The node argument passes an instance of Node to the + resource + """ + return StaticRoute(node) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index af2c0c8..62f4588 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1585,7 +1585,16 @@ ip virtual-router mac-address advertisement-interval 30 ! no ipv6 hardware fib nexthop-index ! -ip route 0.0.0.0/0 192.68.1.254 1 tag 0 +ip route 0.0.0.0/0 192.168.1.254 1 tag 0 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 +ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1 +ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1 +ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 ! ip icmp redirect ip routing @@ -1663,7 +1672,7 @@ no ip tacacs source-interface no vxlan vni notation dotted ! banner login -this +this is the loging ban that would b emult EOF diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py new file mode 100644 index 0000000..b601b02 --- /dev/null +++ b/test/system/test_api_staticroute.py @@ -0,0 +1,285 @@ +# +# 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. +# +import sys +import os +import unittest + +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from random import choice +from testlib import random_int, random_string +from systestlib import DutSystemTest + +NEXT_HOPS = ['Ethernet1', 'Ethernet2', 'Null0', 'IP'] +DISTANCES = TAGS = ROUTE_NAMES = [None, True] + + +def _ip_addr(): + ip1 = random_int(0, 223) + ip2 = random_int(0, 255) + ip3 = random_int(0, 255) + return "%s.%s.%s.0/24" % (ip1, ip2, ip3) + + +def _next_hop(): + next_hop = choice(NEXT_HOPS) + if next_hop is 'Null0': + return (next_hop, None) + ip1 = random_int(0, 223) + ip2 = random_int(0, 255) + ip3 = random_int(0, 255) + ip4 = random_int(0, 255) + ip_addr = "%s.%s.%s.%s" % (ip1, ip2, ip3, ip4) + if next_hop is 'IP': + return (ip_addr, None) + return (next_hop, ip_addr) + + +def _distance(): + # distance = choice(DISTANCES) + # if distance: + return random_int(1, 255) + # return distance + + +def _tag(): + # tag = choice(TAGS) + # if tag: + return random_int(0, 255) + # return tag + + +def _route_name(): + # route_name = choice(ROUTE_NAMES) + # if route_name: + return random_string(minchar=4, maxchar=10) + # return route_name + + +class TestApiStaticroute(DutSystemTest): + + def test_create(self): + # Validate the create function returns without an error + # when creating routes with varying parameters included. + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing']) + + for t_distance in DISTANCES: + for t_tag in TAGS: + for t_route_name in ROUTE_NAMES: + ip_dest = _ip_addr() + (next_hop, next_hop_ip) = _next_hop() + distance = t_distance + if distance is True: + distance = _distance() + tag = t_tag + if tag is True: + tag = _tag() + route_name = t_route_name + if route_name is True: + route_name = _route_name() + + result = dut.api('staticroute').create( + ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, route_name=route_name) + + self.assertTrue(result) + + def test_get(self): + # Validate the get function returns the exact value that + # is passed in when the route exists on the switch. + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing']) + + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + next_hop_ip = '1.1.1.1' + distance = '1' + tag = '1' + route_name = 'test1' + cmd = "ip route %s %s %s %s tag %s name %s" % \ + (ip_dest, next_hop, next_hop_ip, distance, tag, route_name) + dut.config([cmd]) + + result = \ + dut.api('staticroute').get(ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + + # Make sure the funtion returns a true result (match found) + self.assertTrue(result) + # Then make sure the returned string is what was expected + # self.assertEqual(result.group(0), cmd) + self.assertEqual(result, cmd) + + def test_get_all(self): + # Validate the get_all function returns a list of entries + # containing the matched parameters, and that parameters + # are matched in full (i.e. name 'test1' does not match + # name 'test10'). + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing']) + + # Declare a set of 3 routes with same ip dest and next hop. + # Set different distance, tag and name for each route, + # including values 1 and 10 in each, so the test will verify + # that matching 1 does not also match 10. + route1 = \ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1' + route2 = \ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10' + route3 = \ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 2 tag 10 name test1' + + dut.config([route1, route2, route3]) + + # Check that all three routes are returned when only using + # the ip dest and next hop for the search + exp_size = 3 + result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1') + size = len(result) + # Assert that size of returned list is as expected + self.assertEqual(size, exp_size) + # Assert each expected route is in the list correctly + self.assertTrue(route1 in result) + self.assertTrue(route2 in result) + self.assertTrue(route3 in result) + + # Check that routes 1 and 3 are returned when specifying + # route name as 'test1' + exp_size = 2 + result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + route_name='test1') + size = len(result) + # Assert that size of returned list is as expected + self.assertEqual(size, exp_size) + # Assert each expected route is in the list correctly + self.assertTrue(route1 in result) + self.assertTrue(route3 in result) + + # Check that routes 1 and 2 are returned when specifying + # tag as '1' + exp_size = 2 + result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + tag='1') + size = len(result) + # Assert that size of returned list is as expected + self.assertEqual(size, exp_size) + # Assert each expected route is in the list correctly + self.assertTrue(route1 in result) + self.assertTrue(route2 in result) + + # Check that only route 1 is returned when specifying the + # distance as 10 + exp_size = 1 + result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + distance='10') + size = len(result) + # Assert that size of returned list is as expected + self.assertEqual(size, exp_size) + # Assert each expected route is in the list correctly + self.assertTrue(route1 in result) + + def test_delete(self): + # Validate the delete function returns without an error + # when deleting routes with varying parameters included. + # Note: the routes do not have to exist for the + # delete command to succeed, but only that the command + # does not error. + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing']) + + for t_distance in DISTANCES: + for t_tag in TAGS: + for t_route_name in ROUTE_NAMES: + ip_dest = _ip_addr() + (next_hop, next_hop_ip) = _next_hop() + distance = t_distance + if distance is True: + distance = _distance() + tag = t_tag + if tag is True: + tag = _tag() + route_name = t_route_name + if route_name is True: + route_name = _route_name() + + result = dut.api('staticroute').delete( + ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, route_name=route_name) + + self.assertTrue(result) + + def test_default(self): + # Validate the default function returns without an error + # when deleting routes with varying parameters included. + # Note: currently EOS functionality of 'default ip route ...' + # is equivalent to 'no ip route ...', which is the delete + # function. + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing']) + + for t_distance in DISTANCES: + for t_tag in TAGS: + for t_route_name in ROUTE_NAMES: + ip_dest = _ip_addr() + (next_hop, next_hop_ip) = _next_hop() + distance = t_distance + if distance is True: + distance = _distance() + tag = t_tag + if tag is True: + tag = _tag() + route_name = t_route_name + if route_name is True: + route_name = _route_name() + + result = dut.api('staticroute').default( + ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, route_name=route_name) + + self.assertTrue(result) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py new file mode 100644 index 0000000..5971d54 --- /dev/null +++ b/test/unit/test_api_staticroute.py @@ -0,0 +1,318 @@ +# +# 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. +# +import sys +import os +import unittest + +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from random import choice +from testlib import get_fixture, function, random_int, random_string +from testlib import EapiConfigUnitTest + +import pyeapi.api.staticroute + +IP_DESTS = ['11.111.11.0/24', '222.22.222.0/24', '33.34.35.0/24'] +NEXT_HOPS = [('Ethernet1', '3.3.3.3'), ('Ethernet2', '2.2.2.2'), + ('Null0', None), ('44.44.44.0', None)] +DISTANCES = TAGS = ROUTE_NAMES = [None, True] + + +class TestApiStaticroute(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiStaticroute, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.staticroute.StaticRoute(None) + self.config = open(get_fixture('running_config.text')).read() + + def test_get(self): + # Test retrieval of a specific static route entry + # Assumes running_config.text file contains the ip route line: + # ip route 0.0.0.0/0 192.168.1.254 1 tag 0 + + ip_dest = '0.0.0.0/0' + next_hop = '192.168.1.254' + params = dict(next_hop_ip=None, + distance=1, + tag=0, + route_name=None) + result = self.instance.get(ip_dest, next_hop, **params) + self.assertTrue(result) + + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip='1.1.1.1', + distance=1, + tag=1, + route_name='test1') + result = self.instance.get(ip_dest, next_hop, **params) + self.assertTrue(result) + + def test_get_all(self): + # Test retrieval of static route entries + # Assumes running_config.text file contains the following + # ip route specifications, and that no additional routes + # are specified with similar attributes. + + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 + + # Test when all values are explicitly specified. Should be + # a single match + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip='1.1.1.1', + distance=1, + tag=1, + route_name='test1') + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + # Test when none of the optional parameters are specified. + # Should return a complete set. + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1' + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip=None, + distance=None, + tag=None, + route_name=None) + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + # Test when next_hop_ip is not specified + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1', + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip=None, + distance=1, + tag=1, + route_name='test1') + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + # Test when distance is not specified + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1', + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip='1.1.1.1', + distance=None, + tag=1, + route_name='test1') + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + # Test when tag is not specified + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1', + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip='1.1.1.1', + distance=1, + tag=None, + route_name='test1') + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + # Test when name is not specified + exp_result = [ + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1' + ] + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + params = dict(next_hop_ip='1.1.1.1', + distance=1, + tag=1, + route_name=None) + result = self.instance.get_all(ip_dest, next_hop, **params) + + self.assertEqual(result, exp_result) + + def test_create(self): + # Test passing in a full set of parameters to 'create' + # Some parameters may be not set: None + for ip_dest in IP_DESTS: + # Get the parameters for the call + (next_hop, next_hop_ip) = choice(NEXT_HOPS) + distance = choice(DISTANCES) + if distance: + distance = random_int(0, 255) + tag = choice(TAGS) + if tag: + tag = random_int(0, 255) + route_name = choice(ROUTE_NAMES) + if route_name: + route_name = random_string(minchar=4, maxchar=10) + + func = function('create', ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, + tag=tag, + route_name=route_name) + + # Build the expected string for comparison + # A value of None will default to an empty string, and + # add the tag or name keywords where appropriate + cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' + if next_hop_ip is not None: + cmd_next_hop_ip = " %s" % next_hop_ip + if distance is not None: + cmd_distance = " %d" % distance + if tag is not None: + cmd_tag = " tag %d" % tag + if route_name is not None: + cmd_route_name = " name %s" % route_name + cmds = "ip route %s %s%s%s%s%s" % \ + (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, + cmd_tag, cmd_route_name) + + self.eapi_positive_config_test(func, cmds) + + def test_delete(self): + # Test passing in a full set of parameters to 'delete' + # Some parameters may be not set: None + for ip_dest in IP_DESTS: + (next_hop, next_hop_ip) = choice(NEXT_HOPS) + distance = choice(DISTANCES) + if distance: + distance = random_int(0, 255) + tag = choice(TAGS) + if tag: + tag = random_int(0, 255) + route_name = choice(ROUTE_NAMES) + if route_name: + route_name = random_string(minchar=4, maxchar=10) + + func = function('delete', ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, + tag=tag, + route_name=route_name) + + # Build the expected string for comparison + # A value of None will default to an empty string, and + # add the tag or name keywords where appropriate + cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' + if next_hop_ip is not None: + cmd_next_hop_ip = " %s" % next_hop_ip + if distance is not None: + cmd_distance = " %d" % distance + if tag is not None: + cmd_tag = " tag %d" % tag + if route_name is not None: + cmd_route_name = " name %s" % route_name + cmds = "no ip route %s %s%s%s%s%s" % \ + (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, + cmd_tag, cmd_route_name) + + self.eapi_positive_config_test(func, cmds) + + def test_default(self): + # Test passing in a full set of parameters to 'default' + # Some parameters may be not set: None + for ip_dest in IP_DESTS: + (next_hop, next_hop_ip) = choice(NEXT_HOPS) + distance = choice(DISTANCES) + if distance: + distance = random_int(0, 255) + tag = choice(TAGS) + if tag: + tag = random_int(0, 255) + route_name = choice(ROUTE_NAMES) + if route_name: + route_name = random_string(minchar=4, maxchar=10) + + func = function('default', ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, + tag=tag, + route_name=route_name) + + # Build the expected string for comparison + # A value of None will default to an empty string, and + # add the tag or name keywords where appropriate + cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' + if next_hop_ip is not None: + cmd_next_hop_ip = " %s" % next_hop_ip + if distance is not None: + cmd_distance = " %d" % distance + if tag is not None: + cmd_tag = " tag %d" % tag + if route_name is not None: + cmd_route_name = " name %s" % route_name + cmds = "default ip route %s %s%s%s%s%s" % \ + (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, + cmd_tag, cmd_route_name) + + self.eapi_positive_config_test(func, cmds) + + +if __name__ == '__main__': + unittest.main() From 0b002fe42ef92fe14a9f107bdb273769c65a7c7c Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 14 Oct 2015 17:32:11 -0600 Subject: [PATCH 29/83] update --- pyeapi/api/staticroute.py | 190 +++++++++++----------------- test/fixtures/running_config.text | 8 +- test/system/test_api_staticroute.py | 139 ++++++++++++-------- test/unit/test_api_staticroute.py | 182 +++++++++----------------- 4 files changed, 216 insertions(+), 303 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 15cf8d6..2a8ac9e 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -29,9 +29,8 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -# XXX fix the documentation below """Module for working with EOS static routes -XXX + The staticroute resource provides configuration management of static route resources on an EOS node. It provides the following class implementations: @@ -58,15 +57,32 @@ from pyeapi.api import EntityCollection # Create the regular expression for matching ip route strings -ROUTES_RE = re.compile(r'''(?<=^ip\sroute\s) - (?P[^\s]+)\s - (?P[^\s$]+) - [\s|$]{0,1}(?P\d+\.\d+\.\d+\.\d+)? - [\s|$](?P\d+) - [\s|$]{1}(?:tag\s(?P\d+))? - [\s|$]{1}(?:name\s(?P.+))? - ''', re.X) - +# ROUTES_RE = re.compile(r'''(?<=^ip\sroute\s) +# (?P[^\s]+)\s +# (?P[^\s$]+) +# [\s|$]{0,1}(?P\d+\.\d+\.\d+\.\d+)? +# [\s|$](?P\d+) +# [\s|$]{1}(?:tag\s(?P\d+))? +# [\s|$]{1}(?:name\s(?P.+))? +# ''', re.X) +# Define the regex to match ip route lines (by lines in regex): +# 'ip route' header +# ip_dest +# next_hop +# next_hop_ip +# distance +# tag +# name +ROUTES_RE = re.compile(r'(?<=^ip route)' + r' (\d+\.\d+\.\d+\.\d+\/\d+)' + r' (\d+\.\d+\.\d+\.\d+|\S+)' + r'(?: (\d+\.\d+\.\d+\.\d+))?' + r' (\d+)' + r'(?: tag (\d+))?' + r'(?: name (\S+))?', re.M) +# (?<=^ip route )(\d+\.\d+\.\d+\.\d+\/\d+) (\d+\.\d+\.\d+\.\d+|[^\s]+)(?: (\d+\.\d+\.\d+\.\d+))?( \d+)(?: tag (\d+))?(?: name (\S+))? + +ROUTE_ID = "%s--%s--%s" class StaticRoute(EntityCollection): """The StaticRoute class provides a configuration instance @@ -77,131 +93,67 @@ class StaticRoute(EntityCollection): def __str__(self): return 'StaticRoute' - def get(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): - """Check the running config for a route that matches the input - values exactly. - - This may be used as a route_exists function. + def get(self, ip_dest, next_hop, distance): + """Retrieves the ip route information for the route specified + by the ip_dest, next_hop, and distance parameters Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface distance (int): Administrative distance for this route - tag (int): Route tag - route_name (string): Route name - Returns the matched ip route string if the route entry exists, - otherwise returns False. + Returns: + dict: An ip route dict object + + If the unique route specified by the ip_dest, next_hop, and + distance does not exist, then None is returned. """ - # If distance is None, then distance on the switch is 1 by default + # If distance is None, then set to 1 to match what EOS will + # do when distance is not specified. if distance is None: distance = 1 - # If tag is None, then tag on the switch is 0 by default - if tag is None: - tag = 0 + # Make the unique route_id for the requested route + route_id = ROUTE_ID % (ip_dest, next_hop, distance) - # Create the match string based on the parameters received - match_str = self._build_commands(ip_dest, next_hop, - next_hop_ip=next_hop_ip, - distance=distance, - tag=tag, - route_name=route_name) - ip_route_re = re.compile(r'^%s$' % match_str, re.M) + # Return the route configuration if found, or return None + return self.getall().get(route_id) - # Search the configuration for the match string - config = ip_route_re.search(self.config) + def getall(self): + """Return all ip routes configured on the switch as a resource dict - # Return the matched string from the config if found, otherwise - # will return None - return config.group(0) - - def get_all(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): - # XXX documentation - """Return a list of all static route strings that contain - exact matches for each of the specified parameters. If a - parameter is not specified, then static routes with or without - that parameter can be matched - - Args: - ip_dest (string): The ip address of the destination in the - form of A.B.C.D/E - next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (int): Administrative distance for this route - tag (int): Route tag - route_name (string): Route name - - Returns a list of the routes on the list that match all the - specified parameters exactly, or None if no matches are found. + Returns: + dict: A dict of unique ip route names with a nested route + dict object. The unique name is built with the ip destination, + next hop, and distance values for the route. """ - # Initialize the return array - match_list = [] - - # Initialize the match string with the required parameters - # and the 'ip route' label - match_str = "ip route %s %s" % (ip_dest, next_hop) - - # Each segment should match a specific value if passed in as - # a parameter, or if not specified, it should match any value - # in that place or an empty string where that toke should be. - # The name and tag negation prevents matching tag and name - # to an earlier regex which would prevent the exact match - # later in the string. - - # If next_hop_ip is specified, look for an exact match, - # otherwise allow any or no next_hop_ip to match - if next_hop_ip is not None: - match_str += " %s" % next_hop_ip - else: - match_str += "( (?!name)(?!tag)[^\s]*)?" - - # If distance is specified, look for an exact match, - # otherwise allow any or no distance to match - if distance is not None: - match_str += " %s" % distance - else: - match_str += "( (?!name)(?!tag)[^\s]*)?" - - # If tag is specified, look for an exact match, - # otherwise allow any or no tag to match - if tag is not None: - match_str += " tag %s" % tag - else: - match_str += "( (?!name)[^\s]*)?( (?!name)[^\s]*)?" - - # If route_name is specified, look for an exact match, - # otherwise allow any or no route_name to match - if route_name is not None: - match_str += " name %s" % route_name - else: - match_str += "( [^\s]*)?( [^\s]*)?" - - # Enclose the entire string in parenthesis to capture - # the whole string as well as any groups from above - match_str = "^(%s)$" % match_str - - ip_route_re = re.compile(r'%s' % match_str, re.M) - - # Add the matching full strings to the match_list, using - # the first entry when the result has other groups that - # match (when one or more parameters is unspecified), or - # using the single string when only the entire string matches. - for route in ip_route_re.findall(self.config): - if type(route).__name__ == 'str': - match_list.append(route) - else: - match_list.append(route[0]) - - return match_list + # Find all the ip routes in the config + matches = ROUTES_RE.findall(self.config) + + # Parse the routes and add them to the routes dict + routes = dict() + for match in matches: + # Set the route dict to the returned values, replacing + # empty strings with None + route = dict() + route['ip_dest'] = match[0] + route['next_hop'] = match[1] + route['next_hop_ip'] = None if match[2] is '' else match[2] + route['distance'] = match[3] + route['tag'] = None if match[4] is '' else match[4] + route['route_name'] = None if match[5] is '' else match[5] + + # Build a unique route_id from the ip_dest, next_hop, and distance + route_id = ROUTE_ID % \ + (route['ip_dest'], route['next_hop'], route['distance']) + + # Update the routes dict + routes.update({route_id: route}) + + return routes def create(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index 62f4588..b1b395e 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1587,14 +1587,8 @@ no ipv6 hardware fib nexthop-index ! ip route 0.0.0.0/0 192.168.1.254 1 tag 0 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 -ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10 -ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 -ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1 -ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1 -ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1 -ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1 -ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 +ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 ! ip icmp redirect ip routing diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index b601b02..9e4fe2b 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -65,6 +65,7 @@ def _next_hop(): def _distance(): + # XXX # distance = choice(DISTANCES) # if distance: return random_int(1, 255) @@ -133,20 +134,22 @@ def test_get(self): cmd = "ip route %s %s %s %s tag %s name %s" % \ (ip_dest, next_hop, next_hop_ip, distance, tag, route_name) dut.config([cmd]) + route = {'ip_dest': ip_dest, + 'next_hop': next_hop, + 'next_hop_ip': next_hop_ip, + 'distance': distance, + 'tag': tag, + 'route_name': route_name} - result = \ - dut.api('staticroute').get(ip_dest, next_hop, - next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + result = dut.api('staticroute').get(ip_dest, next_hop, distance) # Make sure the funtion returns a true result (match found) self.assertTrue(result) # Then make sure the returned string is what was expected # self.assertEqual(result.group(0), cmd) - self.assertEqual(result, cmd) + self.assertEqual(result, route) - def test_get_all(self): + def test_getall(self): # Validate the get_all function returns a list of entries # containing the matched parameters, and that parameters # are matched in full (i.e. name 'test1' does not match @@ -169,52 +172,82 @@ def test_get_all(self): dut.config([route1, route2, route3]) - # Check that all three routes are returned when only using - # the ip dest and next hop for the search - exp_size = 3 - result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1') - size = len(result) - # Assert that size of returned list is as expected - self.assertEqual(size, exp_size) - # Assert each expected route is in the list correctly - self.assertTrue(route1 in result) - self.assertTrue(route2 in result) - self.assertTrue(route3 in result) - - # Check that routes 1 and 3 are returned when specifying - # route name as 'test1' - exp_size = 2 - result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - route_name='test1') - size = len(result) - # Assert that size of returned list is as expected - self.assertEqual(size, exp_size) - # Assert each expected route is in the list correctly - self.assertTrue(route1 in result) - self.assertTrue(route3 in result) - - # Check that routes 1 and 2 are returned when specifying - # tag as '1' - exp_size = 2 - result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - tag='1') - size = len(result) - # Assert that size of returned list is as expected - self.assertEqual(size, exp_size) - # Assert each expected route is in the list correctly - self.assertTrue(route1 in result) - self.assertTrue(route2 in result) - - # Check that only route 1 is returned when specifying the - # distance as 10 - exp_size = 1 - result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - distance='10') - size = len(result) - # Assert that size of returned list is as expected - self.assertEqual(size, exp_size) - # Assert each expected route is in the list correctly - self.assertTrue(route1 in result) + routes = { + '1.2.3.0/24--Ethernet1--10': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '1.1.1.1', + 'distance': '10', + 'tag': '1', + 'route_name': 'test1'}, + '1.2.3.0/24--Ethernet1--1': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '1.1.1.1', + 'distance': '1', + 'tag': '1', + 'route_name': 'test10'}, + '1.2.3.0/24--Ethernet1--2': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '1.1.1.1', + 'distance': '2', + 'tag': '10', + 'route_name': 'test1'} + } + + # Get the list of ip routes from the switch + result = dut.api('staticroute').getall() + + # Assert that the result dict is equivalent to the routes dict + self.assertEqual(result, routes) + + # # Check that all three routes are returned when only using + # # the ip dest and next hop for the search + # exp_size = 3 + # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1') + # size = len(result) + # # Assert that size of returned list is as expected + # self.assertEqual(size, exp_size) + # # Assert each expected route is in the list correctly + # self.assertTrue(route1 in result) + # self.assertTrue(route2 in result) + # self.assertTrue(route3 in result) + # + # # Check that routes 1 and 3 are returned when specifying + # # route name as 'test1' + # exp_size = 2 + # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + # route_name='test1') + # size = len(result) + # # Assert that size of returned list is as expected + # self.assertEqual(size, exp_size) + # # Assert each expected route is in the list correctly + # self.assertTrue(route1 in result) + # self.assertTrue(route3 in result) + # + # # Check that routes 1 and 2 are returned when specifying + # # tag as '1' + # exp_size = 2 + # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + # tag='1') + # size = len(result) + # # Assert that size of returned list is as expected + # self.assertEqual(size, exp_size) + # # Assert each expected route is in the list correctly + # self.assertTrue(route1 in result) + # self.assertTrue(route2 in result) + # + # # Check that only route 1 is returned when specifying the + # # distance as 10 + # exp_size = 1 + # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', + # distance='10') + # size = len(result) + # # Assert that size of returned list is as expected + # self.assertEqual(size, exp_size) + # # Assert each expected route is in the list correctly + # self.assertTrue(route1 in result) def test_delete(self): # Validate the delete function returns without an error diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 5971d54..fe3b929 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -56,144 +56,78 @@ def __init__(self, *args, **kwargs): def test_get(self): # Test retrieval of a specific static route entry - # Assumes running_config.text file contains the ip route line: + # Assumes running_config.text file contains the ip route lines: # ip route 0.0.0.0/0 192.168.1.254 1 tag 0 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 ip_dest = '0.0.0.0/0' next_hop = '192.168.1.254' - params = dict(next_hop_ip=None, - distance=1, - tag=0, - route_name=None) - result = self.instance.get(ip_dest, next_hop, **params) - self.assertTrue(result) + distance = '1' + route = dict(ip_dest=ip_dest, + next_hop=next_hop, + next_hop_ip=None, + distance=distance, + tag='0', + route_name=None) + result = self.instance.get(ip_dest, next_hop, distance) + self.assertEqual(result, route) ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' - params = dict(next_hop_ip='1.1.1.1', - distance=1, - tag=1, - route_name='test1') - result = self.instance.get(ip_dest, next_hop, **params) - self.assertTrue(result) + distance = '1' + route = dict(ip_dest=ip_dest, + next_hop=next_hop, + next_hop_ip='1.1.1.1', + distance='1', + tag='1', + route_name='test1') + result = self.instance.get(ip_dest, next_hop, distance) + self.assertEqual(result, route) def test_get_all(self): - # Test retrieval of static route entries + # Test retrieval of all static route entries # Assumes running_config.text file contains the following # ip route specifications, and that no additional routes - # are specified with similar attributes. + # are specified. + # ip route 0.0.0.0/0 192.168.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10 - # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1 - # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 - - # Test when all values are explicitly specified. Should be - # a single match - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip='1.1.1.1', - distance=1, - tag=1, - route_name='test1') - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) - - # Test when none of the optional parameters are specified. - # Should return a complete set. - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1' - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip=None, - distance=None, - tag=None, - route_name=None) - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) - - # Test when next_hop_ip is not specified - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 10.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1 tag 1 name test1', - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip=None, - distance=1, - tag=1, - route_name='test1') - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) - - # Test when distance is not specified - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 0 tag 1 name test1', - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip='1.1.1.1', - distance=None, - tag=1, - route_name='test1') - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) - - # Test when tag is not specified - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 10 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test1', - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip='1.1.1.1', - distance=1, - tag=None, - route_name='test1') - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) - - # Test when name is not specified - exp_result = [ - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10', - 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1' - ] - ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - params = dict(next_hop_ip='1.1.1.1', - distance=1, - tag=1, - route_name=None) - result = self.instance.get_all(ip_dest, next_hop, **params) - - self.assertEqual(result, exp_result) + # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 + + routes = { + '0.0.0.0/0--192.168.1.254--1': + {'ip_dest': '0.0.0.0/0', + 'next_hop': '192.168.1.254', + 'next_hop_ip': None, + 'distance': '1', + 'tag': '0', + 'route_name': None}, + '1.2.3.0/24--Ethernet1--1': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '1.1.1.1', + 'distance': '1', + 'tag': '1', + 'route_name': 'test1'}, + '1.2.3.0/24--Ethernet1--10': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '1.1.1.1', + 'distance': '10', + 'tag': '1', + 'route_name': 'test1'}, + '1.2.3.0/24--Ethernet1--20': + {'ip_dest': '1.2.3.0/24', + 'next_hop': 'Ethernet1', + 'next_hop_ip': '10.1.1.1', + 'distance': '20', + 'tag': '1', + 'route_name': 'test1'} + } + + result = self.instance.getall() + self.assertEqual(result, routes) def test_create(self): # Test passing in a full set of parameters to 'create' From c1eb13f85213e0ce14687f05088e38b9cd1d516b Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 14 Oct 2015 17:33:13 -0600 Subject: [PATCH 30/83] update --- test/unit/test_api_staticroute.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index fe3b929..808c65b 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -57,11 +57,11 @@ def __init__(self, *args, **kwargs): def test_get(self): # Test retrieval of a specific static route entry # Assumes running_config.text file contains the ip route lines: - # ip route 0.0.0.0/0 192.168.1.254 1 tag 0 + # ip route 0.0.0.0/0 192.68.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 ip_dest = '0.0.0.0/0' - next_hop = '192.168.1.254' + next_hop = '192.68.1.254' distance = '1' route = dict(ip_dest=ip_dest, next_hop=next_hop, @@ -90,15 +90,15 @@ def test_get_all(self): # ip route specifications, and that no additional routes # are specified. - # ip route 0.0.0.0/0 192.168.1.254 1 tag 0 + # ip route 0.0.0.0/0 192.68.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { - '0.0.0.0/0--192.168.1.254--1': + '0.0.0.0/0--192.68.1.254--1': {'ip_dest': '0.0.0.0/0', - 'next_hop': '192.168.1.254', + 'next_hop': '192.68.1.254', 'next_hop_ip': None, 'distance': '1', 'tag': '0', From 5c3d4ae4389d50c05a9fd51b2d7f31087de04f02 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 15 Oct 2015 09:02:52 -0600 Subject: [PATCH 31/83] Add next_hop_ip value to unique route id string, other fixes --- pyeapi/api/staticroute.py | 22 ++++----- test/fixtures/running_config.text | 2 +- test/system/test_api_staticroute.py | 72 ++++------------------------- test/unit/test_api_staticroute.py | 22 +++++---- 4 files changed, 30 insertions(+), 88 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 2a8ac9e..f9e985c 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -56,15 +56,6 @@ from pyeapi.api import EntityCollection -# Create the regular expression for matching ip route strings -# ROUTES_RE = re.compile(r'''(?<=^ip\sroute\s) -# (?P[^\s]+)\s -# (?P[^\s$]+) -# [\s|$]{0,1}(?P\d+\.\d+\.\d+\.\d+)? -# [\s|$](?P\d+) -# [\s|$]{1}(?:tag\s(?P\d+))? -# [\s|$]{1}(?:name\s(?P.+))? -# ''', re.X) # Define the regex to match ip route lines (by lines in regex): # 'ip route' header # ip_dest @@ -80,9 +71,11 @@ r' (\d+)' r'(?: tag (\d+))?' r'(?: name (\S+))?', re.M) -# (?<=^ip route )(\d+\.\d+\.\d+\.\d+\/\d+) (\d+\.\d+\.\d+\.\d+|[^\s]+)(?: (\d+\.\d+\.\d+\.\d+))?( \d+)(?: tag (\d+))?(?: name (\S+))? -ROUTE_ID = "%s--%s--%s" +# Define a format for the unique route id +# The four parts in order are ip_dest, next_hop, next_hop_ip, and distance +ROUTE_ID = "%s--%s--%s--%s" + class StaticRoute(EntityCollection): """The StaticRoute class provides a configuration instance @@ -93,7 +86,7 @@ class StaticRoute(EntityCollection): def __str__(self): return 'StaticRoute' - def get(self, ip_dest, next_hop, distance): + def get(self, ip_dest, next_hop, distance, next_hop_ip=None): """Retrieves the ip route information for the route specified by the ip_dest, next_hop, and distance parameters @@ -116,7 +109,7 @@ def get(self, ip_dest, next_hop, distance): distance = 1 # Make the unique route_id for the requested route - route_id = ROUTE_ID % (ip_dest, next_hop, distance) + route_id = ROUTE_ID % (ip_dest, next_hop, next_hop_ip, distance) # Return the route configuration if found, or return None return self.getall().get(route_id) @@ -148,7 +141,8 @@ def getall(self): # Build a unique route_id from the ip_dest, next_hop, and distance route_id = ROUTE_ID % \ - (route['ip_dest'], route['next_hop'], route['distance']) + (route['ip_dest'], route['next_hop'], + route['next_hop_ip'], route['distance']) # Update the routes dict routes.update({route_id: route}) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index b1b395e..0a6666e 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1585,7 +1585,7 @@ ip virtual-router mac-address advertisement-interval 30 ! no ipv6 hardware fib nexthop-index ! -ip route 0.0.0.0/0 192.168.1.254 1 tag 0 +ip route 0.0.0.0/0 192.68.1.254 1 tag 0 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 9e4fe2b..34461f0 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -65,25 +65,15 @@ def _next_hop(): def _distance(): - # XXX - # distance = choice(DISTANCES) - # if distance: - return random_int(1, 255) - # return distance + return random_int(1, 255) def _tag(): - # tag = choice(TAGS) - # if tag: - return random_int(0, 255) - # return tag + return random_int(0, 255) def _route_name(): - # route_name = choice(ROUTE_NAMES) - # if route_name: - return random_string(minchar=4, maxchar=10) - # return route_name + return random_string(minchar=4, maxchar=10) class TestApiStaticroute(DutSystemTest): @@ -141,7 +131,8 @@ def test_get(self): 'tag': tag, 'route_name': route_name} - result = dut.api('staticroute').get(ip_dest, next_hop, distance) + result = dut.api('staticroute').get(ip_dest, next_hop, distance, + next_hop_ip=next_hop_ip) # Make sure the funtion returns a true result (match found) self.assertTrue(result) @@ -173,21 +164,21 @@ def test_getall(self): dut.config([route1, route2, route3]) routes = { - '1.2.3.0/24--Ethernet1--10': + '1.2.3.0/24--Ethernet1--1.1.1.1--10': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '10', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--1': + '1.2.3.0/24--Ethernet1--1.1.1.1--1': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '1', 'tag': '1', 'route_name': 'test10'}, - '1.2.3.0/24--Ethernet1--2': + '1.2.3.0/24--Ethernet1--1.1.1.1--2': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', @@ -202,53 +193,6 @@ def test_getall(self): # Assert that the result dict is equivalent to the routes dict self.assertEqual(result, routes) - # # Check that all three routes are returned when only using - # # the ip dest and next hop for the search - # exp_size = 3 - # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1') - # size = len(result) - # # Assert that size of returned list is as expected - # self.assertEqual(size, exp_size) - # # Assert each expected route is in the list correctly - # self.assertTrue(route1 in result) - # self.assertTrue(route2 in result) - # self.assertTrue(route3 in result) - # - # # Check that routes 1 and 3 are returned when specifying - # # route name as 'test1' - # exp_size = 2 - # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - # route_name='test1') - # size = len(result) - # # Assert that size of returned list is as expected - # self.assertEqual(size, exp_size) - # # Assert each expected route is in the list correctly - # self.assertTrue(route1 in result) - # self.assertTrue(route3 in result) - # - # # Check that routes 1 and 2 are returned when specifying - # # tag as '1' - # exp_size = 2 - # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - # tag='1') - # size = len(result) - # # Assert that size of returned list is as expected - # self.assertEqual(size, exp_size) - # # Assert each expected route is in the list correctly - # self.assertTrue(route1 in result) - # self.assertTrue(route2 in result) - # - # # Check that only route 1 is returned when specifying the - # # distance as 10 - # exp_size = 1 - # result = dut.api('staticroute').get_all('1.2.3.0/24', 'Ethernet1', - # distance='10') - # size = len(result) - # # Assert that size of returned list is as expected - # self.assertEqual(size, exp_size) - # # Assert each expected route is in the list correctly - # self.assertTrue(route1 in result) - def test_delete(self): # Validate the delete function returns without an error # when deleting routes with varying parameters included. diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 808c65b..fb53292 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -62,29 +62,33 @@ def test_get(self): ip_dest = '0.0.0.0/0' next_hop = '192.68.1.254' + next_hop_ip = None distance = '1' route = dict(ip_dest=ip_dest, next_hop=next_hop, - next_hop_ip=None, + next_hop_ip=next_hop_ip, distance=distance, tag='0', route_name=None) - result = self.instance.get(ip_dest, next_hop, distance) + result = self.instance.get(ip_dest, next_hop, distance, + next_hop_ip=next_hop_ip) self.assertEqual(result, route) ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' + next_hop_ip = '1.1.1.1' distance = '1' route = dict(ip_dest=ip_dest, next_hop=next_hop, - next_hop_ip='1.1.1.1', + next_hop_ip=next_hop_ip, distance='1', tag='1', route_name='test1') - result = self.instance.get(ip_dest, next_hop, distance) + result = self.instance.get(ip_dest, next_hop, distance, + next_hop_ip=next_hop_ip) self.assertEqual(result, route) - def test_get_all(self): + def test_getall(self): # Test retrieval of all static route entries # Assumes running_config.text file contains the following # ip route specifications, and that no additional routes @@ -96,28 +100,28 @@ def test_get_all(self): # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { - '0.0.0.0/0--192.68.1.254--1': + '0.0.0.0/0--192.68.1.254--None--1': {'ip_dest': '0.0.0.0/0', 'next_hop': '192.68.1.254', 'next_hop_ip': None, 'distance': '1', 'tag': '0', 'route_name': None}, - '1.2.3.0/24--Ethernet1--1': + '1.2.3.0/24--Ethernet1--1.1.1.1--1': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '1', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--10': + '1.2.3.0/24--Ethernet1--1.1.1.1--10': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '10', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--20': + '1.2.3.0/24--Ethernet1--10.1.1.1--20': {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '10.1.1.1', From 75f5e87fb8acc74197e7ebb93271fc8830608290 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 15 Oct 2015 15:22:37 -0400 Subject: [PATCH 32/83] Close #46 - This commit adds a 'mode' keyword argument to the set_members method. The set_members method already looks to get the lacp_mode in use from existing members but ultimately uses 'on' if no other members exist. This commit allows you to supercede that pass in the desired lacp mode. --- pyeapi/api/interfaces.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index ac33d90..d2b0aae 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -590,7 +590,7 @@ def get_members(self, name): return re.findall(r'\b(?!Peer)Ethernet[\d/]*\b', config[0]['result']['output']) - def set_members(self, name, members): + def set_members(self, name, members, mode=None): """Configures the array of member interfaces for the Port-Channel Args: @@ -600,11 +600,17 @@ def set_members(self, name, members): members(list): The list of Ethernet interfaces that should be member interfaces + mode(str): The LACP mode to configure the member interfaces to. + Valid values are 'on, 'passive', 'active' + Returns: True if the operation succeeds otherwise False """ current_members = self.get_members(name) - lacp_mode = self.get_lacp_mode(name) + if mode: + lacp_mode = mode + else: + lacp_mode = self.get_lacp_mode(name) grpid = re.search(r'(\d+)', name).group() commands = list() From 98bd949ea52d163ce76bfbff2223f7a7b9e33bba Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 15 Oct 2015 15:12:31 -0600 Subject: [PATCH 33/83] use a tuple for the dict key instead of formatted string --- pyeapi/api/staticroute.py | 40 ++++++++++++++--------------- test/system/test_api_staticroute.py | 6 ++--- test/unit/test_api_staticroute.py | 9 ++++--- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index f9e985c..7427cf6 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -72,9 +72,9 @@ r'(?: tag (\d+))?' r'(?: name (\S+))?', re.M) -# Define a format for the unique route id -# The four parts in order are ip_dest, next_hop, next_hop_ip, and distance -ROUTE_ID = "%s--%s--%s--%s" +# # Define a format for the unique route id +# # The four parts in order are ip_dest, next_hop, next_hop_ip, and distance +# ROUTE_ID = "%s--%s--%s--%s" class StaticRoute(EntityCollection): @@ -94,7 +94,7 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - distance (int): Administrative distance for this route + distance (string): Administrative distance for this route Returns: dict: An ip route dict object @@ -108,8 +108,8 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): if distance is None: distance = 1 - # Make the unique route_id for the requested route - route_id = ROUTE_ID % (ip_dest, next_hop, next_hop_ip, distance) + # Make the unique route_id tuple for the requested route + route_id = (ip_dest, next_hop, next_hop_ip, distance) # Return the route configuration if found, or return None return self.getall().get(route_id) @@ -139,10 +139,10 @@ def getall(self): route['tag'] = None if match[4] is '' else match[4] route['route_name'] = None if match[5] is '' else match[5] - # Build a unique route_id from the ip_dest, next_hop, and distance - route_id = ROUTE_ID % \ - (route['ip_dest'], route['next_hop'], - route['next_hop_ip'], route['distance']) + # Build a unique route_id tuple from the ip_dest, + # next_hop, and distance + route_id = (route['ip_dest'], route['next_hop'], + route['next_hop_ip'], route['distance']) # Update the routes dict routes.update({route_id: route}) @@ -159,8 +159,8 @@ def create(self, ip_dest, next_hop, next_hop_ip=None, next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface - distance (int): Administrative distance for this route - tag (int): Route tag + distance (string): Administrative distance for this route + tag (string): Route tag route_name (string): Route name Returns: @@ -182,8 +182,8 @@ def delete(self, ip_dest, next_hop, next_hop_ip=None, next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface - distance (int): Administrative distance for this route - tag (int): Route tag + distance (string): Administrative distance for this route + tag (string): Route tag route_name (string): Route name Returns: @@ -205,8 +205,8 @@ def default(self, ip_dest, next_hop, next_hop_ip=None, next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface - distance (int): Administrative distance for this route - tag (int): Route tag + distance (string): Administrative distance for this route + tag (string): Route tag route_name (string): Route name Returns: @@ -228,8 +228,8 @@ def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface - distance (int): Administrative distance for this route - tag (int): Route tag + distance (string): Administrative distance for this route + tag (string): Route tag route_name (string): Route name Returns the ip route command string to be sent to the switch for @@ -260,8 +260,8 @@ def _set_route(self, ip_dest, next_hop, next_hop_ip=None, next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface - distance (int): Administrative distance for this route - tag (int): Route tag + distance (string): Administrative distance for this route + tag (string): Route tag route_name (string): Route name delete (boolean): If true, deletes the specified route instead of creating or setting values for the route diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 34461f0..ff933c9 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -164,21 +164,21 @@ def test_getall(self): dut.config([route1, route2, route3]) routes = { - '1.2.3.0/24--Ethernet1--1.1.1.1--10': + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "10"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '10', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--1.1.1.1--1': + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "1"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '1', 'tag': '1', 'route_name': 'test10'}, - '1.2.3.0/24--Ethernet1--1.1.1.1--2': + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "2"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index fb53292..92758d3 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -100,28 +100,28 @@ def test_getall(self): # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { - '0.0.0.0/0--192.68.1.254--None--1': + ("0.0.0.0/0", "192.68.1.254", None, "1"): {'ip_dest': '0.0.0.0/0', 'next_hop': '192.68.1.254', 'next_hop_ip': None, 'distance': '1', 'tag': '0', 'route_name': None}, - '1.2.3.0/24--Ethernet1--1.1.1.1--1': + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "1"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '1', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--1.1.1.1--10': + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "10"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '10', 'tag': '1', 'route_name': 'test1'}, - '1.2.3.0/24--Ethernet1--10.1.1.1--20': + ("1.2.3.0/24", "Ethernet1", "10.1.1.1", "20"): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '10.1.1.1', @@ -130,6 +130,7 @@ def test_getall(self): 'route_name': 'test1'} } + self.maxDiff = None result = self.instance.getall() self.assertEqual(result, routes) From e9fd938a03150a9de748b62293b1dd2eadb0286c Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 15 Oct 2015 18:26:50 -0600 Subject: [PATCH 34/83] cast distance in key tuple to int --- pyeapi/api/staticroute.py | 4 ++-- test/system/test_api_staticroute.py | 6 +++--- test/unit/test_api_staticroute.py | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 7427cf6..7fd09d9 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -109,7 +109,7 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): distance = 1 # Make the unique route_id tuple for the requested route - route_id = (ip_dest, next_hop, next_hop_ip, distance) + route_id = (ip_dest, next_hop, next_hop_ip, int(distance)) # Return the route configuration if found, or return None return self.getall().get(route_id) @@ -142,7 +142,7 @@ def getall(self): # Build a unique route_id tuple from the ip_dest, # next_hop, and distance route_id = (route['ip_dest'], route['next_hop'], - route['next_hop_ip'], route['distance']) + route['next_hop_ip'], int(route['distance'])) # Update the routes dict routes.update({route_id: route}) diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index ff933c9..da78d0e 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -164,21 +164,21 @@ def test_getall(self): dut.config([route1, route2, route3]) routes = { - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "10"): + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 10): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '10', 'tag': '1', 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "1"): + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 1): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', 'distance': '1', 'tag': '1', 'route_name': 'test10'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "2"): + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 2): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 92758d3..ba42e86 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -100,32 +100,32 @@ def test_getall(self): # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { - ("0.0.0.0/0", "192.68.1.254", None, "1"): + ("0.0.0.0/0", "192.68.1.254", None, 1): {'ip_dest': '0.0.0.0/0', 'next_hop': '192.68.1.254', 'next_hop_ip': None, - 'distance': '1', + 'distance': "1", 'tag': '0', 'route_name': None}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "1"): + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 1): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', - 'distance': '1', + 'distance': "1", 'tag': '1', 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", "10"): + ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 10): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '1.1.1.1', - 'distance': '10', + 'distance': "10", 'tag': '1', 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "10.1.1.1", "20"): + ("1.2.3.0/24", "Ethernet1", "10.1.1.1", 20): {'ip_dest': '1.2.3.0/24', 'next_hop': 'Ethernet1', 'next_hop_ip': '10.1.1.1', - 'distance': '20', + 'distance': "20", 'tag': '1', 'route_name': 'test1'} } From a47f3dc8dc6a59119d20e78ba7c9428d3b0f8494 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 16 Oct 2015 09:12:09 -0600 Subject: [PATCH 35/83] add file placeholders to branch --- pyeapi/api/vrrp.py | 0 test/system/test_api_vrrp.py | 0 test/unit/test_api_vrrp.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyeapi/api/vrrp.py create mode 100644 test/system/test_api_vrrp.py create mode 100644 test/unit/test_api_vrrp.py diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py new file mode 100644 index 0000000..e69de29 diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py new file mode 100644 index 0000000..e69de29 From 4825105974673c40ffd3f5d1127e010614a4fde6 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 16 Oct 2015 14:08:46 -0600 Subject: [PATCH 36/83] test that distance value can be cast as int, clean up documentation --- pyeapi/api/staticroute.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 7fd09d9..14ce4fa 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -72,10 +72,6 @@ r'(?: tag (\d+))?' r'(?: name (\S+))?', re.M) -# # Define a format for the unique route id -# # The four parts in order are ip_dest, next_hop, next_hop_ip, and distance -# ROUTE_ID = "%s--%s--%s--%s" - class StaticRoute(EntityCollection): """The StaticRoute class provides a configuration instance @@ -94,7 +90,7 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - distance (string): Administrative distance for this route + distance (int): Administrative distance for this route Returns: dict: An ip route dict object @@ -108,8 +104,15 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): if distance is None: distance = 1 + # Distance may have been passed in as a string. Convert it + # to an integer if possible. + try: + distance = int(distance) + except ValueError: + return False + # Make the unique route_id tuple for the requested route - route_id = (ip_dest, next_hop, next_hop_ip, int(distance)) + route_id = (ip_dest, next_hop, next_hop_ip, distance) # Return the route configuration if found, or return None return self.getall().get(route_id) From 6e5c24d96f5d6ce7de50499ba941929e315e01d8 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 16 Oct 2015 14:09:33 -0600 Subject: [PATCH 37/83] test that distance value can be cast as int, clean up documentation --- pyeapi/api/staticroute.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 14ce4fa..35a10c5 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -109,6 +109,7 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): try: distance = int(distance) except ValueError: + # XXX what should be done here? return False # Make the unique route_id tuple for the requested route From bd914a13c07c0c95948cad90995fe13d42e47986 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 16 Oct 2015 14:25:31 -0600 Subject: [PATCH 38/83] add set_tag and set_route_name methods --- pyeapi/api/staticroute.py | 60 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 35a10c5..70b106b 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -171,7 +171,7 @@ def create(self, ip_dest, next_hop, next_hop_ip=None, True if the operation succeeds, otherwise False. """ - # Call _set_route with the delete flag set to True + # Call _set_route with delete and default set to False return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) @@ -217,11 +217,67 @@ def default(self, ip_dest, next_hop, next_hop_ip=None, True if the operation succeeds, otherwise False. """ - # Call _set_route with the delete flag set to True + # Call _set_route with the default flag set to True return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name, default=True) + def set_tag(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Set the tag value for the specified route + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name + + Returns: + True if the operation succeeds, otherwise False. + + Notes: + Any existing route_name value must be included in call to + set_tag, otherwise the tag will be reset + by the call to EOS. + """ + + # Call _set_route with the delete flag set to True + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + + def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, + distance=None, tag=None, route_name=None): + """Set the route_name value for the specified route + + Args: + ip_dest (string): The ip address of the destination in the + form of A.B.C.D/E + next_hop (string): The next hop interface or ip address + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name + + Returns: + True if the operation succeeds, otherwise False. + + Notes: + Any existing tag value must be included in call to + set_route_name, otherwise the tag will be reset + by the call to EOS. + """ + + # Call _set_route with the delete flag set to True + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): """Build the EOS command string for ip route interactions. From 3e3888a4f2eaf3e29258e14d4b14efa44578dd93 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 16 Oct 2015 15:07:38 -0600 Subject: [PATCH 39/83] fix ValueError handling --- pyeapi/api/staticroute.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 70b106b..f622bfc 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -109,8 +109,7 @@ def get(self, ip_dest, next_hop, distance, next_hop_ip=None): try: distance = int(distance) except ValueError: - # XXX what should be done here? - return False + raise ValueError("distance parameter must be numerical or None") # Make the unique route_id tuple for the requested route route_id = (ip_dest, next_hop, next_hop_ip, distance) From b821f9aef107ded6e9e8d0ba9878af0b57618ff0 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 20 Oct 2015 11:12:47 -0400 Subject: [PATCH 40/83] Modify API to reset channel-group members --- pyeapi/api/interfaces.py | 26 +++++++++++++++++------- test/system/test_api_interfaces.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index d2b0aae..aeddec5 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -601,19 +601,31 @@ def set_members(self, name, members, mode=None): member interfaces mode(str): The LACP mode to configure the member interfaces to. - Valid values are 'on, 'passive', 'active' + Valid values are 'on, 'passive', 'active'. When there are + existing channel-group members and their lacp mode differs + from this attribute, all of those members will be removed and + then re-added using the specified lacp mode. If this attribute + is omitted, the existing lacp mode will be used for new + member additions. Returns: True if the operation succeeds otherwise False """ + commands = list() + grpid = re.search(r'(\d+)', name).group() current_members = self.get_members(name) - if mode: + lacp_mode = self.get_lacp_mode(name) + if mode and mode != lacp_mode: lacp_mode = mode - else: - lacp_mode = self.get_lacp_mode(name) - grpid = re.search(r'(\d+)', name).group() - - commands = list() + # remove all members from the current port-channel interface + # so that we can change the mode below + for member in current_members: + commands.append('interface %s' % member) + commands.append('no channel-group %s' % grpid) + # add all members back with the specified lacp_mode + for member in current_members: + commands.append('interface %s' % member) + commands.append('channel-group %s mode %s' % (grpid, lacp_mode)) # remove members from the current port-channel interface for member in set(current_members).difference(members): diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 337e760..7f3c638 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -242,6 +242,38 @@ def test_set_members(self): config[0]['output'], 'dut=%s' % dut) + def test_set_members_with_mode(self): + for dut in self.duts: + et1 = random_interface(dut) + et2 = random_interface(dut, exclude=[et1]) + et3 = random_interface(dut, exclude=[et1, et2]) + + dut.config(['no interface Port-Channel1', + 'default interface %s' % et1, + 'interface %s' % et1, + 'channel-group 1 mode on', + 'default interface %s' % et2, + 'interface %s' % et2, + 'channel-group 1 mode on', + 'default interface %s' % et3]) + + api = dut.api('interfaces') + result = api.set_members('Port-Channel1', [et1, et3], mode='active') + self.assertTrue(result, 'dut=%s' % dut) + + cmd = 'show running-config interfaces %s' + + # check to make sure et1 is still in the lag and et3 was + # added to the lag + for interface in [et1, et3]: + config = dut.run_commands(cmd % interface, 'text') + self.assertIn('channel-group 1 mode active', + config[0]['output'], 'dut=%s' % dut) + + # checks to make sure et2 was remvoved form the lag + config = dut.run_commands(cmd % et2, 'text') + self.assertNotIn('channel-group 1 mode on', + config[0]['output'], 'dut=%s' % dut) def test_minimum_links_valid(self): From 05111f7a3d2a8090137d088a82752cfeb9ae5507 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Tue, 20 Oct 2015 11:22:47 -0600 Subject: [PATCH 41/83] additional tests for coverage; fix documentation --- pyeapi/api/staticroute.py | 8 ++++---- test/system/test_api_staticroute.py | 27 +++++++++++++++++++++++++++ test/unit/test_api_staticroute.py | 9 +++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index f622bfc..0b1804d 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -348,11 +348,11 @@ def _set_route(self, ip_dest, next_hop, next_hop_ip=None, def instance(node): - """Returns an instance of Vlans + """Returns an instance of StaticRoute - This method will create and return an instance of the Vlans object passing - the value of API to the object. The instance method is required for the - resource to be autoloaded by the Node object + This method will create and return an instance of the StaticRoute + object passing the value of API to the object. The instance method + is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument passes an instance of Node to the diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index da78d0e..2dc7988 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -257,6 +257,33 @@ def test_default(self): self.assertTrue(result) + def test_set_tag(self): + # Validate the set_tag function returns without an error + # when modifying the tag on an existing route + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 99']) + + result = dut.api('staticroute').set_tag( + '1.2.3.0/24', 'Ethernet1', next_hop_ip='1.1.1.1', + distance=10, tag=3) + self.assertTrue(result) + + def test_set_route_name(self): + # Validate the set_route_name function returns without an error + # when modifying the tag on an existing route + + for dut in self.duts: + dut.config(['no ip routing delete-static-routes', + 'ip routing', + 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test99']) + + result = dut.api('staticroute').set_route_name( + '1.2.3.0/24', 'Ethernet1', next_hop_ip='1.1.1.1', + distance=1, route_name='test3') + self.assertTrue(result) if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index ba42e86..51b25b6 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -54,6 +54,15 @@ def __init__(self, *args, **kwargs): self.instance = pyeapi.api.staticroute.StaticRoute(None) self.config = open(get_fixture('running_config.text')).read() + def test_instance(self): + result = pyeapi.api.staticroute.instance(None) + self.assertIsInstance(result, pyeapi.api.staticroute.StaticRoute) + + def test_get_with_invalid_distance(self): + with self.assertRaises(ValueError): + self.instance.get('1.2.3.0/24', 'Ethernet1', 'z', + next_hop_ip='1.1.1.1') + def test_get(self): # Test retrieval of a specific static route entry # Assumes running_config.text file contains the ip route lines: From cd7432d0cc7ee224269be34c3782e607cfe401ae Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Tue, 20 Oct 2015 11:29:29 -0600 Subject: [PATCH 42/83] testing changes --- pyeapi/api/staticroute.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 0b1804d..d72651e 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -244,10 +244,11 @@ def set_tag(self, ip_dest, next_hop, next_hop_ip=None, by the call to EOS. """ - # Call _set_route with the delete flag set to True - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + # Call _set_route with the new tag information + res = self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + return res def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): @@ -272,10 +273,11 @@ def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, by the call to EOS. """ - # Call _set_route with the delete flag set to True - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + # Call _set_route with the new route_name information + res = self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) + return res def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): From 7ffbcdb8dce55c98e3233558d24c8a9d4c2d27ae Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Tue, 20 Oct 2015 11:39:49 -0600 Subject: [PATCH 43/83] test cases --- pyeapi/api/staticroute.py | 14 ++++++------- test/unit/test_api_staticroute.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index d72651e..bfedf59 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -245,10 +245,9 @@ def set_tag(self, ip_dest, next_hop, next_hop_ip=None, """ # Call _set_route with the new tag information - res = self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) - return res + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): @@ -274,10 +273,9 @@ def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, """ # Call _set_route with the new route_name information - res = self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) - return res + return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, + distance=distance, tag=tag, + route_name=route_name) def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, distance=None, tag=None, route_name=None): diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 51b25b6..3629ba9 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -261,6 +261,40 @@ def test_default(self): self.eapi_positive_config_test(func, cmds) + def test_set_tag(self): + # Test passing in a new tag to the set_tag function + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + next_hop_ip = '1.1.1.1' + distance = 10 + tag = '99' + + func = function('set_tag', ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, tag=tag) + + cmd = "ip route %s %s %s %s tag %s" % \ + (ip_dest, next_hop, next_hop_ip, distance, tag) + + self.eapi_positive_config_test(func, cmd) + + def test_set_route_name(self): + # Test passing in a new tag to the set_tag function + ip_dest = '1.2.3.0/24' + next_hop = 'Ethernet1' + next_hop_ip = '1.1.1.1' + distance = 10 + route_name = 'test99' + + func = function('set_route_name', ip_dest, next_hop, + next_hop_ip=next_hop_ip, + distance=distance, route_name=route_name) + + cmd = "ip route %s %s %s %s name %s" % \ + (ip_dest, next_hop, next_hop_ip, distance, route_name) + + self.eapi_positive_config_test(func, cmd) + if __name__ == '__main__': unittest.main() From daa47b10d7b261ed7c88197a4f10a8c36d515a5d Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 20 Oct 2015 14:14:13 -0400 Subject: [PATCH 44/83] Update set_members to use set_lacp_mode --- pyeapi/api/interfaces.py | 10 +--------- test/unit/test_api_interfaces.py | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index aeddec5..650fd63 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -617,15 +617,7 @@ def set_members(self, name, members, mode=None): lacp_mode = self.get_lacp_mode(name) if mode and mode != lacp_mode: lacp_mode = mode - # remove all members from the current port-channel interface - # so that we can change the mode below - for member in current_members: - commands.append('interface %s' % member) - commands.append('no channel-group %s' % grpid) - # add all members back with the specified lacp_mode - for member in current_members: - commands.append('interface %s' % member) - commands.append('channel-group %s mode %s' % (grpid, lacp_mode)) + self.set_lacp_mode(grpid, lacp_mode) # remove members from the current port-channel interface for member in set(current_members).difference(members): diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index e7d8cbe..5cd01af 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -303,6 +303,27 @@ def test_set_members(self): ['Ethernet5', 'Ethernet7']) self.eapi_positive_config_test(func, cmds) + def test_set_members_same_mode(self): + cmds = ['interface Ethernet6', 'no channel-group 1', + 'interface Ethernet7', 'channel-group 1 mode on'] + func = function('set_members', 'Port-Channel1', + ['Ethernet5', 'Ethernet7']) + self.eapi_positive_config_test(func, cmds) + + def test_set_members_update_mode(self): + cmds = ['interface Ethernet6', 'no channel-group 1', + 'interface Ethernet7', 'channel-group 1 mode active'] + func = function('set_members', 'Port-Channel1', + ['Ethernet5', 'Ethernet7'], mode='active') + self.eapi_positive_config_test(func, cmds) + + def test_set_members_mode_none(self): + cmds = ['interface Ethernet6', 'no channel-group 1', + 'interface Ethernet7', 'channel-group 1 mode on'] + func = function('set_members', 'Port-Channel1', + ['Ethernet5', 'Ethernet7'], mode=None) + self.eapi_positive_config_test(func, cmds) + def test_set_members_no_changes(self): func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet6']) @@ -410,5 +431,3 @@ def test_remove_vtep_from_vlan(self): if __name__ == '__main__': unittest.main() - - From d6d318e6b8120bc09ed45b6d92487af6864bbe2f Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Tue, 20 Oct 2015 15:46:21 -0400 Subject: [PATCH 45/83] Add more unit tests --- pyeapi/api/interfaces.py | 8 +------- test/unit/test_api_interfaces.py | 4 ++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 650fd63..73ad159 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -108,7 +108,7 @@ def getall(self): for name in interfaces_re.findall(self.config): interface = self.get(name) if interface: - response[name] = interface + response['name'] = interface return response def __getattr__(self, name): @@ -172,7 +172,6 @@ def get(self, name): resource.update(self._parse_description(config)) return resource - def _parse_shutdown(self, config): """Scans the specified config block and returns the shutdown value @@ -334,7 +333,6 @@ def get(self, name): resource.update(self._parse_flowcontrol_receive(config)) return resource - def _parse_sflow(self, config): """Scans the specified config block and returns the sflow value @@ -383,7 +381,6 @@ def _parse_flowcontrol_receive(self, config): value = match.group(1) return dict(flowcontrol_receive=value) - def create(self, name): """Creating Ethernet interfaces is currently not supported @@ -549,7 +546,6 @@ def _parse_minimum_links(self, config): value = int(match.group(1)) return dict(minimum_links=value) - def get_lacp_mode(self, name): """Returns the LACP mode for the specified Port-Channel interface @@ -571,8 +567,6 @@ def get_lacp_mode(self, name): self.get_block('^interface %s' % member)) return match.group('value') - - def get_members(self, name): """Returns the member interfaces for the specified Port-Channel diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 5cd01af..9a7f1bb 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -77,6 +77,10 @@ def test_get_interface_ethernet(self): result = self.instance.get('Ethernet1') self.assertEqual(result['type'], 'ethernet') + def test_get_invalid_interface(self): + result = self.instance.get('Foo1') + self.assertEqual(result, None) + def test_proxy_method_success(self): result = self.instance.set_sflow('Ethernet1', True) self.assertTrue(result) From 6088eff52fe398208ba754f3706bfbdc0d02f619 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Oct 2015 11:01:41 -0400 Subject: [PATCH 46/83] Add unittests - coverage now at 99% - Add make coverage_report to see coverage --- Makefile | 4 +++- pyeapi/api/varp.py | 5 +++-- test/unit/test_api_varp.py | 27 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6378f1d..4818b2d 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ VERSION := $(shell cat VERSION) ######################################################## -all: clean check pep8 pyflakes tests +all: clean check pep8 pyflakes tests pep8: -pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225,E261,E241 pyeapi/ test/ @@ -62,3 +62,5 @@ unittest: clean systest: clean $(COVERAGE) run -m unittest discover test/system -v +coverage_report: + $(COVERAGE) report -m diff --git a/pyeapi/api/varp.py b/pyeapi/api/varp.py index c7bc758..688367d 100644 --- a/pyeapi/api/varp.py +++ b/pyeapi/api/varp.py @@ -36,7 +36,7 @@ Example: { - "mac_address": "aaaa.bbbb.cccc", + "mac_address": "aa:bb:cc:dd:ee:ff", "interfaces": { "Vlan100": { "addresses": [ "1.1.1.1", "2.2.2.2"] @@ -104,7 +104,8 @@ def get(self): return resource def _parse_mac_address(self): - mac_address_re = re.compile(r'^ip\svirtual-router\smac-address\s((?:[a-f0-9]{2}:){5}[a-f0-9]{2})$', re.M) + mac_address_re = re.compile(r'^ip\svirtual-router\smac-address\s' + r'((?:[a-f0-9]{2}:){5}[a-f0-9]{2})$', re.M) mac = mac_address_re.search(self.config) mac = mac.group(1) if mac else None return dict(mac_address=mac) diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py index d1f826c..4375502 100644 --- a/test/unit/test_api_varp.py +++ b/test/unit/test_api_varp.py @@ -58,6 +58,16 @@ def test_get(self): self.assertIsNotNone(self.instance.get()['mac_address']) self.assertIsNotNone(self.instance.get()['interfaces']) + def test_get_interfaces_none(self): + self._interfaces = None + result = self.instance.interfaces() + self.assertIsNotNone(result) + + def test_get_interfaces_already_defined(self): + self.instance.interfaces() + result = self.instance.interfaces() + self.assertIsNotNone(result) + def test_set_mac_address_with_value(self): value = 'aa:bb:cc:dd:ee:ff' func = function('set_mac_address', mac_address=value) @@ -75,6 +85,10 @@ def test_set_mac_address_with_no_value(self): cmds = 'no ip virtual-router mac-address' self.eapi_positive_config_test(func, cmds) + def test_set_mac_address_with_bad_value(self): + with self.assertRaises(ValueError): + self.instance.set_mac_address(mac_address='0011.2233.4455') + def test_set_mac_address_with_default(self): func = function('set_mac_address', default=True) cmds = 'default ip virtual-router mac-address' @@ -87,12 +101,25 @@ def __init__(self, *args, **kwargs): self.instance = pyeapi.api.varp.VarpInterfaces(None) self.config = open(get_fixture('running_config.varp')).read() + def test_get_with_no_interface(self): + self.config = "" + self.setUp() + result = self.instance.get('Vlan1000') + self.assertIsNone(result) + def test_add_address_with_value(self): func = function('set_addresses', 'Vlan4001', addresses=['1.1.1.4']) cmds = ['interface Vlan4001', 'no ip virtual-router address 1.1.1.2', 'ip virtual-router address 1.1.1.4'] self.eapi_positive_config_test(func, cmds) + def test_add_address_when_interface_does_not_exist(self): + self.config = "" + self.setUp() + func = function('set_addresses', 'Vlan10', addresses=['1.1.1.4']) + cmds = ['interface Vlan10', 'ip virtual-router address 1.1.1.4'] + self.eapi_positive_config_test(func, cmds) + def test_add_address_with_no_value(self): func = function('set_addresses', 'Vlan4002') cmds = ['interface Vlan4002', 'no ip virtual-router address'] From 064bacaaf0263e5a90f7a6405b5c0d95972d2358 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Oct 2015 16:36:36 -0400 Subject: [PATCH 47/83] Update routemaps object - Code coverage 100% --- pyeapi/api/routemaps.py | 71 ++++++++++++++++---------- test/fixtures/running_config.routemaps | 24 +++++++++ test/unit/test_api_routemaps.py | 33 ++++++++++-- 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index e41216f..34f504d 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -55,46 +55,61 @@ import re -from pyeapi.api import Entity +from pyeapi.api import Entity, EntityCollection -class Routemaps(Entity): +class Routemaps(EntityCollection): """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 get(self, name): + if not self.get_block(r'route-map\s%s\s\w+\s\d+' % name): + return None + + return self._parse_entries(name) 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) + routemaps_re = re.compile(r'^route-map\s(\w+)\s\w+\s\d+$', re.M) + for name in routemaps_re.findall(self.config): + routemap = self.get(name) if routemap: - key = (name, action, seqno) - resources[key] = routemap + resources[name] = 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)) - resource.update(self._parse_description(routemap)) - return resource + def _parse_entries(self, name): + + routemap_re = re.compile(r'^route-map\s%s\s(\w+)\s(\d+)$' + % name, re.M) + entries = list() + for entry in routemap_re.findall(self.config): + resource = dict() + action, seqno = entry + routemap = self.get_block(r'route-map\s%s\s%s\s%s' + % (name, action, seqno)) + + resource = dict(name=name, action=action, seqno=seqno, attr=dict()) + resource['attr'].update(self._parse_match_statements(routemap)) + resource['attr'].update(self._parse_set_statements(routemap)) + resource['attr'].update(self._parse_continue_statement(routemap)) + resource['attr'].update(self._parse_description(routemap)) + entries.append(resource) + + return self._merge_entries(entries) + + def _merge_entries(self, entries): + response = dict() + for e in entries: + action = e['action'] + seqno = int(e['seqno']) + if not response.get(action): + response[action] = dict() + response[action][seqno] = e['attr'] + + return response def _parse_match_statements(self, config): match_re = re.compile(r'^\s+match\s(.+)$', re.M) @@ -196,7 +211,7 @@ def set_match_statements(self, name, action, seqno, statements): True if the operation succeeds otherwise False """ try: - current_statements = self._get_instance(name, action, seqno)['match'] + current_statements = self.get(name)[action][seqno]['match'] except: current_statements = [] @@ -232,7 +247,7 @@ def set_set_statements(self, name, action, seqno, statements): True if the operation succeeds otherwise False """ try: - current_statements = self._get_instance(name, action, seqno)['set'] + current_statements = self.get(name)[action][seqno]['set'] except: current_statements = [] diff --git a/test/fixtures/running_config.routemaps b/test/fixtures/running_config.routemaps index 8570136..e6ccd54 100644 --- a/test/fixtures/running_config.routemaps +++ b/test/fixtures/running_config.routemaps @@ -9,3 +9,27 @@ route-map TEST permit 20 match interface Ethernet2 continue 200 ! +route-map TEST deny 30 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! +route-map FOO deny 20 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! +route-map FOOBAR permit 20 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! +route-map FOOBAR permit 20 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index 25b1aad..93afb65 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -52,13 +52,12 @@ def test_instance(self): 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', - 'description'] + result = self.instance.get('TEST') + keys = ['deny', 'permit'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_get_not_configured(self): - self.assertIsNone(self.instance.get('blah', 'blah', 1)) + self.assertIsNone(self.instance.get('blah')) def test_getall(self): result = self.instance.getall() @@ -110,6 +109,20 @@ def test_set_continue(self): func = function('set_continue', 'TEST', 'permit', 10, 100) self.eapi_positive_config_test(func, cmds) + def test_set_continue_with_invalid_integer(self): + with self.assertRaises(ValueError): + self.instance.set_continue('TEST', 'permit', 10, -1) + + def test_set_continue_to_default(self): + cmds = ['route-map TEST permit 10', 'default continue'] + func = function('set_continue', 'TEST', 'permit', 10, default=True) + self.eapi_positive_config_test(func, cmds) + + def test_negate_continue(self): + cmds = ['route-map TEST permit 10', 'no continue'] + func = function('set_continue', 'TEST', 'permit', 10) + self.eapi_positive_config_test(func, cmds) + def test_set_description_with_value(self): value = random_string() cmds = ['route-map TEST permit 10', 'no description', @@ -117,5 +130,17 @@ def test_set_description_with_value(self): func = function('set_description', 'TEST', 'permit', 10, value) self.eapi_positive_config_test(func, cmds) + def test_negate_description(self): + value = random_string() + cmds = ['route-map TEST permit 10', 'no description'] + func = function('set_description', 'TEST', 'permit', 10) + self.eapi_positive_config_test(func, cmds) + + def test_set_description_with_default(self): + value = random_string() + cmds = ['route-map TEST permit 10', 'default description'] + func = function('set_description', 'TEST', 'permit', 10, default=True) + self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() From dd5312ae0631e68a6f47af860efc4ca1ea88b1cb Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Oct 2015 16:59:58 -0400 Subject: [PATCH 48/83] Fix key name --- pyeapi/api/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 73ad159..4326879 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -108,7 +108,7 @@ def getall(self): for name in interfaces_re.findall(self.config): interface = self.get(name) if interface: - response['name'] = interface + response[name] = interface return response def __getattr__(self, name): From 438393d48e9bde4378520f47556e00035c192b6b Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 21 Oct 2015 15:11:39 -0600 Subject: [PATCH 49/83] use **kwargs instead of named parameters --- pyeapi/api/staticroute.py | 151 ++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 72 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index bfedf59..670cef4 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -152,88 +152,84 @@ def getall(self): return routes - def create(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + def create(self, ip_dest, next_hop, **kwargs): """Create a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with delete and default set to False - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + return self._set_route(ip_dest, next_hop, **kwargs) - def delete(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + def delete(self, ip_dest, next_hop, **kwargs): """Delete a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with the delete flag set to True - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name, delete=True) + kwargs.update({'delete': True}) + return self._set_route(ip_dest, next_hop, **kwargs) - def default(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + def default(self, ip_dest, next_hop, **kwargs): """Set a static route to default (i.e. delete the matching route) Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with the default flag set to True - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name, default=True) + kwargs.update({'default': True}) + return self._set_route(ip_dest, next_hop, **kwargs) - def set_tag(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + def set_tag(self, ip_dest, next_hop, **kwargs): """Set the tag value for the specified route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns: True if the operation succeeds, otherwise False. @@ -245,23 +241,21 @@ def set_tag(self, ip_dest, next_hop, next_hop_ip=None, """ # Call _set_route with the new tag information - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + return self._set_route(ip_dest, next_hop, **kwargs) - def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + def set_route_name(self, ip_dest, next_hop, **kwargs): """Set the route_name value for the specified route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns: True if the operation succeeds, otherwise False. @@ -273,23 +267,23 @@ def set_route_name(self, ip_dest, next_hop, next_hop_ip=None, """ # Call _set_route with the new route_name information - return self._set_route(ip_dest, next_hop, next_hop_ip=next_hop_ip, - distance=distance, tag=tag, - route_name=route_name) + return self._set_route(ip_dest, next_hop, **kwargs) - def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None): + # def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, + # distance=None, tag=None, route_name=None): + def _build_commands(self, ip_dest, next_hop, **kwargs): """Build the EOS command string for ip route interactions. Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name Returns the ip route command string to be sent to the switch for the given set of parameters. @@ -297,6 +291,11 @@ def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, commands = "ip route %s %s" % (ip_dest, next_hop) + next_hop_ip = kwargs.get('next_hop_ip', None) + distance = kwargs.get('distance', None) + tag = kwargs.get('tag', None) + route_name = kwargs.get('route_name', None) + if next_hop_ip is not None: commands += " %s" % next_hop_ip if distance is not None: @@ -308,33 +307,41 @@ def _build_commands(self, ip_dest, next_hop, next_hop_ip=None, return commands - def _set_route(self, ip_dest, next_hop, next_hop_ip=None, - distance=None, tag=None, route_name=None, - delete=False, default=False): + # def _set_route(self, ip_dest, next_hop, next_hop_ip=None, + # distance=None, tag=None, route_name=None, + # delete=False, default=False): + def _set_route(self, ip_dest, next_hop, **kwargs): """Configure a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address - next_hop_ip (string): The next hop address on destination - interface - distance (string): Administrative distance for this route - tag (string): Route tag - route_name (string): Route name - delete (boolean): If true, deletes the specified route - instead of creating or setting values for the route + kwargs (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name + delete (boolean): If true, deletes the specified route + instead of creating or setting values for the route + default (boolean): If true, defaults the specified route + instead of creating or setting values for the route Returns: True if the operation succeeds, otherwise False. """ # Build the route string based on the parameters given - commands = self._build_commands(ip_dest, next_hop, - next_hop_ip=next_hop_ip, - distance=distance, - tag=tag, - route_name=route_name) + # commands = self._build_commands(ip_dest, next_hop, + # next_hop_ip=next_hop_ip, + # distance=distance, + # tag=tag, + # route_name=route_name) + commands = self._build_commands(ip_dest, next_hop, **kwargs) + + delete = kwargs.get('delete', False) + default = kwargs.get('default', False) # Prefix with 'no' if delete is set if delete: From 2750dcb827ac49767ba9f42d19a054e5181e2554 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Oct 2015 17:48:45 -0400 Subject: [PATCH 50/83] Update enable return dict - Fix #35 - Note - the result key was adding to the dict when strict equals True. The response key will be removed in a later version. Please update any scripts that are using response. --- pyeapi/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyeapi/client.py b/pyeapi/client.py index 502b09d..d470302 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -581,10 +581,15 @@ def enable(self, commands, encoding='json', strict=False): raise TypeError('config mode commands not supported') results = list() + # IMPORTANT: There are two keys (response, result) that both + # return the same value. 'response' was originally placed + # 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) for index, response in enumerate(responses): results.append(dict(command=commands[index], + result=response, response=response, encoding=encoding)) else: From 18a268a7f4163f48c8cb944b9c62d999a6b5cf8e Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Oct 2015 17:58:28 -0400 Subject: [PATCH 51/83] Update tests --- test/system/test_api_routemaps.py | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py index 10e1677..b5eb3a7 100644 --- a/test/system/test_api_routemaps.py +++ b/test/system/test_api_routemaps.py @@ -47,13 +47,13 @@ def test_get(self): 'route-map TEST deny 10', 'set weight 100', 'match tag 50']) - response = dut.api('routemaps').get('TEST', 'deny', 10) + response = dut.api('routemaps').get('TEST') 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) + result = dut.api('routemaps').get('TEST') self.assertIsNone(result) def test_getall(self): @@ -65,37 +65,37 @@ def test_getall(self): '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) + self.assertIn(('TEST'), result) + self.assertIn(('TEST2'), 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)) + self.assertIsNone(api.get('TEST')) result = dut.api('routemaps').create('TEST', 'deny', 10) self.assertTrue(result) - self.assertIsNotNone(api.get('TEST', 'deny', 10)) + self.assertIsNotNone(api.get('TEST')) 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)) + self.assertIsNone(api.get('TEST-1')) result = dut.api('routemaps').create('TEST-1', 'deny', 10) self.assertTrue(result) - self.assertIsNotNone(api.get('TEST-1', 'deny', 10)) + self.assertIsNotNone(api.get('TEST-1')) 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)) + self.assertIsNone(api.get('TEST_1')) result = dut.api('routemaps').create('TEST_1', 'deny', 10) self.assertTrue(result) - self.assertIsNotNone(api.get('TEST_1', 'deny', 10)) + self.assertIsNotNone(api.get('TEST_1')) dut.config(['no route-map TEST_1 deny 10']) def test_delete(self): @@ -104,10 +104,10 @@ def test_delete(self): 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') - self.assertIsNotNone(api.get('TEST', 'deny', 10)) + self.assertIsNotNone(api.get('TEST')) result = dut.api('routemaps').delete('TEST', 'deny', 10) self.assertTrue(result) - self.assertIsNone(api.get('TEST', 'deny', 10)) + self.assertIsNone(api.get('TEST')) def test_default(self): for dut in self.duts: @@ -115,10 +115,10 @@ def test_default(self): 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') - self.assertIsNotNone(api.get('TEST', 'deny', 10)) + self.assertIsNotNone(api.get('TEST')) result = dut.api('routemaps').default('TEST', 'deny', 10) self.assertTrue(result) - self.assertIsNone(api.get('TEST', 'deny', 10)) + self.assertIsNone(api.get('TEST')) def test_set_description(self): for dut in self.duts: @@ -233,7 +233,7 @@ def test_set_continue(self): 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']) + self.assertEqual(100, api.get('TEST')['deny'][10]['continue']) def test_update_continue(self): for dut in self.duts: @@ -245,7 +245,7 @@ def test_update_continue(self): 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']) + self.assertEqual(60, api.get('TEST')['deny'][10]['continue']) def test_default_continue(self): for dut in self.duts: @@ -254,11 +254,11 @@ def test_default_continue(self): 'continue 100']) api = dut.api('routemaps') self.assertIn('continue 100', - api.get_block('route-map TEST deny 10')) + 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']) + self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) def test_negate_continue(self): for dut in self.duts: @@ -267,11 +267,11 @@ def test_negate_continue(self): 'continue 100']) api = dut.api('routemaps') self.assertIn('continue 100', - api.get_block('route-map TEST deny 10')) + 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']) + self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) if __name__ == '__main__': unittest.main() From c8e4e20ef3cb94a48d97e4076deff391fe5a246e Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 12:38:33 -0400 Subject: [PATCH 52/83] Add autogenerator script to list all modules - Break modules into client and api modules --- .gitignore | 1 + docs/description.rst | 8 ++-- docs/generate_modules.py | 86 ++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 8 ++-- docs/modules.rst | 7 ++-- docs/pyeapi.api.rst | 70 -------------------------------- docs/pyeapi.rst | 45 --------------------- docs/quickstart.rst | 17 ++++---- setup.py | 17 ++++++++ 9 files changed, 123 insertions(+), 136 deletions(-) create mode 100644 docs/generate_modules.py delete mode 100644 docs/pyeapi.api.rst delete mode 100644 docs/pyeapi.rst diff --git a/.gitignore b/.gitignore index e850f36..6e1c6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/*_modules/ # PyBuilder target/ diff --git a/docs/description.rst b/docs/description.rst index 454701c..966ff23 100644 --- a/docs/description.rst +++ b/docs/description.rst @@ -1,5 +1,5 @@ -Python Client for eAPI -====================== +Introduction +============ The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring @@ -10,9 +10,9 @@ The Python library can be used to communicate with EOS either locally to specify one or more nodes and connection profiles. The pyeapi library also provides an API layer for building native Python -objects to interact with the destination nodes. The API layer is a convienent +objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for -developing custom implemenations. +developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort diff --git a/docs/generate_modules.py b/docs/generate_modules.py new file mode 100644 index 0000000..85fcdf4 --- /dev/null +++ b/docs/generate_modules.py @@ -0,0 +1,86 @@ +#!/usr/bin/python + +from os import path +from os import listdir +from os.path import isfile, join +import pprint as pp + +HERE = path.abspath(path.dirname(__file__)) +MODULES_PATH = '%s/../pyeapi/' % HERE +AUTOGEN = '.. This file has been autogenerated by generate_modules.py\n\n' + + +def get_module_names(p): + '''Accepts a path to search for modules. The method will filter on files + that end in .pyc or files that start with __. + + Arguments: + p (string): The path to search + Returns: + list of file names + ''' + mods = list() + mods = [f.split('.')[0] for f in listdir(p) + if isfile(join(p, f)) and not f.endswith('.pyc') and not f.startswith('__')] + return mods + +def process_modules(modules): + '''Accepts dictionary of 'client' and 'api' modules and creates + the corresponding files. + ''' + for mod in modules['client']: + write_module_file(mod, '%s/client_modules' % HERE, 'pyeapi') + for mod in modules['api']: + write_module_file(mod, '%s/api_modules' % HERE, 'pyeapi.api') + + create_index(modules) + +def create_index(modules): + '''This takes a dict of modules and created the RST index file.''' + for key in modules.keys(): + file_path = join(HERE, '%s_modules/_list_of_modules.rst' % key) + list_file = open(file_path, 'w') + + # Write the generic header + list_file.write('%s\n' % AUTOGEN) + list_file.write('%s\n' % key.title()) + list_file.write('=' * len(key)) + list_file.write('\n\n') + list_file.write('.. toctree::\n') + list_file.write(' :maxdepth: 2\n\n') + + for module in modules[key]: + list_file.write(' %s\n' % module) + + +def write_module_file(name, path, package): + '''Creates an RST file for the module name passed in. It places it in the + path defined + ''' + file_path = join(path, '%s.rst' % name) + mod_file = open(file_path, 'w') + + mod_file.write('%s\n' % AUTOGEN) + mod_file.write('%s\n' % name.title()) + mod_file.write('=' * len(name)) + mod_file.write('\n\n') + mod_file.write('.. toctree::\n') + mod_file.write(' :maxdepth: 1\n\n') + mod_file.write('.. automodule:: %s.%s\n' % (package, name)) + mod_file.write(' :members:\n') + mod_file.write(' :undoc-members:\n') + mod_file.write(' :show-inheritance:\n') + +def main(): + modules = dict(client=None, api=None) + modules['client'] = get_module_names(MODULES_PATH) + modules['api'] = get_module_names('%s/api' % MODULES_PATH) + process_modules(modules) + + + + + + +if __name__ == '__main__': + main() diff --git a/docs/index.rst b/docs/index.rst index df6420a..4cd986b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,10 +9,10 @@ Welcome to Python Client for eAPI's documentation! Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 3 - pyeapi - pyeapi.api + description + quickstart modules license @@ -23,5 +23,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` - diff --git a/docs/modules.rst b/docs/modules.rst index 4e8c689..edac209 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,7 +1,8 @@ -pyeapi -====== +Modules +======= .. toctree:: :maxdepth: 4 - pyeapi + client_modules/_list_of_modules + api_modules/_list_of_modules diff --git a/docs/pyeapi.api.rst b/docs/pyeapi.api.rst deleted file mode 100644 index 55566e3..0000000 --- a/docs/pyeapi.api.rst +++ /dev/null @@ -1,70 +0,0 @@ -pyeapi.api package -================== - -Submodules ----------- - -pyeapi.api.abstract module --------------------------- - -.. automodule:: pyeapi.api.abstract - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.interfaces module ----------------------------- - -.. automodule:: pyeapi.api.interfaces - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.ipinterfaces module ------------------------------- - -.. automodule:: pyeapi.api.ipinterfaces - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.spanningtree module ------------------------------- - -.. automodule:: pyeapi.api.spanningtree - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.stp module ---------------------- - -.. automodule:: pyeapi.api.stp - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.switchports module ------------------------------ - -.. automodule:: pyeapi.api.switchports - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.vlans module ------------------------ - -.. automodule:: pyeapi.api.vlans - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyeapi.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pyeapi.rst b/docs/pyeapi.rst deleted file mode 100644 index 958d2a2..0000000 --- a/docs/pyeapi.rst +++ /dev/null @@ -1,45 +0,0 @@ -pyeapi package -============== - -Subpackages ------------ - -.. toctree:: - - pyeapi.api - -Submodules ----------- - -pyeapi.client module --------------------- - -.. automodule:: pyeapi.client - :members: - :undoc-members: - :show-inheritance: - -pyeapi.eapilib module ---------------------- - -.. automodule:: pyeapi.eapilib - :members: - :undoc-members: - :show-inheritance: - -pyeapi.utils module -------------------- - -.. automodule:: pyeapi.utils - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyeapi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 79aed30..59bfb8b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,5 +1,5 @@ -Python Client for eAPI -====================== +Quickstart +========== The Python library for Arista's eAPI command API implementation provides a client API work using eAPI and communicating with EOS nodes. The Python @@ -27,7 +27,7 @@ through Github issues. In order to use pyeapi, the EOS command API must be enabled using ``management api http-commands`` configuration mode. This library supports eAPI calls over both HTTP and UNIX Domain Sockets. Once the command API is enabled on the -destination node, create a configuration file with the node properities. +destination node, create a configuration file with the node properities. **Note:** The default search path for the conf file is ``~/.eapi.conf`` followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting @@ -52,7 +52,7 @@ The following configuration options are available for defining node entries: * socket (available in EOS 4.14.5 or later) * http_local (available in EOS 4.14.5 or later) * http - * https + * https * **port** - Configures the port to use for the eAPI connection. A default port is used if this parameter is absent, based on the transport setting using the following values: @@ -65,7 +65,7 @@ using the following values: _Note:_ See the EOS User Manual found at arista.com for more details on configuring eAPI values. -All configuration values are optional. +All configuration values are optional. ``` [connection:veos01] @@ -108,7 +108,7 @@ values in the conf file will overwrite the default. ## Using pyeapi The Python client for eAPI was designed to be easy to use and implement for writing tools and applications that interface with the Arista EOS management -plane. +plane. ### Creating a connection and sending commands Once EOS is configured properly and the config file created, getting started @@ -195,7 +195,7 @@ and uploaded to PyPi. # Testing The pyeapi library provides both unit tests and system tests. The unit tests can be run without an EOS node. To run the system tests, you will need to -update the ``dut.conf`` file found in test/fixtures. +update the ``dut.conf`` file found in test/fixtures. * To run the unit tests, simply run ``make unittest`` from the root of the pyeapi source folder @@ -209,9 +209,8 @@ update the ``dut.conf`` file found in test/fixtures. Contributing pull requests are gladly welcomed for this repository. Please note that all contributions that modify the library behavior require -corresponding test cases otherwise the pull request will be rejected. +corresponding test cases otherwise the pull request will be rejected. # License New BSD, See [LICENSE](LICENSE) file - diff --git a/setup.py b/setup.py index 257384b..0b9d4e2 100644 --- a/setup.py +++ b/setup.py @@ -71,3 +71,20 @@ 'test': ['coverage', 'mock'], }, ) + +def install(): + if "install" in sys.argv: + return True + else: + return False + +# Use the following to dynamically build pyeapi module documentation +if install() and os.environ.get('READTHEDOCS'): + print 'This method is only called by READTHEDOCS.' + from subprocess import Popen + proc = Popen(['make', 'modules'], cwd='docs/') + (_, err) = proc.communicate() + return_code = proc.wait() + + if return_code or err: + raise ('Failed to make modules.(%s:%s)' % (return_code, err)) From a3afd4968b3b346944cfa817ea616f649d172ea0 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 22 Oct 2015 12:02:44 -0600 Subject: [PATCH 53/83] fix get() to take single 'name' parameter; reformat output from get() and getall() --- pyeapi/api/staticroute.py | 120 ++++++++++++++++---------- test/system/test_api_staticroute.py | 66 ++++++++------- test/unit/test_api_staticroute.py | 126 +++++++++++++++------------- 3 files changed, 176 insertions(+), 136 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 670cef4..3a60364 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -82,48 +82,69 @@ class StaticRoute(EntityCollection): def __str__(self): return 'StaticRoute' - def get(self, ip_dest, next_hop, distance, next_hop_ip=None): - """Retrieves the ip route information for the route specified - by the ip_dest, next_hop, and distance parameters + def get(self, name): + """Retrieves the ip route information for the destination + ip address specified. Args: - ip_dest (string): The ip address of the destination in the + name (string): The ip address of the destination in the form of A.B.C.D/E - next_hop (string): The next hop interface or ip address - distance (int): Administrative distance for this route Returns: - dict: An ip route dict object + dict: An dict object of static route entries in the form + { ip_dest: + { next_hop: + { next_hop_ip: + { distance: + { 'tag': tag, + 'route_name': route_name + } + } + } + } + } + + If the ip address specified does not have any associated + static routes, then None is returned. - If the unique route specified by the ip_dest, next_hop, and - distance does not exist, then None is returned. + Notes: + The keys ip_dest, next_hop, next_hop_ip, and distance in + the returned dictionary are the values of those components + of the ip route specification. If a route does not contain + a next_hop_ip, then that key value will be set as 'None'. """ - # If distance is None, then set to 1 to match what EOS will - # do when distance is not specified. - if distance is None: - distance = 1 - - # Distance may have been passed in as a string. Convert it - # to an integer if possible. - try: - distance = int(distance) - except ValueError: - raise ValueError("distance parameter must be numerical or None") - - # Make the unique route_id tuple for the requested route - route_id = (ip_dest, next_hop, next_hop_ip, distance) - - # Return the route configuration if found, or return None - return self.getall().get(route_id) + # Return the route configurations for the specified ip address + routes = self.getall().get(name) + if routes: + return {name: routes} + return None def getall(self): """Return all ip routes configured on the switch as a resource dict Returns: - dict: A dict of unique ip route names with a nested route - dict object. The unique name is built with the ip destination, - next hop, and distance values for the route. + dict: An dict object of static route entries in the form + { ip_dest: + { next_hop: + { next_hop_ip: + { distance: + { 'tag': tag, + 'route_name': route_name + } + } + } + } + } + + If the ip address specified does not have any associated + static routes, then None is returned. + + Notes: + The keys ip_dest, next_hop, next_hop_ip, and distance in + the returned dictionary are the values of those components + of the ip route specification. If a route does not contain + a next_hop_ip, then that key value will be set as 'None'. """ # Find all the ip routes in the config @@ -132,23 +153,28 @@ def getall(self): # Parse the routes and add them to the routes dict routes = dict() for match in matches: - # Set the route dict to the returned values, replacing - # empty strings with None - route = dict() - route['ip_dest'] = match[0] - route['next_hop'] = match[1] - route['next_hop_ip'] = None if match[2] is '' else match[2] - route['distance'] = match[3] - route['tag'] = None if match[4] is '' else match[4] - route['route_name'] = None if match[5] is '' else match[5] - - # Build a unique route_id tuple from the ip_dest, - # next_hop, and distance - route_id = (route['ip_dest'], route['next_hop'], - route['next_hop_ip'], int(route['distance'])) - - # Update the routes dict - routes.update({route_id: route}) + + # Get the four identifying components + ip_dest = match[0] + next_hop = match[1] + next_hop_ip = None if match[2] is '' else match[2] + distance = int(match[3]) + + # Create the data dict with the remaining components + data = {} + data['tag'] = None if match[4] is '' else int(match[4]) + data['route_name'] = None if match[5] is '' else match[5] + + # Build the complete dict entry from the four components + # and the data. + # temp_dict = parent_dict[key] = parent_dict.get(key, {}) + # This creates the keyed dict in the parent_dict if it doesn't + # exist, or reuses the existing keyed dict. + # The temp_dict is used to make things more readable. + ip_dict = routes[ip_dest] = routes.get(ip_dest, {}) + nh_dict = ip_dict[next_hop] = ip_dict.get(next_hop, {}) + nhip_dict = nh_dict[next_hop_ip] = nh_dict.get(next_hop_ip, {}) + nhip_dict[distance] = data return routes @@ -351,6 +377,8 @@ def _set_route(self, ip_dest, next_hop, **kwargs): if default: commands = "default " + commands + import syslog + syslog.syslog("XXX calling: %s\n" % commands) return self.configure(commands) diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 2dc7988..5acf7e0 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -118,21 +118,28 @@ def test_get(self): ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' next_hop_ip = '1.1.1.1' - distance = '1' - tag = '1' + distance = 1 + tag = 1 route_name = 'test1' + cmd = "ip route %s %s %s %s tag %s name %s" % \ (ip_dest, next_hop, next_hop_ip, distance, tag, route_name) dut.config([cmd]) - route = {'ip_dest': ip_dest, - 'next_hop': next_hop, - 'next_hop_ip': next_hop_ip, - 'distance': distance, - 'tag': tag, - 'route_name': route_name} - result = dut.api('staticroute').get(ip_dest, next_hop, distance, - next_hop_ip=next_hop_ip) + route = { + ip_dest: { + next_hop: { + next_hop_ip: { + distance: { + 'tag': tag, + 'route_name': route_name + } + } + } + } + } + + result = dut.api('staticroute').get(ip_dest) # Make sure the funtion returns a true result (match found) self.assertTrue(result) @@ -164,27 +171,24 @@ def test_getall(self): dut.config([route1, route2, route3]) routes = { - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 10): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '1.1.1.1', - 'distance': '10', - 'tag': '1', - 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 1): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '1.1.1.1', - 'distance': '1', - 'tag': '1', - 'route_name': 'test10'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 2): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '1.1.1.1', - 'distance': '2', - 'tag': '10', - 'route_name': 'test1'} + '1.2.3.0/24': { + 'Ethernet1': { + '1.1.1.1': { + 10: { + 'tag': 1, + 'route_name': 'test1' + }, + 1: { + 'tag': 1, + 'route_name': 'test10' + }, + 2: { + 'tag': 10, + 'route_name': 'test1' + } + } + } + } } # Get the list of ip routes from the switch diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 3629ba9..66f3bc6 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -58,44 +58,55 @@ def test_instance(self): result = pyeapi.api.staticroute.instance(None) self.assertIsInstance(result, pyeapi.api.staticroute.StaticRoute) - def test_get_with_invalid_distance(self): - with self.assertRaises(ValueError): - self.instance.get('1.2.3.0/24', 'Ethernet1', 'z', - next_hop_ip='1.1.1.1') - def test_get(self): # Test retrieval of a specific static route entry - # Assumes running_config.text file contains the ip route lines: + # Assumes running_config.text file contains the following + # ip route specifications, and that no additional routes + # are specified. + # ip route 0.0.0.0/0 192.68.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 + # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 + # Get the route(s) for ip_dest 0.0.0.0/24 ip_dest = '0.0.0.0/0' - next_hop = '192.68.1.254' - next_hop_ip = None - distance = '1' - route = dict(ip_dest=ip_dest, - next_hop=next_hop, - next_hop_ip=next_hop_ip, - distance=distance, - tag='0', - route_name=None) - result = self.instance.get(ip_dest, next_hop, distance, - next_hop_ip=next_hop_ip) - self.assertEqual(result, route) + routes = { + ip_dest: { + '192.68.1.254': { + None: { + 1: {'route_name': None, + 'tag': 0} + } + } + }, + } + result = self.instance.get(ip_dest) + self.assertEqual(result, routes) + # Get the route(s) for ip_dest 1.2.3.0/24 ip_dest = '1.2.3.0/24' - next_hop = 'Ethernet1' - next_hop_ip = '1.1.1.1' - distance = '1' - route = dict(ip_dest=ip_dest, - next_hop=next_hop, - next_hop_ip=next_hop_ip, - distance='1', - tag='1', - route_name='test1') - result = self.instance.get(ip_dest, next_hop, distance, - next_hop_ip=next_hop_ip) - self.assertEqual(result, route) + routes = { + ip_dest: { + 'Ethernet1': { + '1.1.1.1': { + 1: { + 'route_name': 'test1', + 'tag': 1}, + 10: { + 'route_name': 'test1', + 'tag': 1} + }, + '10.1.1.1': { + 20: { + 'route_name': 'test1', + 'tag': 1} + } + } + } + } + result = self.instance.get(ip_dest) + self.assertEqual(result, routes) def test_getall(self): # Test retrieval of all static route entries @@ -109,35 +120,32 @@ def test_getall(self): # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { - ("0.0.0.0/0", "192.68.1.254", None, 1): - {'ip_dest': '0.0.0.0/0', - 'next_hop': '192.68.1.254', - 'next_hop_ip': None, - 'distance': "1", - 'tag': '0', - 'route_name': None}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 1): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '1.1.1.1', - 'distance': "1", - 'tag': '1', - 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "1.1.1.1", 10): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '1.1.1.1', - 'distance': "10", - 'tag': '1', - 'route_name': 'test1'}, - ("1.2.3.0/24", "Ethernet1", "10.1.1.1", 20): - {'ip_dest': '1.2.3.0/24', - 'next_hop': 'Ethernet1', - 'next_hop_ip': '10.1.1.1', - 'distance': "20", - 'tag': '1', - 'route_name': 'test1'} - } + '0.0.0.0/0': { + '192.68.1.254': { + None: { + 1: {'route_name': None, + 'tag': 0} + } + } + }, + '1.2.3.0/24': { + 'Ethernet1': { + '1.1.1.1': { + 1: { + 'route_name': 'test1', + 'tag': 1}, + 10: { + 'route_name': 'test1', + 'tag': 1} + }, + '10.1.1.1': { + 20: { + 'route_name': 'test1', + 'tag': 1} + } + } + } + } self.maxDiff = None result = self.instance.getall() From 27411309ea1b1939a506d8f6abe63317b160d803 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 14:07:11 -0400 Subject: [PATCH 54/83] Fix docstrings for readthedocs --- docs/conf.py | 2 + docs/quickstart.rst | 143 +++++++++++++++++++------------------ pyeapi/api/interfaces.py | 55 +++++++------- pyeapi/api/ipinterfaces.py | 15 ++-- pyeapi/api/routemaps.py | 49 ++++++++++--- pyeapi/api/spanningtree.py | 2 +- pyeapi/api/stp.py | 14 ++-- pyeapi/api/varp.py | 42 ++++------- pyeapi/client.py | 1 + 9 files changed, 170 insertions(+), 153 deletions(-) mode change 100644 => 100755 docs/conf.py diff --git a/docs/conf.py b/docs/conf.py old mode 100644 new mode 100755 index 205c966..c0540be --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,8 @@ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', + 'sphinx.ext.doctest', + 'sphinxcontrib.napoleon' ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 59bfb8b..0f29ce9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -49,17 +49,17 @@ The following configuration options are available for defining node entries: * **enablepwd** - The enable mode password if required by the destination node * **transport** - Configures the type of transport connection to use. The default value is _https_. Valid values are: - * socket (available in EOS 4.14.5 or later) - * http_local (available in EOS 4.14.5 or later) - * http - * https + * socket (available in EOS 4.14.5 or later) + * http_local (available in EOS 4.14.5 or later) + * http + * https * **port** - Configures the port to use for the eAPI connection. A default port is used if this parameter is absent, based on the transport setting -using the following values: - * transport: http, default port: 80 - * transport: https, deafult port: 443 - * transport: https_local, default port: 8080 - * transport: socket, default port: n/a + using the following values: + * transport: http, default port: 80 + * transport: https, deafult port: 443 + * transport: https_local, default port: 8080 + * transport: socket, default port: n/a _Note:_ See the EOS User Manual found at arista.com for more details on @@ -67,29 +67,30 @@ configuring eAPI values. All configuration values are optional. -``` -[connection:veos01] -username: eapi -password: password -transport: http +.. code-block:: console -[connection:veos02] -transport: http + [connection:veos01] + username: eapi + password: password + transport: http -[connection:veos03] -transport: socket + [connection:veos02] + transport: http -[connection:veos04] -host: 172.16.10.1 -username: eapi -password: password -enablepwd: itsasecret -port: 1234 -transport: https + [connection:veos03] + transport: socket + + [connection:veos04] + host: 172.16.10.1 + username: eapi + password: password + enablepwd: itsasecret + port: 1234 + transport: https + + [connection:localhost] + transport: http_local -[connection:localhost] -transport: http_local -``` The above example shows different ways to define EOS node connections. All configuration options will attempt to use default values if not explicitly @@ -115,33 +116,34 @@ Once EOS is configured properly and the config file created, getting started with a connection to EOS is simple. Below demonstrates a basic connection using pyeapi. For more examples, please see the examples folder. -``` -# start by importing the library -import pyeapi +.. code-block:: python + + # start by importing the library + import pyeapi -# create a node object by specifying the node to work with -node = pyeapi.connect_to('veos01') + # create a node object by specifying the node to work with + node = pyeapi.connect_to('veos01') -# send one or more commands to the node -node.enable('show hostname') -[{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': -u'veos01.arista.com'}, 'encoding': 'json'}] + # send one or more commands to the node + node.enable('show hostname') + [{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': + u'veos01.arista.com'}, 'encoding': 'json'}] -# use the config method to send configuration commands -node.config('hostname veos01') -[{}] + # use the config method to send configuration commands + node.config('hostname veos01') + [{}] -# multiple commands can be sent by using a list (works for both enable or -config) -node.config(['interface Ethernet1', 'description foo']) -[{}, {}] + # multiple commands can be sent by using a list (works for both enable or + config) + node.config(['interface Ethernet1', 'description foo']) + [{}, {}] -# return the running or startup configuration from the node (output omitted for -brevity) -node.running_config + # return the running or startup configuration from the node (output omitted for + brevity) + node.running_config + + node.startup_config -node.startup_config -``` ### Using the API @@ -150,32 +152,33 @@ eAPI as well as an API for working directly with EOS resources. The API is designed to be easy and straightforward to use yet also extensible. Below is an example of working with the ``vlans`` API -``` -# create a connection to the node -import pyeapi -node = pyeapi.connect_to('veos01') +.. code-block:: python + + # create a connection to the node + import pyeapi + node = pyeapi.connect_to('veos01') + + # get the instance of the API (in this case vlans) + vlans = node.api('vlans') -# get the instance of the API (in this case vlans) -vlans = node.api('vlans') + # return all vlans from the node + vlans.getall() + {'1': {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []}, + '10': {'state': 'active', 'name': 'VLAN0010', 'vlan_id': 10, 'trunk_groups': + []}} -# return all vlans from the node -vlans.getall() -{'1': {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []}, -'10': {'state': 'active', 'name': 'VLAN0010', 'vlan_id': 10, 'trunk_groups': -[]}} + # return a specific vlan from the node + vlans.get(1) + {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} -# return a specific vlan from the node -vlans.get(1) -{'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} + # add a new vlan to the node + vlans.create(100) + True -# add a new vlan to the node -vlans.create(100) -True + # set the new vlan name + vlans.set_name(100, 'foo') + True -# set the new vlan name -vlans.set_name(100, 'foo') -True -``` All API implementations developed by Arista EOS+ CS are found in the pyeapi/api folder. See the examples folder for additional examples. diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 4326879..9228e23 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -92,15 +92,15 @@ def get(self, name): def getall(self): """Returns all interfaces in a dict object. - Example: - { - "Ethernet1": {...}, - "Ethernet2": {...} - } - Returns: A Python dictionary object containing all interface - configuration indexed by interface name + configuration indexed by interface name:: + + { + "Ethernet1": {...}, + "Ethernet2": {...} + } + """ interfaces_re = re.compile(r'(?<=^interface\s)(.+)$', re.M) @@ -303,15 +303,6 @@ def __str__(self): def get(self, name): """Returns an interface as a set of key/value pairs - Example: - { - "name": , - "type": "ethernet", - "sflow": [true, false], - "flowcontrol_send": [on, off], - "flowcontrol_receive": [on, off] - } - Args: name (string): the interface identifier to retrieve the from the configuration @@ -319,7 +310,15 @@ def get(self, name): Returns: A Python dictionary object of key/value pairs that represent the current configuration for the specified node. If the - specified interface name does not exist, then None is returned. + specified interface name does not exist, then None is returned:: + + { + "name": , + "type": "ethernet", + "sflow": [true, false], + "flowcontrol_send": [on, off], + "flowcontrol_receive": [on, off] + } """ config = self.get_block('^interface %s' % name) @@ -509,23 +508,23 @@ def __str__(self): def get(self, name): """Returns a Port-Channel interface as a set of key/value pairs - Example: - { - "name": , - "type": "portchannel", - "members": , - "minimum_links: , - "lacp_mode": [on, active, passive] - } - Args: name (str): The interface identifier to retrieve from the running-configuration Returns: A Python dictionary object of key/value pairs that represents - the interface configuration. If the specified interface - does not exist, then None is returned + the interface configuration. If the specified interface + does not exist, then None is returned:: + + { + "name": , + "type": "portchannel", + "members": , + "minimum_links: , + "lacp_mode": [on, active, passive] + } + """ config = self.get_block('^interface %s' % name) if not config: diff --git a/pyeapi/api/ipinterfaces.py b/pyeapi/api/ipinterfaces.py index d53a359..9b8d750 100644 --- a/pyeapi/api/ipinterfaces.py +++ b/pyeapi/api/ipinterfaces.py @@ -120,16 +120,15 @@ def _parse_mtu(self, config): def getall(self): """ Returns all of the IP interfaces found in the running-config - Example: - { - 'Ethernet1': {...}, - 'Ethernet2': {...} - } - Returns: A Python dictionary object of key/value pairs keyed by interface - name that represents all of the IP interfaces on - the current node. + name that represents all of the IP interfaces on + the current node:: + + { + 'Ethernet1': {...}, + 'Ethernet2': {...} + } """ interfaces_re = re.compile(r'^interface\s(.+)', re.M) diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index 34f504d..b1b10d7 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -36,17 +36,6 @@ * Routemaps - Configures routemaps in EOS -Routemaps Attributes: - name (string): The name given to the routemap clause - 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 - 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 @@ -66,6 +55,44 @@ class Routemaps(EntityCollection): """ def get(self, name): + """Provides a method to retrieve all routemap configuration + related to the name attribute. + + Args: + name (string): The name of the routemap. + + Returns: + None if the specified routemap does not exists. If the routermap + exists a dictionary will be provided as follows:: + + { + 'deny': { + 30: { + 'continue': 200, + 'description': None, + 'match': ['as 2000', + 'source-protocol ospf', + 'interface Ethernet2'], + 'set': [] + } + }, + 'permit': { + 10: { + 'continue': 100, + 'description': None, + 'match': ['interface Ethernet1'], + 'set': ['tag 50']}, + 20: { + 'continue': 200, + 'description': None, + 'match': ['as 2000', + 'source-protocol ospf', + 'interface Ethernet2'], + 'set': [] + } + } + } + """ if not self.get_block(r'route-map\s%s\s\w+\s\d+' % name): return None diff --git a/pyeapi/api/spanningtree.py b/pyeapi/api/spanningtree.py index be66be1..8d3eb5b 100644 --- a/pyeapi/api/spanningtree.py +++ b/pyeapi/api/spanningtree.py @@ -31,7 +31,7 @@ # import warnings -warnings.warn("Api module spanningtree is dereciated. Please update api " +warnings.warn("Api module spanningtree is depricated. Please update api " "calls to use stp instead") from pyeapi.api.stp import instance # flake8: noqa diff --git a/pyeapi/api/stp.py b/pyeapi/api/stp.py index 88b2755..03db8fa 100644 --- a/pyeapi/api/stp.py +++ b/pyeapi/api/stp.py @@ -98,20 +98,18 @@ def get(self): interfaces and instances. See the StpInterfaces and StpInstances classes for the key/value pair definitions. - Example - { - "mode": [mstp, none], - "interfaces": {...}, - "instances": {...} - } - Note: See the individual classes for detailed message structures Returns: A Python dictionary object of key/value pairs the represent - the entire supported spanning-tree configuration + the entire supported spanning-tree configuration:: + { + "mode": [mstp, none], + "interfaces": {...}, + "instances": {...} + } """ return dict(interfaces=self.interfaces.getall(), instances=self.instances.getall()) diff --git a/pyeapi/api/varp.py b/pyeapi/api/varp.py index 688367d..b3ef80f 100644 --- a/pyeapi/api/varp.py +++ b/pyeapi/api/varp.py @@ -34,18 +34,7 @@ This module provides an API for configuring VARP resources using EOS and eAPI. -Example: - { - "mac_address": "aa:bb:cc:dd:ee:ff", - "interfaces": { - "Vlan100": { - "addresses": [ "1.1.1.1", "2.2.2.2"] - }, - "Vlan200": [...] - } - } - -Parameters: +Arguments: name (string): The interface name the configuration is in reference to. The interface name is the full interface identifier @@ -85,18 +74,18 @@ def get(self): Return: A Python dictionary object of key/value pairs that represents - the current configuration of the node. If the specified - interface does not exist then None is returned. - Example: - { - "mac_address": "aa:bb:cc:dd:ee:ff", - "interfaces": { - "Vlan100": { - "addresses": [ "1.1.1.1", "2.2.2.2"] - }, - "Vlan200": [...] + the current configuration of the node. If the specified + interface does not exist then None is returned:: + + { + "mac_address": "aa:bb:cc:dd:ee:ff", + "interfaces": { + "Vlan100": { + "addresses": [ "1.1.1.1", "2.2.2.2"] + }, + "Vlan200": [...] + } } - } """ resource = dict() resource.update(self._parse_mac_address()) @@ -122,11 +111,10 @@ def set_mac_address(self, mac_address=None, default=False): Args: mac_address (string): The mac address that will be assigned as - the virtual-router mac address. This should be in the format, - aa:bb:cc:dd:ee:ff. - + the virtual-router mac address. This should be in the format, + aa:bb:cc:dd:ee:ff. default (bool): Sets the virtual-router mac address to the system - default (which is to remove the configuration line). + default (which is to remove the configuration line). Returns: True if the set operation succeeds otherwise False. diff --git a/pyeapi/client.py b/pyeapi/client.py index d470302..8d627a0 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -347,6 +347,7 @@ def hosts_for_tag(tag): Returns: list: A Python list object that includes the list of hosts assoicated with the specified tag. + None: If the specified tag does not exist, then None is returned. """ return config.tags.get(tag) From c1cb59306b4513bd0d0ac1716169df0bc5ebdfc3 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 22 Oct 2015 13:15:10 -0600 Subject: [PATCH 55/83] remove top level key from get() result --- pyeapi/api/staticroute.py | 8 +++--- test/system/test_api_staticroute.py | 12 ++++----- test/unit/test_api_staticroute.py | 40 +++++++++++++---------------- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index 3a60364..b374435 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -114,11 +114,9 @@ def get(self, name): a next_hop_ip, then that key value will be set as 'None'. """ - # Return the route configurations for the specified ip address - routes = self.getall().get(name) - if routes: - return {name: routes} - return None + # Return the route configurations for the specified ip address, + # or None if its not found + return self.getall().get(name) def getall(self): """Return all ip routes configured on the switch as a resource dict diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 5acf7e0..5207b39 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -127,13 +127,11 @@ def test_get(self): dut.config([cmd]) route = { - ip_dest: { - next_hop: { - next_hop_ip: { - distance: { - 'tag': tag, - 'route_name': route_name - } + next_hop: { + next_hop_ip: { + distance: { + 'tag': tag, + 'route_name': route_name } } } diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py index 66f3bc6..4cc507b 100644 --- a/test/unit/test_api_staticroute.py +++ b/test/unit/test_api_staticroute.py @@ -72,14 +72,12 @@ def test_get(self): # Get the route(s) for ip_dest 0.0.0.0/24 ip_dest = '0.0.0.0/0' routes = { - ip_dest: { - '192.68.1.254': { - None: { - 1: {'route_name': None, - 'tag': 0} - } + '192.68.1.254': { + None: { + 1: {'route_name': None, + 'tag': 0} } - }, + } } result = self.instance.get(ip_dest) self.assertEqual(result, routes) @@ -87,21 +85,19 @@ def test_get(self): # Get the route(s) for ip_dest 1.2.3.0/24 ip_dest = '1.2.3.0/24' routes = { - ip_dest: { - 'Ethernet1': { - '1.1.1.1': { - 1: { - 'route_name': 'test1', - 'tag': 1}, - 10: { - 'route_name': 'test1', - 'tag': 1} - }, - '10.1.1.1': { - 20: { - 'route_name': 'test1', - 'tag': 1} - } + 'Ethernet1': { + '1.1.1.1': { + 1: { + 'route_name': 'test1', + 'tag': 1}, + 10: { + 'route_name': 'test1', + 'tag': 1} + }, + '10.1.1.1': { + 20: { + 'route_name': 'test1', + 'tag': 1} } } } From 502081a604934a065fd0b7b9516d5072b6c2a773 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:18:28 -0400 Subject: [PATCH 56/83] Doc updates - Add install - Add make docs which wipes, builds modules, and html. Good for local testing --- docs/Makefile | 5 ++ docs/description.rst | 4 +- docs/generate_modules.py | 0 docs/index.rst | 3 +- docs/install.rst | 116 +++++++++++++++++++++++++++++++++++++++ docs/intro.rst | 21 +++++++ docs/quickstart.rst | 2 +- 7 files changed, 147 insertions(+), 4 deletions(-) mode change 100644 => 100755 docs/generate_modules.py create mode 100644 docs/install.rst create mode 100644 docs/intro.rst diff --git a/docs/Makefile b/docs/Makefile index d1d0fa5..7ae726d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -54,6 +54,11 @@ html: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +modules: + generate_modules.py + +docs: clean modules html + dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo diff --git a/docs/description.rst b/docs/description.rst index 966ff23..01d2657 100644 --- a/docs/description.rst +++ b/docs/description.rst @@ -1,5 +1,5 @@ -Introduction -============ +The Python Client for eAPI +========================== The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring diff --git a/docs/generate_modules.py b/docs/generate_modules.py old mode 100644 new mode 100755 diff --git a/docs/index.rst b/docs/index.rst index 4cd986b..af0a153 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,8 @@ Contents: .. toctree:: :maxdepth: 3 - description + intro + install quickstart modules license diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..ff8e517 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,116 @@ +############ +Installation +############ + +The installation of pyeapi is straightforward and simple. As mentioned in the +:ref:`intro`, pyeapi can be run on-box or off-box. The instructions below +will provide some tips to help you for either platform. + +.. contents:: + :depth: 2 + +*********************** +Pip with Network Access +*********************** + +If your platform has internet access you can use the Python Package manager +to install pyeapi + +.. code-block:: console + + admin:~ admin$ sudo pip install pyeapi + +.. Note:: You will likely notice Pip install netaddr, a depency of pyeapi. + + +************************** +Pip without Network Access +************************** + +If you want to install pyeapi on a switch with no internet access: + +**Step 1:** Download Pypi Package + +- `Download `_ the latest version of **pyeapi** on your local machine. +- You will also need a dependency package `netaddr `_. + +**Step 2:** SCP both files to the Arista switch and install + +.. code-block:: console + + admin:~ admin$ scp path/to/pyeapi-.tar.gz ansible@veos01:/mnt/flash/ + admin:~ admin$ scp path/to/netaddr-.tar.gz ansible@veos01:/mnt/flash/ + +Then SSH into your node and install it. Be sure to replace ```` with the +actual filename: + +.. code-block:: console + + [admin@veos ~]$ sudo pip install /mnt/flash/netaddr-.tar.gz + [admin@veos ~]$ sudo pip install /mnt/flash/pyeapi-.tar.gz + +These packages will be blown away on switch reboot. Therefore, add the install +commands to ``/mnt/flash/rc.eos`` to install on reboot: + +.. code-block:: console + + [admin@veos ~]$ vi /mnt/flash/rc.eos + +Add the lines (the #! may already exist in your rc.eos) + +.. code-block:: console + + #!/bin/bash + sudo pip install /mnt/flash/netaddr-.tar.gz + sudo pip install /mnt/flash/pyeapi-.tar.gz + + +************************************ +Development - Run pyeapi from Source +************************************ + +.. Tip:: We recommend running pyeapi in a virtual environment. For more + information, `read this. `_ + +These instructions will help you install and run pyeapi from source. This +is useful if you plan on contributing or if you'd always like to see the latest +code in the develop branch. + +.. Important:: These steps require Pip and Git + +**Step 1:** Clone the pyeapi Github repo + +.. code-block:: console + + # Go to a directory where you'd like to keep the source + admin:~ admin$ cd ~/projects + admin:~ admin$ git clone https://github.com/arista-eosplus/pyeapi.git + admin:~ admin$ cd pyeapi + +**Step 2:** Check out the desired version or branch + +.. code-block:: console + + # Go to a directory where you'd like to keep the source + admin:~ admin$ cd ~/projects/pyeapi + + # To see a list of available versions or branches + admin:~ admin$ git tag + admin:~ admin$ git branch + + # Checkout the desired version of code + admin:~ admin$ git checkout v0.3.3 + +**Step 3:** Install pyeapi using Pip with -e switch + +.. code-block:: console + + # Go to a directory where you'd like to keep the source + admin:~ admin$ cd ~/projects/pyeapi + + # Install + admin:~ admin$ sudo pip install -e ~/projects/pyeapi + +.. Tip:: If you start using pyeapi and get import errors, make sure your PATH + is set to include the path to pyeapi. You can also set your PYTHONPATH + to include the pyeapi source path. diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 0000000..b020d85 --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,21 @@ +.. _intro: + +Introduction +============ + +The Python Client for eAPI (pyeapi) is a native Python library wrapper around +Arista EOS eAPI. It provides a set of Python language bindings for configuring +Arista EOS nodes. + +The Python library can be used to communicate with EOS either locally +(on-box) or remotely (off-box). It uses a standard INI-style configuration file +to specify one or more nodes and connection profiles. + +The pyeapi library also provides an API layer for building native Python +objects to interact with the destination nodes. The API layer is a convenient +implementation for working with the EOS configuration and is extensible for +developing custom solutions. + +This library is freely provided to the open source community for building +robust applications using Arista EOS. Support is provided as best effort +through Github issues. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0f29ce9..cb4df9d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -57,7 +57,7 @@ The following configuration options are available for defining node entries: port is used if this parameter is absent, based on the transport setting using the following values: * transport: http, default port: 80 - * transport: https, deafult port: 443 + * transport: https, default port: 443 * transport: https_local, default port: 8080 * transport: socket, default port: n/a From 34f00bff04c10b5da43fcae12c28d9a8f6ba03f6 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:20:01 -0400 Subject: [PATCH 57/83] Add sys to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0b9d4e2..908afa6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup, find_packages from codecs import open from os import path +import sys here = path.abspath(path.dirname(__file__)) From 6756b8d9cc1dde296dd327fae7b9365f0dd6ecc4 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:21:45 -0400 Subject: [PATCH 58/83] Add os.environ to import --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 908afa6..8c65f1f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages from codecs import open -from os import path +from os import path, environ import sys here = path.abspath(path.dirname(__file__)) @@ -80,7 +80,7 @@ def install(): return False # Use the following to dynamically build pyeapi module documentation -if install() and os.environ.get('READTHEDOCS'): +if install() and environ.get('READTHEDOCS'): print 'This method is only called by READTHEDOCS.' from subprocess import Popen proc = Popen(['make', 'modules'], cwd='docs/') From d45cab6f1eecee902f862e187acf6857bfa01ca0 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 22 Oct 2015 13:23:38 -0600 Subject: [PATCH 59/83] remove unnecessary code --- pyeapi/api/staticroute.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py index b374435..6f43ae0 100644 --- a/pyeapi/api/staticroute.py +++ b/pyeapi/api/staticroute.py @@ -375,8 +375,6 @@ def _set_route(self, ip_dest, next_hop, **kwargs): if default: commands = "default " + commands - import syslog - syslog.syslog("XXX calling: %s\n" % commands) return self.configure(commands) From 7b6ee8fc9f16aac20b7c5ff2348d3c13d6983040 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:37:18 -0400 Subject: [PATCH 60/83] Add path to command --- docs/Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 7ae726d..da55edf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,6 +6,9 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build +APIDIR = api_modules +CLIENTDIR = client_modules +CWD := $(shell pwd) # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) @@ -48,6 +51,8 @@ help: clean: rm -rf $(BUILDDIR)/* + rm -rf $(CLIENTDIR)/* + rm -rf $(APIDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @@ -55,7 +60,7 @@ html: @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." modules: - generate_modules.py + python $(CWD)/generate_modules.py docs: clean modules html From 96e425441240dc16c997425d6320e9c5417da1d4 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:54:23 -0400 Subject: [PATCH 61/83] Change to relative paths --- docs/generate_modules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/generate_modules.py b/docs/generate_modules.py index 85fcdf4..58cb060 100755 --- a/docs/generate_modules.py +++ b/docs/generate_modules.py @@ -5,7 +5,9 @@ from os.path import isfile, join import pprint as pp -HERE = path.abspath(path.dirname(__file__)) +HERE = './' +# Readthedocs doesn't like Abspaths, so just using relative. Danger zone. +# path.abspath(path.dirname(__file__)) MODULES_PATH = '%s/../pyeapi/' % HERE AUTOGEN = '.. This file has been autogenerated by generate_modules.py\n\n' From f4e26555db50e4f479bcf85091339bd8a632afb6 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 15:56:39 -0400 Subject: [PATCH 62/83] Change path for RTD --- docs/generate_modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/generate_modules.py b/docs/generate_modules.py index 58cb060..038a959 100755 --- a/docs/generate_modules.py +++ b/docs/generate_modules.py @@ -5,7 +5,7 @@ from os.path import isfile, join import pprint as pp -HERE = './' +HERE = '.' # Readthedocs doesn't like Abspaths, so just using relative. Danger zone. # path.abspath(path.dirname(__file__)) MODULES_PATH = '%s/../pyeapi/' % HERE @@ -74,6 +74,7 @@ def write_module_file(name, path, package): mod_file.write(' :show-inheritance:\n') def main(): + print path.abspath(path.dirname(__file__)) modules = dict(client=None, api=None) modules['client'] = get_module_names(MODULES_PATH) modules['api'] = get_module_names('%s/api' % MODULES_PATH) From c27790f61e6f4f2e1dbba4e8629018bebf097833 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 22 Oct 2015 16:03:30 -0400 Subject: [PATCH 63/83] Add check to see if dir exists --- docs/generate_modules.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/generate_modules.py b/docs/generate_modules.py index 038a959..a52be82 100755 --- a/docs/generate_modules.py +++ b/docs/generate_modules.py @@ -1,13 +1,11 @@ #!/usr/bin/python -from os import path -from os import listdir -from os.path import isfile, join +from os import listdir, path, makedirs +from os.path import isfile, join, exists import pprint as pp -HERE = '.' -# Readthedocs doesn't like Abspaths, so just using relative. Danger zone. -# path.abspath(path.dirname(__file__)) +HERE = path.abspath(path.dirname(__file__)) + MODULES_PATH = '%s/../pyeapi/' % HERE AUTOGEN = '.. This file has been autogenerated by generate_modules.py\n\n' @@ -31,9 +29,16 @@ def process_modules(modules): the corresponding files. ''' for mod in modules['client']: - write_module_file(mod, '%s/client_modules' % HERE, 'pyeapi') + directory = '%s/client_modules' % HERE + if not exists(directory): + makedirs(directory) + write_module_file(mod, directory, 'pyeapi') + for mod in modules['api']: - write_module_file(mod, '%s/api_modules' % HERE, 'pyeapi.api') + directory = '%s/api_modules' % HERE + if not exists(directory): + makedirs(directory) + write_module_file(mod, directory, 'pyeapi.api') create_index(modules) @@ -74,7 +79,6 @@ def write_module_file(name, path, package): mod_file.write(' :show-inheritance:\n') def main(): - print path.abspath(path.dirname(__file__)) modules = dict(client=None, api=None) modules['client'] = get_module_names(MODULES_PATH) modules['api'] = get_module_names('%s/api' % MODULES_PATH) From 09554716ad7a8d4be8837e215723b19be1d63355 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 23 Oct 2015 00:14:37 -0400 Subject: [PATCH 64/83] Update Quickstart --- docs/configfile.rst | 91 ++++++++++++++++++ docs/index.rst | 37 +++++--- docs/install.rst | 2 + docs/intro.rst | 21 ----- docs/quickstart.rst | 211 ++++++++++++++++-------------------------- docs/requirements.rst | 11 +++ 6 files changed, 212 insertions(+), 161 deletions(-) create mode 100644 docs/configfile.rst delete mode 100644 docs/intro.rst create mode 100644 docs/requirements.rst diff --git a/docs/configfile.rst b/docs/configfile.rst new file mode 100644 index 0000000..a88112e --- /dev/null +++ b/docs/configfile.rst @@ -0,0 +1,91 @@ +######################### +Pyeapi Configuration File +######################### + +In order to use pyeapi, the EOS command API must be enabled using configuration +mode. This library supports eAPI calls over both HTTP/S and UNIX Domain +Sockets. Once the command API is enabled on the destination node, create a +configuration file with the node properties. + +************** +Install Pyeapi +************** + +Follow the instructions on the :ref:`install` guide to prepare your node for +pyeapi. + +************************ +Create an eapi.conf file +************************ + +This method would be used to run a pyeapi script on-box. In this mode, eAPI +can be configured to require or not require authentication. A quick summary: + +=========== ======================== +Type Authentication Required +=========== ======================== +https Yes +http Yes +http_local No +socket No +=========== ======================== + +The default transport for pyeapi is ``socket`` and the default host is +``localhost``. Therefore, if running a pyeapi script on-box and have +Unix Sockets enabled, you do not need an eapi.conf, nor do you need to pass +any credentials (quite handy!). + +If instead, ``https``, ``http`` or ``http_local`` is configured on your +node, then you will need an eapi.conf file in ``/mnt/flash/eapi.conf``. It +would contain something like: + +.. code-block:: console + + [connection:localhost] + transport: http_local + +.. code-block:: console + + [connection:localhost] + transport: https + username: admin + password: admin + +.. code-block:: console + + [connection:localhost] + transport: http + username: admin + password: admin + +.. Note:: The default search path for the conf file is ``~/.eapi.conf`` + followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting + ``EAPI_CONF=`` in your environment. + +The following configuration options are available for defining node entries: + +:host: The IP address or FQDN of the remote device. If the host + parameter is omitted then the connection name is used + +:username: The eAPI username to use for authentication (only required for + http or https connections) + +:password: The eAPI password to use for authentication (only required for + http or https connections) + +:enablepwd: The enable mode password if required by the destination node + +:transport: Configures the type of transport connection to use. Valid + values are: + - socket (default, available in EOS 4.14.5 or later) + - http_local (available in EOS 4.14.5 or later) + - http + - https + +:port: Configures the port to use for the eAPI connection. A default + port is used if this parameter is absent, based on the transport setting + using the following values: + - transport: http, default port: 80 + - transport: https, deafult port: 443 + - transport: http_local, default port: 8080 + - transport: socket, default port: n/a diff --git a/docs/index.rst b/docs/index.rst index af0a153..4e379c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,26 +1,41 @@ -.. Python Client for eAPI documentation master file, created by - sphinx-quickstart on Tue Mar 24 21:01:31 2015. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. _intro: -Welcome to Python Client for eAPI's documentation! -================================================== +############ +Introduction +############ + +The Python Client for eAPI (pyeapi) is a native Python library wrapper around +Arista EOS eAPI. It provides a set of Python language bindings for configuring +Arista EOS nodes. + +The Python library can be used to communicate with EOS either locally +(on-box) or remotely (off-box). It uses a standard INI-style configuration file +to specify one or more nodes and connection profiles. + +The pyeapi library also provides an API layer for building native Python +objects to interact with the destination nodes. The API layer is a convenient +implementation for working with the EOS configuration and is extensible for +developing custom solutions. + +This library is freely provided to the open source community for building +robust applications using Arista EOS. Support is provided as best effort +through Github issues. -Contents: .. toctree:: - :maxdepth: 3 + :maxdepth: 1 - intro install quickstart + configfile modules + requirements license - +****************** Indices and tables -================== +****************** * :ref:`genindex` * :ref:`modindex` diff --git a/docs/install.rst b/docs/install.rst index ff8e517..51fa81b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,3 +1,5 @@ +.. _install: + ############ Installation ############ diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index b020d85..0000000 --- a/docs/intro.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. _intro: - -Introduction -============ - -The Python Client for eAPI (pyeapi) is a native Python library wrapper around -Arista EOS eAPI. It provides a set of Python language bindings for configuring -Arista EOS nodes. - -The Python library can be used to communicate with EOS either locally -(on-box) or remotely (off-box). It uses a standard INI-style configuration file -to specify one or more nodes and connection profiles. - -The pyeapi library also provides an API layer for building native Python -objects to interact with the destination nodes. The API layer is a convenient -implementation for working with the EOS configuration and is extensible for -developing custom solutions. - -This library is freely provided to the open source community for building -robust applications using Arista EOS. Support is provided as best effort -through Github issues. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index cb4df9d..cd24226 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,71 +1,41 @@ +########## Quickstart -========== - -The Python library for Arista's eAPI command API implementation provides a -client API work using eAPI and communicating with EOS nodes. The Python -library can be used to communicate with EOS either locally (on-box) or remotely -(off-box). It uses a standard INI-style configuration file to specify one or -more nodes and connection properites. - -The pyeapi library also provides an API layer for building native Python -objects to interact with the destination nodes. The API layer is a convienent -implementation for working with the EOS configuration and is extensible for -developing custom implemenations. - -This library is freely provided to the open source community for building -robust applications using Arista EOS. Support is provided as best effort -through Github issues. - -## Requirements - -* Arista EOS 4.12 or later -* Arista eAPI enabled for at least one transport (see Official EOS Config Guide - at arista.com for details) -* Python 2.7 - -# Getting Started -In order to use pyeapi, the EOS command API must be enabled using ``management -api http-commands`` configuration mode. This library supports eAPI calls over -both HTTP and UNIX Domain Sockets. Once the command API is enabled on the -destination node, create a configuration file with the node properities. - -**Note:** The default search path for the conf file is ``~/.eapi.conf`` -followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting -``EAPI_CONF=`` in your environment. - -## Example eapi.conf File -Below is an example of an eAPI conf file. The conf file can contain more than -one node. Each node section must be prefaced by **connection:\** where -\ is the name of the connection. - -The following configuration options are available for defining node entries: - -* **host** - The IP address or FQDN of the remote device. If the host - parameter is omitted then the connection name is used -* **username** - The eAPI username to use for authentication (only required for - http or https connections) -* **password** - The eAPI password to use for authentication (only required for - http or https connections) -* **enablepwd** - The enable mode password if required by the destination node -* **transport** - Configures the type of transport connection to use. The - default value is _https_. Valid values are: - * socket (available in EOS 4.14.5 or later) - * http_local (available in EOS 4.14.5 or later) - * http - * https -* **port** - Configures the port to use for the eAPI connection. A default - port is used if this parameter is absent, based on the transport setting - using the following values: - * transport: http, default port: 80 - * transport: https, default port: 443 - * transport: https_local, default port: 8080 - * transport: socket, default port: n/a - - -_Note:_ See the EOS User Manual found at arista.com for more details on -configuring eAPI values. - -All configuration values are optional. +########## + +In order to use pyeapi, the EOS command API must be enabled using configuration +mode. This library supports eAPI calls over both HTTP/S and UNIX Domain +Sockets. Once the command API is enabled on the destination node, create a +configuration file with the node properties. + +************** +Install Pyeapi +************** + +Follow the instructions on the :ref:`install` guide to prepare your node for +pyeapi. + +************************ +Create an eapi.conf file +************************ + +The eAPI configuration file provides a way to keep an inventory of your +switches in one central place. You can quickly connect to a switch in your +inventory as shown below. The contents of eapi.conf will change depending upon +where you run pyeapi. Some examples are provided below. + + +Running pyeapi from a central server +==================================== + +This method would be used to connect to various Arista nodes from a central +server. The eapi.conf file would then contain all of the switches and would +likely include an HTTP or HTTPS transport method. + +Here's an example + +The conf file can contain more than one node. Each node section must be +prefaced by ``connection:`` where is the name of the connection. +When no ``host`` key is present, the connection name will be used (ie DNS). .. code-block:: console @@ -75,12 +45,6 @@ All configuration values are optional. transport: http [connection:veos02] - transport: http - - [connection:veos03] - transport: socket - - [connection:veos04] host: 172.16.10.1 username: eapi password: password @@ -88,33 +52,64 @@ All configuration values are optional. port: 1234 transport: https +Running pyeapi locally on a switch +================================== + +This method would be used to run a pyeapi script on-box. In this mode, eAPI +can be configured to require or not require authentication. A quick summary: + +=========== ======================== +Type Authentication Required +=========== ======================== +https Yes +http Yes +http_local No +socket No +=========== ======================== + +The default transport for pyeapi is ``socket`` and the default host is +``localhost``. Therefore, if running a pyeapi script on-box and have +Unix Sockets enabled, you do not need an eapi.conf, nor do you need to pass +any credentials (quite handy!). + +If instead, ``https``, ``http`` or ``http_local`` is configured on your +node, then you will need an eapi.conf file in ``/mnt/flash/eapi.conf``. It +would contain something like: + +.. code-block:: console + [connection:localhost] transport: http_local +.. code-block:: console -The above example shows different ways to define EOS node connections. All -configuration options will attempt to use default values if not explicitly -defined. If the host parameter is not set for a given entry, then the -connection name will be used as the host address. + [connection:localhost] + transport: https + username: admin + password: admin + +.. code-block:: console + + [connection:localhost] + transport: http + username: admin + password: admin -### Configuring \[connection:localhost] -The pyeapi library automatically installs a single default configuration entry -for connecting to localhost host using a transport of sockets. If using the -pyeapi library locally on an EOS node, simply enable the command API to use -sockets and no further configuration is needed for pyeapi to function. If you -specify an entry in a conf file with the name ``[connection:localhost]``, the -values in the conf file will overwrite the default. +***************** +Connect to a Node +***************** -## Using pyeapi The Python client for eAPI was designed to be easy to use and implement for writing tools and applications that interface with the Arista EOS management plane. -### Creating a connection and sending commands Once EOS is configured properly and the config file created, getting started with a connection to EOS is simple. Below demonstrates a basic connection -using pyeapi. For more examples, please see the examples folder. +using pyeapi. For more examples, please see the examples folder. + +This first example shows how to instantiate the Node object. The Node object +provides some helpful methods and attributes to work with the switch. .. code-block:: python @@ -144,9 +139,6 @@ using pyeapi. For more examples, please see the examples folder. node.startup_config - -### Using the API - The pyeapi library provides both a client for send and receiving commands over eAPI as well as an API for working directly with EOS resources. The API is designed to be easy and straightforward to use yet also extensible. Below is @@ -178,42 +170,3 @@ an example of working with the ``vlans`` API # set the new vlan name vlans.set_name(100, 'foo') True - - -All API implementations developed by Arista EOS+ CS are found in the pyeapi/api -folder. See the examples folder for additional examples. - -# Installation - -The source code for pyeapi is provided on Github at -http://github.com/arista-eosplus/pyeapi. All current development is done in -the develop branch. Stable released versions are tagged in the master branch -and uploaded to PyPi. - -* To install the latest stable version of pyeapi, simply run ``pip install - pyeapi`` (or ``pip install --upgrade pyeapi``) -* To install the latest development version from Github, simply clone the - develop branch and run ``python setup.py install`` - -# Testing -The pyeapi library provides both unit tests and system tests. The unit tests -can be run without an EOS node. To run the system tests, you will need to -update the ``dut.conf`` file found in test/fixtures. - -* To run the unit tests, simply run ``make unittest`` from the root of the - pyeapi source folder -* To run the system tests, simply run ``make systest`` from the root of the - pyeapi source fodler -* To run all tests, use ``make tests`` from the root of the pyeapi source - folder - - -# Contributing - -Contributing pull requests are gladly welcomed for this repository. Please -note that all contributions that modify the library behavior require -corresponding test cases otherwise the pull request will be rejected. - -# License - -New BSD, See [LICENSE](LICENSE) file diff --git a/docs/requirements.rst b/docs/requirements.rst new file mode 100644 index 0000000..a28925e --- /dev/null +++ b/docs/requirements.rst @@ -0,0 +1,11 @@ +############ +Requirements +############ + +* Arista EOS 4.12 or later +* Arista eAPI enabled for at least one transport (see Official EOS Config Guide + at arista.com for details) +* Python 2.7 or 3.4+ (Python 3 support is work in progress) +* Pyeapi required the netaddr Python module + +.. Note:: netaddr gets installed automatically if you use pip to install pyeapi From 27e3fa75a4d93e1e7c3d81fca89a6756059f3a1d Mon Sep 17 00:00:00 2001 From: GaryCarneiro Date: Fri, 23 Oct 2015 00:55:20 -0700 Subject: [PATCH 65/83] Making configure RADIUS compatible * The RBAC and RADIUS compatibility do not allow short notations of command. Hence making elongating to full command. --- pyeapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index d470302..623df56 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -504,7 +504,7 @@ def config(self, commands): commands = list(commands) # push the configure command onto the command stack - commands.insert(0, 'configure') + commands.insert(0, 'configure terminal') response = self.run_commands(commands) if self.autorefresh: From bf05ede8ee39004689bc7b58731beccb677fc6d0 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 23 Oct 2015 12:22:41 -0400 Subject: [PATCH 66/83] Doc updates --- docs/configfile.rst | 216 +++++++++++++++++++++++++++++++----------- docs/install.rst | 9 +- docs/quickstart.rst | 108 +++++---------------- docs/requirements.rst | 2 +- pyeapi/client.py | 2 +- 5 files changed, 190 insertions(+), 147 deletions(-) diff --git a/docs/configfile.rst b/docs/configfile.rst index a88112e..b27ce5e 100644 --- a/docs/configfile.rst +++ b/docs/configfile.rst @@ -1,66 +1,23 @@ +.. _configfile: + ######################### Pyeapi Configuration File ######################### -In order to use pyeapi, the EOS command API must be enabled using configuration -mode. This library supports eAPI calls over both HTTP/S and UNIX Domain -Sockets. Once the command API is enabled on the destination node, create a -configuration file with the node properties. - -************** -Install Pyeapi -************** - -Follow the instructions on the :ref:`install` guide to prepare your node for -pyeapi. +The pyeapi configuration file is a convenient place to store node connection +information. By keeping connection information central, your pyeapi scripts +can effortlessly reference nodes without any manual import of credentials +or location information. Therefore, the pyeapi configuration file becomes +a reflection of your switch inventory and the way in which the EOS Command +API is enabled on the node. The following explains how to craft your +pyeapi configuration file to address specific implementation styles. -************************ -Create an eapi.conf file -************************ +.. contents:: + :depth: 2 -This method would be used to run a pyeapi script on-box. In this mode, eAPI -can be configured to require or not require authentication. A quick summary: - -=========== ======================== -Type Authentication Required -=========== ======================== -https Yes -http Yes -http_local No -socket No -=========== ======================== - -The default transport for pyeapi is ``socket`` and the default host is -``localhost``. Therefore, if running a pyeapi script on-box and have -Unix Sockets enabled, you do not need an eapi.conf, nor do you need to pass -any credentials (quite handy!). - -If instead, ``https``, ``http`` or ``http_local`` is configured on your -node, then you will need an eapi.conf file in ``/mnt/flash/eapi.conf``. It -would contain something like: - -.. code-block:: console - - [connection:localhost] - transport: http_local - -.. code-block:: console - - [connection:localhost] - transport: https - username: admin - password: admin - -.. code-block:: console - - [connection:localhost] - transport: http - username: admin - password: admin - -.. Note:: The default search path for the conf file is ``~/.eapi.conf`` - followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting - ``EAPI_CONF=`` in your environment. +******************** +eapi.conf Parameters +******************** The following configuration options are available for defining node entries: @@ -89,3 +46,148 @@ The following configuration options are available for defining node entries: - transport: https, deafult port: 443 - transport: http_local, default port: 8080 - transport: socket, default port: n/a + +********************************* +When is an eapi.conf file needed? +********************************* + +It's important to understand the nuances of the pyeapi configuration file so +you can simplify your implementation. Here's a quick summary of when the +eapi.conf file is needed: + +=========== ================== =============== ======================== +Transport eapi.conf Required Script run from Authentication Required +=========== ================== =============== ======================== +http Yes On/Off-switch Yes +https Yes On/Off-switch Yes +http_local Yes On-switch only No +socket No On-switch only No +=========== ================== =============== ======================== + + +******************************** +Where should the file be placed? +******************************** + +============ ================================================= +Search Order Search Location +============ ================================================= +1 Environment Variable EAPI_CONF=/path/to/eapi.conf +2 $HOME/.eapi.conf +3 /mnt/flash/eapi.conf +============ ================================================= + +.. Note:: There is a slight difference in #2 ``.eapi.conf`` versus + #3 ``eapi.conf`` + +************************************ +eapi.conf for On-box Implementations +************************************ + +This method would be used to run a pyeapi script on-box. In this mode, eAPI +can be configured to require or not require authentication depending upon +how you enabled EOS Command API. + +Notice from the table above, that if EOS Command API Unix Sockets are enabled, +or HTTP Local, you get the benefit of not needing to pass in credentials +since the connection can only be made from localhost and it assumes the user +has already authenticated to get on the switch. + +Using Unix Sockets +================== + +This is the preferred method. The default transport for pyeapi is ``socket`` +and the default host is ``localhost``. Therefore, if running a pyeapi script +on-box and have Unix Sockets enabled, you do not need an eapi.conf, nor do you +need to pass any credentials (quite handy!). + +.. Note:: Unix Sockets are supported on EOS 4.14.5+ + +Using HTTP Local +================ + +As the table above indicates, a pyeapi configuration file is required in +``/mnt/flash/eapi.conf``. It would contain something like: + +.. code-block:: console + + [connection:localhost] + transport: http_local + +Using HTTP or HTTPS +=================== + +As the table above indicates, a pyeapi configuration file is required in +``/mnt/flash/eapi.conf``. It would contain something like: + +.. code-block:: console + + [connection:localhost] + transport: http[s] + username: admin + password: admin + +************************************* +eapi.conf for Off-box Implementations +************************************* + +This method would be used to run a pyeapi script from another server. In this +mode, eAPI will require authentication. The only real option is whether you +connect over HTTP or HTTPS. + +.. Note:: The ``socket`` and ``http_local`` transport options are not + applicable. + +Notice from the table above, that if EOS Command API Unix Sockets are enabled, +or HTTP Local, you get the benefit of not needing to pass in credentials +since the connection can only be made from localhost and it assumes the user +has already authenticated to get on the switch. + +Using HTTP or HTTPS +=================== + +As the table above indicates, a pyeapi configuration file is required in +``$HOME/.eapi.conf``. It would contain something like: + +.. code-block:: console + + [connection:veos01] + transport: http + username: paul + password: nottelling + + [connection:veos03] + transport: https + username: bob + password: mysecret + + [connection:veos04] + host: 192.168.2.10 + transport: https + username: admin + password: admin + +******************* +The DEFAULT Section +******************* + +The [DEFAULT] section can be used to gather globally applicable settings. +Say that all of your nodes use the same transport or username, you can do +something like: + +.. code-block:: console + + [connection:veos01] + + [connection:veos03] + transport: https + username: bob + password: mysecret + + [connection:veos04] + host: 192.168.2.10 + + [DEFAULT] + transport: https + username: admin + password: admin diff --git a/docs/install.rst b/docs/install.rst index 51fa81b..3e0e954 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -51,8 +51,8 @@ actual filename: [admin@veos ~]$ sudo pip install /mnt/flash/netaddr-.tar.gz [admin@veos ~]$ sudo pip install /mnt/flash/pyeapi-.tar.gz -These packages will be blown away on switch reboot. Therefore, add the install -commands to ``/mnt/flash/rc.eos`` to install on reboot: +These packages must be re-installed on reboot. Therefore, add the install +commands to ``/mnt/flash/rc.eos`` to trigger the install on reboot: .. code-block:: console @@ -113,6 +113,5 @@ code in the develop branch. # Install admin:~ admin$ sudo pip install -e ~/projects/pyeapi -.. Tip:: If you start using pyeapi and get import errors, make sure your PATH - is set to include the path to pyeapi. You can also set your PYTHONPATH - to include the pyeapi source path. +.. Tip:: If you start using pyeapi and get import errors, make sure your + PYTHONPATH is set to include the path to pyeapi. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index cd24226..bcd2d09 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,7 +5,21 @@ Quickstart In order to use pyeapi, the EOS command API must be enabled using configuration mode. This library supports eAPI calls over both HTTP/S and UNIX Domain Sockets. Once the command API is enabled on the destination node, create a -configuration file with the node properties. +configuration file with the node properties. There are some nuances about the +configuration file that are important to understand; take a minute and read +about the :ref:`configfile`. + +********************** +Enable EOS Command API +********************** + +Refer to your official Arista EOS Configuration Guide to learn how to enable +EOS Command API. Depending upon your software version, the options available +include: + - HTTP + - HTTPS + - HTTP Local + - Unix Socket ************** Install Pyeapi @@ -18,83 +32,9 @@ pyeapi. Create an eapi.conf file ************************ -The eAPI configuration file provides a way to keep an inventory of your -switches in one central place. You can quickly connect to a switch in your -inventory as shown below. The contents of eapi.conf will change depending upon -where you run pyeapi. Some examples are provided below. - - -Running pyeapi from a central server -==================================== - -This method would be used to connect to various Arista nodes from a central -server. The eapi.conf file would then contain all of the switches and would -likely include an HTTP or HTTPS transport method. - -Here's an example - -The conf file can contain more than one node. Each node section must be -prefaced by ``connection:`` where is the name of the connection. -When no ``host`` key is present, the connection name will be used (ie DNS). - -.. code-block:: console - - [connection:veos01] - username: eapi - password: password - transport: http - - [connection:veos02] - host: 172.16.10.1 - username: eapi - password: password - enablepwd: itsasecret - port: 1234 - transport: https - -Running pyeapi locally on a switch -================================== - -This method would be used to run a pyeapi script on-box. In this mode, eAPI -can be configured to require or not require authentication. A quick summary: - -=========== ======================== -Type Authentication Required -=========== ======================== -https Yes -http Yes -http_local No -socket No -=========== ======================== - -The default transport for pyeapi is ``socket`` and the default host is -``localhost``. Therefore, if running a pyeapi script on-box and have -Unix Sockets enabled, you do not need an eapi.conf, nor do you need to pass -any credentials (quite handy!). - -If instead, ``https``, ``http`` or ``http_local`` is configured on your -node, then you will need an eapi.conf file in ``/mnt/flash/eapi.conf``. It -would contain something like: - -.. code-block:: console - - [connection:localhost] - transport: http_local - -.. code-block:: console - - [connection:localhost] - transport: https - username: admin - password: admin - -.. code-block:: console - - [connection:localhost] - transport: http - username: admin - password: admin - +Follow the instructions on the :ref:`configfile` guide to create a pyeapi +configuration file. You can skip this step if you are running the pyeapi +script on-box and Unix Sockets are enabled for EOS Command API. ***************** Connect to a Node @@ -106,7 +46,9 @@ plane. Once EOS is configured properly and the config file created, getting started with a connection to EOS is simple. Below demonstrates a basic connection -using pyeapi. For more examples, please see the examples folder. +using pyeapi. For more examples, please see the +`examples `_ +folder on Github. This first example shows how to instantiate the Node object. The Node object provides some helpful methods and attributes to work with the switch. @@ -128,13 +70,13 @@ provides some helpful methods and attributes to work with the switch. node.config('hostname veos01') [{}] - # multiple commands can be sent by using a list (works for both enable or - config) + # multiple commands can be sent by using a list + # (works for both enable or config) node.config(['interface Ethernet1', 'description foo']) [{}, {}] - # return the running or startup configuration from the node (output omitted for - brevity) + # return the running or startup configuration from the + # node (output omitted for brevity) node.running_config node.startup_config diff --git a/docs/requirements.rst b/docs/requirements.rst index a28925e..fe7cb19 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -6,6 +6,6 @@ Requirements * Arista eAPI enabled for at least one transport (see Official EOS Config Guide at arista.com for details) * Python 2.7 or 3.4+ (Python 3 support is work in progress) -* Pyeapi required the netaddr Python module +* Pyeapi requires the netaddr Python module .. Note:: netaddr gets installed automatically if you use pip to install pyeapi diff --git a/pyeapi/client.py b/pyeapi/client.py index 8d627a0..16eb1e8 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -31,7 +31,7 @@ # """Python Client for eAPI -This module provides the client for eAPI. It provides to primary functions +This module provides the client for eAPI. It provides the primary functions for building applications that work with Arista EOS eAPI-enabled nodes. The first function is to provide a client for sending and receiving eAPI request and response objects on a per node basis. The second function From 049d8848dbf2600fc2f6a232e25e114c2075b77e Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 23 Oct 2015 12:27:04 -0400 Subject: [PATCH 67/83] Sort modules alpha --- docs/generate_modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/generate_modules.py b/docs/generate_modules.py index a52be82..a431e34 100755 --- a/docs/generate_modules.py +++ b/docs/generate_modules.py @@ -82,6 +82,8 @@ def main(): modules = dict(client=None, api=None) modules['client'] = get_module_names(MODULES_PATH) modules['api'] = get_module_names('%s/api' % MODULES_PATH) + modules['client'].sort() + modules['api'].sort() process_modules(modules) From db8eb63d4807890d148e150b818c7891fe2045bd Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 26 Oct 2015 15:52:51 -0400 Subject: [PATCH 68/83] Update _parse_shutdown method - Fix #55 --- pyeapi/api/bgp.py | 7 ++++--- test/unit/test_api_bgp.py | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 6da7f7a..810300e 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -210,9 +210,10 @@ def _parse_send_community(self, config, name): return dict(send_community=not value) def _parse_shutdown(self, config, name): - exp = 'no neighbor {} shutdown'.format(name) - value = exp in config - return dict(shutdown=not value) + regexp = r'(? Date: Tue, 27 Oct 2015 12:17:05 -0600 Subject: [PATCH 69/83] add vrrp functionality to pyeapi --- pyeapi/api/vrrp.py | 766 ++++++++++++++++++++++++++++++ test/fixtures/running_config.vrrp | 525 ++++++++++++++++++++ test/system/test_api_vrrp.py | 421 ++++++++++++++++ test/unit/test_api_vrrp.py | 697 +++++++++++++++++++++++++++ 4 files changed, 2409 insertions(+) create mode 100644 test/fixtures/running_config.vrrp diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index e69de29..3b01f34 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -0,0 +1,766 @@ +# +# 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 VRRP resources + +The Vrrp resource provides configuration management of interface specific +vrrp resources on and EOS node. It provides the following class +implementations: + + * Vrrp - Configure vrrps in EOS + +Vrrp Attributes: + enable (boolean): The shutdown state of the vrrp + primary_ip (string): The ip address of the vrrp + secondary_ip (dict): The secondary ip addresses configured for the vrrp + This is a dictionary in the format + { key: [ list of ip addresses ] } + where key is 'add', 'remove', or 'exists'. 'add' is + used to add the list of secondary ip addresses + to the vrrp. 'remove' will remove the list of + secondary ip addresses from the vrrp. 'exists' is + a report only key for retrieving the current + secondary ip addresses on a vrrp. + priority (int): The priority rank of the vrrp + description (string): The description for the vrrp + ip_version (int): The ip version value for the vrrp + timers_advertise (int): The timers advertise setting for the vrrp + mac_address_advertisement_interval (int): The mac-address advertisement- + interval setting for the vrrp + preempt (boolean): The preempt state of the vrrp + preempt_delay_minimum (int): The preempt delay minimum setting for the vrrp + preempt_delay_reload (int): The preempt delay reload setting for the vrrp + delay_reload (int): The delay reload setting for the vrrp + authentication (string): The authentication setting for the vrrp + track (dict): The object tracking settings for the vrrp + bfd_ip (string): The bfd ip set for the vrrp + +Notes: + The get method will return a dictionary of all the currently configured + vrrps on a single interface, with the VRID of each vrrp as the keys + in the dictionary: + { + vrrp1: { data }, + vrrp2: { data }, + } + + The getall method will return a dictionary of all the currently configured + vrrps on the node, with the interface name as the top-level keys, with + the VRIDs for each vrrp on an interface as a sub-key of that interface: + { + interface1: { + vrrp1: { data }, + vrrp2: { data }, + }, + interface2: { + vrrp1: { data }, + vrrp2: { data }, + } + } + + The data for a configured vrrp is a dictionary with the following format: + { + enable: + primary_ip: + priority: + description: + secondary_ip: { + exists: [ , ] + } + ip_version: + timers_advertise: + mac_address_advertisement_interval: + preempt: + preempt_delay_minimum: + preempt_delay_reload: + delay_reload: + authentication: + track: { + (, ): , + (, ): , + } + bfd_ip: + } + + The create and update methods accept a kwargs dictionary which + defines the properties to be applied to the new or existing vrrp + configuration. The available keywords and values are as follows: + enable: True to enable (no shutdown)|False to disable (shutdown) + primary_ip: |no|default|None + priority: |no|default|None + secondary_ip: may include the following: + add: + remove: + ip_version: |no|default|None + timers_advertise: |no|default|None + mac_address_advertisement_interval: |no|default|None + preempt: True to enable (preempt)|False to disable (no preempt) + preempt_delay_minimum: |no|default|None + preempt_delay_reload: |no|default|None + delay_reload: |no|default|None + authentication: NOTE: currently not implemented + track: consisting of entries in the following format: + (, ): + for shutdown, may be one of the following: + no|None: disable shutdown tracking for the named object + default: default shutdown tracking for the named object + : enable shutdown tracking for the named object + for decrement, may be one of the following: + no|None: disable decremental tracking for the named object + default: default decremental tracking for the named object + : set the decrement amount for the named object + bfd_ip: |no|default|None + +""" + +import re + +from pyeapi.api import EntityCollection + +ENABLED = {'no': False, '': True} +SHUTDOWN = {'no': True, '': False} +PROPERTIES = ['primary_ip', 'priority', 'description', 'secondary_ip', + 'ip_version', 'enable', 'timers_advertise', + 'mac_address_advertisement_interval', 'preempt', + 'preempt_delay_minimum', 'preempt_delay_reload', + 'delay_reload', 'authentication', 'track', 'bfd_ip'] + + +class Vrrp(EntityCollection): + """The Vrrp class provides management of the VRRP configuration + + The Vrrp class is derived from EntityCollection and provides an API for + working with the node's vrrp configurations. + """ + + def get(self, name): + """Get the vrrp configurations for a single node interface + + Args: + name (string): The name of the interface for which vrrp + configurations will be retrieved. + + Returns: + A dictionary containing the vrrp configurations on the interface. + Returns None if no vrrp configurations are defined or + if the interface is not configured. + """ + + # Validate the interface and vrid are specified + interface = name + if not interface: + raise ValueError("Vrrp.get(): interface must contain a value.") + + # Get the config for the interface. Return None if the + # interface is not defined + config = self.get_block('interface %s' % interface) + if config is None: + return config + + match = set(re.findall(r'^\s*(?:no |)vrrp (\d+)', config, re.M)) + if not match: + return None + + # Initialize the result dict + result = dict() + + for vrid in match: + subd = dict() + + # Parse the vrrp configuration for the vrid(s) in the list + subd.update(self._parse_authentication(config, vrid)) + subd.update(self._parse_delay_reload(config, vrid)) + subd.update(self._parse_description(config, vrid)) + subd.update(self._parse_enable(config, vrid)) + subd.update(self._parse_ip_version(config, vrid)) + subd.update(self._parse_mac_addr_adv_interval(config, vrid)) + subd.update(self._parse_preempt(config, vrid)) + subd.update(self._parse_preempt_delay_minimum(config, vrid)) + subd.update(self._parse_preempt_delay_reload(config, vrid)) + subd.update(self._parse_primary_ip(config, vrid)) + subd.update(self._parse_priority(config, vrid)) + subd.update(self._parse_secondary_ip(config, vrid)) + subd.update(self._parse_timers_advertise(config, vrid)) + subd.update(self._parse_track(config, vrid)) + subd.update(self._parse_bfd_ip(config, vrid)) + + result.update({int(vrid): subd}) + + # If result dict is empty, return None, otherwise return result + return result if result else None + + def getall(self): + """Get the vrrp configurations for all interfaces on a node + + Returns: + A dictionary containing the vrrp configurations on the node, + keyed by interface. + """ + + vrrps = dict() + + # Find the available interfaces + interfaces = re.findall(r'^interface\s(Vlan\d+|Port-Channel\d+' + r'|Ethernet\d+)', self.config, re.M) + + # Get the vrrps defined for each interface + for interface in interfaces: + vrrp = self.get(interface) + # Only add those interfaces that have vrrps defined + if vrrp: + vrrps.update({interface: vrrp}) + + return vrrps + + def _parse_primary_ip(self, config, vrid): + match = re.search(r'^\s*vrrp %s ip (\d+\.\d+\.\d+\.\d+)$' % + vrid, config, re.M) + value = match.group(1) if match else None + return dict(primary_ip=value) + + def _parse_priority(self, config, vrid): + match = re.search(r'^\s*vrrp %s priority (\d+)$' % vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(priority=value) + + def _parse_timers_advertise(self, config, vrid): + match = re.search(r'^\s*vrrp %s timers advertise (\d+)$' % + vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(timers_advertise=value) + + def _parse_preempt(self, config, vrid): + match = re.search(r'^\s*(no|) vrrp %s preempt$' % vrid, config, re.M) + value = match.group(1) if match else None + value = ENABLED.get(value, 'Error') + return dict(preempt=value) + + def _parse_enable(self, config, vrid): + match = re.search(r'^\s*(no|) vrrp %s shutdown$' % vrid, config, re.M) + value = match.group(1) if match else None + value = SHUTDOWN.get(value, 'Error') + return dict(enable=value) + + def _parse_secondary_ip(self, config, vrid): + matches = re.findall(r'^\s*vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' + r'secondary$' % vrid, config, re.M) + value = matches if matches else None + return dict(secondary_ip={'exists': value}) + + def _parse_description(self, config, vrid): + match = re.search(r'^\s*(no|) vrrp %s description(.*)$' % + vrid, config, re.M) + enabled = match.group(1) if match else None + enabled = ENABLED.get(enabled, 'Error') + value = match.group(2).lstrip() if enabled is True else '' + value = match.group(2).lstrip() + return dict(description=value) + + def _parse_mac_addr_adv_interval(self, config, vrid): + match = re.search(r'^\s*vrrp %s mac-address advertisement-interval ' + r'(\d+)$' % vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(mac_address_advertisement_interval=value) + + def _parse_preempt_delay_minimum(self, config, vrid): + match = re.search(r'^\s*vrrp %s preempt delay minimum (\d+)$' % + vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(preempt_delay_minimum=value) + + def _parse_preempt_delay_reload(self, config, vrid): + match = re.search(r'^\s*vrrp %s preempt delay reload (\d+)$' % + vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(preempt_delay_reload=value) + + def _parse_authentication(self, config, vrid): + match = re.search(r'^\s*(no|) vrrp %s authentication' + r'($| ietf-md5 key-string 7 .*$| text .*$)' % + vrid, config, re.M) + enabled = match.group(1) if match else None + enabled = ENABLED.get(enabled) + value = match.group(2).lstrip() if enabled is True else '' + return dict(authentication=value) + + def _parse_bfd_ip(self, config, vrid): + match = re.search(r'^\s*(no|) vrrp %s bfd ip' + r'(?: (\d+\.\d+\.\d+\.\d+)|)$' % + vrid, config, re.M) + enabled = match.group(1) if match else None + enabled = ENABLED.get(enabled, 'Error') + value = match.group(2) if enabled is True else '' + return dict(bfd_ip=value) + + def _parse_ip_version(self, config, vrid): + match = re.search(r'^\s*vrrp %s ip version (\d+)$' % + vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(ip_version=value) + + def _parse_delay_reload(self, config, vrid): + match = re.search(r'^\s*vrrp %s delay reload (\d+)$' % + vrid, config, re.M) + value = int(match.group(1)) if match else None + return dict(delay_reload=value) + + def _parse_track(self, config, vrid): + matches = re.findall(r'^\s*vrrp %s track (\S+) ' + r'(decrement|shutdown)(?:( \d+$|$))' % + vrid, config, re.M) + value = dict() + for match in matches: + object = match[0] + action = match[1] + amount = None if match[2] == '' else int(match[2]) + key = (object, action) + value.update({key: amount}) + return dict(track=value) + + def create(self, interface, vrid, **kwargs): + """Creates a vrrp instance from an interface + + Note: + This method will attempt to create a vrrp in the node's + operational config. If the vrrp already exists on the + interface, then this method will set the properties of + the existing vrrp to those that have been passed in, if + possible. + + Args: + interface (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be created. + kwargs (dict): A dictionary specifying the properties to + be applied to the new vrrp instance. See library + documentation for available keys and values. + + Returns: + True if the vrrp could be created otherwise False (see Node) + + """ + + if 'enable' not in kwargs: + kwargs['enable'] = False + + return self._vrrp_set(interface, vrid, **kwargs) + + def delete(self, interface, vrid): + """Deletes a vrrp instance from an interface + + Note: + This method will attempt to delete the vrrp from the node's + operational config. If the vrrp does not exist on the + interface then this method will not perform any changes + but still return True + + Args: + interface (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be deleted. + + Returns: + True if the vrrp could be deleted otherwise False (see Node) + + """ + + vrrp_str = "no vrrp %d" % vrid + return self.configure_interface(interface, vrrp_str) + + def default(self, interface, vrid): + """Defaults a vrrp instance from an interface + + Note: + This method will attempt to default the vrrp on the node's + operational config. Default results in the deletion of the + specified vrrp . If the vrrp does not exist on the + interface then this method will not perform any changes + but still return True + + Args: + interface (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be defaulted. + + Returns: + True if the vrrp could be defaulted otherwise False (see Node) + + """ + + vrrp_str = "default vrrp %d" % vrid + return self.configure_interface(interface, vrrp_str) + + def update(self, interface, vrid, **kwargs): + """Update a vrrp instance from an interface + + Note: + This method will attempt to set parameters for the vrrp on + the node's operational config. Parameters specified in the + kwargs argument will be matched against the current + parameters for the vrrp, and only those parameters that + have changed from the current configuration will be updated. + + Args: + interface (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + kwargs (dict): A dictionary specifying the properties to + be applied to the vrrp instance. See library documentation + for available keys and values. + + Returns: + True if the vrrp could be updated otherwise False (see Node) + + """ + + update = dict(kwargs) + + # Get the current configuration for the vrrp from the switch. + # Raise an error if the vrrp does not exist on the interface. + try: + current = self.get(interface)[vrid] + except: + raise ValueError("Attempt to configure a non-existent vrrp") + + # Keep only those properties in the update dictionary that + # are different than the current vrrp configuration. + for prop in PROPERTIES: + if prop in update: + if update[prop] == current[prop]: + del update[prop] + + # Configure the vrrp + return self._vrrp_set(interface, vrid, **update) + + def _vrrp_set(self, interface, vrid, **kwargs): + # Configure the commands to create or update a vrrp + # configuration, and send the commands to the node. + + vrconf = kwargs + + # step through the individual vrrp properties and + # set those that need to be changed + commands = [] + + primary_ip = vrconf.get('primary_ip', '__NONE__') + if primary_ip != '__NONE__': + if primary_ip in ('no', None): + vrrps = self.get(interface) + primary_ip = vrrps[vrid]['primary_ip'] + commands.append("no vrrp %d ip %s" % (vrid, primary_ip)) + elif primary_ip is 'default': + vrrps = self.get(interface) + primary_ip = vrrps[vrid]['primary_ip'] + commands.append("default vrrp %d ip %s" % (vrid, primary_ip)) + else: + commands.append("vrrp %d ip %s" % (vrid, primary_ip)) + + priority = vrconf.get('priority', '__NONE__') + if priority != '__NONE__': + if priority in ('no', None): + commands.append("no vrrp %d priority" % vrid) + elif priority == 'default': + commands.append("default vrrp %d priority" % vrid) + else: + commands.append("vrrp %d priority %d" % (vrid, priority)) + + description = vrconf.get('description', '__NONE__') + if description != '__NONE__': + if description in ('no', None): + commands.append("no vrrp %d description" % vrid) + elif description == 'default': + commands.append("default vrrp %d description" % vrid) + else: + commands.append("vrrp %d description %s" % (vrid, description)) + + secondary_ip = vrconf.get('secondary_ip', '__NONE__') + if secondary_ip != '__NONE__': + secondary_add = secondary_ip.get('add', '__NONE__') + secondary_remove = secondary_ip.get('remove', '__NONE__') + if secondary_add != '__NONE__': + for s_ip in secondary_add: + commands.append("vrrp %d ip %s secondary" % (vrid, s_ip)) + if secondary_remove != '__NONE__': + for s_ip in secondary_remove: + commands.append("no vrrp %d ip %s secondary" + % (vrid, s_ip)) + + ip_version = vrconf.get('ip_version', '__NONE__') + if ip_version != '__NONE__': + if ip_version in ('no', None): + commands.append("no vrrp %d ip version" % vrid) + elif ip_version == 'default': + commands.append("default vrrp %d ip version" % vrid) + elif ip_version in (2, 3): + commands.append("vrrp %d ip version %d" % (vrid, ip_version)) + else: + raise ValueError("vrrp property 'ip_version' must be " + "2, 3, 'no', 'default', or None") + + enable = vrconf.get('enable', '__NONE__') + if enable != '__NONE__': + if enable in ('no', True): + commands.append("no vrrp %d shutdown" % vrid) + elif enable is False: + commands.append("vrrp %d shutdown" % vrid) + elif enable == 'default': + commands.append("default vrrp %d shutdown" % vrid) + else: + raise ValueError("vrrp property 'enable' must " + "be True, False, 'no', or 'default'") + + timers_advertise = vrconf.get('timers_advertise', '__NONE__') + if timers_advertise != '__NONE__': + if timers_advertise in ('no', None): + commands.append("no vrrp %d timers advertise" % vrid) + elif timers_advertise == 'default': + commands.append("default vrrp %d timers advertise" % vrid) + elif 1 <= timers_advertise <= 255: + commands.append("vrrp %d timers advertise %d" + % (vrid, timers_advertise)) + else: + raise ValueError("vrrp property 'timers_advertise' must " + "be in the range 1-255, 'no', 'default', " + "or None") + + mac_add_adv_int = \ + vrconf.get('mac_address_advertisement_interval', '__NONE__') + if mac_add_adv_int != '__NONE__': + if mac_add_adv_int in ('no', None): + commands.append("no vrrp %d mac-address " + "advertisement-interval" % vrid) + elif mac_add_adv_int == 'default': + commands.append("default vrrp %d mac-address " + "advertisement-interval" % vrid) + elif 1 <= mac_add_adv_int <= 3600: + commands.append("vrrp %d mac-address advertisement-interval %d" + % (vrid, mac_add_adv_int)) + else: + raise ValueError("vrrp property 'mac_address_advertisement_" + "interval must be in the range 1-3600, 'no', " + "'default', or None") + + preempt = vrconf.get('preempt', '__NONE__') + if preempt != '__NONE__': + if preempt in ('no', False): + commands.append("no vrrp %d preempt" % vrid) + elif preempt == 'default': + commands.append("default vrrp %d preempt" % vrid) + elif preempt is True: + commands.append("vrrp %d preempt" % vrid) + else: + raise ValueError("vrrp property 'preempt' must " + "be True, False, 'no', or 'default'") + + preempt_delay_minimum = vrconf.get('preempt_delay_minimum', + '__NONE__') + if preempt_delay_minimum != '__NONE__': + if preempt_delay_minimum in ('no', None): + commands.append("no vrrp %d preempt delay minimum" % vrid) + elif preempt_delay_minimum == 'default': + commands.append("default vrrp %d preempt delay minimum" % vrid) + elif 0 <= preempt_delay_minimum <= 3600: + commands.append("vrrp %d preempt delay minimum %d" % + (vrid, preempt_delay_minimum)) + else: + raise ValueError("vrrp property 'preempt_delay_minimum' " + "must an integer in the range 0-3600, None " + "'no', or 'default'") + + preempt_delay_reload = vrconf.get('preempt_delay_reload', '__NONE__') + if preempt_delay_reload != '__NONE__': + if preempt_delay_reload in ('no', None): + commands.append("no vrrp %d preempt delay reload" % vrid) + elif preempt_delay_reload == 'default': + commands.append("default vrrp %d preempt delay reload" % vrid) + elif 0 <= preempt_delay_reload <= 3600: + commands.append("vrrp %d preempt delay reload %d" % + (vrid, preempt_delay_reload)) + else: + raise ValueError("vrrp property 'preempt_delay_reload' " + "must be an integer in the range 0-3600, " + "None, 'no', or 'default'") + + delay_reload = vrconf.get('delay_reload', '__NONE__') + if delay_reload != '__NONE__': + if delay_reload in ('no', None): + commands.append("no vrrp %d delay reload" % vrid) + elif delay_reload == 'default': + commands.append("default vrrp %d delay reload" % vrid) + elif 0 <= delay_reload <= 3600: + commands.append("vrrp %d delay reload %d" % + (vrid, delay_reload)) + else: + raise ValueError("vrrp property 'delay_reload' " + "must be an integer in the range 0-3600, " + "None, 'no', or 'default'") + + authentication = vrconf.get('authentication', '__NONE__') + if authentication != '__NONE__': + pass + # XXX not yet implemented + # needs some handling, because input string does not + # necessarily match the status string. + + track = vrconf.get('track', '__NONE__') + if track != '__NONE__': + for (key, amount) in track.iteritems(): + (tracked, action) = key + if amount in ('no', None): + commands.append("no vrrp %d track %s %s" + % (vrid, tracked, action)) + elif amount is 'default': + commands.append("default vrrp %d track %s %s" + % (vrid, tracked, action)) + elif action == 'shutdown': + # if action is shutdown, and amount is not 'no', None, + # or 'default, track shutdown for this object + commands.append("vrrp %d track %s %s" + % (vrid, tracked, action)) + elif amount is int(amount) and action == 'decrement': + # if the amount is an integer and the action is + # decrement, use decremental tracking for this object + commands.append("vrrp %d track %s %s %d" + % (vrid, tracked, action, amount)) + else: + # The action/amount combination did not match above, + # there is something wrong. + raise ValueError("vrrp property 'track' contains " + "improperly formatted data (%s, %s): " + "%s. See library documentation for " + "formatting example." + % (tracked, action, amount)) + + bfd_ip = vrconf.get('bfd_ip', '__NONE__') + if bfd_ip != '__NONE__': + if bfd_ip in ('no', None): + commands.append("no vrrp %d bfd ip" % vrid) + elif bfd_ip == 'default': + commands.append("default vrrp %d bfd ip" % vrid) + else: + commands.append("vrrp %d bfd ip %s" % (vrid, bfd_ip)) + + # Send the commands to the requested interface + result = self.configure_interface(interface, commands) + # And verify the commands succeeded + if result is False: + return self.error + return result + + def vrconf_format(self, vrconfig): + """Formats a vrrp configuration dictionary to match the + information as presented from the get and getall methods. + vrrp configuration dictionaries passed to the create and update + methods may contain data for setting properties which results + in a default value on the node. In these instances, the data + for setting or changing the property is replaced with the + value that would be returned from the get and getall methods. + + Intended for validating updated vrrp configurations. + """ + + fixed = dict(vrconfig) + + # primary_ip: default, no, None results in address of 0.0.0.0 + if fixed['primary_ip'] in ('no', 'default', None): + fixed['primary_ip'] = '0.0.0.0' + # priority: default, no, None results in priority of 100 + if fixed['priority'] in ('no', 'default', None): + fixed['priority'] = 100 + # description: default, no, None results in None + if fixed['description'] in ('no', 'default', None): + fixed['description'] = None + # secondary_ip: add key becomes exists key + if 'secondary_ip' in fixed: + if 'add' in dict(fixed['secondary_ip']): + fixed['secondary_ip']['exists'] = fixed['secondary_ip']['add'] + del fixed['secondary_ip']['add'] + if 'remove' in fixed['secondary_ip']: + del fixed['secondary_ip']['remove'] + # # secondary_ip: default, no, None results in None + # if fixed['secondary_ip'] in ('no', 'default', None): + # fixed['secondary_ip'] = None + # ip_version: default, no, None results in value of 2 + if fixed['ip_version'] in ('no', 'default', None): + fixed['ip_version'] = 2 + # timers_advertise: default, no, None results in value of 1 + if fixed['timers_advertise'] in ('no', 'default', None): + fixed['timers_advertise'] = 1 + # mac_address_advertisement_interaval: + # default, no, None results in value of 30 + if fixed['mac_address_advertisement_interval'] in \ + ('no', 'default', None): + fixed['mac_address_advertisement_interval'] = 30 + # preempt: default, no results in value of False + if fixed['preempt'] in ('no', 'default'): + fixed['preempt'] = False + # preempt_delay_minimum: default, no, None results in value of 0 + if fixed['preempt_delay_minimum'] in ('no', 'default', None): + fixed['preempt_delay_minimum'] = 0 + # preempt_delay_reload: default, no, None results in value of 0 + if fixed['preempt_delay_reload'] in ('no', 'default', None): + fixed['preempt_delay_reload'] = 0 + # delay_reload: default, no, None results in value of 0 + if fixed['delay_reload'] in ('no', 'default', None): + fixed['delay_reload'] = 0 + # authenticetion -> XXX needs implemented + # track: default, no, None removes the entry + if 'track' in fixed: + # Work with a temporary copy to keep things straight + tracks = {} + for (key, amount) in fixed['track'].iteritems(): + (tracked, action) = key + if amount in ('no', 'default', None): + # This tracked object should have been deleted + # from the config. Do not keep it in the dictionary. + pass + elif action == 'shutdown': + # This is a valid tracked shutdown. Set it's value to None. + tracks[key] = None + else: + # This is a valid decerment. Copy it exactly. + tracks[key] = amount + # Copy back the temporary dict into the original + fixed['track'] = dict(tracks) + # bfd_ip: default, no, None results in '' + if fixed['bfd_ip'] in ('no', 'default', None): + fixed['bfd_ip'] = '' + + return fixed + + +def instance(node): + """Returns an instance of Vrrp + + Args: + node (Node): The node argument passes an instance of Node to the + resource + + Returns: + object: An instance of Vrrp + """ + return Vrrp(node) diff --git a/test/fixtures/running_config.vrrp b/test/fixtures/running_config.vrrp new file mode 100644 index 0000000..852f62c --- /dev/null +++ b/test/fixtures/running_config.vrrp @@ -0,0 +1,525 @@ +! +logging level VRRP debugging +! +default snmp-server enable traps vrrp +default snmp-server enable traps vrrp trap-new-master +! +interface Port-Channel1 + no description + no shutdown + default load-interval + logging event link-status use-global + switchport access vlan 1 + switchport trunk native vlan 1 + switchport trunk allowed vlan 1-4094 + switchport mode access + switchport mac address learning + no switchport private-vlan mapping + switchport + default encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + no port-channel min-links + no port-channel lacp fallback + port-channel lacp fallback timeout 90 + no l2 mtu + no mlag + no switchport port-security + switchport port-security maximum 1 + default qos trust + qos cos 5 + qos dscp 2 + no shape rate + mc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 4 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 5 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 6 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 7 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + sflow enable + no spanning-tree portfast + spanning-tree portfast auto + no spanning-tree link-type + no spanning-tree bpduguard + no spanning-tree bpdufilter + no spanning-tree cost + spanning-tree port-priority 128 + no spanning-tree guard + no spanning-tree bpduguard rate-limit + logging event spanning-tree use-global + switchport tap native vlan 1 + no switchport tap identity + switchport tap allowed vlan 1-4094 + switchport tool allowed vlan 1-4094 + no switchport tool identity + no switchport tap truncation + no switchport tool truncation + no switchport tap default group + no switchport tool group + no switchport tool dot1q remove outer +! +interface Port-Channel10 + no description + no shutdown + default load-interval + logging event link-status use-global + switchport access vlan 1 + switchport trunk native vlan 1 + switchport trunk allowed vlan 1-4094 + switchport mode access + switchport mac address learning + no switchport private-vlan mapping + switchport + default encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + no ip proxy-arp + no ip local-proxy-arp + ip address 10.10.5.1/24 + no ip verify unicast + no port-channel min-links + no port-channel lacp fallback + port-channel lacp fallback timeout 90 + no l2 mtu + no mlag + no switchport port-security + switchport port-security maximum 1 + default qos trust + qos cos 5 + qos dscp 2 + no shape rate + mc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 4 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 5 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 6 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 7 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + sflow enable + no spanning-tree portfast + spanning-tree portfast auto + no spanning-tree link-type + no spanning-tree bpduguard + no spanning-tree bpdufilter + no spanning-tree cost + spanning-tree port-priority 128 + no spanning-tree guard + no spanning-tree bpduguard rate-limit + logging event spanning-tree use-global + switchport tap native vlan 1 + no switchport tap identity + switchport tap allowed vlan 1-4094 + switchport tool allowed vlan 1-4094 + no switchport tool identity + no switchport tap truncation + no switchport tool truncation + no switchport tap default group + no switchport tool group + no switchport tool dot1q remove outer + vrrp 10 priority 150 + vrrp 10 timers advertise 1 + vrrp 10 mac-address advertisement-interval 30 + vrrp 10 preempt + vrrp 10 preempt delay minimum 0 + vrrp 10 preempt delay reload 0 + vrrp 10 delay reload 0 + no vrrp 10 authentication + vrrp 10 ip 10.10.5.10 + vrrp 10 ip 10.10.5.20 secondary + vrrp 10 ipv6 :: + vrrp 10 description vrrp 10 on Port-Channel10 + no vrrp 10 shutdown + no vrrp 10 bfd ip + no vrrp 10 bfd ipv6 + vrrp 10 ip version 2 +! +interface Ethernet1 + no description + no shutdown + default load-interval + logging event link-status use-global + no dcbx mode + no mac-address + no link-debounce + no flowcontrol send + no flowcontrol receive + no mac timestamp + no speed + no l2 mtu + default logging event congestion-drops + default unidirectional + switchport access vlan 1 + switchport trunk native vlan 1 + switchport trunk allowed vlan 1-4094 + switchport mode access + switchport mac address learning + no switchport private-vlan mapping + switchport + default encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + no ip proxy-arp + no ip local-proxy-arp + ip address 10.10.6.1/24 + no ip verify unicast + no channel-group + lacp rate normal + lacp port-priority 32768 + lldp transmit + lldp receive + no msrp + no mvrp + no switchport port-security + switchport port-security maximum 1 + default qos trust + qos cos 5 + qos dscp 2 + no shape rate + mc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 4 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 5 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 6 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 7 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + sflow enable + no spanning-tree portfast + spanning-tree portfast auto + no spanning-tree link-type + no spanning-tree bpduguard + no spanning-tree bpdufilter + no spanning-tree cost + spanning-tree port-priority 128 + no spanning-tree guard + no spanning-tree bpduguard rate-limit + logging event spanning-tree use-global + switchport tap native vlan 1 + no switchport tap identity + switchport tap allowed vlan 1-4094 + switchport tool allowed vlan 1-4094 + no switchport tool identity + no switchport tap truncation + no switchport tool truncation + no switchport tap default group + no switchport tool group + no switchport tool dot1q remove outer + vrrp 10 priority 175 + vrrp 10 timers advertise 1 + vrrp 10 mac-address advertisement-interval 30 + vrrp 10 preempt + vrrp 10 preempt delay minimum 0 + vrrp 10 preempt delay reload 0 + vrrp 10 delay reload 0 + no vrrp 10 authentication + vrrp 10 ip 10.10.6.10 + vrrp 10 ipv6 :: + vrrp 10 description vrrp 10 on Ethernet1 + no vrrp 10 shutdown + no vrrp 10 bfd ip + no vrrp 10 bfd ipv6 + vrrp 10 ip version 2 +! +interface Vlan50 + no description + no shutdown + default load-interval + mtu 1500 + logging event link-status use-global + autostate + no private-vlan mapping + snmp trap link-status + no ip proxy-arp + no ip local-proxy-arp + ip address 10.10.4.1/24 + no ip verify unicast + default arp timeout 14400 + default ipv6 nd cache expire 14400 + bfd interval 300 min_rx 300 multiplier 3 + no bfd echo + default ip dhcp smart-relay + no ip helper-address + no ipv6 dhcp relay destination + ip dhcp relay information option circuit-id Vlan50 + no ip igmp + ip igmp version 3 + ip igmp last-member-query-count 2 + ip igmp last-member-query-interval 10 + ip igmp query-max-response-time 100 + ip igmp query-interval 125 + ip igmp startup-query-count 2 + ip igmp startup-query-interval 310 + ip igmp router-alert optional connected + ip igmp host-proxy + no ip igmp host-proxy report-interval + ip igmp host-proxy version 3 + no ip igmp host-proxy + no ipv6 enable + no ipv6 address + no ipv6 verify unicast + no ipv6 nd ra suppress + ipv6 nd ra interval msec 200000 + ipv6 nd ra lifetime 1800 + no ipv6 nd ra mtu suppress + no ipv6 nd managed-config-flag + no ipv6 nd other-config-flag + ipv6 nd reachable-time 0 + ipv6 nd router-preference medium + ipv6 nd ra dns-servers lifetime 300 + ipv6 nd ra dns-suffixes lifetime 300 + ipv6 nd ra hop-limit 64 + ip mfib fastdrop + default ntp serve + no ip pim sparse-mode + no ip pim border-router + ip pim query-interval 30 + ip pim join-prune-interval 60 + ip pim dr-priority 1 + no ip pim neighbor-filter + default ip pim bfd-instance + no ip pim bsr-border + no ip virtual address + vrrp 10 priority 200 + vrrp 10 timers advertise 3 + vrrp 10 mac-address advertisement-interval 30 + vrrp 10 preempt + vrrp 10 preempt delay minimum 0 + vrrp 10 preempt delay reload 0 + vrrp 10 delay reload 0 + no vrrp 10 authentication + vrrp 10 ip 10.10.4.10 + vrrp 10 ip 10.10.4.21 secondary + vrrp 10 ipv6 :: + no vrrp 10 description + no vrrp 10 shutdown + vrrp 10 track Ethernet1 shutdown + no vrrp 10 bfd ip + no vrrp 10 bfd ipv6 + vrrp 10 ip version 2 + vrrp 20 priority 100 + vrrp 20 timers advertise 5 + vrrp 20 mac-address advertisement-interval 30 + no vrrp 20 preempt + vrrp 20 preempt delay minimum 0 + vrrp 20 preempt delay reload 0 + vrrp 20 delay reload 0 + vrrp 20 authentication text 12345 + vrrp 20 ip 10.10.4.20 + vrrp 20 ipv6 :: + no vrrp 20 description + vrrp 20 shutdown + vrrp 20 track Ethernet1 shutdown + vrrp 20 track Ethernet2 decrement 1 + vrrp 20 track Ethernet2 shutdown + no vrrp 20 bfd ip + no vrrp 20 bfd ipv6 + vrrp 20 ip version 2 + vrrp 30 priority 50 + vrrp 30 timers advertise 1 + vrrp 30 mac-address advertisement-interval 30 + vrrp 30 preempt + vrrp 30 preempt delay minimum 0 + vrrp 30 preempt delay reload 0 + vrrp 30 delay reload 0 + vrrp 30 authentication ietf-md5 key-string 7 bu1yTgzm0RDgraNS0MNkaA== + vrrp 30 ip 10.10.4.30 + vrrp 30 ipv6 :: + no vrrp 30 description + no vrrp 30 shutdown + vrrp 30 bfd ip 10.10.4.33 + no vrrp 30 bfd ipv6 + vrrp 30 ip version 2 +! +! diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index e69de29..bebb98c 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -0,0 +1,421 @@ +# +# 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 systestlib import DutSystemTest +from testlib import random_string + +IP_PREFIX = '10.10.10.' +VR_CONFIG = { + 'primary_ip': '10.10.10.2', + 'priority': 200, + 'description': 'modified vrrp 10 on an interface', + 'secondary_ip': { + 'add': ['10.10.10.11'], + }, + 'ip_version': 3, + 'enable': False, + 'timers_advertise': 2, + 'mac_address_advertisement_interval': 3, + 'preempt': False, + 'preempt_delay_minimum': 1, + 'preempt_delay_reload': None, + 'delay_reload': 1, + 'authentication': '', + 'track': { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): 10, + }, + 'bfd_ip': '10.10.10.150', +} + +# Define various test input +PRIMARY_IP = ['10.10.10.2', 'default', '10.10.10.3', 'no', '10.10.10.4', None] +PRIORITY = [200, 'default', 175, 'no', 190, None] +DESCRIPTION = ['1st modified vrrp', 'default', '2nd modified vrrp', 'no', + '3rd modified vrrp', None] +SECONDARY_IP = [ + {'add': ['10.10.10.51', '10.10.10.52']}, + {'add': ['10.10.10.53', '10.10.10.54'], + 'remove': ['10.10.10.51', '10.10.10.52']}, +] +IP_VERSION = [2, 3, 'default', 3, 'no', 3, None] +ENABLE = [True, False, True] +TIMERS_ADVERTISE = [10, 'default', 20, 'no', 30, None] +MAC_ADDR_ADV_INTVL = [50, 'default', 55, 'no', 60, None] +PREEMPT = [True, False, True] +PREEMPT_DELAY_MINIMUM = [3600, 'default', 500, 'no', 150, None] +PREEMPT_DELAY_RELOAD = [3600, 'default', 500, 'no', 150, None] +DELAY_RELOAD = [30, 'default', 25, 'no', 15, None] +TRACK = [ + { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): 10, + }, + { + ('Ethernet2', 'shutdown'): None, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): 'default', + }, + { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): 2, + ('Ethernet2', 'decrement'): 20, + }, + { + ('Ethernet2', 'shutdown'): None, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): 'no', + }, + { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): 2, + ('Ethernet2', 'decrement'): 20, + }, + { + ('Ethernet2', 'shutdown'): None, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): None, + }, +] +BFD_IP = ['10.10.10.160', 'default', '10.10.10.161', 'no', + '10.10.10.162', None] + + +class TestApiVrrp(DutSystemTest): + + def _vlan_setup(self, dut): + dut.config(['no interface vlan 101', + 'interface vlan 101', + 'ip address %s1/24' % IP_PREFIX, + 'exit']) + return 'Vlan101' + + def test_get(self): + vrid = 98 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'vrrp %d shutdown' % vrid, + 'exit']) + response = dut.api('vrrp').get(interface) + + self.assertIsNotNone(response) + + def test_create(self): + vrid = 99 + import copy + vrrp_conf = copy.deepcopy(VR_CONFIG) + # vrrp_conf = dict(VR_CONFIG) + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'exit']) + + response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) + self.assertIs(response, True) + + # Fix the configuration dict for proper output + vrrp_conf = dut.api('vrrp').vrconf_format(vrrp_conf) + + response = dut.api('vrrp').get(interface)[vrid] + self.maxDiff = None + self.assertEqual(response, vrrp_conf) + + def test_delete(self): + vrid = 101 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + response = dut.api('vrrp').delete(interface, vrid) + self.assertIs(response, True) + + def test_default(self): + vrid = 102 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + response = dut.api('vrrp').delete(interface, vrid) + self.assertIs(response, True) + + def test_update(self): + pass + vrid = 103 + import copy + vrrp_conf = copy.deepcopy(VR_CONFIG) + # vrrp_conf = dict(VR_CONFIG) + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'exit']) + + # Create the inital vrrp on the interface + response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) + self.assertIs(response, True) + + # Update some of the information on the vrrp + vrrp_update = { + 'primary_ip': '10.10.10.12', + 'priority': 200, + 'description': 'updated vrrp 10 on an interface', + 'secondary_ip': { + 'add': ['10.10.10.13', '10.10.10.23'], + 'remove': ['10.10.10.11'], + }, + 'ip_version': 2, + 'enable': True, + 'timers_advertise': None, + 'mac_address_advertisement_interval': 'default', + 'preempt': True, + 'preempt_delay_minimum': 'default', + 'preempt_delay_reload': 'default', + 'delay_reload': 'default', + 'authentication': '', + 'track': { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): None, + ('Ethernet2', 'decrement'): 1, + }, + 'bfd_ip': None, + } + + response = dut.api('vrrp').update(interface, vrid, **vrrp_update) + self.assertIs(response, True) + vrrp_update = dut.api('vrrp').vrconf_format(vrrp_update) + + response = dut.api('vrrp').get(interface)[vrid] + + self.maxDiff = None + self.assertEqual(response, vrrp_update) + + def test_update_primary_ip(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for p_ip in PRIMARY_IP: + vrconfig = {'primary_ip': p_ip} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_priority(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for priority in PRIORITY: + vrconfig = {'priority': priority} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_description(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for description in DESCRIPTION: + vrconfig = {'description': description} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_secondary_ip(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for s_ip in SECONDARY_IP: + vrconfig = {'secondary_ip': s_ip} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_ip_version(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for ip_version in IP_VERSION: + vrconfig = {'ip_version': ip_version} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_timers_advertise(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for timers_advertise in TIMERS_ADVERTISE: + vrconfig = {'timers_advertise': timers_advertise} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_mac_address_advertisement_interval(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for mac_addr_adv_intvl in MAC_ADDR_ADV_INTVL: + vrconfig = {'mac_address_advertisement_interval': + mac_addr_adv_intvl} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_preempt(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for preempt in PREEMPT: + vrconfig = {'preempt': preempt} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_preempt_delay_minimum(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for preempt_delay_minimum in PREEMPT_DELAY_MINIMUM: + vrconfig = {'preempt_delay_minimum': preempt_delay_minimum} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_preempt_delay_reload(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for preempt_delay_reload in PREEMPT_DELAY_RELOAD: + vrconfig = {'preempt_delay_reload': preempt_delay_reload} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_delay_reload(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for delay_reload in DELAY_RELOAD: + vrconfig = {'delay_reload': delay_reload} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_track(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for track in TRACK: + vrconfig = {'track': track} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + def test_update_bfd_ip(self): + vrid = 104 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'exit']) + + for bfd_ip in BFD_IP: + vrconfig = {'bfd_ip': bfd_ip} + response = dut.api('vrrp').update(interface, vrid, **vrconfig) + self.assertIs(response, True) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py index e69de29..ca60cb9 100644 --- a/test/unit/test_api_vrrp.py +++ b/test/unit/test_api_vrrp.py @@ -0,0 +1,697 @@ +# +# right (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.vrrp + +upd_intf = 'Vlan50' +upd_vrid = 10 +upd_cmd = 'interface %s' % upd_intf +known_vrrps = { + 'Ethernet1': { + 10: {'priority': 175, + 'timers_advertise': 1, + 'mac_address_advertisement_interval': 30, + 'preempt': True, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': '', + 'primary_ip': '10.10.6.10', + 'secondary_ip': {'exists': None}, + 'description': 'vrrp 10 on Ethernet1', + 'enable': True, + 'track': {}, + 'bfd_ip': '', + 'ip_version': 2} + }, + 'Port-Channel10': { + 10: {'priority': 150, + 'timers_advertise': 1, + 'mac_address_advertisement_interval': 30, + 'preempt': True, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': '', + 'primary_ip': '10.10.5.10', + 'secondary_ip': { + 'exists': ['10.10.5.20'] + }, + 'description': 'vrrp 10 on Port-Channel10', + 'enable': True, + 'track': {}, + 'bfd_ip': '', + 'ip_version': 2} + }, + 'Vlan50': { + 10: {'priority': 200, + 'timers_advertise': 3, + 'mac_address_advertisement_interval': 30, + 'preempt': True, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': '', + 'primary_ip': '10.10.4.10', + 'secondary_ip': { + 'exists': ['10.10.4.21'] + }, + 'description': '', + 'enable': True, + 'track': { + ('Ethernet1', 'shutdown'): None + }, + 'bfd_ip': '', + 'ip_version': 2}, + 20: {'priority': 100, + 'timers_advertise': 5, + 'mac_address_advertisement_interval': 30, + 'preempt': False, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': 'text 12345', + 'primary_ip': '10.10.4.20', + 'secondary_ip': {'exists': None}, + 'description': '', + 'enable': False, + 'track': { + ('Ethernet1', 'shutdown'): None, + ('Ethernet2', 'decrement'): 1, + ('Ethernet2', 'shutdown'): None + }, + 'bfd_ip': '', + 'ip_version': 2}, + 30: {'priority': 50, + 'timers_advertise': 1, + 'mac_address_advertisement_interval': 30, + 'preempt': True, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': + 'ietf-md5 key-string 7 bu1yTgzm0RDgraNS0MNkaA==', + 'primary_ip': '10.10.4.30', + 'secondary_ip': {'exists': None}, + 'description': '', + 'enable': True, + 'track': {}, + 'bfd_ip': '10.10.4.33', + 'ip_version': 2} + } + } + + +class TestApiVrrp(EapiConfigUnitTest): + + maxDiff = None + + def __init__(self, *args, **kwargs): + super(TestApiVrrp, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.vrrp.Vrrp(None) + self.config = open(get_fixture('running_config.vrrp')).read() + + def test_instance(self): + result = pyeapi.api.vrrp.instance(None) + self.assertIsInstance(result, pyeapi.api.vrrp.Vrrp) + + def test_get(self): + # Request various sets of vrrp configurations + vrrp = ['Vlan50', 'Ethernet1', 'Port-Channel10'] + # vrrp = [('Vlan50', 10), + # ('Vlan50', 20), + # ('Ethernet1', 10), + # ('Port-Channel10', 10)] + + for interface in vrrp: + known = known_vrrps.get(interface) + result = self.instance.get(interface) + self.assertEqual(result, known) + + def test_get_non_existent_interface(self): + # Request vrrp configuration for an interface that + # is not defined + result = self.instance.get('Vlan2000') + self.assertIsNone(result) + + def test_get_invalid_parameters(self): + # Pass empty, None, or other invalid parameters to get() + with self.assertRaises(ValueError): + self.instance.get('') + with self.assertRaises(ValueError): + self.instance.get(None) + + def test_getall(self): + # Get all the vrrp configurations from the config + result = self.instance.getall() + self.assertEqual(result, known_vrrps) + + def test_create(self): + interface = 'Ethernet1' + vrid = 10 + configuration = { + 'primary_ip': '10.10.60.10', + 'priority': 200, + 'description': 'modified vrrp 10 on Ethernet1', + 'secondary_ip': { + 'add': ['10.10.60.20'], + 'remove': ['10.10.60.30'], + }, + 'ip_version': 3, + 'timers_advertise': 2, + 'mac_address_advertisement_interval': 3, + 'preempt': False, + 'preempt_delay_minimum': 1, + 'preempt_delay_reload': 1, + 'delay_reload': 1, + 'authentication': '', + 'track': { + ('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'shutdown'): 1, + ('Ethernet2', 'decrement'): 1, + ('Ethernet1', 'decrement'): None, + }, + 'bfd_ip': '10.10.60.30', + } + + cmds = [ + 'interface Ethernet1', + 'vrrp 10 ip 10.10.60.10', + 'vrrp 10 priority 200', + 'vrrp 10 description modified vrrp 10 on Ethernet1', + 'vrrp 10 ip 10.10.60.20 secondary', + 'no vrrp 10 ip 10.10.60.30 secondary', + 'vrrp 10 ip version 3', + 'vrrp 10 shutdown', + 'vrrp 10 timers advertise 2', + 'vrrp 10 mac-address advertisement-interval 3', + 'no vrrp 10 preempt', + 'vrrp 10 preempt delay minimum 1', + 'vrrp 10 preempt delay reload 1', + 'vrrp 10 delay reload 1', + # 'no vrrp 10 authentication', + 'vrrp 10 track Ethernet2 shutdown', + 'no vrrp 10 track Ethernet1 decrement', + 'vrrp 10 track Ethernet2 decrement 1', + 'vrrp 10 track Ethernet1 shutdown', + 'vrrp 10 bfd ip 10.10.60.30', + ] + func = function('create', interface, vrid, **configuration) + + self.eapi_positive_config_test(func, cmds) + + def test_delete(self): + interface = 'Ethernet1' + vrid = 10 + + cmds = [ + 'interface Ethernet1', + 'no vrrp 10', + ] + + func = function('delete', interface, vrid) + + self.eapi_positive_config_test(func, cmds) + + def test_default(self): + interface = 'Ethernet1' + vrid = 10 + + cmds = [ + 'interface Ethernet1', + 'default vrrp 10', + ] + + func = function('default', interface, vrid) + + self.eapi_positive_config_test(func, cmds) + + def test_update_primary_ip(self): + # vrrp 10 ip 10.10.4.10 + ip1 = '10.10.4.110' + ipcurr = '10.10.4.10' + cases = [ + (ip1, 'vrrp %d ip %s' % (upd_vrid, ip1)), + ('default', 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), + ('no', 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), + (None, 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), + ] + + for (primary_ip, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + primary_ip=primary_ip) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_update_priority(self): + # vrrp 10 priority 200 + cases = [ + (150, 'vrrp %d priority 150' % upd_vrid), + ('default', 'default vrrp %d priority' % upd_vrid), + ('no', 'no vrrp %d priority' % upd_vrid), + (None, 'no vrrp %d priority' % upd_vrid), + ] + + for (priority, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + priority=priority) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_update_description(self): + # no vrrp 10 description + desc = 'test description' + cases = [ + (desc, 'vrrp %d description %s' % (upd_vrid, desc)), + ('default', 'default vrrp %d description' % upd_vrid), + ('no', 'no vrrp %d description' % upd_vrid), + (None, 'no vrrp %d description' % upd_vrid), + ] + + for (description, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + description=description) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_update_secondary_ip(self): + # vrrp 10 ip 10.10.4.21 secondary + ip1 = '10.10.4.41' + ip2 = '10.10.4.42' + ipcurr = '10.10.4.21' + cases = [ + ( + {'add': [ip1, ip2], 'remove': [ipcurr]}, + ['vrrp %d ip %s secondary' % (upd_vrid, ip1), + 'vrrp %d ip %s secondary' % (upd_vrid, ip2), + 'no vrrp %d ip %s secondary' % (upd_vrid, ipcurr)] + ), + ] + + for (secondary_ip, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + secondary_ip=secondary_ip) + exp_cmds = [upd_cmd] + cmd + self.eapi_positive_config_test(func, exp_cmds) + + def test_update_ip_version(self): + # vrrp 10 ip version 2 + cases = [ + (3, 'vrrp %d ip version 3' % upd_vrid), + ('default', 'default vrrp %d ip version' % upd_vrid), + ('no', 'no vrrp %d ip version' % upd_vrid), + (None, 'no vrrp %d ip version' % upd_vrid), + ] + + for (ip_version, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + ip_version=ip_version) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 5] + + for ip_version in cases: + func = function('update', upd_intf, upd_vrid, + ip_version=ip_version) + self.eapi_exception_config_test(func, ValueError) + + def test_update_enable(self): + # Test when going from enabled to disabled + # no vrrp 10 shutdown + cases = [ + (False, 'vrrp %d shutdown' % upd_vrid), + (True, None), + ('no', 'no vrrp %d shutdown' % upd_vrid), + ('default', 'default vrrp %d shutdown' % upd_vrid), + ] + + for (enable, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + enable=enable) + if cmd is not None: + exp_cmds = [upd_cmd] + [cmd] + else: + exp_cmds = [upd_cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test against vrrp 20 for going from disabled to enabled + # vrrp 20 shutdown + alt_vrid = 20 + cases = [ + (True, 'no vrrp %d shutdown' % alt_vrid), + (False, None), + ('no', 'no vrrp %d shutdown' % alt_vrid), + ('default', 'default vrrp %d shutdown' % alt_vrid) + ] + + for (enable, cmd) in cases: + func = function('update', upd_intf, alt_vrid, + enable=enable) + if cmd is not None: + exp_cmds = [upd_cmd] + [cmd] + else: + exp_cmds = [upd_cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 200] + + for enable in cases: + func = function('update', upd_intf, upd_vrid, + enable=enable) + self.eapi_exception_config_test(func, ValueError) + + def test_update_timers_advertise(self): + # vrrp 10 timers advertise 3 + cases = [ + (255, 'vrrp %d timers advertise 255' % upd_vrid), + ('default', 'default vrrp %d timers advertise' % upd_vrid), + ('no', 'no vrrp %d timers advertise' % upd_vrid), + (None, 'no vrrp %d timers advertise' % upd_vrid), + ] + + for (timers_adv, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + timers_advertise=timers_adv) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + cases = [256, 0, 'a'] + + for timers_adv in cases: + func = function('update', upd_intf, upd_vrid, + timers_advertise=timers_adv) + self.eapi_exception_config_test(func, ValueError) + + def test_update_mac_addr_adv_int(self): + # vrrp 10 mac-address advertisement-interval 30 + cases = [ + (3, 'vrrp %d mac-address advertisement-interval 3' % upd_vrid), + ('default', 'default vrrp %d mac-address advertisement-interval' + % upd_vrid), + ('no', 'no vrrp %d mac-address advertisement-interval' % upd_vrid), + (None, 'no vrrp %d mac-address advertisement-interval' % upd_vrid), + ] + + for (mac_add_adv_int, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + mac_address_advertisement_interval=mac_add_adv_int) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 10000] + + for mac_add_adv_int in cases: + func = function('update', upd_intf, upd_vrid, + mac_address_advertisement_interval=mac_add_adv_int) + self.eapi_exception_config_test(func, ValueError) + + def test_update_preempt(self): + # vrrp 10 preempt + cases = [ + (False, 'no vrrp %d preempt' % upd_vrid), + ('default', 'default vrrp %d preempt' % upd_vrid), + ('no', 'no vrrp %d preempt' % upd_vrid), + ] + + for (preempt, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + preempt=preempt) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test turning on preempt + # no vrrp 20 preempt + alt_vrid = 20 + cases = [ + (True, 'vrrp %d preempt' % alt_vrid), + ('default', 'default vrrp %d preempt' % alt_vrid), + ('no', 'no vrrp %d preempt' % alt_vrid), + ] + + for (preempt, cmd) in cases: + func = function('update', upd_intf, alt_vrid, + preempt=preempt) + if cmd is not None: + exp_cmds = [upd_cmd] + [cmd] + else: + exp_cmds = [upd_cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 5] + + for preempt in cases: + func = function('update', upd_intf, upd_vrid, + preempt=preempt) + self.eapi_exception_config_test(func, ValueError) + + def test_update_preempt_delay_minimum(self): + # vrrp 10 preempt delay minimum 0 + cases = [ + (3, 'vrrp %d preempt delay minimum 3' % upd_vrid), + ('default', 'default vrrp %d preempt delay minimum' % upd_vrid), + ('no', 'no vrrp %d preempt delay minimum' % upd_vrid), + (None, 'no vrrp %d preempt delay minimum' % upd_vrid), + ] + + for (preempt_dly_min, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + preempt_delay_minimum=preempt_dly_min) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 3601] + + for preempt_dly_min in cases: + func = function('update', upd_intf, upd_vrid, + preempt_delay_minimum=preempt_dly_min) + self.eapi_exception_config_test(func, ValueError) + + def test_update_preempt_delay_reload(self): + # vrrp 10 preempt delay reload 0 + cases = [ + (3, 'vrrp %d preempt delay reload 3' % upd_vrid), + ('default', 'default vrrp %d preempt delay reload' % upd_vrid), + ('no', 'no vrrp %d preempt delay reload' % upd_vrid), + (None, 'no vrrp %d preempt delay reload' % upd_vrid), + ] + + for (preempt_dly_rld, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + preempt_delay_reload=preempt_dly_rld) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 3601] + + for preempt_dly_rld in cases: + func = function('update', upd_intf, upd_vrid, + preempt_delay_reload=preempt_dly_rld) + self.eapi_exception_config_test(func, ValueError) + + def test_update_delay_reload(self): + # vrrp 10 delay reload 0 + cases = [ + (3, 'vrrp %d delay reload 3' % upd_vrid), + ('default', 'default vrrp %d delay reload' % upd_vrid), + ('no', 'no vrrp %d delay reload' % upd_vrid), + (None, 'no vrrp %d delay reload' % upd_vrid), + ] + + for (delay_reload, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + delay_reload=delay_reload) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 3601] + + for delay_reload in cases: + func = function('update', upd_intf, upd_vrid, + delay_reload=delay_reload) + self.eapi_exception_config_test(func, ValueError) + + # def test_update_authentication(self): + # # no vrrp 10 authentication + # cases = [ + # # XXX fix test cases + # ] + # + # for (authentication, cmd) in cases: + # func = function('update', upd_intf, upd_vrid, + # authentication=authentication) + # exp_cmds = [upd_cmd] + [cmd] + # self.eapi_positive_config_test(func, exp_cmds) + # + # # Test raising ValueError by entering invalid parameters + # cases = [ + # # XXX fix test cases + # ] + # + # for authentication in cases: + # func = function('update', upd_intf, upd_vrid, + # authentication=authentication) + # self.eapi_exception_config_test(func, ValueError) + + def test_update_track(self): + # vrrp 10 track Ethernet1 shutdown + # Send various tracking commands in one go, then + # send a command with no tracking specified and expect no commands + cases = [ + ( + {('Ethernet2', 'shutdown'): 1, + ('Ethernet1', 'decrement'): 10, + ('Ethernet3', 'shutdown'): 'default', + ('Ethernet2', 'decrement'): 'no', + ('Ethernet1', 'shutdown'): None}, + ['vrrp %d track Ethernet2 shutdown' % upd_vrid, + 'vrrp %d track Ethernet1 decrement 10' % upd_vrid, + 'default vrrp %d track Ethernet3 shutdown' % upd_vrid, + 'no vrrp %d track Ethernet2 decrement' % upd_vrid, + 'no vrrp %d track Ethernet1 shutdown' % upd_vrid] + ), + ({}, []) + ] + + for (track, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + track=track) + exp_cmds = [upd_cmd] + cmd + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = [ + {('Ethernet1', 'disable'): 10}, + {('Ethernet1', 'decrement'): True}, + ] + + for track in cases: + func = function('update', upd_intf, upd_vrid, + track=track) + self.eapi_exception_config_test(func, ValueError) + + def test_update_bfd_ip(self): + # no vrrp 10 bfd ip + bfd_addr = '10.10.4.101' + cases = [ + (bfd_addr, 'vrrp %d bfd ip %s' % (upd_vrid, bfd_addr)), + ('default', 'default vrrp %d bfd ip' % upd_vrid), + ('no', 'no vrrp %d bfd ip' % upd_vrid), + (None, 'no vrrp %d bfd ip' % upd_vrid), + ] + + for (bfd_ip, cmd) in cases: + func = function('update', upd_intf, upd_vrid, + bfd_ip=bfd_ip) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_update_invalid_vrid(self): + # Test raising ValueError by updating a non-existent vrrp + non_vrid = 1000 + params = {'priority': 100, 'enable': True} + + func = function('update', upd_intf, non_vrid, **params) + self.eapi_exception_config_test(func, ValueError) + + def test_vrconf_format(self): + # Test the function to format a vrrp configuration to + # match the output from get/getall + vrconf = {'priority': None, + 'timers_advertise': None, + 'mac_address_advertisement_interval': None, + 'preempt': 'default', + 'preempt_delay_minimum': None, + 'preempt_delay_reload': None, + 'delay_reload': None, + 'authentication': '', + 'primary_ip': None, + 'secondary_ip': { + 'add': ['10.10.4.21'], + 'remove': ['10.10.4.22'] + }, + 'description': None, + 'enable': True, + 'track': { + ('Ethernet1', 'shutdown'): None, + ('Ethernet1', 'decrement'): 10, + ('Ethernet2', 'shutdown'): 'on', + }, + 'bfd_ip': None, + 'ip_version': None} + + fixed = {'priority': 100, + 'timers_advertise': 1, + 'mac_address_advertisement_interval': 30, + 'preempt': False, + 'preempt_delay_minimum': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': '', + 'primary_ip': '0.0.0.0', + 'secondary_ip': { + 'exists': ['10.10.4.21'] + }, + 'description': None, + 'enable': True, + 'track': { + ('Ethernet1', 'decrement'): 10, + ('Ethernet2', 'shutdown'): None, + }, + 'bfd_ip': '', + 'ip_version': 2} + + # Get the vrconf_format method from the library + func = getattr(self.instance, 'vrconf_format') + # Call the method with the vrconf dictionary + result = func(vrconf) + # And verify the result is a properly formatted dictionary + self.assertEqual(fixed, result) + + +if __name__ == '__main__': + unittest.main() From f34d89bbc7d3f1dbe23c29a2fa964b8cc39e87ef Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Tue, 27 Oct 2015 14:50:54 -0600 Subject: [PATCH 70/83] update documentation --- pyeapi/api/vrrp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 3b01f34..1151428 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -116,6 +116,7 @@ enable: True to enable (no shutdown)|False to disable (shutdown) primary_ip: |no|default|None priority: |no|default|None + description: |no|default|None secondary_ip: may include the following: add: remove: From e46a5e3967490fdc8688318a72e070df9e4c92a1 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 28 Oct 2015 09:37:12 -0600 Subject: [PATCH 71/83] fix regex matches --- pyeapi/api/vrrp.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 1151428..877c8b9 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -186,7 +186,9 @@ def get(self, name): if config is None: return config - match = set(re.findall(r'^\s*(?:no |)vrrp (\d+)', config, re.M)) + # Find all occurrences of vrids in this interface and make + # a set of the unique vrid numbers + match = set(re.findall(r'^\s+(?:no |)vrrp (\d+)', config, re.M)) if not match: return None @@ -229,8 +231,7 @@ def getall(self): vrrps = dict() # Find the available interfaces - interfaces = re.findall(r'^interface\s(Vlan\d+|Port-Channel\d+' - r'|Ethernet\d+)', self.config, re.M) + interfaces = re.findall(r'^interface\s(\S+)', self.config, re.M) # Get the vrrps defined for each interface for interface in interfaces: @@ -242,42 +243,42 @@ def getall(self): return vrrps def _parse_primary_ip(self, config, vrid): - match = re.search(r'^\s*vrrp %s ip (\d+\.\d+\.\d+\.\d+)$' % + match = re.search(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+)$' % vrid, config, re.M) value = match.group(1) if match else None return dict(primary_ip=value) def _parse_priority(self, config, vrid): - match = re.search(r'^\s*vrrp %s priority (\d+)$' % vrid, config, re.M) + match = re.search(r'^\s+vrrp %s priority (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(priority=value) def _parse_timers_advertise(self, config, vrid): - match = re.search(r'^\s*vrrp %s timers advertise (\d+)$' % + match = re.search(r'^\s+vrrp %s timers advertise (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(timers_advertise=value) def _parse_preempt(self, config, vrid): - match = re.search(r'^\s*(no|) vrrp %s preempt$' % vrid, config, re.M) + match = re.search(r'^\s+(no|) vrrp %s preempt$' % vrid, config, re.M) value = match.group(1) if match else None value = ENABLED.get(value, 'Error') return dict(preempt=value) def _parse_enable(self, config, vrid): - match = re.search(r'^\s*(no|) vrrp %s shutdown$' % vrid, config, re.M) + match = re.search(r'^\s+(no|) vrrp %s shutdown$' % vrid, config, re.M) value = match.group(1) if match else None value = SHUTDOWN.get(value, 'Error') return dict(enable=value) def _parse_secondary_ip(self, config, vrid): - matches = re.findall(r'^\s*vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' + matches = re.findall(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' r'secondary$' % vrid, config, re.M) value = matches if matches else None return dict(secondary_ip={'exists': value}) def _parse_description(self, config, vrid): - match = re.search(r'^\s*(no|) vrrp %s description(.*)$' % + match = re.search(r'^\s+(no|) vrrp %s description(.*)$' % vrid, config, re.M) enabled = match.group(1) if match else None enabled = ENABLED.get(enabled, 'Error') @@ -286,25 +287,25 @@ def _parse_description(self, config, vrid): return dict(description=value) def _parse_mac_addr_adv_interval(self, config, vrid): - match = re.search(r'^\s*vrrp %s mac-address advertisement-interval ' + match = re.search(r'^\s+vrrp %s mac-address advertisement-interval ' r'(\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(mac_address_advertisement_interval=value) def _parse_preempt_delay_minimum(self, config, vrid): - match = re.search(r'^\s*vrrp %s preempt delay minimum (\d+)$' % + match = re.search(r'^\s+vrrp %s preempt delay minimum (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(preempt_delay_minimum=value) def _parse_preempt_delay_reload(self, config, vrid): - match = re.search(r'^\s*vrrp %s preempt delay reload (\d+)$' % + match = re.search(r'^\s+vrrp %s preempt delay reload (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(preempt_delay_reload=value) def _parse_authentication(self, config, vrid): - match = re.search(r'^\s*(no|) vrrp %s authentication' + match = re.search(r'^\s+(no|) vrrp %s authentication' r'($| ietf-md5 key-string 7 .*$| text .*$)' % vrid, config, re.M) enabled = match.group(1) if match else None @@ -313,7 +314,7 @@ def _parse_authentication(self, config, vrid): return dict(authentication=value) def _parse_bfd_ip(self, config, vrid): - match = re.search(r'^\s*(no|) vrrp %s bfd ip' + match = re.search(r'^\s+(no|) vrrp %s bfd ip' r'(?: (\d+\.\d+\.\d+\.\d+)|)$' % vrid, config, re.M) enabled = match.group(1) if match else None @@ -322,19 +323,19 @@ def _parse_bfd_ip(self, config, vrid): return dict(bfd_ip=value) def _parse_ip_version(self, config, vrid): - match = re.search(r'^\s*vrrp %s ip version (\d+)$' % + match = re.search(r'^\s+vrrp %s ip version (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(ip_version=value) def _parse_delay_reload(self, config, vrid): - match = re.search(r'^\s*vrrp %s delay reload (\d+)$' % + match = re.search(r'^\s+vrrp %s delay reload (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(delay_reload=value) def _parse_track(self, config, vrid): - matches = re.findall(r'^\s*vrrp %s track (\S+) ' + matches = re.findall(r'^\s+vrrp %s track (\S+) ' r'(decrement|shutdown)(?:( \d+$|$))' % vrid, config, re.M) value = dict() From 2cc73cd19645f526470619b13d38bb58f7502e30 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 28 Oct 2015 17:48:37 -0600 Subject: [PATCH 72/83] fix track property format; fix variable names; fix regex handling --- pyeapi/api/vrrp.py | 183 ++++++++++++++++--------------- test/system/test_api_vrrp.py | 201 ++++++++++++++++++++++++++--------- test/unit/test_api_vrrp.py | 192 +++++++++++++++++++++++---------- 3 files changed, 386 insertions(+), 190 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 877c8b9..d335c05 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -53,14 +53,14 @@ description (string): The description for the vrrp ip_version (int): The ip version value for the vrrp timers_advertise (int): The timers advertise setting for the vrrp - mac_address_advertisement_interval (int): The mac-address advertisement- + mac_addr_adv_interval (int): The mac-address advertisement- interval setting for the vrrp preempt (boolean): The preempt state of the vrrp - preempt_delay_minimum (int): The preempt delay minimum setting for the vrrp + preempt_delay_min (int): The preempt delay minimum setting for the vrrp preempt_delay_reload (int): The preempt delay reload setting for the vrrp delay_reload (int): The delay reload setting for the vrrp authentication (string): The authentication setting for the vrrp - track (dict): The object tracking settings for the vrrp + track (list): The object tracking settings for the vrrp bfd_ip (string): The bfd ip set for the vrrp Notes: @@ -97,16 +97,24 @@ } ip_version: timers_advertise: - mac_address_advertisement_interval: + mac_addr_adv_interval: preempt: - preempt_delay_minimum: + preempt_delay_min: preempt_delay_reload: delay_reload: authentication: - track: { - (, ): , - (, ): , - } + track: [ + { + name: + track_action: + track_amount: |default|no|None + }, + { + name: + track_action: + track_amount: |default|no|None + }, + ] bfd_ip: } @@ -122,22 +130,18 @@ remove: ip_version: |no|default|None timers_advertise: |no|default|None - mac_address_advertisement_interval: |no|default|None + mac_addr_adv_interval: |no|default|None preempt: True to enable (preempt)|False to disable (no preempt) - preempt_delay_minimum: |no|default|None + preempt_delay_min: |no|default|None preempt_delay_reload: |no|default|None delay_reload: |no|default|None authentication: NOTE: currently not implemented - track: consisting of entries in the following format: - (, ): - for shutdown, may be one of the following: - no|None: disable shutdown tracking for the named object - default: default shutdown tracking for the named object - : enable shutdown tracking for the named object - for decrement, may be one of the following: - no|None: disable decremental tracking for the named object - default: default decremental tracking for the named object - : set the decrement amount for the named object + track: of dicts in the following format: + { + name: + track_action: + track_amount: |default|no|None + } bfd_ip: |no|default|None """ @@ -146,12 +150,10 @@ from pyeapi.api import EntityCollection -ENABLED = {'no': False, '': True} -SHUTDOWN = {'no': True, '': False} PROPERTIES = ['primary_ip', 'priority', 'description', 'secondary_ip', 'ip_version', 'enable', 'timers_advertise', - 'mac_address_advertisement_interval', 'preempt', - 'preempt_delay_minimum', 'preempt_delay_reload', + 'mac_addr_adv_interval', 'preempt', + 'preempt_delay_min', 'preempt_delay_reload', 'delay_reload', 'authentication', 'track', 'bfd_ip'] @@ -206,7 +208,7 @@ def get(self, name): subd.update(self._parse_ip_version(config, vrid)) subd.update(self._parse_mac_addr_adv_interval(config, vrid)) subd.update(self._parse_preempt(config, vrid)) - subd.update(self._parse_preempt_delay_minimum(config, vrid)) + subd.update(self._parse_preempt_delay_min(config, vrid)) subd.update(self._parse_preempt_delay_reload(config, vrid)) subd.update(self._parse_primary_ip(config, vrid)) subd.update(self._parse_priority(config, vrid)) @@ -260,16 +262,16 @@ def _parse_timers_advertise(self, config, vrid): return dict(timers_advertise=value) def _parse_preempt(self, config, vrid): - match = re.search(r'^\s+(no|) vrrp %s preempt$' % vrid, config, re.M) - value = match.group(1) if match else None - value = ENABLED.get(value, 'Error') - return dict(preempt=value) + match = re.search(r'^\s+vrrp %s preempt$' % vrid, config, re.M) + if match: + return dict(preempt=True) + return dict(preempt=False) def _parse_enable(self, config, vrid): - match = re.search(r'^\s+(no|) vrrp %s shutdown$' % vrid, config, re.M) - value = match.group(1) if match else None - value = SHUTDOWN.get(value, 'Error') - return dict(enable=value) + match = re.search(r'^\s+vrrp %s shutdown$' % vrid, config, re.M) + if match: + return dict(enable=False) + return dict(enable=True) def _parse_secondary_ip(self, config, vrid): matches = re.findall(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' @@ -278,25 +280,23 @@ def _parse_secondary_ip(self, config, vrid): return dict(secondary_ip={'exists': value}) def _parse_description(self, config, vrid): - match = re.search(r'^\s+(no|) vrrp %s description(.*)$' % + match = re.search(r'^\s+vrrp %s description(.*)$' % vrid, config, re.M) - enabled = match.group(1) if match else None - enabled = ENABLED.get(enabled, 'Error') - value = match.group(2).lstrip() if enabled is True else '' - value = match.group(2).lstrip() - return dict(description=value) + if match: + return dict(description=match.group(1).lstrip()) + return dict(description='') def _parse_mac_addr_adv_interval(self, config, vrid): match = re.search(r'^\s+vrrp %s mac-address advertisement-interval ' r'(\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None - return dict(mac_address_advertisement_interval=value) + return dict(mac_addr_adv_interval=value) - def _parse_preempt_delay_minimum(self, config, vrid): + def _parse_preempt_delay_min(self, config, vrid): match = re.search(r'^\s+vrrp %s preempt delay minimum (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None - return dict(preempt_delay_minimum=value) + return dict(preempt_delay_min=value) def _parse_preempt_delay_reload(self, config, vrid): match = re.search(r'^\s+vrrp %s preempt delay reload (\d+)$' % @@ -305,22 +305,20 @@ def _parse_preempt_delay_reload(self, config, vrid): return dict(preempt_delay_reload=value) def _parse_authentication(self, config, vrid): - match = re.search(r'^\s+(no|) vrrp %s authentication' + match = re.search(r'^\s+vrrp %s authentication' r'($| ietf-md5 key-string 7 .*$| text .*$)' % vrid, config, re.M) - enabled = match.group(1) if match else None - enabled = ENABLED.get(enabled) - value = match.group(2).lstrip() if enabled is True else '' - return dict(authentication=value) + if match: + return dict(authentication=match.group(1).lstrip()) + return dict(authentication='') def _parse_bfd_ip(self, config, vrid): - match = re.search(r'^\s+(no|) vrrp %s bfd ip' + match = re.search(r'^\s+vrrp %s bfd ip' r'(?: (\d+\.\d+\.\d+\.\d+)|)$' % vrid, config, re.M) - enabled = match.group(1) if match else None - enabled = ENABLED.get(enabled, 'Error') - value = match.group(2) if enabled is True else '' - return dict(bfd_ip=value) + if match: + return dict(bfd_ip=match.group(1)) + return dict(bfd_ip='') def _parse_ip_version(self, config, vrid): match = re.search(r'^\s+vrrp %s ip version (\d+)$' % @@ -338,14 +336,20 @@ def _parse_track(self, config, vrid): matches = re.findall(r'^\s+vrrp %s track (\S+) ' r'(decrement|shutdown)(?:( \d+$|$))' % vrid, config, re.M) - value = dict() + value = [] for match in matches: object = match[0] action = match[1] amount = None if match[2] == '' else int(match[2]) - key = (object, action) - value.update({key: amount}) - return dict(track=value) + entry = { + 'name': object, + 'track_action': action, + 'track_amount': amount + } + value.append(entry) + + # Return the list, sorted for easier comparison + return dict(track=sorted(value)) def create(self, interface, vrid, **kwargs): """Creates a vrrp instance from an interface @@ -483,6 +487,8 @@ def _vrrp_set(self, interface, vrid, **kwargs): priority = vrconf.get('priority', '__NONE__') if priority != '__NONE__': + import XXX + XXX.print_file_XXX(priority, "XXX priority") if priority in ('no', None): commands.append("no vrrp %d priority" % vrid) elif priority == 'default': @@ -550,7 +556,7 @@ def _vrrp_set(self, interface, vrid, **kwargs): "or None") mac_add_adv_int = \ - vrconf.get('mac_address_advertisement_interval', '__NONE__') + vrconf.get('mac_addr_adv_interval', '__NONE__') if mac_add_adv_int != '__NONE__': if mac_add_adv_int in ('no', None): commands.append("no vrrp %d mac-address " @@ -578,18 +584,18 @@ def _vrrp_set(self, interface, vrid, **kwargs): raise ValueError("vrrp property 'preempt' must " "be True, False, 'no', or 'default'") - preempt_delay_minimum = vrconf.get('preempt_delay_minimum', + preempt_delay_min = vrconf.get('preempt_delay_min', '__NONE__') - if preempt_delay_minimum != '__NONE__': - if preempt_delay_minimum in ('no', None): + if preempt_delay_min != '__NONE__': + if preempt_delay_min in ('no', None): commands.append("no vrrp %d preempt delay minimum" % vrid) - elif preempt_delay_minimum == 'default': + elif preempt_delay_min == 'default': commands.append("default vrrp %d preempt delay minimum" % vrid) - elif 0 <= preempt_delay_minimum <= 3600: + elif 0 <= preempt_delay_min <= 3600: commands.append("vrrp %d preempt delay minimum %d" % - (vrid, preempt_delay_minimum)) + (vrid, preempt_delay_min)) else: - raise ValueError("vrrp property 'preempt_delay_minimum' " + raise ValueError("vrrp property 'preempt_delay_min' " "must an integer in the range 0-3600, None " "'no', or 'default'") @@ -630,8 +636,11 @@ def _vrrp_set(self, interface, vrid, **kwargs): track = vrconf.get('track', '__NONE__') if track != '__NONE__': - for (key, amount) in track.iteritems(): - (tracked, action) = key + for entry in track: + tracked = entry['name'] + action = entry['track_action'] + amount = entry['track_amount'] + if amount in ('no', None): commands.append("no vrrp %d track %s %s" % (vrid, tracked, action)) @@ -714,15 +723,15 @@ def vrconf_format(self, vrconfig): fixed['timers_advertise'] = 1 # mac_address_advertisement_interaval: # default, no, None results in value of 30 - if fixed['mac_address_advertisement_interval'] in \ + if fixed['mac_addr_adv_interval'] in \ ('no', 'default', None): - fixed['mac_address_advertisement_interval'] = 30 + fixed['mac_addr_adv_interval'] = 30 # preempt: default, no results in value of False if fixed['preempt'] in ('no', 'default'): fixed['preempt'] = False - # preempt_delay_minimum: default, no, None results in value of 0 - if fixed['preempt_delay_minimum'] in ('no', 'default', None): - fixed['preempt_delay_minimum'] = 0 + # preempt_delay_min: default, no, None results in value of 0 + if fixed['preempt_delay_min'] in ('no', 'default', None): + fixed['preempt_delay_min'] = 0 # preempt_delay_reload: default, no, None results in value of 0 if fixed['preempt_delay_reload'] in ('no', 'default', None): fixed['preempt_delay_reload'] = 0 @@ -732,22 +741,28 @@ def vrconf_format(self, vrconfig): # authenticetion -> XXX needs implemented # track: default, no, None removes the entry if 'track' in fixed: - # Work with a temporary copy to keep things straight - tracks = {} - for (key, amount) in fixed['track'].iteritems(): - (tracked, action) = key + tracks = [] + for entry in fixed['track']: + tracked = entry['name'] + action = entry['track_action'] + amount = entry['track_amount'] + if amount in ('no', 'default', None): # This tracked object should have been deleted - # from the config. Do not keep it in the dictionary. - pass + # from the config. Do not keep it in the list. + tracked = None elif action == 'shutdown': # This is a valid tracked shutdown. Set it's value to None. - tracks[key] = None - else: - # This is a valid decerment. Copy it exactly. - tracks[key] = amount - # Copy back the temporary dict into the original - fixed['track'] = dict(tracks) + amount = None + + if tracked is not None: + tracks.append({ + 'name': tracked, + 'track_action': action, + 'track_amount': amount, + }) + # Copy the new list over the original, sorted for easier comparison + fixed['track'] = sorted(tracks) # bfd_ip: default, no, None results in '' if fixed['bfd_ip'] in ('no', 'default', None): fixed['bfd_ip'] = '' diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index bebb98c..807cd9d 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -49,17 +49,29 @@ 'ip_version': 3, 'enable': False, 'timers_advertise': 2, - 'mac_address_advertisement_interval': 3, + 'mac_addr_adv_interval': 3, 'preempt': False, - 'preempt_delay_minimum': 1, + 'preempt_delay_min': 1, 'preempt_delay_reload': None, 'delay_reload': 1, 'authentication': '', - 'track': { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): 10, - }, + 'track': [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 10 + }, + ], 'bfd_ip': '10.10.10.150', } @@ -78,40 +90,112 @@ TIMERS_ADVERTISE = [10, 'default', 20, 'no', 30, None] MAC_ADDR_ADV_INTVL = [50, 'default', 55, 'no', 60, None] PREEMPT = [True, False, True] -PREEMPT_DELAY_MINIMUM = [3600, 'default', 500, 'no', 150, None] +PREEMPT_DELAY_MIN = [3600, 'default', 500, 'no', 150, None] PREEMPT_DELAY_RELOAD = [3600, 'default', 500, 'no', 150, None] DELAY_RELOAD = [30, 'default', 25, 'no', 15, None] TRACK = [ - { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): 10, - }, - { - ('Ethernet2', 'shutdown'): None, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): 'default', - }, - { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): 2, - ('Ethernet2', 'decrement'): 20, - }, - { - ('Ethernet2', 'shutdown'): None, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): 'no', - }, - { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): 2, - ('Ethernet2', 'decrement'): 20, - }, - { - ('Ethernet2', 'shutdown'): None, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): None, - }, + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 10 + }, + ], + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 'default' + }, + ], + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 2 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 20 + }, + ], + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 'no' + }, + ], + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 2 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 20 + }, + ], + [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': None + }, + ], ] BFD_IP = ['10.10.10.160', 'default', '10.10.10.161', 'no', '10.10.10.162', None] @@ -153,8 +237,13 @@ def test_create(self): # Fix the configuration dict for proper output vrrp_conf = dut.api('vrrp').vrconf_format(vrrp_conf) + # temp = sorted(vrrp_conf['track']) + # vrrp_conf['track'] = temp response = dut.api('vrrp').get(interface)[vrid] + # temp = sorted(response['track']) + # response['track'] = temp + self.maxDiff = None self.assertEqual(response, vrrp_conf) @@ -210,17 +299,29 @@ def test_update(self): 'ip_version': 2, 'enable': True, 'timers_advertise': None, - 'mac_address_advertisement_interval': 'default', + 'mac_addr_adv_interval': 'default', 'preempt': True, - 'preempt_delay_minimum': 'default', + 'preempt_delay_min': 'default', 'preempt_delay_reload': 'default', 'delay_reload': 'default', 'authentication': '', - 'track': { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): None, - ('Ethernet2', 'decrement'): 1, - }, + 'track': [ + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 1 + }, + ], 'bfd_ip': None, } @@ -317,7 +418,7 @@ def test_update_timers_advertise(self): response = dut.api('vrrp').update(interface, vrid, **vrconfig) self.assertIs(response, True) - def test_update_mac_address_advertisement_interval(self): + def test_update_mac_addr_adv_interval(self): vrid = 104 for dut in self.duts: interface = self._vlan_setup(dut) @@ -327,7 +428,7 @@ def test_update_mac_address_advertisement_interval(self): 'exit']) for mac_addr_adv_intvl in MAC_ADDR_ADV_INTVL: - vrconfig = {'mac_address_advertisement_interval': + vrconfig = {'mac_addr_adv_interval': mac_addr_adv_intvl} response = dut.api('vrrp').update(interface, vrid, **vrconfig) self.assertIs(response, True) @@ -346,7 +447,7 @@ def test_update_preempt(self): response = dut.api('vrrp').update(interface, vrid, **vrconfig) self.assertIs(response, True) - def test_update_preempt_delay_minimum(self): + def test_update_preempt_delay_min(self): vrid = 104 for dut in self.duts: interface = self._vlan_setup(dut) @@ -355,8 +456,8 @@ def test_update_preempt_delay_minimum(self): 'vrrp %d shutdown' % vrid, 'exit']) - for preempt_delay_minimum in PREEMPT_DELAY_MINIMUM: - vrconfig = {'preempt_delay_minimum': preempt_delay_minimum} + for preempt_delay_min in PREEMPT_DELAY_MIN: + vrconfig = {'preempt_delay_min': preempt_delay_min} response = dut.api('vrrp').update(interface, vrid, **vrconfig) self.assertIs(response, True) diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py index ca60cb9..438b19d 100644 --- a/test/unit/test_api_vrrp.py +++ b/test/unit/test_api_vrrp.py @@ -47,9 +47,9 @@ 'Ethernet1': { 10: {'priority': 175, 'timers_advertise': 1, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': True, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': '', @@ -57,16 +57,16 @@ 'secondary_ip': {'exists': None}, 'description': 'vrrp 10 on Ethernet1', 'enable': True, - 'track': {}, + 'track': [], 'bfd_ip': '', 'ip_version': 2} }, 'Port-Channel10': { 10: {'priority': 150, 'timers_advertise': 1, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': True, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': '', @@ -76,16 +76,16 @@ }, 'description': 'vrrp 10 on Port-Channel10', 'enable': True, - 'track': {}, + 'track': [], 'bfd_ip': '', 'ip_version': 2} }, 'Vlan50': { 10: {'priority': 200, 'timers_advertise': 3, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': True, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': '', @@ -95,16 +95,20 @@ }, 'description': '', 'enable': True, - 'track': { - ('Ethernet1', 'shutdown'): None - }, + 'track': [ + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': None + } + ], 'bfd_ip': '', 'ip_version': 2}, 20: {'priority': 100, 'timers_advertise': 5, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': False, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': 'text 12345', @@ -112,18 +116,30 @@ 'secondary_ip': {'exists': None}, 'description': '', 'enable': False, - 'track': { - ('Ethernet1', 'shutdown'): None, - ('Ethernet2', 'decrement'): 1, - ('Ethernet2', 'shutdown'): None + 'track': [ + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': None }, + ], 'bfd_ip': '', 'ip_version': 2}, 30: {'priority': 50, 'timers_advertise': 1, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': True, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': @@ -132,7 +148,7 @@ 'secondary_ip': {'exists': None}, 'description': '', 'enable': True, - 'track': {}, + 'track': [], 'bfd_ip': '10.10.4.33', 'ip_version': 2} } @@ -196,18 +212,34 @@ def test_create(self): }, 'ip_version': 3, 'timers_advertise': 2, - 'mac_address_advertisement_interval': 3, + 'mac_addr_adv_interval': 3, 'preempt': False, - 'preempt_delay_minimum': 1, + 'preempt_delay_min': 1, 'preempt_delay_reload': 1, 'delay_reload': 1, 'authentication': '', - 'track': { - ('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'shutdown'): 1, - ('Ethernet2', 'decrement'): 1, - ('Ethernet1', 'decrement'): None, - }, + 'track': [ + { + 'name': 'Ethernet1', + 'track_action': 'decrement', + 'track_amount': None + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 1 + }, + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + ], 'bfd_ip': '10.10.60.30', } @@ -227,10 +259,10 @@ def test_create(self): 'vrrp 10 preempt delay reload 1', 'vrrp 10 delay reload 1', # 'no vrrp 10 authentication', - 'vrrp 10 track Ethernet2 shutdown', 'no vrrp 10 track Ethernet1 decrement', - 'vrrp 10 track Ethernet2 decrement 1', 'vrrp 10 track Ethernet1 shutdown', + 'vrrp 10 track Ethernet2 decrement 1', + 'vrrp 10 track Ethernet2 shutdown', 'vrrp 10 bfd ip 10.10.60.30', ] func = function('create', interface, vrid, **configuration) @@ -434,7 +466,7 @@ def test_update_mac_addr_adv_int(self): for (mac_add_adv_int, cmd) in cases: func = function('update', upd_intf, upd_vrid, - mac_address_advertisement_interval=mac_add_adv_int) + mac_addr_adv_interval=mac_add_adv_int) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) @@ -443,7 +475,7 @@ def test_update_mac_addr_adv_int(self): for mac_add_adv_int in cases: func = function('update', upd_intf, upd_vrid, - mac_address_advertisement_interval=mac_add_adv_int) + mac_addr_adv_interval=mac_add_adv_int) self.eapi_exception_config_test(func, ValueError) def test_update_preempt(self): @@ -486,7 +518,7 @@ def test_update_preempt(self): preempt=preempt) self.eapi_exception_config_test(func, ValueError) - def test_update_preempt_delay_minimum(self): + def test_update_preempt_delay_min(self): # vrrp 10 preempt delay minimum 0 cases = [ (3, 'vrrp %d preempt delay minimum 3' % upd_vrid), @@ -497,7 +529,7 @@ def test_update_preempt_delay_minimum(self): for (preempt_dly_min, cmd) in cases: func = function('update', upd_intf, upd_vrid, - preempt_delay_minimum=preempt_dly_min) + preempt_delay_min=preempt_dly_min) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) @@ -506,7 +538,7 @@ def test_update_preempt_delay_minimum(self): for preempt_dly_min in cases: func = function('update', upd_intf, upd_vrid, - preempt_delay_minimum=preempt_dly_min) + preempt_delay_min=preempt_dly_min) self.eapi_exception_config_test(func, ValueError) def test_update_preempt_delay_reload(self): @@ -583,18 +615,38 @@ def test_update_track(self): # send a command with no tracking specified and expect no commands cases = [ ( - {('Ethernet2', 'shutdown'): 1, - ('Ethernet1', 'decrement'): 10, - ('Ethernet3', 'shutdown'): 'default', - ('Ethernet2', 'decrement'): 'no', - ('Ethernet1', 'shutdown'): None}, + [{ + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 1 + }, + { + 'name': 'Ethernet1', + 'track_action': 'decrement', + 'track_amount': 10 + }, + { + 'name': 'Ethernet3', + 'track_action': 'shutdown', + 'track_amount': 'default' + }, + { + 'name': 'Ethernet2', + 'track_action': 'decrement', + 'track_amount': 'no' + }, + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': None + }], ['vrrp %d track Ethernet2 shutdown' % upd_vrid, 'vrrp %d track Ethernet1 decrement 10' % upd_vrid, 'default vrrp %d track Ethernet3 shutdown' % upd_vrid, 'no vrrp %d track Ethernet2 decrement' % upd_vrid, 'no vrrp %d track Ethernet1 shutdown' % upd_vrid] ), - ({}, []) + ([], []) ] for (track, cmd) in cases: @@ -605,8 +657,16 @@ def test_update_track(self): # Test raising ValueError by entering invalid parameters cases = [ - {('Ethernet1', 'disable'): 10}, - {('Ethernet1', 'decrement'): True}, + [{ + 'name': 'Ethernet1', + 'track_action': 'disable', + 'track_amount': 10 + }], + [{ + 'name': 'Ethernet1', + 'track_action': 'decrement', + 'track_amount': True + }] ] for track in cases: @@ -643,9 +703,9 @@ def test_vrconf_format(self): # match the output from get/getall vrconf = {'priority': None, 'timers_advertise': None, - 'mac_address_advertisement_interval': None, + 'mac_addr_adv_interval': None, 'preempt': 'default', - 'preempt_delay_minimum': None, + 'preempt_delay_min': None, 'preempt_delay_reload': None, 'delay_reload': None, 'authentication': '', @@ -656,19 +716,31 @@ def test_vrconf_format(self): }, 'description': None, 'enable': True, - 'track': { - ('Ethernet1', 'shutdown'): None, - ('Ethernet1', 'decrement'): 10, - ('Ethernet2', 'shutdown'): 'on', - }, + 'track': [ + { + 'name': 'Ethernet1', + 'track_action': 'shutdown', + 'track_amount': None + }, + { + 'name': 'Ethernet1', + 'track_action': 'decrement', + 'track_amount': 10 + }, + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': 'on' + }, + ], 'bfd_ip': None, 'ip_version': None} fixed = {'priority': 100, 'timers_advertise': 1, - 'mac_address_advertisement_interval': 30, + 'mac_addr_adv_interval': 30, 'preempt': False, - 'preempt_delay_minimum': 0, + 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'authentication': '', @@ -678,10 +750,18 @@ def test_vrconf_format(self): }, 'description': None, 'enable': True, - 'track': { - ('Ethernet1', 'decrement'): 10, - ('Ethernet2', 'shutdown'): None, - }, + 'track': [ + { + 'name': 'Ethernet1', + 'track_action': 'decrement', + 'track_amount': 10 + }, + { + 'name': 'Ethernet2', + 'track_action': 'shutdown', + 'track_amount': None + }, + ], 'bfd_ip': '', 'ip_version': 2} From 6b5f13e8ac3ce6ea32f79c152c818518b5c56e7f Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Wed, 28 Oct 2015 17:56:21 -0600 Subject: [PATCH 73/83] remove degugging lines --- pyeapi/api/vrrp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index d335c05..76d2ab7 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -487,8 +487,6 @@ def _vrrp_set(self, interface, vrid, **kwargs): priority = vrconf.get('priority', '__NONE__') if priority != '__NONE__': - import XXX - XXX.print_file_XXX(priority, "XXX priority") if priority in ('no', None): commands.append("no vrrp %d priority" % vrid) elif priority == 'default': From 77713ac53732cffca84e919ff0e4fef598923527 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 30 Oct 2015 12:23:57 -0600 Subject: [PATCH 74/83] add setters; update track and secondary_ip format --- pyeapi/api/vrrp.py | 1056 ++++++++++++++++++++++++----- test/fixtures/running_config.vrrp | 8 + test/system/test_api_vrrp.py | 171 +---- test/unit/test_api_vrrp.py | 834 +++++++++++++++-------- 4 files changed, 1459 insertions(+), 610 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 76d2ab7..f6f04c7 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -106,13 +106,13 @@ track: [ { name: - track_action: - track_amount: |default|no|None + action: + amount: |default|no|None }, { name: - track_action: - track_amount: |default|no|None + action: + amount: |default|no|None }, ] bfd_ip: @@ -139,8 +139,8 @@ track: of dicts in the following format: { name: - track_action: - track_amount: |default|no|None + action: + amount: |default|no|None } bfd_ip: |no|default|None @@ -244,6 +244,12 @@ def getall(self): return vrrps + def _parse_enable(self, config, vrid): + match = re.search(r'^\s+vrrp %s shutdown$' % vrid, config, re.M) + if match: + return dict(enable=False) + return dict(enable=True) + def _parse_primary_ip(self, config, vrid): match = re.search(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+)$' % vrid, config, re.M) @@ -267,17 +273,11 @@ def _parse_preempt(self, config, vrid): return dict(preempt=True) return dict(preempt=False) - def _parse_enable(self, config, vrid): - match = re.search(r'^\s+vrrp %s shutdown$' % vrid, config, re.M) - if match: - return dict(enable=False) - return dict(enable=True) - def _parse_secondary_ip(self, config, vrid): matches = re.findall(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' r'secondary$' % vrid, config, re.M) - value = matches if matches else None - return dict(secondary_ip={'exists': value}) + value = matches if matches else [] + return dict(secondary_ip=value) def _parse_description(self, config, vrid): match = re.search(r'^\s+vrrp %s description(.*)$' % @@ -338,18 +338,20 @@ def _parse_track(self, config, vrid): vrid, config, re.M) value = [] for match in matches: - object = match[0] + tr_obj = match[0] action = match[1] amount = None if match[2] == '' else int(match[2]) entry = { - 'name': object, - 'track_action': action, - 'track_amount': amount + 'name': tr_obj, + 'action': action, } + if amount: + entry.update({'amount': amount}) value.append(entry) # Return the list, sorted for easier comparison - return dict(track=sorted(value)) + track_list = sorted(value, key=lambda k: (k['name'], k['action'])) + return dict(track=track_list) def create(self, interface, vrid, **kwargs): """Creates a vrrp instance from an interface @@ -421,7 +423,706 @@ def default(self, interface, vrid): vrrp_str = "default vrrp %d" % vrid return self.configure_interface(interface, vrrp_str) - def update(self, interface, vrid, **kwargs): + def set_enable(self, name, vrid, value=False, run=True): + """Set the enable property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (boolean): True to enable the vrrp, False to disable. + run (boolean): True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure + If run is False: + The formatted command string which can be passed to the node + + """ + + if value is False: + cmd = "vrrp %d shutdown" % vrid + elif value is True: + cmd = "no vrrp %d shutdown" % vrid + else: + raise ValueError("vrrp property 'enable' must be " + "True or False") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_primary_ip(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the primary_ip property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (string): IP address to be set. + disable (boolean): Unset primary ip if True. + default (boolean): Set primary ip to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + vrrps = self.get(name) + primary_ip = vrrps[vrid]['primary_ip'] + cmd = "default vrrp %d ip %s" % (vrid, primary_ip) + elif disable is True or value is None: + vrrps = self.get(name) + primary_ip = vrrps[vrid]['primary_ip'] + cmd = "no vrrp %d ip %s" % (vrid, primary_ip) + elif re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): + cmd = "vrrp %d ip %s" % (vrid, value) + else: + raise ValueError("vrrp property 'primary_ip' must be " + "a properly formatted IP address") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_priority(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the primary_ip property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): Priority to assign to the vrrp. + disable (boolean): Unset priority if True. + default (boolean): Set priority to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d priority" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d priority" % vrid + elif re.match(r'^\d+$', str(value)) and 1 <= value <= 254: + cmd = "vrrp %d priority %s" % (vrid, value) + else: + raise ValueError("vrrp property 'priority' must be " + "an integer in the range 1-254") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_description(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the description property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (string): Description to assign to the vrrp. + disable (boolean): Unset description if True. + default (boolean): Set description to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d description" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d description" % vrid + else: + cmd = "vrrp %d description %s" % (vrid, value) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_ip_version(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the ip_version property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): IP version to assign to the vrrp. + disable (boolean): Unset ip_version if True. + default (boolean): Set ip_version to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d ip version" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d ip version" % vrid + elif value in (2, 3): + cmd = "vrrp %d ip version %d" % (vrid, value) + else: + raise ValueError("vrrp property 'ip_version' must be 2 or 3") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_secondary_ips(self, name, vrid, secondary_ips, run=True): + """Configure the secondary_ip property of the vrrp + + Notes: + set_secondary_ips takes a list of secondary ip addresses + which are to be set on the virtal router. An empty list will + remove any existing secondary ip addresses from the vrrp. + A list containing addresses will configure the virtual router + with only the addresses specified in the list - any existing + addresses not included in the list will be removed. + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + secondary_ips (list): A list of secondary ip addresses to + be assigned to the virtual router. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + cmds = [] + + # Get the current set of tracks defined for the vrrp + curr_sec_ips = [] + vrrps = self.get(name) + if vrrps and vrid in vrrps: + curr_sec_ips = vrrps[vrid]['secondary_ip'] + + # Validate the list of ip addresses + for sec_ip in secondary_ips: + if type(sec_ip) is not str or \ + not re.match(r'^\d+\.\d+\.\d+\.\d+$', sec_ip): + raise ValueError("vrrp property 'secondary_ip' must be a list " + "of properly formatted ip address strings") + + intersection = list(set(curr_sec_ips) & set(secondary_ips)) + + # Delete the intersection from both lists to determine which + # addresses need to be added or removed from the vrrp + remove = list(set(curr_sec_ips) - set(intersection)) + add = list(set(secondary_ips) - set(intersection)) + + # Build the commands to add and remove the secondary ip addresses + for sec_ip in remove: + cmds.append("no vrrp %d ip %s secondary" % (vrid, sec_ip)) + + for sec_ip in add: + cmds.append("vrrp %d ip %s secondary" % (vrid, sec_ip)) + + cmds = sorted(cmds) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmds) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmds + + def set_timers_advertise(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the ip_version property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): Timers advertise value to assign to the vrrp. + disable (boolean): Unset timers advertise if True. + default (boolean): Set timers advertise to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d timers advertise" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d timers advertise" % vrid + elif 1 <= value <= 255: + cmd = "vrrp %d timers advertise %d" % (vrid, value) + else: + raise ValueError("vrrp property 'timers_advertise' must be" + "in the range 1-255") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_mac_addr_adv_interval(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the mac_addr_adv_interval property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): mac-address advertisement-interval value to + assign to the vrrp. + disable (boolean): Unset mac-address advertisement-interval + if True. + default (boolean): Set mac-address advertisement-interval to + default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d mac-address advertisement-interval" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d mac-address advertisement-interval" % vrid + elif 1 <= value <= 3600: + cmd = "vrrp %d mac-address advertisement-interval %d" \ + % (vrid, value) + else: + raise ValueError("vrrp property 'mac_addr_adv_interval' must be" + "in the range 1-3600") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_preempt(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the preempt property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (boolean): True to enable preempt, False to disable + preempt on the vrrp. + disable (boolean): Unset preempt if True. + default (boolean): Set preempt to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt" % vrid + elif disable is True or value is None or value is False: + cmd = "no vrrp %d preempt" % vrid + elif value is True: + cmd = "vrrp %d preempt" % vrid + else: + raise ValueError("vrrp property 'preempt' must be True or False") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_preempt_delay_min(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the preempt_delay_min property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): Preempt delay minimum value to set on the vrrp. + disable (boolean): Unset preempt delay minimum if True. + default (boolean): Set preempt delay minimum to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt delay minimum" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d preempt delay minimum" % vrid + elif 0 <= value <= 3600: + cmd = "vrrp %d preempt delay minimum %d" % (vrid, value) + else: + raise ValueError("vrrp property 'preempt_delay_min' must be" + "in the range 0-3600 %r" % value) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_preempt_delay_reload(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the preempt_delay_min property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): Preempt delay reload value to set on the vrrp. + disable (boolean): Unset preempt delay reload if True. + default (boolean): Set preempt delay reload to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt delay reload" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d preempt delay reload" % vrid + elif 0 <= value <= 3600: + cmd = "vrrp %d preempt delay reload %d" % (vrid, value) + else: + raise ValueError("vrrp property 'preempt_delay_reload' must be" + "in the range 0-3600 %r" % value) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_delay_reload(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the preempt_delay_min property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (integer): Preempt delay reload value to set on the vrrp. + disable (boolean): Unset preempt delay reload if True. + default (boolean): Set preempt delay reload to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d delay reload" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d delay reload" % vrid + elif 0 <= value <= 3600: + cmd = "vrrp %d delay reload %d" % (vrid, value) + else: + raise ValueError("vrrp property 'delay_reload' must be" + "in the range 0-3600 %r" % value) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def set_tracks(self, name, vrid, tracks, run=True): + """Configure the track property of the vrrp + + Notes: + set_tracks takes a list of tracked objects which are + to be set on the virtual router. An empty list will remove + any existing tracked objects from the vrrp. A list containing + track entries configures the virtual router to track only the + objects specified in the list - any existing tracked objects + on the vrrp not included in the list will be removed. + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + tracks (list): A list of track definition dictionaries. Each + dictionary is a definition of a tracked object in one + of the two formats: + {'name': tracked_object_name, + 'action': 'shutdown'} + {'name': tracked_object_name, + 'action': 'decrement', + 'amount': amount_of_decrement} + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + cmds = [] + + # Get the current set of tracks defined for the vrrp + curr_tracks = [] + vrrps = self.get(name) + if vrrps and vrid in vrrps: + curr_tracks = vrrps[vrid]['track'] + + # Determine which tracked objects are in both lists using + # sets of temporary strings built from the track specifications + unset = '_none_' + tracks_set = [] + for track in tracks: + tr_obj = track['name'] + action = track['action'] + amount = track['amount'] if 'amount' in track else unset + + # Validate track definition + error = False + if action not in ('shutdown', 'decrement'): + error = True + if action == 'shutdown' and amount != unset: + error = True + if amount != unset and not str(amount).isdigit(): + error = True + if error: + raise ValueError("Error found in vrrp property 'track'. " + "See documentation for format specification.") + + tid = "%s %s %s" % (tr_obj, action, amount) + tracks_set.append(tid) + + curr_set = [] + for track in curr_tracks: + tr_obj = track['name'] + action = track['action'] + amount = track['amount'] if 'amount' in track else unset + + # Validate track definition + error = False + if action not in ('shutdown', 'decrement'): + error = True + if action == 'shutdown' and amount != unset: + error = True + if amount != unset and not str(amount).isdigit(): + error = True + if error: + raise ValueError("Error found in vrrp property 'track'. " + "See documentation for format specification.") + + tid = "%s %s %s" % (tr_obj, action, amount) + curr_set.append(tid) + + intersection = list(set(tracks_set) & set(curr_set)) + + # Delete the intersection from both lists to determine which + # track definitions need to be added or removed from the vrrp + remove = list(set(curr_set) - set(intersection)) + add = list(set(tracks_set) - set(intersection)) + + # Build the commands to add and remove the tracked objects + for track in remove: + match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) + if match: + (tr_obj, action, amount) = (match.group(1), match.group(2), match.group(3)) + + if amount == unset: + amount = '' + t_cmd = "no vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount) + cmds.append(t_cmd.rstrip()) + + for track in add: + match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) + if match: + (tr_obj, action, amount) = (match.group(1), match.group(2), match.group(3)) + + if amount == unset: + amount = '' + t_cmd = "vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount) + cmds.append(t_cmd.rstrip()) + + cmds = sorted(cmds) + + # Run the command if requested + if run: + result = self.configure_interface(name, cmds) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmds + + def set_bfd_ip(self, name, vrid, value=None, disable=False, + default=False, run=True): + """Set the bfd_ip property of the vrrp + + Args: + name (string): The interface to configure. + vrid (integer): The vrid number for the vrrp to be updated. + value (string): The bfd ip address to be set. + disable (boolean): Unset bfd ip if True. + default (boolean): Set bfd ip to default if True. + run (boolean): Set to True to execute the command, False to + return a string with the formatted command. + + Returns: + If run is True: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d bfd ip" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d bfd ip" % vrid + elif re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): + cmd = "vrrp %d bfd ip %s" % (vrid, value) + else: + raise ValueError("vrrp property 'bfd_ip' must be " + "a properly formatted IP address") + + # Run the command if requested + if run: + result = self.configure_interface(name, cmd) + # And verify the command succeeded + if result is False: + return self.error + return result + + # Otherwise return the formatted command + return cmd + + def update(self, name, vrid, **kwargs): """Update a vrrp instance from an interface Note: @@ -432,7 +1133,7 @@ def update(self, interface, vrid, **kwargs): have changed from the current configuration will be updated. Args: - interface (string): The interface to configure. + name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be updated. kwargs (dict): A dictionary specifying the properties to be applied to the vrrp instance. See library documentation @@ -448,7 +1149,7 @@ def update(self, interface, vrid, **kwargs): # Get the current configuration for the vrrp from the switch. # Raise an error if the vrrp does not exist on the interface. try: - current = self.get(interface)[vrid] + current = self.get(name)[vrid] except: raise ValueError("Attempt to configure a non-existent vrrp") @@ -460,9 +1161,9 @@ def update(self, interface, vrid, **kwargs): del update[prop] # Configure the vrrp - return self._vrrp_set(interface, vrid, **update) + return self._vrrp_set(name, vrid, **update) - def _vrrp_set(self, interface, vrid, **kwargs): + def _vrrp_set(self, name, vrid, **kwargs): # Configure the commands to create or update a vrrp # configuration, and send the commands to the node. @@ -472,158 +1173,189 @@ def _vrrp_set(self, interface, vrid, **kwargs): # set those that need to be changed commands = [] + enable = vrconf.get('enable', '__NONE__') + if enable != '__NONE__': + cmd = self.set_enable(name, vrid, value=enable, run=False) + commands.append(cmd) + primary_ip = vrconf.get('primary_ip', '__NONE__') if primary_ip != '__NONE__': if primary_ip in ('no', None): - vrrps = self.get(interface) - primary_ip = vrrps[vrid]['primary_ip'] - commands.append("no vrrp %d ip %s" % (vrid, primary_ip)) + cmd = self.set_primary_ip(name, vrid, value=None, + disable=True, run=False) + commands.append(cmd) elif primary_ip is 'default': - vrrps = self.get(interface) - primary_ip = vrrps[vrid]['primary_ip'] - commands.append("default vrrp %d ip %s" % (vrid, primary_ip)) + cmd = self.set_primary_ip(name, vrid, value=None, + default=True, run=False) + commands.append(cmd) else: - commands.append("vrrp %d ip %s" % (vrid, primary_ip)) + cmd = self.set_primary_ip(name, vrid, value=primary_ip, + run=False) + commands.append(cmd) priority = vrconf.get('priority', '__NONE__') if priority != '__NONE__': if priority in ('no', None): - commands.append("no vrrp %d priority" % vrid) + cmd = self.set_priority(name, vrid, value=priority, + disable=True, run=False) + commands.append(cmd) elif priority == 'default': - commands.append("default vrrp %d priority" % vrid) + cmd = self.set_priority(name, vrid, value=priority, + default=True, run=False) + commands.append(cmd) else: - commands.append("vrrp %d priority %d" % (vrid, priority)) + cmd = self.set_priority(name, vrid, value=priority, run=False) + commands.append(cmd) description = vrconf.get('description', '__NONE__') if description != '__NONE__': if description in ('no', None): - commands.append("no vrrp %d description" % vrid) + cmd = self.set_description(name, vrid, value=description, + disable=True, run=False) + commands.append(cmd) elif description == 'default': - commands.append("default vrrp %d description" % vrid) + cmd = self.set_description(name, vrid, value=description, + default=True, run=False) + commands.append(cmd) else: - commands.append("vrrp %d description %s" % (vrid, description)) - - secondary_ip = vrconf.get('secondary_ip', '__NONE__') - if secondary_ip != '__NONE__': - secondary_add = secondary_ip.get('add', '__NONE__') - secondary_remove = secondary_ip.get('remove', '__NONE__') - if secondary_add != '__NONE__': - for s_ip in secondary_add: - commands.append("vrrp %d ip %s secondary" % (vrid, s_ip)) - if secondary_remove != '__NONE__': - for s_ip in secondary_remove: - commands.append("no vrrp %d ip %s secondary" - % (vrid, s_ip)) + cmd = self.set_description(name, vrid, value=description, + run=False) + commands.append(cmd) ip_version = vrconf.get('ip_version', '__NONE__') if ip_version != '__NONE__': if ip_version in ('no', None): - commands.append("no vrrp %d ip version" % vrid) + cmd = self.set_ip_version(name, vrid, value=ip_version, + disable=True, run=False) + commands.append(cmd) elif ip_version == 'default': - commands.append("default vrrp %d ip version" % vrid) - elif ip_version in (2, 3): - commands.append("vrrp %d ip version %d" % (vrid, ip_version)) + cmd = self.set_ip_version(name, vrid, value=ip_version, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'ip_version' must be " - "2, 3, 'no', 'default', or None") + cmd = self.set_ip_version(name, vrid, value=ip_version, + run=False) + commands.append(cmd) - enable = vrconf.get('enable', '__NONE__') - if enable != '__NONE__': - if enable in ('no', True): - commands.append("no vrrp %d shutdown" % vrid) - elif enable is False: - commands.append("vrrp %d shutdown" % vrid) - elif enable == 'default': - commands.append("default vrrp %d shutdown" % vrid) - else: - raise ValueError("vrrp property 'enable' must " - "be True, False, 'no', or 'default'") + secondary_ip = vrconf.get('secondary_ip', '__NONE__') + if secondary_ip != '__NONE__': + cmds = self.set_secondary_ips(name, vrid, secondary_ip, run=False) + for cmd in cmds: + commands.append(cmd) + # secondary_add = secondary_ip.get('add', '__NONE__') + # secondary_remove = secondary_ip.get('remove', '__NONE__') + # if secondary_add != '__NONE__': + # for s_ip in secondary_add: + # commands.append("vrrp %d ip %s secondary" % (vrid, s_ip)) + # if secondary_remove != '__NONE__': + # for s_ip in secondary_remove: + # commands.append("no vrrp %d ip %s secondary" + # % (vrid, s_ip)) timers_advertise = vrconf.get('timers_advertise', '__NONE__') if timers_advertise != '__NONE__': if timers_advertise in ('no', None): - commands.append("no vrrp %d timers advertise" % vrid) + cmd = self.set_timers_advertise(name, vrid, + value=timers_advertise, + disable=True, run=False) + commands.append(cmd) elif timers_advertise == 'default': - commands.append("default vrrp %d timers advertise" % vrid) - elif 1 <= timers_advertise <= 255: - commands.append("vrrp %d timers advertise %d" - % (vrid, timers_advertise)) + cmd = self.set_timers_advertise(name, vrid, + value=timers_advertise, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'timers_advertise' must " - "be in the range 1-255, 'no', 'default', " - "or None") + cmd = self.set_timers_advertise(name, vrid, + value=timers_advertise, + run=False) + commands.append(cmd) - mac_add_adv_int = \ + mac_addr_adv_interval = \ vrconf.get('mac_addr_adv_interval', '__NONE__') - if mac_add_adv_int != '__NONE__': - if mac_add_adv_int in ('no', None): - commands.append("no vrrp %d mac-address " - "advertisement-interval" % vrid) - elif mac_add_adv_int == 'default': - commands.append("default vrrp %d mac-address " - "advertisement-interval" % vrid) - elif 1 <= mac_add_adv_int <= 3600: - commands.append("vrrp %d mac-address advertisement-interval %d" - % (vrid, mac_add_adv_int)) + if mac_addr_adv_interval != '__NONE__': + if mac_addr_adv_interval in ('no', None): + cmd = \ + self.set_mac_addr_adv_interval(name, vrid, + value=mac_addr_adv_interval, + disable=True, run=False) + commands.append(cmd) + elif mac_addr_adv_interval == 'default': + cmd = \ + self.set_mac_addr_adv_interval(name, vrid, + value=mac_addr_adv_interval, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'mac_address_advertisement_" - "interval must be in the range 1-3600, 'no', " - "'default', or None") + cmd = \ + self.set_mac_addr_adv_interval(name, vrid, + value=mac_addr_adv_interval, + run=False) + commands.append(cmd) preempt = vrconf.get('preempt', '__NONE__') if preempt != '__NONE__': if preempt in ('no', False): - commands.append("no vrrp %d preempt" % vrid) + cmd = self.set_preempt(name, vrid, value=preempt, + disable=True, run=False) + commands.append(cmd) elif preempt == 'default': - commands.append("default vrrp %d preempt" % vrid) - elif preempt is True: - commands.append("vrrp %d preempt" % vrid) + cmd = self.set_preempt(name, vrid, value=preempt, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'preempt' must " - "be True, False, 'no', or 'default'") + cmd = self.set_preempt(name, vrid, value=preempt, run=False) + commands.append(cmd) - preempt_delay_min = vrconf.get('preempt_delay_min', - '__NONE__') + preempt_delay_min = vrconf.get('preempt_delay_min', '__NONE__') if preempt_delay_min != '__NONE__': if preempt_delay_min in ('no', None): - commands.append("no vrrp %d preempt delay minimum" % vrid) + cmd = self.set_preempt_delay_min(name, vrid, + value=preempt_delay_min, + disable=True, run=False) + commands.append(cmd) elif preempt_delay_min == 'default': - commands.append("default vrrp %d preempt delay minimum" % vrid) - elif 0 <= preempt_delay_min <= 3600: - commands.append("vrrp %d preempt delay minimum %d" % - (vrid, preempt_delay_min)) + cmd = self.set_preempt_delay_min(name, vrid, + value=preempt_delay_min, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'preempt_delay_min' " - "must an integer in the range 0-3600, None " - "'no', or 'default'") + cmd = self.set_preempt_delay_min(name, vrid, + value=preempt_delay_min, + run=False) + commands.append(cmd) preempt_delay_reload = vrconf.get('preempt_delay_reload', '__NONE__') if preempt_delay_reload != '__NONE__': if preempt_delay_reload in ('no', None): - commands.append("no vrrp %d preempt delay reload" % vrid) + cmd = self.set_preempt_delay_reload(name, vrid, + value=preempt_delay_reload, + disable=True, run=False) + commands.append(cmd) elif preempt_delay_reload == 'default': - commands.append("default vrrp %d preempt delay reload" % vrid) - elif 0 <= preempt_delay_reload <= 3600: - commands.append("vrrp %d preempt delay reload %d" % - (vrid, preempt_delay_reload)) + cmd = self.set_preempt_delay_reload(name, vrid, + value=preempt_delay_reload, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'preempt_delay_reload' " - "must be an integer in the range 0-3600, " - "None, 'no', or 'default'") + cmd = self.set_preempt_delay_reload(name, vrid, + value=preempt_delay_reload, + run=False) + commands.append(cmd) delay_reload = vrconf.get('delay_reload', '__NONE__') if delay_reload != '__NONE__': if delay_reload in ('no', None): - commands.append("no vrrp %d delay reload" % vrid) + cmd = self.set_delay_reload(name, vrid, value=delay_reload, + disable=True, run=False) + commands.append(cmd) elif delay_reload == 'default': - commands.append("default vrrp %d delay reload" % vrid) - elif 0 <= delay_reload <= 3600: - commands.append("vrrp %d delay reload %d" % - (vrid, delay_reload)) + cmd = self.set_delay_reload(name, vrid, value=delay_reload, + default=True, run=False) + commands.append(cmd) else: - raise ValueError("vrrp property 'delay_reload' " - "must be an integer in the range 0-3600, " - "None, 'no', or 'default'") + cmd = self.set_delay_reload(name, vrid, value=delay_reload, + run=False) + commands.append(cmd) authentication = vrconf.get('authentication', '__NONE__') if authentication != '__NONE__': @@ -634,47 +1366,26 @@ def _vrrp_set(self, interface, vrid, **kwargs): track = vrconf.get('track', '__NONE__') if track != '__NONE__': - for entry in track: - tracked = entry['name'] - action = entry['track_action'] - amount = entry['track_amount'] - - if amount in ('no', None): - commands.append("no vrrp %d track %s %s" - % (vrid, tracked, action)) - elif amount is 'default': - commands.append("default vrrp %d track %s %s" - % (vrid, tracked, action)) - elif action == 'shutdown': - # if action is shutdown, and amount is not 'no', None, - # or 'default, track shutdown for this object - commands.append("vrrp %d track %s %s" - % (vrid, tracked, action)) - elif amount is int(amount) and action == 'decrement': - # if the amount is an integer and the action is - # decrement, use decremental tracking for this object - commands.append("vrrp %d track %s %s %d" - % (vrid, tracked, action, amount)) - else: - # The action/amount combination did not match above, - # there is something wrong. - raise ValueError("vrrp property 'track' contains " - "improperly formatted data (%s, %s): " - "%s. See library documentation for " - "formatting example." - % (tracked, action, amount)) + cmds = self.set_tracks(name, vrid, track, run=False) + for cmd in cmds: + commands.append(cmd) bfd_ip = vrconf.get('bfd_ip', '__NONE__') if bfd_ip != '__NONE__': if bfd_ip in ('no', None): - commands.append("no vrrp %d bfd ip" % vrid) + cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, + disable=True, run=False) + commands.append(cmd) elif bfd_ip == 'default': - commands.append("default vrrp %d bfd ip" % vrid) + cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, + default=True, run=False) + commands.append(cmd) else: - commands.append("vrrp %d bfd ip %s" % (vrid, bfd_ip)) + cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, run=False) + commands.append(cmd) # Send the commands to the requested interface - result = self.configure_interface(interface, commands) + result = self.configure_interface(name, commands) # And verify the commands succeeded if result is False: return self.error @@ -703,16 +1414,10 @@ def vrconf_format(self, vrconfig): # description: default, no, None results in None if fixed['description'] in ('no', 'default', None): fixed['description'] = None - # secondary_ip: add key becomes exists key + # secondary_ip: list should be exactly what is required, + # just sort it for easier comparison if 'secondary_ip' in fixed: - if 'add' in dict(fixed['secondary_ip']): - fixed['secondary_ip']['exists'] = fixed['secondary_ip']['add'] - del fixed['secondary_ip']['add'] - if 'remove' in fixed['secondary_ip']: - del fixed['secondary_ip']['remove'] - # # secondary_ip: default, no, None results in None - # if fixed['secondary_ip'] in ('no', 'default', None): - # fixed['secondary_ip'] = None + fixed['secondary_ip'] = sorted(fixed['secondary_ip']) # ip_version: default, no, None results in value of 2 if fixed['ip_version'] in ('no', 'default', None): fixed['ip_version'] = 2 @@ -737,30 +1442,11 @@ def vrconf_format(self, vrconfig): if fixed['delay_reload'] in ('no', 'default', None): fixed['delay_reload'] = 0 # authenticetion -> XXX needs implemented - # track: default, no, None removes the entry + # track: list should be exactly what is required, + # just sort it for easier comparison if 'track' in fixed: - tracks = [] - for entry in fixed['track']: - tracked = entry['name'] - action = entry['track_action'] - amount = entry['track_amount'] - - if amount in ('no', 'default', None): - # This tracked object should have been deleted - # from the config. Do not keep it in the list. - tracked = None - elif action == 'shutdown': - # This is a valid tracked shutdown. Set it's value to None. - amount = None - - if tracked is not None: - tracks.append({ - 'name': tracked, - 'track_action': action, - 'track_amount': amount, - }) - # Copy the new list over the original, sorted for easier comparison - fixed['track'] = sorted(tracks) + fixed['track'] = \ + sorted(fixed['track'], key=lambda k: (k['name'], k['action'])) # bfd_ip: default, no, None results in '' if fixed['bfd_ip'] in ('no', 'default', None): fixed['bfd_ip'] = '' diff --git a/test/fixtures/running_config.vrrp b/test/fixtures/running_config.vrrp index 852f62c..d847eff 100644 --- a/test/fixtures/running_config.vrrp +++ b/test/fixtures/running_config.vrrp @@ -481,10 +481,18 @@ interface Vlan50 no vrrp 10 authentication vrrp 10 ip 10.10.4.10 vrrp 10 ip 10.10.4.21 secondary + vrrp 10 ip 10.10.4.22 secondary + vrrp 10 ip 10.10.4.23 secondary + vrrp 10 ip 10.10.4.24 secondary vrrp 10 ipv6 :: no vrrp 10 description no vrrp 10 shutdown + vrrp 10 track Ethernet1 decrement 10 vrrp 10 track Ethernet1 shutdown + vrrp 10 track Ethernet2 decrement 50 + vrrp 10 track Ethernet2 shutdown + vrrp 10 track Ethernet11 decrement 75 + vrrp 10 track Ethernet11 shutdown no vrrp 10 bfd ip no vrrp 10 bfd ipv6 vrrp 10 ip version 2 diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index 807cd9d..769a404 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -43,9 +43,7 @@ 'primary_ip': '10.10.10.2', 'priority': 200, 'description': 'modified vrrp 10 on an interface', - 'secondary_ip': { - 'add': ['10.10.10.11'], - }, + 'secondary_ip': ['10.10.10.11'], 'ip_version': 3, 'enable': False, 'timers_advertise': 2, @@ -56,21 +54,9 @@ 'delay_reload': 1, 'authentication': '', 'track': [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 10 - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, + {'name': 'Ethernet2', 'action': 'shutdown'}, ], 'bfd_ip': '10.10.10.150', } @@ -81,9 +67,8 @@ DESCRIPTION = ['1st modified vrrp', 'default', '2nd modified vrrp', 'no', '3rd modified vrrp', None] SECONDARY_IP = [ - {'add': ['10.10.10.51', '10.10.10.52']}, - {'add': ['10.10.10.53', '10.10.10.54'], - 'remove': ['10.10.10.51', '10.10.10.52']}, + ['10.10.10.51', '10.10.10.52'], + ['10.10.10.53', '10.10.10.54'], ] IP_VERSION = [2, 3, 'default', 3, 'no', 3, None] ENABLE = [True, False, True] @@ -95,106 +80,20 @@ DELAY_RELOAD = [30, 'default', 25, 'no', 15, None] TRACK = [ [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 10 - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, + {'name': 'Ethernet2', 'action': 'shutdown'}, ], [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 'default' - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, ], [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 2 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 20 - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 20}, + {'name': 'Ethernet2', 'action': 'shutdown'}, ], [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 'no' - }, - ], - [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 2 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 20 - }, - ], - [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': None - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, ], ] BFD_IP = ['10.10.10.160', 'default', '10.10.10.161', 'no', @@ -221,6 +120,22 @@ def test_get(self): self.assertIsNotNone(response) + def test_getall(self): + vrid = 98 + vrid2 = 198 + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'vrrp %d shutdown' % vrid, + 'exit', + 'interface vlan $', + 'vrrp %d shutdown' % vrid, + 'vrrp %d shutdown' % vrid2, + 'exit']) + response = dut.api('vrrp').getall() + + self.assertIsNotNone(response) + def test_create(self): vrid = 99 import copy @@ -237,12 +152,8 @@ def test_create(self): # Fix the configuration dict for proper output vrrp_conf = dut.api('vrrp').vrconf_format(vrrp_conf) - # temp = sorted(vrrp_conf['track']) - # vrrp_conf['track'] = temp response = dut.api('vrrp').get(interface)[vrid] - # temp = sorted(response['track']) - # response['track'] = temp self.maxDiff = None self.assertEqual(response, vrrp_conf) @@ -292,10 +203,7 @@ def test_update(self): 'primary_ip': '10.10.10.12', 'priority': 200, 'description': 'updated vrrp 10 on an interface', - 'secondary_ip': { - 'add': ['10.10.10.13', '10.10.10.23'], - 'remove': ['10.10.10.11'], - }, + 'secondary_ip': ['10.10.10.13', '10.10.10.23'], 'ip_version': 2, 'enable': True, 'timers_advertise': None, @@ -306,21 +214,8 @@ def test_update(self): 'delay_reload': 'default', 'authentication': '', 'track': [ - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 1 - }, + {'name': 'Ethernet2', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, ], 'bfd_ip': None, } diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py index 438b19d..d8831c8 100644 --- a/test/unit/test_api_vrrp.py +++ b/test/unit/test_api_vrrp.py @@ -54,7 +54,7 @@ 'delay_reload': 0, 'authentication': '', 'primary_ip': '10.10.6.10', - 'secondary_ip': {'exists': None}, + 'secondary_ip': [], 'description': 'vrrp 10 on Ethernet1', 'enable': True, 'track': [], @@ -71,9 +71,7 @@ 'delay_reload': 0, 'authentication': '', 'primary_ip': '10.10.5.10', - 'secondary_ip': { - 'exists': ['10.10.5.20'] - }, + 'secondary_ip': ['10.10.5.20'], 'description': 'vrrp 10 on Port-Channel10', 'enable': True, 'track': [], @@ -90,17 +88,17 @@ 'delay_reload': 0, 'authentication': '', 'primary_ip': '10.10.4.10', - 'secondary_ip': { - 'exists': ['10.10.4.21'] - }, + 'secondary_ip': ['10.10.4.21', '10.10.4.22', + '10.10.4.23', '10.10.4.24'], 'description': '', 'enable': True, 'track': [ - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': None - } + {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 50}, + {'name': 'Ethernet2', 'action': 'shutdown'}, + {'name': 'Ethernet11', 'action': 'decrement', 'amount': 75}, + {'name': 'Ethernet11', 'action': 'shutdown'}, ], 'bfd_ip': '', 'ip_version': 2}, @@ -113,25 +111,13 @@ 'delay_reload': 0, 'authentication': 'text 12345', 'primary_ip': '10.10.4.20', - 'secondary_ip': {'exists': None}, + 'secondary_ip': [], 'description': '', 'enable': False, 'track': [ - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': None - }, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, + {'name': 'Ethernet2', 'action': 'shutdown'}, ], 'bfd_ip': '', 'ip_version': 2}, @@ -145,7 +131,7 @@ 'authentication': 'ietf-md5 key-string 7 bu1yTgzm0RDgraNS0MNkaA==', 'primary_ip': '10.10.4.30', - 'secondary_ip': {'exists': None}, + 'secondary_ip': [], 'description': '', 'enable': True, 'track': [], @@ -170,14 +156,10 @@ def test_instance(self): def test_get(self): # Request various sets of vrrp configurations - vrrp = ['Vlan50', 'Ethernet1', 'Port-Channel10'] - # vrrp = [('Vlan50', 10), - # ('Vlan50', 20), - # ('Ethernet1', 10), - # ('Port-Channel10', 10)] - - for interface in vrrp: + for interface in known_vrrps: known = known_vrrps.get(interface) + for vrid in known: + known[vrid] = self.instance.vrconf_format(known[vrid]) result = self.instance.get(interface) self.assertEqual(result, known) @@ -206,10 +188,7 @@ def test_create(self): 'primary_ip': '10.10.60.10', 'priority': 200, 'description': 'modified vrrp 10 on Ethernet1', - 'secondary_ip': { - 'add': ['10.10.60.20'], - 'remove': ['10.10.60.30'], - }, + 'secondary_ip': ['10.10.60.20', '10.10.60.30'], 'ip_version': 3, 'timers_advertise': 2, 'mac_addr_adv_interval': 3, @@ -219,39 +198,23 @@ def test_create(self): 'delay_reload': 1, 'authentication': '', 'track': [ - { - 'name': 'Ethernet1', - 'track_action': 'decrement', - 'track_amount': None - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 1 - }, - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - ], + {'name': 'Ethernet1', 'action': 'decrement', 'amount': 1}, + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, + {'name': 'Ethernet2', 'action': 'shutdown'}, + ], 'bfd_ip': '10.10.60.30', } cmds = [ 'interface Ethernet1', + 'vrrp 10 shutdown', 'vrrp 10 ip 10.10.60.10', 'vrrp 10 priority 200', 'vrrp 10 description modified vrrp 10 on Ethernet1', - 'vrrp 10 ip 10.10.60.20 secondary', - 'no vrrp 10 ip 10.10.60.30 secondary', 'vrrp 10 ip version 3', - 'vrrp 10 shutdown', + 'vrrp 10 ip 10.10.60.20 secondary', + 'vrrp 10 ip 10.10.60.30 secondary', 'vrrp 10 timers advertise 2', 'vrrp 10 mac-address advertisement-interval 3', 'no vrrp 10 preempt', @@ -259,7 +222,7 @@ def test_create(self): 'vrrp 10 preempt delay reload 1', 'vrrp 10 delay reload 1', # 'no vrrp 10 authentication', - 'no vrrp 10 track Ethernet1 decrement', + 'vrrp 10 track Ethernet1 decrement 1', 'vrrp 10 track Ethernet1 shutdown', 'vrrp 10 track Ethernet2 decrement 1', 'vrrp 10 track Ethernet2 shutdown', @@ -295,10 +258,87 @@ def test_default(self): self.eapi_positive_config_test(func, cmds) - def test_update_primary_ip(self): + def test_set_enable(self): + # no vrrp 10 shutdown + + # Test set_enable gives properly formatted commands + cases = [ + (False, 'vrrp %d shutdown' % upd_vrid), + (True, 'no vrrp %d shutdown' % upd_vrid), + ] + + for (enable, cmd) in cases: + func = function('set_enable', upd_intf, upd_vrid, value=enable) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError from invalid parameters + cases = ['a', 200] + + for enable in cases: + func = function('set_enable', upd_intf, upd_vrid, value=enable) + self.eapi_exception_config_test(func, ValueError) + + # Test set_enable called through update when going from + # enabled to disabled (True to False) + cases = [ + (False, 'vrrp %d shutdown' % upd_vrid), + (True, None), + ] + + for (enable, cmd) in cases: + func = function('update', upd_intf, upd_vrid, enable=enable) + if cmd is not None: + exp_cmds = [upd_cmd] + [cmd] + else: + exp_cmds = [upd_cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test set_enable called through update against vrrp 20 for going + # from disabled to enabled (False to True) + # vrrp 20 shutdown + alt_vrid = 20 + cases = [ + (True, 'no vrrp %d shutdown' % alt_vrid), + (False, None), + ] + + for (enable, cmd) in cases: + func = function('update', upd_intf, alt_vrid, enable=enable) + if cmd is not None: + exp_cmds = [upd_cmd] + [cmd] + else: + exp_cmds = [upd_cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_set_primary_ip(self): # vrrp 10 ip 10.10.4.10 + + # Test set_primary_ip gives properly formatted commands ip1 = '10.10.4.110' ipcurr = '10.10.4.10' + cases = [ + (ip1, None, None, 'vrrp %d ip %s' % (upd_vrid, ip1)), + (ip1, True, None, 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), + (ip1, None, True, 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), + (ip1, True, True, 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), + ] + + for (primary_ip, disable, default, cmd) in cases: + func = function('set_primary_ip', upd_intf, upd_vrid, + value=primary_ip, disable=disable, default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError from invalid parameters + cases = ['abc', 500, '101.101'] + + for primary_ip in cases: + func = function('set_primary_ip', upd_intf, upd_vrid, + value=primary_ip) + self.eapi_exception_config_test(func, ValueError) + + # Test set_primary_ip through update cases = [ (ip1, 'vrrp %d ip %s' % (upd_vrid, ip1)), ('default', 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), @@ -312,8 +352,32 @@ def test_update_primary_ip(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - def test_update_priority(self): + def test_set_priority(self): # vrrp 10 priority 200 + + # Test set_primary_ip gives properly formatted commands + cases = [ + (150, None, None, 'vrrp %d priority 150' % upd_vrid), + (None, None, True, 'default vrrp %d priority' % upd_vrid), + (None, True, True, 'default vrrp %d priority' % upd_vrid), + (None, True, None, 'no vrrp %d priority' % upd_vrid), + ] + + for (priority, disable, default, cmd) in cases: + func = function('set_priority', upd_intf, upd_vrid, + value=priority, disable=disable, default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError from invalid parameters + cases = ['abc', 500, False] + + for priority in cases: + func = function('set_priority', upd_intf, upd_vrid, + value=priority) + self.eapi_exception_config_test(func, ValueError) + + # Test set_priority through update cases = [ (150, 'vrrp %d priority 150' % upd_vrid), ('default', 'default vrrp %d priority' % upd_vrid), @@ -327,9 +391,27 @@ def test_update_priority(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - def test_update_description(self): + def test_set_description(self): # no vrrp 10 description + desc = 'test description' + + # Test set_description gives properly formatted commands + cases = [ + (desc, None, None, 'vrrp %d description %s' % (upd_vrid, desc)), + (None, None, True, 'default vrrp %d description' % upd_vrid), + (None, True, True, 'default vrrp %d description' % upd_vrid), + (None, True, None, 'no vrrp %d description' % upd_vrid), + ] + + for (description, disable, default, cmd) in cases: + func = function('set_description', upd_intf, upd_vrid, + value=description, disable=disable, + default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test set_description through update cases = [ (desc, 'vrrp %d description %s' % (upd_vrid, desc)), ('default', 'default vrrp %d description' % upd_vrid), @@ -343,28 +425,32 @@ def test_update_description(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - def test_update_secondary_ip(self): - # vrrp 10 ip 10.10.4.21 secondary - ip1 = '10.10.4.41' - ip2 = '10.10.4.42' - ipcurr = '10.10.4.21' + def test_set_ip_version(self): + # vrrp 10 ip version 2 + + # Test set_description gives properly formatted commands cases = [ - ( - {'add': [ip1, ip2], 'remove': [ipcurr]}, - ['vrrp %d ip %s secondary' % (upd_vrid, ip1), - 'vrrp %d ip %s secondary' % (upd_vrid, ip2), - 'no vrrp %d ip %s secondary' % (upd_vrid, ipcurr)] - ), + (2, None, None, 'vrrp %d ip version 2' % upd_vrid), + (None, None, True, 'default vrrp %d ip version' % upd_vrid), + (None, True, True, 'default vrrp %d ip version' % upd_vrid), + (None, True, None, 'no vrrp %d ip version' % upd_vrid), ] - for (secondary_ip, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - secondary_ip=secondary_ip) - exp_cmds = [upd_cmd] + cmd + for (ip_version, disable, default, cmd) in cases: + func = function('set_ip_version', upd_intf, upd_vrid, + value=ip_version, disable=disable, default=default) + exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - def test_update_ip_version(self): - # vrrp 10 ip version 2 + # Test raising ValueError by entering invalid parameters + cases = ['a', 5] + + for ip_version in cases: + func = function('set_ip_version', upd_intf, upd_vrid, + value=ip_version) + self.eapi_exception_config_test(func, ValueError) + + # Test set_ip_version through update cases = [ (3, 'vrrp %d ip version 3' % upd_vrid), ('default', 'default vrrp %d ip version' % upd_vrid), @@ -378,62 +464,112 @@ def test_update_ip_version(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) + def test_set_secondary_ips(self): + # vrrp 10 ip 10.10.4.21 secondary + # vrrp 10 ip 10.10.4.22 secondary + # vrrp 10 ip 10.10.4.23 secondary + + curr1 = '10.10.4.21' + curr2 = '10.10.4.22' + curr3 = '10.10.4.23' + curr4 = '10.10.4.24' + + new1 = '10.10.4.31' + new2 = '10.10.4.32' + new3 = '10.10.4.33' + new4 = curr4 + + # Test set_secondary_ips gives properly formatted commands + cases = [ + ([new1, new2, new3], + {'add': [new1, new2, new3], + 'remove': [curr1, curr2, curr3, curr4]}), + ([new1, new2, new4], + {'add': [new1, new2], + 'remove': [curr1, curr2, curr3]}), + ([], + {'add': [], + 'remove': [curr1, curr2, curr3, curr4]}), + ] + + for (secondary_ips, cmd_dict) in cases: + cmds = [] + for sec_ip in cmd_dict['add']: + cmds.append("vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) + + for sec_ip in cmd_dict['remove']: + cmds.append("no vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) + + func = function('set_secondary_ips', upd_intf, upd_vrid, + secondary_ips) + exp_cmds = [upd_cmd] + sorted(cmds) + self.eapi_positive_config_test(func, exp_cmds) + # Test raising ValueError by entering invalid parameters - cases = ['a', 5] + cases = [ + [new1, new2, 'abc'], + [new1, new2, '10.10.10'], + [new1, new2, True], + ] - for ip_version in cases: - func = function('update', upd_intf, upd_vrid, - ip_version=ip_version) + for secondary_ips in cases: + func = function('set_secondary_ips', upd_intf, upd_vrid, + secondary_ips) self.eapi_exception_config_test(func, ValueError) - def test_update_enable(self): - # Test when going from enabled to disabled - # no vrrp 10 shutdown + # Test set_secondary_ips through update cases = [ - (False, 'vrrp %d shutdown' % upd_vrid), - (True, None), - ('no', 'no vrrp %d shutdown' % upd_vrid), - ('default', 'default vrrp %d shutdown' % upd_vrid), + ([new1, new2, new3], + {'add': [new1, new2, new3], + 'remove': [curr1, curr2, curr3, curr4]}), + ([new1, new2, new4], + {'add': [new1, new2], + 'remove': [curr1, curr2, curr3]}), + ([], + {'add': [], + 'remove': [curr1, curr2, curr3, curr4]}), ] - for (enable, cmd) in cases: + for (secondary_ips, cmd_dict) in cases: + cmds = [] + for sec_ip in cmd_dict['add']: + cmds.append("vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) + + for sec_ip in cmd_dict['remove']: + cmds.append("no vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) + func = function('update', upd_intf, upd_vrid, - enable=enable) - if cmd is not None: - exp_cmds = [upd_cmd] + [cmd] - else: - exp_cmds = [upd_cmd] + secondary_ip=secondary_ips) + exp_cmds = [upd_cmd] + sorted(cmds) self.eapi_positive_config_test(func, exp_cmds) - # Test against vrrp 20 for going from disabled to enabled - # vrrp 20 shutdown - alt_vrid = 20 + def test_set_timers_advertise(self): + # vrrp 10 timers advertise 3 + + # Test set_timers_advertise gives properly formatted commands cases = [ - (True, 'no vrrp %d shutdown' % alt_vrid), - (False, None), - ('no', 'no vrrp %d shutdown' % alt_vrid), - ('default', 'default vrrp %d shutdown' % alt_vrid) + (50, None, None, 'vrrp %d timers advertise 50' % upd_vrid), + (None, None, True, 'default vrrp %d timers advertise' % upd_vrid), + (None, True, True, 'default vrrp %d timers advertise' % upd_vrid), + (None, True, None, 'no vrrp %d timers advertise' % upd_vrid), ] - for (enable, cmd) in cases: - func = function('update', upd_intf, alt_vrid, - enable=enable) - if cmd is not None: - exp_cmds = [upd_cmd] + [cmd] - else: - exp_cmds = [upd_cmd] + for (timers_advertise, disable, default, cmd) in cases: + func = function('set_timers_advertise', upd_intf, upd_vrid, + value=timers_advertise, disable=disable, + default=default) + exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters - cases = ['a', 200] + cases = [256, 0, 'a'] - for enable in cases: - func = function('update', upd_intf, upd_vrid, - enable=enable) + for timers_advertise in cases: + func = function('set_timers_advertise', upd_intf, upd_vrid, + value=timers_advertise) self.eapi_exception_config_test(func, ValueError) - def test_update_timers_advertise(self): - # vrrp 10 timers advertise 3 + # Test set_timers_advertise through update cases = [ (255, 'vrrp %d timers advertise 255' % upd_vrid), ('default', 'default vrrp %d timers advertise' % upd_vrid), @@ -441,21 +577,40 @@ def test_update_timers_advertise(self): (None, 'no vrrp %d timers advertise' % upd_vrid), ] - for (timers_adv, cmd) in cases: + for (timers_advertise, cmd) in cases: func = function('update', upd_intf, upd_vrid, - timers_advertise=timers_adv) + timers_advertise=timers_advertise) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - cases = [256, 0, 'a'] + def test_set_mac_addr_adv_interval(self): + # vrrp 10 mac-address advertisement-interval 30 - for timers_adv in cases: - func = function('update', upd_intf, upd_vrid, - timers_advertise=timers_adv) + # Test set_timers_advertise gives properly formatted commands + maadvint = 'mac-address advertisement-interval' + cases = [ + (50, None, None, 'vrrp %d %s 50' % (upd_vrid, maadvint)), + (None, None, True, 'default vrrp %d %s' % (upd_vrid, maadvint)), + (None, True, True, 'default vrrp %d %s' % (upd_vrid, maadvint)), + (None, True, None, 'no vrrp %d %s' % (upd_vrid, maadvint)), + ] + + for (mac_addr_adv_interval, disable, default, cmd) in cases: + func = function('set_mac_addr_adv_interval', upd_intf, upd_vrid, + value=mac_addr_adv_interval, disable=disable, + default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError by entering invalid parameters + cases = ['a', 10000] + + for mac_addr_adv_interval in cases: + func = function('set_mac_addr_adv_interval', upd_intf, upd_vrid, + value=mac_addr_adv_interval) self.eapi_exception_config_test(func, ValueError) - def test_update_mac_addr_adv_int(self): - # vrrp 10 mac-address advertisement-interval 30 + # Test set_mac_addr_adv_interval through update cases = [ (3, 'vrrp %d mac-address advertisement-interval 3' % upd_vrid), ('default', 'default vrrp %d mac-address advertisement-interval' @@ -464,22 +619,39 @@ def test_update_mac_addr_adv_int(self): (None, 'no vrrp %d mac-address advertisement-interval' % upd_vrid), ] - for (mac_add_adv_int, cmd) in cases: + for (set_mac_addr_adv_interval, cmd) in cases: func = function('update', upd_intf, upd_vrid, - mac_addr_adv_interval=mac_add_adv_int) + mac_addr_adv_interval=set_mac_addr_adv_interval) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_set_preempt(self): + # vrrp 10 preempt + + # Test set_description gives properly formatted commands + cases = [ + (False, None, None, 'no vrrp %d preempt' % upd_vrid), + (True, None, None, 'vrrp %d preempt' % upd_vrid), + (None, None, True, 'default vrrp %d preempt' % upd_vrid), + (None, True, True, 'default vrrp %d preempt' % upd_vrid), + (None, True, None, 'no vrrp %d preempt' % upd_vrid), + ] + + for (preempt, disable, default, cmd) in cases: + func = function('set_preempt', upd_intf, upd_vrid, + value=preempt, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters - cases = ['a', 10000] + cases = ['a', 5] - for mac_add_adv_int in cases: - func = function('update', upd_intf, upd_vrid, - mac_addr_adv_interval=mac_add_adv_int) + for preempt in cases: + func = function('set_preempt', upd_intf, upd_vrid, + value=preempt) self.eapi_exception_config_test(func, ValueError) - def test_update_preempt(self): - # vrrp 10 preempt + # Test turning off preempt with set_preempt through update cases = [ (False, 'no vrrp %d preempt' % upd_vrid), ('default', 'default vrrp %d preempt' % upd_vrid), @@ -492,7 +664,7 @@ def test_update_preempt(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - # Test turning on preempt + # Test turning on preept through update # no vrrp 20 preempt alt_vrid = 20 cases = [ @@ -510,16 +682,36 @@ def test_update_preempt(self): exp_cmds = [upd_cmd] self.eapi_positive_config_test(func, exp_cmds) + def test_set_preempt_delay_min(self): + # vrrp 10 preempt delay minimum 0 + + # Test set_preempt_delay_min gives properly formatted commands + cases = [ + (2500, None, None, + 'vrrp %d preempt delay minimum 2500' % upd_vrid), + (None, None, True, + 'default vrrp %d preempt delay minimum' % upd_vrid), + (None, True, True, + 'default vrrp %d preempt delay minimum' % upd_vrid), + (None, True, None, 'no vrrp %d preempt delay minimum' % upd_vrid), + ] + + for (preempt_delay_min, disable, default, cmd) in cases: + func = function('set_preempt_delay_min', upd_intf, upd_vrid, + value=preempt_delay_min, disable=disable, + default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + # Test raising ValueError by entering invalid parameters - cases = ['a', 5] + cases = ['a', 3601] - for preempt in cases: - func = function('update', upd_intf, upd_vrid, - preempt=preempt) + for preempt_delay_min in cases: + func = function('set_preempt_delay_min', upd_intf, upd_vrid, + value=preempt_delay_min) self.eapi_exception_config_test(func, ValueError) - def test_update_preempt_delay_min(self): - # vrrp 10 preempt delay minimum 0 + # Test set_preempt_delay_min through update cases = [ (3, 'vrrp %d preempt delay minimum 3' % upd_vrid), ('default', 'default vrrp %d preempt delay minimum' % upd_vrid), @@ -527,22 +719,42 @@ def test_update_preempt_delay_min(self): (None, 'no vrrp %d preempt delay minimum' % upd_vrid), ] - for (preempt_dly_min, cmd) in cases: + for (preempt_delay_min, cmd) in cases: func = function('update', upd_intf, upd_vrid, - preempt_delay_min=preempt_dly_min) + preempt_delay_min=preempt_delay_min) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_set_preempt_delay_reload(self): + # vrrp 10 preempt delay reload 0 + + # Test set_preempt_delay_min gives properly formatted commands + cases = [ + (1500, None, None, + 'vrrp %d preempt delay reload 1500' % upd_vrid), + (None, None, True, + 'default vrrp %d preempt delay reload' % upd_vrid), + (None, True, True, + 'default vrrp %d preempt delay reload' % upd_vrid), + (None, True, None, 'no vrrp %d preempt delay reload' % upd_vrid), + ] + + for (preempt_delay_reload, disable, default, cmd) in cases: + func = function('set_preempt_delay_reload', upd_intf, upd_vrid, + value=preempt_delay_reload, disable=disable, + default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 3601] - for preempt_dly_min in cases: - func = function('update', upd_intf, upd_vrid, - preempt_delay_min=preempt_dly_min) + for preempt_delay_reload in cases: + func = function('set_preempt_delay_reload', upd_intf, upd_vrid, + value=preempt_delay_reload) self.eapi_exception_config_test(func, ValueError) - def test_update_preempt_delay_reload(self): - # vrrp 10 preempt delay reload 0 + # Test set_preempt_delay_reload through update cases = [ (3, 'vrrp %d preempt delay reload 3' % upd_vrid), ('default', 'default vrrp %d preempt delay reload' % upd_vrid), @@ -550,22 +762,39 @@ def test_update_preempt_delay_reload(self): (None, 'no vrrp %d preempt delay reload' % upd_vrid), ] - for (preempt_dly_rld, cmd) in cases: + for (preempt_delay_reload, cmd) in cases: func = function('update', upd_intf, upd_vrid, - preempt_delay_reload=preempt_dly_rld) + preempt_delay_reload=preempt_delay_reload) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + def test_set_delay_reload(self): + # vrrp 10 delay reload 0 + + # Test set_delay_min gives properly formatted commands + cases = [ + (1750, None, None, 'vrrp %d delay reload 1750' % upd_vrid), + (None, None, True, 'default vrrp %d delay reload' % upd_vrid), + (None, True, True, 'default vrrp %d delay reload' % upd_vrid), + (None, True, None, 'no vrrp %d delay reload' % upd_vrid), + ] + + for (delay_reload, disable, default, cmd) in cases: + func = function('set_delay_reload', upd_intf, upd_vrid, + value=delay_reload, disable=disable, + default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 3601] - for preempt_dly_rld in cases: - func = function('update', upd_intf, upd_vrid, - preempt_delay_reload=preempt_dly_rld) + for delay_reload in cases: + func = function('set_delay_reload', upd_intf, upd_vrid, + value=delay_reload) self.eapi_exception_config_test(func, ValueError) - def test_update_delay_reload(self): - # vrrp 10 delay reload 0 + # Test test_set_delay_reload through update cases = [ (3, 'vrrp %d delay reload 3' % upd_vrid), ('default', 'default vrrp %d delay reload' % upd_vrid), @@ -579,14 +808,6 @@ def test_update_delay_reload(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - # Test raising ValueError by entering invalid parameters - cases = ['a', 3601] - - for delay_reload in cases: - func = function('update', upd_intf, upd_vrid, - delay_reload=delay_reload) - self.eapi_exception_config_test(func, ValueError) - # def test_update_authentication(self): # # no vrrp 10 authentication # cases = [ @@ -609,74 +830,137 @@ def test_update_delay_reload(self): # authentication=authentication) # self.eapi_exception_config_test(func, ValueError) - def test_update_track(self): + def test_set_tracks(self): + # vrrp 10 track Ethernet1 decrement 10 # vrrp 10 track Ethernet1 shutdown - # Send various tracking commands in one go, then - # send a command with no tracking specified and expect no commands - cases = [ - ( - [{ - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 1 - }, - { - 'name': 'Ethernet1', - 'track_action': 'decrement', - 'track_amount': 10 - }, - { - 'name': 'Ethernet3', - 'track_action': 'shutdown', - 'track_amount': 'default' - }, - { - 'name': 'Ethernet2', - 'track_action': 'decrement', - 'track_amount': 'no' - }, - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': None - }], - ['vrrp %d track Ethernet2 shutdown' % upd_vrid, - 'vrrp %d track Ethernet1 decrement 10' % upd_vrid, - 'default vrrp %d track Ethernet3 shutdown' % upd_vrid, - 'no vrrp %d track Ethernet2 decrement' % upd_vrid, - 'no vrrp %d track Ethernet1 shutdown' % upd_vrid] - ), - ([], []) - ] - - for (track, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - track=track) - exp_cmds = [upd_cmd] + cmd + # vrrp 10 track Ethernet2 decrement 50 + # vrrp 10 track Ethernet2 shutdown + # vrrp 10 track Ethernet11 decrement 75 + # vrrp 10 track Ethernet11 shutdown + + curr1 = {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10} + curr2 = {'name': 'Ethernet1', 'action': 'shutdown'} + curr3 = {'name': 'Ethernet2', 'action': 'decrement', 'amount': 50} + curr4 = {'name': 'Ethernet2', 'action': 'shutdown'} + curr5 = {'name': 'Ethernet11', 'action': 'decrement', 'amount': 75} + curr6 = {'name': 'Ethernet11', 'action': 'shutdown'} + + new1 = curr1 + new2 = {'name': 'Ethernet2', 'action': 'decrement', 'amount': 49} + new3 = {'name': 'Ethernet3', 'action': 'shutdown'} + new4 = {'name': 'Ethernet4', 'action': 'decrement', 'amount': 50} + new5 = {'name': 'Ethernet5', 'action': 'shutdown'} + new6 = {'name': 'Ethernet9', 'action': 'decrement', 'amount': 75} + + # Test set_track gives properly formatted commands + cases = [ + ([curr6, curr5, new1, new2], + {'add': [new2], + 'remove': [curr2, curr3, curr4]}), + ([new2, new3, new4, new5, new6], + {'add': [new2, new3, new4, new5, new6], + 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), + ([], + {'add': [], + 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), + ] + + for (tracks, cmd_dict) in cases: + cmds = [] + for add in cmd_dict['add']: + tr_obj = add['name'] + action = add['action'] + amount = add['amount'] if 'amount' in add else '' + cmd = ("vrrp %d track %s %s %s" + % (upd_vrid, tr_obj, action, amount)) + cmds.append(cmd.rstrip()) + + for remove in cmd_dict['remove']: + tr_obj = remove['name'] + action = remove['action'] + amount = remove['amount'] if 'amount' in remove else '' + cmd = ("no vrrp %d track %s %s %s" + % (upd_vrid, tr_obj, action, amount)) + cmds.append(cmd.rstrip()) + + func = function('set_tracks', upd_intf, upd_vrid, tracks) + exp_cmds = [upd_cmd] + sorted(cmds) self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = [ - [{ - 'name': 'Ethernet1', - 'track_action': 'disable', - 'track_amount': 10 - }], - [{ - 'name': 'Ethernet1', - 'track_action': 'decrement', - 'track_amount': True - }] + [{'name': 'Ethernet1', 'action': 'disable', 'amount': 10}], + [{'name': 'Ethernet1', 'action': 'decrement', 'amount': True}] ] - for track in cases: - func = function('update', upd_intf, upd_vrid, - track=track) + for tracks in cases: + func = function('set_tracks', upd_intf, upd_vrid, tracks) self.eapi_exception_config_test(func, ValueError) - def test_update_bfd_ip(self): + # Test set_tracks through update + cases = [ + ([curr6, curr5, new1, new2], + {'add': [new2], + 'remove': [curr2, curr3, curr4]}), + ([new2, new3, new4, new5, new6], + {'add': [new2, new3, new4, new5, new6], + 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), + ([], + {'add': [], + 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), + ] + + for (tracks, cmd_dict) in cases: + cmds = [] + for add in cmd_dict['add']: + tr_obj = add['name'] + action = add['action'] + amount = add['amount'] if 'amount' in add else '' + cmd = ("vrrp %d track %s %s %s" + % (upd_vrid, tr_obj, action, amount)) + cmds.append(cmd.rstrip()) + + for remove in cmd_dict['remove']: + tr_obj = remove['name'] + action = remove['action'] + amount = remove['amount'] if 'amount' in remove else '' + cmd = ("no vrrp %d track %s %s %s" + % (upd_vrid, tr_obj, action, amount)) + cmds.append(cmd.rstrip()) + + func = function('update', upd_intf, upd_vrid, + track=tracks) + exp_cmds = [upd_cmd] + sorted(cmds) + self.eapi_positive_config_test(func, exp_cmds) + + def test_set_bfd_ip(self): # no vrrp 10 bfd ip + bfd_addr = '10.10.4.101' + + # Test bfd_ip gives properly formatted commands + cases = [ + (bfd_addr, None, None, 'vrrp %d bfd ip %s' % (upd_vrid, bfd_addr)), + (None, True, None, 'no vrrp %d bfd ip' % upd_vrid), + (None, None, True, 'default vrrp %d bfd ip' % upd_vrid), + (None, True, True, 'default vrrp %d bfd ip' % upd_vrid), + ] + + for (bfd_ip, disable, default, cmd) in cases: + func = function('set_bfd_ip', upd_intf, upd_vrid, + value=bfd_ip, disable=disable, default=default) + exp_cmds = [upd_cmd] + [cmd] + self.eapi_positive_config_test(func, exp_cmds) + + # Test raising ValueError from invalid parameters + cases = ['abc', 500, '101.101'] + + for bfd_ip in cases: + func = function('set_bfd_ip', upd_intf, upd_vrid, + value=bfd_ip) + self.eapi_exception_config_test(func, ValueError) + + # Test set_bfd_ip through update cases = [ (bfd_addr, 'vrrp %d bfd ip %s' % (upd_vrid, bfd_addr)), ('default', 'default vrrp %d bfd ip' % upd_vrid), @@ -701,69 +985,45 @@ def test_update_invalid_vrid(self): def test_vrconf_format(self): # Test the function to format a vrrp configuration to # match the output from get/getall - vrconf = {'priority': None, - 'timers_advertise': None, - 'mac_addr_adv_interval': None, - 'preempt': 'default', - 'preempt_delay_min': None, - 'preempt_delay_reload': None, - 'delay_reload': None, - 'authentication': '', - 'primary_ip': None, - 'secondary_ip': { - 'add': ['10.10.4.21'], - 'remove': ['10.10.4.22'] - }, - 'description': None, - 'enable': True, - 'track': [ - { - 'name': 'Ethernet1', - 'track_action': 'shutdown', - 'track_amount': None - }, - { - 'name': 'Ethernet1', - 'track_action': 'decrement', - 'track_amount': 10 - }, - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': 'on' - }, - ], - 'bfd_ip': None, - 'ip_version': None} - - fixed = {'priority': 100, - 'timers_advertise': 1, - 'mac_addr_adv_interval': 30, - 'preempt': False, - 'preempt_delay_min': 0, - 'preempt_delay_reload': 0, - 'delay_reload': 0, - 'authentication': '', - 'primary_ip': '0.0.0.0', - 'secondary_ip': { - 'exists': ['10.10.4.21'] - }, - 'description': None, - 'enable': True, - 'track': [ - { - 'name': 'Ethernet1', - 'track_action': 'decrement', - 'track_amount': 10 - }, - { - 'name': 'Ethernet2', - 'track_action': 'shutdown', - 'track_amount': None - }, - ], - 'bfd_ip': '', - 'ip_version': 2} + vrconf = { + 'priority': None, + 'timers_advertise': None, + 'mac_addr_adv_interval': None, + 'preempt': 'default', + 'preempt_delay_min': None, + 'preempt_delay_reload': None, + 'delay_reload': None, + 'authentication': '', + 'primary_ip': None, + 'secondary_ip': ['10.10.4.22', '10.10.4.21'], + 'description': None, + 'enable': True, + 'track': [ + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, + ], + 'bfd_ip': None, + 'ip_version': None} + + fixed = { + 'priority': 100, + 'timers_advertise': 1, + 'mac_addr_adv_interval': 30, + 'preempt': False, + 'preempt_delay_min': 0, + 'preempt_delay_reload': 0, + 'delay_reload': 0, + 'authentication': '', + 'primary_ip': '0.0.0.0', + 'secondary_ip': ['10.10.4.21', '10.10.4.22'], + 'description': None, + 'enable': True, + 'track': [ + {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, + {'name': 'Ethernet1', 'action': 'shutdown'}, + ], + 'bfd_ip': '', + 'ip_version': 2} # Get the vrconf_format method from the library func = getattr(self.instance, 'vrconf_format') From 70a07910466f4e60bea52c36fbf21fe00fe9ed25 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 30 Oct 2015 12:35:35 -0600 Subject: [PATCH 75/83] fix python3 errors --- pyeapi/api/vrrp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index f6f04c7..adae6e5 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -722,7 +722,7 @@ def set_timers_advertise(self, name, vrid, value=None, disable=False, cmd = "default vrrp %d timers advertise" % vrid elif disable is True or value is None: cmd = "no vrrp %d timers advertise" % vrid - elif 1 <= value <= 255: + elif int(value) and 1 <= int(value) <= 255: cmd = "vrrp %d timers advertise %d" % (vrid, value) else: raise ValueError("vrrp property 'timers_advertise' must be" @@ -767,7 +767,7 @@ def set_mac_addr_adv_interval(self, name, vrid, value=None, disable=False, cmd = "default vrrp %d mac-address advertisement-interval" % vrid elif disable is True or value is None: cmd = "no vrrp %d mac-address advertisement-interval" % vrid - elif 1 <= value <= 3600: + elif int(value) and 1 <= int(value) <= 3600: cmd = "vrrp %d mac-address advertisement-interval %d" \ % (vrid, value) else: @@ -852,7 +852,7 @@ def set_preempt_delay_min(self, name, vrid, value=None, disable=False, cmd = "default vrrp %d preempt delay minimum" % vrid elif disable is True or value is None: cmd = "no vrrp %d preempt delay minimum" % vrid - elif 0 <= value <= 3600: + elif int(value) and 0 <= int(value) <= 3600: cmd = "vrrp %d preempt delay minimum %d" % (vrid, value) else: raise ValueError("vrrp property 'preempt_delay_min' must be" @@ -894,7 +894,7 @@ def set_preempt_delay_reload(self, name, vrid, value=None, disable=False, cmd = "default vrrp %d preempt delay reload" % vrid elif disable is True or value is None: cmd = "no vrrp %d preempt delay reload" % vrid - elif 0 <= value <= 3600: + elif int(value) and 0 <= int(value) <= 3600: cmd = "vrrp %d preempt delay reload %d" % (vrid, value) else: raise ValueError("vrrp property 'preempt_delay_reload' must be" @@ -936,7 +936,7 @@ def set_delay_reload(self, name, vrid, value=None, disable=False, cmd = "default vrrp %d delay reload" % vrid elif disable is True or value is None: cmd = "no vrrp %d delay reload" % vrid - elif 0 <= value <= 3600: + elif int(value) and 0 <= int(value) <= 3600: cmd = "vrrp %d delay reload %d" % (vrid, value) else: raise ValueError("vrrp property 'delay_reload' must be" From 981395623fbcd6958259ca8c0d61e6ab24615558 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 30 Oct 2015 13:08:27 -0600 Subject: [PATCH 76/83] fix long lines --- pyeapi/api/vrrp.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index adae6e5..42234a2 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -1050,21 +1050,25 @@ def set_tracks(self, name, vrid, tracks, run=True): for track in remove: match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) if match: - (tr_obj, action, amount) = (match.group(1), match.group(2), match.group(3)) + (tr_obj, action, amount) = \ + (match.group(1), match.group(2), match.group(3)) if amount == unset: amount = '' - t_cmd = "no vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount) + t_cmd = ("no vrrp %d track %s %s %s" + % (vrid, tr_obj, action, amount)) cmds.append(t_cmd.rstrip()) for track in add: match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) if match: - (tr_obj, action, amount) = (match.group(1), match.group(2), match.group(3)) + (tr_obj, action, amount) = \ + (match.group(1), match.group(2), match.group(3)) if amount == unset: amount = '' - t_cmd = "vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount) + t_cmd = ("vrrp %d track %s %s %s" + % (vrid, tr_obj, action, amount)) cmds.append(t_cmd.rstrip()) cmds = sorted(cmds) From 95798604aefe3c0543a95cdbc8c59ff083e9e0e1 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 2 Nov 2015 10:47:38 -0700 Subject: [PATCH 77/83] remove redundant commands.append lines; remove authentication placeholders --- pyeapi/api/vrrp.py | 75 ++++++------------------------------ test/system/test_api_vrrp.py | 2 - test/unit/test_api_vrrp.py | 32 --------------- 3 files changed, 12 insertions(+), 97 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 42234a2..b2e1a9f 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -59,7 +59,6 @@ preempt_delay_min (int): The preempt delay minimum setting for the vrrp preempt_delay_reload (int): The preempt delay reload setting for the vrrp delay_reload (int): The delay reload setting for the vrrp - authentication (string): The authentication setting for the vrrp track (list): The object tracking settings for the vrrp bfd_ip (string): The bfd ip set for the vrrp @@ -102,7 +101,6 @@ preempt_delay_min: preempt_delay_reload: delay_reload: - authentication: track: [ { name: @@ -135,7 +133,6 @@ preempt_delay_min: |no|default|None preempt_delay_reload: |no|default|None delay_reload: |no|default|None - authentication: NOTE: currently not implemented track: of dicts in the following format: { name: @@ -154,7 +151,7 @@ 'ip_version', 'enable', 'timers_advertise', 'mac_addr_adv_interval', 'preempt', 'preempt_delay_min', 'preempt_delay_reload', - 'delay_reload', 'authentication', 'track', 'bfd_ip'] + 'delay_reload', 'track', 'bfd_ip'] class Vrrp(EntityCollection): @@ -201,7 +198,6 @@ def get(self, name): subd = dict() # Parse the vrrp configuration for the vrid(s) in the list - subd.update(self._parse_authentication(config, vrid)) subd.update(self._parse_delay_reload(config, vrid)) subd.update(self._parse_description(config, vrid)) subd.update(self._parse_enable(config, vrid)) @@ -304,14 +300,6 @@ def _parse_preempt_delay_reload(self, config, vrid): value = int(match.group(1)) if match else None return dict(preempt_delay_reload=value) - def _parse_authentication(self, config, vrid): - match = re.search(r'^\s+vrrp %s authentication' - r'($| ietf-md5 key-string 7 .*$| text .*$)' % - vrid, config, re.M) - if match: - return dict(authentication=match.group(1).lstrip()) - return dict(authentication='') - def _parse_bfd_ip(self, config, vrid): match = re.search(r'^\s+vrrp %s bfd ip' r'(?: (\d+\.\d+\.\d+\.\d+)|)$' % @@ -1187,74 +1175,57 @@ def _vrrp_set(self, name, vrid, **kwargs): if primary_ip in ('no', None): cmd = self.set_primary_ip(name, vrid, value=None, disable=True, run=False) - commands.append(cmd) elif primary_ip is 'default': cmd = self.set_primary_ip(name, vrid, value=None, default=True, run=False) - commands.append(cmd) else: cmd = self.set_primary_ip(name, vrid, value=primary_ip, run=False) - commands.append(cmd) + commands.append(cmd) priority = vrconf.get('priority', '__NONE__') if priority != '__NONE__': if priority in ('no', None): cmd = self.set_priority(name, vrid, value=priority, disable=True, run=False) - commands.append(cmd) elif priority == 'default': cmd = self.set_priority(name, vrid, value=priority, default=True, run=False) - commands.append(cmd) else: cmd = self.set_priority(name, vrid, value=priority, run=False) - commands.append(cmd) + commands.append(cmd) description = vrconf.get('description', '__NONE__') if description != '__NONE__': if description in ('no', None): cmd = self.set_description(name, vrid, value=description, disable=True, run=False) - commands.append(cmd) elif description == 'default': cmd = self.set_description(name, vrid, value=description, default=True, run=False) - commands.append(cmd) else: cmd = self.set_description(name, vrid, value=description, run=False) - commands.append(cmd) + commands.append(cmd) ip_version = vrconf.get('ip_version', '__NONE__') if ip_version != '__NONE__': if ip_version in ('no', None): cmd = self.set_ip_version(name, vrid, value=ip_version, disable=True, run=False) - commands.append(cmd) elif ip_version == 'default': cmd = self.set_ip_version(name, vrid, value=ip_version, default=True, run=False) - commands.append(cmd) else: cmd = self.set_ip_version(name, vrid, value=ip_version, run=False) - commands.append(cmd) + commands.append(cmd) secondary_ip = vrconf.get('secondary_ip', '__NONE__') if secondary_ip != '__NONE__': cmds = self.set_secondary_ips(name, vrid, secondary_ip, run=False) for cmd in cmds: commands.append(cmd) - # secondary_add = secondary_ip.get('add', '__NONE__') - # secondary_remove = secondary_ip.get('remove', '__NONE__') - # if secondary_add != '__NONE__': - # for s_ip in secondary_add: - # commands.append("vrrp %d ip %s secondary" % (vrid, s_ip)) - # if secondary_remove != '__NONE__': - # for s_ip in secondary_remove: - # commands.append("no vrrp %d ip %s secondary" - # % (vrid, s_ip)) timers_advertise = vrconf.get('timers_advertise', '__NONE__') if timers_advertise != '__NONE__': @@ -1262,17 +1233,15 @@ def _vrrp_set(self, name, vrid, **kwargs): cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, disable=True, run=False) - commands.append(cmd) elif timers_advertise == 'default': cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, default=True, run=False) - commands.append(cmd) else: cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, run=False) - commands.append(cmd) + commands.append(cmd) mac_addr_adv_interval = \ vrconf.get('mac_addr_adv_interval', '__NONE__') @@ -1282,33 +1251,29 @@ def _vrrp_set(self, name, vrid, **kwargs): self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, disable=True, run=False) - commands.append(cmd) elif mac_addr_adv_interval == 'default': cmd = \ self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, default=True, run=False) - commands.append(cmd) else: cmd = \ self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, run=False) - commands.append(cmd) + commands.append(cmd) preempt = vrconf.get('preempt', '__NONE__') if preempt != '__NONE__': if preempt in ('no', False): cmd = self.set_preempt(name, vrid, value=preempt, disable=True, run=False) - commands.append(cmd) elif preempt == 'default': cmd = self.set_preempt(name, vrid, value=preempt, default=True, run=False) - commands.append(cmd) else: cmd = self.set_preempt(name, vrid, value=preempt, run=False) - commands.append(cmd) + commands.append(cmd) preempt_delay_min = vrconf.get('preempt_delay_min', '__NONE__') if preempt_delay_min != '__NONE__': @@ -1316,17 +1281,15 @@ def _vrrp_set(self, name, vrid, **kwargs): cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, disable=True, run=False) - commands.append(cmd) elif preempt_delay_min == 'default': cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, default=True, run=False) - commands.append(cmd) else: cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, run=False) - commands.append(cmd) + commands.append(cmd) preempt_delay_reload = vrconf.get('preempt_delay_reload', '__NONE__') if preempt_delay_reload != '__NONE__': @@ -1334,39 +1297,28 @@ def _vrrp_set(self, name, vrid, **kwargs): cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, disable=True, run=False) - commands.append(cmd) elif preempt_delay_reload == 'default': cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, default=True, run=False) - commands.append(cmd) else: cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, run=False) - commands.append(cmd) + commands.append(cmd) delay_reload = vrconf.get('delay_reload', '__NONE__') if delay_reload != '__NONE__': if delay_reload in ('no', None): cmd = self.set_delay_reload(name, vrid, value=delay_reload, disable=True, run=False) - commands.append(cmd) elif delay_reload == 'default': cmd = self.set_delay_reload(name, vrid, value=delay_reload, default=True, run=False) - commands.append(cmd) else: cmd = self.set_delay_reload(name, vrid, value=delay_reload, run=False) - commands.append(cmd) - - authentication = vrconf.get('authentication', '__NONE__') - if authentication != '__NONE__': - pass - # XXX not yet implemented - # needs some handling, because input string does not - # necessarily match the status string. + commands.append(cmd) track = vrconf.get('track', '__NONE__') if track != '__NONE__': @@ -1379,14 +1331,12 @@ def _vrrp_set(self, name, vrid, **kwargs): if bfd_ip in ('no', None): cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, disable=True, run=False) - commands.append(cmd) elif bfd_ip == 'default': cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, default=True, run=False) - commands.append(cmd) else: cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, run=False) - commands.append(cmd) + commands.append(cmd) # Send the commands to the requested interface result = self.configure_interface(name, commands) @@ -1445,7 +1395,6 @@ def vrconf_format(self, vrconfig): # delay_reload: default, no, None results in value of 0 if fixed['delay_reload'] in ('no', 'default', None): fixed['delay_reload'] = 0 - # authenticetion -> XXX needs implemented # track: list should be exactly what is required, # just sort it for easier comparison if 'track' in fixed: diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index 769a404..145b36b 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -52,7 +52,6 @@ 'preempt_delay_min': 1, 'preempt_delay_reload': None, 'delay_reload': 1, - 'authentication': '', 'track': [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, @@ -212,7 +211,6 @@ def test_update(self): 'preempt_delay_min': 'default', 'preempt_delay_reload': 'default', 'delay_reload': 'default', - 'authentication': '', 'track': [ {'name': 'Ethernet2', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py index d8831c8..3c13029 100644 --- a/test/unit/test_api_vrrp.py +++ b/test/unit/test_api_vrrp.py @@ -52,7 +52,6 @@ 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': '', 'primary_ip': '10.10.6.10', 'secondary_ip': [], 'description': 'vrrp 10 on Ethernet1', @@ -69,7 +68,6 @@ 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': '', 'primary_ip': '10.10.5.10', 'secondary_ip': ['10.10.5.20'], 'description': 'vrrp 10 on Port-Channel10', @@ -86,7 +84,6 @@ 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': '', 'primary_ip': '10.10.4.10', 'secondary_ip': ['10.10.4.21', '10.10.4.22', '10.10.4.23', '10.10.4.24'], @@ -109,7 +106,6 @@ 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': 'text 12345', 'primary_ip': '10.10.4.20', 'secondary_ip': [], 'description': '', @@ -128,8 +124,6 @@ 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': - 'ietf-md5 key-string 7 bu1yTgzm0RDgraNS0MNkaA==', 'primary_ip': '10.10.4.30', 'secondary_ip': [], 'description': '', @@ -196,7 +190,6 @@ def test_create(self): 'preempt_delay_min': 1, 'preempt_delay_reload': 1, 'delay_reload': 1, - 'authentication': '', 'track': [ {'name': 'Ethernet1', 'action': 'decrement', 'amount': 1}, {'name': 'Ethernet1', 'action': 'shutdown'}, @@ -221,7 +214,6 @@ def test_create(self): 'vrrp 10 preempt delay minimum 1', 'vrrp 10 preempt delay reload 1', 'vrrp 10 delay reload 1', - # 'no vrrp 10 authentication', 'vrrp 10 track Ethernet1 decrement 1', 'vrrp 10 track Ethernet1 shutdown', 'vrrp 10 track Ethernet2 decrement 1', @@ -808,28 +800,6 @@ def test_set_delay_reload(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - # def test_update_authentication(self): - # # no vrrp 10 authentication - # cases = [ - # # XXX fix test cases - # ] - # - # for (authentication, cmd) in cases: - # func = function('update', upd_intf, upd_vrid, - # authentication=authentication) - # exp_cmds = [upd_cmd] + [cmd] - # self.eapi_positive_config_test(func, exp_cmds) - # - # # Test raising ValueError by entering invalid parameters - # cases = [ - # # XXX fix test cases - # ] - # - # for authentication in cases: - # func = function('update', upd_intf, upd_vrid, - # authentication=authentication) - # self.eapi_exception_config_test(func, ValueError) - def test_set_tracks(self): # vrrp 10 track Ethernet1 decrement 10 # vrrp 10 track Ethernet1 shutdown @@ -993,7 +963,6 @@ def test_vrconf_format(self): 'preempt_delay_min': None, 'preempt_delay_reload': None, 'delay_reload': None, - 'authentication': '', 'primary_ip': None, 'secondary_ip': ['10.10.4.22', '10.10.4.21'], 'description': None, @@ -1013,7 +982,6 @@ def test_vrconf_format(self): 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, - 'authentication': '', 'primary_ip': '0.0.0.0', 'secondary_ip': ['10.10.4.21', '10.10.4.22'], 'description': None, From 93934a436a0ad1f2594cad2d1f9680d1ec08850a Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 2 Nov 2015 12:39:46 -0700 Subject: [PATCH 78/83] remove update method from api and fix affected test cases --- pyeapi/api/vrrp.py | 41 ---- test/system/test_api_vrrp.py | 272 +++++++++++++++++---------- test/unit/test_api_vrrp.py | 355 ++++++++--------------------------- 3 files changed, 256 insertions(+), 412 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index b2e1a9f..9d35faa 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -1114,47 +1114,6 @@ def set_bfd_ip(self, name, vrid, value=None, disable=False, # Otherwise return the formatted command return cmd - def update(self, name, vrid, **kwargs): - """Update a vrrp instance from an interface - - Note: - This method will attempt to set parameters for the vrrp on - the node's operational config. Parameters specified in the - kwargs argument will be matched against the current - parameters for the vrrp, and only those parameters that - have changed from the current configuration will be updated. - - Args: - name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. - kwargs (dict): A dictionary specifying the properties to - be applied to the vrrp instance. See library documentation - for available keys and values. - - Returns: - True if the vrrp could be updated otherwise False (see Node) - - """ - - update = dict(kwargs) - - # Get the current configuration for the vrrp from the switch. - # Raise an error if the vrrp does not exist on the interface. - try: - current = self.get(name)[vrid] - except: - raise ValueError("Attempt to configure a non-existent vrrp") - - # Keep only those properties in the update dictionary that - # are different than the current vrrp configuration. - for prop in PROPERTIES: - if prop in update: - if update[prop] == current[prop]: - del update[prop] - - # Configure the vrrp - return self._vrrp_set(name, vrid, **update) - def _vrrp_set(self, name, vrid, **kwargs): # Configure the commands to create or update a vrrp # configuration, and send the commands to the node. diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index 145b36b..fd1ee10 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -36,7 +36,6 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest -from testlib import random_string IP_PREFIX = '10.10.10.' VR_CONFIG = { @@ -60,44 +59,6 @@ 'bfd_ip': '10.10.10.150', } -# Define various test input -PRIMARY_IP = ['10.10.10.2', 'default', '10.10.10.3', 'no', '10.10.10.4', None] -PRIORITY = [200, 'default', 175, 'no', 190, None] -DESCRIPTION = ['1st modified vrrp', 'default', '2nd modified vrrp', 'no', - '3rd modified vrrp', None] -SECONDARY_IP = [ - ['10.10.10.51', '10.10.10.52'], - ['10.10.10.53', '10.10.10.54'], -] -IP_VERSION = [2, 3, 'default', 3, 'no', 3, None] -ENABLE = [True, False, True] -TIMERS_ADVERTISE = [10, 'default', 20, 'no', 30, None] -MAC_ADDR_ADV_INTVL = [50, 'default', 55, 'no', 60, None] -PREEMPT = [True, False, True] -PREEMPT_DELAY_MIN = [3600, 'default', 500, 'no', 150, None] -PREEMPT_DELAY_RELOAD = [3600, 'default', 500, 'no', 150, None] -DELAY_RELOAD = [30, 'default', 25, 'no', 15, None] -TRACK = [ - [ - {'name': 'Ethernet1', 'action': 'shutdown'}, - {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, - {'name': 'Ethernet2', 'action': 'shutdown'}, - ], - [ - {'name': 'Ethernet1', 'action': 'shutdown'}, - ], - [ - {'name': 'Ethernet1', 'action': 'shutdown'}, - {'name': 'Ethernet2', 'action': 'decrement', 'amount': 20}, - {'name': 'Ethernet2', 'action': 'shutdown'}, - ], - [ - {'name': 'Ethernet1', 'action': 'shutdown'}, - ], -] -BFD_IP = ['10.10.10.160', 'default', '10.10.10.161', 'no', - '10.10.10.162', None] - class TestApiVrrp(DutSystemTest): @@ -181,7 +142,7 @@ def test_default(self): response = dut.api('vrrp').delete(interface, vrid) self.assertIs(response, True) - def test_update(self): + def test_update_with_create(self): pass vrid = 103 import copy @@ -218,7 +179,7 @@ def test_update(self): 'bfd_ip': None, } - response = dut.api('vrrp').update(interface, vrid, **vrrp_update) + response = dut.api('vrrp').create(interface, vrid, **vrrp_update) self.assertIs(response, True) vrrp_update = dut.api('vrrp').vrconf_format(vrrp_update) @@ -227,8 +188,36 @@ def test_update(self): self.maxDiff = None self.assertEqual(response, vrrp_update) - def test_update_primary_ip(self): + def test_set_enable(self): + vrid = 104 + enable_cases = [ + {'value': True}, + {'value': False}, + {'value': True}, + {'value': False}, + ] + for dut in self.duts: + interface = self._vlan_setup(dut) + dut.config(['interface %s' % interface, + 'no vrrp %d' % vrid, + 'vrrp %d shutdown' % vrid, + 'vrrp %d ip 10.10.10.2' % vrid, + 'exit']) + + for enable in enable_cases: + response = dut.api('vrrp').set_enable( + interface, vrid, **enable) + self.assertIs(response, True) + + def test_set_primary_ip(self): vrid = 104 + primary_ip_cases = [ + {'value': '10.10.10.2'}, + {'default': True}, + {'value': '10.10.10.3'}, + {'disable': True}, + {'value': '10.10.10.4'}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -236,13 +225,20 @@ def test_update_primary_ip(self): 'vrrp %d shutdown' % vrid, 'exit']) - for p_ip in PRIMARY_IP: - vrconfig = {'primary_ip': p_ip} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for p_ip in primary_ip_cases: + response = dut.api('vrrp').set_primary_ip( + interface, vrid, **p_ip) self.assertIs(response, True) - def test_update_priority(self): + def test_set_priority(self): vrid = 104 + priority_cases = [ + {'value': 200}, + {'default': True}, + {'value': 175}, + {'disable': True}, + {'value': 190} + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -250,13 +246,20 @@ def test_update_priority(self): 'vrrp %d shutdown' % vrid, 'exit']) - for priority in PRIORITY: - vrconfig = {'priority': priority} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for priority in priority_cases: + response = dut.api('vrrp').set_priority( + interface, vrid, **priority) self.assertIs(response, True) - def test_update_description(self): + def test_set_description(self): vrid = 104 + desc_cases = [ + {'value': '1st modified vrrp'}, + {'default': True}, + {'value': '2nd modified vrrp'}, + {'disable': True}, + {'value': '3rd modified vrrp'}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -264,13 +267,18 @@ def test_update_description(self): 'vrrp %d shutdown' % vrid, 'exit']) - for description in DESCRIPTION: - vrconfig = {'description': description} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for description in desc_cases: + response = dut.api('vrrp').set_description( + interface, vrid, **description) self.assertIs(response, True) - def test_update_secondary_ip(self): + def test_set_secondary_ips(self): vrid = 104 + secondary_ip_cases = [ + ['10.10.10.51', '10.10.10.52'], + ['10.10.10.53', '10.10.10.54'], + [], + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -278,13 +286,21 @@ def test_update_secondary_ip(self): 'vrrp %d shutdown' % vrid, 'exit']) - for s_ip in SECONDARY_IP: - vrconfig = {'secondary_ip': s_ip} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for s_ip_list in secondary_ip_cases: + response = dut.api('vrrp').set_secondary_ips( + interface, vrid, s_ip_list) self.assertIs(response, True) - def test_update_ip_version(self): + def test_set_ip_version(self): vrid = 104 + ip_version_cases = [ + {'value': 2}, + {'value': 3}, + {'default': True}, + {'value': 3}, + {'disable': True}, + {'value': 3}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -292,13 +308,20 @@ def test_update_ip_version(self): 'vrrp %d shutdown' % vrid, 'exit']) - for ip_version in IP_VERSION: - vrconfig = {'ip_version': ip_version} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for ip_version in ip_version_cases: + response = dut.api('vrrp').set_ip_version( + interface, vrid, **ip_version) self.assertIs(response, True) - def test_update_timers_advertise(self): + def test_set_timers_advertise(self): vrid = 104 + timers_adv_cases = [ + {'value': 10}, + {'default': True}, + {'value': 20}, + {'disable': True}, + {'value': 30}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -306,13 +329,20 @@ def test_update_timers_advertise(self): 'vrrp %d shutdown' % vrid, 'exit']) - for timers_advertise in TIMERS_ADVERTISE: - vrconfig = {'timers_advertise': timers_advertise} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for timers_advertise in timers_adv_cases: + response = dut.api('vrrp').set_timers_advertise( + interface, vrid, **timers_advertise) self.assertIs(response, True) - def test_update_mac_addr_adv_interval(self): + def test_set_mac_addr_adv_interval(self): vrid = 104 + mac_addr_adv_int_cases = [ + {'value': 50}, + {'default': True}, + {'value': 55}, + {'disable': True}, + {'value': 60}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -320,14 +350,21 @@ def test_update_mac_addr_adv_interval(self): 'vrrp %d shutdown' % vrid, 'exit']) - for mac_addr_adv_intvl in MAC_ADDR_ADV_INTVL: - vrconfig = {'mac_addr_adv_interval': - mac_addr_adv_intvl} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for mac_addr_adv_intvl in mac_addr_adv_int_cases: + response = dut.api('vrrp').set_mac_addr_adv_interval( + interface, vrid, **mac_addr_adv_intvl) self.assertIs(response, True) - def test_update_preempt(self): + def test_set_preempt(self): vrid = 104 + preempt_cases = [ + {'value': True}, + {'default': True}, + {'value': True}, + {'disable': True}, + {'value': True}, + {'value': False}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -335,13 +372,20 @@ def test_update_preempt(self): 'vrrp %d shutdown' % vrid, 'exit']) - for preempt in PREEMPT: - vrconfig = {'preempt': preempt} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for preempt in preempt_cases: + response = dut.api('vrrp').set_preempt( + interface, vrid, **preempt) self.assertIs(response, True) - def test_update_preempt_delay_min(self): + def test_set_preempt_delay_min(self): vrid = 104 + preempt_delay_min_cases = [ + {'value': 3600}, + {'default': True}, + {'value': 500}, + {'disable': True}, + {'value': 150}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -349,13 +393,20 @@ def test_update_preempt_delay_min(self): 'vrrp %d shutdown' % vrid, 'exit']) - for preempt_delay_min in PREEMPT_DELAY_MIN: - vrconfig = {'preempt_delay_min': preempt_delay_min} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for preempt_delay_min in preempt_delay_min_cases: + response = dut.api('vrrp').set_preempt_delay_min( + interface, vrid, **preempt_delay_min) self.assertIs(response, True) - def test_update_preempt_delay_reload(self): + def test_set_preempt_delay_reload(self): vrid = 104 + preempt_delay_reload_cases = [ + {'value': 3600}, + {'default': True}, + {'value': 500}, + {'disable': True}, + {'value': 150}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -363,13 +414,20 @@ def test_update_preempt_delay_reload(self): 'vrrp %d shutdown' % vrid, 'exit']) - for preempt_delay_reload in PREEMPT_DELAY_RELOAD: - vrconfig = {'preempt_delay_reload': preempt_delay_reload} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for preempt_delay_reload in preempt_delay_reload_cases: + response = dut.api('vrrp').set_preempt_delay_reload( + interface, vrid, **preempt_delay_reload) self.assertIs(response, True) - def test_update_delay_reload(self): + def test_set_delay_reload(self): vrid = 104 + delay_reload_cases = [ + {'value': 30}, + {'default': True}, + {'value': 25}, + {'disable': True}, + {'value': 15}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -377,13 +435,32 @@ def test_update_delay_reload(self): 'vrrp %d shutdown' % vrid, 'exit']) - for delay_reload in DELAY_RELOAD: - vrconfig = {'delay_reload': delay_reload} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for delay_reload in delay_reload_cases: + response = dut.api('vrrp').set_delay_reload( + interface, vrid, **delay_reload) self.assertIs(response, True) - def test_update_track(self): + def test_set_tracks(self): vrid = 104 + track_cases = [ + [ + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, + {'name': 'Ethernet2', 'action': 'shutdown'}, + ], + [ + {'name': 'Ethernet1', 'action': 'shutdown'}, + ], + [ + {'name': 'Ethernet1', 'action': 'shutdown'}, + {'name': 'Ethernet2', 'action': 'decrement', 'amount': 20}, + {'name': 'Ethernet2', 'action': 'shutdown'}, + ], + [ + {'name': 'Ethernet1', 'action': 'shutdown'}, + ], + [], + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -391,13 +468,20 @@ def test_update_track(self): 'vrrp %d shutdown' % vrid, 'exit']) - for track in TRACK: - vrconfig = {'track': track} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for track_list in track_cases: + response = dut.api('vrrp').set_tracks( + interface, vrid, track_list) self.assertIs(response, True) - def test_update_bfd_ip(self): + def test_set_bfd_ip(self): vrid = 104 + bfd_ip_cases = [ + {'value': '10.10.10.160'}, + {'default': True}, + {'value': '10.10.10.161'}, + {'disable': True}, + {'value': '10.10.10.162'}, + ] for dut in self.duts: interface = self._vlan_setup(dut) dut.config(['interface %s' % interface, @@ -405,9 +489,9 @@ def test_update_bfd_ip(self): 'vrrp %d shutdown' % vrid, 'exit']) - for bfd_ip in BFD_IP: - vrconfig = {'bfd_ip': bfd_ip} - response = dut.api('vrrp').update(interface, vrid, **vrconfig) + for bfd_ip in bfd_ip_cases: + response = dut.api('vrrp').set_bfd_ip( + interface, vrid, **bfd_ip) self.assertIs(response, True) diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py index 3c13029..d4948c9 100644 --- a/test/unit/test_api_vrrp.py +++ b/test/unit/test_api_vrrp.py @@ -178,6 +178,8 @@ def test_getall(self): def test_create(self): interface = 'Ethernet1' vrid = 10 + + # Test create with a normal configuration configuration = { 'primary_ip': '10.10.60.10', 'priority': 200, @@ -186,7 +188,7 @@ def test_create(self): 'ip_version': 3, 'timers_advertise': 2, 'mac_addr_adv_interval': 3, - 'preempt': False, + 'preempt': True, 'preempt_delay_min': 1, 'preempt_delay_reload': 1, 'delay_reload': 1, @@ -210,7 +212,7 @@ def test_create(self): 'vrrp 10 ip 10.10.60.30 secondary', 'vrrp 10 timers advertise 2', 'vrrp 10 mac-address advertisement-interval 3', - 'no vrrp 10 preempt', + 'vrrp 10 preempt', 'vrrp 10 preempt delay minimum 1', 'vrrp 10 preempt delay reload 1', 'vrrp 10 delay reload 1', @@ -224,6 +226,78 @@ def test_create(self): self.eapi_positive_config_test(func, cmds) + # Test create setting possible parameters to 'no' + configuration = { + 'primary_ip': 'no', + 'priority': 'no', + 'description': 'no', + 'secondary_ip': [], + 'ip_version': 'no', + 'timers_advertise': 'no', + 'mac_addr_adv_interval': 'no', + 'preempt': 'no', + 'preempt_delay_min': 'no', + 'preempt_delay_reload': 'no', + 'delay_reload': 'no', + 'track': [], + 'bfd_ip': 'no', + } + + cmds = [ + 'interface Ethernet1', + 'vrrp 10 shutdown', + 'no vrrp 10 ip 10.10.6.10', + 'no vrrp 10 priority', + 'no vrrp 10 description', + 'no vrrp 10 ip version', + 'no vrrp 10 timers advertise', + 'no vrrp 10 mac-address advertisement-interval', + 'no vrrp 10 preempt', + 'no vrrp 10 preempt delay minimum', + 'no vrrp 10 preempt delay reload', + 'no vrrp 10 delay reload', + 'no vrrp 10 bfd ip', + ] + func = function('create', interface, vrid, **configuration) + + self.eapi_positive_config_test(func, cmds) + + # Test create setting possible parameters to 'default' + configuration = { + 'primary_ip': 'default', + 'priority': 'default', + 'description': 'default', + 'secondary_ip': [], + 'ip_version': 'default', + 'timers_advertise': 'default', + 'mac_addr_adv_interval': 'default', + 'preempt': 'default', + 'preempt_delay_min': 'default', + 'preempt_delay_reload': 'default', + 'delay_reload': 'default', + 'track': [], + 'bfd_ip': 'default', + } + + cmds = [ + 'interface Ethernet1', + 'vrrp 10 shutdown', + 'default vrrp 10 ip 10.10.6.10', + 'default vrrp 10 priority', + 'default vrrp 10 description', + 'default vrrp 10 ip version', + 'default vrrp 10 timers advertise', + 'default vrrp 10 mac-address advertisement-interval', + 'default vrrp 10 preempt', + 'default vrrp 10 preempt delay minimum', + 'default vrrp 10 preempt delay reload', + 'default vrrp 10 delay reload', + 'default vrrp 10 bfd ip', + ] + func = function('create', interface, vrid, **configuration) + + self.eapi_positive_config_test(func, cmds) + def test_delete(self): interface = 'Ethernet1' vrid = 10 @@ -271,38 +345,6 @@ def test_set_enable(self): func = function('set_enable', upd_intf, upd_vrid, value=enable) self.eapi_exception_config_test(func, ValueError) - # Test set_enable called through update when going from - # enabled to disabled (True to False) - cases = [ - (False, 'vrrp %d shutdown' % upd_vrid), - (True, None), - ] - - for (enable, cmd) in cases: - func = function('update', upd_intf, upd_vrid, enable=enable) - if cmd is not None: - exp_cmds = [upd_cmd] + [cmd] - else: - exp_cmds = [upd_cmd] - self.eapi_positive_config_test(func, exp_cmds) - - # Test set_enable called through update against vrrp 20 for going - # from disabled to enabled (False to True) - # vrrp 20 shutdown - alt_vrid = 20 - cases = [ - (True, 'no vrrp %d shutdown' % alt_vrid), - (False, None), - ] - - for (enable, cmd) in cases: - func = function('update', upd_intf, alt_vrid, enable=enable) - if cmd is not None: - exp_cmds = [upd_cmd] + [cmd] - else: - exp_cmds = [upd_cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_primary_ip(self): # vrrp 10 ip 10.10.4.10 @@ -330,20 +372,6 @@ def test_set_primary_ip(self): value=primary_ip) self.eapi_exception_config_test(func, ValueError) - # Test set_primary_ip through update - cases = [ - (ip1, 'vrrp %d ip %s' % (upd_vrid, ip1)), - ('default', 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), - ('no', 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), - (None, 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), - ] - - for (primary_ip, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - primary_ip=primary_ip) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_priority(self): # vrrp 10 priority 200 @@ -369,20 +397,6 @@ def test_set_priority(self): value=priority) self.eapi_exception_config_test(func, ValueError) - # Test set_priority through update - cases = [ - (150, 'vrrp %d priority 150' % upd_vrid), - ('default', 'default vrrp %d priority' % upd_vrid), - ('no', 'no vrrp %d priority' % upd_vrid), - (None, 'no vrrp %d priority' % upd_vrid), - ] - - for (priority, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - priority=priority) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_description(self): # no vrrp 10 description @@ -403,20 +417,6 @@ def test_set_description(self): exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) - # Test set_description through update - cases = [ - (desc, 'vrrp %d description %s' % (upd_vrid, desc)), - ('default', 'default vrrp %d description' % upd_vrid), - ('no', 'no vrrp %d description' % upd_vrid), - (None, 'no vrrp %d description' % upd_vrid), - ] - - for (description, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - description=description) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_ip_version(self): # vrrp 10 ip version 2 @@ -442,20 +442,6 @@ def test_set_ip_version(self): value=ip_version) self.eapi_exception_config_test(func, ValueError) - # Test set_ip_version through update - cases = [ - (3, 'vrrp %d ip version 3' % upd_vrid), - ('default', 'default vrrp %d ip version' % upd_vrid), - ('no', 'no vrrp %d ip version' % upd_vrid), - (None, 'no vrrp %d ip version' % upd_vrid), - ] - - for (ip_version, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - ip_version=ip_version) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_secondary_ips(self): # vrrp 10 ip 10.10.4.21 secondary # vrrp 10 ip 10.10.4.22 secondary @@ -509,32 +495,6 @@ def test_set_secondary_ips(self): secondary_ips) self.eapi_exception_config_test(func, ValueError) - # Test set_secondary_ips through update - cases = [ - ([new1, new2, new3], - {'add': [new1, new2, new3], - 'remove': [curr1, curr2, curr3, curr4]}), - ([new1, new2, new4], - {'add': [new1, new2], - 'remove': [curr1, curr2, curr3]}), - ([], - {'add': [], - 'remove': [curr1, curr2, curr3, curr4]}), - ] - - for (secondary_ips, cmd_dict) in cases: - cmds = [] - for sec_ip in cmd_dict['add']: - cmds.append("vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) - - for sec_ip in cmd_dict['remove']: - cmds.append("no vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) - - func = function('update', upd_intf, upd_vrid, - secondary_ip=secondary_ips) - exp_cmds = [upd_cmd] + sorted(cmds) - self.eapi_positive_config_test(func, exp_cmds) - def test_set_timers_advertise(self): # vrrp 10 timers advertise 3 @@ -561,20 +521,6 @@ def test_set_timers_advertise(self): value=timers_advertise) self.eapi_exception_config_test(func, ValueError) - # Test set_timers_advertise through update - cases = [ - (255, 'vrrp %d timers advertise 255' % upd_vrid), - ('default', 'default vrrp %d timers advertise' % upd_vrid), - ('no', 'no vrrp %d timers advertise' % upd_vrid), - (None, 'no vrrp %d timers advertise' % upd_vrid), - ] - - for (timers_advertise, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - timers_advertise=timers_advertise) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_mac_addr_adv_interval(self): # vrrp 10 mac-address advertisement-interval 30 @@ -602,21 +548,6 @@ def test_set_mac_addr_adv_interval(self): value=mac_addr_adv_interval) self.eapi_exception_config_test(func, ValueError) - # Test set_mac_addr_adv_interval through update - cases = [ - (3, 'vrrp %d mac-address advertisement-interval 3' % upd_vrid), - ('default', 'default vrrp %d mac-address advertisement-interval' - % upd_vrid), - ('no', 'no vrrp %d mac-address advertisement-interval' % upd_vrid), - (None, 'no vrrp %d mac-address advertisement-interval' % upd_vrid), - ] - - for (set_mac_addr_adv_interval, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - mac_addr_adv_interval=set_mac_addr_adv_interval) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_preempt(self): # vrrp 10 preempt @@ -643,37 +574,6 @@ def test_set_preempt(self): value=preempt) self.eapi_exception_config_test(func, ValueError) - # Test turning off preempt with set_preempt through update - cases = [ - (False, 'no vrrp %d preempt' % upd_vrid), - ('default', 'default vrrp %d preempt' % upd_vrid), - ('no', 'no vrrp %d preempt' % upd_vrid), - ] - - for (preempt, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - preempt=preempt) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - - # Test turning on preept through update - # no vrrp 20 preempt - alt_vrid = 20 - cases = [ - (True, 'vrrp %d preempt' % alt_vrid), - ('default', 'default vrrp %d preempt' % alt_vrid), - ('no', 'no vrrp %d preempt' % alt_vrid), - ] - - for (preempt, cmd) in cases: - func = function('update', upd_intf, alt_vrid, - preempt=preempt) - if cmd is not None: - exp_cmds = [upd_cmd] + [cmd] - else: - exp_cmds = [upd_cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_preempt_delay_min(self): # vrrp 10 preempt delay minimum 0 @@ -703,20 +603,6 @@ def test_set_preempt_delay_min(self): value=preempt_delay_min) self.eapi_exception_config_test(func, ValueError) - # Test set_preempt_delay_min through update - cases = [ - (3, 'vrrp %d preempt delay minimum 3' % upd_vrid), - ('default', 'default vrrp %d preempt delay minimum' % upd_vrid), - ('no', 'no vrrp %d preempt delay minimum' % upd_vrid), - (None, 'no vrrp %d preempt delay minimum' % upd_vrid), - ] - - for (preempt_delay_min, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - preempt_delay_min=preempt_delay_min) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_preempt_delay_reload(self): # vrrp 10 preempt delay reload 0 @@ -746,20 +632,6 @@ def test_set_preempt_delay_reload(self): value=preempt_delay_reload) self.eapi_exception_config_test(func, ValueError) - # Test set_preempt_delay_reload through update - cases = [ - (3, 'vrrp %d preempt delay reload 3' % upd_vrid), - ('default', 'default vrrp %d preempt delay reload' % upd_vrid), - ('no', 'no vrrp %d preempt delay reload' % upd_vrid), - (None, 'no vrrp %d preempt delay reload' % upd_vrid), - ] - - for (preempt_delay_reload, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - preempt_delay_reload=preempt_delay_reload) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_delay_reload(self): # vrrp 10 delay reload 0 @@ -786,20 +658,6 @@ def test_set_delay_reload(self): value=delay_reload) self.eapi_exception_config_test(func, ValueError) - # Test test_set_delay_reload through update - cases = [ - (3, 'vrrp %d delay reload 3' % upd_vrid), - ('default', 'default vrrp %d delay reload' % upd_vrid), - ('no', 'no vrrp %d delay reload' % upd_vrid), - (None, 'no vrrp %d delay reload' % upd_vrid), - ] - - for (delay_reload, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - delay_reload=delay_reload) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - def test_set_tracks(self): # vrrp 10 track Ethernet1 decrement 10 # vrrp 10 track Ethernet1 shutdown @@ -860,49 +718,14 @@ def test_set_tracks(self): # Test raising ValueError by entering invalid parameters cases = [ [{'name': 'Ethernet1', 'action': 'disable', 'amount': 10}], - [{'name': 'Ethernet1', 'action': 'decrement', 'amount': True}] + [{'name': 'Ethernet1', 'action': 'decrement', 'amount': True}], + [{'name': 'Ethernet1', 'action': 'shutdown', 'amount': 10}], ] for tracks in cases: func = function('set_tracks', upd_intf, upd_vrid, tracks) self.eapi_exception_config_test(func, ValueError) - # Test set_tracks through update - cases = [ - ([curr6, curr5, new1, new2], - {'add': [new2], - 'remove': [curr2, curr3, curr4]}), - ([new2, new3, new4, new5, new6], - {'add': [new2, new3, new4, new5, new6], - 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), - ([], - {'add': [], - 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), - ] - - for (tracks, cmd_dict) in cases: - cmds = [] - for add in cmd_dict['add']: - tr_obj = add['name'] - action = add['action'] - amount = add['amount'] if 'amount' in add else '' - cmd = ("vrrp %d track %s %s %s" - % (upd_vrid, tr_obj, action, amount)) - cmds.append(cmd.rstrip()) - - for remove in cmd_dict['remove']: - tr_obj = remove['name'] - action = remove['action'] - amount = remove['amount'] if 'amount' in remove else '' - cmd = ("no vrrp %d track %s %s %s" - % (upd_vrid, tr_obj, action, amount)) - cmds.append(cmd.rstrip()) - - func = function('update', upd_intf, upd_vrid, - track=tracks) - exp_cmds = [upd_cmd] + sorted(cmds) - self.eapi_positive_config_test(func, exp_cmds) - def test_set_bfd_ip(self): # no vrrp 10 bfd ip @@ -930,28 +753,6 @@ def test_set_bfd_ip(self): value=bfd_ip) self.eapi_exception_config_test(func, ValueError) - # Test set_bfd_ip through update - cases = [ - (bfd_addr, 'vrrp %d bfd ip %s' % (upd_vrid, bfd_addr)), - ('default', 'default vrrp %d bfd ip' % upd_vrid), - ('no', 'no vrrp %d bfd ip' % upd_vrid), - (None, 'no vrrp %d bfd ip' % upd_vrid), - ] - - for (bfd_ip, cmd) in cases: - func = function('update', upd_intf, upd_vrid, - bfd_ip=bfd_ip) - exp_cmds = [upd_cmd] + [cmd] - self.eapi_positive_config_test(func, exp_cmds) - - def test_update_invalid_vrid(self): - # Test raising ValueError by updating a non-existent vrrp - non_vrid = 1000 - params = {'priority': 100, 'enable': True} - - func = function('update', upd_intf, non_vrid, **params) - self.eapi_exception_config_test(func, ValueError) - def test_vrconf_format(self): # Test the function to format a vrrp configuration to # match the output from get/getall From 68eee06b95d9084e1239748605bf40cf74d8a5fa Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 2 Nov 2015 12:48:32 -0700 Subject: [PATCH 79/83] documentation --- pyeapi/api/vrrp.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py index 9d35faa..e9dcc07 100644 --- a/pyeapi/api/vrrp.py +++ b/pyeapi/api/vrrp.py @@ -116,7 +116,7 @@ bfd_ip: } - The create and update methods accept a kwargs dictionary which + The create and method accepts a kwargs dictionary which defines the properties to be applied to the new or existing vrrp configuration. The available keywords and values are as follows: enable: True to enable (no shutdown)|False to disable (shutdown) @@ -416,7 +416,7 @@ def set_enable(self, name, vrid, value=False, run=True): Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (boolean): True to enable the vrrp, False to disable. run (boolean): True to execute the command, False to return a string with the formatted command. @@ -454,7 +454,7 @@ def set_primary_ip(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (string): IP address to be set. disable (boolean): Unset primary ip if True. default (boolean): Set primary ip to default if True. @@ -500,7 +500,7 @@ def set_priority(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): Priority to assign to the vrrp. disable (boolean): Unset priority if True. default (boolean): Set priority to default if True. @@ -542,7 +542,7 @@ def set_description(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (string): Description to assign to the vrrp. disable (boolean): Unset description if True. default (boolean): Set description to default if True. @@ -581,7 +581,7 @@ def set_ip_version(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): IP version to assign to the vrrp. disable (boolean): Unset ip_version if True. default (boolean): Set ip_version to default if True. @@ -629,7 +629,7 @@ def set_secondary_ips(self, name, vrid, secondary_ips, run=True): Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. secondary_ips (list): A list of secondary ip addresses to be assigned to the virtual router. run (boolean): Set to True to execute the command, False to @@ -691,7 +691,7 @@ def set_timers_advertise(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): Timers advertise value to assign to the vrrp. disable (boolean): Unset timers advertise if True. default (boolean): Set timers advertise to default if True. @@ -733,7 +733,7 @@ def set_mac_addr_adv_interval(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): mac-address advertisement-interval value to assign to the vrrp. disable (boolean): Unset mac-address advertisement-interval @@ -779,7 +779,7 @@ def set_preempt(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (boolean): True to enable preempt, False to disable preempt on the vrrp. disable (boolean): Unset preempt if True. @@ -821,7 +821,7 @@ def set_preempt_delay_min(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay minimum value to set on the vrrp. disable (boolean): Unset preempt delay minimum if True. default (boolean): Set preempt delay minimum to default if True. @@ -863,7 +863,7 @@ def set_preempt_delay_reload(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay reload value to set on the vrrp. disable (boolean): Unset preempt delay reload if True. default (boolean): Set preempt delay reload to default if True. @@ -905,7 +905,7 @@ def set_delay_reload(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay reload value to set on the vrrp. disable (boolean): Unset preempt delay reload if True. default (boolean): Set preempt delay reload to default if True. @@ -954,7 +954,7 @@ def set_tracks(self, name, vrid, tracks, run=True): Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. tracks (list): A list of track definition dictionaries. Each dictionary is a definition of a tracked object in one of the two formats: @@ -1078,7 +1078,7 @@ def set_bfd_ip(self, name, vrid, value=None, disable=False, Args: name (string): The interface to configure. - vrid (integer): The vrid number for the vrrp to be updated. + vrid (integer): The vrid number for the vrrp to be managed. value (string): The bfd ip address to be set. disable (boolean): Unset bfd ip if True. default (boolean): Set bfd ip to default if True. @@ -1307,8 +1307,8 @@ def _vrrp_set(self, name, vrid, **kwargs): def vrconf_format(self, vrconfig): """Formats a vrrp configuration dictionary to match the information as presented from the get and getall methods. - vrrp configuration dictionaries passed to the create and update - methods may contain data for setting properties which results + vrrp configuration dictionaries passed to the create + method may contain data for setting properties which results in a default value on the node. In these instances, the data for setting or changing the property is replaced with the value that would be returned from the get and getall methods. From eda5673ba92ca6ec1bb7e3f7478fc32b273a8db2 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 2 Nov 2015 17:34:22 -0500 Subject: [PATCH 80/83] Add new docs - Support, Contribute - Add to Install - Add badges to README - Remove most redundant content from README --- README.md | 219 +++++--------------------------------------- docs/contribute.rst | 43 +++++++++ docs/index.rst | 2 + docs/install.rst | 23 ++++- docs/support.rst | 33 +++++++ 5 files changed, 123 insertions(+), 197 deletions(-) create mode 100644 docs/contribute.rst create mode 100644 docs/support.rst diff --git a/README.md b/README.md index c7de1d2..a2ace67 100644 --- a/README.md +++ b/README.md @@ -1,220 +1,42 @@ # Arista eAPI Python Library -[![Build Status](https://travis-ci.org/arista-eosplus/pyeapi.svg?branch=develop)](https://travis-ci.org/arista-eosplus/pyeapi) [![Coverage Status](https://coveralls.io/repos/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/r/arista-eosplus/pyeapi?branch=develop) +[![Build Status](https://travis-ci.org/arista-eosplus/pyeapi.svg?branch=develop)](https://travis-ci.org/arista-eosplus/pyeapi) [![Coverage Status](https://coveralls.io/repos/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/r/arista-eosplus/pyeapi?branch=develop) [![Documentation Status](https://readthedocs.org/projects/pyeapi/badge/?version=latest)](http://readthedocs.org/docs/pyeapi/en/latest/?badge=latest) The Python library for Arista's eAPI command API implementation provides a client API work using eAPI and communicating with EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or -more nodes and connection properites. +more nodes and connection properties. The pyeapi library also provides an API layer for building native Python -objects to interact with the destination nodes. The API layer is a convienent +objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for -developing custom implemenations. +developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. -## Requirements - -* Arista EOS 4.12 or later -* Arista eAPI enabled for at least one transport (see Official EOS Config Guide - at arista.com for details) -* Python 2.7 / 3.4+ (Python 3 support is work in progress) - -# Getting Started -In order to use pyeapi, the EOS command API must be enabled using ``management -api http-commands`` configuration mode. This library supports eAPI calls over -both HTTP and UNIX Domain Sockets. Once the command API is enabled on the -destination node, create a configuration file with the node properities. - -**Note:** The default search path for the conf file is ``~/.eapi.conf`` -followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting -``EAPI_CONF=`` in your environment. - -## Example eapi.conf File -Below is an example of an eAPI conf file. The conf file can contain more than -one node. Each node section must be prefaced by **connection:\** where -\ is the name of the connection. - -The following configuration options are available for defining node entries: - -* **host** - The IP address or FQDN of the remote device. If the host - parameter is omitted then the connection name is used -* **username** - The eAPI username to use for authentication (only required for - http or https connections) -* **password** - The eAPI password to use for authentication (only required for - http or https connections) -* **enablepwd** - The enable mode password if required by the destination node -* **transport** - Configures the type of transport connection to use. The - default value is _https_. Valid values are: - * socket (available in EOS 4.14.5 or later) - * http_local (available in EOS 4.14.5 or later) - * http - * https -* **port** - Configures the port to use for the eAPI connection. A default - port is used if this parameter is absent, based on the transport setting -using the following values: - * transport: http, default port: 80 - * transport: https, deafult port: 443 - * transport: https_local, default port: 8080 - * transport: socket, default port: n/a - - -_Note:_ See the EOS User Manual found at arista.com for more details on -configuring eAPI values. - -All configuration values are optional. - -``` -[connection:veos01] -username: eapi -password: password -transport: http - -[connection:veos02] -transport: http - -[connection:veos03] -transport: socket - -[connection:veos04] -host: 172.16.10.1 -username: eapi -password: password -enablepwd: itsasecret -port: 1234 -transport: https - -[connection:localhost] -transport: http_local -``` - -The above example shows different ways to define EOS node connections. All -configuration options will attempt to use default values if not explicitly -defined. If the host parameter is not set for a given entry, then the -connection name will be used as the host address. - -### Configuring \[connection:localhost] - -The pyeapi library automatically installs a single default configuration entry -for connecting to localhost host using a transport of sockets. If using the -pyeapi library locally on an EOS node, simply enable the command API to use -sockets and no further configuration is needed for pyeapi to function. If you -specify an entry in a conf file with the name ``[connection:localhost]``, the -values in the conf file will overwrite the default. - -## Using pyeapi -The Python client for eAPI was designed to be easy to use and implement for -writing tools and applications that interface with the Arista EOS management -plane. - -### Creating a connection and sending commands -Once EOS is configured properly and the config file created, getting started -with a connection to EOS is simple. Below demonstrates a basic connection -using pyeapi. For more examples, please see the examples folder. - -``` -# start by importing the library -import pyeapi - -# create a node object by specifying the node to work with -node = pyeapi.connect_to('veos01') - -# send one or more commands to the node -node.enable('show hostname') -[{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': -u'veos01.arista.com'}, 'encoding': 'json'}] - -# use the config method to send configuration commands -node.config('hostname veos01') -[{}] - -# multiple commands can be sent by using a list (works for both enable or -config) -node.config(['interface Ethernet1', 'description foo']) -[{}, {}] - -# return the running or startup configuration from the node (output omitted for -brevity) -node.running_config - -node.startup_config -``` - -### Using the API - -The pyeapi library provides both a client for send and receiving commands over -eAPI as well as an API for working directly with EOS resources. The API is -designed to be easy and straightforward to use yet also extensible. Below is -an example of working with the ``vlans`` API - -``` -# create a connection to the node -import pyeapi -node = pyeapi.connect_to('veos01') - -# get the instance of the API (in this case vlans) -vlans = node.api('vlans') +## Documentation -# return all vlans from the node -vlans.getall() -{'1': {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []}, -'10': {'state': 'active', 'name': 'VLAN0010', 'vlan_id': 10, 'trunk_groups': -[]}} - -# return a specific vlan from the node -vlans.get(1) -{'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} - -# add a new vlan to the node -vlans.create(100) -True - -# set the new vlan name -vlans.set_name(100, 'foo') -True -``` - -All API implementations developed by Arista EOS+ CS are found in the pyeapi/api -folder. See the examples folder for additional examples. - -# Installation - -The source code for pyeapi is provided on Github at -http://github.com/arista-eosplus/pyeapi. All current development is done in -the develop branch. Stable released versions are tagged in the master branch -and uploaded to PyPi. +* [Quickstart] [quickstart] +* [Installation] [install] +* [Modules] [modules] +* [Contribute] [contribute] -* To install the latest stable version of pyeapi, simply run ``pip install - pyeapi`` (or ``pip install --upgrade pyeapi``) -* To install the latest development version from Github, simply clone the - develop branch and run ``python setup.py install`` - -# Testing -The pyeapi library provides both unit tests and system tests. The unit tests -can be run without an EOS node. To run the system tests, you will need to -update the ``dut.conf`` file found in test/fixtures. +### Building Local Documentation -* To run the unit tests, simply run ``make unittest`` from the root of the - pyeapi source folder -* To run the system tests, simply run ``make systest`` from the root of the - pyeapi source fodler -* To run all tests, use ``make tests`` from the root of the pyeapi source - folder +If you cannot access readthedocs.org you have the option of building the +documentation locally. - -# Contributing - -Contributing pull requests are gladly welcomed for this repository. Please -note that all contributions that modify the library behavior require -corresponding test cases otherwise the pull request will be rejected. +1. ``pip install -r dev-requirements.txt`` +2. ``cd docs`` +3. ``make html`` +4. ``open _build/html/index.html`` # License -Copyright (c) 2014, Arista Networks EOS+ +Copyright (c) 2015, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -242,3 +64,10 @@ 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. + +[pyeapi]: https://github.com/arista-eosplus/pyeapi +[quickstart]: http://pyeapi.readthedocs.org/en/master/quickstart.html +[install]: http://pyeapi.readthedocs.org/en/master/install.html +[contribute]: http://pyeapi.readthedocs.org/en/master/contribute.html +[modules]: http://pyeapi.readthedocs.org/en/master/modules.html +[support]: http://pyeapi.readthedocs.org/en/master/support.html diff --git a/docs/contribute.rst b/docs/contribute.rst new file mode 100644 index 0000000..b94866f --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,43 @@ + +########## +Contribute +########## + +The Arista EOS+ team is happy to accept community contributions to the Pyeapi +project. Please note that all contributions that modify the library behavior +require corresponding test cases otherwise the pull request will be rejected. + + +******* +Testing +******* + +The pyeapi library provides both unit tests and system tests. The unit tests +can be run without an EOS node. To run the system tests, you will need to +update the ``dut.conf`` file found in test/fixtures. + + +Unit Test +========= + +To run the unit tests, simply run ``make unittest`` from the root of the +pyeapi source folder + +System Test +=========== + +To run the system tests, simply run ``make systest`` from the root of the +pyeapi source folder. + +.. Tip:: To run all tests, use ``make tests`` from the root of the pyeapi source + folder + +******** +Coverage +******** + +Contributions should maintain 100% code coverage. You can check this locally +before submitting your Pull Request. + +1. Run ``make unittest`` +2. Run ``make coverage_report`` to confirm code coverage. diff --git a/docs/index.rst b/docs/index.rst index 4e379c4..d626402 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,8 @@ through Github issues. configfile modules requirements + contribute + support license diff --git a/docs/install.rst b/docs/install.rst index 3e0e954..f40a867 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,7 +9,7 @@ The installation of pyeapi is straightforward and simple. As mentioned in the will provide some tips to help you for either platform. .. contents:: - :depth: 2 + :depth: 3 *********************** Pip with Network Access @@ -22,7 +22,15 @@ to install pyeapi admin:~ admin$ sudo pip install pyeapi -.. Note:: You will likely notice Pip install netaddr, a depency of pyeapi. +.. Note:: You will likely notice Pip install netaddr, a dependency of pyeapi. + + +Pip - Upgrade Pyeapi +==================== + +.. code-block:: console + + admin:~ admin$ sudo pip install --upgrade pyeapi ************************** @@ -115,3 +123,14 @@ code in the develop branch. .. Tip:: If you start using pyeapi and get import errors, make sure your PYTHONPATH is set to include the path to pyeapi. + + Development - Upgrade Pyeapi + ============================ + + .. code-block:: console + + admin:~ admin$ cd ~/projects/pyeapi + admin:~ admin$ git pull + +.. Note:: If you followed the directions above and used ``pip install -e``, + pip will automatically use the updated code. diff --git a/docs/support.rst b/docs/support.rst new file mode 100644 index 0000000..282f366 --- /dev/null +++ b/docs/support.rst @@ -0,0 +1,33 @@ +####### +Support +####### + +******* +Contact +******* + +Pyeapi is developed by Arista EOS+ CS and supported by the Arista +EOS+ community. Support for the code is provided on a best effort basis by the +Arista EOS+ CS team and the community. You can contact the team that develops +these modules by sending an email to eosplus-dev@arista.com. + +For customers that are looking for a premium level of support, please contact +your local account team or email eosplus@arista.com for help. + +***************** +Submitting Issues +***************** + +The Arista EOS+ CS development team uses Github Issues to track discovered +bugs and enhancement requests. The issues tracker can +be found at https://github.com/arista-eosplus/pyeapi/issues. + +For defect issues, please provide as much relevant data as possible as to what +is causing the issue, if and how it is reproducible, the version of EOS, python, +and any OS details. + +For enhancement requests, please provide a brief description of the +enhancement request and the version of EOS to be supported. + +The issue tracker is monitored by Arista EOS+ CS and issues submitted are +categorized and scheduled for inclusion in upcoming Pyeapi versions. From 59a69e4ea4fdfd9e676cadd2e0523a763abab3a4 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 2 Nov 2015 18:27:37 -0500 Subject: [PATCH 81/83] Add Release notes - past and present release - Link to RTD for CHANGELOG --- CHANGELOG.md | 63 ++---------------------------------- docs/index.rst | 1 + docs/release-notes-0.1.0.rst | 12 +++++++ docs/release-notes-0.1.1.rst | 12 +++++++ docs/release-notes-0.2.0.rst | 13 ++++++++ docs/release-notes-0.2.1.rst | 7 ++++ docs/release-notes-0.2.2.rst | 7 ++++ docs/release-notes-0.2.3.rst | 7 ++++ docs/release-notes-0.2.4.rst | 7 ++++ docs/release-notes-0.3.0.rst | 13 ++++++++ docs/release-notes-0.3.1.rst | 9 ++++++ docs/release-notes-0.3.2.rst | 7 ++++ docs/release-notes-0.3.3.rst | 9 ++++++ docs/release-notes-0.4.0.rst | 53 ++++++++++++++++++++++++++++++ docs/release-notes.rst | 20 ++++++++++++ 15 files changed, 179 insertions(+), 61 deletions(-) create mode 100644 docs/release-notes-0.1.0.rst create mode 100644 docs/release-notes-0.1.1.rst create mode 100644 docs/release-notes-0.2.0.rst create mode 100644 docs/release-notes-0.2.1.rst create mode 100644 docs/release-notes-0.2.2.rst create mode 100644 docs/release-notes-0.2.3.rst create mode 100644 docs/release-notes-0.2.4.rst create mode 100644 docs/release-notes-0.3.0.rst create mode 100644 docs/release-notes-0.3.1.rst create mode 100644 docs/release-notes-0.3.2.rst create mode 100644 docs/release-notes-0.3.3.rst create mode 100644 docs/release-notes-0.4.0.rst create mode 100644 docs/release-notes.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f73b61..84fcc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,66 +1,7 @@ Python Client for eAPI ====================== -## v0.3.2, 7/16/2015 +Full [release notes] [rns] hosted at readthedocs -- fixes a problem with parsing the hostname value in the system module -## v0.3.1, 6/14/2015 - -- make pyeapi compatible under Python 3.4 with all unit tests passing ok -- added socket_error property to connection to capture socket errors -- adds function to create per vlan vtep flood lists - -## v0.3.0, 5/4/2015 - -- fixes an issue with configuring stp portfast edge correctly -- fixes #13 -- fixes #11 -- added initial support for system api module -- added initial support for acl api module (standard) -- added initial api support for mlag configuration -- added tag feature to eapi.conf - -## v0.2.4, 4/30/2015 - -- adds required docs/description.rst for setup.py - -## v0.2.3, 4/29/2015 - -- fixes issue with importing syslog module on Windows - -## v0.2.2, 04/15/2015 - -- fixes an issue with eAPI error messages that do not return a data key - -## v0.2.1, 03/28/2015 - -- restores default certificate validation behavior for py2.7.9 - -## v0.2.0, 3/19/2015 - -- adds udp_port, vlans and flood_list attributes to vxlan interfaces -- renames spanningtree api module to stp for consistency -- depreciated spanningtree api module in favor of stp -- interfaces module now properly responds to hasattr calls -- fixes an issue with collecting the vxlan global flood list from the config -- fixes an issue with properly parsing portchannel configurations -- adds portfast_type attribute to stp interfaces resource - -## v0.1.1, 2/17/2015 - -- adds introspection properties to CommandError for more details (#4) -- changed the default transport from HTTP to HTTPS to align with EOS -- updates the message returned if the connection profile name is not found -- fixes connection name not copied to host parameter if host not configured -- fixes an issue where an ipinterface wasnt properly recognized -- fixes an issue where a switchport interface was propertly recognized - -## v0.1.0, 1/23/2015 - -- initial public release of pyeapi -- initial support for vlans -- initial support for interfaces -- initial support for spanningtree -- initial support for switchports -- initial support for ipinterfaces +[rns]: http://pyeapi.readthedocs.org/en/master/release-notes.html diff --git a/docs/index.rst b/docs/index.rst index d626402..f2aeb5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ through Github issues. modules requirements contribute + release-notes support license diff --git a/docs/release-notes-0.1.0.rst b/docs/release-notes-0.1.0.rst new file mode 100644 index 0000000..1dfb56f --- /dev/null +++ b/docs/release-notes-0.1.0.rst @@ -0,0 +1,12 @@ +###### +v0.1.0 +###### + +2015-01-23 + +- initial public release of pyeapi +- initial support for vlans +- initial support for interfaces +- initial support for spanningtree +- initial support for switchports +- initial support for ipinterfaces diff --git a/docs/release-notes-0.1.1.rst b/docs/release-notes-0.1.1.rst new file mode 100644 index 0000000..bd50d6c --- /dev/null +++ b/docs/release-notes-0.1.1.rst @@ -0,0 +1,12 @@ +###### +v0.1.1 +###### + +2015-02-17 + +- adds introspection properties to CommandError for more details (#4) +- changed the default transport from HTTP to HTTPS to align with EOS +- updates the message returned if the connection profile name is not found +- fixes connection name not copied to host parameter if host not configured +- fixes an issue where an ipinterface wasnt properly recognized +- fixes an issue where a switchport interface was propertly recognized diff --git a/docs/release-notes-0.2.0.rst b/docs/release-notes-0.2.0.rst new file mode 100644 index 0000000..0ca95fd --- /dev/null +++ b/docs/release-notes-0.2.0.rst @@ -0,0 +1,13 @@ +###### +v0.2.0 +###### + +2015-03-19 + +- adds udp_port, vlans and flood_list attributes to vxlan interfaces +- renames spanningtree api module to stp for consistency +- depreciated spanningtree api module in favor of stp +- interfaces module now properly responds to hasattr calls +- fixes an issue with collecting the vxlan global flood list from the config +- fixes an issue with properly parsing portchannel configurations +- adds portfast_type attribute to stp interfaces resource diff --git a/docs/release-notes-0.2.1.rst b/docs/release-notes-0.2.1.rst new file mode 100644 index 0000000..8f4a340 --- /dev/null +++ b/docs/release-notes-0.2.1.rst @@ -0,0 +1,7 @@ +###### +v0.2.1 +###### + +2015-03-28 + +- restores default certificate validation behavior for py2.7.9 diff --git a/docs/release-notes-0.2.2.rst b/docs/release-notes-0.2.2.rst new file mode 100644 index 0000000..b23a08c --- /dev/null +++ b/docs/release-notes-0.2.2.rst @@ -0,0 +1,7 @@ +###### +v0.2.2 +###### + +2015-04-15 + +- fixes an issue with eAPI error messages that do not return a data key diff --git a/docs/release-notes-0.2.3.rst b/docs/release-notes-0.2.3.rst new file mode 100644 index 0000000..565b5a3 --- /dev/null +++ b/docs/release-notes-0.2.3.rst @@ -0,0 +1,7 @@ +###### +v0.2.3 +###### + +2015-04-29 + +- fixes issue with importing syslog module on Windows diff --git a/docs/release-notes-0.2.4.rst b/docs/release-notes-0.2.4.rst new file mode 100644 index 0000000..efd8ec5 --- /dev/null +++ b/docs/release-notes-0.2.4.rst @@ -0,0 +1,7 @@ +###### +v0.2.4 +###### + +2015-04-30 + +- adds required docs/description.rst for setup.py diff --git a/docs/release-notes-0.3.0.rst b/docs/release-notes-0.3.0.rst new file mode 100644 index 0000000..03d74aa --- /dev/null +++ b/docs/release-notes-0.3.0.rst @@ -0,0 +1,13 @@ +###### +v0.3.0 +###### + +2015-05-04 + +- fixes an issue with configuring stp portfast edge correctly +- fixes #13 +- fixes #11 +- added initial support for system api module +- added initial support for acl api module (standard) +- added initial api support for mlag configuration +- added tag feature to eapi.conf diff --git a/docs/release-notes-0.3.1.rst b/docs/release-notes-0.3.1.rst new file mode 100644 index 0000000..8101cbc --- /dev/null +++ b/docs/release-notes-0.3.1.rst @@ -0,0 +1,9 @@ +###### +v0.3.1 +###### + +2015-06-14 + +- make pyeapi compatible under Python 3.4 with all unit tests passing ok +- added socket_error property to connection to capture socket errors +- adds function to create per vlan vtep flood lists diff --git a/docs/release-notes-0.3.2.rst b/docs/release-notes-0.3.2.rst new file mode 100644 index 0000000..621d255 --- /dev/null +++ b/docs/release-notes-0.3.2.rst @@ -0,0 +1,7 @@ +###### +v0.3.2 +###### + +2015-07-16 + +- fixes a problem with parsing the hostname value in the system module diff --git a/docs/release-notes-0.3.3.rst b/docs/release-notes-0.3.3.rst new file mode 100644 index 0000000..93d034c --- /dev/null +++ b/docs/release-notes-0.3.3.rst @@ -0,0 +1,9 @@ +###### +v0.3.3 +###### + +2015-07-31 + +- added initial support for bgp api module +- add trunk group functionality to switchports +- add ip routing to system api diff --git a/docs/release-notes-0.4.0.rst b/docs/release-notes-0.4.0.rst new file mode 100644 index 0000000..68e357c --- /dev/null +++ b/docs/release-notes-0.4.0.rst @@ -0,0 +1,53 @@ +###### +v0.4.0 +###### + +2015-11-03 + +New APIs +^^^^^^^^ + +* Add support for vrrp api (`57 `_) [`grybak `_] + .. comment +* Add support for staticroute api (`45 `_) [`grybak `_] + The staticroute API enables you to set static IPv4 routes on your EOS device. +* Add support for varp api (`43 `_) [`phil-arista `_] + The Varp API includes the subclass VarpInterfaces. These two combine to provide methods to set virtual IP addresses on interfaces as well as set the global virtual-router mac-address. +* Add support for routemap api (`40 `_) [`phil-arista `_] + .. comment + +Enhancements +^^^^^^^^^^^^ + +* Making configure RADIUS compatible (`53 `_) [`GaryCarneiro `_] + Modifies the syntax of the ``config`` method to use ``configure terminal`` instead of just ``configure``. +* Add lacp mode to set_members() method (`47 `_) [`phil-arista `_] + This enhancement allows you to set the LACP Mode while executing the set_members method. The call would look like ``node.api('interfaces').set_members(1, [Ethernet1,Ethernet2], mode='active')`` +* Added support to specify timeout (`41 `_) [`dbarrosop `_] + This enhancement provides a way to specify a connection timeout. The default is set to 60 seconds. +* Add BGP maximum-paths support (`36 `_) [`phil-arista `_] + This enhancement adds more attributes to ``eos_bgp_config``. This provides the ability to configure ``maximum-paths N ecmp M`` in your ``router bgp R`` configuration. +* Add sshkey support to users API (`34 `_) [`phil-arista `_] + This enhancement augments the ``users`` API to now support SSH Keys. + +Fixed +^^^^^ + +* client.py 'def enable' returned dictionary key inconsistency (`35 `_) + The key that's supposed to be returned is ``result`` but instead the method formerly returned the key ``response``. For now, both ``response`` and ``result`` will be returned with the same data, but ``response`` will be removed in a future release. +* [API Users] Can't run set_role with no value (`33 `_) + The node.api('users').set_role('test') method didn't remove the role or default the role as you would expect. This bug fix resolves that. +* [API Users] Can't run set_privilege with no value (`32 `_) + The set_privilege('user') method did not properly negate the privilege level when no argument was passed into the method. +* [ API interfaces ] get_members regex wrongly includes PeerEthernet when lag is up (`28 `_) + The get_members() method wrongly included a duplicate member when the ``show port-channel N all-ports`` showed the PeerEthernetX. The regular expression has been updated to ignore these entries. +* [API] users - can't create password with non-alpha/int characters (`23 `_) + The characters ``(){}[]`` cannot be part of a username. Documentation has been updated to reflect this. +* Users with sha512 passwords don't get processed correctly using api('users').getall() (`22 `_) + Fixed regex to extract the encrypted passwords accurately. + +Known Caveats +^^^^^^^^^^^^^ + +* failure when eapi.conf is not formatted correctly (`38 `_) + .. comment diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..a4591ec --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,20 @@ +############# +Release Notes +############# + +.. toctree:: + :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 From 3db0b087951ffcfb91c9805574643e843188218c Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Mon, 2 Nov 2015 18:32:09 -0500 Subject: [PATCH 82/83] Update release notes link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a2ace67..7d1e831 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ through Github issues. * [Quickstart] [quickstart] * [Installation] [install] * [Modules] [modules] +* [Release Notes] [rns] * [Contribute] [contribute] ### Building Local Documentation @@ -71,3 +72,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [contribute]: http://pyeapi.readthedocs.org/en/master/contribute.html [modules]: http://pyeapi.readthedocs.org/en/master/modules.html [support]: http://pyeapi.readthedocs.org/en/master/support.html +[rns]: http://pyeapi.readthedocs.org/en/master/release-notes.html From 66d91f03fc85fa1388610a391d682b2a6cbaa9bc Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 5 Nov 2015 11:44:17 -0800 Subject: [PATCH 83/83] Bump to version 0.4.0 --- VERSION | 2 +- docs/release-notes-0.4.0.rst | 14 +++++++------- pyeapi/__init__.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 6563189..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +0.4.0 diff --git a/docs/release-notes-0.4.0.rst b/docs/release-notes-0.4.0.rst index 68e357c..40b3475 100644 --- a/docs/release-notes-0.4.0.rst +++ b/docs/release-notes-0.4.0.rst @@ -2,18 +2,18 @@ v0.4.0 ###### -2015-11-03 +2015-11-05 New APIs ^^^^^^^^ -* Add support for vrrp api (`57 `_) [`grybak `_] - .. comment -* Add support for staticroute api (`45 `_) [`grybak `_] +* Add VRRP (`57 `_) [`grybak `_] + Add support for VRRP configuration. +* Add Staticroute (`45 `_) [`grybak `_] The staticroute API enables you to set static IPv4 routes on your EOS device. -* Add support for varp api (`43 `_) [`phil-arista `_] +* Add VARP (`43 `_) [`phil-arista `_] The Varp API includes the subclass VarpInterfaces. These two combine to provide methods to set virtual IP addresses on interfaces as well as set the global virtual-router mac-address. -* Add support for routemap api (`40 `_) [`phil-arista `_] +* Add Routemap (`40 `_) [`phil-arista `_] .. comment Enhancements @@ -21,7 +21,7 @@ Enhancements * Making configure RADIUS compatible (`53 `_) [`GaryCarneiro `_] Modifies the syntax of the ``config`` method to use ``configure terminal`` instead of just ``configure``. -* Add lacp mode to set_members() method (`47 `_) [`phil-arista `_] +* Close #46 (`47 `_) [`phil-arista `_] This enhancement allows you to set the LACP Mode while executing the set_members method. The call would look like ``node.api('interfaces').set_members(1, [Ethernet1,Ethernet2], mode='active')`` * Added support to specify timeout (`41 `_) [`dbarrosop `_] This enhancement provides a way to specify a connection timeout. The default is set to 60 seconds. diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index e646e7e..4863f17 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.4.0' __author__ = 'Arista EOS+'