Skip to content

Commit

Permalink
[GH-50] Add Detail Job Failed Message.
Browse files Browse the repository at this point in the history
The Exception of job failure doesn't include detail task failure message.

Enhance the JobStateError to include task error descriptions.
Clean up the code of StoropsException.

Bump up the version to 0.2.25.
  • Loading branch information
Cedric Zhuang committed Nov 14, 2016
1 parent f9a54bc commit 2990d51
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ StorOps: The Python Library for VNX & Unity
.. image:: https://img.shields.io/pypi/v/storops.svg
:target: https://pypi.python.org/pypi/storops

VERSION: 0.2.24
VERSION: 0.2.25

A minimalist Python library to manage VNX/Unity systems.
This document lies in the source code and go with the release.
Expand Down
80 changes: 47 additions & 33 deletions storops/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,49 +36,60 @@ class StoropsException(Exception):
with the keyword arguments provided to the constructor.
"""
message = "An unknown exception occurred."
code = 500
headers = {}
unknown_message = "An unknown exception occurred."

# class level error message.
message = None

# class level error message template.
message_template = None

# default error code used in message template.
default_code = 500

def __init__(self, message=None, **kwargs):
if message is None:
message = self.message
if self.message_template is not None:
message = self.message_template
elif self.__class__.message is not None:
message = self.__class__.message
else:
message = self.unknown_message

self.kwargs = self._insert_default_code(kwargs)
self.message = self._update_message(message, kwargs)
self._message = message
self._kwargs = kwargs

super(StoropsException, self).__init__(self.message)
super(StoropsException, self).__init__()

@staticmethod
def _update_message(message, kwargs):
if isinstance(message, six.string_types):
@property
def message(self):
ret = self._message
if isinstance(self._message, six.string_types):
try:
message = message.format(**kwargs)
if 'code' not in self._kwargs:
self._kwargs['code'] = self.default_code

ret = self._message.format(**self._kwargs)

except KeyError:
# kwargs doesn't match a variable in the message
# log the issue and the kwargs
log.error(
'missing param in format string: "{}"'.format(message))
'missing param in format string: "{}"'.format(
self._message))
except IndexError:
# format error, use original message
pass
elif isinstance(message, Exception):
message = six.text_type(message)
elif isinstance(self._message, Exception):
ret = six.text_type(self._message)
return ret

return message
@message.setter
def message(self, value):
self._message = value

@classmethod
def _insert_default_code(cls, kwargs):
if 'code' not in kwargs:
try:
kwargs['code'] = cls.code
except AttributeError:
pass
for k, v in six.iteritems(kwargs):
if isinstance(v, Exception):
kwargs[k] = six.text_type(v)
return kwargs
def __str__(self):
return self.message


def to_hex(number):
Expand Down Expand Up @@ -135,7 +146,7 @@ def error_code(self):


class VNXBackendError(VNXException):
message = "vnx backend error. {err}"
message_template = "vnx backend error. {err}"


_rest_exception_factory = MappedErrorCodeDecoratorFactory(
Expand Down Expand Up @@ -482,7 +493,7 @@ class NaviseccliNotAvailableError(VNXException):


class VNXObjectNotFound(VNXException):
message = "object is not found. {err}"
message_template = "object is not found. {err}"


class VNXSetArrayNameError(VNXException):
Expand All @@ -496,7 +507,7 @@ class OptionMissingError(VNXException):
@xmlapi_exception
class VNXInvalidMoverID(VNXException):
error_code = 14227341323
message = "invalid mover or vdm. {id}"
message_template = "invalid mover or vdm. {id}"


class VNXLockRequiredException(VNXException):
Expand Down Expand Up @@ -1079,8 +1090,11 @@ class JobTimeoutException(UnityJobException):


class JobStateError(UnityJobException):
message = "Job failed in {state}."

def __init__(self, **kwargs):
self.message = JobStateError.message.format(**kwargs)
def __init__(self, job):
self.job = job
super(JobStateError, self).__init__(self.message)

@property
def message(self):
return 'Job State: {}. Error Detail: {}'.format(
self.job.state.name, '. '.join(self.job.messages))
50 changes: 45 additions & 5 deletions storops/unity/resource/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@

import retryz
from storops import exception as ex
from storops.exception import get_rest_exception
from storops.lib.common import instance_cache
from storops.unity.resource import UnityResource, UnityResourceList, \
UnityAttributeResource
from storops.unity.enums import FSSupportedProtocolEnum

import storops
from storops.unity import enums

__author__ = 'Cedric Zhuang'


Expand Down Expand Up @@ -75,8 +78,7 @@ def create_nfs_share(cls, cli, pool, nas_server, name, size,
cli.make_body(nfs_share_create))
job_req_body['tasks'].append(task_body)

resp = cli.post(cls().resource_class,
**job_req_body)
resp = cli.post(cls().resource_class, **job_req_body)
resp.raise_if_err()
job = cls(_id=resp.resource_id, cli=cli)
if not async:
Expand All @@ -89,7 +91,7 @@ def check_errors(self):
elif self.state in (enums.JobStateEnum.FAILED,
enums.JobStateEnum.ROLLING_BACK,
enums.JobStateEnum.COMPLETED_WITH_ERROR):
raise ex.JobStateError(state=self.state.name)
raise ex.JobStateError(self)
return False

def wait_job_completion(self, **kwargs):
Expand All @@ -106,6 +108,35 @@ def _do_update():
except retryz.RetryTimeoutError:
raise ex.JobTimeoutException()

@property
@instance_cache
def _task_messages(self):
return [task_message
for task in self.tasks
for task_message in task.messages]

@property
@instance_cache
def _localized_messages(self):
return [message
for messages in self._task_messages
for message in messages.messages]

@property
@instance_cache
def messages(self):
return [message.message
for message in self._localized_messages]

@property
@instance_cache
def exceptions(self):
return list(filter(lambda e: e is not None,
(m.to_exception() for m in self._task_messages)))

def has_exception(self):
return len(self.exceptions) > 0


class UnityJobList(UnityResourceList):
@classmethod
Expand All @@ -124,7 +155,16 @@ def get_resource_class(cls):


class UnityMessage(UnityAttributeResource):
pass
def to_exception(self):
if self.error_code != 0:
clz = get_rest_exception(self.error_code)
ret = clz(self)
else:
ret = None
return ret

def get_messages(self):
return [m.message for m in self.messages]


class UnityMessageList(UnityResourceList):
Expand Down Expand Up @@ -155,7 +195,7 @@ def _do_update():
elif job.state in (enums.JobStateEnum.FAILED,
enums.JobStateEnum.ROLLING_BACK,
enums.JobStateEnum.COMPLETED_WITH_ERROR):
raise ex.JobStateError(state=job.state.name)
raise ex.JobStateError(job)
return False

try:
Expand Down
2 changes: 1 addition & 1 deletion test/test_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


class DemoException(StoropsException):
message = 'hello, {name}.'
message_template = 'hello, {name}.'


class StrangeException(Exception):
Expand Down
35 changes: 33 additions & 2 deletions test/unity/resource/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

from unittest import TestCase

from hamcrest import assert_that, equal_to, instance_of
from hamcrest import assert_that, equal_to, instance_of, contains_string, \
has_item

from storops.exception import UnityFileSystemSizeTooSmallError
from storops.unity.enums import JobStateEnum, JobTaskStateEnum
from storops.unity.resource.job import UnityJob, UnityJobList, \
UnityJobTaskList, \
Expand Down Expand Up @@ -111,9 +113,38 @@ def test_create_nfs_share_failed(self):
nas_server = UnityNasServer.get(cli=t_rest(), _id='nas_6')
self.assertRaisesRegexp(
ex.JobStateError,
"Job failed in {}.".format(JobStateEnum.FAILED.name),
'Job State: FAILED. Error Detail: ',
UnityJob.create_nfs_share,
cli=t_rest(), pool=pool, nas_server=nas_server,
name='613dd8b0-2c22-4da0-888e-494d320303b7',
size=4294967296,
async=False)

@patch_rest
def test_messages(self):
job = UnityJob(_id='B-3', cli=t_rest())
assert_that(len(job.messages), equal_to(1))
assert_that(job.messages[0], contains_string('too small'))

@patch_rest
def test_has_exceptions(self):
job = UnityJob(_id='B-3', cli=t_rest())
assert_that(len(job.exceptions), equal_to(1))
exception = job.exceptions[0]
assert_that(exception,
instance_of(UnityFileSystemSizeTooSmallError))
assert_that(str(exception), contains_string('too small'))
assert_that(job.has_exception(), equal_to(True))

@patch_rest
def test_batch_without_error(self):
job = UnityJob(_id='B-693', cli=t_rest())
assert_that(job.messages, has_item('Success'))
assert_that(len(job.exceptions), equal_to(0))
assert_that(job.has_exception(), equal_to(False))

@patch_rest
def test_normal_without_error(self):
job = UnityJob(_id='N-345', cli=t_rest())
assert_that(job.messages, has_item('Success'))
assert_that(len(job.exceptions), equal_to(0))
17 changes: 15 additions & 2 deletions test/unity/resource/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from hamcrest import assert_that, equal_to, instance_of, raises

from storops.exception import UnityLunNameInUseError
from storops.exception import UnityLunNameInUseError, JobStateError
from storops.unity.enums import RaidTypeEnum, FastVPStatusEnum, \
FastVPRelocationRateEnum, PoolDataRelocationTypeEnum, \
RaidStripeWidthEnum, TierTypeEnum, PoolUnitTypeEnum, \
Expand Down Expand Up @@ -161,6 +161,7 @@ def test_create_lun_with_same_name(self):

def f():
pool.create_lun("openstack_lun")

assert_that(f, raises(UnityLunNameInUseError))

@patch_rest
Expand All @@ -180,11 +181,23 @@ def test_create_lun_with_muitl_property(self):
assert_that(lun, instance_of(UnityLun))

@patch_rest
def test_create_nfs_share(self):
def test_create_nfs_share_success(self):
pool = UnityPool(_id='pool_5', cli=t_rest())
nas_server = UnityNasServer.get(cli=t_rest(), _id='nas_6')
job = pool.create_nfs_share(
nas_server,
name='513dd8b0-2c22-4da0-888e-494d320303b6',
size=4294967296)
assert_that(JobStateEnum.COMPLETED, equal_to(job.state))

@patch_rest
def test_create_nfs_share_failed(self):
def f():
pool = UnityPool(_id='pool_1', cli=t_rest())
nas_server = UnityNasServer.get(cli=t_rest(), _id='nas_1')
pool.create_nfs_share(
nas_server,
name='job_share_failed',
size=1)

assert_that(f, raises(JobStateError, 'too small'))
5 changes: 5 additions & 0 deletions test/unity/rest_data/job/B-3-id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"content": {
"id": "B-3"
}
}
Loading

0 comments on commit 2990d51

Please sign in to comment.