From f4b84d9beb173667341bb42c0e2908130442b799 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:28:35 +0200 Subject: [PATCH 01/18] Update VERSION re-added `rc0` suffix (after fixing master merge) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index afaf360..2464604 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 \ No newline at end of file +1.0.0-rc0 From 8906eb777a0bc19713663d4e1af344396deb0a8c Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:37:27 +0200 Subject: [PATCH 02/18] Update ci.yml reinstating what was done in PR #267 (had to revert that one due to fixing master to develop sync issue). --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5c1d17..b7cdeb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] fail-fast: false name: Test on Python ${{ matrix.python-version }} steps: From ffec330416c4ac77fd83ff43a524c041292816ec Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Tue, 13 Jun 2023 00:23:35 +0200 Subject: [PATCH 03/18] fixed mock import and removed obsolete sys.version check --- test/lib/testlib.py | 2 +- test/system/test_client.py | 2 +- test/unit/test_client.py | 2 +- test/unit/test_eapilib.py | 2 +- test/unit/test_utils.py | 8 ++------ 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/test/lib/testlib.py b/test/lib/testlib.py index 989fb4d..e6cf159 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -34,7 +34,7 @@ import string import unittest -from mock import MagicMock as Mock +from unittest.mock import MagicMock as Mock from pyeapi.client import Node diff --git a/test/system/test_client.py b/test/system/test_client.py index 63d840c..896b01a 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -37,7 +37,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_int, random_string, get_fixture -from mock import patch +from unittest.mock import patch import pyeapi.client import pyeapi.eapilib diff --git a/test/unit/test_client.py b/test/unit/test_client.py index d094905..3472951 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -36,7 +36,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) -from mock import Mock, patch, call +from unittest.mock import Mock, patch, call from testlib import get_fixture, random_string, random_int diff --git a/test/unit/test_eapilib.py b/test/unit/test_eapilib.py index 1bafef8..2bc7baf 100644 --- a/test/unit/test_eapilib.py +++ b/test/unit/test_eapilib.py @@ -1,7 +1,7 @@ import unittest import json -from mock import Mock, patch +from unittest.mock import Mock, patch import pyeapi.eapilib diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py index 907b48c..a41a7b1 100644 --- a/test/unit/test_utils.py +++ b/test/unit/test_utils.py @@ -1,13 +1,9 @@ -import sys import unittest -from mock import patch, Mock +from unittest.mock import patch, Mock import pyeapi.utils -if sys.version_info < (3, 3): - from collections import Iterable -else: - from collections.abc import Iterable +from collections.abc import Iterable class TestUtils(unittest.TestCase): From 5849d500e826a067dbda0201a5dab5ee2aa9176b Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Tue, 13 Jun 2023 00:33:36 +0200 Subject: [PATCH 04/18] updated action plugin versions and amended the RN --- docs/release-notes-1.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes-1.0.0.rst b/docs/release-notes-1.0.0.rst index 136222d..717b93e 100644 --- a/docs/release-notes-1.0.0.rst +++ b/docs/release-notes-1.0.0.rst @@ -4,7 +4,7 @@ Release 1.0.0 2023-06-06 - This is a Python3 (3.7 onwards) release only (Python2 is no longer supported) -- Arista EOS 4.22 or later required +- Arista EOS 4.22 or later required (for on-box use cases only) New Modules ^^^^^^^^^^^ From e1d891e5e06ff42a324f4dd8887875e79d2ab41f Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Tue, 13 Jun 2023 01:13:12 +0200 Subject: [PATCH 05/18] missed this file in the prior update, adding now --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7cdeb5..aa73838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,9 @@ jobs: fail-fast: false name: Test on Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install wheel From 1552c59b1d821899088651e9fb35c56411e5526b Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 02:35:45 +0200 Subject: [PATCH 06/18] fixed all failing unit-test and system tests --- pyeapi/api/interfaces.py | 23 ++++++--- pyeapi/client.py | 4 ++ pyeapi/eapilib.py | 6 ++- pyeapi/utils.py | 17 +++++++ test/fixtures/dut.conf | 5 +- test/lib/testlib.py | 12 ++++- test/system/test_api_interfaces.py | 75 +++++++++++++++++++++++++---- test/system/test_api_staticroute.py | 43 +++++------------ test/system/test_api_vrfs.py | 27 +++++------ test/system/test_api_vrrp.py | 24 +++------ test/system/test_client.py | 34 ++++++------- 11 files changed, 164 insertions(+), 106 deletions(-) diff --git a/pyeapi/api/interfaces.py b/pyeapi/api/interfaces.py index 467d752..01ec10b 100644 --- a/pyeapi/api/interfaces.py +++ b/pyeapi/api/interfaces.py @@ -60,7 +60,7 @@ import re from pyeapi.api import EntityCollection -from pyeapi.utils import ProxyCall +from pyeapi.utils import ProxyCall, CliVariants MIN_LINKS_RE = re.compile(r'(?<=\s{3}min-links\s)(?P.+)$', re.M) @@ -884,8 +884,9 @@ def _parse_multicast_group(self, config): return dict(multicast_group=value) def _parse_multicast_decap(self, config): - value = 'vxlan multicast-group decap' in config - return dict(multicast_decap=bool(value)) + val1 = 'vxlan multicast-group decap' in config + val2 = 'no vxlan multicast-group decap' in config + return dict( multicast_decap=bool(val1 ^ val2) ) def _parse_udp_port(self, config): match = re.search(r'vxlan udp-port (\d+)', config) @@ -956,10 +957,14 @@ def set_multicast_group(self, name, value=None, default=False, Returns: True if the operation succeeds otherwise False """ - string = 'vxlan multicast-group' - cmds = self.command_builder(string, value=value, default=default, - disable=disable) - return self.configure_interface(name, cmds) + string_dpr = 'vxlan multicast-group' + cmds_dpr = self.command_builder(string_dpr, + value=value, default=default, disable=disable) + string_new = 'vxlan multicast-group decap' + cmds_new = self.command_builder(string_new, + value=value, default=default, disable=disable) + return self.configure_interface(name, + CliVariants(cmds_new, cmds_dpr) ) def set_multicast_decap(self, name, default=False, disable=False): @@ -1080,7 +1085,9 @@ def remove_vlan(self, name, vid): True if the command completes successfully """ - return self.configure_interface(name, 'vxlan vlan remove %s vni' % vid) + return self.configure_interface( name, + CliVariants(f'vxlan vlan remove {vid} vni $', + f'vxlan vlan remove {vid} vni') ) INTERFACE_CLASS_MAP = { diff --git a/pyeapi/client.py b/pyeapi/client.py index 2de2fce..16bf300 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -662,6 +662,8 @@ def _configure_terminal(self, commands, **kwargs): # push the configure command onto the command stack commands.insert(0, 'configure terminal') response = self.run_commands(commands, **kwargs) + # after config change the _chunkify lru_cache has to be cleared + self._chunkify.cache_clear() if self.autorefresh: self.refresh() @@ -684,6 +686,8 @@ def _configure_session(self, commands, **kwargs): # push the configure command onto the command stack commands.insert(0, 'configure session %s' % self._session_name) response = self.run_commands(commands, **kwargs) + # after config change the _chunkify lru_cache has to be cleared + self._chunkify.cache_clear() # pop the configure command output off the stack response.pop(0) diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 140d054..3670281 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -433,14 +433,16 @@ def send(self, data): self.transport.endheaders(message_body=data) response = self.transport.getresponse() response_content = response.read() + if isinstance( response_content, bytes ): + response_content = response_content.decode() _LOGGER.debug('Response: status:{status}, reason:{reason}'.format( status=response.status, reason=response.reason)) _LOGGER.debug('Response content: {}'.format(response_content)) if response.status == 401: - raise ConnectionError(str(self), '%s. %s' % (response.reason, - response_content)) + raise ConnectionError(str(self), + f'{response.reason}. {response_content}') # Python 3.7 json.loads() works with bytes or strings, # thus no decoding is required diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 7ad2c0b..36c62ed 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -247,6 +247,23 @@ def __init__(self, *cli): assert len( cli ) >= 2, 'must be initialized with 2 or more arguments' self.variants = [ v if not isinstance(v, str) and isinstance(v, Iterable) else [v] for v in cli ] + @staticmethod + def expand( cmds ): + """cnds is a list of str and CliVariants, this method returns a list + of all full variant combinations present in cmds, e.g.: + expand( 'x', CliVariants( 'a', 'b'), 'y' ) + will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] + """ + assert isinstance(cmds, list), 'argument cmnds must be list type' + if not cmds: + return [ [] ] + head = cmds[0] + tail = cmds[1:] + if isinstance( head, CliVariants ): + return [ v + e for v in head.variants + for e in CliVariants.expand( tail ) ] + else: + return [ [head] + e for e in CliVariants.expand(tail) ] def _interpolate_docstr( *tkns ): diff --git a/test/fixtures/dut.conf b/test/fixtures/dut.conf index 036a7d0..b085476 100644 --- a/test/fixtures/dut.conf +++ b/test/fixtures/dut.conf @@ -1,5 +1,4 @@ [connection:veos01] -host: 192.168.1.16 -username: eapi -password: password +host: roi401 +username: admin transport: http diff --git a/test/lib/testlib.py b/test/lib/testlib.py index e6cf159..c53a299 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -35,6 +35,7 @@ import unittest from unittest.mock import MagicMock as Mock +from pyeapi.utils import CliVariants from pyeapi.client import Node @@ -94,7 +95,16 @@ def eapi_config_test(self, func, cmds=None, *args, **kwargs): result = func(*fargs, **fkwargs) if cmds is not None: - self.node.config.assert_called_with(cmds) + # if config was called with CliVariants, then create all possible + # cli combinations with CliVariants and see if cmds is one of them + called_args = self.node.config.call_args.args[0] + variants = [ x for x in called_args if isinstance(x, CliVariants) ] + if not variants: + self.node.config.assert_called_with(cmds) + return result + # process all variants + cli_variants = CliVariants.expand( called_args ) + self.assertIn( cmds, cli_variants ) else: self.assertEqual(self.node.config.call_count, 0) diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index ab7edb5..21434a4 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -31,13 +31,14 @@ # import os import unittest +from pyeapi.utils import CliVariants import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_string, random_int from systestlib import DutSystemTest, random_interface - +from time import sleep class TestResourceInterfaces(DutSystemTest): @@ -150,12 +151,15 @@ def test_set_encapsulation_non_supported_intf_exception(self): def test_default(self): for dut in self.duts: intf = random_interface(dut) + intf_status = dut.run_commands('show interfaces %s' % intf) + intf_status = intf_status[0]['interfaces'][intf]['interfaceStatus'] dut.config(['interface %s' % intf, 'shutdown']) result = dut.api('interfaces').default(intf) + sleep( 10 ) # if intf was 'connected', give it a time to come up self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] - self.assertEqual(config['interfaceStatus'], 'connected') + self.assertEqual(config['interfaceStatus'], intf_status) def test_set_description(self): for dut in self.duts: @@ -219,6 +223,8 @@ 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) @@ -254,6 +260,32 @@ def test_set_vrf(self): dut.config('no vrf definition test') + 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( CliVariants('vrf instance test', 'vrf definition test') ) + # Verify interface has vrf applied + result = dut.api('interfaces').set_vrf(intf, 'test') + self.assertTrue(result) + config = dut.run_commands( + f'show running-config interfaces {intf}', 'text' ) + self.assertIn('vrf test' if dut.version_number >= '4.23' + else '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( + f'show running-config interfaces {intf}', 'text' ) + self.assertNotIn('vrf test' if dut.version_number >= '4.23' + else 'vrf forwarding test', config[0]['output']) + dut.config( CliVariants( + 'no vrf instance test', 'no vrf definition test') ) + + class TestPortchannelInterface(DutSystemTest): def test_get(self): @@ -407,7 +439,7 @@ def test_minimum_links_valid(self): def test_minimum_links_invalid_value(self): for dut in self.duts: - minlinks = random_int(17, 128) + minlinks = random_int(65, 128) result = dut.api('interfaces').set_minimum_links('Port-Channel1', minlinks) self.assertFalse(result) @@ -508,6 +540,25 @@ def contains(self, text, dut): def notcontains(self, text, dut): self.assertNotIn(text, self.get_config(dut), 'dut=%s' % dut) + def may_contain( self, text, dut, first=False, last=False ): + """handles multiple variants of cli, typically to handle deprecated + variants of the cli. The first and last calls in the variant sequence + must be initialized respectively. At least one call in the sequence + should result in a positive assertion + """ + if first: + self.skip_rest = False + if self.skip_rest: + return + if last: + self.assertIn(text, self.get_config(dut), 'dut=%s' % dut) + return + try: + self.assertIn(text, self.get_config(dut), 'dut=%s' % dut) + self.skip_rest = True + except AssertionError: + pass + def test_set_source_interface(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) @@ -540,12 +591,14 @@ def test_set_multicast_group(self): api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', '239.10.10.10') self.assertTrue(instance) - self.contains('vxlan multicast-group 239.10.10.10', dut) + self.contains('vxlan multicast-group', dut) + self.contains('239.10.10.10', dut) def test_set_multicast_group_default(self): for dut in self.duts: - dut.config(['no interface Vxlan1', 'interface Vxlan1', - 'vxlan multicast-group 239.10.10.10']) + dut.config( ['no interface Vxlan1', 'interface Vxlan1', + CliVariants( 'vxlan multicast-group decap 239.10.10.10', + 'vxlan multicast-group 239.10.10.10') ]) api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', default=True) self.assertTrue(instance) @@ -553,8 +606,9 @@ def test_set_multicast_group_default(self): def test_set_multicast_group_negate(self): for dut in self.duts: - dut.config(['no interface Vxlan1', 'interface Vxlan1', - 'vxlan multicast-group 239.10.10.10']) + dut.config( ['no interface Vxlan1', 'interface Vxlan1', + CliVariants( 'vxlan multicast-group decap 239.10.10.10', + 'vxlan multicast-group 239.10.10.10') ]) api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', disable=True) self.assertTrue(instance) @@ -635,7 +689,8 @@ def test_update_vlan(self): api = dut.api('interfaces') instance = api.update_vlan('Vxlan1', '10', '10') self.assertTrue(instance) - self.contains('vxlan vlan add 10 vni 10', dut) + self.may_contain('vxlan vlan 10 vni 10', dut, first=True) + self.may_contain('vxlan vlan add 10 vni 10', dut, last=True) def test_remove_vlan(self): for dut in self.duts: @@ -643,7 +698,7 @@ def test_remove_vlan(self): api = dut.api('interfaces') instance = api.remove_vlan('Vxlan1', '10') self.assertTrue(instance) - self.notcontains('vxlan vlan remove 10 vni 10', dut) + self.notcontains('vxlan vlan remove 10 vni 10 $', dut) if __name__ == '__main__': diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py index 2b4395f..4c77d3d 100644 --- a/test/system/test_api_staticroute.py +++ b/test/system/test_api_staticroute.py @@ -83,8 +83,7 @@ def test_create(self): # when creating routes with varying parameters included. for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing']) + dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: @@ -112,8 +111,7 @@ def test_get(self): # is passed in when the route exists on the switch. for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing']) + dut.config(['no ip routing', 'ip routing']) ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' @@ -152,9 +150,7 @@ def test_getall(self): # name 'test10'). for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing']) - + dut.config(['no ip routing', 'ip routing']) # Declare a set of 3 routes with same ip dest and next hop. # Set different distance, tag and name for each route, # including values 1 and 10 in each, so the test will verify @@ -165,35 +161,22 @@ def test_getall(self): 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10' route3 = \ 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 2 tag 10 name test1' - dut.config([route1, route2, route3]) - routes = { '1.2.3.0/24': { 'Ethernet1': { '1.1.1.1': { - 10: { - 'tag': 1, - 'route_name': 'test1' - }, - 1: { - 'tag': 1, - 'route_name': 'test10' - }, - 2: { - 'tag': 10, - 'route_name': 'test1' - } + 10: { 'tag': 1, 'route_name': 'test1' }, + 1: { 'tag': 1, 'route_name': 'test10' }, + 2: { 'tag': 10, 'route_name': 'test1' } } } } } - # Get the list of ip routes from the switch result = dut.api('staticroute').getall() - # Assert that the result dict is equivalent to the routes dict - self.assertEqual(result, routes) + self.assertEqual(result['1.2.3.0/24'], routes['1.2.3.0/24']) def test_delete(self): # Validate the delete function returns without an error @@ -203,8 +186,7 @@ def test_delete(self): # does not error. for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing']) + dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: @@ -235,8 +217,7 @@ def test_default(self): # function. for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing']) + dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: @@ -264,8 +245,7 @@ def test_set_tag(self): # when modifying the tag on an existing route for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing', + dut.config(['no ip routing', 'ip routing', 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 99']) result = dut.api('staticroute').set_tag( @@ -278,8 +258,7 @@ def test_set_route_name(self): # when modifying the tag on an existing route for dut in self.duts: - dut.config(['no ip routing delete-static-routes', - 'ip routing', + dut.config(['no ip routing', 'ip routing', 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test99']) result = dut.api('staticroute').set_route_name( diff --git a/test/system/test_api_vrfs.py b/test/system/test_api_vrfs.py index 6d888fb..812c201 100644 --- a/test/system/test_api_vrfs.py +++ b/test/system/test_api_vrfs.py @@ -37,6 +37,7 @@ from testlib import random_string from systestlib import DutSystemTest +from pyeapi.utils import CliVariants class TestApiVrfs(DutSystemTest): @@ -60,21 +61,19 @@ def test_get(self): def test_getall(self): for dut in self.duts: - if dut.version_number >= '4.23': - dut.config(['no vrf instance blah', 'vrf instance blah', - 'no vrf instance second', 'vrf instance second']) - else: - dut.config(['no vrf definition blah', 'vrf definition blah', - 'no vrf definition second', 'vrf definition second']) + dut.config( CliVariants( + ['no vrf instance blah', 'vrf instance blah', + 'no vrf instance second', 'vrf instance second'], + ['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) - if dut.version_number >= '4.23': - dut.config(['no vrf instance blah', 'no vrf instance second']) - else: - dut.config(['no vrf definition blah', 'no vrf definition second']) + self.assertIsInstance( response, dict, f'dut={dut}' ) + self.assertEqual( len(response), 2 ) + for vrf_name in ( 'blah', 'second' ): + self.assertIn( vrf_name, response, f'dut={dut}' ) + dut.config( CliVariants( + ['no vrf instance blah', 'no vrf instance second'], + ['no vrf definition blah', 'no vrf definition second']) ) def test_create_and_return_true(self): for dut in self.duts: diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index 9a20060..ff5cc95 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -117,19 +117,16 @@ def test_create(self): # vrrp_conf = dict(VR_CONFIG) for dut in self.duts: interface = self._vlan_setup(dut) - dut.config(['interface %s' % interface, - 'no vrrp %d' % vrid, - 'exit']) - + dut.config([f'interface {interface}', f'no vrrp {vrid}', 'exit']) response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) self.assertIs(response, True) - # Fix the configuration dict for proper output vrrp_conf = dut.api('vrrp').vrconf_format(vrrp_conf) - response = dut.api('vrrp').get(interface)[vrid] - self.maxDiff = None + # now delete dict items which vary between versions + for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): + del vrrp_conf[ key ], response[ key ] self.assertEqual(response, vrrp_conf) def test_delete(self): @@ -169,21 +166,16 @@ def test_default(self): self.assertIs(response, True) def test_update_with_create(self): - pass vrid = 103 import copy vrrp_conf = copy.deepcopy(VR_CONFIG) # vrrp_conf = dict(VR_CONFIG) for dut in self.duts: interface = self._vlan_setup(dut) - dut.config(['interface %s' % interface, - 'no vrrp %d' % vrid, - 'exit']) - + dut.config([f'interface {interface}', f'no vrrp {vrid}', 'exit']) # Create the inital vrrp on the interface response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) self.assertIs(response, True) - # Update some of the information on the vrrp vrrp_update = { 'primary_ip': '10.10.10.12', @@ -204,13 +196,13 @@ def test_update_with_create(self): ], 'bfd_ip': None, } - response = dut.api('vrrp').create(interface, vrid, **vrrp_update) self.assertIs(response, True) vrrp_update = dut.api('vrrp').vrconf_format(vrrp_update) - response = dut.api('vrrp').get(interface)[vrid] - + # now delete dict items which vary between versions + for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): + del vrrp_update[ key ], response[ key ] self.maxDiff = None self.assertEqual(response, vrrp_update) diff --git a/test/system/test_client.py b/test/system/test_client.py index 896b01a..63dc13f 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -117,7 +117,8 @@ def test_no_enable_single_command(self): def test_no_enable_single_command_no_auth(self): for dut in self.duts: with self.assertRaises(pyeapi.eapilib.CommandError): - dut.run_commands(['disable', '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: @@ -276,33 +277,28 @@ def setUp(self): def test_exception_trace(self): # Send commands that will return an error and validate the errors - # General format of an error message: - rfmt = r'Error \[%d\]: CLI command \d+ of \d+ \'.*\' failed: %s \[%s\]' + rfmt = r'Error \[%d\]: CLI command \d+ of \d+ \'[^\']*\' failed: %s\[%s\]' # Design error tests cases = [] # Send an incomplete command - cases.append(('show run', rfmt - % (1002, 'invalid command', - r'incomplete token \(at token \d+: \'.*\'\)'))) + cases.append( ('show run', rfmt % (1002, r'invalid command \[[^[]+', + r'"Incomplete token \(at token \d+:[^\)]+\)"'))) # Send a mangled command - cases.append(('shwo version', rfmt - % (1002, 'invalid command', - r'Invalid input \(at token \d+: \'.*\'\)'))) + cases.append(('shwo version', rfmt % (1002, r'invalid command \[[^[]+', + r'"Invalid input \(at token \d+:[^\)]+\)"'))) # Send a command that cannot be run through the api # note the command for reload looks to change in new EOS # in 4.15 the reload now is replaced with 'force' if you are # testing some DUT running older code and this test fails # change the error message to the following: # To reload the machine over the API, please use 'reload now' instead - cases.append(('reload', rfmt - % (1004, 'incompatible command', - 'Command not permitted via API access..*'))) + cases.append(('reload', rfmt % (1004, r'incompatible command \[[^[]+', + r"'Command not permitted via API access\..*"))) # Send a command that has insufficient priv - cases.append(('show running-config', rfmt - % (1002, 'invalid command', - r'Invalid input \(privileged mode required\)'))) - + cases.append(('show running-config', rfmt % (1002, + r'invalid command \[[^[]+', + r"'Invalid input \(privileged mode required\)'"))) for dut in self.duts: for (cmd, regex) in cases: try: @@ -317,10 +313,8 @@ def test_exception_trace(self): self.fail('A CommandError should have been raised') except pyeapi.eapilib.CommandError as exc: # Validate the properties of the exception - if cmd != 'show running-config': - self.assertEqual(len(exc.trace), 4) - else: - self.assertEqual(len(exc.trace), 3) + self.assertEqual( len(exc.trace), + 3 if cmd == 'show running-config' else 4 ) self.assertIsNotNone(exc.command_error) self.assertIsNotNone(exc.output) self.assertIsNotNone(exc.commands) From 94bf98bb4eed463dcaf8f69d6176e17fb34a3bc9 Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 02:41:00 +0200 Subject: [PATCH 07/18] fixed indentations --- pyeapi/utils.py | 9 +++++---- test/lib/testlib.py | 4 ++-- test/system/test_api_vrrp.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 36c62ed..685f46b 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -243,10 +243,6 @@ class CliVariants: or with 2 or more sequences of cli (or a mix of list and str types), e.g.: ``CliVariants( ['new cli1', 'new cli2'], 'alt cli3', 'legacy cli4' )`` """ - def __init__(self, *cli): - assert len( cli ) >= 2, 'must be initialized with 2 or more arguments' - self.variants = [ v if not isinstance(v, - str) and isinstance(v, Iterable) else [v] for v in cli ] @staticmethod def expand( cmds ): """cnds is a list of str and CliVariants, this method returns a list @@ -265,6 +261,11 @@ def expand( cmds ): else: return [ [head] + e for e in CliVariants.expand(tail) ] + def __init__(self, *cli): + assert len( cli ) >= 2, 'must be initialized with 2 or more arguments' + self.variants = [ v if not isinstance(v, + str) and isinstance(v, Iterable) else [v] for v in cli ] + def _interpolate_docstr( *tkns ): """Docstring decorator. diff --git a/test/lib/testlib.py b/test/lib/testlib.py index c53a299..a790406 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -100,8 +100,8 @@ def eapi_config_test(self, func, cmds=None, *args, **kwargs): called_args = self.node.config.call_args.args[0] variants = [ x for x in called_args if isinstance(x, CliVariants) ] if not variants: - self.node.config.assert_called_with(cmds) - return result + self.node.config.assert_called_with(cmds) + return result # process all variants cli_variants = CliVariants.expand( called_args ) self.assertIn( cmds, cli_variants ) diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py index ff5cc95..0f6b253 100644 --- a/test/system/test_api_vrrp.py +++ b/test/system/test_api_vrrp.py @@ -126,7 +126,7 @@ def test_create(self): self.maxDiff = None # now delete dict items which vary between versions for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): - del vrrp_conf[ key ], response[ key ] + del vrrp_conf[ key ], response[ key ] self.assertEqual(response, vrrp_conf) def test_delete(self): @@ -202,7 +202,7 @@ def test_update_with_create(self): response = dut.api('vrrp').get(interface)[vrid] # now delete dict items which vary between versions for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): - del vrrp_update[ key ], response[ key ] + del vrrp_update[ key ], response[ key ] self.maxDiff = None self.assertEqual(response, vrrp_update) From f0aca0780264c99b77313b70c4e4aaf6e86464bb Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 02:43:37 +0200 Subject: [PATCH 08/18] more pep8 fixes --- test/system/test_api_interfaces.py | 38 +----------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 21434a4..614b5a5 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -155,7 +155,7 @@ def test_default(self): intf_status = intf_status[0]['interfaces'][intf]['interfaceStatus'] dut.config(['interface %s' % intf, 'shutdown']) result = dut.api('interfaces').default(intf) - sleep( 10 ) # if intf was 'connected', give it a time to come up + sleep( 10 ) # if intf was 'connected', give it a time to come up self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] @@ -224,42 +224,6 @@ def test_set_sflow_default(self): 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) - if dut.version_number >= '4.23': - dut.config('vrf instance test') - else: - 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') - if dut.version_number >= '4.23': - self.assertIn('vrf test', config[0]['output']) - else: - 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') - if dut.version_number >= '4.23': - self.assertIn('vrf test', config[0]['output']) - # Remove test vrf - dut.config('no vrf instance test') - else: - self.assertIn('vrf forwarding test', config[0]['output']) - # Remove test vrf - dut.config('no vrf definition test') - - def test_set_vrf(self): for dut in self.duts: intf = random_interface(dut) From d2e2250043f5940c3974398cb3c28aee87694dc6 Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 03:03:32 +0200 Subject: [PATCH 09/18] fixed py3.7 compatibility issue --- test/lib/testlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/testlib.py b/test/lib/testlib.py index a790406..1386c8e 100644 --- a/test/lib/testlib.py +++ b/test/lib/testlib.py @@ -97,7 +97,7 @@ def eapi_config_test(self, func, cmds=None, *args, **kwargs): if cmds is not None: # if config was called with CliVariants, then create all possible # cli combinations with CliVariants and see if cmds is one of them - called_args = self.node.config.call_args.args[0] + called_args = list( self.node.config.call_args )[ 0 ][ 0 ] variants = [ x for x in called_args if isinstance(x, CliVariants) ] if not variants: self.node.config.assert_called_with(cmds) From f7c3707024f9436c56acb61c7fb4844c697588ce Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 16:14:02 +0200 Subject: [PATCH 10/18] revered dut.conf file --- test/fixtures/dut.conf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fixtures/dut.conf b/test/fixtures/dut.conf index b085476..036a7d0 100644 --- a/test/fixtures/dut.conf +++ b/test/fixtures/dut.conf @@ -1,4 +1,5 @@ [connection:veos01] -host: roi401 -username: admin +host: 192.168.1.16 +username: eapi +password: password transport: http From c3abdb9b1bd9c4238e111b677ccbdc1f6cdf2f1d Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 16:27:56 +0200 Subject: [PATCH 11/18] fixed a typo --- pyeapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 685f46b..e24720c 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -245,7 +245,7 @@ class CliVariants: """ @staticmethod def expand( cmds ): - """cnds is a list of str and CliVariants, this method returns a list + """cmds is a list of str and CliVariants, this method returns a list of all full variant combinations present in cmds, e.g.: expand( 'x', CliVariants( 'a', 'b'), 'y' ) will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] From 68c01d736910282435ff20fb49617609a03468f7 Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 17:53:37 +0200 Subject: [PATCH 12/18] fixed min link case for some duts --- test/system/test_api_interfaces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 614b5a5..96139bf 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -403,9 +403,9 @@ def test_minimum_links_valid(self): def test_minimum_links_invalid_value(self): for dut in self.duts: - minlinks = random_int(65, 128) - result = dut.api('interfaces').set_minimum_links('Port-Channel1', - minlinks) + minlinks = random_int(129, 256) # some duts may support up to 128 + result = dut.api( + 'interfaces').set_minimum_links('Port-Channel1', minlinks) self.assertFalse(result) def test_create_and_delete_portchannel_sub_interface(self): From 5664a047b5b3b252a6a76708f58db5df5470b78d Mon Sep 17 00:00:00 2001 From: Dmitry Lyssenko Date: Thu, 29 Jun 2023 20:39:15 +0200 Subject: [PATCH 13/18] another typo --- pyeapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index e24720c..fa6d326 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -250,7 +250,7 @@ def expand( cmds ): expand( 'x', CliVariants( 'a', 'b'), 'y' ) will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] """ - assert isinstance(cmds, list), 'argument cmnds must be list type' + assert isinstance(cmds, list), 'argument cmds must be list type' if not cmds: return [ [] ] head = cmds[0] From 39e288e98097cdcaaa3c95af16e883a3b67586e2 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Thu, 29 Jun 2023 23:58:19 +0200 Subject: [PATCH 14/18] Update release-notes.rst updated entry for `release-notes-1.0.2.rst` --- docs/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index f70f0aa..9157e6e 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -6,6 +6,7 @@ Release Notes :maxdepth: 2 :titlesonly: + release-notes-1.0.2.rst release-notes-1.0.0.rst release-notes-0.8.4.rst release-notes-0.8.3.rst From fe9ecc305dc38f38d31b60f05862b7863780d776 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Fri, 30 Jun 2023 00:15:21 +0200 Subject: [PATCH 15/18] Create release-notes-1.0.2.rst Created release notes for 1.0.2 --- docs/release-notes-1.0.2.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/release-notes-1.0.2.rst diff --git a/docs/release-notes-1.0.2.rst b/docs/release-notes-1.0.2.rst new file mode 100644 index 0000000..0d47937 --- /dev/null +++ b/docs/release-notes-1.0.2.rst @@ -0,0 +1,23 @@ +Release 1.0.2 +------------- + +2023-06-30 + +New Modules +^^^^^^^^^^^ + +Enhancements +^^^^^^^^^^^^ + +Fixed +^^^^^ + +* Fixed a regression introduced by PR#220 (`220 `_) + Performance enchancement achieved with cacheing in PR#220 has a side effect: if configuration was read before the config + modifications made, then the modifications won't be reflected in the consecutive configuration reads (due to the cached read) +* Fixed all failing system tests, plus made some improvements in unit tests. + +Known Caveats +^^^^^^^^^^^^^ + + From 1aeef50745aaf70049a312d48b997fe85b519c34 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Fri, 30 Jun 2023 00:16:48 +0200 Subject: [PATCH 16/18] Update VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 2464604..6d7de6e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc0 +1.0.2 From 97cb0e8b8dab2d84b4f1163f8662f85fed589378 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Fri, 30 Jun 2023 00:17:42 +0200 Subject: [PATCH 17/18] Update __init__.py --- pyeapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index 0f86b58..a026c2e 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__ = '1.0.0' +__version__ = '1.0.2' __author__ = 'Arista EOS+' From a8b769a0d69227d975dbab270ac6233a48280bc8 Mon Sep 17 00:00:00 2001 From: dlyssenko <88390151+dlyssenko@users.noreply.github.com> Date: Fri, 30 Jun 2023 00:30:18 +0200 Subject: [PATCH 18/18] Update utils.py fixing a documentation error (sphinx build error) --- pyeapi/utils.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyeapi/utils.py b/pyeapi/utils.py index fa6d326..962ea9c 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -245,10 +245,18 @@ class CliVariants: """ @staticmethod def expand( cmds ): - """cmds is a list of str and CliVariants, this method returns a list - of all full variant combinations present in cmds, e.g.: - expand( 'x', CliVariants( 'a', 'b'), 'y' ) - will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] + """ Expands cmds argument into a list of all CLI variants + + The method returns a list of all full variant combinations present + in the the cmds arguement + + Args: + cmds (list): a list made of str and CliVariants types + + Returns: + expanded list, e.g.: + expand( 'x', CliVariants( 'a', 'b'), 'y' ) + will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] """ assert isinstance(cmds, list), 'argument cmds must be list type' if not cmds: