Skip to content

Commit 4af2aba

Browse files
authored
Merge pull request #1252 from napalm-automation/develop
Release 3.1.0
2 parents 456f736 + 70cf5bf commit 4af2aba

File tree

21 files changed

+7181
-90
lines changed

21 files changed

+7181
-90
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[![PyPI](https://img.shields.io/pypi/v/napalm.svg)](https://pypi.python.org/pypi/napalm)
2+
[![PyPI versions](https://img.shields.io/pypi/pyversions/napalm.svg)](https://pypi.python.org/pypi/napalm)
23
[![Build Status](https://travis-ci.org/napalm-automation/napalm.svg?branch=master)](https://travis-ci.org/napalm-automation/napalm)
34
[![Coverage Status](https://coveralls.io/repos/github/napalm-automation/napalm/badge.svg)](https://coveralls.io/github/napalm-automation/napalm)
45
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
@@ -31,6 +32,9 @@ Install
3132
pip install napalm
3233
```
3334

35+
*Note*: Beginning with release 3.0.0 and later, NAPALM offers support for
36+
Python 3.6+ only.
37+
3438

3539
Upgrading
3640
=========

docs/installation/index.rst

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
Installation
22
============
33

4-
54
Full installation
65
-----------------
76

87
You can install napalm with pip:
98

109
.. code-block:: bash
11-
10+
1211
pip install napalm
1312
14-
That will install all the drivers currently available.
13+
That will install all the core drivers currently available.
14+
15+
.. note::
16+
17+
Beginning with release 3.0.0 and later, NAPALM offers support for Python
18+
3.6+ only.
1519

1620

1721
OS Package Managers

napalm/base/base.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,14 @@ def _netmiko_open(self, device_type, netmiko_optional_args=None):
9494
except NetMikoTimeoutException:
9595
raise ConnectionException("Cannot connect to {}".format(self.hostname))
9696

97-
# ensure in enable mode if not force disable
98-
if not self.force_no_enable:
97+
# Disable enable mode if force_no_enable is true (for NAPALM drivers
98+
# that support force_no_enable)
99+
try:
100+
if not self.force_no_enable:
101+
self._netmiko_device.enable()
102+
except AttributeError:
99103
self._netmiko_device.enable()
104+
100105
return self._netmiko_device
101106

102107
def _netmiko_close(self):

napalm/base/helpers.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def textfsm_extractor(cls, template_name, raw_text):
248248
def find_txt(xml_tree, path, default="", namespaces=None):
249249
"""
250250
Extracts the text value from an XML tree, using XPath.
251-
In case of error, will return a default value.
251+
In case of error or text element unavailability, will return a default value.
252252
253253
:param xml_tree: the XML Tree object. Assumed is <type 'lxml.etree._Element'>.
254254
:param path: XPath to be applied, in order to extract the desired data.
@@ -265,7 +265,10 @@ def find_txt(xml_tree, path, default="", namespaces=None):
265265
if xpath_length and xpath_applied[0] is not None:
266266
xpath_result = xpath_applied[0]
267267
if isinstance(xpath_result, type(xml_tree)):
268-
value = xpath_result.text.strip()
268+
if xpath_result.text:
269+
value = xpath_result.text.strip()
270+
else:
271+
value = default
269272
else:
270273
value = xpath_result
271274
else:

napalm/ios/ios.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -799,13 +799,21 @@ def get_optics(self):
799799
for optics_entry in split_output.splitlines():
800800
# Example, Te1/0/1 34.6 3.29 -2.0 -3.5
801801
try:
802+
optics_entry = optics_entry.strip("-")
802803
split_list = optics_entry.split()
803804
except ValueError:
804805
return {}
805806

806-
int_brief = split_list[0]
807-
output_power = split_list[3]
808-
input_power = split_list[4]
807+
current = 0
808+
if len(split_list) == 5:
809+
int_brief = split_list[0]
810+
output_power = split_list[3]
811+
input_power = split_list[4]
812+
elif len(split_list) >= 6:
813+
int_brief = split_list[0]
814+
current = split_list[3]
815+
output_power = split_list[4]
816+
input_power = split_list[5]
809817

810818
port = canonical_interface_name(int_brief)
811819

@@ -841,7 +849,7 @@ def get_optics(self):
841849
"max": -100.0,
842850
},
843851
"laser_bias_current": {
844-
"instant": 0.0,
852+
"instant": (float(current) if "current" else -100.0),
845853
"avg": 0.0,
846854
"min": 0.0,
847855
"max": 0.0,
@@ -3103,7 +3111,7 @@ def get_users(self):
31033111
"""
31043112
username_regex = (
31053113
r"^username\s+(?P<username>\S+)\s+(?:privilege\s+(?P<priv_level>\S+)"
3106-
r"\s+)?(?:secret \d+\s+(?P<pwd_hash>\S+))?$"
3114+
r"\s+)?(?:(password|secret) \d+\s+(?P<pwd_hash>\S+))?$"
31073115
)
31083116
pub_keychain_regex = (
31093117
r"^\s+username\s+(?P<username>\S+)(?P<keys>(?:\n\s+key-hash\s+"
@@ -3112,6 +3120,9 @@ def get_users(self):
31123120
users = {}
31133121
command = "show run | section username"
31143122
output = self._send_command(command)
3123+
if "Invalid input detected" in output:
3124+
command = "show run | include username"
3125+
output = self._send_command(command)
31153126
for match in re.finditer(username_regex, output, re.M):
31163127
users[match.groupdict()["username"]] = {
31173128
"level": int(match.groupdict()["priv_level"])

napalm/junos/junos.py

+3
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@ def get_environment(self):
429429
structured_object_data["class"] = current_class
430430

431431
if structured_object_data["class"] == "Power":
432+
# Make sure naming is consistent
433+
sensor_object = sensor_object.replace("PEM", "Power Supply")
434+
432435
# Create a dict for the 'power' key
433436
try:
434437
environment_data["power"][sensor_object] = {}

napalm/nxos/nxos.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from napalm.base.exceptions import CommandErrorException
4444
from napalm.base.exceptions import ReplaceConfigException
4545
from napalm.base.helpers import generate_regex_or
46+
from napalm.base.helpers import as_number
4647
from napalm.base.netmiko_helpers import netmiko_args
4748
import napalm.base.constants as c
4849

@@ -962,17 +963,15 @@ def get_bgp_neighbors(self):
962963

963964
for neighbor_dict in neighbors_list:
964965
neighborid = napalm.base.helpers.ip(neighbor_dict["neighborid"])
965-
remoteas = napalm.base.helpers.as_number(
966-
neighbor_dict["neighboras"]
967-
)
966+
remoteas = as_number(neighbor_dict["neighboras"])
968967
state = str(neighbor_dict["state"])
969968

970969
bgp_state = bgp_state_dict[state]
971970
afid_dict = af_name_dict[int(af_dict["af-id"])]
972971
safi_name = afid_dict[int(saf_dict["safi"])]
973972

974973
result_peer_dict = {
975-
"local_as": int(vrf_dict["vrf-local-as"]),
974+
"local_as": as_number(vrf_dict["vrf-local-as"]),
976975
"remote_as": remoteas,
977976
"remote_id": neighborid,
978977
"is_enabled": bgp_state["is_enabled"],

napalm/nxos_ssh/nxos_ssh.py

+113
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,17 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None)
429429
hostname, username, password, timeout=timeout, optional_args=optional_args
430430
)
431431
self.platform = "nxos_ssh"
432+
self.connector_type_map = {
433+
"1000base-LH": "LC_CONNECTOR",
434+
"1000base-SX": "LC_CONNECTOR",
435+
"1000base-T": "Unknown",
436+
"10Gbase-LR": "LC_CONNECTOR",
437+
"10Gbase-SR": "LC_CONNECTOR",
438+
"SFP-H10GB-CU1M": "DAC_CONNECTOR",
439+
"SFP-H10GB-CU1.45M": "DAC_CONNECTOR",
440+
"SFP-H10GB-CU3M": "DAC_CONNECTOR",
441+
"SFP-H10GB-CU3.45M": "DAC_CONNECTOR",
442+
}
432443

433444
def open(self):
434445
self.device = self._netmiko_open(
@@ -1528,3 +1539,105 @@ def get_vlans(self):
15281539
"interfaces": self._parse_vlan_ports(vlan["vlanshowplist-ifidx"]),
15291540
}
15301541
return vlans
1542+
1543+
def get_optics(self):
1544+
command = "show interface transceiver details"
1545+
output = self._send_command(command)
1546+
1547+
# Formatting data into return data structure
1548+
optics_detail = {}
1549+
1550+
# Extraction Regexps
1551+
port_ts_re = re.compile(r"^Ether.*?(?=\nEther|\Z)", re.M | re.DOTALL)
1552+
port_re = re.compile(r"^(Ether.*)[ ]*?$", re.M)
1553+
vendor_re = re.compile("name is (.*)$", re.M)
1554+
vendor_part_re = re.compile("part number is (.*)$", re.M)
1555+
vendor_rev_re = re.compile("revision is (.*)$", re.M)
1556+
serial_no_re = re.compile("serial number is (.*)$", re.M)
1557+
type_no_re = re.compile("type is (.*)$", re.M)
1558+
rx_instant_re = re.compile(r"Rx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M)
1559+
tx_instant_re = re.compile(r"Tx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M)
1560+
current_instant_re = re.compile(r"Current[ ]+(?:(\S+?)[ ]+mA|(N.A))", re.M)
1561+
1562+
port_ts_l = port_ts_re.findall(output)
1563+
1564+
for port_ts in port_ts_l:
1565+
port = port_re.search(port_ts).group(1)
1566+
# No transceiver is present in those case
1567+
if "transceiver is not present" in port_ts:
1568+
continue
1569+
if "transceiver is not applicable" in port_ts:
1570+
continue
1571+
port_detail = {"physical_channels": {"channel": []}}
1572+
# No metric present
1573+
vendor = vendor_re.search(port_ts).group(1)
1574+
vendor_part = vendor_part_re.search(port_ts).group(1)
1575+
vendor_rev = vendor_rev_re.search(port_ts).group(1)
1576+
serial_no = serial_no_re.search(port_ts).group(1)
1577+
type_s = type_no_re.search(port_ts).group(1)
1578+
state = {
1579+
"vendor": vendor.strip(),
1580+
"vendor_part": vendor_part.strip(),
1581+
"vendor_rev": vendor_rev.strip(),
1582+
"serial_no": serial_no.strip(),
1583+
"connector_type": self.connector_type_map.get(type_s, "Unknown"),
1584+
}
1585+
if "DOM is not supported" not in port_ts:
1586+
res = rx_instant_re.search(port_ts)
1587+
input_power = res.group(1) or res.group(2)
1588+
res = tx_instant_re.search(port_ts)
1589+
output_power = res.group(1) or res.group(2)
1590+
res = current_instant_re.search(port_ts)
1591+
current = res.group(1) or res.group(2)
1592+
1593+
# If interface is shutdown it returns "N/A" as output power
1594+
# or "N/A" as input power
1595+
# Converting that to -100.0 float
1596+
try:
1597+
float(output_power)
1598+
except ValueError:
1599+
output_power = -100.0
1600+
try:
1601+
float(input_power)
1602+
except ValueError:
1603+
input_power = -100.0
1604+
try:
1605+
float(current)
1606+
except ValueError:
1607+
current = -100.0
1608+
1609+
# Defaulting avg, min, max values to -100.0 since device does not
1610+
# return these values
1611+
optic_states = {
1612+
"index": 0,
1613+
"state": {
1614+
"input_power": {
1615+
"instant": (
1616+
float(input_power) if "input_power" else -100.0
1617+
),
1618+
"avg": -100.0,
1619+
"min": -100.0,
1620+
"max": -100.0,
1621+
},
1622+
"output_power": {
1623+
"instant": (
1624+
float(output_power) if "output_power" else -100.0
1625+
),
1626+
"avg": -100.0,
1627+
"min": -100.0,
1628+
"max": -100.0,
1629+
},
1630+
"laser_bias_current": {
1631+
"instant": (float(current) if "current" else -100.0),
1632+
"avg": 0.0,
1633+
"min": 0.0,
1634+
"max": 0.0,
1635+
},
1636+
},
1637+
}
1638+
port_detail["physical_channels"]["channel"].append(optic_states)
1639+
1640+
port_detail["state"] = state
1641+
optics_detail[port] = port_detail
1642+
1643+
return optics_detail

requirements-dev.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
black==19.10b0
2-
coveralls==2.0.0
2+
coveralls==2.1.1
33
ddt==1.4.1
44
flake8-import-order==0.18.1
5-
pytest==5.4.2
6-
pytest-cov==2.8.1
5+
pytest==5.4.3
6+
pytest-cov==2.10.0
77
pytest-json==0.4.0
88
pytest-pythonpath==0.7.3
99
pylama==7.7.1
1010
mock==4.0.2
11-
tox==3.15.0
11+
tox==3.18.0

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
setup(
1414
name="napalm",
15-
version="3.0.1",
15+
version="3.1.0",
1616
packages=find_packages(exclude=("test*",)),
1717
test_suite="test_base",
1818
author="David Barroso, Kirk Byers, Mircea Ulinic",

0 commit comments

Comments
 (0)