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

Radek Vykydal rvykydal at redhat.com
Tue Aug 6 16:11:04 UTC 2013


Thanks for setting off for painful text network configuration path.
I have some remarks below inline and also some more general comments here:


1) Hub networking status could display also other devices (instead of 
Unknown
in case of multiple devices) by pulling more code from gui status() (else
branch of "if len(cons) == 1:").

2a) The first network spoke screen should contain current device 
configuration
(ie obtained with _refresh_device_cfg from NM), not saved configuration (ie
content of ifcfg files accessed via ksdata) also for static config, not only
for dhcp (at least tobe consistent with GUI approach) We would display saved
configuration as prefiled values in the screen (ConfigureNetworkSpoke) where
the values are set.

2b) To apply saved configuration in GUI, the device has to be switched 
off and
on. In TUI, I'd recommend adding "99) [x] Apply" checkbox to the screen 
where
values are set which would apply (the just saved into ifcfg file) 
configuration by
turning the connection off and on. This might be not easy to get working
in TUI using NMClient.

3) _refresh_device_cfg (polling evert 300ms if we don't have config)
probably doesn't make sense here where we do refresh upon user's request,
instead I'd just directly append to the summary msg (without cfg dict) from
obtained current values.  (I fixed _refresh_device_cfg in GUI to be updated
upon signal from NM instead of polling some timeago but I forgot to commit
the patch which I did yesterday.

4) Multiple network devices.

Currently seems not to be working in case of multiple activated network
devices: single configuration (self.data.network.network[0] is applied 
to ifcfg
files of all activated devices in _update_network_data, I think we 
should check
self.selected. Also instead of modifying values of 
data.network.network[0] we
should use some special NetworkData instance.

How about listing all (plain ethernet for now) devices in first network
configuration screen, showing current configuration for the connected ones,
and giving option to configure each, passing selected dev to
ConfigureNetworkSpoke that would entirely take care of reading (to display
current values) and updating (on apply) of ifcfg values for dev.
ConfigureNetworkSpoke would use its own NetworkData instance effectively
passing the values to NetworkSpoke via modified ifcfg files from which
NetworkSpoke would just update its self.data.network.network as in GUI.

So my idea would be:

Fill NetworkSpoke.devices with ethernet devices (eg [em1, p3p1, p3p2]) in
initialize and use the list instead of just activated_devices as in 
apply() of
the spoke etc.

A) First screen:

Network settings

Wired(p3p1) connected
Netmask: 255.255.255.0
Gateway: 10.34.102.254
IP Address: 10.34.102.40
DNS: 10.34.39.2

Wired(p3p2) connected
Netmask: 255.255.255.0
Gateway: 10.34.102.254
IP Address: 10.34.102.41
DNS: 10.34.39.2

Wired(em1) disconnected

Hostname: blah

  1) Set hostname
  2) Configure p3p1
  3) Configure p3p2
  4) Configure em1

B) Next screen (Configure p3p1) ... level of IPv4/IPv6 selection 
probably removed,
not sure here, need a bit more designing, but I'd go with one flat screen
for bot ipv4 and ipv6.

The values are read from ifcfg file of device passed to the spoke,
stored in some self.nd NetworkData instance, and used to prefill the 
configuration:

for dhcp:

  1) IPv4 configuration (address or "dhcp")
     dhcp
  2) IPv4 netmask
  3) IPv4 gateway
  4) IPv6 configuration (address of "auto", "dhcp", "ignore")
     ignore
  5) IPv6 default gateway
  6) Nameservers (comma separated)
  7) [ ] Apply configuration in installer after continuing

for manual configuration:

  1) IPv4 configuration (address or "dhcp")
     10.34.102.212
  2) IPv4 netmask
     255.255.255.0
  3) IPv4 gateway
     10.34.102.254
  4) IPv6 configuration (address of "auto", "dhcp", "ignore")
     2001::1/64
  5) IPv6 default gateway
  6) Nameservers (comma separated)
     10.34.39.2,10.34.102.43
  7) [ ] Apply configuration in installer after continuing

In apply() of B) (ConfigureNetworkSpoke) the values of self.nd are 
written to
ifcfg file and if 7) is selected the device is turned off and on to 
apply the
configuration in installer.  I am afraid we don't have NetworkData -> ifcfg
file mapping function so you'd need to do the thing similar to what you 
do in
getKSNetworkData (load ifcfg file, update it, save it)

In apply() of A) (NetworkSpoke) self.data.network.network would just be 
updated
with the content of ifcfg files as in GUI

If you want I can post patches for my remarks. I've almost get my idea
working, but I have problems with the Apply configuration stuff.


On 08/01/2013 11:20 PM, Samantha N. Bueno wrote:
> 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

these three constants can go, used only for wifi.

> +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()

nm_state() should be used here

> +        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"))

this does not belong into ifcfg file, for mapping from NetworkData
to ifcfg values see dracut/parse-kickstart

> +                    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"

bootProto is only for ipv4


More information about the anaconda-patches mailing list