Skip to content

Commit

Permalink
netplan: export an IP addresses iterator API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
daniloegea committed Jul 24, 2023
1 parent 64145e0 commit 338782b
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 1 deletion.
65 changes: 65 additions & 0 deletions netplan/libnetplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/types-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
2 changes: 1 addition & 1 deletion src/types.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 77 additions & 0 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
61 changes: 61 additions & 0 deletions tests/test_libnetplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 338782b

Please sign in to comment.