From 8560138cf2269b8bc8f3cf657ed282a2ac9d499f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 10 Sep 2020 13:52:07 -0400 Subject: [PATCH] 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 XML to have --- man/virt-install.pod | 42 ++++++++++ man/virt-xml.pod | 2 + .../cli/compare/virt-install-many-devices.xml | 14 +++- .../data/cli/compare/virt-xml-edit-xpaths.xml | 26 +++++++ tests/test_cli.py | 13 ++++ virtinst/cli.py | 77 ++++++++++++++++++- virtinst/virtinstall.py | 3 + virtinst/virtxml.py | 16 +++- virtinst/xmlapi.py | 8 +- virtinst/xmlbuilder.py | 40 ++++++++++ 10 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 tests/data/cli/compare/virt-xml-edit-xpaths.xml diff --git a/man/virt-install.pod b/man/virt-install.pod index 7e90f89a..5153e5a0 100644 --- a/man/virt-install.pod +++ b/man/virt-install.pod @@ -170,6 +170,48 @@ Use --sysinfo=? to see a list of all available sub options. Complete details at L and L for B 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: + + + ... + + 1 + + + +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. See below for how value is interpreted. + +=item --xml xpath.value=VALUE + +B will be interpreted only as the XPath string, and B 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 + +=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: diff --git a/man/virt-xml.pod b/man/virt-xml.pod index 0aaa63d4..12f9a7b9 100644 --- a/man/virt-xml.pod +++ b/man/virt-xml.pod @@ -240,6 +240,8 @@ variants. =item B<--sysinfo> +=item B<--xml> + =item B<--qemu-commandline> =item B<--launchSecurity> diff --git a/tests/data/cli/compare/virt-install-many-devices.xml b/tests/data/cli/compare/virt-install-many-devices.xml index 421368dc..690267a7 100644 --- a/tests/data/cli/compare/virt-install-many-devices.xml +++ b/tests/data/cli/compare/virt-install-many-devices.xml @@ -1,4 +1,4 @@ - + fedora 00000000-1111-2222-3333-444444444444 @@ -419,8 +419,10 @@ - - + + cd + + @@ -609,4 +611,10 @@ + wib + + + + + diff --git a/tests/data/cli/compare/virt-xml-edit-xpaths.xml b/tests/data/cli/compare/virt-xml-edit-xpaths.xml new file mode 100644 index 00000000..e424f6cd --- /dev/null +++ b/tests/data/cli/compare/virt-xml-edit-xpaths.xml @@ -0,0 +1,26 @@ +- ++ + test-for-virtxml + 12345678-12f4-1234-1234-123456789012 + Test VM for virtxml cli tests +@@ + + + 4194304 +- 4194304 + + 100 + +@@ + AQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAAAQAAAAAOAAA + IHAVENOIDEABUTJUSTPROVIDINGASTRING + ++ ++ ++ 1 ++ ++ + + +Domain 'test-for-virtxml' defined successfully. +Changes will take effect after the domain is fully powered off. \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 869687cb..b26156ea 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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") diff --git a/virtinst/cli.py b/virtinst/cli.py index 17bb1206..5deb1f83 100644 --- a/virtinst/cli.py +++ b/virtinst/cli.py @@ -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 # ######################## diff --git a/virtinst/virtinstall.py b/virtinst/virtinstall.py index b63f17a5..a73da352 100644 --- a/virtinst/virtinstall.py +++ b/virtinst/virtinstall.py @@ -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) diff --git a/virtinst/virtxml.py b/virtinst/virtxml.py index b80a84db..4cb542e2 100644 --- a/virtinst/virtxml.py +++ b/virtinst/virtxml.py @@ -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) diff --git a/virtinst/xmlapi.py b/virtinst/xmlapi.py index 80e43a61..ff270ee2 100644 --- a/virtinst/xmlapi.py +++ b/virtinst/xmlapi.py @@ -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): diff --git a/virtinst/xmlbuilder.py b/virtinst/xmlbuilder.py index 982a35a7..ccc90d4b 100644 --- a/virtinst/xmlbuilder.py +++ b/virtinst/xmlbuilder.py @@ -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)