From fcee20a723aa80432022c2feb634b7104231335a Mon Sep 17 00:00:00 2001 From: Guido Trotter Date: Thu, 20 Jun 2024 13:02:40 +0100 Subject: [PATCH] nxos: implement uptime for get_bgp_neighbors So far we were just ignoring the time value on the switch, and returning -1. This adds a dependency on python-dateutil to calculate years and months lengths, depending on the current date. Alternatively we can just go for a fixed arbitrary time length for years and months. Signed-off-by: Guido Trotter --- napalm/nxos/nxos.py | 61 ++++++++++++++++++- requirements.txt | 1 + .../issue_870/expected_result.json | 4 +- .../show_bgp_all_summary_vrf_all.json | 2 +- test/nxos/test_getters.py | 4 ++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 48a67f483..8ac4dbbee 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations under # the License. +import datetime import ipaddress import json import os @@ -25,6 +26,7 @@ from abc import abstractmethod from builtins import super from collections import defaultdict +from dateutil import relativedelta # import third party lib from typing import ( @@ -79,6 +81,55 @@ }, ) +# Some uptimes are given in ISO8601 duration format +# This regexp implements a subset of that format, as used by the +# nxos switches, with integer values only. +ISO8601_INTEGER_PERIOD_REGEX = re.compile( + r"""(?x) + ^P(?!\b) # start with the literal letter P, with no word boundaries + ((?P[0-9]+)Y)? + ((?P[0-9]+)M)? + ((?P[0-9]+)W)? + ((?P[0-9]+)D)? + (T # optional time component + ((?P[0-9]+)H)? + ((?P[0-9]+)M)? + ((?P[0-9]+)S)? + )?$ + """ +) + + +def duration_to_seconds( + duration: str, endtime: Optional[datetime.datetime] = None +) -> int: + """ + Try to convert an ISO8601 to seconds, using the current time as a base. + + The current time is needed as the duration may include "years" and "months", + which have a variable number of seconds depending on when we start counting from. + """ + if not endtime: + # Use fromtimestamp for ease/uniformity of mocking in unit tests + endtime = datetime.datetime.fromtimestamp(time.time()) + match = ISO8601_INTEGER_PERIOD_REGEX.match(duration) + if not match: + raise ValueError("Can't parse ISO8601 duration") + + mg = match.groupdict(default="0") + rd = relativedelta.relativedelta( + years=int(mg["years"]), + months=int(mg["months"]), + weeks=int(mg["weeks"]), + days=int(mg["days"]), + hours=int(mg["hours"]), + minutes=int(mg["minutes"]), + seconds=int(mg["seconds"]), + ) + starttime = endtime - rd + timediff = endtime - starttime + return int(timediff.total_seconds()) + def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" @@ -1090,6 +1141,9 @@ def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: except NXAPICommandError: vrf_list = [] + # Use fromtimestamp for ease/uniformity of mocking in unit tests + dtnow = datetime.datetime.fromtimestamp(time.time()) + for vrf_dict in vrf_list: result_vrf_dict: models.BGPStateNeighborsPerVRFDict = { "router_id": str(vrf_dict["vrf-router-id"]), @@ -1117,13 +1171,18 @@ def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: bgp_state = bgp_state_dict[state] afid_dict = af_name_dict[int(af_dict["af-id"])] safi_name = afid_dict[int(saf_dict["safi"])] + uptime = -1 + try: + uptime = duration_to_seconds(neighbor_dict["time"], dtnow) + except ValueError: + pass result_peer_dict: models.BGPStateNeighborDict = { "local_as": as_number(vrf_dict["vrf-local-as"]), "remote_as": remoteas, "remote_id": neighborid, "is_enabled": bgp_state["is_enabled"], - "uptime": -1, + "uptime": uptime, "description": "", "is_up": bgp_state["is_up"], "address_family": { diff --git a/requirements.txt b/requirements.txt index ccd3ef5b2..ff1aebbb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ ttp ttp_templates netutils>=1.0.0 typing-extensions>=4.3.0 +python-dateutil diff --git a/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/expected_result.json b/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/expected_result.json index c1d2c4121..ea9de8266 100644 --- a/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/expected_result.json +++ b/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/expected_result.json @@ -159,7 +159,7 @@ "received_prefixes": 13 } }, - "uptime": -1, + "uptime": 4088562, "remote_as": 64862, "description": "", "remote_id": "10.1.1.179", @@ -175,7 +175,7 @@ "received_prefixes": 0 } }, - "uptime": -1, + "uptime": 1496562, "remote_as": 64862, "description": "", "remote_id": "10.1.1.177", diff --git a/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/show_bgp_all_summary_vrf_all.json b/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/show_bgp_all_summary_vrf_all.json index d3e9af1b5..3c96ac340 100644 --- a/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/show_bgp_all_summary_vrf_all.json +++ b/test/nxos/mocked_data/test_get_bgp_neighbors/issue_870/show_bgp_all_summary_vrf_all.json @@ -57,7 +57,7 @@ "inq": "0", "outq": "0", "neighboras": "64862", - "time": "6w3d", + "time": "P1M17DT7H42M42S", "state": "Established", "prefixreceived": "13" } diff --git a/test/nxos/test_getters.py b/test/nxos/test_getters.py index 3baa5c4b2..9f38d6322 100644 --- a/test/nxos/test_getters.py +++ b/test/nxos/test_getters.py @@ -27,3 +27,7 @@ def test_get_interfaces(self, test_case): assert helpers.test_model(models.InterfaceDict, interface_data) return get_interfaces + + test_get_bgp_neighbors = patch("time.time", mock_time)( + BaseTestGetters.test_get_bgp_neighbors + )