From ac3361a6788eb39bd1148ca43d15def711cfed08 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Thu, 8 Sep 2016 15:24:56 -0700 Subject: [PATCH 01/44] revert version back to develop --- VERSION | 2 +- pyeapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index faef31a..6563189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +develop diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index 2d0d25f..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.7.0' +__version__ = 'develop' __author__ = 'Arista EOS+' From 15543774e992733c490e674aa705c4d3304b3b52 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Fri, 9 Sep 2016 08:28:58 -0700 Subject: [PATCH 02/44] Update coveralls badge url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d1e831..21d6c46 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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) [![Documentation Status](https://readthedocs.org/projects/pyeapi/badge/?version=latest)](http://readthedocs.org/docs/pyeapi/en/latest/?badge=latest) +[![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/github/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/github/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 From cc621e2c0687ceaad9e498f271947790c4b334a3 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Sat, 10 Sep 2016 22:39:46 -0400 Subject: [PATCH 03/44] update the regex in _parse_multicast and add support for multicast-group decap --- pyeapi/api/interfaces.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 4b5ebc3..4f89542 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -710,6 +710,8 @@ def get(self, name): * udp_port (int): The vxlan udp-port value * vlans (dict): The vlan to vni mappings * flood_list (list): The list of global VTEP flood list + * multicast_decap (bool): If the mutlicast decap + feature is configured Args: name (str): The interface identifier to retrieve from the @@ -732,6 +734,7 @@ def get(self, name): response.update(self._parse_udp_port(config)) response.update(self._parse_vlans(config)) response.update(self._parse_flood_list(config)) + response.update(self._parse_multicast_decap(config)) return response @@ -753,9 +756,14 @@ def _parse_source_interface(self, config): return dict(source_interface=value) def _parse_multicast_group(self, config): - match = re.search(r'vxlan multicast-group ([^\s]+)', config) + match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+.[\d]+.[\d]+)', + config) value = match.group(1) if match else self.DEFAULT_MCAST_GRP return dict(multicast_group=value) + + def _parse_multicast_decap(self, config): + value = 'vxlan mutlicast-group decap' in config + return dict(multicast_decap=bool(value)) def _parse_udp_port(self, config): match = re.search(r'vxlan udp-port (\d+)', config) @@ -831,6 +839,31 @@ def set_multicast_group(self, name, value=None, default=False, disable=disable) return self.configure_interface(name, cmds) + def set_multicast_decap(self, name, default=False, + disable=False): + """Configures the Vxlan multicast-group decap feature + + EosVersion: + 4.15.0M + + Args: + name(str): The interface identifier to configure, defaults to + Vxlan1 + default(bool): Configures the mulitcast-group decap value to default + disable(bool): Negates the multicast-group decap value + + Returns: + True if the operation succeeds otherwise False + """ + string = 'vxlan multicast-group decap' + if(default or disable): + cmds = self.command_builder(string, value=None, default=default, + disable=disable) + else: + cmds = [string] + return self.configure_interface(name, cmds) + + def set_udp_port(self, name, value=None, default=False, disable=False): """Configures vxlan udp-port value From 4d82858005286f3d7054df2a59860daf7ce404cd Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Sat, 10 Sep 2016 22:40:39 -0400 Subject: [PATCH 04/44] test for the new _parse_multicast regex --- test/unit/test_api_interfaces.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 06a78a3..86a1e4a 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -357,7 +357,8 @@ def __init__(self, *args, **kwargs): def test_get(self): keys = ['name', 'type', 'description', 'shutdown', 'source_interface', - 'multicast_group', 'udp_port', 'vlans', 'flood_list'] + 'multicast_group', 'udp_port', 'vlans', 'flood_list', + 'multicast_decap'] result = self.instance.get('Vxlan1') self.assertEqual(sorted(keys), sorted(result.keys())) @@ -391,6 +392,23 @@ def test_set_multicast_group_with_default(self): func = function('set_multicast_group', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) + def test_set_multicast_decap(self): + cmds = ['interface Vxlan1', 'vxlan multicast-group decap'] + func = function('set_multicast_decap', 'Vxlan1') + self.eapi_positive_config_test(func, cmds) + + def test_set_multicast_decap_with_no_value(self): + cmds = ['interface Vxlan1', 'no vxlan multicast-group decap'] + func = function('set_multicast_decap', 'Vxlan1', disable=True) + self.eapi_positive_config_test(func, cmds) + + def test_set_multicast_decap_with_default(self): + cmds = ['interface Vxlan1', 'default vxlan multicast-group decap'] + func = function('set_multicast_decap', 'Vxlan1', default=True) + self.eapi_positive_config_test(func, cmds) + + + def test_set_udp_port_with_value(self): cmds = ['interface Vxlan1', 'vxlan udp-port 1024'] func = function('set_udp_port', 'Vxlan1', '1024') From 7669fc42739bac703bbf9df2cf3382d7f04494a5 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Sat, 10 Sep 2016 22:40:57 -0400 Subject: [PATCH 05/44] test for the new _parse_multicast regex --- test/system/test_api_interfaces.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index adefd2e..00e10bf 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -341,6 +341,8 @@ def test_get(self): self.assertEqual(result['description'], None) self.assertEqual(result['source_interface'], '') self.assertEqual(result['multicast_group'], '') + self.assertEqual(result['multicast_decap'], False) + def get_config(self, dut): cmd = 'show running-config all interfaces Vxlan1' @@ -405,6 +407,16 @@ def test_set_multicast_group_negate(self): self.assertTrue(instance) self.contains('no vxlan multicast-group', dut) + '''commenting this one out as it will only parse on a trident based DUT + def test_set_multicast_decap(self): + for dut in self.duts: + dut.config(['no interface Vxlan1', 'interface Vxlan1']) + api = dut.api('interfaces') + instance = api.set_multicast_decap('Vxlan1') + self.assertTrue(instance) + self.contains('vxlan multicast-group decap', dut) + ''' + def test_set_udp_port(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', From a18861922ce382d8460bfb535549a3efe351ba0e Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Mon, 12 Sep 2016 08:30:26 -0400 Subject: [PATCH 06/44] polish up the regex and add in unittest --- pyeapi/api/interfaces.py | 2 +- test/fixtures/running_config.vxlan | 1 + test/fixtures/vxlan.json | 1 + test/unit/test_api_interfaces.py | 2 -- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 4f89542..de1536c 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -756,7 +756,7 @@ def _parse_source_interface(self, config): return dict(source_interface=value) def _parse_multicast_group(self, config): - match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+.[\d]+.[\d]+)', + match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', config) value = match.group(1) if match else self.DEFAULT_MCAST_GRP return dict(multicast_group=value) diff --git a/test/fixtures/running_config.vxlan b/test/fixtures/running_config.vxlan index 88356bd..f24ab40 100644 --- a/test/fixtures/running_config.vxlan +++ b/test/fixtures/running_config.vxlan @@ -46,4 +46,5 @@ interface Vxlan1 no vxlan vlan flood vtep no vxlan learn-restrict vtep no vxlan vlan learn-restrict vtep + no vxlan multicast-group decap ! diff --git a/test/fixtures/vxlan.json b/test/fixtures/vxlan.json index 88153b4..cc7b0b5 100644 --- a/test/fixtures/vxlan.json +++ b/test/fixtures/vxlan.json @@ -9,6 +9,7 @@ "vlanToVtepList": {}, "mtu": 0, "hardware": "vxlan", + "mcastGrpDecap": "", "replicationMode": "multicast", "bandwidth": 0, "floodMcastGrp": "239.10.10.10", diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 86a1e4a..4c064de 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -407,8 +407,6 @@ def test_set_multicast_decap_with_default(self): func = function('set_multicast_decap', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) - - def test_set_udp_port_with_value(self): cmds = ['interface Vxlan1', 'vxlan udp-port 1024'] func = function('set_udp_port', 'Vxlan1', '1024') From 3371d9f3ea2f5341d83a67421b0585475c0e8453 Mon Sep 17 00:00:00 2001 From: Dave Thelen Date: Mon, 12 Sep 2016 10:07:42 -0400 Subject: [PATCH 07/44] more systest fixes --- test/system/test_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/system/test_client.py b/test/system/test_client.py index 58da65d..9aa0fac 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -71,9 +71,8 @@ def test_no_enable_single_command(self): def test_no_enable_single_command_no_auth(self): for dut in self.duts: - dut.run_commands('disable') with self.assertRaises(pyeapi.eapilib.CommandError): - dut.run_commands('show running-config', 'json', send_enable=False) + dut.run_commands(['disable', 'show running-config'], 'json', send_enable=False) def test_enable_multiple_commands(self): for dut in self.duts: @@ -187,7 +186,7 @@ def test_exception_trace(self): # Send a continuous command that requires a break cases.append(('watch 10 show int e1 count rates', rfmt % (1000, 'could not run command', - 'init error \(cbreak\(\) returned ERR\)'))) + 'init error.*'))) # Send a command that has insufficient priv cases.append(('show running-config', rfmt % (1002, 'invalid command', From 24aafa28d3daa2a39889524d45a0afb967ef79ac Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 25 Sep 2016 17:35:19 +0200 Subject: [PATCH 08/44] Fix test case when random is a valid VLAN A random string (like '214') can be a valid VLAN. Fix the test case such that the not-so-random string can never be a VLAN. --- test/unit/test_api_vlans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_api_vlans.py b/test/unit/test_api_vlans.py index 3849e68..7652ec9 100644 --- a/test/unit/test_api_vlans.py +++ b/test/unit/test_api_vlans.py @@ -49,7 +49,7 @@ def __init__(self, *args, **kwargs): self.config = open(get_fixture('running_config.text')).read() def test_isvlan_with_string(self): - self.assertFalse(pyeapi.api.vlans.isvlan(random_string())) + self.assertFalse(pyeapi.api.vlans.isvlan('a' + random_string())) def test_isvlan_valid_value(self): self.assertTrue(pyeapi.api.vlans.isvlan('1234')) From d38fef45d646fbb7dfef6e7cfe547653bfc70e47 Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Tue, 8 Nov 2016 13:12:42 +0100 Subject: [PATCH 09/44] expose portchannel attributes :lacp fallback, lacp fallback timeout --- pyeapi/api/interfaces.py | 67 ++++++++++++++++++++++++++++++-- test/unit/test_api_interfaces.py | 1 + 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index de1536c..de24e1d 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -65,6 +65,8 @@ MIN_LINKS_RE = re.compile(r'(?<=\s{3}min-links\s)(?P.+)$', re.M) DEFAULT_LACP_MODE = 'on' +DEFAULT_LACP_FALLBACK = 'disabled' +DEFAULT_LACP_FALLBACK_TIMEOUT = 90 VALID_INTERFACES = frozenset([ 'Ethernet', @@ -548,6 +550,8 @@ def get(self, name): response['members'] = self.get_members(name) response['lacp_mode'] = self.get_lacp_mode(name) response.update(self._parse_minimum_links(config)) + response.update(self._parse_lacp_timeout(config)) + response.update(self._parse_lacp_fallback(config)) return response def _parse_minimum_links(self, config): @@ -557,6 +561,20 @@ def _parse_minimum_links(self, config): value = int(match.group(1)) return dict(minimum_links=value) + def _parse_lacp_fallback(self, config): + value = DEFAULT_LACP_FALLBACK + match = re.search(r'lacp fallback (static|individual)', config) + if match: + value = match.group(1) + return dict(lacp_fallback=value) + + def _parse_lacp_timeout(self, config): + value = DEFAULT_LACP_FALLBACK_TIMEOUT + match = re.search(r'lacp fallback timeout (\d+)', config) + if match: + value = int(match.group(1)) + return dict(lacp_timeout=value) + def get_lacp_mode(self, name): """Returns the LACP mode for the specified Port-Channel interface @@ -689,6 +707,50 @@ def set_minimum_links(self, name, value=None, default=False, disable=disable)) return self.configure(commands) + def set_lacp_fallback(self, name, mode=None): + """Configures the Port-Channel lacp_fallback + + Args: + name(str): The Port-Channel interface name + + mode(str): The Port-Channel LACP fallback setting + Valid values are 'disabled', 'static', 'individual': + + * static - Fallback to static LAG mode + * individual - Fallback to individual ports + * disabled - Disable LACP fallback + + Returns: + True if the operation succeeds otherwise False is returned + """ + if mode not in ['disabled', 'static', 'individual']: + return False + disable = False + if mode == 'disabled': + disable = True + commands = ['interface %s' % name] + commands.append(self.command_builder('port-channel lacp fallback', + value=mode, disable=disable)) + return self.configure(commands) + + def set_lacp_timeout(self, name, value=None): + """Configures the Port-Channel LACP fallback timeout + The fallback timeout configures the period an interface in + fallback mode remains in LACP mode without receiving a PDU. + + Args: + name(str): The Port-Channel interface name + + value(int): port-channel lacp fallback timeout in seconds + + Returns: + True if the operation succeeds otherwise False is returned + """ + commands = ['interface %s' % name] + commands.append(self.command_builder('port-channel lacp fallback timeout', + value=value)) + return self.configure(commands) + class VxlanInterface(BaseInterface): @@ -839,8 +901,7 @@ def set_multicast_group(self, name, value=None, default=False, disable=disable) return self.configure_interface(name, cmds) - def set_multicast_decap(self, name, default=False, - disable=False): + def set_multicast_decap(self, name, default=False, disable=False): """Configures the Vxlan multicast-group decap feature EosVersion: @@ -858,7 +919,7 @@ def set_multicast_decap(self, name, default=False, string = 'vxlan multicast-group decap' if(default or disable): cmds = self.command_builder(string, value=None, default=default, - disable=disable) + disable=disable) else: cmds = [string] return self.configure_interface(name, cmds) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 4c064de..2125a1a 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -268,6 +268,7 @@ def test_get(self): values = dict(name='Port-Channel1', type='portchannel', description=None, shutdown=False, lacp_mode='on', minimum_links=0, + lacp_fallback='disabled', lacp_timeout=90, members=['Ethernet5', 'Ethernet6']) self.assertEqual(values, result) From eb3ac77c34d2d174aaa8ad33f85b107d01f9426e Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Mon, 21 Nov 2016 20:56:26 +0100 Subject: [PATCH 10/44] Fix flake8 errors --- pyeapi/api/interfaces.py | 16 ++++++++-------- pyeapi/client.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index de24e1d..3a0e24b 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -735,7 +735,7 @@ def set_lacp_fallback(self, name, mode=None): def set_lacp_timeout(self, name, value=None): """Configures the Port-Channel LACP fallback timeout - The fallback timeout configures the period an interface in + The fallback timeout configures the period an interface in fallback mode remains in LACP mode without receiving a PDU. Args: @@ -747,8 +747,8 @@ def set_lacp_timeout(self, name, value=None): True if the operation succeeds otherwise False is returned """ commands = ['interface %s' % name] - commands.append(self.command_builder('port-channel lacp fallback timeout', - value=value)) + string = 'port-channel lacp fallback timeout' + commands.append(self.command_builder(string, value=value)) return self.configure(commands) @@ -772,7 +772,7 @@ def get(self, name): * udp_port (int): The vxlan udp-port value * vlans (dict): The vlan to vni mappings * flood_list (list): The list of global VTEP flood list - * multicast_decap (bool): If the mutlicast decap + * multicast_decap (bool): If the mutlicast decap feature is configured Args: @@ -818,11 +818,11 @@ def _parse_source_interface(self, config): return dict(source_interface=value) def _parse_multicast_group(self, config): - match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', + match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', config) value = match.group(1) if match else self.DEFAULT_MCAST_GRP return dict(multicast_group=value) - + def _parse_multicast_decap(self, config): value = 'vxlan mutlicast-group decap' in config return dict(multicast_decap=bool(value)) @@ -902,7 +902,7 @@ def set_multicast_group(self, name, value=None, default=False, return self.configure_interface(name, cmds) def set_multicast_decap(self, name, default=False, disable=False): - """Configures the Vxlan multicast-group decap feature + """Configures the Vxlan multicast-group decap feature EosVersion: 4.15.0M @@ -921,7 +921,7 @@ def set_multicast_decap(self, name, default=False, disable=False): cmds = self.command_builder(string, value=None, default=default, disable=disable) else: - cmds = [string] + cmds = [string] return self.configure_interface(name, cmds) diff --git a/pyeapi/client.py b/pyeapi/client.py index ab6fb0f..8282d5d 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -560,8 +560,8 @@ def section(self, regex, config='running_config'): block_end = line_end + block_end return config[block_start:block_end] - def enable(self, commands, encoding='json', strict=False, - send_enable=True): + def enable(self, commands, encoding='json', strict=False, + send_enable=True): """Sends the array of commands to the node in enable mode This method will send the commands to the node and evaluate From 8d4570e72b355104298fa0809f07cc3bea5f861f Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Mon, 21 Nov 2016 21:04:33 +0100 Subject: [PATCH 11/44] Fix flake8 errors in test --- test/unit/test_api_interfaces.py | 1 + test/unit/test_client.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 2125a1a..d1c63a0 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -453,5 +453,6 @@ def test_remove_vtep_from_vlan(self): func = function('remove_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 57b1f3e..8e43d38 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -116,7 +116,7 @@ def test_config_with_single_multiline(self): def test_config_with_multiple_multilines(self): commands = [random_string(), ('banner login MULTILINE:This is a new banner\n' - 'with different lines!!!'), + 'with different lines!!!'), random_string()] self.node.run_commands = Mock(return_value=[{}, {}, {}, {}]) From b55083d69c288dadaa201aefbf425bc48b9ed9a4 Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Mon, 21 Nov 2016 21:14:43 +0100 Subject: [PATCH 12/44] Fix more flake8 errors in test --- test/unit/test_api_interfaces.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index d1c63a0..2ad7064 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -270,7 +270,6 @@ def test_get(self): lacp_mode='on', minimum_links=0, lacp_fallback='disabled', lacp_timeout=90, members=['Ethernet5', 'Ethernet6']) - self.assertEqual(values, result) def test_set_minimum_links_with_value(self): From 54c5de9c1343587546977d3ad1996b3a896dc68b Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Mon, 21 Nov 2016 21:21:29 +0100 Subject: [PATCH 13/44] Fix more flake8 errors in the test --- test/unit/test_api_interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 2ad7064..2125a1a 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -270,6 +270,7 @@ def test_get(self): lacp_mode='on', minimum_links=0, lacp_fallback='disabled', lacp_timeout=90, members=['Ethernet5', 'Ethernet6']) + self.assertEqual(values, result) def test_set_minimum_links_with_value(self): @@ -452,6 +453,5 @@ def test_remove_vtep_from_vlan(self): func = function('remove_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) - if __name__ == '__main__': unittest.main() From 088c81484721561d9e261efc09675917fe0e9e82 Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Tue, 22 Nov 2016 22:30:37 +0100 Subject: [PATCH 14/44] Make Flake8 happy again --- test/lib/testlib.py | 1 + test/system/test_api_interfaces.py | 1 + test/system/test_api_ipinterfaces.py | 1 + test/system/test_api_routemaps.py | 1 + test/system/test_api_staticroute.py | 1 + test/system/test_api_stp.py | 1 + test/system/test_api_switchports.py | 1 + test/system/test_api_users.py | 1 + test/system/test_api_varp.py | 1 + test/unit/test_api_interfaces.py | 1 + test/unit/test_api_ipinterfaces.py | 1 + test/unit/test_api_ospf.py | 1 + test/unit/test_api_routemaps.py | 1 + test/unit/test_api_switchports.py | 1 + test/unit/test_api_system.py | 1 + test/unit/test_api_varp.py | 1 + test/unit/test_api_vlans.py | 1 + 17 files changed, 17 insertions(+) diff --git a/test/lib/testlib.py b/test/lib/testlib.py index 0320468..5ec9bc1 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -54,6 +54,7 @@ def random_vlan(): def random_int(minvalue, maxvalue): return random.randint(minvalue, maxvalue) + from collections import namedtuple Function = namedtuple('Function', 'name args kwargs') diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 00e10bf..f2eae96 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -492,5 +492,6 @@ def test_remove_vlan(self): self.assertTrue(instance) self.notcontains('vxlan vlan 10 vni 10', dut) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_ipinterfaces.py b/test/system/test_api_ipinterfaces.py index af74425..acf352a 100644 --- a/test/system/test_api_ipinterfaces.py +++ b/test/system/test_api_ipinterfaces.py @@ -131,5 +131,6 @@ def test_set_mtu_value_as_string(self): self.assertIn('mtu 2000', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py index 1a808c4..615355e 100644 --- a/test/system/test_api_routemaps.py +++ b/test/system/test_api_routemaps.py @@ -273,5 +273,6 @@ def test_negate_continue(self): self.assertTrue(result) self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 2bae9bf..1082812 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -287,5 +287,6 @@ def test_set_route_name(self): distance=1, route_name='test3') self.assertTrue(result) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_stp.py b/test/system/test_api_stp.py index e6a9f4f..49f096c 100644 --- a/test/system/test_api_stp.py +++ b/test/system/test_api_stp.py @@ -140,5 +140,6 @@ def test_set_portfast_to_network(self): result = resource.set_portfast_type(intf, 'normal') self.assertTrue(result, 'dut=%s' % dut) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_switchports.py b/test/system/test_api_switchports.py index e0c51be..a0865c8 100644 --- a/test/system/test_api_switchports.py +++ b/test/system/test_api_switchports.py @@ -132,5 +132,6 @@ def test_set_trunk_allowed_vlans(self): config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_users.py b/test/system/test_api_users.py index 576ea9c..5ee6e57 100644 --- a/test/system/test_api_users.py +++ b/test/system/test_api_users.py @@ -177,5 +177,6 @@ def test_set_sshkey_with_no_value(self): self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_varp.py b/test/system/test_api_varp.py index 32276f4..4fd6a10 100644 --- a/test/system/test_api_varp.py +++ b/test/system/test_api_varp.py @@ -232,5 +232,6 @@ def test_no_attr_virtual_addrs(self): 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_interfaces.py b/test/unit/test_api_interfaces.py index 2125a1a..d1c63a0 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -453,5 +453,6 @@ def test_remove_vtep_from_vlan(self): func = function('remove_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_ipinterfaces.py b/test/unit/test_api_ipinterfaces.py index 77cad8a..0e07681 100644 --- a/test/unit/test_api_ipinterfaces.py +++ b/test/unit/test_api_ipinterfaces.py @@ -134,5 +134,6 @@ def test_set_mtu_invalid_value_raises_value_error(self): func = function('set_mtu', intf, value) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index cd2844c..afd6b63 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -132,6 +132,7 @@ def test_no_delete(self): result = self.instance.delete() self.assertTrue(result) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index f7ea1c9..0413375 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -154,5 +154,6 @@ def test_set_description_with_invalid_value(self): func = function('set_description', 'TEST', 'permit', 10, value=None) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_switchports.py b/test/unit/test_api_switchports.py index 6c72103..c8944fd 100644 --- a/test/unit/test_api_switchports.py +++ b/test/unit/test_api_switchports.py @@ -201,5 +201,6 @@ def test_remove_trunk_group(self): func = function('remove_trunk_group', intf, 'foo') self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_system.py b/test/unit/test_api_system.py index 240fbd4..88eb3f2 100644 --- a/test/unit/test_api_system.py +++ b/test/unit/test_api_system.py @@ -98,5 +98,6 @@ def test_set_banner_default_disable(self): cmds = 'no banner motd' self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py index c3777dd..33db3d1 100644 --- a/test/unit/test_api_varp.py +++ b/test/unit/test_api_varp.py @@ -146,5 +146,6 @@ def test_add_address_with_disable(self): cmds = ['interface Vlan4001', 'no ip virtual-router address'] self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_vlans.py b/test/unit/test_api_vlans.py index 7652ec9..f7d42ac 100644 --- a/test/unit/test_api_vlans.py +++ b/test/unit/test_api_vlans.py @@ -150,5 +150,6 @@ def test_remove_trunk_group(self): func = function('remove_trunk_group', vid, tg) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() From 8e1c7f3f0c334a7fa19262144eec78e569cbd981 Mon Sep 17 00:00:00 2001 From: Ruslan Lutsenko Date: Tue, 22 Nov 2016 23:13:59 +0100 Subject: [PATCH 15/44] Unit tests of lacp fallback functionality --- pyeapi/api/interfaces.py | 4 +--- test/unit/test_api_interfaces.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 3a0e24b..9ad56cd 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -725,9 +725,7 @@ def set_lacp_fallback(self, name, mode=None): """ if mode not in ['disabled', 'static', 'individual']: return False - disable = False - if mode == 'disabled': - disable = True + disable = True if mode == 'disabled' else False commands = ['interface %s' % name] commands.append(self.command_builder('port-channel lacp fallback', value=mode, disable=disable)) diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index d1c63a0..4ddc31a 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -295,6 +295,31 @@ def test_set_minimum_links_with_disable(self): func = function('set_minimum_links', 'Port-Channel1', disable=True) self.eapi_positive_config_test(func, cmds) + def test_set_lacp_timeout_with_value(self): + timeout = random_int(1, 16) + cmds = ['interface Port-Channel1', 'port-channel lacp fallback timeout %s' % timeout] + func = function('set_lacp_timeout', 'Port-Channel1', timeout) + self.eapi_positive_config_test(func, cmds) + + def test_set_lacp_fallback_with_individual(self): + cmds = ['interface Port-Channel1', 'port-channel lacp fallback individual'] + func = function('set_lacp_fallback', 'Port-Channel1', 'individual') + self.eapi_positive_config_test(func, cmds) + + def test_set_lacp_fallback_with_static(self): + cmds = ['interface Port-Channel1', 'port-channel lacp fallback static'] + func = function('set_lacp_fallback', 'Port-Channel1', 'static') + self.eapi_positive_config_test(func, cmds) + + def test_set_lacp_fallback_with_disabled(self): + cmds = ['interface Port-Channel1', 'no port-channel lacp fallback'] + func = function('set_lacp_fallback', 'Port-Channel1', 'disabled') + self.eapi_positive_config_test(func, cmds) + + def test_set_lacp_fallback_invalid_mode(self): + func = function('set_lacp_fallback', 'Port-Channel1', random_string()) + self.eapi_negative_config_test(func) + def test_get_lacp_mode(self): result = self.instance.get_lacp_mode('Port-Channel1') self.assertEqual(result, 'on') From 9091c14140287eb841d4878389b7114432d52435 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Dec 2016 12:13:03 -0500 Subject: [PATCH 16/44] Cast port to integer --- pyeapi/client.py | 4 ++-- pyeapi/eapilib.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index ab6fb0f..8282d5d 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -560,8 +560,8 @@ def section(self, regex, config='running_config'): block_end = line_end + block_end return config[block_start:block_end] - def enable(self, commands, encoding='json', strict=False, - send_enable=True): + def enable(self, commands, encoding='json', strict=False, + send_enable=True): """Sends the array of commands to the node in enable mode This method will send the commands to the node and evaluate diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 26b867e..d4e84d4 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -489,7 +489,7 @@ def __init__(self, path=None, timeout=60, **kwargs): class HttpLocalEapiConnection(EapiConnection): def __init__(self, port=None, path=None, timeout=60, **kwargs): super(HttpLocalEapiConnection, self).__init__() - port = port or DEFAULT_HTTP_LOCAL_PORT + port = int(port) or DEFAULT_HTTP_LOCAL_PORT path = path or DEFAULT_HTTP_PATH self.transport = HttpConnection(path, 'localhost', port, timeout=timeout) @@ -498,7 +498,7 @@ class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, timeout=60, **kwargs): super(HttpEapiConnection, self).__init__() - port = port or DEFAULT_HTTP_PORT + port = int(port) or DEFAULT_HTTP_PORT path = path or DEFAULT_HTTP_PATH self.transport = HttpConnection(path, host, port, timeout=timeout) self.authentication(username, password) @@ -507,7 +507,7 @@ class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, context=None, timeout=60, **kwargs): super(HttpsEapiConnection, self).__init__() - port = port or DEFAULT_HTTPS_PORT + port = int(port) or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH enforce_verification = kwargs.get('enforce_verification') From f8c75c72ae746326983e121ff4da5e19bacc597a Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Dec 2016 12:16:23 -0500 Subject: [PATCH 17/44] Update gitignore --- .gitignore | 1 - pyeapi/eapilib.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6e1c6fb..074bba5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ develop-eggs/ dist/ downloads/ eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index d4e84d4..2d014b2 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -489,25 +489,25 @@ def __init__(self, path=None, timeout=60, **kwargs): class HttpLocalEapiConnection(EapiConnection): def __init__(self, port=None, path=None, timeout=60, **kwargs): super(HttpLocalEapiConnection, self).__init__() - port = int(port) or DEFAULT_HTTP_LOCAL_PORT + port = port or DEFAULT_HTTP_LOCAL_PORT path = path or DEFAULT_HTTP_PATH - self.transport = HttpConnection(path, 'localhost', port, + self.transport = HttpConnection(path, 'localhost', int(port), timeout=timeout) class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, timeout=60, **kwargs): super(HttpEapiConnection, self).__init__() - port = int(port) or DEFAULT_HTTP_PORT + port = port or DEFAULT_HTTP_PORT path = path or DEFAULT_HTTP_PATH - self.transport = HttpConnection(path, host, port, timeout=timeout) + self.transport = HttpConnection(path, host, int(port), timeout=timeout) self.authentication(username, password) class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, context=None, timeout=60, **kwargs): super(HttpsEapiConnection, self).__init__() - port = int(port) or DEFAULT_HTTPS_PORT + port = port or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH enforce_verification = kwargs.get('enforce_verification') @@ -515,7 +515,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, + self.transport = https_connection_factory(path, host, int(port), context, timeout) self.authentication(username, password) From 4de8c9b8521db6c6622168edf3cb32136b5c7414 Mon Sep 17 00:00:00 2001 From: Philip DiLeo Date: Wed, 21 Dec 2016 14:56:37 -0500 Subject: [PATCH 18/44] Fix flake8 pep issues --- pyeapi/api/interfaces.py | 15 ++++++++------- pyeapi/client.py | 5 +++-- pyeapi/eapilib.py | 2 +- pyeapi/utils.py | 3 ++- test/lib/testlib.py | 1 + test/system/test_api_interfaces.py | 1 + test/system/test_api_ipinterfaces.py | 1 + test/system/test_api_routemaps.py | 1 + test/system/test_api_staticroute.py | 1 + test/system/test_api_stp.py | 1 + test/system/test_api_switchports.py | 1 + test/system/test_api_users.py | 1 + test/system/test_api_varp.py | 1 + test/unit/test_api_interfaces.py | 1 + test/unit/test_api_ipinterfaces.py | 1 + test/unit/test_api_ospf.py | 2 +- test/unit/test_api_routemaps.py | 1 + test/unit/test_api_switchports.py | 1 + test/unit/test_api_system.py | 1 + test/unit/test_api_varp.py | 1 + test/unit/test_api_vlans.py | 1 + 21 files changed, 31 insertions(+), 12 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index de1536c..913dc63 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -710,7 +710,7 @@ def get(self, name): * udp_port (int): The vxlan udp-port value * vlans (dict): The vlan to vni mappings * flood_list (list): The list of global VTEP flood list - * multicast_decap (bool): If the mutlicast decap + * multicast_decap (bool): If the mutlicast decap feature is configured Args: @@ -756,11 +756,12 @@ def _parse_source_interface(self, config): return dict(source_interface=value) def _parse_multicast_group(self, config): - match = re.search(r'vxlan multicast-group ([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', + match = re.search(r'vxlan multicast-group ' + r'([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', config) value = match.group(1) if match else self.DEFAULT_MCAST_GRP return dict(multicast_group=value) - + def _parse_multicast_decap(self, config): value = 'vxlan mutlicast-group decap' in config return dict(multicast_decap=bool(value)) @@ -840,8 +841,8 @@ def set_multicast_group(self, name, value=None, default=False, return self.configure_interface(name, cmds) def set_multicast_decap(self, name, default=False, - disable=False): - """Configures the Vxlan multicast-group decap feature + disable=False): + """Configures the Vxlan multicast-group decap feature EosVersion: 4.15.0M @@ -858,9 +859,9 @@ def set_multicast_decap(self, name, default=False, string = 'vxlan multicast-group decap' if(default or disable): cmds = self.command_builder(string, value=None, default=default, - disable=disable) + disable=disable) else: - cmds = [string] + cmds = [string] return self.configure_interface(name, cmds) diff --git a/pyeapi/client.py b/pyeapi/client.py index 8282d5d..645d11a 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -91,8 +91,6 @@ """ import os -import sys -import logging import re try: @@ -312,8 +310,11 @@ def add_connection(self, name, **kwargs): # TODO: This is a global variable (in the module) - to review the impact on # having a shared state for the config file. + + config = Config() + def load_config(filename): """Function method that loads a conf file diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 2d014b2..988418d 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -50,7 +50,7 @@ # Use Python 2.7 import as a fallback from httplib import HTTPConnection, HTTPSConnection -from pyeapi.utils import debug, make_iterable +from pyeapi.utils import make_iterable _LOGGER = logging.getLogger(__name__) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 82fd140..3a3db71 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -57,11 +57,12 @@ _LOGGER.addHandler(_syslog_handler) # Create a handler to log messages to stderr -_stderr_formatter = logging.Formatter('\n\n******** LOG NOTE ********\n%(message)s\n') +_stderr_formatter = logging.Formatter('\n\n**** LOG NOTE ****\n%(message)s\n') _stderr_handler = logging.StreamHandler() _stderr_handler.setFormatter(_stderr_formatter) _LOGGER.addHandler(_stderr_handler) + def import_module(name): """ Imports a module into the current runtime environment diff --git a/test/lib/testlib.py b/test/lib/testlib.py index 0320468..5ec9bc1 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -54,6 +54,7 @@ def random_vlan(): def random_int(minvalue, maxvalue): return random.randint(minvalue, maxvalue) + from collections import namedtuple Function = namedtuple('Function', 'name args kwargs') diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 00e10bf..f2eae96 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -492,5 +492,6 @@ def test_remove_vlan(self): self.assertTrue(instance) self.notcontains('vxlan vlan 10 vni 10', dut) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_ipinterfaces.py b/test/system/test_api_ipinterfaces.py index af74425..acf352a 100644 --- a/test/system/test_api_ipinterfaces.py +++ b/test/system/test_api_ipinterfaces.py @@ -131,5 +131,6 @@ def test_set_mtu_value_as_string(self): self.assertIn('mtu 2000', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py index 1a808c4..615355e 100644 --- a/test/system/test_api_routemaps.py +++ b/test/system/test_api_routemaps.py @@ -273,5 +273,6 @@ def test_negate_continue(self): self.assertTrue(result) self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 2bae9bf..1082812 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -287,5 +287,6 @@ def test_set_route_name(self): distance=1, route_name='test3') self.assertTrue(result) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_stp.py b/test/system/test_api_stp.py index e6a9f4f..49f096c 100644 --- a/test/system/test_api_stp.py +++ b/test/system/test_api_stp.py @@ -140,5 +140,6 @@ def test_set_portfast_to_network(self): result = resource.set_portfast_type(intf, 'normal') self.assertTrue(result, 'dut=%s' % dut) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_switchports.py b/test/system/test_api_switchports.py index e0c51be..a0865c8 100644 --- a/test/system/test_api_switchports.py +++ b/test/system/test_api_switchports.py @@ -132,5 +132,6 @@ def test_set_trunk_allowed_vlans(self): config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_users.py b/test/system/test_api_users.py index 576ea9c..5ee6e57 100644 --- a/test/system/test_api_users.py +++ b/test/system/test_api_users.py @@ -177,5 +177,6 @@ def test_set_sshkey_with_no_value(self): self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) + if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_varp.py b/test/system/test_api_varp.py index 32276f4..4fd6a10 100644 --- a/test/system/test_api_varp.py +++ b/test/system/test_api_varp.py @@ -232,5 +232,6 @@ def test_no_attr_virtual_addrs(self): 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_interfaces.py b/test/unit/test_api_interfaces.py index 4c064de..5372d37 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -452,5 +452,6 @@ def test_remove_vtep_from_vlan(self): func = function('remove_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_ipinterfaces.py b/test/unit/test_api_ipinterfaces.py index 77cad8a..0e07681 100644 --- a/test/unit/test_api_ipinterfaces.py +++ b/test/unit/test_api_ipinterfaces.py @@ -134,5 +134,6 @@ def test_set_mtu_invalid_value_raises_value_error(self): func = function('set_mtu', intf, value) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index cd2844c..5f48cd9 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -132,6 +132,6 @@ def test_no_delete(self): result = self.instance.delete() self.assertTrue(result) + if __name__ == '__main__': unittest.main() - diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index f7ea1c9..0413375 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -154,5 +154,6 @@ def test_set_description_with_invalid_value(self): func = function('set_description', 'TEST', 'permit', 10, value=None) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_switchports.py b/test/unit/test_api_switchports.py index 6c72103..c8944fd 100644 --- a/test/unit/test_api_switchports.py +++ b/test/unit/test_api_switchports.py @@ -201,5 +201,6 @@ def test_remove_trunk_group(self): func = function('remove_trunk_group', intf, 'foo') self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_system.py b/test/unit/test_api_system.py index 240fbd4..88eb3f2 100644 --- a/test/unit/test_api_system.py +++ b/test/unit/test_api_system.py @@ -98,5 +98,6 @@ def test_set_banner_default_disable(self): cmds = 'no banner motd' self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py index c3777dd..33db3d1 100644 --- a/test/unit/test_api_varp.py +++ b/test/unit/test_api_varp.py @@ -146,5 +146,6 @@ def test_add_address_with_disable(self): cmds = ['interface Vlan4001', 'no ip virtual-router address'] self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_api_vlans.py b/test/unit/test_api_vlans.py index 7652ec9..f7d42ac 100644 --- a/test/unit/test_api_vlans.py +++ b/test/unit/test_api_vlans.py @@ -150,5 +150,6 @@ def test_remove_trunk_group(self): func = function('remove_trunk_group', vid, tg) self.eapi_positive_config_test(func, cmds) + if __name__ == '__main__': unittest.main() From 3350fd2b89196656ed59cd332632f39fca5370b1 Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 22 Dec 2016 11:47:44 -0500 Subject: [PATCH 19/44] Add support for autoComplete parameter when using execute with a connection. --- pyeapi/eapilib.py | 9 ++++++++- test/unit/test_eapilib.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 988418d..d2d6123 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -251,7 +251,7 @@ def authentication(self, username, password): _LOGGER.debug('Autentication string is: {}'.format(self._auth)) - def request(self, commands, encoding=None, reqid=None): + def request(self, commands, encoding=None, reqid=None, **kwargs): """Generates an eAPI request object This method will take a list of EOS commands and generate a valid @@ -290,6 +290,8 @@ def request(self, commands, encoding=None, reqid=None): commands = make_iterable(commands) reqid = id(self) if reqid is None else reqid params = {"version": 1, "cmds": commands, "format": encoding} + if 'autoComplete' in kwargs: + params["autoComplete"] = kwargs["autoComplete"] return json.dumps({"jsonrpc": "2.0", "method": "runCmds", "params": params, "id": str(reqid)}) @@ -393,6 +395,11 @@ def send(self, data): if 'error' in decoded: (code, msg, err, out) = self._parse_error_message(decoded) + if "unexpected keyword argument 'autoComplete'" in msg: + auto_msg = ("autoComplete parameter is not supported in" + " this version of EOS.") + _LOGGER.error(auto_msg) + msg = msg + ". " + auto_msg raise CommandError(code, msg, command_error=err, output=out) return decoded diff --git a/test/unit/test_eapilib.py b/test/unit/test_eapilib.py index 2049b94..cd9199b 100644 --- a/test/unit/test_eapilib.py +++ b/test/unit/test_eapilib.py @@ -110,7 +110,6 @@ def test_send_raises_connection_error(self): with self.assertRaises(pyeapi.eapilib.ConnectionError): instance.send('test') - def test_send_raises_command_error(self): error = dict(code=9999, message='test', data=[{'errors': ['test']}]) response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) @@ -126,6 +125,41 @@ def test_send_raises_command_error(self): with self.assertRaises(pyeapi.eapilib.CommandError): instance.send('test') + def test_send_raises_autocomplete_command_error(self): + message = "runCmds() got an unexpected keyword argument 'autoComplete'" + error = dict(code=9999, message=message, data=[{'errors': ['test']}]) + response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) + response_json = json.dumps(response_dict) + + mock_transport = Mock(name='transport') + mockcfg = {'getresponse.return_value.read.return_value': response_json} + mock_transport.configure_mock(**mockcfg) + + instance = pyeapi.eapilib.EapiConnection() + instance.transport = mock_transport + + try: + instance.send('test') + except pyeapi.eapilib.CommandError as error: + match = ("autoComplete parameter is not supported in this version" + " of EOS.") + self.assertIn(match, error.message) + + def test_request_adds_autocomplete(self): + instance = pyeapi.eapilib.EapiConnection() + request = instance.request(['sh ver'], encoding='json', + autoComplete=True) + data = json.loads(request) + self.assertIn('autoComplete', data['params']) + + def test_request_ignores_unknown_param(self): + instance = pyeapi.eapilib.EapiConnection() + request = instance.request(['sh ver'], encoding='json', + unknown=True) + data = json.loads(request) + self.assertNotIn('unknown', data['params']) + + class TestCommandError(unittest.TestCase): def test_create_command_error(self): From 12c7c63224a2188555990833e0dd71272cc16337 Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 22 Dec 2016 15:11:54 -0500 Subject: [PATCH 20/44] Added version dependent system test for autocomplete feature. --- test/system/test_client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/system/test_client.py b/test/system/test_client.py index 9aa0fac..f30fffa 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -142,6 +142,25 @@ def test_get_block_none(self): txtstr = api.get_block('interface Ethernet1', config='config') self.assertEqual(txtstr, None) + def test_execute_with_autocomplete(self): + # There are some versions of EOS before 4.17.x that have the + # autocomplete feature available. If system tests are run on one of + # those version of EOS this system test will fail. + for dut in self.duts: + result = dut.connection.execute(['show version'], encoding='json') + version = result['result'][0]['version'] + version = version.split('.') + if int(version[0]) >= 4 and int(version[1]) >= 17: + result = dut.connection.execute(['sh ver'], encoding='json', + autoComplete=True) + self.assertIn('version', result['result'][0]) + else: + # Verify exception thrown for EOS version that does not + # support autocomplete parameter with EAPI + with self.assertRaises(pyeapi.eapilib.CommandError): + dut.connection.execute(['sh ver'], encoding='json', + autoComplete=True) + def tearDown(self): for dut in self.duts: dut.config("no enable secret") From e0b9c3f977ec3a390ac44366d85c110d7fab4690 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sat, 14 Jan 2017 07:51:04 +0100 Subject: [PATCH 21/44] Fix test case when random string is a valid MTU A random string (like '214') can be a valid MTU. Fix the test case such that the not-so-random string can never be a MTU. --- test/unit/test_api_ipinterfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_api_ipinterfaces.py b/test/unit/test_api_ipinterfaces.py index 0e07681..00ed1b4 100644 --- a/test/unit/test_api_ipinterfaces.py +++ b/test/unit/test_api_ipinterfaces.py @@ -123,7 +123,7 @@ def test_set_mtu_default(self): def test_set_mtu_invalid_value_raises_value_error(self): for intf in self.INTERFACES: - for value in [67, 65536, random_string()]: + for value in [67, 65536, 'a' + random_string()]: func = function('set_mtu', intf, value) self.eapi_exception_config_test(func, ValueError) for value in [None]: From 34ce20ece0b679679b70dfaaab1b38c332ca3d24 Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 17 Jan 2017 12:54:23 -0500 Subject: [PATCH 22/44] Update Varp API, systests and unittests to support new command format in EOS 4.17.x --- pyeapi/api/varp.py | 13 +++++++----- test/system/test_api_varp.py | 39 +++++++++++++++++++++++++++--------- test/unit/test_api_varp.py | 4 ++-- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/pyeapi/api/varp.py b/pyeapi/api/varp.py index 8476c9b..b68b126 100644 --- a/pyeapi/api/varp.py +++ b/pyeapi/api/varp.py @@ -115,12 +115,13 @@ def set_mac_address(self, mac_address=None, default=False, disable=False): 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). - disabel (bool): Negates the virtual-router mac address using + disable (bool): Negates the virtual-router mac address using the system no configuration command Returns: True if the set operation succeeds otherwise False. """ + base_command = 'ip virtual-router mac-address' if not default and not disable: if mac_address is not None: # Check to see if mac_address matches expected format @@ -131,10 +132,12 @@ def set_mac_address(self, mac_address=None, default=False, disable=False): else: raise ValueError('mac_address must be a properly formatted ' 'address string') - - commands = self.command_builder('ip virtual-router mac-address', - value=mac_address, default=default, - disable=disable) + if default or disable and not mac_address: + current_mac = self._parse_mac_address() + if current_mac['mac_address']: + base_command = base_command + ' ' + current_mac['mac_address'] + commands = self.command_builder(base_command, value=mac_address, + default=default, disable=disable) return self.configure(commands) diff --git a/test/system/test_api_varp.py b/test/system/test_api_varp.py index 4fd6a10..c976963 100644 --- a/test/system/test_api_varp.py +++ b/test/system/test_api_varp.py @@ -37,37 +37,54 @@ from systestlib import DutSystemTest -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 _null_virtual_mac_command(self, dut): + # Default format of ip virtual-router mac-address changed in EOS 4.17.x + # pre 4.17.x - no ip virtual-router mac-address + # 4.17.x and on - ip virtual-router mac-address 00:00:00:00:00:00 + mac_dict = dut.api('varp')._parse_mac_address() + virt_null_base = 'no ip virtual-router mac-address' + if mac_dict['mac_address']: + return virt_null_base + ' ' + mac_dict['mac_address'] + return virt_null_base + def test_basic_get(self): for dut in self.duts: - dut.config([VIRT_NULL]) + virt_null = self._null_virtual_mac_command(dut) + 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]) + virt_null = self._null_virtual_mac_command(dut) + 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): + # None response is for code versions before 4.17.x due to default + # configuration change in 4.17.x. Default configuration difference + # shown above in _null_virtual_mac_command for dut in self.duts: - dut.config([VIRT_NULL]) + virt_null = self._null_virtual_mac_command(dut) + dut.config([virt_null]) response = dut.api('varp').get() self.assertIsNotNone(response) - self.assertEqual(response['mac_address'], None) + self.assertIn(response['mac_address'], [None, '00:00:00:00:00:00']) def test_set_mac_address_with_value(self): for dut in self.duts: - dut.config([VIRT_NULL]) + virt_null = self._null_virtual_mac_command(dut) + 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') @@ -76,7 +93,8 @@ def test_set_mac_address_with_value(self): def test_change_mac_address(self): for dut in self.duts: - dut.config([VIRT_NULL, VIRT_ENTRY_A]) + virt_null = self._null_virtual_mac_command(dut) + 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') @@ -85,7 +103,8 @@ def test_change_mac_address(self): def test_remove_mac_address(self): for dut in self.duts: - dut.config([VIRT_NULL, VIRT_ENTRY_A]) + virt_null = self._null_virtual_mac_command(dut) + 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(disable=True) @@ -94,13 +113,15 @@ def test_remove_mac_address(self): def test_set_mac_address_with_bad_value(self): for dut in self.duts: - dut.config([VIRT_NULL]) + virt_null = self._null_virtual_mac_command(dut) + 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): diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py index 33db3d1..b3183c8 100644 --- a/test/unit/test_api_varp.py +++ b/test/unit/test_api_varp.py @@ -83,7 +83,7 @@ def test_set_mac_address_with_positional_value(self): def test_set_mac_address_with_disable(self): func = function('set_mac_address', disable=True) - cmds = 'no ip virtual-router mac-address' + cmds = 'no ip virtual-router mac-address 00:11:22:33:44:55' self.eapi_positive_config_test(func, cmds) def test_set_mac_address_with_no_value(self): @@ -96,7 +96,7 @@ def test_set_mac_address_with_bad_value(self): def test_set_mac_address_with_default(self): func = function('set_mac_address', default=True) - cmds = 'default ip virtual-router mac-address' + cmds = 'default ip virtual-router mac-address 00:11:22:33:44:55' self.eapi_positive_config_test(func, cmds) From 808bbc90be17f37da9edfb790ba0220b78a41595 Mon Sep 17 00:00:00 2001 From: mharista Date: Fri, 20 Jan 2017 13:28:39 -0500 Subject: [PATCH 23/44] Add support for eAPI expandAliases parameter and allow Node methods to send extra eAPI parameters. --- pyeapi/client.py | 31 ++++++++++++++++++++++--------- pyeapi/eapilib.py | 16 ++++++++++++---- test/system/test_client.py | 29 ++++++++++++++++++++++++++--- test/unit/test_eapilib.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 645d11a..624abec 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -499,7 +499,7 @@ def enable_authentication(self, password): """ self._enablepwd = str(password).strip() - def config(self, commands): + def config(self, commands, **kwargs): """Configures the node with the specified commands This method is used to send configuration commands to the node. It @@ -512,6 +512,9 @@ def config(self, commands): a list. The list of commands will also be prepended with the necessary commands to put the session in config mode. + **kwargs: Additional keyword arguments for expanded eAPI + functionality. Only supported eAPI params are used in building + the request Returns: The config method will return a list of dictionaries with the @@ -523,7 +526,7 @@ def config(self, commands): # push the configure command onto the command stack commands.insert(0, 'configure terminal') - response = self.run_commands(commands) + response = self.run_commands(commands, **kwargs) if self.autorefresh: self.refresh() @@ -562,7 +565,7 @@ def section(self, regex, config='running_config'): return config[block_start:block_end] def enable(self, commands, encoding='json', strict=False, - send_enable=True): + send_enable=True, **kwargs): """Sends the array of commands to the node in enable mode This method will send the commands to the node and evaluate @@ -580,6 +583,9 @@ def enable(self, commands, encoding='json', strict=False, command with text encoding if JSON encoding fails send_enable (bool): If True the enable command will be prepended to the command list automatically. + **kwargs: Additional keyword arguments for expanded eAPI + functionality. Only supported eAPI params are used in building + the request Returns: A dict object that includes the response for each command along @@ -608,7 +614,8 @@ def enable(self, commands, encoding='json', strict=False, # there in error and both are now present to avoid breaking # existing scripts. 'response' will be removed in a future release. if strict: - responses = self.run_commands(commands, encoding, send_enable) + responses = self.run_commands(commands, encoding, send_enable, + **kwargs) for index, response in enumerate(responses): results.append(dict(command=commands[index], result=response, @@ -617,13 +624,15 @@ def enable(self, commands, encoding='json', strict=False, else: for command in commands: try: - resp = self.run_commands(command, encoding, send_enable) + resp = self.run_commands(command, encoding, send_enable, + **kwargs) results.append(dict(command=command, result=resp[0], encoding=encoding)) except CommandError as exc: if exc.error_code == 1003: - resp = self.run_commands(command, 'text', send_enable) + resp = self.run_commands(command, 'text', send_enable, + **kwargs) results.append(dict(command=command, result=resp[0], encoding='text')) @@ -631,12 +640,13 @@ def enable(self, commands, encoding='json', strict=False, raise return results - def run_commands(self, commands, encoding='json', send_enable=True): + def run_commands(self, commands, encoding='json', send_enable=True, + **kwargs): """Sends the commands over the transport to the device This method sends the commands to the device using the nodes transport. This is a lower layer function that shouldn't normally - need to be used, prefering instead to use config() or enable(). + need to be used, preferring instead to use config() or enable(). Args: commands (list): The ordered list of commands to send to the @@ -645,6 +655,9 @@ def run_commands(self, commands, encoding='json', send_enable=True): excpected response. send_enable (bool): If True the enable command will be prepended to the command list automatically. + **kwargs: Additional keyword arguments for expanded eAPI + functionality. Only supported eAPI params are used in building + the request Returns: This method will return the raw response from the connection @@ -670,7 +683,7 @@ def run_commands(self, commands, encoding='json', send_enable=True): else: commands.insert(0, 'enable') - response = self._connection.execute(commands, encoding) + response = self._connection.execute(commands, encoding, **kwargs) # pop enable command from the response only if we sent enable if send_enable: diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index d2d6123..cd07fcc 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -42,6 +42,7 @@ import base64 import logging import ssl +import re try: # Try Python 3.x import first @@ -282,6 +283,9 @@ def request(self, commands, encoding=None, reqid=None, **kwargs): parameter in the eAPI request reqid (string): A custom value to assign to the request ID field. This value is automatically generated if not passed + **kwargs: Additional keyword arguments for expanded eAPI + functionality. Only supported eAPI params are used in building + the request Returns: A JSON encoding request structure that can be send over eAPI @@ -290,8 +294,10 @@ def request(self, commands, encoding=None, reqid=None, **kwargs): commands = make_iterable(commands) reqid = id(self) if reqid is None else reqid params = {"version": 1, "cmds": commands, "format": encoding} - if 'autoComplete' in kwargs: + if "autoComplete" in kwargs: params["autoComplete"] = kwargs["autoComplete"] + if "expandAliases" in kwargs: + params["expandAliases"] = kwargs["expandAliases"] return json.dumps({"jsonrpc": "2.0", "method": "runCmds", "params": params, "id": str(reqid)}) @@ -395,9 +401,11 @@ def send(self, data): if 'error' in decoded: (code, msg, err, out) = self._parse_error_message(decoded) - if "unexpected keyword argument 'autoComplete'" in msg: - auto_msg = ("autoComplete parameter is not supported in" - " this version of EOS.") + pattern = "unexpected keyword argument '(.*)'" + match = re.search(pattern, msg) + if match: + auto_msg = ("%s parameter is not supported in this" + " version of EOS." % match.group(1)) _LOGGER.error(auto_msg) msg = msg + ". " + auto_msg raise CommandError(code, msg, command_error=err, output=out) diff --git a/test/system/test_client.py b/test/system/test_client.py index f30fffa..9383d29 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -147,8 +147,7 @@ def test_execute_with_autocomplete(self): # autocomplete feature available. If system tests are run on one of # those version of EOS this system test will fail. for dut in self.duts: - result = dut.connection.execute(['show version'], encoding='json') - version = result['result'][0]['version'] + version = self._dut_eos_version(dut) version = version.split('.') if int(version[0]) >= 4 and int(version[1]) >= 17: result = dut.connection.execute(['sh ver'], encoding='json', @@ -156,11 +155,35 @@ def test_execute_with_autocomplete(self): self.assertIn('version', result['result'][0]) else: # Verify exception thrown for EOS version that does not - # support autocomplete parameter with EAPI + # support autoComplete parameter with EAPI with self.assertRaises(pyeapi.eapilib.CommandError): dut.connection.execute(['sh ver'], encoding='json', autoComplete=True) + def test_execute_with_expandaliases(self): + # There are some versions of EOS before 4.17.x that have the + # expandaliases feature available. If system tests are run on one of + # those version of EOS this system test will fail. + for dut in self.duts: + # configure an alias for show version command + dut.config(['alias test show version']) + version = self._dut_eos_version(dut) + version = version.split('.') + if int(version[0]) >= 4 and int(version[1]) >= 17: + result = dut.connection.execute(['test'], encoding='json', + expandAliases=True) + self.assertIn('version', result['result'][0]) + else: + # Verify exception thrown for EOS version that does not + # support expandAliases parameter with EAPI + with self.assertRaises(pyeapi.eapilib.CommandError): + dut.connection.execute(['test'], encoding='json', + expandAliases=True) + + def _dut_eos_version(self, dut): + result = dut.connection.execute(['show version'], encoding='json') + return result['result'][0]['version'] + def tearDown(self): for dut in self.duts: dut.config("no enable secret") diff --git a/test/unit/test_eapilib.py b/test/unit/test_eapilib.py index cd9199b..cfe78ba 100644 --- a/test/unit/test_eapilib.py +++ b/test/unit/test_eapilib.py @@ -145,6 +145,27 @@ def test_send_raises_autocomplete_command_error(self): " of EOS.") self.assertIn(match, error.message) + def test_send_raises_expandaliases_command_error(self): + message = "runCmds() got an unexpected keyword argument" \ + " 'expandAliases'" + error = dict(code=9999, message=message, data=[{'errors': ['test']}]) + response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) + response_json = json.dumps(response_dict) + + mock_transport = Mock(name='transport') + mockcfg = {'getresponse.return_value.read.return_value': response_json} + mock_transport.configure_mock(**mockcfg) + + instance = pyeapi.eapilib.EapiConnection() + instance.transport = mock_transport + + try: + instance.send('test') + except pyeapi.eapilib.CommandError as error: + match = ("expandAliases parameter is not supported in this version" + " of EOS.") + self.assertIn(match, error.message) + def test_request_adds_autocomplete(self): instance = pyeapi.eapilib.EapiConnection() request = instance.request(['sh ver'], encoding='json', @@ -152,6 +173,13 @@ def test_request_adds_autocomplete(self): data = json.loads(request) self.assertIn('autoComplete', data['params']) + def test_request_adds_expandaliases(self): + instance = pyeapi.eapilib.EapiConnection() + request = instance.request(['test'], encoding='json', + expandAliases=True) + data = json.loads(request) + self.assertIn('expandAliases', data['params']) + def test_request_ignores_unknown_param(self): instance = pyeapi.eapilib.EapiConnection() request = instance.request(['sh ver'], encoding='json', From 725684d8f966f387d2357b483d6a38d8ecdcfc95 Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 24 Jan 2017 09:54:35 -0500 Subject: [PATCH 24/44] Convert double quotes to single quotes. --- pyeapi/eapilib.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index cd07fcc..e83474a 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -293,13 +293,13 @@ def request(self, commands, encoding=None, reqid=None, **kwargs): """ commands = make_iterable(commands) reqid = id(self) if reqid is None else reqid - params = {"version": 1, "cmds": commands, "format": encoding} - if "autoComplete" in kwargs: - params["autoComplete"] = kwargs["autoComplete"] - if "expandAliases" in kwargs: - params["expandAliases"] = kwargs["expandAliases"] - return json.dumps({"jsonrpc": "2.0", "method": "runCmds", - "params": params, "id": str(reqid)}) + params = {'version': 1, 'cmds': commands, 'format': encoding} + if 'autoComplete' in kwargs: + params['autoComplete'] = kwargs['autoComplete'] + if 'expandAliases' in kwargs: + params['expandAliases'] = kwargs['expandAliases'] + return json.dumps({'jsonrpc': '2.0', 'method': 'runCmds', + 'params': params, 'id': str(reqid)}) def send(self, data): """Sends the eAPI request to the destination node @@ -363,7 +363,7 @@ def send(self, data): code and error message from the eAPI response. """ try: - _LOGGER.debug("Request content: {}".format(data)) + _LOGGER.debug('Request content: {}'.format(data)) # debug('eapi_request: %s' % data) self.transport.putrequest('POST', '/command-api') @@ -387,10 +387,10 @@ def send(self, data): response = self.transport.getresponse() response_content = response.read() - _LOGGER.debug("Response: status:{status}, reason:{reason}".format( + _LOGGER.debug('Response: status:{status}, reason:{reason}'.format( status=response.status, reason=response.reason)) - _LOGGER.debug("Response content: {}".format(response_content)) + _LOGGER.debug('Response content: {}'.format(response_content)) # Work around for Python 2.7/3.x compatibility if not type(response_content) == str: @@ -404,10 +404,10 @@ def send(self, data): pattern = "unexpected keyword argument '(.*)'" match = re.search(pattern, msg) if match: - auto_msg = ("%s parameter is not supported in this" - " version of EOS." % match.group(1)) + auto_msg = ('%s parameter is not supported in this' + ' version of EOS.' % match.group(1)) _LOGGER.error(auto_msg) - msg = msg + ". " + auto_msg + msg = msg + '. ' + auto_msg raise CommandError(code, msg, command_error=err, output=out) return decoded From f30808d1fe6124496b1250a83924b9de11a6d8c6 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 1 Feb 2017 11:17:16 -0500 Subject: [PATCH 25/44] Change hardcoded path in eapi connection to use transport path. --- pyeapi/eapilib.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index e83474a..806d0c6 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -68,6 +68,7 @@ def https_connection_factory(path, host, port, context=None, timeout=60): 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 @@ -85,6 +86,7 @@ def __init__(self, message, commands=None): self.commands = commands super(EapiError, self).__init__(message) + class CommandError(EapiError): """Base exception raised for command errors @@ -160,7 +162,6 @@ def __init__(self, connection_type, message, commands=None): super(ConnectionError, self).__init__(message) - class SocketConnection(HTTPConnection): def __init__(self, path, timeout=60): @@ -179,6 +180,7 @@ def connect(self): self.sock.settimeout(self.timeout) self.sock.connect(self.path) + class HttpConnection(HTTPConnection): def __init__(self, path, *args, **kwargs): @@ -191,6 +193,7 @@ def __str__(self): def __repr__(self): return 'http://%s:%s/%s' % (self.host, self.port, self.path) + class HttpsConnection(HTTPSConnection): def __init__(self, path, *args, **kwargs): @@ -203,6 +206,7 @@ def __str__(self): def __repr__(self): return 'https://%s:%s/%s' % (self.host, self.port, self.path) + class EapiConnection(object): """Creates a connection to eAPI for sending and receiving eAPI requests @@ -251,7 +255,6 @@ def authentication(self, username, password): _LOGGER.debug('Autentication string is: {}'.format(self._auth)) - def request(self, commands, encoding=None, reqid=None, **kwargs): """Generates an eAPI request object @@ -366,14 +369,14 @@ def send(self, data): _LOGGER.debug('Request content: {}'.format(data)) # debug('eapi_request: %s' % data) - self.transport.putrequest('POST', '/command-api') + self.transport.putrequest('POST', self.transport.path) self.transport.putheader('Content-type', 'application/json-rpc') self.transport.putheader('Content-length', '%d' % len(data)) if self._auth: self.transport.putheader('Authorization', - 'Basic %s' % (self._auth)) + 'Basic %s' % self._auth) if int(sys.version[0]) > 2: # For Python 3.x compatibility @@ -449,10 +452,7 @@ def _parse_error_message(self, message): err = ' '.join(message['error']['data'][-1]['errors']) out = message['error']['data'] - return (code, msg, err, out) - - - + return code, msg, err, out def execute(self, commands, encoding='json', **kwargs): """Executes the list of commands on the destination node @@ -495,12 +495,14 @@ def execute(self, commands, encoding='json', **kwargs): self.error = exc raise + class SocketEapiConnection(EapiConnection): def __init__(self, path=None, timeout=60, **kwargs): super(SocketEapiConnection, self).__init__() path = path or DEFAULT_UNIX_SOCKET self.transport = SocketConnection(path, timeout) + class HttpLocalEapiConnection(EapiConnection): def __init__(self, port=None, path=None, timeout=60, **kwargs): super(HttpLocalEapiConnection, self).__init__() @@ -509,6 +511,7 @@ def __init__(self, port=None, path=None, timeout=60, **kwargs): self.transport = HttpConnection(path, 'localhost', int(port), timeout=timeout) + class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, timeout=60, **kwargs): @@ -518,6 +521,7 @@ def __init__(self, host, port=None, path=None, username=None, self.transport = HttpConnection(path, host, int(port), timeout=timeout) self.authentication(username, password) + class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, context=None, timeout=60, **kwargs): From f785e7a6099580dd50fccce009b106db13788ec4 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 8 Feb 2017 14:38:29 -0500 Subject: [PATCH 26/44] Add node attributes from show version command --- pyeapi/client.py | 43 ++++++++++++++++++++++++++++++++++++++ test/system/test_client.py | 7 +++++++ test/unit/test_client.py | 31 +++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/pyeapi/client.py b/pyeapi/client.py index 624abec..938d014 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -454,6 +454,9 @@ def __init__(self, connection, **kwargs): self._connection = connection self._running_config = None self._startup_config = None + self._version = None + self._version_number = None + self._model = None self._enablepwd = kwargs.get('enablepwd') self.autorefresh = kwargs.get('autorefresh', True) @@ -485,6 +488,46 @@ def startup_config(self): as_string=True) return self._startup_config + @property + def version(self): + if self._version: + return self._version + self._get_version_properties() + return self._version + + @property + def version_number(self): + if self._version_number: + return self._version_number + self._get_version_properties() + return self._version_number + + @property + def model(self): + if self._model: + return self._model + self._get_version_properties() + return self._model + + def _get_version_properties(self): + """Parses version and model information out of 'show version' output + and uses the output to populate class properties. + """ + # Parse out version info + output = self.enable('show version') + self._version = str(output[0]['result']['version']) + match = re.match('[\d.\d]+', output[0]['result']['version']) + if match: + self._version_number = str(match.group(0)) + else: + self._version_number = str(output[0]['result']['version']) + # Parse out model number + match = re.search('\d\d\d\d', output[0]['result']['modelName']) + if match: + self._model = str(match.group(0)) + else: + self._model = str(output[0]['result']['modelName']) + def enable_authentication(self, password): """Configures the enable mode authentication password diff --git a/test/system/test_client.py b/test/system/test_client.py index 9383d29..084cfc2 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -57,6 +57,13 @@ def setUp(self): # enable password on the dut and clear it on tearDown dut.config("enable secret %s" % dut._enablepwd) + def test_populate_version_properties(self): + for dut in self.duts: + result = dut.run_commands('show version') + self.assertEqual(dut.version, result[0]['version']) + self.assertIn(dut.model, result[0]['modelName']) + self.assertIn(dut.version_number, result[0]['version']) + def test_enable_single_command(self): for dut in self.duts: result = dut.run_commands('show version') diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 8e43d38..0157f4f 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -51,6 +51,35 @@ def setUp(self): self.connection = Mock() self.node = pyeapi.client.Node(self.connection) + def test_get_version_properties_match_version_number_no_match_model(self): + self.node.enable = Mock() + version = '4.17.1.1F-3512479.41711F (engineering build)' + self.node.enable.return_value = [{'result': {'version': version, + 'modelName': 'vEOS'}}] + self.node._get_version_properties() + self.assertEqual(self.node.version_number, '4.17.1.1') + self.assertEqual(self.node.model, 'vEOS') + + def test_get_version_properties_no_match_version_number_match_model(self): + self.node.enable = Mock() + version = 'special-4.17.1.1F-3512479.41711F (engineering build)' + model = 'DCS-7260QX-64-F' + self.node.enable.return_value = [{'result': {'version': version, + 'modelName': model}}] + self.node._get_version_properties() + self.assertEqual(self.node.version_number, version) + self.assertEqual(self.node.model, '7260') + + def test_version_properties_populate(self): + self.node.enable = Mock() + version = '4.17.1.1F-3512479.41711F (engineering build)' + self.node.enable.return_value = [{'result': {'version': version, + 'modelName': 'vEOS'}}] + self.node.version_number + self.assertEqual(self.node.version_number, '4.17.1.1') + self.assertEqual(self.node.version, version) + self.assertEqual(self.node.model, 'vEOS') + def test_enable_with_single_command(self): command = random_string() response = ['enable', command] @@ -71,8 +100,6 @@ def test_no_enable_with_single_command(self): self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) - - def test_enable_with_multiple_commands(self): commands = list() for i in range(0, random_int(2, 5)): From 7f394f32506fcaf931b9d6a2b41e78a3d184ca03 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 8 Feb 2017 15:09:46 -0500 Subject: [PATCH 27/44] Added more unit tests for new attributes. --- test/unit/test_client.py | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 0157f4f..5bda0cf 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -311,7 +311,7 @@ def test_node_returns_startup_config(self): node.get_config = get_config_mock self.assertIsInstance(node.startup_config, str) - def test_node_returns_cached_startup_confgi(self): + def test_node_returns_cached_startup_config(self): node = pyeapi.client.Node(None) config_file = open(get_fixture('running_config.text')) config = config_file.read() @@ -319,6 +319,49 @@ def test_node_returns_cached_startup_confgi(self): node._startup_config = config self.assertEqual(node.startup_config, config) + def test_node_returns_version(self): + node = pyeapi.client.Node(None) + version = '4.17.1.1F-3512479.41711F (engineering build)' + node.enable = Mock() + node.enable.return_value = [{'result': {'version': version, + 'modelName': 'vEOS'}}] + self.assertIsInstance(node.version, str) + self.assertEqual(node.version, version) + + def test_node_returns_cached_version(self): + node = pyeapi.client.Node(None) + node._version = '4.16.7R' + self.assertEqual(node.version, '4.16.7R') + + def test_node_returns_version_number(self): + node = pyeapi.client.Node(None) + version = '4.17.1.1F-3512479.41711F (engineering build)' + node.enable = Mock() + node.enable.return_value = [{'result': {'version': version, + 'modelName': 'vEOS'}}] + self.assertIsInstance(node.version_number, str) + self.assertIn(node.version_number, version) + + def test_node_returns_cached_version_number(self): + node = pyeapi.client.Node(None) + node._version_number = '4.16.7' + self.assertEqual(node.version_number, '4.16.7') + + def test_node_returns_model(self): + node = pyeapi.client.Node(None) + version = '4.17.1.1F-3512479.41711F (engineering build)' + model = 'DCS-7260QX-64-F' + node.enable = Mock() + node.enable.return_value = [{'result': {'version': version, + 'modelName': model}}] + self.assertIsInstance(node.model, str) + self.assertIn(node.model, model) + + def test_node_returns_cached_model(self): + node = pyeapi.client.Node(None) + node._model = '7777' + self.assertEqual(node.model, '7777') + def test_connect_default_type(self): transport = Mock() with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): From 8964b2feb41628788ad2f7eec5a20e596026c62c Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 16 Feb 2017 11:28:23 -0500 Subject: [PATCH 28/44] Add support for creating and deleting ethernet subinterfaces --- pyeapi/api/interfaces.py | 55 ++++++++++++++++++++++- test/system/test_api_interfaces.py | 63 +++++++++++++++++++++++--- test/unit/test_api_interfaces.py | 71 ++++++++++++++++++++++++++---- 3 files changed, 173 insertions(+), 16 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 3abd14d..b93a13b 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -394,8 +394,12 @@ def create(self, name): NotImplementedError: creating Ethernet interfaces is not supported """ + ver = self.node.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + return self.configure('interface %s' % name) raise NotImplementedError('creating Ethernet interfaces is ' - 'not supported') + 'not supported in EOS version %s' + % self.node.version) def delete(self, name): """Deleting Ethernet interfaces is currently not supported @@ -407,8 +411,55 @@ def delete(self, name): NotImplementedError: Deleting Ethernet interfaces is not supported """ + ver = self.node.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + return self.configure('no interface %s' % name) raise NotImplementedError('deleting Ethernet interfaces is ' - 'not supported') + 'not supported in EOS version %s' + % self.node.version) + + def create_subinterface(self, phys_int_name, logical_int, vlan=None): + """Create Ethernet sub interface + + Args: + phys_int_name (str): The physical interface name + logical_int (int): The logical sub interface number + vlan (int): The vlan number + + Raises: + NotImplementedError: creating sub Ethernet interfaces is not + supported in EOS before version 4.15 + + """ + ver = self.node.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + if self.node.api('ipinterfaces').create(phys_int_name): + int_comm = 'interface %s.%s' % (phys_int_name, logical_int) + vlan_comm = 'encapsulation dot1q vlan %s' % (vlan or + logical_int) + return self.configure([int_comm, vlan_comm]) + raise NotImplementedError('creating Ethernet sub interfaces is ' + 'not supported in EOS version %s' + % self.node.version) + + def delete_subinterface(self, phys_int_name, logical_int): + """Delete Ethernet sub interface + + Args: + phys_int_name (str): The physical interface name + logical_int (int): The logical sub interface number + + Raises: + NotImplementedError: Deleting Ethernet interfaces is not supported + + """ + ver = self.node.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + command = 'no interface %s.%s' % (phys_int_name, logical_int) + return self.configure(command) + raise NotImplementedError('deleting Ethernet sub interfaces is ' + 'not supported in EOS version %s' + % self.node.version) def set_flowcontrol_send(self, name, value=None, default=False, disable=False): diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index f2eae96..b9a56e8 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -78,9 +78,11 @@ def test_create_and_return_true(self): self.assertIn('Loopback0', config[0]['interfaces']) def test_create_ethernet_raises_not_implemented_error(self): - with self.assertRaises(NotImplementedError): - for dut in self.duts: - dut.api('interfaces').create(random_interface(dut)) + for dut in self.duts: + ver = dut.version_number.split('.') + if not (int(ver[0]) >= 4 and int(ver[1]) >= 15): + with self.assertRaises(NotImplementedError): + dut.api('interfaces').create(random_interface(dut)) def test_delete_and_return_true(self): for dut in self.duts: @@ -91,9 +93,58 @@ def test_delete_and_return_true(self): self.assertNotIn('Loopback0', config[0]['interfaces']) def test_delete_ethernet_raises_not_implemented_error(self): - with self.assertRaises(NotImplementedError): - for dut in self.duts: - dut.api('interfaces').delete(random_interface(dut)) + for dut in self.duts: + ver = dut.version_number.split('.') + if not (int(ver[0]) >= 4 and int(ver[1]) >= 15): + with self.assertRaises(NotImplementedError): + dut.api('interfaces').delete(random_interface(dut)) + + def test_create_and_delete_ethernet_sub_interface_no_vlan(self): + for dut in self.duts: + ver = dut.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + # Default Ethernet1 + dut.api('interfaces').default('Ethernet1') + # Create subint Ethernet1.1 + res = dut.api('interfaces').create_subinterface('Ethernet1', 1) + self.assertTrue(res) + config = dut.run_commands('show interfaces Ethernet1') + self.assertEqual( + config[0]['interfaces']['Ethernet1']['forwardingModel'], + 'routed') + command = 'show running-config interfaces Ethernet1.1' + output = dut.run_commands(command, encoding='text') + vlan_config = 'encapsulation dot1q vlan 1' + self.assertIn(vlan_config, output[0]['output']) + # Delete subint Ethernet1.1 + res = dut.api('interfaces').delete_subinterface('Ethernet1', 1) + self.assertTrue(res) + output = dut.run_commands(command, encoding='text') + self.assertEqual(output[0]['output'], '') + + def test_create_and_delete_ethernet_sub_interface_with_vlan(self): + for dut in self.duts: + ver = dut.version_number.split('.') + if int(ver[0]) >= 4 and int(ver[1]) >= 15: + # Default Ethernet1 + dut.api('interfaces').default('Ethernet1') + # Create subint Ethernet1.1 with vlan 4 + res = dut.api('interfaces').create_subinterface( + 'Ethernet1', 1, 4) + self.assertTrue(res) + config = dut.run_commands('show interfaces Ethernet1') + self.assertEqual( + config[0]['interfaces']['Ethernet1']['forwardingModel'], + 'routed') + command = 'show running-config interfaces Ethernet1.1' + output = dut.run_commands(command, encoding='text') + vlan_config = 'encapsulation dot1q vlan 4' + self.assertIn(vlan_config, output[0]['output']) + # Delete subint Ethernet1.1 + res = dut.api('interfaces').delete_subinterface('Ethernet1', 1) + self.assertTrue(res) + output = dut.run_commands(command, encoding='text') + self.assertEqual(output[0]['output'], '') def test_default(self): for dut in self.duts: diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 4ddc31a..e1f74c4 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -160,23 +160,78 @@ def test_get(self): self.assertEqual(values, result) def test_instance_functions(self): + self.node._version_number = '4.17.1' for intf in self.INTERFACES: for name in ['create', 'delete', 'default']: if name == 'create': - if intf[0:2] not in ['Et', 'Ma']: - cmds = 'interface %s' % intf - func = function(name, intf) - self.eapi_positive_config_test(func, cmds) + cmds = 'interface %s' % intf + func = function(name, intf) + self.eapi_positive_config_test(func, cmds) elif name == 'delete': - if intf[0:2] not in ['Et', 'Ma']: - cmds = 'no interface %s' % intf - func = function(name, intf) - self.eapi_positive_config_test(func, cmds) + cmds = 'no interface %s' % intf + func = function(name, intf) + self.eapi_positive_config_test(func, cmds) elif name == 'default': cmds = 'default interface %s' % intf func = function(name, intf) self.eapi_positive_config_test(func, cmds) + def test_instance_functions_exceptions(self): + self.node._version_number = '4.14.9' + self.node._version = '4.14.9F' + for intf in self.INTERFACES: + for name in ['create', 'delete']: + if name == 'create': + cmds = 'interface %s' % intf + func = function(name, intf) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) + elif name == 'delete': + cmds = 'no interface %s' % intf + func = function(name, intf) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) + + def test_create_subinterface_no_vlan(self): + self.node._version_number = '4.17.1' + for intf in self.INTERFACES: + cmds = ['interface %s.%s' % (intf, 1), + 'encapsulation dot1q vlan 1'] + func = function('create_subinterface', intf, 1) + self.eapi_positive_config_test(func, cmds) + + def test_create_subinterface_vlan(self): + self.node._version_number = '4.17.1' + for intf in self.INTERFACES: + cmds = ['interface %s.%s' % (intf, 1), + 'encapsulation dot1q vlan 2'] + func = function('create_subinterface', intf, 1, 2) + self.eapi_positive_config_test(func, cmds) + + def test_create_subinterface_exception(self): + self.node._version_number = '4.14.9' + self.node._version = '4.14.9F' + for intf in self.INTERFACES: + cmds = ['interface %s.%s' % (intf, 1), + 'encapsulation dot1q vlan 1'] + func = function('create_subinterface', intf, 1) + self.eapi_exception_config_test(func, NotImplementedError, cmds) + + def test_delete_subinterface(self): + self.node._version_number = '4.17.1' + for intf in self.INTERFACES: + cmd = 'no interface %s.%s' % (intf, 1) + func = function('delete_subinterface', intf, 1) + self.eapi_positive_config_test(func, cmd) + + def test_delete_subinterface_exception(self): + self.node._version_number = '4.14.9' + self.node._version = '4.14.9F' + for intf in self.INTERFACES: + cmd = 'no interface %s.%s' % (intf, 1) + func = function('delete_subinterface', intf, 1) + self.eapi_exception_config_test(func, NotImplementedError, cmd) + def test_set_flowcontrol_with_value(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: From 29d21d551056620b388c1e91125faae7477ad733 Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 16 Feb 2017 19:18:57 -0500 Subject: [PATCH 29/44] Changes based on code review comments. --- pyeapi/api/interfaces.py | 109 ++++++++---------- test/system/test_api_interfaces.py | 178 ++++++++++++++++++++--------- test/unit/test_api_interfaces.py | 108 ++++++++--------- 3 files changed, 218 insertions(+), 177 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index b93a13b..f4aa177 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -253,6 +253,34 @@ def default(self, name): """ return self.configure('default interface %s' % name) + def set_encapsulation(self, name, vid, default=False, disable=False): + """Configures the subinterface encapsulation value + + Args: + name (string): The interface identifier. It must be a full + interface name (ie Ethernet, not Et) + vid (int): The vlan id number + default (boolean): Specifies to default the subinterface + encapsulation + disable (boolean): Specifies to disable the subinterface + encapsulation + + Returns: + True if the operation succeeds otherwise False is returned + """ + if '.' not in name: + raise NotImplementedError('parameter encapsulation can only be' + ' set on subinterfaces') + if name[0:2] not in ['Et', 'Po']: + raise NotImplementedError('parameter encapsulation can only be' + ' set on Ethernet and Port-Channel' + ' subinterfaces') + commands = ['interface %s' % name] + commands.append(self.command_builder('encapsulation dot1q vlan', + str(vid), default=default, + disable=disable)) + return self.configure(commands) + def set_description(self, name, value=None, default=False, disable=False): """Configures the interface description @@ -385,81 +413,36 @@ def _parse_flowcontrol_receive(self, config): return dict(flowcontrol_receive=value) def create(self, name): - """Creating Ethernet interfaces is currently not supported + """Create an Ethernet sub interface Args: - name (string): The interface name + name (string): The sub interface name. Ex: Ethernet1.1 Raises: - NotImplementedError: creating Ethernet interfaces is not supported - + NotImplementedError: creating physical Ethernet interfaces is not + supported. Only subinterfaces can be created. """ - ver = self.node.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - return self.configure('interface %s' % name) - raise NotImplementedError('creating Ethernet interfaces is ' - 'not supported in EOS version %s' - % self.node.version) + if '.' not in name: + raise NotImplementedError('creating physical Ethernet interfaces' + ' is not supported. Only subinterfaces' + ' can be created') + return self.configure(['interface %s' % name]) def delete(self, name): - """Deleting Ethernet interfaces is currently not supported + """Delete an Ethernet sub interfaces Args: - name (string): The interface name + name (string): The sub interface name. Ex: Ethernet1.1 Raises: - NotImplementedError: Deleting Ethernet interfaces is not supported - - """ - ver = self.node.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - return self.configure('no interface %s' % name) - raise NotImplementedError('deleting Ethernet interfaces is ' - 'not supported in EOS version %s' - % self.node.version) - - def create_subinterface(self, phys_int_name, logical_int, vlan=None): - """Create Ethernet sub interface - - Args: - phys_int_name (str): The physical interface name - logical_int (int): The logical sub interface number - vlan (int): The vlan number - - Raises: - NotImplementedError: creating sub Ethernet interfaces is not - supported in EOS before version 4.15 - - """ - ver = self.node.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - if self.node.api('ipinterfaces').create(phys_int_name): - int_comm = 'interface %s.%s' % (phys_int_name, logical_int) - vlan_comm = 'encapsulation dot1q vlan %s' % (vlan or - logical_int) - return self.configure([int_comm, vlan_comm]) - raise NotImplementedError('creating Ethernet sub interfaces is ' - 'not supported in EOS version %s' - % self.node.version) - - def delete_subinterface(self, phys_int_name, logical_int): - """Delete Ethernet sub interface - - Args: - phys_int_name (str): The physical interface name - logical_int (int): The logical sub interface number - - Raises: - NotImplementedError: Deleting Ethernet interfaces is not supported - + NotImplementedError: creating physical Ethernet interfaces is not + supported. Only subinterfaces can be created. """ - ver = self.node.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - command = 'no interface %s.%s' % (phys_int_name, logical_int) - return self.configure(command) - raise NotImplementedError('deleting Ethernet sub interfaces is ' - 'not supported in EOS version %s' - % self.node.version) + if '.' not in name: + raise NotImplementedError('deleting physical Ethernet interfaces' + ' is not supported. Only subinterfaces' + ' can be created') + return self.configure(['no interface %s' % name]) def set_flowcontrol_send(self, name, value=None, default=False, disable=False): diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index b9a56e8..2bda510 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -79,10 +79,8 @@ def test_create_and_return_true(self): def test_create_ethernet_raises_not_implemented_error(self): for dut in self.duts: - ver = dut.version_number.split('.') - if not (int(ver[0]) >= 4 and int(ver[1]) >= 15): - with self.assertRaises(NotImplementedError): - dut.api('interfaces').create(random_interface(dut)) + with self.assertRaises(NotImplementedError): + dut.api('interfaces').create(random_interface(dut)) def test_delete_and_return_true(self): for dut in self.duts: @@ -94,57 +92,60 @@ def test_delete_and_return_true(self): def test_delete_ethernet_raises_not_implemented_error(self): for dut in self.duts: - ver = dut.version_number.split('.') - if not (int(ver[0]) >= 4 and int(ver[1]) >= 15): - with self.assertRaises(NotImplementedError): - dut.api('interfaces').delete(random_interface(dut)) - - def test_create_and_delete_ethernet_sub_interface_no_vlan(self): - for dut in self.duts: - ver = dut.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - # Default Ethernet1 - dut.api('interfaces').default('Ethernet1') - # Create subint Ethernet1.1 - res = dut.api('interfaces').create_subinterface('Ethernet1', 1) - self.assertTrue(res) - config = dut.run_commands('show interfaces Ethernet1') - self.assertEqual( - config[0]['interfaces']['Ethernet1']['forwardingModel'], - 'routed') - command = 'show running-config interfaces Ethernet1.1' - output = dut.run_commands(command, encoding='text') - vlan_config = 'encapsulation dot1q vlan 1' - self.assertIn(vlan_config, output[0]['output']) - # Delete subint Ethernet1.1 - res = dut.api('interfaces').delete_subinterface('Ethernet1', 1) - self.assertTrue(res) - output = dut.run_commands(command, encoding='text') - self.assertEqual(output[0]['output'], '') - - def test_create_and_delete_ethernet_sub_interface_with_vlan(self): - for dut in self.duts: - ver = dut.version_number.split('.') - if int(ver[0]) >= 4 and int(ver[1]) >= 15: - # Default Ethernet1 - dut.api('interfaces').default('Ethernet1') - # Create subint Ethernet1.1 with vlan 4 - res = dut.api('interfaces').create_subinterface( - 'Ethernet1', 1, 4) - self.assertTrue(res) - config = dut.run_commands('show interfaces Ethernet1') - self.assertEqual( - config[0]['interfaces']['Ethernet1']['forwardingModel'], - 'routed') - command = 'show running-config interfaces Ethernet1.1' - output = dut.run_commands(command, encoding='text') - vlan_config = 'encapsulation dot1q vlan 4' - self.assertIn(vlan_config, output[0]['output']) - # Delete subint Ethernet1.1 - res = dut.api('interfaces').delete_subinterface('Ethernet1', 1) - self.assertTrue(res) - output = dut.run_commands(command, encoding='text') - self.assertEqual(output[0]['output'], '') + with self.assertRaises(NotImplementedError): + dut.api('interfaces').delete(random_interface(dut)) + + def test_create_and_delete_ethernet_sub_interface(self): + for dut in self.duts: + # Default Ethernet1 + dut.api('interfaces').default('Ethernet1') + # Create subint Ethernet1.1 + res = dut.api('interfaces').create('Ethernet1.1') + self.assertTrue(res) + command = 'show running-config interfaces Ethernet1.1' + output = dut.run_commands(command, encoding='text') + self.assertIn('Ethernet1.1', output[0]['output']) + # Delete subint Ethernet1.1 + res = dut.api('interfaces').delete('Ethernet1.1') + self.assertTrue(res) + output = dut.run_commands(command, encoding='text') + self.assertEqual(output[0]['output'], '') + + def test_ethernet_set_and_unset_encapsulation(self): + for dut in self.duts: + # Default Ethernet1 + dut.api('interfaces').default('Ethernet1') + # Create subint Ethernet1.1 + res = dut.api('interfaces').create('Ethernet1.1') + self.assertTrue(res) + # Set encapsulation + res = dut.api('interfaces').set_encapsulation('Ethernet1.1', 4) + self.assertTrue(res) + command = 'show running-config interfaces Ethernet1.1' + output = dut.run_commands(command, encoding='text') + encap = 'encapsulation dot1q vlan 4' + self.assertIn(encap, output[0]['output']) + # Remove encapsulation + res = dut.api('interfaces').set_encapsulation('Ethernet1.1', 4, + disable=True) + self.assertTrue(res) + output = dut.run_commands(command, encoding='text') + self.assertNotIn(encap, output[0]['output']) + # Delete subint Ethernet1.1 + res = dut.api('interfaces').delete('Ethernet1.1') + self.assertTrue(res) + + def test_set_encapsulation_non_subintf_exception(self): + for dut in self.duts: + with self.assertRaises(NotImplementedError): + dut.api('interfaces').set_encapsulation(random_interface(dut), + 1) + + def test_set_encapsulation_non_supported_intf_exception(self): + for dut in self.duts: + with self.assertRaises(NotImplementedError): + dut.api('interfaces').set_encapsulation('Vlan1234', + 1) def test_default(self): for dut in self.duts: @@ -377,6 +378,73 @@ def test_minimum_links_invalid_value(self): minlinks) self.assertFalse(result) + def test_create_and_delete_portchannel_sub_interface(self): + for dut in self.duts: + et1 = random_interface(dut) + et2 = random_interface(dut, exclude=[et1]) + + 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']) + # Create subint Port-Channel1.1 + api = dut.api('interfaces') + result = api.create('Port-Channel1.1') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config interfaces Port-Channel1.1' + output = dut.run_commands(command, encoding='text') + self.assertIn('Port-Channel1.1', output[0]['output']) + # Delete subint Port-Channel1.1 + result = dut.api('interfaces').delete('Port-Channel1.1') + self.assertTrue(result) + output = dut.run_commands(command, encoding='text') + self.assertEqual(output[0]['output'], '') + # Remove port-channel and default interfaces + dut.config(['no interface Port-Channel1', + 'default interface %s' % et1, + 'default interface %s' % et2]) + + def test_set_and_unset_portchannel_sub_intf_encapsulation(self): + for dut in self.duts: + et1 = random_interface(dut) + et2 = random_interface(dut, exclude=[et1]) + + 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']) + # Create subint Port-Channel1.1 + api = dut.api('interfaces') + result = api.create('Port-Channel1.1') + self.assertTrue(result) + # Set encapsulation + result = api.set_encapsulation('Port-Channel1.1', 4) + self.assertTrue(result) + command = 'show running-config interfaces Port-Channel1.1' + output = dut.run_commands(command, encoding='text') + encap = 'encapsulation dot1q vlan 4' + self.assertIn(encap, output[0]['output']) + # Unset encapsulation + result = api.set_encapsulation('Port-Channel1.1', 4, default=True) + self.assertTrue(result) + output = dut.run_commands(command, encoding='text') + self.assertNotIn(encap, output[0]['output']) + # Delete subint Port-Channel1.1 + result = dut.api('interfaces').delete('Port-Channel1.1') + self.assertTrue(result) + output = dut.run_commands(command, encoding='text') + self.assertEqual(output[0]['output'], '') + # Remove port-channel and default interfaces + dut.config(['no interface Port-Channel1', + 'default interface %s' % et1, + 'default interface %s' % et2]) + class TestApiVxlanInterface(DutSystemTest): diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index e1f74c4..74ce60a 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -141,6 +141,35 @@ def test_set_shutdown_with_default(self): func = function('set_shutdown', intf, default=True) self.eapi_positive_config_test(func, cmds) + def test_set_encapsulation_non_subintf(self): + cmds = ['interface Ethernet1', 'encapsulation dot1q vlan 4'] + func = function('set_encapsulation', 'Ethernet1', 4) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) + + def test_set_encapsulation_non_supported_intf(self): + cmds = ['interface Vlan1234', 'encapsulation dot1q vlan 4'] + func = function('set_encapsulation', 'Vlan1234', 4) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) + + def test_set_encapsulation_ethernet_subintf(self): + cmds = ['interface Ethernet1.1', 'encapsulation dot1q vlan 4'] + func = function('set_encapsulation', 'Ethernet1.1', 4) + self.eapi_positive_config_test(func, cmds) + + def test_set_encapsulation_portchannel_subintf_disable(self): + cmds = ['interface Port-Channel1.1', 'no encapsulation dot1q vlan'] + func = function('set_encapsulation', 'Port-Channel1.1', 4, + disable=True) + self.eapi_positive_config_test(func, cmds) + + def test_set_encapsulation_ethernet_subintf_default(self): + cmds = ['interface Ethernet1.1', 'default encapsulation dot1q vlan'] + func = function('set_encapsulation', 'Ethernet1.1', 4, + default=True) + self.eapi_positive_config_test(func, cmds) + class TestApiEthernetInterface(EapiConfigUnitTest): @@ -160,16 +189,19 @@ def test_get(self): self.assertEqual(values, result) def test_instance_functions(self): - self.node._version_number = '4.17.1' for intf in self.INTERFACES: for name in ['create', 'delete', 'default']: if name == 'create': - cmds = 'interface %s' % intf - func = function(name, intf) + # Test create for subinterfaces + subintf = intf + '.1' + cmds = ['interface %s' % subintf] + func = function(name, subintf) self.eapi_positive_config_test(func, cmds) elif name == 'delete': - cmds = 'no interface %s' % intf - func = function(name, intf) + # Test delete for subinterfaces + subintf = intf + '.1' + cmds = ['no interface %s' % subintf] + func = function(name, subintf) self.eapi_positive_config_test(func, cmds) elif name == 'default': cmds = 'default interface %s' % intf @@ -177,60 +209,18 @@ def test_instance_functions(self): self.eapi_positive_config_test(func, cmds) def test_instance_functions_exceptions(self): - self.node._version_number = '4.14.9' - self.node._version = '4.14.9F' - for intf in self.INTERFACES: - for name in ['create', 'delete']: - if name == 'create': - cmds = 'interface %s' % intf - func = function(name, intf) - self.eapi_exception_config_test(func, NotImplementedError, - cmds) - elif name == 'delete': - cmds = 'no interface %s' % intf - func = function(name, intf) - self.eapi_exception_config_test(func, NotImplementedError, - cmds) - - def test_create_subinterface_no_vlan(self): - self.node._version_number = '4.17.1' - for intf in self.INTERFACES: - cmds = ['interface %s.%s' % (intf, 1), - 'encapsulation dot1q vlan 1'] - func = function('create_subinterface', intf, 1) - self.eapi_positive_config_test(func, cmds) - - def test_create_subinterface_vlan(self): - self.node._version_number = '4.17.1' - for intf in self.INTERFACES: - cmds = ['interface %s.%s' % (intf, 1), - 'encapsulation dot1q vlan 2'] - func = function('create_subinterface', intf, 1, 2) - self.eapi_positive_config_test(func, cmds) - - def test_create_subinterface_exception(self): - self.node._version_number = '4.14.9' - self.node._version = '4.14.9F' - for intf in self.INTERFACES: - cmds = ['interface %s.%s' % (intf, 1), - 'encapsulation dot1q vlan 1'] - func = function('create_subinterface', intf, 1) - self.eapi_exception_config_test(func, NotImplementedError, cmds) - - def test_delete_subinterface(self): - self.node._version_number = '4.17.1' - for intf in self.INTERFACES: - cmd = 'no interface %s.%s' % (intf, 1) - func = function('delete_subinterface', intf, 1) - self.eapi_positive_config_test(func, cmd) - - def test_delete_subinterface_exception(self): - self.node._version_number = '4.14.9' - self.node._version = '4.14.9F' - for intf in self.INTERFACES: - cmd = 'no interface %s.%s' % (intf, 1) - func = function('delete_subinterface', intf, 1) - self.eapi_exception_config_test(func, NotImplementedError, cmd) + intf = 'Ethernet1' + for name in ['create', 'delete']: + if name == 'create': + cmds = 'interface %s' % intf + func = function(name, intf) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) + elif name == 'delete': + cmds = 'no interface %s' % intf + func = function(name, intf) + self.eapi_exception_config_test(func, NotImplementedError, + cmds) def test_set_flowcontrol_with_value(self): for intf in self.INTERFACES: From 655ae5d582be0bd0ce87acc8211a245d40771c79 Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 16 Feb 2017 19:49:01 -0500 Subject: [PATCH 30/44] Added subinterface usage documentation. --- docs/subinterfaces.rst | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/subinterfaces.rst diff --git a/docs/subinterfaces.rst b/docs/subinterfaces.rst new file mode 100644 index 0000000..f369fca --- /dev/null +++ b/docs/subinterfaces.rst @@ -0,0 +1,45 @@ +Configuring Sub-interfaces Using Python Client for eAPI +====================================================== + +Sub-interfaces can be configured on Ethernet and Port-Channel interfaces. To do this in +eAPI simply call create or delete with your sub-interface name. + +import pyeapi +node = pyeapi.connect_to('veos01') +node.api('interfaces').create('Ethernet1.1') + +At this point the below should be in your running configuration. + +! +interface Ethernet1 +! +interface Ethernet1.1 +! + +Sub-interfaces require that the primary interface be in routed mode. + +node.api('ipinterfaces').create('Ethernet1') + +! +interface Ethernet1 + no switchport +! +interface Ethernet1.1 +! + +Sub-interfaces also require a vlan to be applied to them. + +node.api('interfaces').set_encapsulation('Ethernet1.1', 4) + +! +interface Ethernet1 + no switchport +! +interface Ethernet1.1 + encapsulation dot1q vlan 4 +! + +Using delete in the same format as create will remove the sub-interface. + +For more detailed information about configuring sub-interfaces in EOS, reference the user +manual for the version of EOS running on your switch. From 39bbc320d27b6d3069006e34e30c2763dcbef65e Mon Sep 17 00:00:00 2001 From: mharista Date: Fri, 17 Feb 2017 10:46:10 -0500 Subject: [PATCH 31/44] Documentation and comment updates. --- docs/examples.rst | 8 +++++ docs/index.rst | 1 + docs/subinterfaces.rst | 69 ++++++++++++++++++++++------------------ pyeapi/api/interfaces.py | 8 ++--- 4 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 docs/examples.rst diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..47ca47b --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,8 @@ +Configuration Examples Using pyeapi +=================================== + + +.. toctree:: + :maxdepth: 1 + + subinterfaces diff --git a/docs/index.rst b/docs/index.rst index f2aeb5c..6ec5c25 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ through Github issues. configfile modules requirements + examples contribute release-notes support diff --git a/docs/subinterfaces.rst b/docs/subinterfaces.rst index f369fca..5af93ca 100644 --- a/docs/subinterfaces.rst +++ b/docs/subinterfaces.rst @@ -1,45 +1,52 @@ -Configuring Sub-interfaces Using Python Client for eAPI -====================================================== +Configuring Subinterfaces Using Python Client for eAPI +======================================================= -Sub-interfaces can be configured on Ethernet and Port-Channel interfaces. To do this in -eAPI simply call create or delete with your sub-interface name. +Subinterfaces can be configured on Ethernet and Port-Channel interfaces. To do this in +pyeapi simply call create or delete with your subinterface name. -import pyeapi -node = pyeapi.connect_to('veos01') -node.api('interfaces').create('Ethernet1.1') +Subinterfaces require that the primary interface be in routed mode. + +.. code-block:: python + import pyeapi + node = pyeapi.connect_to('veos01') + node.api('ipinterfaces').create('Ethernet1') At this point the below should be in your running configuration. -! -interface Ethernet1 -! -interface Ethernet1.1 -! +.. code-block:: + ! + interface Ethernet1 + no switchport + ! -Sub-interfaces require that the primary interface be in routed mode. +Next step is to create the subinterface -node.api('ipinterfaces').create('Ethernet1') +.. code-block:: python + node.api('interfaces').create('Ethernet1.1') -! -interface Ethernet1 - no switchport -! -interface Ethernet1.1 -! +.. code-block:: + ! + interface Ethernet1 + no switchport + ! + interface Ethernet1.1 + ! -Sub-interfaces also require a vlan to be applied to them. +Subinterfaces also require a vlan to be applied to them. -node.api('interfaces').set_encapsulation('Ethernet1.1', 4) +.. code-block:: python + node.api('interfaces').set_encapsulation('Ethernet1.1', 4) -! -interface Ethernet1 - no switchport -! -interface Ethernet1.1 - encapsulation dot1q vlan 4 -! +.. code-block:: + ! + interface Ethernet1 + no switchport + ! + interface Ethernet1.1 + encapsulation dot1q vlan 4 + ! -Using delete in the same format as create will remove the sub-interface. +Using delete in the same format as create will remove the subinterface. -For more detailed information about configuring sub-interfaces in EOS, reference the user +For more detailed information about configuring subinterfaces in EOS, reference the user manual for the version of EOS running on your switch. diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index f4aa177..9b297b7 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -413,10 +413,10 @@ def _parse_flowcontrol_receive(self, config): return dict(flowcontrol_receive=value) def create(self, name): - """Create an Ethernet sub interface + """Create an Ethernet subinterface Args: - name (string): The sub interface name. Ex: Ethernet1.1 + name (string): The subinterface name. Ex: Ethernet1.1 Raises: NotImplementedError: creating physical Ethernet interfaces is not @@ -429,10 +429,10 @@ def create(self, name): return self.configure(['interface %s' % name]) def delete(self, name): - """Delete an Ethernet sub interfaces + """Delete an Ethernet subinterfaces Args: - name (string): The sub interface name. Ex: Ethernet1.1 + name (string): The subinterface name. Ex: Ethernet1.1 Raises: NotImplementedError: creating physical Ethernet interfaces is not From 4d83016e64bb6a959c3eeaacbf13fed6b3302aaf Mon Sep 17 00:00:00 2001 From: mharista Date: Mon, 27 Feb 2017 21:28:29 -0500 Subject: [PATCH 32/44] Base API for VRF support. --- pyeapi/api/vrfs.py | 352 +++++++++++++++++++++++++++++++ test/fixtures/running_config.vrf | 223 ++++++++++++++++++++ test/unit/test_api_vrfs.py | 186 ++++++++++++++++ 3 files changed, 761 insertions(+) create mode 100644 pyeapi/api/vrfs.py create mode 100644 test/fixtures/running_config.vrf create mode 100644 test/unit/test_api_vrfs.py diff --git a/pyeapi/api/vrfs.py b/pyeapi/api/vrfs.py new file mode 100644 index 0000000..f898fcb --- /dev/null +++ b/pyeapi/api/vrfs.py @@ -0,0 +1,352 @@ +# +# 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. +# +"""Module for working with EOS VRF resources + +The Vrfs resource provides configuration of VRF resources for an EOS +node. + +Parameters: + + name (string): The name parameter maps to the VRF name in EOS. Valid + values include any consecutive sequence of numbers, letters and + underscore up to the maximum number of characters. This parameter + is defaultable. + description (string): The vrf description set by the user + ipv4routing (bool): Tells whether IPv4 routing is enabled on the VRF + ipv6routing (bool): Tells whether IPv6 unicast routing is enabled on the + VRF + +""" + +import re + +from pyeapi.api import EntityCollection +from pyeapi.utils import make_iterable + +RD_RE = re.compile(r'(?:\srd\s)(?P.*)$', re.M) +DESCRIPTION_RE = re.compile(r'(?:description\s)(?P.*)$', re.M) +IP_REGEX = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$') + + + +def isrd(value): + """Checks if the argument is a valid VRF RD (route distinguisher) + + A valid RD has the following format admin_ID:local_assignment. The admin_ID + can be an AS number or globally assigned IPv4 address. The local_assignment + can be an integer between 0-65,535 if the admin_ID is an IPv4 address and + can be between 0-4,294,967,295 if the admin_ID is an AS number. If the + admin_ID is an AS number the local_assignment could also be in the form of + an IPv4 address. + + Args: + value: The value to check if is a valid VRF RD + + Returns: + True if the supplied value is a valid VRF RD otherwise False + """ + try: + admin_id, local_assignment = value.split(':') + if IP_REGEX.match(admin_id): + local_assignment = int(local_assignment) + if 0 <= local_assignment <= 65535: + return True + else: + admin_id = int(admin_id) + if 0 <= admin_id <= 65535: + if IP_REGEX.match(local_assignment): + return True + local_assignment = int(local_assignment) + if 0 <= local_assignment <= 4294967295: + return True + except ValueError: + pass + return False + + +class Vrfs(EntityCollection): + """The Vrfs class provides a configuration resource for VRFs + + The Vrfs class is derived from ResourceBase a standard set of methods + for working with VRF configurations on an EOS node. + + """ + + def get(self, value): + """Returns the VRF configuration as a resource dict. + + Args: + value (string): The vrf name to retrieve from the + running configuration. + + Returns: + A Python dict object containing the VRF attributes as + key/value pairs. + + """ + config = self.get_block('vrf definition %s' % value) + if not config: + return None + response = dict(vrf_name=value) + response.update(self._parse_rd(config)) + response.update(self._parse_description(config)) + config = self.get_block('no ip routing vrf %s' % value) + if config: + response['ipv4routing'] = False + else: + response['ipv4routing'] = True + config = self.get_block('no ipv6 unicast-routing vrf %s' % value) + if config: + response['ipv6routing'] = False + else: + response['ipv6routing'] = True + + return response + + def _parse_rd(self, config): + """ _parse_name scans the provided configuration block and extracts + the vrf rd. The return dict is intended to be merged into the response + dict. + + Args: + config (str): The vrf configuration block from the nodes running + configuration + + Returns: + dict: resource dict attribute + """ + match = RD_RE.search(config) + if match: + value = match.group('value') + else: + value = match + return dict(rd=value) + + def _parse_description(self, config): + """ _parse_description scans the provided configuration block and + extracts the vrf description value. The return dict is inteded to + be merged into the response dict. + + Args: + config (str): The vrf configuration block from the nodes + running configuration + + Returns: + dict: resource dict attribute + """ + value = DESCRIPTION_RE.search(config).group('value') + return dict(description=value) + + def getall(self): + """Returns a dict object of all VRFs in the running-config + + Returns: + A dict object of VRF attributes + + """ + vrfs_re = re.compile(r'(?<=^vrf definition\s)(\w+)', re.M) + + response = dict() + for vrf in vrfs_re.findall(self.config): + response[vrf] = self.get(vrf) + return response + + def create(self, vrf_name): + """ Creates a new VRF resource + + Args: + vrf_name (str): The VRF name to create + + Returns: + True if create was successful otherwise False + """ + command = 'vrf definition %s' % vrf_name + return self.configure(command) + + def delete(self, vrf_name): + """ Deletes a VRF from the running configuration + + Args: + vrf_name (str): The VRF name to delete + + Returns: + True if the operation was successful otherwise False + """ + command = 'no vrf definition %s' % vrf_name + return self.configure(command) + + def default(self, vrf_name): + """ Defaults the VRF configuration for given name + + Args: + vrf_name (str): The VRF name to default + + Returns: + True if the operation was successful otherwise False + """ + command = 'default vrf definition %s' % vrf_name + return self.configure(command) + + def configure_vrf(self, vrf_name, commands): + """ Configures the specified VRF using commands + + Args: + vrf_name (str): The VRF name to configure + commands: The list of commands to configure + + Returns: + True if the commands completed successfully + """ + commands = make_iterable(commands) + commands.insert(0, 'vrf definition %s' % vrf_name) + return self.configure(commands) + + def set_rd(self, vrf_name, rd): + """ Configures the VRF rd + + EosVersion: + 4.xx.xx + + Args: + vrf_name (str): The VRF name to set rd for + rd (str): The value to configure the vrf rd + + Returns: + True if the operation was successful otherwise False + """ + if not isrd(rd): + return False + cmds = self.command_builder('rd', value=rd) + return self.configure_vrf(vrf_name, cmds) + + def set_description(self, vrf_name, description=None, default=False, + disable=False): + """ Configures the VRF description + + EosVersion: + 4.xx.xx + + Args: + vrf_name (str): The VRF name to configure + description(str): The string to set the vrf description to + default (bool): Configures the vrf description to its default value + disable (bool): Negates the vrf description + + Returns: + True if the operation was successful otherwise False + """ + cmds = self.command_builder('description', value=description, + default=default, disable=disable) + return self.configure_vrf(vrf_name, cmds) + + def set_ipv4_routing(self, vrf_name, default=False, disable=False): + """ Configures ipv4 routing for the vrf + + EosVersion: + 4.xx.xx + + Args: + vrf_name (str): The VRF name to configure + default (bool): Configures ipv4 routing for the vrf value to + default if this value is true + disable (bool): Negates the ipv4 routing for the vrf if set to true + + Returns: + True if the operation was successful otherwise False + + """ + cmd = 'ip routing vrf %s' % vrf_name + if default: + cmd = 'default %s' % cmd + elif disable: + cmd = 'no %s' % cmd + cmd = make_iterable(cmd) + return self.configure(cmd) + + def set_ipv6_routing(self, vrf_name, default=False, disable=False): + """ Configures ipv6 unicast routing for the vrf + + EosVersion: + 4.xx.xx + + Args: + vrf_name (str): The VRF name to configure + default (bool): Configures ipv6 unicast routing for the vrf value + to default if this value is true + disable (bool): Negates the ipv6 unicast routing for the vrf if set + to true + + Returns: + True if the operation was successful otherwise False + + """ + cmd = 'ipv6 unicast-routing vrf %s' % vrf_name + if default: + cmd = 'default %s' % cmd + elif disable: + cmd = 'no %s' % cmd + cmd = make_iterable(cmd) + return self.configure(cmd) + + def set_interface(self, vrf_name, interface, default=False, disable=False): + """ Adds a VRF to an interface + + EosVersion: + 4.xx.xx + + Args: + vrf_name (str): The VRF name to configure + interface (str): The interface to add the VRF too + default (bool): Set interface VRF forwarding to default + disable (bool): Negate interface VRF forwarding + + Returns: + True if the operation was successful otherwise False + """ + cmds = ['interface %s' % interface] + cmds.append(self.command_builder('vrf forwarding', value=vrf_name, + default=default, disable=disable)) + return self.configure(cmds) + + +def instance(node): + """Returns an instance of Vrfs + + This method will create and return an instance of the Vrfs 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 Vrfs(node) diff --git a/test/fixtures/running_config.vrf b/test/fixtures/running_config.vrf new file mode 100644 index 0000000..1e31b48 --- /dev/null +++ b/test/fixtures/running_config.vrf @@ -0,0 +1,223 @@ +logging level VRF debugging +! +no snmp-server vrf default source-interface +snmp-server vrf default +! +vrf definition blah + rd 10:10 + description blah desc +! +vrf definition second + no description +! +interface Ethernet1 + no description + no shutdown + default load-interval + mtu 1500 + 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 + no traffic-loopback + default error-correction encoding + no error-correction reed-solomon bypass + switchport dot1q ethertype 0x8100 + no switchport + no encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + vrf forwarding blah + no ip proxy-arp + no ip local-proxy-arp + ip address 10.10.10.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 Ethernet1 + 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 + 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 + no channel-group + lacp rate normal + lacp port-priority 32768 + lldp transmit + lldp receive + no ip multicast static + ip mfib fastdrop + mpls ip + no msrp + no mvrp + default ntp serve + no ip pim sparse-mode + no ip pim bidirectional + no ip pim border-router + ip pim query-interval 30 + ip pim query-count 3.5 + 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 + 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 + no ip rip v2-broadcast + sflow enable + no ip virtual address +! +ip routing vrf blah +no ip icmp source-interface vrf blah +no ip routing vrf second +no ip icmp source-interface vrf second +! +no ipv6 unicast-routing vrf blah +no ipv6 unicast-routing vrf second +! +control-plane + ip access-group default-control-plane-acl in + ip access-group default-control-plane-acl vrf blah in + ip access-group default-control-plane-acl vrf second in + ipv6 access-group default-control-plane-acl in + ipv6 access-group default-control-plane-acl vrf blah in + ipv6 access-group default-control-plane-acl vrf second in +! +management api http-commands + protocol https port 443 + protocol http + no protocol http localhost port 8080 + no protocol unix-socket + no protocol https certificate + no protocol https ssl profile + no cors allowed-origin + protocol https cipher aes256-cbc aes128-cbc + protocol https key-exchange rsa diffie-hellman-ephemeral-rsa + protocol https mac hmac-sha1 + qos dscp 0 + no shutdown + vrf default + no shutdown +! +management cvx + shutdown + no server host + no source-interface + heartbeat-interval 20 + heartbeat-timeout 60 + no ssl profile + vrf default + service debug + no shutdown + interval 1 +! +management xmpp + shutdown + no connection unencrypted permit + vrf default + session privilege 1 +! \ No newline at end of file diff --git a/test/unit/test_api_vrfs.py b/test/unit/test_api_vrfs.py new file mode 100644 index 0000000..bc6c5c6 --- /dev/null +++ b/test/unit/test_api_vrfs.py @@ -0,0 +1,186 @@ +# +# 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 testlib import get_fixture, random_string, function +from testlib import EapiConfigUnitTest + +import pyeapi.api.vrfs + + +class TestApiVrfs(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiVrfs, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.vrfs.instance(None) + self.config = open(get_fixture('running_config.vrf')).read() + + def test_isrd_valid_value_number_number(self): + self.assertTrue(pyeapi.api.vrfs.isrd('10:10')) + + def test_isrd_valid_value_ipaddress_number(self): + self.assertTrue(pyeapi.api.vrfs.isrd('10.10.10.10:99')) + + def test_isrd_valid_value_number_ipaddress(self): + self.assertTrue(pyeapi.api.vrfs.isrd('99:10.10.10.10')) + + def test_isrd_invalid_with_string(self): + self.assertFalse(pyeapi.api.vrfs.isrd('a' + random_string())) + + def test_isrd_invalid_value_number_one_out_of_range(self): + self.assertFalse(pyeapi.api.vrfs.isrd('70000:100')) + + def test_isrd_invalid_value_number_two_out_of_range(self): + self.assertFalse(pyeapi.api.vrfs.isrd('5000:5000000000')) + + def test_isrd_invalid_first_ipaddress(self): + self.assertFalse(pyeapi.api.vrfs.isrd('255.255.255.300:10')) + + def test_isrd_invalid_second_ipaddress(self): + self.assertFalse(pyeapi.api.vrfs.isrd('10:355.255.255.0')) + + def test_isrd_invalid_ipaddress_number_two_out_of_range(self): + self.assertFalse(pyeapi.api.vrfs.isrd('10.10.10.1:70000')) + + def test_isrd_invalid_ipaddress_ipaddress(self): + self.assertFalse(pyeapi.api.vrfs.isrd('10.10.10.1:192.168.1.1')) + + def test_get(self): + result = self.instance.get('blah') + vrf = dict(rd='10:10', vrf_name='blah', description='blah desc', + ipv4routing=True, ipv6routing=False) + self.assertEqual(vrf, result) + + def test_get_not_configured(self): + self.assertIsNone(self.instance.get('notthere')) + + def test_getall(self): + result = self.instance.getall() + self.assertIsInstance(result, dict) + self.assertEqual(len(result), 2) + + def test_vrf_functions(self): + for name in ['create', 'delete', 'default']: + vrf_name = 'testvrf' + if name == 'create': + cmds = 'vrf definition %s' % vrf_name + elif name == 'delete': + cmds = 'no vrf definition %s' % vrf_name + elif name == 'default': + cmds = 'default vrf definition %s' % vrf_name + func = function(name, vrf_name) + self.eapi_positive_config_test(func, cmds) + + def test_set_rd(self): + vrf_name = 'testrdvrf' + rd = '10:10' + cmds = ['vrf definition %s' % vrf_name, 'rd %s' % rd] + func = function('set_rd', vrf_name, rd) + self.eapi_positive_config_test(func, cmds) + + def test_set_description(self): + for state in ['config', 'negate', 'default']: + vrf_name = 'testdescvrf' + if state == 'config': + description = 'testing' + cmds = ['vrf definition %s' % vrf_name, + 'description %s' % description] + func = function('set_description', vrf_name, description) + self.eapi_positive_config_test(func, cmds) + elif state == 'negate': + cmds = ['vrf definition %s' % vrf_name, 'no description'] + func = function('set_description', vrf_name, disable=True) + self.eapi_positive_config_test(func, cmds) + elif state == 'default': + cmds = ['vrf definition %s' % vrf_name, 'default description'] + func = function('set_description', vrf_name, default=True) + self.eapi_positive_config_test(func, cmds) + + def test_set_ipv4_routing(self): + for state in ['config', 'negate', 'default']: + vrf_name = 'testipv4vrf' + if state == 'config': + cmds = ['ip routing vrf %s' % vrf_name] + func = function('set_ipv4_routing', vrf_name) + self.eapi_positive_config_test(func, cmds) + elif state == 'negate': + cmds = ['no ip routing vrf %s' % vrf_name] + func = function('set_ipv4_routing', vrf_name, disable=True) + self.eapi_positive_config_test(func, cmds) + elif state == 'default': + cmds = ['default ip routing vrf %s' % vrf_name] + func = function('set_ipv4_routing', vrf_name, default=True) + self.eapi_positive_config_test(func, cmds) + + def test_set_ipv6_routing(self): + for state in ['config', 'negate', 'default']: + vrf_name = 'testipv6vrf' + if state == 'config': + cmds = ['ipv6 unicast-routing vrf %s' % vrf_name] + func = function('set_ipv6_routing', vrf_name) + self.eapi_positive_config_test(func, cmds) + elif state == 'negate': + cmds = ['no ipv6 unicast-routing vrf %s' % vrf_name] + func = function('set_ipv6_routing', vrf_name, disable=True) + self.eapi_positive_config_test(func, cmds) + elif state == 'default': + cmds = ['default ipv6 unicast-routing vrf %s' % vrf_name] + func = function('set_ipv6_routing', vrf_name, default=True) + self.eapi_positive_config_test(func, cmds) + + def test_set_interface(self): + for state in ['config', 'negate', 'default']: + vrf_name = 'testintvrf' + interface = 'Ethernet1' + if state == 'config': + cmds = ['interface %s' % interface, + 'vrf forwarding %s' % vrf_name] + func = function('set_interface', vrf_name, interface) + self.eapi_positive_config_test(func, cmds) + elif state == 'negate': + cmds = ['interface %s' % interface, 'no vrf forwarding'] + func = function('set_interface', vrf_name, interface, + disable=True) + self.eapi_positive_config_test(func, cmds) + elif state == 'default': + cmds = ['interface %s' % interface, 'default vrf forwarding'] + func = function('set_interface', vrf_name, interface, + default=True) + self.eapi_positive_config_test(func, cmds) + + +if __name__ == '__main__': + unittest.main() From ff17ef0dd00eea89ecca4476e626a96e1bf0e5f7 Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 28 Feb 2017 12:16:42 -0500 Subject: [PATCH 33/44] Add system tests --- pyeapi/api/vrfs.py | 4 +- test/system/test_api_vrfs.py | 190 +++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 test/system/test_api_vrfs.py diff --git a/pyeapi/api/vrfs.py b/pyeapi/api/vrfs.py index f898fcb..e7e6a1e 100644 --- a/pyeapi/api/vrfs.py +++ b/pyeapi/api/vrfs.py @@ -133,7 +133,7 @@ def get(self, value): return response def _parse_rd(self, config): - """ _parse_name scans the provided configuration block and extracts + """ _parse_rd scans the provided configuration block and extracts the vrf rd. The return dict is intended to be merged into the response dict. @@ -153,7 +153,7 @@ def _parse_rd(self, config): def _parse_description(self, config): """ _parse_description scans the provided configuration block and - extracts the vrf description value. The return dict is inteded to + extracts the vrf description value. The return dict is intended to be merged into the response dict. Args: diff --git a/test/system/test_api_vrfs.py b/test/system/test_api_vrfs.py new file mode 100644 index 0000000..bbd1188 --- /dev/null +++ b/test/system/test_api_vrfs.py @@ -0,0 +1,190 @@ +# +# 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 os +import unittest + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from testlib import random_int, random_string +from systestlib import DutSystemTest + + +class TestApiVrfs(DutSystemTest): + + def test_get(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'rd 10:10', 'description blah desc']) + response = dut.api('vrfs').get('blah') + values = dict(rd='10:10', vrf_name='blah', description='blah desc', + ipv4routing=False, ipv6routing=False) + self.assertEqual(values, response) + dut.config(['no vrf definition blah']) + + def test_getall(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'no vrf definition second', 'vrf definition second']) + response = dut.api('vrfs').getall() + self.assertIsInstance(response, dict, 'dut=%s' % dut) + self.assertEqual(len(response), 2) + for vrf_name in ['blah', 'second']: + self.assertIn(vrf_name, response, 'dut=%s' % dut) + dut.config(['no vrf definition blah', 'no vrf definition second']) + + def test_create_and_return_true(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah']) + result = dut.api('vrfs').create('blah') + self.assertTrue(result, 'dut=%s' % dut) + config = dut.run_commands('show vrf', encoding='text') + self.assertIn('blah', config[0]['output'], 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_create_and_return_false(self): + for dut in self.duts: + result = dut.api('vrfs').create('a%') + self.assertFalse(result, 'dut=%s' % dut) + + def test_delete_and_return_true(self): + for dut in self.duts: + dut.config('vrf definition blah') + result = dut.api('vrfs').delete('blah') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertNotIn('vrf definition blah', config[0]['output'], + 'dut=%s' % dut) + + def test_delete_and_return_false(self): + for dut in self.duts: + result = dut.api('vrfs').delete('a%') + self.assertFalse(result, 'dut=%s' % dut) + + def test_default(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'description test desc']) + result = dut.api('vrfs').default('blah') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertNotIn('vrf definition blah', config[0]['output'], + 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_set_rd(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah']) + result = dut.api('vrfs').set_rd('blah', '10:10') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('blah', config[0]['output'], 'dut=%s' % dut) + self.assertIn('10:10', config[0]['output'], 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_set_description(self): + for dut in self.duts: + description = random_string() + dut.config(['no vrf definition blah', 'vrf definition blah']) + result = dut.api('vrfs').set_description('blah', description) + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('description %s' % description, config[0]['output'], + 'dut=%s' % dut) + result = dut.api('vrfs').set_description('blah', default=True) + self.assertTrue(result, 'dut=%s' % dut) + config = dut.run_commands(command, encoding='text') + self.assertNotIn('description %s' % description, + config[0]['output'], 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_set_ipv4_routing(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'rd 10:10', 'description test']) + result = dut.api('vrfs').set_ipv4_routing('blah') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('ip routing vrf blah', config[0]['output'], + 'dut=%s' % dut) + result = dut.api('vrfs').set_ipv4_routing('blah', default=True) + self.assertTrue(result, 'dut=%s' % dut) + config = dut.run_commands(command, encoding='text') + self.assertIn('no ip routing vrf blah', config[0]['output'], + 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_set_ipv6_routing(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'rd 10:10', 'description test']) + result = dut.api('vrfs').set_ipv6_routing('blah') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config all section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('ipv6 unicast-routing vrf blah', config[0]['output'], + 'dut=%s' % dut) + result = dut.api('vrfs').set_ipv6_routing('blah', default=True) + self.assertTrue(result, 'dut=%s' % dut) + config = dut.run_commands(command, encoding='text') + self.assertIn('no ipv6 unicast-routing vrf blah', + config[0]['output'], 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + + def test_set_interface(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah', + 'rd 10:10', 'default interface Ethernet1', + 'interface Ethernet1', 'no switchport']) + result = dut.api('vrfs').set_interface('blah', 'Ethernet1') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config interfaces Ethernet1' + config = dut.run_commands(command, encoding='text') + self.assertIn('vrf forwarding blah', config[0]['output'], + 'dut=%s' % dut) + result = dut.api('vrfs').set_interface('blah', 'Ethernet1', + disable=True) + self.assertTrue(result, 'dut=%s' % dut) + config = dut.run_commands(command, encoding='text') + self.assertNotIn('vrf forwarding blah', config[0]['output'], + 'dut=%s' % dut) + dut.config(['no vrf definition blah', + 'default interface Ethernet1']) + + +if __name__ == '__main__': + unittest.main() From 7bf001217031cd7327ceb87306a545554762bbce Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 28 Feb 2017 14:25:00 -0500 Subject: [PATCH 34/44] Format cleanup. --- pyeapi/api/vrfs.py | 3 ++- test/system/test_api_vrfs.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/vrfs.py b/pyeapi/api/vrfs.py index e7e6a1e..a7fce4d 100644 --- a/pyeapi/api/vrfs.py +++ b/pyeapi/api/vrfs.py @@ -54,7 +54,8 @@ RD_RE = re.compile(r'(?:\srd\s)(?P.*)$', re.M) DESCRIPTION_RE = re.compile(r'(?:description\s)(?P.*)$', re.M) -IP_REGEX = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$') +IP_REGEX = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)' + r'{3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$') diff --git a/test/system/test_api_vrfs.py b/test/system/test_api_vrfs.py index bbd1188..316936e 100644 --- a/test/system/test_api_vrfs.py +++ b/test/system/test_api_vrfs.py @@ -35,7 +35,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) -from testlib import random_int, random_string +from testlib import random_string from systestlib import DutSystemTest From 37fbd03677bc0ed786a5da98e7c962025fa9af91 Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 28 Feb 2017 15:50:30 -0500 Subject: [PATCH 35/44] Added .vrf test file to MANIFEST.in. --- MANIFEST.in | 1 + pyeapi/api/vrfs.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 70e6dc3..d28ebb9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,6 +17,7 @@ recursive-include test *.ospf recursive-include test *.routemaps recursive-include test *.varp recursive-include test *.varp_null +recursive-include test *.vrf recursive-include test *.vrrp recursive-include test *.yaml recursive-include docs description.rst diff --git a/pyeapi/api/vrfs.py b/pyeapi/api/vrfs.py index a7fce4d..bde2166 100644 --- a/pyeapi/api/vrfs.py +++ b/pyeapi/api/vrfs.py @@ -321,6 +321,11 @@ def set_ipv6_routing(self, vrf_name, default=False, disable=False): def set_interface(self, vrf_name, interface, default=False, disable=False): """ Adds a VRF to an interface + Notes: + Requires interface to be in routed mode. Must apply ip address + after VRF has been applied. This feature can also be accessed + through the interfaces api. + EosVersion: 4.xx.xx From e0924a978ee5d8910658109d915fcc0f2540cd4a Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 1 Mar 2017 08:40:45 -0500 Subject: [PATCH 36/44] Completed VRF base module code coverage. --- test/fixtures/running_config.vrf | 9 +++++++++ test/unit/test_api_vrfs.py | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/test/fixtures/running_config.vrf b/test/fixtures/running_config.vrf index 1e31b48..9228560 100644 --- a/test/fixtures/running_config.vrf +++ b/test/fixtures/running_config.vrf @@ -10,6 +10,10 @@ vrf definition blah vrf definition second no description ! +vrf definition test + rd 200:500 + no description +! interface Ethernet1 no description no shutdown @@ -175,17 +179,22 @@ ip routing vrf blah no ip icmp source-interface vrf blah no ip routing vrf second no ip icmp source-interface vrf second +no ip routing vrf test +no ip icmp source-interface vrf test ! no ipv6 unicast-routing vrf blah no ipv6 unicast-routing vrf second +ipv6 unicast-routing vrf test ! control-plane ip access-group default-control-plane-acl in ip access-group default-control-plane-acl vrf blah in ip access-group default-control-plane-acl vrf second in + ip access-group default-control-plane-acl vrf test in ipv6 access-group default-control-plane-acl in ipv6 access-group default-control-plane-acl vrf blah in ipv6 access-group default-control-plane-acl vrf second in + ipv6 access-group default-control-plane-acl vrf test in ! management api http-commands protocol https port 443 diff --git a/test/unit/test_api_vrfs.py b/test/unit/test_api_vrfs.py index bc6c5c6..c4099f0 100644 --- a/test/unit/test_api_vrfs.py +++ b/test/unit/test_api_vrfs.py @@ -83,6 +83,10 @@ def test_get(self): vrf = dict(rd='10:10', vrf_name='blah', description='blah desc', ipv4routing=True, ipv6routing=False) self.assertEqual(vrf, result) + result2 = self.instance.get('test') + vrf2 = dict(rd='200:500', vrf_name='test', description='!', + ipv4routing=False, ipv6routing=True) + self.assertEqual(vrf2, result2) def test_get_not_configured(self): self.assertIsNone(self.instance.get('notthere')) @@ -90,7 +94,7 @@ def test_get_not_configured(self): def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) - self.assertEqual(len(result), 2) + self.assertEqual(len(result), 3) def test_vrf_functions(self): for name in ['create', 'delete', 'default']: @@ -111,6 +115,12 @@ def test_set_rd(self): func = function('set_rd', vrf_name, rd) self.eapi_positive_config_test(func, cmds) + def test_set_rd_invalid(self): + vrf_name = 'testbadrdvrf' + rd = '300.199.301.5:10' + func = function('set_rd', vrf_name, rd) + self.eapi_negative_config_test(func) + def test_set_description(self): for state in ['config', 'negate', 'default']: vrf_name = 'testdescvrf' From 615edc3921bb933ef5af2c7200914feaaea71e57 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 1 Mar 2017 10:12:12 -0500 Subject: [PATCH 37/44] Add VRF config to interfaces API. --- pyeapi/api/interfaces.py | 22 ++++++++++++++++++++++ test/system/test_api_interfaces.py | 23 +++++++++++++++++++++++ test/unit/test_api_interfaces.py | 7 +++++++ 3 files changed, 52 insertions(+) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 9b297b7..2fc45a9 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -547,6 +547,28 @@ def set_sflow(self, name, value=None, default=False, disable=False): default=default, disable=disable)) return self.configure(commands) + def set_vrf(self, name, vrf, default=False, disable=False): + """Applies a VRF to the interface + + Note: VRF being applied to interface must already exist in switch + config. Ethernet port must be in routed mode. This functionality + can also be handled in the VRF api. + + Args: + name (str): The interface identifier. It must be a full + interface name (ie Ethernet, not Et) + vrf (str): The vrf name to be applied to the interface + default (bool): Specifies the default value for sFlow + disable (bool): Specifies to disable sFlow + + Returns: + True if the operation succeeds otherwise False is returned + """ + commands = ['interface %s' % name] + commands.append(self.command_builder('vrf forwarding', vrf, + default=default, disable=disable)) + return self.configure(commands) + class PortchannelInterface(BaseInterface): diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 2bda510..129106a 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -219,6 +219,29 @@ def test_set_sflow_default(self): intf, 'text') self.assertNotIn('no sflow enable', config[0]['output']) + def test_set_vrf(self): + for dut in self.duts: + intf = random_interface(dut) + dut.config('default interface %s' % intf) + # Verify set_vrf returns False if no vrf by name is configured + result = dut.api('interfaces').set_vrf(intf, 'test') + self.assertFalse(result) + dut.config('vrf definition test') + # Verify interface has vrf applied + result = dut.api('interfaces').set_vrf(intf, 'test') + self.assertTrue(result) + config = dut.run_commands('show running-config interfaces %s' % + intf, 'text') + self.assertIn('vrf forwarding test', config[0]['output']) + # Verify interface has vrf removed + result = dut.api('interfaces').set_vrf(intf, 'test', disable=True) + self.assertTrue(result) + config = dut.run_commands('show running-config interfaces %s' % + intf, 'text') + self.assertNotIn('vrf forwarding test', config[0]['output']) + # Remove test vrf + dut.config('no vrf definition test') + class TestPortchannelInterface(DutSystemTest): diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index 74ce60a..45a156b 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -294,6 +294,13 @@ def test_set_sflow_invalid_value_raises_value_error(self): func = function('set_sflow', intf, random_string()) self.eapi_exception_config_test(func, ValueError) + def test_set_vrf(self): + for intf in INTERFACES: + vrf = 'testvrf' + cmds = ['interface %s' % intf, 'vrf forwarding %s' % vrf] + func = function('set_vrf', intf, vrf) + self.eapi_positive_config_test(func, cmds) + class TestApiPortchannelInterface(EapiConfigUnitTest): From 2c7fa8f0e390120d1fe0a5c68e4851436809a199 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 1 Mar 2017 12:36:22 -0500 Subject: [PATCH 38/44] Added option for using VRFs with OSPF. --- pyeapi/api/ospf.py | 34 ++++++++++++++---- test/fixtures/running_config.ospf | 5 +++ test/system/test_api_ospf.py | 59 ++++++++++++++++++++++++------- test/unit/test_api_ospf.py | 20 +++++++++-- 4 files changed, 96 insertions(+), 22 deletions(-) diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py index 28c1325..771a796 100644 --- a/pyeapi/api/ospf.py +++ b/pyeapi/api/ospf.py @@ -48,14 +48,15 @@ def __init__(self, *args, **kwargs): super(Ospf, self).__init__(*args, **kwargs) pass - def get(self): + def get(self, vrf=None): """Returns the OSPF routing configuration Args: - None + vrf (str): VRF name to return OSPF routing config for Returns: dict: keys: router_id (int): OSPF router-id + ospf_vrf (str): VRF of the OSPF process networks (dict): All networks that are advertised in OSPF ospf_process_id (int): OSPF proc id @@ -65,13 +66,16 @@ def get(self): shutdown (bool): Gives the current shutdown off the process """ - - config = self.get_block('^router ospf .*') + match = '^router ospf .*' + if vrf: + match += ' vrf %s' % vrf + config = self.get_block(match) if not config: return None response = dict() response.update(self._parse_router_id(config)) + response.update(self._parse_vrf(config)) response.update(self._parse_networks(config)) response.update(self._parse_ospf_process_id(config)) response.update(self._parse_redistribution(config)) @@ -90,6 +94,19 @@ def _parse_ospf_process_id(self, config): match = re.search(r'^router ospf (\d+)', config) return dict(ospf_process_id=int(match.group(1))) + def _parse_vrf(self, config): + """Parses config file for the OSPF vrf name + + Args: + config(str): Running configuration + Returns: + dict: key: ospf_vrf (str) + """ + match = re.search(r'^router ospf \d+ vrf (.*)', config) + if match: + return dict(ospf_vrf=match.group(1)) + return dict(ospf_vrf='default') + def _parse_router_id(self, config): """Parses config file for the OSPF router ID @@ -200,11 +217,12 @@ def delete(self): command = 'no router ospf {}'.format(config['ospf_process_id']) return self.configure(command) - def create(self, ospf_process_id): - """Creates a OSPF process in the default VRF + def create(self, ospf_process_id, vrf=None): + """Creates a OSPF process in the specified VRF or the default VRF. Args: - ospf_process_id (str): The OSPF proccess Id value + ospf_process_id (str): The OSPF process Id value + vrf (str): The VRF to apply this OSPF process to Returns: bool: True if the command completed successfully Exception: @@ -215,6 +233,8 @@ def create(self, ospf_process_id): if not 0 < value < 65536: raise ValueError('ospf as must be between 1 and 65535') command = 'router ospf {}'.format(ospf_process_id) + if vrf: + command += ' vrf %s' % vrf return self.configure(command) def configure_ospf(self, cmd): diff --git a/test/fixtures/running_config.ospf b/test/fixtures/running_config.ospf index 92f03b6..a306340 100644 --- a/test/fixtures/running_config.ospf +++ b/test/fixtures/running_config.ospf @@ -26,4 +26,9 @@ router ospf 65000 point-to-point routes no graceful-restart ! +router ospf 10 vrf test + router-id 2.2.2.2 + network 172.18.10.0/24 area 0.0.0.0 + network 172.19.0.0/16 area 0.0.0.0 + shutdown ! diff --git a/test/system/test_api_ospf.py b/test/system/test_api_ospf.py index bed21cd..2ec61f5 100644 --- a/test/system/test_api_ospf.py +++ b/test/system/test_api_ospf.py @@ -36,27 +36,50 @@ from random import randint from systestlib import DutSystemTest -def clear_ospf_config(dut, id=None): - if id is None: + +def clear_ospf_config(dut, pid=None): + if pid is None: try: - id = int(dut.get_config(params="section ospf")[0].split()[2]) - dut.config(['no router ospf %d' % id]) + pid = int(dut.get_config(params="section ospf")[0].split()[2]) + dut.config(['no router ospf %d' % pid]) except IndexError: '''No OSPF configured''' pass + else: + dut.config(['no router ospf %d' % pid]) + class TestApiOspf(DutSystemTest): + def test_get(self): for dut in self.duts: clear_ospf_config(dut) - dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", - "redistribute bgp"]) + dut.config(["router ospf 1", "router-id 1.1.1.1", + "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() config = dict(router_id="1.1.1.1", ospf_process_id=1, - networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], - redistributions=[dict(protocol="bgp")], shutdown=False) + ospf_vrf='default', + networks=[dict(netmask='24', network="2.2.2.0", + area="0.0.0.0")], + redistributions=[dict(protocol="bgp")], + shutdown=False) self.assertEqual(ospf_response, config) + def test_get_with_vrf(self): + for dut in self.duts: + clear_ospf_config(dut) + dut.config(["router ospf 10 vrf test", "router-id 1.1.1.2", + "network 2.2.2.0/24 area 0", "redistribute bgp"]) + ospf_response = dut.api('ospf').get() + config = dict(router_id="1.1.1.2", ospf_process_id=10, + ospf_vrf='test', + networks=[dict(netmask='24', network="2.2.2.0", + area="0.0.0.0")], + redistributions=[dict(protocol="bgp")], + shutdown=False) + self.assertEqual(ospf_response, config) + clear_ospf_config(dut, 10) + def test_shutdown(self): for dut in self.duts: clear_ospf_config(dut) @@ -88,18 +111,28 @@ def test_delete(self): def test_create_valid_id(self): for dut in self.duts: clear_ospf_config(dut) - id = randint(1, 65536) + pid = randint(1, 65536) ospf = dut.api("ospf") - response = ospf.create(id) + response = ospf.create(pid) self.assertTrue(response) - self.assertIn("router ospf {}".format(id), dut.get_config()) + self.assertIn("router ospf {}".format(pid), dut.get_config()) def test_create_invalid_id(self): for dut in self.duts: clear_ospf_config(dut) - id = randint(70000, 100000) + pid = randint(70000, 100000) with self.assertRaises(ValueError): - dut.api("ospf").create(id) + dut.api("ospf").create(pid) + + def test_create_with_vrf(self): + for dut in self.duts: + clear_ospf_config(dut) + pid = randint(1, 65536) + ospf = dut.api("ospf") + response = ospf.create(pid, vrf='test') + self.assertTrue(response) + self.assertIn("router ospf {} vrf {}".format(pid, 'test'), + dut.get_config()) def test_configure_ospf(self): for dut in self.duts: diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index 5f48cd9..e4fa34d 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -17,10 +17,19 @@ def __init__(self, *args, **kwargs): self.instance = pyeapi.api.ospf.instance(None) self.config = open(get_fixture('running_config.ospf')).read() - def test_get(self): + def test_get_no_vrf(self): result = self.instance.get() - keys = ['networks', 'ospf_process_id', 'redistributions', 'router_id', 'shutdown'] + keys = ['networks', 'ospf_process_id', 'ospf_vrf', 'redistributions', + 'router_id', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertEqual(result['ospf_vrf'], 'default') + + def test_get_with_vrf(self): + result = self.instance.get(vrf='test') + keys = ['networks', 'ospf_process_id', 'ospf_vrf', 'redistributions', + 'router_id', 'shutdown'] + self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertEqual(result['ospf_vrf'], 'test') def test_create(self): for ospf_id in ['65000', 65000]: @@ -28,6 +37,13 @@ def test_create(self): cmds = 'router ospf {}'.format(ospf_id) self.eapi_positive_config_test(func, cmds) + def test_create_with_vrf(self): + for ospf_id in ['65000', 65000]: + vrf_name = 'test' + func = function('create', ospf_id, vrf_name) + cmds = 'router ospf {} vrf {}'.format(ospf_id, vrf_name) + self.eapi_positive_config_test(func, cmds) + def test_create_invalid_id(self): for ospf_id in ['66000', 66000]: with self.assertRaises(ValueError): From e6bd09a73fd75c67b269c71cb667e02826f46f5e Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 2 Mar 2017 12:08:27 -0500 Subject: [PATCH 39/44] Code review updates. --- pyeapi/api/interfaces.py | 6 +-- pyeapi/api/ospf.py | 8 +-- pyeapi/api/vrfs.py | 92 ++++++++++---------------------- test/fixtures/running_config.vrf | 2 +- test/system/test_api_ospf.py | 5 +- test/system/test_api_vrfs.py | 29 +++++++++- test/unit/test_api_ospf.py | 8 +-- test/unit/test_api_vrfs.py | 51 ++++-------------- 8 files changed, 79 insertions(+), 122 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 2fc45a9..2492624 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, Arista Networks, Inc. +# Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -558,8 +558,8 @@ def set_vrf(self, name, vrf, default=False, disable=False): name (str): The interface identifier. It must be a full interface name (ie Ethernet, not Et) vrf (str): The vrf name to be applied to the interface - default (bool): Specifies the default value for sFlow - disable (bool): Specifies to disable sFlow + default (bool): Specifies the default value for VRF + disable (bool): Specifies to disable VRF Returns: True if the operation succeeds otherwise False is returned diff --git a/pyeapi/api/ospf.py b/pyeapi/api/ospf.py index 771a796..a1c50ef 100644 --- a/pyeapi/api/ospf.py +++ b/pyeapi/api/ospf.py @@ -56,7 +56,7 @@ def get(self, vrf=None): Returns: dict: keys: router_id (int): OSPF router-id - ospf_vrf (str): VRF of the OSPF process + vrf (str): VRF of the OSPF process networks (dict): All networks that are advertised in OSPF ospf_process_id (int): OSPF proc id @@ -102,10 +102,10 @@ def _parse_vrf(self, config): Returns: dict: key: ospf_vrf (str) """ - match = re.search(r'^router ospf \d+ vrf (.*)', config) + match = re.search(r'^router ospf \d+ vrf (\w+)', config) if match: - return dict(ospf_vrf=match.group(1)) - return dict(ospf_vrf='default') + return dict(vrf=match.group(1)) + return dict(vrf='default') def _parse_router_id(self, config): """Parses config file for the OSPF router ID diff --git a/pyeapi/api/vrfs.py b/pyeapi/api/vrfs.py index bde2166..b09b529 100644 --- a/pyeapi/api/vrfs.py +++ b/pyeapi/api/vrfs.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, Arista Networks, Inc. +# Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -41,8 +41,8 @@ underscore up to the maximum number of characters. This parameter is defaultable. description (string): The vrf description set by the user - ipv4routing (bool): Tells whether IPv4 routing is enabled on the VRF - ipv6routing (bool): Tells whether IPv6 unicast routing is enabled on the + ipv4_routing (bool): Tells whether IPv4 routing is enabled on the VRF + ipv6_routing (bool): Tells whether IPv6 unicast routing is enabled on the VRF """ @@ -54,44 +54,6 @@ RD_RE = re.compile(r'(?:\srd\s)(?P.*)$', re.M) DESCRIPTION_RE = re.compile(r'(?:description\s)(?P.*)$', re.M) -IP_REGEX = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)' - r'{3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$') - - - -def isrd(value): - """Checks if the argument is a valid VRF RD (route distinguisher) - - A valid RD has the following format admin_ID:local_assignment. The admin_ID - can be an AS number or globally assigned IPv4 address. The local_assignment - can be an integer between 0-65,535 if the admin_ID is an IPv4 address and - can be between 0-4,294,967,295 if the admin_ID is an AS number. If the - admin_ID is an AS number the local_assignment could also be in the form of - an IPv4 address. - - Args: - value: The value to check if is a valid VRF RD - - Returns: - True if the supplied value is a valid VRF RD otherwise False - """ - try: - admin_id, local_assignment = value.split(':') - if IP_REGEX.match(admin_id): - local_assignment = int(local_assignment) - if 0 <= local_assignment <= 65535: - return True - else: - admin_id = int(admin_id) - if 0 <= admin_id <= 65535: - if IP_REGEX.match(local_assignment): - return True - local_assignment = int(local_assignment) - if 0 <= local_assignment <= 4294967295: - return True - except ValueError: - pass - return False class Vrfs(EntityCollection): @@ -122,14 +84,14 @@ def get(self, value): response.update(self._parse_description(config)) config = self.get_block('no ip routing vrf %s' % value) if config: - response['ipv4routing'] = False + response['ipv4_routing'] = False else: - response['ipv4routing'] = True + response['ipv4_routing'] = True config = self.get_block('no ipv6 unicast-routing vrf %s' % value) if config: - response['ipv6routing'] = False + response['ipv6_routing'] = False else: - response['ipv6routing'] = True + response['ipv6_routing'] = True return response @@ -181,17 +143,27 @@ def getall(self): response[vrf] = self.get(vrf) return response - def create(self, vrf_name): + def create(self, vrf_name, rd=None): """ Creates a new VRF resource + Note: A valid RD has the following format admin_ID:local_assignment. + The admin_ID can be an AS number or globally assigned IPv4 address. + The local_assignment can be an integer between 0-65,535 if the + admin_ID is an IPv4 address and can be between 0-4,294,967,295 if + the admin_ID is an AS number. If the admin_ID is an AS number the + local_assignment could also be in the form of an IPv4 address. + Args: vrf_name (str): The VRF name to create + rd (str): The value to configure the vrf rd Returns: True if create was successful otherwise False """ - command = 'vrf definition %s' % vrf_name - return self.configure(command) + commands = ['vrf definition %s' % vrf_name] + if rd: + commands.append('rd %s' % rd) + return self.configure(commands) def delete(self, vrf_name): """ Deletes a VRF from the running configuration @@ -232,10 +204,14 @@ def configure_vrf(self, vrf_name, commands): return self.configure(commands) def set_rd(self, vrf_name, rd): - """ Configures the VRF rd + """ Configures the VRF rd (route distinguisher) - EosVersion: - 4.xx.xx + Note: A valid RD has the following format admin_ID:local_assignment. + The admin_ID can be an AS number or globally assigned IPv4 address. + The local_assignment can be an integer between 0-65,535 if the + admin_ID is an IPv4 address and can be between 0-4,294,967,295 if + the admin_ID is an AS number. If the admin_ID is an AS number the + local_assignment could also be in the form of an IPv4 address. Args: vrf_name (str): The VRF name to set rd for @@ -244,8 +220,6 @@ def set_rd(self, vrf_name, rd): Returns: True if the operation was successful otherwise False """ - if not isrd(rd): - return False cmds = self.command_builder('rd', value=rd) return self.configure_vrf(vrf_name, cmds) @@ -253,9 +227,6 @@ def set_description(self, vrf_name, description=None, default=False, disable=False): """ Configures the VRF description - EosVersion: - 4.xx.xx - Args: vrf_name (str): The VRF name to configure description(str): The string to set the vrf description to @@ -272,9 +243,6 @@ def set_description(self, vrf_name, description=None, default=False, def set_ipv4_routing(self, vrf_name, default=False, disable=False): """ Configures ipv4 routing for the vrf - EosVersion: - 4.xx.xx - Args: vrf_name (str): The VRF name to configure default (bool): Configures ipv4 routing for the vrf value to @@ -296,9 +264,6 @@ def set_ipv4_routing(self, vrf_name, default=False, disable=False): def set_ipv6_routing(self, vrf_name, default=False, disable=False): """ Configures ipv6 unicast routing for the vrf - EosVersion: - 4.xx.xx - Args: vrf_name (str): The VRF name to configure default (bool): Configures ipv6 unicast routing for the vrf value @@ -326,9 +291,6 @@ def set_interface(self, vrf_name, interface, default=False, disable=False): after VRF has been applied. This feature can also be accessed through the interfaces api. - EosVersion: - 4.xx.xx - Args: vrf_name (str): The VRF name to configure interface (str): The interface to add the VRF too diff --git a/test/fixtures/running_config.vrf b/test/fixtures/running_config.vrf index 9228560..d093a6e 100644 --- a/test/fixtures/running_config.vrf +++ b/test/fixtures/running_config.vrf @@ -229,4 +229,4 @@ management xmpp no connection unencrypted permit vrf default session privilege 1 -! \ No newline at end of file +! diff --git a/test/system/test_api_ospf.py b/test/system/test_api_ospf.py index 2ec61f5..e7a968b 100644 --- a/test/system/test_api_ospf.py +++ b/test/system/test_api_ospf.py @@ -58,7 +58,7 @@ def test_get(self): "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() config = dict(router_id="1.1.1.1", ospf_process_id=1, - ospf_vrf='default', + vrf='default', networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], redistributions=[dict(protocol="bgp")], @@ -71,8 +71,7 @@ def test_get_with_vrf(self): dut.config(["router ospf 10 vrf test", "router-id 1.1.1.2", "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() - config = dict(router_id="1.1.1.2", ospf_process_id=10, - ospf_vrf='test', + config = dict(router_id="1.1.1.2", ospf_process_id=10, vrf='test', networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], redistributions=[dict(protocol="bgp")], diff --git a/test/system/test_api_vrfs.py b/test/system/test_api_vrfs.py index 316936e..df2d7b8 100644 --- a/test/system/test_api_vrfs.py +++ b/test/system/test_api_vrfs.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, Arista Networks, Inc. +# Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -47,7 +47,7 @@ def test_get(self): 'rd 10:10', 'description blah desc']) response = dut.api('vrfs').get('blah') values = dict(rd='10:10', vrf_name='blah', description='blah desc', - ipv4routing=False, ipv6routing=False) + ipv4_routing=False, ipv6_routing=False) self.assertEqual(values, response) dut.config(['no vrf definition blah']) @@ -71,11 +71,36 @@ def test_create_and_return_true(self): self.assertIn('blah', config[0]['output'], 'dut=%s' % dut) dut.config(['no vrf definition blah']) + def test_create_with_valid_rd(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah']) + result = dut.api('vrfs').create('blah', rd='10:10') + self.assertTrue(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('vrf definition blah', config[0]['output'], + 'dut=%s' % dut) + self.assertIn('rd 10:10', config[0]['output'], 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + def test_create_and_return_false(self): for dut in self.duts: result = dut.api('vrfs').create('a%') self.assertFalse(result, 'dut=%s' % dut) + def test_create_with_invalid_rd(self): + for dut in self.duts: + dut.config(['no vrf definition blah', 'vrf definition blah']) + result = dut.api('vrfs').create('blah', rd='192.168.1.1:99999999') + self.assertFalse(result, 'dut=%s' % dut) + command = 'show running-config section vrf' + config = dut.run_commands(command, encoding='text') + self.assertIn('vrf definition blah', config[0]['output'], + 'dut=%s' % dut) + self.assertNotIn('rd', config[0]['output'], + 'dut=%s' % dut) + dut.config(['no vrf definition blah']) + def test_delete_and_return_true(self): for dut in self.duts: dut.config('vrf definition blah') diff --git a/test/unit/test_api_ospf.py b/test/unit/test_api_ospf.py index e4fa34d..7b09956 100644 --- a/test/unit/test_api_ospf.py +++ b/test/unit/test_api_ospf.py @@ -19,17 +19,17 @@ def __init__(self, *args, **kwargs): def test_get_no_vrf(self): result = self.instance.get() - keys = ['networks', 'ospf_process_id', 'ospf_vrf', 'redistributions', + keys = ['networks', 'ospf_process_id', 'vrf', 'redistributions', 'router_id', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) - self.assertEqual(result['ospf_vrf'], 'default') + self.assertEqual(result['vrf'], 'default') def test_get_with_vrf(self): result = self.instance.get(vrf='test') - keys = ['networks', 'ospf_process_id', 'ospf_vrf', 'redistributions', + keys = ['networks', 'ospf_process_id', 'vrf', 'redistributions', 'router_id', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) - self.assertEqual(result['ospf_vrf'], 'test') + self.assertEqual(result['vrf'], 'test') def test_create(self): for ospf_id in ['65000', 65000]: diff --git a/test/unit/test_api_vrfs.py b/test/unit/test_api_vrfs.py index c4099f0..3428076 100644 --- a/test/unit/test_api_vrfs.py +++ b/test/unit/test_api_vrfs.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, Arista Networks, Inc. +# Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -48,44 +48,14 @@ def __init__(self, *args, **kwargs): self.instance = pyeapi.api.vrfs.instance(None) self.config = open(get_fixture('running_config.vrf')).read() - def test_isrd_valid_value_number_number(self): - self.assertTrue(pyeapi.api.vrfs.isrd('10:10')) - - def test_isrd_valid_value_ipaddress_number(self): - self.assertTrue(pyeapi.api.vrfs.isrd('10.10.10.10:99')) - - def test_isrd_valid_value_number_ipaddress(self): - self.assertTrue(pyeapi.api.vrfs.isrd('99:10.10.10.10')) - - def test_isrd_invalid_with_string(self): - self.assertFalse(pyeapi.api.vrfs.isrd('a' + random_string())) - - def test_isrd_invalid_value_number_one_out_of_range(self): - self.assertFalse(pyeapi.api.vrfs.isrd('70000:100')) - - def test_isrd_invalid_value_number_two_out_of_range(self): - self.assertFalse(pyeapi.api.vrfs.isrd('5000:5000000000')) - - def test_isrd_invalid_first_ipaddress(self): - self.assertFalse(pyeapi.api.vrfs.isrd('255.255.255.300:10')) - - def test_isrd_invalid_second_ipaddress(self): - self.assertFalse(pyeapi.api.vrfs.isrd('10:355.255.255.0')) - - def test_isrd_invalid_ipaddress_number_two_out_of_range(self): - self.assertFalse(pyeapi.api.vrfs.isrd('10.10.10.1:70000')) - - def test_isrd_invalid_ipaddress_ipaddress(self): - self.assertFalse(pyeapi.api.vrfs.isrd('10.10.10.1:192.168.1.1')) - def test_get(self): result = self.instance.get('blah') vrf = dict(rd='10:10', vrf_name='blah', description='blah desc', - ipv4routing=True, ipv6routing=False) + ipv4_routing=True, ipv6_routing=False) self.assertEqual(vrf, result) result2 = self.instance.get('test') vrf2 = dict(rd='200:500', vrf_name='test', description='!', - ipv4routing=False, ipv6routing=True) + ipv4_routing=False, ipv6_routing=True) self.assertEqual(vrf2, result2) def test_get_not_configured(self): @@ -100,7 +70,7 @@ def test_vrf_functions(self): for name in ['create', 'delete', 'default']: vrf_name = 'testvrf' if name == 'create': - cmds = 'vrf definition %s' % vrf_name + cmds = ['vrf definition %s' % vrf_name] elif name == 'delete': cmds = 'no vrf definition %s' % vrf_name elif name == 'default': @@ -108,6 +78,13 @@ def test_vrf_functions(self): func = function(name, vrf_name) self.eapi_positive_config_test(func, cmds) + def test_vrf_create_with_rd(self): + vrf_name = 'testvrfrd' + rd = '10:10' + cmds = ['vrf definition %s' % vrf_name, 'rd %s' % rd] + func = function('create', vrf_name, rd=rd) + self.eapi_positive_config_test(func, cmds) + def test_set_rd(self): vrf_name = 'testrdvrf' rd = '10:10' @@ -115,12 +92,6 @@ def test_set_rd(self): func = function('set_rd', vrf_name, rd) self.eapi_positive_config_test(func, cmds) - def test_set_rd_invalid(self): - vrf_name = 'testbadrdvrf' - rd = '300.199.301.5:10' - func = function('set_rd', vrf_name, rd) - self.eapi_negative_config_test(func) - def test_set_description(self): for state in ['config', 'negate', 'default']: vrf_name = 'testdescvrf' From 12e3479c18d310bc356ffddcfa82cbbf205e2f2a Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 2 Mar 2017 12:16:08 -0500 Subject: [PATCH 40/44] Fix flake8 error. --- test/unit/test_api_vrfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_api_vrfs.py b/test/unit/test_api_vrfs.py index 3428076..b01628c 100644 --- a/test/unit/test_api_vrfs.py +++ b/test/unit/test_api_vrfs.py @@ -35,7 +35,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) -from testlib import get_fixture, random_string, function +from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.vrfs From a2e50219674ccfcd51b2714833104511c539341b Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 2 Mar 2017 14:12:48 -0500 Subject: [PATCH 41/44] New line at end of running_confg.vrf. --- test/fixtures/running_config.vrf | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/running_config.vrf b/test/fixtures/running_config.vrf index d093a6e..823c921 100644 --- a/test/fixtures/running_config.vrf +++ b/test/fixtures/running_config.vrf @@ -230,3 +230,4 @@ management xmpp vrf default session privilege 1 ! + From 59edd90e669d55f9de209c965af3d952a4adc354 Mon Sep 17 00:00:00 2001 From: mharista Date: Mon, 6 Mar 2017 14:49:15 -0500 Subject: [PATCH 42/44] Add base extended ACL support. --- pyeapi/api/acl.py | 182 +++++++++++++++++++++++++++--- test/fixtures/running_config.text | 9 ++ test/system/test_api_acl.py | 102 ++++++++++++++++- test/unit/test_api_acl.py | 168 +++++++++++++++++++++++++-- 4 files changed, 434 insertions(+), 27 deletions(-) diff --git a/pyeapi/api/acl.py b/pyeapi/api/acl.py index 5303cce..4475cae 100644 --- a/pyeapi/api/acl.py +++ b/pyeapi/api/acl.py @@ -47,6 +47,9 @@ import netaddr from pyeapi.api import EntityCollection +from pyeapi.utils import ProxyCall + +VALID_ACLS = frozenset(['standard', 'extended']) def mask_to_prefixlen(mask): @@ -76,6 +79,75 @@ def prefixlen_to_mask(prefixlen): return str(netaddr.IPNetwork(addr).netmask) +class Acls(EntityCollection): + + def __init__(self, node, *args, **kwargs): + super(Acls, self).__init__(node, *args, **kwargs) + self._instances = dict() + + def get(self, name): + return self.get_instance(name)[name] + + def getall(self): + """Returns all ACLs in a dict object. + + Returns: + A Python dictionary object containing all ACL + configuration indexed by ACL name:: + + { + "": {...}, + "": {...} + } + + """ + acl_re = re.compile(r'^ip access-list (?:(standard) )?(.+)$', re.M) + response = {'standard': {}, 'extended': {}} + for acl_type, name in acl_re.findall(self.config): + acl = self.get(name) + if acl_type and acl_type == 'standard': + response['standard'][name] = acl + else: + response['extended'][name] = acl + return response + + def __getattr__(self, name): + return ProxyCall(self.marshall, name) + + def marshall(self, name, *args, **kwargs): + acl_name = args[0] + acl_instance = self.get_instance(acl_name) + if not hasattr(acl_instance, name): + raise AttributeError("'%s' object has no attribute '%s'" % + (acl_instance, name)) + method = getattr(acl_instance, name) + return method(*args, **kwargs) + + def get_instance(self, name): + if name in self._instances: + return self._instances[name] + acl_re = re.compile(r'^ip access-list (?:(standard) )?(%s)$' % name, + re.M) + match = acl_re.search(self.config) + if match: + acl_type = match.group(1) or 'extended' + return self.create_instance(match.group(2), acl_type) + return {name: None} + + def create_instance(self, name, acl_type): + if acl_type not in VALID_ACLS: + acl_type = 'standard' + acl_instance = ACL_CLASS_MAP.get(acl_type) + self._instances[name] = acl_instance(self.node) + return self._instances[name] + + def create(self, name, type='standard'): + # Create ACL instance for ACL type Standard or Extended then call + # create method for specific ACL class. + acl_instance = self.create_instance(name, type) + return acl_instance.create(name) + + class StandardAcls(EntityCollection): entry_re = re.compile(r'(\d+)' @@ -91,30 +163,21 @@ def get(self, name): config = self.get_block('ip access-list standard %s' % name) if not config: return None - resource = dict(name=name, type='standard') resource.update(self._parse_entries(config)) return resource - def getall(self): - resources = dict() - acls = re.compile(r'ip access-list standard ([^\s]+)') - for name in acls.findall(self.config): - resources[name] = self.get(name) - return resources - def _parse_entries(self, config): entries = dict() for item in re.finditer(r'\d+ [p|d].*$', config, re.M): match = self.entry_re.match(item.group(0)) - if match: - (seq, act, anyip, host, ip, mlen, mask, log) = match.groups() - entry = dict() - entry['action'] = act - entry['srcaddr'] = ip or '0.0.0.0' - entry['srclen'] = mlen or mask_to_prefixlen(mask) - entry['log'] = log is not None - entries[seq] = entry + (seq, act, anyip, host, ip, mlen, mask, log) = match.groups() + entry = dict() + entry['action'] = act + entry['srcaddr'] = ip or '0.0.0.0' + entry['srclen'] = mlen or mask_to_prefixlen(mask) + entry['log'] = log is not None + entries[seq] = entry return dict(entries=entries) def create(self, name): @@ -152,5 +215,90 @@ def remove_entry(self, name, seqno): return self.configure(cmds) +class ExtendedAcls(EntityCollection): + + entry_re = re.compile(r'(\d+)' + r'(?: ([p|d]\w+))' + r'(?: (\w+|\d+))' + r'(?: (any))?' + r'(?: (host))?' + r'(?: ([0-9]+(?:\.[0-9]+){3}))?' + r'(?:/([0-9]{1,2}))?' + r'(?: (eq [\w-]+))?' + r'(?: (any))?' + r'(?: (host))?' + r'(?: ([0-9]+(?:\.[0-9]+){3}))?' + r'(?:/([0-9]{1,2}))?' + r'(?: ([0-9]+(?:\.[0-9]+){3}))?' + r'(?: (eq [\w-]+))?' + r'(?: (.+))?') + + def get(self, name): + config = self.get_block('ip access-list %s' % name) + if not config: + return None + resource = dict(name=name, type='extended') + resource.update(self._parse_entries(config)) + return resource + + def _parse_entries(self, config): + entries = dict() + for item in re.finditer(r'\d+ [p|d].*$', config, re.M): + match = self.entry_re.match(item.group(0)) + entry = dict() + entry['action'] = match.group(2) + entry['protocol'] = match.group(3) + entry['srcaddr'] = match.group(6) or 'any' + entry['srclen'] = match.group(7) + entry['srcport'] = match.group(8) + entry['dstaddr'] = match.group(11) or 'any' + entry['dstlen'] = match.group(12) + entry['dstport'] = match.group(14) + entry['other'] = match.group(15) + entries[match.group(1)] = entry + return dict(entries=entries) + + def create(self, name): + return self.configure('ip access-list %s' % name) + + def delete(self, name): + return self.configure('no ip access-list %s' % name) + + def default(self, name): + return self.configure('default ip access-list %s' % name) + + def update_entry(self, name, seqno, action, protocol, srcaddr, + srcprefixlen, dstaddr, dstprefixlen, log=False): + cmds = ['ip access-list %s' % name] + cmds.append('no %s' % seqno) + entry = '%s %s %s %s/%s %s/%s' % (seqno, action, protocol, srcaddr, + srcprefixlen, dstaddr, dstprefixlen) + if log: + entry += ' log' + cmds.append(entry) + cmds.append('exit') + return self.configure(cmds) + + def add_entry(self, name, action, protocol, srcaddr, srcprefixlen, + dstaddr, dstprefixlen, log=False, seqno=None): + cmds = ['ip access-list %s' % name] + entry = '%s %s %s/%s %s/%s' % (action, protocol, srcaddr, + srcprefixlen, dstaddr, dstprefixlen) + if seqno is not None: + entry = '%s %s' % (seqno, entry) + if log: + entry += ' log' + cmds.append(entry) + cmds.append('exit') + return self.configure(cmds) + + def remove_entry(self, name, seqno): + cmds = ['ip access-list %s' % name, 'no %s' % seqno, 'exit'] + return self.configure(cmds) + + +ACL_CLASS_MAP = {'standard': StandardAcls, 'extended': ExtendedAcls} + + def instance(node): - return StandardAcls(node) + return Acls(node) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index d0ea092..200513b 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1579,6 +1579,15 @@ ip access-list standard test 50 permit 16.0.0.0/8 60 permit any log ! +ip access-list exttest + no statistics per-entry + fragment-rules + 10 permit tcp host 1.1.1.1 eq telnet host 2.2.2.2 log + 20 permit icmp 3.3.3.0/24 any ttl eq 2 tracked log + 30 permit tcp host 7.7.7.7 eq https any eq https log + 40 permit udp 4.4.0.0/16 any eq pkt-krb-ipsec + 50 deny ip any any log +! mac address-table aging-time 300 ! monitor hadoop diff --git a/test/system/test_api_acl.py b/test/system/test_api_acl.py index 454a98b..9ce7735 100644 --- a/test/system/test_api_acl.py +++ b/test/system/test_api_acl.py @@ -58,7 +58,7 @@ def test_getall(self): dut.config(['no ip access-list standard test', 'ip access-list standard test']) result = dut.api('acl').getall() - self.assertIn('test', result) + self.assertIn('test', result['standard']) def test_create(self): for dut in self.duts: @@ -138,7 +138,107 @@ def test_remove_entry(self): api.get_block('ip access-list standard test')) +class TestApiExtendedAcls(DutSystemTest): + def test_get(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest']) + response = dut.api('acl').get('exttest') + self.assertIsNotNone(response) + + def test_get_none(self): + for dut in self.duts: + dut.config('no ip access-list exttest') + result = dut.api('acl').get('exttest') + self.assertIsNone(result) + + def test_getall(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest']) + result = dut.api('acl').getall() + self.assertIn('exttest', result['extended']) + + def test_create(self): + for dut in self.duts: + dut.config('no ip access-list exttest') + api = dut.api('acl') + self.assertIsNone(api.get('exttest')) + result = dut.api('acl').create('exttest', 'extended') + self.assertTrue(result) + self.assertIsNotNone(api.get('exttest')) + + def test_delete(self): + for dut in self.duts: + dut.config('ip access-list exttest') + api = dut.api('acl') + self.assertIsNotNone(api.get('exttest')) + result = dut.api('acl').delete('exttest') + self.assertTrue(result) + self.assertIsNone(api.get('exttest')) + + def test_default(self): + for dut in self.duts: + dut.config('ip access-list exttest') + api = dut.api('acl') + self.assertIsNotNone(api.get('exttest')) + result = dut.api('acl').default('exttest') + self.assertTrue(result) + self.assertIsNone(api.get('exttest')) + + def test_update_entry(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest']) + api = dut.api('acl') + self.assertNotIn('10 permit ip any any', + api.get_block('ip access-list exttest')) + result = dut.api('acl').update_entry('exttest', '10', 'permit', + 'ip', '0.0.0.0', '0', + '0.0.0.0', '0', False) + self.assertTrue(result) + self.assertIn('10 permit ip any any', + api.get_block('ip access-list exttest')) + + def test_update_entry_existing(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest', '10 permit ip any any log']) + api = dut.api('acl') + self.assertIn('10 permit ip any any log', + api.get_block('ip access-list exttest')) + result = dut.api('acl').update_entry('exttest', '10', 'deny', 'ip', + '0.0.0.0', '0', '0.0.0.0', + '0', True) + self.assertTrue(result) + self.assertIn('10 deny ip any any log', + api.get_block('ip access-list exttest')) + + def test_add_entry(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest']) + api = dut.api('acl') + self.assertNotIn('10 permit ip any any log', + api.get_block('ip access-list exttest')) + result = api.add_entry('exttest', 'permit', 'ip', '0.0.0.0', '0', + '0.0.0.0', '0', True) + self.assertTrue(result) + self.assertIn('10 permit ip any any log', + api.get_block('ip access-list exttest')) + + def test_remove_entry(self): + for dut in self.duts: + dut.config(['no ip access-list exttest', + 'ip access-list exttest', '10 permit ip any any log']) + api = dut.api('acl') + self.assertIn('10 permit ip any any log', + api.get_block('ip access-list exttest')) + result = api.remove_entry('exttest', '10') + self.assertTrue(result) + self.assertNotIn('10 permit ip any any log', + api.get_block('ip access-list exttest')) if __name__ == '__main__': diff --git a/test/unit/test_api_acl.py b/test/unit/test_api_acl.py index a33d953..134b869 100644 --- a/test/unit/test_api_acl.py +++ b/test/unit/test_api_acl.py @@ -50,29 +50,95 @@ def test_prefixlen_to_mask(self): result = pyeapi.api.acl.prefixlen_to_mask(24) self.assertEqual(result, '255.255.255.0') -class TestApiStandardAcls(EapiConfigUnitTest): + +class TestApiAcls(EapiConfigUnitTest): def __init__(self, *args, **kwargs): - super(TestApiStandardAcls, self).__init__(*args, **kwargs) - self.instance = pyeapi.api.acl.StandardAcls(None) + super(TestApiAcls, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.acl.Acls(None) self.config = open(get_fixture('running_config.text')).read() def test_instance(self): result = pyeapi.api.acl.instance(None) + self.assertIsInstance(result, pyeapi.api.acl.Acls) + + def test_getall(self): + result = self.instance.getall() + self.assertIsInstance(result, dict) + self.assertIn('exttest', result['extended']) + self.assertIn('test', result['standard']) + + def test_get_not_configured(self): + self.assertIsNone(self.instance.get('unconfigured')) + + def test_get(self): + result = self.instance.get('test') + keys = ['name', 'type', 'entries'] + self.assertEqual(sorted(keys), sorted(result.keys())) + + def test_get_instance(self): + result = self.instance.get_instance('test') + self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) + self.instance._instances['test'] = result + result = self.instance.get_instance('exttest') + self.assertIsInstance(result, pyeapi.api.acl.ExtendedAcls) + result = self.instance.get_instance('unconfigured') + self.assertIsInstance(result, dict) + self.assertIsNone(result['unconfigured']) + result = self.instance.get_instance('test') + self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) + self.assertEqual(len(self.instance._instances), 2) + + def test_create_instance_standard(self): + result = self.instance.create_instance('test', 'standard') self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) + self.assertEqual(len(self.instance._instances), 1) + + def test_create_instance_extended(self): + result = self.instance.create_instance('exttest', 'extended') + self.assertIsInstance(result, pyeapi.api.acl.ExtendedAcls) + self.assertEqual(len(self.instance._instances), 1) + + def test_create_standard(self): + cmds = 'ip access-list standard test' + func = function('create', 'test') + self.eapi_positive_config_test(func, cmds) + + def test_create_extended(self): + cmds = 'ip access-list exttest' + func = function('create', 'exttest', 'extended') + self.eapi_positive_config_test(func, cmds) + + def test_create_unknown_type_creates_standard(self): + cmds = 'ip access-list standard test' + func = function('create', 'test', 'unknown') + self.eapi_positive_config_test(func, cmds) + + def test_proxy_method_success(self): + result = self.instance.remove_entry('test', '10') + self.assertTrue(result) + + def test_proxy_method_raises_attribute_error(self): + with self.assertRaises(AttributeError): + self.instance.nonmethod('test', '10') + + +class TestApiStandardAcls(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiStandardAcls, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.acl.StandardAcls(None) + self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('test') keys = ['name', 'type', 'entries'] self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertEqual(result['type'], 'standard') def test_get_not_configured(self): self.assertIsNone(self.instance.get('unconfigured')) - def test_getall(self): - result = self.instance.getall() - self.assertIsInstance(result, dict) - def test_acl_functions(self): for name in ['create', 'delete', 'default']: if name == 'create': @@ -91,23 +157,107 @@ def test_update_entry(self): '32', True) self.eapi_positive_config_test(func, cmds) + def test_update_entry_no_log(self): + cmds = ['ip access-list standard test', 'no 10', + '10 permit 0.0.0.0/32', 'exit'] + func = function('update_entry', 'test', '10', 'permit', '0.0.0.0', + '32') + self.eapi_positive_config_test(func, cmds) + def test_remove_entry(self): cmds = ['ip access-list standard test', 'no 10', 'exit'] func = function('remove_entry', 'test', '10') self.eapi_positive_config_test(func, cmds) def test_add_entry(self): - cmds = ['ip access-list standard test', 'permit 0.0.0.0/32 log', 'exit'] + cmds = ['ip access-list standard test', 'permit 0.0.0.0/32 log', + 'exit'] func = function('add_entry', 'test', 'permit', '0.0.0.0', '32', True) self.eapi_positive_config_test(func, cmds) + def test_add_entry_no_log(self): + cmds = ['ip access-list standard test', 'permit 0.0.0.0/32', + 'exit'] + func = function('add_entry', 'test', 'permit', '0.0.0.0', + '32') + self.eapi_positive_config_test(func, cmds) + def test_add_entry_with_seqno(self): - cmds = ['ip access-list standard test', '30 permit 0.0.0.0/32 log', 'exit'] + cmds = ['ip access-list standard test', '30 permit 0.0.0.0/32 log', + 'exit'] func = function('add_entry', 'test', 'permit', '0.0.0.0', '32', True, 30) self.eapi_positive_config_test(func, cmds) +class TestApiExtendedAcls(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiExtendedAcls, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.acl.ExtendedAcls(None) + self.config = open(get_fixture('running_config.text')).read() + + def test_get(self): + result = self.instance.get('exttest') + keys = ['name', 'type', 'entries'] + self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertEqual(result['type'], 'extended') + + def test_get_not_configured(self): + self.assertIsNone(self.instance.get('unconfigured')) + + def test_acl_functions(self): + for name in ['create', 'delete', 'default']: + if name == 'create': + cmds = 'ip access-list exttest' + elif name == 'delete': + cmds = 'no ip access-list exttest' + elif name == 'default': + cmds = 'default ip access-list exttest' + func = function(name, 'exttest') + self.eapi_positive_config_test(func, cmds) + + def test_update_entry(self): + cmds = ['ip access-list exttest', 'no 10', + '10 permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] + func = function('update_entry', 'exttest', '10', 'permit', 'ip', + '0.0.0.0', '32', '1.1.1.1', '32', True) + self.eapi_positive_config_test(func, cmds) + + def test_update_entry_no_log(self): + cmds = ['ip access-list exttest', 'no 10', + '10 permit ip 0.0.0.0/32 1.1.1.1/32', 'exit'] + func = function('update_entry', 'exttest', '10', 'permit', 'ip', + '0.0.0.0', '32', '1.1.1.1', '32') + self.eapi_positive_config_test(func, cmds) + + def test_remove_entry(self): + cmds = ['ip access-list exttest', 'no 10', 'exit'] + func = function('remove_entry', 'exttest', '10') + self.eapi_positive_config_test(func, cmds) + + def test_add_entry(self): + cmds = ['ip access-list exttest', + 'permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] + func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', + '32', '1.1.1.1', '32', True) + self.eapi_positive_config_test(func, cmds) + + def test_add_entry_no_log(self): + cmds = ['ip access-list exttest', 'permit ip 0.0.0.0/32 1.1.1.1/32', + 'exit'] + func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', + '32', '1.1.1.1', '32') + self.eapi_positive_config_test(func, cmds) + + def test_add_entry_with_seqno(self): + cmds = ['ip access-list exttest', + '30 permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] + func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', + '32', '1.1.1.1', '32', True, 30) + self.eapi_positive_config_test(func, cmds) + + if __name__ == '__main__': unittest.main() From ae1767f7edcd2939af0ec2faa567570801903a9c Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 7 Mar 2017 11:21:56 -0500 Subject: [PATCH 43/44] Update extended ACLs parsing regex. --- pyeapi/api/acl.py | 24 +++++++++++------------- test/fixtures/running_config.text | 4 +++- test/unit/test_api_acl.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pyeapi/api/acl.py b/pyeapi/api/acl.py index 4475cae..498e544 100644 --- a/pyeapi/api/acl.py +++ b/pyeapi/api/acl.py @@ -220,17 +220,15 @@ class ExtendedAcls(EntityCollection): entry_re = re.compile(r'(\d+)' r'(?: ([p|d]\w+))' r'(?: (\w+|\d+))' - r'(?: (any))?' - r'(?: (host))?' + r'(?: ([a|h]\w+))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?:/([0-9]{1,2}))?' - r'(?: (eq [\w-]+))?' - r'(?: (any))?' - r'(?: (host))?' + r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?' + r'(?: ([a|h]\w+))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?:/([0-9]{1,2}))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' - r'(?: (eq [\w-]+))?' + r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?' r'(?: (.+))?') def get(self, name): @@ -248,13 +246,13 @@ def _parse_entries(self, config): entry = dict() entry['action'] = match.group(2) entry['protocol'] = match.group(3) - entry['srcaddr'] = match.group(6) or 'any' - entry['srclen'] = match.group(7) - entry['srcport'] = match.group(8) - entry['dstaddr'] = match.group(11) or 'any' - entry['dstlen'] = match.group(12) - entry['dstport'] = match.group(14) - entry['other'] = match.group(15) + entry['srcaddr'] = match.group(5) or 'any' + entry['srclen'] = match.group(6) + entry['srcport'] = match.group(7) + entry['dstaddr'] = match.group(9) or 'any' + entry['dstlen'] = match.group(10) + entry['dstport'] = match.group(12) + entry['other'] = match.group(13) entries[match.group(1)] = entry return dict(entries=entries) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index 200513b..9b0a503 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1586,7 +1586,9 @@ ip access-list exttest 20 permit icmp 3.3.3.0/24 any ttl eq 2 tracked log 30 permit tcp host 7.7.7.7 eq https any eq https log 40 permit udp 4.4.0.0/16 any eq pkt-krb-ipsec - 50 deny ip any any log + 50 permit ip any host 1.1.1.2 + 60 deny ip any any log + 70 permit tcp 8.8.8.0/24 neq irc host 3.3.3.3 lt ipp urg ttl eq 24 fragments tracked log ! mac address-table aging-time 300 ! diff --git a/test/unit/test_api_acl.py b/test/unit/test_api_acl.py index 134b869..6c700f7 100644 --- a/test/unit/test_api_acl.py +++ b/test/unit/test_api_acl.py @@ -203,6 +203,18 @@ def test_get(self): keys = ['name', 'type', 'entries'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(result['type'], 'extended') + self.assertIn('entries', result) + self.assertIn('50', result['entries']) + entry = dict(action='permit', dstaddr='1.1.1.2', dstlen=None, + dstport=None, other=None, protocol='ip', srcaddr='any', + srclen=None, srcport=None) + self.assertEqual(entry, result['entries']['50']) + self.assertIn('70', result['entries']) + entry = dict(action='permit', dstaddr='3.3.3.3', dstlen=None, + dstport='lt ipp', protocol='tcp', srcaddr='8.8.8.0', + other='urg ttl eq 24 fragments tracked log', + srclen='24', srcport='neq irc') + self.assertEqual(entry, result['entries']['70']) def test_get_not_configured(self): self.assertIsNone(self.instance.get('unconfigured')) From 768db47682f1ca00c308b047b9ffe0d6efd763ae Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 14 Mar 2017 00:45:11 -0400 Subject: [PATCH 44/44] Release 0.8.0 - Adds VRF API - Allows creation of Ethernet subinterfaces - Allow usage of expandAliases and autoComplete parameters - Adds extended ACL support --- VERSION | 2 +- docs/release-notes-0.8.0.rst | 37 ++++++++++++++++++++++++++++++++++++ docs/release-notes.rst | 1 + docs/subinterfaces.rst | 12 +++++++++--- pyeapi/__init__.py | 2 +- 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 docs/release-notes-0.8.0.rst diff --git a/VERSION b/VERSION index 6563189..a3df0a6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +0.8.0 diff --git a/docs/release-notes-0.8.0.rst b/docs/release-notes-0.8.0.rst new file mode 100644 index 0000000..f6698bd --- /dev/null +++ b/docs/release-notes-0.8.0.rst @@ -0,0 +1,37 @@ +###### +v0.8.0 +###### + +2017-03-14 + +New Modules +^^^^^^^^^^^ + +* Base API for VRF support. (`133 `_) [`mharista `_] + Added new API module for creating VRFs. In addition to creating, configuring and removing VRFs the updates allow for applying a VRF to an interface and creating a VRF specific OSPF instance. + +Enhancements +^^^^^^^^^^^^ + +* Add base extended ACL support. (`135 `_) [`mharista `_] + Updated ACL api to include extended ACLs in addition to standard. To create an extended ACL provide the type as extended when creating the ACL (default is standard). Currently extended ACL statements can be added with action, protocol, and source/destination address. The API will determine the type of ACL by name after it has been created for future updates. +* Add support for creating and deleting ethernet subinterfaces (`132 `_) [`mharista `_] + Allow for creation and deletion of ethernet subinterfaces as part of the EthernetInterface class. Subinterfaces are also supported on PortChannel interfaces. An example using the API to create an ethernet subinterface is provided in the docs. +* Add node attributes from show version command (`131 `_) [`mharista `_] + Added information from show version as attributes to a node. Version, version number and model are added. Version number is simply the numeric portion of the version. For example 4.17.1 if the version is 4.17.1M. All three parameters are populated from the output of show version when any one of the parameters is accessed the first time. +* Add support for eAPI expandAliases parameter (`127 `_) [`mharista `_] + Allowed users to provide the expandAliases parameter to eAPI calls. This allows users to use aliased commands via the API. For example if an alias is configured as 'sv' for 'show version' then an API call with sv and the expandAliases parameter will return the output of show version. +* Add support for autoComplete parameter. Issue 119 (`123 `_) [`mharista `_] + Allows users to use short hand commands in eAPI calls. With this parameter included a user can send 'sh ver' via eAPI to get the output of show version. +* expose portchannel attributes :lacp fallback, lacp fallback timeout (`118 `_) [`lruslan `_] + Helps for configuring LACP fallback in EOS. + +Fixed +^^^^^ + +* API path is hardcoded in EapiConnection.send() (`129 `_) + Updated the previously hardcoded path to /command-api in the EAPI connection to use the transport objects path. +* Cannot run 'no ip virtual-router mac-address' in eos 4.17.x (`122 `_) + Fixed command format for negating an ip virtual-router mac-address. Default and disable forms of the command changed and require the mac-address value in EOS 4.17. Update fixes this for newer versions and is backwards compatible. +* Non-standard HTTP/s port would cause connection to fail (`120 `_) + Bug fixed in PR (`121 `_) where a port passed in via eapi.conf as a unicode value caused the http connection to fail. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index c9a806c..eaf275f 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -6,6 +6,7 @@ Release Notes :maxdepth: 2 :titlesonly: + release-notes-0.8.0.rst release-notes-0.7.0.rst release-notes-0.6.1.rst release-notes-0.6.0.rst diff --git a/docs/subinterfaces.rst b/docs/subinterfaces.rst index 5af93ca..8537de5 100644 --- a/docs/subinterfaces.rst +++ b/docs/subinterfaces.rst @@ -7,13 +7,15 @@ pyeapi simply call create or delete with your subinterface name. Subinterfaces require that the primary interface be in routed mode. .. code-block:: python + import pyeapi node = pyeapi.connect_to('veos01') node.api('ipinterfaces').create('Ethernet1') At this point the below should be in your running configuration. -.. code-block:: +.. code-block:: shell + ! interface Ethernet1 no switchport @@ -22,9 +24,11 @@ At this point the below should be in your running configuration. Next step is to create the subinterface .. code-block:: python + node.api('interfaces').create('Ethernet1.1') -.. code-block:: +.. code-block:: shell + ! interface Ethernet1 no switchport @@ -35,9 +39,11 @@ Next step is to create the subinterface Subinterfaces also require a vlan to be applied to them. .. code-block:: python + node.api('interfaces').set_encapsulation('Ethernet1.1', 4) -.. code-block:: +.. code-block:: shell + ! interface Ethernet1 no switchport diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index e646e7e..ce24118 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.8.0' __author__ = 'Arista EOS+'