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:
parent
72ed8e27f4
commit
4d21be6947
|
@ -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}}
|
||||
|
|
|
@ -47,3 +47,5 @@ KEYPAIR = "keypair"
|
|||
JOB_SUCCESS = "SUCCESS"
|
||||
|
||||
JOB_FAILURE = "FAILURE"
|
||||
|
||||
IMAGE = "image"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Reference in New Issue