Commit 1804a9df authored by Zuul's avatar Zuul Committed by Gerrit Code Review

Merge "Migrate object to OVO (7)"

parents 989cc2b2 b215bf69
......@@ -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(
......
# 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,199 +12,106 @@
# 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 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
from designate.objects import validation_error
from designate.schema import validators
from designate.schema import format
from designate.i18n import _
LOG = logging.getLogger(__name__)
class NotSpecifiedSentinel:
pass
def get_attrname(name):
"""Return the mangled name of the attribute's underlying storage."""
return '_obj_field_%s' % name
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)
def _get_attrname(name):
return "_obj_{}".format(name)
return setattr(self, get_attrname(name), value)
setattr(cls, field, property(getter, setter))
def get_dict_attr(klass, attr):
for klass in [klass] + klass.mro():
if attr in klass.__dict__:
return klass.__dict__[attr]
raise AttributeError
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)
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})
super(DesignateObject, self).__init__(self, *args, **kwargs)
self._obj_original_values = dict()
self.FIELDS = self.fields
@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]
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
@classmethod
def from_primitive(cls, primitive):
"""
Construct an object from primitive types
def __str__(self):
return (self._make_obj_str(self.STRING_KEYS)
% self)
This is used while deserializing the object.
"""
objcls = cls.obj_cls_from_name(primitive['designate_object.name'])
return objcls._obj_from_primitive(primitive)
def save(self, context):
pass
@classmethod
def _obj_from_primitive(cls, primitive):
instance = cls()
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)
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)
def to_dict(self):
"""Convert the object to a simple dictionary."""
data = {}
instance._obj_changes = set(primitive['designate_object.changes'])
instance._obj_original_values = \
primitive['designate_object.original_values']
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):
data[field] = val.to_list()
elif isinstance(val, DesignateObject):
data[field] = val.to_dict()
else:
data[field] = val
return instance
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
instance.FIELDS[field].get('relation', False)):
relation_cls_name = instance.FIELDS[field]['relation_cls']
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_cls_from_name(relation_cls_name)
relation_cls = cls.obj_class_from_name(
relation_cls_name, '1.0')
if isinstance(value, list):
setattr(instance, field, relation_cls.from_list(value))
......@@ -220,165 +127,119 @@ class DesignateObject(object):
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 __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)
def __init__(self, **kwargs):
self._obj_changes = set()
self._obj_original_values = dict()
def __eq__(self, other):
if self.__class__ != other.__class__:
return False
for name, value in kwargs.items():
if name in list(six.iterkeys(self.FIELDS)):
setattr(self, name, value)
else:
raise TypeError("__init__() got an unexpected keyword "
"argument '%(name)s'" % {'name': name})
return self.obj_to_primitive() == other.obj_to_primitive()
def __str__(self):
return (self._make_obj_str(self.STRING_KEYS)
% self)
def __ne__(self, other):
return not (self.__eq__(other))
@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 __repr__(self):
return "OVO Objects"
def to_primitive(self):
# TODO(daidv): all of bellow functions should
# be removed when we completed migration.
def nested_sort(self, key, value):
"""
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.
This function ensure that change fields list
is sorted.
:param key:
:param value:
:return:
"""
data = {}
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)
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 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)
@property
def is_valid(self):
"""Returns True if the Object is valid."""
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
make_class_validator(self)
def to_primitive(self):
return self.nested_sort(None, self.obj_to_primitive())
return self._obj_validator.is_valid(self.to_dict())
@classmethod
def from_primitive(cls, primitive, context=None):
return cls.obj_from_primitive(primitive, context)
def validate(self):
@classmethod
def obj_cls_from_name(cls, name):
return cls.obj_class_from_name(name, '1.0')
make_class_validator(self)
@classmethod
def obj_get_schema(cls):
return cls.to_json_schema()
ValidationErrorList = validation_error.ValidationErrorList
ValidationError = validation_error.ValidationError
def obj_reset_changes(self, fields=None, recursive=False):
"""Reset the list of fields that have been changed.
errors = ValidationErrorList()
: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.
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)
LOG.debug("Validating '%(name)s' object with values: %(values)r", {
'name': self.obj_name(),
'values': values,
})
for error in self._obj_validator.iter_errors(values):
errors.append(ValidationError.from_js_error(error))
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)
This is NOT "revert to previous values".
def obj_attr_is_set(self, name):
Specifying fields on recursive resets will only be honored at the top
level. Everything below the top will reset all.
"""
Return True or False depending of if a particular attribute has had
an attribute's value explicitly set.
"""
return hasattr(self, get_attrname(name))
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
def obj_what_changed(self):
"""Returns a set of fields that have been modified."""
return set(self._obj_changes)
# Skip any fields that are unset
if not self.obj_attr_is_set(field):
continue
def obj_get_changes(self):
"""Returns a dict of changed fields and their new values."""
changes = {}
value = getattr(self, field)
for key in self.obj_what_changed():
changes[key] = getattr(self, key)
# Don't reset nulled fields
if value is None:
continue
return changes
# 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)
else:
raise AttributeError(
"Designate object '%(type)s' has no attribute '%(name)s'" % {
'type': self.obj_name(),
'name': name,
})
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)
@property
def obj_fields(self):
return list(self.fields.keys()) + self.obj_extra_fields
return c_obj
@property
def obj_context(self):
return self._context
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,