API validations for image synchronization.

Added validation of source image
Added test cases for the same.

Depends-on: I66fbf4ab207a6615f8070d3a22ea7561e608dcd9
Depends-on: I022bd5b972aca159b4a8f6f88d201afa5c21ab8e

Partially Implements: blueprint image-synchronization

Change-Id: I316ff7e79afb0ea092eec2a4bf32a78c609198ab
This commit is contained in:
Goutham Pratapa 2017-05-16 17:21:41 +05:30
parent 72ed8e27f4
commit 4d21be6947
8 changed files with 163 additions and 34 deletions

View File

@ -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}}

View File

@ -47,3 +47,5 @@ KEYPAIR = "keypair"
JOB_SUCCESS = "SUCCESS"
JOB_FAILURE = "FAILURE"
IMAGE = "image"

View File

@ -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")

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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')

View File

@ -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():