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:
Alex Schultz 2016-11-22 18:17:46 -07:00
parent c212fbd065
commit 7dd16b1da2
10 changed files with 289 additions and 23 deletions

View File

@ -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

View File

@ -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):

View 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'})

View File

@ -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):

View File

@ -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)

View File

@ -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

View 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.")

View File

@ -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):

View File

@ -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',