Merge "Migrate object to OVO (7)"
This commit is contained in:
commit
1804a9dff6
|
@ -152,8 +152,7 @@ class Audit(NotificationPlugin):
|
|||
changes = []
|
||||
|
||||
for arg in arglist:
|
||||
if isinstance(arg, (objects.DesignateObject,
|
||||
objects.OVODesignateObject)):
|
||||
if isinstance(arg, objects.DesignateObject):
|
||||
for change in arg.obj_what_changed():
|
||||
if change != 'records':
|
||||
old_value = arg.obj_get_original_value(change)
|
||||
|
|
|
@ -13,13 +13,9 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects.base import DesignateObject # noqa
|
||||
from designate.objects.ovo_base import DesignateObject as OVODesignateObject # noqa
|
||||
from designate.objects.base import ListObjectMixin # noqa
|
||||
from designate.objects.ovo_base import ListObjectMixin as OVOListObjectMixin # noqa
|
||||
from designate.objects.base import DictObjectMixin # noqa
|
||||
from designate.objects.ovo_base import DictObjectMixin as OVODictObjectMixin # noqa
|
||||
from designate.objects.base import PagedListObjectMixin # noqa
|
||||
from designate.objects.ovo_base import PagedListObjectMixin as OVOPagedListObjectMixin # noqa
|
||||
from designate.objects.blacklist import Blacklist, BlacklistList # noqa
|
||||
from designate.objects.zone import Zone, ZoneList # noqa
|
||||
from designate.objects.zone_attribute import ZoneAttribute, ZoneAttributeList # noqa
|
||||
|
|
|
@ -15,8 +15,7 @@ from six.moves.urllib import parse
|
|||
from oslo_config import cfg
|
||||
|
||||
from designate.objects.adapters import base
|
||||
from designate.objects import base as obj_base
|
||||
from designate.objects import ovo_base as ovoobj_base
|
||||
from designate.objects import base as ovoobj_base
|
||||
from designate import exceptions
|
||||
|
||||
|
||||
|
@ -45,8 +44,7 @@ class APIv2Adapter(base.DesignateAdapter):
|
|||
r_list['links'] = cls._get_collection_links(
|
||||
list_object, kwargs['request'])
|
||||
# Check if we should include metadata
|
||||
if isinstance(list_object, (obj_base.PagedListObjectMixin,
|
||||
ovoobj_base.PagedListObjectMixin)):
|
||||
if isinstance(list_object, ovoobj_base.PagedListObjectMixin):
|
||||
metadata = {}
|
||||
if list_object.total_count is not None:
|
||||
metadata['total_count'] = list_object.total_count
|
||||
|
|
|
@ -78,8 +78,7 @@ class ValidationErrorAPIv2Adapter(base.APIv2Adapter):
|
|||
|
||||
# Check if the object is a list - lists will just have an index as a
|
||||
# value, ands this can't be renamed
|
||||
if issubclass(obj_adapter.ADAPTER_OBJECT,
|
||||
(objects.ListObjectMixin, objects.OVOListObjectMixin)):
|
||||
if issubclass(obj_adapter.ADAPTER_OBJECT, objects.ListObjectMixin):
|
||||
obj_adapter = cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT,
|
||||
obj_adapter.ADAPTER_OBJECT.LIST_ITEM_TYPE.obj_name())
|
||||
|
@ -110,7 +109,7 @@ class ValidationErrorAPIv2Adapter(base.APIv2Adapter):
|
|||
obj_adapter = cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT, obj_cls)
|
||||
|
||||
object = objects.OVODesignateObject.obj_cls_from_name(obj_cls)() # noqa
|
||||
object = objects.DesignateObject.obj_cls_from_name(obj_cls)() # noqa
|
||||
# Recurse down into this object
|
||||
path_segment, obj_adapter = cls._rename_path_segment(
|
||||
obj_adapter, object, path_segment)
|
||||
|
|
|
@ -60,8 +60,7 @@ class DesignateAdapter(object):
|
|||
|
||||
@classmethod
|
||||
def get_object_adapter(cls, format_, object):
|
||||
if isinstance(object, (objects.DesignateObject,
|
||||
objects.OVODesignateObject)):
|
||||
if isinstance(object, objects.DesignateObject):
|
||||
key = '%s:%s' % (format_, object.obj_name())
|
||||
else:
|
||||
key = '%s:%s' % (format_, object)
|
||||
|
@ -82,8 +81,7 @@ class DesignateAdapter(object):
|
|||
@classmethod
|
||||
def render(cls, format_, object, *args, **kwargs):
|
||||
|
||||
if isinstance(object, (objects.ListObjectMixin,
|
||||
objects.OVOListObjectMixin)):
|
||||
if isinstance(object, objects.ListObjectMixin):
|
||||
# type_ = 'list'
|
||||
return cls.get_object_adapter(
|
||||
format_, object)._render_list(object, *args, **kwargs)
|
||||
|
@ -177,8 +175,7 @@ class DesignateAdapter(object):
|
|||
LOG.debug(output_object)
|
||||
|
||||
try:
|
||||
if isinstance(output_object, (objects.ListObjectMixin,
|
||||
objects.OVOListObjectMixin)):
|
||||
if isinstance(output_object, objects.ListObjectMixin):
|
||||
# type_ = 'list'
|
||||
return cls.get_object_adapter(
|
||||
format_,
|
||||
|
@ -260,7 +257,7 @@ class DesignateAdapter(object):
|
|||
check_field, 'objname'):
|
||||
# (daidv): Check if field is OVO field and have a relation
|
||||
obj_class_name = output_object.FIELDS.get(obj_key).objname
|
||||
obj_class = objects.OVODesignateObject.obj_cls_from_name(
|
||||
obj_class = objects.DesignateObject.obj_cls_from_name(
|
||||
obj_class_name)
|
||||
obj = cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT, obj_class_name).parse(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2014 Rackspace Hosting
|
||||
# Copyright (c) 2017 Fujitsu Limited
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
|
@ -12,240 +12,46 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import copy
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
import jsonschema
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from designate import exceptions
|
||||
from designate.objects import validation_error
|
||||
from designate.schema import validators
|
||||
from designate.schema import format
|
||||
from oslo_versionedobjects import exception
|
||||
from oslo_utils import excutils
|
||||
from designate.i18n import _
|
||||
from designate.i18n import _LE
|
||||
from oslo_versionedobjects import base
|
||||
from oslo_versionedobjects.base import VersionedObjectDictCompat as DictObjectMixin # noqa
|
||||
|
||||
from designate.objects import fields
|
||||
from designate import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotSpecifiedSentinel:
|
||||
pass
|
||||
def _get_attrname(name):
|
||||
return "_obj_{}".format(name)
|
||||
|
||||
|
||||
def get_attrname(name):
|
||||
"""Return the mangled name of the attribute's underlying storage."""
|
||||
return '_obj_field_%s' % name
|
||||
def get_dict_attr(klass, attr):
|
||||
for klass in [klass] + klass.mro():
|
||||
if attr in klass.__dict__:
|
||||
return klass.__dict__[attr]
|
||||
raise AttributeError
|
||||
|
||||
|
||||
def make_class_properties(cls):
|
||||
"""Build getter and setter methods for all the objects attributes"""
|
||||
# Prepare an empty dict to gather the merged/final set of fields
|
||||
fields = {}
|
||||
|
||||
# Add each supercls's fields
|
||||
for supercls in cls.mro()[1:-1]:
|
||||
if not hasattr(supercls, 'FIELDS'):
|
||||
continue
|
||||
fields.update(supercls.FIELDS)
|
||||
|
||||
# Add our fields
|
||||
fields.update(cls.FIELDS)
|
||||
|
||||
# Store the results
|
||||
cls.FIELDS = fields
|
||||
|
||||
for field in six.iterkeys(cls.FIELDS):
|
||||
def getter(self, name=field):
|
||||
self._obj_check_relation(name)
|
||||
return getattr(self, get_attrname(name), None)
|
||||
|
||||
def setter(self, value, name=field):
|
||||
if (self.obj_attr_is_set(name) and value != getattr(self, name)
|
||||
or not self.obj_attr_is_set(name)):
|
||||
self._obj_changes.add(name)
|
||||
|
||||
if (self.obj_attr_is_set(name) and value != getattr(self, name)
|
||||
and name not in list(six.iterkeys(
|
||||
self._obj_original_values))):
|
||||
self._obj_original_values[name] = getattr(self, name)
|
||||
|
||||
return setattr(self, get_attrname(name), value)
|
||||
|
||||
setattr(cls, field, property(getter, setter))
|
||||
|
||||
|
||||
def _schema_ref_resolver(uri):
|
||||
"""
|
||||
Fetches an DesignateObject's schema from a JSON Schema Reference URI
|
||||
|
||||
Sample URI: obj://ObjectName#/subpathA/subpathB
|
||||
"""
|
||||
obj_name = parse.urlsplit(uri).netloc
|
||||
obj = DesignateObject.obj_cls_from_name(obj_name)
|
||||
|
||||
return obj.obj_get_schema()
|
||||
|
||||
|
||||
def make_class_validator(obj):
|
||||
|
||||
schema = {
|
||||
'$schema': 'http://json-schema.org/draft-04/hyper-schema',
|
||||
'title': obj.obj_name(),
|
||||
'description': 'Designate %s Object' % obj.obj_name(),
|
||||
}
|
||||
|
||||
if isinstance(obj, ListObjectMixin):
|
||||
|
||||
schema['type'] = 'array',
|
||||
schema['items'] = make_class_validator(obj.LIST_ITEM_TYPE())
|
||||
|
||||
else:
|
||||
schema['type'] = 'object'
|
||||
schema['additionalProperties'] = False
|
||||
schema['required'] = []
|
||||
schema['properties'] = {}
|
||||
|
||||
for name, properties in obj.FIELDS.items():
|
||||
if properties.get('relation', False):
|
||||
if obj.obj_attr_is_set(name):
|
||||
schema['properties'][name] = \
|
||||
make_class_validator(getattr(obj, name))
|
||||
else:
|
||||
schema['properties'][name] = properties.get('schema', {})
|
||||
|
||||
if properties.get('required', False):
|
||||
schema['required'].append(name)
|
||||
|
||||
resolver = jsonschema.RefResolver.from_schema(
|
||||
schema, handlers={'obj': _schema_ref_resolver})
|
||||
|
||||
obj._obj_validator = validators.Draft4Validator(
|
||||
schema, resolver=resolver, format_checker=format.draft4_format_checker)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
class DesignateObjectMetaclass(type):
|
||||
def __init__(cls, names, bases, dict_):
|
||||
if not hasattr(cls, '_obj_classes'):
|
||||
# This means we're working on the base DesignateObject class,
|
||||
# and can skip the remaining Metaclass functionality
|
||||
cls._obj_classes = {}
|
||||
return
|
||||
|
||||
make_class_properties(cls)
|
||||
|
||||
# Add a reference to the finished class into the _obj_classes
|
||||
# dictionary, allowing us to lookup classes by their name later - this
|
||||
# is useful for e.g. referencing another DesignateObject in a
|
||||
# validation schema.
|
||||
if cls.obj_name() not in cls._obj_classes:
|
||||
cls._obj_classes[cls.obj_name()] = cls
|
||||
else:
|
||||
raise Exception("Duplicate DesignateObject with name '%(name)s'" %
|
||||
{'name': cls.obj_name()})
|
||||
|
||||
|
||||
@six.add_metaclass(DesignateObjectMetaclass)
|
||||
class DesignateObject(object):
|
||||
FIELDS = {}
|
||||
class DesignateObject(base.VersionedObject):
|
||||
OBJ_SERIAL_NAMESPACE = 'designate_object'
|
||||
OBJ_PROJECT_NAMESPACE = 'designate'
|
||||
|
||||
STRING_KEYS = []
|
||||
|
||||
def _obj_check_relation(self, name):
|
||||
if name in self.FIELDS and self.FIELDS[name].get('relation', False):
|
||||
if not self.obj_attr_is_set(name):
|
||||
raise exceptions.RelationNotLoaded(object=self, relation=name)
|
||||
|
||||
@classmethod
|
||||
def obj_cls_from_name(cls, name):
|
||||
"""Retrieves a object cls from the registry by name and returns it."""
|
||||
return cls._obj_classes[name]
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
"""
|
||||
Construct an object from primitive types
|
||||
|
||||
This is used while deserializing the object.
|
||||
"""
|
||||
objcls = cls.obj_cls_from_name(primitive['designate_object.name'])
|
||||
return objcls._obj_from_primitive(primitive)
|
||||
|
||||
@classmethod
|
||||
def _obj_from_primitive(cls, primitive):
|
||||
instance = cls()
|
||||
|
||||
for field, value in primitive['designate_object.data'].items():
|
||||
if isinstance(value, dict) and 'designate_object.name' in value:
|
||||
setattr(instance, field, DesignateObject.from_primitive(value))
|
||||
else:
|
||||
# data typically doesn't have a schema..
|
||||
schema = cls.FIELDS[field].get("schema", None)
|
||||
if schema is not None and value is not None:
|
||||
if "format" in schema and schema["format"] == "date-time":
|
||||
value = timeutils.parse_strtime(value)
|
||||
setattr(instance, field, value)
|
||||
|
||||
instance._obj_changes = set(primitive['designate_object.changes'])
|
||||
instance._obj_original_values = \
|
||||
primitive['designate_object.original_values']
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, _dict):
|
||||
instance = cls()
|
||||
|
||||
for field, value in _dict.items():
|
||||
if (field in instance.FIELDS and
|
||||
instance.FIELDS[field].get('relation', False)):
|
||||
relation_cls_name = instance.FIELDS[field]['relation_cls']
|
||||
# We're dealing with a relation, we'll want to create the
|
||||
# correct object type and recurse
|
||||
relation_cls = cls.obj_cls_from_name(relation_cls_name)
|
||||
|
||||
if isinstance(value, list):
|
||||
setattr(instance, field, relation_cls.from_list(value))
|
||||
else:
|
||||
setattr(instance, field, relation_cls.from_dict(value))
|
||||
|
||||
else:
|
||||
setattr(instance, field, value)
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, _list):
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
"""Return a canonical name for this object which will be used over
|
||||
the wire and in validation schemas.
|
||||
"""
|
||||
return cls.__name__
|
||||
|
||||
@classmethod
|
||||
def obj_get_schema(cls):
|
||||
"""Returns the JSON Schema for this Object."""
|
||||
return cls._obj_validator.schema
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._obj_changes = set()
|
||||
self._obj_original_values = dict()
|
||||
|
||||
for name, value in kwargs.items():
|
||||
if name in list(six.iterkeys(self.FIELDS)):
|
||||
setattr(self, name, value)
|
||||
else:
|
||||
def __init__(self, *args, **kwargs):
|
||||
for name in kwargs:
|
||||
if name not in self.fields:
|
||||
raise TypeError("__init__() got an unexpected keyword "
|
||||
"argument '%(name)s'" % {'name': name})
|
||||
|
||||
def __str__(self):
|
||||
return (self._make_obj_str(self.STRING_KEYS)
|
||||
% self)
|
||||
super(DesignateObject, self).__init__(self, *args, **kwargs)
|
||||
self._obj_original_values = dict()
|
||||
self.FIELDS = self.fields
|
||||
|
||||
@classmethod
|
||||
def _make_obj_str(cls, keys):
|
||||
|
@ -255,35 +61,29 @@ class DesignateObject(object):
|
|||
msg += ">"
|
||||
return msg
|
||||
|
||||
def to_primitive(self):
|
||||
"""
|
||||
Convert the object to primitive types so that the object can be
|
||||
serialized.
|
||||
NOTE: Currently all the designate objects contain primitive types that
|
||||
do not need special handling. If this changes we need to modify this
|
||||
function.
|
||||
"""
|
||||
data = {}
|
||||
def __str__(self):
|
||||
return (self._make_obj_str(self.STRING_KEYS)
|
||||
% self)
|
||||
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if self.obj_attr_is_set(field):
|
||||
if isinstance(getattr(self, field), DesignateObject):
|
||||
data[field] = getattr(self, field).to_primitive()
|
||||
else:
|
||||
data[field] = getattr(self, field)
|
||||
def save(self, context):
|
||||
pass
|
||||
|
||||
return {
|
||||
'designate_object.name': self.obj_name(),
|
||||
'designate_object.data': data,
|
||||
'designate_object.changes': sorted(self._obj_changes),
|
||||
'designate_object.original_values': dict(self._obj_original_values)
|
||||
}
|
||||
def _obj_check_relation(self, name):
|
||||
if name in self.fields:
|
||||
if hasattr(self.fields.get(name), 'objname'):
|
||||
if not self.obj_attr_is_set(name):
|
||||
raise exceptions.RelationNotLoaded(
|
||||
object=self, relation=name)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert the object to a simple dictionary."""
|
||||
data = {}
|
||||
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if isinstance(self, ListObjectMixin):
|
||||
return {
|
||||
'objects': self.to_list()
|
||||
}
|
||||
for field in self.fields:
|
||||
if self.obj_attr_is_set(field):
|
||||
val = getattr(self, field)
|
||||
if isinstance(val, ListObjectMixin):
|
||||
|
@ -300,85 +100,146 @@ class DesignateObject(object):
|
|||
for k, v in values.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Returns True if the Object is valid."""
|
||||
@classmethod
|
||||
def from_dict(cls, _dict):
|
||||
instance = cls()
|
||||
|
||||
make_class_validator(self)
|
||||
for field, value in _dict.items():
|
||||
if (field in instance.fields and
|
||||
hasattr(instance.fields.get(field), 'objname')):
|
||||
relation_cls_name = instance.fields[field].objname
|
||||
# We're dealing with a relation, we'll want to create the
|
||||
# correct object type and recurse
|
||||
relation_cls = cls.obj_class_from_name(
|
||||
relation_cls_name, '1.0')
|
||||
|
||||
return self._obj_validator.is_valid(self.to_dict())
|
||||
if isinstance(value, list):
|
||||
setattr(instance, field, relation_cls.from_list(value))
|
||||
else:
|
||||
setattr(instance, field, relation_cls.from_dict(value))
|
||||
|
||||
def validate(self):
|
||||
else:
|
||||
setattr(instance, field, value)
|
||||
|
||||
make_class_validator(self)
|
||||
return instance
|
||||
|
||||
ValidationErrorList = validation_error.ValidationErrorList
|
||||
ValidationError = validation_error.ValidationError
|
||||
@classmethod
|
||||
def from_list(cls, _list):
|
||||
raise NotImplementedError()
|
||||
|
||||
errors = ValidationErrorList()
|
||||
def __setattr__(self, name, value):
|
||||
"""Enforces all object attributes are private or well defined"""
|
||||
if not (name[0:5] == '_obj_'
|
||||
or name[0:7] == '_change'
|
||||
or name == '_context'
|
||||
or name in list(six.iterkeys(self.fields))
|
||||
or name == 'FIELDS'
|
||||
or name == 'VERSION'
|
||||
or name == 'fields'):
|
||||
raise AttributeError(
|
||||
"Designate object '%(type)s' has no"
|
||||
"attribute '%(name)s'" % {
|
||||
'type': self.obj_name(),
|
||||
'name': name,
|
||||
})
|
||||
super(DesignateObject, self).__setattr__(name, value)
|
||||
|
||||
try:
|
||||
values = self.to_dict()
|
||||
except exceptions.RelationNotLoaded as e:
|
||||
e = ValidationError()
|
||||
e.path = ['type']
|
||||
e.validator = 'required'
|
||||
e.validator_value = [e.relation]
|
||||
e.message = "'%s' is a required property" % e.relation
|
||||
errors.append(e)
|
||||
raise exceptions.InvalidObject(
|
||||
"Provided object does not match "
|
||||
"schema", errors=errors, object=self)
|
||||
def __eq__(self, other):
|
||||
if self.__class__ != other.__class__:
|
||||
return False
|
||||
|
||||
LOG.debug("Validating '%(name)s' object with values: %(values)r", {
|
||||
'name': self.obj_name(),
|
||||
'values': values,
|
||||
})
|
||||
return self.obj_to_primitive() == other.obj_to_primitive()
|
||||
|
||||
for error in self._obj_validator.iter_errors(values):
|
||||
errors.append(ValidationError.from_js_error(error))
|
||||
def __ne__(self, other):
|
||||
return not (self.__eq__(other))
|
||||
|
||||
if len(errors) > 0:
|
||||
LOG.debug(
|
||||
"Error Validating '%(name)s' object with values: "
|
||||
"%(values)r", {
|
||||
'name': self.obj_name(),
|
||||
'values': values,
|
||||
}
|
||||
)
|
||||
raise exceptions.InvalidObject(
|
||||
"Provided object does not match "
|
||||
"schema", errors=errors, object=self)
|
||||
def __repr__(self):
|
||||
return "OVO Objects"
|
||||
|
||||
def obj_attr_is_set(self, name):
|
||||
# TODO(daidv): all of bellow functions should
|
||||
# be removed when we completed migration.
|
||||
def nested_sort(self, key, value):
|
||||
"""
|
||||
Return True or False depending of if a particular attribute has had
|
||||
an attribute's value explicitly set.
|
||||
This function ensure that change fields list
|
||||
is sorted.
|
||||
:param key:
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
return hasattr(self, get_attrname(name))
|
||||
if key == 'designate_object.changes':
|
||||
return sorted(value)
|
||||
if isinstance(value, list):
|
||||
_list = []
|
||||
for item in value:
|
||||
_list.append(self.nested_sort(None, item))
|
||||
return _list
|
||||
elif isinstance(value, dict):
|
||||
_dict = {}
|
||||
for k, v in value.items():
|
||||
_dict[k] = self.nested_sort(k, v)
|
||||
return _dict
|
||||
else:
|
||||
return value
|
||||
|
||||
def obj_what_changed(self):
|
||||
"""Returns a set of fields that have been modified."""
|
||||
return set(self._obj_changes)
|
||||
def to_primitive(self):
|
||||
return self.nested_sort(None, self.obj_to_primitive())
|
||||
|
||||
def obj_get_changes(self):
|
||||
"""Returns a dict of changed fields and their new values."""
|
||||
changes = {}
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive, context=None):
|
||||
return cls.obj_from_primitive(primitive, context)
|
||||
|
||||
for key in self.obj_what_changed():
|
||||
changes[key] = getattr(self, key)
|
||||
@classmethod
|
||||
def obj_cls_from_name(cls, name):
|
||||
return cls.obj_class_from_name(name, '1.0')
|
||||
|
||||
return changes
|
||||
@classmethod
|
||||
def obj_get_schema(cls):
|
||||
return cls.to_json_schema()
|
||||
|
||||
def obj_reset_changes(self, fields=None, recursive=False):
|
||||
"""Reset the list of fields that have been changed.
|
||||
|
||||
:param fields: List of fields to reset, or "all" if None.
|
||||
:param recursive: Call obj_reset_changes(recursive=True) on
|
||||
any sub-objects within the list of fields
|
||||
being reset.
|
||||
|
||||
This is NOT "revert to previous values".
|
||||
|
||||
Specifying fields on recursive resets will only be honored at the top
|
||||
level. Everything below the top will reset all.
|
||||
"""
|
||||
if recursive:
|
||||
for field in self.obj_get_changes():
|
||||
|
||||
# Ignore fields not in requested set (if applicable)
|
||||
if fields and field not in fields:
|
||||
continue
|
||||
|
||||
# Skip any fields that are unset
|
||||
if not self.obj_attr_is_set(field):
|
||||
continue
|
||||
|
||||
value = getattr(self, field)
|
||||
|
||||
# Don't reset nulled fields
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
# Reset straight Object and ListOfObjects fields
|
||||
if isinstance(self.fields[field], self.obj_fields.ObjectField):
|
||||
value.obj_reset_changes(recursive=True)
|
||||
elif isinstance(self.fields[field],
|
||||
self.obj_fields.ListOfObjectsField):
|
||||
for thing in value:
|
||||
thing.obj_reset_changes(recursive=True)
|
||||
|
||||
def obj_reset_changes(self, fields=None):
|
||||
"""Reset the list of fields that have been changed."""
|
||||
if fields:
|
||||
self._obj_changes -= set(fields)
|
||||
self._changed_fields -= set(fields)
|
||||
for field in fields:
|
||||
self._obj_original_values.pop(field, None)
|
||||
|
||||
else:
|
||||
self._obj_changes.clear()
|
||||
self._changed_fields.clear()
|
||||
self._obj_original_values = dict()
|
||||
|
||||
def obj_get_original_value(self, field):
|
||||
|
@ -390,116 +251,89 @@ class DesignateObject(object):
|
|||
else:
|
||||
raise KeyError(field)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""Enforces all object attributes are private or well defined"""
|
||||
if name[0:5] == '_obj_' or name in list(six.iterkeys(self.FIELDS)) \
|
||||
or name == 'FIELDS':
|
||||
super(DesignateObject, self).__setattr__(name, value)
|
||||
@property
|
||||
def obj_fields(self):
|
||||
return list(self.fields.keys()) + self.obj_extra_fields
|
||||
|
||||
else:
|
||||
raise AttributeError(
|
||||
"Designate object '%(type)s' has no attribute '%(name)s'" % {
|
||||
'type': self.obj_name(),
|
||||
'name': name,
|
||||
})
|
||||
@property
|
||||
def obj_context(self):
|
||||
return self._context
|
||||
|
||||
def __deepcopy__(self, memodict=None):
|
||||
"""
|
||||
Efficiently make a deep copy of this object.
|
||||
|
||||
"Efficiently" is used here a relative term, this will be faster
|
||||
than allowing python to naively deepcopy the object.
|
||||
"""
|
||||
|
||||
memodict = memodict or {}
|
||||
|
||||
c_obj = self.__class__()
|
||||
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if self.obj_attr_is_set(field):
|
||||
c_field = copy.deepcopy(getattr(self, field), memodict)
|
||||
setattr(c_obj, field, c_field)
|
||||
|
||||
c_obj._obj_changes = set(self._obj_changes)
|
||||
|
||||
return c_obj
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.__class__ != other.__class__:
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Returns True if the Object is valid."""
|
||||
try:
|
||||
self.validate()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return self.to_primitive() == other.to_primitive()
|
||||
|
||||
def __ne__(self, other):
|
||||
return not(self.__eq__(other))
|
||||
|
||||
|
||||
class DictObjectMixin(object):
|
||||
"""
|
||||
Mixin to allow DesignateObjects to behave like dictionaries
|
||||
|
||||
Eventually, this should be removed as other code is updated to use object
|
||||
rather than dictionary accessors.
|
||||
"""
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in list(six.iterkeys(self.FIELDS))
|
||||
|
||||
def get(self, key, default=NotSpecifiedSentinel):
|
||||
if key not in list(six.iterkeys(self.FIELDS)):
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (
|
||||
self.__class__, key))
|
||||
|
||||
if default != NotSpecifiedSentinel and not self.obj_attr_is_set(key):
|
||||
return default
|
||||
else:
|
||||
return getattr(self, key)
|
||||
return True
|
||||
|
||||
def items(self):
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if self.obj_attr_is_set(field):
|
||||
yield field, getattr(self, field)
|
||||
def validate(self):
|
||||
# NOTE(kiall, daidv): We make use of the Object registry here
|
||||
# in order to avoid an impossible circular import.
|
||||
ValidationErrorList = self.obj_cls_from_name('ValidationErrorList')
|
||||
ValidationError = self.obj_cls_from_name('ValidationError')
|
||||
self.fields = self.FIELDS
|
||||
for name in self.fields:
|
||||
field = self.fields[name]
|
||||
if self.obj_attr_is_set(name):
|
||||
value = getattr(self, name) # Check relation
|
||||
if isinstance(value, ListObjectMixin):
|
||||
for obj in value.objects:
|
||||
obj.validate()
|
||||
elif isinstance(value, DesignateObject):
|
||||
value.validate()
|
||||
else:
|
||||
try:
|
||||
field.coerce(self, name, value) # Check value
|
||||
except Exception as e:
|
||||
raise exceptions.InvalidObject(
|
||||
"{} is invalid".format(name))
|
||||
elif not field.nullable:
|
||||
# Check required is True ~ nullable is False
|
||||
errors = ValidationErrorList()
|
||||
e = ValidationError()
|
||||
e.path = ['records', 0]
|
||||
e.validator = 'required'
|
||||
e.validator_value = [name]
|
||||
e.message = "'%s' is a required property" % name
|
||||
errors.append(e)
|
||||
raise exceptions.InvalidObject(
|
||||
"Provided object does not match "
|
||||
"schema", errors=errors, object=self)
|
||||
|
||||
# Compatibility with jsonutils to_primitive(). See bug
|
||||
# https://bugs.launchpad.net/designate/+bug/1481377
|
||||
iteritems = items
|
||||
|
||||
def __iter__(self):
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if self.obj_attr_is_set(field):
|
||||
yield field, getattr(self, field)
|
||||
def obj_attr_is_set(self, name):
|
||||
"""
|
||||
Return True or False depending of if a particular attribute has had
|
||||
an attribute's value explicitly set.
|
||||
"""
|
||||
return hasattr(self, _get_attrname(name))
|
||||
|
||||
|
||||
class ListObjectMixin(object):
|
||||
"""Mixin to allow DesignateObjects to behave like python lists."""
|
||||
FIELDS = {
|
||||
'objects': {
|
||||
'relation': True
|
||||
}
|
||||
}
|
||||
class ListObjectMixin(base.ObjectListBase):
|
||||
LIST_ITEM_TYPE = DesignateObject
|
||||
|
||||
@classmethod
|
||||
def _obj_from_primitive(cls, primitive):
|
||||
def _obj_from_primitive(cls, context, objver, primitive):
|
||||
instance = cls()
|
||||
instance.VERSION = objver
|
||||
instance._context = context
|
||||
|
||||
for field, value in primitive['designate_object.data'].items():
|
||||
if field == 'objects':
|
||||
instance.objects = [DesignateObject.from_primitive(v) for v in
|
||||
value]
|
||||
instance.objects = [
|
||||
DesignateObject.obj_from_primitive(v) for v in value]
|
||||
elif isinstance(value, dict) and 'designate_object.name' in value:
|
||||
setattr(instance, field, DesignateObject.from_primitive(value))
|
||||
setattr(instance, field,
|
||||
DesignateObject.obj_from_primitive(value))
|
||||
else:
|
||||
setattr(instance, field, value)
|
||||
|
||||
instance._obj_changes = set(primitive['designate_object.changes'])
|
||||
instance._obj_changes = set(
|
||||
primitive.get('designate_object.changes', []))
|
||||
instance._obj_original_values = \
|
||||
primitive['designate_object.original_values']
|
||||
primitive.get('designate_object.original_values', {})
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -526,31 +360,6 @@ class ListObjectMixin(object):
|
|||
|
||||
return list_
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ListObjectMixin, self).__init__(*args, **kwargs)
|
||||
if 'objects' not in kwargs:
|
||||
self.objects = []
|
||||
self.obj_reset_changes(['objects'])
|
||||
|
||||
def to_primitive(self):
|
||||
data = {}
|
||||
|
||||
for field in six.iterkeys(self.FIELDS):
|
||||
if self.obj_attr_is_set(field):
|
||||
if field == 'objects':
|
||||
data[field] = [o.to_primitive() for o in self.objects]
|
||||
elif isinstance(getattr(self, field), DesignateObject):
|
||||
data[field] = getattr(self, field).to_primitive()
|
||||
else:
|
||||
data[field] = getattr(self, field)
|
||||
|
||||
return {
|
||||
'designate_object.name': self.obj_name(),
|
||||
'designate_object.data': data,
|
||||
'designate_object.changes': list(self._obj_changes),
|
||||
'designate_object.original_values': dict(self._obj_original_values)
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return (_("<%(type)s count:'%(count)s' object:'%(list_type)s'>")
|
||||
% {'count': len(self),
|
||||
|
@ -561,10 +370,6 @@ class ListObjectMixin(object):
|
|||
"""List iterator interface"""
|
||||
return iter(self.objects)
|
||||
|
||||
def __len__(self):
|
||||
"""List length"""
|
||||
return len(self.objects)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""List index access"""
|
||||
if isinstance(index, slice):
|
||||
|
@ -610,24 +415,15 @@ class ListObjectMixin(object):
|
|||
"""List count of value occurrences"""
|
||||
return self.objects.count(value)
|
||||
|
||||
def sort(self, key=None, reverse=False):
|
||||
self.objects.sort(key=key, reverse=reverse)
|
||||
|
||||
def obj_what_changed(self):
|
||||
changes = set(self._obj_changes)
|
||||
for item in self.objects:
|
||||
if item.obj_what_changed():
|
||||
changes.add('objects')
|
||||
return changes
|
||||
|
||||
|
||||
class AttributeListObjectMixin(ListObjectMixin):
|
||||
"""
|
||||
Mixin class for "Attribute" objects.
|
||||
|
||||
Attribute objects are ListObjects, who's members have a "key" and "value"
|
||||
Attribute objects are ListObjects, who's memebers have a "key" and "value"
|
||||
property, which should be exposed on the list itself as list.<key>.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, _dict):
|
||||
instances = cls.from_list([{'key': k, 'value': v} for k, v
|
||||
|
@ -657,38 +453,13 @@ class PersistentObjectMixin(object):
|
|||
|
||||
This adds the fields that we use in common for all persistent objects.
|
||||
"""
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid',
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'created_at': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'updated_at': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'format': 'date-time',
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'version': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
'read_only': True
|
||||
}
|
||||
fields = {
|
||||
'id': fields.UUIDFields(nullable=True),
|
||||
'created_at': fields.DateTimeField(nullable=True),
|
||||
'updated_at': fields.DateTimeField(nullable=True),
|
||||
'version': fields.IntegerFields(nullable=True)
|
||||
}
|
||||
|
||||
STRING_KEYS = ['id']
|
||||
|
||||
|
||||
class SoftDeleteObjectMixin(object):
|
||||
"""
|
||||
|
@ -696,20 +467,9 @@ class SoftDeleteObjectMixin(object):
|
|||
|
||||
This adds the fields that we use in common for all soft-deleted objects.
|
||||
"""
|
||||
FIELDS = {
|
||||
'deleted': {
|
||||
'schema': {
|
||||
'type': ['string', 'integer'],
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'deleted_at': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'format': 'date-time',
|
||||
},
|
||||
'read_only': True
|
||||
}
|
||||
fields = {
|
||||
'deleted': fields.StringFields(nullable=True),
|
||||
'deleted_at': fields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
|
||||
|
@ -719,10 +479,45 @@ class PagedListObjectMixin(object):
|
|||
|
||||
This adds fields that would populate API metadata for collections.
|
||||
"""
|
||||
FIELDS = {
|
||||
'total_count': {
|
||||
'schema': {
|
||||
'type': ['integer'],
|
||||
}
|
||||
}
|
||||
fields = {
|
||||
'total_count': fields.IntegerFields(nullable=True)
|
||||
}
|
||||
|
||||
|
||||
class DesignateRegistry(base.VersionedObjectRegistry):
|
||||
def registration_hook(self, cls, index):
|
||||
for name, field in six.iteritems(cls.fields):
|
||||
attr = get_dict_attr(cls, name)
|
||||
|
||||
def getter(self, name=name):
|
||||
attrname = _get_attrname(name)
|
||||
self._obj_check_relation(name)
|
||||
return getattr(self, attrname, None)
|
||||
|
||||
def setter(self, value, name=name, field=field):
|
||||
attrname = _get_attrname(name)
|
||||
field_value = field.coerce(self, name, value)
|
||||
if field.read_only and hasattr(self, attrname):
|
||||
# Note(yjiang5): _from_db_object() may iterate
|
||||
# every field and write, no exception in such situation.
|
||||
if getattr(self, attrname) != field_value:
|
||||
raise exception.ReadOnlyFieldError(field=name)
|
||||
else:
|
||||
return
|
||||
|
||||
self._changed_fields.add(name)
|
||||
# TODO(daidv): _obj_original_values shoud be removed
|
||||
# after OVO migration completed.
|
||||
if (self.obj_attr_is_set(name) and value != getattr(self, name)
|
||||
and name not in list(six.iterkeys(
|
||||
self._obj_original_values))):
|
||||
self._obj_original_values[name] = getattr(self, name)
|
||||
try:
|
||||
return setattr(self, attrname, field_value)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(
|
||||
_LE('Error setting %{obj_name}s.%{field_name}s'),
|
||||
{"obj_name": self.obj_name(), "field_name": name})
|
||||
|
||||
setattr(cls, name, property(getter, setter, attr.fdel))
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -165,8 +165,18 @@ class IPV4AndV6AddressField(ovoo_fields.IPV4AndV6AddressField):
|
|||
return str(value)
|
||||
|
||||
|
||||
class EnumField(ovoo_fields.EnumField):
|
||||
pass
|
||||
class Enum(ovoo_fields.Enum):
|
||||
def get_schema(self):
|
||||
return {
|
||||
'enum': self._valid_values,
|
||||
'type': 'any'
|
||||
}
|
||||
|
||||
|
||||
class EnumField(ovoo_fields.BaseEnumField):
|
||||
def __init__(self, valid_values, **kwargs):
|
||||
self.AUTO_TYPE = Enum(valid_values=valid_values)
|
||||
super(EnumField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class DomainField(StringFields):
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -1,452 +0,0 @@
|
|||
# Copyright (c) 2017 Fujitsu Vietnam Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import six
|
||||
from oslo_log import log as logging
|
||||
from oslo_versionedobjects import exception
|
||||
from oslo_utils import excutils
|
||||
from oslo_versionedobjects import base
|
||||
from oslo_versionedobjects.base import VersionedObjectDictCompat as DictObjectMixin # noqa
|
||||
|
||||
from designate.i18n import _
|
||||
from designate.i18n import _LE
|
||||
from designate.objects import fields
|
||||
from designate import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_attrname(name):
|
||||
return "_obj_{}".format(name)
|
||||
|
||||
|
||||
def get_dict_attr(klass, attr):
|
||||
for klass in [klass] + klass.mro():
|
||||
if attr in klass.__dict__:
|
||||
return klass.__dict__[attr]
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class DesignateObject(base.VersionedObject):
|
||||
OBJ_SERIAL_NAMESPACE = 'designate_object'
|
||||
OBJ_PROJECT_NAMESPACE = 'designate'
|
||||
|
||||
STRING_KEYS = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DesignateObject, self).__init__(self, *args, **kwargs)
|
||||
self._obj_original_values = dict()
|
||||
self.FIELDS = self.fields
|
||||
|
||||
@classmethod
|
||||
def _make_obj_str(cls, keys):
|
||||
msg = "<%(name)s" % {'name': cls.obj_name()}
|
||||
for key in keys:
|
||||
msg += " {0}:'%({0})s'".format(key)
|
||||
msg += ">"
|
||||
return msg
|
||||
|
||||
def __str__(self):
|
||||
return (self._make_obj_str(self.STRING_KEYS)
|
||||
% self)
|
||||
|
||||
def save(self, context):
|
||||
pass
|
||||
|
||||
def _obj_check_relation(self, name):
|
||||
if name in self.fields:
|
||||
if hasattr(self.fields.get(name), 'objname'):
|
||||
if not self.obj_attr_is_set(name):
|
||||
raise exceptions.RelationNotLoaded(
|
||||
object=self, relation=name)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert the object to a simple dictionary."""
|
||||
data = {}
|
||||
|
||||
for field in six.iterkeys(self.fields):
|
||||
if self.obj_attr_is_set(field):
|
||||
val = getattr(self, field)
|
||||
if isinstance(val, ListObjectMixin):
|
||||
data[field] = val.to_list()
|
||||
elif isinstance(val, DesignateObject):
|
||||
data[field] = val.to_dict()
|
||||
else:
|
||||
data[field] = val
|
||||
|
||||
return data
|
||||
|
||||
def update(self, values):
|
||||
"""Update a object's fields with the supplied key/value pairs"""
|
||||
for k, v in values.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, _dict):
|
||||
instance = cls()
|
||||
|
||||
for field, value in _dict.items():
|
||||
if (field in instance.fields and
|
||||
hasattr(instance.fields.get(field), 'objname')):
|
||||
relation_cls_name = instance.fields[field].objname
|
||||
# We're dealing with a relation, we'll want to create the
|
||||
# correct object type and recurse
|
||||
relation_cls = cls.obj_class_from_name(
|
||||
relation_cls_name, '1.0')
|
||||
|
||||
if isinstance(value, list):
|
||||
setattr(instance, field, relation_cls.from_list(value))
|
||||
else:
|
||||
setattr(instance, field, relation_cls.from_dict(value))
|
||||
|
||||
else:
|
||||
setattr(instance, field, value)
|
||||
|
||||
return instance
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.__class__ != other.__class__:
|
||||
return False
|
||||
|
||||
return self.obj_to_primitive() == other.obj_to_primitive()
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self.__eq__(other))
|
||||
|
||||
def __repr__(self):
|
||||
return "OVO Objects"
|
||||
|
||||
# TODO(daidv): all of bellow functions should
|
||||
# be removed when we completed migration.
|
||||
def to_primitive(self):
|
||||
return self.obj_to_primitive()
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive, context=None):
|
||||
return cls.obj_from_primitive(primitive, context)
|
||||
|
||||
@classmethod
|
||||
def obj_cls_from_name(cls, name):
|
||||
return cls.obj_class_from_name(name, '1.0')
|
||||
|
||||
@classmethod
|
||||
def obj_get_schema(cls):
|
||||
return cls.to_json_schema()
|
||||
|
||||
def obj_reset_changes(self, fields=None, recursive=False):
|
||||
"""Reset the list of fields that have been changed.
|
||||
|
||||
:param fields: List of fields to reset, or "all" if None.
|
||||
:param recursive: Call obj_reset_changes(recursive=True) on
|
||||
any sub-objects within the list of fields
|
||||
being reset.
|
||||
|
||||
This is NOT "revert to previous values".
|
||||
|
||||
Specifying fields on recursive resets will only be honored at the top
|
||||
level. Everything below the top will reset all.
|
||||
"""
|
||||
if recursive:
|
||||
for field in self.obj_get_changes():
|
||||
|
||||
# Ignore fields not in requested set (if applicable)
|
||||
if fields and field not in fields:
|
||||
continue
|
||||
|
||||
# Skip any fields that are unset
|
||||
if not self.obj_attr_is_set(field):
|
||||
continue
|
||||
|
||||
value = getattr(self, field)
|
||||
|
||||
# Don't reset nulled fields
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
# Reset straight Object and ListOfObjects fields
|
||||
if isinstance(self.fields[field], self.obj_fields.ObjectField):
|
||||
value.obj_reset_changes(recursive=True)
|
||||
elif isinstance(self.fields[field],
|
||||
self.obj_fields.ListOfObjectsField):
|
||||
for thing in value:
|
||||
thing.obj_reset_changes(recursive=True)
|
||||
|
||||
if fields:
|
||||
self._changed_fields -= set(fields)
|
||||
for field in fields:
|
||||
self._obj_original_values.pop(field, None)
|
||||
else:
|
||||
self._changed_fields.clear()
|
||||
self._obj_original_values = dict()
|
||||
|
||||
def obj_get_original_value(self, field):
|
||||
"""Returns the original value of a field."""
|
||||
if field in list(six.iterkeys(self._obj_original_values)):
|
||||
return self._obj_original_values[field]
|
||||
elif self.obj_attr_is_set(field):
|
||||
return getattr(self, field)
|
||||
else:
|
||||
raise KeyError(field)
|
||||
|
||||
@property
|
||||
def obj_fields(self):
|
||||
return list(self.fields.keys()) + self.obj_extra_fields
|
||||
|
||||
@property
|
||||
def obj_context(self):
|
||||
return self._context
|
||||
|
||||
def validate(self):
|
||||
# NOTE(kiall, daidv): We make use of the Object registry here
|
||||
# in order to avoid an impossible circular import.
|
||||
ValidationErrorList = self.obj_cls_from_name('ValidationErrorList')
|
||||
ValidationError = self.obj_cls_from_name('ValidationError')
|
||||
self.fields = self.FIELDS
|
||||
for name in self.fields:
|
||||
field = self.fields[name]
|
||||
if self.obj_attr_is_set(name):
|
||||
value = getattr(self, name) # Check relation
|
||||
if isinstance(value, ListObjectMixin):
|
||||
for obj in value.objects:
|
||||
obj.validate()
|
||||
else:
|
||||
try:
|
||||
field.coerce(self, name, value) # Check value
|
||||
except Exception as e:
|
||||
raise exceptions.InvalidObject(
|
||||
"{} is invalid".format(name))
|
||||
elif not field.nullable:
|
||||
# Check required is True ~ nullable is False
|
||||
errors = ValidationErrorList()
|
||||
e = ValidationError()
|
||||
e.path = ['records', 0]
|
||||
e.validator = 'required'
|
||||
e.validator_value = [name]
|
||||
e.message = "'%s' is a required property" % name
|
||||
errors.append(e)
|
||||
raise exceptions.InvalidObject(
|
||||
"Provided object does not match "
|
||||
"schema", errors=errors, object=self)
|
||||
|
||||
|
||||
class ListObjectMixin(base.ObjectListBase):
|
||||
LIST_ITEM_TYPE = DesignateObject
|
||||
|
||||
@classmethod
|
||||
def _obj_from_primitive(cls, context, objver, primitive):
|
||||
instance = cls()
|
||||
instance.VERSION = objver
|
||||
instance._context = context
|
||||
|
||||
for field, value in primitive['designate_object.data'].items():
|
||||
if field == 'objects':
|
||||
instance.objects = [
|
||||
DesignateObject.obj_from_primitive(v) for v in value]
|
||||
elif isinstance(value, dict) and 'designate_object.name' in value:
|
||||
setattr(instance, field,
|
||||
DesignateObject.obj_from_primitive(value))
|
||||
else:
|
||||
setattr(instance, field, value)
|
||||
|
||||
instance._obj_changes = set(
|
||||
primitive.get('designate_object.changes', []))
|
||||
instance._obj_original_values = \
|
||||
primitive.get('designate_object.original_values', {})
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, _list):
|
||||
instance = cls()
|
||||
|
||||
for item in _list:
|
||||
instance.append(cls.LIST_ITEM_TYPE.from_dict(item))
|
||||
|
||||
return instance
|
||||
|
||||
def to_list(self):
|
||||
|
||||
list_ = []
|
||||
|
||||
for item in self.objects:
|
||||
if isinstance(item, ListObjectMixin):
|
||||
list_.append(item.to_list())
|
||||
elif isinstance(item, DesignateObject):
|
||||
list_.append(item.to_dict())
|
||||
else:
|
||||
list_.append(item)
|
||||
|
||||
return list_
|
||||
|
||||
def __str__(self):
|
||||
return (_("<%(type)s count:'%(count)s' object:'%(list_type)s'>")
|
||||
% {'count': len(self),
|
||||
'type': self.LIST_ITEM_TYPE.obj_name(),
|
||||
'list_type': self.obj_name()})
|
||||
|
||||
def __iter__(self):
|
||||
"""List iterator interface"""
|
||||
return iter(self.objects)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""List index access"""
|
||||
if isinstance(index, slice):
|
||||
new_obj = self.__class__()
|
||||
new_obj.objects = self.objects[index]
|
||||
new_obj.obj_reset_changes()
|
||||
return new_obj
|
||||
return self.objects[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
"""Set list index value"""
|
||||
self.objects[index] = value
|
||||
|
||||
def __contains__(self, value):
|
||||
"""List membership test"""
|
||||
return value in self.objects
|
||||
|
||||
def append(self, value):
|
||||
"""Append a value to the list"""
|
||||
return self.objects.append(value)
|
||||
|
||||
def extend(self, values):
|
||||
"""Extend the list by appending all the items in the given list"""
|
||||
return self.objects.extend(values)
|
||||
|
||||
def pop(self, index):
|
||||
"""Pop a value from the list"""
|
||||
return self.objects.pop(index)
|
||||
|
||||
def insert(self, index, value):
|
||||
"""Insert a value into the list at the given index"""
|
||||
return self.objects.insert(index, value)
|
||||
|
||||
def remove(self, value):
|
||||
"""Remove a value from the list"""
|
||||
return self.objects.remove(value)
|
||||
|
||||
def index(self, value):
|
||||
"""List index of value"""
|
||||
return self.objects.index(value)
|
||||
|
||||
def count(self, value):
|
||||
"""List count of value occurrences"""
|
||||
return self.objects.count(value)
|
||||
|
||||
|
||||
class AttributeListObjectMixin(ListObjectMixin):
|
||||
"""
|
||||
Mixin class for "Attribute" objects.
|
||||
|
||||
Attribute objects are ListObjects, who's memebers have a "key" and "value"
|
||||
property, which should be exposed on the list itself as list.<key>.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, _dict):
|
||||
instances = cls.from_list([{'key': k, 'value': v} for k, v
|
||||
in _dict.items()])
|
||||
|
||||
return cls.from_list(instances)
|
||||
|
||||
def to_dict(self):
|
||||
data = {}
|
||||
|
||||
for item in self.objects:
|
||||
data[item.key] = item.value
|
||||
|
||||
return data
|
||||
|
||||
def get(self, key, default=None):
|
||||
for obj in self.objects:
|
||||
if obj.key == key:
|
||||
return obj.value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
class PersistentObjectMixin(object):
|
||||
"""
|
||||
Mixin class for Persistent objects.
|
||||
|
||||
This adds the fields that we use in common for all persistent objects.
|
||||
"""
|
||||
fields = {
|
||||
'id': fields.UUIDFields(nullable=True),
|
||||
'created_at': fields.DateTimeField(nullable=True),
|
||||
'updated_at': fields.DateTimeField(nullable=True),
|
||||
'version': fields.IntegerFields(nullable=True)
|
||||
}
|
||||
|
||||
|
||||
class SoftDeleteObjectMixin(object):
|
||||
"""
|
||||
Mixin class for Soft-Deleted objects.
|
||||
|
||||
This adds the fields that we use in common for all soft-deleted objects.
|
||||
"""
|
||||
fields = {
|
||||
'deleted': fields.StringFields(nullable=True),
|
||||
'deleted_at': fields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
|
||||
class PagedListObjectMixin(object):
|
||||
"""
|
||||
Mixin class for List objects.
|
||||
|
||||
This adds fields that would populate API metadata for collections.
|
||||
"""
|
||||
fields = {
|
||||
'total_count': fields.IntegerFields(nullable=True)
|
||||
}
|
||||
|
||||
|
||||
class DesignateRegistry(base.VersionedObjectRegistry):
|
||||
def registration_hook(self, cls, index):
|
||||
for name, field in six.iteritems(cls.fields):
|
||||
attr = get_dict_attr(cls, name)
|
||||
|
||||
def getter(self, name=name):
|
||||
attrname = _get_attrname(name)
|
||||
self._obj_check_relation(name)
|
||||
return getattr(self, attrname, None)
|
||||
|
||||
def setter(self, value, name=name, field=field):
|
||||
attrname = _get_attrname(name)
|
||||
field_value = field.coerce(self, name, value)
|
||||
if field.read_only and hasattr(self, attrname):
|
||||
# Note(yjiang5): _from_db_object() may iterate
|
||||
# every field and write, no exception in such situation.
|
||||
if getattr(self, attrname) != field_value:
|
||||
raise exception.ReadOnlyFieldError(field=name)
|
||||
else:
|
||||
return
|
||||
|
||||
self._changed_fields.add(name)
|
||||
# TODO(daidv): _obj_original_values shoud be removed
|
||||
# after OVO migration completed.
|
||||
if (self.obj_attr_is_set(name) and value != getattr(self, name)
|
||||
and name not in list(six.iterkeys(
|
||||
self._obj_original_values))):
|
||||
self._obj_original_values[name] = getattr(self, name)
|
||||
try:
|
||||
return setattr(self, attrname, field_value)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
attr = "%s.%s" % (self.obj_name(), name)
|
||||
LOG.exception(_LE('Error setting %(attr)s'),
|
||||
{'attr': attr})
|
||||
|
||||
setattr(cls, name, property(getter, setter, attr.fdel))
|
|
@ -14,7 +14,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate import utils
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from oslo_versionedobjects import exception as ovo_exc
|
|||
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
from designate.objects.validation_error import ValidationError
|
||||
from designate.objects.validation_error import ValidationErrorList
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
from designate.objects.record import Record
|
||||
from designate.objects.record import RecordList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from designate import utils
|
|||
from designate import exceptions
|
||||
from designate.objects.validation_error import ValidationError
|
||||
from designate.objects.validation_error import ValidationErrorList
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
from oslo_versionedobjects import base as ovoo_base
|
||||
|
||||
from designate import utils
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import ovo_base as base
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
|
||||
|
|
|
@ -131,10 +131,7 @@ class DesignateObjectSerializer(messaging.NoOpSerializer):
|
|||
|
||||
def deserialize_entity(self, context, entity):
|
||||
if isinstance(entity, dict) and 'designate_object.name' in entity:
|
||||
if 'designate_object.version' in entity:
|
||||
entity = objects.OVODesignateObject.from_primitive(entity)
|
||||
else:
|
||||
entity = objects.DesignateObject.from_primitive(entity)
|
||||
entity = objects.DesignateObject.from_primitive(entity)
|
||||
elif isinstance(entity, (tuple, list, set)):
|
||||
entity = self._process_iterable(context, self.deserialize_entity,
|
||||
entity)
|
||||
|
|
|
@ -21,6 +21,7 @@ import oslotest.base
|
|||
from oslo_utils import timeutils
|
||||
|
||||
from designate import objects
|
||||
from designate.objects import base
|
||||
from designate.objects import adapters
|
||||
|
||||
|
||||
|
@ -37,6 +38,7 @@ class DesignateTestAdapter(adapters.DesignateAdapter):
|
|||
}
|
||||
|
||||
|
||||
@base.DesignateRegistry.register
|
||||
class DesignateTestPersistentObject(
|
||||
objects.DesignateObject, objects.base.PersistentObjectMixin):
|
||||
pass
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
from operator import attrgetter
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
from oslo_log import log as logging
|
||||
import mock
|
||||
|
@ -26,49 +25,42 @@ import testtools
|
|||
|
||||
from designate import exceptions
|
||||
from designate import objects
|
||||
from designate.objects import base
|
||||
from designate.objects import fields
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@base.DesignateRegistry.register
|
||||
class TestObject(objects.DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {},
|
||||
'name': {},
|
||||
'nested': {
|
||||
'relation': True,
|
||||
'relation_cls': 'TestObject',
|
||||
},
|
||||
'nested_list': {
|
||||
'relation': True,
|
||||
'relation_cls': 'TestObjectList',
|
||||
},
|
||||
fields = {
|
||||
'id': fields.AnyField(nullable=True),
|
||||
'name': fields.AnyField(nullable=True),
|
||||
'nested': fields.ObjectFields('TestObject', nullable=True),
|
||||
'nested_list': fields.ObjectFields('TestObjectList', nullable=True),
|
||||
}
|
||||
|
||||
|
||||
class TestObjectDict(objects.DictObjectMixin, TestObject):
|
||||
@base.DesignateRegistry.register
|
||||
class TestObjectDict(TestObject, objects.DictObjectMixin):
|
||||
pass
|
||||
|
||||
|
||||
@base.DesignateRegistry.register
|
||||
class TestObjectList(objects.ListObjectMixin, objects.DesignateObject):
|
||||
LIST_ITEM_TYPE = TestObject
|
||||
|
||||
fields = {
|
||||
'objects': fields.ListOfObjectsField('TestObject'),
|
||||
}
|
||||
|
||||
|
||||
@base.DesignateRegistry.register
|
||||
class TestValidatableObject(objects.DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid',
|
||||
},
|
||||
'required': True,
|
||||
},
|
||||
'nested': {
|
||||
'relation': True,
|
||||
'relation_cls': 'TestValidatableObject',
|
||||
'schema': {
|
||||
'$ref': 'obj://TestValidatableObject#/'
|
||||
}
|
||||
}
|
||||
fields = {
|
||||
'id': fields.UUIDFields(),
|
||||
'nested': fields.ObjectFields('TestValidatableObject',
|
||||
nullable=True),
|
||||
}
|
||||
|
||||
|
||||
|
@ -87,16 +79,17 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
primitive = {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
},
|
||||
'designate_object.changes': [],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
|
||||
obj = objects.DesignateObject.from_primitive(primitive)
|
||||
|
||||
# Validate it has been thawed correctly
|
||||
self.assertEqual('MyID', obj.id)
|
||||
self.assertEqual(1, obj.id)
|
||||
|
||||
# Ensure the ID field has a value
|
||||
self.assertTrue(obj.obj_attr_is_set('id'))
|
||||
|
@ -111,33 +104,35 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
primitive = {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'nested': {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID-Nested',
|
||||
'id': 2,
|
||||
},
|
||||
'designate_object.changes': [],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
},
|
||||
'designate_object.changes': [],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
|
||||
obj = objects.DesignateObject.from_primitive(primitive)
|
||||
|
||||
# Validate it has been thawed correctly
|
||||
self.assertEqual('MyID', obj.id)
|
||||
self.assertEqual('MyID-Nested', obj.nested.id)
|
||||
self.assertEqual(1, obj.id)
|
||||
self.assertEqual(2, obj.nested.id)
|
||||
|
||||
def test_from_dict(self):
|
||||
obj = TestObject.from_dict({
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
})
|
||||
|
||||
# Validate it has been thawed correctly
|
||||
self.assertEqual('MyID', obj.id)
|
||||
self.assertEqual(1, obj.id)
|
||||
|
||||
# Ensure the ID field has a value
|
||||
self.assertTrue(obj.obj_attr_is_set('id'))
|
||||
|
@ -150,15 +145,15 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
|
||||
def test_from_dict_recursive(self):
|
||||
obj = TestObject.from_dict({
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'nested': {
|
||||
'id': 'MyID-Nested',
|
||||
'id': 2,
|
||||
},
|
||||
})
|
||||
|
||||
# Validate it has been thawed correctly
|
||||
self.assertEqual('MyID', obj.id)
|
||||
self.assertEqual('MyID-Nested', obj.nested.id)
|
||||
self.assertEqual(1, obj.id)
|
||||
self.assertEqual(2, obj.nested.id)
|
||||
|
||||
# Ensure the changes list has two entries, one for the id field and the
|
||||
# other for the nested field
|
||||
|
@ -169,18 +164,18 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
|
||||
def test_from_dict_nested_list(self):
|
||||
obj = TestObject.from_dict({
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'nested_list': [{
|
||||
'id': 'MyID-Nested1',
|
||||
'id': 2,
|
||||
}, {
|
||||
'id': 'MyID-Nested2',
|
||||
'id': 3,
|
||||
}],
|
||||
})
|
||||
|
||||
# Validate it has been thawed correctly
|
||||
self.assertEqual('MyID', obj.id)
|
||||
self.assertEqual('MyID-Nested1', obj.nested_list[0].id)
|
||||
self.assertEqual('MyID-Nested2', obj.nested_list[1].id)
|
||||
self.assertEqual(1, obj.id)
|
||||
self.assertEqual(2, obj.nested_list[0].id)
|
||||
self.assertEqual(3, obj.nested_list[1].id)
|
||||
|
||||
# Ensure the changes list has two entries, one for the id field and the
|
||||
# other for the nested field
|
||||
|
@ -190,36 +185,6 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
with testtools.ExpectedException(NotImplementedError):
|
||||
TestObject.from_list([])
|
||||
|
||||
def test_get_schema(self):
|
||||
obj = TestValidatableObject()
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
obj.validate()
|
||||
self.assertTrue(hasattr(obj, '_obj_validator'))
|
||||
|
||||
expected = {
|
||||
'description': 'Designate TestValidatableObject Object',
|
||||
'title': 'TestValidatableObject', 'required': ['id'],
|
||||
'additionalProperties': False,
|
||||
'$schema': 'http://json-schema.org/draft-04/hyper-schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string', 'format': 'uuid'
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = obj._obj_validator.schema
|
||||
self.assertEqual(expected, schema)
|
||||
|
||||
with testtools.ExpectedException(AttributeError): # bug
|
||||
schema = obj.obj_get_schema()
|
||||
|
||||
@unittest.expectedFailure # bug
|
||||
def test__schema_ref_resolver(self):
|
||||
from designate.objects import base
|
||||
base._schema_ref_resolver(
|
||||
'obj://TestValidatableObject#/subpathA/subpathB')
|
||||
|
||||
def test_init_invalid(self):
|
||||
with testtools.ExpectedException(TypeError):
|
||||
TestObject(extra_field='Fail')
|
||||
|
@ -242,8 +207,8 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
def test_setattr(self):
|
||||
obj = TestObject()
|
||||
|
||||
obj.id = 'MyID'
|
||||
self.assertEqual('MyID', obj.id)
|
||||
obj.id = 1
|
||||
self.assertEqual(1, obj.id)
|
||||
self.assertEqual(1, len(obj.obj_what_changed()))
|
||||
|
||||
obj.name = 'MyName'
|
||||
|
@ -257,17 +222,18 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
obj.badthing = 'demons'
|
||||
|
||||
def test_to_primitive(self):
|
||||
obj = TestObject(id='MyID')
|
||||
obj = TestObject(id=1)
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
primitive = obj.to_primitive()
|
||||
expected = {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
},
|
||||
'designate_object.changes': ['id'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
|
@ -279,44 +245,47 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
expected = {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'name': None,
|
||||
},
|
||||
'designate_object.changes': ['id', 'name'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
def test_to_primitive_recursive(self):
|
||||
obj = TestObject(id='MyID', nested=TestObject(id='MyID-Nested'))
|
||||
obj = TestObject(id=1, nested=TestObject(id=2))
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
primitive = obj.to_primitive()
|
||||
expected = {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'nested': {
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.data': {
|
||||
'id': 'MyID-Nested',
|
||||
'id': 2,
|
||||
},
|
||||
'designate_object.changes': ['id'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
},
|
||||
'designate_object.changes': ['id', 'nested'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
def test_to_dict(self):
|
||||
obj = TestObject(id='MyID')
|
||||
obj = TestObject(id=1)
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
|
@ -326,68 +295,36 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
# Ensure both the id and name attributes are returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'name': None,
|
||||
}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_to_dict_recursive(self):
|
||||
obj = TestObject(id='MyID', nested=TestObject(id='MyID-Nested'))
|
||||
obj = TestObject(id=1, nested=TestObject(id=2))
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
'id': 1,
|
||||
'nested': {
|
||||
'id': 'MyID-Nested',
|
||||
'id': 2,
|
||||
},
|
||||
}
|
||||
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_update(self):
|
||||
obj = TestObject(id='MyID', name='test')
|
||||
obj = TestObject(id=1, name='test')
|
||||
obj.update({'id': 'new_id', 'name': 'new_name'})
|
||||
self.assertEqual('new_id', obj.id)
|
||||
self.assertEqual('new_name', obj.name)
|
||||
|
||||
def test_update_unexpected_attribute(self):
|
||||
obj = TestObject(id='MyID', name='test')
|
||||
obj = TestObject(id=1, name='test')
|
||||
with testtools.ExpectedException(AttributeError):
|
||||
obj.update({'id': 'new_id', 'new_key': 3})
|
||||
|
||||
def test_is_valid(self):
|
||||
obj = TestValidatableObject(id='MyID')
|
||||
|
||||
# ID should be a UUID, So - Not Valid.
|
||||
self.assertFalse(obj.is_valid)
|
||||
|
||||
# Correct the ID field
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
|
||||
# ID is now a UUID, So - Valid.
|
||||
self.assertTrue(obj.is_valid)
|
||||
|
||||
def test_is_valid_recursive(self):
|
||||
obj = TestValidatableObject(
|
||||
id='MyID',
|
||||
nested=TestValidatableObject(id='MyID'))
|
||||
|
||||
# ID should be a UUID, So - Not Valid.
|
||||
self.assertFalse(obj.is_valid)
|
||||
|
||||
# Correct the outer objects ID field
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
|
||||
# Outer ID is now a UUID, Nested ID is Not. So - Invalid.
|
||||
self.assertFalse(obj.is_valid)
|
||||
|
||||
# Correct the nested objects ID field
|
||||
obj.nested.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
|
||||
# Outer and Nested IDs are now UUIDs. So - Valid.
|
||||
self.assertTrue(obj.is_valid)
|
||||
|
||||
def test_validate(self):
|
||||
obj = TestValidatableObject()
|
||||
|
||||
|
@ -395,45 +332,29 @@ class DesignateObjectTest(oslotest.base.BaseTestCase):
|
|||
with testtools.ExpectedException(exceptions.InvalidObject):
|
||||
obj.validate()
|
||||
|
||||
# Set the ID field to an invalid value
|
||||
obj.id = 'MyID'
|
||||
|
||||
# ID is now set, but to an invalid value, still invalid
|
||||
with testtools.ExpectedException(exceptions.InvalidObject):
|
||||
obj.validate()
|
||||
with testtools.ExpectedException(ValueError):
|
||||
obj.id = 'MyID'
|
||||
|
||||
# Set the ID field to a valid value
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
obj.validate()
|
||||
|
||||
def test_validate_recursive(self):
|
||||
with testtools.ExpectedException(ValueError):
|
||||
TestValidatableObject(
|
||||
id='MyID',
|
||||
nested=TestValidatableObject(id='MyID'))
|
||||
|
||||
with testtools.ExpectedException(ValueError):
|
||||
TestValidatableObject(
|
||||
id='ffded5c4-e4f6-4e02-a175-48e13c5c12a0',
|
||||
nested=TestValidatableObject(
|
||||
id='MyID'))
|
||||
|
||||
obj = TestValidatableObject(
|
||||
id='MyID',
|
||||
nested=TestValidatableObject(id='MyID'))
|
||||
|
||||
# ID should be a UUID, So - Invalid.
|
||||
with testtools.ExpectedException(exceptions.InvalidObject):
|
||||
obj.validate()
|
||||
|
||||
# Correct the outer objects ID field
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
|
||||
# Outer ID is now set, Inner ID is not, still invalid.
|
||||
e = self.assertRaises(exceptions.InvalidObject, obj.validate)
|
||||
|
||||
# Ensure we have exactly one error and fetch it
|
||||
self.assertEqual(1, len(e.errors))
|
||||
error = e.errors.pop(0)
|
||||
|
||||
# Ensure the format validator has triggered the failure.
|
||||
self.assertEqual('format', error.validator)
|
||||
|
||||
# Ensure the nested ID field has triggered the failure.
|
||||
# For some reason testtools turns lists into deques :/
|
||||
self.assertEqual(error.path, ['nested', 'id'])
|
||||
|
||||
# Set the Nested ID field to a valid value
|
||||
obj.nested.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
id='ffded5c4-e4f6-4e02-a175-48e13c5c12a0',
|
||||
nested=TestValidatableObject(
|
||||
id='ffded5c4-e4f6-4e02-a175-48e13c5c12a0'))
|
||||
obj.validate()
|
||||
|
||||
def test_obj_attr_is_set(self):
|
||||
|
@ -577,11 +498,11 @@ class DictObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
def test_cast_to_dict(self):
|
||||
# Create an object
|
||||
obj = TestObjectDict()
|
||||
obj.id = "My ID"
|
||||
obj.id = 1
|
||||
obj.name = "My Name"
|
||||
|
||||
expected = {
|
||||
'id': 'My ID',
|
||||
'id': 1,
|
||||
'name': 'My Name',
|
||||
}
|
||||
|
||||
|
@ -613,7 +534,7 @@ class DictObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
|
||||
def test_get_default(self):
|
||||
obj = TestObjectDict(name='n')
|
||||
v = obj.get('name', default='default')
|
||||
v = obj.get('name', value='default')
|
||||
self.assertEqual('n', v)
|
||||
|
||||
def test_get_default_with_patch(self):
|
||||
|
@ -621,7 +542,7 @@ class DictObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
fname = 'designate.objects.base.DesignateObject.obj_attr_is_set'
|
||||
with mock.patch(fname) as attr_is_set:
|
||||
attr_is_set.return_value = False
|
||||
v = obj.get('name', default='default')
|
||||
v = obj.get('name', value='default')
|
||||
self.assertEqual('default', v)
|
||||
|
||||
def test_iteritems(self):
|
||||
|
@ -648,15 +569,18 @@ class ListObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'One'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'},
|
||||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'Two'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'},
|
||||
],
|
||||
},
|
||||
'designate_object.changes': ['objects'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0',
|
||||
}
|
||||
|
||||
obj = objects.DesignateObject.from_primitive(primitive)
|
||||
|
@ -701,15 +625,18 @@ class ListObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'One'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'},
|
||||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'Two'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'},
|
||||
],
|
||||
},
|
||||
'designate_object.changes': ['objects'],
|
||||
'designate_object.original_values': {},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'
|
||||
}
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
|
@ -718,7 +645,7 @@ class ListObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
obj_one = TestObject()
|
||||
obj_two = TestObject()
|
||||
obj_two.id = "Two"
|
||||
obj_one.id = obj_two
|
||||
obj_one.nested = obj_two
|
||||
|
||||
# Create a ListObject
|
||||
obj = TestObjectList(objects=[obj_one, obj_two])
|
||||
|
@ -729,19 +656,26 @@ class ListObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
'designate_object.changes': ['objects'],
|
||||
'designate_object.data': {
|
||||
'objects': [
|
||||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id':
|
||||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'Two'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}}},
|
||||
{'designate_object.changes': ['nested'],
|
||||
'designate_object.data': {'nested':
|
||||
{
|
||||
'designate_object.changes': [
|
||||
'id'],
|
||||
'designate_object.data': {
|
||||
'id': 'Two'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'}},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'},
|
||||
{'designate_object.changes': ['id'],
|
||||
'designate_object.data': {'id': 'Two'},
|
||||
'designate_object.name': 'TestObject',
|
||||
'designate_object.original_values': {}}]},
|
||||
'designate_object.original_values': {}}
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'}]},
|
||||
'designate_object.namespace': 'designate',
|
||||
'designate_object.version': '1.0'}
|
||||
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
|
@ -879,21 +813,12 @@ class ListObjectMixinTest(oslotest.base.BaseTestCase):
|
|||
|
||||
self.assertEqual([obj_one, obj_two, obj_three], obj.objects)
|
||||
|
||||
def test_to_dict(self):
|
||||
# Create a ListObject containing a DesignateObject
|
||||
obj_one = objects.DesignateObject()
|
||||
obj = TestObjectList(objects=obj_one)
|
||||
|
||||
dict_ = obj.to_dict()
|
||||
expected = {'objects': {}}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_to_dict_list_mixin(self):
|
||||
# Create a ListObject containing an ObjectList
|
||||
obj = TestObjectList(objects=TestObjectList())
|
||||
obj = TestObjectList(objects=[TestObject()])
|
||||
|
||||
dict_ = obj.to_dict()
|
||||
expected = {'objects': []}
|
||||
expected = {'objects': [{}]}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_to_list(self):
|
||||
|
|
|
@ -37,17 +37,6 @@ def debug(*a, **kw):
|
|||
LOG.debug("%s: %s", k, repr(kw[k]))
|
||||
|
||||
|
||||
class TestRecordSet(objects.RecordSet):
|
||||
FIELDS = {
|
||||
'id': {},
|
||||
'name': {},
|
||||
'records': {
|
||||
'relation': True,
|
||||
'relation_cls': 'RecordList',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_test_recordset():
|
||||
rs = objects.RecordSet(
|
||||
name='www.example.org.',
|
||||
|
|
Loading…
Reference in New Issue