def test_generate(self): self.mock_netplan_cmd = MockCmd("netplan") os.environ["TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path self.assertTrue(lib.netplan_generate(self.workdir.name.encode())) self.assertEquals(self.mock_netplan_cmd.calls(), [ ["netplan", "generate", "--root-dir", self.workdir.name], ])
def setUp(self): self.tmp = tempfile.mkdtemp() os.makedirs(os.path.join(self.tmp, "etc", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "lib", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "run", "netplan"), 0o700) # Create main test YAML in /etc/netplan/ test_file = os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml') with open(test_file, 'w') as f: f.write("""network: version: 2 ethernets: eth0: dhcp4: true""") 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")
class TestNetworkManagerBackend(TestBase): '''Test libnetplan functionality as used by NetworkManager backend''' def setUp(self): super().setUp() os.makedirs(self.confdir) def tearDown(self): shutil.rmtree(self.workdir.name) super().tearDown() def test_get_id_from_filename(self): out = lib.netplan_get_id_from_nm_filename( '/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) self.assertEqual(out, b'some-id') def test_get_id_from_filename_rootdir(self): out = lib.netplan_get_id_from_nm_filename( '/some/rootdir/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) self.assertEqual(out, b'some-id') def test_get_id_from_filename_wifi(self): out = lib.netplan_get_id_from_nm_filename( '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection'.encode(), 'SOME-SSID'.encode()) self.assertEqual(out, b'some-id') def test_get_id_from_filename_wifi_invalid_suffix(self): out = lib.netplan_get_id_from_nm_filename( '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID'.encode(), 'SOME-SSID'.encode()) self.assertEqual(out, None) def test_get_id_from_filename_invalid_prefix(self): out = lib.netplan_get_id_from_nm_filename('INVALID/netplan-some-id.nmconnection'.encode(), None) self.assertEqual(out, None) def test_generate(self): self.mock_netplan_cmd = MockCmd("netplan") os.environ["TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path self.assertTrue(lib.netplan_generate(self.workdir.name.encode())) self.assertEquals(self.mock_netplan_cmd.calls(), [ ["netplan", "generate", "--root-dir", self.workdir.name], ]) def test_delete_connection(self): os.environ["TEST_NETPLAN_CMD"] = exe_cli FILENAME = os.path.join(self.confdir, 'some-filename.yaml') with open(FILENAME, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true''') self.assertTrue(os.path.isfile(FILENAME)) # Parse all YAML and delete 'some-netplan-id' connection file self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) self.assertFalse(os.path.isfile(FILENAME)) def test_delete_connection_id_not_found(self): FILENAME = os.path.join(self.confdir, 'some-filename.yaml') with open(FILENAME, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true''') self.assertTrue(os.path.isfile(FILENAME)) self.assertFalse(lib.netplan_delete_connection('unknown-id'.encode(), self.workdir.name.encode())) self.assertTrue(os.path.isfile(FILENAME)) def test_delete_connection_two_in_file(self): os.environ["TEST_NETPLAN_CMD"] = exe_cli FILENAME = os.path.join(self.confdir, 'some-filename.yaml') with open(FILENAME, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true other-id: dhcp6: true''') self.assertTrue(os.path.isfile(FILENAME)) self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) self.assertTrue(os.path.isfile(FILENAME)) # Verify the file still exists and still contains the other connection with open(FILENAME, 'r') as f: self.assertEquals(f.read(), 'network:\n ethernets:\n other-id:\n dhcp6: true\n') def test_serialize_gsm(self): self.maxDiff = None UUID = 'a08c5805-7cf5-43f7-afb9-12cb30f6eca3' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] id=T-Mobile Funkadelic 2 uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 type=gsm [gsm] apn=internet2.voicestream.com device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 home-only=true network-id=254098 password=parliament2 pin=123456 sim-id=89148000000060671234 sim-operator-id=310260 username=george.clinton.again [ipv4] dns-search= method=auto [ipv6] addr-gen-mode=stable-privacy dns-search= method=auto ''') lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3'.encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: {{}} apn: "internet2.voicestream.com" device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" network-id: "254098" pin: "123456" sim-id: "89148000000060671234" sim-operator-id: "310260" networkmanager: uuid: {} name: "T-Mobile Funkadelic 2" passthrough: gsm.home-only: "true" gsm.password: "******" gsm.username: "******" ipv4.dns-search: "" ipv4.method: "auto" ipv6.addr-gen-mode: "stable-privacy" ipv6.dns-search: "" ipv6.method: "auto" '''.format(UUID, UUID)) def test_serialize_gsm_via_bluetooth(self): self.maxDiff = None UUID = 'a08c5805-7cf5-43f7-afb9-12cb30f6eca3' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] id=T-Mobile Funkadelic 2 uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 type=bluetooth [gsm] apn=internet2.voicestream.com device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 home-only=true network-id=254098 password=parliament2 pin=123456 sim-id=89148000000060671234 sim-operator-id=310260 username=george.clinton.again [ipv4] dns-search= method=auto [ipv6] addr-gen-mode=stable-privacy dns-search= method=auto [proxy] ''') lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3'.encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 nm-devices: NM-{}: renderer: NetworkManager networkmanager: uuid: {} name: "T-Mobile Funkadelic 2" passthrough: connection.type: "bluetooth" gsm.apn: "internet2.voicestream.com" gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" gsm.home-only: "true" gsm.network-id: "254098" gsm.password: "******" gsm.pin: "123456" gsm.sim-id: "89148000000060671234" gsm.sim-operator-id: "310260" gsm.username: "******" ipv4.dns-search: "" ipv4.method: "auto" ipv6.addr-gen-mode: "stable-privacy" ipv6.dns-search: "" ipv6.method: "auto" proxy._: "" '''.format(UUID, UUID)) def _template_serialize_keyfile(self, nd_type, nm_type, supported=True): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('[connection]\ntype={}\nuuid={}'.format(nm_type, UUID)) self.assertEqual(lib.netplan_clear_netdefs(), 0) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) t = '\n passthrough:\n connection.type: "{}"'.format(nm_type) if not supported else '' match = '\n match: {}' if nd_type in ['ethernets', 'modems', 'wifis'] else '' with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 {}: NM-{}: renderer: NetworkManager{} networkmanager: uuid: {}{} '''.format(nd_type, UUID, match, UUID, t)) def test_serialize_keyfile_ethernet(self): self._template_serialize_keyfile('ethernets', 'ethernet') def test_serialize_keyfile_type_modem_gsm(self): self._template_serialize_keyfile('modems', 'gsm') def test_serialize_keyfile_type_modem_cdma(self): self._template_serialize_keyfile('modems', 'cdma') def test_serialize_keyfile_type_bridge(self): self._template_serialize_keyfile('bridges', 'bridge') def test_serialize_keyfile_type_bond(self): self._template_serialize_keyfile('bonds', 'bond') def test_serialize_keyfile_type_vlan(self): self._template_serialize_keyfile('vlans', 'vlan') def test_serialize_keyfile_type_tunnel(self): self._template_serialize_keyfile('tunnels', 'ip-tunnel', False) def test_serialize_keyfile_type_wireguard(self): self._template_serialize_keyfile('tunnels', 'wireguard', False) def test_serialize_keyfile_type_other(self): self._template_serialize_keyfile('nm-devices', 'dummy', False) def test_serialize_keyfile_missing_uuid(self): FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('[connection]\ntype=ethernets') self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) def test_serialize_keyfile_missing_type(self): UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('[connection]\nuuid={}'.format(UUID)) self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) def test_serialize_keyfile_missing_file(self): FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) def test_serialize_keyfile_type_wifi(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure hidden=true [ipv4] method=auto dns-search='''.format(UUID)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" access-points: "SOME-SSID": hidden: true mode: infrastructure networkmanager: uuid: {} name: "myid with spaces" passthrough: connection.permissions: "" ipv4.method: "auto" ipv4.dns-search: "" networkmanager: uuid: {} name: "myid with spaces" '''.format(UUID, UUID, UUID)) def _template_serialize_keyfile_type_wifi(self, nd_mode, nm_mode): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=wifi uuid={} id=myid with spaces [ipv4] method=auto [wifi] ssid=SOME-SSID mode={}'''.format(UUID, nm_mode)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) wifi_mode = '' if nm_mode != nd_mode: wifi_mode = '\n wifi.mode: "{}"'.format(nm_mode) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: {{}} access-points: "SOME-SSID": mode: {} networkmanager: uuid: {} name: "myid with spaces" passthrough: ipv4.method: "auto"{} networkmanager: uuid: {} name: "myid with spaces" '''.format(UUID, nd_mode, UUID, wifi_mode, UUID)) def test_serialize_keyfile_type_wifi_ap(self): self._template_serialize_keyfile_type_wifi('ap', 'ap') def test_serialize_keyfile_type_wifi_adhoc(self): self._template_serialize_keyfile_type_wifi('adhoc', 'adhoc') def test_serialize_keyfile_type_wifi_unknown(self): self._template_serialize_keyfile_type_wifi('infrastructure', 'mesh') def test_serialize_keyfile_type_wifi_missing_ssid(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection]\ntype=wifi\nuuid={}\nid=myid with spaces'''.format(UUID)) self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) self.assertFalse(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) def test_serialize_keyfile_wake_on_lan(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=ethernet uuid={} id=myid with spaces [ethernet] wake-on-lan=2 [ipv4] method=auto'''.format(UUID)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: {{}} wakeonlan: true networkmanager: uuid: {} name: "myid with spaces" passthrough: ethernet.wake-on-lan: "2" ipv4.method: "auto" '''.format(UUID, UUID)) def test_serialize_keyfile_wake_on_lan_nm_default(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=ethernet uuid={} id=myid with spaces [ethernet] [ipv4] method=auto'''.format(UUID)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: {{}} wakeonlan: true networkmanager: uuid: {} name: "myid with spaces" passthrough: ethernet._: "" ipv4.method: "auto" '''.format(UUID, UUID)) def test_serialize_keyfile_modem_gsm(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=gsm uuid={} id=myid with spaces [ipv4] method=auto [gsm] auto-config=true'''.format(UUID)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: {{}} auto-config: true networkmanager: uuid: {} name: "myid with spaces" passthrough: ipv4.method: "auto" '''.format(UUID, UUID)) def test_serialize_keyfile_existing_id(self): self.maxDiff = None UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' FILE = os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-mybr.nmconnection') os.makedirs(os.path.dirname(FILE)) with open(FILE, 'w') as file: file.write('''[connection] type=bridge uuid={} id=renamed netplan bridge [ipv4] method=auto'''.format(UUID)) lib.netplan_parse_keyfile(FILE.encode(), None) lib._write_netplan_conf('mybr'.encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 bridges: mybr: renderer: NetworkManager networkmanager: uuid: {} name: "renamed netplan bridge" passthrough: ipv4.method: "auto" '''.format(UUID)) def test_keyfile_yaml_wifi_hotspot(self): self.maxDiff = None UUID = 'ff9d6ebc-226d-4f82-a485-b7ff83b9607f' FILE_KF = os.path.join(self.workdir.name, 'tmp/Hotspot.nmconnection') CONTENT_KF = '''[connection] id=Hotspot-1 type=wifi uuid={} interface-name=wlan0 #Netplan: passthrough setting autoconnect=false #Netplan: passthrough setting permissions= [ipv4] method=shared #Netplan: passthrough setting dns-search= [ipv6] method=ignore #Netplan: passthrough setting addr-gen-mode=stable-privacy #Netplan: passthrough setting dns-search= [wifi] ssid=my-hotspot mode=ap #Netplan: passthrough setting mac-address-blacklist= [wifi-security] #Netplan: passthrough setting group=ccmp; #Netplan: passthrough setting key-mgmt=wpa-psk #Netplan: passthrough setting pairwise=ccmp; #Netplan: passthrough setting proto=rsn; #Netplan: passthrough setting psk=test1234 [proxy] '''.format(UUID) os.makedirs(os.path.dirname(FILE_KF)) with open(FILE_KF, 'w') as file: file.write(CONTENT_KF) # Convert Keyfile to YAML and compare lib.netplan_parse_keyfile(FILE_KF.encode(), None) lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) lib.netplan_clear_netdefs() FILE_YAML = os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)) CONTENT_YAML = '''network: version: 2 wifis: NM-ff9d6ebc-226d-4f82-a485-b7ff83b9607f: renderer: NetworkManager match: name: "wlan0" access-points: "my-hotspot": mode: ap networkmanager: uuid: ff9d6ebc-226d-4f82-a485-b7ff83b9607f name: "Hotspot-1" passthrough: connection.autoconnect: "false" connection.permissions: "" ipv4.method: "shared" ipv4.dns-search: "" ipv6.method: "ignore" ipv6.addr-gen-mode: "stable-privacy" ipv6.dns-search: "" wifi.mac-address-blacklist: "" wifi-security.group: "ccmp;" wifi-security.key-mgmt: "wpa-psk" wifi-security.pairwise: "ccmp;" wifi-security.proto: "rsn;" wifi-security.psk: "test1234" proxy._: "" networkmanager: uuid: {} name: "Hotspot-1" '''.format(UUID) self.assertTrue(os.path.isfile(FILE_YAML)) with open(FILE_YAML, 'r') as f: self.assertEqual(f.read(), CONTENT_YAML) # Convert YAML back to Keyfile and compare to original KF os.remove(FILE_YAML) self.generate(CONTENT_YAML) self.assert_nm({'NM-ff9d6ebc-226d-4f82-a485-b7ff83b9607f-my-hotspot': CONTENT_KF})
class TestNetplanDBus(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp() os.makedirs(os.path.join(self.tmp, "etc", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "lib", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "run", "netplan"), 0o700) # Create main test YAML in /etc/netplan/ test_file = os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml') with open(test_file, 'w') as f: f.write("""network: version: 2 ethernets: eth0: dhcp4: true""") 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 os.environ["DBUS_TEST_NETPLAN_ROOT"] = self.tmp p = subprocess.Popen(NETPLAN_DBUS_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(1) # Give some time for our dbus daemon to be ready 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 _check_dbus_error(self, cmd, returncode=1): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() self.assertEqual(p.returncode, returncode) self.assertEqual(p.stdout.read().decode("utf-8"), "") return p.stderr.read().decode("utf-8") def _new_config_object(self): BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Config", ] # Create new config object / config state out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertIn(b'o "/io/netplan/Netplan/config/', out) cid = out.decode('utf-8').split('/')[-1].replace('"\n', '') # Verify that the state folders were created in /tmp tmpdir = '/tmp/netplan-config-{}'.format(cid) self.assertTrue(os.path.isdir(tmpdir)) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'etc', 'netplan'))) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'run', 'netplan'))) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'lib', 'netplan'))) # Return random config ID return cid 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_noroot(self): # Process should fail instantly, if not: kill it after 5 sec r = subprocess.run(NETPLAN_DBUS_CMD, timeout=5, capture_output=True) self.assertEquals(r.returncode, 1) self.assertIn(b'Failed to acquire service name', r.stderr) 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_info(self): BUSCTL_NETPLAN_INFO = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Info", ] output = subprocess.check_output(BUSCTL_NETPLAN_INFO) self.assertIn("Features", output.decode("utf-8")) def test_netplan_dbus_config(self): # Create test YAML test_file_lib = os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml') with open(test_file_lib, 'w') as f: f.write('TESTING-lib') test_file_run = os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml') with open(test_file_run, 'w') as f: f.write('TESTING-run') self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml'))) cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) self.addClassCleanup(shutil.rmtree, tmpdir) # Verify the object path has been created, by calling .Config.Get() on that object # it would throw an error if it does not exist BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Get", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD, universal_newlines=True) self.assertIn(r's ""', out) # No output as 'netplan get' is actually mocked self.assertEquals( self.mock_netplan_cmd.calls(), [["netplan", "get", "all", "--root-dir={}".format(tmpdir)]]) # Verify all *.yaml files have been copied self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'etc', 'netplan', 'main_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'lib', 'netplan', 'lib_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'run', 'netplan', 'run_test.yaml'))) def test_netplan_dbus_no_such_command(self): err = self._check_dbus_error([ "busctl", "call", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "NoSuchCommand" ]) self.assertIn("Unknown method", err) def test_netplan_dbus_config_set(self): cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) self.addCleanup(shutil.rmtree, tmpdir) # Verify .Config.Set() on the config object # No actual YAML file will be created, as the netplan command is mocked BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth42.dhcp6=true", "", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) print(self.mock_netplan_cmd.calls(), flush=True) self.assertEquals(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth42.dhcp6=true", "--root-dir={}".format(tmpdir) ]]) def test_netplan_dbus_config_get(self): cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) self.addCleanup(shutil.rmtree, tmpdir) # Verify .Config.Get() on the config object self.mock_netplan_cmd.set_output("network:\n eth42:\n dhcp6: true") BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Get", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD, universal_newlines=True) self.assertIn(r's "network:\n eth42:\n dhcp6: true\n"', out) self.assertEquals( self.mock_netplan_cmd.calls(), [["netplan", "get", "all", "--root-dir={}".format(tmpdir)]]) def test_netplan_dbus_config_cancel(self): cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) # Verify .Config.Cancel() teardown of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) time.sleep(1) # Give some time for 'Cancel' to clean up self.assertFalse(os.path.isdir(tmpdir)) # Verify the object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn( 'Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) def test_netplan_dbus_config_apply(self): cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) with open(os.path.join(tmpdir, 'etc', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') with open(os.path.join(tmpdir, 'lib', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') with open(os.path.join(tmpdir, 'run', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') # Verify .Config.Apply() teardown of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply"]]) time.sleep(1) # Give some time for 'Apply' to clean up self.assertFalse(os.path.isdir(tmpdir)) # Verify the new YAML files were copied over self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'apply_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'run', 'netplan', 'apply_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'lib', 'netplan', 'apply_test.yaml'))) # Verify the object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn( 'Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) def test_netplan_dbus_config_try_cancel(self): # self-terminate after 30 dsec = 3 sec, if not cancelled before self.mock_netplan_cmd.set_timeout(30) cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) backup = '/tmp/netplan-config-BACKUP' with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') # Verify .Config.Try() setup of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Verify the temp state still exists self.assertTrue(os.path.isdir(tmpdir)) self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'))) # Verify the backup has been created self.assertTrue(os.path.isdir(backup)) self.assertTrue( os.path.isfile( os.path.join(backup, 'etc', 'netplan', 'main_test.yaml'))) # Verify the new YAML files were copied over self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) time.sleep(1) # Give some time for 'Cancel' to clean up # Verify the backup andconfig state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) # Verify the backup has been restored self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) # Verify the config object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn( 'Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify 'netplan try' has been called self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=3"]]) def test_netplan_dbus_config_try_cb(self): self.mock_netplan_cmd.set_timeout( 1) # actually self-terminate after 0.1 sec cid = self._new_config_object() tmpdir = '/tmp/netplan-config-{}'.format(cid) backup = '/tmp/netplan-config-BACKUP' with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "1", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) time.sleep(1.5) # Give some time for the timeout to happen # Verify the backup andconfig state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) # Verify the backup has been restored self.assertTrue( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertFalse( os.path.isfile( os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) # Verify the config object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn( 'Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify 'netplan try' has been called self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=1"]]) def test_netplan_dbus_config_try_apply(self): self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Apply", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('Another \'netplan try\' process is already running', err) def test_netplan_dbus_config_try_config_try(self): self.mock_netplan_cmd.set_timeout(50) # 50 dsec = 5 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Try", "u", "5", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('Another Try() is currently in progress: PID ', err) def test_netplan_dbus_config_set_invalidate(self): self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Calling Set() on the same config object still works BUSCTL_NETPLAN_CMD1 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=yes", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) self.assertEqual(b'b true\n', out) cid2 = self._new_config_object() # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) # Calling Try() on another config object fails BUSCTL_NETPLAN_CMD3 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Try", "u", "3", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD3) self.assertIn('This config was invalidated by another config object', err) # Calling Apply() on another config object fails BUSCTL_NETPLAN_CMD4 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Apply", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD4) self.assertIn('This config was invalidated by another config object', err) # Calling Apply() on the same config object still works BUSCTL_NETPLAN_CMD5 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD5) self.assertEqual(b'b true\n', out) # Verify that Set()/Apply() was only called by one config object self.assertEquals(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid) ], [ "netplan", "set", "ethernets.eth0.dhcp4=yes", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid) ], ["netplan", "apply"]]) # Now it works again cid3 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid3), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid3), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) def test_netplan_dbus_config_set_uninvalidate(self): self.mock_netplan_cmd.set_timeout(2) cid = self._new_config_object() cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) # Calling Cancel() clears the dirty state BUSCTL_NETPLAN_CMD3 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD3) self.assertEqual(b'b true\n', out) # Calling Set() on the other config object works now out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) # Verify the call stack self.assertEquals(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid) ], [ "netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid2) ]]) def test_netplan_dbus_config_set_uninvalidate_timeout(self): self.mock_netplan_cmd.set_timeout( 1) # actually self-terminate process after 0.1 sec cid = self._new_config_object() cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD1 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "1", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) self.assertEqual(b'b true\n', out) # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) time.sleep(1.5) # Wait for the child process to self-terminate # Calling Set() on the other config object works now out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) # Verify the call stack self.assertEquals(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid) ], ["netplan", "try", "--timeout=1"], [ "netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", "--root-dir=/tmp/netplan-config-{}".format(cid2) ]])