421 lines
18 KiB
Python
421 lines
18 KiB
Python
# Copyright 2016 ATT
|
|
#
|
|
# 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 itertools
|
|
import os
|
|
|
|
import mock
|
|
from oslo_config import cfg
|
|
|
|
from ord.common import exceptions as exc
|
|
from ord.common import utils
|
|
from ord.engine import workerfactory
|
|
from ord.tests import base
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
# FIXME: pep8 compatible - camelcase attributes
|
|
class TestWorkerThread(base.BaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestWorkerThread, self).setUp()
|
|
|
|
self.operation = utils.OPERATION_CREATE
|
|
self.path_to_tempate = 'test_path'
|
|
self.stack_name = 'test_stack'
|
|
self.template_status_id = '1'
|
|
self.resource_type = 'image'
|
|
self.template_type = 'hot'
|
|
self.threadId = 123
|
|
self.local_repo = 'aic-orm-resources-labs'
|
|
|
|
self._temp_repo_client = mock.Mock()
|
|
self._temp_repo_client.pull_template.return_value = self.pull_client\
|
|
= mock.Mock()
|
|
self.patch('ord.engine.workerfactory.getrepo').return_value\
|
|
= self._temp_repo_client
|
|
|
|
self.db_api = mock.Mock()
|
|
self.db_api.update_target_data.return_value = self.db_client\
|
|
= mock.Mock()
|
|
self.patch('ord.db.sqlalchemy.api').return_value\
|
|
= self.db_api
|
|
|
|
self.WorkerFactory = mock.Mock()
|
|
self.WorkerFactory.removeWorker.return_value = self.remove_clinet\
|
|
= mock.Mock()
|
|
self.patch('ord.engine.workerfactory.WorkerFactory').return_value\
|
|
= self.WorkerFactory
|
|
|
|
self.workerThread = workerfactory.WorkerThread(
|
|
self.threadId, self.operation, self.path_to_tempate,
|
|
self.stack_name, self.template_status_id,
|
|
self.resource_type)
|
|
self.workerThread._heat_client = self.heat_client = mock.Mock()
|
|
self.workerThread._temp_repo_client = self._temp_repo_client
|
|
self.workerThread.db_api = self.db_api
|
|
|
|
def test_extract_resource_extra_metadata(self):
|
|
stack = base.Dummy(id='1', stack_name=self.stack_name)
|
|
image_data = {'checksum': 'dae557b1365b606e57fbd5d8c9d4516a',
|
|
'size': '10',
|
|
'virtual_size': '12'}
|
|
input_payload = {'rds-listener':
|
|
{'request-id': '2',
|
|
'resource-id': '1',
|
|
'resource-operation': 'create',
|
|
'resource-type': 'image'}
|
|
}
|
|
output_payload = {'rds-listener':
|
|
{'request-id': '2',
|
|
'resource-id': '1',
|
|
'resource-operation': 'create',
|
|
'resource-type': 'image',
|
|
'resource_extra_metadata':
|
|
{'checksum': 'dae557b1365b606e57fbd5d8c9d4516a',
|
|
'size': '10',
|
|
'virtual_size': '12'}}}
|
|
|
|
self.heat_client.get_stack_by_name.return_value = stack
|
|
self.heat_client.get_image_data_by_stackid.return_value = image_data
|
|
self.workerThread.extract_resource_extra_metadata(
|
|
input_payload, utils.STATUS_SUCCESS)
|
|
|
|
self.heat_client.get_stack_by_name.assert_called_once_with(
|
|
stack.stack_name)
|
|
self.heat_client.\
|
|
get_image_data_by_stackid.assert_called_once_with(stack.id)
|
|
self.assertEqual(output_payload, input_payload)
|
|
|
|
def test_fetch_template(self):
|
|
self.workerThread._fetch_template()
|
|
self._temp_repo_client.pull_template\
|
|
.assert_called_with(self.local_repo, self.path_to_tempate)
|
|
|
|
def test_create_stack(self):
|
|
self.heat_client.create_stack.return_value = {'stack': {'id': 1}}
|
|
template = os.path.join(
|
|
os.path.expanduser('~'), self.local_repo, self.path_to_tempate)
|
|
|
|
self.workerThread._create_stack(template)
|
|
|
|
self.heat_client.create_stack.assert_called_once_with(
|
|
self.stack_name, template)
|
|
|
|
def test_update_stack(self):
|
|
stack = base.Dummy(id='1', stack_name=self.stack_name)
|
|
template = os.path.join(
|
|
os.path.expanduser('~'), self.local_repo, self.path_to_tempate)
|
|
|
|
self.heat_client.get_stack_by_name.return_value = stack
|
|
|
|
self.workerThread._update_stack(template)
|
|
|
|
self.heat_client.get_stack_by_name.assert_called_once_with(
|
|
self.stack_name)
|
|
self.heat_client.update_stack.\
|
|
assert_called_with(stack.id, template)
|
|
|
|
def test_delete_stack(self):
|
|
stack = base.Dummy(id='1', stack_name=self.stack_name)
|
|
self.heat_client.get_stack_by_name.return_value = stack
|
|
|
|
self.workerThread._delete_stack()
|
|
|
|
self.heat_client.get_stack_by_name.assert_called_once_with(
|
|
stack.stack_name)
|
|
self.heat_client.delete_stack.assert_called_once_with(stack.id)
|
|
|
|
def test_wait_for_heat(self):
|
|
time_time = self.patch('time.time', side_effect=itertools.count(1))
|
|
time_sleep = self.patch('time.sleep')
|
|
|
|
stack_wait = base.Dummy(
|
|
id='1', stack_name=self.stack_name,
|
|
stack_status='CREATE_IN_PROGRESS')
|
|
stack_ready = base.Dummy(
|
|
id='1', stack_name=self.stack_name, stack_status='CREATE_COMPLETE')
|
|
status_responses = [stack_wait] * 4 + [stack_ready]
|
|
|
|
self.heat_client.get_stack.side_effect = status_responses
|
|
|
|
# raise exception in case of failure
|
|
self.workerThread._wait_for_heat(stack_wait, utils.OPERATION_CREATE)
|
|
|
|
self.assertEqual(
|
|
[mock.call(CONF.heat_poll_interval)] * 5,
|
|
time_sleep.mock_calls)
|
|
self.assertEqual(6, time_time.call_count)
|
|
|
|
def test_wait_for_heat_fail(self):
|
|
self.patch('time.time', side_effect=itertools.count(1))
|
|
self.patch('time.sleep')
|
|
|
|
stack_wait = base.Dummy(
|
|
id='1', stack_name=self.stack_name,
|
|
stack_status='CREATE_IN_PROGRESS')
|
|
stack_ready = base.Dummy(
|
|
id='1', stack_name=self.stack_name, stack_status='CREATE_FAILED',
|
|
stack_status_reason='Stack fail due to resource creation')
|
|
|
|
status_responses = [stack_wait] * 4 + [stack_ready]
|
|
|
|
self.heat_client.get_stack.side_effect = status_responses
|
|
|
|
self.assertRaises(
|
|
exc.HEATStackCreateError, self.workerThread._wait_for_heat,
|
|
stack_wait, utils.OPERATION_CREATE)
|
|
|
|
def test_wait_for_heat_race(self):
|
|
self.patch('time.time', side_effect=itertools.count(1))
|
|
self.patch('time.sleep')
|
|
|
|
stack_initial = base.Dummy(
|
|
id='1', stack_name=self.stack_name, stack_status='UPDATE_COMPLETE',
|
|
updated_time='2016-06-02T16:30:48Z')
|
|
stack_wait = base.Dummy(
|
|
id='1', stack_name=self.stack_name,
|
|
stack_status='UPDATE_IN_PROGRESS',
|
|
updated_time='2016-06-02T16:30:48Z')
|
|
stack_ready = base.Dummy(
|
|
id='1', stack_name=self.stack_name, stack_status='UPDATE_COMPLETE',
|
|
updated_time='2016-06-02T16:30:50Z')
|
|
status_responses = [stack_initial]
|
|
status_responses += [stack_wait] * 2
|
|
status_responses += [stack_ready]
|
|
|
|
status_transition = workerfactory.StatusTransitions('_unittest_')
|
|
self.patch(
|
|
'ord.engine.workerfactory.StatusTransitions',
|
|
return_value=status_transition)
|
|
|
|
self.heat_client.get_stack.side_effect = status_responses
|
|
|
|
self.workerThread._wait_for_heat(stack_initial, utils.OPERATION_MODIFY)
|
|
|
|
self.assertEqual('UPDATE_COMPLETE', status_transition.transitions[-1])
|
|
|
|
def test_run(self):
|
|
self.workerThread._fetch_template = fetch_template = mock.Mock()
|
|
template_absolute_path = os.path.join(
|
|
os.path.expanduser('~'), self.local_repo, self.path_to_tempate)
|
|
fetch_template.return_value = template_absolute_path
|
|
self.workerThread._execute_operation = execute = mock.Mock()
|
|
execute.return_value = 'OPERATION_STATUS'
|
|
self.workerThread._update_permanent_storage = \
|
|
save_results = mock.Mock()
|
|
self.workerThread._cleanup_template = mock.Mock()
|
|
self.workerThread._send_operation_results = send_results = mock.Mock()
|
|
|
|
self.workerThread.run()
|
|
fetch_template.assert_called_once_with()
|
|
execute.assert_called_with(template_absolute_path)
|
|
save_results.assert_called_once_with()
|
|
send_results.assert_called_once_with()
|
|
|
|
def test_run_fail(self):
|
|
self.workerThread._fetch_template = fetch_template = mock.Mock()
|
|
template_absolute_path = os.path.join(
|
|
os.path.expanduser('~'), self.local_repo, self.path_to_tempate)
|
|
fetch_template.return_value = template_absolute_path
|
|
error = exc.StackOperationError(operation='unittest', stack='dummy')
|
|
|
|
self.workerThread._execute_operation = execute = mock.Mock(
|
|
side_effect=error)
|
|
self.workerThread._update_permanent_storage = save_status = mock.Mock()
|
|
self.workerThread._send_operation_results = send_results = mock.Mock()
|
|
self.workerThread._cleanup_template = mock.Mock()
|
|
|
|
self.workerThread.run()
|
|
|
|
fetch_template.assert_called_once_with()
|
|
execute.assert_called_with(template_absolute_path)
|
|
save_status.assert_called_once_with(error)
|
|
send_results.assert_called_once_with()
|
|
|
|
def test_run_fail_uncontrolled(self):
|
|
error = ZeroDivisionError()
|
|
|
|
self.workerThread._fetch_template = fetch_template = mock.Mock()
|
|
template_absolute_path = os.path.join(
|
|
os.path.expanduser('~'), self.local_repo, self.path_to_tempate)
|
|
fetch_template.return_value = template_absolute_path
|
|
self.workerThread._execute_operation = execute = mock.Mock(
|
|
side_effect=error)
|
|
self.workerThread._update_permanent_storage = save_status = mock.Mock()
|
|
self.workerThread._send_operation_results = send_results = mock.Mock()
|
|
self.workerThread._cleanup_template = mock.Mock()
|
|
|
|
self.workerThread.run()
|
|
|
|
fetch_template.assert_called_once_with()
|
|
execute.assert_called_with(template_absolute_path)
|
|
|
|
def test_update_permanent_storage(self):
|
|
db_api = self.patch('ord.engine.workerfactory.db_api')
|
|
|
|
self.workerThread._update_permanent_storage()
|
|
db_api.update_target_data.assert_called_once_with(
|
|
self.template_status_id, utils.STATUS_SUCCESS,
|
|
error_code=exc.SUCCESS_CODE)
|
|
|
|
def test_update_permanent_storage_error(self):
|
|
db_api = self.patch('ord.engine.workerfactory.db_api')
|
|
|
|
generic_error = ZeroDivisionError()
|
|
ord_error = exc.IntegrationError('unit-test')
|
|
stack_error = exc.StackOperationError(
|
|
stack='ord-stack-error-without-rollback', operation='unit-test')
|
|
stack_error_rollback = exc.StackOperationError(
|
|
stack='ord-stack-error-with-rollback',
|
|
operation=utils.OPERATION_CREATE, rollback_status=True)
|
|
stack_error_rollback_fail0 = exc.StackOperationError(
|
|
stack='ord-stack-error-with-rollback-fail',
|
|
operation=utils.OPERATION_CREATE, rollback_status=False)
|
|
stack_error_rollback_fail1 = exc.StackOperationError(
|
|
stack='ord-stack-error-with-rollback-fail-and-message',
|
|
operation=utils.OPERATION_CREATE, rollback_status=False,
|
|
rollback_message='a\nbb\nccc')
|
|
|
|
for error, status, error_code, error_message in (
|
|
(generic_error, utils.STATUS_INTERNAL_ERROR,
|
|
exc.ERROR_UNKNOWN_EXCEPTION, str(generic_error)),
|
|
(ord_error, utils.STATUS_INTERNAL_ERROR,
|
|
ord_error.error_code, ord_error.message),
|
|
(stack_error, utils.STATUS_ERROR,
|
|
stack_error.error_code, stack_error.message),
|
|
(stack_error_rollback, utils.STATUS_ERROR,
|
|
stack_error_rollback.error_code,
|
|
'{}\n[ROLLBACK] success'.format(
|
|
stack_error_rollback.message)),
|
|
(stack_error_rollback_fail0, utils.STATUS_ERROR,
|
|
stack_error_rollback_fail0.error_code,
|
|
'{}\n[ROLLBACK] fail'.format(
|
|
stack_error_rollback_fail0.message)),
|
|
(stack_error_rollback_fail1, utils.STATUS_ERROR,
|
|
stack_error_rollback_fail1.error_code,
|
|
'{}\n[ROLLBACK] a\n[ROLLBACK] bb\n[ROLLBACK] ccc'.format(
|
|
stack_error_rollback_fail1.message))):
|
|
self.workerThread._update_permanent_storage(error)
|
|
|
|
db_api.update_target_data.assert_called_once_with(
|
|
self.template_status_id, status,
|
|
error_code=error_code, error_msg=error_message)
|
|
db_api.update_target_data.reset_mock()
|
|
|
|
|
|
class TestStatusTransitions(base.BaseTestCase):
|
|
def test(self):
|
|
for data, expect in [
|
|
('A', 'A'),
|
|
('AA', 'A(2)'),
|
|
('ABC', 'A ~> B ~> C'),
|
|
('AABBCC', 'A(2) ~> B(2) ~> C(2)')]:
|
|
subject = workerfactory.StatusTransitions(data[0])
|
|
for entity in data[1:]:
|
|
subject.add(entity)
|
|
self.assertEqual(expect, str(subject))
|
|
|
|
|
|
class TestHEATIntermediateStatusChecker(base.BaseTestCase):
|
|
def test_scenario(self):
|
|
cls = workerfactory.HEATIntermediateStatusChecker
|
|
|
|
scenario_create = [
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_COMPLETE)))]
|
|
|
|
scenario_create_fail = [
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_FAIL)))]
|
|
scenario_delete = [
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_COMPLETE))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_DELETE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_DELETE, cls.STATUS_COMPLETE)))]
|
|
scenario_delete_fail = [
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_COMPLETE))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_DELETE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_DELETE, cls.STATUS_FAIL)))]
|
|
scenario_update = [
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_CREATE, cls.STATUS_COMPLETE))),
|
|
base.Dummy(
|
|
updated_time=None, stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time='2016-06-02T16:30:00Z', stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_COMPLETE)))]
|
|
scenario_update_update = [
|
|
base.Dummy(
|
|
updated_time='2016-06-02T16:30:00Z', stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_COMPLETE))),
|
|
base.Dummy(
|
|
updated_time='2016-06-02T16:30:00Z', stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_COMPLETE))),
|
|
base.Dummy(
|
|
updated_time='2016-06-02T16:30:00Z', stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_IN_PROGRESS))),
|
|
base.Dummy(
|
|
updated_time='2016-06-02T16:30:01Z', stack_status='_'.join((
|
|
cls.ACTION_UPDATE, cls.STATUS_COMPLETE)))]
|
|
|
|
for scenario, operation, is_fail in (
|
|
(scenario_create, utils.OPERATION_CREATE, False),
|
|
(scenario_create_fail, utils.OPERATION_CREATE, True),
|
|
(scenario_delete, utils.OPERATION_DELETE, False),
|
|
(scenario_delete_fail, utils.OPERATION_DELETE, True),
|
|
(scenario_update, utils.OPERATION_MODIFY, False),
|
|
(scenario_update_update, utils.OPERATION_MODIFY, False)):
|
|
status_check = cls(scenario[0], operation)
|
|
for step in scenario[:-1]:
|
|
self.assertEqual(True, status_check(step))
|
|
self.assertEqual(False, status_check(scenario[-1]))
|
|
self.assertEqual(is_fail, status_check.is_fail)
|
|
|
|
def test_extract_action_and_status(self):
|
|
cls = workerfactory.HEATIntermediateStatusChecker
|
|
stack = base.Dummy(stack_status='a_b_c')
|
|
action, status = cls._extract_action_and_status(stack)
|
|
|
|
self.assertEqual('a', action)
|
|
self.assertEqual('b_c', status)
|
|
|
|
def test_extract_action_and_status_fail(self):
|
|
cls = workerfactory.HEATIntermediateStatusChecker
|
|
stack = base.Dummy(stack_status='abc')
|
|
self.assertRaises(
|
|
exc.HEATIntegrationError, cls._extract_action_and_status, stack)
|