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')