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:
parent
72671fffe5
commit
0cb09cc560
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
features:
|
||||
- |
|
||||
Support to wait for volume creation until it completes.
|
||||
The command is: ``cinder create --poll <volume_size>``
|
Loading…
Reference in New Issue