Skip to content

Commit

Permalink
Merge pull request #276 from arista-eosplus/release-1.0.2
Browse files Browse the repository at this point in the history
Release 1.0.2
  • Loading branch information
dlyssenko authored Jun 29, 2023
2 parents 57b801b + a8b769a commit b397df6
Show file tree
Hide file tree
Showing 19 changed files with 193 additions and 141 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ 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:
- 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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
1.0.2
2 changes: 1 addition & 1 deletion docs/release-notes-1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^
Expand Down
23 changes: 23 additions & 0 deletions docs/release-notes-1.0.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Release 1.0.2
-------------

2023-06-30

New Modules
^^^^^^^^^^^

Enhancements
^^^^^^^^^^^^

Fixed
^^^^^

* Fixed a regression introduced by PR#220 (`220 <https://github.com/arista-eosplus/pyeapi/pull/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
^^^^^^^^^^^^^


1 change: 1 addition & 0 deletions docs/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyeapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+'


Expand Down
23 changes: 15 additions & 8 deletions pyeapi/api/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<value>.+)$', re.M)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions pyeapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions pyeapi/eapilib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions pyeapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,32 @@ 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' )``
"""
@staticmethod
def expand( cmds ):
""" 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:
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 __init__(self, *cli):
assert len( cli ) >= 2, 'must be initialized with 2 or more arguments'
self.variants = [ v if not isinstance(v,
Expand Down
14 changes: 12 additions & 2 deletions test/lib/testlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
import string
import unittest

from mock import MagicMock as Mock
from unittest.mock import MagicMock as Mock
from pyeapi.utils import CliVariants

from pyeapi.client import Node

Expand Down Expand Up @@ -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 = 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)
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)

Expand Down
83 changes: 51 additions & 32 deletions test/system/test_api_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -219,39 +223,31 @@ 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)
if dut.version_number >= '4.23':
dut.config('vrf instance test')
else:
dut.config('vrf definition test')
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('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'])
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('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')
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):
Expand Down Expand Up @@ -407,9 +403,9 @@ def test_minimum_links_valid(self):

def test_minimum_links_invalid_value(self):
for dut in self.duts:
minlinks = random_int(17, 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):
Expand Down Expand Up @@ -508,6 +504,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'])
Expand Down Expand Up @@ -540,21 +555,24 @@ 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)
self.contains('no vxlan multicast-group', dut)

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)
Expand Down Expand Up @@ -635,15 +653,16 @@ 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:
dut.config(['no interface Vxlan1', 'interface Vxlan1'])
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__':
Expand Down
Loading

0 comments on commit b397df6

Please sign in to comment.