Skip to content

Commit e5efa6e

Browse files
committed
feat(opennebula): support ETHx_ROUTES static routes in network config
Add `get_routes()` to `OpenNebulaNetwork` to parse the `ETHx_ROUTES` context variable (format: "NETWORK via GATEWAY, ...") and emit the resulting routes into the Netplan v2 `routes:` list in `gen_conf()`. Malformed entries are skipped with a warning. No `routes` key is emitted when the variable is absent or empty, preserving backward compatibility.
1 parent a870544 commit e5efa6e

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

cloudinit/sources/DataSourceOpenNebula.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,28 @@ def get_gateway6(self, dev):
217217
def get_mask(self, dev):
218218
return self.get_field(dev, "mask", "255.255.255.0")
219219

220+
def get_routes(self, dev):
221+
"""Parse ETHx_ROUTES into a list of Netplan route dicts.
222+
223+
Expected format: "NETWORK via GATEWAY[, NETWORK via GATEWAY, ...]"
224+
e.g. "10.0.0.0/8 via 192.168.1.1, 192.168.100.0/24 via 10.0.0.1"
225+
Returns an empty list when the variable is absent or empty.
226+
"""
227+
raw = self.get_field(dev, "routes", "")
228+
routes = []
229+
for entry in raw.split(","):
230+
entry = entry.strip()
231+
if not entry:
232+
continue
233+
parts = entry.split()
234+
if len(parts) == 3 and parts[1].lower() == "via":
235+
routes.append({"to": parts[0], "via": parts[2]})
236+
else:
237+
LOG.warning(
238+
"Unparseable ETHx_ROUTES entry for %s: %r", dev, entry
239+
)
240+
return routes
241+
220242
def get_field(self, dev, name, default=None):
221243
"""return the field name in context for device dev.
222244
@@ -285,6 +307,11 @@ def gen_conf(self):
285307
if mtu:
286308
devconf["mtu"] = mtu
287309

310+
# Set static routes
311+
extra_routes = self.get_routes(c_dev)
312+
if extra_routes:
313+
devconf["routes"] = extra_routes
314+
288315
ethernets[dev] = devconf
289316

290317
netconf["ethernets"] = ethernets
@@ -312,15 +339,13 @@ def switch_user_cmd(user):
312339

313340
def varprinter():
314341
"""print the shell environment variables within delimiters to be parsed"""
315-
return textwrap.dedent(
316-
"""
342+
return textwrap.dedent("""
317343
printf "%s\\0" _start_
318344
[ $0 != 'sh' ] && set -o posix
319345
set
320346
[ $0 != 'sh' ] && set +o posix
321347
printf "%s\\0" _start_
322-
"""
323-
)
348+
""")
324349

325350

326351
def parse_shell_config(content, asuser=None):

doc/rtd/reference/datasources/opennebula.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,15 @@ the OpenNebula documentation.
7272
ETH<x>_IP6_ULA
7373
ETH<x>_IP6_PREFIX_LENGTH
7474
ETH<x>_IP6_GATEWAY
75+
ETH<x>_ROUTES
7576

7677
Static `network configuration`_.
7778

79+
``ETH<x>_ROUTES`` is a comma-separated list of static routes in the form
80+
``NETWORK via GATEWAY``. For example::
81+
82+
ETH0_ROUTES="10.0.0.0/8 via 192.168.1.1, 172.16.0.0/12 via 192.168.1.254"
83+
7884
::
7985

8086
SET_HOSTNAME

tests/unittests/sources/test_opennebula.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,87 @@ def test_multiple_nics(self):
986986

987987
assert expected == net.gen_conf()
988988

989+
# ------------------------------------------------------------------ #
990+
# ETHx_ROUTES #
991+
# ------------------------------------------------------------------ #
992+
993+
def test_get_routes_absent(self):
994+
"""get_routes returns empty list when ETHx_ROUTES is not set."""
995+
net = ds.OpenNebulaNetwork({}, mock.Mock())
996+
assert net.get_routes("eth0") == []
997+
998+
def test_get_routes_empty_string(self):
999+
"""get_routes returns empty list when ETHx_ROUTES is empty string."""
1000+
net = ds.OpenNebulaNetwork({"ETH0_ROUTES": ""}, mock.Mock())
1001+
assert net.get_routes("eth0") == []
1002+
1003+
def test_get_routes_single(self):
1004+
"""get_routes parses a single 'NETWORK via GATEWAY' entry."""
1005+
net = ds.OpenNebulaNetwork(
1006+
{"ETH0_ROUTES": "10.0.0.0/8 via 192.168.1.1"}, mock.Mock()
1007+
)
1008+
assert net.get_routes("eth0") == [
1009+
{"to": "10.0.0.0/8", "via": "192.168.1.1"}
1010+
]
1011+
1012+
def test_get_routes_multiple(self):
1013+
"""get_routes parses multiple comma-separated entries."""
1014+
net = ds.OpenNebulaNetwork(
1015+
{
1016+
"ETH0_ROUTES": (
1017+
"10.0.0.0/8 via 192.168.1.1,"
1018+
" 172.16.0.0/12 via 192.168.1.254"
1019+
)
1020+
},
1021+
mock.Mock(),
1022+
)
1023+
assert net.get_routes("eth0") == [
1024+
{"to": "10.0.0.0/8", "via": "192.168.1.1"},
1025+
{"to": "172.16.0.0/12", "via": "192.168.1.254"},
1026+
]
1027+
1028+
def test_get_routes_malformed_entry_skipped(self):
1029+
"""get_routes silently skips entries it cannot parse."""
1030+
net = ds.OpenNebulaNetwork(
1031+
{"ETH0_ROUTES": "bad-entry, 10.0.0.0/8 via 192.168.1.1"},
1032+
mock.Mock(),
1033+
)
1034+
assert net.get_routes("eth0") == [
1035+
{"to": "10.0.0.0/8", "via": "192.168.1.1"}
1036+
]
1037+
1038+
@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
1039+
def test_gen_conf_routes(self, m_get_phys_by_mac):
1040+
"""Routes from ETHx_ROUTES appear in gen_conf() output."""
1041+
self.maxDiff = None
1042+
context = {
1043+
"ETH0_MAC": "02:00:0a:12:01:01",
1044+
"ETH0_IP": "10.0.0.5",
1045+
"ETH0_MASK": "255.255.255.0",
1046+
"ETH0_GATEWAY": "10.0.0.1",
1047+
"ETH0_ROUTES": (
1048+
"192.168.0.0/16 via 10.0.0.1, 172.16.0.0/12 via 10.0.0.1"
1049+
),
1050+
}
1051+
for nic in self.system_nics:
1052+
m_get_phys_by_mac.return_value = {MACADDR: nic}
1053+
net = ds.OpenNebulaNetwork(context, mock.Mock())
1054+
conf = net.gen_conf()
1055+
routes = conf["ethernets"][nic].get("routes", [])
1056+
assert {"to": "192.168.0.0/16", "via": "10.0.0.1"} in routes
1057+
assert {"to": "172.16.0.0/12", "via": "10.0.0.1"} in routes
1058+
1059+
@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
1060+
def test_gen_conf_no_routes_key_when_absent(self, m_get_phys_by_mac):
1061+
"""gen_conf() does not emit 'routes' key when ETHx_ROUTES is unset."""
1062+
context = {
1063+
"ETH0_MAC": "02:00:0a:12:01:01",
1064+
}
1065+
m_get_phys_by_mac.return_value = {MACADDR: "eth0"}
1066+
net = ds.OpenNebulaNetwork(context, mock.Mock())
1067+
conf = net.gen_conf()
1068+
assert "routes" not in conf["ethernets"]["eth0"]
1069+
9891070

9901071
class TestParseShellConfig:
9911072
@pytest.mark.allow_subp_for("bash", "sh")

0 commit comments

Comments
 (0)