Browse Source

Implement overcloud delete command

This change adds an overcloud delete that will delete the stack and
issue a plan delete for the overcloud in a single command.

Change-Id: I97a2b5606f47deb929972c06c869cd1eda0dc9a6
Closes-Bug: #1632271
tags/5.6.0
Alex Schultz 2 years ago
parent
commit
7dd16b1da2

+ 1
- 0
setup.cfg View File

@@ -63,6 +63,7 @@ openstack.tripleoclient.v1 =
63 63
     baremetal_configure_ready_state = tripleoclient.v1.baremetal:ConfigureReadyState
64 64
     baremetal_configure_boot = tripleoclient.v1.baremetal:ConfigureBaremetalBoot
65 65
     overcloud_netenv_validate = tripleoclient.v1.overcloud_netenv_validate:ValidateOvercloudNetenv
66
+    overcloud_delete = tripleoclient.v1.overcloud_delete:DeleteOvercloud
66 67
     overcloud_deploy = tripleoclient.v1.overcloud_deploy:DeployOvercloud
67 68
     overcloud_image_build = tripleoclient.v1.overcloud_image:BuildOvercloudImage
68 69
     overcloud_image_upload = tripleoclient.v1.overcloud_image:UploadOvercloudImage

+ 42
- 0
tripleoclient/tests/test_utils.py View File

@@ -656,6 +656,48 @@ class TestAssignVerifyProfiles(TestCase):
656 656
         self._test(0, 0)
657 657
 
658 658
 
659
+class TestPromptUser(TestCase):
660
+    def setUp(self):
661
+        super(TestPromptUser, self).setUp()
662
+        self.logger = mock.MagicMock()
663
+        self.logger.info = mock.MagicMock()
664
+
665
+    @mock.patch('sys.stdin')
666
+    def test_user_accepts(self, stdin_mock):
667
+        stdin_mock.isatty.return_value = True
668
+        stdin_mock.readline.return_value = "yes"
669
+        result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
670
+        self.assertTrue(result)
671
+
672
+    @mock.patch('sys.stdin')
673
+    def test_user_declines(self, stdin_mock):
674
+        stdin_mock.isatty.return_value = True
675
+        stdin_mock.readline.return_value = "no"
676
+        result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
677
+        self.assertFalse(result)
678
+
679
+    @mock.patch('sys.stdin')
680
+    def test_user_no_tty(self, stdin_mock):
681
+        stdin_mock.isatty.return_value = False
682
+        stdin_mock.readline.return_value = "yes"
683
+        result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
684
+        self.assertFalse(result)
685
+
686
+    @mock.patch('sys.stdin')
687
+    def test_user_aborts_control_c(self, stdin_mock):
688
+        stdin_mock.isatty.return_value = False
689
+        stdin_mock.readline.side_effect = KeyboardInterrupt()
690
+        result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
691
+        self.assertFalse(result)
692
+
693
+    @mock.patch('sys.stdin')
694
+    def test_user_aborts_with_control_d(self, stdin_mock):
695
+        stdin_mock.isatty.return_value = False
696
+        stdin_mock.readline.side_effect = EOFError()
697
+        result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
698
+        self.assertFalse(result)
699
+
700
+
659 701
 class TestReplaceLinks(TestCase):
660 702
 
661 703
     def setUp(self):

+ 0
- 0
tripleoclient/tests/v1/overcloud_delete/__init__.py View File


+ 66
- 0
tripleoclient/tests/v1/overcloud_delete/test_overcloud_delete.py View File

@@ -0,0 +1,66 @@
1
+#   Copyright 2016 Red Hat, Inc.
2
+#
3
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#   not use this file except in compliance with the License. You may obtain
5
+#   a copy of the License at
6
+#
7
+#        http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#   Unless required by applicable law or agreed to in writing, software
10
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#   License for the specific language governing permissions and limitations
13
+#   under the License.
14
+#
15
+
16
+import mock
17
+
18
+from tripleoclient.tests.v1.overcloud_deploy import fakes
19
+from tripleoclient.v1 import overcloud_delete
20
+
21
+
22
+class TestDeleteOvercloud(fakes.TestDeployOvercloud):
23
+
24
+    def setUp(self):
25
+        super(TestDeleteOvercloud, self).setUp()
26
+
27
+        self.cmd = overcloud_delete.DeleteOvercloud(self.app, None)
28
+        self.app.client_manager.workflow_engine = mock.Mock()
29
+        self.workflow = self.app.client_manager.workflow_engine
30
+
31
+    @mock.patch('tripleoclient.utils.wait_for_stack_ready',
32
+                autospec=True)
33
+    def test_stack_delete(self, wait_for_stack_ready_mock):
34
+        clients = self.app.client_manager
35
+        orchestration_client = clients.orchestration
36
+
37
+        self.cmd._stack_delete(orchestration_client, 'overcloud')
38
+
39
+        orchestration_client.stacks.get.assert_called_once_with('overcloud')
40
+        wait_for_stack_ready_mock.assert_called_once_with(
41
+            orchestration_client=orchestration_client,
42
+            stack_name='overcloud',
43
+            action='DELETE'
44
+        )
45
+
46
+    def test_stack_delete_no_stack(self):
47
+        clients = self.app.client_manager
48
+        orchestration_client = clients.orchestration
49
+        type(orchestration_client.stacks.get).return_value = None
50
+        self.cmd.log.warning = mock.MagicMock()
51
+
52
+        self.cmd._stack_delete(orchestration_client, 'overcloud')
53
+
54
+        orchestration_client.stacks.get.assert_called_once_with('overcloud')
55
+        self.cmd.log.warning.assert_called_once_with(
56
+            "No stack found ('overcloud'), skipping delete")
57
+
58
+    @mock.patch(
59
+        'tripleoclient.workflows.plan_management.delete_deployment_plan',
60
+        autospec=True)
61
+    def test_plan_delete(self, delete_deployment_plan_mock):
62
+        self.cmd._plan_delete(self.workflow, 'overcloud')
63
+
64
+        delete_deployment_plan_mock.assert_called_once_with(
65
+            self.workflow,
66
+            input={'container': 'overcloud'})

+ 17
- 15
tripleoclient/tests/v1/test_overcloud_plan.py View File

@@ -58,33 +58,35 @@ class TestOvercloudDeletePlan(utils.TestCommand):
58 58
         self.app.client_manager.workflow_engine = mock.Mock()
59 59
         self.workflow = self.app.client_manager.workflow_engine
60 60
 
61
-    def test_delete_plan(self):
61
+    @mock.patch(
62
+        'tripleoclient.workflows.plan_management.delete_deployment_plan',
63
+        autospec=True)
64
+    def test_delete_plan(self, delete_deployment_plan_mock):
62 65
         parsed_args = self.check_parser(self.cmd, ['test-plan'],
63 66
                                         [('plans', ['test-plan'])])
64 67
 
65
-        self.workflow.action_executions.create.return_value = (
66
-            mock.Mock(output='{"result": null}'))
67
-
68 68
         self.cmd.take_action(parsed_args)
69 69
 
70
-        self.workflow.action_executions.create.assert_called_once_with(
71
-            'tripleo.plan.delete', input={'container': 'test-plan'})
70
+        delete_deployment_plan_mock.assert_called_once_with(
71
+            self.workflow,
72
+            input={'container': 'test-plan'})
72 73
 
73
-    def test_delete_multiple_plans(self):
74
+    @mock.patch(
75
+        'tripleoclient.workflows.plan_management.delete_deployment_plan',
76
+        autospec=True)
77
+    def test_delete_multiple_plans(self, delete_deployment_plan_mock):
74 78
         argslist = ['test-plan1', 'test-plan2']
75 79
         verifylist = [('plans', ['test-plan1', 'test-plan2'])]
76 80
         parsed_args = self.check_parser(self.cmd, argslist, verifylist)
77 81
 
78
-        self.workflow.action_executions.create.return_value = (
79
-            mock.Mock(output='{"result": null}'))
80
-
81 82
         self.cmd.take_action(parsed_args)
82 83
 
83
-        self.workflow.action_executions.create.assert_has_calls(
84
-            [mock.call('tripleo.plan.delete',
85
-                       input={'container': 'test-plan1'}),
86
-             mock.call('tripleo.plan.delete',
87
-                       input={'container': 'test-plan2'})])
84
+        expected = [
85
+            mock.call(self.workflow, input={'container': 'test-plan1'}),
86
+            mock.call(self.workflow, input={'container': 'test-plan2'}),
87
+        ]
88
+        self.assertEqual(delete_deployment_plan_mock.call_args_list,
89
+                         expected)
88 90
 
89 91
 
90 92
 class TestOvercloudCreatePlan(utils.TestCommand):

+ 13
- 0
tripleoclient/tests/workflows/test_plan_management.py View File

@@ -112,3 +112,16 @@ class TestPlanCreationWorkflows(utils.TestCommand):
112 112
 
113 113
         self.tripleoclient.object_store.put_object.assert_called_once_with(
114 114
             'test-overcloud', 'roles_data.yaml', mock_open_context())
115
+
116
+    def test_delete_plan(self):
117
+        self.workflow.action_executions.create.return_value = (
118
+            mock.Mock(output='{"result": null}'))
119
+
120
+        plan_management.delete_deployment_plan(
121
+            self.workflow,
122
+            input={'container': 'overcloud'})
123
+
124
+        self.workflow.action_executions.create.assert_called_once_with(
125
+            'tripleo.plan.delete',
126
+            {'input': {'container': 'overcloud'}},
127
+            run_sync=True, save_result=True)

+ 37
- 0
tripleoclient/utils.py View File

@@ -33,6 +33,7 @@ import yaml
33 33
 from heatclient.common import event_utils
34 34
 from heatclient.exc import HTTPNotFound
35 35
 from osc_lib.i18n import _
36
+from osc_lib.i18n import _LI
36 37
 from six.moves import configparser
37 38
 from six.moves import urllib
38 39
 
@@ -819,6 +820,42 @@ def parse_env_file(env_file, file_type=None):
819 820
     return nodes_config
820 821
 
821 822
 
823
+def prompt_user_for_confirmation(message, logger, positive_response='y'):
824
+    """Prompt user for a y/N confirmation
825
+
826
+    Use this function to prompt the user for a y/N confirmation
827
+    with the provided message. The [y/N] should be included in
828
+    the provided message to this function to indicate the expected
829
+    input for confirmation. You can customize the positive response if
830
+    y/N is not a desired input.
831
+
832
+    :param message: Confirmation string prompt
833
+    :param logger: logger object used to write info logs
834
+    :param positive_response: Beginning character for a positive user input
835
+    :return boolean true for valid confirmation, false for all others
836
+    """
837
+    try:
838
+        if not sys.stdin.isatty():
839
+            logger.error(_LI('User interaction required, cannot confirm.'))
840
+            return False
841
+        else:
842
+            sys.stdout.write(message)
843
+            prompt_response = sys.stdin.readline().lower()
844
+            if not prompt_response.startswith(positive_response):
845
+                logger.info(_LI(
846
+                    'User did not confirm action so taking no action.'))
847
+                return False
848
+            logger.info(_LI('User confirmed action.'))
849
+            return True
850
+    except KeyboardInterrupt:  # ctrl-c
851
+        logger.info(_LI(
852
+            'User did not confirm action (ctrl-c) so taking no action.'))
853
+    except EOFError:  # ctrl-d
854
+        logger.info(_LI(
855
+            'User did not confirm action (ctrl-d) so taking no action.'))
856
+    return False
857
+
858
+
822 859
 def replace_links_in_template_contents(contents, link_replacement):
823 860
     """Replace get_file and type file links in Heat template contents
824 861
 

+ 97
- 0
tripleoclient/v1/overcloud_delete.py View File

@@ -0,0 +1,97 @@
1
+#   Copyright 2016 Red Hat, Inc.
2
+#
3
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#   not use this file except in compliance with the License. You may obtain
5
+#   a copy of the License at
6
+#
7
+#        http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#   Unless required by applicable law or agreed to in writing, software
10
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#   License for the specific language governing permissions and limitations
13
+#   under the License.
14
+#
15
+
16
+import logging
17
+
18
+from osc_lib.command import command
19
+from osc_lib import exceptions as oscexc
20
+from osc_lib.i18n import _
21
+from osc_lib import utils as osc_utils
22
+
23
+from tripleoclient import utils
24
+from tripleoclient.workflows import plan_management
25
+
26
+
27
+class DeleteOvercloud(command.Command):
28
+    """Delete overcloud stack and plan"""
29
+
30
+    log = logging.getLogger(__name__ + ".DeleteOvercloud")
31
+
32
+    def get_parser(self, prog_name):
33
+        parser = super(DeleteOvercloud, self).get_parser(prog_name)
34
+        parser.add_argument('stack', nargs='?',
35
+                            help=_('Name or ID of heat stack to delete'
36
+                                   '(default=Env: OVERCLOUD_STACK_NAME)'),
37
+                            default=osc_utils.env('OVERCLOUD_STACK_NAME'))
38
+        parser.add_argument('-y', '--yes',
39
+                            help=_('Skip yes/no prompt (assume yes).'),
40
+                            default=False,
41
+                            action="store_true")
42
+        return parser
43
+
44
+    def _validate_args(self, parsed_args):
45
+        if parsed_args.stack in (None, ''):
46
+            raise oscexc.CommandError(
47
+                "You must specify a stack name")
48
+
49
+    def _stack_delete(self, orchestration_client, stack_name):
50
+        print("Deleting stack {s}...".format(s=stack_name))
51
+        stack = utils.get_stack(orchestration_client, stack_name)
52
+        if stack is None:
53
+            self.log.warning("No stack found ('{s}'), skipping delete".
54
+                             format(s=stack_name))
55
+        else:
56
+            try:
57
+                utils.wait_for_stack_ready(
58
+                    orchestration_client=orchestration_client,
59
+                    stack_name=stack_name,
60
+                    action='DELETE')
61
+            except Exception as e:
62
+                self.log.error("Exception while waiting for stack to delete "
63
+                               "{}".format(e))
64
+                raise oscexc.CommandError(
65
+                    "Error occurred while waiting for stack to delete {}".
66
+                    format(e))
67
+
68
+    def _plan_delete(self, workflow_client, stack_name):
69
+        print("Deleting plan {s}...".format(s=stack_name))
70
+        try:
71
+            plan_management.delete_deployment_plan(
72
+                workflow_client,
73
+                input={'container': stack_name})
74
+        except Exception as err:
75
+            raise oscexc.CommandError(
76
+                "Error occurred while deleting plan {}".format(err))
77
+
78
+    def take_action(self, parsed_args):
79
+        self.log.debug("take_action({args})".format(args=parsed_args))
80
+
81
+        self._validate_args(parsed_args)
82
+
83
+        if not parsed_args.yes:
84
+            confirm = utils.prompt_user_for_confirmation(
85
+                message=_("Are you sure you want to delete this overcloud "
86
+                          "[y/N]?"),
87
+                logger=self.log)
88
+            if not confirm:
89
+                raise oscexc.CommandError("Action not confirmed, exiting.")
90
+
91
+        clients = self.app.client_manager
92
+        orchestration_client = clients.orchestration
93
+        workflow_client = self.app.client_manager.workflow_engine
94
+
95
+        self._stack_delete(orchestration_client, parsed_args.stack)
96
+        self._plan_delete(workflow_client, parsed_args.stack)
97
+        print("Success.")

+ 4
- 8
tripleoclient/v1/overcloud_plan.py View File

@@ -68,16 +68,12 @@ class DeletePlan(command.Command):
68 68
 
69 69
         for plan in parsed_args.plans:
70 70
             print("Deleting plan %s..." % plan)
71
-            execution = workflow_client.action_executions.create(
72
-                'tripleo.plan.delete', input={'container': plan})
73
-
71
+            workflow_input = {'container': plan}
74 72
             try:
75
-                json_results = json.loads(execution.output)['result']
76
-                if json_results is not None:
77
-                    print(json_results)
73
+                plan_management.delete_deployment_plan(workflow_client,
74
+                                                       input=workflow_input)
78 75
             except Exception:
79
-                self.log.exception(
80
-                    "Error parsing action result %s", execution.output)
76
+                self.log.exception("Error deleting plan")
81 77
 
82 78
 
83 79
 class CreatePlan(command.Command):

+ 12
- 0
tripleoclient/workflows/plan_management.py View File

@@ -82,6 +82,18 @@ def create_deployment_plan(clients, **workflow_input):
82 82
             'Exception creating plan: {}'.format(payload['message']))
83 83
 
84 84
 
85
+def delete_deployment_plan(workflow_client, **input_):
86
+    try:
87
+        results = base.call_action(workflow_client,
88
+                                   'tripleo.plan.delete',
89
+                                   **input_)
90
+        if results is not None:
91
+            print(results)
92
+    except Exception as err:
93
+        raise exceptions.WorkflowServiceError(
94
+            'Exception deleting plan: {}'.format(err))
95
+
96
+
85 97
 def update_deployment_plan(clients, **workflow_input):
86 98
     payload = _create_update_deployment_plan(
87 99
         clients, 'tripleo.plan_management.v1.update_deployment_plan',

Loading…
Cancel
Save