Add timeout parameter to bay create
Add timeout parameter to bay create Update test cases for the timeout parameter Change-Id: If8603fc47ba3659eba145e91bcaa30095cabd094 Closes-bug: #1433109
This commit is contained in:
parent
b0f46abcf9
commit
1d358a8be3
|
@ -439,15 +439,21 @@
|
|||
# (string value)
|
||||
#cluster_coe = kubernetes
|
||||
|
||||
# Number of attempts to query the Heat stack for finding out the
|
||||
# status of the created stack and getting url of the DU created in the
|
||||
# stack (integer value)
|
||||
# Number of attempts to query the Heat stack for
|
||||
# finding out the status of the created stack and
|
||||
# getting template outputs. This value is ignored
|
||||
# during bay creation if timeout is set as the poll
|
||||
# will continue until bay creation either ends
|
||||
# or times out.
|
||||
#max_attempts = 2000
|
||||
|
||||
# Sleep time interval between two attempts of querying the Heat stack.
|
||||
# This interval is in seconds. (integer value)
|
||||
#wait_interval = 1
|
||||
|
||||
# The length of time to let bay creation continue. This
|
||||
# interval is in minutes. Set to None for no timeout.
|
||||
#bay_create_timeout = None
|
||||
|
||||
[keystone_authtoken]
|
||||
|
||||
|
|
|
@ -75,6 +75,9 @@ class Bay(base.APIBase):
|
|||
node_count = wtypes.IntegerType(minimum=1)
|
||||
"""The node count for this bay"""
|
||||
|
||||
bay_create_timeout = wtypes.IntegerType(minimum=0)
|
||||
"""Timeout for creating the bay in minutes. Set to 0 for no timeout."""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated bay links"""
|
||||
|
||||
|
@ -99,7 +102,8 @@ class Bay(base.APIBase):
|
|||
def _convert_with_links(bay, url, expand=True):
|
||||
if not expand:
|
||||
bay.unset_fields_except(['uuid', 'name', 'baymodel_id',
|
||||
'node_count', 'status'])
|
||||
'node_count', 'status',
|
||||
'bay_create_timeout'])
|
||||
|
||||
bay.links = [link.Link.make_link('self', url,
|
||||
'bays', bay.uuid),
|
||||
|
@ -120,6 +124,7 @@ class Bay(base.APIBase):
|
|||
name='example',
|
||||
baymodel_id='4a96ac4b-2447-43f1-8ca6-9fd6f36d146d',
|
||||
node_count=1,
|
||||
bay_create_timeout=15,
|
||||
status="CREATED",
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
|
@ -250,7 +255,8 @@ class BaysController(rest.RestController):
|
|||
bay_dict['project_id'] = auth_token['project']['id']
|
||||
bay_dict['user_id'] = auth_token['user']['id']
|
||||
new_bay = objects.Bay(context, **bay_dict)
|
||||
res_bay = pecan.request.rpcapi.bay_create(new_bay)
|
||||
res_bay = pecan.request.rpcapi.bay_create(new_bay,
|
||||
bay.bay_create_timeout)
|
||||
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('bays', res_bay.uuid)
|
||||
|
|
|
@ -47,8 +47,9 @@ class API(rpc_service.API):
|
|||
|
||||
# Bay Operations
|
||||
|
||||
def bay_create(self, bay):
|
||||
return self._call('bay_create', bay=bay)
|
||||
def bay_create(self, bay, bay_create_timeout):
|
||||
return self._call('bay_create', bay=bay,
|
||||
bay_create_timeout=bay_create_timeout)
|
||||
|
||||
def bay_list(self, context, limit, marker, sort_key, sort_dir):
|
||||
return objects.Bay.list(context, limit, marker, sort_key, sort_dir)
|
||||
|
|
|
@ -40,11 +40,18 @@ k8s_heat_opts = [
|
|||
default=2000,
|
||||
help=('Number of attempts to query the Heat stack for '
|
||||
'finding out the status of the created stack and '
|
||||
'getting url of the DU created in the stack')),
|
||||
'getting template outputs. This value is ignored '
|
||||
'during bay creation if timeout is set as the poll '
|
||||
'will continue until bay creation either ends '
|
||||
'or times out.')),
|
||||
cfg.IntOpt('wait_interval',
|
||||
default=1,
|
||||
help=('Sleep time interval between two attempts of querying '
|
||||
'the Heat stack. This interval is in seconds.')),
|
||||
'the Heat stack. This interval is in seconds.')),
|
||||
cfg.IntOpt('bay_create_timeout',
|
||||
default=None,
|
||||
help=('The length of time to let bay creation continue. This '
|
||||
'interval is in minutes. The default is no timeout.'))
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(k8s_heat_opts, group='k8s_heat')
|
||||
|
@ -61,17 +68,26 @@ def _extract_template_definition(context, bay):
|
|||
return definition.extract_definition(baymodel, bay)
|
||||
|
||||
|
||||
def _create_stack(context, osc, bay):
|
||||
def _create_stack(context, osc, bay, bay_create_timeout):
|
||||
template_path, heat_params = _extract_template_definition(context, bay)
|
||||
|
||||
tpl_files, template = template_utils.get_template_contents(template_path)
|
||||
# Make sure no duplicate stack name
|
||||
stack_name = '%s-%s' % (bay.name, short_id.generate_id())
|
||||
if bay_create_timeout:
|
||||
heat_timeout = bay_create_timeout
|
||||
elif bay_create_timeout == 0:
|
||||
heat_timeout = None
|
||||
else:
|
||||
# no bay_create_timeout value was passed in to the request
|
||||
# so falling back on configuration file value
|
||||
heat_timeout = cfg.CONF.k8s_heat.bay_create_timeout
|
||||
fields = {
|
||||
'stack_name': stack_name,
|
||||
'parameters': heat_params,
|
||||
'template': template,
|
||||
'files': dict(list(tpl_files.items()))
|
||||
'files': dict(list(tpl_files.items())),
|
||||
'timeout_mins': heat_timeout
|
||||
}
|
||||
created_stack = osc.heat().stacks.create(**fields)
|
||||
|
||||
|
@ -104,13 +120,14 @@ class Handler(object):
|
|||
|
||||
# Bay Operations
|
||||
|
||||
def bay_create(self, context, bay):
|
||||
def bay_create(self, context, bay, bay_create_timeout):
|
||||
LOG.debug('k8s_heat bay_create')
|
||||
|
||||
osc = clients.OpenStackClients(context)
|
||||
|
||||
try:
|
||||
created_stack = _create_stack(context, osc, bay)
|
||||
created_stack = _create_stack(context, osc, bay,
|
||||
bay_create_timeout)
|
||||
except Exception as e:
|
||||
if isinstance(e, exc.HTTPBadRequest):
|
||||
raise exception.InvalidParameterValue(message=str(e))
|
||||
|
@ -216,10 +233,23 @@ class HeatPoller(object):
|
|||
LOG.error(_LE('Unable to delete bay, stack_id: %s')
|
||||
% self.bay.stack_id)
|
||||
raise loopingcall.LoopingCallDone()
|
||||
if self.attempts > cfg.CONF.k8s_heat.max_attempts:
|
||||
LOG.error(_LE('Bay check exit after %(attempts)s attempts,'
|
||||
'stack_id: %(id)s, stack_status: %(status)s') %
|
||||
{'attempts': cfg.CONF.k8s_heat.max_attempts,
|
||||
'id': self.bay.stack_id,
|
||||
'status': stack.stack_status})
|
||||
raise loopingcall.LoopingCallDone()
|
||||
# only check max attempts when the stack is being created when
|
||||
# the timeout hasn't been set. If the timeout has been set then
|
||||
# the loop will end when the stack completes or the timeout occurs
|
||||
if stack.stack_status == 'CREATE_IN_PROGRESS':
|
||||
if (stack.timeout_mins is None and
|
||||
self.attempts > cfg.CONF.k8s_heat.max_attempts):
|
||||
LOG.error(_LE('Bay check exit after %(attempts)s attempts,'
|
||||
'stack_id: %(id)s, stack_status: %(status)s') %
|
||||
{'attempts': cfg.CONF.k8s_heat.max_attempts,
|
||||
'id': self.bay.stack_id,
|
||||
'status': stack.stack_status})
|
||||
raise loopingcall.LoopingCallDone()
|
||||
else:
|
||||
if self.attempts > cfg.CONF.k8s_heat.max_attempts:
|
||||
LOG.error(_LE('Bay check exit after %(attempts)s attempts,'
|
||||
'stack_id: %(id)s, stack_status: %(status)s') %
|
||||
{'attempts': cfg.CONF.k8s_heat.max_attempts,
|
||||
'id': self.bay.stack_id,
|
||||
'status': stack.stack_status})
|
||||
raise loopingcall.LoopingCallDone()
|
||||
|
|
|
@ -377,7 +377,7 @@ class TestPost(api_base.FunctionalTest):
|
|||
self.mock_bay_create.side_effect = self._simulate_rpc_bay_create
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
def _simulate_rpc_bay_create(self, bay):
|
||||
def _simulate_rpc_bay_create(self, bay, bay_create_timeout):
|
||||
bay.create()
|
||||
return bay
|
||||
|
||||
|
@ -457,6 +457,35 @@ class TestPost(api_base.FunctionalTest):
|
|||
self.assertEqual(400, response.status_int)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_create_bay_with_timeout_none(self):
|
||||
bdict = apiutils.bay_post_data()
|
||||
bdict['bay_create_timeout'] = None
|
||||
response = self.post_json('/bays', bdict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
def test_create_bay_with_no_timeout(self):
|
||||
bdict = apiutils.bay_post_data()
|
||||
del bdict['bay_create_timeout']
|
||||
response = self.post_json('/bays', bdict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
def test_create_bay_with_timeout_negative(self):
|
||||
bdict = apiutils.bay_post_data()
|
||||
bdict['bay_create_timeout'] = -1
|
||||
response = self.post_json('/bays', bdict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_create_bay_with_timeout_zero(self):
|
||||
bdict = apiutils.bay_post_data()
|
||||
bdict['bay_create_timeout'] = 0
|
||||
response = self.post_json('/bays', bdict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
|
||||
class TestDelete(api_base.FunctionalTest):
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ def baymodel_post_data(**kw):
|
|||
|
||||
def bay_post_data(**kw):
|
||||
bay = utils.get_test_bay(**kw)
|
||||
# the timeout property is a part of the request and doesn't persist
|
||||
# in the bay db
|
||||
bay['bay_create_timeout'] = kw.get('bay_create_timeout', 15)
|
||||
internal = bay_controller.BayPatchType.internal_attrs()
|
||||
return remove_internal(bay, internal)
|
||||
|
||||
|
|
|
@ -418,6 +418,7 @@ class TestBayK8sHeat(base.TestCase):
|
|||
expected_template_contents = 'template_contents'
|
||||
exptected_files = []
|
||||
dummy_bay_name = 'expected_stack_name'
|
||||
expected_timeout = 15
|
||||
|
||||
mock_tpl_files = mock.MagicMock()
|
||||
mock_tpl_files.items.return_value = exptected_files
|
||||
|
@ -431,13 +432,96 @@ class TestBayK8sHeat(base.TestCase):
|
|||
mock_bay = mock.MagicMock()
|
||||
mock_bay.name = dummy_bay_name
|
||||
|
||||
bay_k8s_heat._create_stack(self.context, mock_osc, mock_bay)
|
||||
bay_k8s_heat._create_stack(self.context, mock_osc,
|
||||
mock_bay, expected_timeout)
|
||||
|
||||
expected_args = {
|
||||
'stack_name': expected_stack_name,
|
||||
'parameters': {},
|
||||
'template': expected_template_contents,
|
||||
'files': dict(exptected_files)
|
||||
'files': dict(exptected_files),
|
||||
'timeout_mins': expected_timeout
|
||||
}
|
||||
mock_heat_client.stacks.create.assert_called_once_with(**expected_args)
|
||||
|
||||
@patch('magnum.common.short_id.generate_id')
|
||||
@patch('heatclient.common.template_utils.get_template_contents')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat'
|
||||
'._extract_template_definition')
|
||||
def test_create_stack_no_timeout_specified(self,
|
||||
mock_extract_template_definition,
|
||||
mock_get_template_contents,
|
||||
mock_generate_id):
|
||||
|
||||
mock_generate_id.return_value = 'xx-xx-xx-xx'
|
||||
expected_stack_name = 'expected_stack_name-xx-xx-xx-xx'
|
||||
expected_template_contents = 'template_contents'
|
||||
exptected_files = []
|
||||
dummy_bay_name = 'expected_stack_name'
|
||||
expected_timeout = cfg.CONF.k8s_heat.bay_create_timeout
|
||||
|
||||
mock_tpl_files = mock.MagicMock()
|
||||
mock_tpl_files.items.return_value = exptected_files
|
||||
mock_get_template_contents.return_value = [
|
||||
mock_tpl_files, expected_template_contents]
|
||||
mock_extract_template_definition.return_value = ('template/path',
|
||||
{})
|
||||
mock_heat_client = mock.MagicMock()
|
||||
mock_osc = mock.MagicMock()
|
||||
mock_osc.heat.return_value = mock_heat_client
|
||||
mock_bay = mock.MagicMock()
|
||||
mock_bay.name = dummy_bay_name
|
||||
|
||||
bay_k8s_heat._create_stack(self.context, mock_osc,
|
||||
mock_bay, None)
|
||||
|
||||
expected_args = {
|
||||
'stack_name': expected_stack_name,
|
||||
'parameters': {},
|
||||
'template': expected_template_contents,
|
||||
'files': dict(exptected_files),
|
||||
'timeout_mins': expected_timeout
|
||||
}
|
||||
mock_heat_client.stacks.create.assert_called_once_with(**expected_args)
|
||||
|
||||
@patch('magnum.common.short_id.generate_id')
|
||||
@patch('heatclient.common.template_utils.get_template_contents')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat'
|
||||
'._extract_template_definition')
|
||||
def test_create_stack_timeout_is_zero(self,
|
||||
mock_extract_template_definition,
|
||||
mock_get_template_contents,
|
||||
mock_generate_id):
|
||||
|
||||
mock_generate_id.return_value = 'xx-xx-xx-xx'
|
||||
expected_stack_name = 'expected_stack_name-xx-xx-xx-xx'
|
||||
expected_template_contents = 'template_contents'
|
||||
exptected_files = []
|
||||
dummy_bay_name = 'expected_stack_name'
|
||||
bay_timeout = 0
|
||||
expected_timeout = None
|
||||
|
||||
mock_tpl_files = mock.MagicMock()
|
||||
mock_tpl_files.items.return_value = exptected_files
|
||||
mock_get_template_contents.return_value = [
|
||||
mock_tpl_files, expected_template_contents]
|
||||
mock_extract_template_definition.return_value = ('template/path',
|
||||
{})
|
||||
mock_heat_client = mock.MagicMock()
|
||||
mock_osc = mock.MagicMock()
|
||||
mock_osc.heat.return_value = mock_heat_client
|
||||
mock_bay = mock.MagicMock()
|
||||
mock_bay.name = dummy_bay_name
|
||||
|
||||
bay_k8s_heat._create_stack(self.context, mock_osc,
|
||||
mock_bay, bay_timeout)
|
||||
|
||||
expected_args = {
|
||||
'stack_name': expected_stack_name,
|
||||
'parameters': {},
|
||||
'template': expected_template_contents,
|
||||
'files': dict(exptected_files),
|
||||
'timeout_mins': expected_timeout
|
||||
}
|
||||
mock_heat_client.stacks.create.assert_called_once_with(**expected_args)
|
||||
|
||||
|
@ -538,6 +622,70 @@ class TestBayK8sHeat(base.TestCase):
|
|||
self.assertEqual(bay.status, 'DELETE_IN_PROGRESS')
|
||||
self.assertEqual(bay.destroy.call_count, 1)
|
||||
|
||||
def test_poll_delete_in_progress_timeout_set(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'DELETE_IN_PROGRESS'
|
||||
mock_heat_stack.timeout_mins = 60
|
||||
# timeout only affects stack creation so expecting this
|
||||
# to process normally
|
||||
poller.poll_and_check()
|
||||
|
||||
def test_poll_delete_in_progress_max_attempts_reached(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'DELETE_IN_PROGRESS'
|
||||
poller.attempts = cfg.CONF.k8s_heat.max_attempts
|
||||
self.assertRaises(loopingcall.LoopingCallDone, poller.poll_and_check)
|
||||
|
||||
def test_poll_create_in_prog_max_att_reached_no_timeout(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_IN_PROGRESS'
|
||||
poller.attempts = cfg.CONF.k8s_heat.max_attempts
|
||||
mock_heat_stack.timeout_mins = None
|
||||
self.assertRaises(loopingcall.LoopingCallDone, poller.poll_and_check)
|
||||
|
||||
def test_poll_create_in_prog_max_att_reached_timeout_set(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_IN_PROGRESS'
|
||||
poller.attempts = cfg.CONF.k8s_heat.max_attempts
|
||||
mock_heat_stack.timeout_mins = 60
|
||||
# since the timeout is set the max attempts gets ignored since
|
||||
# the timeout will eventually stop the poller either when
|
||||
# the stack gets created or the timeout gets reached
|
||||
poller.poll_and_check()
|
||||
|
||||
def test_poll_create_in_prog_max_att_reached_timed_out(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_FAILED'
|
||||
poller.attempts = cfg.CONF.k8s_heat.max_attempts
|
||||
mock_heat_stack.timeout_mins = 60
|
||||
self.assertRaises(loopingcall.LoopingCallDone, poller.poll_and_check)
|
||||
|
||||
def test_poll_create_in_prog_max_att_not_reached_no_timeout(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_IN_PROGRESS'
|
||||
mock_heat_stack.timeout.mins = None
|
||||
poller.poll_and_check()
|
||||
|
||||
def test_poll_create_in_prog_max_att_not_reached_timeout_set(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_IN_PROGRESS'
|
||||
mock_heat_stack.timeout_mins = 60
|
||||
poller.poll_and_check()
|
||||
|
||||
def test_poll_create_in_prog_max_att_not_reached_timed_out(self):
|
||||
mock_heat_stack, bay, poller = self.setup_poll_test()
|
||||
|
||||
mock_heat_stack.stack_status = 'CREATE_FAILED'
|
||||
mock_heat_stack.timeout_mins = 60
|
||||
self.assertRaises(loopingcall.LoopingCallDone, poller.poll_and_check)
|
||||
|
||||
|
||||
class TestHandler(db_base.DbTestCase):
|
||||
|
||||
|
@ -592,8 +740,10 @@ class TestHandler(db_base.DbTestCase):
|
|||
@patch('magnum.common.clients.OpenStackClients')
|
||||
def test_create(self, mock_openstack_client_class, mock_create_stack):
|
||||
mock_create_stack.side_effect = exc.HTTPBadRequest
|
||||
timeout = 15
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.handler.bay_create, self.context, self.bay)
|
||||
self.handler.bay_create, self.context,
|
||||
self.bay, timeout)
|
||||
|
||||
@patch('magnum.common.clients.OpenStackClients')
|
||||
def test_bay_delete(self, mock_openstack_client_class):
|
||||
|
|
|
@ -76,7 +76,8 @@ class RPCAPITestCase(base.DbTestCase):
|
|||
self._test_rpcapi('bay_create',
|
||||
'call',
|
||||
version='1.0',
|
||||
bay=self.fake_bay)
|
||||
bay=self.fake_bay,
|
||||
bay_create_timeout=15)
|
||||
|
||||
def test_bay_delete(self):
|
||||
self._test_rpcapi('bay_delete',
|
||||
|
|
Loading…
Reference in New Issue