From 88ddaab1c7fb2e659c5a493975d358be197b16d5 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 26 Oct 2006 17:04:39 +0000 Subject: Add (slightly) sugarized NetworkManager applet An implementation of the NetworkManagerInfo service (for storing local networking config) and GUI bits for discovering network status and controlling networking, written in Python. --- (limited to 'services/nm') diff --git a/services/nm/Makefile.am b/services/nm/Makefile.am new file mode 100644 index 0000000..dfb3672 --- /dev/null +++ b/services/nm/Makefile.am @@ -0,0 +1,12 @@ +sugardir = $(pkgdatadir)/services/nm +sugar_PYTHON = \ + __init__.py \ + nmclient.py \ + nminfo.py + +bin_SCRIPTS = sugar-nm-applet + +dbusservicedir = $(sysconfdir) +dbusservice_DATA = NetworkManagerInfo.conf + +EXTRA_DIST = $(dbusservice_DATA) $(bin_SCRIPTS) diff --git a/services/nm/NetworkManagerInfo.conf b/services/nm/NetworkManagerInfo.conf new file mode 100644 index 0000000..4fb8270 --- /dev/null +++ b/services/nm/NetworkManagerInfo.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + 512 + + diff --git a/services/nm/__init__.py b/services/nm/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/services/nm/__init__.py diff --git a/services/nm/nmclient.py b/services/nm/nmclient.py new file mode 100644 index 0000000..09cc4fd --- /dev/null +++ b/services/nm/nmclient.py @@ -0,0 +1,349 @@ +# vi: ts=4 ai noet +# +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import dbus +import dbus.glib +import dbus.decorators +import gobject +import gtk + +import nminfo + +NM_STATE_STRINGS=("Unknown", + "Asleep", + "Connecting", + "Connected", + "Disconnected" +) + +NM_DEVICE_STAGE_STRINGS=("Unknown", + "Prepare", + "Config", + "Need Users Key", + "IP Config", + "IP Config Get", + "IP Config Commit", + "Activated", + "Failed", + "Cancled" +) + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' +NM_PATH = '/org/freedesktop/NetworkManager' + +DEVICE_TYPE_UNKNOWN = 0 +DEVICE_TYPE_802_3_ETHERNET = 1 +DEVICE_TYPE_802_11_WIRELESS = 2 + + +sys_bus = dbus.SystemBus() + + +class Network(gobject.GObject): + __gsignals__ = { + 'init-failed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self, op): + gobject.GObject.__init__(self) + self._op = op + self._ssid = None + self._mode = None + self._strength = 0 + + obj = sys_bus.get_object(NM_SERVICE, self._op) + net = dbus.Interface(obj, NM_IFACE_DEVICES) + net.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _update_reply_cb(self, *props): + self._ssid = props[1] + self._strength = props[3] + self._mode = props[6] + print "Net(%s): ssid '%s', mode %d, strength %d" % (self._op, self._ssid, self._mode, self._strength) + + def _update_error_cb(self, err): + print "Net(%s): failed to update." % self._op + self.emit('init-failed') + + def get_ssid(self): + return self._ssid + + def get_op(self): + return self._op + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + self._strength = strength + + +class Device(gobject.GObject): + __gsignals__ = { + 'init-failed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self, op): + gobject.GObject.__init__(self) + self._op = op + self._iface = None + self._type = 0 + self._udi = None + self._active = False + self._strength = 0 + self._link = False + self._networks = {} + + obj = sys_bus.get_object(NM_SERVICE, self._op) + dev = dbus.Interface(obj, NM_IFACE_DEVICES) + dev.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _update_reply_cb(self, *props): + self._iface = props[1] + self._type = props[2] + self._udi = props[3] + self._active = props[4] + self._link = props[15] + + if self._type == DEVICE_TYPE_802_11_WIRELESS: + self._strength = props[14] + self._update_networks(props[20], props[19]) + + def _update_networks(self, net_ops, active_op): + for op in net_ops: + net = Network(op) + self._networks[op] = net + net.connect('init-failed', self._net_init_failed) + if op == active_op: + self._active_net = net + + def _update_error_cb(self, err): + print "Device(%s): failed to update from dbus." % self._op + self.emit('init-failed') + + def _net_init_failed(self, net): + net_op = net.get_op() + if not self._networks.has_key(net_op): + return + net = self._networks[net_op] + if net == self._active_net: + self._active_net = None + del self._networks[net_op] + + def get_op(self): + return self._op + + def get_network(self, op): + if self._networks.has_key(op): + return self._networks[op] + return None + + def get_network_ops(self): + return self._networks.keys() + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + if strength >= 0 and strength <= 100: + self._strength = strength + else: + self._strength = 0 + + def network_appeared(self, network): + if self._networks.has_key(network): + return + net = Network(network) + self._networks[network] = net + net.connect('init-failed', self._net_init_failed) + + def network_disappeared(self, network): + if not self._networks.has_key(network): + return + del self._networks[network] + +class NMClientApp: + def __init__(self): + self.menu = None + self.nminfo = None + try: + self.nminfo = nminfo.NMInfo() + except RuntimeError: + pass + self._setup_dbus() + + self._devices = {} + self._update_devices() + + self._setup_trayicon() + + def _setup_trayicon(self): + self.trayicon = gtk.status_icon_new_from_file("/home/dcbw/Development/olpc/nm-python-client/icons/nm-no-connection.png") + self.trayicon.connect("popup_menu", self._popup) + self.trayicon.connect("activate", self._popup) + + def _popup(self, status, button=0, time=None): + def menu_pos(menu): + return gtk.status_icon_position_menu(menu, self.trayicon) + + if time is None: + time = gtk.get_current_event_time() + if self.menu: + del self.menu + self.menu = self._construct_new_menu() + self.menu.popup(None, None, menu_pos, button, time) + self.menu.show_all() + + def _construct_new_menu(self): + menu = gtk.Menu() + item = gtk.CheckMenuItem() + label = gtk.Label("foobar") + label.set_alignment(0.0, 0.5) + item.add(label) + label.show() + menu.add(item) + return menu + + def _update_devices_reply_cb(self, ops): + for op in ops: + dev = Device(op) + self._devices[op] = dev + dev.connect('init-failed', self._dev_init_failed_cb) + + def _dev_init_failed_cb(self, dev): + # Device failed to initialize, likely due to dbus errors or something + op = dev.get_op() + if self._devices.has_key(op): + del self._devices[op] + + def _update_devices_error_cb(self, err): + print "Error updating devices; %s" % err + + def _update_devices(self): + for dev_name in self._devices.keys(): + del self._devices[dev_name] + self._devices = {} + + nm_obj = sys_bus.get_object(NM_SERVICE, NM_PATH) + nm = dbus.Interface(nm_obj, NM_IFACE) + nm.getDevices(reply_handler=self._update_devices_reply_cb, \ + error_handler=self._update_devices_error_cb) + + def _setup_dbus(self): + sig_handlers = { + 'DeviceActivationStage': self.device_activation_stage_sig_handler, + 'StateChange': self.state_change_sig_handler, + 'DeviceActivating': self.device_activating_sig_handler, + 'DeviceNowActive': self.device_now_active_sig_handler, + 'WirelessNetworkAppeared': self.wireless_network_appeared_sig_handler, + 'WirelessNetworkDisappeared': self.wireless_network_disappeared_sig_handler, + 'DeviceStrengthChanged': self.wireless_device_strength_changed_sig_handler, + 'WirelessNetworkStrengthChanged': self.wireless_network_strength_changed_sig_handler + } + + self.nm_proxy = sys_bus.get_object(NM_SERVICE, NM_PATH) + + sys_bus.add_signal_receiver(self.name_owner_changed_sig_handler, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + sys_bus.add_signal_receiver(self.catchall_signal_handler, + dbus_interface=NM_IFACE) + + sys_bus.add_signal_receiver(self.catchall_signal_handler, + dbus_interface=NM_IFACE + 'Devices') + + for (signal, handler) in sig_handlers.items(): + sys_bus.add_signal_receiver(handler, signal_name=signal, dbus_interface=NM_IFACE) + + @dbus.decorators.explicitly_pass_message + def catchall_signal_handler(*args, **keywords): + dbus_message = keywords['dbus_message'] + mem = dbus_message.get_member() + iface = dbus_message.get_interface() + + if iface == NM_IFACE and \ + (mem == 'DeviceActivationStage' or \ + mem == 'StateChange' or \ + mem == 'DeviceActivating' or \ + mem == 'DeviceNowActive' or \ + mem == 'DeviceStrengthChanged' or \ + mem == 'WirelessNetworkAppeared' or \ + mem == 'WirelessNetworkDisappeared' or \ + mem == 'WirelessNetworkStrengthChanged'): + return + + print 'Caught signal %s.%s' % (dbus_message.get_interface(), mem) + for arg in args: + print ' ' + str(arg) + + def device_activation_stage_sig_handler(self, device, stage): + print 'Network Manager Device Stage "%s" for device %s'%(NM_DEVICE_STAGE_STRINGS[stage], device) + + def state_change_sig_handler(self, state): + print 'Network Manager State "%s"'%NM_STATE_STRINGS[state] + + def device_activating_sig_handler(self, device): + print 'Device %s activating'%device + + def device_now_active_sig_handler(self, device, essid=None): + print 'Device %s now activated (%s)'%(device, essid) + + def name_owner_changed_sig_handler(self, name, old, new): + if name != NM_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # NM went away + pass + elif (not old and not len(old)) and (new and len(new)): + # NM started up + self._update_devices() + + def wireless_network_appeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_appeared(network) + + def wireless_network_disappeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_disappeared(network) + + def wireless_device_strength_changed_sig_handler(self, device, strength): + if not self._devices.has_key(device): + return + self._devices[device].set_strength(strength) + + def wireless_network_strength_changed_sig_handler(self, device, network, strength): + if not self._devices.has_key(device): + return + net = self._devices[device].get_network(network) + if net: + net.set_strength(strength) + + def run(self): + loop = gobject.MainLoop() + try: + loop.run() + except KeyboardInterrupt: + pass + diff --git a/services/nm/nminfo.py b/services/nm/nminfo.py new file mode 100644 index 0000000..746ed7f --- /dev/null +++ b/services/nm/nminfo.py @@ -0,0 +1,346 @@ +# vi: ts=4 ai noet +# +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import dbus +import dbus.service +import time +import os +import binascii +from ConfigParser import ConfigParser +try: + from sugar import env +except ImportError: + pass + +NM_INFO_IFACE='org.freedesktop.NetworkManagerInfo' +NM_INFO_PATH='/org/freedesktop/NetworkManagerInfo' + + +class NoNetworks(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = NM_INFO_IFACE + '.NoNetworks' + + +class NetworkInvalidError(Exception): + pass + + +class NMConfig(ConfigParser): + def get_bool(self, section, name): + opt = self.get(section, name) + if type(opt) == type(""): + if opt.lower() == 'yes' or opt.lower() == 'true': + return True + elif opt.lower() == 'no' or opt.lower() == 'false': + return False + raise ValueError("Invalid format for %s/%s. Should be one of [yes, no, true, false]." % (section, name)) + + def get_list(self, section, name): + opt = self.get(section, name) + if type(opt) == type(""): + if not len(opt): + return [] + try: + return opt.split() + except Exception: + pass + raise ValueError("Invalid format for %s/%s. Should be a space-separate list." % (section, name)) + + def get_int(self, section, name): + opt = self.get(section, name) + try: + return int(opt) + except Exception: + pass + raise ValueError("Invalid format for %s/%s. Should be a valid integer." % (section, name)) + + +IW_AUTH_CIPHER_NONE = 0x00000001 +IW_AUTH_CIPHER_WEP40 = 0x00000002 +IW_AUTH_CIPHER_TKIP = 0x00000004 +IW_AUTH_CIPHER_CCMP = 0x00000008 +IW_AUTH_CIPHER_WEP104 = 0x00000010 + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +NETWORK_TYPE_UNKNOWN = 0 +NETWORK_TYPE_ALLOWED = 1 +NETWORK_TYPE_INVALID = 2 + + +class Security(object): + def __init__(self, we_cipher): + self._we_cipher = we_cipher + + def read_from_config(self, cfg, name): + pass + + def read_from_args(self, args): + pass + + def new_from_config(cfg, name): + security = None + try: + we_cipher = cfg.get_int(name, "we_cipher") + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + else: + # FIXME: find a way to make WPA config option matrix not + # make you want to throw up + raise ValueError("Unsupported security combo") + security.read_from_config(cfg, name) + except (NoOptionError, ValueError), e: + return None + return security + new_from_config = staticmethod(new_from_config) + + def new_from_args(cfg, we_cipher, args): + security = None + try: + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + else: + # FIXME: find a way to make WPA config option matrix not + # make you want to throw up + raise ValueError("Unsupported security combo") + security.read_from_args(args) + except (NoOptionError, ValueError), e: + del security + return None + return security + new_from_args = staticmethod(new_from_args) + + def get_properties(self): + return [self._we_cipher] + + def write_to_config(self, section, config): + config.set(section, "we_cipher", self._we_cipher) + + +class WEPSecurity(Security): + def read_from_args(self, *args): + if len(args) != 2: + raise ValueError("not enough arguments") + if not isinstance(args[0], str): + raise ValueError("wrong argument type for key") + if not isinstance(args[1], int): + raise ValueError("wrong argument type for auth_alg") + self._key = args[0] + self._auth_alg = args[1] + + def read_from_config(self, cfg, name): + # Key should be a hex encoded string + self._key = cfg.get(name, "key") + if self._we_cipher == IW_AUTH_CIPHER_WEP40 and len(self._key) != 10: + raise ValueError("Key length not right for 40-bit WEP") + if self._we_cipher == IW_AUTH_CIPHER_WEP104 and len(self._key) != 26: + raise ValueError("Key length not right for 104-bit WEP") + + try: + a = binascii.a2b_hex(self._key) + except TypeError: + raise ValueError("Key was not a hexadecimal string.") + + self._auth_alg = cfg.get_int(name, "auth_alg") + if self._auth_alg != IW_AUTH_ALG_OPEN_SYSTEM and self._auth_alg != IW_AUTH_ALG_SHARED_KEY: + raise ValueError("Invalid authentication algorithm %d" % self._auth_alg) + + def get_properties(self): + args = Security.get_properties(self) + args.append(self._key) + args.append(self._auth_alg) + return args + + def write_to_config(self, section, config): + Security.write_to_config(self, section, config) + config.set(section, "key", self._key) + config.set(section, "auth_alg", self._auth_alg) + + +class Network: + def __init__(ssid): + self.ssid = ssid + self.timestamp = time.now() + self.fallback = False + self.bssids = [] + self.we_cipher = 0 + self._security = None + + def get_properties(self): + args = [network.ssid, network.timestamp, network.fallback, network.bssids] + args += self._security.get_properties() + return tuple(args) + + def read_from_args(self, auto, fallback, bssid, we_cipher, *args): + if auto == False: + self.timestamp = time.now() + self.fallback = True + if not self.bssids.contains(bssid): + self.bssids.append(bssid) + + self._security = Security.new_from_args(we_cipher, args) + if not self._security: + raise NetworkInvalidError(e) + + def read_from_config(self, config): + try: + self.timestamp = config.get_int(self.ssid, "timestamp") + self.fallback = config.get_bool(self.ssid, "fallback") + except (NoOptionError, ValueError), e: + raise NetworkInvalidError(e) + + self._security = Security.new_from_config(config, self.ssid) + if not self._security: + raise NetworkInvalidError(e) + + # The following don't need to be present + try: + self.bssids = config.get_list(self.ssid, "bssids") + except (NoOptionError, ValueError), e: + pass + + def write_to_config(self, config): + config.add_section(self.ssid) + config.set(self.ssid, "timestamp", self.timestamp) + config.set(self.ssid, "fallback", self.fallback) + if len(self.bssids) > 0: + opt = "" + opt.join(self.bssids, " ") + config.set(self.ssid, "bssids", opt) + self._security.write_to_config(self.ssid, config) + + +class NMInfoDBusServiceHelper(dbus.service.Object): + def __init__(self, parent): + self._parent = parent + bus = dbus.SystemBus() + + # If NMI is already around, don't grab the NMI service + bus_object = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + name = None + try: + name = bus_object.GetNameOwner("org.freedesktop.NetworkManagerInfo", \ + dbus_interface='org.freedesktop.DBus') + except dbus.DBusException: + pass + if name: + print "NMI service already owned by %s, won't claim it." % name + raise RuntimeError + + bus_name = dbus.service.BusName(NM_INFO_IFACE, bus=bus) + dbus.service.Object.__init__(self, bus_name, NM_INFO_PATH) + + @dbus.service.method(NM_INFO_IFACE, in_signature='i', out_signature='as') + def getNetworks(self, net_type): + ssids = self._parent.get_networks(net_type) + if len(ssids) > 0: + return dbus.Array(ssids) + + raise NoNetworks + + @dbus.service.method(NM_INFO_IFACE, in_signature='si', out_signature='sibbas') + def getNetworkProperties(self, ssid, net_type): + props = self._parent.get_network_properties(ssid, net_type) + if not props: + raise NoNetworks + return props + + @dbus.service.method(NM_INFO_IFACE) + def updateNetworkInfo(self, ssid, bauto, bfallback, bssid, cipher, *args): + self._parent.update_network_info(ssid, bauto, bfallback, bssid, cipher, args) + + +class NMInfo(object): + def __init__(self): + try: + profile_path = env.get_profile_path() + except NameError: + home = os.path.expanduser("~") + profile_path = os.path.join(home, ".sugar", "default") + self._cfg_file = os.path.join(profile_path, "nm", "networks.cfg") + self._allowed_networks = self._read_config() + self._dbus_helper = NMInfoDBusServiceHelper(self) + + def save_config(self): + self._write_config(self._allowed_networks) + + def _read_config(self): + if not os.path.exists(os.path.dirname(self._cfg_file)): + os.makedirs(os.path.dirname(self._cfg_file), 0755) + if not os.path.exists(self._cfg_file): + self._write_config({}) + return {} + + config = NMConfig() + config.read(self._cfg_file) + networks = {} + for name in config.sections(): + net = Network(name) + try: + net.read_from_config(config) + networks[name] = net + except NetworkInvalidError, e: + print "Bad network!! %s" % e + del net + del config + return networks + + def _write_config(self, networks): + fp = open(self._cfg_file, 'w') + config = NMConfig() + for net in networks: + net.write_to_config(config) + config.write(fp) + fp.close() + del config + + def get_networks(self, net_type): + if net_type != NETWORK_TYPE_ALLOWED: + raise ValueError("Bad network type") + nets = [] + for net in self._allowed_networks: + nets.append(net.ssid) + print "Returning networks: %s" % nets + return nets + + def get_network_properties(self, ssid, net_type): + if net_type != NETWORK_TYPE_ALLOWED: + raise ValueError("Bad network type") + if not self._allowed_networks.has_key(ssid): + return None + network = self._allowed_networks[ssid] + props = network.get_properties() + print "Returning props for %s: %s" % (ssid, props) + return props + + def update_network_info(self, ssid, bauto, bfallback, bssid, we_cipher, *args): + if self._allowed_networks.has_key(ssid): + del self._allowed_networks[ssid] + net = Network(ssid) + try: + net.read_from_args(auto, fallback, bssid, we_cipher, args) + self._allowed_networks[ssid] = net + except InvalidNetworkError, e: + print "Bad network!! %s" % e + del net diff --git a/services/nm/sugar-nm-applet b/services/nm/sugar-nm-applet new file mode 100644 index 0000000..537c3ec --- /dev/null +++ b/services/nm/sugar-nm-applet @@ -0,0 +1,34 @@ +#! /bin/env python +# vi: ts=4 ai noet +# +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import logging +from sugar import logger +from sugar import env + +sys.path.insert(0, env.get_services_dir()) + +from nm import nmclient + +logger.start('nm-applet') + +logging.info('Starting network applet') + +app = nmclient.NMClientApp() +app.run() -- cgit v0.9.1