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>
|
|
|
|
#
|
2018-04-04 21:35:41 +08:00
|
|
|
# This work is licensed under the GNU GPLv2 or later.
|
2018-03-21 03:00:02 +08:00
|
|
|
# See the COPYING file in the top-level directory.
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-03-20 23:06:23 +08:00
|
|
|
import collections
|
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
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
from .xmlapi import XMLAPI
|
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 = []
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
class _XMLPropertyCache(object):
|
|
|
|
"""
|
|
|
|
Cache lookup tables mapping classes to their associated
|
|
|
|
XMLProperty and XMLChildProperty classes
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
self._name_to_prop = {}
|
|
|
|
self._prop_to_name = {}
|
|
|
|
|
|
|
|
def _get_prop_cache(self, cls, checkclass):
|
|
|
|
cachename = str(cls) + "-" + checkclass.__name__
|
|
|
|
if cachename not in self._name_to_prop:
|
|
|
|
ret = {}
|
|
|
|
for c in reversed(type.mro(cls)[:-1]):
|
|
|
|
for key, val in c.__dict__.items():
|
|
|
|
if isinstance(val, checkclass):
|
|
|
|
ret[key] = val
|
|
|
|
self._prop_to_name[val] = key
|
|
|
|
self._name_to_prop[cachename] = ret
|
|
|
|
return self._name_to_prop[cachename]
|
|
|
|
|
|
|
|
def get_xml_props(self, inst):
|
|
|
|
return self._get_prop_cache(inst.__class__, XMLProperty)
|
|
|
|
|
|
|
|
def get_child_props(self, inst):
|
|
|
|
return self._get_prop_cache(inst.__class__, XMLChildProperty)
|
|
|
|
|
|
|
|
def get_prop_name(self, propinst):
|
|
|
|
return self._prop_to_name[propinst]
|
|
|
|
|
|
|
|
|
|
|
|
_PropCache = _XMLPropertyCache()
|
|
|
|
|
|
|
|
|
2018-02-08 06:27:56 +08:00
|
|
|
class _XMLChildList(list):
|
|
|
|
"""
|
|
|
|
Little wrapper for a list containing XMLChildProperty output.
|
|
|
|
This is just to insert a dynamically created add_new() function
|
|
|
|
which instantiates and appends a new child object
|
|
|
|
"""
|
|
|
|
def __init__(self, childclass, copylist, xmlbuilder):
|
|
|
|
list.__init__(self)
|
|
|
|
self._childclass = childclass
|
|
|
|
self._xmlbuilder = xmlbuilder
|
|
|
|
for i in copylist:
|
|
|
|
self.append(i)
|
|
|
|
|
|
|
|
def new(self):
|
|
|
|
"""
|
|
|
|
Instantiate a new child object and return it
|
|
|
|
"""
|
|
|
|
return self._childclass(self._xmlbuilder.conn)
|
|
|
|
|
|
|
|
def add_new(self):
|
|
|
|
"""
|
|
|
|
Instantiate a new child object, append it, and return it
|
|
|
|
"""
|
|
|
|
obj = self.new()
|
|
|
|
self._xmlbuilder.add_child(obj)
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
class _XMLPropertyBase(property):
|
|
|
|
def __init__(self, fget, fset):
|
|
|
|
self._propname = None
|
|
|
|
property.__init__(self, fget=fget, fset=fset)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def propname(self):
|
|
|
|
"""
|
|
|
|
The variable name associated with this XMLProperty. So with
|
|
|
|
a definition like
|
|
|
|
|
|
|
|
foo = XMLProperty("./@bar")
|
|
|
|
|
|
|
|
and this will return "foo".
|
|
|
|
"""
|
|
|
|
if not self._propname:
|
|
|
|
self._propname = _PropCache.get_prop_name(self)
|
|
|
|
return self._propname
|
|
|
|
|
|
|
|
|
|
|
|
class XMLChildProperty(_XMLPropertyBase):
|
2013-09-11 06:32:10 +08:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
2018-03-21 18:01:28 +08:00
|
|
|
@child_class: XMLBuilder class this property is tracking. So for
|
|
|
|
guest.devices.disk this is DeviceDisk
|
2013-09-11 22:16:12 +08:00
|
|
|
@relative_xpath: Relative location where the class is rooted compared
|
2018-03-21 22:49:29 +08:00
|
|
|
to its xmlbuilder root path. So if xmlbuilder is ./foo and we
|
|
|
|
want to track ./foo/bar/baz instances, set relative_xpath=./bar
|
|
|
|
@is_single: If True, this represents an XML node that is only expected
|
|
|
|
to appear once, like <domain><cpu>
|
2013-09-11 06:32:10 +08:00
|
|
|
"""
|
2018-03-21 18:01:28 +08:00
|
|
|
def __init__(self, child_class, relative_xpath=".", is_single=False):
|
|
|
|
self.child_class = child_class
|
2013-09-11 23:47:09 +08:00
|
|
|
self.is_single = is_single
|
2018-03-21 22:49:29 +08:00
|
|
|
self.relative_xpath = relative_xpath
|
2013-09-11 23:47:09 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
_XMLPropertyBase.__init__(self, self._fget, None)
|
2013-09-11 06:32:10 +08:00
|
|
|
|
|
|
|
def __repr__(self):
|
2018-03-21 18:01:28 +08:00
|
|
|
return "<XMLChildProperty %s %s>" % (str(self.child_class), id(self))
|
2013-09-11 06:32:10 +08:00
|
|
|
|
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def _get(self, xmlbuilder):
|
2018-03-20 23:56:12 +08:00
|
|
|
if self.propname not in xmlbuilder._propstore and not self.is_single:
|
|
|
|
xmlbuilder._propstore[self.propname] = []
|
|
|
|
return xmlbuilder._propstore[self.propname]
|
2013-09-11 06:32:10 +08:00
|
|
|
|
|
|
|
def _fget(self, xmlbuilder):
|
2013-09-11 23:47:09 +08:00
|
|
|
if self.is_single:
|
|
|
|
return self._get(xmlbuilder)
|
2018-03-21 18:01:28 +08:00
|
|
|
return _XMLChildList(self.child_class,
|
2018-02-08 06:27:56 +08:00
|
|
|
self._get(xmlbuilder),
|
|
|
|
xmlbuilder)
|
2013-09-11 23:47:09 +08:00
|
|
|
|
|
|
|
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):
|
2018-03-21 18:01:28 +08:00
|
|
|
self._get(xmlbuilder).append(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):
|
2018-03-20 23:56:12 +08:00
|
|
|
xmlbuilder._propstore[self.propname] = obj
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2018-03-21 22:49:29 +08:00
|
|
|
def get_prop_xpath(self, _xmlbuilder, obj):
|
2018-03-21 22:53:34 +08:00
|
|
|
return self.relative_xpath + "/" + obj.XML_NAME
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2013-09-11 06:32:10 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
class XMLProperty(_XMLPropertyBase):
|
2018-02-23 09:44:09 +08:00
|
|
|
def __init__(self, xpath,
|
2013-09-03 14:47:26 +08:00
|
|
|
is_bool=False, is_int=False, is_yesno=False, is_onoff=False,
|
2018-09-03 03:08:10 +08:00
|
|
|
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
|
|
|
|
2018-02-14 20:17:31 +08:00
|
|
|
:param xpath: xpath string which maps to the associated property
|
2013-07-14 05:54:46 +08:00
|
|
|
in a typical XML document
|
2018-02-14 20:17:31 +08:00
|
|
|
:param name: Just a string to print for debugging, only needed
|
2013-07-14 10:13:13 +08:00
|
|
|
if xpath isn't specified.
|
2018-02-14 20:17:31 +08:00
|
|
|
:param is_bool: Whether this is a boolean property in the XML
|
|
|
|
: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
|
|
|
|
: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-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-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
|
|
|
|
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
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
_XMLPropertyBase.__init__(self, self.getter, self.setter)
|
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 #
|
|
|
|
####################
|
|
|
|
|
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
|
2018-09-03 03:08:10 +08:00
|
|
|
if self._is_bool:
|
2013-07-25 00:51:53 +08:00
|
|
|
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
|
|
|
|
2018-09-04 05:03:02 +08:00
|
|
|
def _convert_set_value(self, val):
|
2018-09-03 03:08:10 +08:00
|
|
|
if self._do_abspath and val is not None:
|
2014-09-24 02:25:14 +08:00
|
|
|
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
|
|
|
return val
|
|
|
|
|
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
|
2013-07-14 21:31:14 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
if self.propname in propstore:
|
|
|
|
del(propstore[self.propname])
|
|
|
|
propstore[self.propname] = val
|
2013-07-14 21:31:14 +08:00
|
|
|
|
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
|
|
|
|
"""
|
2018-03-20 23:56:12 +08:00
|
|
|
return xmlbuilder._propstore.get(self.propname, None)
|
2013-07-18 05:58:24 +08:00
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
def clear(self, xmlbuilder):
|
2018-02-15 04:35:53 +08:00
|
|
|
# We only unset the cached data, since XML will be cleared elsewhere
|
|
|
|
propstore = xmlbuilder._propstore
|
2018-03-20 23:56:12 +08:00
|
|
|
if self.propname in propstore:
|
2018-02-15 04:35:53 +08:00
|
|
|
self.setter(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
|
|
|
|
|
2018-09-03 03:08:10 +08:00
|
|
|
if self.propname in xmlbuilder._propstore:
|
2013-07-25 02:37:07 +08:00
|
|
|
val = self._nonxml_fget(xmlbuilder)
|
2018-03-20 23:56:12 +08:00
|
|
|
else:
|
|
|
|
val = self._get_xml(xmlbuilder)
|
|
|
|
return self._convert_get_value(val)
|
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
|
|
|
|
"""
|
2018-02-15 04:35:53 +08:00
|
|
|
xpath = xmlbuilder._xmlstate.make_abs_xpath(self._xpath)
|
|
|
|
return xmlbuilder._xmlstate.xmlapi.get_xpath_content(
|
|
|
|
xpath, self._is_bool)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-09-04 04:13:49 +08:00
|
|
|
def setter(self, xmlbuilder, val):
|
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
|
2018-09-01 04:52:02 +08:00
|
|
|
get_xml time.
|
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
|
|
|
|
|
2018-09-04 05:03:02 +08:00
|
|
|
setval = self._convert_set_value(val)
|
2018-09-04 04:13:49 +08:00
|
|
|
self._nonxml_fset(xmlbuilder, setval)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-02-14 22:57:22 +08:00
|
|
|
def _set_xml(self, xmlbuilder, setval):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Actually set the passed value in the XML document
|
|
|
|
"""
|
2018-02-15 04:35:53 +08:00
|
|
|
xpath = xmlbuilder._xmlstate.make_abs_xpath(self._xpath)
|
|
|
|
xmlbuilder._xmlstate.xmlapi.set_xpath_content(xpath, 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):
|
2017-03-06 07:32:53 +08:00
|
|
|
def __init__(self, root_name, parsexml, parentxmlstate,
|
|
|
|
relative_object_xpath):
|
|
|
|
self._root_name = root_name
|
2018-02-24 03:39:47 +08:00
|
|
|
self._namespace = ""
|
|
|
|
if ":" in self._root_name:
|
|
|
|
ns = self._root_name.split(":")[0]
|
|
|
|
self._namespace = " xmlns:%s='%s'" % (ns, XMLAPI.NAMESPACES[ns])
|
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"
|
2017-03-06 07:32:53 +08:00
|
|
|
self._parent_xpath = (
|
2018-02-15 04:35:53 +08:00
|
|
|
parentxmlstate and parentxmlstate.abs_xpath()) or ""
|
2013-07-25 23:02:56 +08:00
|
|
|
|
2018-02-14 23:48:53 +08:00
|
|
|
self.xmlapi = None
|
2018-09-03 03:08:10 +08:00
|
|
|
self.is_build = not parsexml and not parentxmlstate
|
2018-02-08 04:38:30 +08:00
|
|
|
self.parse(parsexml, parentxmlstate)
|
2017-03-06 07:32:53 +08:00
|
|
|
|
2018-02-08 04:38:30 +08:00
|
|
|
def parse(self, parsexml, parentxmlstate):
|
2017-03-06 07:32:53 +08:00
|
|
|
if parentxmlstate:
|
2018-02-08 04:38:30 +08:00
|
|
|
self.is_build = parentxmlstate.is_build or self.is_build
|
2018-02-14 23:48:53 +08:00
|
|
|
self.xmlapi = parentxmlstate.xmlapi
|
2017-03-06 07:32:53 +08:00
|
|
|
return
|
2014-02-26 01:45:15 +08:00
|
|
|
|
2018-02-24 03:39:47 +08:00
|
|
|
# Make sure passed in XML has required xmlns inserted
|
2017-03-06 07:32:53 +08:00
|
|
|
if not parsexml:
|
2018-02-24 03:39:47 +08:00
|
|
|
parsexml = "<%s%s/>" % (self._root_name, self._namespace)
|
|
|
|
elif self._namespace and "xmlns" not in parsexml:
|
2018-02-27 03:56:24 +08:00
|
|
|
parsexml = parsexml.replace("<" + self._root_name,
|
|
|
|
"<" + self._root_name + self._namespace)
|
2014-02-26 01:45:15 +08:00
|
|
|
|
2017-03-06 07:32:53 +08:00
|
|
|
try:
|
2018-02-15 04:35:53 +08:00
|
|
|
self.xmlapi = XMLAPI(parsexml)
|
2017-07-24 16:26:48 +08:00
|
|
|
except Exception:
|
2017-03-06 07:32:53 +08:00
|
|
|
logging.debug("Error parsing xml=\n%s", parsexml)
|
|
|
|
raise
|
|
|
|
|
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 ""
|
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
def _join_xpath(self, x1, x2):
|
|
|
|
if x1.endswith("/"):
|
|
|
|
x1 = x1[:-1]
|
|
|
|
if x2.startswith("."):
|
|
|
|
x2 = x2[1:]
|
|
|
|
return x1 + x2
|
2013-07-25 23:02:56 +08:00
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
def abs_xpath(self):
|
|
|
|
return self._join_xpath(self._parent_xpath or ".",
|
|
|
|
self._relative_object_xpath or ".")
|
|
|
|
|
|
|
|
def make_abs_xpath(self, xpath):
|
|
|
|
"""
|
2018-03-21 00:18:35 +08:00
|
|
|
Convert a relative xpath to an absolute xpath. So for DeviceDisk
|
2018-02-15 04:35:53 +08:00
|
|
|
that's part of a Guest, accessing driver_name will do convert:
|
|
|
|
./driver/@name
|
|
|
|
to an absolute xpath like:
|
|
|
|
./devices/disk[3]/driver/@name
|
|
|
|
"""
|
|
|
|
return self._join_xpath(self.abs_xpath() or ".", xpath)
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
|
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
|
2018-03-21 22:53:34 +08:00
|
|
|
XML_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
|
|
|
|
|
2017-03-06 07:32:53 +08:00
|
|
|
def __init__(self, conn, parsexml=None,
|
|
|
|
parentxmlstate=None, relative_object_xpath=None):
|
2013-03-18 05:06:52 +08:00
|
|
|
"""
|
|
|
|
Initialize state
|
|
|
|
|
2018-03-21 00:18:35 +08:00
|
|
|
:param conn: VirtinstConnection to validate device against
|
2018-02-14 20:17:31 +08:00
|
|
|
:param parsexml: Optional XML string to parse
|
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:
|
2017-10-11 19:35:58 +08:00
|
|
|
if hasattr(parsexml, 'decode'):
|
|
|
|
parsexml = parsexml.decode("ascii", "ignore").encode("ascii")
|
|
|
|
else:
|
|
|
|
parsexml = parsexml.encode("ascii", "ignore").decode("ascii")
|
|
|
|
|
2015-03-27 06:04:23 +08:00
|
|
|
parsexml = "".join([c for c in parsexml if c in string.printable])
|
|
|
|
|
2018-03-20 23:06:23 +08:00
|
|
|
self._propstore = collections.OrderedDict()
|
2018-03-21 22:53:34 +08:00
|
|
|
self._xmlstate = _XMLState(self.XML_NAME,
|
2017-03-06 07:32:53 +08:00
|
|
|
parsexml, parentxmlstate,
|
|
|
|
relative_object_xpath)
|
2013-09-11 22:16:12 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
self._validate_xmlbuilder()
|
2013-09-20 01:25:52 +08:00
|
|
|
self._initial_child_parse()
|
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
def _validate_xmlbuilder(self):
|
|
|
|
# This is one time validation we run once per XMLBuilder class
|
|
|
|
cachekey = self.__class__.__name__ + "_xmlbuilder_validated"
|
|
|
|
if getattr(self.__class__, cachekey, False):
|
|
|
|
return
|
|
|
|
|
|
|
|
xmlprops = self._all_xml_props()
|
|
|
|
childprops = self._all_child_props()
|
|
|
|
for key in self._XML_PROP_ORDER:
|
|
|
|
if key not in xmlprops and key not in childprops:
|
|
|
|
raise RuntimeError("programming error: key '%s' must be "
|
|
|
|
"xml prop or child prop" % key)
|
|
|
|
|
|
|
|
childclasses = []
|
|
|
|
for childprop in childprops.values():
|
|
|
|
if childprop.child_class in childclasses:
|
|
|
|
raise RuntimeError("programming error: can't register "
|
|
|
|
"duplicate child_classs=%s" % childprop.child_class)
|
|
|
|
childclasses.append(childprop.child_class)
|
|
|
|
|
|
|
|
setattr(self.__class__, cachekey, True)
|
|
|
|
|
2013-09-20 01:25:52 +08:00
|
|
|
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
|
2017-10-11 19:35:46 +08:00
|
|
|
for xmlprop in list(self._all_child_props().values()):
|
2018-03-21 18:01:28 +08:00
|
|
|
child_class = xmlprop.child_class
|
|
|
|
prop_path = xmlprop.get_prop_xpath(self, child_class)
|
|
|
|
|
2013-09-11 23:47:09 +08:00
|
|
|
if xmlprop.is_single:
|
|
|
|
obj = child_class(self.conn,
|
2017-03-06 07:32:53 +08:00
|
|
|
parentxmlstate=self._xmlstate,
|
2013-09-11 23:47:09 +08:00
|
|
|
relative_object_xpath=prop_path)
|
|
|
|
xmlprop.set(self, obj)
|
|
|
|
continue
|
|
|
|
|
2018-03-21 18:01:28 +08:00
|
|
|
nodecount = self._xmlstate.xmlapi.count(
|
|
|
|
self._xmlstate.make_abs_xpath(prop_path))
|
|
|
|
for idx in range(nodecount):
|
|
|
|
idxstr = "[%d]" % (idx + 1)
|
|
|
|
obj = child_class(self.conn,
|
|
|
|
parentxmlstate=self._xmlstate,
|
|
|
|
relative_object_xpath=(prop_path + idxstr))
|
|
|
|
xmlprop.append(self, obj)
|
2013-09-20 01:25:52 +08:00
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
def __repr__(self):
|
|
|
|
return "<%s %s %s>" % (self.__class__.__name__.split(".")[-1],
|
2018-03-21 22:53:34 +08:00
|
|
|
self.XML_NAME, id(self))
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
############################
|
|
|
|
# Public XML managing APIs #
|
|
|
|
############################
|
2013-07-24 20:46:55 +08:00
|
|
|
|
2018-09-01 04:52:02 +08:00
|
|
|
def get_xml(self):
|
2013-07-25 23:02:56 +08:00
|
|
|
"""
|
|
|
|
Return XML string of the object
|
|
|
|
"""
|
2018-02-23 08:49:08 +08:00
|
|
|
xmlapi = self._xmlstate.xmlapi
|
|
|
|
if self._xmlstate.is_build:
|
|
|
|
xmlapi = xmlapi.copy_api()
|
|
|
|
|
|
|
|
self._add_parse_bits(xmlapi)
|
|
|
|
ret = xmlapi.get_xml(self._xmlstate.make_abs_xpath("."))
|
|
|
|
|
|
|
|
if ret and not ret.endswith("\n"):
|
|
|
|
ret += "\n"
|
|
|
|
return ret
|
2013-07-24 20:46:55 +08:00
|
|
|
|
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
|
|
|
"""
|
2018-02-15 04:35:53 +08:00
|
|
|
props = list(self._all_xml_props().values())
|
|
|
|
props += list(self._all_child_props().values())
|
|
|
|
for prop in props:
|
|
|
|
prop.clear(self)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
is_child = bool(re.match("^.*\[\d+\]$", self._xmlstate.abs_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
|
2018-02-15 04:35:53 +08:00
|
|
|
self._xmlstate.xmlapi.node_clear(self._xmlstate.abs_xpath())
|
2015-09-05 05:03:31 +08:00
|
|
|
else:
|
2018-02-15 04:35:53 +08:00
|
|
|
self._xmlstate.xmlapi.node_force_remove(self._xmlstate.abs_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
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
def get_xml_id(self):
|
|
|
|
"""
|
|
|
|
Return the location of the object in the XML document. This is
|
|
|
|
basically the absolute xpath, but the value returned should be
|
|
|
|
treated as opaque, it's just for cross XML comparisons. Used
|
|
|
|
in virt-manager code
|
|
|
|
"""
|
|
|
|
return self._xmlstate.abs_xpath()
|
|
|
|
|
2018-03-22 02:42:50 +08:00
|
|
|
def get_xml_idx(self):
|
|
|
|
"""
|
|
|
|
This is basically the offset parsed out of the object's xpath,
|
|
|
|
minus 1. So if this is the fifth <disk> in a <domain>, ret=4.
|
|
|
|
If this is the only <cpu> in a domain, ret=0.
|
|
|
|
"""
|
|
|
|
xpath = self._xmlstate.abs_xpath()
|
|
|
|
if "[" not in xpath:
|
|
|
|
return 0
|
|
|
|
return int(xpath.rsplit("[", 1)[1].strip("]")) - 1
|
|
|
|
|
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 _all_xml_props(self):
|
|
|
|
"""
|
|
|
|
Return a list of all XMLProperty instances that this class has.
|
|
|
|
"""
|
2018-03-20 23:56:12 +08:00
|
|
|
return _PropCache.get_xml_props(self)
|
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
|
|
|
"""
|
2018-03-20 23:56:12 +08:00
|
|
|
return _PropCache.get_child_props(self)
|
2013-09-20 23:30:33 +08:00
|
|
|
|
2018-03-20 23:56:12 +08:00
|
|
|
def _find_child_prop(self, child_class):
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprops = self._all_child_props()
|
2017-10-11 19:35:46 +08:00
|
|
|
for xmlprop in list(xmlprops.values()):
|
2018-03-20 23:56:12 +08:00
|
|
|
if xmlprop.is_single:
|
2013-09-11 23:47:09 +08:00
|
|
|
continue
|
2018-03-21 18:01:28 +08:00
|
|
|
if child_class is xmlprop.child_class:
|
2013-09-11 06:32:10 +08:00
|
|
|
return xmlprop
|
|
|
|
raise RuntimeError("programming error: "
|
|
|
|
"Didn't find child property for child_class=%s" %
|
|
|
|
child_class)
|
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
def _set_xpaths(self, parent_xpath, relative_object_xpath=-1):
|
|
|
|
"""
|
|
|
|
Change the object hierarchy's cached xpaths
|
|
|
|
"""
|
|
|
|
self._xmlstate.set_parent_xpath(parent_xpath)
|
|
|
|
if relative_object_xpath != -1:
|
|
|
|
self._xmlstate.set_relative_object_xpath(relative_object_xpath)
|
|
|
|
for propname in self._all_child_props():
|
|
|
|
for p in util.listify(getattr(self, propname, [])):
|
|
|
|
p._set_xpaths(self._xmlstate.abs_xpath())
|
|
|
|
|
|
|
|
def _set_child_xpaths(self):
|
|
|
|
"""
|
|
|
|
Walk the list of child properties and make sure their
|
|
|
|
xpaths point at their particular element. Needs to be called
|
|
|
|
whenever child objects are added or removed
|
|
|
|
"""
|
|
|
|
typecount = {}
|
|
|
|
for propname, xmlprop in self._all_child_props().items():
|
|
|
|
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]
|
|
|
|
|
|
|
|
prop_path = xmlprop.get_prop_xpath(self, obj)
|
|
|
|
obj._set_xpaths(self._xmlstate.abs_xpath(),
|
|
|
|
prop_path + idxstr)
|
|
|
|
|
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
|
|
|
|
"""
|
2018-02-08 04:38:30 +08:00
|
|
|
self._xmlstate.parse(*args, **kwargs)
|
2014-01-28 08:54:41 +08:00
|
|
|
for propname in self._all_child_props():
|
|
|
|
for p in util.listify(getattr(self, propname, [])):
|
2018-02-15 04:35:53 +08:00
|
|
|
p._parse_with_children(None, self._xmlstate)
|
2014-01-28 08:54:41 +08:00
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
xmlprop = self._find_child_prop(obj.__class__)
|
2018-09-01 04:52:02 +08:00
|
|
|
xml = obj.get_xml()
|
2013-09-11 06:32:10 +08:00
|
|
|
xmlprop.append(self, obj)
|
|
|
|
self._set_child_xpaths()
|
|
|
|
|
2018-09-03 03:08:10 +08:00
|
|
|
# Only insert the XML directly into the parent XML for !is_build
|
|
|
|
# This is the only way to dictate XML ordering when building
|
|
|
|
# from scratch, otherwise elements appear in the order they
|
|
|
|
# are set. It's just a style issue but annoying nonetheless
|
2013-09-11 06:32:10 +08:00
|
|
|
if not obj._xmlstate.is_build:
|
2018-02-15 04:35:53 +08:00
|
|
|
use_xpath = obj._xmlstate.abs_xpath().rsplit("/", 1)[0]
|
|
|
|
indent = 2 * obj._xmlstate.abs_xpath().count("/")
|
|
|
|
self._xmlstate.xmlapi.node_add_xml(
|
2018-03-20 22:35:22 +08:00
|
|
|
util.xml_indent(xml, indent), use_xpath)
|
2017-03-06 07:32:53 +08:00
|
|
|
obj._parse_with_children(None, self._xmlstate)
|
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)
|
|
|
|
|
2018-02-15 04:35:53 +08:00
|
|
|
xpath = obj._xmlstate.abs_xpath()
|
2018-09-01 04:52:02 +08:00
|
|
|
xml = obj.get_xml()
|
2018-02-15 04:35:53 +08:00
|
|
|
obj._set_xpaths(None, None)
|
2014-01-28 08:54:41 +08:00
|
|
|
obj._parse_with_children(xml, None)
|
2018-02-15 04:35:53 +08:00
|
|
|
self._xmlstate.xmlapi.node_force_remove(xpath)
|
2013-09-11 06:32:10 +08:00
|
|
|
self._set_child_xpaths()
|
|
|
|
|
2018-09-02 21:02:32 +08:00
|
|
|
def _prop_is_unset(self, propname):
|
|
|
|
"""
|
|
|
|
Return True if the property name has never had a value set
|
|
|
|
"""
|
|
|
|
if getattr(self, propname):
|
|
|
|
return False
|
|
|
|
return propname not in self._propstore
|
|
|
|
|
2013-07-25 23:02:56 +08:00
|
|
|
|
|
|
|
#################################
|
|
|
|
# Private XML building routines #
|
|
|
|
#################################
|
2013-07-25 02:37:07 +08:00
|
|
|
|
2018-02-14 23:48:53 +08:00
|
|
|
def _add_parse_bits(self, xmlapi):
|
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
|
|
|
origpropstore = self._propstore.copy()
|
2018-02-14 23:48:53 +08:00
|
|
|
origapi = self._xmlstate.xmlapi
|
2013-07-25 23:02:56 +08:00
|
|
|
try:
|
2018-02-14 23:48:53 +08:00
|
|
|
self._xmlstate.xmlapi = xmlapi
|
|
|
|
return self._do_add_parse_bits()
|
2013-07-25 23:02:56 +08:00
|
|
|
finally:
|
2018-02-14 23:48:53 +08:00
|
|
|
self._xmlstate.xmlapi = origapi
|
2013-07-25 23:02:56 +08:00
|
|
|
self._propstore = origpropstore
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-02-14 23:48:53 +08:00
|
|
|
def _do_add_parse_bits(self):
|
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-15 05:02:42 +08:00
|
|
|
# Set up preferred XML ordering
|
2018-03-20 23:06:23 +08:00
|
|
|
do_order = [p for p in self._propstore if p not in childprops]
|
2013-07-15 21:49:46 +08:00
|
|
|
for key in reversed(self._XML_PROP_ORDER):
|
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
|
|
|
|
2018-02-21 00:41:04 +08:00
|
|
|
for key in sorted(list(childprops.keys())):
|
2013-09-20 23:17:11 +08:00
|
|
|
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:
|
2018-02-14 22:57:22 +08:00
|
|
|
xmlprops[key]._set_xml(self, self._propstore[key])
|
2013-09-20 23:17:11 +08:00
|
|
|
elif key in childprops:
|
|
|
|
for obj in util.listify(getattr(self, key)):
|
2018-02-14 23:48:53 +08:00
|
|
|
obj._add_parse_bits(self._xmlstate.xmlapi)
|