From 3609a0d4f86488a9eb68066675c758f49cf4c52d Mon Sep 17 00:00:00 2001
From: LingxianKong <konglingxian@huawei.com>
Date: Fri, 24 Apr 2015 19:04:13 +0800
Subject: [PATCH] Support resource deletion in batches

Support CLI commands like:

usage: mistral workflow-delete [-h] name [name ...]

Change-Id: I467ecfddf88d30e437d00de2671c2a0487557693
Implements: blueprint mistral-batch-deleting-support
---
 mistralclient/commands/v2/actions.py          | 11 +++++--
 mistralclient/commands/v2/cron_triggers.py    | 11 +++++--
 mistralclient/commands/v2/environments.py     | 12 +++++--
 mistralclient/commands/v2/executions.py       | 15 +++++++--
 mistralclient/commands/v2/workbooks.py        | 11 +++++--
 mistralclient/commands/v2/workflows.py        | 11 +++++--
 .../tests/unit/v2/test_cli_actions.py         | 16 ++++++++--
 .../tests/unit/v2/test_cli_cron_triggers.py   | 16 ++++++++--
 .../tests/unit/v2/test_cli_environments.py    | 16 ++++++++--
 .../tests/unit/v2/test_cli_executions.py      | 16 ++++++++--
 .../tests/unit/v2/test_cli_workbooks.py       | 16 ++++++++--
 .../tests/unit/v2/test_cli_workflows.py       | 16 ++++++++--
 mistralclient/utils.py                        | 31 +++++++++++++++++++
 13 files changed, 170 insertions(+), 28 deletions(-)
 create mode 100644 mistralclient/utils.py

diff --git a/mistralclient/commands/v2/actions.py b/mistralclient/commands/v2/actions.py
index 01ea0af2..ad14fb25 100644
--- a/mistralclient/commands/v2/actions.py
+++ b/mistralclient/commands/v2/actions.py
@@ -22,6 +22,7 @@ from cliff import show
 
 from mistralclient.api.v2 import actions
 from mistralclient.commands.v2 import base
+from mistralclient import utils
 
 LOG = logging.getLogger(__name__)
 
@@ -125,12 +126,18 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('name', help='Action name')
+        parser.add_argument('name', nargs='+', help='Name of action(s).')
 
         return parser
 
     def take_action(self, parsed_args):
-        actions.ActionManager(self.app.client).delete(parsed_args.name)
+        action_mgr = actions.ActionManager(self.app.client)
+        utils.do_action_on_many(
+            lambda s: action_mgr.delete(s),
+            parsed_args.name,
+            "Request to delete action %s has been accepted.",
+            "Unable to delete the specified action(s)."
+        )
 
 
 class Update(base.MistralLister):
diff --git a/mistralclient/commands/v2/cron_triggers.py b/mistralclient/commands/v2/cron_triggers.py
index 9972f563..e975265d 100644
--- a/mistralclient/commands/v2/cron_triggers.py
+++ b/mistralclient/commands/v2/cron_triggers.py
@@ -22,6 +22,7 @@ from cliff import show
 
 from mistralclient.api.v2 import cron_triggers
 from mistralclient.commands.v2 import base
+from mistralclient import utils
 
 LOG = logging.getLogger(__name__)
 
@@ -160,11 +161,15 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('name', help='Cron trigger name')
+        parser.add_argument('name', nargs='+', help='Name of cron trigger(s).')
 
         return parser
 
     def take_action(self, parsed_args):
         mgr = cron_triggers.CronTriggerManager(self.app.client)
-
-        mgr.delete(parsed_args.name)
+        utils.do_action_on_many(
+            lambda s: mgr.delete(s),
+            parsed_args.name,
+            "Request to delete cron trigger %s has been accepted.",
+            "Unable to delete the specified cron trigger(s)."
+        )
diff --git a/mistralclient/commands/v2/environments.py b/mistralclient/commands/v2/environments.py
index ef181c6f..90bc1c5f 100644
--- a/mistralclient/commands/v2/environments.py
+++ b/mistralclient/commands/v2/environments.py
@@ -22,6 +22,7 @@ import yaml
 
 from mistralclient.api.v2 import environments
 from mistralclient.commands.v2 import base
+from mistralclient import utils
 
 LOG = logging.getLogger(__name__)
 
@@ -153,13 +154,18 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('name', help='Environment name')
+        parser.add_argument('name', nargs='+', help='Name of environment(s).')
 
         return parser
 
     def take_action(self, parsed_args):
-        environments.EnvironmentManager(self.app.client).delete(
-            parsed_args.name)
+        env_mgr = environments.EnvironmentManager(self.app.client)
+        utils.do_action_on_many(
+            lambda s: env_mgr.delete(s),
+            parsed_args.name,
+            "Request to delete environment %s has been accepted.",
+            "Unable to delete the specified environment(s)."
+        )
 
 
 class Update(show.ShowOne):
diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py
index ba6f33f6..b065ad84 100644
--- a/mistralclient/commands/v2/executions.py
+++ b/mistralclient/commands/v2/executions.py
@@ -22,6 +22,7 @@ from cliff import show
 
 from mistralclient.api.v2 import executions
 from mistralclient.commands.v2 import base
+from mistralclient import utils
 
 LOG = logging.getLogger(__name__)
 
@@ -140,12 +141,22 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('id', help='Execution identifier')
+        parser.add_argument(
+            'id',
+            nargs='+',
+            help='Id of execution identifier(s).'
+        )
 
         return parser
 
     def take_action(self, parsed_args):
-        executions.ExecutionManager(self.app.client).delete(parsed_args.id)
+        exe_mgr = executions.ExecutionManager(self.app.client)
+        utils.do_action_on_many(
+            lambda s: exe_mgr.delete(s),
+            parsed_args.id,
+            "Request to delete execution %s has been accepted.",
+            "Unable to delete the specified execution(s)."
+        )
 
 
 class Update(show.ShowOne):
diff --git a/mistralclient/commands/v2/workbooks.py b/mistralclient/commands/v2/workbooks.py
index 635eec65..8b9edccc 100644
--- a/mistralclient/commands/v2/workbooks.py
+++ b/mistralclient/commands/v2/workbooks.py
@@ -22,6 +22,7 @@ from cliff import show
 from mistralclient.api.v2 import workbooks
 from mistralclient.commands.v2 import base
 from mistralclient import exceptions as exc
+from mistralclient import utils
 
 
 LOG = logging.getLogger(__name__)
@@ -110,12 +111,18 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('name', help='Workbook name')
+        parser.add_argument('name', nargs='+', help='Name of workbook(s).')
 
         return parser
 
     def take_action(self, parsed_args):
-        workbooks.WorkbookManager(self.app.client).delete(parsed_args.name)
+        wb_mgr = workbooks.WorkbookManager(self.app.client)
+        utils.do_action_on_many(
+            lambda s: wb_mgr.delete(s),
+            parsed_args.name,
+            "Request to delete workbook %s has been accepted.",
+            "Unable to delete the specified workbook(s)."
+        )
 
 
 class Update(show.ShowOne):
diff --git a/mistralclient/commands/v2/workflows.py b/mistralclient/commands/v2/workflows.py
index 6138214e..47abb110 100644
--- a/mistralclient/commands/v2/workflows.py
+++ b/mistralclient/commands/v2/workflows.py
@@ -22,6 +22,7 @@ from cliff import show
 from mistralclient.api.v2 import workflows
 from mistralclient.commands.v2 import base
 from mistralclient import exceptions as exc
+from mistralclient import utils
 
 
 LOG = logging.getLogger(__name__)
@@ -119,12 +120,18 @@ class Delete(command.Command):
     def get_parser(self, prog_name):
         parser = super(Delete, self).get_parser(prog_name)
 
-        parser.add_argument('name', help='Workflow name')
+        parser.add_argument('name', nargs='+', help='Name of workflow(s).')
 
         return parser
 
     def take_action(self, parsed_args):
-        workflows.WorkflowManager(self.app.client).delete(parsed_args.name)
+        wf_mgr = workflows.WorkflowManager(self.app.client)
+        utils.do_action_on_many(
+            lambda s: wf_mgr.delete(s),
+            parsed_args.name,
+            "Request to delete workflow %s has been accepted.",
+            "Unable to delete the specified workflow(s)."
+        )
 
 
 class Update(base.MistralLister):
diff --git a/mistralclient/tests/unit/v2/test_cli_actions.py b/mistralclient/tests/unit/v2/test_cli_actions.py
index 3efc7e71..a8df078b 100644
--- a/mistralclient/tests/unit/v2/test_cli_actions.py
+++ b/mistralclient/tests/unit/v2/test_cli_actions.py
@@ -95,8 +95,20 @@ class TestCLIActionsV2(base.BaseCommandTest):
         )
 
     @mock.patch('mistralclient.api.v2.actions.ActionManager.delete')
-    def test_delete(self, mock):
-        self.assertIsNone(self.call(action_cmd.Delete, app_args=['name']))
+    def test_delete(self, del_mock):
+        self.call(action_cmd.Delete, app_args=['name'])
+
+        del_mock.assert_called_once_with('name')
+
+    @mock.patch('mistralclient.api.v2.actions.ActionManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(action_cmd.Delete, app_args=['name1', 'name2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('name1'), mock.call('name2')],
+            del_mock.call_args_list
+        )
 
     @mock.patch('mistralclient.api.v2.actions.'
                 'ActionManager.get')
diff --git a/mistralclient/tests/unit/v2/test_cli_cron_triggers.py b/mistralclient/tests/unit/v2/test_cli_cron_triggers.py
index 1ea865ca..62520e70 100644
--- a/mistralclient/tests/unit/v2/test_cli_cron_triggers.py
+++ b/mistralclient/tests/unit/v2/test_cli_cron_triggers.py
@@ -80,7 +80,17 @@ class TestCLIWorkbooksV2(base.BaseCommandTest):
         )
 
     @mock.patch('mistralclient.api.v2.cron_triggers.CronTriggerManager.delete')
-    def test_delete(self, mock):
-        self.assertIsNone(
-            self.call(cron_triggers_cmd.Delete, app_args=['name'])
+    def test_delete(self, del_mock):
+        self.call(cron_triggers_cmd.Delete, app_args=['name'])
+
+        del_mock.assert_called_once_with('name')
+
+    @mock.patch('mistralclient.api.v2.cron_triggers.CronTriggerManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(cron_triggers_cmd.Delete, app_args=['name1', 'name2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('name1'), mock.call('name2')],
+            del_mock.call_args_list
         )
diff --git a/mistralclient/tests/unit/v2/test_cli_environments.py b/mistralclient/tests/unit/v2/test_cli_environments.py
index 0c7af656..8dbbfed7 100644
--- a/mistralclient/tests/unit/v2/test_cli_environments.py
+++ b/mistralclient/tests/unit/v2/test_cli_environments.py
@@ -115,5 +115,17 @@ class TestCLIEnvironmentsV2(base.BaseCommandTest):
         self.assertEqual(EXPECTED_RESULT, result[1])
 
     @mock.patch('mistralclient.api.v2.environments.EnvironmentManager.delete')
-    def test_delete(self, mock):
-        self.assertIsNone(self.call(environment_cmd.Delete, app_args=['name']))
+    def test_delete(self, del_mock):
+        self.call(environment_cmd.Delete, app_args=['name'])
+
+        del_mock.assert_called_once_with('name')
+
+    @mock.patch('mistralclient.api.v2.environments.EnvironmentManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(environment_cmd.Delete, app_args=['name1', 'name2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('name1'), mock.call('name2')],
+            del_mock.call_args_list
+        )
diff --git a/mistralclient/tests/unit/v2/test_cli_executions.py b/mistralclient/tests/unit/v2/test_cli_executions.py
index 4f5c8a21..e6b29317 100644
--- a/mistralclient/tests/unit/v2/test_cli_executions.py
+++ b/mistralclient/tests/unit/v2/test_cli_executions.py
@@ -82,7 +82,17 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
                           '1', '1'), result[1])
 
     @mock.patch('mistralclient.api.v2.executions.ExecutionManager.delete')
-    def test_delete(self, mock):
-        result = self.call(execution_cmd.Delete, app_args=['id'])
+    def test_delete(self, del_mock):
+        self.call(execution_cmd.Delete, app_args=['id'])
 
-        self.assertIsNone(result)
+        del_mock.assert_called_once_with('id')
+
+    @mock.patch('mistralclient.api.v2.executions.ExecutionManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(execution_cmd.Delete, app_args=['id1', 'id2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('id1'), mock.call('id2')],
+            del_mock.call_args_list
+        )
diff --git a/mistralclient/tests/unit/v2/test_cli_workbooks.py b/mistralclient/tests/unit/v2/test_cli_workbooks.py
index 1530680c..b8c55fae 100644
--- a/mistralclient/tests/unit/v2/test_cli_workbooks.py
+++ b/mistralclient/tests/unit/v2/test_cli_workbooks.py
@@ -86,8 +86,20 @@ class TestCLIWorkbooksV2(base.BaseCommandTest):
         self.assertEqual(('a', 'a, b', '1', '1'), result[1])
 
     @mock.patch('mistralclient.api.v2.workbooks.WorkbookManager.delete')
-    def test_delete(self, mock):
-        self.assertIsNone(self.call(workbook_cmd.Delete, app_args=['name']))
+    def test_delete(self, del_mock):
+        self.call(workbook_cmd.Delete, app_args=['name'])
+
+        del_mock.assert_called_once_with('name')
+
+    @mock.patch('mistralclient.api.v2.workbooks.WorkbookManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(workbook_cmd.Delete, app_args=['name1', 'name2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('name1'), mock.call('name2')],
+            del_mock.call_args_list
+        )
 
     @mock.patch('mistralclient.api.v2.workbooks.WorkbookManager.get')
     def test_get_definition(self, mock):
diff --git a/mistralclient/tests/unit/v2/test_cli_workflows.py b/mistralclient/tests/unit/v2/test_cli_workflows.py
index c15ae4e7..51202b8b 100644
--- a/mistralclient/tests/unit/v2/test_cli_workflows.py
+++ b/mistralclient/tests/unit/v2/test_cli_workflows.py
@@ -80,8 +80,20 @@ class TestCLIWorkflowsV2(base.BaseCommandTest):
         self.assertEqual(('a', 'a, b', 'param', '1', '1'), result[1])
 
     @mock.patch('mistralclient.api.v2.workflows.WorkflowManager.delete')
-    def test_delete(self, mock):
-        self.assertIsNone(self.call(workflow_cmd.Delete, app_args=['name']))
+    def test_delete(self, del_mock):
+        self.call(workflow_cmd.Delete, app_args=['name'])
+
+        del_mock.assert_called_once_with('name')
+
+    @mock.patch('mistralclient.api.v2.workflows.WorkflowManager.delete')
+    def test_delete_with_multi_names(self, del_mock):
+        self.call(workflow_cmd.Delete, app_args=['name1', 'name2'])
+
+        self.assertEqual(2, del_mock.call_count)
+        self.assertEqual(
+            [mock.call('name1'), mock.call('name2')],
+            del_mock.call_args_list
+        )
 
     @mock.patch('mistralclient.api.v2.workflows.'
                 'WorkflowManager.get')
diff --git a/mistralclient/utils.py b/mistralclient/utils.py
new file mode 100644
index 00000000..748d7321
--- /dev/null
+++ b/mistralclient/utils.py
@@ -0,0 +1,31 @@
+# Copyright 2015 - Huawei Technologies Co. Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+
+import exceptions
+
+
+def do_action_on_many(action, resources, success_msg, error_msg):
+    """Helper to run an action on many resources."""
+    failure_flag = False
+
+    for resource in resources:
+        try:
+            action(resource)
+            print(success_msg % resource)
+        except Exception as e:
+            failure_flag = True
+            print(e)
+
+    if failure_flag:
+        raise exceptions.MistralClientException(error_msg)