]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Sync volume versionedobject to ORM
authorThang Pham <thang.g.pham@gmail.com>
Fri, 28 Aug 2015 14:02:50 +0000 (07:02 -0700)
committerThang Pham <thang.g.pham@gmail.com>
Tue, 1 Sep 2015 14:49:10 +0000 (07:49 -0700)
The following patch syncs the volume object to match the
ORM.  More specifically, it adds metadata, admin_metdata,
volume_attachment, and volume_type to the volume object.
These fields are required to convert cinder internals to
use volume versionedobject.  It was originally part of
a large patch, but is broken up to make reviews more
bearable.

Change-Id: I80c1734072cb26acbe207914e812a3b169b69dfb
Partial-Implements: blueprint cinder-objects

cinder/objects/__init__.py
cinder/objects/base.py
cinder/objects/volume.py
cinder/objects/volume_attachment.py [new file with mode: 0644]
cinder/objects/volume_type.py [new file with mode: 0644]
cinder/tests/unit/fake_volume.py
cinder/tests/unit/objects/test_base.py
cinder/tests/unit/objects/test_volume.py
cinder/tests/unit/objects/test_volume_attachment.py [new file with mode: 0644]
cinder/tests/unit/objects/test_volume_type.py [new file with mode: 0644]

index 28f1e9c55a09af79e38c2079c24d08dffcb18fda..b0b82323bd281bce46a21022b5c6dadaa95d5e46 100644 (file)
@@ -24,8 +24,10 @@ def register_all():
     # NOTE(danms): You must make sure your object gets imported in this
     # function in order for it to be registered by services that may
     # need to receive it via RPC.
-    __import__('cinder.objects.volume')
-    __import__('cinder.objects.service')
-    __import__('cinder.objects.snapshot')
     __import__('cinder.objects.backup')
     __import__('cinder.objects.consistencygroup')
+    __import__('cinder.objects.service')
+    __import__('cinder.objects.snapshot')
+    __import__('cinder.objects.volume')
+    __import__('cinder.objects.volume_attachment')
+    __import__('cinder.objects.volume_type')
index 578129516d6e163d6ace91fdf74484b462bfaad4..8353cee453b3d8fa02d0a83c2bfee1c062c5a964 100644 (file)
@@ -142,7 +142,10 @@ class CinderPersistentObject(object):
 
 
 class CinderComparableObject(base.ComparableVersionedObject):
-    pass
+    def __eq__(self, obj):
+        if hasattr(obj, 'obj_to_primitive'):
+            return self.obj_to_primitive() == obj.obj_to_primitive()
+        return False
 
 
 class ObjectListBase(base.ObjectListBase):
index a4d999e82a8b888ad339aa74db7f2a2cb1a2efe6..c2ece420381c07052541d17741c475114ae98be4 100644 (file)
@@ -24,15 +24,18 @@ from cinder.objects import base
 from cinder import utils
 
 CONF = cfg.CONF
-OPTIONAL_FIELDS = []
+OPTIONAL_FIELDS = ['metadata', 'admin_metadata',
+                   'volume_type', 'volume_attachment']
 LOG = logging.getLogger(__name__)
 
 
 @base.CinderObjectRegistry.register
 class Volume(base.CinderPersistentObject, base.CinderObject,
-             base.CinderObjectDictCompat):
+             base.CinderObjectDictCompat, base.CinderComparableObject):
     # Version 1.0: Initial version
-    VERSION = '1.0'
+    # Version 1.1: Added metadata, admin_metadata, volume_attachment, and
+    #              volume_type
+    VERSION = '1.1'
 
     fields = {
         'id': fields.UUIDField(),
@@ -70,12 +73,19 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
 
         'deleted': fields.BooleanField(default=False),
         'bootable': fields.BooleanField(default=False),
+        'multiattach': fields.BooleanField(default=False),
 
         'replication_status': fields.StringField(nullable=True),
         'replication_extended_status': fields.StringField(nullable=True),
         'replication_driver_data': fields.StringField(nullable=True),
 
         'previous_status': fields.StringField(nullable=True),
+
+        'metadata': fields.DictOfStringsField(nullable=True),
+        'admin_metadata': fields.DictOfStringsField(nullable=True),
+        'volume_type': fields.ObjectField('VolumeType', nullable=True),
+        'volume_attachment': fields.ListOfObjectsField('VolumeAttachment',
+                                                       nullable=True),
     }
 
     # NOTE(thangp): obj_extra_fields is used to hold properties that are not
@@ -94,19 +104,80 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
     def name(self):
         return CONF.volume_name_template % self.name_id
 
+    def __init__(self, *args, **kwargs):
+        super(Volume, self).__init__(*args, **kwargs)
+        self._orig_metadata = {}
+        self._orig_admin_metadata = {}
+
+        self._reset_metadata_tracking()
+
+    def obj_reset_changes(self, fields=None):
+        super(Volume, self).obj_reset_changes(fields)
+        self._reset_metadata_tracking(fields=fields)
+
+    def _reset_metadata_tracking(self, fields=None):
+        if fields is None or 'metadata' in fields:
+            self._orig_metadata = (dict(self.metadata)
+                                   if 'metadata' in self else {})
+        if fields is None or 'admin_metadata' in fields:
+            self._orig_admin_metadata = (dict(self.admin_metadata)
+                                         if 'admin_metadata' in self
+                                         else {})
+
+    def obj_what_changed(self):
+        changes = super(Volume, self).obj_what_changed()
+        if 'metadata' in self and self.metadata != self._orig_metadata:
+            changes.add('metadata')
+        if ('admin_metadata' in self and
+                self.admin_metadata != self._orig_admin_metadata):
+            changes.add('admin_metadata')
+
+        return changes
+
     def obj_make_compatible(self, primitive, target_version):
         """Make an object representation compatible with a target version."""
         super(Volume, self).obj_make_compatible(primitive, target_version)
         target_version = utils.convert_version_to_tuple(target_version)
 
     @staticmethod
-    def _from_db_object(context, volume, db_volume):
+    def _from_db_object(context, volume, db_volume, expected_attrs=None):
+        if expected_attrs is None:
+            expected_attrs = []
         for name, field in volume.fields.items():
-            value = db_volume[name]
+            if name in OPTIONAL_FIELDS:
+                continue
+            value = db_volume.get(name)
             if isinstance(field, fields.IntegerField):
                 value = value or 0
             volume[name] = value
 
+        # Get data from db_volume object that was queried by joined query
+        # from DB
+        if 'metadata' in expected_attrs:
+            volume.metadata = {}
+            metadata = db_volume.get('volume_metadata', [])
+            if metadata:
+                volume.metadata = {item['key']: item['value']
+                                   for item in metadata}
+        if 'admin_metadata' in expected_attrs:
+            volume.admin_metadata = {}
+            metadata = db_volume.get('volume_admin_metadata', [])
+            if metadata:
+                volume.admin_metadata = {item['key']: item['value']
+                                         for item in metadata}
+        if 'volume_type' in expected_attrs:
+            db_volume_type = db_volume.get('volume_type')
+            if db_volume_type:
+                volume.volume_type = objects.VolumeType._from_db_object(
+                    context, objects.VolumeType(), db_volume_type,
+                    expected_attrs='extra_specs')
+        if 'volume_attachment' in expected_attrs:
+            attachments = base.obj_make_list(
+                context, objects.VolumeAttachmentList(context),
+                objects.VolumeAttachment,
+                db_volume.get('volume_attachment'))
+            volume.volume_attachment = attachments.objects
+
         volume._context = context
         volume.obj_reset_changes()
         return volume
@@ -114,7 +185,9 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
     @base.remotable_classmethod
     def get_by_id(cls, context, id):
         db_volume = db.volume_get(context, id)
-        return cls._from_db_object(context, cls(context), db_volume)
+        expected_attrs = ['admin_metadata', 'metadata']
+        return cls._from_db_object(context, cls(context), db_volume,
+                                   expected_attrs=expected_attrs)
 
     @base.remotable
     def create(self):
@@ -129,30 +202,109 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
     def save(self):
         updates = self.cinder_obj_get_changes()
         if updates:
-            db.volume_update(self._context, self.id, updates)
+            if 'metadata' in updates:
+                # Metadata items that are not specified in the
+                # self.metadata will be deleted
+                metadata = updates.pop('metadata', None)
+                self.metadata = db.volume_metadata_update(self._context,
+                                                          self.id, metadata,
+                                                          True)
+            if self._context.is_admin and 'admin_metadata' in updates:
+                metadata = updates.pop('admin_metadata', None)
+                self.admin_metadata = db.volume_admin_metadata_update(
+                    self._context, self.id, metadata, True)
 
-        self.obj_reset_changes()
+            db.volume_update(self._context, self.id, updates)
+            self.obj_reset_changes()
 
     @base.remotable
     def destroy(self):
-        db.volume_destroy(self._context, self.id)
+        with self.obj_as_admin():
+            db.volume_destroy(self._context, self.id)
+
+    def obj_load_attr(self, attrname):
+        if attrname not in OPTIONAL_FIELDS:
+            raise exception.ObjectActionError(
+                action='obj_load_attr',
+                reason=_('attribute %s not lazy-loadable') % attrname)
+        if not self._context:
+            raise exception.OrphanedObjectError(method='obj_load_attr',
+                                                objtype=self.obj_name())
+
+        if attrname == 'metadata':
+            self.metadata = db.volume_metadata_get(self._context, self.id)
+        elif attrname == 'admin_metadata':
+            self.admin_metadata = {}
+            if self._context.is_admin:
+                self.admin_metadata = db.volume_admin_metadata_get(
+                    self._context, self.id)
+        elif attrname == 'volume_type':
+            self.volume_type = objects.VolumeType.get_by_id(
+                self._context, self.volume_type_id)
+        elif attrname == 'volume_attachment':
+            attachments = (
+                objects.VolumeAttachmentList.get_all_by_volume_id(
+                    self._context, self.id))
+            self.volume_attachment = attachments.objects
+
+        self.obj_reset_changes(fields=[attrname])
+
+    def delete_metadata_key(self, key):
+        db.volume_metadata_delete(self._context, self.id, key)
+        md_was_changed = 'metadata' in self.obj_what_changed()
+
+        del self.metadata[key]
+        self._orig_metadata.pop(key, None)
+
+        if not md_was_changed:
+            self.obj_reset_changes(['metadata'])
 
 
 @base.CinderObjectRegistry.register
 class VolumeList(base.ObjectListBase, base.CinderObject):
-    VERSION = '1.0'
+    VERSION = '1.1'
 
     fields = {
         'objects': fields.ListOfObjectsField('Volume'),
     }
+
     child_versions = {
-        '1.0': '1.0'
+        '1.0': '1.0',
+        '1.1': '1.1',
     }
 
     @base.remotable_classmethod
-    def get_all(cls, context, marker, limit, sort_key, sort_dir,
-                filters=None):
-        volumes = db.volume_get_all(context, marker, limit, sort_key,
-                                    sort_dir, filters=filters)
+    def get_all(cls, context, marker, limit, sort_keys=None, sort_dirs=None,
+                filters=None, offset=None):
+        volumes = db.volume_get_all(context, marker, limit,
+                                    sort_keys=sort_keys, sort_dirs=sort_dirs,
+                                    filters=filters, offset=offset)
+        expected_attrs = ['admin_metadata', 'metadata']
+        return base.obj_make_list(context, cls(context), objects.Volume,
+                                  volumes, expected_attrs=expected_attrs)
+
+    @base.remotable_classmethod
+    def get_all_by_host(cls, context, host, filters=None):
+        volumes = db.volume_get_all_by_host(context, host, filters)
+        expected_attrs = ['admin_metadata', 'metadata']
+        return base.obj_make_list(context, cls(context), objects.Volume,
+                                  volumes, expected_attrs=expected_attrs)
+
+    @base.remotable_classmethod
+    def get_all_by_group(cls, context, group_id, filters=None):
+        volumes = db.volume_get_all_by_group(context, group_id, filters)
+        expected_attrs = ['admin_metadata', 'metadata']
+        return base.obj_make_list(context, cls(context), objects.Volume,
+                                  volumes, expected_attrs=expected_attrs)
+
+    @base.remotable_classmethod
+    def get_all_by_project(cls, context, project_id, marker, limit,
+                           sort_keys=None, sort_dirs=None, filters=None,
+                           offset=None):
+        volumes = db.volume_get_all_by_project(context, project_id, marker,
+                                               limit, sort_keys=sort_keys,
+                                               sort_dirs=sort_dirs,
+                                               filters=filters, offset=offset)
+        expected_attrs = ['admin_metadata', 'metadata']
         return base.obj_make_list(context, cls(context), objects.Volume,
-                                  volumes)
+                                  volumes, expected_attrs=expected_attrs)
diff --git a/cinder/objects/volume_attachment.py b/cinder/objects/volume_attachment.py
new file mode 100644 (file)
index 0000000..213118b
--- /dev/null
@@ -0,0 +1,104 @@
+#    Copyright 2015 SimpliVity Corp.
+#
+#    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.
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_versionedobjects import fields
+
+from cinder import db
+from cinder import objects
+from cinder.objects import base
+
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+
+@base.CinderObjectRegistry.register
+class VolumeAttachment(base.CinderPersistentObject, base.CinderObject,
+                       base.CinderObjectDictCompat,
+                       base.CinderComparableObject):
+    # Version 1.0: Initial version
+    VERSION = '1.0'
+
+    fields = {
+        'id': fields.UUIDField(),
+        'volume_id': fields.UUIDField(),
+        'instance_uuid': fields.UUIDField(nullable=True),
+        'attached_host': fields.StringField(nullable=True),
+        'mountpoint': fields.StringField(nullable=True),
+
+        'attach_time': fields.DateTimeField(nullable=True),
+        'detach_time': fields.DateTimeField(nullable=True),
+
+        'attach_status': fields.StringField(nullable=True),
+        'attach_mode': fields.StringField(nullable=True),
+    }
+
+    @staticmethod
+    def _from_db_object(context, attachment, db_attachment):
+        for name, field in attachment.fields.items():
+            value = db_attachment.get(name)
+            if isinstance(field, fields.IntegerField):
+                value = value or 0
+            attachment[name] = value
+
+        attachment._context = context
+        attachment.obj_reset_changes()
+        return attachment
+
+    @base.remotable_classmethod
+    def get_by_id(cls, context, id):
+        db_attach = db.volume_attachment_get(context, id)
+        return cls._from_db_object(context, cls(context), db_attach)
+
+    @base.remotable
+    def save(self):
+        updates = self.cinder_obj_get_changes()
+        if updates:
+            db.volume_attachment_update(self._context, self.id, updates)
+            self.obj_reset_changes()
+
+
+@base.CinderObjectRegistry.register
+class VolumeAttachmentList(base.ObjectListBase, base.CinderObject):
+    VERSION = '1.0'
+
+    fields = {
+        'objects': fields.ListOfObjectsField('VolumeAttachment'),
+    }
+
+    child_versions = {
+        '1.0': '1.0',
+    }
+
+    @base.remotable_classmethod
+    def get_all_by_volume_id(cls, context, volume_id):
+        attachments = db.volume_attachment_get_used_by_volume_id(context,
+                                                                 volume_id)
+        return base.obj_make_list(context, cls(context),
+                                  objects.VolumeAttachment, attachments)
+
+    @base.remotable_classmethod
+    def get_all_by_host(cls, context, volume_id, host):
+        attachments = db.volume_attachment_get_by_host(context, volume_id,
+                                                       host)
+        return base.obj_make_list(context, cls(context),
+                                  objects.VolumeAttachment, attachments)
+
+    @base.remotable_classmethod
+    def get_all_by_instance_uuid(cls, context, volume_id, instance_uuid):
+        attachments = db.volume_attachment_get_by_instance_uuid(
+            context, volume_id, instance_uuid)
+        return base.obj_make_list(context, cls(context),
+                                  objects.VolumeAttachment, attachments)
diff --git a/cinder/objects/volume_type.py b/cinder/objects/volume_type.py
new file mode 100644 (file)
index 0000000..78196d2
--- /dev/null
@@ -0,0 +1,125 @@
+#    Copyright 2015 SimpliVity Corp.
+#
+#    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.
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_versionedobjects import fields
+
+from cinder import exception
+from cinder.i18n import _
+from cinder import objects
+from cinder.objects import base
+from cinder.volume import volume_types
+
+CONF = cfg.CONF
+OPTIONAL_FIELDS = ['extra_specs', 'projects']
+LOG = logging.getLogger(__name__)
+
+
+@base.CinderObjectRegistry.register
+class VolumeType(base.CinderPersistentObject, base.CinderObject,
+                 base.CinderObjectDictCompat, base.CinderComparableObject):
+    # Version 1.0: Initial version
+    VERSION = '1.0'
+
+    fields = {
+        'id': fields.UUIDField(),
+        'name': fields.StringField(nullable=True),
+        'description': fields.StringField(nullable=True),
+        'is_public': fields.BooleanField(default=True),
+        'projects': fields.ListOfStringsField(nullable=True),
+        'extra_specs': fields.DictOfStringsField(nullable=True),
+    }
+
+    @staticmethod
+    def _from_db_object(context, type, db_type, expected_attrs=None):
+        if expected_attrs is None:
+            expected_attrs = []
+        for name, field in type.fields.items():
+            if name in OPTIONAL_FIELDS:
+                continue
+            value = db_type[name]
+            if isinstance(field, fields.IntegerField):
+                value = value or 0
+            type[name] = value
+
+        # Get data from db_type object that was queried by joined query
+        # from DB
+        if 'extra_specs' in expected_attrs:
+            type.extra_specs = {}
+            specs = db_type.get('extra_specs')
+            if specs and isinstance(specs, list):
+                type.extra_specs = {item['key']: item['value']
+                                    for item in specs}
+            elif specs and isinstance(specs, dict):
+                type.extra_specs = specs
+        if 'projects' in expected_attrs:
+            type.projects = db_type.get('projects', [])
+
+        type._context = context
+        type.obj_reset_changes()
+        return type
+
+    @base.remotable_classmethod
+    def get_by_id(cls, context, id):
+        db_volume_type = volume_types.get_volume_type(
+            context, id, expected_fields=['extra_specs', 'projects'])
+        expected_attrs = ['extra_specs', 'projects']
+        return cls._from_db_object(context, cls(context), db_volume_type,
+                                   expected_attrs=expected_attrs)
+
+    @base.remotable
+    def create(self):
+        if self.obj_attr_is_set('id'):
+            raise exception.ObjectActionError(action='create',
+                                              reason=_('already created'))
+        db_volume_type = volume_types.create(self._context, self.name,
+                                             self.extra_specs,
+                                             self.is_public, self.projects,
+                                             self.description)
+        self._from_db_object(self._context, self, db_volume_type)
+
+    @base.remotable
+    def save(self):
+        updates = self.cinder_obj_get_changes()
+        if updates:
+            volume_types.update(self._context, self.id, self.name,
+                                self.description)
+            self.obj_reset_changes()
+
+    @base.remotable
+    def destroy(self):
+        with self.obj_as_admin():
+            volume_types.destroy(self._context, self.id)
+
+
+@base.CinderObjectRegistry.register
+class VolumeTypeList(base.ObjectListBase, base.CinderObject):
+    VERSION = '1.0'
+
+    fields = {
+        'objects': fields.ListOfObjectsField('VolumeType'),
+    }
+
+    child_versions = {
+        '1.0': '1.0',
+    }
+
+    @base.remotable_classmethod
+    def get_all(cls, context, inactive=0, search_opts=None):
+        types = volume_types.get_all_types(context, inactive, search_opts)
+        expected_attrs = ['extra_specs', 'projects']
+        return base.obj_make_list(context, cls(context),
+                                  objects.VolumeType, types,
+                                  expected_attrs=expected_attrs)
index 57b8b736fbfc44499489c6807ceb7b17f7190c58..3eda5a2a4124bdee3c1cfbd74ee0550347e1b563 100644 (file)
@@ -25,7 +25,12 @@ def fake_db_volume(**updates):
         'availability_zone': 'fake_availability_zone',
         'status': 'available',
         'attach_status': 'detached',
-        'previous_status': None
+        'previous_status': None,
+        'metadata': {},
+        'admin_metadata': {},
+        'volume_attachment': [],
+        'volume_metadata': [],
+        'volume_admin_metadata': [],
     }
 
     for name, field in objects.Volume.fields.items():
@@ -36,7 +41,7 @@ def fake_db_volume(**updates):
         elif field.default != fields.UnspecifiedDefault:
             db_volume[name] = field.default
         else:
-            raise Exception('fake_db_volume needs help with %s' % name)
+            raise Exception('fake_db_volume needs help with %s.' % name)
 
     if updates:
         db_volume.update(updates)
@@ -44,6 +49,70 @@ def fake_db_volume(**updates):
     return db_volume
 
 
+def fake_db_volume_type(**updates):
+    db_volume_type = {
+        'id': '1',
+        'name': 'type-1',
+        'description': 'A fake volume type',
+        'is_public': True,
+        'projects': [],
+        'extra_specs': {},
+    }
+
+    for name, field in objects.VolumeType.fields.items():
+        if name in db_volume_type:
+            continue
+        if field.nullable:
+            db_volume_type[name] = None
+        elif field.default != fields.UnspecifiedDefault:
+            db_volume_type[name] = field.default
+        else:
+            raise Exception('fake_db_volume_type needs help with %s.' % name)
+
+    if updates:
+        db_volume_type.update(updates)
+
+    return db_volume_type
+
+
+def fake_db_volume_attachment(**updates):
+    db_volume_attachment = {
+        'id': '1',
+        'volume_id': '1',
+    }
+
+    for name, field in objects.VolumeAttachment.fields.items():
+        if name in db_volume_attachment:
+            continue
+        if field.nullable:
+            db_volume_attachment[name] = None
+        elif field.default != fields.UnspecifiedDefault:
+            db_volume_attachment[name] = field.default
+        else:
+            raise Exception(
+                'fake_db_volume_attachment needs help with %s.' % name)
+
+    if updates:
+        db_volume_attachment.update(updates)
+
+    return db_volume_attachment
+
+
 def fake_volume_obj(context, **updates):
-    return objects.Volume._from_db_object(context, objects.Volume(),
-                                          fake_db_volume(**updates))
+    expected_attrs = updates.pop('expected_attrs',
+                                 ['metadata', 'admin_metadata'])
+    vol = objects.Volume._from_db_object(context, objects.Volume(),
+                                         fake_db_volume(**updates),
+                                         expected_attrs=expected_attrs)
+    return vol
+
+
+def fake_volume_type_obj(context, **updates):
+    return objects.VolumeType._from_db_object(
+        context, objects.VolumeType(), fake_db_volume_type(**updates))
+
+
+def fake_volume_attachment_obj(context, **updates):
+    return objects.VolumeAttachment._from_db_object(
+        context, objects.VolumeAttachment(),
+        fake_db_volume_attachment(**updates))
index aa0b83c6d9111a0e29942e123b132cbbfb13ae26..8f47a488f67b70475491f5c5712f17ff6c3223c8 100644 (file)
@@ -16,6 +16,7 @@ import datetime
 import uuid
 
 from iso8601 import iso8601
+from oslo_versionedobjects import fields
 
 from cinder.objects import base
 from cinder.tests.unit import objects as test_objects
@@ -75,3 +76,24 @@ class TestCinderObject(test_objects.BaseObjectsTestCase):
         self.obj.scheduled_at = now_tz
         self.assertDictEqual({'scheduled_at': now},
                              self.obj.cinder_obj_get_changes())
+
+
+class TestCinderComparableObject(test_objects.BaseObjectsTestCase):
+    def test_comparable_objects(self):
+        @base.CinderObjectRegistry.register
+        class MyComparableObj(base.CinderObject,
+                              base.CinderObjectDictCompat,
+                              base.CinderComparableObject):
+            fields = {'foo': fields.Field(fields.Integer())}
+
+        class NonVersionedObject(object):
+            pass
+
+        obj1 = MyComparableObj(foo=1)
+        obj2 = MyComparableObj(foo=1)
+        obj3 = MyComparableObj(foo=2)
+        obj4 = NonVersionedObject()
+        self.assertTrue(obj1 == obj2)
+        self.assertFalse(obj1 == obj3)
+        self.assertFalse(obj1 == obj4)
+        self.assertNotEqual(obj1, None)
index 8f79b3855492990888cedb216376bb862e6bda17..0e75c8d2cf598203586ea057215b5c98e05a54a3 100644 (file)
 
 import mock
 
+from cinder import context
 from cinder import objects
 from cinder.tests.unit import fake_volume
 from cinder.tests.unit import objects as test_objects
 
 
 class TestVolume(test_objects.BaseObjectsTestCase):
+    def setUp(self):
+        super(TestVolume, self).setUp()
+        # NOTE (e0ne): base tests contains original RequestContext from
+        # oslo_context. We change it to our RequestContext implementation
+        # to have 'elevated' method
+        self.context = context.RequestContext(self.user_id, self.project_id,
+                                              is_admin=False)
+
     @staticmethod
     def _compare(test, db, obj):
         for field, value in db.items():
+            if not hasattr(obj, field):
+                continue
+
             test.assertEqual(db[field], obj[field])
 
+    @mock.patch('cinder.db.volume_glance_metadata_get', return_value={})
     @mock.patch('cinder.db.volume_get')
-    def test_get_by_id(self, volume_get):
+    def test_get_by_id(self, volume_get, volume_glance_metadata_get):
         db_volume = fake_volume.fake_db_volume()
         volume_get.return_value = db_volume
         volume = objects.Volume.get_by_id(self.context, 1)
@@ -50,13 +63,56 @@ class TestVolume(test_objects.BaseObjectsTestCase):
         volume_update.assert_called_once_with(self.context, volume.id,
                                               {'display_name': 'foobar'})
 
+    @mock.patch('cinder.db.volume_metadata_update',
+                return_value={'key1': 'value1'})
+    @mock.patch('cinder.db.volume_update')
+    def test_save_with_metadata(self, volume_update, metadata_update):
+        db_volume = fake_volume.fake_db_volume()
+        volume = objects.Volume._from_db_object(self.context,
+                                                objects.Volume(), db_volume)
+        volume.display_name = 'foobar'
+        volume.metadata = {'key1': 'value1'}
+        self.assertEqual({'display_name': 'foobar',
+                          'metadata': {'key1': 'value1'}},
+                         volume.obj_get_changes())
+        volume.save()
+        volume_update.assert_called_once_with(self.context, volume.id,
+                                              {'display_name': 'foobar'})
+        metadata_update.assert_called_once_with(self.context, volume.id,
+                                                {'key1': 'value1'}, True)
+
+    @mock.patch('cinder.db.volume_admin_metadata_update',
+                return_value={'key1': 'value1'})
+    @mock.patch('cinder.db.volume_update')
+    def test_save_with_admin_metadata(self, volume_update,
+                                      admin_metadata_update):
+        # Test with no admin context
+        db_volume = fake_volume.fake_db_volume()
+        volume = objects.Volume._from_db_object(self.context,
+                                                objects.Volume(), db_volume)
+        volume.admin_metadata = {'key1': 'value1'}
+        volume.save()
+        self.assertFalse(admin_metadata_update.called)
+
+        # Test with admin context
+        admin_context = context.RequestContext(self.user_id, self.project_id,
+                                               is_admin=True)
+        volume = objects.Volume._from_db_object(admin_context,
+                                                objects.Volume(), db_volume)
+        volume.admin_metadata = {'key1': 'value1'}
+        volume.save()
+        admin_metadata_update.assert_called_once_with(
+            admin_context, volume.id, {'key1': 'value1'}, True)
+
     @mock.patch('cinder.db.volume_destroy')
     def test_destroy(self, volume_destroy):
         db_volume = fake_volume.fake_db_volume()
         volume = objects.Volume._from_db_object(self.context,
                                                 objects.Volume(), db_volume)
         volume.destroy()
-        volume_destroy.assert_called_once_with(self.context, '1')
+        self.assertTrue(volume_destroy.called)
+        admin_context = volume_destroy.call_args[0][0]
+        self.assertTrue(admin_context.is_admin)
 
     def test_obj_fields(self):
         volume = objects.Volume(context=self.context, id=2, _name_id=2)
@@ -69,10 +125,20 @@ class TestVolume(test_objects.BaseObjectsTestCase):
                                 previous_status='backing-up')
         self.assertEqual('backing-up', volume.previous_status)
 
+    @mock.patch('cinder.db.volume_metadata_delete')
+    def test_delete_metadata_key(self, metadata_delete):
+        volume = objects.Volume(self.context, id=1)
+        volume.metadata = {'key1': 'value1', 'key2': 'value2'}
+        self.assertEqual({}, volume._orig_metadata)
+        volume.delete_metadata_key('key2')
+        self.assertEqual({'key1': 'value1'}, volume.metadata)
+        metadata_delete.assert_called_once_with(self.context, '1', 'key2')
+
 
 class TestVolumeList(test_objects.BaseObjectsTestCase):
+    @mock.patch('cinder.db.volume_glance_metadata_get', return_value={})
     @mock.patch('cinder.db.volume_get_all')
-    def test_get_all(self, volume_get_all):
+    def test_get_all(self, volume_get_all, volume_glance_metadata_get):
         db_volume = fake_volume.fake_db_volume()
         volume_get_all.return_value = [db_volume]
 
@@ -83,3 +149,35 @@ class TestVolumeList(test_objects.BaseObjectsTestCase):
                                              mock.sentinel.sort_dir)
         self.assertEqual(1, len(volumes))
         TestVolume._compare(self, db_volume, volumes[0])
+
+    @mock.patch('cinder.db.volume_get_all_by_host')
+    def test_get_by_host(self, get_all_by_host):
+        db_volume = fake_volume.fake_db_volume()
+        get_all_by_host.return_value = [db_volume]
+
+        volumes = objects.VolumeList.get_all_by_host(
+            self.context, 'fake-host')
+        self.assertEqual(1, len(volumes))
+        TestVolume._compare(self, db_volume, volumes[0])
+
+    @mock.patch('cinder.db.volume_get_all_by_group')
+    def test_get_by_group(self, get_all_by_group):
+        db_volume = fake_volume.fake_db_volume()
+        get_all_by_group.return_value = [db_volume]
+
+        volumes = objects.VolumeList.get_all_by_group(
+            self.context, 'fake-host')
+        self.assertEqual(1, len(volumes))
+        TestVolume._compare(self, db_volume, volumes[0])
+
+    @mock.patch('cinder.db.volume_get_all_by_project')
+    def test_get_by_project(self, get_all_by_project):
+        db_volume = fake_volume.fake_db_volume()
+        get_all_by_project.return_value = [db_volume]
+
+        volumes = objects.VolumeList.get_all_by_project(
+            self.context, mock.sentinel.project_id, mock.sentinel.marker,
+            mock.sentinel.limit, mock.sentinel.sorted_keys,
+            mock.sentinel.sorted_dirs, mock.sentinel.filters)
+        self.assertEqual(1, len(volumes))
+        TestVolume._compare(self, db_volume, volumes[0])
diff --git a/cinder/tests/unit/objects/test_volume_attachment.py b/cinder/tests/unit/objects/test_volume_attachment.py
new file mode 100644 (file)
index 0000000..bce6606
--- /dev/null
@@ -0,0 +1,73 @@
+#    Copyright 2015 SimpliVity Corp.
+#
+#    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 mock
+
+from cinder import objects
+from cinder.tests.unit import fake_volume
+from cinder.tests.unit import objects as test_objects
+
+
+class TestVolumeAttachment(test_objects.BaseObjectsTestCase):
+    @staticmethod
+    def _compare(test, db, obj):
+        for field, value in db.items():
+            test.assertEqual(db[field], obj[field])
+
+    @mock.patch('cinder.db.volume_attachment_get')
+    def test_get_by_id(self, volume_attachment_get):
+        db_attachment = fake_volume.fake_db_volume_attachment()
+        volume_attachment_get.return_value = db_attachment
+        attachment = objects.VolumeAttachment.get_by_id(self.context, '1')
+        self._compare(self, db_attachment, attachment)
+
+    @mock.patch('cinder.db.volume_attachment_update')
+    def test_save(self, volume_attachment_update):
+        attachment = fake_volume.fake_volume_attachment_obj(self.context)
+        attachment.attach_status = 'attaching'
+        attachment.save()
+        volume_attachment_update.assert_called_once_with(
+            self.context, attachment.id, {'attach_status': 'attaching'})
+
+
+class TestVolumeAttachmentList(test_objects.BaseObjectsTestCase):
+    @mock.patch('cinder.db.volume_attachment_get_used_by_volume_id')
+    def test_get_all_by_volume_id(self, get_used_by_volume_id):
+        db_attachment = fake_volume.fake_db_volume_attachment()
+        get_used_by_volume_id.return_value = [db_attachment]
+
+        attachments = objects.VolumeAttachmentList.get_all_by_volume_id(
+            self.context, mock.sentinel.volume_id)
+        self.assertEqual(1, len(attachments))
+        TestVolumeAttachment._compare(self, db_attachment, attachments[0])
+
+    @mock.patch('cinder.db.volume_attachment_get_by_host')
+    def test_get_all_by_host(self, get_by_host):
+        db_attachment = fake_volume.fake_db_volume_attachment()
+        get_by_host.return_value = [db_attachment]
+
+        attachments = objects.VolumeAttachmentList.get_all_by_host(
+            self.context, mock.sentinel.volume_id, mock.sentinel.host)
+        self.assertEqual(1, len(attachments))
+        TestVolumeAttachment._compare(self, db_attachment, attachments[0])
+
+    @mock.patch('cinder.db.volume_attachment_get_by_instance_uuid')
+    def test_get_all_by_instance_uuid(self, get_by_instance_uuid):
+        db_attachment = fake_volume.fake_db_volume_attachment()
+        get_by_instance_uuid.return_value = [db_attachment]
+
+        attachments = objects.VolumeAttachmentList.get_all_by_instance_uuid(
+            self.context, mock.sentinel.volume_id, mock.sentinel.uuid)
+        self.assertEqual(1, len(attachments))
+        TestVolumeAttachment._compare(self, db_attachment, attachments[0])
diff --git a/cinder/tests/unit/objects/test_volume_type.py b/cinder/tests/unit/objects/test_volume_type.py
new file mode 100644 (file)
index 0000000..356a523
--- /dev/null
@@ -0,0 +1,95 @@
+#    Copyright 2015 SimpliVity Corp.
+#
+#    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 mock
+
+from cinder import context
+from cinder import objects
+from cinder.tests.unit import fake_volume
+from cinder.tests.unit import objects as test_objects
+
+
+class TestVolumeType(test_objects.BaseObjectsTestCase):
+    def setUp(self):
+        super(TestVolumeType, self).setUp()
+        # NOTE (e0ne): base tests contains original RequestContext from
+        # oslo_context. We change it to our RequestContext implementation
+        # to have 'elevated' method
+        self.context = context.RequestContext(self.user_id, self.project_id,
+                                              is_admin=False)
+
+    @staticmethod
+    def _compare(test, db, obj):
+        for field, value in db.items():
+            test.assertEqual(db[field], obj[field])
+
+    @mock.patch('cinder.db.volume_type_get')
+    def test_get_by_id(self, volume_type_get):
+        db_volume_type = fake_volume.fake_db_volume_type()
+        volume_type_get.return_value = db_volume_type
+        volume_type = objects.VolumeType.get_by_id(self.context, '1')
+        self._compare(self, db_volume_type, volume_type)
+
+    @mock.patch('cinder.volume.volume_types.create')
+    def test_create(self, volume_type_create):
+        db_volume_type = fake_volume.fake_db_volume_type()
+        volume_type_create.return_value = db_volume_type
+
+        volume_type = objects.VolumeType(context=self.context)
+        volume_type.name = db_volume_type['name']
+        volume_type.extra_specs = db_volume_type['extra_specs']
+        volume_type.is_public = db_volume_type['is_public']
+        volume_type.projects = db_volume_type['projects']
+        volume_type.description = db_volume_type['description']
+        volume_type.create()
+
+        volume_type_create.assert_called_once_with(
+            self.context, db_volume_type['name'],
+            db_volume_type['extra_specs'], db_volume_type['is_public'],
+            db_volume_type['projects'], db_volume_type['description'])
+
+    @mock.patch('cinder.volume.volume_types.update')
+    def test_save(self, volume_type_update):
+        db_volume_type = fake_volume.fake_db_volume_type()
+        volume_type = objects.VolumeType._from_db_object(self.context,
+                                                         objects.VolumeType(),
+                                                         db_volume_type)
+        volume_type.description = 'foobar'
+        volume_type.save()
+        volume_type_update.assert_called_once_with(self.context,
+                                                   volume_type.id,
+                                                   volume_type.name,
+                                                   volume_type.description)
+
+    @mock.patch('cinder.volume.volume_types.destroy')
+    def test_destroy(self, volume_type_destroy):
+        db_volume_type = fake_volume.fake_db_volume_type()
+        volume_type = objects.VolumeType._from_db_object(self.context,
+                                                         objects.VolumeType(),
+                                                         db_volume_type)
+        volume_type.destroy()
+        self.assertTrue(volume_type_destroy.called)
+        admin_context = volume_type_destroy.call_args[0][0]
+        self.assertTrue(admin_context.is_admin)
+
+
+class TestVolumeTypeList(test_objects.BaseObjectsTestCase):
+    @mock.patch('cinder.volume.volume_types.get_all_types')
+    def test_get_all(self, get_all_types):
+        db_volume_type = fake_volume.fake_db_volume_type()
+        get_all_types.return_value = [db_volume_type]
+
+        volume_types = objects.VolumeTypeList.get_all(self.context)
+        self.assertEqual(1, len(volume_types))
+        TestVolumeType._compare(self, db_volume_type, volume_types[0])