diff -Nru nplan-0.32~16.04.6/dbus/io.netplan.Netplan.conf nplan-0.32~16.04.7/dbus/io.netplan.Netplan.conf
--- nplan-0.32~16.04.6/dbus/io.netplan.Netplan.conf 1970-01-01 00:00:00.000000000 +0000
+++ nplan-0.32~16.04.7/dbus/io.netplan.Netplan.conf 2019-09-03 15:58:30.000000000 +0000
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff -Nru nplan-0.32~16.04.6/dbus/io.netplan.Netplan.service.in nplan-0.32~16.04.7/dbus/io.netplan.Netplan.service.in
--- nplan-0.32~16.04.6/dbus/io.netplan.Netplan.service.in 1970-01-01 00:00:00.000000000 +0000
+++ nplan-0.32~16.04.7/dbus/io.netplan.Netplan.service.in 2019-09-04 19:15:27.000000000 +0000
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=io.netplan.Netplan
+Exec=@ROOTLIBEXECDIR@/netplan/netplan-dbus
+User=root
+AssumedAppArmorLabel=unconfined
diff -Nru nplan-0.32~16.04.6/debian/changelog nplan-0.32~16.04.7/debian/changelog
--- nplan-0.32~16.04.6/debian/changelog 2018-07-03 16:55:11.000000000 +0000
+++ nplan-0.32~16.04.7/debian/changelog 2019-09-05 16:58:36.000000000 +0000
@@ -1,3 +1,11 @@
+nplan (0.32~16.04.7) xenial; urgency=medium
+
+ * Backport DBus support to 16.04. (LP: #1842511)
+ * debian/control: add libsystemd-dev to Build-Depends, required for this new
+ DBus support.
+
+ -- Mathieu Trudel-Lapierre Thu, 05 Sep 2019 12:58:36 -0400
+
nplan (0.32~16.04.6) xenial; urgency=medium
[ Mathieu Trudel-Lapierre ]
diff -Nru nplan-0.32~16.04.6/debian/control nplan-0.32~16.04.7/debian/control
--- nplan-0.32~16.04.6/debian/control 2018-06-29 17:19:22.000000000 +0000
+++ nplan-0.32~16.04.7/debian/control 2019-09-03 20:20:52.000000000 +0000
@@ -11,6 +11,7 @@
python3 (>= 3.1),
python3-coverage,
python3-yaml,
+ libsystemd-dev,
systemd,
pyflakes3,
pycodestyle | pep8,
diff -Nru nplan-0.32~16.04.6/Makefile nplan-0.32~16.04.7/Makefile
--- nplan-0.32~16.04.6/Makefile 2018-06-29 17:19:22.000000000 +0000
+++ nplan-0.32~16.04.7/Makefile 2019-09-05 16:58:30.000000000 +0000
@@ -1,6 +1,11 @@
+PREFIX ?= /usr
+SBINDIR ?= $(PREFIX)/sbin
+LIBEXECDIR ?= $(PREFIX)/lib
+
BUILDFLAGS = \
-std=c99 \
-D_XOPEN_SOURCE=500 \
+ -DSBINDIR=\"$(SBINDIR)\" \
-Wall \
-Werror=incompatible-pointer-types \
-Werror=implicit-function-declaration \
@@ -9,14 +14,18 @@
SYSTEMD_GENERATOR_DIR=$(shell pkg-config --variable=systemdsystemgeneratordir systemd)
-PYCODE = src/netplan $(wildcard src/*.py) $(wildcard tests/*.py)
+PYCODE = src/netplan $(wildcard src/*.py) $(wildcard tests/*.py) $(wildcard tests/dbus/*.py)
-default: generate doc/netplan.5 doc/netplan.html
+default: generate doc/netplan.5 doc/netplan.html netplan-dbus dbus/io.netplan.Netplan.service
generate: src/generate.[hc] src/parse.[hc] src/util.[hc] src/networkd.[hc] src/nm.[hc]
$(CC) $(BUILDFLAGS) $(CFLAGS) -o $@ $(filter %.c, $^) `pkg-config --cflags --libs glib-2.0 yaml-0.1 uuid`
+netplan-dbus: src/dbus.c
+ $(CC) $(BUILDFLAGS) $(CFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0`
+
clean:
+ rm -f netplan-dbus dbus/*.service
rm -f generate doc/*.html doc/*.[1-9]
rm -rf test-coverage .coverage
@@ -48,6 +57,14 @@
install -m 644 doc/*.html $(DESTDIR)/usr/share/doc/netplan/
install -m 644 doc/*.5 $(DESTDIR)/usr/share/man/man5/
install -D -m 644 src/netplan-wpa@.service $(DESTDIR)/`pkg-config --variable=systemdsystemunitdir systemd`/netplan-wpa@.service
+ # dbus
+ mkdir -p $(DESTDIR)/usr/share/dbus-1/system.d $(DESTDIR)/usr/share/dbus-1/system-services
+ install -m 755 netplan-dbus $(DESTDIR)/lib/netplan/
+ install -m 644 dbus/io.netplan.Netplan.conf $(DESTDIR)/usr/share/dbus-1/system.d/
+ install -m 644 dbus/io.netplan.Netplan.service $(DESTDIR)/usr/share/dbus-1/system-services/
+
+%.service: %.service.in
+ sed -e "s#@ROOTLIBEXECDIR@#/lib#" $< > $@
%.html: %.md
pandoc -s --toc -o $@ $<
diff -Nru nplan-0.32~16.04.6/src/dbus.c nplan-0.32~16.04.7/src/dbus.c
--- nplan-0.32~16.04.6/src/dbus.c 1970-01-01 00:00:00.000000000 +0000
+++ nplan-0.32~16.04.7/src/dbus.c 2019-09-03 15:58:30.000000000 +0000
@@ -0,0 +1,93 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+static int method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ g_autoptr(GError) err = NULL;
+ g_autofree gchar *stdout = NULL;
+ g_autofree gchar *stderr = NULL;
+ gint exit_status = 0;
+
+ gchar *argv[] = {SBINDIR "/" "netplan", "apply", NULL};
+
+ // for tests only: allow changing what netplan to run
+ if (getuid() != 0 && getenv("DBUS_TEST_NETPLAN_CMD") != 0) {
+ argv[0] = getenv("DBUS_TEST_NETPLAN_CMD");
+ }
+
+ g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err);
+ if (err != NULL) {
+ return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message);
+ }
+ g_spawn_check_exit_status(exit_status, &err);
+ if (err != NULL) {
+ return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr);
+ }
+
+ return sd_bus_reply_method_return(m, "b", true);
+}
+
+static const sd_bus_vtable netplan_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_METHOD("Apply", "", "b", method_apply, 0),
+ SD_BUS_VTABLE_END
+};
+
+int main(int argc, char *argv[]) {
+ sd_bus_slot *slot = NULL;
+ sd_bus *bus = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0) {
+ fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
+ goto finish;
+ }
+
+ r = sd_bus_add_object_vtable(bus,
+ &slot,
+ "/io/netplan/Netplan", /* object path */
+ "io.netplan.Netplan", /* interface name */
+ netplan_vtable,
+ NULL);
+ if (r < 0) {
+ fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));
+ goto finish;
+ }
+
+ r = sd_bus_request_name(bus, "io.netplan.Netplan", 0);
+ if (r < 0) {
+ fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));
+ goto finish;
+ }
+
+ for (;;) {
+ r = sd_bus_process(bus, NULL);
+ if (r < 0) {
+ fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));
+ goto finish;
+ }
+ if (r > 0)
+ continue;
+
+ /* Wait for the next request to process */
+ r = sd_bus_wait(bus, (uint64_t) -1);
+ if (r < 0) {
+ fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));
+ goto finish;
+ }
+ }
+
+finish:
+ sd_bus_slot_unref(slot);
+ sd_bus_unref(bus);
+
+ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff -Nru nplan-0.32~16.04.6/src/netplan nplan-0.32~16.04.7/src/netplan
--- nplan-0.32~16.04.6/src/netplan 2018-06-29 17:19:22.000000000 +0000
+++ nplan-0.32~16.04.7/src/netplan 2019-09-04 18:57:27.000000000 +0000
@@ -22,6 +22,7 @@
import os
import sys
import re
+import shutil
import subprocess
from glob import glob
@@ -294,6 +295,30 @@
def command_apply(): # pragma: nocover (covered in autopkgtest)
+ # if we are inside a snap, then call dbus to run netplan apply instead
+ if "SNAP" in os.environ:
+ # TODO: maybe check if we are inside a classic snap and don't do
+ # this if we are in a classic snap?
+ busctl = shutil.which("busctl")
+ if busctl is None:
+ raise RuntimeError("missing busctl utility")
+ res = subprocess.call([busctl, "call", "--quiet", "--system",
+ "io.netplan.Netplan", # the service
+ "/io/netplan/Netplan", # the object
+ "io.netplan.Netplan", # the interface
+ "Apply", # the method
+ ])
+ if res != 0:
+ if res == 130:
+ raise PermissionError(
+ "failed to communicate with dbus service")
+ elif res == 1:
+ raise RuntimeError(
+ "failed to communicate with dbus service")
+ sys.exit(res)
+ else:
+ return
+
if subprocess.call([path_generate]) != 0:
sys.exit(1)
diff -Nru nplan-0.32~16.04.6/tests/dbus/test_dbus.py nplan-0.32~16.04.7/tests/dbus/test_dbus.py
--- nplan-0.32~16.04.6/tests/dbus/test_dbus.py 1970-01-01 00:00:00.000000000 +0000
+++ nplan-0.32~16.04.7/tests/dbus/test_dbus.py 2019-09-03 15:58:30.000000000 +0000
@@ -0,0 +1,173 @@
+#
+# Copyright (C) 2019 Canonical, Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+rootdir = os.path.dirname(os.path.dirname(
+ os.path.dirname(os.path.abspath(__file__))))
+exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')]
+if shutil.which('python3-coverage'):
+ exe_cli = ['python3-coverage', 'run', '--append', '--'] + exe_cli
+
+# Make sure we can import our development netplan.
+os.environ.update({'PYTHONPATH': '.'})
+
+
+class MockCmd:
+ """MockCmd will mock a given command name and capture all calls to it"""
+
+ def __init__(self, name):
+ self._tmp = tempfile.TemporaryDirectory()
+ self.name = name
+ self.path = os.path.join(self._tmp.name, name)
+ self.call_log = os.path.join(self._tmp.name, "call.log")
+ with open(self.path, "w") as fp:
+ fp.write("""#!/bin/bash
+printf "%%s" "$(basename "$0")" >> %(log)s
+printf '\\0' >> %(log)s
+
+for arg in "$@"; do
+ printf "%%s" "$arg" >> %(log)s
+ printf '\\0' >> %(log)s
+done
+
+printf '\\0' >> %(log)s
+""" % {'log': self.call_log})
+ os.chmod(self.path, 0o755)
+
+ def calls(self):
+ """
+ calls() returns the calls to the given mock command in the form of
+ [ ["cmd", "call1-arg1"], ["cmd", "call2-arg1"], ... ]
+ """
+ with open(self.call_log) as fp:
+ b = fp.read()
+ calls = []
+ for raw_call in b.rstrip("\0\0").split("\0\0"):
+ call = raw_call.rstrip("\0")
+ calls.append(call.split("\0"))
+ return calls
+
+
+class TestNetplanDBus(unittest.TestCase):
+
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.tmp)
+ self.mock_netplan_cmd = MockCmd("netplan")
+ self._create_mock_system_bus()
+ self._run_netplan_dbus_on_mock_bus()
+ self._mock_snap_env()
+ self.mock_busctl_cmd = MockCmd("busctl")
+
+ def _mock_snap_env(self):
+ os.environ["SNAP"] = "test-netplan-apply-snapd"
+
+ def _create_mock_system_bus(self):
+ env = {}
+ output = subprocess.check_output(["dbus-launch"], env={})
+ for s in output.decode("utf-8").split("\n"):
+ if s == "":
+ continue
+ k, v = s.split("=", 1)
+ env[k] = v
+ # override system bus with the fake one
+ os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = env["DBUS_SESSION_BUS_ADDRESS"]
+ self.addCleanup(os.kill, int(env["DBUS_SESSION_BUS_PID"]), 15)
+
+ def _run_netplan_dbus_on_mock_bus(self):
+ # run netplan-dbus in a fake system bus
+ os.environ["DBUS_TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path
+ p = subprocess.Popen(
+ os.path.join(os.path.dirname(__file__), "..", "..", "netplan-dbus"),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.addCleanup(self._cleanup_netplan_dbus, p)
+
+ def _cleanup_netplan_dbus(self, p):
+ p.terminate()
+ p.wait()
+ # netplan-dbus does not produce output
+ self.assertEqual(p.stdout.read(), b"")
+ self.assertEqual(p.stderr.read(), b"")
+
+ def test_netplan_apply_in_snap_uses_dbus(self):
+ p = subprocess.Popen(
+ exe_cli + ["apply"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.assertEqual(p.stdout.read(), b"")
+ self.assertEqual(p.stderr.read(), b"")
+ self.assertEquals(self.mock_netplan_cmd.calls(), [
+ ["netplan", "apply"],
+ ])
+
+ def test_netplan_apply_in_snap_calls_busctl(self):
+ newenv = os.environ.copy()
+ busctlDir = os.path.dirname(self.mock_busctl_cmd.path)
+ newenv["PATH"] = busctlDir+":"+os.environ["PATH"]
+ p = subprocess.Popen(
+ exe_cli + ["apply"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ env=newenv)
+ self.assertEqual(p.stdout.read(), b"")
+ self.assertEqual(p.stderr.read(), b"")
+ self.assertEquals(self.mock_busctl_cmd.calls(), [
+ ["busctl", "call", "--quiet", "--system",
+ "io.netplan.Netplan", # the service
+ "/io/netplan/Netplan", # the object
+ "io.netplan.Netplan", # the interface
+ "Apply", # the method
+ ],
+ ])
+
+ def test_netplan_dbus_happy(self):
+ BUSCTL_NETPLAN_APPLY = [
+ "busctl", "call", "--system",
+ "io.netplan.Netplan",
+ "/io/netplan/Netplan",
+ "io.netplan.Netplan",
+ "Apply",
+ ]
+ output = subprocess.check_output(BUSCTL_NETPLAN_APPLY)
+ self.assertEqual(output.decode("utf-8"), "b true\n")
+ # one call to netplan apply in total
+ self.assertEquals(self.mock_netplan_cmd.calls(), [
+ ["netplan", "apply"],
+ ])
+
+ # and again!
+ output = subprocess.check_output(BUSCTL_NETPLAN_APPLY)
+ self.assertEqual(output.decode("utf-8"), "b true\n")
+ # and another call to netplan apply
+ self.assertEquals(self.mock_netplan_cmd.calls(), [
+ ["netplan", "apply"],
+ ["netplan", "apply"],
+ ])
+
+ def test_netplan_dbus_no_such_command(self):
+ p = subprocess.Popen(
+ ["busctl", "call",
+ "io.netplan.Netplan",
+ "/io/netplan/Netplan",
+ "io.netplan.Netplan",
+ "NoSuchCommand"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ p.wait()
+ self.assertEqual(p.returncode, 1)
+ self.assertEqual(p.stdout.read().decode("utf-8"), "")
+ self.assertIn("Unknown method", p.stderr.read().decode("utf-8"))