diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa73838..4bebc38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ --- name: CI -on: [ pull_request, workflow_dispatch ] +on: [ pull_request, workflow_dispatch, pull_request_target ] jobs: build: runs-on: ubuntu-latest diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e8f8c59..9a69d5a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,6 +13,10 @@ build: # nodejs: "20" # rust: "1.70" # golang: "1.20" + jobs: + pre_build: + - make -C docs modules + # Build documentation in the "docs/" directory with Sphinx sphinx: @@ -33,4 +37,4 @@ python: install: - requirements: dev-requirements.txt - method: pip - path: . \ No newline at end of file + path: . diff --git a/VERSION b/VERSION index 21e8796..ee90284 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/dev-requirements.txt b/dev-requirements.txt index 55ef3ec..a929ba7 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,4 @@ -r requirements.txt -mock coveralls twine check-manifest diff --git a/docs/Makefile b/docs/Makefile index e7c16e6..2fff752 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -60,7 +60,7 @@ html: @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." modules: - python $(CWD)/generate_modules.py + python3 $(CWD)/generate_modules.py docs: clean modules html diff --git a/docs/release-notes-1.0.4.rst b/docs/release-notes-1.0.4.rst new file mode 100644 index 0000000..48c7e8b --- /dev/null +++ b/docs/release-notes-1.0.4.rst @@ -0,0 +1,22 @@ +Release 1.0.4 +------------- + +2024-08-19 + \- the major reason for this release is to fix documentation issue on `readthedocs `_ site + +New Modules +^^^^^^^^^^^ + +Enhancements +^^^^^^^^^^^^ + +Fixed +^^^^^ +* System test fixes ( `#285 `_, `#291 `_, + `#302 `_, `#303 `_ ) +* Fixed PR `#289 `_: allow specifying API version in requests. +* Fixed PR `#286 `_: a regression introduced by PR #220, where parsing a non-empty banner section may fail +* Fixed *modules* section on `readthedocs `_ site (PR `#300 `_) + +Known Caveats +^^^^^^^^^^^^^ diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index f1bb9c3..8b3a217 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.3' +__version__ = '1.0.4' __author__ = 'Arista EOS+' diff --git a/pyeapi/client.py b/pyeapi/client.py index 16bf300..582acfe 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -718,27 +718,39 @@ def _chunkify( self, config, indent=0 ): last parsed (sub)section, which in turn may contain sub-sections """ def is_subsection_present( section, indent ): - return any( [line[ indent ] == ' ' for line in section] ) + return any( line[ indent ] == ' ' for line in section ) + + def get_indent( line ): + return len( line ) - len( line.lstrip() ) + sections = {} key = None + banner = None for line in config.splitlines( keepends=True )[ indent > 0: ]: - # indent > 0: no need processing subsection line, which is 1st line - if line[ indent ] == ' ': # section continuation - sections[key] += line + line_rs = line.rstrip() + if indent == 0: + if banner: + sections[ banner ] += line + if line_rs == 'EOF': + banner = None + continue + if line.startswith( 'banner ' ): + banner = line_rs + sections[ banner ] = line + continue + if get_indent( line_rs ) > indent: # i.e. subsection line + # key is always expected to be set by now + sections[ key ] += line continue - # new section is found (if key is not None) - if key: # process prior (last recorded) section - lines = sections[key].splitlines()[ 1: ] - if len( lines ): # section may contain sub-sections - ind = len( lines[0] ) - len( lines[0].lstrip() ) - if is_subsection_present( lines, ind ): - subs = self._chunkify( sections[key], indent=ind ) - subs.update( sections ) - sections = subs - elif indent > 0: # record only subsections - del sections[key] - key = line.rstrip() - sections[key] = line + subsection = sections.get( key, '' ).splitlines()[ 1: ] + if subsection: + sub_indent = get_indent( subsection[0] ) + if is_subsection_present( subsection, sub_indent ): + parsed = self._chunkify( sections[key], indent=sub_indent ) + parsed.update( sections ) + sections = parsed + key = line_rs + sections[ key ] = line return sections def section(self, regex, config='running_config'): diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 3670281..f3e01a2 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -346,6 +346,8 @@ def request(self, commands, encoding=None, reqid=None, **kwargs): reqid = id(self) if reqid is None else reqid params = {'version': 1, 'cmds': commands, 'format': encoding} streaming = False + if 'apiVersion' in kwargs: + params['version'] = kwargs['apiVersion'] if 'autoComplete' in kwargs: params['autoComplete'] = kwargs['autoComplete'] if 'expandAliases' in kwargs: diff --git a/setup.py b/setup.py index 6f5b9d4..56897e3 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ # $ pip install -e .[dev,test] extras_require={ 'dev': ['check-manifest', 'pep8', 'pyflakes', 'twine'], - 'test': ['coverage', 'mock'], + 'test': ['coverage'], }, ) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index 73c07a3..247d472 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -404,6 +404,22 @@ vlan 300 state active no private-vlan ! +banner login ++++++++++++++++++++++++++++++++++++++++++ + banner: + +vlan 1 +this +is the loging ban +that would b emult +EOF +! + +banner motd +this text +can be multine +EOF +! interface Port-Channel10 no description no shutdown @@ -2106,16 +2122,6 @@ no ip tacacs source-interface ! no vxlan vni notation dotted ! -banner login -this -is the loging ban -that would b emult -EOF -banner motd -this text -can be multine -EOF -! system coredump compressed ! no dot1x system-auth-control diff --git a/test/lib/systestlib.py b/test/lib/systestlib.py index 3c0834f..1c1d8e4 100644 --- a/test/lib/systestlib.py +++ b/test/lib/systestlib.py @@ -33,6 +33,7 @@ import random from testlib import get_fixture +from pyeapi.utils import CliVariants import pyeapi.client @@ -48,9 +49,16 @@ def setUp(self): self.duts = list() for name in config.sections(): - if name.startswith('connection:') and 'localhost' not in name: - name = name.split(':')[1] - self.duts.append(pyeapi.client.connect_to(name)) + if not name.startswith('connection:'): + continue + if 'localhost' in name: + continue + name = name.split(':')[1] + self.duts.append( pyeapi.client.connect_to(name) ) + # revert to a legacy behavior for interface availability + if self.duts[ -1 ]: + self.duts[ -1 ].config( CliVariants( + 'service interface inactive expose', 'enable') ) def sort_dict_by_keys(self, d): keys = sorted(d.keys()) @@ -58,9 +66,15 @@ def sort_dict_by_keys(self, d): def random_interface(dut, exclude=None): + # interfaces read in 'show run all' and those actually present may differ, + # thus interface list must be picked from the actually present + if not getattr( random_interface, 'present', False ): + random_interface.present = dut.run_commands( + 'show interfaces', send_enable=False )[ 0 ][ 'interfaces' ].keys() exclude = [] if exclude is None else exclude interfaces = dut.api('interfaces') - names = [name for name in list(interfaces.keys()) if name.startswith('Et')] + names = [ name for name in list(interfaces.keys()) if name.startswith('Et') ] + names = [ name for name in names if name in random_interface.present ] exclude_interfaces = dut.settings.get('exclude_interfaces', []) if exclude_interfaces: diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 96139bf..1d5a1c2 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -44,7 +44,7 @@ class TestResourceInterfaces(DutSystemTest): def test_get(self): for dut in self.duts: - intf = random_interface(dut) + intf = random_interface( dut, exclude=['Ethernet1'] ) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'description this is a test', @@ -390,7 +390,7 @@ def test_get_lacp_mode_with_default(self): def test_minimum_links_valid(self): for dut in self.duts: - minlinks = random_int(1, 16) + minlinks = random_int(1, 8) # some physical duts may have only 8 links dut.config(['no interface Port-Channel1', 'interface Port-Channel1']) result = dut.api('interfaces').set_minimum_links('Port-Channel1', @@ -403,7 +403,7 @@ def test_minimum_links_valid(self): def test_minimum_links_invalid_value(self): for dut in self.duts: - minlinks = random_int(129, 256) # some duts may support up to 128 + minlinks = 1025 # hope it will hold for a while result = dut.api( 'interfaces').set_minimum_links('Port-Channel1', minlinks) self.assertFalse(result) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 3472951..0136add 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -32,7 +32,7 @@ import sys import os import unittest -import imp +import importlib sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) @@ -227,7 +227,7 @@ class TestClient(unittest.TestCase): def setUp(self): if 'EAPI_CONF' in os.environ: del os.environ['EAPI_CONF'] - imp.reload(pyeapi.client) + importlib.reload(pyeapi.client) def test_load_config_for_connection_with_filename(self): conf = get_fixture('eapi.conf')