From 2bae00e63c438a988401e9e368314b95ff83592e Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Fri, 18 Mar 2022 15:30:28 +0100 Subject: [PATCH 1/3] Add wrapper for translating RpcErrors This will translate unhandled RpcErrors into PortAdmin ProtocolErrors, which are more gracefully handled in the interaction between backend and the UI. Decorate any method that may potentially raise RpcErrors with this. --- python/nav/portadmin/napalm/juniper.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/python/nav/portadmin/napalm/juniper.py b/python/nav/portadmin/napalm/juniper.py index a55a5e1214..84110d3f86 100644 --- a/python/nav/portadmin/napalm/juniper.py +++ b/python/nav/portadmin/napalm/juniper.py @@ -33,6 +33,7 @@ from django.template.loader import get_template from napalm.base.exceptions import ConnectAuthError, ConnectionException from jnpr.junos.op.vlan import VlanTable +from jnpr.junos.exception import RpcError from nav.napalm import connect as napalm_connect from nav.enterprise.ids import VENDOR_ID_JUNIPER_NETWORKS_INC @@ -42,6 +43,7 @@ DeviceNotConfigurableError, AuthenticationError, NoResponseError, + ProtocolError, ) from nav.junos.nav_views import ( EthernetSwitchingInterfaceTable, @@ -61,6 +63,21 @@ SNMP_STATUS_MAP = {"up": 1, "down": 2, True: 1, False: 2} +def wrap_unhandled_rpc_errors(func): + """Decorates RPC-enabled handler function to ensure unhandled RpcErrors are + translated into ProtocolErrors, which can be reported nicely to the end user by + the PortAdmin framework + """ + + def wrap_rpc_errors(*args, **kwargs): + try: + return func(*args, **kwargs) + except RpcError as error: + raise ProtocolError(f"Device raised RpcError: {error.message}") from error + + return wrap_rpc_errors + + class Juniper(ManagementHandler): """Juniper specific version of a Napalm PortAdmin handler. From e7cf409675b6d698ee1001d82afadc47e73a7045 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Fri, 18 Mar 2022 15:31:16 +0100 Subject: [PATCH 2/3] Wrap config changing methods with RpcError handler --- python/nav/portadmin/napalm/juniper.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/nav/portadmin/napalm/juniper.py b/python/nav/portadmin/napalm/juniper.py index 84110d3f86..1c7c4591ce 100644 --- a/python/nav/portadmin/napalm/juniper.py +++ b/python/nav/portadmin/napalm/juniper.py @@ -248,6 +248,7 @@ def get_native_and_trunked_vlans(self, interface) -> Tuple[int, List[int]]: untagged = first_true(vlans, pred=lambda vlan: not vlan.tagged) return (untagged.tag if untagged else None), tagged + @wrap_unhandled_rpc_errors def set_interface_description(self, interface: manage.Interface, description: str): # never set description on units but on master interface master, _ = split_master_unit(interface.ifname) @@ -260,9 +261,11 @@ def set_interface_description(self, interface: manage.Interface, description: st config = template.render(context) self.device.load_merge_candidate(config=config) + @wrap_unhandled_rpc_errors def set_vlan(self, interface: manage.Interface, vlan: int): self.set_access(interface, vlan) + @wrap_unhandled_rpc_errors def set_access(self, interface: manage.Interface, access_vlan: int): master, unit = split_master_unit(interface.ifname) current = InterfaceConfigTable(self.device.device).get(master)[master] @@ -290,6 +293,7 @@ def _save_access_interface(interface: manage.Interface, access_vlan: int): pass interface.save() + @wrap_unhandled_rpc_errors def set_trunk( self, interface: manage.Interface, native_vlan: int, trunk_vlans: Sequence[int] ): @@ -340,6 +344,7 @@ def cycle_interfaces( # and that operation will likely delay at least as much as the wait would have return super().cycle_interfaces(interfaces=interfaces, wait=0, commit=True) + @wrap_unhandled_rpc_errors def set_interface_down(self, interface: manage.Interface): # does not set oper on logical units, only on physical masters master, _unit = split_master_unit(interface.ifname) @@ -349,6 +354,7 @@ def set_interface_down(self, interface: manage.Interface): self._save_interface_oper(interface, interface.OPER_DOWN) + @wrap_unhandled_rpc_errors def set_interface_up(self, interface: manage.Interface): # does not set oper on logical units, only on physical masters master, _unit = split_master_unit(interface.ifname) @@ -368,6 +374,7 @@ def _save_interface_oper(interface: manage.Interface, ifoperstatus: int): ) master_interface.update(ifoperstatus=ifoperstatus) + @wrap_unhandled_rpc_errors def commit_configuration(self): # Only take our sweet time to commit if there are pending changes if self.device.compare_config(): From 7227b5f08f2ac2d4fe1231773894564bc862c6a0 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Mon, 21 Mar 2022 09:19:26 +0100 Subject: [PATCH 3/3] Add test coverage for wrap_unhandled_rpc_errors --- tests/unittests/portadmin/napalm/__init__.py | 0 .../portadmin/napalm/juniper_test.py | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/unittests/portadmin/napalm/__init__.py create mode 100644 tests/unittests/portadmin/napalm/juniper_test.py diff --git a/tests/unittests/portadmin/napalm/__init__.py b/tests/unittests/portadmin/napalm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unittests/portadmin/napalm/juniper_test.py b/tests/unittests/portadmin/napalm/juniper_test.py new file mode 100644 index 0000000000..033e8661ef --- /dev/null +++ b/tests/unittests/portadmin/napalm/juniper_test.py @@ -0,0 +1,39 @@ +# +# Copyright (C) 2022 Sikt AS +# +# This file is part of Network Administration Visualized (NAV). +# +# NAV is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3 as published by +# the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. You should have received a copy of the GNU General Public +# License along with NAV. If not, see . +# +import pytest + +from jnpr.junos.exception import RpcError + +from nav.portadmin.handlers import ProtocolError +from nav.portadmin.napalm.juniper import wrap_unhandled_rpc_errors + + +class TestWrapUnhandledRpcErrors: + def test_rpcerrors_should_become_protocolerrors(self): + @wrap_unhandled_rpc_errors + def wrapped_function(): + raise RpcError("bogus") + + with pytest.raises(ProtocolError): + wrapped_function() + + def test_non_rpcerrors_should_pass_through(self): + @wrap_unhandled_rpc_errors + def wrapped_function(): + raise TypeError("bogus") + + with pytest.raises(TypeError): + wrapped_function()