Skip to content

Commit

Permalink
Add pyeapi.Node shim to handle CLI translation
Browse files Browse the repository at this point in the history
Create a new Node class that checks EOS versions for forward and
backward compatible command translation for FN-0039, along with unit
tests for version detection and translation
  • Loading branch information
praniq authored and bewing committed Apr 29, 2020
1 parent e021fab commit 38a3a02
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 6 deletions.
31 changes: 28 additions & 3 deletions napalm/eos/eos.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
CommandErrorException,
)
from napalm.eos.constants import LLDP_CAPAB_TRANFORM_TABLE
from napalm.eos.pyeapi_syntax_wrapper import Node
from napalm.eos.utils.versions import EOSVersion
import napalm.base.constants as c

# local modules
Expand Down Expand Up @@ -83,6 +85,25 @@ class EOSDriver(NetworkDriver):
re.VERBOSE,
)

def _determine_syntax_version(self, ver_output):
"""
Determine cli syntax version from "sh ver" output
Syntax versions:
1: all EOS versions before 4.23.0
2: all EOS version 4.23.0 and higher
:param ver_output: list of lines for "sh ver" command output
:return: int: cli version
"""
regexp = re.compile(r"^Software image version:\s+(?P<version>\d+\.\d+\.\d+)")

for line in ver_output.split("\n"):
m = regexp.match(line)

if m and EOSVersion(m.group("version")) >= EOSVersion("4.23.0"):
return 2

return 1

def __init__(self, hostname, username, password, timeout=60, optional_args=None):
"""
Initialize EOS Driver.
Expand Down Expand Up @@ -156,11 +177,15 @@ def open(self):
)

if self.device is None:
self.device = pyeapi.client.Node(connection, enablepwd=self.enablepwd)
self.device = Node(connection, enablepwd=self.enablepwd)
# does not raise an Exception if unusable

# let's try to run a very simple command
self.device.run_commands(["show clock"], encoding="text")
# let's try to determine if we need to use new EOS cli syntax
cli_version = self._determine_syntax_version(
self.device.run_commands(["show version"], encoding="text")[0]["output"]
)

self.device.update_cli_version(cli_version)
except ConnectionError as ce:
# and this is raised either if device not avaiable
# either if HTTP(S) agent is not enabled
Expand Down
40 changes: 40 additions & 0 deletions napalm/eos/pyeapi_syntax_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""pyeapi wrapper to fix cli syntax change"""
import pyeapi
from napalm.eos.utils.cli_syntax import cli_convert


class Node(pyeapi.client.Node):
"""
pyeapi node wrapper to fix cli syntax change
"""

def __init__(self, *args, **kwargs):
if "cli_version" in kwargs:
self.cli_version = kwargs["cli_version"]
del kwargs["cli_version"]
else:
self.cli_version = 1

super(Node, self).__init__(*args, **kwargs)

def update_cli_version(self, version):
"""
Update CLI version number for this device
:param version: int: version number
:return: None
"""
self.cli_version = version

def run_commands(self, commands, **kwargs):
"""
Run commands wrapper
:param commands: list of commands
:param kwargs: other args
:return: list of outputs
"""
if isinstance(commands, str):
new_commands = [cli_convert(commands, self.cli_version)]
else:
new_commands = [cli_convert(cmd, self.cli_version) for cmd in commands]

return super(Node, self).run_commands(new_commands, **kwargs)
357 changes: 357 additions & 0 deletions napalm/eos/utils/cli_syntax.py

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions napalm/eos/utils/versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Some functions to work with EOS version numbers"""
import re


class EOSVersion:
"""
Class to represent EOS version
"""

def __init__(self, version):
"""
Create object
:param version: str: version string
"""
self.version = version
self.numbers = []
self.type = ""

self._parse(version)

def _parse(self, version):
"""
Parse version string
:param version: str: version
:return: None
"""
m = re.match(r"^(?P<numbers>\d[\d.]+\d)", version)

if m:
self.numbers = m.group("numbers").split(".")

def __lt__(self, other):
if not len(self.numbers):
return True

for x, y in zip(self.numbers, other.numbers):
if x < y:
return True
elif x > y:
return False

return False

def __gt__(self, other):
if not len(self.numbers):
return False

for x, y in zip(self.numbers, other.numbers):
if x > y:
return True
elif x < y:
return False

return False

def __eq__(self, other):
if len(self.numbers) != len(other.numbers):
return False

for x, y in zip(self.numbers, other.numbers):
if x != y:
return False

return True

def __le__(self, other):
return self < other or self == other

def __ge__(self, other):
return self > other or self == other
8 changes: 8 additions & 0 deletions test/eos/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,11 @@ def run_commands(self, command_list, encoding="json"):
result.append({"output": self.read_txt_file(full_path)})

return result

def update_cli_version(self, version):
"""
Update CLI version number for this device
:param version: int: version number
:return: None
"""
self.cli_version = version
3 changes: 0 additions & 3 deletions test/eos/mocked_data/show_clock.text

This file was deleted.

13 changes: 13 additions & 0 deletions test/eos/mocked_data/show_version.text
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Arista DCS-7150S-64-CL-R
Hardware version: 01.01
Serial number: JPE00000000
System MAC address: 001c.7327.07e0

Software image version: 4.22.4M-2GB
Architecture: i686
Internal build version: 4.22.4M-2GB-15583082.4224M
Internal build ID: 523a3357-484c-4110-9019-39750ffa8af5

Uptime: 3 weeks, 1 days, 2 hours and 1 minutes
Total memory: 4009188 kB
Free memory: 2515256 kB
51 changes: 51 additions & 0 deletions test/eos/test_cli_syntax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Tests for EOS cli_syntax
"""
from napalm.eos.utils.cli_syntax import cli_convert


def test_cli_no_change_v2():
"""
Test no change for basic commands in version 2
:return:
"""
commands = ["show version", "show interfaces"]

for c in commands:
assert c == cli_convert(c, 2)
assert c == cli_convert(c, 1)


def test_cli_no_change_non_exist_version():
"""
Test no change for basic commands and non-existing versions
:return:
"""
commands = ["show version", "show interfaces"]

for c in commands:
assert c == cli_convert(c, 100000)


def test_cli_change_exact():
"""
Test cli change for exact commands
"""
commands = ["show ipv6 bgp neighbors", "show lldp traffic"]
expect = ["show ipv6 bgp peers", "show lldp counters"]

for c, e in zip(commands, expect):
assert e == cli_convert(c, 2)
assert c == cli_convert(e, 1)


def test_cli_change_long_commands():
"""
Test cli change for long commands
"""
commands = ["show ipv6 bgp neighbors vrf all", "show lldp traffic | include test"]
expect = ["show ipv6 bgp peers vrf all", "show lldp counters | include test"]

for c, e in zip(commands, expect):
assert e == cli_convert(c, 2)
assert c == cli_convert(e, 1)
28 changes: 28 additions & 0 deletions test/eos/test_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Tests for versions utils"""
from napalm.eos.utils.versions import EOSVersion


def test_version_create():
"""
Test we can create version object
"""
versions = ["4.21.7.1M", "4.20.24F-2GB", "blablabla"]

for v in versions:
assert v == EOSVersion(v).version


def test_version_comparisons():
"""
Test version comparison
"""
old_version = "4.21.7.1M"
new_verion = "4.23.0F"

assert EOSVersion(old_version) < EOSVersion(new_verion)
assert EOSVersion(new_verion) > EOSVersion(old_version)
assert EOSVersion(old_version) <= EOSVersion(new_verion)
assert EOSVersion(new_verion) >= EOSVersion(old_version)
assert not EOSVersion(old_version) < EOSVersion(old_version)
assert EOSVersion(old_version) == EOSVersion(old_version)
assert EOSVersion(old_version) <= EOSVersion(old_version)

0 comments on commit 38a3a02

Please sign in to comment.