From fea5fecb3209fed9b8e4df9d6c8bed7416901608 Mon Sep 17 00:00:00 2001 From: Kien Nguyen Date: Wed, 6 Dec 2017 17:23:40 +0700 Subject: [PATCH] Add etcd db for VolumeMapping Change-Id: Ifb2f401f7ec3b96f1de7b14f0b56d51b060c3c4f --- zun/db/etcd/api.py | 86 +++++++++++ zun/db/etcd/models.py | 22 +++ zun/db/sqlalchemy/api.py | 23 +-- zun/tests/unit/db/test_volume_mapping.py | 181 +++++++++++++++++++++++ 4 files changed, 301 insertions(+), 11 deletions(-) diff --git a/zun/db/etcd/api.py b/zun/db/etcd/api.py index 91f1baa8f..eb800746d 100644 --- a/zun/db/etcd/api.py +++ b/zun/db/etcd/api.py @@ -82,6 +82,8 @@ def translate_etcd_result(etcd_result, model_type): ret = models.Capsule(data) elif model_type == 'pcidevice': ret = models.PciDevice(data) + elif model_type == 'volume_mapping': + ret = models.VolumeMapping(data) else: raise exception.InvalidParameterValue( _('The model_type value: %s is invalid.'), model_type) @@ -861,3 +863,87 @@ class EtcdAPI(object): raise return translate_etcd_result(target, 'pcidevice') + + def list_volume_mappings(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + try: + res = getattr(self.client.read( + '/volume_mappings'), 'children', None) + except etcd.EtcdKeyNotFound: + return [] + except Exception as e: + LOG.error( + "Error occurred while reading from etcd server: %s", + six.text_type(e)) + raise + + volume_mappings = [] + for vm in res: + if vm.value is not None: + volume_mappings.append( + translate_etcd_result(vm, 'volume_mapping')) + filters = self._add_tenant_filters(context, filters) + filtered_vms = self._filter_resources(volume_mappings, filters) + return self._process_list_result(filtered_vms, limit=limit, + sort_key=sort_key) + + def create_volume_mapping(self, context, volume_mapping_data): + if not volume_mapping_data.get('uuid'): + volume_mapping_data['uuid'] = uuidutils.generate_uuid() + + volume_mapping = models.VolumeMapping(volume_mapping_data) + try: + volume_mapping.save() + except Exception as e: + LOG.error('Error occurred while creating volume mapping: %s', + six.text_type(e)) + raise + + return volume_mapping + + def get_volume_mapping_by_uuid(self, context, volume_mapping_uuid): + try: + res = self.client.read('/volume_mappings/' + volume_mapping_uuid) + volume_mapping = translate_etcd_result(res, 'volume_mapping') + filtered_vms = self._filter_resources( + [volume_mapping], self._add_tenant_filters(context, {})) + if filtered_vms: + return filtered_vms[0] + else: + raise exception.VolumeMappingNotFound( + volume_mapping=volume_mapping_uuid) + except etcd.EtcdKeyNotFound: + raise exception.VolumeMappingNotFound( + volume_mapping=volume_mapping_uuid) + except Exception as e: + LOG.error('Error occurred while retrieving volume mapping: %s', + six.text_type(e)) + raise + + def destroy_volume_mapping(self, context, volume_mapping_uuid): + volume_mapping = self.get_volume_mapping_by_uuid( + context, volume_mapping_uuid) + self.client.delete('/volume_mappings/' + volume_mapping.uuid) + + def update_volume_mapping(self, context, volume_mapping_uuid, values): + if 'uuid' in values: + msg = _('Cannot overwrite UUID for an existing VolumeMapping.') + raise exception.InvalidParameterValue(err=msg) + + try: + target_uuid = self.get_volume_mapping_by_uuid( + context, volume_mapping_uuid).uuid + target = self.client.read('/volume_mapping/' + target_uuid) + target_value = json.loads(target.value) + target_value.update(values) + target.value = json.dump_as_bytes(target_value) + self.client.update(target) + except etcd.EtcdKeyNotFound: + raise exception.VolumeMappingNotFound( + volume_mapping=volume_mapping_uuid) + except Exception as e: + LOG.error('Error occurred while updating volume mappping: %s', + six.text_type(e)) + raise + + return translate_etcd_result(target, 'volume_mapping') diff --git a/zun/db/etcd/models.py b/zun/db/etcd/models.py index 17d11b555..31c5af732 100644 --- a/zun/db/etcd/models.py +++ b/zun/db/etcd/models.py @@ -278,3 +278,25 @@ class PciDevice(Base): @classmethod def fields(cls): return cls._fields + + +class VolumeMapping(Base): + """Represents a VolumeMapping.""" + _path = '/volume_mapping' + + _fields = objects.VolumeMapping.fields.keys() + + def __init__(self, volume_mapping_data): + self.path = VolumeMapping.path() + for f in VolumeMapping.fields(): + setattr(self, f, None) + self.id = 1 + self.update(volume_mapping_data) + + @classmethod + def path(cls): + return cls._path + + @classmethod + def fields(cls): + return cls._fields diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index 10a72b528..366842aee 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -273,41 +273,42 @@ class Connection(object): value=values['uuid']) return volume_mapping - def get_volume_mapping_by_uuid(self, context, vm_uuid): + def get_volume_mapping_by_uuid(self, context, volume_mapping_uuid): query = model_query(models.VolumeMapping) query = self._add_tenant_filters(context, query) - query = query.filter_by(uuid=vm_uuid) + query = query.filter_by(uuid=volume_mapping_uuid) try: return query.one() except NoResultFound: - raise exception.VolumeMappingNotFound(vm_uuid) + raise exception.VolumeMappingNotFound(volume_mapping_uuid) - def destroy_volume_mapping(self, context, vm_id): + def destroy_volume_mapping(self, context, volume_mapping_uuid): session = get_session() with session.begin(): query = model_query(models.VolumeMapping, session=session) - query = add_identity_filter(query, vm_id) + query = add_identity_filter(query, volume_mapping_uuid) count = query.delete() if count != 1: - raise exception.VolumeMappingNotFound(vm_id) + raise exception.VolumeMappingNotFound( + volume_mapping_uuid) - def update_volume_mapping(self, context, vm_id, values): + def update_volume_mapping(self, context, volume_mapping_uuid, values): # NOTE(dtantsur): this can lead to very strange errors if 'uuid' in values: msg = _("Cannot overwrite UUID for an existing VolumeMapping.") raise exception.InvalidParameterValue(err=msg) - return self._do_update_volume_mapping(vm_id, values) + return self._do_update_volume_mapping(volume_mapping_uuid, values) - def _do_update_volume_mapping(self, vm_id, values): + def _do_update_volume_mapping(self, volume_mapping_uuid, values): session = get_session() with session.begin(): query = model_query(models.VolumeMapping, session=session) - query = add_identity_filter(query, vm_id) + query = add_identity_filter(query, volume_mapping_uuid) try: ref = query.with_lockmode('update').one() except NoResultFound: - raise exception.VolumeMappingNotFound(vm_id) + raise exception.VolumeMappingNotFound(volume_mapping_uuid) ref.update(values) return ref diff --git a/zun/tests/unit/db/test_volume_mapping.py b/zun/tests/unit/db/test_volume_mapping.py index 90783b479..602afa54c 100644 --- a/zun/tests/unit/db/test_volume_mapping.py +++ b/zun/tests/unit/db/test_volume_mapping.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import json + +import etcd +from etcd import Client as etcd_client +import mock from oslo_config import cfg from oslo_utils import uuidutils import six @@ -19,6 +24,8 @@ import zun.conf from zun.db import api as dbapi from zun.tests.unit.db import base from zun.tests.unit.db import utils +from zun.tests.unit.db.utils import FakeEtcdMultipleResult +from zun.tests.unit.db.utils import FakeEtcdResult CONF = zun.conf.CONF @@ -149,3 +156,177 @@ class DbVolumeMappingTestCase(base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, dbapi.update_volume_mapping, self.context, volume_mapping.id, {'uuid': ''}) + + +class EtcdDbVolumeMappingTestCase(base.DbTestCase): + + def setUp(self): + cfg.CONF.set_override('db_type', 'etcd') + super(EtcdDbVolumeMappingTestCase, self).setUp() + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_create_volume_mapping(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + utils.create_test_volume_mapping(context=self.context) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_create_volume_mapping_already_exists(self, mock_write, + mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + utils.create_test_volume_mapping(context=self.context) + mock_read.side_effect = lambda *args: None + self.assertRaises(exception.ResourceExists, + utils.create_test_volume_mapping, + context=self.context) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_get_volume_mapping_by_uuid(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + volume_mapping = utils.create_test_volume_mapping( + context=self.context) + mock_read.side_effect = lambda *args: FakeEtcdResult( + volume_mapping.as_dict()) + res = dbapi.get_volume_mapping_by_uuid(self.context, + volume_mapping.uuid) + self.assertEqual(volume_mapping.id, res.id) + self.assertEqual(volume_mapping.uuid, res.uuid) + + @mock.patch.object(etcd_client, 'read') + def test_get_volume_mapping_that_does_not_exist(self, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.VolumeMappingNotFound, + dbapi.get_volume_mapping_by_uuid, + self.context, + uuidutils.generate_uuid()) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_list_volume_mappings(self, mock_write, mock_read): + uuids = [] + volume_mappings = [] + mock_read.side_effect = etcd.EtcdKeyNotFound + for i in range(0, 6): + volume_mapping = utils.create_test_volume_mapping( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='volume_mapping' + str(i)) + volume_mappings.append(volume_mapping.as_dict()) + uuids.append(six.text_type(volume_mapping['uuid'])) + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + volume_mappings) + res = dbapi.list_volume_mappings(self.context) + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), sorted(res_uuids)) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_list_volume_mappings_sorted(self, mock_write, mock_read): + uuids = [] + volume_mappings = [] + mock_read.side_effect = etcd.EtcdKeyNotFound + for i in range(0, 6): + volume_mapping = utils.create_test_volume_mapping( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='volume_mapping' + str(i)) + volume_mappings.append(volume_mapping.as_dict()) + uuids.append(six.text_type(volume_mapping['uuid'])) + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + volume_mappings) + res = dbapi.list_volume_mappings(self.context, sort_key='uuid') + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), res_uuids) + self.assertRaises(exception.InvalidParameterValue, + dbapi.list_volume_mappings, + self.context, + sort_key='wrong_key') + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_list_volume_mappings_with_filters(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + + volume_mapping1 = utils.create_test_volume_mapping( + name='volume_mapping1', + uuid=uuidutils.generate_uuid(), + context=self.context) + volume_mapping2 = utils.create_test_volume_mapping( + name='volume_mapping2', + uuid=uuidutils.generate_uuid(), + context=self.context,) + + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + [volume_mapping1.as_dict(), volume_mapping2.as_dict()]) + + res = dbapi.list_volume_mappings( + self.context, filters={'uuid': volume_mapping1.uuid}) + self.assertEqual([volume_mapping1.id], [r.id for r in res]) + + res = dbapi.list_volume_mappings( + self.context, filters={'uuid': volume_mapping2.uuid}) + self.assertEqual([volume_mapping2.id], [r.id for r in res]) + + res = dbapi.list_volume_mappings( + self.context, filters={'uuid': 'unknow-uuid'}) + self.assertEqual([], [r.id for r in res]) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + @mock.patch.object(etcd_client, 'delete') + def test_destroy_volume_mapping_by_uuid(self, mock_delete, + mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + volume_mapping = utils.create_test_volume_mapping( + context=self.context) + mock_read.side_effect = lambda *args: FakeEtcdResult( + volume_mapping.as_dict()) + dbapi.destroy_volume_mapping(self.context, volume_mapping.uuid) + mock_delete.assert_called_once_with( + '/volume_mappings/%s' % volume_mapping.uuid) + + @mock.patch.object(etcd_client, 'read') + def test_destroy_volume_mapping_that_does_not_exist(self, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.VolumeMappingNotFound, + dbapi.destroy_volume_mapping, self.context, + uuidutils.generate_uuid()) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + @mock.patch.object(etcd_client, 'update') + def test_update_volume_mapping(self, mock_update, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + volume_mapping = utils.create_test_volume_mapping( + context=self.context) + new_conn_info = 'new-conn-info' + + mock_read.side_effect = lambda *args: FakeEtcdResult( + volume_mapping.as_dict()) + dbapi.update_volume_mapping(self.context, volume_mapping.uuid, + {'container_info': new_conn_info}) + self.assertEqual(new_conn_info, json.loads( + mock_update.call_args_list[0][0][0].value.decode('utf-8')) + ['container_info']) + + @mock.patch.object(etcd_client, 'read') + def test_update_volume_mapping_not_found(self, mock_read): + volume_mapping_uuid = uuidutils.generate_uuid() + new_conn_info = 'new-conn-info' + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.VolumeMappingNotFound, + dbapi.update_volume_mapping, self.context, + volume_mapping_uuid, + {'container_info': new_conn_info}) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_update_volume_mapping_uuid(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + volume_mapping = utils.create_test_volume_mapping( + context=self.context) + self.assertRaises(exception.InvalidParameterValue, + dbapi.update_volume_mapping, self.context, + volume_mapping.uuid, {'uuid': ''})