diff --git a/kingbird/api/controllers/v1/sync_manager.py b/kingbird/api/controllers/v1/sync_manager.py index b4cbe59..92714de 100644 --- a/kingbird/api/controllers/v1/sync_manager.py +++ b/kingbird/api/controllers/v1/sync_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ericsson AB +# Copyright (c) 2017 Ericsson AB. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -27,6 +27,7 @@ from kingbird.common.endpoint_cache import EndpointCache from kingbird.common import exceptions from kingbird.common.i18n import _ from kingbird.db.sqlalchemy import api as db_api +from kingbird.drivers.openstack.glance_v2 import GlanceClient from kingbird.drivers.openstack.nova_v2 import NovaClient from kingbird.rpc import client as rpc_client @@ -53,6 +54,25 @@ class ResourceSyncController(object): # Route the request to specific methods with parameters pass + def _entries_to_database(self, context, target_regions, source_region, + resources, resource_type, job_id): + """Manage the entries to database for both Keypair and image.""" + # Insert into the parent table + try: + result = db_api.sync_job_create(context, job_id=job_id) + except exceptions.InternalError: + pecan.abort(500, _('Internal Server Error.')) + # Insert into the child table + for region in target_regions: + for resource in resources: + try: + db_api.resource_sync_create(context, result, + region, source_region, + resource, resource_type) + except exceptions.JobNotFound: + pecan.abort(404, _('Job not found')) + return result + @index.when(method='GET', template='json') def get(self, project, action=None): """Get details about Sync Job. @@ -103,6 +123,7 @@ class ResourceSyncController(object): source_resources = payload.get('resources') if not source_resources: pecan.abort(400, _('Source resources required')) + job_id = uuidutils.generate_uuid() if resource_type == consts.KEYPAIR: session = EndpointCache().get_session_from_token( context.auth_token, context.project) @@ -114,22 +135,25 @@ class ResourceSyncController(object): get_keypairs(source_keypair) if not source_keypair: pecan.abort(404) - job_id = uuidutils.generate_uuid() - # Insert into the parent table - try: - result = db_api.sync_job_create(context, job_id=job_id) - except exceptions.JobNotFound: - pecan.abort(404, _('Job not found')) - # Insert into the child table - for region in target_regions: - for keypair in source_resources: - try: - db_api.resource_sync_create(context, result, - region, source_region, - keypair, consts.KEYPAIR) - except exceptions.JobNotFound: - pecan.abort(404, _('Job not found')) + result = self._entries_to_database(context, target_regions, + source_region, + source_resources, + resource_type, job_id) return self._keypair_sync(job_id, payload, context, result) + + elif resource_type == consts.IMAGE: + # Create Source Region glance_object + glance_driver = GlanceClient(source_region, context) + # Check for images in Source Region + for image in source_resources: + source_image = glance_driver.check_image(image) + if image != source_image: + pecan.abort(404) + result = self._entries_to_database(context, target_regions, + source_region, + source_resources, + resource_type, job_id) + return self._image_sync(job_id, payload, context, result) else: pecan.abort(400, _('Bad resource_type')) @@ -173,3 +197,16 @@ class ResourceSyncController(object): return {'job_status': {'id': result.id, 'status': result.sync_status, 'created_at': result.created_at}} + + def _image_sync(self, job_id, payload, context, result): + """Make an rpc call to engine. + + :param job_id: ID of the job to update values in database based on + the job_id. + :param payload: payload object. + :param context: context of the request. + :param result: Result object to return an output. + """ + self.rpc_client.image_sync(context, job_id, payload) + return {'job_status': {'id': result.id, 'status': result.sync_status, + 'created_at': result.created_at}} diff --git a/kingbird/common/consts.py b/kingbird/common/consts.py index da171d7..e1b3147 100644 --- a/kingbird/common/consts.py +++ b/kingbird/common/consts.py @@ -47,3 +47,5 @@ KEYPAIR = "keypair" JOB_SUCCESS = "SUCCESS" JOB_FAILURE = "FAILURE" + +IMAGE = "image" diff --git a/kingbird/common/exceptions.py b/kingbird/common/exceptions.py index 9ed331d..963a7b9 100644 --- a/kingbird/common/exceptions.py +++ b/kingbird/common/exceptions.py @@ -31,6 +31,7 @@ class KingbirdException(Exception): a 'message' property. That message will get printf'd with the keyword arguments provided to the constructor. """ + message = _("An unknown exception occurred.") def __init__(self, **kwargs): @@ -111,3 +112,7 @@ class InternalError(KingbirdException): class InvalidInputError(KingbirdException): message = _("An invalid value was provided") + + +class ResourceNotFound(NotFound): + message = _("Resource not available") diff --git a/kingbird/drivers/openstack/glance_v2.py b/kingbird/drivers/openstack/glance_v2.py index 4b156f9..b1c304b 100644 --- a/kingbird/drivers/openstack/glance_v2.py +++ b/kingbird/drivers/openstack/glance_v2.py @@ -68,11 +68,18 @@ class GlanceClient(object): 'X-Identity-Status': status, } - def get_image(self, resource_identifier): + def check_image(self, resource_identifier): """Get the image details for the specified resource_identifier. :param resource_identifier: resource_id for which the details have to be retrieved. """ - return self.glance_client.images.get(resource_identifier) + try: + image = self.glance_client.images.get(resource_identifier) + LOG.info("Source image: %s", image.name) + return image.id + except exceptions.ResourceNotFound(): + LOG.error('Exception Occurred: Source Image %s not available.', + resource_identifier) + pass diff --git a/kingbird/rpc/client.py b/kingbird/rpc/client.py index 9a594df..dff3107 100644 --- a/kingbird/rpc/client.py +++ b/kingbird/rpc/client.py @@ -74,3 +74,8 @@ class EngineClient(object): ctxt, self.make_msg('keypair_sync_for_user', job_id=job_id, payload=payload)) + + def image_sync(self, ctxt, job_id, payload): + return self.cast( + ctxt, + self.make_msg('image_sync', job_id=job_id, payload=payload)) diff --git a/kingbird/tests/unit/api/v1/controllers/test_sync_manager.py b/kingbird/tests/unit/api/v1/controllers/test_sync_manager.py index 0b1f696..628ed3f 100644 --- a/kingbird/tests/unit/api/v1/controllers/test_sync_manager.py +++ b/kingbird/tests/unit/api/v1/controllers/test_sync_manager.py @@ -26,9 +26,12 @@ from kingbird.tests import utils DEFAULT_FORCE = False SOURCE_KEYPAIR = 'fake_key1' +SOURCE_IMAGE_NAME = 'fake_image' +SOURCE_IMAGE_ID = utils.UUID4 +WRONG_SOURCE_IMAGE_ID = utils.UUID5 FAKE_TARGET_REGION = ['fake_target_region'] FAKE_SOURCE_REGION = 'fake_source_region' -FAKE_RESOURCE_ID = 'fake_id' +FAKE_RESOURCE_ID = ['fake_id'] FAKE_RESOURCE_TYPE = 'keypair' FAKE_TENANT = utils.UUID1 FAKE_JOB = utils.UUID2 @@ -47,6 +50,19 @@ class FakeKeypair(object): self.public_key = public_key +class FakeImage(object): + def __init__(self, id, name): + self.id = id + self.name = name + + +class Result(object): + def __init__(self, job_id, status, time): + self.job_id = job_id + self.status = status + self.time = time + + class SyncJob(object): def __init__(self, id, sync_status, created_at): self.id = id @@ -89,7 +105,32 @@ class TestResourceManager(testroot.KBApiTest): self.assertEqual(response.status_int, 200) @mock.patch.object(rpc_client, 'EngineClient') - def test_post_keypair_sync_wrong_url(self, mock_rpc_client): + @mock.patch.object(sync_manager, 'GlanceClient') + @mock.patch.object(sync_manager, 'db_api') + def test_post_image_sync(self, mock_db_api, mock_glance, mock_rpc_client): + time_now = timeutils.utcnow() + data = {"resource_set": {"resources": [SOURCE_IMAGE_ID], + "resource_type": "image", + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + fake_image = FakeImage(SOURCE_IMAGE_ID, SOURCE_IMAGE_NAME) + sync_job_result = SyncJob(FAKE_JOB, consts.JOB_PROGRESS, time_now) + mock_glance().check_image.return_value = fake_image.id + mock_db_api.sync_job_create.return_value = sync_job_result + response = self.app.post_json(FAKE_URL, + headers=FAKE_HEADERS, + params=data) + self.assertEqual(1, + mock_glance().check_image.call_count) + self.assertEqual(1, + mock_db_api.resource_sync_create.call_count) + self.assertEqual(1, + mock_db_api.sync_job_create.call_count) + self.assertEqual(response.status_int, 200) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_resource_sync_wrong_url(self, mock_rpc_client): data = {"resource_set": {"resources": [SOURCE_KEYPAIR], "force": "True", "source": FAKE_SOURCE_REGION, @@ -133,7 +174,7 @@ class TestResourceManager(testroot.KBApiTest): headers=FAKE_HEADERS, params=data) @mock.patch.object(rpc_client, 'EngineClient') - def test_post_no_keys_in_body(self, mock_rpc_client): + def test_post_no_resources_in_body(self, mock_rpc_client): data = {"resource_set": {"force": "True", "source": FAKE_SOURCE_REGION, "target": [FAKE_TARGET_REGION]}} @@ -168,6 +209,21 @@ class TestResourceManager(testroot.KBApiTest): self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'GlanceClient') + def test_post_no_images_in_source_region(self, mock_glance, + mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_IMAGE_ID], + "resource_type": "image", + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + wrong_image = FakeImage(WRONG_SOURCE_IMAGE_ID, SOURCE_IMAGE_NAME) + mock_glance().check_image.return_value = wrong_image + self.assertRaisesRegexp(webtest.app.AppError, "404 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + @mock.patch.object(rpc_client, 'EngineClient') @mock.patch.object(sync_manager, 'db_api') def test_delete_jobs(self, mock_db_api, mock_rpc_client): @@ -232,3 +288,16 @@ class TestResourceManager(testroot.KBApiTest): get_url = FAKE_URL + '/' + FAKE_JOB self.app.get(get_url, headers=FAKE_HEADERS) self.assertEqual(1, mock_db_api.resource_sync_list_by_job.call_count) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_entries_to_database(self, mock_db_api, mock_rpc_client): + time_now = timeutils.utcnow() + result = Result(FAKE_JOB, FAKE_STATUS, time_now) + mock_db_api.sync_job_create.return_value = result + sync_manager.ResourceSyncController()._entries_to_database( + self.ctx, FAKE_TARGET_REGION, FAKE_SOURCE_REGION, + FAKE_RESOURCE_ID, FAKE_RESOURCE_TYPE, FAKE_JOB) + mock_db_api.resource_sync_create.assert_called_once_with( + self.ctx, result, FAKE_TARGET_REGION[0], FAKE_SOURCE_REGION, + FAKE_RESOURCE_ID[0], FAKE_RESOURCE_TYPE) diff --git a/kingbird/tests/unit/drivers/test_glance_v2.py b/kingbird/tests/unit/drivers/test_glance_v2.py index 439b24b..b026825 100644 --- a/kingbird/tests/unit/drivers/test_glance_v2.py +++ b/kingbird/tests/unit/drivers/test_glance_v2.py @@ -1,14 +1,17 @@ -# 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 +# Copyright (c) 2017 Ericsson AB. +# 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. +# 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 mock import patch @@ -55,13 +58,14 @@ class TestGlanceClient(base.KingbirdTestCase): @patch('kingbird.drivers.openstack.glance_v2.KeystoneClient') @patch('kingbird.drivers.openstack.glance_v2.Client') - def test_get_image(self, mock_glance_client, mock_keystone_client): - """Mock get_image method of glance.""" + def test_check_image(self, mock_glance_client, mock_keystone_client): + """Test get_image method of glance.""" fake_service = FakeService('image', 'fake_type', 'fake_id') fake_endpoint = FakeEndpoint('fake_url', fake_service.id, 'fake_region', 'public') mock_keystone_client().services_list = [fake_service] mock_keystone_client().endpoints_list = [fake_endpoint] - GlanceClient('fake_region', self.ctx).get_image('fake_resource') + glance_client = GlanceClient('fake_region', self.ctx) + glance_client.check_image('fake_resource') mock_glance_client().images.get.\ assert_called_once_with('fake_resource') diff --git a/kingbird/tests/utils.py b/kingbird/tests/utils.py index cab7270..94106e2 100644 --- a/kingbird/tests/utils.py +++ b/kingbird/tests/utils.py @@ -42,8 +42,8 @@ class UUIDStub(object): uuid.uuid4 = self.uuid4 -UUIDs = (UUID1, UUID2, UUID3) = sorted([str(uuid.uuid4()) - for x in range(3)]) +UUIDs = (UUID1, UUID2, UUID3, UUID4, UUID5) = sorted([str(uuid.uuid4()) + for x in range(5)]) def random_name():