diff --git a/kingbird/api/controllers/root.py b/kingbird/api/controllers/root.py index 3071ef6..7b8ebf5 100644 --- a/kingbird/api/controllers/root.py +++ b/kingbird/api/controllers/root.py @@ -17,6 +17,7 @@ import pecan from kingbird.api.controllers import quota_manager +from kingbird.api.controllers import sync_manager from kingbird.api.controllers.v1 import quota_class @@ -60,6 +61,7 @@ class V1Controller(object): self.sub_controllers = { "os-quota-sets": quota_manager.QuotaManagerController, "os-quota-class-sets": quota_class.QuotaClassSetController, + "os-sync": sync_manager.ResourceSyncController } for name, ctrl in self.sub_controllers.items(): setattr(self, name, ctrl) diff --git a/kingbird/api/controllers/sync_manager.py b/kingbird/api/controllers/sync_manager.py new file mode 100644 index 0000000..a2c88f8 --- /dev/null +++ b/kingbird/api/controllers/sync_manager.py @@ -0,0 +1,153 @@ +# 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. + +import restcomm + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import uuidutils + +import collections +import pecan +from pecan import expose +from pecan import request + +from kingbird.common import consts +from kingbird.common import exceptions +from kingbird.common.i18n import _ +from kingbird.db.sqlalchemy import api as db_api +from kingbird.drivers.openstack import sdk +from kingbird.rpc import client as rpc_client + +CONF = cfg.CONF + + +LOG = logging.getLogger(__name__) + + +class ResourceSyncController(object): + VERSION_ALIASES = { + 'Newton': '1.0', + } + + def __init__(self, *args, **kwargs): + super(ResourceSyncController, self).__init__(*args, **kwargs) + self.rpc_client = rpc_client.EngineClient() + + # to do the version compatibility for future purpose + def _determine_version_cap(self, target): + version_cap = 1.0 + return version_cap + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, project, action=None): + context = restcomm.extract_context_from_environ() + if not uuidutils.is_uuid_like(project) or project != context.project: + pecan.abort(400, _('Invalid request URL')) + result = collections.defaultdict(dict) + if not action or action == 'active': + result['job_set'] = db_api.sync_job_list(context, action) + elif uuidutils.is_uuid_like(action): + try: + result['job_set'] = db_api.resource_sync_list_by_job( + context, action) + except exceptions.JobNotFound: + pecan.abort(404, _('Job not found')) + else: + pecan.abort(400, _('Invalid request URL')) + return result + + @index.when(method='POST', template='json') + def post(self, project): + context = restcomm.extract_context_from_environ() + if not uuidutils.is_uuid_like(project) or project != context.project: + pecan.abort(400, _('Invalid request URL')) + if not request.body: + pecan.abort(400, _('Body required')) + payload = eval(request.body) + payload = payload.get('resource_set') + if not payload: + pecan.abort(400, _('resource_set required')) + target_regions = payload.get('target') + if not target_regions or not isinstance(target_regions, list): + pecan.abort(400, _('Target regions required')) + source_region = payload.get('source') + if not source_region or not isinstance(source_region, str): + pecan.abort(400, _('Source region required')) + if payload.get('resource_type') == consts.KEYPAIR: + user_id = context.user + source_keys = payload.get('resources') + if not source_keys: + pecan.abort(400, _('Source keypairs required')) + # Create Source Region object + source_os_client = sdk.OpenStackDriver(source_region) + # Check for keypairs in Source Region + for source_keypair in source_keys: + source_keypair = source_os_client.get_keypairs(user_id, + 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_keys: + try: + db_api.resource_sync_create(context, result, + region, keypair, + consts.KEYPAIR) + except exceptions.JobNotFound: + pecan.abort(404, _('Job not found')) + return self._keypair_sync(job_id, user_id, payload, context, + result) + else: + pecan.abort(400, _('Bad resource_type')) + + @index.when(method='delete', template='json') + def delete(self, project, job_id): + context = restcomm.extract_context_from_environ() + if not uuidutils.is_uuid_like(project) or project != context.project: + pecan.abort(400, _('Invalid request URL')) + if uuidutils.is_uuid_like(job_id): + try: + status = db_api.sync_job_status(context, job_id) + except exceptions.JobNotFound: + pecan.abort(404, _('Job not found')) + if status == consts.JOB_PROGRESS: + pecan.abort(406, _('action not supported' + ' while sync is in progress')) + try: + db_api.sync_job_delete(context, job_id) + except exceptions.JobNotFound: + pecan.abort(404, _('Job not found')) + return 'job %s deleted from the database.' % job_id + else: + pecan.abort(400, _('Bad request')) + + def _keypair_sync(self, job_id, user_id, payload, context, result): + self.rpc_client.keypair_sync_for_user(context, job_id, payload, + user_id) + + 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 cf8cee9..da171d7 100644 --- a/kingbird/common/consts.py +++ b/kingbird/common/consts.py @@ -36,10 +36,14 @@ NEUTRON_QUOTA_FIELDS = ("network", "security_group_rule", ) -SYNC_STATUS = "IN_PROGRESS" +JOB_PROGRESS = "IN_PROGRESS" RPC_API_VERSION = "1.0" TOPIC_KB_ENGINE = "kingbird-engine" +KEYPAIR = "keypair" + JOB_SUCCESS = "SUCCESS" + +JOB_FAILURE = "FAILURE" diff --git a/kingbird/db/sqlalchemy/api.py b/kingbird/db/sqlalchemy/api.py index df8133e..dd41c3d 100644 --- a/kingbird/db/sqlalchemy/api.py +++ b/kingbird/db/sqlalchemy/api.py @@ -402,7 +402,7 @@ def sync_job_list(context, action=None): if action == 'active': rows = model_query(context, models.SyncJob). \ filter_by(user_id=context.user, project_id=context.project, - sync_status=consts.SYNC_STATUS). \ + sync_status=consts.JOB_PROGRESS). \ all() else: rows = model_query(context, models.SyncJob). \ diff --git a/kingbird/db/sqlalchemy/migrate_repo/versions/006_sync_job.py b/kingbird/db/sqlalchemy/migrate_repo/versions/006_sync_job.py index 1b98b86..75d0eee 100644 --- a/kingbird/db/sqlalchemy/migrate_repo/versions/006_sync_job.py +++ b/kingbird/db/sqlalchemy/migrate_repo/versions/006_sync_job.py @@ -25,7 +25,7 @@ def upgrade(migrate_engine): sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True), sqlalchemy.Column('sync_status', sqlalchemy.String(length=36), - default=consts.SYNC_STATUS, nullable=False), + default=consts.JOB_PROGRESS, nullable=False), sqlalchemy.Column('user_id', sqlalchemy.String(36), nullable=False), sqlalchemy.Column('project_id', sqlalchemy.String(36), @@ -50,7 +50,7 @@ def upgrade(migrate_engine): sqlalchemy.Column('resource_type', sqlalchemy.String(36), nullable=False), sqlalchemy.Column('sync_status', sqlalchemy.String(36), - default=consts.SYNC_STATUS, nullable=False), + default=consts.JOB_PROGRESS, nullable=False), sqlalchemy.Column('updated_at', sqlalchemy.DateTime), sqlalchemy.Column('created_at', sqlalchemy.DateTime), sqlalchemy.Column('deleted_at', sqlalchemy.DateTime), diff --git a/kingbird/db/sqlalchemy/models.py b/kingbird/db/sqlalchemy/models.py index bef2d77..488ffef 100644 --- a/kingbird/db/sqlalchemy/models.py +++ b/kingbird/db/sqlalchemy/models.py @@ -156,7 +156,7 @@ class SyncJob(BASE, KingbirdBase): id = Column('id', String(36), primary_key=True) - sync_status = Column(String(36), default=consts.SYNC_STATUS, + sync_status = Column(String(36), default=consts.JOB_PROGRESS, nullable=False) job_relation = relationship('ResourceSync', backref='sync_job') @@ -180,5 +180,5 @@ class ResourceSync(BASE, KingbirdBase): resource_type = Column('resource_type', String(36), nullable=False) - sync_status = Column(String(36), default=consts.SYNC_STATUS, + sync_status = Column(String(36), default=consts.JOB_PROGRESS, nullable=False) diff --git a/kingbird/drivers/openstack/nova_v2.py b/kingbird/drivers/openstack/nova_v2.py index 0bf3674..e063fac 100644 --- a/kingbird/drivers/openstack/nova_v2.py +++ b/kingbird/drivers/openstack/nova_v2.py @@ -17,16 +17,17 @@ from oslo_log import log from kingbird.common import consts from kingbird.common import exceptions +from kingbird.common.i18n import _LI, _LE from kingbird.drivers import base from novaclient import client LOG = log.getLogger(__name__) -API_VERSION = '2.1' +API_VERSION = '2.37' class NovaClient(base.DriverBase): - '''Nova V2.1 driver.''' + '''Nova V2.37 driver.''' def __init__(self, region, disabled_quotas, session): try: self.nova_client = client.Client(API_VERSION, @@ -40,7 +41,7 @@ class NovaClient(base.DriverBase): raise def get_resource_usages(self, project_id): - """Collects resource usages for a given project + """Collect resource usages for a given project :params: project_id :return: dictionary of corresponding resources with its usage @@ -69,7 +70,7 @@ class NovaClient(base.DriverBase): raise def update_quota_limits(self, project_id, **new_quota): - """Updates quota limits for a given project + """Update quota limits for a given project :params: project_id, dictionary with the quota limits to update :return: Nothing @@ -97,3 +98,37 @@ class NovaClient(base.DriverBase): return self.nova_client.quotas.delete(project_id) except exceptions.InternalError: raise + + def get_keypairs(self, user_id, res_id): + """Display keypair of the specified User + + :params: user_id and resource_identifier + :return: Keypair + """ + try: + keypair = self.nova_client.keypairs.get(res_id, user_id) + LOG.info(_LI("Source Keypair: %s"), keypair.name) + return keypair + + except Exception as exception: + LOG.error(_LE('Exception Occurred: %s'), exception.message) + pass + + def create_keypairs(self, force, keypair, user_id): + """Create keypair for the specified User + + :params: user_id, keypair, force + :return: Creates a Keypair + """ + if force: + try: + self.nova_client.keypairs.delete(keypair, user_id) + LOG.info(_LI("Deleted Keypair: %s"), keypair.name) + except Exception as exception: + LOG.error(_LE('Exception Occurred: %s'), exception.message) + pass + LOG.info(_LI("Created Keypair: %s"), keypair.name) + return self.nova_client.keypairs. \ + create(keypair.name, + user_id=user_id, + public_key=keypair.public_key) diff --git a/kingbird/drivers/openstack/sdk.py b/kingbird/drivers/openstack/sdk.py index a29e74e..19eae3d 100644 --- a/kingbird/drivers/openstack/sdk.py +++ b/kingbird/drivers/openstack/sdk.py @@ -182,3 +182,9 @@ class OpenStackDriver(object): return False else: return True + + def get_keypairs(self, user_id, resource_identifier): + return self.nova_client.get_keypairs(user_id, resource_identifier) + + def create_keypairs(self, force, keypair, user_id): + return self.nova_client.create_keypairs(force, keypair, user_id) diff --git a/kingbird/engine/service.py b/kingbird/engine/service.py index bf47b2d..a10d6c4 100644 --- a/kingbird/engine/service.py +++ b/kingbird/engine/service.py @@ -25,6 +25,7 @@ from kingbird.common.i18n import _, _LE, _LI from kingbird.common import messaging as rpc_messaging from kingbird.engine.quota_manager import QuotaManager from kingbird.engine import scheduler +from kingbird.engine.sync_manager import SyncManager from kingbird.objects import service as service_obj from oslo_service import service from oslo_utils import timeutils @@ -72,6 +73,7 @@ class EngineService(service.Service): self.target = None self._rpc_server = None self.qm = None + self.sm = None def init_tgm(self): self.TG = scheduler.ThreadGroupManager() @@ -79,10 +81,14 @@ class EngineService(service.Service): def init_qm(self): self.qm = QuotaManager() + def init_sm(self): + self.sm = SyncManager() + def start(self): self.engine_id = uuidutils.generate_uuid() self.init_tgm() self.init_qm() + self.init_sm() target = oslo_messaging.Target(version=self.rpc_api_version, server=self.host, topic=self.topic) @@ -147,6 +153,11 @@ class EngineService(service.Service): project_id) self.qm.quota_sync_for_project(project_id) + @request_context + def keypair_sync_for_user(self, ctxt, user_id, job_id, payload): + # Keypair Sync for a user, will be triggered by KB-API + self.sm.keypair_sync_for_user(user_id, job_id, payload) + def _stop_rpc_server(self): # Stop RPC connection to prevent new requests LOG.debug(_("Attempting to stop engine service...")) diff --git a/kingbird/engine/sync_manager.py b/kingbird/engine/sync_manager.py new file mode 100644 index 0000000..5bd4870 --- /dev/null +++ b/kingbird/engine/sync_manager.py @@ -0,0 +1,106 @@ +# Copyright 2017 Ericsson AB +# +# 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. + +import threading + +from oslo_log import log as logging + +from kingbird.common import consts +from kingbird.common import context +from kingbird.common import exceptions +from kingbird.common.i18n import _LE +from kingbird.common.i18n import _LI +from kingbird.common import manager +from kingbird.db.sqlalchemy import api as db_api +from kingbird.drivers.openstack import sdk + +LOG = logging.getLogger(__name__) + + +class SyncManager(manager.Manager): + """Manages tasks related to resource management""" + + def __init__(self, *args, **kwargs): + super(SyncManager, self).__init__(service_name="sync_manager", + *args, **kwargs) + self.context = context.get_admin_context() + + def _create_keypairs_in_region(self, job_id, force, target_regions, + source_keypair, user_id): + regions_thread = list() + for region in target_regions: + thread = threading.Thread(target=self._create_keypairs, + args=(job_id, force, region, + source_keypair, user_id,)) + regions_thread.append(thread) + thread.start() + for region_thread in regions_thread: + region_thread.join() + + def _create_keypairs(self, job_id, force, region, source_keypair, user_id): + os_client = sdk.OpenStackDriver(region) + try: + os_client.create_keypairs(force, source_keypair, user_id) + LOG.info(_LI('keypair %(keypair)s created in %(region)s') + % {'keypair': source_keypair.name, 'region': region}) + try: + db_api.resource_sync_update(self.context, job_id, region, + source_keypair.name, + consts.JOB_SUCCESS) + except exceptions.JobNotFound(): + raise + except Exception as exc: + LOG.error(_LE('Exception Occurred: %(msg)s in %(region)s') + % {'msg': exc.message, 'region': region}) + try: + db_api.resource_sync_update(self.context, job_id, region, + source_keypair.name, + consts.JOB_FAILURE) + except exceptions.JobNotFound(): + raise + pass + + def keypair_sync_for_user(self, user_id, job_id, payload): + LOG.info(_LI("Keypair sync Called for user: %s"), + user_id) + keypairs_thread = list() + target_regions = payload['target'] + force = eval(str(payload.get('force', False))) + resource_ids = payload.get('resources') + source_os_client = sdk.OpenStackDriver(payload['source']) + for keypair in resource_ids: + source_keypair = source_os_client.get_keypairs(user_id, + keypair) + thread = threading.Thread(target=self._create_keypairs_in_region, + args=(job_id, force, target_regions, + source_keypair, user_id,)) + keypairs_thread.append(thread) + thread.start() + for keypair_thread in keypairs_thread: + keypair_thread.join() + + # Update the parent_db after the sync + try: + resource_sync_details = db_api.\ + resource_sync_status(self.context, job_id) + except exceptions.JobNotFound: + raise + result = consts.JOB_SUCCESS + if consts.JOB_FAILURE in resource_sync_details: + result = consts.JOB_FAILURE + try: + db_api.sync_job_update(self.context, job_id, result) + except exceptions.JobNotFound: + raise diff --git a/kingbird/rpc/client.py b/kingbird/rpc/client.py index 87f3898..023b18a 100644 --- a/kingbird/rpc/client.py +++ b/kingbird/rpc/client.py @@ -68,3 +68,9 @@ class EngineClient(object): def quota_sync_for_project(self, ctxt, project_id): return self.cast(ctxt, self.make_msg('quota_sync_for_project', project_id=project_id)) + + def keypair_sync_for_user(self, ctxt, job_id, payload, user_id): + return self.cast( + ctxt, + self.make_msg('keypair_sync_for_user', + user_id=user_id, job_id=job_id, payload=payload)) diff --git a/kingbird/tests/unit/api/controllers/test_sync_manager.py b/kingbird/tests/unit/api/controllers/test_sync_manager.py new file mode 100644 index 0000000..db49ea5 --- /dev/null +++ b/kingbird/tests/unit/api/controllers/test_sync_manager.py @@ -0,0 +1,225 @@ +# 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. + +import mock +import webtest + +from oslo_utils import timeutils + +from kingbird.api.controllers import sync_manager +from kingbird.common import consts +from kingbird.rpc import client as rpc_client +from kingbird.tests.unit.api import testroot +from kingbird.tests import utils + +DEFAULT_FORCE = False +SOURCE_KEYPAIR = 'fake_key1' +FAKE_TARGET_REGION = ['fake_target_region'] +FAKE_SOURCE_REGION = 'fake_source_region' +FAKE_RESOURCE_ID = 'fake_id' +FAKE_RESOURCE_TYPE = 'keypair' +FAKE_TENANT = utils.UUID1 +FAKE_JOB = utils.UUID2 +FAKE_URL = '/v1.0/' + FAKE_TENANT + '/os-sync/' +WRONG_URL = '/v1.0/wrong/os-sync/' +fake_user = utils.UUID3 +FAKE_STATUS = consts.JOB_PROGRESS +FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin'} +NON_ADMIN_HEADERS = {'X-Tenant-Id': FAKE_TENANT} + + +class FakeKeypair(object): + def __init__(self, name, public_key): + self.name = name + self.public_key = public_key + + +class SyncJob(object): + def __init__(self, id, sync_status, created_at): + self.id = id + self.sync_status = sync_status + self.created_at = created_at + + +class TestResourceManager(testroot.KBApiTest): + def setUp(self): + super(TestResourceManager, self).setUp() + self.ctx = utils.dummy_context() + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'sdk') + @mock.patch.object(sync_manager, 'db_api') + def test_post_keypair_sync(self, mock_db_api, mock_sdk, mock_rpc_client): + time_now = timeutils.utcnow() + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "resource_type": "keypair", + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + fake_key = FakeKeypair('fake_name', 'fake-rsa') + sync_job_result = SyncJob(FAKE_JOB, consts.JOB_PROGRESS, time_now) + mock_sdk.OpenStackDriver().get_keypairs.return_value = fake_key + 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_sdk.OpenStackDriver().get_keypairs.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_keypair_sync_wrong_url(self, mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, WRONG_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_no_body(self, mock_rpc_client): + data = {} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_wrong_payload(self, mock_rpc_client): + data = {"resources": [SOURCE_KEYPAIR], + "force": "True", "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_no_target_regions(self, mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "force": "True", + "source": FAKE_SOURCE_REGION}} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_no_source_regions(self, mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "force": "True", + "target": [FAKE_TARGET_REGION]}} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_no_keys_in_body(self, mock_rpc_client): + data = {"resource_set": {"force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_post_no_resource_type(self, mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + self.assertRaisesRegexp(webtest.app.AppError, "400 *", + self.app.post_json, FAKE_URL, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'sdk') + def test_post_no_keypairs_in_region(self, mock_sdk, mock_rpc_client): + data = {"resource_set": {"resources": [SOURCE_KEYPAIR], + "resource_type": "keypair", + "force": "True", + "source": FAKE_SOURCE_REGION, + "target": [FAKE_TARGET_REGION]}} + mock_sdk.OpenStackDriver().get_keypairs.return_value = None + 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): + delete_url = FAKE_URL + '/' + FAKE_JOB + self.app.delete_json(delete_url, headers=FAKE_HEADERS) + self.assertEqual(1, mock_db_api.sync_job_delete.call_count) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_delete_wrong_request(self, mock_rpc_client): + delete_url = WRONG_URL + '/' + FAKE_JOB + self.assertRaisesRegex(webtest.app.AppError, "400 *", + self.app.delete_json, delete_url, + headers=FAKE_HEADERS) + + @mock.patch.object(rpc_client, 'EngineClient') + def test_delete_invalid_job_id(self, mock_rpc_client): + delete_url = FAKE_URL + '/fake' + self.assertRaisesRegex(webtest.app.AppError, "400 *", + self.app.delete_json, delete_url, + headers=FAKE_HEADERS) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_delete_in_progress_job(self, mock_db_api, mock_rpc_client): + delete_url = FAKE_URL + '/' + FAKE_JOB + mock_db_api.sync_job_status.return_value = consts.JOB_PROGRESS + self.assertRaises(KeyError, self.app.delete_json, delete_url, + headers=FAKE_HEADERS) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_get_job(self, mock_db_api, mock_rpc_client): + get_url = FAKE_URL + self.app.get(get_url, headers=FAKE_HEADERS) + self.assertEqual(1, mock_db_api.sync_job_list.call_count) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_get_wrong_request(self, mock_db_api, mock_rpc_client): + get_url = WRONG_URL + '/list' + self.assertRaisesRegex(webtest.app.AppError, "400 *", + self.app.get, get_url, + headers=FAKE_HEADERS) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_get_wrong_action(self, mock_db_api, mock_rpc_client): + get_url = FAKE_URL + '/fake' + self.assertRaises(webtest.app.AppError, self.app.get, get_url, + headers=FAKE_HEADERS) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_get_active_job(self, mock_db_api, mock_rpc_client): + get_url = FAKE_URL + '/active' + self.app.get(get_url, headers=FAKE_HEADERS) + self.assertEqual(1, mock_db_api.sync_job_list.call_count) + + @mock.patch.object(rpc_client, 'EngineClient') + @mock.patch.object(sync_manager, 'db_api') + def test_get_detail_job(self, mock_db_api, mock_rpc_client): + 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) diff --git a/kingbird/tests/unit/api/testroot.py b/kingbird/tests/unit/api/testroot.py index df878de..608bbaf 100644 --- a/kingbird/tests/unit/api/testroot.py +++ b/kingbird/tests/unit/api/testroot.py @@ -116,9 +116,11 @@ class TestV1Controller(KBApiTest): v1_link = links[0] quota_class_link = links[1] quota_manager_link = links[2] + sync_manager_link = links[3] self.assertEqual('self', v1_link['rel']) self.assertEqual('os-quota-sets', quota_manager_link['rel']) self.assertEqual('os-quota-class-sets', quota_class_link['rel']) + self.assertEqual('os-sync', sync_manager_link['rel']) def _test_method_returns_405(self, method): api_method = getattr(self.app, method) diff --git a/kingbird/tests/unit/db/test_resource_sync_db_api.py b/kingbird/tests/unit/db/test_resource_sync_db_api.py index b276a3e..af8ec1f 100644 --- a/kingbird/tests/unit/db/test_resource_sync_db_api.py +++ b/kingbird/tests/unit/db/test_resource_sync_db_api.py @@ -72,9 +72,10 @@ class DBAPIResourceSyncTest(base.KingbirdTestCase): def test_create_sync_job(self): job = self.sync_job_create(self.ctx, job_id=UUID1) self.assertIsNotNone(job) - self.assertEqual(consts.SYNC_STATUS, job.sync_status) + self.assertEqual(consts.JOB_PROGRESS, job.sync_status) created_job = db_api.sync_job_list(self.ctx, "active") - self.assertEqual(consts.SYNC_STATUS, created_job[0].get('sync_status')) + self.assertEqual(consts.JOB_PROGRESS, + created_job[0].get('sync_status')) def test_primary_key_sync_job(self): self.sync_job_create(self.ctx, job_id=UUID1) @@ -98,7 +99,7 @@ class DBAPIResourceSyncTest(base.KingbirdTestCase): job = self.sync_job_create(self.ctx, job_id=UUID1) self.assertIsNotNone(job) query = db_api.sync_job_status(self.ctx, job_id=UUID1) - self.assertEqual(query, consts.SYNC_STATUS) + self.assertEqual(query, consts.JOB_PROGRESS) def test_update_invalid_job(self): job = self.sync_job_create(self.ctx, job_id=UUID1) @@ -113,7 +114,7 @@ class DBAPIResourceSyncTest(base.KingbirdTestCase): self.ctx, job=job, region='Fake_region', resource='fake_key', resource_type='keypair') self.assertIsNotNone(resource_sync_create) - self.assertEqual(consts.SYNC_STATUS, resource_sync_create.sync_status) + self.assertEqual(consts.JOB_PROGRESS, resource_sync_create.sync_status) def test_resource_sync_status(self): job = self.sync_job_create(self.ctx, job_id=UUID1) @@ -122,7 +123,7 @@ class DBAPIResourceSyncTest(base.KingbirdTestCase): resource='fake_key', resource_type='keypair') self.assertIsNotNone(resource_sync_create) status = db_api.resource_sync_status(self.ctx, job.id) - self.assertEqual(consts.SYNC_STATUS, status[0]) + self.assertEqual(consts.JOB_PROGRESS, status[0]) def test_resource_sync_update(self): job = self.sync_job_create(self.ctx, job_id=UUID1) @@ -130,7 +131,7 @@ class DBAPIResourceSyncTest(base.KingbirdTestCase): self.ctx, job=job, region='Fake_region', resource='fake_key', resource_type='keypair') self.assertIsNotNone(resource_sync_create) - self.assertEqual(consts.SYNC_STATUS, resource_sync_create.sync_status) + self.assertEqual(consts.JOB_PROGRESS, resource_sync_create.sync_status) db_api.resource_sync_update( self.ctx, job.id, 'Fake_region', 'fake_key', consts.JOB_SUCCESS) updated_job = db_api.resource_sync_list_by_job(self.ctx, job.id) diff --git a/kingbird/tests/unit/drivers/test_nova_v2.py b/kingbird/tests/unit/drivers/test_nova_v2.py index de2c43e..d0229ce 100644 --- a/kingbird/tests/unit/drivers/test_nova_v2.py +++ b/kingbird/tests/unit/drivers/test_nova_v2.py @@ -51,6 +51,14 @@ FAKE_LIMITS = {'absolute': u'maxTotalRAMSize': 51200, u'maxTotalCores': 10 } } +FAKE_RESOURCE_ID = 'fake_id' +DEFAULT_FORCE = False + + +class FakeKeypair(object): + def __init__(self, name, public_key): + self.name = name + self.public_key = public_key class TestNovaClient(base.KingbirdTestCase): @@ -59,6 +67,7 @@ class TestNovaClient(base.KingbirdTestCase): self.ctx = utils.dummy_context() self.session = 'fake_session' self.project = 'fake_project' + self.user = 'fake_user' def test_init(self): nv_client = nova_v2.NovaClient('fake_region', DISABLED_QUOTAS, @@ -104,3 +113,36 @@ class TestNovaClient(base.KingbirdTestCase): mock_novaclient.Client().quotas.delete.assert_called_once_with( self.project) + + @mock.patch.object(nova_v2, 'client') + def test_get_keypairs(self, mock_novaclient): + nv_client = nova_v2.NovaClient('fake_region', DISABLED_QUOTAS, + self.session) + nv_client.get_keypairs(self.user, FAKE_RESOURCE_ID) + mock_novaclient.Client().keypairs.get.return_value = 'key1' + mock_novaclient.Client().keypairs.get.\ + assert_called_once_with(FAKE_RESOURCE_ID, self.user) + + @mock.patch.object(nova_v2, 'client') + def test_create_keypairs_with_force_false(self, mock_novaclient): + nv_client = nova_v2.NovaClient('fake_region', DISABLED_QUOTAS, + self.session) + fake_key = FakeKeypair('fake_name', 'fake-rsa') + nv_client.create_keypairs(DEFAULT_FORCE, fake_key, self.user) + self.assertEqual(0, + mock_novaclient.Client().keypairs.delete.call_count) + mock_novaclient.Client().keypairs.create.\ + assert_called_once_with(fake_key.name, user_id=self.user, + public_key=fake_key.public_key) + + @mock.patch.object(nova_v2, 'client') + def test_create_keypairs_with_force_true(self, mock_novaclient): + nv_client = nova_v2.NovaClient('fake_region', DISABLED_QUOTAS, + self.session) + fake_key = FakeKeypair('fake_name', 'fake-rsa') + nv_client.create_keypairs(True, fake_key, self.user) + mock_novaclient.Client().keypairs.delete.\ + assert_called_once_with(fake_key, self.user) + mock_novaclient.Client().keypairs.create.\ + assert_called_once_with(fake_key.name, user_id=self.user, + public_key=fake_key.public_key) diff --git a/kingbird/tests/unit/drivers/test_openstack_driver.py b/kingbird/tests/unit/drivers/test_openstack_driver.py index 2790c7d..31564ee 100644 --- a/kingbird/tests/unit/drivers/test_openstack_driver.py +++ b/kingbird/tests/unit/drivers/test_openstack_driver.py @@ -18,6 +18,11 @@ from kingbird.drivers.openstack import sdk from kingbird.tests import base from kingbird.tests import utils +FAKE_USER_ID = 'user123' +FAKE_RESOURCE_ID = 'fake_id' +DEFAULT_FORCE = False +SOURCE_KEYPAIR = 'fake_key1' + class FakeService(object): @@ -30,6 +35,12 @@ class FakeService(object): self.name = name +class FakeKeypair(object): + def __init__(self, name, public_key): + self.name = name + self.public_key = public_key + + class User(object): def __init__(self, user_name, id, enabled=True): self.user_name = user_name @@ -292,3 +303,53 @@ class TestOpenStackDriver(base.KingbirdTestCase): os_driver = sdk.OpenStackDriver() expected = os_driver._is_token_valid() self.assertEqual(expected, False) + + @mock.patch.object(sdk.OpenStackDriver, 'os_clients_dict') + @mock.patch.object(sdk, 'KeystoneClient') + @mock.patch.object(sdk, 'NovaClient') + @mock.patch.object(sdk, 'NeutronClient') + @mock.patch.object(sdk, 'CinderClient') + def test_get_keypairs(self, mock_cinder_client, + mock_network_client, + mock_nova_client, mock_keystone_client, + mock_os_client): + os_driver = sdk.OpenStackDriver() + os_driver.get_keypairs(FAKE_USER_ID, FAKE_RESOURCE_ID) + mock_nova_client().get_keypairs.\ + assert_called_once_with(FAKE_USER_ID, FAKE_RESOURCE_ID) + + @mock.patch.object(sdk.OpenStackDriver, 'os_clients_dict') + @mock.patch.object(sdk, 'KeystoneClient') + @mock.patch.object(sdk, 'NovaClient') + @mock.patch.object(sdk, 'NeutronClient') + @mock.patch.object(sdk, 'CinderClient') + def test_create_keypairs_with_force_false(self, mock_cinder_client, + mock_network_client, + mock_nova_client, + mock_keystone_client, + mock_os_client): + os_driver = sdk.OpenStackDriver() + fake_key = FakeKeypair('fake_name', 'fake-rsa') + os_driver.create_keypairs(False, fake_key, + FAKE_USER_ID) + mock_nova_client().create_keypairs.\ + assert_called_once_with(False, fake_key, + FAKE_USER_ID) + + @mock.patch.object(sdk.OpenStackDriver, 'os_clients_dict') + @mock.patch.object(sdk, 'KeystoneClient') + @mock.patch.object(sdk, 'NovaClient') + @mock.patch.object(sdk, 'NeutronClient') + @mock.patch.object(sdk, 'CinderClient') + def test_create_keypairs_with_force_true(self, mock_cinder_client, + mock_network_client, + mock_nova_client, + mock_keystone_client, + mock_os_client): + os_driver = sdk.OpenStackDriver() + fake_key = FakeKeypair('fake_name', 'fake-rsa') + os_driver.create_keypairs(True, fake_key, + FAKE_USER_ID) + mock_nova_client().create_keypairs.\ + assert_called_once_with(True, fake_key, + FAKE_USER_ID) diff --git a/kingbird/tests/unit/engine/test_service.py b/kingbird/tests/unit/engine/test_service.py index 47f318f..3bed385 100644 --- a/kingbird/tests/unit/engine/test_service.py +++ b/kingbird/tests/unit/engine/test_service.py @@ -19,6 +19,8 @@ from kingbird.tests import utils from oslo_config import cfg CONF = cfg.CONF +FAKE_USER = utils.UUID1 +FAKE_JOB = utils.UUID2 class TestEngineService(base.KingbirdTestCase): @@ -30,6 +32,9 @@ class TestEngineService(base.KingbirdTestCase): tenant=self.tenant_id) self.service_obj = service.EngineService('kingbird', 'kingbird-engine') + self.payload = {} + self.user_id = FAKE_USER + self.job_id = FAKE_JOB def test_init(self): self.assertEqual(self.service_obj.host, 'localhost') @@ -48,6 +53,11 @@ class TestEngineService(base.KingbirdTestCase): self.service_obj.init_qm() self.assertIsNotNone(self.service_obj.qm) + @mock.patch.object(service, 'SyncManager') + def test_init_sm(self, mock_resource_manager): + self.service_obj.init_sm() + self.assertIsNotNone(self.service_obj.sm) + @mock.patch.object(service.EngineService, 'service_registry_cleanup') @mock.patch.object(service, 'QuotaManager') @mock.patch.object(service, 'rpc_messaging') @@ -99,3 +109,13 @@ class TestEngineService(base.KingbirdTestCase): self.service_obj.start() self.service_obj.stop() mock_rpc.get_rpc_server().stop.assert_called_once_with() + + @mock.patch.object(service, 'SyncManager') + def test_resource_sync_for_user(self, mock_sync_manager): + self.service_obj.init_tgm() + self.service_obj.init_sm() + self.service_obj.keypair_sync_for_user( + self.context, self.user_id, + self.job_id, self.payload,) + mock_sync_manager().keypair_sync_for_user.\ + assert_called_once_with(self.user_id, self.job_id, self.payload) diff --git a/kingbird/tests/unit/engine/test_sync_manager.py b/kingbird/tests/unit/engine/test_sync_manager.py new file mode 100644 index 0000000..e90a3f9 --- /dev/null +++ b/kingbird/tests/unit/engine/test_sync_manager.py @@ -0,0 +1,94 @@ +# Copyright 2017 Ericsson AB +# +# 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. +import mock + +from kingbird.engine import sync_manager +from kingbird.tests import base +from kingbird.tests import utils + +DEFAULT_FORCE = False +SOURCE_KEYPAIR = 'fake_key1' +FAKE_USER_ID = 'user123' +FAKE_TARGET_REGION = 'fake_target_region' +FAKE_SOURCE_REGION = 'fake_source_region' +FAKE_RESOURCE_ID = 'fake_id' +FAKE_JOB_ID = utils.UUID1 + + +class FakeKeypair(object): + def __init__(self, name, public_key): + self.name = name + self.public_key = public_key + + +class TestSyncManager(base.KingbirdTestCase): + def setUp(self): + super(TestSyncManager, self).setUp() + self.ctxt = utils.dummy_context() + + @mock.patch.object(sync_manager, 'context') + def test_init(self, mock_context): + mock_context.get_admin_context.return_value = self.ctxt + sm = sync_manager.SyncManager() + self.assertIsNotNone(sm) + self.assertEqual('sync_manager', sm.service_name) + self.assertEqual('localhost', sm.host) + self.assertEqual(self.ctxt, sm.context) + + @mock.patch.object(sync_manager, 'sdk') + @mock.patch.object(sync_manager.SyncManager, '_create_keypairs') + @mock.patch.object(sync_manager, 'db_api') + def test_keypair_sync_force_false(self, mock_db_api, mock_create_keypair, + mock_sdk): + payload = {} + payload['target'] = [FAKE_TARGET_REGION] + payload['force'] = DEFAULT_FORCE + payload['source'] = FAKE_SOURCE_REGION + payload['resources'] = [SOURCE_KEYPAIR] + fake_key = FakeKeypair('fake_name', 'fake-rsa') + mock_sdk.OpenStackDriver().get_keypairs.return_value = fake_key + sm = sync_manager.SyncManager() + sm.keypair_sync_for_user(FAKE_USER_ID, FAKE_JOB_ID, payload) + mock_create_keypair.assert_called_once_with( + FAKE_JOB_ID, payload['force'], payload['target'][0], fake_key, + FAKE_USER_ID) + + @mock.patch.object(sync_manager, 'sdk') + @mock.patch.object(sync_manager.SyncManager, '_create_keypairs') + @mock.patch.object(sync_manager, 'db_api') + def test_keypair_sync_force_true(self, mock_db_api, mock_create_keypair, + mock_sdk): + + payload = dict() + payload['target'] = [FAKE_TARGET_REGION] + payload['force'] = True + payload['source'] = FAKE_SOURCE_REGION + payload['resources'] = [SOURCE_KEYPAIR] + fake_key = FakeKeypair('fake_name', 'fake-rsa') + mock_sdk.OpenStackDriver().get_keypairs.return_value = fake_key + sm = sync_manager.SyncManager() + sm.keypair_sync_for_user(FAKE_USER_ID, FAKE_JOB_ID, payload) + mock_create_keypair.assert_called_once_with( + FAKE_JOB_ID, payload['force'], FAKE_TARGET_REGION, fake_key, + FAKE_USER_ID) + + @mock.patch.object(sync_manager, 'sdk') + @mock.patch.object(sync_manager, 'db_api') + def test_create_keypair(self, mock_db_api, mock_sdk): + fake_key = FakeKeypair('fake_name', 'fake-rsa') + sm = sync_manager.SyncManager() + sm._create_keypairs(FAKE_JOB_ID, DEFAULT_FORCE, FAKE_TARGET_REGION, + fake_key, FAKE_USER_ID) + mock_sdk.OpenStackDriver().create_keypairs.\ + assert_called_once_with(DEFAULT_FORCE, fake_key, FAKE_USER_ID)