Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cloudinit/sources/DataSourceOpenNebula.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,28 @@ def get_field(
# allow empty string to return the default.
return default if val in (None, "") else val

def get_alias_addresses(self, c_dev: str) -> List[str]:
"""Return list of alias IP/prefix strings for context device c_dev.

Scans context for ETHx_ALIASn_IP / ETHx_ALIASn_MASK keys, where x
matches c_dev (e.g. 'ETH0'). Missing MASK defaults to /32.
Stops at the first gap in the alias index sequence.
"""
aliases: List[str] = []
prefix = c_dev.upper() + "_ALIAS"
idx = 0
while True:
ip_key = "%s%d_IP" % (prefix, idx)
ip = self.context.get(ip_key)
if not ip:
break
mask_key = "%s%d_MASK" % (prefix, idx)
mask = self.context.get(mask_key) or "255.255.255.255"
net_prefix = str(net.ipv4_mask_to_net_prefix(mask))
aliases.append("%s/%s" % (ip, net_prefix))
idx += 1
return aliases

def gen_conf(self) -> Dict[str, Any]:
netconf: Dict[str, Any] = {"version": 2, "ethernets": {}}

Expand All @@ -300,6 +322,11 @@ def gen_conf(self) -> Dict[str, Any]:
prefix = str(net.ipv4_mask_to_net_prefix(mask))
devconf["addresses"].append(self.get_ip(c_dev, mac) + "/" + prefix)

# Set alias (anycast) IPv4 addresses
alias_addresses: List[str] = self.get_alias_addresses(c_dev)
if alias_addresses:
devconf["addresses"].extend(alias_addresses)

# Set IPv6 Global and ULA address
addresses6 = self.get_ip6(c_dev)
if addresses6:
Expand Down
10 changes: 10 additions & 0 deletions doc/rtd/reference/datasources/opennebula.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ Static `network configuration`_.

ETH0_ROUTES="10.0.0.0/8 via 192.168.1.1, 172.16.0.0/12 via 192.168.1.254"

::

ETH<x>_ALIAS<n>_IP
ETH<x>_ALIAS<n>_MASK

Additional (anycast) IPv4 addresses for interface ``ETH<x>``. Aliases are
numbered from 0 (e.g. ``ETH0_ALIAS0_IP``, ``ETH0_ALIAS1_IP``, …). The
``MASK`` field defaults to ``255.255.255.255`` (``/32``) when absent. All
alias addresses are added to the interface alongside the primary address.

::

SET_HOSTNAME
Expand Down
104 changes: 104 additions & 0 deletions tests/unittests/sources/test_opennebula.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,110 @@ def test_gen_conf_no_routes_key_when_absent(self, m_get_phys_by_mac):
conf = net.gen_conf()
assert "routes" not in conf["ethernets"]["eth0"]

# ------------------------------------------------------------------ #
# ETHx_ALIASn #
# ------------------------------------------------------------------ #

def test_get_alias_addresses_single(self):
"""Single alias on ETH0 produces one extra address."""
context = {
"ETH0_ALIAS0_IP": "192.168.1.10",
"ETH0_ALIAS0_MASK": "255.255.255.0",
}
net = ds.OpenNebulaNetwork(context, mock.Mock())
aliases = net.get_alias_addresses("ETH0")
assert aliases == ["192.168.1.10/24"]

def test_get_alias_addresses_multiple(self):
"""Multiple aliases on same interface are all returned."""
context = {
"ETH0_ALIAS0_IP": "192.168.1.10",
"ETH0_ALIAS0_MASK": "255.255.255.0",
"ETH0_ALIAS1_IP": "192.168.1.11",
"ETH0_ALIAS1_MASK": "255.255.255.0",
"ETH0_ALIAS2_IP": "192.168.1.12",
"ETH0_ALIAS2_MASK": "255.255.255.0",
}
net = ds.OpenNebulaNetwork(context, mock.Mock())
aliases = net.get_alias_addresses("ETH0")
assert aliases == [
"192.168.1.10/24",
"192.168.1.11/24",
"192.168.1.12/24",
]

def test_get_alias_addresses_none(self):
"""No alias variables → empty list."""
net = ds.OpenNebulaNetwork({}, mock.Mock())
aliases = net.get_alias_addresses("ETH0")
assert aliases == []

def test_get_alias_addresses_default_mask(self):
"""Alias without MASK uses default /32."""
context = {
"ETH0_ALIAS0_IP": "10.0.0.5",
}
net = ds.OpenNebulaNetwork(context, mock.Mock())
aliases = net.get_alias_addresses("ETH0")
assert aliases == ["10.0.0.5/32"]

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_aliases_in_addresses(self, m_get_phys_by_mac):
"""gen_conf includes alias IPs in addresses list."""
context = {
"ETH0_MAC": MACADDR,
"ETH0_IP": PUBLIC_IP,
"ETH0_MASK": "255.255.255.0",
"ETH0_ALIAS0_IP": "192.168.1.10",
"ETH0_ALIAS0_MASK": "255.255.255.0",
"ETH0_ALIAS1_IP": "192.168.1.11",
"ETH0_ALIAS1_MASK": "255.255.255.0",
}
for nic in self.system_nics:
m_get_phys_by_mac.return_value = {MACADDR: nic}
net = ds.OpenNebulaNetwork(context, mock.Mock())
conf = net.gen_conf()
addresses = conf["ethernets"][nic]["addresses"]
assert PUBLIC_IP + "/24" in addresses
assert "192.168.1.10/24" in addresses
assert "192.168.1.11/24" in addresses

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_no_aliases_unchanged(self, m_get_phys_by_mac):
"""gen_conf without aliases produces same output as before."""
context = {
"ETH0_MAC": MACADDR,
"ETH0_IP": PUBLIC_IP,
"ETH0_MASK": "255.255.255.0",
}
for nic in self.system_nics:
m_get_phys_by_mac.return_value = {MACADDR: nic}
net = ds.OpenNebulaNetwork(context, mock.Mock())
conf = net.gen_conf()
assert conf["ethernets"][nic]["addresses"] == [PUBLIC_IP + "/24"]

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_aliases_on_second_nic(self, m_get_phys_by_mac):
"""Aliases on a second NIC do not bleed into the first."""
MAC_1 = "02:00:0a:12:01:01"
MAC_2 = "02:00:0a:12:01:02"
context = {
"ETH0_MAC": MAC_1,
"ETH0_IP": "10.0.0.1",
"ETH1_MAC": MAC_2,
"ETH1_IP": "10.0.1.1",
"ETH1_ALIAS0_IP": "10.0.1.100",
"ETH1_ALIAS0_MASK": "255.255.255.0",
}
net = ds.OpenNebulaNetwork(
context,
mock.Mock(),
system_nics_by_mac={MAC_1: "eth0", MAC_2: "eth1"},
)
conf = net.gen_conf()
assert conf["ethernets"]["eth0"]["addresses"] == ["10.0.0.1/24"]
assert "10.0.1.100/24" in conf["ethernets"]["eth1"]["addresses"]


class TestParseShellConfig:
@pytest.mark.allow_subp_for("bash", "sh")
Expand Down
Loading