mirror of
https://github.com/virt-manager/virt-manager.git
synced 2024-12-23 17:34:21 +03:00
990b932c17
In some setups, it is useful to have Spice input, clipboard, audio, etc., but not video, for instance when doing GPU passthrough -- one can interact inside the VM via Spice rather than USB passthrough, and use a plugged-in monitor or alternate VM viewers like Looking Glass[1] for video. It is already possible to specify a "none" video device by manually typing into the "Model" combobox and hitting "Apply". Yet, this is unintuitive. Despite being documented everywhere GPU passthrough is brought up, in the Looking Glass community we still get ~daily support requests from users who couldn't figure out how to disable Spice video. This patch makes "None" an explicit option in the video model combobox, in the hopes that this is more straightforward for users to get right. [1]: https://looking-glass.io/ Signed-off-by: Tudor Brindus <contact@tbrindus.ca>
1594 lines
55 KiB
Python
1594 lines
55 KiB
Python
# Copyright (C) 2006-2007, 2012-2015 Red Hat, Inc.
|
|
# Copyright (C) 2006 Hugh O. Brock <hbrock@redhat.com>
|
|
#
|
|
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
import traceback
|
|
|
|
from gi.repository import Gtk
|
|
|
|
from virtinst import (DeviceChannel, DeviceConsole,
|
|
DeviceController, DeviceDisk, DeviceHostdev,
|
|
DeviceInput, DeviceInterface, DevicePanic, DeviceParallel,
|
|
DeviceRedirdev, DeviceRng, DeviceSerial, DeviceSmartcard,
|
|
DeviceSound, DeviceVideo, DeviceVsock, DeviceWatchdog)
|
|
from virtinst import log
|
|
|
|
from .lib import uiutil
|
|
from .asyncjob import vmmAsyncJob
|
|
from .baseclass import vmmGObjectUI
|
|
from .device.addstorage import vmmAddStorage
|
|
from .device.fsdetails import vmmFSDetails
|
|
from .device.gfxdetails import vmmGraphicsDetails
|
|
from .device.netlist import vmmNetworkList
|
|
from .device.tpmdetails import vmmTPMDetails
|
|
from .device.vsockdetails import vmmVsockDetails
|
|
from .storagebrowse import vmmStorageBrowser
|
|
from .xmleditor import vmmXMLEditor
|
|
|
|
|
|
(PAGE_DISK,
|
|
PAGE_CONTROLLER,
|
|
PAGE_NETWORK,
|
|
PAGE_INPUT,
|
|
PAGE_GRAPHICS,
|
|
PAGE_SOUND,
|
|
PAGE_HOSTDEV,
|
|
PAGE_CHAR,
|
|
PAGE_VIDEO,
|
|
PAGE_WATCHDOG,
|
|
PAGE_FILESYSTEM,
|
|
PAGE_SMARTCARD,
|
|
PAGE_USBREDIR,
|
|
PAGE_TPM,
|
|
PAGE_RNG,
|
|
PAGE_PANIC,
|
|
PAGE_VSOCK) = range(17)
|
|
|
|
|
|
class vmmAddHardware(vmmGObjectUI):
|
|
def __init__(self, vm):
|
|
vmmGObjectUI.__init__(self, "addhardware.ui", "vmm-add-hardware")
|
|
|
|
self.vm = vm
|
|
self.conn = vm.conn
|
|
|
|
self._storagebrowser = None
|
|
|
|
self._remove_usb_controller = None
|
|
self._selected_model = None
|
|
|
|
self._gfxdetails = vmmGraphicsDetails(
|
|
self.vm, self.builder, self.topwin)
|
|
self.widget("graphics-align").add(self._gfxdetails.top_box)
|
|
|
|
self._fsdetails = vmmFSDetails(self.vm, self.builder, self.topwin)
|
|
self.widget("fs-box").add(self._fsdetails.top_box)
|
|
|
|
self._netlist = vmmNetworkList(self.conn, self.builder, self.topwin)
|
|
self.widget("network-source-label-align").add(self._netlist.top_label)
|
|
self.widget("network-source-ui-align").add(self._netlist.top_box)
|
|
|
|
self.addstorage = vmmAddStorage(self.conn, self.builder, self.topwin)
|
|
self.widget("storage-align").add(self.addstorage.top_box)
|
|
self.widget("storage-advanced-align").add(
|
|
self.addstorage.advanced_top_box)
|
|
self.addstorage.connect("browse-clicked", self._browse_storage_cb)
|
|
|
|
self._vsockdetails = vmmVsockDetails(self.vm, self.builder, self.topwin)
|
|
self.widget("vsock-align").add(self._vsockdetails.top_box)
|
|
|
|
self._tpmdetails = vmmTPMDetails(self.vm, self.builder, self.topwin)
|
|
self.widget("tpm-align").add(self._tpmdetails.top_box)
|
|
|
|
self._xmleditor = vmmXMLEditor(self.builder, self.topwin,
|
|
self.widget("create-pages-align"),
|
|
self.widget("create-pages"))
|
|
self._xmleditor.connect("xml-requested",
|
|
self._xmleditor_xml_requested_cb)
|
|
|
|
self.builder.connect_signals({
|
|
"on_create_cancel_clicked": self.close,
|
|
"on_vmm_create_delete_event": self.close,
|
|
"on_create_finish_clicked": self._finish,
|
|
"on_hw_list_changed": self._hw_selected_cb,
|
|
|
|
"on_storage_devtype_changed": self._change_storage_devtype,
|
|
"on_storage_bustype_changed": self._storage_bus_changed_cb,
|
|
|
|
"on_mac_address_clicked": self._change_macaddr_use,
|
|
|
|
"on_char_device_type_changed": self._change_char_device_type,
|
|
"on_char_target_name_changed": self._change_char_target_name,
|
|
"on_char_auto_socket_toggled": self._change_char_auto_socket,
|
|
|
|
"on_usbredir_type_changed": self._change_usbredir_type,
|
|
|
|
"on_controller_type_changed": self._change_controller_type,
|
|
})
|
|
self.bind_escape_key_close()
|
|
|
|
self._set_initial_state()
|
|
|
|
def show(self, parent):
|
|
log.debug("Showing addhw")
|
|
self._reset_state()
|
|
self.topwin.set_transient_for(parent)
|
|
self.topwin.present()
|
|
self.conn.schedule_priority_tick(pollnet=True,
|
|
pollpool=True,
|
|
pollnodedev=True)
|
|
|
|
def close(self, ignore1=None, ignore2=None):
|
|
if self.is_visible():
|
|
log.debug("Closing addhw")
|
|
self.topwin.hide()
|
|
if self._storagebrowser:
|
|
self._storagebrowser.close()
|
|
|
|
return 1
|
|
|
|
def _cleanup(self):
|
|
self.vm = None
|
|
self.conn = None
|
|
|
|
if self._storagebrowser:
|
|
self._storagebrowser.cleanup()
|
|
self._storagebrowser = None
|
|
|
|
self._gfxdetails.cleanup()
|
|
self._gfxdetails = None
|
|
self._fsdetails.cleanup()
|
|
self._fsdetails = None
|
|
self._netlist.cleanup()
|
|
self._netlist = None
|
|
self.addstorage.cleanup()
|
|
self.addstorage = None
|
|
self._vsockdetails.cleanup()
|
|
self._vsockdetails = None
|
|
self._tpmdetails.cleanup()
|
|
self._tpmdetails = None
|
|
self._xmleditor.cleanup()
|
|
self._xmleditor = None
|
|
|
|
|
|
##########################
|
|
# Initialization methods #
|
|
##########################
|
|
|
|
def _set_initial_state(self):
|
|
self.widget("create-pages").set_show_tabs(False)
|
|
self.widget("top-pages").set_show_tabs(False)
|
|
|
|
hw_col = Gtk.TreeViewColumn(_("Hardware"))
|
|
hw_col.set_spacing(6)
|
|
hw_col.set_min_width(165)
|
|
|
|
icon = Gtk.CellRendererPixbuf()
|
|
icon.set_property("stock-size", Gtk.IconSize.BUTTON)
|
|
text = Gtk.CellRendererText()
|
|
text.set_property("xpad", 6)
|
|
|
|
hw_col.pack_start(icon, False)
|
|
hw_col.pack_start(text, True)
|
|
hw_col.add_attribute(icon, 'icon-name', 1)
|
|
hw_col.add_attribute(text, 'text', 0)
|
|
hw_col.add_attribute(text, 'sensitive', 3)
|
|
self.widget("hw-list").append_column(hw_col)
|
|
|
|
# Individual HW page UI
|
|
self.build_disk_bus_combo(self.vm, self.widget("storage-bustype"))
|
|
self._build_disk_device_combo()
|
|
self.build_network_model_combo(self.vm, self.widget("net-model"))
|
|
self._build_input_combo()
|
|
self.build_sound_combo(self.vm, self.widget("sound-model"))
|
|
self._build_hostdev_treeview()
|
|
self.build_video_combo(self.vm, self.widget("video-model"))
|
|
uiutil.build_simple_combo(self.widget("char-device-type"), [])
|
|
self._build_char_target_type_combo()
|
|
self._build_char_target_name_combo()
|
|
self.build_watchdogmodel_combo(self.vm, self.widget("watchdog-model"))
|
|
self.build_watchdogaction_combo(self.vm, self.widget("watchdog-action"))
|
|
self.build_smartcard_mode_combo(self.vm, self.widget("smartcard-mode"))
|
|
self._build_redir_type_combo()
|
|
self._build_panic_model_combo()
|
|
uiutil.build_simple_combo(self.widget("controller-model"), [])
|
|
self._build_controller_type_combo()
|
|
|
|
|
|
# Available HW options
|
|
is_local = not self.conn.is_remote()
|
|
have_storage = (is_local or self.conn.support.conn_storage())
|
|
storage_tooltip = None
|
|
if not have_storage: # pragma: no cover
|
|
storage_tooltip = _("Connection does not support storage"
|
|
" management.")
|
|
|
|
# Name, icon name, page number, is sensitive, tooltip, icon size,
|
|
# device type (serial/parallel)...
|
|
model = Gtk.ListStore(str, str, int, bool, str, str)
|
|
self.widget("hw-list").set_model(model)
|
|
|
|
def add_hw_option(name, icon, page, sensitive, errortxt, devtype=None):
|
|
model.append([name, icon, page, sensitive, errortxt, devtype])
|
|
|
|
add_hw_option(_("Storage"), "drive-harddisk", PAGE_DISK, have_storage,
|
|
have_storage and storage_tooltip or None)
|
|
add_hw_option(_("Controller"), "device_pci", PAGE_CONTROLLER, True, None)
|
|
add_hw_option(_("Network"), "network-idle", PAGE_NETWORK, True, None)
|
|
add_hw_option(_("Input"), "input-mouse", PAGE_INPUT, self.vm.is_hvm(),
|
|
_("Not supported for this guest type."))
|
|
add_hw_option(_("Graphics"), "video-display", PAGE_GRAPHICS,
|
|
True, None)
|
|
add_hw_option(_("Sound"), "audio-card", PAGE_SOUND,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this guest type."))
|
|
add_hw_option(_("Serial"), "device_serial", PAGE_CHAR,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this guest type."),
|
|
"serial")
|
|
add_hw_option(_("Parallel"), "device_serial", PAGE_CHAR,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this guest type."),
|
|
"parallel")
|
|
add_hw_option(_("Console"), "device_serial", PAGE_CHAR,
|
|
True, None, "console")
|
|
add_hw_option(_("Channel"), "device_serial", PAGE_CHAR,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this guest type."),
|
|
"channel")
|
|
add_hw_option(_("USB Host Device"), "device_usb", PAGE_HOSTDEV,
|
|
self.conn.support.conn_nodedev(),
|
|
_("Connection does not support host device enumeration"),
|
|
"usb")
|
|
|
|
nodedev_enabled = self.conn.support.conn_nodedev()
|
|
nodedev_errstr = _("Connection does not support "
|
|
"host device enumeration")
|
|
if self.vm.is_container():
|
|
nodedev_enabled = False
|
|
nodedev_errstr = _("Not supported for containers")
|
|
add_hw_option(_("PCI Host Device"), "device_pci", PAGE_HOSTDEV,
|
|
nodedev_enabled, nodedev_errstr, "pci")
|
|
|
|
add_hw_option(_("MDEV Host Device"), "device_pci", PAGE_HOSTDEV,
|
|
self.conn.support.conn_nodedev(),
|
|
_("Connection does not support host device enumeration"),
|
|
"mdev")
|
|
add_hw_option(_("Video"), "video-display", PAGE_VIDEO, True,
|
|
_("Libvirt version does not support video devices."))
|
|
add_hw_option(_("Watchdog"), "device_pci", PAGE_WATCHDOG,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this guest type."))
|
|
add_hw_option(_("Filesystem"), "folder", PAGE_FILESYSTEM, True, None)
|
|
add_hw_option(_("Smartcard"), "device_serial", PAGE_SMARTCARD,
|
|
True, None)
|
|
add_hw_option(_("USB Redirection"), "device_usb", PAGE_USBREDIR,
|
|
True, None)
|
|
add_hw_option(_("TPM"), "device_cpu", PAGE_TPM,
|
|
True, None)
|
|
add_hw_option(_("RNG"), "system-run", PAGE_RNG, True, None)
|
|
add_hw_option(_("Panic Notifier"), "system-run", PAGE_PANIC,
|
|
bool(DevicePanic.get_models(self.vm.get_xmlobj())),
|
|
_("Not supported for this hypervisor/libvirt/arch combination."))
|
|
add_hw_option(_("VirtIO VSOCK"), "network-idle", PAGE_VSOCK,
|
|
self.vm.is_hvm(),
|
|
_("Not supported for this hypervisor/libvirt/arch combination."))
|
|
|
|
|
|
def _reset_state(self):
|
|
# Hide all notebook pages, otherwise the wizard window is as large
|
|
# as the largest page
|
|
for page in range(self.widget("create-pages").get_n_pages()):
|
|
widget = self.widget("create-pages").get_nth_page(page)
|
|
widget.hide()
|
|
|
|
self._set_hw_selection(0)
|
|
|
|
# Storage params
|
|
self.widget("storage-devtype").set_active(0)
|
|
self.widget("storage-devtype").emit("changed")
|
|
self.addstorage.reset_state()
|
|
|
|
|
|
# Network init
|
|
newmac = DeviceInterface.generate_mac(self.conn.get_backend())
|
|
self.widget("mac-address").set_active(bool(newmac))
|
|
self.widget("create-mac-address").set_text(newmac)
|
|
self._change_macaddr_use()
|
|
|
|
self._netlist.reset_state()
|
|
|
|
netmodel = self.widget("net-model")
|
|
self.populate_network_model_combo(self.vm, netmodel)
|
|
|
|
|
|
# Char parameters
|
|
self.widget("char-path").set_text("")
|
|
self.widget("char-channel").set_text("")
|
|
self.widget("char-auto-socket").set_active(True)
|
|
|
|
|
|
# RNG params
|
|
default_rng = "/dev/random"
|
|
if self.conn.support.conn_rng_urandom():
|
|
default_rng = "/dev/urandom"
|
|
self.widget("rng-device").set_text(default_rng)
|
|
|
|
|
|
# Remaining devices
|
|
self._fsdetails.reset_state()
|
|
self._gfxdetails.reset_state()
|
|
self._vsockdetails.reset_state()
|
|
self._tpmdetails.reset_state()
|
|
|
|
@staticmethod
|
|
def change_config_helper(define_func, define_args, vm, err,
|
|
devobj=None, hotplug_args=None):
|
|
"""
|
|
UI helper that handles the logic and reports errors for the
|
|
requested VM define and hotplug changes
|
|
|
|
Used here and in details.py
|
|
"""
|
|
hotplug_args = hotplug_args or {}
|
|
|
|
# Persistent config change
|
|
try:
|
|
if devobj:
|
|
# Device XML editing
|
|
define_func(devobj=devobj, do_hotplug=False, **define_args)
|
|
else:
|
|
# Guest XML editing
|
|
define_func(**define_args)
|
|
except Exception as e:
|
|
err.show_err((_("Error changing VM configuration: %s") %
|
|
str(e)))
|
|
return False
|
|
|
|
if not vm.is_active():
|
|
return True
|
|
|
|
# Hotplug change
|
|
hotplug_err = None
|
|
did_hotplug = False
|
|
try:
|
|
if devobj:
|
|
define_func(devobj=devobj, do_hotplug=True, **define_args)
|
|
did_hotplug = True
|
|
elif hotplug_args:
|
|
did_hotplug = True
|
|
vm.hotplug(**hotplug_args)
|
|
except Exception as e:
|
|
did_hotplug = True
|
|
log.debug("Hotplug failed: %s", str(e))
|
|
hotplug_err = ((str(e), "".join(traceback.format_exc())))
|
|
|
|
if did_hotplug and not hotplug_err:
|
|
return True
|
|
|
|
msg = _("These changes will take effect after "
|
|
"the next guest shutdown.")
|
|
dtype = (hotplug_err and
|
|
Gtk.MessageType.WARNING or Gtk.MessageType.INFO)
|
|
hotplug_msg = ""
|
|
if hotplug_err:
|
|
hotplug_msg += (hotplug_err[0] + "\n\n" +
|
|
hotplug_err[1] + "\n")
|
|
|
|
err.show_err(msg,
|
|
details=hotplug_msg,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
dialog_type=dtype)
|
|
|
|
return True
|
|
|
|
|
|
#####################
|
|
# Pretty UI helpers #
|
|
#####################
|
|
|
|
@staticmethod
|
|
def char_recommended_types(char_class):
|
|
if char_class.XML_NAME == "console":
|
|
return [DeviceSerial.TYPE_PTY]
|
|
|
|
ret = [DeviceSerial.TYPE_PTY,
|
|
DeviceSerial.TYPE_FILE,
|
|
DeviceSerial.TYPE_UNIX]
|
|
if char_class.XML_NAME == "channel":
|
|
ret = [DeviceSerial.TYPE_SPICEVMC,
|
|
DeviceSerial.TYPE_SPICEPORT] + ret
|
|
return ret
|
|
|
|
@staticmethod
|
|
def char_pretty_channel_name(val):
|
|
labels = {
|
|
DeviceChannel.CHANNEL_NAME_SPICE: "spice",
|
|
DeviceChannel.CHANNEL_NAME_QEMUGA: "qemu-ga",
|
|
DeviceChannel.CHANNEL_NAME_LIBGUESTFS: "libguestfs",
|
|
DeviceChannel.CHANNEL_NAME_SPICE_WEBDAV: "spice-webdav",
|
|
}
|
|
return labels.get(val, None)
|
|
|
|
@staticmethod
|
|
def char_pretty_type(val):
|
|
"""
|
|
Return a human readable description of the passed char type
|
|
"""
|
|
labels = {
|
|
DeviceSerial.TYPE_PTY: _("Pseudo TTY"),
|
|
DeviceSerial.TYPE_FILE: _("Output to a file"),
|
|
DeviceSerial.TYPE_TCP: _("TCP net console"),
|
|
DeviceSerial.TYPE_UDP: _("UDP net console"),
|
|
DeviceSerial.TYPE_UNIX: _("UNIX socket"),
|
|
DeviceSerial.TYPE_SPICEVMC: _("Spice agent"),
|
|
DeviceSerial.TYPE_SPICEPORT: _("Spice port"),
|
|
}
|
|
return labels.get(val, val)
|
|
|
|
@staticmethod
|
|
def controller_recommended_types():
|
|
return [DeviceController.TYPE_SCSI,
|
|
DeviceController.TYPE_USB,
|
|
DeviceController.TYPE_VIRTIOSERIAL,
|
|
DeviceController.TYPE_CCID]
|
|
|
|
@staticmethod
|
|
def controller_pretty_type(val):
|
|
labels = {
|
|
DeviceController.TYPE_IDE: _("IDE"),
|
|
DeviceController.TYPE_FDC: _("Floppy"),
|
|
DeviceController.TYPE_SCSI: _("SCSI"),
|
|
DeviceController.TYPE_SATA: _("SATA"),
|
|
DeviceController.TYPE_VIRTIOSERIAL: _("VirtIO Serial"),
|
|
DeviceController.TYPE_USB: _("USB"),
|
|
DeviceController.TYPE_PCI: _("PCI"),
|
|
DeviceController.TYPE_CCID: _("CCID"),
|
|
DeviceController.TYPE_XENBUS: _("xenbus"),
|
|
}
|
|
return labels.get(val, val)
|
|
|
|
@staticmethod
|
|
def controller_pretty_desc(dev):
|
|
if dev.type == DeviceController.TYPE_SCSI:
|
|
if dev.model == "virtio-scsi":
|
|
return _("VirtIO SCSI")
|
|
if dev.type == DeviceController.TYPE_PCI:
|
|
if dev.model == "pcie-root":
|
|
return _("PCIe")
|
|
return vmmAddHardware.controller_pretty_type(dev.type)
|
|
|
|
@staticmethod
|
|
def disk_old_recommended_buses(guest):
|
|
ret = []
|
|
if guest.os.is_hvm() or guest.conn.is_test():
|
|
if not guest.os.is_q35():
|
|
ret.append("ide")
|
|
ret.append("sata")
|
|
ret.append("fdc")
|
|
ret.append("scsi")
|
|
ret.append("usb")
|
|
|
|
if guest.type in ["qemu", "kvm", "test"]:
|
|
ret.append("sd")
|
|
ret.append("virtio")
|
|
|
|
if guest.conn.is_xen() or guest.conn.is_test():
|
|
ret.append("xen")
|
|
|
|
return ret
|
|
|
|
@staticmethod
|
|
def disk_recommended_buses(guest, domcaps, devtype):
|
|
# try to get supported disk bus types from domain capabilities
|
|
if "bus" in domcaps.devices.disk.enum_names():
|
|
buses = domcaps.devices.disk.get_enum("bus").get_values()
|
|
else:
|
|
buses = vmmAddHardware.disk_old_recommended_buses(guest)
|
|
|
|
bus_map = {
|
|
"disk": ["ide", "sata", "scsi", "sd", "usb", "virtio", "xen"],
|
|
"floppy": ["fdc"],
|
|
"cdrom": ["ide", "sata", "scsi", "usb"],
|
|
"lun": ["scsi"],
|
|
}
|
|
return [bus for bus in buses if bus in bus_map.get(devtype, [])]
|
|
|
|
@staticmethod
|
|
def disk_pretty_bus(bus):
|
|
bus_mappings = {
|
|
"ide": _("IDE"),
|
|
"sata": _("SATA"),
|
|
"scsi": _("SCSI"),
|
|
"sd": _("SD"),
|
|
"usb": _("USB"),
|
|
"virtio": _("VirtIO"),
|
|
"xen": _("Xen"),
|
|
}
|
|
return bus_mappings.get(bus, bus)
|
|
|
|
@staticmethod
|
|
def panic_pretty_model(val):
|
|
labels = {
|
|
DevicePanic.MODEL_ISA: _("ISA"),
|
|
DevicePanic.MODEL_PSERIES: _("pSeries"),
|
|
DevicePanic.MODEL_HYPERV: _("Hyper-V"),
|
|
DevicePanic.MODEL_S390: _("s390"),
|
|
}
|
|
return labels.get(val, val)
|
|
|
|
@staticmethod
|
|
def rng_pretty_type(val):
|
|
labels = {
|
|
DeviceRng.TYPE_RANDOM: _("Random"),
|
|
DeviceRng.TYPE_EGD: _("Entropy Gathering Daemon"),
|
|
DeviceRng.TYPE_BUILTIN: _("Builtin RNG"),
|
|
}
|
|
return labels.get(val, val)
|
|
|
|
@staticmethod
|
|
def sound_recommended_models(_guest):
|
|
return ["ich6", "ich9", "ac97"]
|
|
|
|
@staticmethod
|
|
def sound_pretty_model(model):
|
|
ret = model.upper()
|
|
if model in ["ich6", "ich9"]:
|
|
ret = "HDA (%s)" % model.upper()
|
|
return ret
|
|
|
|
@staticmethod
|
|
def watchdog_pretty_action(val):
|
|
labels = {
|
|
DeviceWatchdog.ACTION_RESET: _("Forcefully reset the guest"),
|
|
DeviceWatchdog.ACTION_SHUTDOWN: _("Gracefully shutdown the guest"),
|
|
DeviceWatchdog.ACTION_POWEROFF: _("Forcefully power off the guest"),
|
|
DeviceWatchdog.ACTION_PAUSE: _("Pause the guest"),
|
|
DeviceWatchdog.ACTION_NONE: _("No action"),
|
|
DeviceWatchdog.ACTION_DUMP: _("Dump guest memory core"),
|
|
}
|
|
return labels.get(val, val)
|
|
|
|
@staticmethod
|
|
def input_pretty_name(typ, bus):
|
|
if typ == DeviceInput.TYPE_TABLET and bus == DeviceInput.BUS_USB:
|
|
return _("EvTouch USB Graphics Tablet")
|
|
|
|
typ_labels = {
|
|
DeviceInput.TYPE_KEYBOARD: _("Keyboard"),
|
|
DeviceInput.TYPE_MOUSE: _("Mouse"),
|
|
DeviceInput.TYPE_TABLET: _("Tablet"),
|
|
}
|
|
|
|
bus_labels = {
|
|
DeviceInput.BUS_PS2: _("PS/2"),
|
|
DeviceInput.BUS_USB: _("USB"),
|
|
DeviceInput.BUS_VIRTIO: _("VirtIO"),
|
|
DeviceInput.BUS_XEN: _("Xen"),
|
|
}
|
|
|
|
bus_label = bus_labels.get(bus, bus)
|
|
typ_label = typ_labels.get(typ, typ)
|
|
# translators: Examples: 'USB Mouse', 'PS/2 Keyboard'
|
|
ret = _("%(input_bus)s %(input_type)s") % {
|
|
"input_bus": bus_label, "input_type": typ_label}
|
|
return ret
|
|
|
|
@staticmethod
|
|
def interface_recommended_models(guest):
|
|
if not guest.os.is_hvm():
|
|
return []
|
|
|
|
ret = []
|
|
if guest.type in ["kvm", "qemu", "vz", "test"]:
|
|
ret.append("virtio")
|
|
if guest.os.is_x86():
|
|
if guest.os.is_q35():
|
|
ret.append("e1000e")
|
|
else:
|
|
ret.append("rtl8139")
|
|
ret.append("e1000")
|
|
if guest.type in ["xen", "test"]:
|
|
ret.append("netfront")
|
|
|
|
ret.sort()
|
|
return ret
|
|
|
|
@staticmethod
|
|
def redirdev_pretty_type(typ):
|
|
if typ == "tcp":
|
|
return "TCP"
|
|
if typ == "spicevmc":
|
|
return "SpiceVMC"
|
|
return typ and typ.capitalize()
|
|
|
|
@staticmethod
|
|
def video_recommended_models(guest):
|
|
if guest.conn.is_xen():
|
|
return ["xen", "vga"]
|
|
if guest.conn.is_qemu() or guest.conn.is_test():
|
|
return ["vga", "bochs", "qxl", "virtio", "ramfb", "none"]
|
|
return []
|
|
|
|
@staticmethod
|
|
def video_pretty_model(model):
|
|
if model in ["qxl", "vmvga", "vga"]:
|
|
return model.upper()
|
|
return model.capitalize()
|
|
|
|
@staticmethod
|
|
def hostdev_pretty_name(hostdev):
|
|
def dehex(val):
|
|
if val.startswith("0x"):
|
|
val = val[2:]
|
|
return val
|
|
|
|
def safeint(val, fmt="%.3d"):
|
|
try:
|
|
int(val)
|
|
except Exception: # pragma: no cover
|
|
return str(val)
|
|
return fmt % int(val)
|
|
|
|
label = hostdev.type.upper()
|
|
|
|
if hostdev.vendor and hostdev.product:
|
|
label += " %s:%s" % (dehex(hostdev.vendor), dehex(hostdev.product))
|
|
|
|
elif hostdev.bus and hostdev.device:
|
|
label += " %s:%s" % (safeint(hostdev.bus), safeint(hostdev.device))
|
|
|
|
elif (hostdev.bus and hostdev.slot and
|
|
hostdev.function and hostdev.domain):
|
|
label += (" %s:%s:%s.%s" %
|
|
(dehex(hostdev.domain), dehex(hostdev.bus),
|
|
dehex(hostdev.slot), dehex(hostdev.function)))
|
|
|
|
elif hostdev.uuid:
|
|
label += " %s" % (str(hostdev.uuid))
|
|
|
|
return label
|
|
|
|
|
|
#########################
|
|
# UI init/reset helpers #
|
|
#########################
|
|
|
|
def _build_disk_device_combo(self):
|
|
target_list = self.widget("storage-devtype")
|
|
# [device, icon, label]
|
|
target_model = Gtk.ListStore(str, str, str)
|
|
target_list.set_model(target_model)
|
|
icon = Gtk.CellRendererPixbuf()
|
|
icon.set_property("stock-size", Gtk.IconSize.BUTTON)
|
|
target_list.pack_start(icon, False)
|
|
target_list.add_attribute(icon, 'icon-name', 1)
|
|
text = Gtk.CellRendererText()
|
|
text.set_property("xpad", 6)
|
|
target_list.pack_start(text, True)
|
|
target_list.add_attribute(text, 'text', 2)
|
|
target_model.append([DeviceDisk.DEVICE_DISK,
|
|
"drive-harddisk", _("Disk device")])
|
|
target_model.append([DeviceDisk.DEVICE_CDROM,
|
|
"media-optical", _("CDROM device")])
|
|
target_model.append([DeviceDisk.DEVICE_FLOPPY,
|
|
"media-floppy", _("Floppy device")])
|
|
if self.conn.is_qemu() or self.conn.is_test():
|
|
target_model.append([DeviceDisk.DEVICE_LUN,
|
|
"drive-harddisk", _("LUN Passthrough")])
|
|
target_list.set_active(0)
|
|
|
|
@staticmethod
|
|
def build_disk_bus_combo(_vm, combo):
|
|
uiutil.build_simple_combo(combo, [])
|
|
|
|
@staticmethod
|
|
def populate_disk_bus_combo(vm, devtype, model):
|
|
domcaps = vm.get_domain_capabilities()
|
|
buses = vmmAddHardware.disk_recommended_buses(vm.xmlobj, domcaps, devtype)
|
|
|
|
model.clear()
|
|
for bus in buses:
|
|
model.append([bus, vmmAddHardware.disk_pretty_bus(bus)])
|
|
|
|
|
|
@staticmethod
|
|
def populate_network_model_combo(vm, combo):
|
|
model = combo.get_model()
|
|
model.clear()
|
|
|
|
# [xml value, label]
|
|
model.append([None, _("Hypervisor default")])
|
|
for netmodel in vmmAddHardware.interface_recommended_models(vm.xmlobj):
|
|
model.append([netmodel, netmodel])
|
|
|
|
uiutil.set_list_selection(
|
|
combo, DeviceInterface.default_model(vm.xmlobj))
|
|
|
|
@staticmethod
|
|
def build_network_model_combo(vm, combo):
|
|
uiutil.build_simple_combo(combo, [])
|
|
vmmAddHardware.populate_network_model_combo(vm, combo)
|
|
|
|
|
|
def _build_input_combo(self):
|
|
devices = [
|
|
(DeviceInput.TYPE_TABLET, DeviceInput.BUS_USB),
|
|
(DeviceInput.TYPE_MOUSE, DeviceInput.BUS_USB),
|
|
(DeviceInput.TYPE_KEYBOARD, DeviceInput.BUS_USB),
|
|
(DeviceInput.TYPE_KEYBOARD, DeviceInput.BUS_VIRTIO),
|
|
(DeviceInput.TYPE_TABLET, DeviceInput.BUS_VIRTIO),
|
|
]
|
|
|
|
cvals = [((t, b), vmmAddHardware.input_pretty_name(t, b))
|
|
for t, b in devices]
|
|
uiutil.build_simple_combo(self.widget("input-type"), cvals)
|
|
|
|
|
|
@staticmethod
|
|
def build_sound_combo(vm, combo):
|
|
values = []
|
|
for m in vmmAddHardware.sound_recommended_models(vm.xmlobj):
|
|
values.append([m, vmmAddHardware.sound_pretty_model(m)])
|
|
|
|
default = DeviceSound.default_model(vm.xmlobj)
|
|
uiutil.build_simple_combo(combo, values, default_value=default)
|
|
|
|
|
|
def _build_hostdev_treeview(self):
|
|
host_dev = self.widget("host-device")
|
|
# [ xmlobj, label, sensitive, tooltip]
|
|
host_dev_model = Gtk.ListStore(object, str, bool, str)
|
|
host_dev.set_model(host_dev_model)
|
|
host_col = Gtk.TreeViewColumn()
|
|
text = Gtk.CellRendererText()
|
|
host_col.pack_start(text, True)
|
|
host_col.add_attribute(text, 'text', 1)
|
|
host_col.add_attribute(text, 'sensitive', 2)
|
|
host_dev.set_tooltip_column(3)
|
|
host_dev_model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
|
|
host_dev.append_column(host_col)
|
|
|
|
|
|
def _hostdev_row_selected_cb(self, selection):
|
|
model, treeiter = selection.get_selected()
|
|
sensitive = treeiter and model[treeiter][2] or False
|
|
self.widget("create-finish").set_sensitive(sensitive)
|
|
|
|
|
|
def _populate_hostdev_model(self, devtype):
|
|
devlist = self.widget("host-device")
|
|
model = devlist.get_model()
|
|
model.clear()
|
|
|
|
devs = self.conn.filter_nodedevs(devtype)
|
|
netdevs = self.conn.filter_nodedevs("net")
|
|
for dev in devs:
|
|
if dev.xmlobj.is_usb_linux_root_hub():
|
|
continue
|
|
if dev.xmlobj.is_pci_bridge():
|
|
continue
|
|
prettyname = dev.pretty_name()
|
|
|
|
if devtype == "pci":
|
|
for subdev in netdevs:
|
|
if dev.xmlobj.name == subdev.xmlobj.parent:
|
|
prettyname += " (%s)" % subdev.pretty_name()
|
|
|
|
# parent device names are appended with mdev names in
|
|
# libvirt 7.8.0
|
|
if devtype == "mdev" and len(prettyname) <= 41:
|
|
for parentdev in self.conn.list_nodedevs():
|
|
if dev.xmlobj.parent == parentdev.xmlobj.name:
|
|
prettyname = "%s %s" % (
|
|
parentdev.pretty_name(), prettyname)
|
|
|
|
tooltip = None
|
|
sensitive = dev.is_active()
|
|
if not sensitive:
|
|
tooltip = _("%s is not active in the host system.\n"
|
|
"Please start the mdev in the host system before "
|
|
"adding it to the guest.") % prettyname
|
|
model.append([dev.xmlobj, prettyname, sensitive, tooltip])
|
|
|
|
if len(model) == 0:
|
|
model.append([None, _("No Devices Available"), False, None])
|
|
|
|
uiutil.set_list_selection_by_number(devlist, 0)
|
|
|
|
devlist.get_selection().connect(
|
|
"changed", self._hostdev_row_selected_cb)
|
|
devlist.get_selection().emit("changed")
|
|
|
|
|
|
@staticmethod
|
|
def build_video_combo(vm, combo):
|
|
values = []
|
|
for m in vmmAddHardware.video_recommended_models(vm.xmlobj):
|
|
values.append([m, vmmAddHardware.video_pretty_model(m)])
|
|
if not values:
|
|
values.append([None, _("Hypervisor default")])
|
|
default = DeviceVideo.default_model(vm.xmlobj)
|
|
uiutil.build_simple_combo(combo, values, default_value=default)
|
|
|
|
|
|
def _build_char_target_type_combo(self):
|
|
values = []
|
|
if self.conn.is_qemu():
|
|
values.append(["virtio", "VirtIO"])
|
|
else:
|
|
values.append([None, _("Hypervisor default")])
|
|
uiutil.build_simple_combo(self.widget("char-target-type"), values)
|
|
|
|
def _build_char_target_name_combo(self):
|
|
values = []
|
|
for n in DeviceChannel.CHANNEL_NAMES:
|
|
values.append([n, n])
|
|
uiutil.build_simple_combo(self.widget("char-target-name"), values)
|
|
|
|
def _populate_char_device_type_combo(self):
|
|
char_class = self._get_char_class()
|
|
model = self.widget("char-device-type").get_model()
|
|
model.clear()
|
|
|
|
for t in vmmAddHardware.char_recommended_types(char_class):
|
|
model.append([t, vmmAddHardware.char_pretty_type(t) + " (%s)" % t])
|
|
uiutil.set_list_selection(self.widget("char-device-type"), "pty")
|
|
|
|
|
|
@staticmethod
|
|
def build_watchdogmodel_combo(_vm, combo):
|
|
values = []
|
|
for m in DeviceWatchdog.MODELS:
|
|
values.append([m, m.upper()])
|
|
uiutil.build_simple_combo(combo, values, default_value=DeviceWatchdog.MODEL_I6300)
|
|
|
|
@staticmethod
|
|
def build_watchdogaction_combo(_vm, combo):
|
|
values = []
|
|
for m in DeviceWatchdog.ACTIONS:
|
|
values.append([m, vmmAddHardware.watchdog_pretty_action(m)])
|
|
uiutil.build_simple_combo(combo, values, default_value=DeviceWatchdog.ACTION_RESET)
|
|
|
|
|
|
@staticmethod
|
|
def build_smartcard_mode_combo(_vm, combo):
|
|
values = [
|
|
["passthrough", _("Passthrough")],
|
|
["host", _("Host")],
|
|
]
|
|
uiutil.build_simple_combo(combo, values)
|
|
|
|
|
|
def _build_redir_type_combo(self):
|
|
values = [["spicevmc", _("Spice channel")]]
|
|
uiutil.build_simple_combo(self.widget("usbredir-list"), values)
|
|
|
|
|
|
def _build_panic_model_combo(self):
|
|
values = []
|
|
for m in DevicePanic.get_models(self.vm.get_xmlobj()):
|
|
values.append([m, vmmAddHardware.panic_pretty_model(m)])
|
|
|
|
default = DevicePanic.get_default_model(self.vm.get_xmlobj())
|
|
uiutil.build_simple_combo(self.widget("panic-model"), values, default_value=default)
|
|
|
|
|
|
def _build_controller_type_combo(self):
|
|
values = []
|
|
for t in vmmAddHardware.controller_recommended_types():
|
|
values.append([t, vmmAddHardware.controller_pretty_type(t)])
|
|
|
|
uiutil.build_simple_combo(self.widget("controller-type"), values,
|
|
default_value=DeviceController.TYPE_SCSI)
|
|
|
|
@staticmethod
|
|
def populate_controller_model_combo(combo, controller_type):
|
|
model = combo.get_model()
|
|
model.clear()
|
|
|
|
rows = []
|
|
if controller_type == DeviceController.TYPE_USB:
|
|
rows.append(["usb3", _("USB 3")])
|
|
rows.append(["ich9-ehci1", _("USB 2")])
|
|
elif controller_type == DeviceController.TYPE_SCSI:
|
|
rows.append(["virtio-scsi", _("VirtIO SCSI")])
|
|
rows.append([None, _("Hypervisor default")])
|
|
|
|
for row in rows:
|
|
model.append(row)
|
|
uiutil.set_list_selection(combo, rows[0][0])
|
|
|
|
|
|
|
|
#########################
|
|
# Internal misc helpers #
|
|
#########################
|
|
|
|
def _get_char_class(self):
|
|
row = self._get_hw_selection()
|
|
label = "serial"
|
|
|
|
if row:
|
|
label = row[5]
|
|
|
|
if label == "parallel":
|
|
return DeviceParallel
|
|
elif label == "channel":
|
|
return DeviceChannel
|
|
elif label == "console":
|
|
return DeviceConsole
|
|
return DeviceSerial
|
|
|
|
def _set_hw_selection(self, page):
|
|
uiutil.set_list_selection_by_number(self.widget("hw-list"), page)
|
|
|
|
def _get_hw_selection(self):
|
|
return uiutil.get_list_selected_row(self.widget("hw-list"))
|
|
|
|
def _set_error_page(self, msg=None):
|
|
self.widget("top-pages").set_current_page(1)
|
|
self.widget("error-label").set_text(msg or "Hardware selection error.")
|
|
self.widget("create-finish").set_sensitive(False)
|
|
|
|
|
|
################
|
|
# UI listeners #
|
|
################
|
|
|
|
def _hw_selected_cb(self, src):
|
|
self.widget("create-finish").set_sensitive(True)
|
|
self._xmleditor.reset_state()
|
|
|
|
row = self._get_hw_selection()
|
|
if not row or not row[3]:
|
|
self._set_error_page(row and row[4] or None)
|
|
return
|
|
|
|
page = row[2]
|
|
|
|
if page == PAGE_CHAR:
|
|
# Need to do this here, since we share the char page between
|
|
# multiple different HW options
|
|
self._populate_char_device_type_combo()
|
|
self.widget("char-device-type").emit("changed")
|
|
self.widget("char-target-name").emit("changed")
|
|
|
|
if page == PAGE_HOSTDEV:
|
|
# Need to do this here, since we share the hostdev page
|
|
# between different HW options
|
|
row = self._get_hw_selection()
|
|
devtype = "usb_device"
|
|
if row and row[5] == "pci":
|
|
devtype = "pci"
|
|
if row and row[5] == "mdev":
|
|
devtype = "mdev"
|
|
self._populate_hostdev_model(devtype)
|
|
|
|
if page == PAGE_CONTROLLER:
|
|
# We need to trigger this as it can desensitive 'finish'
|
|
self.widget("controller-type").emit("changed")
|
|
|
|
self._set_page_title(page)
|
|
self.widget("create-pages").get_nth_page(page).show()
|
|
self.widget("create-pages").set_current_page(page)
|
|
self.widget("top-pages").set_current_page(0)
|
|
|
|
def _dev_to_title(self, page):
|
|
if page == PAGE_DISK:
|
|
return _("Storage")
|
|
if page == PAGE_CONTROLLER:
|
|
return _("Controller")
|
|
if page == PAGE_NETWORK:
|
|
return _("Network")
|
|
if page == PAGE_INPUT:
|
|
return _("Input")
|
|
if page == PAGE_GRAPHICS:
|
|
return _("Graphics")
|
|
if page == PAGE_SOUND:
|
|
return _("Sound")
|
|
if page == PAGE_VIDEO:
|
|
return _("Video Device")
|
|
if page == PAGE_WATCHDOG:
|
|
return _("Watchdog Device")
|
|
if page == PAGE_FILESYSTEM:
|
|
return _("Filesystem Passthrough")
|
|
if page == PAGE_SMARTCARD:
|
|
return _("Smartcard")
|
|
if page == PAGE_USBREDIR:
|
|
return _("USB Redirection")
|
|
if page == PAGE_TPM:
|
|
return _("TPM")
|
|
if page == PAGE_RNG:
|
|
return _("Random Number Generator")
|
|
if page == PAGE_PANIC:
|
|
return _("Panic Notifier")
|
|
if page == PAGE_VSOCK:
|
|
return _("VM Sockets")
|
|
|
|
if page == PAGE_CHAR:
|
|
devclass = self._get_char_class()(self.conn.get_backend())
|
|
return _("%s Device") % devclass.DEVICE_TYPE.capitalize()
|
|
if page == PAGE_HOSTDEV:
|
|
row = self._get_hw_selection()
|
|
if row and row[5] == "pci":
|
|
return _("PCI Device")
|
|
if row and row[5] == "mdev":
|
|
return _("MDEV Device")
|
|
return _("USB Device")
|
|
|
|
raise RuntimeError("Unknown page %s" % page) # pragma: no cover
|
|
|
|
def _set_page_title(self, page):
|
|
title = self._dev_to_title(page)
|
|
self.widget("page-title-label").set_markup(title)
|
|
|
|
def _xmleditor_xml_requested_cb(self, src):
|
|
dev = self._build_device(check_xmleditor=False)
|
|
self._xmleditor.set_xml(dev and dev.get_xml() or "")
|
|
|
|
|
|
#########################
|
|
# Device page listeners #
|
|
#########################
|
|
|
|
def _refresh_disk_bus(self, devtype):
|
|
widget = self.widget("storage-bustype")
|
|
model = widget.get_model()
|
|
self.populate_disk_bus_combo(self.vm, devtype, model)
|
|
|
|
# By default, select bus of the first disk assigned to the VM
|
|
default_bus = None
|
|
for i in self.vm.xmlobj.devices.disk:
|
|
if i.device == devtype:
|
|
default_bus = i.bus
|
|
break
|
|
|
|
if default_bus:
|
|
uiutil.set_list_selection(widget, default_bus)
|
|
elif len(model) > 0:
|
|
widget.set_active(0)
|
|
|
|
def _change_storage_devtype(self, ignore):
|
|
devtype = uiutil.get_list_selection(
|
|
self.widget("storage-devtype"))
|
|
self._refresh_disk_bus(devtype)
|
|
|
|
allow_create = devtype not in ["cdrom", "floppy"]
|
|
self.addstorage.widget("storage-create-box").set_sensitive(
|
|
allow_create)
|
|
if not allow_create:
|
|
self.addstorage.widget("storage-select").set_active(True)
|
|
|
|
def _storage_bus_changed_cb(self, src):
|
|
bus = uiutil.get_list_selection(
|
|
self.widget("storage-bustype"))
|
|
self.addstorage.set_disk_bus(bus)
|
|
|
|
def _change_macaddr_use(self, ignore=None):
|
|
if self.widget("mac-address").get_active():
|
|
self.widget("create-mac-address").set_sensitive(True)
|
|
else:
|
|
self.widget("create-mac-address").set_sensitive(False)
|
|
|
|
def _change_char_auto_socket(self, src):
|
|
if not src.get_visible():
|
|
return
|
|
|
|
doshow = not src.get_active()
|
|
uiutil.set_grid_row_visible(self.widget("char-path-label"), doshow)
|
|
|
|
def _change_char_target_name(self, src):
|
|
if not src.get_visible():
|
|
return
|
|
|
|
text = src.get_child().get_text()
|
|
settype = None
|
|
if text == DeviceChannel.CHANNEL_NAME_SPICE:
|
|
settype = "spicevmc"
|
|
elif text == DeviceChannel.CHANNEL_NAME_SPICE_WEBDAV:
|
|
settype = "spiceport"
|
|
self.widget("char-channel").set_text(text)
|
|
elif (text == DeviceChannel.CHANNEL_NAME_QEMUGA or
|
|
text == DeviceChannel.CHANNEL_NAME_LIBGUESTFS):
|
|
settype = "unix"
|
|
if settype:
|
|
uiutil.set_list_selection(
|
|
self.widget("char-device-type"), settype)
|
|
|
|
def _change_char_device_type(self, src):
|
|
devtype = uiutil.get_list_selection(src)
|
|
if devtype is None:
|
|
return
|
|
|
|
char_class = self._get_char_class()
|
|
dev = char_class(self.conn.get_backend())
|
|
dev.type = devtype
|
|
|
|
ischan = dev.DEVICE_TYPE == "channel"
|
|
iscon = dev.DEVICE_TYPE == "console"
|
|
show_auto = devtype == "unix" and ischan
|
|
|
|
supports_path = [dev.TYPE_FILE, dev.TYPE_UNIX,
|
|
dev.TYPE_DEV, dev.TYPE_PIPE]
|
|
supports_channel = [dev.TYPE_SPICEPORT]
|
|
|
|
uiutil.set_grid_row_visible(self.widget("char-path-label"),
|
|
devtype in supports_path)
|
|
uiutil.set_grid_row_visible(self.widget("char-channel-label"),
|
|
devtype in supports_channel)
|
|
|
|
uiutil.set_grid_row_visible(
|
|
self.widget("char-target-name-label"), ischan)
|
|
uiutil.set_grid_row_visible(
|
|
self.widget("char-target-type-label"), iscon)
|
|
uiutil.set_grid_row_visible(
|
|
self.widget("char-auto-socket-label"), show_auto)
|
|
self.widget("char-auto-socket").emit("toggled")
|
|
|
|
def _change_usbredir_type(self, src):
|
|
pass
|
|
|
|
def _change_controller_type(self, src):
|
|
ignore = src
|
|
combo = self.widget("controller-model")
|
|
|
|
def show_tooltip(model_tooltip, show):
|
|
vmname = self.vm.get_name()
|
|
tooltip = (_("%s already has a USB controller attached.\n"
|
|
"Adding more than one USB controller is not supported.\n"
|
|
"You can change the USB controller type in the VM details screen.")
|
|
% vmname)
|
|
model_tooltip.set_visible(show)
|
|
model_tooltip.set_tooltip_text(tooltip)
|
|
|
|
controller_type = uiutil.get_list_selection(
|
|
self.widget("controller-type"))
|
|
combo.set_sensitive(True)
|
|
model_tooltip = self.widget("controller-tooltip")
|
|
show_tooltip(model_tooltip, False)
|
|
|
|
controllers = self.vm.xmlobj.devices.controller
|
|
if controller_type == DeviceController.TYPE_USB:
|
|
usb_controllers = [x for x in controllers if
|
|
(x.type == DeviceController.TYPE_USB)]
|
|
if (len(usb_controllers) == 0):
|
|
self.widget("create-finish").set_sensitive(True)
|
|
elif (len(usb_controllers) == 1 and
|
|
usb_controllers[0].model == "none"):
|
|
self._remove_usb_controller = usb_controllers[0]
|
|
self.widget("create-finish").set_sensitive(True)
|
|
else:
|
|
show_tooltip(model_tooltip, True)
|
|
self.widget("create-finish").set_sensitive(False)
|
|
else:
|
|
self.widget("create-finish").set_sensitive(True)
|
|
|
|
self.populate_controller_model_combo(combo, controller_type)
|
|
uiutil.set_grid_row_visible(combo, len(combo.get_model()) > 1)
|
|
|
|
|
|
######################
|
|
# Add device methods #
|
|
######################
|
|
|
|
def _setup_device(self, asyncjob, dev):
|
|
if dev.DEVICE_TYPE != "disk":
|
|
return
|
|
|
|
poolname = None
|
|
if (dev.wants_storage_creation() and
|
|
dev.get_parent_pool()):
|
|
poolname = dev.get_parent_pool().name()
|
|
|
|
log.debug("Running build_storage() for device=%s", dev)
|
|
dev.build_storage(meter=asyncjob.get_meter())
|
|
log.debug("build_storage() complete")
|
|
|
|
if poolname:
|
|
try:
|
|
pool = self.conn.get_pool_by_name(poolname)
|
|
self.idle_add(pool.refresh)
|
|
except Exception: # pragma: no cover
|
|
log.debug("Error looking up pool=%s for refresh after "
|
|
"storage creation.", poolname, exc_info=True)
|
|
|
|
|
|
def _add_device(self, dev):
|
|
xml = dev.get_xml()
|
|
log.debug("Adding device:\n%s", xml)
|
|
|
|
if self._remove_usb_controller:
|
|
kwargs = {}
|
|
kwargs["model"] = self._selected_model
|
|
|
|
self.change_config_helper(self.vm.define_controller,
|
|
kwargs, self.vm, self.err,
|
|
devobj=self._remove_usb_controller)
|
|
|
|
self._remove_usb_controller = None
|
|
self._selected_model = None
|
|
|
|
return
|
|
|
|
controller = getattr(dev, "vmm_controller", None)
|
|
if controller is not None:
|
|
log.debug("Adding controller:\n%s",
|
|
controller.get_xml())
|
|
# Hotplug device
|
|
attach_err = False
|
|
try:
|
|
if controller is not None:
|
|
self.vm.attach_device(controller)
|
|
self.vm.attach_device(dev)
|
|
except Exception as e:
|
|
log.debug("Device could not be hotplugged: %s", str(e))
|
|
attach_err = (str(e), "".join(traceback.format_exc()))
|
|
|
|
if attach_err:
|
|
res = self.err.show_err(
|
|
_("Are you sure you want to add this device?"),
|
|
details=(attach_err[0] + "\n\n" + attach_err[1]),
|
|
text2=(
|
|
_("This device could not be attached to the running machine. "
|
|
"Would you like to make the device available after the "
|
|
"next guest shutdown?")),
|
|
dialog_type=Gtk.MessageType.WARNING,
|
|
buttons=Gtk.ButtonsType.YES_NO,
|
|
modal=True)
|
|
|
|
if not res:
|
|
return False
|
|
|
|
# Alter persistent config
|
|
if controller is not None:
|
|
self.vm.add_device(controller)
|
|
self.vm.add_device(dev)
|
|
|
|
return False
|
|
|
|
def _finish_cb(self, error, details, dev):
|
|
failure = True
|
|
if not error:
|
|
try:
|
|
failure = self._add_device(dev)
|
|
except Exception as e:
|
|
failure = True
|
|
error = _("Unable to add device: %s") % str(e)
|
|
details = "".join(traceback.format_exc())
|
|
|
|
if error is not None:
|
|
self.err.show_err(error, details=details)
|
|
|
|
self.reset_finish_cursor()
|
|
|
|
if not failure:
|
|
self.close()
|
|
|
|
def _finish(self, ignore=None):
|
|
dev = self._build_device(check_xmleditor=True)
|
|
if not dev:
|
|
return
|
|
|
|
try:
|
|
if self._validate_device(dev) is False:
|
|
return
|
|
except Exception as e:
|
|
self.err.show_err(
|
|
_("Error validating device parameters: %s") % str(e))
|
|
return
|
|
|
|
self.set_finish_cursor()
|
|
progWin = vmmAsyncJob(self._setup_device, [dev],
|
|
self._finish_cb, [dev],
|
|
_("Creating device"),
|
|
_("Depending on the device, this may take "
|
|
"a few minutes to complete."),
|
|
self.topwin)
|
|
progWin.run()
|
|
|
|
|
|
###########################
|
|
# Device build/validation #
|
|
###########################
|
|
|
|
def _validate_hostdev_collision(self, dev):
|
|
names = []
|
|
nodedev = getattr(dev, "vmm_nodedev", None)
|
|
if not nodedev:
|
|
return # pragma: no cover
|
|
|
|
for vm in self.conn.list_vms():
|
|
for hostdev in vm.xmlobj.devices.hostdev:
|
|
if nodedev.compare_to_hostdev(hostdev):
|
|
names.append(vm.get_name())
|
|
if names:
|
|
res = self.err.yes_no(
|
|
_('The device is already in use by other guests %s') %
|
|
(names),
|
|
_("Do you really want to use the device?"))
|
|
if not res:
|
|
return False
|
|
|
|
def _validate_device(self, dev):
|
|
if dev.DEVICE_TYPE == "disk":
|
|
if self.addstorage.validate_device(dev) is False:
|
|
return False
|
|
|
|
if dev.DEVICE_TYPE == "interface":
|
|
self._netlist.validate_device(dev)
|
|
|
|
if dev.DEVICE_TYPE == "hostdev":
|
|
if self._validate_hostdev_collision(dev) is False:
|
|
return False
|
|
|
|
dev.validate()
|
|
|
|
def _build_xmleditor_device(self, srcdev):
|
|
xml = self._xmleditor.get_xml()
|
|
log.debug("Using XML from xmleditor:\n%s", xml)
|
|
devclass = srcdev.__class__
|
|
dev = devclass(srcdev.conn, parsexml=xml)
|
|
|
|
if srcdev.DEVICE_TYPE == "disk":
|
|
if (srcdev.get_source_path() == dev.get_source_path() and
|
|
srcdev.get_vol_install()):
|
|
dev.set_vol_install(srcdev.get_vol_install())
|
|
elif dev.get_source_path():
|
|
# Needed to convince disk.validate() to validate a passed path
|
|
dev.set_backend_for_existing_path()
|
|
|
|
return dev
|
|
|
|
def _build_device(self, check_xmleditor):
|
|
page_num = self.widget("create-pages").get_current_page()
|
|
try:
|
|
dev = self._build_device_page(page_num)
|
|
|
|
if check_xmleditor and self._xmleditor.is_xml_selected():
|
|
dev = self._build_xmleditor_device(dev)
|
|
|
|
return dev
|
|
except Exception as e:
|
|
self.err.show_err(
|
|
_("Error building device XML: %s") % str(e))
|
|
return
|
|
|
|
def _build_device_page(self, page_num):
|
|
# pylint: disable=assignment-from-no-return
|
|
if page_num == PAGE_DISK:
|
|
dev = self._build_storage()
|
|
elif page_num == PAGE_CONTROLLER:
|
|
dev = self._build_controller()
|
|
elif page_num == PAGE_NETWORK:
|
|
dev = self._build_network()
|
|
elif page_num == PAGE_INPUT:
|
|
dev = self._build_input()
|
|
elif page_num == PAGE_GRAPHICS:
|
|
dev = self._build_graphics()
|
|
elif page_num == PAGE_SOUND:
|
|
dev = self._build_sound()
|
|
elif page_num == PAGE_HOSTDEV:
|
|
dev = self._build_hostdev()
|
|
elif page_num == PAGE_CHAR:
|
|
dev = self._build_char()
|
|
elif page_num == PAGE_VIDEO:
|
|
dev = self._build_video()
|
|
elif page_num == PAGE_WATCHDOG:
|
|
dev = self._build_watchdog()
|
|
elif page_num == PAGE_FILESYSTEM:
|
|
dev = self._build_filesystem()
|
|
elif page_num == PAGE_SMARTCARD:
|
|
dev = self._build_smartcard()
|
|
elif page_num == PAGE_USBREDIR:
|
|
dev = self._build_usbredir()
|
|
elif page_num == PAGE_TPM:
|
|
dev = self._build_tpm()
|
|
elif page_num == PAGE_RNG:
|
|
dev = self._build_rng()
|
|
elif page_num == PAGE_PANIC:
|
|
dev = self._build_panic()
|
|
elif page_num == PAGE_VSOCK:
|
|
dev = self._build_vsock()
|
|
|
|
dev.set_defaults(self.vm.get_xmlobj())
|
|
return dev
|
|
|
|
def _set_disk_controller(self, disk):
|
|
# Add a SCSI controller with model virtio-scsi if needed
|
|
disk.vmm_controller = None
|
|
if not self.vm.xmlobj.can_default_virtioscsi():
|
|
return
|
|
|
|
controller = DeviceController(self.conn.get_backend())
|
|
controller.type = "scsi"
|
|
controller.model = "virtio-scsi"
|
|
controller.index = 0
|
|
disk.vmm_controller = controller
|
|
|
|
def _build_storage(self):
|
|
bus = uiutil.get_list_selection(
|
|
self.widget("storage-bustype"))
|
|
device = uiutil.get_list_selection(
|
|
self.widget("storage-devtype"))
|
|
|
|
disk = self.addstorage.build_device(self.vm.get_name(),
|
|
collideguest=self.vm.xmlobj, device=device)
|
|
|
|
used = []
|
|
disk.bus = bus
|
|
|
|
# Generate target
|
|
disks = (self.vm.xmlobj.devices.disk +
|
|
self.vm.get_xmlobj(inactive=True).devices.disk)
|
|
for d in disks:
|
|
if d.target not in used:
|
|
used.append(d.target)
|
|
|
|
self._set_disk_controller(disk)
|
|
disk.generate_target(used)
|
|
return disk
|
|
|
|
def _build_network(self):
|
|
model = uiutil.get_list_selection(self.widget("net-model"))
|
|
mac = None
|
|
if self.widget("mac-address").get_active():
|
|
mac = self.widget("create-mac-address").get_text()
|
|
|
|
dev = self._netlist.build_device(mac, model)
|
|
return dev
|
|
|
|
def _build_input(self):
|
|
typ, bus = uiutil.get_list_selection(self.widget("input-type"))
|
|
dev = DeviceInput(self.conn.get_backend())
|
|
dev.type = typ
|
|
dev.bus = bus
|
|
return dev
|
|
|
|
def _build_graphics(self):
|
|
return self._gfxdetails.build_device()
|
|
|
|
def _build_sound(self):
|
|
smodel = uiutil.get_list_selection(self.widget("sound-model"))
|
|
dev = DeviceSound(self.conn.get_backend())
|
|
dev.model = smodel
|
|
return dev
|
|
|
|
def _build_hostdev(self):
|
|
nodedev = uiutil.get_list_selection(self.widget("host-device"))
|
|
dev = DeviceHostdev(self.conn.get_backend())
|
|
dev.set_from_nodedev(nodedev)
|
|
setattr(dev, "vmm_nodedev", nodedev)
|
|
return dev
|
|
|
|
def _build_char(self):
|
|
char_class = self._get_char_class()
|
|
devtype = uiutil.get_list_selection(self.widget("char-device-type"))
|
|
|
|
typebox = self.widget("char-target-type")
|
|
source_path = self.widget("char-path").get_text()
|
|
source_channel = self.widget("char-channel").get_text()
|
|
target_name = self.widget("char-target-name").get_child().get_text()
|
|
target_type = uiutil.get_list_selection(typebox)
|
|
|
|
if not self.widget("char-path").get_visible():
|
|
source_path = None
|
|
if not self.widget("char-channel").get_visible():
|
|
source_channel = None
|
|
if not self.widget("char-target-name").get_visible():
|
|
target_name = None
|
|
if not typebox.get_visible():
|
|
target_type = None
|
|
|
|
dev = char_class(self.conn.get_backend())
|
|
dev.type = devtype
|
|
dev.source.path = source_path
|
|
dev.source.channel = source_channel
|
|
dev.target_name = target_name
|
|
dev.target_type = target_type
|
|
return dev
|
|
|
|
def _build_video(self):
|
|
model = uiutil.get_list_selection(self.widget("video-model"))
|
|
dev = DeviceVideo(self.conn.get_backend())
|
|
dev.model = model
|
|
return dev
|
|
|
|
def _build_watchdog(self):
|
|
model = uiutil.get_list_selection(self.widget("watchdog-model"))
|
|
action = uiutil.get_list_selection(self.widget("watchdog-action"))
|
|
dev = DeviceWatchdog(self.conn.get_backend())
|
|
dev.model = model
|
|
dev.action = action
|
|
return dev
|
|
|
|
def _build_filesystem(self):
|
|
return self._fsdetails.build_device()
|
|
|
|
def _build_smartcard(self):
|
|
mode = uiutil.get_list_selection(self.widget("smartcard-mode"))
|
|
dev = DeviceSmartcard(self.conn.get_backend())
|
|
dev.mode = mode
|
|
return dev
|
|
|
|
def _build_usbredir(self):
|
|
stype = uiutil.get_list_selection(self.widget("usbredir-list"))
|
|
dev = DeviceRedirdev(self.conn.get_backend())
|
|
dev.type = stype
|
|
return dev
|
|
|
|
def _build_tpm(self):
|
|
return self._tpmdetails.build_device()
|
|
|
|
def _build_panic(self):
|
|
model = uiutil.get_list_selection(self.widget("panic-model"))
|
|
dev = DevicePanic(self.conn.get_backend())
|
|
dev.model = model
|
|
return dev
|
|
|
|
def _build_vsock(self):
|
|
auto_cid, cid = self._vsockdetails.get_values()
|
|
dev = DeviceVsock(self.conn.get_backend())
|
|
dev.auto_cid = auto_cid
|
|
dev.cid = cid
|
|
return dev
|
|
|
|
def _build_controller(self):
|
|
controller_type = uiutil.get_list_selection(
|
|
self.widget("controller-type"))
|
|
model = uiutil.get_list_selection(self.widget("controller-model"))
|
|
|
|
self._selected_model = model
|
|
if model == "usb3":
|
|
dev = DeviceController.get_usb3_controller(
|
|
self.conn.get_backend(), self.vm.xmlobj)
|
|
model = None
|
|
else:
|
|
dev = DeviceController(self.conn.get_backend())
|
|
|
|
controllers = self.vm.xmlobj.devices.controller
|
|
controller_num = [x for x in controllers if
|
|
(x.type == controller_type)]
|
|
if len(controller_num) > 0:
|
|
index_new = max([x.index for x in controller_num]) + 1
|
|
dev.index = index_new
|
|
|
|
dev.type = controller_type
|
|
|
|
if model and model != "none":
|
|
dev.model = model
|
|
return dev
|
|
|
|
def _build_rng(self):
|
|
device = self.widget("rng-device").get_text()
|
|
dev = DeviceRng(self.conn.get_backend())
|
|
dev.backend_model = DeviceRng.TYPE_RANDOM
|
|
dev.device = device
|
|
return dev
|
|
|
|
|
|
####################
|
|
# Unsorted helpers #
|
|
####################
|
|
|
|
def _browse_storage_cb(self, ignore, widget):
|
|
self._browse_file(widget)
|
|
|
|
def _browse_file(self, textent, isdir=False):
|
|
def set_storage_cb(src, path):
|
|
if path:
|
|
textent.set_text(path)
|
|
|
|
reason = (isdir and
|
|
self.config.CONFIG_DIR_FS or
|
|
self.config.CONFIG_DIR_IMAGE)
|
|
if self._storagebrowser is None:
|
|
self._storagebrowser = vmmStorageBrowser(self.conn)
|
|
|
|
self._storagebrowser.set_finish_cb(set_storage_cb)
|
|
self._storagebrowser.set_browse_reason(reason)
|
|
|
|
self._storagebrowser.show(self.topwin)
|