# Copyright (C) 2015 Red Hat, Inc. # # This work is licensed under the GNU GPLv2. # See the COPYING file in the top-level directory. import logging from gi.repository import Gdk from gi.repository import Gtk from virtinst import StoragePool from virtinst import DeviceDisk from . import uiutil from .asyncjob import vmmAsyncJob from .baseclass import vmmGObjectUI from .createpool import vmmCreatePool from .createvol import vmmCreateVolume EDIT_POOL_IDS = ( EDIT_POOL_NAME, EDIT_POOL_AUTOSTART, ) = list(range(2)) VOL_NUM_COLUMNS = 7 (VOL_COLUMN_KEY, VOL_COLUMN_NAME, VOL_COLUMN_CAPACITY, VOL_COLUMN_SIZESTR, VOL_COLUMN_FORMAT, VOL_COLUMN_INUSEBY, VOL_COLUMN_SENSITIVE) = range(VOL_NUM_COLUMNS) POOL_NUM_COLUMNS = 4 (POOL_COLUMN_CONNKEY, POOL_COLUMN_LABEL, POOL_COLUMN_ISACTIVE, POOL_COLUMN_PERCENT) = range(POOL_NUM_COLUMNS) ICON_RUNNING = "state_running" ICON_SHUTOFF = "state_shutoff" def _get_pool_size_percent(pool): cap = pool.get_capacity() alloc = pool.get_allocation() if not cap or alloc is None: per = 0 else: per = int(((float(alloc) / float(cap)) * 100)) return "%s%%" % int(per) class vmmStorageList(vmmGObjectUI): __gsignals__ = { "browse-clicked": (vmmGObjectUI.RUN_FIRST, None, []), "volume-chosen": (vmmGObjectUI.RUN_FIRST, None, [object]), "cancel-clicked": (vmmGObjectUI.RUN_FIRST, None, []), } def __init__(self, conn, builder, topwin, vol_sensitive_cb=None): vmmGObjectUI.__init__(self, "storagelist.ui", None, builder=builder, topwin=topwin) self.conn = conn # Callback function for setting volume row sensitivity. Used # by storage browser to disallow selecting certain volumes self._vol_sensitive_cb = vol_sensitive_cb # Name hint passed to addvol. Set by storagebrowser self._name_hint = None self._active_edits = [] self._addpool = None self._addvol = None self._volmenu = None self.top_box = self.widget("storage-grid") self.builder.connect_signals({ "on_pool_add_clicked": self._pool_add, "on_pool_stop_clicked": self._pool_stop, "on_pool_start_clicked": self._pool_start, "on_pool_delete_clicked": self._pool_delete, "on_pool_refresh_clicked": self._pool_refresh, "on_pool_apply_clicked": (lambda *x: self._pool_apply()), "on_vol_delete_clicked": self._vol_delete, "on_vol_list_button_press_event": self._vol_popup_menu, "on_vol_list_changed": self._vol_selected, "on_vol_add_clicked": self._vol_add, "on_browse_cancel_clicked": self._cancel_clicked, "on_browse_local_clicked": self._browse_local_clicked, "on_choose_volume_clicked": self._choose_volume_clicked, "on_vol_list_row_activated": self._vol_list_row_activated, "on_pool_name_changed": (lambda *x: self._enable_pool_apply(x, EDIT_POOL_NAME)), "on_pool_autostart_toggled": self._pool_autostart_changed, }) self._init_ui() def _cleanup(self): try: self.conn.disconnect_by_obj(self) except Exception: pass self.conn = None if self._addpool: self._addpool.cleanup() self._addpool = None if self._addvol: self._addvol.cleanup() self._addvol = None self._volmenu.destroy() self._volmenu = None def close(self, ignore1=None, ignore2=None): if self._addvol: self._addvol.close() if self._addpool: self._addpool.close() if self._volmenu: self._volmenu.hide() ########################## # Initialization methods # ########################## def _cap_sort_func(self, model, iter1, iter2, ignore): def _cmp(a, b): return ((a > b) - (a < b)) return _cmp(int(model[iter1][VOL_COLUMN_CAPACITY]), int(model[iter2][VOL_COLUMN_CAPACITY])) def _init_ui(self): self.widget("storage-pages").set_show_tabs(False) # These are enabled in storagebrowser.py self.widget("browse-local").set_visible(False) self.widget("browse-cancel").set_visible(False) self.widget("choose-volume").set_visible(False) # Volume list popup menu self._volmenu = Gtk.Menu() volCopyPath = Gtk.ImageMenuItem.new_with_label(_("Copy Volume Path")) volCopyImage = Gtk.Image() volCopyImage.set_from_stock(Gtk.STOCK_COPY, Gtk.IconSize.MENU) volCopyPath.set_image(volCopyImage) volCopyPath.show() volCopyPath.connect("activate", self._vol_copy_path) self._volmenu.add(volCopyPath) # Volume list # [key, name, sizestr, capacity, format, in use by string, sensitive] volListModel = Gtk.ListStore(str, str, str, str, str, str, bool) self.widget("vol-list").set_model(volListModel) volCol = Gtk.TreeViewColumn(_("Volumes")) vol_txt1 = Gtk.CellRendererText() volCol.pack_start(vol_txt1, True) volCol.add_attribute(vol_txt1, 'text', VOL_COLUMN_NAME) volCol.add_attribute(vol_txt1, 'sensitive', VOL_COLUMN_SENSITIVE) volCol.set_sort_column_id(VOL_COLUMN_NAME) self.widget("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', VOL_COLUMN_SIZESTR) volSizeCol.add_attribute(vol_txt2, 'sensitive', VOL_COLUMN_SENSITIVE) volSizeCol.set_sort_column_id(VOL_COLUMN_CAPACITY) self.widget("vol-list").append_column(volSizeCol) volListModel.set_sort_func(VOL_COLUMN_CAPACITY, self._cap_sort_func) volFormatCol = Gtk.TreeViewColumn(_("Format")) vol_txt3 = Gtk.CellRendererText() volFormatCol.pack_start(vol_txt3, False) volFormatCol.add_attribute(vol_txt3, 'text', VOL_COLUMN_FORMAT) volFormatCol.add_attribute(vol_txt3, 'sensitive', VOL_COLUMN_SENSITIVE) volFormatCol.set_sort_column_id(VOL_COLUMN_FORMAT) self.widget("vol-list").append_column(volFormatCol) volUseCol = Gtk.TreeViewColumn(_("Used By")) vol_txt4 = Gtk.CellRendererText() volUseCol.pack_start(vol_txt4, False) volUseCol.add_attribute(vol_txt4, 'text', VOL_COLUMN_INUSEBY) volUseCol.add_attribute(vol_txt4, 'sensitive', VOL_COLUMN_SENSITIVE) volUseCol.set_sort_column_id(VOL_COLUMN_INUSEBY) self.widget("vol-list").append_column(volUseCol) volListModel.set_sort_column_id(VOL_COLUMN_NAME, Gtk.SortType.ASCENDING) # Init pool list # [connkey, label, pool.is_active(), percent string] pool_list = self.widget("pool-list") poolListModel = Gtk.ListStore(str, str, bool, str) pool_list.set_model(poolListModel) poolCol = Gtk.TreeViewColumn(_("Storage Pools")) pool_txt = Gtk.CellRendererText() pool_per = Gtk.CellRendererText() poolCol.pack_start(pool_per, False) poolCol.pack_start(pool_txt, True) poolCol.add_attribute(pool_txt, 'markup', POOL_COLUMN_LABEL) poolCol.add_attribute(pool_txt, 'sensitive', POOL_COLUMN_ISACTIVE) poolCol.add_attribute(pool_per, 'markup', POOL_COLUMN_PERCENT) pool_list.append_column(poolCol) poolListModel.set_sort_column_id(POOL_COLUMN_LABEL, Gtk.SortType.ASCENDING) pool_list.get_selection().connect("changed", self._pool_selected) pool_list.get_selection().set_select_function( (lambda *x: self._confirm_changes()), None) # Populate list and connect conn signals self._populate_pools() self.conn.connect("pool-added", self._conn_pool_count_changed) self.conn.connect("pool-removed", self._conn_pool_count_changed) self.conn.connect("state-changed", self._conn_state_changed) self._conn_state_changed() ############### # Public APIs # ############### def refresh_page(self): self._populate_vols() self.conn.schedule_priority_tick(pollpool=True) def set_name_hint(self, val): self._name_hint = val #################### # Internal helpers # #################### def _current_pool(self): connkey = uiutil.get_list_selection(self.widget("pool-list")) return connkey and self.conn.get_pool(connkey) def _current_vol(self): pool = self._current_pool() if not pool: return None connkey = uiutil.get_list_selection(self.widget("vol-list")) return connkey and pool.get_volume(connkey) def _enable_pool_apply(self, *arglist): edittype = arglist[-1] self.widget("pool-apply").set_sensitive(True) if edittype not in self._active_edits: self._active_edits.append(edittype) def _disable_pool_apply(self): for i in EDIT_POOL_IDS: if i in self._active_edits: self._active_edits.remove(i) self.widget("pool-apply").set_sensitive(False) def _update_pool_row(self, connkey): for row in self.widget("pool-list").get_model(): if row[POOL_COLUMN_CONNKEY] != connkey: continue # Update active sensitivity and percent available for passed key pool = self.conn.get_pool(connkey) row[POOL_COLUMN_ISACTIVE] = pool.is_active() row[POOL_COLUMN_PERCENT] = _get_pool_size_percent(pool) break curpool = self._current_pool() if not curpool or curpool.get_connkey() != connkey: return # Currently selected pool changed state: force a 'pool_selected' to # update vol list self._pool_selected(self.widget("pool-list").get_selection()) def _reset_pool_state(self): self.widget("pool-details").set_sensitive(False) self.widget("pool-name-entry").set_text("") self.widget("pool-sizes").set_markup("") self.widget("pool-location").set_text("") self.widget("pool-state-icon").set_from_icon_name( ICON_SHUTOFF, Gtk.IconSize.BUTTON) self.widget("pool-state").set_text(_("Inactive")) self.widget("vol-list").get_model().clear() self.widget("pool-autostart").set_label(_("On Boot")) self.widget("pool-autostart").set_active(False) self.widget("pool-delete").set_sensitive(False) self.widget("pool-stop").set_sensitive(False) self.widget("pool-start").set_sensitive(False) self.widget("pool-refresh").set_sensitive(False) self.widget("vol-add").set_sensitive(False) self.widget("vol-delete").set_sensitive(False) self.widget("vol-list").set_sensitive(False) self._disable_pool_apply() def _populate_pool_state(self, connkey): pool = self.conn.get_pool(connkey) auto = pool.get_autostart() active = pool.is_active() # Set pool details state self.widget("pool-details").set_sensitive(True) self.widget("pool-name-entry").set_text(pool.get_name()) self.widget("pool-name-entry").set_editable(not active) self.widget("pool-sizes").set_markup( _("%s Free / %s In Use") % (pool.get_pretty_available(), pool.get_pretty_allocation())) self.widget("pool-location").set_text( pool.get_target_path()) self.widget("pool-state-icon").set_from_icon_name( ((active and ICON_RUNNING) or ICON_SHUTOFF), Gtk.IconSize.BUTTON) self.widget("pool-state").set_text( (active and _("Active")) or _("Inactive")) self.widget("pool-autostart").set_label(_("On Boot")) self.widget("pool-autostart").set_active(auto) self.widget("vol-list").set_sensitive(active) self._populate_vols() self.widget("pool-delete").set_sensitive(not active) self.widget("pool-stop").set_sensitive(active) self.widget("pool-start").set_sensitive(not active) self.widget("pool-refresh").set_sensitive(active) self.widget("vol-add").set_sensitive(active) self.widget("vol-add").set_tooltip_text(_("Create new volume")) self.widget("vol-delete").set_sensitive(False) if active and not pool.supports_volume_creation(): self.widget("vol-add").set_sensitive(False) self.widget("vol-add").set_tooltip_text( _("Pool does not support volume creation")) def _set_storage_error_page(self, msg): self._reset_pool_state() self.widget("storage-pages").set_current_page(1) self.widget("storage-error-label").set_text(msg) def _populate_pools(self): pool_list = self.widget("pool-list") curpool = self._current_pool() model = pool_list.get_model() # Prevent events while the model is modified pool_list.set_model(None) try: pool_list.get_selection().unselect_all() model.clear() for pool in self.conn.list_pools(): pool.disconnect_by_obj(self) pool.connect("state-changed", self._pool_changed) pool.connect("refreshed", self._pool_changed) name = pool.get_name() typ = StoragePool.get_pool_type_desc(pool.get_type()) label = "%s\n%s" % (name, typ) row = [None] * POOL_NUM_COLUMNS row[POOL_COLUMN_CONNKEY] = pool.get_connkey() row[POOL_COLUMN_LABEL] = label row[POOL_COLUMN_ISACTIVE] = pool.is_active() row[POOL_COLUMN_PERCENT] = _get_pool_size_percent(pool) model.append(row) finally: pool_list.set_model(model) uiutil.set_list_selection(pool_list, curpool and curpool.get_connkey() or None) def _populate_vols(self): list_widget = self.widget("vol-list") pool = self._current_pool() vols = pool and pool.get_volumes() or [] model = list_widget.get_model() list_widget.get_selection().unselect_all() model.clear() vadj = self.widget("vol-scroll").get_vadjustment() vscroll_percent = vadj.get_value() // max(vadj.get_upper(), 1) for vol in vols: key = vol.get_connkey() try: path = vol.get_target_path() name = vol.get_pretty_name(pool.get_type()) cap = str(vol.get_capacity()) sizestr = vol.get_pretty_capacity() fmt = vol.get_format() or "" except Exception: logging.debug("Error getting volume info for '%s', " "hiding it", key, exc_info=True) continue namestr = None try: if path: names = DeviceDisk.path_in_use_by(vol.conn.get_backend(), path) namestr = ", ".join(names) if not namestr: namestr = None except Exception: logging.exception("Failed to determine if storage volume in " "use.") sensitive = True if self._vol_sensitive_cb: sensitive = self._vol_sensitive_cb(fmt) row = [None] * VOL_NUM_COLUMNS row[VOL_COLUMN_KEY] = key row[VOL_COLUMN_NAME] = name row[VOL_COLUMN_SIZESTR] = sizestr row[VOL_COLUMN_CAPACITY] = cap row[VOL_COLUMN_FORMAT] = fmt row[VOL_COLUMN_INUSEBY] = namestr row[VOL_COLUMN_SENSITIVE] = sensitive model.append(row) def _reset_vscroll_position(): vadj.set_value(vadj.get_upper() * vscroll_percent) self.idle_add(_reset_vscroll_position) def _confirm_changes(self): if not self._active_edits: return True if self.err.chkbox_helper( self.config.get_confirm_unapplied, self.config.set_confirm_unapplied, text1=(_("There are unapplied changes. " "Would you like to apply them now?")), chktext=_("Don't warn me again."), default=False): if all([edit in EDIT_POOL_IDS for edit in self._active_edits]): self._pool_apply() self._active_edits = [] return True ############# # Listeners # ############# def _browse_local_clicked(self, src): ignore = src self.emit("browse-clicked") def _choose_volume_clicked(self, src): ignore = src self.emit("volume-chosen", self._current_vol()) def _vol_list_row_activated(self, src, treeiter, viewcol): ignore = src ignore = treeiter ignore = viewcol self.emit("volume-chosen", self._current_vol()) def _pool_selected(self, src): model, treeiter = src.get_selected() if treeiter is None: self._set_storage_error_page(_("No storage pool selected.")) return self.widget("storage-pages").set_current_page(0) connkey = model[treeiter][0] try: self._populate_pool_state(connkey) except Exception as e: logging.exception(e) self._set_storage_error_page(_("Error selecting pool: %s") % e) self._disable_pool_apply() def _pool_created(self, src, connkey): # The pool list will have already been updated, since this # signal arrives only after pool-added. So all we do here is # select the pool we just created. ignore = src uiutil.set_list_selection(self.widget("pool-list"), connkey) def _vol_created(self, src, pool_connkey, volname): # The vol list will have already been updated, since this # signal arrives only after pool-refreshed. So all we do here is # select the vol we just created. ignore = src pool = self._current_pool() if not pool or pool.get_connkey() != pool_connkey: return # Select the new volume uiutil.set_list_selection(self.widget("vol-list"), volname) def _pool_autostart_changed(self, src): ignore = src self._enable_pool_apply(EDIT_POOL_AUTOSTART) def _vol_selected(self, src): model, treeiter = src.get_selected() self.widget("vol-delete").set_sensitive(bool(treeiter)) can_choose = bool(treeiter and model[treeiter][VOL_COLUMN_SENSITIVE]) self.widget("choose-volume").set_sensitive(can_choose) def _vol_popup_menu(self, widget_ignore, event): if event.button != 3: return self._volmenu.popup(None, None, None, None, 0, event.time) def _cancel_clicked(self, src): ignore = src self.emit("cancel-clicked") ############################## # Connection event listeners # ############################## def _conn_state_changed(self, ignore=None): conn_active = self.conn.is_active() self.widget("pool-add").set_sensitive(conn_active and self.conn.is_storage_capable()) if conn_active and not self.conn.is_storage_capable(): self._set_storage_error_page( _("Libvirt connection does not support storage management.")) if conn_active: uiutil.set_list_selection_by_number(self.widget("pool-list"), 0) return self._set_storage_error_page(_("Connection not active.")) self._populate_pools() def _pool_changed(self, pool): self._update_pool_row(pool.get_connkey()) def _conn_pool_count_changed(self, src, connkey): ignore = src ignore = connkey self._populate_pools() ######################### # Pool action listeners # ######################### def _pool_stop(self, src_ignore): pool = self._current_pool() if pool is None: return logging.debug("Stopping pool '%s'", pool.get_name()) vmmAsyncJob.simple_async_noshow(pool.stop, [], self, _("Error stopping pool '%s'") % pool.get_name()) def _pool_start(self, src): ignore = src pool = self._current_pool() if pool is None: return logging.debug("Starting pool '%s'", pool.get_name()) vmmAsyncJob.simple_async_noshow(pool.start, [], self, _("Error starting pool '%s'") % pool.get_name()) def _pool_add(self, src): ignore = src logging.debug("Launching 'Add Pool' wizard") try: if self._addpool is None: self._addpool = vmmCreatePool(self.conn) self._addpool.connect("pool-created", self._pool_created) self._addpool.show(self.topwin) except Exception as e: self.err.show_err(_("Error launching pool wizard: %s") % str(e)) def _pool_delete(self, src): ignore = src pool = self._current_pool() if pool is None: return result = self.err.yes_no(_("Are you sure you want to permanently " "delete the pool %s?") % pool.get_name()) if not result: return logging.debug("Deleting pool '%s'", pool.get_name()) vmmAsyncJob.simple_async_noshow(pool.delete, [], self, _("Error deleting pool '%s'") % pool.get_name()) def _pool_refresh(self, src): ignore = src if not self._confirm_changes(): return pool = self._current_pool() if pool is None: return logging.debug("Refresh pool '%s'", pool.get_name()) vmmAsyncJob.simple_async_noshow(pool.refresh, [], self, _("Error refreshing pool '%s'") % pool.get_name()) def _pool_apply(self): pool = self._current_pool() if pool is None: return logging.debug("Applying changes for pool '%s'", pool.get_name()) try: if EDIT_POOL_AUTOSTART in self._active_edits: auto = self.widget("pool-autostart").get_active() pool.set_autostart(auto) if EDIT_POOL_NAME in self._active_edits: pool.define_name(self.widget("pool-name-entry").get_text()) self.idle_add(self._populate_pools) except Exception as e: self.err.show_err(_("Error changing pool settings: %s") % str(e)) return self._disable_pool_apply() ########################### # Volume action listeners # ########################### def _vol_copy_path(self, src): ignore = src vol = self._current_vol() if not vol: return clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) target_path = vol.get_target_path() if target_path: clipboard.set_text(target_path, -1) def _vol_add(self, src): ignore = src pool = self._current_pool() if pool is None: return logging.debug("Launching 'Add Volume' wizard for pool '%s'", pool.get_name()) try: if self._addvol is None: self._addvol = vmmCreateVolume(self.conn, pool) self._addvol.connect("vol-created", self._vol_created) else: self._addvol.set_parent_pool(self.conn, pool) self._addvol.set_modal(self.topwin.get_modal()) self._addvol.set_name_hint(self._name_hint) self._addvol.show(self.topwin) except Exception as e: self.err.show_err(_("Error launching volume wizard: %s") % str(e)) def _vol_delete(self, src_ignore): vol = self._current_vol() if vol is None: return pool = self._current_pool() result = self.err.yes_no(_("Are you sure you want to permanently " "delete the volume %s?") % vol.get_name()) if not result: return def cb(): vol.delete() def idlecb(): pool.refresh() self.idle_add(idlecb) logging.debug("Deleting volume '%s'", vol.get_name()) vmmAsyncJob.simple_async_noshow(cb, [], self, _("Error deleting volume '%s'") % vol.get_name())