From 3af149cfef71b943674b23340206977dfdadf253 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 28 Aug 2015 07:02:50 -0700 Subject: [PATCH] Sync volume versionedobject to ORM 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 | 8 +- cinder/objects/base.py | 5 +- cinder/objects/volume.py | 184 ++++++++++++++++-- cinder/objects/volume_attachment.py | 104 ++++++++++ cinder/objects/volume_type.py | 125 ++++++++++++ cinder/tests/unit/fake_volume.py | 77 +++++++- cinder/tests/unit/objects/test_base.py | 22 +++ cinder/tests/unit/objects/test_volume.py | 104 +++++++++- .../unit/objects/test_volume_attachment.py | 73 +++++++ cinder/tests/unit/objects/test_volume_type.py | 95 +++++++++ 10 files changed, 770 insertions(+), 27 deletions(-) create mode 100644 cinder/objects/volume_attachment.py create mode 100644 cinder/objects/volume_type.py create mode 100644 cinder/tests/unit/objects/test_volume_attachment.py create mode 100644 cinder/tests/unit/objects/test_volume_type.py diff --git a/cinder/objects/__init__.py b/cinder/objects/__init__.py index 28f1e9c55..b0b82323b 100644 --- a/cinder/objects/__init__.py +++ b/cinder/objects/__init__.py @@ -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') diff --git a/cinder/objects/base.py b/cinder/objects/base.py index 578129516..8353cee45 100644 --- a/cinder/objects/base.py +++ b/cinder/objects/base.py @@ -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): diff --git a/cinder/objects/volume.py b/cinder/objects/volume.py index a4d999e82..c2ece4203 100644 --- a/cinder/objects/volume.py +++ b/cinder/objects/volume.py @@ -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 index 000000000..213118b34 --- /dev/null +++ b/cinder/objects/volume_attachment.py @@ -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 index 000000000..78196d2ee --- /dev/null +++ b/cinder/objects/volume_type.py @@ -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) diff --git a/cinder/tests/unit/fake_volume.py b/cinder/tests/unit/fake_volume.py index 57b8b736f..3eda5a2a4 100644 --- a/cinder/tests/unit/fake_volume.py +++ b/cinder/tests/unit/fake_volume.py @@ -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)) diff --git a/cinder/tests/unit/objects/test_base.py b/cinder/tests/unit/objects/test_base.py index aa0b83c6d..8f47a488f 100644 --- a/cinder/tests/unit/objects/test_base.py +++ b/cinder/tests/unit/objects/test_base.py @@ -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) diff --git a/cinder/tests/unit/objects/test_volume.py b/cinder/tests/unit/objects/test_volume.py index 8f79b3855..0e75c8d2c 100644 --- a/cinder/tests/unit/objects/test_volume.py +++ b/cinder/tests/unit/objects/test_volume.py @@ -14,19 +14,32 @@ 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 index 000000000..bce660696 --- /dev/null +++ b/cinder/tests/unit/objects/test_volume_attachment.py @@ -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 index 000000000..356a523d1 --- /dev/null +++ b/cinder/tests/unit/objects/test_volume_type.py @@ -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]) -- 2.45.2