Poll functionality for stack create action

Poll functionality is required for long running stacks.

When stack-create is passed with --poll argument, it will first print
stack-show output and then continously print the events in log format
until stack completes its action with success/failure.

This patch only implements poll for stack create action.

DocImpact
A new option --poll is added to stack-create.

Partial-Bug: #1420541

Change-Id: Ib7d35b66521f0ccca8544fd18fb70e04eaf98e5a
This commit is contained in:
Rakesh H S 2015-06-04 11:43:03 +05:30
parent 948dcb57d2
commit 04b3880cb4
5 changed files with 322 additions and 113 deletions

View File

@ -190,3 +190,7 @@ class NoTokenLookupException(Exception):
class EndpointNotFound(Exception):
"""DEPRECATED."""
pass
class StackFailure(Exception):
pass

View File

@ -93,6 +93,83 @@ def mock_script_heat_list(show_nested=False):
return resp, resp_dict
def mock_script_event_list(
stack_name="teststack", resource_name=None,
rsrc_eventid1="7fecaeed-d237-4559-93a5-92d5d9111205",
rsrc_eventid2="e953547a-18f8-40a7-8e63-4ec4f509648b",
action="CREATE", final_state="COMPLETE", fakehttp=True):
resp_dict = {"events": [
{"event_time": "2013-12-05T14:14:31Z",
"id": rsrc_eventid1,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "myDeployment",
"physical_resource_id": None,
"resource_name": resource_name if resource_name else "testresource",
"resource_status": "%s_IN_PROGRESS" % action,
"resource_status_reason": "state changed"},
{"event_time": "2013-12-05T14:14:32Z",
"id": rsrc_eventid2,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "myDeployment",
"physical_resource_id": "bce15ec4-8919-4a02-8a90-680960fb3731",
"resource_name": resource_name if resource_name else "testresource",
"resource_status": "%s_%s" % (action, final_state),
"resource_status_reason": "state changed"}]}
if resource_name is None:
# if resource_name is not specified,
# then request is made for stack events. Hence include the stack event
stack_event1 = "0159dccd-65e1-46e8-a094-697d20b009e5"
stack_event2 = "8f591a36-7190-4adb-80da-00191fe22388"
resp_dict["events"].insert(
0, {"event_time": "2013-12-05T14:14:30Z",
"id": stack_event1,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id": None,
"resource_name": stack_name,
"resource_status": "%s_IN_PROGRESS" % action,
"resource_status_reason": "state changed"})
resp_dict["events"].append(
{"event_time": "2013-12-05T14:14:33Z",
"id": stack_event2,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id": None,
"resource_name": stack_name,
"resource_status": "%s_%s" % (action, final_state),
"resource_status_reason": "state changed"})
resp = FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(resp_dict)) if fakehttp else None
return resp, resp_dict
def script_heat_normal_error(client=http.HTTPClient):
resp_dict = {
"explanation": "The resource could not be found.",

View File

@ -388,33 +388,10 @@ class ShellTestNoMox(TestCase):
status_code=302,
headers=h)
resp_dict = {"events": [
{"event_time": "2014-12-05T14:14:30Z",
"id": eventid1,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "myDeployment",
"physical_resource_id": None,
"resource_name": "myDeployment",
"resource_status": "CREATE_IN_PROGRESS",
"resource_status_reason": "state changed"},
{"event_time": "2014-12-05T14:14:30Z",
"id": eventid2,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "myDeployment",
"physical_resource_id": uuid.uuid4().hex,
"resource_name": "myDeployment",
"resource_status": "CREATE_COMPLETE",
"resource_status_reason": "state changed"}]}
resp, resp_dict = fakes.mock_script_event_list(
resource_name="myDeployment", rsrc_eventid1=eventid1,
rsrc_eventid2=eventid2, fakehttp=False
)
self.requests.get('http://heat.example.com/stacks/myStack%2F60f83b5e/'
'resources/myDeployment/events',
@ -434,8 +411,8 @@ class ShellTestNoMox(TestCase):
eventid2,
'state changed',
'CREATE_IN_PROGRESS',
'2014-12-05T14:14:30Z',
'2014-12-05T14:14:30Z',
'2013-12-05T14:14:31Z',
'2013-12-05T14:14:32Z',
]
for r in required:
@ -1145,6 +1122,165 @@ class ShellTestUserPass(ShellBase):
for r in required:
self.assertRegexpMatches(create_text, r)
def test_create_success_with_poll(self):
self.register_keystone_auth_fixture()
stack_create_resp_dict = {"stack": {
"id": "teststack2/2",
"stack_name": "teststack2",
"stack_status": 'CREATE_IN_PROGRESS',
"creation_time": "2012-10-25T01:58:47Z"
}}
stack_create_resp = fakes.FakeHTTPResponse(
201,
'Created',
{'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'},
jsonutils.dumps(stack_create_resp_dict))
if self.client == http.SessionClient:
headers = {}
self.client.request(
'/stacks', 'POST', data=mox.IgnoreArg(),
headers=headers).AndReturn(stack_create_resp)
else:
headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'}
self.client.json_request(
'POST', '/stacks', data=mox.IgnoreArg(),
headers=headers
).AndReturn((stack_create_resp, None))
fakes.script_heat_list(client=self.client)
stack_show_resp_dict = {"stack": {
"id": "1",
"stack_name": "teststack",
"stack_status": 'CREATE_COMPLETE',
"creation_time": "2012-10-25T01:58:47Z"
}}
stack_show_resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(stack_show_resp_dict))
event_list_resp, event_list_resp_dict = fakes.mock_script_event_list(
stack_name="teststack2")
stack_id = 'teststack2'
if self.client == http.SessionClient:
self.client.request(
'/stacks/teststack2', 'GET').MultipleTimes().AndReturn(
stack_show_resp)
self.client.request(
'/stacks/%s/events?sort_dir=asc' % stack_id, 'GET'
).MultipleTimes().AndReturn(event_list_resp)
else:
self.client.json_request(
'GET', '/stacks/teststack2').MultipleTimes().AndReturn(
(stack_show_resp, stack_show_resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/%s/events?sort_dir=asc' % stack_id
).MultipleTimes().AndReturn((event_list_resp,
event_list_resp_dict))
self.m.ReplayAll()
template_file = os.path.join(TEST_VAR_DIR, 'minimal.template')
create_text = self.shell(
'stack-create teststack2 '
'--poll 4 '
'--template-file=%s '
'--parameters="InstanceType=m1.large;DBUsername=wp;'
'DBPassword=verybadpassword;KeyName=heat_key;'
'LinuxDistribution=F17"' % template_file)
required = [
'id',
'stack_name',
'stack_status',
'2',
'teststack2',
'IN_PROGRESS',
'14:14:30', '2013-12-05', '0159dccd-65e1-46e8-a094-697d20b009e5',
'CREATE_IN_PROGRESS', 'state changed',
'14:14:31', '7fecaeed-d237-4559-93a5-92d5d9111205',
'testresource',
'14:14:32', 'e953547a-18f8-40a7-8e63-4ec4f509648b',
'CREATE_COMPLETE',
'14:14:33', '8f591a36-7190-4adb-80da-00191fe22388'
]
for r in required:
self.assertRegexpMatches(create_text, r)
def test_create_failed_with_poll(self):
self.register_keystone_auth_fixture()
stack_create_resp_dict = {"stack": {
"id": "teststack2/2",
"stack_name": "teststack2",
"stack_status": 'CREATE_IN_PROGRESS',
"creation_time": "2012-10-25T01:58:47Z"
}}
stack_create_resp = fakes.FakeHTTPResponse(
201,
'Created',
{'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'},
jsonutils.dumps(stack_create_resp_dict))
if self.client == http.SessionClient:
headers = {}
self.client.request(
'/stacks', 'POST', data=mox.IgnoreArg(),
headers=headers).AndReturn(stack_create_resp)
else:
headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'}
self.client.json_request(
'POST', '/stacks', data=mox.IgnoreArg(),
headers=headers
).AndReturn((stack_create_resp, None))
fakes.script_heat_list(client=self.client)
stack_show_resp_dict = {"stack": {
"id": "1",
"stack_name": "teststack",
"stack_status": 'CREATE_COMPLETE',
"creation_time": "2012-10-25T01:58:47Z"
}}
stack_show_resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(stack_show_resp_dict))
event_list_resp, event_list_resp_dict = fakes.mock_script_event_list(
stack_name="teststack2", action="CREATE", final_state="FAILED")
stack_id = 'teststack2'
if self.client == http.SessionClient:
self.client.request(
'/stacks/teststack2', 'GET').MultipleTimes().AndReturn(
stack_show_resp)
self.client.request(
'/stacks/%s/events?sort_dir=asc' % stack_id, 'GET'
).MultipleTimes().AndReturn(event_list_resp)
else:
self.client.json_request(
'GET', '/stacks/teststack2').MultipleTimes().AndReturn(
(stack_show_resp, stack_show_resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/%s/events?sort_dir=asc' % stack_id
).MultipleTimes().AndReturn((event_list_resp,
event_list_resp_dict))
self.m.ReplayAll()
template_file = os.path.join(TEST_VAR_DIR, 'minimal.template')
e = self.assertRaises(exc.StackFailure, self.shell,
'stack-create teststack2 --poll '
'--template-file=%s --parameters="InstanceType='
'm1.large;DBUsername=wp;DBPassword=password;'
'KeyName=heat_key;LinuxDistribution=F17' %
template_file)
self.assertEqual("\n Stack teststack2 CREATE_FAILED \n",
str(e))
def test_stack_create_param_file(self):
self.register_keystone_auth_fixture()
resp = fakes.FakeHTTPResponse(
@ -2598,39 +2734,11 @@ class ShellTestEvents(ShellBase):
def test_event_list(self):
self.register_keystone_auth_fixture()
resp_dict = {"events": [
{"event_time": "2013-12-05T14:14:30Z",
"id": self.event_id_one,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id": None,
"resource_name": "aResource",
"resource_status": "CREATE_IN_PROGRESS",
"resource_status_reason": "state changed"},
{"event_time": "2013-12-05T14:14:30Z",
"id": self.event_id_two,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id":
"bce15ec4-8919-4a02-8a90-680960fb3731",
"resource_name": "aResource",
"resource_status": "CREATE_COMPLETE",
"resource_status_reason": "state changed"}]}
resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(resp_dict))
resp, resp_dict = fakes.mock_script_event_list(
resource_name="aResource",
rsrc_eventid1=self.event_id_one,
rsrc_eventid2=self.event_id_two
)
stack_id = 'teststack/1'
resource_name = 'testresource/1'
http.SessionClient.request(
@ -2656,47 +2764,20 @@ class ShellTestEvents(ShellBase):
'state changed',
'CREATE_IN_PROGRESS',
'CREATE_COMPLETE',
'2013-12-05T14:14:30Z',
'2013-12-05T14:14:30Z',
'2013-12-05T14:14:31Z',
'2013-12-05T14:14:32Z',
]
for r in required:
self.assertRegexpMatches(event_list_text, r)
def test_stack_event_list_log(self):
self.register_keystone_auth_fixture()
resp_dict = {"events": [
{"event_time": "2013-12-05T14:14:30Z",
"id": self.event_id_one,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id": None,
"resource_name": "aResource",
"resource_status": "CREATE_IN_PROGRESS",
"resource_status_reason": "state changed"},
{"event_time": "2013-12-05T14:14:30Z",
"id": self.event_id_two,
"links": [{"href": "http://heat.example.com:8004/foo",
"rel": "self"},
{"href": "http://heat.example.com:8004/foo2",
"rel": "resource"},
{"href": "http://heat.example.com:8004/foo3",
"rel": "stack"}],
"logical_resource_id": "aResource",
"physical_resource_id":
"bce15ec4-8919-4a02-8a90-680960fb3731",
"resource_name": "aResource",
"resource_status": "CREATE_COMPLETE",
"resource_status_reason": "state changed"}]}
resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(resp_dict))
resp, resp_dict = fakes.mock_script_event_list(
resource_name="aResource",
rsrc_eventid1=self.event_id_one,
rsrc_eventid2=self.event_id_two
)
stack_id = 'teststack/1'
if self.client == http.SessionClient:
self.client.request(
@ -2713,9 +2794,9 @@ class ShellTestEvents(ShellBase):
event_list_text = self.shell('event-list {0} --format log'.format(
stack_id))
expected = '14:14:30 2013-12-05 %s [aResource]: ' \
expected = '14:14:31 2013-12-05 %s [aResource]: ' \
'CREATE_IN_PROGRESS state changed\n' \
'14:14:30 2013-12-05 %s [aResource]: CREATE_COMPLETE ' \
'14:14:32 2013-12-05 %s [aResource]: CREATE_COMPLETE ' \
'state changed\n' % (self.event_id_one, self.event_id_two)
self.assertEqual(expected, event_list_text)

View File

@ -41,6 +41,7 @@ class TestHooks(testtools.TestCase):
type(self.args).rollback = mock.PropertyMock(return_value=None)
type(self.args).pre_create = mock.PropertyMock(return_value=False)
type(self.args).pre_update = mock.PropertyMock(return_value=False)
type(self.args).poll = mock.PropertyMock(return_value=None)
def test_create_hooks_in_args(self):
type(self.args).pre_create = mock.PropertyMock(

View File

@ -20,6 +20,7 @@ from oslo_serialization import jsonutils
from oslo_utils import strutils
import six
from six.moves.urllib import request
import time
import yaml
from heatclient.common import deployment_utils
@ -89,6 +90,10 @@ def _authenticated_fetcher(hc):
'This can be specified multiple times. Parameter value '
'would be the content of the file'),
action='append')
@utils.arg('--poll', metavar='SECONDS', type=int, nargs='?', const=5,
help=_('Poll and report events until stack completes. '
'Optional poll interval in seconds can be provided as '
'argument, default 5.'))
@utils.arg('name', metavar='<STACK_NAME>',
help=_('Name of the stack to create.'))
@utils.arg('--tags', metavar='<TAG1,TAG2>',
@ -134,6 +139,8 @@ def do_stack_create(hc, args):
hc.stacks.create(**fields)
do_stack_list(hc)
if args.poll is not None:
_poll_for_events(hc, args.name, 'CREATE', poll_period=args.poll)
def hooks_to_env(env, arg_hooks, hook):
@ -380,20 +387,7 @@ def do_action_check(hc, args):
def do_stack_show(hc, args):
'''Describe the stack.'''
fields = {'stack_id': args.id}
try:
stack = hc.stacks.get(**fields)
except exc.HTTPNotFound:
raise exc.CommandError(_('Stack not found: %s') % args.id)
else:
formatters = {
'description': utils.text_wrap_formatter,
'template_description': utils.text_wrap_formatter,
'stack_status_reason': utils.text_wrap_formatter,
'parameters': utils.json_formatter,
'outputs': utils.json_formatter,
'links': utils.link_formatter
}
utils.print_dict(stack.to_dict(), formatters=formatters)
_do_stack_show(hc, fields)
@utils.arg('-f', '--template-file', metavar='<FILE>',
@ -1442,3 +1436,55 @@ def do_template_function_list(hc, args):
_('Template version not found: %s') % args.template_version)
else:
utils.print_list(functions, ['functions', 'description'])
def _do_stack_show(hc, fields):
try:
stack = hc.stacks.get(**fields)
except exc.HTTPNotFound:
raise exc.CommandError(_('Stack not found: %s') %
fields.get('stack_id'))
else:
formatters = {
'description': utils.text_wrap_formatter,
'template_description': utils.text_wrap_formatter,
'stack_status_reason': utils.text_wrap_formatter,
'parameters': utils.json_formatter,
'outputs': utils.json_formatter,
'links': utils.link_formatter
}
utils.print_dict(stack.to_dict(), formatters=formatters)
def _poll_for_events(hc, stack_name, action, poll_period):
"""When an action is performed on a stack, continuously poll for its
events and display to user as logs.
"""
fields = {'stack_id': stack_name}
_do_stack_show(hc, fields)
marker = None
while True:
events = event_utils.get_events(hc, stack_id=stack_name,
event_args={'sort_dir': 'asc',
'marker': marker})
if len(events) >= 1:
# set marker to last event that was received.
marker = getattr(events[-1], 'id', None)
events_log = utils.event_log_formatter(events)
print(events_log)
for event in events:
# check if stack event was also received
if getattr(event, 'resource_name', '') == stack_name:
stack_status = getattr(event, 'resource_status', '')
msg = _("\n Stack %(name)s %(status)s \n") % dict(
name=stack_name, status=stack_status)
if stack_status == '%s_COMPLETE' % action:
_do_stack_show(hc, fields)
print(msg)
return
elif stack_status == '%s_FAILED' % action:
_do_stack_show(hc, fields)
raise exc.StackFailure(msg)
time.sleep(poll_period)