[master/rhel7-branch][PATCH] Add support for network configuration in TUI. (#909299)

Samantha N. Bueno sbueno+anaconda at redhat.com
Thu Aug 1 21:20:20 UTC 2013


After re-writing it about three times, I think this is as non-ugly as
the TUI network spoke is going to get.

There are a couple of exceptionally ugly hacks in here. I wanted to use
nmcli to be able to add/configure devices, but that functionality is not
present--hence the need for the ugly hacks. nmcli is, however, under
active development to add these capabilities, so hopefully it is not too
long before a new NM is released with a more robust nmcli. When that
happens, large swaths of this can be re-written much more cleanly.

That said, only wired connections are supported in this. Support for
vlan/bonding/wifi devices would add far too much complexity for what
should be a clean and simple interface--at least at this point. So,
users need to install graphically or via kickstart if they want to
configure those devices.

Also, I hope Radek won't object to my including his name on the authors
list. I copied over a non-trivial portion of the back-end logic, so I
feel it should get added.
---
 pyanaconda/ui/tui/hubs/summary.py   |   2 +-
 pyanaconda/ui/tui/spokes/network.py | 422 ++++++++++++++++++++++++++++++++++++
 2 files changed, 423 insertions(+), 1 deletion(-)
 create mode 100644 pyanaconda/ui/tui/spokes/network.py

diff --git a/pyanaconda/ui/tui/hubs/summary.py b/pyanaconda/ui/tui/hubs/summary.py
index 1ae5cca..3f5f53e 100644
--- a/pyanaconda/ui/tui/hubs/summary.py
+++ b/pyanaconda/ui/tui/hubs/summary.py
@@ -29,7 +29,7 @@ import time
 class SummaryHub(TUIHub):
     title = _("Installation")
     ## FIXME: this should be pulling data from somewhere, not just a static list
-    categories = ["source", "localization", "destination", "password", "software"]
+    categories = ["source", "localization", "destination", "password", "software", "network"]
 
     def setup(self, environment="anaconda"):
         TUIHub.setup(self, environment=environment)
diff --git a/pyanaconda/ui/tui/spokes/network.py b/pyanaconda/ui/tui/spokes/network.py
new file mode 100644
index 0000000..5550fd6
--- /dev/null
+++ b/pyanaconda/ui/tui/spokes/network.py
@@ -0,0 +1,422 @@
+# Network configuration spoke classes
+#
+# Copyright (C) 2013  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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 Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Samantha N. Bueno <sbueno at redhat.com>,
+#                    Radek Vykydal <rvykydal at redhat.com>
+#
+
+from pyanaconda.flags import can_touch_runtime_system
+from pyanaconda.ui.tui.spokes import EditTUISpoke, OneShotEditTUIDialog
+from pyanaconda.ui.tui.spokes import EditTUISpokeEntry as Entry
+from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget
+from pyanaconda.i18n import _
+from pyanaconda import network
+from pyanaconda.nm import nm_activated_devices, nm_is_connecting
+
+# pylint: disable-msg=E0611
+from gi.repository import GLib, NetworkManager, NMClient
+import dbus
+import socket
+import struct
+
+import ctypes
+ctypes.cdll.LoadLibrary("libnm-util.so.2")
+nm_utils = ctypes.CDLL("libnm-util.so.2")
+
+import re
+import logging
+LOG = logging.getLogger("anaconda")
+
+
+# These are required for dbus API use we need because of
+# NM_GI_BUGS: 767998, 773678
+NM_SERVICE = "org.freedesktop.NetworkManager"
+NM_802_11_AP_FLAGS_PRIVACY = 0x1
+NM_802_11_AP_SEC_NONE = 0x0
+NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x200
+DBUS_PROPS_IFACE = "org.freedesktop.DBus.Properties"
+
+
+__all__ = ["NetworkSpoke"]
+
+
+class NetworkSpoke(EditTUISpoke):
+    """ Spoke used to configure network settings. """
+    title = _("Network settings")
+    category = "network"
+
+    def __init__(self, app, data, storage, payload, instclass):
+        EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
+        self.hostname = OneShotEditTUIDialog(app, data, storage, payload, instclass)
+        self.devices = []
+        self.errors = []
+        # NM Client stuff
+        self.client = NMClient.Client.new()
+        self.selected = None
+        self.cfg = {}
+
+    def initialize(self):
+        EditTUISpoke.initialize(self)
+        if not self.data.network.seen:
+            self._update_network_data()
+            # if we have a network device available, go ahead and default to
+            # selecting the first device in the list
+            if len(self.activated_connections()) > 0:
+                self.selected = self.activated_connections()[0]
+
+    @property
+    def completed(self):
+        return (not can_touch_runtime_system("require network connection")
+                or self.activated_connections())
+
+    @property
+    def status(self):
+        """ Short msg telling what devices are active. """
+        msg = _("Unknown")
+
+        state = nm_is_connecting()
+        if state == NetworkManager.State.CONNECTING:
+            msg = _("Connecting...")
+        elif state == NetworkManager.State.DISCONNECTING:
+            msg = _("Disconnecting...")
+        else:
+            cons = self.activated_connections()
+            if cons:
+                if len(cons) == 1:
+                    name, tipe, dev = cons[0]
+                    if tipe == NetworkManager.DeviceType.ETHERNET:
+                        msg = _("Wired %s connected" % name)
+            else:
+                msg = _("Not connected")
+
+        if len(self.activated_connections()) == 0:
+            msg = _("No network devices available")
+
+        return msg
+
+    def getNMObjProperty(self, obj, nm_iface_suffix, prop):
+        """ Get property of NM object. """
+        props_iface = dbus.Interface(obj, DBUS_PROPS_IFACE)
+        return props_iface.Get("org.freedesktop.NetworkManager"+nm_iface_suffix, prop)
+
+    def activated_connections(self):
+        """ Returns list of tuples with info about active devices. """
+        active_devs = []
+
+        for con in self.client.get_active_connections():
+            if con.get_state() != NetworkManager.ActiveConnectionState.ACTIVATED:
+                continue
+            dev = con.get_devices()[0]
+            dev_type, dev_name = dev.get_device_type(), None
+            if dev_type == NetworkManager.DeviceType.ETHERNET:
+                dev_name = dev.get_iface()
+            if dev_name and (dev_type == NetworkManager.DeviceType.ETHERNET):
+                # only allowing wired connections in TUI
+                # too much of a pain to add all the config options to make
+                # wifi, etc. available
+                active_devs.append((dev_name, dev_type, dev))
+        return active_devs
+
+    def _update_summary(self):
+        """ Update summary screen with current dev info and hostname. """
+        msg = ""
+        dev = self.selected
+
+        if not self.activated_connections():
+            msg = _("No network devices available.")
+
+        if dev:
+            name, tipe, device = dev
+            # grab our dev configuration settings
+            if self.data.network.network:
+                _data = self.data.network.network[0]
+                if _data.bootProto == "dhcp":
+                    # only call this function if we're using dhcp.
+                    # since we do not interface with network manager when
+                    # devices are manually configured, _refresh_device_cfg
+                    # will obliterate our settings
+                    self._refresh_device_cfg(device, 3, device.get_state())
+                else:
+                    self._set_device_info_value("subnet", "Netmask", _data.netmask)
+                    self._set_device_info_value("route", "Gateway", _data.gateway)
+                    self._set_device_info_value("dns", "DNS", _data.nameserver)
+                    if _data.ip and _data.ipv6:
+                        self._set_device_info_value("ipv4", "IPv4 Address", _data.ip)
+                        self._set_device_info_value("ipv6", "IPv6 Address", _data.ipv6)
+                    elif _data.ip:
+                        self._set_device_info_value("ipv4", "IP Address", _data.ip)
+                    elif _data.ipv6:
+                        self._set_device_info_value("ipv6", "IP Address", _data.ipv6)
+
+            if tipe == NetworkManager.DeviceType.ETHERNET:
+                msg = _("Wired (%(interface_name)s) connected\n") \
+                    % {"interface_name": name}
+            # update the display message with config settings
+            for i in self.cfg:
+                msg += _("%s: %s\n" % (self.cfg[i][0], self.cfg[i][1]))
+
+            # make sure to display the hostname as well
+            if self.hostname.value:
+                msg += _("\nHostname: %s") % self.hostname.value
+
+        return msg
+
+    def _refresh_device_cfg(self, dev, num_of_tries, state):
+        """ Refresh the device configuration. """
+        ipv4cfg = None
+        ipv6cfg = None
+
+        if num_of_tries > 0:
+            ipv4cfg = dev.get_ip4_config()
+            ipv6cfg = dev.get_ip6_config()
+            if not ipv4cfg and not ipv6cfg:
+                GLib.timeout_add(300, self._refresh_device_cfg, dev, num_of_tries-1, state)
+                return False
+
+        if state is None:
+            state = dev.get_state()
+        if (ipv4cfg and state == NetworkManager.DeviceState.ACTIVATED):
+            addr = socket.inet_ntoa(struct.pack('=L',
+                                                ipv4cfg.get_addresses()[0].get_address()))
+            dnss = " ".join(socket.inet_ntoa(struct.pack('=L', addr))
+                            for addr in ipv4cfg.get_nameservers())
+            self._set_device_info_value("dns", "DNS", dnss)
+            gateway = socket.inet_ntoa(struct.pack('=L',
+                                       ipv4cfg.get_addresses()[0].get_gateway()))
+            self._set_device_info_value("route", "Gateway", gateway)
+            prefix = ipv4cfg.get_addresses()[0].get_prefix()
+            nm_utils.nm_utils_ip4_prefix_to_netmask.argtypes = [ctypes.c_uint32]
+            nm_utils.nm_utils_ip4_prefix_to_netmask.restype = ctypes.c_uint32
+            netmask = nm_utils.nm_utils_ip4_prefix_to_netmask(prefix)
+            netmask = socket.inet_ntoa(struct.pack('=L', netmask))
+            self._set_device_info_value("subnet", "Netmask", netmask)
+        else:
+            self._set_device_info_value("ipv4", "IPv4 Address", None)
+            self._set_device_info_value("subnet", "Netmask", None)
+            self._set_device_info_value("route", "Gateway", None)
+            self._set_device_info_value("dns", "DNS", None)
+
+        # TODO NM_GI_BUGS - segfaults on get_addres(), get_prefix()
+        ipv6_addr = None
+        if (ipv6cfg and state == NetworkManager.DeviceState.ACTIVATED):
+            config = dbus.SystemBus().get_object(NM_SERVICE, ipv6cfg.get_path())
+            addr, prefix, gateway = self.getNMObjProperty(config, ".IP6Config",
+                                                        "Addresses")[0]
+            ipv6_addr = socket.inet_ntop(socket.AF_INET6, "".join(chr(byte) for byte in addr))
+
+        if ipv4cfg and ipv6_addr:
+            self._set_device_info_value("ipv4", "IPv4 Address", addr)
+            self._set_device_info_value("ipv6", "IPv6 Address", ipv6_addr)
+        elif ipv4cfg:
+            self._set_device_info_value("ipv4", "IP Address", addr)
+        elif ipv6_addr:
+            self._set_device_info_value("ipv6", "IP Address", ipv6_addr)
+
+        return False
+
+    def _set_device_info_value(self, key, desc_str, value_str):
+        """ Set info about a network device; this is stored in a dict. """
+        self.cfg[key] = [desc_str, value_str]
+
+    def refresh(self, args=None):
+        """ Refresh screen. """
+        EditTUISpoke.refresh(self, args)
+
+        summary = self._update_summary()
+        self._window += [TextWidget(summary), ""]
+
+        # if we have any errors, display them
+        while len(self.errors) > 0:
+            self._window += [TextWidget(self.errors.pop()), ""]
+
+        def _prep(i, w):
+            """ Mangle our text to make it look pretty on screen. """
+            number = TextWidget("%2d)" % (i + 1))
+            return ColumnWidget([(4, [number]), (None, [w])], 1)
+
+        _opts = [_("Set hostname"), _("Configure network")]
+        _protocols = [_("IPv4"), _("IPv6")]
+        if args == 2:
+            # user's chosen 'configure network' from _opts
+            text = [TextWidget(p) for p in _protocols]
+        else:
+            text = [TextWidget(o) for o in _opts]
+
+        # make everything presentable on screen
+        choices = [_prep(i, w) for i, w in enumerate(text)]
+        displayed = ColumnWidget([(78, choices)], 1)
+        self._window.append(displayed)
+
+        return True
+
+    def input(self, args, key):
+        """ Handle the input. """
+        try:
+            num = int(key)
+            if args == 2:
+                # configure network
+                if num:
+                    # 'num' is the IP protocol they wish to configure, ipv4 or
+                    # ipv6 pass this option to the configuration spoke so it
+                    # knows which options to display to the user
+                    newspoke = ConfigureNetworkSpoke(self.app, self.data, self.storage,
+                                            self.payload, self.instclass, num)
+                    self.app.switch_screen_modal(newspoke)
+                    self.apply()
+                    return True
+            else:
+                if num == 1:
+                    # set hostname
+                    self.app.switch_screen_modal(self.hostname, Entry(_("Hostname"),
+                                        "hostname", re.compile(".*$"), True))
+                    self.apply()
+                    return True
+                else:
+                    self.app.switch_screen(self, num)
+            return None
+        except (ValueError, IndexError):
+            return key
+
+    def apply(self):
+        " Apply all of our settings. """
+        # first update the network data in case any changes have been made
+        self._update_network_data()
+        # then make sure to update the summary so users can see any changes
+        self._update_summary()
+
+    def _update_hostname(self):
+        """ Update hostname value. """
+        if not self.hostname.value:
+            # set it to the default value
+            self.hostname.value = network.DEFAULT_HOSTNAME
+
+        # if we are set to the default value, try and make a guess as to what
+        # the hostname should be
+        if self.hostname.value == network.DEFAULT_HOSTNAME:
+            hostname = network.getHostname()
+            network.update_hostname_data(self.data, hostname)
+
+        # run sanity check on hostname
+        (valid, error) = network.sanityCheckHostname(self.hostname.value)
+        if not valid:
+            self.errors.append(_("Hostname is not valid: %s") % error)
+            # set hostname value back to default if sanity check fails
+            self.hostname.value = network.DEFAULT_HOSTNAME
+        else:
+            # if our hostname is valid, go ahead and update network settings
+            network.update_hostname_data(self.data, self.hostname.value)
+
+    def _update_network_data(self):
+        """ Update all of the network data. """
+        # the reason for not setting self.network.network.data = []
+        # here as in the GUI is because there may be values stored
+        # from manual network configuration, and we don't want to
+        # blow them all away until setting the values in the ifcfg file(s)
+        _data = []
+
+        for con in self.activated_connections():
+            (name, tipe, dev) = con
+            network_data = self.getKSNetworkData(dev)
+            if network_data is not None:
+                _data.append(network_data)
+        self.data.network.network = _data
+
+        self._update_hostname()
+
+    def getKSNetworkData(self, device):
+        """ Get network data. """
+        retval = None
+
+        ifcfg_suffix = None
+        if device.get_device_type() == NetworkManager.DeviceType.ETHERNET:
+            ifcfg_suffix = device.get_iface()
+
+        if ifcfg_suffix:
+            ifcfg_suffix = ifcfg_suffix.replace(' ', '_')
+            device_cfg = network.NetworkDevice(network.netscriptsDir, ifcfg_suffix)
+            try:
+                device_cfg.loadIfcfgFile()
+                # if we have a device cfg file and have set custom config opts
+                # apply them here ....unfortunately the network manager cli
+                # tool with the ability to configure networking is not yet
+                # available. when it is, this needs to get stripped since it's
+                # really, really ugly
+                if self.data.network.network:
+                    _data = self.data.network.network[0]
+                    if _data.ip:
+                        device_cfg.set(('IPADDR', _data.ip))
+                    if _data.netmask:
+                        device_cfg.set(('BOOTPROTO', "static"))
+                        device_cfg.set(('NETMASK', _data.netmask))
+                    if _data.gateway:
+                        device_cfg.set(('GATEWAY', _data.gateway))
+                    if _data.nameserver:
+                        device_cfg.set(('DNS', _data.nameserver))
+                    if _data.ipv6:
+                        device_cfg.set(('IPV6ADDR', _data.ipv6))
+                        device_cfg.set(('NETWORKING_IPV6', "yes"))
+                    if _data.ipv6gateway:
+                        device_cfg.set(('IPV6_DEFAULTGW', _data.ipv6gateway))
+                    # if we set any values above, we need to rewrite the cfg.
+                    device_cfg.writeIfcfgFile()
+
+            except IOError as err:
+                LOG.debug("getKSNetworkData %s: %s" % (ifcfg_suffix, err))
+                return None
+            retval = network.kickstartNetworkData(ifcfg=device_cfg)
+            if retval and device.get_iface() in nm_activated_devices():
+                retval.activate = True
+
+        return retval
+
+
+class ConfigureNetworkSpoke(EditTUISpoke):
+    """ Spoke to set various configuration options for net devices. """
+    title = _("Network settings")
+    category = "network"
+
+    edit_fields = [
+        Entry(_("IPv4 Address"), "ip", re.compile(".*$"), lambda self, args: self.proto == 1),
+        Entry(_("Netmask"), "netmask", re.compile(".*$"), True),
+        Entry(_("Gateway"), "gateway", re.compile(".*$"), lambda self, args: self.proto == 1),
+        Entry(_("DNS"), "nameserver", re.compile(".*$"), True),
+        Entry(_("IPv6 Address"), "ipv6", re.compile(".*$"), lambda self, args: self.proto == 2),
+        Entry(_("Gateway"), "ipv6gateway", re.compile(".*$"), lambda self, args: self.proto == 2)
+    ]
+
+    def __init__(self, app, data, storage, payload, instclass, proto):
+        EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
+        self.proto = proto
+        if self.data.network:
+            self.args = self.data.network.network[0]
+        else:
+            self.args = self.data.NetworkData()
+
+    def refresh(self, args=None):
+        """ Refresh window. """
+        return EditTUISpoke.refresh(self, args)
+
+    @property
+    def indirect(self):
+        return True
+
+    def apply(self):
+        """ Apply our changes. """
+        # set this one manually, apply in parent spoke takes care of the rest
+        if self.args.ip or self.args.ipv6:
+            self.args.bootProto = "static"
-- 
1.8.3.1



More information about the anaconda-patches mailing list