Skip to content

Commit

Permalink
Merge branch 'develop' into fn-0039
Browse files Browse the repository at this point in the history
  • Loading branch information
mirceaulinic committed Mar 23, 2020
2 parents 81090d9 + 8deb8b6 commit bcd7fa2
Show file tree
Hide file tree
Showing 55 changed files with 4,412 additions and 77 deletions.
22 changes: 22 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py

# Optionally build your docs in additional formats such as PDF and ePub
formats: all

# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- method: pip
path: .
- requirements: docs/requirements.txt
- requirements: requirements.txt
5 changes: 3 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

# General information about the project.
project = u"NAPALM"
copyright = u"2016, David Barroso"
copyright = u"2020, David Barroso"

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
Expand All @@ -64,7 +64,7 @@
# The short X.Y version.
version = "0"
# The full version, including alpha/beta/rc tags.
release = "1"
release = "3"

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down Expand Up @@ -302,6 +302,7 @@
METHOD_ALIASES = {
"get_config_filtered": "get_config",
"get_arp_table_with_vrf": "get_arp_table",
"get_route_to_longer": "get_route_to",
}


Expand Down
1 change: 1 addition & 0 deletions docs/support/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ ____________________________________
* :code:`allow_agent` (ios, iosxr, nxos_ssh) - Paramiko argument, enable connecting to the SSH agent (default: ``False``).
* :code:`alt_host_keys` (ios, iosxr, nxos_ssh) - If ``True``, host keys will be loaded from the file specified in ``alt_key_file``.
* :code:`alt_key_file` (ios, iosxr, nxos_ssh) - SSH host key file to use (if ``alt_host_keys`` is ``True``).
* :code:`auto_probe` (junos) - A timeout in seconds, for which to probe the device. Probing determines if the device accepts remote connections. If `auto_probe` is set to ``0``, no probing will be done. (default: ``0``).
* :code:`auto_rollback_on_error` (ios) - Disable automatic rollback (certain versions of IOS support configure replace, but not rollback on error) (default: ``True``).
* :code:`config_lock` (iosxr, junos) - Lock the config during open() (default: ``False``).
* :code:`lock_disable` (junos) - Disable all configuration locking for management by an external system (default: ``False``).
Expand Down
4 changes: 4 additions & 0 deletions napalm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pkg_resources
import sys
import logging

from napalm.base import get_network_driver
from napalm._SUPPORTED_DRIVERS import SUPPORTED_DRIVERS

Expand All @@ -26,3 +28,5 @@
__version__ = "Not installed"

__all__ = ("get_network_driver", "SUPPORTED_DRIVERS")

logger = logging.getLogger("napalm")
5 changes: 3 additions & 2 deletions napalm/base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ def get_interfaces_ip(self):
Returns all configured IP addresses on all interfaces as a dictionary of dictionaries.
Keys of the main dictionary represent the name of the interface.
Values of the main dictionary represent are dictionaries that may consist of two keys
'ipv4' and 'ipv6' (one, both or none) which are themselvs dictionaries witht the IP
'ipv4' and 'ipv6' (one, both or none) which are themselves dictionaries with the IP
addresses as keys.
Each IP Address dictionary has the following keys:
Expand Down Expand Up @@ -1003,14 +1003,15 @@ def get_mac_address_table(self):
"""
raise NotImplementedError

def get_route_to(self, destination="", protocol=""):
def get_route_to(self, destination="", protocol="", longer=False):

"""
Returns a dictionary of dictionaries containing details of all available routes to a
destination.
:param destination: The destination prefix to be used when filtering the routes.
:param protocol (optional): Retrieve the routes only for a specific protocol.
:param longer (optional): Retrieve more specific routes as well.
Each inner dictionary contains the following fields:
Expand Down
5 changes: 5 additions & 0 deletions napalm/base/canonical_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
"Tunnel": "Tunnel",
"Tun": "Tunnel",
"Tu": "Tunnel",
"Twe": "TwentyFiveGigE",
"Tw": "TwoGigabitEthernet",
"Two": "TwoGigabitEthernet",
"Virtual-Access": "Virtual-Access",
"Vi": "Virtual-Access",
"Virtual-Template": "Virtual-Template",
Expand Down Expand Up @@ -99,6 +102,8 @@
"Serial": "Se",
"TenGigabitEthernet": "Te",
"Tunnel": "Tu",
"TwoGigabitEthernet": "Two",
"TwentyFiveGigE": "Twe",
"Virtual-Access": "Vi",
"Virtual-Template": "Vt",
"VLAN": "Vl",
Expand Down
65 changes: 51 additions & 14 deletions napalm/base/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import sys
import itertools
import logging

# third party libs
import jinja2
Expand All @@ -19,20 +20,25 @@
from napalm.base.utils.jinja_filters import CustomJinjaFilters
from napalm.base.canonical_map import base_interfaces, reverse_mapping

# -------------------------------------------------------------------
# Functional Global
# -------------------------------------------------------------------
logger = logging.getLogger(__name__)

# ----------------------------------------------------------------------------------------------------------------------

# -------------------------------------------------------------------
# helper classes -- will not be exported
# ----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------
class _MACFormat(mac_unix):
pass


_MACFormat.word_fmt = "%.2X"


# ----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------
# callable helpers
# ----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------
def load_template(
cls,
template_name,
Expand Down Expand Up @@ -155,12 +161,19 @@ def regex_find_txt(pattern, text, default=""):
value = re.findall(pattern, text)
try:
if not value:
raise Exception
logger.error("No Regex match found for pattern: %s" % (str(pattern)))
raise Exception("No Regex match found for pattern: %s" % (str(pattern)))
if not isinstance(value, type(default)):
if isinstance(value, list) and len(value) == 1:
value = value[0]
value = type(default)(value)
except Exception: # in case of any exception, returns default
except Exception as regexFindTxtErr01: # in case of any exception, returns default
logger.error(
'errorCode="regexFindTxtErr01" in napalm.base.helpers with systemMessage="%s"\
message="Error while attempting to find regex pattern, \
default to empty string"'
% (regexFindTxtErr01)
)
value = default
return value

Expand Down Expand Up @@ -203,9 +216,21 @@ def textfsm_extractor(cls, template_name, raw_text):
textfsm_data.append(entry)

return textfsm_data
except IOError: # Template not present in this class
except IOError as textfsmExtractorErr01: # Template not present in this class
logger.error(
'errorCode="textfsmExtractorErr01" in napalm.base.helpers with systemMessage="%s"\
message="Error while attempting to apply a textfsm template to \
format the output returned from the device,\
continuing loop..."'
% (textfsmExtractorErr01)
)
continue # Continue up the MRO
except textfsm.TextFSMTemplateError as tfte:
logging.error(
"Wrong format of TextFSM template {template_name}: {error}".format(
template_name=template_name, error=str(tfte)
)
)
raise napalm.base.exceptions.TemplateRenderException(
"Wrong format of TextFSM template {template_name}: {error}".format(
template_name=template_name, error=str(tfte)
Expand All @@ -219,26 +244,38 @@ def textfsm_extractor(cls, template_name, raw_text):
)


def find_txt(xml_tree, path, default=""):
def find_txt(xml_tree, path, default="", namespaces=None):
"""
Extracts the text value from an XML tree, using XPath.
In case of error, will return a default value.
:param xml_tree: the XML Tree object. Assumed is <type 'lxml.etree._Element'>.
:param path: XPath to be applied, in order to extract the desired data.
:param default: Value to be returned in case of error.
:param xml_tree: the XML Tree object. Assumed is <type 'lxml.etree._Element'>.
:param path: XPath to be applied, in order to extract the desired data.
:param default: Value to be returned in case of error.
:param namespaces: prefix-namespace mappings to process XPath
:return: a str value.
"""
value = ""
try:
xpath_applied = xml_tree.xpath(path) # will consider the first match only
if len(xpath_applied) and xpath_applied[0] is not None:
xpath_applied = xml_tree.xpath(
path, namespaces=namespaces
) # will consider the first match only
xpath_length = len(xpath_applied) # get a count of items in XML tree
if xpath_length and xpath_applied[0] is not None:
xpath_result = xpath_applied[0]
if isinstance(xpath_result, type(xml_tree)):
value = xpath_result.text.strip()
else:
value = xpath_result
except Exception: # in case of any exception, returns default
else:
if xpath_applied == "":
logger.debug(
"Unable to find the specified-text-element/XML path: %s in \
the XML tree provided. Total Items in XML tree: %d "
% (path, xpath_length)
)
except Exception as findTxtErr01: # in case of any exception, returns default
logger.error(findTxtErr01)
value = default
return str(value)

Expand Down
18 changes: 18 additions & 0 deletions napalm/base/test/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,24 @@ def test_get_route_to(self, test_case):

return get_route_to

@wrap_test_cases
def test_get_route_to_longer(self, test_case):
"""Test get_route_to with longer=True"""
destination = "1.0.4.0/24"
protocol = "bgp"

get_route_to = self.device.get_route_to(
destination=destination, protocol=protocol, longer=True
)

assert len(get_route_to) > 0

for prefix, routes in get_route_to.items():
for route in routes:
assert helpers.test_model(models.route, route)

return get_route_to

@wrap_test_cases
def test_get_snmp_information(self, test_case):
"""Test get_snmp_information."""
Expand Down
53 changes: 41 additions & 12 deletions napalm/eos/eos.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

# third party libs
import pyeapi
from pyeapi.eapilib import ConnectionError
from pyeapi.eapilib import ConnectionError, CommandError

# NAPALM base
import napalm.base.helpers
Expand Down Expand Up @@ -1168,7 +1168,7 @@ def get_mac_address_table(self):

return mac_table

def get_route_to(self, destination="", protocol=""):
def get_route_to(self, destination="", protocol="", longer=False):
routes = {}

# Placeholder for vrf arg
Expand All @@ -1191,12 +1191,17 @@ def get_route_to(self, destination="", protocol=""):
commands = []
for _vrf in vrfs:
commands.append(
"show ip{ipv} route vrf {_vrf} {destination} {protocol} detail".format(
ipv=ipv, _vrf=_vrf, destination=destination, protocol=protocol
"show ip{ipv} route vrf {_vrf} {destination} {longer} {protocol} detail".format(
ipv=ipv,
_vrf=_vrf,
destination=destination,
longer="longer-prefixes" if longer else "",
protocol=protocol,
)
)

commands_output = self.device.run_commands(commands)
vrf_cache = {}

for _vrf, command_output in zip(vrfs, commands_output):
if ipv == "v6":
Expand Down Expand Up @@ -1231,14 +1236,38 @@ def get_route_to(self, destination="", protocol=""):
nexthop_ip = napalm.base.helpers.ip(next_hop.get("nexthopAddr"))
nexthop_interface_map[nexthop_ip] = next_hop.get("interface")
metric = route_details.get("metric")
command = "show ip{ipv} bgp {destination} detail vrf {_vrf}".format(
ipv=ipv, destination=prefix, _vrf=_vrf
)
vrf_details = (
self.device.run_commands([command])[0]
.get("vrfs", {})
.get(_vrf, {})
)
if _vrf not in vrf_cache.keys():
try:
command = "show ip{ipv} bgp {dest} {longer} detail vrf {_vrf}".format(
ipv=ipv,
dest=destination,
longer="longer-prefixes" if longer else "",
_vrf=_vrf,
)
vrf_cache.update(
{
_vrf: self.device.run_commands([command])[0]
.get("vrfs", {})
.get(_vrf, {})
}
)
except CommandError:
# Newer EOS can't mix longer-prefix and detail
command = "show ip{ipv} bgp {dest} {longer} vrf {_vrf}".format(
ipv=ipv,
dest=destination,
longer="longer-prefixes" if longer else "",
_vrf=_vrf,
)
vrf_cache.update(
{
_vrf: self.device.run_commands([command])[0]
.get("vrfs", {})
.get(_vrf, {})
}
)

vrf_details = vrf_cache.get(_vrf)
local_as = vrf_details.get("asn")
bgp_routes = (
vrf_details.get("bgpRouteEntries", {})
Expand Down
Loading

0 comments on commit bcd7fa2

Please sign in to comment.