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
This commit is contained in:
parent
c212fbd065
commit
7dd16b1da2
@ -63,6 +63,7 @@ openstack.tripleoclient.v1 =
|
||||
baremetal_configure_ready_state = tripleoclient.v1.baremetal:ConfigureReadyState
|
||||
baremetal_configure_boot = tripleoclient.v1.baremetal:ConfigureBaremetalBoot
|
||||
overcloud_netenv_validate = tripleoclient.v1.overcloud_netenv_validate:ValidateOvercloudNetenv
|
||||
overcloud_delete = tripleoclient.v1.overcloud_delete:DeleteOvercloud
|
||||
overcloud_deploy = tripleoclient.v1.overcloud_deploy:DeployOvercloud
|
||||
overcloud_image_build = tripleoclient.v1.overcloud_image:BuildOvercloudImage
|
||||
overcloud_image_upload = tripleoclient.v1.overcloud_image:UploadOvercloudImage
|
||||
|
@ -656,6 +656,48 @@ class TestAssignVerifyProfiles(TestCase):
|
||||
self._test(0, 0)
|
||||
|
||||
|
||||
class TestPromptUser(TestCase):
|
||||
def setUp(self):
|
||||
super(TestPromptUser, self).setUp()
|
||||
self.logger = mock.MagicMock()
|
||||
self.logger.info = mock.MagicMock()
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_user_accepts(self, stdin_mock):
|
||||
stdin_mock.isatty.return_value = True
|
||||
stdin_mock.readline.return_value = "yes"
|
||||
result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
|
||||
self.assertTrue(result)
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_user_declines(self, stdin_mock):
|
||||
stdin_mock.isatty.return_value = True
|
||||
stdin_mock.readline.return_value = "no"
|
||||
result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_user_no_tty(self, stdin_mock):
|
||||
stdin_mock.isatty.return_value = False
|
||||
stdin_mock.readline.return_value = "yes"
|
||||
result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_user_aborts_control_c(self, stdin_mock):
|
||||
stdin_mock.isatty.return_value = False
|
||||
stdin_mock.readline.side_effect = KeyboardInterrupt()
|
||||
result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_user_aborts_with_control_d(self, stdin_mock):
|
||||
stdin_mock.isatty.return_value = False
|
||||
stdin_mock.readline.side_effect = EOFError()
|
||||
result = utils.prompt_user_for_confirmation("[y/N]?", self.logger)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class TestReplaceLinks(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
0
tripleoclient/tests/v1/overcloud_delete/__init__.py
Normal file
0
tripleoclient/tests/v1/overcloud_delete/__init__.py
Normal file
@ -0,0 +1,66 @@
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from tripleoclient.tests.v1.overcloud_deploy import fakes
|
||||
from tripleoclient.v1 import overcloud_delete
|
||||
|
||||
|
||||
class TestDeleteOvercloud(fakes.TestDeployOvercloud):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeleteOvercloud, self).setUp()
|
||||
|
||||
self.cmd = overcloud_delete.DeleteOvercloud(self.app, None)
|
||||
self.app.client_manager.workflow_engine = mock.Mock()
|
||||
self.workflow = self.app.client_manager.workflow_engine
|
||||
|
||||
@mock.patch('tripleoclient.utils.wait_for_stack_ready',
|
||||
autospec=True)
|
||||
def test_stack_delete(self, wait_for_stack_ready_mock):
|
||||
clients = self.app.client_manager
|
||||
orchestration_client = clients.orchestration
|
||||
|
||||
self.cmd._stack_delete(orchestration_client, 'overcloud')
|
||||
|
||||
orchestration_client.stacks.get.assert_called_once_with('overcloud')
|
||||
wait_for_stack_ready_mock.assert_called_once_with(
|
||||
orchestration_client=orchestration_client,
|
||||
stack_name='overcloud',
|
||||
action='DELETE'
|
||||
)
|
||||
|
||||
def test_stack_delete_no_stack(self):
|
||||
clients = self.app.client_manager
|
||||
orchestration_client = clients.orchestration
|
||||
type(orchestration_client.stacks.get).return_value = None
|
||||
self.cmd.log.warning = mock.MagicMock()
|
||||
|
||||
self.cmd._stack_delete(orchestration_client, 'overcloud')
|
||||
|
||||
orchestration_client.stacks.get.assert_called_once_with('overcloud')
|
||||
self.cmd.log.warning.assert_called_once_with(
|
||||
"No stack found ('overcloud'), skipping delete")
|
||||
|
||||
@mock.patch(
|
||||
'tripleoclient.workflows.plan_management.delete_deployment_plan',
|
||||
autospec=True)
|
||||
def test_plan_delete(self, delete_deployment_plan_mock):
|
||||
self.cmd._plan_delete(self.workflow, 'overcloud')
|
||||
|
||||
delete_deployment_plan_mock.assert_called_once_with(
|
||||
self.workflow,
|
||||
input={'container': 'overcloud'})
|
@ -58,33 +58,35 @@ class TestOvercloudDeletePlan(utils.TestCommand):
|
||||
self.app.client_manager.workflow_engine = mock.Mock()
|
||||
self.workflow = self.app.client_manager.workflow_engine
|
||||
|
||||
def test_delete_plan(self):
|
||||
@mock.patch(
|
||||
'tripleoclient.workflows.plan_management.delete_deployment_plan',
|
||||
autospec=True)
|
||||
def test_delete_plan(self, delete_deployment_plan_mock):
|
||||
parsed_args = self.check_parser(self.cmd, ['test-plan'],
|
||||
[('plans', ['test-plan'])])
|
||||
|
||||
self.workflow.action_executions.create.return_value = (
|
||||
mock.Mock(output='{"result": null}'))
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.workflow.action_executions.create.assert_called_once_with(
|
||||
'tripleo.plan.delete', input={'container': 'test-plan'})
|
||||
delete_deployment_plan_mock.assert_called_once_with(
|
||||
self.workflow,
|
||||
input={'container': 'test-plan'})
|
||||
|
||||
def test_delete_multiple_plans(self):
|
||||
@mock.patch(
|
||||
'tripleoclient.workflows.plan_management.delete_deployment_plan',
|
||||
autospec=True)
|
||||
def test_delete_multiple_plans(self, delete_deployment_plan_mock):
|
||||
argslist = ['test-plan1', 'test-plan2']
|
||||
verifylist = [('plans', ['test-plan1', 'test-plan2'])]
|
||||
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
|
||||
|
||||
self.workflow.action_executions.create.return_value = (
|
||||
mock.Mock(output='{"result": null}'))
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.workflow.action_executions.create.assert_has_calls(
|
||||
[mock.call('tripleo.plan.delete',
|
||||
input={'container': 'test-plan1'}),
|
||||
mock.call('tripleo.plan.delete',
|
||||
input={'container': 'test-plan2'})])
|
||||
expected = [
|
||||
mock.call(self.workflow, input={'container': 'test-plan1'}),
|
||||
mock.call(self.workflow, input={'container': 'test-plan2'}),
|
||||
]
|
||||
self.assertEqual(delete_deployment_plan_mock.call_args_list,
|
||||
expected)
|
||||
|
||||
|
||||
class TestOvercloudCreatePlan(utils.TestCommand):
|
||||
|
@ -112,3 +112,16 @@ class TestPlanCreationWorkflows(utils.TestCommand):
|
||||
|
||||
self.tripleoclient.object_store.put_object.assert_called_once_with(
|
||||
'test-overcloud', 'roles_data.yaml', mock_open_context())
|
||||
|
||||
def test_delete_plan(self):
|
||||
self.workflow.action_executions.create.return_value = (
|
||||
mock.Mock(output='{"result": null}'))
|
||||
|
||||
plan_management.delete_deployment_plan(
|
||||
self.workflow,
|
||||
input={'container': 'overcloud'})
|
||||
|
||||
self.workflow.action_executions.create.assert_called_once_with(
|
||||
'tripleo.plan.delete',
|
||||
{'input': {'container': 'overcloud'}},
|
||||
run_sync=True, save_result=True)
|
||||
|
@ -33,6 +33,7 @@ import yaml
|
||||
from heatclient.common import event_utils
|
||||
from heatclient.exc import HTTPNotFound
|
||||
from osc_lib.i18n import _
|
||||
from osc_lib.i18n import _LI
|
||||
from six.moves import configparser
|
||||
from six.moves import urllib
|
||||
|
||||
@ -819,6 +820,42 @@ def parse_env_file(env_file, file_type=None):
|
||||
return nodes_config
|
||||
|
||||
|
||||
def prompt_user_for_confirmation(message, logger, positive_response='y'):
|
||||
"""Prompt user for a y/N confirmation
|
||||
|
||||
Use this function to prompt the user for a y/N confirmation
|
||||
with the provided message. The [y/N] should be included in
|
||||
the provided message to this function to indicate the expected
|
||||
input for confirmation. You can customize the positive response if
|
||||
y/N is not a desired input.
|
||||
|
||||
:param message: Confirmation string prompt
|
||||
:param logger: logger object used to write info logs
|
||||
:param positive_response: Beginning character for a positive user input
|
||||
:return boolean true for valid confirmation, false for all others
|
||||
"""
|
||||
try:
|
||||
if not sys.stdin.isatty():
|
||||
logger.error(_LI('User interaction required, cannot confirm.'))
|
||||
return False
|
||||
else:
|
||||
sys.stdout.write(message)
|
||||
prompt_response = sys.stdin.readline().lower()
|
||||
if not prompt_response.startswith(positive_response):
|
||||
logger.info(_LI(
|
||||
'User did not confirm action so taking no action.'))
|
||||
return False
|
||||
logger.info(_LI('User confirmed action.'))
|
||||
return True
|
||||
except KeyboardInterrupt: # ctrl-c
|
||||
logger.info(_LI(
|
||||
'User did not confirm action (ctrl-c) so taking no action.'))
|
||||
except EOFError: # ctrl-d
|
||||
logger.info(_LI(
|
||||
'User did not confirm action (ctrl-d) so taking no action.'))
|
||||
return False
|
||||
|
||||
|
||||
def replace_links_in_template_contents(contents, link_replacement):
|
||||
"""Replace get_file and type file links in Heat template contents
|
||||
|
||||
|
97
tripleoclient/v1/overcloud_delete.py
Normal file
97
tripleoclient/v1/overcloud_delete.py
Normal file
@ -0,0 +1,97 @@
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions as oscexc
|
||||
from osc_lib.i18n import _
|
||||
from osc_lib import utils as osc_utils
|
||||
|
||||
from tripleoclient import utils
|
||||
from tripleoclient.workflows import plan_management
|
||||
|
||||
|
||||
class DeleteOvercloud(command.Command):
|
||||
"""Delete overcloud stack and plan"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".DeleteOvercloud")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteOvercloud, self).get_parser(prog_name)
|
||||
parser.add_argument('stack', nargs='?',
|
||||
help=_('Name or ID of heat stack to delete'
|
||||
'(default=Env: OVERCLOUD_STACK_NAME)'),
|
||||
default=osc_utils.env('OVERCLOUD_STACK_NAME'))
|
||||
parser.add_argument('-y', '--yes',
|
||||
help=_('Skip yes/no prompt (assume yes).'),
|
||||
default=False,
|
||||
action="store_true")
|
||||
return parser
|
||||
|
||||
def _validate_args(self, parsed_args):
|
||||
if parsed_args.stack in (None, ''):
|
||||
raise oscexc.CommandError(
|
||||
"You must specify a stack name")
|
||||
|
||||
def _stack_delete(self, orchestration_client, stack_name):
|
||||
print("Deleting stack {s}...".format(s=stack_name))
|
||||
stack = utils.get_stack(orchestration_client, stack_name)
|
||||
if stack is None:
|
||||
self.log.warning("No stack found ('{s}'), skipping delete".
|
||||
format(s=stack_name))
|
||||
else:
|
||||
try:
|
||||
utils.wait_for_stack_ready(
|
||||
orchestration_client=orchestration_client,
|
||||
stack_name=stack_name,
|
||||
action='DELETE')
|
||||
except Exception as e:
|
||||
self.log.error("Exception while waiting for stack to delete "
|
||||
"{}".format(e))
|
||||
raise oscexc.CommandError(
|
||||
"Error occurred while waiting for stack to delete {}".
|
||||
format(e))
|
||||
|
||||
def _plan_delete(self, workflow_client, stack_name):
|
||||
print("Deleting plan {s}...".format(s=stack_name))
|
||||
try:
|
||||
plan_management.delete_deployment_plan(
|
||||
workflow_client,
|
||||
input={'container': stack_name})
|
||||
except Exception as err:
|
||||
raise oscexc.CommandError(
|
||||
"Error occurred while deleting plan {}".format(err))
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action({args})".format(args=parsed_args))
|
||||
|
||||
self._validate_args(parsed_args)
|
||||
|
||||
if not parsed_args.yes:
|
||||
confirm = utils.prompt_user_for_confirmation(
|
||||
message=_("Are you sure you want to delete this overcloud "
|
||||
"[y/N]?"),
|
||||
logger=self.log)
|
||||
if not confirm:
|
||||
raise oscexc.CommandError("Action not confirmed, exiting.")
|
||||
|
||||
clients = self.app.client_manager
|
||||
orchestration_client = clients.orchestration
|
||||
workflow_client = self.app.client_manager.workflow_engine
|
||||
|
||||
self._stack_delete(orchestration_client, parsed_args.stack)
|
||||
self._plan_delete(workflow_client, parsed_args.stack)
|
||||
print("Success.")
|
@ -68,16 +68,12 @@ class DeletePlan(command.Command):
|
||||
|
||||
for plan in parsed_args.plans:
|
||||
print("Deleting plan %s..." % plan)
|
||||
execution = workflow_client.action_executions.create(
|
||||
'tripleo.plan.delete', input={'container': plan})
|
||||
|
||||
workflow_input = {'container': plan}
|
||||
try:
|
||||
json_results = json.loads(execution.output)['result']
|
||||
if json_results is not None:
|
||||
print(json_results)
|
||||
plan_management.delete_deployment_plan(workflow_client,
|
||||
input=workflow_input)
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Error parsing action result %s", execution.output)
|
||||
self.log.exception("Error deleting plan")
|
||||
|
||||
|
||||
class CreatePlan(command.Command):
|
||||
|
@ -82,6 +82,18 @@ def create_deployment_plan(clients, **workflow_input):
|
||||
'Exception creating plan: {}'.format(payload['message']))
|
||||
|
||||
|
||||
def delete_deployment_plan(workflow_client, **input_):
|
||||
try:
|
||||
results = base.call_action(workflow_client,
|
||||
'tripleo.plan.delete',
|
||||
**input_)
|
||||
if results is not None:
|
||||
print(results)
|
||||
except Exception as err:
|
||||
raise exceptions.WorkflowServiceError(
|
||||
'Exception deleting plan: {}'.format(err))
|
||||
|
||||
|
||||
def update_deployment_plan(clients, **workflow_input):
|
||||
payload = _create_update_deployment_plan(
|
||||
clients, 'tripleo.plan_management.v1.update_deployment_plan',
|
||||
|
Loading…
Reference in New Issue
Block a user