The attached patch adds a libvirt-storage-aware browser, to replace the standard file chooser when adding storage on remote connections. Screenshot here: http://fedorapeople.org/~crobinso/virt-manager/vmm-storage-browse.png (Greyed out pools are inactive.) In the near future, this will be enabled for local connections as well, which will allow provisioning more complex storage on demand, as well as encourage all storage to go through the libvirt APIs (while still allowing the use of unmanaged storage through the 'Browse local' option, or manually entering a path). Before we turn it on in the local case though, default pool setup needs to be more configurable and robust. Questions or comments appreciated. Thanks, Cole
# HG changeset patch # User Cole Robinson <crobinso@xxxxxxxxxx> # Node ID 0a90fb21b232139a620112ab474f7e018eff073d # Parent 0cda99ffcb16df1ba79ea1420ef1b6aee3fe2de5 Add libvirt storage aware file browser. Hook it up to the New VM and Add Hardware wizards for remote connections only. diff -r 0cda99ffcb16 -r 0a90fb21b232 src/virtManager/addhardware.py --- a/src/virtManager/addhardware.py Sun Mar 08 15:43:52 2009 -0400 +++ b/src/virtManager/addhardware.py Sun Mar 08 15:44:26 2009 -0400 @@ -32,6 +32,7 @@ from virtManager.asyncjob import vmmAsyncJob from virtManager.error import vmmErrorDialog from virtManager.createmeter import vmmCreateMeter +from virtManager.storagebrowse import vmmStorageBrowser VM_STORAGE_PARTITION = 1 VM_STORAGE_FILE = 2 @@ -65,6 +66,8 @@ self.install_error = "" self.install_details = "" + self.storage_browser = None + self.topwin.hide() self.window.signal_autoconnect({ "on_create_pages_switch_page" : self.page_changed, @@ -402,8 +405,6 @@ self.populate_target_device_model(target.get_model()) target.set_active(0) - self.window.get_widget("storage-partition-address-browse").set_sensitive(not remote) - self.window.get_widget("storage-file-address-browse").set_sensitive(not remote) elif page_number == PAGE_NETWORK: netmodel = self.window.get_widget("net-model") @@ -632,24 +633,46 @@ def browse_storage_partition_address(self, src, ignore=None): - part = self._browse_file(_("Locate Storage Partition"), "/dev") + textent = self.window.get_widget("storage-partition-address") + part = self._browse_file(_("Locate Storage Partition"), textent, + "/dev") if part != None: - self.window.get_widget("storage-partition-address").set_text(part) + textent.set_text(part) def browse_storage_file_address(self, src, ignore=None): + textent = self.window.get_widget("storage-file-address") folder = self.config.get_default_image_dir(self.vm.get_connection()) - filename = self._browse_file(_("Locate or Create New Storage File"), \ - folder=folder, confirm_overwrite=True) + filename = self._browse_file(_("Locate or Create New Storage File"), + textent, folder=folder, + confirm_overwrite=True) if filename != None: - self.window.get_widget("storage-file-address").set_text(filename) + textent.set_text(filename) - def _browse_file(self, dialog_name, folder=None, _type=None, + def _browse_file(self, dialog_name, textent, folder=None, confirm_overwrite=False): confirm_func = None if confirm_overwrite: confirm_func = self.confirm_overwrite_callback - return vmmutil.browse_local(self.topwin, dialog_name, folder, _type, + + def set_storage_cb(src, path): + if path: + textent.set_text(path) + + conn = self.vm.get_connection() + if conn.is_remote(): + if self.storage_browser == None: + self.storage_browser = vmmStorageBrowser(self.config, conn) + self.storage_browser.connect("storage-browse-finish", + set_storage_cb) + self.storage_browser.local_args = { "dialog_name": dialog_name, + "start_folder": folder, + "confirm_func": confirm_func, + } + self.storage_browser.show(conn) + return None + + return vmmutil.browse_local(self.topwin, dialog_name, folder, confirm_func=confirm_func) def toggle_storage_size(self, ignore1=None, ignore2=None): diff -r 0cda99ffcb16 -r 0a90fb21b232 src/virtManager/create.py --- a/src/virtManager/create.py Sun Mar 08 15:43:52 2009 -0400 +++ b/src/virtManager/create.py Sun Mar 08 15:44:26 2009 -0400 @@ -37,6 +37,7 @@ from virtManager.asyncjob import vmmAsyncJob from virtManager.createmeter import vmmCreateMeter from virtManager.opticalhelper import vmmOpticalDriveHelper +from virtManager.storagebrowse import vmmStorageBrowser OS_GENERIC = "generic" @@ -379,7 +380,6 @@ # Install local self.window.get_widget("install-local-cdrom-box").set_sensitive(is_local) - self.window.get_widget("install-local-browse").set_sensitive(is_local) if not is_local: self.window.get_widget("install-local-iso").set_active(True) @@ -432,7 +432,6 @@ use_storage = self.window.get_widget("config-storage-select") storage_area = self.window.get_widget("config-storage-area") - self.window.get_widget("config-storage-browse").set_sensitive(is_local) storage_area.set_sensitive(have_storage) if not have_storage: @@ -976,7 +975,7 @@ self.window.get_widget("config-storage-box").set_sensitive(src.get_active()) def browse_storage(self, ignore1): - f = self._browse_file(_("Locate existing storage.")) + f = self._browse_file(_("Locate existing storage")) if f != None: self.window.get_widget("config-storage-entry").set_text(f) @@ -988,7 +987,12 @@ self.window.get_widget("config-macaddr").set_sensitive(src.get_active()) def set_storage_path(self, src, path): - self.window.get_widget("config-storage-entry").set_text(path) + notebook = self.window.get_widget("create-pages") + curpage = notebook.get_current_page() + if curpage == PAGE_INSTALL: + self.window.get_widget("install-local-entry").set_text(path) + elif curpage == PAGE_STORAGE: + self.window.get_widget("config-storage-entry").set_text(path) # Navigation methods def set_install_page(self): @@ -1636,14 +1640,20 @@ logging.exception("Error detecting distro.") self.detectedDistro = (None, None) - def _browse_file(self, dialog_name, folder=None, _type=None): + def _browse_file(self, dialog_name, folder=None): - if self.conn.is_remote() or True: - # FIXME: This will eventually call a special storage browser - pass + if self.conn.is_remote(): + if self.storage_browser == None: + self.storage_browser = vmmStorageBrowser(self.config, + self.conn) + self.storage_browser.connect("storage-browse-finish", + self.set_storage_path) + self.storage_browser.local_args = { "dialog_name": dialog_name, + "start_folder": folder} + self.storage_browser.show(self.conn) + return None - # FIXME: Pass local browse fun to storage_browser - return util.browse_local(self.topwin, dialog_name, folder, _type) + return util.browse_local(self.topwin, dialog_name, folder) def show_help(self, ignore): # No help available yet. diff -r 0cda99ffcb16 -r 0a90fb21b232 src/virtManager/storagebrowse.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/virtManager/storagebrowse.py Sun Mar 08 15:44:26 2009 -0400 @@ -0,0 +1,282 @@ +# +# Copyright (C) 2009 Red Hat, Inc. +# Copyright (C) 2009 Cole Robinson <crobinso@xxxxxxxxxx> +# +# 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 Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import gobject +import gtk.glade + +import traceback +import logging + +import virtinst + +import virtManager.host +import virtManager.util as util +from virtManager.createvol import vmmCreateVolume +from virtManager.error import vmmErrorDialog + +class vmmStorageBrowser(gobject.GObject): + __gsignals__ = { + #"vol-created": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, []) + "storage-browse-finish": (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, [str]), + } + + def __init__(self, config, conn): + self.__gobject_init__() + self.window = gtk.glade.XML(config.get_glade_dir() + \ + "/vmm-storage-browse.glade", + "vmm-storage-browse", + domain="virt-manager") + self.config = config + self.conn = conn + self.conn_signal_ids = [] + + self.topwin = self.window.get_widget("vmm-storage-browse") + self.err = vmmErrorDialog(self.topwin, + 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, + _("Unexpected Error"), + _("An unexpected error occurred")) + self.topwin.hide() + + # Add Volume wizard + self.addvol = None + + # Arguments to pass to util.browse_local for local storage + self.local_args = {"dialog_name": _("Choose local storage")} + + self.window.signal_autoconnect({ + "on_vmm_storage_browse_delete_event" : self.close, + "on_browse_cancel_clicked" : self.close, + "on_browse_local_clicked" : self.browse_local, + "on_new_volume_clicked" : self.new_volume, + "on_choose_volume_clicked" : self.finish, + "on_vol_list_row_activated" : self.finish, + }) + + self.set_initial_state() + + def show(self, conn=None): + self.reset_state(conn) + self.topwin.show() + self.topwin.present() + + def close(self, ignore1=None, ignore2=None): + self.topwin.hide() + if self.addvol: + self.addvol.close() + return 1 + + def set_initial_state(self): + pool_list = self.window.get_widget("pool-list") + virtManager.host.init_pool_list(pool_list, self.pool_selected) + + # (Key, Name, Cap, Format, Used By) + vol_list = self.window.get_widget("vol-list") + volListModel = gtk.ListStore(str, str, str, str, str) + vol_list.set_model(volListModel) + + vol_list.get_selection().connect("changed", self.vol_selected) + volCol = gtk.TreeViewColumn(_("Name")) + vol_txt1 = gtk.CellRendererText() + volCol.pack_start(vol_txt1, True) + volCol.add_attribute(vol_txt1, 'text', 1) + volCol.set_sort_column_id(1) + vol_list.append_column(volCol) + + volSizeCol = gtk.TreeViewColumn(_("Size")) + vol_txt2 = gtk.CellRendererText() + volSizeCol.pack_start(vol_txt2, False) + volSizeCol.add_attribute(vol_txt2, 'text', 2) + volSizeCol.set_sort_column_id(2) + vol_list.append_column(volSizeCol) + + volPathCol = gtk.TreeViewColumn(_("Format")) + vol_txt4 = gtk.CellRendererText() + volPathCol.pack_start(vol_txt4, False) + volPathCol.add_attribute(vol_txt4, 'text', 3) + volPathCol.set_sort_column_id(3) + vol_list.append_column(volPathCol) + + volUseCol = gtk.TreeViewColumn(_("Used By")) + vol_txt5 = gtk.CellRendererText() + volUseCol.pack_start(vol_txt5, False) + volUseCol.add_attribute(vol_txt5, 'text', 4) + volUseCol.set_sort_column_id(4) + vol_list.append_column(volUseCol) + + volListModel.set_sort_column_id(1, gtk.SORT_ASCENDING) + + + def reset_state(self, conn=None): + if conn and conn != self.conn: + for i in self.conn_signal_ids: + self.conn.disconnect(i) + self.conn = conn + + pool_list = self.window.get_widget("pool-list") + virtManager.host.populate_storage_pools(pool_list, self.conn) + + ids = [] + ids.append(self.conn.connect("pool-added", + self.repopulate_storage_pools)) + ids.append(self.conn.connect("pool-removed", + self.repopulate_storage_pools)) + ids.append(self.conn.connect("pool-started", + self.refresh_storage_pool)) + ids.append(self.conn.connect("pool-stopped", + self.refresh_storage_pool)) + self.conn_signal_ids = ids + + # FIXME: Need a connection specific "vol-added" function? + # Won't be able to pick that change up from outside? + + # Manually trigger vol_selected, so buttons are in the correct state + self.vol_selected() + + tooltip = None + is_remote = self.conn.is_remote() + self.window.get_widget("browse-local").set_sensitive(not is_remote) + if is_remote: + tooltip = _("Cannot use local storage on remote connection.") + util.tooltip_wrapper(self.window.get_widget("browse-local"), + tooltip) + + # Convenience helpers + def current_pool(self): + sel = self.window.get_widget("pool-list").get_selection() + active = sel.get_selected() + if active[1] != None: + curruuid = active[0].get_value(active[1], 0) + return self.conn.get_pool(curruuid) + return None + + def current_vol(self): + pool = self.current_pool() + if not pool: + return None + sel = self.window.get_widget("vol-list").get_selection() + active = sel.get_selected() + if active[1] != None: + curruuid = active[0].get_value(active[1], 0) + return pool.get_volume(curruuid) + return None + + def refresh_storage_pool(self, src, uri, uuid): + pool_list = self.window.get_widget("pool-list") + virtManager.host.refresh_pool_in_list(pool_list, self.conn, uuid) + curpool = self.current_pool() + if curpool.uuid != uuid: + return + + # Currently selected pool changed state: force a 'pool_selected' to + # update vol list + self.pool_selected(self.window.get_widget("pool-list").get_selection()) + + def repopulate_storage_pools(self, src, uri, uuid): + pool_list = self.window.get_widget("pool-list") + virtManager.host.populate_storage_pools(pool_list, self.conn) + + + # Listeners + + def pool_selected(self, src): + pool = self.current_pool() + self.window.get_widget("new-volume").set_sensitive(bool(pool)) + if pool: + self.window.get_widget("new-volume").set_sensitive(pool.is_active()) + self.populate_storage_volumes() + + def vol_selected(self, ignore=None): + vol = self.current_vol() + self.window.get_widget("choose-volume").set_sensitive(bool(vol)) + + def refresh_current_pool(self, ignore): + cp = self.current_pool() + if cp is None: + return + cp.refresh() + self.refresh_storage_pool(None, None, cp.get_uuid()) + + def new_volume(self, src): + pool = self.current_pool() + if pool is None: + return + + try: + if self.addvol is None: + self.addvol = vmmCreateVolume(self.config, self.conn, pool) + self.addvol.connect("vol-created", self.refresh_current_pool) + else: + self.addvol.set_parent_pool(pool) + self.addvol.set_modal(True) + self.addvol.show() + except Exception, e: + self.show_err(_("Error launching volume wizard: %s") % str(e), + "".join(traceback.format_exc())) + + def browse_local(self, src): + filename = util.browse_local(parent=self.topwin, **self.local_args) + if filename: + self._do_finish(path=filename) + + def finish(self, ignore=None, ignore1=None, ignore2=None): + self._do_finish() + + def _do_finish(self, path=None): + if not path: + path = self.current_vol().get_path() + self.emit("storage-browse-finish", path) + self.close() + + # Do stuff! + def populate_storage_volumes(self): + model = self.window.get_widget("vol-list").get_model() + model.clear() + + pool = self.current_pool() + if not pool: + return + + vols = pool.get_volumes() + for key in vols.keys(): + vol = vols[key] + + path = vol.get_target_path() + namestr = None + try: + if path: + disk = virtinst.VirtualDisk(conn=self.conn.vmm, path=path, + size=.001) + names = disk.is_conflict_disk(self.conn.vmm, + return_names=True) + namestr = ", ".join(names) + if not namestr: + namestr = None + except: + logging.exception("Failed to determine if storage volume in " + "use.") + + model.append([key, vol.get_name(), vol.get_pretty_capacity(), + vol.get_format() or "", namestr]) + + def show_err(self, info, details): + self.show_err(info, details, async=False) + +gobject.type_register(vmmStorageBrowser) diff -r 0cda99ffcb16 -r 0a90fb21b232 src/vmm-storage-browse.glade --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/vmm-storage-browse.glade Sun Mar 08 15:44:26 2009 -0400 @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--Generated with glade3 3.4.5 on Sat Mar 7 19:17:19 2009 --> +<glade-interface> + <widget class="GtkWindow" id="vmm-storage-browse"> + <property name="border_width">10</property> + <property name="title" translatable="yes">Choose Volume</property> + <property name="modal">True</property> + <property name="default_width">750</property> + <property name="default_height">500</property> + <property name="destroy_with_parent">True</property> + <signal name="delete_event" handler="on_vmm_storage_browse_delete_event"/> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="top_padding">15</property> + <child> + <widget class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="spacing">10</property> + <child> + <widget class="GtkVBox" id="vbox3"> + <property name="visible">True</property> + <property name="spacing">10</property> + <child> + <widget class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <child> + <widget class="GtkTreeView" id="pool-list"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_clickable">True</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="spacing">3</property> + <child> + <widget class="GtkButton" id="browse-local"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_browse_local_clicked"/> + <child> + <widget class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <child> + <widget class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Browse Local</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">browse-local</property> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="spacing">10</property> + <child> + <widget class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <child> + <widget class="GtkTreeView" id="vol-list"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_clickable">True</property> + <signal name="row_activated" handler="on_vol_list_row_activated"/> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkButton" id="new-volume"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_new_volume_clicked"/> + <child> + <widget class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <property name="spacing">3</property> + <child> + <widget class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="stock">gtk-new</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label1234"> + <property name="visible">True</property> + <property name="label" translatable="yes">_New Volume</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new-volume</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <widget class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="browse-cancel"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="label" translatable="yes">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_browse_cancel_clicked"/> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="choose-volume"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_choose_volume_clicked"/> + <child> + <widget class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <property name="spacing">4</property> + <child> + <widget class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-open</property> + </widget> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Choose _Volume</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">choose-volume</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> +</glade-interface>
_______________________________________________ et-mgmt-tools mailing list et-mgmt-tools@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/et-mgmt-tools