diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py
index a32399a01..b6437d88f 100644
--- a/netplan_cli/cli/state.py
+++ b/netplan_cli/cli/state.py
@@ -17,20 +17,20 @@
# along with this program. If not, see .
-from collections import defaultdict, namedtuple
import ipaddress
import json
import logging
import re
-from socket import inet_ntop, AF_INET, AF_INET6
+import shutil
import subprocess
import sys
+from collections import defaultdict, namedtuple
from io import StringIO
+from socket import AF_INET, AF_INET6, inet_ntop
from typing import Dict, List, Type, Union
import yaml
-import dbus
import netplan
from . import utils
@@ -579,14 +579,29 @@ def query_resolved(cls) -> tuple:
addresses = None
search = None
try:
- ipc = dbus.SystemBus()
- resolve1 = ipc.get_object('org.freedesktop.resolve1', '/org/freedesktop/resolve1')
- resolve1_if = dbus.Interface(resolve1, 'org.freedesktop.DBus.Properties')
- res = resolve1_if.GetAll('org.freedesktop.resolve1.Manager')
- addresses = res['DNS']
- search = res['Domains']
- except Exception as e:
- logging.debug('Cannot query resolved DNS data: {}'.format(str(e)))
+ busctl = shutil.which('busctl')
+ if busctl is None:
+ raise RuntimeError('missing busctl utility')
+ json_out = subprocess.check_output(
+ [busctl, '--json=short', 'call', '--system',
+ 'org.freedesktop.resolve1', # the service
+ '/org/freedesktop/resolve1', # the object
+ 'org.freedesktop.DBus.Properties', # the interface
+ 'GetAll', 's', # the method and signature
+ 'org.freedesktop.resolve1.Manager', # the parameter
+ ], text=True)
+ res = json.loads(json_out)
+ data = res.get('data', [{}])[0]
+ # make sure the type doesn't change. We expect an array of two
+ # intergers and an array of bytes (IP address)
+ assert data.get('DNS', {}).get('type') == 'a(iiay)', 'DNS address type doesn\'t match'
+ addresses = data.get('DNS', {}).get('data')
+ # make sure the type dosn't change. We expect an array of an integer
+ # a string (DNS search domain) and a boolean
+ assert data.get('Domains', {}).get('type') == 'a(isb)', 'DNS search type doesn\'t match'
+ search = data.get('Domains', {}).get('data')
+ except Exception as err:
+ logging.debug('Cannot query resolved DNS data: %s', str(err))
return (addresses, search)
@classmethod
diff --git a/tests/cli/test_state.py b/tests/cli/test_state.py
index 798af5b1d..646d7b5c4 100644
--- a/tests/cli/test_state.py
+++ b/tests/cli/test_state.py
@@ -19,35 +19,23 @@
# along with this program. If not, see .
import copy
+import json
import os
import shutil
import subprocess
import tempfile
import unittest
+from unittest.mock import call, mock_open, patch
+
import yaml
-from unittest.mock import patch, call, mock_open
-from netplan_cli.cli.state import Interface, NetplanConfigState, SystemConfigState
+from netplan_cli.cli.state import (Interface, NetplanConfigState,
+ SystemConfigState)
+
from .test_status import (BRIDGE, DNS_ADDRESSES, DNS_IP4, DNS_SEARCH, FAKE_DEV,
IPROUTE2, NETWORKD, NMCLI, ROUTE4, ROUTE6)
-class resolve1_ipc_mock():
- def get_object(self, _foo, _bar):
- return {} # dbus Object
-
-
-class resolve1_iface_mock():
- def __init__(self, _foo, _bar):
- pass # dbus Interface
-
- def GetAll(self, _):
- return {
- 'DNS': DNS_ADDRESSES,
- 'Domains': DNS_SEARCH,
- }
-
-
class TestSystemState(unittest.TestCase):
'''Test netplan state module'''
@@ -137,11 +125,17 @@ def test_query_routes_fail(self, mock):
self.assertIsNone(res6)
self.assertIn('DEBUG:root:Cannot query iproute2 route data:', cm.output[0])
- @patch('dbus.Interface')
- @patch('dbus.SystemBus')
- def test_query_resolved(self, mock_ipc, mock_iface):
- mock_ipc.return_value = resolve1_ipc_mock()
- mock_iface.return_value = resolve1_iface_mock('foo', 'bar')
+ @patch('subprocess.check_output')
+ def test_query_resolved(self, mock_busctl):
+ mock_busctl.return_value = '''{"data":[{
+ "DNS": {
+ "type": "a(iiay)",
+ "data": '''+json.dumps(DNS_ADDRESSES)+'''
+ },
+ "Domains": {
+ "type": "a(isb)",
+ "data": '''+json.dumps(DNS_SEARCH)+'''
+ }}]}'''
addresses, search = SystemConfigState.query_resolved()
self.assertEqual(len(addresses), 4)
self.assertListEqual([addr[0] for addr in addresses],
@@ -150,15 +144,23 @@ def test_query_resolved(self, mock_ipc, mock_iface):
self.assertListEqual([s[1] for s in search],
['search.domain', 'search.domain'])
- @patch('dbus.SystemBus')
- def test_query_resolved_fail(self, mock):
- mock.return_value = resolve1_ipc_mock()
- mock.side_effect = Exception(1, '', 'ERR')
+ @patch('subprocess.check_output')
+ def test_query_resolved_fail(self, mock_busctl):
+ mock_busctl.return_value = '{"data":[{"DNS":{"type":"invalid","data":"garbage"}}]}'
+ with self.assertLogs(level='DEBUG') as cm:
+ addresses, search = SystemConfigState.query_resolved()
+ self.assertIsNone(addresses)
+ self.assertIsNone(search)
+ self.assertIn('DEBUG:root:Cannot query resolved DNS data: DNS address type doesn\'t match', cm.output[0])
+
+ @patch('shutil.which')
+ def test_query_resolved_fail_missing_busctl(self, mock):
+ mock.return_value = None
with self.assertLogs(level='DEBUG') as cm:
addresses, search = SystemConfigState.query_resolved()
self.assertIsNone(addresses)
self.assertIsNone(search)
- self.assertIn('DEBUG:root:Cannot query resolved DNS data:', cm.output[0])
+ self.assertIn('DEBUG:root:Cannot query resolved DNS data: missing busctl utility', cm.output[0])
def test_query_resolvconf(self):
with patch('builtins.open', mock_open(read_data='''\
diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py
index fbe0bc882..4d38acbfc 100644
--- a/tests/cli/test_status.py
+++ b/tests/cli/test_status.py
@@ -33,8 +33,8 @@
NMCLI = 'wlan0:MYCON:b6b7a21d-186e-45e1-b3a6-636da1735563:/run/NetworkManager/system-connections/netplan-NM-b6b7a21d-186e-45e1-b3a6-636da1735563-MYCON.nmconnection:802-11-wireless:yes' # nopep8
ROUTE4 = '[{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"enp0s31f6","table":"main","protocol":"dhcp","scope":"global","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"wlan0","table":"main","protocol":"dhcp","scope":"global","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"10.10.0.0/24","dev":"wg0","table":"main","protocol":"kernel","scope":"link","prefsrc":"10.10.0.2","flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"wlan0","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.142","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.1","dev":"enp0s31f6","table":"1234","protocol":"dhcp","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"broadcast","dst":"192.168.178.255","dev":"enp0s31f6","table":"local","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","flags":[]}]' # nopep8
ROUTE6 = '[{"family":10,"type":"unicast","dst":"::1","dev":"lo","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":7199,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:dead:beef::/64","dev":"tun0","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"wlan0","table":"main","protocol":"kernel","scope":"global","metric":1024,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"1234","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"metrics":[{"mtu":1492}],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":20600,"flags":[],"pref":"medium"}]' # nopep8
-DNS_IP4 = bytearray([192, 168, 178, 1])
-DNS_IP6 = bytearray([0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xce, 0xce, 0x1e, 0xff, 0xfe, 0x3d, 0xc7, 0x37])
+DNS_IP4 = ([192, 168, 178, 1])
+DNS_IP6 = ([0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xce, 0xce, 0x1e, 0xff, 0xfe, 0x3d, 0xc7, 0x37])
DNS_ADDRESSES = [(5, 2, DNS_IP4), (5, 10, DNS_IP6), (2, 2, DNS_IP4), (2, 10, DNS_IP6)] # (IFidx, IPfamily, IPbytes)
DNS_SEARCH = [(5, 'search.domain', False), (2, 'search.domain', False)]
FAKE_DEV = {'ifindex': 42, 'ifname': 'fakedev0', 'flags': [], 'operstate': 'DOWN'}