]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: taskmanager: support exception handler
authorRicardo Dias <rdias@suse.com>
Wed, 11 Apr 2018 22:00:05 +0000 (23:00 +0100)
committerRicardo Dias <rdias@suse.com>
Fri, 13 Apr 2018 14:58:49 +0000 (15:58 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/tests/helper.py
src/pybind/mgr/dashboard/tests/test_task.py
src/pybind/mgr/dashboard/tools.py

index ecfd697cad5bc195eac39cf7d5d29d6498df1eea..8843cbad2e36683639e1b8b85d5d2cb2e66c8009 100644 (file)
@@ -204,7 +204,12 @@ class DashboardTestCase(MgrTestCase):
             elif method == 'DELETE':
                 cls._resp.status_code = 204
             return thread.res_task['ret_value']
-        raise Exception(thread.res_task['exception'])
+        else:
+            if 'status' in thread.res_task['exception']:
+                cls._resp.status_code = thread.res_task['exception']['status']
+            else:
+                cls._resp.status_code = 500
+            return thread.res_task['exception']
 
     @classmethod
     def _task_post(cls, url, data=None, timeout=60):
index 24095c06d510cf288335a706a0cd37c0d412bfa0..e417a1152e326604978d3220245670d0decd9053 100644 (file)
@@ -231,7 +231,8 @@ class RbdTest(DashboardTestCase):
         res = self.create_image('rbd', 'test_rbd_twice', 10240)
 
         res = self.create_image('rbd', 'test_rbd_twice', 10240)
-        self.assertEqual(res, {"errno": 17,
+        self.assertStatus(409)
+        self.assertEqual(res, {"errno": 17, "status": 409, "component": "rbd",
                                "detail": "[errno 17] error creating image"})
         self.remove_image('rbd', 'test_rbd_twice')
         self.assertStatus(204)
@@ -286,7 +287,8 @@ class RbdTest(DashboardTestCase):
 
     def test_delete_non_existent_image(self):
         res = self.remove_image('rbd', 'i_dont_exist')
-        self.assertEqual(res, {"errno": 2,
+        self.assertStatus(409)
+        self.assertEqual(res, {"errno": 2, "status": 409, "component": "rbd",
                                "detail": "[errno 2] error removing image"})
 
     def test_image_delete(self):
index 2cecd02a26ffec5da9341cdb0163c5fe6fd342b4..6ad5345a2ca0cecf677c454ded5fe1fd5f116953 100644 (file)
@@ -306,12 +306,19 @@ class Task(object):
                         md[k] = arg_map[v[1:-1]]
                 else:
                     md[k] = v
-            task = TaskManager.run(self.name, md, func, args, kwargs)
+            task = TaskManager.run(self.name, md, func, args, kwargs,
+                                   exception_handler=self.exception_handler)
             try:
                 status, value = task.wait(self.wait_for)
             except Exception as ex:
-                if self.exception_handler:
-                    return self.exception_handler(ex)
+                if task.ret_value:
+                    # exception was handled by task.exception_handler
+                    if 'status' in task.ret_value:
+                        status = task.ret_value['status']
+                    else:
+                        status = 500
+                    cherrypy.response.status = status
+                    return task.ret_value
                 raise ex
             if status == TaskManager.VALUE_EXECUTING:
                 cherrypy.response.status = 202
index 58106b5b278eab15f7690b542f75404748af8c8a..7f99aa38402463543cca559b86ac76ff1747d0f4 100644 (file)
@@ -4,6 +4,7 @@ from __future__ import absolute_import
 
 import math
 import cherrypy
+import rados
 import rbd
 
 from . import ApiController, AuthRequired, RESTController, Task
@@ -15,8 +16,11 @@ from ..tools import ViewCache
 # pylint: disable=inconsistent-return-statements
 def _rbd_exception_handler(ex):
     if isinstance(ex, rbd.OSError):
-        cherrypy.response.status = 409
-        return {'detail': str(ex), 'errno': ex.errno}
+        return {'status': 409, 'detail': str(ex), 'errno': ex.errno,
+                'component': 'rbd'}
+    elif isinstance(ex, rados.OSError):
+        return {'status': 409, 'detail': str(ex), 'errno': ex.errno,
+                'component': 'rados'}
     raise ex
 
 
index 473276f59310b2016b4f4caf6a389c4f878c97df..6eb58a4e835b61ce98873c946d828787c00b601d 100644 (file)
@@ -107,8 +107,20 @@ class ControllerTestCase(helper.CPWebCase):
         logger.info("task (%s, %s) finished", task_name, task_metadata)
         if thread.res_task['success']:
             self.body = json.dumps(thread.res_task['ret_value'])
+            if method == 'POST':
+                self.status = '201 Created'
+            elif method == 'PUT':
+                self.status = '200 OK'
+            elif method == 'DELETE':
+                self.status = '204 No Content'
+            return
+        else:
+            if 'status' in thread.res_task['exception']:
+                self.status = thread.res_task['exception']['status']
+            else:
+                self.status = 500
+            self.body = json.dumps(thread.rest_task['exception'])
             return
-        raise Exception(thread.res_task['exception'])
 
     def _task_post(self, url, data=None, timeout=60):
         self._task_request('POST', url, data, timeout)
index 3978211c4466741ab6d899f6d4fc72da7005abb2..0547a00a43e4af38b95a5a5469a3ebd4ac45a289 100644 (file)
@@ -31,24 +31,32 @@ class MyTask(object):
                 self.finish(result, None)
 
     # pylint: disable=too-many-arguments
-    def __init__(self, op_seconds, wait=False, fail=False, progress=50, is_async=False):
+    def __init__(self, op_seconds, wait=False, fail=False, progress=50,
+                 is_async=False, handle_ex=False):
         self.op_seconds = op_seconds
         self.wait = wait
         self.fail = fail
         self.progress = progress
         self.is_async = is_async
+        self.handle_ex = handle_ex
         self._event = threading.Event()
 
+    def _handle_exception(self, ex):
+        return {'status': 409, 'detail': str(ex)}
+
     def run(self, ns, timeout=None):
         args = ['dummy arg']
         kwargs = {'dummy': 'arg'}
+        h_ex = self._handle_exception if self.handle_ex else None
         if not self.is_async:
             task = TaskManager.run(
-                ns, self.metadata(), self.task_op, args, kwargs)
+                ns, self.metadata(), self.task_op, args, kwargs,
+                exception_handler=h_ex)
         else:
             task = TaskManager.run(
                 ns, self.metadata(), self.task_async_op, args, kwargs,
-                executor=MyTask.CallbackExecutor(self.fail, self.progress))
+                executor=MyTask.CallbackExecutor(self.fail, self.progress),
+                exception_handler=h_ex)
         return task.wait(timeout)
 
     def task_op(self, *args, **kwargs):
@@ -82,7 +90,8 @@ class MyTask(object):
             'wait': self.wait,
             'fail': self.fail,
             'progress': self.progress,
-            'is_async': self.is_async
+            'is_async': self.is_async,
+            'handle_ex': self.handle_ex
         }
 
 
@@ -381,4 +390,32 @@ class TaskTest(unittest.TestCase):
         self.assertEqual(fn_t[0]['progress'], 50)
         self.assertFalse(fn_t[0]['success'])
         self.assertIsNotNone(fn_t[0]['exception'])
-        self.assertEqual(fn_t[0]['exception'], "Task Unexpected Exception")
+        self.assertEqual(fn_t[0]['exception'],
+                         {"detail": "Task Unexpected Exception"})
+
+    def test_task_serialization_format_on_failure_with_handler(self):
+        task1 = MyTask(1, fail=True, handle_ex=True)
+        task1.run('test15/task1', 0.5)
+        self.wait_for_task('test15/task1')
+        ex_t, fn_t = TaskManager.list_serializable('test15/*')
+        self.assertEqual(len(ex_t), 0)
+        self.assertEqual(len(fn_t), 1)
+        # validate finished tasks attributes
+
+        try:
+            json.dumps(fn_t)
+        except TypeError as ex:
+            self.fail("Failed to serialize finished tasks: {}".format(str(ex)))
+
+        self.assertEqual(len(fn_t[0].keys()), 9)
+        self.assertEqual(fn_t[0]['name'], 'test15/task1')
+        self.assertEqual(fn_t[0]['metadata'], task1.metadata())
+        self.assertIsNotNone(fn_t[0]['begin_time'])
+        self.assertIsNotNone(fn_t[0]['end_time'])
+        self.assertGreaterEqual(fn_t[0]['duration'], 1.0)
+        self.assertEqual(fn_t[0]['progress'], 50)
+        self.assertFalse(fn_t[0]['success'])
+        self.assertIsNotNone(fn_t[0]['exception'])
+        self.assertEqual(fn_t[0]['exception'],
+                         {"status": 409,
+                          "detail": "Task Unexpected Exception"})
index 7c05e1a098b215c40baae71ec32dab142a8319a4..911251ae7a7f3baae3bc6fc59c863fc1eb7168bd 100644 (file)
@@ -413,14 +413,16 @@ class TaskManager(object):
             cls._finished_tasks.append(task)
 
     @classmethod
-    def run(cls, name, metadata, fn, args=None, kwargs=None, executor=None):
+    def run(cls, name, metadata, fn, args=None, kwargs=None, executor=None,
+            exception_handler=None):
         if not args:
             args = []
         if not kwargs:
             kwargs = {}
         if not executor:
             executor = ThreadedExecutor()
-        task = Task(name, metadata, fn, args, kwargs, executor)
+        task = Task(name, metadata, fn, args, kwargs, executor,
+                    exception_handler)
         with cls._lock:
             if task in cls._executing_tasks:
                 logger.debug("TM: task already executing: %s", task)
@@ -488,8 +490,9 @@ class TaskManager(object):
             'duration': t.duration,
             'progress': t.progress,
             'success': not t.exception,
-            'ret_value': t.ret_value,
-            'exception': str(t.exception) if t.exception else None
+            'ret_value': t.ret_value if not t.exception else None,
+            'exception': t.ret_value if t.exception and t.ret_value else (
+                {'detail': str(t.exception)} if t.exception else None)
         } for t in fn_t]
 
 
@@ -539,13 +542,15 @@ class ThreadedExecutor(TaskExecutor):
 
 
 class Task(object):
-    def __init__(self, name, metadata, fn, args, kwargs, executor):
+    def __init__(self, name, metadata, fn, args, kwargs, executor,
+                 exception_handler=None):
         self.name = name
         self.metadata = metadata
         self.fn = fn
         self.fn_args = args
         self.fn_kwargs = kwargs
         self.executor = executor
+        self.ex_handler = exception_handler
         self.running = False
         self.event = threading.Event()
         self.progress = None
@@ -577,6 +582,12 @@ class Task(object):
 
     def _complete(self, ret_value, exception=None):
         now = time.time()
+        if exception and self.ex_handler:
+            # pylint: disable=broad-except
+            try:
+                ret_value = self.ex_handler(exception)
+            except Exception as ex:
+                exception = ex
         with self.lock:
             assert self.running, "_complete cannot be called before _run"
             self.end_time = now