Skip to content

Commit

Permalink
Add cc_netplan_nm_patch module
Browse files Browse the repository at this point in the history
Signed-off-by: paulober <44974737+paulober@users.noreply.github.com>
  • Loading branch information
paulober committed Nov 14, 2024
1 parent 9bba409 commit 7cdcb3c
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 1 deletion.
129 changes: 129 additions & 0 deletions cloudinit/config/cc_netplan_nm_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright (C) 2024, Raspberry Pi Ltd.
#
# Author: Paul Oberosler <paul.oberosler@raspberrypi.com>
#
# This file is part of cloud-init. See LICENSE file for license information.

from cloudinit import subp
from cloudinit.cloud import Cloud
from cloudinit.config import Config
from cloudinit.config.schema import MetaSchema
from cloudinit.distros import ALL_DISTROS
from cloudinit.net.netplan import CLOUDINIT_NETPLAN_FILE
from cloudinit.settings import PER_INSTANCE
import logging
import os
import re

LOG = logging.getLogger(__name__)

meta: MetaSchema = {
"id": "cc_netplan_nm_patch",
"distros": [ALL_DISTROS],
"frequency": PER_INSTANCE,
"activate_by_schema_keys": [],
}


def exec_cmd(command: str) -> str | None:
try:
result = subp.subp(command)
if result.stdout is not None:
return result.stdout
except subp.ProcessExecutionError as e:
LOG.error("Failed to execute command: %s", e)
return None
LOG.debug("Command has no stdout: %s", command)
return None


def get_netplan_generated_configs() -> list[str]:
"""Get the UUIDs of all connections starting with 'netplan-'."""
output = exec_cmd(["nmcli", "connection", "show"])
if output is None:
return []

netplan_conns = []
for line in output.splitlines():
if line.startswith("netplan-"):
parts = line.split()
if len(parts) > 1:
# name = parts[0]
uuid = parts[1]
netplan_conns.append(uuid)
return netplan_conns


def get_connection_object_path(uuid: str) -> str | None:
"""Get the D-Bus object path for a connection by UUID."""
output = exec_cmd(
[
"busctl",
"call",
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager/Settings",
"org.freedesktop.NetworkManager.Settings",
"GetConnectionByUuid",
"s",
uuid,
]
)

path_match = (
re.search(
r'o\s+"(/org/freedesktop/NetworkManager/Settings/\d+)"', output
)
if output
else None
)
if path_match:
return path_match.group(1)
else:
LOG.error("Failed to find object path for connection: %s", uuid)
return None


def save_connection(obj_path: str) -> None:
"""Call the Save method on the D-Bus obj path for a connection."""
result = exec_cmd(
[
"busctl",
"call",
"org.freedesktop.NetworkManager",
obj_path,
"org.freedesktop.NetworkManager.Settings.Connection",
"Save",
]
)

if result is None:
LOG.error("Failed to save connection: %s", obj_path)
else:
LOG.debug("Saved connection: %s", obj_path)


def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None:
LOG.debug("Applying netplan patch")

# remove cloud-init file after NetworkManager has generated
# replacement netplan configurations to avoid conflicts in the
# future

try:
np_conns = get_netplan_generated_configs()
if not np_conns:
LOG.debug("No netplan connections found")
return

for conn_uuid in np_conns:
obj_path = get_connection_object_path(conn_uuid)
if obj_path is None:
continue
save_connection(obj_path)

os.remove(CLOUDINIT_NETPLAN_FILE)
LOG.debug("Netplan cfg has been patched: %s", CLOUDINIT_NETPLAN_FILE)
except subp.ProcessExecutionError as e:
LOG.error("Failed to patch netplan cfg: %s", e)
except Exception as e:
LOG.error("Failed to patch netplan cfg: %s", e)
9 changes: 9 additions & 0 deletions cloudinit/config/schemas/schema-cloud-config-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"lxd",
"mcollective",
"mounts",
"netplan-nm-patch",
"netplan_nm_patch",
"ntp",
"package-update-upgrade-install",
"package_update_upgrade_install",
Expand Down Expand Up @@ -2098,6 +2100,10 @@
}
}
},
"cc_netplan_nm_patch": {
"type": "object",
"properties": {}
},
"cc_ntp": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -3939,6 +3945,9 @@
{
"$ref": "#/$defs/cc_mounts"
},
{
"$ref": "#/$defs/cc_netplan_nm_patch"
},
{
"$ref": "#/$defs/cc_ntp"
},
Expand Down
3 changes: 3 additions & 0 deletions config/cloud.cfg.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ cloud_final_modules:
- mcollective
- salt_minion
- reset_rmc
{% endif %}
{% if variant == "raspberry-pi-os" %}
- netplan_nm_patch
{% endif %}
- scripts_vendor
- scripts_per_once
Expand Down
13 changes: 13 additions & 0 deletions doc/module-docs/cc_netplan_nm_patch/data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
cc_netplan_nm_patch:
description: |
This module resolves the issue where if you edit a netplan generated network configuration
in NetworkManager, the changes are saved to netplan as new configuration files.
This will cause netplan overwrite the NetworkManager generated netplan configuration on the next boot.
This module will give NetworkManager a hint to generated netplan configuration files
and then removes the cloud-init generated netplan configuration file.
This module expects that NetworkManager has the netplan integration patch applied.
examples:
name: Netplan NetworkManager Patch
title: Patch NetworkManager netplan interop issue
2 changes: 2 additions & 0 deletions doc/rtd/reference/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Modules
:template: modules.tmpl
.. datatemplate:yaml:: ../../module-docs/cc_mounts/data.yaml
:template: modules.tmpl
.. datatemplate:yaml:: ../../module-docs/cc_netplan_nm_patch/data.yaml
:template: modules.tmpl
.. datatemplate:yaml:: ../../module-docs/cc_ntp/data.yaml
:template: modules.tmpl
.. datatemplate:yaml:: ../../module-docs/cc_package_update_upgrade_install/data.yaml
Expand Down
9 changes: 9 additions & 0 deletions tests/unittests/config/test_cc_netplan_nm_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file is part of cloud-init. See LICENSE file for license information.

from tests.unittests.helpers import skipUnlessJsonSchema


@skipUnlessJsonSchema()
class TestCCNetplanNmPatch:
def test_schema_validation(self):
pass
1 change: 1 addition & 0 deletions tests/unittests/config/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def test_get_schema_coalesces_known_schema(self):
{"$ref": "#/$defs/cc_lxd"},
{"$ref": "#/$defs/cc_mcollective"},
{"$ref": "#/$defs/cc_mounts"},
{"$ref": "#/$defs/cc_netplan_nm_patch"},
{"$ref": "#/$defs/cc_ntp"},
{"$ref": "#/$defs/cc_package_update_upgrade_install"},
{"$ref": "#/$defs/cc_phone_home"},
Expand Down
2 changes: 1 addition & 1 deletion tests/unittests/test_render_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def test_variant_sets_default_user_in_cloud_cfg(self, variant, tmpdir):
("ubuntu", ["netplan", "eni", "sysconfig"]),
(
"raspberry-pi-os",
["netplan", "network-manager", "networkd", "eni"]
["netplan", "network-manager"]
)
),
)
Expand Down

0 comments on commit 7cdcb3c

Please sign in to comment.