diff --git a/kingbird/tests/tempest/scenario/consts.py b/kingbird/tests/tempest/scenario/consts.py index 4ee2569..6ec8263 100644 --- a/kingbird/tests/tempest/scenario/consts.py +++ b/kingbird/tests/tempest/scenario/consts.py @@ -25,3 +25,13 @@ DEFAULT_QUOTAS = { u'port': 50, u'security_groups': 10, u'network': 10 } } + +KEYPAIR_RESOURCE_TYPE = "keypair" + +JOB_SUCCESS = "SUCCESS" + +JOB_PROGRESS = "IN_PROGRESS" + +JOB_ACTIVE = "active" + +JOB_FAILURE = "FAILURE" diff --git a/kingbird/tests/tempest/scenario/resource_management/__init__.py b/kingbird/tests/tempest/scenario/resource_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kingbird/tests/tempest/scenario/resource_management/resource_sync_client.py b/kingbird/tests/tempest/scenario/resource_management/resource_sync_client.py new file mode 100644 index 0000000..b6aa73c --- /dev/null +++ b/kingbird/tests/tempest/scenario/resource_management/resource_sync_client.py @@ -0,0 +1,151 @@ +# 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 json +import requests + +from keystoneclient.auth.identity import v3 +from keystoneclient import session +from keystoneclient.v3 import client as ks_client +from novaclient import client as nv_client +from oslo_log import log as logging +from tempest import config + +CONF = config.CONF +NOVA_API_VERSION = "2.37" +KEYPAIRS = ["kb_test_keypair1", "kb_test_keypair2"] +resource_sync_url = "/os-sync/" + +LOG = logging.getLogger(__name__) + + +def get_session(): + return get_current_session( + CONF.auth.admin_username, + CONF.auth.admin_password, + CONF.auth.admin_project_name + ) + + +def get_current_session(username, password, tenant_name): + auth = v3.Password( + auth_url=CONF.identity.uri_v3, + username=username, + password=password, + project_name=tenant_name, + user_domain_name=CONF.auth.admin_domain_name, + project_domain_name=CONF.auth.admin_domain_name) + sess = session.Session(auth=auth) + return sess + + +def get_openstack_drivers(keystone_client, region, project_name, + user_name, password): + resources = dict() + # Create Project, User and assign role to new user + project = keystone_client.projects.create(project_name, + CONF.auth.admin_domain_name) + user = keystone_client.users.create(user_name, CONF.auth.admin_domain_name, + project.id, password) + sess = get_current_session(user_name, password, project_name) + # Create new client to form session + new_key_client = ks_client.Client(session=sess) + # Create member role to which we create keypair and Sync + mem_role = [current_role.id for current_role in + keystone_client.roles.list() + if current_role.name == 'Member'][0] + keystone_client.roles.grant(mem_role, user=user, project=project) + token = new_key_client.session.get_token() + nova_client = nv_client.Client(NOVA_API_VERSION, + session=sess, + region_name=region) + resources = {"user_id": user.id, "project_id": project.id, + "session": sess, "token": token, + "nova_version": NOVA_API_VERSION, + "keypairs": KEYPAIRS, + "os_drivers": {'keystone': keystone_client, + 'nova': nova_client}} + + return resources + + +def get_keystone_client(session): + return ks_client.Client(session=session) + + +def get_resource_sync_url_and_headers(token, project_id, api_url): + headers = { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + } + url_string = CONF.kingbird.endpoint_url + CONF.kingbird.api_version + \ + "/" + project_id + api_url + + return headers, url_string + + +def sync_resource(token, project_id, post_body): + body = json.dumps(post_body) + headers, url_string = get_resource_sync_url_and_headers(token, project_id, + resource_sync_url) + response = requests.post(url_string, headers=headers, data=body) + return response + + +def get_sync_job_list(token, project_id, action=None): + headers, url_string = get_resource_sync_url_and_headers(token, project_id, + resource_sync_url) + if action: + url_string = url_string + action + response = requests.get(url_string, headers=headers) + return response + + +def delete_db_entries(token, project_id, job_id): + headers, url_string = get_resource_sync_url_and_headers(token, project_id, + resource_sync_url) + url_string = url_string + job_id + response = requests.delete(url_string, headers=headers) + return response.status_code + + +def get_regions(key_client): + return [current_region.id for current_region in + key_client.regions.list()] + + +def cleanup_resources(openstack_drivers, resource_ids): + keystone_client = openstack_drivers['keystone'] + keystone_client.projects.delete(resource_ids['project_id']) + keystone_client.users.delete(resource_ids['user_id']) + + +def cleanup_keypairs(regions, resource_ids, + current_sess): + for current_region in regions: + for keypair in KEYPAIRS: + nova_client = nv_client.Client(NOVA_API_VERSION, + session=current_sess, + region_name=current_region) + nova_client.keypairs.delete(keypair, + user_id=resource_ids['user_id']) + + +def create_keypairs(openstack_drivers): + nova_client = openstack_drivers['nova'] + resources = dict() + for keypair in KEYPAIRS: + resources[keypair] = nova_client.keypairs.create(keypair) + return resources diff --git a/kingbird/tests/tempest/scenario/resource_management/sync_tests/__init__.py b/kingbird/tests/tempest/scenario/resource_management/sync_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kingbird/tests/tempest/scenario/resource_management/sync_tests/base.py b/kingbird/tests/tempest/scenario/resource_management/sync_tests/base.py new file mode 100644 index 0000000..0344f80 --- /dev/null +++ b/kingbird/tests/tempest/scenario/resource_management/sync_tests/base.py @@ -0,0 +1,98 @@ +# Copyright 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. + +from tempest.lib.common import api_version_utils +from tempest.lib.common.utils import data_utils +import tempest.test + +from kingbird.tests.tempest.scenario.resource_management \ + import resource_sync_client + + +class BaseKingbirdTest(api_version_utils.BaseMicroversionTest, + tempest.test.BaseTestCase): + """Base test case class for all Kingbird Resource sync API tests.""" + + @classmethod + def skip_checks(cls): + super(BaseKingbirdTest, cls).skip_checks() + + def setUp(self): + super(BaseKingbirdTest, self).setUp() + + @classmethod + def setup_credentials(cls): + super(BaseKingbirdTest, cls).setup_credentials() + session = resource_sync_client.get_session() + cls.keystone_client = resource_sync_client.get_keystone_client(session) + cls.regions = resource_sync_client.get_regions(cls.keystone_client) + + @classmethod + def setup_clients(cls): + super(BaseKingbirdTest, cls).setup_clients() + + @classmethod + def create_resources(cls): + # Create Project, User, flavor, subnet & network for test + project_name = data_utils.rand_name('kb-project') + user_name = data_utils.rand_name('kb-user') + password = data_utils.rand_name('kb-password') + cls.openstack_details = resource_sync_client.get_openstack_drivers( + cls.keystone_client, + cls.regions[0], + project_name, + user_name, + password) + cls.openstack_drivers = cls.openstack_details['os_drivers'] + cls.token = cls.openstack_details['token'] + cls.session = cls.openstack_details['session'] + + @classmethod + def resource_cleanup(cls): + super(BaseKingbirdTest, cls).resource_cleanup() + + @classmethod + def create_keypairs(cls): + cls.resource_ids = resource_sync_client.\ + create_keypairs(cls.openstack_drivers) + cls.resource_ids.update(cls.openstack_details) + + @classmethod + def delete_keypairs(cls): + resource_sync_client.cleanup_keypairs(cls.regions, + cls.resource_ids, cls.session) + + @classmethod + def delete_resources(cls): + resource_sync_client.cleanup_resources(cls.openstack_drivers, + cls.resource_ids) + + @classmethod + def sync_keypair(cls, project_id, post_body): + response = resource_sync_client.sync_resource(cls.token, project_id, + post_body) + return response + + @classmethod + def get_sync_list(cls, project_id, action=None): + response = resource_sync_client.get_sync_job_list( + cls.token, project_id, action) + return response + + @classmethod + def delete_db_entries(cls, project_id, job_id): + response = resource_sync_client.delete_db_entries( + cls.token, project_id, job_id) + return response diff --git a/kingbird/tests/tempest/scenario/resource_management/sync_tests/test_keypair_sync_api.py b/kingbird/tests/tempest/scenario/resource_management/sync_tests/test_keypair_sync_api.py new file mode 100644 index 0000000..0adce05 --- /dev/null +++ b/kingbird/tests/tempest/scenario/resource_management/sync_tests/test_keypair_sync_api.py @@ -0,0 +1,234 @@ +# Copyright 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. + +from kingbird.tests.tempest.scenario.resource_management \ + import resource_sync_client +from kingbird.tests.tempest.scenario.resource_management. \ + sync_tests import base +from kingbird.tests.tempest.scenario import consts +from kingbird.tests import utils + +from novaclient import client as nv_client + +FORCE = "True" +DEFAULT_FORCE = "False" + + +class KingbirdKPTest(base.BaseKingbirdTest): + + @classmethod + def setup_clients(self): + super(KingbirdKPTest, self).setup_clients() + + def tearDown(self): + super(KingbirdKPTest, self).tearDown() + + @classmethod + def resource_cleanup(self): + super(KingbirdKPTest, self).resource_cleanup() + self.delete_keypairs() + self.delete_resources() + + @classmethod + def resource_setup(self): + super(KingbirdKPTest, self).resource_setup() + self.create_resources() + self.create_keypairs() + + @classmethod + def setup_credentials(self): + super(KingbirdKPTest, self).setup_credentials() + self.session = resource_sync_client.get_session() + self.key_client = resource_sync_client.\ + get_keystone_client(self.session) + self.regions = resource_sync_client.get_regions(self.key_client) + + def _check_job_status(self): + # Wait until the status of the job is not "IN_PROGRESS" + job_list_resp = self.get_sync_list(self.resource_ids["project_id"]) + status = job_list_resp.json().get('job_set')[0].get('sync_status') + return status != consts.JOB_PROGRESS + + def _sync_job_create(self, force): + body = {"resource_set": {"resource_type": consts.KEYPAIR_RESOURCE_TYPE, + "resources": self.resource_ids["keypairs"], + "source": self.regions[0], "force": force, + "target": self.regions[1:]}} + response = self.sync_keypair(self.resource_ids["project_id"], body) + return response + + def _delete_entries_in_db(self, project, job): + response = self.delete_db_entries(self.resource_ids["project_id"], job) + return response + + def test_keypair_sync(self): + create_response = self._sync_job_create(force=FORCE) + job_id = create_response.json().get('job_status').get('id') + self.assertEqual(create_response.status_code, 200) + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + target_regions = self.regions[1:] + for region in target_regions: + for keypair in self.resource_ids["keypairs"]: + nova_client = nv_client.Client( + self.resource_ids["nova_version"], session=self.session, + region_name=region) + source_keypair = nova_client.keypairs.get( + keypair, self.resource_ids["user_id"]) + self.assertEqual(source_keypair.name, keypair) + # Clean_up the database entries + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + + def test_get_sync_list(self): + create_response = self._sync_job_create(force=FORCE) + self.assertEqual(create_response.status_code, 200) + job_id = create_response.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + job_list_resp = self.get_sync_list(self.resource_ids["project_id"]) + self.assertEqual(job_list_resp.status_code, 200) + # Clean_up the database entries + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + + def test_get_sync_job_details(self): + create_response = self._sync_job_create(force=FORCE) + self.assertEqual(create_response.status_code, 200) + job_id = create_response.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + job_list_resp = self.get_sync_list(self.resource_ids["project_id"], + job_id) + self.assertEqual(job_list_resp.status_code, 200) + self.assertEqual( + job_list_resp.json().get('job_set')[0].get('resource'), + self.resource_ids["keypairs"][0]) + self.assertEqual( + job_list_resp.json().get('job_set')[1].get('resource'), + self.resource_ids["keypairs"][1]) + self.assertEqual( + job_list_resp.json().get('job_set')[0].get('resource_type'), + consts.KEYPAIR_RESOURCE_TYPE) + # Clean_up the database entries + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + + def test_get_active_jobs(self): + create_response = self._sync_job_create(force=FORCE) + self.assertEqual(create_response.status_code, 200) + job_id = create_response.json().get('job_status').get('id') + active_job = self.get_sync_list(self.resource_ids["project_id"], + consts.JOB_ACTIVE) + status = active_job.json().get('job_set')[0].get('sync_status') + self.assertEqual(active_job.status_code, 200) + self.assertEqual(status, consts.JOB_PROGRESS) + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + # Clean_up the database entries + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + + def test_delete_active_jobs(self): + create_response = self._sync_job_create(force=FORCE) + self.assertEqual(create_response.status_code, 200) + job_id = create_response.json().get('job_status').get('id') + response = self._delete_entries_in_db(self.resource_ids["project_id"], + job_id) + # Actual result when we try and delete an active_job + self.assertEqual(response, 406) + # Clean_up the database entries + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + + def test_delete_already_deleted_job(self): + create_response = self._sync_job_create(force=FORCE) + self.assertEqual(create_response.status_code, 200) + job_id = create_response.json().get('job_status').get('id') + # Clean_up the database entries + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id)) + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response, 200) + delete_response2 = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id) + self.assertEqual(delete_response2, 404) + + def test_keypair_sync_with_force_true(self): + create_response_1 = self._sync_job_create(force=FORCE) + self.assertEqual(create_response_1.status_code, 200) + job_id_1 = create_response_1.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id_1)) + delete_response = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id_1) + self.assertEqual(delete_response, 200) + create_response_2 = self._sync_job_create(force=FORCE) + self.assertEqual(create_response_2.status_code, 200) + job_id_2 = create_response_2.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id_2)) + # Clean_up the database entries + delete_response_2 = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id_2) + self.assertEqual(delete_response_2, 200) + + def test_keypair_sync_with_force_false(self): + create_response_1 = self._sync_job_create(force=DEFAULT_FORCE) + self.assertEqual(create_response_1.status_code, 200) + job_id_1 = create_response_1.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id_1)) + delete_response_1 = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id_1) + self.assertEqual(delete_response_1, 200) + create_response_2 = self._sync_job_create(force=DEFAULT_FORCE) + self.assertEqual(create_response_2.status_code, 200) + job_id_2 = create_response_2.json().get('job_status').get('id') + utils.wait_until_true( + lambda: self._check_job_status(), + exception=RuntimeError("Timed out waiting for job %s " % job_id_2)) + job_list_resp = self.get_sync_list(self.resource_ids["project_id"], + job_id_2) + self.assertEqual(job_list_resp.status_code, 200) + # This job fail because resoruce is already created. + # We can use force to recreate that resource. + self.assertEqual( + job_list_resp.json().get('job_set')[0].get('sync_status'), + consts.JOB_FAILURE) + self.assertEqual( + job_list_resp.json().get('job_set')[1].get('sync_status'), + consts.JOB_FAILURE) + # Clean_up the database entries + delete_response_2 = self._delete_entries_in_db( + self.resource_ids["project_id"], job_id_2) + self.assertEqual(delete_response_2, 200) diff --git a/kingbird/tests/utils.py b/kingbird/tests/utils.py index d447d74..c756812 100644 --- a/kingbird/tests/utils.py +++ b/kingbird/tests/utils.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import eventlet import random import sqlalchemy import string @@ -89,3 +90,9 @@ def dummy_context(user='test_username', tenant='test_project_id', 'is_admin': True, 'region_name': region_name }) + + +def wait_until_true(predicate, timeout=60, sleep=1, exception=None): + with eventlet.timeout.Timeout(timeout, exception): + while not predicate(): + eventlet.sleep(sleep)