diff options
Diffstat (limited to 'src/jarabe/model/network.py')
-rw-r--r-- | src/jarabe/model/network.py | 389 |
1 files changed, 343 insertions, 46 deletions
diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py index 3a949da..f265ae4 100644 --- a/src/jarabe/model/network.py +++ b/src/jarabe/model/network.py @@ -1,6 +1,6 @@ # Copyright (C) 2008 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009-2010 One Laptop per Child # Copyright (C) 2009 Paraguay Educa, Martin Abente # Copyright (C) 2010 Plan Ceibal, Daniel Castelo # @@ -18,19 +18,23 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +from gettext import gettext as _ import logging import os import time import dbus +import dbus.service import gobject import ConfigParser import gconf +import ctypes from sugar import dispatch from sugar import env from sugar.util import unique_id + DEVICE_TYPE_802_3_ETHERNET = 1 DEVICE_TYPE_802_11_WIRELESS = 2 DEVICE_TYPE_GSM_MODEM = 3 @@ -54,6 +58,49 @@ NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0 NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1 NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 + +NM_DEVICE_STATE_REASON_UNKNOWN = 0 +NM_DEVICE_STATE_REASON_NONE = 1 +NM_DEVICE_STATE_REASON_NOW_MANAGED = 2 +NM_DEVICE_STATE_REASON_NOW_UNMANAGED = 3 +NM_DEVICE_STATE_REASON_CONFIG_FAILED = 4 +NM_DEVICE_STATE_REASON_CONFIG_UNAVAILABLE = 5 +NM_DEVICE_STATE_REASON_CONFIG_EXPIRED = 6 +NM_DEVICE_STATE_REASON_NO_SECRETS = 7 +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 +NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED = 9 +NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED = 10 +NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT = 11 +NM_DEVICE_STATE_REASON_PPP_START_FAILED = 12 +NM_DEVICE_STATE_REASON_PPP_DISCONNECT = 13 +NM_DEVICE_STATE_REASON_PPP_FAILED = 14 +NM_DEVICE_STATE_REASON_DHCP_START_FAILED = 15 +NM_DEVICE_STATE_REASON_DHCP_ERROR = 16 +NM_DEVICE_STATE_REASON_DHCP_FAILED = 17 +NM_DEVICE_STATE_REASON_SHARED_START_FAILED = 18 +NM_DEVICE_STATE_REASON_SHARED_FAILED = 19 +NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED = 20 +NM_DEVICE_STATE_REASON_AUTOIP_ERROR = 21 +NM_DEVICE_STATE_REASON_AUTOIP_FAILED = 22 +NM_DEVICE_STATE_REASON_MODEM_BUSY = 23 +NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE = 24 +NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER = 25 +NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT = 26 +NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED = 27 +NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED = 28 +NM_DEVICE_STATE_REASON_GSM_APN_FAILED = 29 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING = 30 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED = 31 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT = 32 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED = 33 +NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED = 34 +NM_DEVICE_STATE_REASON_FIRMWARE_MISSING = 35 +NM_DEVICE_STATE_REASON_REMOVED = 36 +NM_DEVICE_STATE_REASON_SLEEPING = 37 +NM_DEVICE_STATE_REASON_CONNECTION_REMOVED = 38 +NM_DEVICE_STATE_REASON_USER_REQUESTED = 39 +NM_DEVICE_STATE_REASON_CARRIER = 40 + NM_802_11_AP_FLAGS_NONE = 0x00000000 NM_802_11_AP_FLAGS_PRIVACY = 0x00000001 @@ -83,11 +130,16 @@ NM_802_11_DEVICE_CAP_RSN = 0x00000020 SETTINGS_SERVICE = 'org.freedesktop.NetworkManagerUserSettings' +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_PATH = '/org/freedesktop/NetworkManager' +NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' NM_SETTINGS_PATH = '/org/freedesktop/NetworkManagerSettings' NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManagerSettings' NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection' NM_SECRETS_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection.Secrets' NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username' GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password' @@ -99,6 +151,137 @@ GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk' _nm_settings = None _conn_counter = 0 +_nm_device_state_reason_description = None + + +def get_error_by_reason(reason): + global _nm_device_state_reason_description + + if _nm_device_state_reason_description is None: + _nm_device_state_reason_description = { + NM_DEVICE_STATE_REASON_UNKNOWN: + _('The reason for the device state change is unknown.'), + NM_DEVICE_STATE_REASON_NONE: + _('The state change is normal.'), + NM_DEVICE_STATE_REASON_NOW_MANAGED: + _('The device is now managed.'), + NM_DEVICE_STATE_REASON_NOW_UNMANAGED: + _('The device is no longer managed.'), + NM_DEVICE_STATE_REASON_CONFIG_FAILED: + _('The device could not be readied for configuration.'), + NM_DEVICE_STATE_REASON_CONFIG_UNAVAILABLE: + _('IP configuration could not be reserved ' + '(no available address, timeout, etc).'), + NM_DEVICE_STATE_REASON_CONFIG_EXPIRED: + _('The IP configuration is no longer valid.'), + NM_DEVICE_STATE_REASON_NO_SECRETS: + _('Secrets were required, but not provided.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT: + _('The 802.1X supplicant disconnected from ' + 'the access point or authentication server.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED: + _('Configuration of the 802.1X supplicant failed.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED: + _('The 802.1X supplicant quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT: + _('The 802.1X supplicant took too long to authenticate.'), + NM_DEVICE_STATE_REASON_PPP_START_FAILED: + _('The PPP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_PPP_DISCONNECT: + _('The PPP service disconnected unexpectedly.'), + NM_DEVICE_STATE_REASON_PPP_FAILED: + _('The PPP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_DHCP_START_FAILED: + _('The DHCP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_DHCP_ERROR: + _('The DHCP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_DHCP_FAILED: + _('The DHCP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SHARED_START_FAILED: + _('The shared connection service failed to start.'), + NM_DEVICE_STATE_REASON_SHARED_FAILED: + _('The shared connection service quit or failed' + ' unexpectedly.'), + NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED: + _('The AutoIP service failed to start.'), + NM_DEVICE_STATE_REASON_AUTOIP_ERROR: + _('The AutoIP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_AUTOIP_FAILED: + _('The AutoIP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_MODEM_BUSY: + _('Dialing failed because the line was busy.'), + NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE: + _('Dialing failed because there was no dial tone.'), + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER: + _('Dialing failed because there was no carrier.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT: + _('Dialing timed out.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED: + _('Dialing failed.'), + NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED: + _('Modem initialization failed.'), + NM_DEVICE_STATE_REASON_GSM_APN_FAILED: + _('Failed to select the specified GSM APN'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING: + _('Not searching for networks.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED: + _('Network registration was denied.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT: + _('Network registration timed out.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED: + _('Failed to register with the requested GSM network.'), + NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED: + _('PIN check failed.'), + NM_DEVICE_STATE_REASON_FIRMWARE_MISSING: + _('Necessary firmware for the device may be missing.'), + NM_DEVICE_STATE_REASON_REMOVED: + _('The device was removed.'), + NM_DEVICE_STATE_REASON_SLEEPING: + _('NetworkManager went to sleep.'), + NM_DEVICE_STATE_REASON_CONNECTION_REMOVED: + _("The device's active connection was removed " + "or disappeared."), + NM_DEVICE_STATE_REASON_USER_REQUESTED: + _('A user or client requested the disconnection.'), + NM_DEVICE_STATE_REASON_CARRIER: + _("The device's carrier/link changed.")} + + return _nm_device_state_reason_description[reason] + + +def frequency_to_channel(frequency): + """Returns the channel matching a given radio channel frequency. If a + frequency is not in the dictionary channel 1 will be returned. + + Keyword arguments: + frequency -- The radio channel frequency in MHz. + + Return: Channel + + """ + ftoc = {2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + if frequency not in ftoc: + logging.warning('The frequency %s can not be mapped to a channel, ' + 'defaulting to channel 1.', frequency) + return 1 + return ftoc[frequency] + + +def is_sugar_adhoc_network(ssid): + """Checks whether an access point is a sugar Ad-hoc network. + + Keyword arguments: + ssid -- Ssid of the access point. + + Return: Boolean + + """ + return ssid.startswith('Ad-hoc Network') + + class WirelessSecurity(object): def __init__(self): self.key_mgmt = None @@ -118,14 +301,16 @@ class WirelessSecurity(object): wireless_security['group'] = self.group return wireless_security + class Wireless(object): - nm_name = "802-11-wireless" + nm_name = '802-11-wireless' def __init__(self): self.ssid = None self.security = None self.mode = None self.band = None + self.channel = None def get_dict(self): wireless = {'ssid': self.ssid} @@ -135,11 +320,13 @@ class Wireless(object): wireless['mode'] = self.mode if self.band: wireless['band'] = self.band + if self.channel: + wireless['channel'] = self.channel return wireless class OlpcMesh(object): - nm_name = "802-11-olpc-mesh" + nm_name = '802-11-olpc-mesh' def __init__(self, channel, anycast_addr): self.channel = channel @@ -147,12 +334,12 @@ class OlpcMesh(object): def get_dict(self): ret = { - "ssid": dbus.ByteArray("olpc-mesh"), - "channel": self.channel, + 'ssid': dbus.ByteArray('olpc-mesh'), + 'channel': self.channel, } if self.anycast_addr: - ret["dhcp-anycast-address"] = dbus.ByteArray(self.anycast_addr) + ret['dhcp-anycast-address'] = dbus.ByteArray(self.anycast_addr) return ret @@ -173,6 +360,7 @@ class Connection(object): connection['timestamp'] = self.timestamp return connection + class IP4Config(object): def __init__(self): self.method = None @@ -183,6 +371,7 @@ class IP4Config(object): ip4_config['method'] = self.method return ip4_config + class Serial(object): def __init__(self): self.baud = None @@ -195,6 +384,7 @@ class Serial(object): return serial + class Ppp(object): def __init__(self): pass @@ -203,6 +393,7 @@ class Ppp(object): ppp = {} return ppp + class Gsm(object): def __init__(self): self.apn = None @@ -221,6 +412,7 @@ class Gsm(object): return gsm + class Settings(object): def __init__(self, wireless_cfg=None): self.connection = Connection() @@ -243,6 +435,7 @@ class Settings(object): settings['ipv4'] = self.ip4_config.get_dict() return settings + class Secrets(object): def __init__(self, settings): self.settings = settings @@ -268,6 +461,7 @@ class Secrets(object): return settings + class SettingsGsm(object): def __init__(self): self.connection = Connection() @@ -287,22 +481,24 @@ class SettingsGsm(object): return settings + class SecretsGsm(object): def __init__(self): self.password = None self.pin = None self.puk = None - + def get_dict(self): secrets = {} if self.password is not None: secrets['password'] = self.password if self.pin is not None: secrets['pin'] = self.pin - if self.puk is not None: + if self.puk is not None: secrets['puk'] = self.puk return {'gsm': secrets} + class NMSettings(dbus.service.Object): def __init__(self): bus = dbus.SystemBus() @@ -330,10 +526,19 @@ class NMSettings(dbus.service.Object): self.secrets_request.send(self, connection=sender, response=kwargs['response']) + def clear_wifi_connections(self): + for uuid in self.connections.keys(): + conn = self.connections[uuid] + if conn._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS: + conn.Removed() + self.connections.pop(uuid) + + class SecretsResponse(object): - ''' Intermediate object to report the secrets from the dialog + """Intermediate object to report the secrets from the dialog back to the connection object and which will inform NM - ''' + """ def __init__(self, connection, reply_cb, error_cb): self._connection = connection self._reply_cb = reply_cb @@ -346,6 +551,7 @@ class SecretsResponse(object): def set_error(self, error): self._error_cb(error) + class NMSettingsConnection(dbus.service.Object): def __init__(self, path, settings, secrets): bus = dbus.SystemBus() @@ -358,27 +564,57 @@ class NMSettingsConnection(dbus.service.Object): self._settings = settings self._secrets = secrets + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='') + def Removed(self): + pass + + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='a{sa{sv}}') + def Updated(self, settings): + pass + def set_connected(self): if self._settings.connection.type == NM_CONNECTION_TYPE_GSM: self._settings.connection.timestamp = int(time.time()) - else: - if not self._settings.connection.autoconnect: - self._settings.connection.autoconnect = True - self._settings.connection.timestamp = int(time.time()) - if self._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: - self.save() + elif not self._settings.connection.autoconnect: + self._settings.connection.autoconnect = True + self._settings.connection.timestamp = int(time.time()) + if (self._settings.connection.type == + NM_CONNECTION_TYPE_802_11_WIRELESS): + self.Updated(self._settings.get_dict()) + self.save() + + try: + # try to flush resolver cache - SL#1940 + # ctypes' syntactic sugar does not work + # so we must get the func ptr explicitly + libc = ctypes.CDLL('libc.so.6') + res_init = getattr(libc, '__res_init') + res_init(None) + except: + # pylint: disable=W0702 + logging.exception('Error calling libc.__res_init') + + def disable_autoconnect(self): + if self._settings.connection.type != NM_CONNECTION_TYPE_GSM and \ + self._settings.connection.autoconnect: + self._settings.connection.autoconnect = False + self._settings.connection.timestamp = None + self.Updated(self._settings.get_dict()) + self.save() def set_secrets(self, secrets): self._secrets = secrets - if self._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: + if self._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS: self.save() def get_settings(self): return self._settings def save(self): - profile_path = env.get_profile_path() - config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + config_path = _get_wifi_connections_path() config = ConfigParser.ConfigParser() try: @@ -443,22 +679,31 @@ class NMSettingsConnection(dbus.service.Object): in_signature='sasb', out_signature='a{sa{sv}}') def GetSecrets(self, setting_name, hints, request_new, reply, error): logging.debug('Secrets requested for connection %s request_new=%s', - self.path, request_new) - if request_new or self._secrets is None: - # request_new is for example the case when the pw on the AP changes - response = SecretsResponse(self, reply, error) - try: - self.secrets_request.send(self, response=response) - except Exception: - logging.exception('Error requesting the secrets via dialog') + self.path, request_new) + if self._settings.connection.type is not NM_CONNECTION_TYPE_GSM: + if request_new or self._secrets is None: + # request_new is for example the case when the pw on the AP + # changes + response = SecretsResponse(self, reply, error) + try: + self.secrets_request.send(self, response=response) + except Exception: + logging.exception('Error requesting the secrets via' + ' dialog') + else: + reply(self._secrets.get_dict()) else: - reply(self._secrets.get_dict()) + if not request_new: + reply(self._secrets.get_dict()) + else: + raise Exception('The stored GSM secret has already been' + ' supplied') class AccessPoint(gobject.GObject): __gsignals__ = { 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) + ([gobject.TYPE_PYOBJECT])), } def __init__(self, device, model): @@ -475,10 +720,10 @@ class AccessPoint(gobject.GObject): self.wpa_flags = 0 self.rsn_flags = 0 self.mode = 0 + self.channel = 0 def initialize(self): - model_props = dbus.Interface(self.model, - 'org.freedesktop.DBus.Properties') + model_props = dbus.Interface(self.model, dbus.PROPERTIES_IFACE) model_props.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True, reply_handler=self._ap_properties_changed_cb, error_handler=self._get_all_props_error_cb) @@ -523,7 +768,7 @@ class AccessPoint(gobject.GObject): else: fl |= 1 << 6 - hashstr = str(fl) + "@" + self.name + hashstr = str(fl) + '@' + self.name return hash(hashstr) def _update_properties(self, properties): @@ -544,6 +789,9 @@ class AccessPoint(gobject.GObject): self.rsn_flags = properties['RsnFlags'] if 'Mode' in properties: self.mode = properties['Mode'] + if 'Frequency' in properties: + self.channel = frequency_to_channel(properties['Frequency']) + self._initialized = True self.emit('props-changed', old_hash) @@ -559,6 +807,7 @@ class AccessPoint(gobject.GObject): path=self.model.object_path, dbus_interface=NM_ACCESSPOINT_IFACE) + def get_settings(): global _nm_settings if _nm_settings is None: @@ -569,17 +818,20 @@ def get_settings(): load_connections() return _nm_settings + def find_connection_by_ssid(ssid): connections = get_settings().connections for conn_index in connections: connection = connections[conn_index] - if connection._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: - if connection._settings.wireless.ssid == ssid: - return connection + if connection._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS and \ + connection._settings.wireless.ssid == ssid: + return connection return None + def add_connection(uuid, settings, secrets=None): global _conn_counter @@ -590,19 +842,26 @@ def add_connection(uuid, settings, secrets=None): _nm_settings.add_connection(uuid, conn) return conn -def load_wifi_connections(): + +def _get_wifi_connections_path(): profile_path = env.get_profile_path() - config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + return os.path.join(profile_path, 'nm', 'connections.cfg') - config = ConfigParser.ConfigParser() + +def _create_wifi_connections(config_path): + if not os.path.exists(os.path.dirname(config_path)): + os.makedirs(os.path.dirname(config_path), 0755) + f = open(config_path, 'w') + f.close() + + +def load_wifi_connections(): + config_path = _get_wifi_connections_path() if not os.path.exists(config_path): - if not os.path.exists(os.path.dirname(config_path)): - os.makedirs(os.path.dirname(config_path), 0755) - f = open(config_path, 'w') - config.write(f) - f.close() + _create_wifi_connections(config_path) + config = ConfigParser.ConfigParser() try: if not config.read(config_path): logging.error('Error reading the nm config file') @@ -672,10 +931,10 @@ def load_gsm_connection(): if username and number and apn: settings = SettingsGsm() - settings.gsm.username = username + settings.gsm.username = username settings.gsm.number = number settings.gsm.apn = apn - + secrets = SecretsGsm() secrets.pin = pin secrets.puk = puk @@ -693,12 +952,14 @@ def load_gsm_connection(): except Exception: logging.exception('Error adding gsm connection to NMSettings.') else: - logging.exception("No gsm connection was set in GConf.") + logging.warning('No gsm connection was set in GConf.') + def load_connections(): load_wifi_connections() load_gsm_connection() + def find_gsm_connection(): connections = get_settings().connections @@ -708,3 +969,39 @@ def find_gsm_connection(): logging.debug('There is no gsm connection in the NMSettings.') return None + + +def have_wifi_connections(): + return bool(get_settings().connections) + + +def clear_wifi_connections(): + if _nm_settings is not None: + _nm_settings.clear_wifi_connections() + + config_path = _get_wifi_connections_path() + _create_wifi_connections(config_path) + + +def disconnect_access_points(ap_paths): + """ + Disconnect all devices connected to any of the given access points. + """ + bus = dbus.SystemBus() + netmgr_obj = bus.get_object(NM_SERVICE, NM_PATH) + netmgr = dbus.Interface(netmgr_obj, NM_IFACE) + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connection_paths = netmgr_props.Get(NM_IFACE, 'ActiveConnections') + + for conn_path in active_connection_paths: + conn_obj = bus.get_object(NM_IFACE, conn_path) + conn_props = dbus.Interface(conn_obj, dbus.PROPERTIES_IFACE) + ap_path = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_path == '/' or ap_path not in ap_paths: + continue + + dev_paths = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'Devices') + for dev_path in dev_paths: + dev_obj = bus.get_object(NM_SERVICE, dev_path) + dev = dbus.Interface(dev_obj, NM_DEVICE_IFACE) + dev.Disconnect() |