diff --git a/mogan/common/exception.py b/mogan/common/exception.py index 474abeff..704d8077 100644 --- a/mogan/common/exception.py +++ b/mogan/common/exception.py @@ -180,6 +180,14 @@ class ComputePortNotFound(NotFound): msg_fmt = _("ComputePort %(port)s could not be found.") +class ComputeDiskAlreadyExists(MoganException): + _msg_fmt = _("ComputeDisk with disk_uuid %(disk)s already exists.") + + +class ComputeDiskNotFound(NotFound): + msg_fmt = _("ComputeDisk %(disk)s could not be found.") + + class NodeNotFound(NotFound): msg_fmt = _("Node associated with instance %(instance)s " "could not be found.") diff --git a/mogan/db/api.py b/mogan/db/api.py index 777a4bc9..b77a2d38 100644 --- a/mogan/db/api.py +++ b/mogan/db/api.py @@ -132,6 +132,31 @@ class Connection(object): def compute_port_update(self, context, port_uuid, values): """Update a compute port.""" + # Compute disks + @abc.abstractmethod + def compute_disk_create(self, context, values): + """Create a new compute disk.""" + + @abc.abstractmethod + def compute_disk_get(self, context, disk_uuid): + """Get compute disk by disk uuid.""" + + @abc.abstractmethod + def compute_disk_get_all(self, context): + """Get all compute disks.""" + + @abc.abstractmethod + def compute_disk_get_by_node_uuid(self, context, node_uuid): + """Get compute disks by node_uuid.""" + + @abc.abstractmethod + def compute_disk_destroy(self, context, disk_uuid): + """Delete a compute disk.""" + + @abc.abstractmethod + def compute_disk_update(self, context, disk_uuid, values): + """Update a compute disk.""" + # Instance Type extra specs @abc.abstractmethod def extra_specs_update_or_create(self, context, diff --git a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py index 84aed49a..45485005 100644 --- a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py +++ b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py @@ -126,6 +126,22 @@ def upgrade(): mysql_ENGINE='InnoDB', mysql_DEFAULT_CHARSET='UTF8' ) + op.create_table( + 'compute_disks', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('disk_type', sa.String(length=255), nullable=False), + sa.Column('size_gb', sa.Integer(), nullable=False), + sa.Column('disk_uuid', sa.String(length=36), nullable=False), + sa.Column('node_uuid', sa.String(length=36), nullable=False), + sa.Column('extra_specs', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('disk_uuid', name='uniq_compute_disks0disk_uuid'), + sa.ForeignKeyConstraint(['node_uuid'], ['compute_nodes.node_uuid'], ), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8' + ) op.create_table( 'instance_nics', sa.Column('created_at', sa.DateTime(), nullable=True), diff --git a/mogan/db/sqlalchemy/api.py b/mogan/db/sqlalchemy/api.py index f858fb6b..8443621d 100644 --- a/mogan/db/sqlalchemy/api.py +++ b/mogan/db/sqlalchemy/api.py @@ -287,6 +287,10 @@ class Connection(api.Connection): node_uuid=node_uuid) port_query.delete() + disk_query = model_query(context, models.ComputeDisk).filter_by( + node_uuid=node_uuid) + disk_query.delete() + count = query.delete() if count != 1: raise exception.ComputeNodeNotFound(node=node_uuid) @@ -355,6 +359,57 @@ class Connection(api.Connection): ref.update(values) return ref + def compute_disk_create(self, context, values): + compute_disk = models.ComputeDisk() + compute_disk.update(values) + with _session_for_write() as session: + try: + session.add(compute_disk) + session.flush() + except db_exc.DBDuplicateEntry: + raise exception.ComputeDiskAlreadyExists( + disk=values['disk_uuid']) + return compute_disk + + def compute_disk_get(self, context, disk_uuid): + query = model_query( + context, + models.ComputeDisk).filter_by(disk_uuid=disk_uuid) + try: + return query.one() + except NoResultFound: + raise exception.ComputeDiskNotFound(disk=disk_uuid) + + def compute_disk_get_all(self, context): + return model_query(context, models.ComputeDisk) + + def compute_disk_get_by_node_uuid(self, context, node_uuid): + return model_query(context, models.ComputeDisk).filter_by( + node_uuid=node_uuid).all() + + def compute_disk_destroy(self, context, disk_uuid): + with _session_for_write(): + query = model_query( + context, + models.ComputeDisk).filter_by(disk_uuid=disk_uuid) + + count = query.delete() + if count != 1: + raise exception.ComputeDiskNotFound(disk=disk_uuid) + + def compute_disk_update(self, context, disk_uuid, values): + with _session_for_write(): + query = model_query( + context, + models.ComputeDisk).filter_by(disk_uuid=disk_uuid) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.ComputeDiskNotFound(disk=disk_uuid) + + ref.update(values) + return ref + def extra_specs_update_or_create(self, context, instance_type_uuid, specs, max_retries=10): diff --git a/mogan/db/sqlalchemy/models.py b/mogan/db/sqlalchemy/models.py index 0b1aa8d5..4fab496a 100644 --- a/mogan/db/sqlalchemy/models.py +++ b/mogan/db/sqlalchemy/models.py @@ -131,6 +131,28 @@ class ComputePort(Base): primaryjoin='ComputeNode.node_uuid == ComputePort.node_uuid') +class ComputeDisk(Base): + """Represents the compute disks.""" + + __tablename__ = 'compute_disks' + __table_args__ = ( + schema.UniqueConstraint('disk_uuid', + name='uniq_compute_disks0disk_uuid'), + table_args() + ) + id = Column(Integer, primary_key=True) + disk_type = Column(String(255), nullable=False) + size_gb = Column(Integer, nullable=False) + disk_uuid = Column(String(36), nullable=False) + node_uuid = Column(String(36), nullable=False) + extra_specs = Column(db_types.JsonEncodedDict) + _node = orm.relationship( + "ComputeNode", + backref='disks', + foreign_keys=node_uuid, + primaryjoin='ComputeNode.node_uuid == ComputeDisk.node_uuid') + + class InstanceNic(Base): """Represents the NIC info for instances.""" diff --git a/mogan/objects/__init__.py b/mogan/objects/__init__.py index 37b8412e..184fc0dc 100644 --- a/mogan/objects/__init__.py +++ b/mogan/objects/__init__.py @@ -31,3 +31,4 @@ def register_all(): __import__('mogan.objects.instance_fault') __import__('mogan.objects.compute_node') __import__('mogan.objects.compute_port') + __import__('mogan.objects.compute_disk') diff --git a/mogan/objects/compute_disk.py b/mogan/objects/compute_disk.py new file mode 100644 index 00000000..668d7a7f --- /dev/null +++ b/mogan/objects/compute_disk.py @@ -0,0 +1,95 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_versionedobjects import base as object_base + +from mogan.db import api as dbapi +from mogan.objects import base +from mogan.objects import fields as object_fields + + +@base.MoganObjectRegistry.register +class ComputeDisk(base.MoganObject, object_base.VersionedObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'id': object_fields.IntegerField(read_only=True), + 'disk_type': object_fields.StringField(), + 'size_gb': object_fields.IntegerField(), + 'disk_uuid': object_fields.UUIDField(read_only=True), + 'node_uuid': object_fields.UUIDField(read_only=True), + 'extra_specs': object_fields.FlexibleDictField(nullable=True), + } + + @classmethod + def list(cls, context): + """Return a list of ComputeDisk objects.""" + db_compute_disks = cls.dbapi.compute_disk_get_all(context) + return cls._from_db_object_list(context, db_compute_disks) + + @classmethod + def get(cls, context, disk_uuid): + """Find a compute disk and return a ComputeDisk object.""" + db_compute_disk = cls.dbapi.compute_disk_get(context, disk_uuid) + compute_disk = cls._from_db_object(context, cls(context), + db_compute_disk) + return compute_disk + + def create(self, context=None): + """Create a ComputeDisk record in the DB.""" + values = self.obj_get_changes() + db_compute_disk = self.dbapi.compute_disk_create(context, values) + self._from_db_object(context, self, db_compute_disk) + + def destroy(self, context=None): + """Delete the ComputeDisk from the DB.""" + self.dbapi.compute_disk_destroy(context, self.disk_uuid) + self.obj_reset_changes() + + def save(self, context=None): + """Save updates to this ComputeDisk.""" + updates = self.obj_get_changes() + self.dbapi.compute_disk_update(context, self.disk_uuid, updates) + self.obj_reset_changes() + + def refresh(self, context=None): + """Refresh the object by re-fetching from the DB.""" + current = self.__class__.get(context, self.disk_uuid) + self.obj_refresh(current) + + +@base.MoganObjectRegistry.register +class ComputeDiskList(object_base.ObjectListBase, base.MoganObject, + object_base.VersionedObjectDictCompat): + # Version 1.0: Initial version + + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'objects': object_fields.ListOfObjectsField('ComputeDisk') + } + + @classmethod + def get_by_node_uuid(cls, context, node_uuid): + db_disks = cls.dbapi.compute_disk_get_by_node_uuid( + context, node_uuid) + return object_base.obj_make_list(context, cls(context), + ComputeDisk, db_disks) diff --git a/mogan/tests/unit/db/test_compute_disks.py b/mogan/tests/unit/db/test_compute_disks.py new file mode 100644 index 00000000..70196f7d --- /dev/null +++ b/mogan/tests/unit/db/test_compute_disks.py @@ -0,0 +1,78 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for manipulating ComputeDisks via the DB API""" + +from oslo_utils import uuidutils +import six + +from mogan.common import exception +from mogan.tests.unit.db import base +from mogan.tests.unit.db import utils + + +class DbComputeDiskTestCase(base.DbTestCase): + + def test_compute_disk_create(self): + utils.create_test_compute_disk() + + def test_compute_disk_create_with_same_uuid(self): + utils.create_test_compute_disk(disk_uuid='uuid') + self.assertRaises(exception.ComputeDiskAlreadyExists, + utils.create_test_compute_disk, + disk_uuid='uuid') + + def test_compute_disk_get_by_uuid(self): + disk = utils.create_test_compute_disk() + res = self.dbapi.compute_disk_get(self.context, disk.disk_uuid) + self.assertEqual(disk.disk_uuid, res.disk_uuid) + + def test_compute_disk_get_not_exist(self): + self.assertRaises(exception.ComputeDiskNotFound, + self.dbapi.compute_disk_get, + self.context, + '12345678-9999-0000-aaaa-123456789012') + + def test_compute_disk_get_all(self): + disk_uuids = [] + for i in range(0, 3): + disk = utils.create_test_compute_disk( + disk_uuid=uuidutils.generate_uuid()) + disk_uuids.append(six.text_type(disk['disk_uuid'])) + + res = self.dbapi.compute_disk_get_all(self.context) + res_uuids = [r.disk_uuid for r in res] + self.assertItemsEqual(disk_uuids, res_uuids) + + def test_compute_disk_destroy(self): + disk = utils.create_test_compute_disk() + self.dbapi.compute_disk_destroy(self.context, disk.disk_uuid) + self.assertRaises(exception.ComputeDiskNotFound, + self.dbapi.compute_disk_get, + self.context, + disk.disk_uuid) + + def test_compute_disk_destroy_not_exist(self): + self.assertRaises(exception.ComputeDiskNotFound, + self.dbapi.compute_disk_destroy, + self.context, + '12345678-9999-0000-aaaa-123456789012') + + def test_compute_disk_update(self): + disk = utils.create_test_compute_disk() + res = self.dbapi.compute_disk_update(self.context, + disk.disk_uuid, + {'disk_type': 'foo'}) + self.assertEqual('foo', res.disk_type) diff --git a/mogan/tests/unit/db/utils.py b/mogan/tests/unit/db/utils.py index 077e277d..4cd29aea 100644 --- a/mogan/tests/unit/db/utils.py +++ b/mogan/tests/unit/db/utils.py @@ -160,6 +160,40 @@ def create_test_compute_port(context={}, **kw): return dbapi.compute_port_create(context, port) +def get_test_compute_disk(**kw): + return { + 'id': kw.get('id', 123), + 'disk_type': kw.get('disk_type', 'SSD'), + 'size_gb': kw.get('size_gb', 100), + 'disk_uuid': kw.get('disk_uuid', + 'f978ef48-d4af-4dad-beec-e6174309bc73'), + 'node_uuid': kw.get('node_uuid', + 'f978ef48-d4af-4dad-beec-e6174309bc71'), + 'extra_specs': kw.get('extra_specs', {}), + 'updated_at': kw.get('updated_at'), + 'created_at': kw.get('created_at'), + } + + +def create_test_compute_disk(context={}, **kw): + """Create test compute disk entry in DB and return ComputeDisk DB object. + + Function to be used to create test ComputeDisk objects in the database. + + :param context: The request context, for access checks. + :param kw: kwargs with overriding values for port's attributes. + :returns: Test ComputeDisk DB object. + + """ + disk = get_test_compute_disk(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del disk['id'] + dbapi = db_api.get_instance() + + return dbapi.compute_disk_create(context, disk) + + def get_test_instance_type(**kw): return { 'uuid': kw.get('uuid', uuidutils.generate_uuid()), diff --git a/mogan/tests/unit/objects/test_compute_disk.py b/mogan/tests/unit/objects/test_compute_disk.py new file mode 100644 index 00000000..b7dcd01e --- /dev/null +++ b/mogan/tests/unit/objects/test_compute_disk.py @@ -0,0 +1,94 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import mock +from oslo_context import context + +from mogan import objects +from mogan.tests.unit.db import base +from mogan.tests.unit.db import utils +from mogan.tests.unit.objects import utils as obj_utils + + +class TestComputeDiskObject(base.DbTestCase): + + def setUp(self): + super(TestComputeDiskObject, self).setUp() + self.ctxt = context.get_admin_context() + self.fake_disk = utils.get_test_compute_disk(context=self.ctxt) + self.disk = obj_utils.get_test_compute_disk( + self.ctxt, **self.fake_disk) + + def test_get(self): + disk_uuid = self.fake_disk['disk_uuid'] + with mock.patch.object(self.dbapi, 'compute_disk_get', + autospec=True) as mock_disk_get: + mock_disk_get.return_value = self.fake_disk + + disk = objects.ComputeDisk.get(self.context, disk_uuid) + + mock_disk_get.assert_called_once_with(self.context, disk_uuid) + self.assertEqual(self.context, disk._context) + + def test_list(self): + with mock.patch.object(self.dbapi, 'compute_disk_get_all', + autospec=True) as mock_disk_get_all: + mock_disk_get_all.return_value = [self.fake_disk] + + disks = objects.ComputeDisk.list(self.context) + + mock_disk_get_all.assert_called_once_with(self.context) + self.assertIsInstance(disks[0], objects.ComputeDisk) + self.assertEqual(self.context, disks[0]._context) + + def test_create(self): + with mock.patch.object(self.dbapi, 'compute_disk_create', + autospec=True) as mock_disk_create: + mock_disk_create.return_value = self.fake_disk + disk = objects.ComputeDisk(self.context, **self.fake_disk) + disk.obj_get_changes() + disk.create(self.context) + expected_called = copy.deepcopy(self.fake_disk) + mock_disk_create.assert_called_once_with(self.context, + expected_called) + self.assertEqual(self.fake_disk['disk_uuid'], disk['disk_uuid']) + + def test_destroy(self): + uuid = self.fake_disk['disk_uuid'] + with mock.patch.object(self.dbapi, 'compute_disk_destroy', + autospec=True) as mock_disk_destroy: + disk = objects.ComputeDisk(self.context, **self.fake_disk) + disk.destroy(self.context) + mock_disk_destroy.assert_called_once_with(self.context, uuid) + + def test_save(self): + uuid = self.fake_disk['disk_uuid'] + with mock.patch.object(self.dbapi, 'compute_disk_update', + autospec=True) as mock_disk_update: + mock_disk_update.return_value = self.fake_disk + disk = objects.ComputeDisk(self.context, **self.fake_disk) + updates = disk.obj_get_changes() + disk.save(self.context) + mock_disk_update.assert_called_once_with( + self.context, uuid, updates) + + def test_save_after_refresh(self): + db_disk = utils.create_test_compute_disk(context=self.ctxt) + disk = objects.ComputeDisk.get(self.context, db_disk.disk_uuid) + disk.refresh(self.context) + disk.disk_type = 'refresh' + disk.save(self.context) diff --git a/mogan/tests/unit/objects/test_objects.py b/mogan/tests/unit/objects/test_objects.py index 854653e7..d1246049 100644 --- a/mogan/tests/unit/objects/test_objects.py +++ b/mogan/tests/unit/objects/test_objects.py @@ -387,6 +387,8 @@ expected_object_fingerprints = { 'ComputeNodeList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', 'ComputePort': '1.0-ca4c1817ad7324286813f2cfcdcf802e', 'ComputePortList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', + 'ComputeDisk': '1.0-4db463b2d9954dab8e4ac655f7be0f00', + 'ComputeDiskList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', 'InstanceFault': '1.0-6b5b01b2cc7b6b547837acb168ec6eb9', 'InstanceFaultList': '1.0-43e8aad0258652921f929934e9e048fd', 'InstanceType': '1.0-589b096651fcdb30898ff50f748dd948', diff --git a/mogan/tests/unit/objects/utils.py b/mogan/tests/unit/objects/utils.py index 7b852b38..0c460718 100644 --- a/mogan/tests/unit/objects/utils.py +++ b/mogan/tests/unit/objects/utils.py @@ -153,3 +153,32 @@ def create_test_compute_port(ctxt, **kw): port = get_test_compute_port(ctxt, **kw) port.create() return port + + +def get_test_compute_disk(ctxt, **kw): + """Return a ComputeDisk object with appropriate attributes. + + NOTE: The object leaves the attributes marked as changed, such + that a create() could be used to commit it to the DB. + """ + kw['object_type'] = 'compute_disk' + get_db_compute_disk_checked = check_keyword_arguments( + db_utils.get_test_compute_disk) + db_disk = get_db_compute_disk_checked(**kw) + + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del db_disk['id'] + disk = objects.ComputeDisk(ctxt, **db_disk) + return disk + + +def create_test_compute_disk(ctxt, **kw): + """Create and return a test compute disk object. + + Create a compute disk in the DB and return a ComputeDisk object with + appropriate attributes. + """ + disk = get_test_compute_disk(ctxt, **kw) + disk.create() + return disk