# 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 copy from mock import patch from oslo_config import cfg from oslo_config import fixture as fixture_config from oslo_utils import timeutils from oslo_utils import uuidutils from six.moves import xrange import pecan from pecan.configuration import set_config from pecan.testing import load_test_app from tricircle.api import app from tricircle.common import constants from tricircle.common import context from tricircle.common import policy from tricircle.common import xrpcapi from tricircle.db import api as db_api from tricircle.db import core from tricircle.db import models from tricircle.tests import base OPT_GROUP_NAME = 'keystone_authtoken' cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token") def fake_admin_context(): context_paras = {'is_admin': True} return context.Context(**context_paras) def fake_non_admin_context(): context_paras = {} return context.Context(**context_paras) class API_FunctionalTest(base.TestCase): def setUp(self): super(API_FunctionalTest, self).setUp() self.addCleanup(set_config, {}, overwrite=True) cfg.CONF.clear() cfg.CONF.register_opts(app.common_opts) self.CONF = self.useFixture(fixture_config.Config()).conf self.CONF.set_override('auth_strategy', 'noauth') self.CONF.set_override('tricircle_db_connection', 'sqlite://') core.initialize() core.ModelBase.metadata.create_all(core.get_engine()) self.context = context.get_admin_context() policy.populate_default_rules() self.app = self._make_app() def _make_app(self, enable_acl=False): self.config = { 'app': { 'root': 'tricircle.api.controllers.root.RootController', 'modules': ['tricircle.api'], 'enable_acl': enable_acl, }, } return load_test_app(self.config) def tearDown(self): super(API_FunctionalTest, self).tearDown() cfg.CONF.unregister_opts(app.common_opts) pecan.set_config({}, overwrite=True) core.ModelBase.metadata.drop_all(core.get_engine()) policy.reset() class TestAsyncJobController(API_FunctionalTest): """Test version listing on root URI.""" def setUp(self): super(TestAsyncJobController, self).setUp() self.job_resource_map = constants.job_resource_map self.all_job_types = list(self.job_resource_map.keys()) def fake_new_job(context, project_id, type, resource_id): raise Exception def fake_invoke_method(self, context, project_id, method, type, id): db_api.new_job(context, project_id, type, id) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) def test_post_no_input(self): job = self._prepare_job_element(constants.JT_CONFIGURE_ROUTE) jobs = [ # missing job { "job_xxx": job, "expected_error": 400 }, ] for test_job in jobs: response = self.app.post_json( '/v1.0/jobs', dict(job_xxx=test_job['job_xxx']), expect_errors=True) self.assertEqual(response.status_int, test_job['expected_error']) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) @patch.object(db_api, 'new_job', new=fake_new_job) def test_post_exception(self): job = self._prepare_job_element(constants.JT_CONFIGURE_ROUTE) jobs = [ { "job": job, "expected_error": 500 }, ] self._test_and_check(jobs) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) def test_post_invalid_input(self): for job_type in self.all_job_types: job = self._prepare_job_element(job_type) # wrong job type parameter: no job type is provided job_1 = copy.deepcopy(job) job_1.pop('type') # wrong job type parameter: job type is empty job_2 = copy.deepcopy(job) job_2['type'] = '' # wrong job type parameter: job type is wrong job_3 = copy.deepcopy(job) job_3['type'] = job['type'] + '_1' # wrong resource parameter: no resource is provided job_4 = copy.deepcopy(job) job_4.pop('resource') # wrong resource parameter: lack of necessary resource job_5 = copy.deepcopy(job) job_5['resource'].popitem() # wrong resource parameter: redundant resource job_6 = copy.deepcopy(job) job_6['resource']['fake_resource'] = 'fake_resource' # wrong project id parameter: no project id is provided job_7 = copy.deepcopy(job) job_7.pop('project_id') # wrong project id parameter: project id is empty job_8 = copy.deepcopy(job) job_8['project_id'] = '' # wrong project id parameter: project is not the # owner of resource job_9 = copy.deepcopy(job) job_9['project_id'] = uuidutils.generate_uuid() jobs = [ { "job": job_1, "expected_error": 400 }, { "job": job_2, "expected_error": 400 }, { "job": job_3, "expected_error": 400 }, { "job": job_4, "expected_error": 400 }, { "job": job_5, "expected_error": 400 }, { "job": job_6, "expected_error": 400 }, { "job": job_7, "expected_error": 400 }, { "job": job_8, "expected_error": 400 }, { "job": job_9, "expected_error": 400 }, ] self._test_and_check(jobs) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) @patch.object(xrpcapi.XJobAPI, 'invoke_method', new=fake_invoke_method) def test_post_job(self): for job_type in self.all_job_types: job = self._prepare_job_element(job_type) jobs = [ # create an entirely new job { "job": job, "expected_error": 200 }, # target job already exists in the job table and its status # is NEW, then this newer job will be picked by job handler. { "job": job, "expected_error": 200 }, ] self._test_and_check(jobs) @patch.object(xrpcapi.XJobAPI, 'invoke_method', new=fake_invoke_method) @patch.object(context, 'extract_context_from_environ') def test_get_one_and_get_all(self, mock_context): self.context.project_id = "fake_project_id" mock_context.return_value = self.context all_job_ids = {} all_job_project_ids = {} index = 0 for job_type in self.all_job_types: if index == 0: # the first job has a project ID that differs from # context.project_id job = self._prepare_job_element(job_type) else: job = self._prepare_job_element(job_type, self.context.project_id) job = {"job": job, "expected_error": 200} back_jobid = self._test_and_obtain_id(job) all_job_ids[index] = back_jobid all_job_project_ids[job_type] = job['job']['project_id'] index = index + 1 service_uris = ['jobs', 'jobs/detail'] amount_of_all_jobs = len(self.all_job_types) # with no filters all jobs are returned for service_uri in service_uris: response_1 = self.app.get('/v1.0/%(service_uri)s' % { 'service_uri': service_uri}) return_jobs_1 = response_1.json self.assertEqual(amount_of_all_jobs - 1, len(return_jobs_1['jobs'])) self.assertIn('status', response_1) self.assertIn('resource', response_1) self.assertIn('project_id', response_1) self.assertIn('id', response_1) self.assertIn('timestamp', response_1) self.assertIn('type', response_1) self.assertNotIn('extra_id', response_1) self.assertNotIn('resource_id', response_1) # use job status filter response_2 = self.app.get('/v1.0/jobs?status=new') return_jobs_2 = response_2.json self.assertEqual(amount_of_all_jobs - 1, len(return_jobs_2['jobs'])) response = self.app.get('/v1.0/jobs?status=fail') return_jobs_3 = response.json self.assertEqual(0, len(return_jobs_3['jobs'])) amount_of_fail_jobs = int(amount_of_all_jobs / 3) for i in xrange(amount_of_fail_jobs): db_api.finish_job(self.context, all_job_ids[i], False, timeutils.utcnow()) amount_of_succ_jobs = int(amount_of_all_jobs / 3) for i in xrange(amount_of_succ_jobs): db_api.finish_job(self.context, all_job_ids[amount_of_fail_jobs + i], True, timeutils.utcnow()) for service_uri in service_uris: response = self.app.get('/v1.0/%(service_uri)s?status=fail' % { 'service_uri': service_uri}) return_jobs = response.json self.assertEqual(amount_of_fail_jobs - 1, len(return_jobs['jobs'])) response = self.app.get('/v1.0/%(service_uri)s?status=success' '' % {'service_uri': service_uri}) return_jobs = response.json self.assertEqual(amount_of_succ_jobs, len(return_jobs['jobs'])) # project ID filter in URL query string will be ignored, and # only the project ID in which the user is authorized will # be used as filter. response = self.app.get( '/v1.0/%(service_uri)s' % {'service_uri': service_uri}) return_job = response.json response1 = self.app.get( '/v1.0/%(service_uri)s?project_id=%(project_id)s' % { 'service_uri': service_uri, 'project_id': uuidutils.generate_uuid()}) return_job1 = response1.json response2 = self.app.get( '/v1.0/%(service_uri)s?project_id=%(project_id)s' % { 'service_uri': service_uri, 'project_id': 'fake_project_id'}) return_job2 = response2.json self.assertEqual(len(return_job2['jobs']), len(return_job1['jobs'])) self.assertEqual(len(return_job['jobs']), len(return_job2['jobs'])) # use job type filter count = 1 for job_type in self.all_job_types: response = self.app.get('/v1.0/%(service_uri)s?type=%(type)s' '' % {'service_uri': service_uri, 'type': job_type}) return_job = response.json if count == 1: self.assertEqual(0, len(return_job['jobs'])) else: self.assertEqual(1, len(return_job['jobs'])) count += 1 # combine job type and job status filter for i in xrange(1, amount_of_all_jobs): if i < amount_of_fail_jobs: # this aims to test service "/v1.0/jobs/{id}" response_1 = self.app.get('/v1.0/jobs/%(id)s' % { 'id': all_job_ids[i]}) return_job_1 = response_1.json response_2 = self.app.get( '/v1.0/%(service_uri)s?' 'type=%(type)s&' 'status=%(status)s' % { 'service_uri': service_uri, 'type': return_job_1['job']['type'], 'status': 'fail'}) return_job_2 = response_2.json self.assertEqual(1, len(return_job_2['jobs'])) elif ((i >= amount_of_fail_jobs ) and (i < amount_of_fail_jobs + amount_of_succ_jobs)): # those jobs are set to 'success' and they are moved to # job log. their job ids are not stored in all_job_ids job_type = self.all_job_types[i] response = self.app.get( '/v1.0/%(service_uri)s?' 'type=%(type)s&status=%(status)s' % { 'service_uri': service_uri, 'type': job_type, 'status': 'success'}) return_job = response.json self.assertEqual(1, len(return_job['jobs'])) response_2 = self.app.get( '/v1.0/%(service_uri)s?status=%(status)s' '&type=%(type)s' % { 'service_uri': service_uri, 'status': "success-x", 'type': job_type}) return_job_2 = response_2.json self.assertEqual(0, len(return_job_2['jobs'])) else: response_1 = self.app.get('/v1.0/jobs/%(id)s' % { 'id': all_job_ids[i]}) return_job_1 = response_1.json response_2 = self.app.get( '/v1.0/%(service_uri)s?' 'type=%(type)s&status=%(status)s' % { 'service_uri': service_uri, 'type': return_job_1['job']['type'], 'status': 'new'}) return_job_2 = response_2.json self.assertEqual(1, len(return_job_2['jobs'])) response_3 = self.app.get( '/v1.0/%(service_uri)s?status=%(status)s' '&type=%(type)s' % { 'service_uri': service_uri, 'status': "new-x", 'type': return_job_1['job']['type']}) return_job_3 = response_3.json self.assertEqual(0, len(return_job_3['jobs'])) # use unsupported filter, it will raise 400 error response = self.app.get('/v1.0/%(service_uri)s?' 'fake_filter=%(fake_filter)s' '' % {'service_uri': service_uri, 'fake_filter': "fake_filter"}, expect_errors=True) self.assertEqual(response.status_int, 400) # use invalid filter, it will return empty set response = self.app.get('/v1.0/%(service_uri)s?status=%(status)s' '' % {'service_uri': service_uri, 'status': "new-x"}) return_job = response.json self.assertEqual(0, len(return_job['jobs'])) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) def test_get_job_schemas(self): response = self.app.get('/v1.0/jobs/schemas') return_job_schemas = response.json job_schemas = [] for job_type in self.all_job_types: job = {} resource = [] for resource_type, resource_id in ( self.job_resource_map[job_type]): resource.append(resource_id) job['resource'] = resource job['type'] = job_type job_schemas.append(job) self.assertEqual(job_schemas, return_job_schemas['schemas']) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) @patch.object(xrpcapi.XJobAPI, 'invoke_method', new=fake_invoke_method) def test_delete_job(self): for job_type in self.all_job_types: job = self._prepare_job_element(job_type) jobs = [ { "job": job, "expected_error": 200 }, ] self._test_and_check(jobs) response = self.app.get('/v1.0/jobs') return_job = response.json jobs = return_job['jobs'] # delete a new job for job in jobs: response_1 = self.app.delete( '/v1.0/jobs/%(id)s' % {'id': job['id']}, expect_errors=True) return_value_1 = response_1.json self.assertEqual(response_1.status_int, 200) self.assertEqual(return_value_1, {}) response_2 = self.app.get('/v1.0/jobs') return_job_2 = response_2.json self.assertEqual(0, len(return_job_2['jobs'])) response_3 = self.app.delete('/v1.0/jobs/123', expect_errors=True) self.assertEqual(response_3.status_int, 404) # delete a running job job_type_4 = constants.JT_NETWORK_UPDATE job_4 = self._prepare_job_element(job_type_4) resource_id_4 = '#'.join([job_4['resource'][resource_id] for resource_type, resource_id in self.job_resource_map[job_type_4]]) job_running_4 = db_api.register_job(self.context, job_4['project_id'], job_type_4, resource_id_4) self.assertEqual(constants.JS_Running, job_running_4['status']) response_4 = self.app.delete('/v1.0/jobs/%(id)s' % { 'id': job_running_4['id']}, expect_errors=True) self.assertEqual(response_4.status_int, 400) # delete a failed job job_type_5 = constants.JT_NETWORK_UPDATE job_5 = self._prepare_job_element(job_type_5) job_dict_5 = { "job": job_5, "expected_error": 200 } response_5 = self.app.post_json('/v1.0/jobs', dict(job=job_dict_5['job']), expect_errors=True) return_job_5 = response_5.json self.assertEqual(response_5.status_int, 200) db_api.finish_job(self.context, return_job_5['job']['id'], False, timeutils.utcnow()) job_fail_5 = db_api.get_job(self.context, return_job_5['job']['id']) self.assertEqual(constants.JS_Fail, job_fail_5['status']) response_6 = self.app.delete('/v1.0/jobs/%(id)s' % { 'id': return_job_5['job']['id']}, expect_errors=True) self.assertEqual(response_6.status_int, 200) # delete a successful job job_type_6 = constants.JT_NETWORK_UPDATE job_6 = self._prepare_job_element(job_type_6) job_dict_6 = { "job": job_6, "expected_error": 200 } response_6 = self.app.post_json('/v1.0/jobs', dict(job=job_dict_6['job']), expect_errors=True) return_job_6 = response_6.json with self.context.session.begin(): job_dict = {'status': constants.JS_Success, 'timestamp': timeutils.utcnow(), 'extra_id': uuidutils.generate_uuid()} core.update_resource(self.context, models.AsyncJob, return_job_6['job']['id'], job_dict) job_succ_6 = db_api.get_job(self.context, return_job_6['job']['id']) self.assertEqual(constants.JS_Success, job_succ_6['status']) response_7 = self.app.delete('/v1.0/jobs/%(id)s' % { 'id': return_job_6['job']['id']}, expect_errors=True) self.assertEqual(response_7.status_int, 200) @patch.object(context, 'extract_context_from_environ', new=fake_admin_context) @patch.object(xrpcapi.XJobAPI, 'invoke_method', new=fake_invoke_method) def test_redo_job(self): for job_type in self.all_job_types: job = self._prepare_job_element(job_type) jobs = [ # create an entirely new job { "job": job, "expected_error": 200 }, ] self._test_and_check(jobs) response = self.app.get('/v1.0/jobs') return_job = response.json jobs = return_job['jobs'] # redo a new job for job in jobs: response_1 = self.app.put('/v1.0/jobs/%(id)s' % {'id': job['id']}, expect_errors=True) self.assertEqual(response_1.status_int, 200) response_2 = self.app.put('/v1.0/jobs/123', expect_errors=True) self.assertEqual(response_2.status_int, 404) # redo a running job job_type_3 = constants.JT_NETWORK_UPDATE job_3 = self._prepare_job_element(job_type_3) resource_id_3 = '#'.join([job_3['resource'][resource_id] for resource_type, resource_id in self.job_resource_map[job_type_3]]) job_running_3 = db_api.register_job(self.context, job_3['project_id'], job_type_3, resource_id_3) self.assertEqual(constants.JS_Running, job_running_3['status']) response_3 = self.app.put('/v1.0/jobs/%(id)s' % { 'id': job_running_3['id']}, expect_errors=True) self.assertEqual(response_3.status_int, 400) # redo a failed job job_type_4 = constants.JT_NETWORK_UPDATE job_4 = self._prepare_job_element(job_type_4) job_dict_4 = { "job": job_4, "expected_error": 200 } response_4 = self.app.post_json('/v1.0/jobs', dict(job=job_dict_4['job']), expect_errors=True) return_job_4 = response_4.json self.assertEqual(response_4.status_int, 200) db_api.finish_job(self.context, return_job_4['job']['id'], False, timeutils.utcnow()) job_fail_4 = db_api.get_job(self.context, return_job_4['job']['id']) self.assertEqual(constants.JS_Fail, job_fail_4['status']) response_5 = self.app.put('/v1.0/jobs/%(id)s' % { 'id': return_job_4['job']['id']}, expect_errors=True) self.assertEqual(response_5.status_int, 200) # redo a successful job job_type_6 = constants.JT_NETWORK_UPDATE job_6 = self._prepare_job_element(job_type_6) job_dict_6 = { "job": job_6, "expected_error": 200 } response_6 = self.app.post_json('/v1.0/jobs', dict(job=job_dict_6['job']), expect_errors=True) return_job_6 = response_6.json with self.context.session.begin(): job_dict = {'status': constants.JS_Success, 'timestamp': timeutils.utcnow(), 'extra_id': uuidutils.generate_uuid()} core.update_resource(self.context, models.AsyncJob, return_job_6['job']['id'], job_dict) job_succ_6 = db_api.get_job(self.context, return_job_6['job']['id']) self.assertEqual(constants.JS_Success, job_succ_6['status']) response_7 = self.app.put('/v1.0/jobs/%(id)s' % { 'id': return_job_6['job']['id']}, expect_errors=True) self.assertEqual(response_7.status_int, 400) @patch.object(context, 'extract_context_from_environ', new=fake_non_admin_context) def test_non_admin_action(self): job_type = constants.JT_NETWORK_UPDATE job = self._prepare_job_element(job_type) jobs = [ { "job": job, "expected_error": 403 }, ] self._test_and_check(jobs) response_1 = self.app.get('/v1.0/jobs/1234567890', expect_errors=True) self.assertEqual(response_1.status_int, 403) response_2 = self.app.get('/v1.0/jobs', expect_errors=True) self.assertEqual(response_2.status_int, 403) response_3 = self.app.delete('/v1.0/jobs/1234567890', expect_errors=True) self.assertEqual(response_3.status_int, 403) response_4 = self.app.put('/v1.0/jobs/1234567890', expect_errors=True) self.assertEqual(response_4.status_int, 403) def _test_and_check(self, jobs): for test_job in jobs: response = self.app.post_json( '/v1.0/jobs', dict(job=test_job['job']), expect_errors=True) self.assertEqual(response.status_int, test_job['expected_error']) def _test_and_obtain_id(self, job): response = self.app.post_json( '/v1.0/jobs', dict(job=job['job']), expect_errors=True) self.assertEqual(response.status_int, job['expected_error']) back_job = response.json return back_job['job']['id'] def _prepare_job_element(self, job_type, project_id=None): # in order to create a job, we need three elements: job type, # job resource and project id. If project_id parameter is not # None then we create resource and job for that project, # or else we create resource and job for an entirely new project. if project_id is None: project_id = uuidutils.generate_uuid() job = {} job['resource'] = {} job['type'] = job_type # these two jobs need no resource routings. We only need to ensure # that job['resource']['project_id'] equals to job['project_id'], which # keeps consistent with job_primary_resource_map in common/constant.py if job_type in (constants.JT_SEG_RULE_SETUP, constants.JT_RESOURCE_RECYCLE): job['resource']['project_id'] = project_id else: for resource_type, resource_id in self.job_resource_map[job_type]: job['resource'][resource_id] = uuidutils.generate_uuid() self._create_resource_for_project(job, project_id) job['project_id'] = project_id return job def _create_resource_for_project(self, job, project_id): # create resource for project ${project_id} pod_id = uuidutils.generate_uuid() resource_type, resource_id = ( constants.job_primary_resource_map[job['type']]) routing = db_api.create_resource_mapping( self.context, job['resource'][resource_id], job['resource'][resource_id], pod_id, project_id, resource_type) self.assertIsNotNone(routing) def _validate_error_code(self, res, code): self.assertEqual(res[list(res.keys())[0]]['code'], code)