From 505f72f1bc77c9413f0fd82f4688ea54dc2be626 Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Sun, 2 Feb 2025 15:05:18 +0100 Subject: [PATCH] ipdb: provide IPDB API to cover kuryr needs + tests --- pyroute2/__init__.py | 2 +- pyroute2/{noipdb.py => ipdb/__init__.py} | 81 +++++++++++++++++---- tests/test_integration/conftest.py | 5 ++ tests/test_integration/test_kuryr.py | 89 ++++++++++++++++++++++-- 4 files changed, 160 insertions(+), 17 deletions(-) rename pyroute2/{noipdb.py => ipdb/__init__.py} (67%) diff --git a/pyroute2/__init__.py b/pyroute2/__init__.py index 7725c3d9a..84bddd723 100644 --- a/pyroute2/__init__.py +++ b/pyroute2/__init__.py @@ -18,6 +18,7 @@ from pyroute2.conntrack import Conntrack, ConntrackEntry from pyroute2.devlink import DL from pyroute2.ethtool.ethtool import Ethtool +from pyroute2.ipdb import IPDB, CommitException, CreateException from pyroute2.iproute import ( AsyncIPRoute, ChaoticIPRoute, @@ -52,7 +53,6 @@ from pyroute2.netlink.rtnl.iprsocket import AsyncIPRSocket, IPRSocket from pyroute2.netlink.taskstats import TaskStats from pyroute2.netlink.uevent import UeventSocket -from pyroute2.noipdb import IPDB, CommitException, CreateException from pyroute2.nslink.nspopen import NSPopen from pyroute2.plan9.client import Plan9ClientSocket from pyroute2.plan9.server import Plan9ServerSocket diff --git a/pyroute2/noipdb.py b/pyroute2/ipdb/__init__.py similarity index 67% rename from pyroute2/noipdb.py rename to pyroute2/ipdb/__init__.py index ecaf7a375..f8d20f337 100644 --- a/pyroute2/noipdb.py +++ b/pyroute2/ipdb/__init__.py @@ -1,6 +1,8 @@ +import errno import logging from pyroute2.ndb.main import NDB +from pyroute2.netlink.exceptions import NetlinkError log = logging.getLogger(__name__) @@ -14,8 +16,12 @@ class CommitException(Exception): class ObjectProxy(dict): - def __init__(self, obj): + + _translate_keys = {} + + def __init__(self, obj, ready=True): self._obj = obj + self._ready = ready def __getattribute__(self, key): if key[:4] == 'set_': @@ -31,15 +37,21 @@ def set_value(value): return super(ObjectProxy, self).__getattribute__(key) def __setattr__(self, key, value): - if key == '_obj': + if key in ('_obj', '_ready', '_translate_keys'): super(ObjectProxy, self).__setattr__(key, value) else: super(ObjectProxy, self).__getattribute__('_obj')[key] = value def __getitem__(self, key): + tk = super().__getattribute__('_translate_keys') + if isinstance(key, str) and key in tk: + return super().__getattribute__('_obj')[tk[key](self)] return super(ObjectProxy, self).__getattribute__('_obj')[key] def __setitem__(self, key, value): + tk = super().__getattribute__('_translate_keys') + if isinstance(key, str) and key in tk: + super().__getattribute__('_obj')[tk[key](self)] = value super(ObjectProxy, self).__getattribute__('_obj')[key] = value def __enter__(self): @@ -58,6 +70,9 @@ def __contains__(self, key): def get_ndb_object(self): return self._obj + def get(self, key, *argv): + return self._obj.get(key, *argv) + def keys(self): return self._obj.keys() @@ -76,12 +91,25 @@ def _mode(self): class Interface(ObjectProxy): - def add_ip(self, *argv, **kwarg): - self._obj.add_ip(*argv, **kwarg) + + _translate_keys = { + 'mode': lambda x: f'{x["kind"]}_mode' + } + + def add_ip(self, address=None, prefixlen=None, **kwarg): + if address is not None: + kwarg['address'] = address + if prefixlen is not None: + kwarg['prefixlen'] = prefixlen + self._obj.add_ip(spec=kwarg) return self - def del_ip(self, *argv, **kwarg): - self._obj.del_ip(*argv, **kwarg) + def del_ip(self, address=None, prefixlen=None, **kwarg): + if address is not None: + kwarg['address'] = address + if prefixlen is not None: + kwarg['prefixlen'] = prefixlen + self._obj.del_ip(spec=kwarg) return self def add_port(self, *argv, **kwarg): @@ -93,7 +121,14 @@ def del_port(self, *argv, **kwarg): return self def commit(self, *argv, **kwarg): - self._obj.commit(*argv, **kwarg) + try: + self._obj.commit(*argv, **kwarg) + except Exception as e: + if self._ready: + raise CommitException(e) + else: + raise CreateException(e) + self._ready = True return self def up(self): @@ -114,7 +149,9 @@ def if_master(self): @property def ipaddr(self): - return tuple(self._obj.ipaddr.dump().select('address', 'prefixlen')) + report = self._obj.ipaddr.dump() + report.select_fields('address', 'prefixlen') + return tuple(report) class Interfaces(ObjectProxy): @@ -123,8 +160,6 @@ class Interfaces(ObjectProxy): differently in IPDB and NDB. IPDB saves the failed object in the database, while the NDB database contains only the system reflection, and the failed object may stay only being referenced by a variable. - -`KeyError: 'object exists'` vs. `CreateException` ''' def __getitem__(self, key): @@ -136,9 +171,24 @@ def __iter__(self): def add(self, *argv, **kwarg): return self.create(*argv, **kwarg) + def get(self, spec, *argv): + try: + return self[spec] + except KeyError: + if len[argv] > 0: + return argv[0] + raise + def create(self, *argv, **kwarg): log.warning(self.text_create) - return Interface(self._obj.create(*argv, **kwarg)) + key = dict( + filter(lambda x: x[0] in ('ifname', 'index'), kwarg.items()) + ) + if key in self: + if kwarg.get('reuse'): + return self[key] + raise CreateException(NetlinkError(errno.EEXIST, 'object exists')) + return Interface(self._obj.create(*argv, **kwarg), ready=False) def keys(self): ret = [] @@ -171,6 +221,7 @@ class IPDB(object): ''' def __init__(self, *argv, **kwarg): + sources = kwarg.pop('sources', [{'target': 'localhost'}]) if argv or kwarg: log.warning( '%s does not support IPDB parameters, ignoring', @@ -183,9 +234,15 @@ def __init__(self, *argv, **kwarg): self.__class__.__name__, ) - self._ndb = NDB() + self._ndb = NDB(sources=sources) self.interfaces = Interfaces(self._ndb.interfaces) + def __enter__(self): + return self + + def __exit__(self, *_): + self.release() + @property def nl(self): log.warning(self.text_nl) diff --git a/tests/test_integration/conftest.py b/tests/test_integration/conftest.py index 0eaaa0432..5dfc4afe6 100644 --- a/tests/test_integration/conftest.py +++ b/tests/test_integration/conftest.py @@ -27,3 +27,8 @@ def link(request, tmpdir, nsname): ipr.link('del', index=link['index']) except: pass + + +@pytest.fixture +def ifname(link): + return link.get('ifname') diff --git a/tests/test_integration/test_kuryr.py b/tests/test_integration/test_kuryr.py index 12423a399..f3d1a5c5b 100644 --- a/tests/test_integration/test_kuryr.py +++ b/tests/test_integration/test_kuryr.py @@ -1,7 +1,88 @@ +import pytest +from net_tools import interface_exists + import pyroute2 +from pyroute2 import IPDB +from pyroute2.common import uifname +from pyroute2.netlink.rtnl.ifinfmsg import ifinfmsg + +# from pyroute2.common import uifname +TADDR = '00:11:22:33:44:55' +KIND = 'ipvlan' +IPVLAN_MODE_L2 = ifinfmsg.ifinfo.data_map['ipvlan'].modes['IPVLAN_MODE_L2'] + + +@pytest.fixture +def ipdb(link): + with IPDB(sources=[{'target': 'localhost', 'netns': link.netns}]) as ip: + yield ip + + +@pytest.mark.parametrize( + 'exc', + ( + pyroute2.NetlinkError, + pyroute2.CreateException, + pyroute2.CommitException, + ), +) +def test_exception_types(exc): + assert issubclass(exc, Exception) + + +def test_ipdb_create_exception(link, ipdb): + with pytest.raises(pyroute2.CreateException): + ipdb.create(ifname=link.get('ifname'), kind='dummy').commit() + + +def test_ipdb_create_reuse(link, ipdb): + ipdb.create(ifname=link.get('ifname'), kind='dummy', reuse=True).commit() + + +@pytest.mark.parametrize( + 'method,argv,check', + ( + ('set_mtu', [1000], lambda x: x['mtu'] == 1000), + ('set_address', [TADDR], lambda x: x['address'] == TADDR), + ('add_ip', ['10.1.2.3', 24], lambda x: '10.1.2.3/24' in x.ipaddr), + ('up', [], lambda x: x['flags'] & 1), + ), +) +def test_ipdb_iface_methods(link, ipdb, method, argv, check): + iface = ipdb.interfaces[link.get('ifname')] + with iface: + getattr(iface, method)(*argv) + assert check(iface) + + +def test_utils_remove(link, ifname, ipdb): + index = ipdb.interfaces.get(ifname, {}).get('index', None) + assert isinstance(index, int) + with ipdb.interfaces[ifname] as iface: + iface.remove() + assert ifname not in ipdb.interfaces + assert not interface_exists(ifname, link.netns, timeout=0.1) + + +def test_get_iface(link, ifname, ipdb): + with ipdb.interfaces[ifname] as link: + link.set_address(TADDR) + target = None + for name, data in ipdb.interfaces.items(): + if data['address'] == TADDR: + target = data['ifname'] + assert target == ifname -def test_exceptions(): - assert issubclass(pyroute2.NetlinkError, Exception) - assert issubclass(pyroute2.CreateException, Exception) - assert issubclass(pyroute2.CommitException, Exception) +def test_create_ipvlan(link, ifname, ipdb): + ipvlname = uifname() + with ipdb.create( + ifname=ipvlname, + kind=KIND, + link=ipdb.interfaces[ifname], + ipvlan_mode=IPVLAN_MODE_L2, + ) as iface: + assert iface['mode'] == IPVLAN_MODE_L2 + assert iface['ifname'] == ipvlname + assert iface['link'] == link.get('index') + assert iface['kind'] == KIND