From 338782b7e20329dedacdaaa782c93959d99d5b70 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Tue, 18 Jul 2023 13:44:36 +0100 Subject: [PATCH] netplan: export an IP addresses iterator API It enables us to retrieve the list of IPs, (V4 and V6) from a given netdef via Python bindings. The same iterator will consume the ip4_addresses, ip6_addresses and address_options (where IPs with options are stored). Each item returned by the _next() function is a NetplanAddressOptions*, even for addresses without options. Each object is released when the next one is requested. --- netplan/libnetplan.py | 65 +++++++++++++++++++++++++++++++++ src/types-internal.h | 3 ++ src/types.c | 2 +- src/util.c | 77 ++++++++++++++++++++++++++++++++++++++++ tests/test_libnetplan.py | 61 +++++++++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) diff --git a/netplan/libnetplan.py b/netplan/libnetplan.py index 75c2f28bf..13b5205e9 100644 --- a/netplan/libnetplan.py +++ b/netplan/libnetplan.py @@ -203,12 +203,27 @@ class _netplan_net_definition(ctypes.Structure): pass +class _NetplanAddress(ctypes.Structure): + _fields_ = [("address", c_char_p), ("lifetime", c_char_p), ("label", c_char_p)] + + +class NetplanAddress: + def __init__(self, address: str, lifetime: str, label: str): + self.address = address + self.lifetime = lifetime + self.label = label + + def __str__(self) -> str: + return self.address + + lib = ctypes.CDLL(ctypes.util.find_library('netplan')) _NetplanErrorPP = ctypes.POINTER(ctypes.POINTER(_NetplanError)) _NetplanParserP = ctypes.POINTER(_netplan_parser) _NetplanStateP = ctypes.POINTER(_netplan_state) _NetplanNetDefinitionP = ctypes.POINTER(_netplan_net_definition) +_NetplanAddressP = ctypes.POINTER(_NetplanAddress) lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p @@ -514,6 +529,10 @@ def __init__(self, np_state, ptr): # the GC invoking netplan_state_free self._parent = np_state + @property + def addresses(self): + return _NetdefAddressIterator(self._ptr) + @property def has_match(self): return bool(lib.netplan_netdef_has_match(self._ptr)) @@ -668,6 +687,52 @@ def __next__(self): return NetDefinition(self.np_state, next_value) +class _NetdefAddressIterator: + _abi_loaded = False + + @classmethod + def _load_abi(cls): + if cls._abi_loaded: + return + + if not hasattr(lib, '_netplan_new_netdef_address_iter'): # pragma: nocover (hard to unit-test against the WRONG lib) + raise NetplanException(''' + The current version of libnetplan does not allow iterating by IP addresses. + Please ensure that both the netplan CLI package and its library are up to date. + ''') + lib._netplan_new_netdef_address_iter.argtypes = [_NetplanNetDefinitionP] + lib._netplan_new_netdef_address_iter.restype = c_void_p + + lib._netplan_netdef_address_iter_next.argtypes = [c_void_p] + lib._netplan_netdef_address_iter_next.restype = _NetplanAddressP + + lib._netplan_netdef_address_free_iter.argtypes = [c_void_p] + lib._netplan_netdef_address_free_iter.restype = None + + cls._abi_loaded = True + + def __init__(self, netdef): + self._load_abi() + self.netdef = netdef + self.iterator = lib._netplan_new_netdef_address_iter(netdef) + + def __del__(self): + lib._netplan_netdef_address_free_iter(self.iterator) + + def __iter__(self): + return self + + def __next__(self): + next_value = lib._netplan_netdef_address_iter_next(self.iterator) + if not next_value: + raise StopIteration + content = next_value.contents + address = content.address.decode('utf-8') if content.address else None + lifetime = content.lifetime.decode('utf-8') if content.lifetime else None + label = content.label.decode('utf-8') if content.label else None + return NetplanAddress(address, lifetime, label) + + lib.netplan_util_create_yaml_patch.argtypes = [c_char_p, c_char_p, c_int, _NetplanErrorPP] lib.netplan_util_create_yaml_patch.restype = c_int diff --git a/src/types-internal.h b/src/types-internal.h index 6639246e7..7d21d7242 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -290,3 +290,6 @@ netplan_state_has_nondefault_globals(const NetplanState* np_state); void clear_netdef_from_list(void* def); + +void +free_address_options(void* ptr); diff --git a/src/types.c b/src/types.c index d9f740ada..9c896467e 100644 --- a/src/types.c +++ b/src/types.c @@ -59,7 +59,7 @@ free_hashtable_with_destructor(GHashTable** hash, void (destructor)(void *)) { } } -static void +NETPLAN_INTERNAL void free_address_options(void* ptr) { NetplanAddressOptions* opts = ptr; diff --git a/src/util.c b/src/util.c index 8512be68e..30b41b187 100644 --- a/src/util.c +++ b/src/util.c @@ -709,6 +709,83 @@ get_unspecified_address(int ip_family) return (ip_family == AF_INET) ? "0.0.0.0" : "::"; } +struct netdef_address_iter { + guint ip4_index; + guint ip6_index; + guint address_options_index; + NetplanNetDefinition* netdef; + NetplanAddressOptions* last_address; +}; + +NETPLAN_INTERNAL struct netdef_address_iter* +_netplan_new_netdef_address_iter(NetplanNetDefinition* netdef) +{ + struct netdef_address_iter* it = g_malloc0(sizeof(struct netdef_address_iter)); + it->ip4_index = 0; + it->ip6_index = 0; + it->address_options_index = 0; + it->netdef = netdef; + it->last_address = NULL; + + return it; +} + +/* + * The netdef address iterator produces NetplanAddressOptions + * for all the addresses stored in ip4_address, ip6_address and + * address_options (in this order). + * + * The current value produced by the iterator is saved in it->last_address + * and the previous one is released. The idea is to not leave to the caller + * the responsibility of releasing each value. The very last value + * will be released either when the iterator is destroyed or when there is + * nothing else to be produced and the iterator was called one last time. + */ +NETPLAN_INTERNAL NetplanAddressOptions* +_netplan_netdef_address_iter_next(struct netdef_address_iter* it) +{ + NetplanAddressOptions* options = NULL; + + if (it->last_address) { + free_address_options(it->last_address); + it->last_address = NULL; + } + + if (it->netdef->ip4_addresses && it->ip4_index < it->netdef->ip4_addresses->len) { + options = g_malloc0(sizeof(NetplanAddressOptions)); + options->address = g_strdup(g_array_index(it->netdef->ip4_addresses, char*, it->ip4_index++)); + it->last_address = options; + return options; + } + + if (it->netdef->ip6_addresses && it->ip6_index < it->netdef->ip6_addresses->len) { + options = g_malloc0(sizeof(NetplanAddressOptions)); + options->address = g_strdup(g_array_index(it->netdef->ip6_addresses, char*, it->ip6_index++)); + it->last_address = options; + return options; + } + + if (it->netdef->address_options && it->address_options_index < it->netdef->address_options->len) { + options = g_malloc0(sizeof(NetplanAddressOptions)); + NetplanAddressOptions* netdef_options = g_array_index(it->netdef->address_options, NetplanAddressOptions*, it->address_options_index++); + options->address = g_strdup(netdef_options->address); + options->lifetime = g_strdup(netdef_options->lifetime); + options->label = g_strdup(netdef_options->label); + it->last_address = options; + return options; + } + + return options; +} + +NETPLAN_INTERNAL void +_netplan_netdef_address_free_iter(struct netdef_address_iter* it) +{ + if (it->last_address) + free_address_options(it->last_address); + g_free(it); +} + struct netdef_pertype_iter { NetplanDefType type; GHashTableIter iter; diff --git a/tests/test_libnetplan.py b/tests/test_libnetplan.py index 47a010b76..0cc42dd12 100644 --- a/tests/test_libnetplan.py +++ b/tests/test_libnetplan.py @@ -195,6 +195,67 @@ def test_iter_ethernets(self): self.assertSetEqual(set(["eth0", "eth1"]), set(d.id for d in libnetplan._NetdefIterator(state, "ethernets"))) +class TestNetdefAddressesIterator(TestBase): + def test_with_empty_ip_addresses(self): + state = state_from_yaml(self.confdir, '''network: + ethernets: + eth0: + dhcp4: true''') + + netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + self.assertSetEqual(set(), set(ip for ip in netdef.addresses)) + + def test_iter_ethernets(self): + state = state_from_yaml(self.confdir, '''network: + ethernets: + eth0: + addresses: + - 192.168.0.1/24 + - 172.16.0.1/24 + - 1234:4321:abcd::cdef/96 + - abcd::1234/64''') + + expected = set(["1234:4321:abcd::cdef/96", "abcd::1234/64", "192.168.0.1/24", "172.16.0.1/24"]) + netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + self.assertSetEqual(expected, set(ip.address for ip in netdef.addresses)) + self.assertSetEqual(expected, set(str(ip) for ip in netdef.addresses)) + + def test_iter_ethernets_with_options(self): + state = state_from_yaml(self.confdir, '''network: + ethernets: + eth0: + addresses: + - 192.168.0.1/24 + - 172.16.0.1/24: + lifetime: 0 + label: label1 + - 1234:4321:abcd::cdef/96: + lifetime: forever + label: label2''') + + expected_ips = set(["1234:4321:abcd::cdef/96", "192.168.0.1/24", "172.16.0.1/24"]) + expected_lifetime_options = set([None, "0", "forever"]) + expected_label_options = set([None, "label1", "label2"]) + netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + self.assertSetEqual(expected_ips, set(ip.address for ip in netdef.addresses)) + self.assertSetEqual(expected_lifetime_options, set(ip.lifetime for ip in netdef.addresses)) + self.assertSetEqual(expected_label_options, set(ip.label for ip in netdef.addresses)) + + def test_drop_iterator_before_finishing(self): + state = state_from_yaml(self.confdir, '''network: + ethernets: + eth0: + addresses: + - 192.168.0.1/24 + - 1234:4321:abcd::cdef/96''') + + netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + iter = netdef.addresses.__iter__() + address = next(iter) + self.assertEqual(address.address, "192.168.0.1/24") + del iter + + class TestParser(TestBase): def test_load_yaml_from_fd_empty(self): parser = libnetplan.Parser()