details: add xmleditor UI

Handling this is a bit different from other bits, because:

1) the <device> editing paradigm is unique. We need to replace the
   device in line in the XML which is a new operation
2) the New VM customize pattern is tricky and needs lots of
   special handling
This commit is contained in:
Cole Robinson 2019-05-05 19:00:40 -04:00
parent df80852952
commit a5f4033493
3 changed files with 154 additions and 4 deletions

View File

@ -5,6 +5,7 @@
# See the COPYING file in the top-level directory.
import logging
import re
import traceback
from gi.repository import Gtk
@ -25,10 +26,13 @@ from .netlist import vmmNetworkList
from .oslist import vmmOSList
from .storagebrowse import vmmStorageBrowser
from .vsockdetails import vmmVsockDetails
from .xmleditor import vmmXMLEditor
# Parameters that can be edited in the details window
(EDIT_NAME,
(EDIT_XML,
EDIT_NAME,
EDIT_TITLE,
EDIT_MACHTYPE,
EDIT_FIRMWARE,
@ -100,7 +104,7 @@ from .vsockdetails import vmmVsockDetails
EDIT_FS,
EDIT_HOSTDEV_ROMBAR) = range(1, 58)
EDIT_HOSTDEV_ROMBAR) = range(1, 59)
# Columns in hw list model
@ -333,6 +337,33 @@ def _warn_cpu_thread_topo(threads, cpu_model):
return False
def _unindent_device_xml(xml):
"""
The device parsed from a domain will have no indent
for the first line, but then <domain> expected indent
from the remaining lines. Try to unindent the remaining
lines so it looks nice in the XML editor.
"""
lines = xml.splitlines()
if not xml.startswith("<") or len(lines) < 2:
return xml
ret = ""
unindent = 0
for c in lines[1]:
if c != " ":
break
unindent += 1
unindent = max(0, unindent - 2)
ret = lines[0] + "\n"
for line in lines[1:]:
if re.match(r"^%s *<.*$" % (unindent * " "), line):
line = line[unindent:]
ret += line + "\n"
return ret
class vmmDetails(vmmGObjectUI):
def __init__(self, vm, builder, topwin, is_customize_dialog):
vmmGObjectUI.__init__(self, "details.ui",
@ -398,6 +429,16 @@ class vmmDetails(vmmGObjectUI):
self.vsockdetails.connect("changed-cid",
lambda *x: self.enable_apply(x, EDIT_VSOCK_CID))
self._xmleditor = vmmXMLEditor(self.builder, self.topwin,
self.widget("hw-panel-align"),
self.widget("hw-panel"))
self._xmleditor.connect("changed",
lambda s: self.enable_apply(EDIT_XML))
self._xmleditor.connect("xml-requested",
self._xmleditor_xml_requested_cb)
self._xmleditor.connect("xml-reset",
self._xmleditor_xml_reset_cb)
self.oldhwkey = None
self.addhwmenu = None
self._addhwmenuitems = None
@ -543,6 +584,8 @@ class vmmDetails(vmmGObjectUI):
self.netlist = None
self.vsockdetails.cleanup()
self.vsockdetails = None
self._xmleditor.cleanup()
self._xmleditor = None
##########################
@ -967,6 +1010,10 @@ class vmmDetails(vmmGObjectUI):
try:
dev = self.get_hw_row()[HW_LIST_COL_DEVICE]
if dev:
self._xmleditor.set_xml(_unindent_device_xml(dev.get_xml()))
else:
self._xmleditor.set_xml_from_libvirtobject(self.vm)
if pagetype == HW_LIST_TYPE_GENERAL:
self.refresh_overview_page()
@ -1045,6 +1092,12 @@ class vmmDetails(vmmGObjectUI):
# External action listeners #
#############################
def _xmleditor_xml_requested_cb(self, src):
self.hw_selected()
def _xmleditor_xml_reset_cb(self, src):
self.hw_selected()
def add_hardware(self, src_ignore):
try:
if self.addhw is None:
@ -1389,7 +1442,12 @@ class vmmDetails(vmmGObjectUI):
success = False
try:
if pagetype is HW_LIST_TYPE_GENERAL:
if self.edited(EDIT_XML):
if dev:
success = self._config_device_xml_apply(dev)
else:
success = self._config_domain_xml_apply()
elif pagetype is HW_LIST_TYPE_GENERAL:
success = self.config_overview_apply()
elif pagetype is HW_LIST_TYPE_OS:
success = self.config_os_apply()
@ -1424,7 +1482,7 @@ class vmmDetails(vmmGObjectUI):
elif pagetype is HW_LIST_TYPE_VSOCK:
success = self.config_vsock_apply(dev)
except Exception as e:
return self.err.show_err(_("Error apply changes: %s") % e)
return self.err.show_err(_("Error applying changes: %s") % e)
if success is not False:
self.disable_apply()
@ -1444,6 +1502,20 @@ class vmmDetails(vmmGObjectUI):
def edited(self, pagetype):
return pagetype in self.active_edits
def _config_domain_xml_apply(self):
newxml = self._xmleditor.get_xml()
def change_cb():
return self.vm.define_xml(newxml)
return vmmAddHardware.change_config_helper(
change_cb, {}, self.vm, self.err)
def _config_device_xml_apply(self, devobj):
newxml = self._xmleditor.get_xml()
def change_cb():
return self.vm.replace_device_xml(devobj, newxml)
return vmmAddHardware.change_config_helper(
change_cb, {}, self.vm, self.err)
def config_overview_apply(self):
kwargs = {}
hotplug_args = {}

View File

@ -468,6 +468,23 @@ class vmmDomain(vmmLibvirtObject):
self._redefine_xmlobj(xmlobj)
def replace_device_xml(self, devobj, newxml):
"""
When device XML is editing from the XML editor window.
"""
do_hotplug = False
devclass = devobj.__class__
newdev = devclass(devobj.conn, parsexml=newxml)
xmlobj = self._make_xmlobj_to_define()
editdev = self._lookup_device_to_define(xmlobj, devobj, do_hotplug)
if not editdev:
return
xmlobj.devices.replace_child(editdev, newdev)
self._redefine_xmlobj(xmlobj)
return editdev, newdev
def define_cpu(self, vcpus=_SENTINEL, maxvcpus=_SENTINEL,
model=_SENTINEL, secure=_SENTINEL, sockets=_SENTINEL,
cores=_SENTINEL, threads=_SENTINEL):
@ -1603,6 +1620,7 @@ class vmmDomainVirtinst(vmmDomain):
def __init__(self, conn, backend, key):
vmmDomain.__init__(self, conn, backend, key)
self._orig_xml = None
self._orig_backend = self._backend
self._refresh_status()
logging.debug("%s initialized with XML=\n%s", self, self._XMLDesc(0))
@ -1640,6 +1658,64 @@ class vmmDomainVirtinst(vmmDomain):
# XML handling #
################
def _sync_disk_storage_params(self, origdisk, newdisk):
"""
When raw disk XML is edited from the customize wizard, the
original DeviceDisk is completely blown away, but that will
lose the storage creation info. This syncs that info across
to the new DeviceDisk
"""
if origdisk.path != newdisk.path:
return
if origdisk.get_vol_object():
logging.debug(
"Syncing vol_object=%s from origdisk=%s to newdisk=%s",
origdisk.get_vol_object(), origdisk, newdisk)
newdisk.set_vol_object(origdisk.get_vol_object(),
origdisk.get_parent_pool())
elif origdisk.get_vol_install():
logging.debug(
"Syncing vol_install=%s from origdisk=%s to newdisk=%s",
origdisk.get_vol_install(), origdisk, newdisk)
newdisk.set_vol_install(origdisk.get_vol_install())
def _replace_domain_xml(self, newxml):
"""
Blow away the Guest instance we are tracking internally with
a new one from the xmleditor UI, and sync over all disk storage
info afterwards
"""
newbackend = Guest(self._backend.conn, parsexml=newxml)
newbackend.installer_instance = self._backend.installer_instance
for origdisk in self._backend.devices.disk:
for newdisk in newbackend.devices.disk:
if origdisk.compare_device(newdisk, newdisk.get_xml_idx()):
self._sync_disk_storage_params(origdisk, newdisk)
break
self._backend = newbackend
def replace_device_xml(self, devobj, newxml):
"""
Overwrite vmmDomain's implementation, since we need to wire in
syncing disk details.
"""
if self._backend == self._orig_backend:
# If the backend hasn't been replace yet, do it, so we don't
# have a mix of is_build Guest with XML parsed objects which
# might contain dragons
self._replace_domain_xml(self._backend.get_xml())
editdev, newdev = vmmDomain.replace_device_xml(self, devobj, newxml)
if editdev.DEVICE_TYPE == "disk":
self._sync_disk_storage_params(editdev, newdev)
def define_xml(self, xml):
origxml = self._backend.get_xml()
self._replace_domain_xml(xml)
self._redefine_xml_internal(origxml, xml)
def define_name(self, newname):
# We need to overwrite this, since the implementation for libvirt
# needs to do some crazy stuff.

View File

@ -362,6 +362,8 @@ class DeviceDisk(Device):
self._set_xmlpath(self.path)
def get_vol_object(self):
if not self._storage_backend:
return None
return self._storage_backend.get_vol_object()
def get_vol_install(self):
if not self._storage_backend: