# # Copyright (C) 2014 Red Hat, Inc. # # 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 logging import os import statvfs import time from gi.repository import GObject from gi.repository import Gtk import virtinst from . import uiutil from .baseclass import vmmGObjectUI class vmmAddStorage(vmmGObjectUI): __gsignals__ = { "browse-clicked": (GObject.SignalFlags.RUN_FIRST, None, [object]), "storage-toggled": (GObject.SignalFlags.RUN_FIRST, None, [object]) } def __init__(self, conn, builder, topwin): vmmGObjectUI.__init__(self, "addstorage.ui", None, builder=builder, topwin=topwin) self.conn = conn self.storage_browser = None self.builder.connect_signals({ "on_config_storage_browse_clicked": self._browse_storage, "on_config_storage_select_toggled": self._toggle_storage_select, }) self.top_box = self.widget("config-storage-box") def _cleanup(self): self.conn = None ########################## # Initialization methods # ########################## def _get_default_dir(self): pool = self.conn.get_default_pool() if pool: return pool.get_target_path() return self.config.get_default_image_dir(self.conn) def _get_ideal_path_info(self, name): path = self._get_default_dir() fmt = self.conn.get_default_storage_format() suffix = virtinst.StorageVolume.get_file_extension_for_format(fmt) return (path, name, suffix or ".img") def _get_ideal_path(self, name): target, name, suffix = self._get_ideal_path_info(name) return os.path.join(target, name) + suffix def _host_disk_space(self): pool = self.conn.get_default_pool() path = self._get_default_dir() avail = 0 if pool and pool.is_active(): # Rate limit this, since it can be spammed at dialog startup time if ((time.time() - pool.get_last_refresh_time()) > 10): pool.refresh() avail = int(pool.get_available()) elif not self.conn.is_remote() and os.path.exists(path): vfs = os.statvfs(os.path.dirname(path)) avail = vfs[statvfs.F_FRSIZE] * vfs[statvfs.F_BAVAIL] return float(avail / 1024.0 / 1024.0 / 1024.0) def _update_host_space(self): widget = self.widget("phys-hd-label") try: max_storage = self._host_disk_space() except: logging.exception("Error determining host disk space") widget.set_markup("") return def pretty_storage(size): return "%.1f GiB" % float(size) hd_label = ("%s available in the default location" % pretty_storage(max_storage)) hd_label = ("%s" % hd_label) widget.set_markup(hd_label) def _check_default_pool_active(self): default_pool = self.conn.get_default_pool() if default_pool and not default_pool.is_active(): res = self.err.yes_no(_("Default pool is not active."), _("Storage pool '%s' is not active. " "Would you like to start the pool " "now?") % default_pool.get_name()) if not res: return False # Try to start the pool try: default_pool.start() logging.info("Started pool '%s'", default_pool.get_name()) except Exception, e: return self.err.show_err(_("Could not start storage_pool " "'%s': %s") % (default_pool.get_name(), str(e))) return True ############## # Public API # ############## @staticmethod def check_path_search(src, conn, path): skip_paths = src.config.get_perms_fix_ignore() user, broken_paths = virtinst.VirtualDisk.check_path_search( conn.get_backend(), path) for p in broken_paths[:]: if p in skip_paths: broken_paths.remove(p) if not broken_paths: return logging.debug("No search access for dirs: %s", broken_paths) resp, chkres = src.err.warn_chkbox( _("The emulator may not have search permissions " "for the path '%s'.") % path, _("Do you want to correct this now?"), _("Don't ask about these directories again."), buttons=Gtk.ButtonsType.YES_NO) if chkres: src.config.add_perms_fix_ignore(broken_paths) if not resp: return logging.debug("Attempting to correct permission issues.") errors = virtinst.VirtualDisk.fix_path_search_for_user( conn.get_backend(), path, user) if not errors: return errmsg = _("Errors were encountered changing permissions for the " "following directories:") details = "" for path, error in errors.items(): if path not in broken_paths: continue details += "%s : %s\n" % (path, error) logging.debug("Permission errors:\n%s", details) ignore, chkres = src.err.err_chkbox(errmsg, details, _("Don't ask about these directories again.")) if chkres: src.config.add_perms_fix_ignore(errors.keys()) def reset_state(self): self._update_host_space() self.widget("config-storage-create").set_active(True) self.widget("config-storage-size").set_value(8) self.widget("config-storage-entry").set_text("") self.widget("config-storage-nosparse").set_active(True) self.widget("config-storage-create-box").set_sensitive(True) fmt = self.conn.get_default_storage_format() can_alloc = fmt in ["raw"] self.widget("config-storage-nosparse").set_active(can_alloc) self.widget("config-storage-nosparse").set_sensitive(can_alloc) self.widget("config-storage-nosparse").set_tooltip_text( not can_alloc and (_("Disk format '%s' does not support full allocation.") % fmt) or "") storage_tooltip = None can_storage = (not self.conn.is_remote() or self.conn.is_storage_capable()) use_storage = self.widget("config-storage-select") storage_area = self.widget("config-storage-box") storage_area.set_sensitive(can_storage) if not can_storage: storage_tooltip = _("Connection does not support storage" " management.") use_storage.set_sensitive(True) storage_area.set_tooltip_text(storage_tooltip or "") def get_default_path(self, name, collidelist=None): collidelist = collidelist or [] pool = self.conn.get_default_pool() default_dir = self._get_default_dir() def path_exists(p): return os.path.exists(p) or p in collidelist if not pool: # Use old generating method origf = os.path.join(default_dir, name + ".img") f = origf n = 1 while path_exists(f) and n < 100: f = os.path.join(default_dir, name + "-" + str(n) + ".img") n += 1 if path_exists(f): f = origf path = f else: target, ignore, suffix = self._get_ideal_path_info(name) # Sanitize collidelist to work with the collision checker newcollidelist = [] for c in collidelist: if c and os.path.dirname(c) == pool.get_target_path(): newcollidelist.append(os.path.basename(c)) path = virtinst.StorageVolume.find_free_name( pool.get_backend(), name, suffix=suffix, collidelist=newcollidelist) path = os.path.join(target, path) return path def is_default_storage(self): return self.widget("config-storage-create").get_active() def _check_ideal_path(self, path, vmname, collidelist): # See if the ideal disk path (/default/pool/vmname.img) # exists, and if unused, prompt the use for using it conn = self.conn.get_backend() ideal = self._get_ideal_path(vmname) if ideal in collidelist: return path do_exist = False ret = True try: do_exist = virtinst.VirtualDisk.path_definitely_exists(conn, ideal) ret = virtinst.VirtualDisk.path_in_use_by(conn, ideal) except: logging.exception("Error checking default path usage") if not do_exist or ret: return path do_use = self.err.yes_no( _("The following storage already exists, but is not\n" "in use by any virtual machine:\n\n%s\n\n" "Would you like to reuse this storage?") % ideal) if do_use: return ideal return path def validate_storage(self, vmname, path=None, size=None, sparse=None, device="disk", fmt=None, collidelist=None): collidelist = collidelist or [] use_storage = self.widget("config-storage-box").is_sensitive() is_default = self.is_default_storage() conn = self.conn.get_backend() # Validate storage if not use_storage: return True # Make sure default pool is running if is_default: ret = self._check_default_pool_active() if not ret: return False readonly = False if device == virtinst.VirtualDisk.DEVICE_CDROM: readonly = True try: if size is None and sparse is None: size = uiutil.spin_get_helper( self.widget("config-storage-size")) sparse = ( not self.widget("config-storage-nosparse").get_active()) if path is None: if is_default: path = self.get_default_path(vmname, collidelist) else: path = self.widget("config-storage-entry").get_text().strip() if is_default: path = self._check_ideal_path(path, vmname, collidelist) if not path and device in ["disk", "lun"]: return self.err.val_err(_("A storage path must be specified.")) disk = virtinst.VirtualDisk(conn) disk.path = path or None disk.read_only = readonly disk.device = device if disk.wants_storage_creation(): pool = disk.get_parent_pool() vol_install = virtinst.VirtualDisk.build_vol_install( disk.conn, os.path.basename(disk.path), pool, size, sparse, fmt=fmt or None) disk.set_vol_install(vol_install) if not fmt: fmt = self.conn.get_default_storage_format() if (self.is_default_storage() and disk.get_vol_install() and fmt in disk.get_vol_install().list_formats()): logging.debug("Setting disk format from prefs: %s", fmt) disk.get_vol_install().format = fmt disk.validate() except Exception, e: return self.err.val_err(_("Storage parameter error."), e) return disk def validate_disk_object(self, disk): isfatal, errmsg = disk.is_size_conflict() if not isfatal and errmsg: # Fatal errors are reported when setting 'size' res = self.err.ok_cancel(_("Not Enough Free Space"), errmsg) if not res: return False # Disk collision names = disk.is_conflict_disk() if names: res = self.err.yes_no( _('Disk "%s" is already in use by other guests %s') % (disk.path, names), _("Do you really want to use the disk?")) if not res: return False self.check_path_search(self, self.conn, disk.path) ############# # Listeners # ############# def _browse_storage(self, ignore): self.emit("browse-clicked", self.widget("config-storage-entry")) def _toggle_storage_select(self, src): act = src.get_active() self.widget("config-storage-browse-box").set_sensitive(act) self.emit("storage-toggled", src)