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:
Jennifer Carlucci 2015-04-15 14:47:35 -05:00
parent b0f46abcf9
commit 1d358a8be3
8 changed files with 251 additions and 25 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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',