summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChaynika Saikia <csaikia@asu.edu>2017-06-19 16:27:49 -0400
committerChaynika Saikia <csaikia@asu.edu>2017-07-24 09:06:28 -0400
commit0cb09cc560436538366cfa4c91136ba5538f5167 (patch)
tree1e2e4302399ea5bc9971f25d2fa18727ff9547c8
parent72671fffe51898446c8671e122cd6ef11171fdb1 (diff)
downloadpython-cinderclient-0cb09cc560436538366cfa4c91136ba5538f5167.tar.gz
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
-rw-r--r--cinderclient/client.py3
-rw-r--r--cinderclient/exceptions.py23
-rw-r--r--cinderclient/shell_utils.py34
-rw-r--r--cinderclient/tests/unit/test_client.py2
-rw-r--r--cinderclient/tests/unit/v3/fakes.py3
-rw-r--r--cinderclient/tests/unit/v3/test_shell.py70
-rw-r--r--cinderclient/v3/shell.py10
-rw-r--r--releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml4
8 files changed, 148 insertions, 1 deletions
diff --git a/cinderclient/client.py b/cinderclient/client.py
index 57ebd17..16152fb 100644
--- a/cinderclient/client.py
+++ b/cinderclient/client.py
@@ -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):
diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py
index 43ae5a4..4f0227e 100644
--- a/cinderclient/exceptions.py
+++ b/cinderclient/exceptions.py
@@ -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.
diff --git a/cinderclient/shell_utils.py b/cinderclient/shell_utils.py
index 35802f8..e01b0cf 100644
--- a/cinderclient/shell_utils.py
+++ b/cinderclient/shell_utils.py
@@ -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)
diff --git a/cinderclient/tests/unit/test_client.py b/cinderclient/tests/unit/test_client.py
index f1121de..194eb55 100644
--- a/cinderclient/tests/unit/test_client.py
+++ b/cinderclient/tests/unit/test_client.py
@@ -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
diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py
index c06ca3e..9d0d5db 100644
--- a/cinderclient/tests/unit/v3/fakes.py
+++ b/cinderclient/tests/unit/v3/fakes.py
@@ -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()
diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py
index 9eae08f..85cd208 100644
--- a/cinderclient/tests/unit/v3/test_shell.py
+++ b/cinderclient/tests/unit/v3/test_shell.py
@@ -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)
diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py
index 05dea07..29f888a 100644
--- a/cinderclient/v3/shell.py
+++ b/cinderclient/v3/shell.py
@@ -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)
diff --git a/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml b/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml
new file mode 100644
index 0000000..5d6a106
--- /dev/null
+++ b/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml
@@ -0,0 +1,4 @@
+features:
+ - |
+ Support to wait for volume creation until it completes.
+ The command is: ``cinder create --poll <volume_size>``