[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

Re: [PATCH 12/14] Add a dialog to configure advanced storage devices.



Looks ok, ack.

On 12/01/2009 09:15 PM, Chris Lumens wrote:
This brings back the old behavior of having a dialog that can prompt for
unusual storage devices that require manual intervention, like FCOE and iSCSI.
After the dialog is run, we have to put and new devices into the UI.  However,
udev isn't going to provide just a list of newly appeared devices so we have to
maintain a list of what was around previously, and compare the current device
list to that.  This promises to be slow but there's not a better option.
---
  iw/advanced_storage.py |  252 ++++++++++++++++++++++++++++++++++++++++++++++++
  iw/filter_gui.py       |  125 ++++++++++++++++++------
  ui/filter.glade        |    4 +-
  3 files changed, 351 insertions(+), 30 deletions(-)
  create mode 100644 iw/advanced_storage.py

diff --git a/iw/advanced_storage.py b/iw/advanced_storage.py
new file mode 100644
index 0000000..945d5ba
--- /dev/null
+++ b/iw/advanced_storage.py
@@ -0,0 +1,252 @@
+#
+# Copyright (C) 2009  Red Hat, Inc.  All rights reserved.
+#
+# 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, see<http://www.gnu.org/licenses/>.
+#
+
+# UI methods for supporting adding advanced storage devices.
+import gobject
+import gtk
+import gtk.glade
+import gui
+import iutil
+import network
+import storage.fcoe
+import storage.iscsi
+
+def addFcoeDrive(anaconda):
+    (dxml, dialog) = gui.getGladeWidget("fcoe-config.glade", "fcoeDialog")
+    combo = dxml.get_widget("fcoeNicCombo")
+    dcb_cb = dxml.get_widget("dcbCheckbutton")
+
+    # Populate the combo
+    cell = gtk.CellRendererText()
+    combo.pack_start(cell, True)
+    combo.set_attributes(cell, text = 0)
+    cell.set_property("wrap-width", 525)
+    combo.set_size_request(480, -1)
+    store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+    combo.set_model(store)
+
+    netdevs = anaconda.id.network.available()
+    keys = netdevs.keys()
+    keys.sort()
+    selected_interface = None
+    for dev in keys:
+        # Skip NICs which are connected (iow in use for a net install)
+        if dev in network.getActiveNetDevs():
+            continue
+
+        i = store.append(None)
+        desc = netdevs[dev].get("DESC")
+        if desc:
+            desc = "%s - %s" %(dev, desc)
+        else:
+            desc = "%s" %(dev,)
+
+        mac = netdevs[dev].get("HWADDR")
+        if mac:
+            desc = "%s - %s" %(desc, mac)
+
+        if selected_interface is None:
+            selected_interface = i
+
+        store[i] = (desc, dev)
+
+    if selected_interface:
+        combo.set_active_iter(selected_interface)
+    else:
+        combo.set_active(0)
+
+    # Show the dialog
+    gui.addFrame(dialog)
+    dialog.show_all()
+    sg = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+    sg.add_widget(dxml.get_widget("fcoeNicCombo"))
+
+    while True:
+        rc = dialog.run()
+
+        if rc in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
+            break
+
+        iter = combo.get_active_iter()
+        if iter is None:
+            anaconda.intf.messageWindow(_("Error"),
+                                        _("You must select a NIC to use."),
+                                        type="warning", custom_icon="error")
+            continue
+
+        try:
+            anaconda.id.storage.fcoe.addSan(store.get_value(iter, 1),
+                                            dcb=dcb_cb.get_active(),
+                                            intf=anaconda.intf)
+        except IOError as e:
+            anaconda.intf.messageWindow(_("Error"), str(e))
+            rc = gtk.RESPONSE_CANCEL
+
+        break
+
+    dialog.destroy()
+    return rc
+
+def addIscsiDrive(anaconda):
+    if not network.hasActiveNetDev():
+        net = NetworkConfigurator(anaconda.id.network)
+        ret = net.run()
+        net.destroy()
+        if ret != gtk.RESPONSE_OK:
+            return ret
+
+    (dxml, dialog) = gui.getGladeWidget("iscsi-config.glade", "iscsiDialog")
+    gui.addFrame(dialog)
+    dialog.show_all()
+    sg = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+    for w in ["iscsiAddrEntry", "iscsiInitiatorEntry", "userEntry",
+              "passEntry", "userinEntry", "passinEntry"]:
+        sg.add_widget(dxml.get_widget(w))
+
+    # get the initiator name if it exists and don't allow changing
+    # once set
+    e = dxml.get_widget("iscsiInitiatorEntry")
+    e.set_text(anaconda.id.storage.iscsi.initiator)
+    if anaconda.id.storage.iscsi.initiatorSet:
+        e.set_sensitive(False)
+
+    while True:
+        rc = dialog.run()
+        if rc in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
+            break
+
+        initiator = e.get_text().strip()
+        if len(initiator) == 0:
+            anaconda.intf.messageWindow(_("Invalid Initiator Name"),
+                                        _("You must provide an initiator name."))
+            continue
+
+        anaconda.id.storage.iscsi.initiator = initiator
+
+        target = dxml.get_widget("iscsiAddrEntry").get_text().strip()
+        user = dxml.get_widget("userEntry").get_text().strip()
+        pw = dxml.get_widget("passEntry").get_text().strip()
+        user_in = dxml.get_widget("userinEntry").get_text().strip()
+        pw_in = dxml.get_widget("passinEntry").get_text().strip()
+
+        try:
+            count = len(target.split(":"))
+            idx = target.rfind("]:")
+            # Check for IPV6 [IPV6-ip]:port
+            if idx != -1:
+                ip = target[1:idx]
+                port = target[idx+2:]
+            # Check for IPV4 aaa.bbb.ccc.ddd:port
+            elif count == 2:
+                idx = target.rfind(":")
+                ip = target[:idx]
+                port = target[idx+1:]
+            else:
+                ip = target
+                port = "3260"
+
+            network.sanityCheckIPString(ip)
+        except (network.IPMissing, network.IPError) as msg:
+            anaconda.intf.messageWindow(_("Error with Data"), msg)
+            continue
+
+        try:
+            anaconda.id.storage.iscsi.addTarget(ip, port, user, pw,
+                                                user_in, pw_in,
+                                                anaconda.intf)
+        except ValueError as e:
+            anaconda.intf.messageWindow(_("Error"), str(e))
+            continue
+        except IOError as e:
+            anaconda.intf.messageWindow(_("Error"), str(e))
+            rc = gtk.RESPONSE_CANCEL
+
+        break
+
+    dialog.destroy()
+    return rc
+
+def addZfcpDrive(anaconda):
+    (dxml, dialog) = gui.getGladeWidget("zfcp-config.glade", "zfcpDialog")
+    gui.addFrame(dialog)
+    dialog.show_all()
+    sg = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+    for w in ["devnumEntry", "wwpnEntry", "fcplunEntry"]:
+        sg.add_widget(dxml.get_widget(w))
+
+    while True:
+        rc = dialog.run()
+        if rc != gtk.RESPONSE_APPLY:
+            break
+
+        devnum = dxml.get_widget("devnumEntry").get_text().strip()
+        wwpn = dxml.get_widget("wwpnEntry").get_text().strip()
+        fcplun = dxml.get_widget("fcplunEntry").get_text().strip()
+
+        try:
+            anaconda.storage.zfcp.addFCP(devnum, wwpn, fcplun)
+        except ValueError as e:
+            anaconda.intf.messageWindow(_("Error"), str(e))
+            continue
+
+        break
+
+    dialog.destroy()
+    return rc
+
+def addDrive(anaconda):
+    (dxml, dialog) = gui.getGladeWidget("adddrive.glade", "addDriveDialog")
+    gui.addFrame(dialog)
+    dialog.show_all()
+    if not iutil.isS390():
+        dxml.get_widget("zfcpRadio").hide()
+        dxml.get_widget("zfcpRadio").set_group(None)
+
+    if not storage.iscsi.has_iscsi():
+        dxml.get_widget("iscsiRadio").set_sensitive(False)
+        dxml.get_widget("iscsiRadio").set_active(False)
+
+    if not storage.fcoe.has_fcoe():
+        dxml.get_widget("fcoeRadio").set_sensitive(False)
+        dxml.get_widget("fcoeRadio").set_active(False)
+
+    #figure out what advanced devices we have available and set sensible default
+    group = dxml.get_widget("iscsiRadio").get_group()
+    for button in group:
+        if button is not None and button.get_property("sensitive"):
+            button.set_active(True)
+            break
+
+    rc = dialog.run()
+    dialog.hide()
+
+    if rc in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
+        return False
+
+    if dxml.get_widget("iscsiRadio").get_active() and storage.iscsi.has_iscsi():
+        rc = addIscsiDrive(anaconda)
+    elif dxml.get_widget("fcoeRadio").get_active() and storage.fcoe.has_fcoe():
+        rc = addFcoeDrive(anaconda)
+    elif dxml.get_widget("zfcpRadio") is not None and dxml.get_widget("zfcpRadio").get_active():
+        rc = addZfcpDrive(anaconda)
+
+    dialog.destroy()
+
+    if rc in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
+        return False
+    else:
+        return True
diff --git a/iw/filter_gui.py b/iw/filter_gui.py
index 972d059..315f174 100644
--- a/iw/filter_gui.py
+++ b/iw/filter_gui.py
@@ -19,6 +19,7 @@
  #

  import block
+import collections
  import gtk, gobject
  import gtk.glade
  import gui
@@ -45,6 +46,40 @@ PORT_COL = 11
  TARGET_COL = 12
  LUN_COL = 13

+# This is kind of a magic class that is used for populating the device store.
+# It mostly acts like a list except for some funny behavior on adding/getting.
+# You must add udev dicts to this list, but when you go to examine the list
+# (by pulling items out, checking membership, etc.) you are comparing based
+# on names.
+#
+# The only reason to have this is to prevent needing two lists in a variety
+# of places throughout FilterWindow.
+class NameCache(collections.MutableSequence):
+    def __init__(self, iterable):
+        self._lst = list(iterable)
+
+    def __contains__(self, item):
+        return item["name"] in iter(self)
+
+    def __delitem__(self, index):
+        return self._lst.__delitem__(index)
+
+    def __getitem__(self, index):
+        return self._lst.__getitem__(index)["name"]
+
+    def __iter__(self):
+        for d in self._lst:
+            yield d["name"]
+
+    def __len__(self):
+        return len(self._lst)
+
+    def __setitem__(self, index, value):
+        return self._lst.__setitem__(index, value)
+
+    def insert(self, index, value):
+        return self._lst.insert(index, value)
+
  # These are global because they need to be accessible across all Callback
  # objects as the same values, and from the AdvancedFilterWindow object to add
  # and remove devices when populating scrolled windows.
@@ -65,7 +100,7 @@ def isRAID(info):
  def isMultipath(info):
      return udev_device_is_multipath_member(info)

-def isOther(info)
+def isOther(info):
      return udev_device_is_iscsi(info) or udev_device_is_fcoe(info)

  class Callbacks(object):
@@ -341,16 +376,26 @@ class FilterWindow(InstallWindow):

          self.anaconda.id.storage.exclusiveDisks.extend(list(selected))

-    def _addTuple(self, tuple):
-        global totalDevices, totalSize
+    def _add_advanced_clicked(self, button):
+        from advanced_storage import addDrive
+
+        if not addDrive(self.anaconda):
+            return
+
+        udev_trigger(subsystem="block")
+        all_new_devices = filter(udev_device_is_disk, udev_get_block_devices())
+        (all_new_devices, new_mpaths, new_partitions) = identifyMultipaths(all_new_devices)
+        (new_raid_devices, new_devices) = self.partition_list(lambda d: isRAID(d) and not isCCISS(d), all_new_devices)

-        self.store.append(None, tuple)
-        totalDevices += 1
-        totalSize += tuple[0]["XXX_SIZE"]
+        devices = filter(lambda d: d not in self._cachedDevices, new_devices)
+        mpaths = filter(lambda d: d not in self._cachedMPaths, new_mpaths)
+        raid_devices = filter(lambda d: d not in self._cachedRaidDevices, new_raid_devices)

-        for pg in self.pages:
-            if pg.cb.isMember(tuple[0]):
-                pg.cb.addToUI(tuple)
+        self.populate(devices, mpaths, raid_devices)
+
+        self._cachedDevices.extend(devices)
+        self._cachedMPaths.extend(mpaths)
+        self._cachedRaidDevices.extend(raid_devices)

      def _makeBasic(self):
          np = NotebookPage(self.store, "basic", self.xml, Callbacks(self.xml))
@@ -379,7 +424,7 @@ class FilterWindow(InstallWindow):
          np.ds.addColumn(_("Paths"), PATHS_COL)
          return np

-    def _makeOther(self)
+    def _makeOther(self):
          np = NotebookPage(self.store, "other", self.xml, OtherCallbacks(self.xml))

          np.ds.addColumn(_("WWID"), WWID_COL)
@@ -414,8 +459,10 @@ class FilterWindow(InstallWindow):
          (self.xml, self.vbox) = gui.getGladeWidget("filter.glade", "vbox")
          self.buttonBox = self.xml.get_widget("buttonBox")
          self.notebook = self.xml.get_widget("notebook")
+        self.addAdvanced = self.xml.get_widget("addAdvancedButton")

          self.notebook.connect("switch-page", self._page_switched)
+        self.addAdvanced.connect("clicked", self._add_advanced_clicked)

          self.pages = []

@@ -458,6 +505,42 @@ class FilterWindow(InstallWindow):
          (raid_devices, devices) = self.partition_list(lambda d: isRAID(d) and not isCCISS(d),
                                                        all_devices)

+        self.populate(devices, mpaths, raid_devices)
+
+        # If the "Add Advanced" button is ever clicked, we need to have a list
+        # of what devices previously existed so we know what's new.  Then we
+        # can just add the new devices to the UI.  This is going to be slow,
+        # but the user has to click a button to get to the slow part.
+        self._cachedDevices = NameCache(all_devices)
+        self._cachedMPaths = NameCache(mpaths)
+        self._cachedRaidDevices = NameCache(raid_devices)
+
+        return self.vbox
+
+    def partition_list(self, pred, lst):
+        pos = []
+        neg = []
+
+        for ele in lst:
+            if pred(ele):
+                pos.append(ele)
+            else:
+                neg.append(ele)
+
+        return (pos, neg)
+
+    def populate(self, devices, mpaths, raidDevices):
+        def _addTuple(tuple):
+            global totalDevices, totalSize
+
+            self.store.append(None, tuple)
+            totalDevices += 1
+            totalSize += tuple[0]["XXX_SIZE"]
+
+            for pg in self.pages:
+                if pg.cb.isMember(tuple[0]):
+                    pg.cb.addToUI(tuple)
+
          for d in devices:
              partedDevice = parted.Device(path="/dev/" + udev_device_get_name(d))
              d["XXX_SIZE"] = int(partedDevice.getSize())
@@ -467,7 +550,7 @@ class FilterWindow(InstallWindow):
                       udev_device_get_vendor(d), udev_device_get_bus(d),
                       udev_device_get_serial(d), udev_device_get_wwid(d),
                       "", "", "", "")
-            self._addTuple(tuple)
+            _addTuple(tuple)

          for md in block.getRaidSets():
              md.activate(mknod=True, mkparts=False)
@@ -478,7 +561,7 @@ class FilterWindow(InstallWindow):
              fstype = ""

              members = map(lambda m: m.get_devpath(), list(md.get_members()))
-            for d in raid_devices:
+            for d in raidDevices:
                  if udev_device_get_name(d) in members:
                      fstype = udev_device_get_format(d)
                      break
@@ -491,7 +574,7 @@ class FilterWindow(InstallWindow):

              tuple = (data, True, False, md.name, partedDevice.model,
                       str(size) + " MB", "", "", "", "", "", "", "", "")
-            self._addTuple(tuple)
+            _addTuple(tuple)

              md.deactivate()

@@ -511,18 +594,4 @@ class FilterWindow(InstallWindow):
                       udev_device_get_serial(mpath[0]),
                       udev_device_get_wwid(mpath[0]),
                       paths, "", "", "")
-            self._addTuple(tuple)
-
-        return self.vbox
-
-    def partition_list(self, pred, lst):
-        pos = []
-        neg = []
-
-        for ele in lst:
-            if pred(ele):
-                pos.append(ele)
-            else:
-                neg.append(ele)
-
-        return (pos, neg)
+            _addTuple(tuple)
diff --git a/ui/filter.glade b/ui/filter.glade
index a889418..5110248 100644
--- a/ui/filter.glade
+++ b/ui/filter.glade
@@ -1264,7 +1264,7 @@ Target WWID</property>
  	<property name="spacing">0</property>

  	<child>
-	<widget class="GtkButton" id="addISCSIButton">
+	<widget class="GtkButton" id="addAdvancedButton">
  	<property name="visible">True</property>
  	<property name="can_default">True</property>
  	<property name="can_focus">True</property>
@@ -1309,7 +1309,7 @@ Target WWID</property>
  		<child>
  			<widget class="GtkLabel" id="label9">
  			<property name="visible">True</property>
-			<property name="label" translatable="yes">Add iSCSI Target</property>
+			<property name="label" translatable="yes">Add Advanced Storage</property>
  			<property name="use_underline">True</property>
  			<property name="use_markup">False</property>
  			<property name="justify">GTK_JUSTIFY_LEFT</property>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]