Add cinder create --poll

Usage: It adds an optional argument --poll to the cinder
create command which waits while the creation of the volume
is completed and the volume goes to available state. In case
there is an error in volume creation, it throws an error message
and exits with a non zero status. The error message printed here
is the async error message in case it generates one.

Depends-On: Ic3ab32b95abd29e995bc071adc11b1e481b32516

Change-Id: I1a4d361d48a44a0daa830491f415be64f2e356e3
This commit is contained in:
Chaynika Saikia 2017-06-19 16:27:49 -04:00
parent 72671fffe5
commit 0cb09cc560
8 changed files with 148 additions and 1 deletions

View File

@ -149,6 +149,9 @@ class SessionClient(adapter.LegacyJsonAdapter):
if raise_exc and resp.status_code >= 400:
raise exceptions.from_response(resp, body)
if not self.global_request_id:
self.global_request_id = resp.headers.get('x-openstack-request-id')
return resp, body
def _cs_request(self, url, method, **kwargs):

View File

@ -21,6 +21,29 @@ from datetime import datetime
from oslo_utils import timeutils
class ResourceInErrorState(Exception):
"""When resource is in Error state"""
def __init__(self, obj, fault_msg):
msg = "'%s' resource is in the error state" % obj.__class__.__name__
if fault_msg:
msg += " due to '%s'" % fault_msg
self.message = "%s." % msg
def __str__(self):
return self.message
class TimeoutException(Exception):
"""When an action exceeds the timeout period to complete the action"""
def __init__(self, obj, action):
self.message = ("The '%(action)s' of the '%(object_name)s' exceeded "
"the timeout period." % {"action": action,
"object_name": obj.__class__.__name__})
def __str__(self):
return self.message
class UnsupportedVersion(Exception):
"""Indicates that the user is trying to use an unsupported
version of the API.

View File

@ -18,6 +18,7 @@ import sys
import time
from cinderclient import utils
from cinderclient import exceptions
_quota_resources = ['volumes', 'snapshots', 'gigabytes',
'backups', 'backup_gigabytes',
@ -276,3 +277,36 @@ def print_qos_specs_and_associations_list(q_specs):
def print_associations_list(associations):
utils.print_list(associations, ['Association_Type', 'Name', 'ID'])
def _poll_for_status(poll_fn, obj_id, info, action, final_ok_states,
timeout_period, global_request_id=None, messages=None,
poll_period=2, status_field="status"):
"""Block while an action is being performed."""
time_elapsed = 0
while True:
time.sleep(poll_period)
time_elapsed += poll_period
obj = poll_fn(obj_id)
status = getattr(obj, status_field)
info[status_field] = status
if status:
status = status.lower()
if status in final_ok_states:
break
elif status == "error":
utils.print_dict(info)
if global_request_id:
search_opts = {
'request_id': global_request_id
}
message_list = messages.list(search_opts=search_opts)
try:
fault_msg = message_list[0].user_message
except IndexError:
fault_msg = "Unknown error. Operation failed."
raise exceptions.ResourceInErrorState(obj, fault_msg)
elif time_elapsed == timeout_period:
utils.print_dict(info)
raise exceptions.TimeoutException(obj, action)

View File

@ -131,9 +131,11 @@ class ClientTest(utils.TestCase):
"code": 202
}
request_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e"
mock_response = utils.TestResponse({
"status_code": 202,
"text": six.b(json.dumps(resp)),
"headers": {"x-openstack-request-id": request_id},
})
# 'request' method of Adaptor will return 202 response

View File

@ -77,8 +77,9 @@ class FakeClient(fakes.FakeClient, client.Client):
'project_id', 'auth_url',
extensions=kwargs.get('extensions'))
self.api_version = api_version
global_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e"
self.client = FakeHTTPClient(api_version=api_version,
**kwargs)
global_request_id=global_id, **kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()

View File

@ -43,11 +43,13 @@ import fixtures
import mock
from requests_mock.contrib import fixture as requests_mock_fixture
import six
import cinderclient
from cinderclient import client
from cinderclient import exceptions
from cinderclient import shell
from cinderclient import utils as cinderclient_utils
from cinderclient import base
from cinderclient.v3 import volumes
from cinderclient.v3 import volume_snapshots
from cinderclient.tests.unit import utils
@ -916,3 +918,71 @@ class ShellTest(utils.TestCase):
'service-set-log %s --binary %s --server %s '
'--prefix %s' % (level, binary, server, prefix))
set_levels_mock.assert_called_once_with(level, binary, server, prefix)
@mock.patch('cinderclient.shell_utils._poll_for_status')
def test_create_with_poll(self, poll_method):
self.run_command('create --poll 1')
self.assert_called_anytime('GET', '/volumes/1234')
volume = self.shell.cs.volumes.get('1234')
info = dict()
info.update(volume._info)
info.pop('links', None)
self.assertEqual(1, poll_method.call_count)
timeout_period = 3600
poll_method.assert_has_calls([mock.call(self.shell.cs.volumes.get,
1234, info, 'creating', ['available'], timeout_period,
self.shell.cs.client.global_request_id,
self.shell.cs.messages)])
@mock.patch('cinderclient.shell_utils.time')
def test_poll_for_status(self, mock_time):
poll_period = 2
some_id = "some-id"
global_request_id = "req-someid"
action = "some"
updated_objects = (
base.Resource(None, info={"not_default_field": "creating"}),
base.Resource(None, info={"not_default_field": "available"}))
poll_fn = mock.MagicMock(side_effect=updated_objects)
cinderclient.shell_utils._poll_for_status(
poll_fn = poll_fn,
obj_id = some_id,
global_request_id = global_request_id,
messages = base.Resource(None, {}),
info = {},
action = action,
status_field = "not_default_field",
final_ok_states = ['available'],
timeout_period=3600)
self.assertEqual([mock.call(poll_period)] * 2,
mock_time.sleep.call_args_list)
self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list)
@mock.patch('cinderclient.v3.messages.MessageManager.list')
@mock.patch('cinderclient.shell_utils.time')
def test_poll_for_status_error(self, mock_time, mock_message_list):
poll_period = 2
some_id = "some_id"
global_request_id = "req-someid"
action = "some"
updated_objects = (
base.Resource(None, info={"not_default_field": "creating"}),
base.Resource(None, info={"not_default_field": "error"}))
poll_fn = mock.MagicMock(side_effect=updated_objects)
msg_object = base.Resource(cinderclient.v3.messages.MessageManager,
info = {"user_message": "ERROR!"})
mock_message_list.return_value = (msg_object,)
self.assertRaises(exceptions.ResourceInErrorState,
cinderclient.shell_utils._poll_for_status,
poll_fn=poll_fn,
obj_id=some_id,
global_request_id=global_request_id,
messages=cinderclient.v3.messages.MessageManager(api=3.34),
info=dict(),
action=action,
final_ok_states=['available'],
status_field="not_default_field",
timeout_period=3600)
self.assertEqual([mock.call(poll_period)] * 2,
mock_time.sleep.call_args_list)
self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list)

View File

@ -19,6 +19,7 @@ from __future__ import print_function
import argparse
import collections
import os
import sys
from oslo_utils import strutils
import six
@ -503,6 +504,9 @@ def do_reset_state(cs, args):
help=('Allow volume to be attached more than once.'
' Default=False'),
default=False)
@utils.arg('--poll',
action="store_true",
help=('Wait for volume creation until it completes.'))
def do_create(cs, args):
"""Creates a volume."""
@ -563,6 +567,12 @@ def do_create(cs, args):
info['readonly'] = info['metadata']['readonly']
info.pop('links', None)
if args.poll:
timeout_period = os.environ.get("POLL_TIMEOUT_PERIOD", 3600)
shell_utils._poll_for_status(cs.volumes.get, volume.id, info, 'creating', ['available'],
timeout_period, cs.client.global_request_id, cs.messages)
utils.print_dict(info)

View File

@ -0,0 +1,4 @@
features:
- |
Support to wait for volume creation until it completes.
The command is: ``cinder create --poll <volume_size>``