From c8ce26d2f89fc286c9795385a4d246840c17d9e0 Mon Sep 17 00:00:00 2001 From: 00129207 Date: Fri, 25 Aug 2017 13:56:11 +0800 Subject: [PATCH] Imply the etcd api of capsule Change-Id: I75e918dac2c7d97b45920563cf7895e43a756213 Co-Authored-By: Kien Nguyen Closes-Bug: #1722698 --- zun/db/api.py | 6 +- zun/db/etcd/api.py | 106 +++++++++ zun/tests/unit/db/test_capsule.py | 342 ++++++++++++++++++++++++++++++ 3 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 zun/tests/unit/db/test_capsule.py diff --git a/zun/db/api.py b/zun/db/api.py index 31d8e08ee..e02bc162b 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -765,7 +765,7 @@ def create_capsule(context, values): @profiler.trace("db") def get_capsule_by_uuid(context, capsule_uuid): - """Return a container. + """Return a capsule. :param context: The security context :param capsule_uuid: The uuid of a capsule. @@ -789,7 +789,7 @@ def get_capsule_by_meta_name(context, capsule_name): @profiler.trace("db") def destroy_capsule(context, capsule_id): - """Destroy a container and all associated interfaces. + """Destroy a capsule and all associated interfaces. :param context: Request context :param capsule_id: The id or uuid of a capsule. @@ -799,7 +799,7 @@ def destroy_capsule(context, capsule_id): @profiler.trace("db") def update_capsule(context, capsule_id, values): - """Update properties of a container. + """Update properties of a capsule. :context: Request context :param container_id: The id or uuid of a capsule. diff --git a/zun/db/etcd/api.py b/zun/db/etcd/api.py index da5ae6172..c5f5250d7 100644 --- a/zun/db/etcd/api.py +++ b/zun/db/etcd/api.py @@ -79,6 +79,8 @@ def translate_etcd_result(etcd_result, model_type): ret = models.ResourceClass(data) elif model_type == 'compute_node': ret = models.ComputeNode(data) + elif model_type == 'capsule': + ret = models.Capsule(data) else: raise exception.InvalidParameterValue( _('The model_type value: %s is invalid.'), model_type) @@ -638,3 +640,107 @@ class EtcdAPI(object): compute_nodes = self._filter_resources(compute_nodes, filters) return self._process_list_result(compute_nodes, limit=limit, sort_key=sort_key) + + def list_capsules(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + try: + res = getattr(self.client.read('/capsules'), 'children', None) + except etcd.EtcdKeyNotFound: + # Before the first container been created, path '/capsules' + # does not exist. + return [] + except Exception as e: + LOG.error( + "Error occurred while reading from etcd server: %s", + six.text_type(e)) + raise + + capsules = [] + for c in res: + if c.value is not None: + capsules.append(translate_etcd_result(c, 'capsule')) + filters = self._add_tenant_filters(context, filters) + filtered_capsules = self._filter_resources( + capsules, filters) + return self._process_list_result(filtered_capsules, + limit=limit, sort_key=sort_key) + + @lockutils.synchronized('etcd_capsule') + def create_capsule(self, context, values): + # ensure defaults are present for new capsules + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + capsule = models.Capsule(values) + try: + capsule.save() + except Exception: + raise + + return capsule + + def get_capsule_by_uuid(self, context, capsule_uuid): + try: + res = self.client.read('/capsules/' + capsule_uuid) + capsule = translate_etcd_result(res, 'capsule') + filtered_capsules = self._filter_resources( + [capsule], self._add_tenant_filters(context, {})) + if len(filtered_capsules) > 0: + return filtered_capsules[0] + else: + raise exception.CapsuleNotFound(capsule=capsule_uuid) + except etcd.EtcdKeyNotFound: + raise exception.CapsuleNotFound(capsule=capsule_uuid) + except Exception as e: + LOG.error('Error occurred while retrieving capsule: %s', + six.text_type(e)) + raise + + def get_capsule_by_meta_name(self, context, capsule_meta_name): + try: + filters = self._add_tenant_filters( + context, {'meta_name': capsule_meta_name}) + capsules = self.list_capsules(context, filters=filters) + except etcd.EtcdKeyNotFound: + raise exception.CapsuleNotFound(capsule=capsule_meta_name) + except Exception as e: + LOG.error('Error occurred while retrieving capsule: %s', + six.text_type(e)) + raise + + if len(capsules) > 1: + raise exception.Conflict('Multiple capsules exist with same ' + 'meta name. Please use the capsule uuid ' + 'instead.') + elif len(capsules) == 0: + raise exception.CapsuleNotFound(capsule=capsule_meta_name) + + return capsules[0] + + @lockutils.synchronized('etcd_capsule') + def destroy_capsule(self, context, capsule_id): + capsule = self.get_capsule_by_uuid(context, capsule_id) + self.client.delete('/capsules/' + capsule.uuid) + + @lockutils.synchronized('etcd_capsule') + def update_capsule(self, context, capsule_id, values): + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing Capsule.") + raise exception.InvalidParameterValue(err=msg) + + try: + target_uuid = self.get_capsule_by_uuid( + context, capsule_id).uuid + target = self.client.read('/capsules/' + 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.CapsuleNotFound(capsule=capsule_id) + except Exception as e: + LOG.error('Error occurred while updating capsule: %s', + six.text_type(e)) + raise + + return translate_etcd_result(target, 'capsule') diff --git a/zun/tests/unit/db/test_capsule.py b/zun/tests/unit/db/test_capsule.py new file mode 100644 index 000000000..89a8315da --- /dev/null +++ b/zun/tests/unit/db/test_capsule.py @@ -0,0 +1,342 @@ +# 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 Capsule via the DB API""" +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 + +from zun.common import exception +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 + + +class SqlDbCapsuleTestCase(base.DbTestCase): + + def setUp(self): + cfg.CONF.set_override('db_type', 'sql') + super(SqlDbCapsuleTestCase, self).setUp() + + def test_create_capsule(self): + utils.create_test_capsule(context=self.context) + + def test_create_capsule_already_exists(self): + utils.create_test_capsule(context=self.context) + self.assertRaises(exception.CapsuleAlreadyExists, + utils.create_test_capsule, + context=self.context) + + def test_get_capsule_by_uuid(self): + capsule = utils.create_test_capsule(context=self.context) + res = dbapi.get_capsule_by_uuid(self.context, + capsule.uuid) + self.assertEqual(capsule.id, res.id) + self.assertEqual(capsule.uuid, res.uuid) + + def test_get_capsule_by_meta_name(self): + capsule = utils.create_test_capsule(context=self.context) + res = dbapi.get_capsule_by_meta_name(self.context, + capsule.meta_name) + self.assertEqual(capsule.id, res.id) + self.assertEqual(capsule.meta_name, res.meta_name) + + def test_get_non_exists_capsule(self): + self.assertRaises(exception.CapsuleNotFound, + dbapi.get_capsule_by_uuid, + self.context, + uuidutils.generate_uuid()) + + def test_list_capsules(self): + uuids = [] + for i in range(1, 6): + capsule = utils.create_test_capsule( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='capsule' + str(i) + ) + uuids.append(six.text_type(capsule['uuid'])) + res = dbapi.list_capsules(self.context) + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), sorted(res_uuids)) + + def test_list_capsules_sorted_with_valid_sort_key(self): + uuids = [] + for i in range(1, 6): + capsule = utils.create_test_capsule( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='capsule' + str(i) + ) + uuids.append(six.text_type(capsule['uuid'])) + res = dbapi.list_capsules(self.context, sort_key='uuid') + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), res_uuids) + + def test_list_capsules_sorted_with_invalid_sort_key(self): + self.assertRaises(exception.InvalidParameterValue, + dbapi.list_capsules, + self.context, + sort_key='foo') + + def test_list_capsules_with_filters(self): + capsule1 = utils.create_test_capsule( + name='capsule1', + uuid=uuidutils.generate_uuid(), + context=self.context) + capsule2 = utils.create_test_capsule( + name='capsule2', + uuid=uuidutils.generate_uuid(), + context=self.context) + + res = dbapi.list_capsules( + self.context, filters={'uuid': capsule1.uuid}) + self.assertEqual([capsule1.id], [r.id for r in res]) + + res = dbapi.list_capsules( + self.context, filters={'uuid': capsule2.uuid}) + self.assertEqual([capsule2.id], [r.id for r in res]) + + res = dbapi.list_capsules( + self.context, filters={'uuid': 'unknow-uuid'}) + self.assertEqual([], [r.id for r in res]) + + def test_destroy_capsule(self): + capsule = utils.create_test_capsule(context=self.context) + dbapi.destroy_capsule(self.context, capsule.id) + self.assertRaises(exception.CapsuleNotFound, + dbapi.get_capsule_by_uuid, + self.context, + capsule.uuid) + + def test_destroy_capsule_by_uuid(self): + capsule = utils.create_test_capsule(context=self.context) + dbapi.destroy_capsule(self.context, capsule.uuid) + self.assertRaises(exception.CapsuleNotFound, + dbapi.get_capsule_by_uuid, + self.context, + capsule.uuid) + + def test_destroy_non_exists_capsule(self): + self.assertRaises(exception.CapsuleNotFound, + dbapi.destroy_capsule, + self.context, + uuidutils.generate_uuid()) + + def test_update_capsule(self): + capsule = utils.create_test_capsule(context=self.context) + current_meta_name = capsule.meta_name + new_meta_name = 'new-meta-name' + self.assertNotEqual(current_meta_name, new_meta_name) + + res = dbapi.update_capsule(self.context, capsule.id, + {'meta_name': new_meta_name}) + self.assertEqual(new_meta_name, res.meta_name) + + def test_update_capsule_not_found(self): + capsule_uuid = uuidutils.generate_uuid() + new_meta_name = 'new-meta-name' + self.assertRaises(exception.CapsuleNotFound, + dbapi.update_capsule, + self.context, capsule_uuid, + {'meta_name': new_meta_name}) + + def test_update_capsule_uuid(self): + capsule = utils.create_test_capsule(context=self.context) + self.assertRaises(exception.InvalidParameterValue, + dbapi.update_capsule, self.context, + capsule.id, {'uuid': ''}) + + +class EtcdDbCapsuleTestCase(base.DbTestCase): + + def setUp(self): + cfg.CONF.set_override('db_type', 'etcd') + super(EtcdDbCapsuleTestCase, self).setUp() + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_create_capsule(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + utils.create_test_capsule(context=self.context) + mock_read.side_effect = lambda *args: None + self.assertRaises(exception.ResourceExists, + utils.create_test_capsule, + context=self.context) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_get_capsule_by_uuid(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + capsule = utils.create_test_capsule(context=self.context) + mock_read.side_effect = lambda *args: FakeEtcdResult( + capsule.as_dict()) + res = dbapi.get_capsule_by_uuid(self.context, + capsule.uuid) + self.assertEqual(capsule.id, res.id) + self.assertEqual(capsule.uuid, res.uuid) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_get_capsule_by_meta_name(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + capsule = utils.create_test_capsule(context=self.context) + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + [capsule.as_dict()]) + res = dbapi.get_capsule_by_meta_name( + self.context, capsule.meta_name) + self.assertEqual(capsule.id, res.id) + self.assertEqual(capsule.uuid, res.uuid) + + @mock.patch.object(etcd_client, 'read') + def test_get_nonexists_capsule(self, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.CapsuleNotFound, + dbapi.get_capsule_by_uuid, + self.context, + uuidutils.generate_uuid()) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_list_capsules(self, mock_write, mock_read): + uuids = [] + capsules = [] + mock_read.side_effect = etcd.EtcdKeyNotFound + for i in range(1, 6): + capsule = utils.create_test_capsule( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='capsule' + str(i)) + capsules.append(capsule.as_dict()) + uuids.append(six.text_type(capsule['uuid'])) + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + capsules) + res = dbapi.list_capsules(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_capsules_sorted(self, mock_write, + mock_read): + uuids = [] + capsules = [] + mock_read.side_effect = etcd.EtcdKeyNotFound + for i in range(1, 6): + capsule = utils.create_test_capsule( + uuid=uuidutils.generate_uuid(), + context=self.context, + name='capsule' + str(i)) + capsules.append(capsule.as_dict()) + uuids.append(six.text_type(capsule['uuid'])) + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + capsules) + res = dbapi.list_capsules(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_capsules, + self.context, + sort_key='foo') + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_list_capsules_with_filters(self, mock_write, + mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + + capsule1 = utils.create_test_capsule( + name='capsule1', + uuid=uuidutils.generate_uuid(), + context=self.context) + capsule2 = utils.create_test_capsule( + name='capsule2', + uuid=uuidutils.generate_uuid(), + context=self.context) + + mock_read.side_effect = lambda *args: FakeEtcdMultipleResult( + [capsule1.as_dict(), capsule2.as_dict()]) + + res = dbapi.list_capsules( + self.context, filters={'uuid': capsule1.uuid}) + self.assertEqual([capsule1.id], [r.id for r in res]) + + res = dbapi.list_capsules( + self.context, filters={'uuid': capsule2.uuid}) + self.assertEqual([capsule2.id], [r.id for r in res]) + + res = dbapi.list_capsules( + 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_capsule_by_uuid(self, mock_delete, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + capsule = utils.create_test_capsule(context=self.context) + mock_read.side_effect = lambda *args: FakeEtcdResult( + capsule.as_dict()) + dbapi.destroy_capsule(self.context, capsule.uuid) + mock_delete.assert_called_once_with('/capsules/%s' % capsule.uuid) + + @mock.patch.object(etcd_client, 'read') + def test_destroy_non_exists_capsule(self, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.CapsuleNotFound, + dbapi.destroy_capsule, 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_capsule(self, mock_update, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + capsule = utils.create_test_capsule(context=self.context) + new_meta_name = 'new_meta_name' + + mock_read.side_effect = lambda *args: FakeEtcdResult( + capsule.as_dict()) + dbapi.update_capsule(self.context, capsule.uuid, + {'meta_name': new_meta_name}) + self.assertEqual(new_meta_name, json.loads( + mock_update.call_args_list[0][0][0].value.decode('utf-8')) + ['meta_name']) + + @mock.patch.object(etcd_client, 'read') + def test_update_capsule_not_found(self, mock_read): + capsule_uuid = uuidutils.generate_uuid() + new_meta_name = 'new-meta-name' + mock_read.side_effect = etcd.EtcdKeyNotFound + self.assertRaises(exception.CapsuleNotFound, + dbapi.update_capsule, self.context, + capsule_uuid, {'meta_name': new_meta_name}) + + @mock.patch.object(etcd_client, 'read') + @mock.patch.object(etcd_client, 'write') + def test_update_capsule_uuid(self, mock_write, mock_read): + mock_read.side_effect = etcd.EtcdKeyNotFound + capsule = utils.create_test_capsule(context=self.context) + self.assertRaises(exception.InvalidParameterValue, + dbapi.update_capsule, self.context, + capsule.uuid, {'uuid': ''})