cli: Add --xml xpath option for virt-install and virt-xml

The --xml option allows users to request raw XML edits to virt-install
or virt-xml generated XML. This gives users a bit of a workaround
incase we don't have proper support for some XML property. The --xml
option can gain more features in the future if it makes sense, like
setting XML namespaces for example.

Basic usage is like: virt-install --xml ./@foo=bar ...

Which will change the generated <domain> XML to have

<domain foo='bar' ...

virt-xml works similarly. It can only be combined with --edit currently.
This only works with xpaths rooted against the entire document.

Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
Cole Robinson 2020-09-10 13:52:07 -04:00
parent aa8572048b
commit 8560138cf2
10 changed files with 233 additions and 8 deletions

View File

@ -170,6 +170,48 @@ Use --sysinfo=? to see a list of all available sub options.
Complete details at L<https://libvirt.org/formatdomain.html#elementsSysinfo>
and L<https://libvirt.org/formatdomain.html#elementsOSBIOS> for B<smbios> XML element.
=item B<--xml> ARGS
Make direct edits to the generated XML using XPath syntax. Take an example like
virt-install --xml ./@foo=bar --xml ./newelement/subelement=1
This will alter the generated XML to contain:
<domain foo='bar' ...>
...
<newelement>
<subelement>1</subelement>
</newelement>
</domain>
The --xml option has 4 sub options:
=over 2
=item --xml xpath.set=XPATH[=VALUE]
The default behavior if no explicit suboption is set. Takes the form XPATH=VALUE
unless paired with B<xpath.value>. See below for how value is interpreted.
=item --xml xpath.value=VALUE
B<xpath.set> will be interpreted only as the XPath string, and B<xpath.value> will
be used as the value to set. May help sidestep problems if the string you need to
set contains a '=' equals sign.
If value is empty, it's treated as unsetting that particular node.
=item --xml xpath.create=XPATH
Create the node as an empty element. Needed for boolean elements like <readonly/>
=item --xml xpath.delete=XPATH
Delete the entire node specified by the xpath, and all its children
=back
=item B<--qemu-commandline> ARGS
Pass options directly to the qemu emulator. Only works for the libvirt qemu driver. The option can take a string of arguments, for example:

View File

@ -240,6 +240,8 @@ variants.
=item B<--sysinfo>
=item B<--xml>
=item B<--qemu-commandline>
=item B<--launchSecurity>

View File

@ -1,4 +1,4 @@
<domain type="kvm">
<domain type="kvm" foo="bar">
<name>fedora</name>
<uuid>00000000-1111-2222-3333-444444444444</uuid>
<metadata>
@ -419,8 +419,10 @@
<tpm model="tpm-crb">
<backend type="emulator" version="2.0"/>
</tpm>
<graphics type="sdl" display=":3.4" xauth="/tmp/.Xauthority"/>
<graphics type="spice" port="-1" tlsPort="-1" autoport="yes">
<graphics type="sdl" display=":3.4" xauth="/tmp/.Xauthority">
<ab>cd</ab>
</graphics>
<graphics type="spice" port="-1" tlsPort="-1" autoport="yes" ef="hg">
<image compression="off"/>
</graphics>
<graphics type="vnc" port="5950" keymap="ja" listen="1.2.3.4" passwd="foo"/>
@ -609,4 +611,10 @@
<qemu:arg value="bar"/>
<qemu:env name="DISPLAY" value=":0.1"/>
</qemu:commandline>
<baz>wib</baz>
<deleteme/>
<t1>
<t2 foo="123"/>
</t1>
<barenode/>
</domain>

View File

@ -0,0 +1,26 @@
-<domain type="test">
+<domain type="test" foo="bar">
<name>test-for-virtxml</name>
<uuid>12345678-12f4-1234-1234-123456789012</uuid>
<description>Test VM for virtxml cli tests
@@
</libosinfo:libosinfo>
</metadata>
<memory unit="KiB">4194304</memory>
- <currentMemory unit="KiB">4194304</currentMemory>
<blkiotune>
<weight>100</weight>
<device>
@@
<dhCert>AQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAA</dhCert>
<session>IHAVENOIDEABUTJUSTPROVIDINGASTRING</session>
</launchSecurity>
+ <new>
+ <element>
+ <test>1</test>
+ </element>
+ </new>
</domain>
Domain 'test-for-virtxml' defined successfully.
Changes will take effect after the domain is fully powered off.

View File

@ -722,6 +722,15 @@ source.reservations.managed=no,source.reservations.source.type=unix,source.reser
--qemu-commandline="-device vfio-pci,addr=05.0,sysfsdev=/sys/class/mdev_bus/0000:00:02.0/f321853c-c584-4a6b-b99a-3eee22a3919c"
--qemu-commandline="-set device.video0.driver=virtio-vga"
--qemu-commandline args="-foo bar"
--xml /domain/@foo=bar
--xml xpath.set=./baz,xpath.value=wib
--xml ./deleteme/deleteme2/deleteme3=foo
--xml ./t1/t2/@foo=123
--xml ./devices/graphics[1]/ab=cd
--xml ./devices/graphics[2]/@ef=hg
--xml xpath.create=./barenode
--xml xpath.delete=./deleteme/deleteme2
""", "many-devices", predefine_check="5.3.0")
@ -831,6 +840,8 @@ c.add_invalid("--boot uefi") # URI doesn't support UEFI bits
c.add_invalid("--connect %(URI-KVM)s --boot uefi,arch=ppc64") # unsupported arch for UEFI
c.add_invalid("--features smm=on --machine pc") # smm=on doesn't work for machine=pc
c.add_invalid("--graphics type=vnc,keymap", grep="Option 'keymap' had no value set.")
c.add_invalid("--xml FOOXPATH", grep="form of XPATH=VALUE") # failure parsing xpath value
c.add_invalid("--xml /@foo=bar", grep="/@foo xmlXPathEval") # failure processing xpath
@ -1186,6 +1197,7 @@ c.add_invalid("test-for-virtxml --edit --graphics password=foo,keymap= --update
c.add_invalid("--build-xml --memory 10,maxmemory=20") # building XML for option that doesn't support it
c.add_invalid("test-state-shutoff --edit sparse=no --disk path=blah", grep="Don't know how to match device type 'disk' property 'sparse'")
c.add_invalid("test --edit --boot network,cdrom --define --no-define")
c.add_invalid("test --add-device --xml ./@foo=bar", grep="--xml can only be used with --edit")
c.add_compare("test --print-xml --edit --vcpus 7", "print-xml") # test --print-xml
c.add_compare("--edit --cpu host-passthrough", "stdin-edit", input_file=(_VIRTXMLDIR + "virtxml-stdin-edit.xml")) # stdin test
c.add_compare("--build-xml --cpu pentium3,+x2apic", "build-cpu")
@ -1203,6 +1215,7 @@ c.add_compare("--connect %(URI-KVM)s test-many-devices --edit --cpu host-copy",
c = vixml.add_category("simple edit diff", "test-for-virtxml --edit --print-diff --define")
c.add_compare("""--xml ./@foo=bar --xml xpath.delete=./currentMemory --xml ./new/element/test=1""", "edit-xpaths")
c.add_compare("""--metadata name=foo-my-new-name,os_name=fedora13,uuid=12345678-12F4-1234-1234-123456789AFA,description="hey this is my
new
very,very=new desc\\\'",title="This is my,funky=new title" """, "edit-simple-metadata")

View File

@ -480,7 +480,7 @@ def get_domain_and_guest(conn, domstr):
def _get_completer_parsers():
return VIRT_PARSERS + [ParserCheck, ParserLocation,
ParserUnattended, ParserInstall, ParserCloudInit]
ParserUnattended, ParserInstall, ParserCloudInit, ParserXML]
def _virtparser_completer(prefix, **kwargs):
@ -917,6 +917,14 @@ def add_os_variant_option(parser, virtinstall):
return osg
def add_xml_option(grp):
grp.add_argument("--xml", action="append", default=[],
help=_("Perform raw XML XPath options on the final XML. Example:\n"
"--xml ./cpu/@mode=host-passthrough\n"
"--xml ./devices/disk[2]/serial=new-serial\n"
"--xml xpath.delete=./clock"))
#############################################
# CLI complex parsing helpers #
# (for options like --disk, --network, etc. #
@ -1535,6 +1543,73 @@ class VirtCLIParser(metaclass=_InitClass):
"""Do nothing callback"""
#################
# --xml parsing #
#################
class _XMLCLIInstance:
"""
Helper class to parse --xml content into.
Generates XMLManualAction which actually performs the work
"""
def __init__(self):
self.xpath_delete = None
self.xpath_set = None
self.xpath_create = None
self.xpath_value = None
def build_action(self):
from .xmlbuilder import XMLManualAction
if self.xpath_delete:
return XMLManualAction(self.xpath_delete,
action=XMLManualAction.ACTION_DELETE)
if self.xpath_create:
return XMLManualAction(self.xpath_create,
action=XMLManualAction.ACTION_CREATE)
xpath = self.xpath_set
if self.xpath_value:
val = self.xpath_value
else:
if "=" not in str(xpath):
fail("%s: Setting xpath must be in the form of XPATH=VALUE" %
xpath)
xpath, val = xpath.rsplit("=", 1)
return XMLManualAction(xpath, val or None)
class ParserXML(VirtCLIParser):
cli_arg_name = "xml"
supports_clearxml = False
@classmethod
def _init_class(cls, **kwargs):
VirtCLIParser._init_class(**kwargs)
cls.add_arg("xpath.delete", "xpath_delete", can_comma=True)
cls.add_arg("xpath.set", "xpath_set", can_comma=True)
cls.add_arg("xpath.create", "xpath_create", can_comma=True)
cls.add_arg("xpath.value", "xpath_value", can_comma=True)
def _parse(self, inst):
if not self.optstr.startswith("xpath."):
self.optdict.clear()
self.optdict["xpath.set"] = self.optstr
super()._parse(inst)
def parse_xmlcli(guest, options):
"""
Parse --xml option strings and add the resulting XMLManualActions
to the Guest instance
"""
for optstr in options.xml:
inst = _XMLCLIInstance()
ParserXML(optstr).parse(inst)
manualaction = inst.build_action()
guest.add_xml_manual_action(manualaction)
########################
# --unattended parsing #
########################

View File

@ -553,12 +553,14 @@ def _build_options_guest(conn, options):
# Fill in guest from the command line content
set_explicit_guest_options(options, guest)
cli.parse_option_strings(options, guest, None)
cli.parse_xmlcli(guest, options)
# Call set_capabilities_defaults explicitly here rather than depend
# on set_defaults calling it. Installer setup needs filled in values.
# However we want to do it after parse_option_strings to ensure
# we are operating on any arch/os/type values passed in with --boot
guest.set_capabilities_defaults()
return guest
@ -946,6 +948,7 @@ def parse_args():
cli.add_memory_option(geng, backcompat=True)
cli.vcpu_cli_options(geng)
cli.add_metadata_option(geng)
cli.add_xml_option(geng)
geng.add_argument("-u", "--uuid", help=argparse.SUPPRESS)
geng.add_argument("--description", help=argparse.SUPPRESS)

View File

@ -127,7 +127,7 @@ def check_action_collision(options):
def check_xmlopt_collision(options):
collisions = []
for parserclass in cli.VIRT_PARSERS:
for parserclass in cli.VIRT_PARSERS + [cli.ParserXML]:
if getattr(options, parserclass.cli_arg_name):
collisions.append(parserclass)
@ -297,9 +297,18 @@ def update_changes(domain, devs, action, confirm):
def prepare_changes(xmlobj, options, parserclass):
origxml = xmlobj.get_xml()
has_edit = options.edit != -1
is_xmlcli = parserclass is cli.ParserXML
if options.edit != -1:
devs = action_edit(xmlobj, options, parserclass)
if is_xmlcli and not has_edit:
fail(_("--xml can only be used with --edit"))
if has_edit:
if is_xmlcli:
devs = []
cli.parse_xmlcli(xmlobj, options)
else:
devs = action_edit(xmlobj, options, parserclass)
action = "update"
elif options.add_device:
@ -391,6 +400,7 @@ def parse_args():
cli.add_metadata_option(g)
cli.add_memory_option(g)
cli.vcpu_cli_options(g, editexample=True)
cli.add_xml_option(g)
cli.add_guest_xml_options(g)
cli.add_boot_options(g)
cli.add_device_options(g)

View File

@ -7,6 +7,7 @@
import libxml2
from . import xmlutil
from .logger import log
# pylint: disable=protected-access
@ -313,7 +314,12 @@ class _Libxml2API(_XMLBase):
def _find(self, fullxpath):
xpath = _XPath(fullxpath).xpath
node = self._ctx.xpathEval(xpath)
try:
node = self._ctx.xpathEval(xpath)
except Exception as e:
log.debug("fullxpath=%s xpath=%s eval failed",
fullxpath, xpath, exc_info=True)
raise RuntimeError("%s %s" % (fullxpath, str(e))) from None
return (node and node[0] or None)
def count(self, xpath):

View File

@ -26,6 +26,35 @@ _allprops = []
_seenprops = []
class XMLManualAction(object):
"""
Helper class for tracking and performing the user requested manual
XML action
"""
ACTION_CREATE = 1
ACTION_DELETE = 2
ACTION_SET = 3
def __init__(self, xpath, value=None, action=-1):
self.xpath = xpath
self._value = value
self._action = self.ACTION_SET
if action != -1:
self._action = action
def perform(self, xmlstate):
xpath = self.xpath
if xpath.startswith("."):
xpath = xmlstate.make_abs_xpath(self.xpath)
if self._action == self.ACTION_DELETE:
setval = False
elif self._action == self.ACTION_CREATE:
setval = True
else:
setval = self._value
xmlstate.xmlapi.set_xpath_content(xpath, setval)
class _XMLPropertyCache(object):
"""
Cache lookup tables mapping classes to their associated
@ -489,6 +518,7 @@ class XMLBuilder(object):
self._validate_xmlbuilder()
self._initial_child_parse()
self._manual_actions = []
def _validate_xmlbuilder(self):
# This is one time validation we run once per XMLBuilder class
@ -615,6 +645,13 @@ class XMLBuilder(object):
return 0
return int(xpath.rsplit("[", 1)[1].strip("]")) - 1
def add_xml_manual_action(self, manualaction):
"""
Register a manual XML action to perform at the end of the
XML building step. Triggered via --xml on the command line
"""
self._manual_actions.append(manualaction)
################
# Internal API #
@ -796,3 +833,6 @@ class XMLBuilder(object):
elif key in childprops:
for obj in xmlutil.listify(getattr(self, key)):
obj._add_parse_bits(self._xmlstate.xmlapi)
for manualaction in self._manual_actions:
manualaction.perform(self._xmlstate)