Add libvirt storage aware file browser.

Hook it up to the New VM and Add Hardware wizards, all storage browsing.
This commit is contained in:
Cole Robinson 2009-03-09 16:19:39 -04:00
parent c83113e79b
commit b1f69cffd8
6 changed files with 582 additions and 21 deletions

View File

@ -31,6 +31,7 @@ import virtManager.util as vmmutil
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
@ -64,6 +65,9 @@ class vmmAddHardware(gobject.GObject):
self.install_error = ""
self.install_details = ""
self.storage_browser = None
self._browse_cb_id = None
self.topwin.hide()
self.window.signal_autoconnect({
"on_create_pages_switch_page" : self.page_changed,
@ -401,8 +405,6 @@ class vmmAddHardware(gobject.GObject):
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")
@ -631,25 +633,47 @@ class vmmAddHardware(gobject.GObject):
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,
confirm_func=confirm_func)
if folder and not os.access(folder, os.R_OK):
folder = None
def set_storage_cb(src, path):
if path:
textent.set_text(path)
conn = self.vm.get_connection()
if self.storage_browser == None:
self.storage_browser = vmmStorageBrowser(self.config, conn)
if self._browse_cb_id:
self.storage_browser.disconnect(self._browse_cb_id)
self._browse_cb_id = 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
def toggle_storage_size(self, ignore1=None, ignore2=None):
filename = self.get_config_disk_image()

View File

@ -36,6 +36,7 @@ from virtManager.error import vmmErrorDialog
from virtManager.asyncjob import vmmAsyncJob
from virtManager.createmeter import vmmCreateMeter
from virtManager.opticalhelper import vmmOpticalDriveHelper
from virtManager.storagebrowse import vmmStorageBrowser
OS_GENERIC = "generic"
@ -381,7 +382,6 @@ class vmmCreate(gobject.GObject):
# 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)
@ -434,7 +434,6 @@ class vmmCreate(gobject.GObject):
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:
@ -971,7 +970,7 @@ class vmmCreate(gobject.GObject):
nodetect_label.show()
def browse_iso(self, ignore1=None, ignore2=None):
f = self._browse_file(_("Locate ISO Image"))
f = self._browse_file(_("Locate ISO Image"), is_media=True)
if f != None:
self.window.get_widget("install-local-entry").set_text(f)
self.window.get_widget("install-local-entry").activate()
@ -980,7 +979,7 @@ class vmmCreate(gobject.GObject):
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)
@ -992,7 +991,12 @@ class vmmCreate(gobject.GObject):
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):
@ -1624,14 +1628,20 @@ class vmmCreate(gobject.GObject):
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, is_media=False):
if self.conn.is_remote() or True:
# FIXME: This will eventually call a special storage browser
pass
if self.conn.is_remote() or not is_media:
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.

View File

@ -0,0 +1,282 @@
#
# Copyright (C) 2009 Red Hat, Inc.
# Copyright (C) 2009 Cole Robinson <crobinso@redhat.com>
#
# 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)

View File

@ -31,6 +31,8 @@ DEFAULT_POOL_PATH = "/var/lib/libvirt/images"
def build_default_pool(conn):
"""Helper to build the 'default' storage pool"""
# FIXME: This should use config.get_default_image_path ?
if not virtinst.util.is_storage_capable(conn):
# VirtualDisk will raise an error for us
return

View File

@ -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>

View File

@ -21,6 +21,7 @@ EMIT_VAR="has no 'emit' member"
ERROR_VBOX="vmmErrorDialog.__init__.*Class 'vbox' has no 'pack_start' member"
EXCEPTHOOK="no '__excepthook__' member"
CONNECT_VAR="no 'connect' member"
DISCONNECT_VAR="no 'disconnect' member"
# os._exit is needed for forked processes.
OS_EXIT="protected member _exit of a client class"
@ -84,6 +85,7 @@ pylint --ignore=IPy.py $FILES \
-ve "$GOBJECT_VAR" \
-ve "$EMIT_VAR" \
-ve "$CONNECT_VAR" \
-ve "$DISCONNECT_VAR" \
-ve "$GETTEXT_VAR" \
-ve "$OS_EXIT" \
-ve "$BUILTIN_TYPE" \