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:
parent
aa8572048b
commit
8560138cf2
|
@ -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:
|
||||
|
|
|
@ -240,6 +240,8 @@ variants.
|
|||
|
||||
=item B<--sysinfo>
|
||||
|
||||
=item B<--xml>
|
||||
|
||||
=item B<--qemu-commandline>
|
||||
|
||||
=item B<--launchSecurity>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
|
@ -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")
|
||||
|
|
|
@ -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 #
|
||||
########################
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue