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: 20 additions & 7 deletions cloudinit/sources/DataSourceOpenNebula.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,20 +279,27 @@ def get_field(
return default if val in (None, "") else val

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

ethernets: Dict[str, Dict[str, Any]] = {}
for mac, dev in self.ifaces.items():
mac = mac.lower()

# c_dev stores name in context 'ETHX' for this device.
# dev stores the current system name.
c_dev = self.context_devname.get(mac, dev)

devconf: Dict[str, Any] = {}
vlan_id: Optional[str] = self.get_field(c_dev, "vlan_id")
devconf: Dict[str, Any]

# Set MAC address
devconf["match"] = {"macaddress": mac}
if vlan_id:
# Parent: just bring it up, no IP configuration
netconf["ethernets"][dev] = {"match": {"macaddress": mac}}
# VLAN sub-interface carries the actual IP config
target_name = "%s.%s" % (dev, vlan_id)
devconf = {"id": int(vlan_id), "link": dev}
else:
Comment on lines +291 to +300
target_name = dev
devconf = {"match": {"macaddress": mac}}

# Set IPv4 address
devconf["addresses"] = []
Expand Down Expand Up @@ -333,9 +340,15 @@ def gen_conf(self) -> Dict[str, Any]:
if extra_routes:
devconf["routes"] = extra_routes

ethernets[dev] = devconf
if vlan_id:
netconf["vlans"][target_name] = devconf
else:
netconf["ethernets"][target_name] = devconf

# Remove empty top-level sections
if not netconf["vlans"]:
del netconf["vlans"]

netconf["ethernets"] = ethernets
return netconf


Expand Down
6 changes: 6 additions & 0 deletions doc/rtd/reference/datasources/opennebula.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ the OpenNebula documentation.
ETH<x>_DNS
ETH<x>_SEARCH_DOMAIN
ETH<x>_MTU
ETH<x>_VLAN_ID
ETH<x>_IP6
ETH<x>_IP6_ULA
ETH<x>_IP6_PREFIX_LENGTH
Expand All @@ -76,6 +77,11 @@ the OpenNebula documentation.

Static `network configuration`_.

When ``ETH<x>_VLAN_ID`` is set, ``cloud-init`` creates an 802.1Q VLAN
sub-interface named ``<dev>.<VLAN_ID>`` (e.g. ``eth0.100``) and assigns
all IP configuration to that sub-interface. The parent interface is
brought up with no address.

``ETH<x>_ROUTES`` is a comma-separated list of static routes in the form
``NETWORK via GATEWAY``. For example::

Expand Down
77 changes: 77 additions & 0 deletions tests/unittests/sources/test_opennebula.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,83 @@ def test_multiple_nics(self):

assert expected == net.gen_conf()

# --- ETHx_VLAN_ID tests ---

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_vlan_id(self, m_get_phys_by_mac):
"""VLAN_ID set → vlans: section; parent has no addresses."""
context = {
"ETH0_MAC": MACADDR,
"ETH0_IP": PUBLIC_IP,
"ETH0_MASK": "255.255.255.0",
"ETH0_VLAN_ID": "100",
}
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()
# Parent ethernet: just MAC match, no addresses
assert nic in conf["ethernets"]
assert "addresses" not in conf["ethernets"][nic]
# VLAN child carries the IP config
vlan_name = "%s.100" % nic
assert "vlans" in conf
assert vlan_name in conf["vlans"]
vlan = conf["vlans"][vlan_name]
assert vlan["id"] == 100
assert vlan["link"] == nic
assert PUBLIC_IP + "/24" in vlan["addresses"]

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_no_vlan_no_vlans_key(self, m_get_phys_by_mac):
"""Without VLAN_ID the output has no 'vlans' key."""
context = {"ETH0_MAC": MACADDR}
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 "vlans" not in conf

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_vlan_with_gateway(self, m_get_phys_by_mac):
"""Gateway on a VLAN interface ends up in the vlans entry."""
context = {
"ETH0_MAC": MACADDR,
"ETH0_IP": PUBLIC_IP,
"ETH0_GATEWAY": "10.0.0.1",
"ETH0_VLAN_ID": "200",
}
m_get_phys_by_mac.return_value = {MACADDR: "eth0"}
net = ds.OpenNebulaNetwork(context, mock.Mock())
conf = net.gen_conf()
vlan = conf["vlans"]["eth0.200"]
assert vlan.get("gateway4") == "10.0.0.1"
assert "gateway4" not in conf["ethernets"]["eth0"]

@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
def test_gen_conf_vlan_mixed_nics(self, m_get_phys_by_mac):
"""One NIC with VLAN, one without: only one vlans entry."""
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",
"ETH0_VLAN_ID": "10",
"ETH1_MAC": MAC_2,
"ETH1_IP": "10.0.1.1",
}
net = ds.OpenNebulaNetwork(
context,
mock.Mock(),
system_nics_by_mac={MAC_1: "eth0", MAC_2: "eth1"},
)
conf = net.gen_conf()
assert "vlans" in conf
assert "eth0.10" in conf["vlans"]
assert "eth1" in conf["ethernets"]
assert "addresses" in conf["ethernets"]["eth1"]
assert "addresses" not in conf["ethernets"]["eth0"]

# ------------------------------------------------------------------ #
# ETHx_ROUTES #
# ------------------------------------------------------------------ #
Expand Down
Loading