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"))