Example #1
0
 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],
     ])
Example #2
0
   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")
Example #3
0
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})
Example #4
0
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)
                           ]])