2013-03-18 05:06:52 +08:00
|
|
|
#
|
|
|
|
# Base class for all VM devices
|
|
|
|
#
|
2013-10-28 04:59:47 +08:00
|
|
|
# Copyright 2008, 2013 Red Hat, Inc.
|
2013-03-18 05:06:52 +08:00
|
|
|
# Cole Robinson <crobinso@redhat.com>
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
2013-10-28 04:59:47 +08:00
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
2013-03-18 05:06:52 +08:00
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
|
|
# MA 02110-1301 USA.
|
|
|
|
|
|
|
|
import copy
|
2014-02-26 03:56:54 +08:00
|
|
|
import logging
|
2013-07-13 03:16:29 +08:00
|
|
|
import os
|
2013-09-11 22:16:12 +08:00
|
|
|
import re
|
2015-03-27 06:04:23 +08:00
|
|
|
import string # pylint: disable=deprecated-module
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
import libxml2
|
|
|
|
|
2014-09-13 03:59:22 +08:00
|
|
|
from . import util
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-08-09 08:47:17 +08:00
|
|
|
|
2014-04-03 06:39:43 +08:00
|
|
|
# pylint: disable=protected-access
|
2013-07-15 21:49:46 +08:00
|
|
|
# This whole file is calling around into non-public functions that we
|
|
|
|
# don't want regular API users to touch
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-02-11 03:47:20 +08:00
|
|
|
_trackprops = bool("VIRTINST_TEST_SUITE" in os.environ)
|
2013-07-13 03:16:29 +08:00
|
|
|
_allprops = []
|
|
|
|
_seenprops = []
|
|
|
|
|
2016-05-21 02:23:39 +08:00
|
|
|
# Convenience global to prevent _remove_xpath_node from unlinking the
|
|
|
|
# top relavtive node in certain cases
|
|
|
|
_top_node = None
|
|
|
|
|
2013-07-13 03:16:29 +08:00
|
|
|
|
2013-07-10 20:05:08 +08:00
|
|
|
class _DocCleanupWrapper(object):
|
|
|
|
def __init__(self, doc):
|
|
|
|
self._doc = doc
|
|
|
|
def __del__(self):
|
|
|
|
self._doc.freeDoc()
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-12 04:48:39 +08:00
|
|
|
|
2013-07-14 08:47:19 +08:00
|
|
|
class _CtxCleanupWrapper(object):
|
|
|
|
def __init__(self, ctx):
|
|
|
|
self._ctx = ctx
|
|
|
|
def __del__(self):
|
|
|
|
self._ctx.xpathFreeContext()
|
|
|
|
self._ctx = None
|
|
|
|
def __getattr__(self, attrname):
|
|
|
|
return getattr(self._ctx, attrname)
|
|
|
|
|
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
def _make_xml_context(node):
|
|
|
|
doc = node.doc
|
|
|
|
ctx = _CtxCleanupWrapper(doc.xpathNewContext())
|
|
|
|
ctx.setContextNode(node)
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2013-04-12 04:48:39 +08:00
|
|
|
def _tuplify_lists(*args):
|
|
|
|
"""
|
|
|
|
Similar to zip(), but use None if lists aren't long enough, and
|
|
|
|
don't skip any None list entry
|
|
|
|
"""
|
|
|
|
args = [util.listify(l) for l in args]
|
|
|
|
maxlen = max([len(l) for l in args])
|
|
|
|
|
|
|
|
ret = []
|
|
|
|
for idx in range(maxlen):
|
|
|
|
tup = tuple()
|
|
|
|
for l in args:
|
|
|
|
tup += (idx >= len(l) and (None,) or (l[idx],))
|
|
|
|
ret.append(tup)
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def _sanitize_libxml_xml(xml):
|
|
|
|
# Strip starting <?...> line
|
|
|
|
if xml.startswith("<?"):
|
|
|
|
ignore, xml = xml.split("\n", 1)
|
|
|
|
if not xml.endswith("\n") and xml.count("\n"):
|
|
|
|
xml += "\n"
|
|
|
|
return xml
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-07-25 00:35:03 +08:00
|
|
|
def _get_xpath_node(ctx, xpath):
|
2013-03-18 05:06:52 +08:00
|
|
|
node = ctx.xpathEval(xpath)
|
2013-07-25 00:35:03 +08:00
|
|
|
return (node and node[0] or None)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2016-07-19 01:23:43 +08:00
|
|
|
def _add_pretty_child(parentnode, newnode):
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
2016-07-19 01:23:43 +08:00
|
|
|
Add 'newnode' as a child of 'parentnode', but try to preserve
|
|
|
|
whitespace and nicely format the result.
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
2016-07-19 01:23:43 +08:00
|
|
|
def node_is_text(n):
|
|
|
|
return bool(n and n.type == "text" and not n.content.count("<"))
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
def prevSibling(node):
|
|
|
|
parent = node.get_parent()
|
|
|
|
if not parent:
|
|
|
|
return None
|
|
|
|
|
|
|
|
prev = None
|
|
|
|
for child in parent.children:
|
|
|
|
if child == node:
|
|
|
|
return prev
|
|
|
|
prev = child
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2016-07-19 01:23:43 +08:00
|
|
|
sib = parentnode.get_last()
|
|
|
|
if not node_is_text(sib):
|
|
|
|
# This case is when we add a child element to a node for the
|
|
|
|
# first time, like:
|
|
|
|
#
|
|
|
|
# <features/>
|
2013-03-18 05:06:52 +08:00
|
|
|
# to
|
|
|
|
# <features>
|
2013-07-15 21:49:46 +08:00
|
|
|
# <acpi/>
|
2013-03-18 05:06:52 +08:00
|
|
|
# </features>
|
2016-07-19 01:23:43 +08:00
|
|
|
prevsib = prevSibling(parentnode)
|
|
|
|
if node_is_text(prevsib):
|
|
|
|
sib = libxml2.newText(prevsib.content)
|
|
|
|
else:
|
|
|
|
sib = libxml2.newText("\n")
|
|
|
|
parentnode.addChild(sib)
|
|
|
|
|
|
|
|
# This case is adding a child element to an already properly
|
|
|
|
# spaced element. Example:
|
|
|
|
# <features>
|
|
|
|
# <acpi/>
|
|
|
|
# </features>
|
|
|
|
# to
|
|
|
|
# <features>
|
|
|
|
# <acpi/>
|
|
|
|
# <apic/>
|
|
|
|
# </features>
|
|
|
|
sib = parentnode.get_last()
|
|
|
|
content = sib.content
|
|
|
|
sib = sib.addNextSibling(libxml2.newText(" "))
|
|
|
|
txt = libxml2.newText(content)
|
|
|
|
|
|
|
|
sib.addNextSibling(newnode)
|
|
|
|
newnode.addNextSibling(txt)
|
|
|
|
return newnode
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-07-19 01:23:43 +08:00
|
|
|
|
2016-07-19 01:34:06 +08:00
|
|
|
def _build_xpath_node(ctx, xpath):
|
2016-07-19 01:23:43 +08:00
|
|
|
"""
|
2016-07-19 01:50:25 +08:00
|
|
|
Build all nodes for the passed xpath. For example, if 'ctx' xml=<foo/>,
|
|
|
|
and xpath=./bar/@baz, after this function the 'ctx' XML will be:
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-07-19 01:50:25 +08:00
|
|
|
<foo>
|
|
|
|
<bar baz=''/>
|
|
|
|
</foo>
|
|
|
|
|
|
|
|
And the node pointing to @baz will be returned, for the caller to
|
|
|
|
do with as they please.
|
2016-07-19 02:40:58 +08:00
|
|
|
|
|
|
|
There's also special handling to ensure that setting
|
|
|
|
xpath=./bar[@baz='foo']/frob will create
|
|
|
|
|
|
|
|
<bar baz='foo'>
|
|
|
|
<frob></frob>
|
|
|
|
</bar>
|
|
|
|
|
|
|
|
Even if <bar> didn't exist before. So we fill in the dependent property
|
|
|
|
expression values
|
2016-07-19 01:50:25 +08:00
|
|
|
"""
|
|
|
|
def _handle_node(nodename, parentnode, parentpath):
|
|
|
|
# If the passed xpath snippet (nodename) exists, return the node
|
|
|
|
# If it doesn't exist, create it, and return the new node
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-07-19 01:50:25 +08:00
|
|
|
# If nodename is a node property, we can handle it up front
|
2013-03-18 05:06:52 +08:00
|
|
|
if nodename.startswith("@"):
|
|
|
|
nodename = nodename.strip("@")
|
2016-07-19 01:50:25 +08:00
|
|
|
return parentnode.setProp(nodename, ""), parentpath
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
if not parentpath:
|
|
|
|
parentpath = nodename
|
|
|
|
else:
|
|
|
|
parentpath += "/%s" % nodename
|
|
|
|
|
2016-07-19 01:50:25 +08:00
|
|
|
# See if the xpath node already exists
|
2013-03-18 05:06:52 +08:00
|
|
|
node = _get_xpath_node(ctx, parentpath)
|
|
|
|
if node:
|
2016-07-19 01:50:25 +08:00
|
|
|
# xpath node already exists, so we don't need to create anything
|
|
|
|
return node, parentpath
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-07-19 01:50:25 +08:00
|
|
|
# If we don't have a parentnode by this point, the root of the
|
|
|
|
# xpath didn't find anything. Usually a coding error
|
2013-03-18 05:06:52 +08:00
|
|
|
if not parentnode:
|
|
|
|
raise RuntimeError("Could not find XML root node")
|
|
|
|
|
2016-07-19 01:50:25 +08:00
|
|
|
# Remove conditional xpath elements for node creation. We preserved
|
|
|
|
# them up until this point since it was needed for proper xpath
|
|
|
|
# lookup, but they aren't valid syntax when creating the node
|
2013-03-18 05:06:52 +08:00
|
|
|
if nodename.count("["):
|
|
|
|
nodename = nodename[:nodename.index("[")]
|
|
|
|
|
|
|
|
newnode = libxml2.newNode(nodename)
|
2016-07-19 01:50:25 +08:00
|
|
|
return _add_pretty_child(parentnode, newnode), parentpath
|
|
|
|
|
|
|
|
|
|
|
|
# Split the xpath and lookup/create each individual piece
|
|
|
|
parentpath = None
|
|
|
|
parentnode = None
|
|
|
|
for nodename in xpath.split("/"):
|
|
|
|
parentnode, parentpath = _handle_node(nodename, parentnode, parentpath)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-07-19 02:40:58 +08:00
|
|
|
# Check if the xpath snippet had an '=' expression in it, example:
|
|
|
|
#
|
|
|
|
# ./foo[@bar='baz']
|
|
|
|
#
|
|
|
|
# If so, we also want to set <foo bar='baz'/>, so that setting
|
|
|
|
# this XML element works as expected in this case.
|
|
|
|
if "[" not in nodename or "=" not in nodename:
|
|
|
|
continue
|
|
|
|
|
|
|
|
propname, val = nodename.split("[")[1].strip("]").split("=")
|
|
|
|
propobj, ignore = _handle_node(propname, parentnode, parentpath)
|
|
|
|
propobj.setContent(val.strip("'"))
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
return parentnode
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2014-01-26 03:48:27 +08:00
|
|
|
def _remove_xpath_node(ctx, xpath, dofree=True):
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
|
|
|
Remove an XML node tree if it has no content
|
|
|
|
"""
|
2015-09-05 05:03:31 +08:00
|
|
|
nextxpath = xpath
|
2014-01-26 03:41:55 +08:00
|
|
|
root_node = ctx.contextNode()
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2015-09-05 05:03:31 +08:00
|
|
|
while nextxpath:
|
|
|
|
curxpath = nextxpath
|
2013-03-18 05:06:52 +08:00
|
|
|
is_orig = (curxpath == xpath)
|
|
|
|
node = _get_xpath_node(ctx, curxpath)
|
2015-09-05 05:03:31 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
if curxpath.count("/"):
|
2015-09-05 05:03:31 +08:00
|
|
|
nextxpath, ignore = curxpath.rsplit("/", 1)
|
2013-03-18 05:06:52 +08:00
|
|
|
else:
|
2015-09-05 05:03:31 +08:00
|
|
|
nextxpath = None
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
if not node:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if node.type not in ["attribute", "element"]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if node.type == "element" and (node.children or node.properties):
|
|
|
|
# Only do a deep unlink if it was the original requested path
|
|
|
|
if not is_orig:
|
|
|
|
continue
|
|
|
|
|
2016-05-21 02:23:39 +08:00
|
|
|
if node == root_node or node == _top_node:
|
|
|
|
# Don't unlink the root node, since it's spread out to all
|
|
|
|
# child objects and it ends up wreaking havoc.
|
|
|
|
break
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
# Look for preceding whitespace and remove it
|
|
|
|
white = node.get_prev()
|
|
|
|
if white and white.type == "text" and not white.content.count("<"):
|
|
|
|
white.unlinkNode()
|
|
|
|
white.freeNode()
|
|
|
|
|
|
|
|
node.unlinkNode()
|
|
|
|
if dofree:
|
|
|
|
node.freeNode()
|
|
|
|
|
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
class XMLChildProperty(property):
|
|
|
|
"""
|
|
|
|
Property that points to a class used for parsing a subsection of
|
|
|
|
of the parent XML. For example when we deligate parsing
|
|
|
|
/domain/cpu/feature of the /domain/cpu class.
|
2013-09-11 22:16:12 +08:00
|
|
|
|
|
|
|
@child_classes: Single class or list of classes to parse state into
|
|
|
|
@relative_xpath: Relative location where the class is rooted compared
|
|
|
|
to its _XML_ROOT_PATH. So interface xml can have nested
|
|
|
|
interfaces rooted at /interface/bridge/interface, so we pass
|
|
|
|
./bridge/interface here for example.
|
2013-09-11 06:32:10 +08:00
|
|
|
"""
|
2013-09-11 23:47:09 +08:00
|
|
|
def __init__(self, child_classes, relative_xpath=".", is_single=False):
|
2013-09-11 06:32:10 +08:00
|
|
|
self.child_classes = util.listify(child_classes)
|
2013-09-11 22:16:12 +08:00
|
|
|
self.relative_xpath = relative_xpath
|
2013-09-11 23:47:09 +08:00
|
|
|
self.is_single = is_single
|
2013-09-20 01:25:52 +08:00
|
|
|
self._propname = None
|
2013-09-11 23:47:09 +08:00
|
|
|
|
|
|
|
if self.is_single and len(self.child_classes) > 1:
|
|
|
|
raise RuntimeError("programming error: Can't specify multiple "
|
|
|
|
"child_classes with is_single")
|
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
property.__init__(self, self._fget)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<XMLChildProperty %s %s>" % (str(self.child_classes), id(self))
|
|
|
|
|
|
|
|
def _findpropname(self, xmlbuilder):
|
2013-09-20 01:25:52 +08:00
|
|
|
if self._propname is None:
|
|
|
|
for key, val in xmlbuilder._all_child_props().items():
|
|
|
|
if val is self:
|
|
|
|
self._propname = key
|
|
|
|
break
|
|
|
|
if self._propname is None:
|
|
|
|
raise RuntimeError("Didn't find expected property=%s" % self)
|
|
|
|
return self._propname
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def _get(self, xmlbuilder):
|
2013-09-11 06:32:10 +08:00
|
|
|
propname = self._findpropname(xmlbuilder)
|
2013-09-11 23:47:09 +08:00
|
|
|
if propname not in xmlbuilder._propstore and not self.is_single:
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlbuilder._propstore[propname] = []
|
|
|
|
return xmlbuilder._propstore[propname]
|
|
|
|
|
|
|
|
def _fget(self, xmlbuilder):
|
2013-09-11 23:47:09 +08:00
|
|
|
if self.is_single:
|
|
|
|
return self._get(xmlbuilder)
|
|
|
|
return self._get(xmlbuilder)[:]
|
|
|
|
|
|
|
|
def clear(self, xmlbuilder):
|
|
|
|
if self.is_single:
|
|
|
|
self._get(xmlbuilder).clear()
|
|
|
|
else:
|
|
|
|
for obj in self._get(xmlbuilder)[:]:
|
2015-09-05 03:45:45 +08:00
|
|
|
xmlbuilder.remove_child(obj)
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-09-20 22:47:36 +08:00
|
|
|
def append(self, xmlbuilder, newobj):
|
|
|
|
# Keep the list ordered by the order of passed in child classes
|
|
|
|
objlist = self._get(xmlbuilder)
|
|
|
|
if len(self.child_classes) == 1:
|
|
|
|
objlist.append(newobj)
|
|
|
|
return
|
|
|
|
|
|
|
|
idx = 0
|
2016-04-19 04:42:12 +08:00
|
|
|
for idx, obj in enumerate(objlist):
|
2013-09-20 22:47:36 +08:00
|
|
|
obj = objlist[idx]
|
|
|
|
if (obj.__class__ not in self.child_classes or
|
|
|
|
(self.child_classes.index(newobj.__class__) <
|
|
|
|
self.child_classes.index(obj.__class__))):
|
|
|
|
break
|
|
|
|
idx += 1
|
|
|
|
|
|
|
|
objlist.insert(idx, newobj)
|
2013-09-11 06:32:10 +08:00
|
|
|
def remove(self, xmlbuilder, obj):
|
2013-09-11 23:47:09 +08:00
|
|
|
self._get(xmlbuilder).remove(obj)
|
|
|
|
def set(self, xmlbuilder, obj):
|
|
|
|
xmlbuilder._propstore[self._findpropname(xmlbuilder)] = obj
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def get_prop_xpath(self, xmlbuilder, obj):
|
|
|
|
relative_xpath = self.relative_xpath + "/" + obj._XML_ROOT_NAME
|
2013-09-11 22:16:12 +08:00
|
|
|
|
|
|
|
match = re.search("%\((.*)\)", self.relative_xpath)
|
|
|
|
if match:
|
|
|
|
valuedict = {}
|
|
|
|
for paramname in match.groups():
|
|
|
|
valuedict[paramname] = getattr(xmlbuilder, paramname)
|
|
|
|
relative_xpath = relative_xpath % valuedict
|
2013-09-11 23:47:09 +08:00
|
|
|
|
|
|
|
return relative_xpath
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-07-14 06:56:09 +08:00
|
|
|
class XMLProperty(property):
|
2015-09-06 05:20:43 +08:00
|
|
|
def __init__(self, xpath, doc=None,
|
|
|
|
set_converter=None, validate_cb=None,
|
2013-09-03 14:47:26 +08:00
|
|
|
is_bool=False, is_int=False, is_yesno=False, is_onoff=False,
|
2014-12-05 04:27:30 +08:00
|
|
|
default_cb=None, default_name=None, do_abspath=False):
|
2013-07-14 05:54:46 +08:00
|
|
|
"""
|
2016-07-19 00:46:59 +08:00
|
|
|
Set a XMLBuilder class property that maps to a value in an XML
|
|
|
|
document, indicated by the passed xpath. For example, for a
|
|
|
|
<domain><name> the definition may look like:
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2016-07-19 00:46:59 +08:00
|
|
|
name = XMLProperty("./name")
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2016-07-19 00:46:59 +08:00
|
|
|
When building XML from scratch (virt-install), 'name' works
|
|
|
|
similar to a regular class property(). When parsing and editing
|
|
|
|
existing guest XML, we use the xpath value to get/set the value
|
|
|
|
in the parsed XML document.
|
2013-07-14 05:54:46 +08:00
|
|
|
|
|
|
|
@param doc: option doc string for the property
|
|
|
|
@param xpath: xpath string which maps to the associated property
|
|
|
|
in a typical XML document
|
2013-07-14 10:13:13 +08:00
|
|
|
@param name: Just a string to print for debugging, only needed
|
|
|
|
if xpath isn't specified.
|
2013-07-14 05:54:46 +08:00
|
|
|
@param set_converter: optional function for converting the property
|
|
|
|
value from the virtinst API to the guest XML. For example,
|
2014-06-17 09:33:14 +08:00
|
|
|
the Guest.memory API was once in MiB, but the libvirt domain
|
|
|
|
memory API is in KiB. So, if xpath is specified, on a 'get'
|
2013-07-14 11:07:01 +08:00
|
|
|
operation we convert the XML value with int(val) / 1024.
|
2013-07-18 05:58:24 +08:00
|
|
|
@param validate_cb: Called once when value is set, should
|
|
|
|
raise a RuntimeError if the value is not proper.
|
2013-07-14 05:54:46 +08:00
|
|
|
@param is_bool: Whether this is a boolean property in the XML
|
2013-09-03 14:47:26 +08:00
|
|
|
@param is_int: Whether this is an integer property in the XML
|
|
|
|
@param is_yesno: Whether this is a yes/no property in the XML
|
|
|
|
@param is_onoff: Whether this is an on/off property in the XML
|
2013-07-15 05:02:42 +08:00
|
|
|
@param default_cb: If building XML from scratch, and this property
|
|
|
|
is never explicitly altered, this function is called for setting
|
|
|
|
a default value in the XML, and for any 'get' call before the
|
|
|
|
first explicit 'set'.
|
2013-07-15 22:57:16 +08:00
|
|
|
@param default_name: If the user does a set and passes in this
|
|
|
|
value, instead use the value of default_cb()
|
2014-09-24 02:25:14 +08:00
|
|
|
@param do_abspath: If True, run os.path.abspath on the passed value
|
2013-07-14 05:54:46 +08:00
|
|
|
"""
|
2013-07-13 03:16:29 +08:00
|
|
|
self._xpath = xpath
|
2015-09-06 05:20:43 +08:00
|
|
|
if not self._xpath:
|
|
|
|
raise RuntimeError("XMLProperty: xpath must be passed.")
|
2013-09-20 01:25:52 +08:00
|
|
|
self._propname = None
|
2013-07-13 03:16:29 +08:00
|
|
|
|
2013-07-25 00:51:53 +08:00
|
|
|
self._is_bool = is_bool
|
2013-07-14 09:49:32 +08:00
|
|
|
self._is_int = is_int
|
2013-07-17 01:04:24 +08:00
|
|
|
self._is_yesno = is_yesno
|
2013-09-02 22:56:22 +08:00
|
|
|
self._is_onoff = is_onoff
|
2014-09-24 02:25:14 +08:00
|
|
|
self._do_abspath = do_abspath
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
self._validate_cb = validate_cb
|
2013-07-14 05:54:46 +08:00
|
|
|
self._convert_value_for_setter_cb = set_converter
|
2013-07-15 05:02:42 +08:00
|
|
|
self._default_cb = default_cb
|
2013-07-15 22:57:16 +08:00
|
|
|
self._default_name = default_name
|
2013-07-15 05:02:42 +08:00
|
|
|
|
2013-07-17 21:57:15 +08:00
|
|
|
if sum([int(bool(i)) for i in
|
2013-09-02 22:56:22 +08:00
|
|
|
[self._is_bool, self._is_int,
|
|
|
|
self._is_yesno, self._is_onoff]]) > 1:
|
2013-07-15 05:02:42 +08:00
|
|
|
raise RuntimeError("Conflict property converter options.")
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2013-07-15 22:57:16 +08:00
|
|
|
if self._default_name and not self._default_cb:
|
|
|
|
raise RuntimeError("default_name requires default_cb.")
|
|
|
|
|
2015-04-23 02:44:52 +08:00
|
|
|
self._is_tracked = False
|
|
|
|
if _trackprops:
|
2013-07-18 05:58:24 +08:00
|
|
|
_allprops.append(self)
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
property.__init__(self, fget=self.getter, fset=self.setter)
|
2013-07-16 21:14:37 +08:00
|
|
|
self.__doc__ = doc
|
2013-07-14 05:54:46 +08:00
|
|
|
|
|
|
|
|
2013-07-13 03:16:29 +08:00
|
|
|
def __repr__(self):
|
2015-09-06 05:20:43 +08:00
|
|
|
return "<XMLProperty %s %s>" % (str(self._xpath), id(self))
|
2013-07-13 03:16:29 +08:00
|
|
|
|
|
|
|
|
2013-07-14 05:54:46 +08:00
|
|
|
####################
|
|
|
|
# Internal helpers #
|
|
|
|
####################
|
|
|
|
|
|
|
|
def _findpropname(self, xmlbuilder):
|
|
|
|
"""
|
|
|
|
Map the raw property() instance to the param name it's exposed
|
|
|
|
as in the XMLBuilder class. This is just for debug purposes.
|
|
|
|
"""
|
2013-09-20 01:25:52 +08:00
|
|
|
if self._propname is None:
|
|
|
|
for key, val in xmlbuilder._all_xml_props().items():
|
|
|
|
if val is self:
|
|
|
|
self._propname = key
|
|
|
|
break
|
|
|
|
if self._propname is None:
|
|
|
|
raise RuntimeError("Didn't find expected property=%s" % self)
|
|
|
|
return self._propname
|
2013-07-12 22:49:15 +08:00
|
|
|
|
2013-09-24 21:25:05 +08:00
|
|
|
def _make_xpath(self, xmlbuilder):
|
2015-09-06 05:20:43 +08:00
|
|
|
return xmlbuilder.fix_relative_xpath(self._xpath)
|
2013-07-15 21:49:46 +08:00
|
|
|
|
2013-07-14 05:54:46 +08:00
|
|
|
|
|
|
|
def _build_node_list(self, xmlbuilder, xpath):
|
|
|
|
"""
|
|
|
|
Build list of nodes that the passed xpaths reference
|
|
|
|
"""
|
2013-07-25 23:02:56 +08:00
|
|
|
nodes = _get_xpath_node(xmlbuilder._xmlstate.xml_ctx, xpath)
|
2013-07-14 05:54:46 +08:00
|
|
|
return util.listify(nodes)
|
|
|
|
|
2013-07-25 00:51:53 +08:00
|
|
|
def _convert_get_value(self, val):
|
2016-04-19 04:42:12 +08:00
|
|
|
# pylint: disable=redefined-variable-type
|
2013-07-25 00:51:53 +08:00
|
|
|
if self._default_name and val == self._default_name:
|
|
|
|
ret = val
|
|
|
|
elif self._is_bool:
|
|
|
|
ret = bool(val)
|
2013-07-15 04:05:59 +08:00
|
|
|
elif self._is_int and val is not None:
|
2013-07-16 09:52:18 +08:00
|
|
|
intkwargs = {}
|
2013-07-15 21:49:46 +08:00
|
|
|
if "0x" in str(val):
|
2013-07-16 09:52:18 +08:00
|
|
|
intkwargs["base"] = 16
|
2013-07-17 21:57:15 +08:00
|
|
|
ret = int(val, **intkwargs)
|
2013-07-17 01:04:24 +08:00
|
|
|
elif self._is_yesno and val is not None:
|
2013-07-17 21:57:15 +08:00
|
|
|
ret = bool(val == "yes")
|
2013-09-02 22:56:22 +08:00
|
|
|
elif self._is_onoff and val is not None:
|
|
|
|
ret = bool(val == "on")
|
2013-07-17 21:57:15 +08:00
|
|
|
else:
|
|
|
|
ret = val
|
|
|
|
return ret
|
2013-07-14 09:04:27 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def _convert_set_value(self, xmlbuilder, val):
|
|
|
|
if self._default_name and val == self._default_name:
|
|
|
|
val = self._default_cb(xmlbuilder)
|
2014-09-24 02:25:14 +08:00
|
|
|
elif self._do_abspath and val is not None:
|
|
|
|
val = os.path.abspath(val)
|
2013-09-02 22:56:22 +08:00
|
|
|
elif self._is_onoff and val is not None:
|
|
|
|
val = bool(val) and "on" or "off"
|
2013-07-26 03:27:42 +08:00
|
|
|
elif self._is_yesno and val is not None:
|
|
|
|
val = bool(val) and "yes" or "no"
|
|
|
|
elif self._is_int and val is not None:
|
|
|
|
intkwargs = {}
|
|
|
|
if "0x" in str(val):
|
|
|
|
intkwargs["base"] = 16
|
|
|
|
val = int(val, **intkwargs)
|
2013-07-25 02:37:07 +08:00
|
|
|
|
|
|
|
if self._convert_value_for_setter_cb:
|
|
|
|
val = self._convert_value_for_setter_cb(xmlbuilder, val)
|
|
|
|
return val
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _prop_is_unset(self, xmlbuilder):
|
2013-07-14 21:31:14 +08:00
|
|
|
propname = self._findpropname(xmlbuilder)
|
2013-07-25 23:02:56 +08:00
|
|
|
return (propname not in xmlbuilder._propstore)
|
2013-07-18 05:58:24 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def _default_get_value(self, xmlbuilder):
|
|
|
|
"""
|
|
|
|
Return (can use default, default value)
|
|
|
|
"""
|
|
|
|
ret = (False, -1)
|
2013-07-26 00:34:37 +08:00
|
|
|
if not xmlbuilder._xmlstate.is_build:
|
|
|
|
return ret
|
2013-07-25 02:37:07 +08:00
|
|
|
if not self._prop_is_unset(xmlbuilder):
|
|
|
|
return ret
|
|
|
|
if not self._default_cb:
|
|
|
|
return ret
|
|
|
|
|
|
|
|
if self._default_name:
|
|
|
|
return (True, self._default_name)
|
|
|
|
return (True, self._default_cb(xmlbuilder))
|
|
|
|
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _set_default(self, xmlbuilder):
|
|
|
|
"""
|
|
|
|
Encode the property default into the XML and propstore, but
|
|
|
|
only if a default is registered, and only if the property was
|
|
|
|
not already explicitly set by the API user.
|
|
|
|
|
|
|
|
This is called during the get_xml_config process and shouldn't
|
|
|
|
be called from outside this file.
|
|
|
|
"""
|
2013-07-25 02:37:07 +08:00
|
|
|
candefault, val = self._default_get_value(xmlbuilder)
|
|
|
|
if not candefault:
|
2013-07-18 05:58:24 +08:00
|
|
|
return
|
2013-07-25 02:37:07 +08:00
|
|
|
self.setter(xmlbuilder, val, validate=False)
|
2013-07-14 21:31:14 +08:00
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _nonxml_fset(self, xmlbuilder, val):
|
2013-07-14 21:31:14 +08:00
|
|
|
"""
|
2013-07-18 05:58:24 +08:00
|
|
|
This stores the value in XMLBuilder._propstore
|
2013-07-14 21:31:14 +08:00
|
|
|
dict as propname->value. This saves us from having to explicitly
|
|
|
|
track every variable.
|
|
|
|
"""
|
2013-07-25 23:02:56 +08:00
|
|
|
propstore = xmlbuilder._propstore
|
|
|
|
proporder = xmlbuilder._proporder
|
2013-07-14 21:31:14 +08:00
|
|
|
|
|
|
|
propname = self._findpropname(xmlbuilder)
|
|
|
|
propstore[propname] = val
|
|
|
|
|
|
|
|
if propname in proporder:
|
|
|
|
proporder.remove(propname)
|
|
|
|
proporder.append(propname)
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _nonxml_fget(self, xmlbuilder):
|
|
|
|
"""
|
|
|
|
The flip side to nonxml_fset, fetch the value from
|
|
|
|
XMLBuilder._propstore
|
|
|
|
"""
|
2013-07-25 02:37:07 +08:00
|
|
|
candefault, val = self._default_get_value(xmlbuilder)
|
|
|
|
if candefault:
|
|
|
|
return val
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
propname = self._findpropname(xmlbuilder)
|
2013-07-25 23:02:56 +08:00
|
|
|
return xmlbuilder._propstore.get(propname, None)
|
2013-07-18 05:58:24 +08:00
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def clear(self, xmlbuilder):
|
2013-07-25 00:35:03 +08:00
|
|
|
self.setter(xmlbuilder, None)
|
2013-07-25 02:37:07 +08:00
|
|
|
self._set_xml(xmlbuilder, None)
|
2013-07-14 21:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
##################################
|
|
|
|
# The actual getter/setter impls #
|
|
|
|
##################################
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def getter(self, xmlbuilder):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Fetch value at user request. If we are parsing existing XML and
|
|
|
|
the user hasn't done a 'set' yet, return the value from the XML,
|
|
|
|
otherwise return the value from propstore
|
2013-07-26 00:34:37 +08:00
|
|
|
|
|
|
|
If this is a built from scratch object, we never pull from XML
|
|
|
|
since it's known to the empty, and we may want to return
|
|
|
|
a 'default' value
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
2015-04-23 02:44:52 +08:00
|
|
|
if _trackprops and not self._is_tracked:
|
|
|
|
_seenprops.append(self)
|
|
|
|
self._is_tracked = True
|
|
|
|
|
2016-04-19 04:42:12 +08:00
|
|
|
# pylint: disable=redefined-variable-type
|
2013-07-26 00:34:37 +08:00
|
|
|
if (self._prop_is_unset(xmlbuilder) and
|
|
|
|
not xmlbuilder._xmlstate.is_build):
|
2013-07-25 02:37:07 +08:00
|
|
|
val = self._get_xml(xmlbuilder)
|
|
|
|
else:
|
|
|
|
val = self._nonxml_fget(xmlbuilder)
|
|
|
|
ret = self._convert_get_value(val)
|
|
|
|
return ret
|
2013-07-14 05:54:46 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def _get_xml(self, xmlbuilder):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Actually fetch the associated value from the backing XML
|
|
|
|
"""
|
2013-09-24 21:25:05 +08:00
|
|
|
xpath = self._make_xpath(xmlbuilder)
|
2013-07-25 23:02:56 +08:00
|
|
|
node = _get_xpath_node(xmlbuilder._xmlstate.xml_ctx, xpath)
|
2013-07-25 00:35:03 +08:00
|
|
|
if not node:
|
2013-07-25 02:37:07 +08:00
|
|
|
return None
|
2013-07-15 04:05:59 +08:00
|
|
|
|
2013-07-25 00:35:03 +08:00
|
|
|
content = node.content
|
|
|
|
if self._is_bool:
|
|
|
|
content = True
|
2013-07-25 02:37:07 +08:00
|
|
|
return content
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def setter(self, xmlbuilder, val, validate=True):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Set the value at user request. This just stores the value
|
|
|
|
in propstore. Setting the actual XML is only done at
|
|
|
|
get_xml_config time.
|
|
|
|
"""
|
2015-04-23 02:44:52 +08:00
|
|
|
if _trackprops and not self._is_tracked:
|
|
|
|
_seenprops.append(self)
|
|
|
|
self._is_tracked = True
|
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
if validate and self._validate_cb:
|
|
|
|
self._validate_cb(xmlbuilder, val)
|
|
|
|
self._nonxml_fset(xmlbuilder,
|
|
|
|
self._convert_set_value(xmlbuilder, val))
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def _set_xml(self, xmlbuilder, setval, root_node=None):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Actually set the passed value in the XML document
|
|
|
|
"""
|
2013-07-25 02:37:07 +08:00
|
|
|
if root_node is None:
|
2013-07-25 23:02:56 +08:00
|
|
|
root_node = xmlbuilder._xmlstate.xml_node
|
2014-01-26 03:41:55 +08:00
|
|
|
ctx = xmlbuilder._xmlstate.xml_ctx
|
|
|
|
else:
|
|
|
|
ctx = _make_xml_context(root_node)
|
|
|
|
|
2013-09-24 21:25:05 +08:00
|
|
|
xpath = self._make_xpath(xmlbuilder)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-12-05 04:27:30 +08:00
|
|
|
if setval is None or setval is False:
|
|
|
|
_remove_xpath_node(ctx, xpath)
|
|
|
|
return
|
2013-07-14 06:04:49 +08:00
|
|
|
|
2016-05-21 01:29:16 +08:00
|
|
|
node = _get_xpath_node(ctx, xpath)
|
2014-12-05 04:27:30 +08:00
|
|
|
if not node:
|
2016-05-21 01:29:16 +08:00
|
|
|
node = _build_xpath_node(ctx, xpath)
|
2013-07-15 04:01:38 +08:00
|
|
|
|
2014-12-05 04:27:30 +08:00
|
|
|
if setval is True:
|
|
|
|
# Boolean property, creating the node is enough
|
|
|
|
return
|
2013-07-15 04:01:38 +08:00
|
|
|
|
2014-12-05 04:27:30 +08:00
|
|
|
node.setContent(util.xml_escape(str(setval)))
|
2013-07-15 04:01:38 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
class _XMLState(object):
|
2013-09-11 23:47:09 +08:00
|
|
|
def __init__(self, root_name, parsexml, parsexmlnode,
|
|
|
|
parent_xpath, relative_object_xpath):
|
|
|
|
self.root_name = root_name
|
|
|
|
self.stub_path = "/%s" % self.root_name
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
self.xml_ctx = None
|
|
|
|
self.xml_node = None
|
|
|
|
self.xml_root_doc = None
|
2013-09-11 22:16:12 +08:00
|
|
|
|
|
|
|
# xpath of this object relative to its parent. So for a standalone
|
|
|
|
# <disk> this is empty, but if the disk is the forth one in a <domain>
|
|
|
|
# it will be set to ./devices/disk[4]
|
2013-09-11 23:47:09 +08:00
|
|
|
self._relative_object_xpath = relative_object_xpath or ""
|
2013-09-11 22:16:12 +08:00
|
|
|
|
|
|
|
# xpath of the parent. For a disk in a standalone <domain>, this
|
|
|
|
# is empty, but if the <domain> is part of a <domainsnapshot>,
|
|
|
|
# it will be "./domain"
|
2013-09-11 23:47:09 +08:00
|
|
|
self._parent_xpath = parent_xpath or ""
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
self.is_build = False
|
2013-09-11 06:32:10 +08:00
|
|
|
if not parsexml and not parsexmlnode:
|
2013-07-25 23:02:56 +08:00
|
|
|
self.is_build = True
|
|
|
|
self._parse(parsexml, parsexmlnode)
|
|
|
|
|
|
|
|
def _parse(self, xml, node):
|
2013-09-11 06:32:10 +08:00
|
|
|
if node:
|
|
|
|
self.xml_root_doc = None
|
|
|
|
self.xml_node = node
|
|
|
|
self.is_build = (getattr(node, "virtinst_is_build", False) or
|
|
|
|
self.is_build)
|
|
|
|
else:
|
|
|
|
if not xml:
|
|
|
|
xml = self.make_xml_stub()
|
2014-02-26 01:45:15 +08:00
|
|
|
|
|
|
|
try:
|
|
|
|
doc = libxml2.parseDoc(xml)
|
|
|
|
except:
|
|
|
|
logging.debug("Error parsing xml=\n%s", xml)
|
|
|
|
raise
|
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
self.xml_root_doc = _DocCleanupWrapper(doc)
|
|
|
|
self.xml_node = doc.children
|
|
|
|
self.xml_node.virtinst_is_build = self.is_build
|
2013-09-11 23:47:09 +08:00
|
|
|
self.xml_node.virtinst_node_top_xpath = self.stub_path
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
# This just stores a reference to our root doc wrapper in
|
|
|
|
# the root node, so when the node goes away it triggers
|
|
|
|
# auto free'ing of the doc
|
|
|
|
self.xml_node.virtinst_root_doc = self.xml_root_doc
|
|
|
|
|
|
|
|
self.xml_ctx = _make_xml_context(self.xml_node)
|
|
|
|
|
|
|
|
def make_xml_stub(self):
|
2013-09-11 23:47:09 +08:00
|
|
|
return "<%s/>" % self.root_name
|
2013-07-25 23:02:56 +08:00
|
|
|
|
2013-09-11 22:16:12 +08:00
|
|
|
def set_relative_object_xpath(self, xpath):
|
|
|
|
self._relative_object_xpath = xpath or ""
|
|
|
|
|
|
|
|
def set_parent_xpath(self, xpath):
|
|
|
|
self._parent_xpath = xpath or ""
|
|
|
|
|
|
|
|
def get_root_xpath(self):
|
2013-09-11 23:47:09 +08:00
|
|
|
relpath = self._relative_object_xpath
|
|
|
|
if not self._parent_xpath:
|
|
|
|
return relpath
|
|
|
|
return self._parent_xpath + (relpath.startswith(".") and
|
|
|
|
relpath[1:] or relpath)
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
def fix_relative_xpath(self, xpath):
|
2013-09-11 22:16:12 +08:00
|
|
|
fullpath = self.get_root_xpath()
|
2013-09-11 23:47:09 +08:00
|
|
|
if not fullpath or fullpath == self.stub_path:
|
2013-07-25 23:02:56 +08:00
|
|
|
return xpath
|
2013-09-11 22:16:12 +08:00
|
|
|
if xpath.startswith("."):
|
|
|
|
return "%s%s" % (fullpath, xpath.strip("."))
|
|
|
|
if xpath.count("/") == 1:
|
|
|
|
return fullpath
|
|
|
|
return fullpath + "/" + xpath.split("/", 2)[2]
|
|
|
|
|
|
|
|
def get_node_top_xpath(self):
|
|
|
|
"""
|
|
|
|
Return the XML path of the root xml_node
|
|
|
|
"""
|
|
|
|
return self.xml_node.virtinst_node_top_xpath
|
|
|
|
|
2014-01-26 03:18:54 +08:00
|
|
|
def get_node_xml(self, ctx):
|
2013-09-11 23:47:09 +08:00
|
|
|
node = _get_xpath_node(ctx, self.fix_relative_xpath("."))
|
2013-07-25 23:02:56 +08:00
|
|
|
if not node:
|
|
|
|
return ""
|
|
|
|
return _sanitize_libxml_xml(node.serialize())
|
|
|
|
|
|
|
|
|
2013-07-14 06:56:09 +08:00
|
|
|
class XMLBuilder(object):
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
|
|
|
Base for all classes which build or parse domain XML
|
|
|
|
"""
|
2013-07-17 00:48:52 +08:00
|
|
|
# Order that we should apply values to the XML. Keeps XML generation
|
|
|
|
# consistent with what the test suite expects.
|
2013-07-15 21:49:46 +08:00
|
|
|
_XML_PROP_ORDER = []
|
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
# Name of the root XML element
|
|
|
|
_XML_ROOT_NAME = None
|
2013-07-18 05:58:24 +08:00
|
|
|
|
2015-03-27 06:04:23 +08:00
|
|
|
# In some cases, libvirt can incorrectly generate unparseable XML.
|
|
|
|
# These are libvirt bugs, but this allows us to work around it in
|
|
|
|
# for specific XML classes.
|
|
|
|
#
|
|
|
|
# Example: nodedev 'system' XML:
|
|
|
|
# https://bugzilla.redhat.com/show_bug.cgi?id=1184131
|
|
|
|
_XML_SANITIZE = False
|
|
|
|
|
2016-08-25 03:55:08 +08:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def xml_indent(xmlstr, level):
|
|
|
|
"""
|
|
|
|
Indent the passed str the specified number of spaces
|
|
|
|
"""
|
|
|
|
xml = ""
|
|
|
|
if not xmlstr:
|
|
|
|
return xml
|
|
|
|
if not level:
|
|
|
|
return xmlstr
|
|
|
|
return "\n".join((" " * level + l) for l in xmlstr.splitlines())
|
|
|
|
|
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def __init__(self, conn, parsexml=None, parsexmlnode=None,
|
|
|
|
parent_xpath=None, relative_object_xpath=None):
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
|
|
|
Initialize state
|
|
|
|
|
|
|
|
@param conn: libvirt connection to validate device against
|
2013-07-05 20:59:58 +08:00
|
|
|
@type conn: VirtualConnection
|
2013-03-18 05:06:52 +08:00
|
|
|
@param parsexml: Optional XML string to parse
|
|
|
|
@type parsexml: C{str}
|
2013-09-11 23:47:09 +08:00
|
|
|
|
|
|
|
The rest of the parameters are for internal use only
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
2013-07-24 20:46:55 +08:00
|
|
|
self.conn = conn
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2015-03-27 06:04:23 +08:00
|
|
|
if self._XML_SANITIZE:
|
|
|
|
parsexml = parsexml.decode('ascii', 'ignore').encode('ascii')
|
|
|
|
parsexml = "".join([c for c in parsexml if c in string.printable])
|
|
|
|
|
2013-07-12 22:49:15 +08:00
|
|
|
self._propstore = {}
|
|
|
|
self._proporder = []
|
2013-09-11 23:47:09 +08:00
|
|
|
self._xmlstate = _XMLState(self._XML_ROOT_NAME,
|
|
|
|
parsexml, parsexmlnode,
|
|
|
|
parent_xpath, relative_object_xpath)
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2013-09-20 01:25:52 +08:00
|
|
|
self._initial_child_parse()
|
|
|
|
|
|
|
|
def _initial_child_parse(self):
|
2013-09-11 22:16:12 +08:00
|
|
|
# Walk the XML tree and hand of parsing to any registered
|
|
|
|
# child classes
|
|
|
|
for xmlprop in self._all_child_props().values():
|
2013-09-11 23:47:09 +08:00
|
|
|
if xmlprop.is_single:
|
|
|
|
child_class = xmlprop.child_classes[0]
|
|
|
|
prop_path = xmlprop.get_prop_xpath(self, child_class)
|
|
|
|
obj = child_class(self.conn,
|
2013-09-20 23:30:33 +08:00
|
|
|
parsexmlnode=self._xmlstate.xml_node,
|
2013-09-11 23:47:09 +08:00
|
|
|
parent_xpath=self.get_root_xpath(),
|
|
|
|
relative_object_xpath=prop_path)
|
|
|
|
xmlprop.set(self, obj)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if self._xmlstate.is_build:
|
|
|
|
continue
|
|
|
|
|
2013-09-11 22:16:12 +08:00
|
|
|
for child_class in xmlprop.child_classes:
|
|
|
|
prop_path = xmlprop.get_prop_xpath(self, child_class)
|
|
|
|
|
2013-09-20 23:30:33 +08:00
|
|
|
nodecount = int(self._xmlstate.xml_node.xpathEval(
|
2013-09-11 23:47:09 +08:00
|
|
|
"count(%s)" % self.fix_relative_xpath(prop_path)))
|
|
|
|
for idx in range(nodecount):
|
|
|
|
idxstr = "[%d]" % (idx + 1)
|
|
|
|
obj = child_class(self.conn,
|
2013-09-20 23:30:33 +08:00
|
|
|
parsexmlnode=self._xmlstate.xml_node,
|
2013-09-11 23:47:09 +08:00
|
|
|
parent_xpath=self.get_root_xpath(),
|
|
|
|
relative_object_xpath=(prop_path + idxstr))
|
|
|
|
xmlprop.append(self, obj)
|
2013-09-20 01:25:52 +08:00
|
|
|
|
2013-09-11 22:16:12 +08:00
|
|
|
self._set_child_xpaths()
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
########################
|
|
|
|
# Public XML Internals #
|
|
|
|
########################
|
2013-07-15 06:31:33 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def copy(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Do a shallow copy of the device
|
|
|
|
"""
|
2013-07-14 09:04:27 +08:00
|
|
|
ret = copy.copy(self)
|
|
|
|
ret._propstore = ret._propstore.copy()
|
|
|
|
ret._proporder = ret._proporder[:]
|
2013-09-11 06:32:10 +08:00
|
|
|
|
|
|
|
# XMLChildProperty stores a list in propstore, which dict shallow
|
|
|
|
# copy won't fix for us.
|
|
|
|
for name, value in ret._propstore.items():
|
|
|
|
if type(value) is not list:
|
|
|
|
continue
|
|
|
|
ret._propstore[name] = [obj.copy() for obj in ret._propstore[name]]
|
|
|
|
|
2013-07-14 09:04:27 +08:00
|
|
|
return ret
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-07-24 20:46:55 +08:00
|
|
|
def get_root_xpath(self):
|
2013-09-11 22:16:12 +08:00
|
|
|
return self._xmlstate.get_root_xpath()
|
2013-07-24 20:46:55 +08:00
|
|
|
|
|
|
|
def fix_relative_xpath(self, xpath):
|
2013-07-25 23:02:56 +08:00
|
|
|
return self._xmlstate.fix_relative_xpath(xpath)
|
|
|
|
|
|
|
|
|
|
|
|
############################
|
|
|
|
# Public XML managing APIs #
|
|
|
|
############################
|
2013-07-24 20:46:55 +08:00
|
|
|
|
2013-07-24 23:32:30 +08:00
|
|
|
def get_xml_config(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Return XML string of the object
|
|
|
|
"""
|
2013-07-24 20:46:55 +08:00
|
|
|
data = self._prepare_get_xml()
|
|
|
|
try:
|
2013-07-25 00:02:37 +08:00
|
|
|
return self._do_get_xml_config()
|
2013-07-24 20:46:55 +08:00
|
|
|
finally:
|
|
|
|
self._finish_get_xml(data)
|
|
|
|
|
2016-05-21 02:23:39 +08:00
|
|
|
def clear(self, leave_stub=False):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Wipe out all properties of the object
|
2016-05-21 02:23:39 +08:00
|
|
|
|
|
|
|
:param leave_stub: if True, don't unlink the top stub node,
|
|
|
|
see virtinst/cli usage for an explanation
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
2016-05-21 02:23:39 +08:00
|
|
|
global _top_node
|
|
|
|
old_top_node = _top_node
|
|
|
|
try:
|
|
|
|
if leave_stub:
|
|
|
|
_top_node = _get_xpath_node(self._xmlstate.xml_ctx,
|
|
|
|
self.get_root_xpath())
|
|
|
|
props = self._all_xml_props().values()
|
|
|
|
props += self._all_child_props().values()
|
|
|
|
for prop in props:
|
|
|
|
prop.clear(self)
|
|
|
|
finally:
|
|
|
|
_top_node = old_top_node
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2015-09-05 05:03:31 +08:00
|
|
|
is_child = bool(re.match("^.*\[\d+\]$", self.get_root_xpath()))
|
2016-05-21 02:23:39 +08:00
|
|
|
if is_child or leave_stub:
|
2015-09-05 05:03:31 +08:00
|
|
|
# User requested to clear an object that is the child of
|
|
|
|
# another object (xpath ends in [1] etc). We can't fully remove
|
|
|
|
# the node in that case, since then the xmlbuilder object is
|
|
|
|
# no longer valid, and all the other child xpaths will be
|
|
|
|
# pointing to the wrong node. So just stub out the content
|
|
|
|
node = _get_xpath_node(self._xmlstate.xml_ctx,
|
|
|
|
self.get_root_xpath())
|
|
|
|
indent = 2 * self.get_root_xpath().count("/")
|
2016-05-21 02:23:39 +08:00
|
|
|
if node:
|
|
|
|
node.setContent("\n" + (indent * " "))
|
2015-09-05 05:03:31 +08:00
|
|
|
else:
|
|
|
|
_remove_xpath_node(self._xmlstate.xml_ctx,
|
|
|
|
self.get_root_xpath())
|
2014-01-26 03:57:10 +08:00
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
def validate(self):
|
|
|
|
"""
|
|
|
|
Validate any set values and raise an exception if there's
|
|
|
|
a problem
|
|
|
|
"""
|
|
|
|
pass
|
2013-07-15 06:31:33 +08:00
|
|
|
|
2013-10-07 01:17:35 +08:00
|
|
|
def set_defaults(self, guest):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Encode any default values if needed
|
|
|
|
"""
|
2013-10-07 01:17:35 +08:00
|
|
|
ignore = guest
|
2013-07-15 06:31:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
###################
|
|
|
|
# Child overrides #
|
|
|
|
###################
|
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _prepare_get_xml(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Subclasses can override this to do any pre-get_xml_config setup.
|
|
|
|
Returns data to pass to finish_get_xml
|
|
|
|
"""
|
2013-07-18 05:58:24 +08:00
|
|
|
return None
|
2013-07-25 23:02:56 +08:00
|
|
|
|
2013-07-18 05:58:24 +08:00
|
|
|
def _finish_get_xml(self, data):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Called after get_xml_config. Data is whatever was returned by
|
|
|
|
_prepare_get_xml
|
|
|
|
"""
|
2013-07-18 05:58:24 +08:00
|
|
|
ignore = data
|
|
|
|
|
2013-07-15 06:31:33 +08:00
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
################
|
|
|
|
# Internal API #
|
|
|
|
################
|
|
|
|
|
2013-09-23 19:37:33 +08:00
|
|
|
def __get_prop_cache(self, cachename, checkclass):
|
|
|
|
if not hasattr(self.__class__, cachename):
|
2013-09-20 23:30:33 +08:00
|
|
|
ret = {}
|
|
|
|
for c in reversed(type.mro(self.__class__)[:-1]):
|
|
|
|
for key, val in c.__dict__.items():
|
2013-09-23 06:13:24 +08:00
|
|
|
if isinstance(val, checkclass):
|
2013-09-20 23:30:33 +08:00
|
|
|
ret[key] = val
|
2013-09-23 19:37:33 +08:00
|
|
|
setattr(self.__class__, cachename, ret)
|
|
|
|
return getattr(self.__class__, cachename)
|
|
|
|
|
|
|
|
def _all_xml_props(self):
|
|
|
|
"""
|
|
|
|
Return a list of all XMLProperty instances that this class has.
|
|
|
|
"""
|
|
|
|
cachename = self.__class__.__name__ + "_cached_xml_props"
|
|
|
|
return self.__get_prop_cache(cachename, XMLProperty)
|
2013-07-25 23:02:56 +08:00
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
def _all_child_props(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
2013-09-11 06:32:10 +08:00
|
|
|
Return a list of all XMLChildProperty instances that this class has.
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
2013-09-23 19:37:33 +08:00
|
|
|
cachename = self.__class__.__name__ + "_cached_child_props"
|
|
|
|
return self.__get_prop_cache(cachename, XMLChildProperty)
|
2013-09-20 23:30:33 +08:00
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-09-11 22:16:12 +08:00
|
|
|
def _set_parent_xpath(self, xpath):
|
|
|
|
self._xmlstate.set_parent_xpath(xpath)
|
2013-09-20 01:25:52 +08:00
|
|
|
for propname in self._all_child_props():
|
|
|
|
for p in util.listify(getattr(self, propname, [])):
|
|
|
|
p._set_parent_xpath(self.get_root_xpath())
|
2013-09-11 22:16:12 +08:00
|
|
|
|
|
|
|
def _set_relative_object_xpath(self, xpath):
|
|
|
|
self._xmlstate.set_relative_object_xpath(xpath)
|
2013-09-20 01:25:52 +08:00
|
|
|
for propname in self._all_child_props():
|
|
|
|
for p in util.listify(getattr(self, propname, [])):
|
|
|
|
p._set_parent_xpath(self.get_root_xpath())
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2015-09-05 05:03:31 +08:00
|
|
|
def _find_child_prop(self, child_class, return_single=False):
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprops = self._all_child_props()
|
|
|
|
for xmlprop in xmlprops.values():
|
2015-09-05 05:03:31 +08:00
|
|
|
if xmlprop.is_single and not return_single:
|
2013-09-11 23:47:09 +08:00
|
|
|
continue
|
2013-09-11 06:32:10 +08:00
|
|
|
if child_class in xmlprop.child_classes:
|
|
|
|
return xmlprop
|
|
|
|
raise RuntimeError("programming error: "
|
|
|
|
"Didn't find child property for child_class=%s" %
|
|
|
|
child_class)
|
|
|
|
|
2014-01-28 08:54:41 +08:00
|
|
|
def _parse_with_children(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Set new backing XML objects in ourselves and all our child props
|
|
|
|
"""
|
|
|
|
self._xmlstate._parse(*args, **kwargs)
|
|
|
|
for propname in self._all_child_props():
|
|
|
|
for p in util.listify(getattr(self, propname, [])):
|
|
|
|
p._xmlstate._parse(None, self._xmlstate.xml_node)
|
|
|
|
|
2015-09-05 03:45:45 +08:00
|
|
|
def add_child(self, obj):
|
2013-09-11 06:32:10 +08:00
|
|
|
"""
|
|
|
|
Insert the passed XMLBuilder object into our XML document. The
|
|
|
|
object needs to have an associated mapping via XMLChildProperty
|
2015-09-05 03:45:45 +08:00
|
|
|
or an error is thrown.
|
2013-09-11 06:32:10 +08:00
|
|
|
"""
|
|
|
|
xmlprop = self._find_child_prop(obj.__class__)
|
2013-09-11 22:16:12 +08:00
|
|
|
xml = obj.get_xml_config()
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprop.append(self, obj)
|
|
|
|
self._set_child_xpaths()
|
|
|
|
|
|
|
|
if not obj._xmlstate.is_build:
|
2013-09-11 22:16:12 +08:00
|
|
|
use_xpath = obj.get_root_xpath().rsplit("/", 1)[0]
|
2013-09-10 05:14:16 +08:00
|
|
|
indent = 2 * obj.get_root_xpath().count("/")
|
2016-08-25 03:55:08 +08:00
|
|
|
newnode = libxml2.parseDoc(self.xml_indent(xml, indent)).children
|
2016-07-19 01:34:06 +08:00
|
|
|
parentnode = _build_xpath_node(self._xmlstate.xml_ctx, use_xpath)
|
|
|
|
# Tack newnode on the end
|
|
|
|
_add_pretty_child(parentnode, newnode)
|
2014-01-28 08:54:41 +08:00
|
|
|
obj._parse_with_children(None, self._xmlstate.xml_node)
|
2013-09-10 07:45:19 +08:00
|
|
|
|
2015-09-05 03:45:45 +08:00
|
|
|
def remove_child(self, obj):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Remove the passed XMLBuilder object from our XML document, but
|
2015-09-05 03:45:45 +08:00
|
|
|
ensure its data isn't altered.
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprop = self._find_child_prop(obj.__class__)
|
|
|
|
xmlprop.remove(self, obj)
|
|
|
|
|
|
|
|
xpath = obj.get_root_xpath()
|
|
|
|
xml = obj.get_xml_config()
|
2013-09-11 22:16:12 +08:00
|
|
|
obj._set_parent_xpath(None)
|
|
|
|
obj._set_relative_object_xpath(None)
|
2014-01-28 08:54:41 +08:00
|
|
|
obj._parse_with_children(xml, None)
|
2013-09-11 06:32:10 +08:00
|
|
|
_remove_xpath_node(self._xmlstate.xml_ctx, xpath, dofree=False)
|
|
|
|
self._set_child_xpaths()
|
|
|
|
|
2015-09-05 04:07:01 +08:00
|
|
|
def list_children_for_class(self, klass):
|
|
|
|
"""
|
|
|
|
Return a list of all XML child objects with the passed class
|
|
|
|
"""
|
|
|
|
ret = []
|
|
|
|
for prop in self._all_child_props().values():
|
|
|
|
ret += [obj for obj in util.listify(prop._get(self))
|
|
|
|
if obj.__class__ == klass]
|
|
|
|
return ret
|
|
|
|
|
2015-09-05 05:03:31 +08:00
|
|
|
def child_class_is_singleton(self, klass):
|
|
|
|
"""
|
|
|
|
Return True if the passed class is registered as a singleton
|
|
|
|
child property
|
|
|
|
"""
|
|
|
|
return self._find_child_prop(klass, return_single=True).is_single
|
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
#################################
|
|
|
|
# Private XML building routines #
|
|
|
|
#################################
|
2013-07-25 02:37:07 +08:00
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
def _set_child_xpaths(self):
|
|
|
|
"""
|
|
|
|
Walk the list of child properties and make sure their
|
|
|
|
xpaths point at their particular element
|
|
|
|
"""
|
|
|
|
typecount = {}
|
2013-09-11 22:16:12 +08:00
|
|
|
for propname, xmlprop in self._all_child_props().items():
|
2013-09-11 23:47:09 +08:00
|
|
|
for obj in util.listify(getattr(self, propname)):
|
|
|
|
idxstr = ""
|
|
|
|
if not xmlprop.is_single:
|
|
|
|
class_type = obj.__class__
|
|
|
|
if class_type not in typecount:
|
|
|
|
typecount[class_type] = 0
|
|
|
|
typecount[class_type] += 1
|
|
|
|
idxstr = "[%d]" % typecount[class_type]
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-09-11 22:16:12 +08:00
|
|
|
prop_path = xmlprop.get_prop_xpath(self, obj)
|
|
|
|
obj._set_parent_xpath(self.get_root_xpath())
|
2013-09-11 23:47:09 +08:00
|
|
|
obj._set_relative_object_xpath(prop_path + idxstr)
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2013-07-25 02:37:07 +08:00
|
|
|
def _do_get_xml_config(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
xmlstub = self._xmlstate.make_xml_stub()
|
2013-07-25 02:37:07 +08:00
|
|
|
|
|
|
|
try:
|
|
|
|
node = None
|
2014-01-26 03:18:54 +08:00
|
|
|
ctx = self._xmlstate.xml_ctx
|
2013-07-26 00:34:37 +08:00
|
|
|
if self._xmlstate.is_build:
|
2013-07-25 23:02:56 +08:00
|
|
|
node = self._xmlstate.xml_node.docCopyNodeList(
|
|
|
|
self._xmlstate.xml_node.doc)
|
2014-01-26 03:18:54 +08:00
|
|
|
ctx = node
|
|
|
|
|
|
|
|
self._add_parse_bits(node)
|
|
|
|
ret = self._xmlstate.get_node_xml(ctx)
|
2013-07-25 02:37:07 +08:00
|
|
|
finally:
|
|
|
|
if node:
|
|
|
|
node.freeNode()
|
|
|
|
|
|
|
|
if ret == xmlstub:
|
|
|
|
ret = ""
|
|
|
|
|
2013-09-20 01:48:28 +08:00
|
|
|
# Ensure top level XML object always ends with a newline, just
|
|
|
|
# for back compat and readability
|
|
|
|
if (ret and not self.get_root_xpath() and not ret.endswith("\n")):
|
2013-07-25 02:37:07 +08:00
|
|
|
ret += "\n"
|
|
|
|
return ret
|
|
|
|
|
2014-01-26 03:18:54 +08:00
|
|
|
def _add_parse_bits(self, node):
|
2013-07-24 20:46:55 +08:00
|
|
|
"""
|
2013-07-25 23:02:56 +08:00
|
|
|
Callback that adds the implicitly tracked XML properties to
|
|
|
|
the backing xml.
|
2013-07-24 20:46:55 +08:00
|
|
|
"""
|
2013-07-25 23:02:56 +08:00
|
|
|
origproporder = self._proporder[:]
|
|
|
|
origpropstore = self._propstore.copy()
|
|
|
|
try:
|
2014-01-26 03:18:54 +08:00
|
|
|
return self._do_add_parse_bits(node)
|
2013-07-25 23:02:56 +08:00
|
|
|
finally:
|
|
|
|
self._proporder = origproporder
|
|
|
|
self._propstore = origpropstore
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-01-26 03:18:54 +08:00
|
|
|
def _do_add_parse_bits(self, node):
|
2013-07-18 05:58:24 +08:00
|
|
|
# Set all defaults if the properties have one registered
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprops = self._all_xml_props()
|
2013-09-20 23:17:11 +08:00
|
|
|
childprops = self._all_child_props()
|
2013-07-12 22:49:15 +08:00
|
|
|
|
2013-07-26 00:34:37 +08:00
|
|
|
for prop in xmlprops.values():
|
|
|
|
prop._set_default(self)
|
2013-07-15 05:02:42 +08:00
|
|
|
|
|
|
|
# Set up preferred XML ordering
|
|
|
|
do_order = self._proporder[:]
|
2013-07-15 21:49:46 +08:00
|
|
|
for key in reversed(self._XML_PROP_ORDER):
|
2013-09-20 23:17:11 +08:00
|
|
|
if key not in xmlprops and key not in childprops:
|
|
|
|
raise RuntimeError("programming error: key '%s' must be "
|
|
|
|
"xml prop or child prop" % key)
|
2013-07-15 05:02:42 +08:00
|
|
|
if key in do_order:
|
|
|
|
do_order.remove(key)
|
|
|
|
do_order.insert(0, key)
|
2013-09-20 23:17:11 +08:00
|
|
|
elif key in childprops:
|
2013-07-18 05:58:24 +08:00
|
|
|
do_order.insert(0, key)
|
2013-07-15 05:02:42 +08:00
|
|
|
|
2013-09-20 23:17:11 +08:00
|
|
|
for key in childprops.keys():
|
|
|
|
if key not in do_order:
|
|
|
|
do_order.append(key)
|
|
|
|
|
2013-07-15 05:02:42 +08:00
|
|
|
# Alter the XML
|
|
|
|
for key in do_order:
|
2013-07-18 05:58:24 +08:00
|
|
|
if key in xmlprops:
|
2013-07-25 02:37:07 +08:00
|
|
|
xmlprops[key]._set_xml(self, self._propstore[key], node)
|
2013-09-20 23:17:11 +08:00
|
|
|
elif key in childprops:
|
|
|
|
for obj in util.listify(getattr(self, key)):
|
2014-01-26 03:18:54 +08:00
|
|
|
obj._add_parse_bits(node)
|
2017-02-21 21:00:51 +08:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<XMLBuilder %s %s>" % (self._XML_ROOT_NAME, id(self))
|