diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index ae456bd53..c73a8881c 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -1104,6 +1104,7 @@ class TestProvisionNode(fakes.TestOvercloudNode): 'tripleo.baremetal_deploy.v1.deploy_roles', workflow_input={'roles': [{'name': 'Compute'}, {'name': 'Controller'}], + 'plan': 'overcloud', 'ssh_keys': ['I am a key'], 'ssh_user_name': 'heat-admin'} ) @@ -1130,11 +1131,22 @@ class TestUnprovisionNode(fakes.TestOvercloudNode): self.cmd = overcloud_node.UnprovisionNode(self.app, None) def test_ok(self): + rv = mock.Mock() + rv.output = json.dumps({ + 'result': { + 'instances': [ + {'hostname': 'compute-0', 'name': 'baremetal-1'}, + {'hostname': 'controller-0', 'name': 'baremetal-2'} + ] + } + }) + + self.workflow.action_executions.create.return_value = rv with tempfile.NamedTemporaryFile() as inp: inp.write(b'- name: Compute\n- name: Controller\n') inp.flush() - argslist = [inp.name] - verifylist = [('input', inp.name)] + argslist = ['--yes', inp.name] + verifylist = [('input', inp.name), ('yes', True)] parsed_args = self.check_parser(self.cmd, argslist, verifylist) @@ -1142,6 +1154,66 @@ class TestUnprovisionNode(fakes.TestOvercloudNode): self.workflow.executions.create.assert_called_once_with( 'tripleo.baremetal_deploy.v1.undeploy_roles', - workflow_input={'roles': [{'name': 'Compute'}, - {'name': 'Controller'}]} + workflow_input={ + 'plan': 'overcloud', + 'roles': [{ + 'name': 'Unprovisioned', + 'count': 0, + 'instances': [ + {'hostname': u'compute-0', 'provisioned': False}, + {'hostname': u'controller-0', 'provisioned': False} + ] + }] + } + ) + + def test_ok_all(self): + rv = mock.Mock() + rv.output = json.dumps({ + 'result': { + 'instances': [ + {'hostname': 'compute-0', 'name': 'baremetal-1'}, + {'hostname': 'controller-0', 'name': 'baremetal-2'} + ] + } + }) + + rv_provisioned = mock.Mock() + rv_provisioned.output = json.dumps({ + 'result': { + 'instances': [ + {'hostname': 'compute-1', 'name': 'baremetal-3'}, + {'hostname': 'controller-1', 'name': 'baremetal-4'} + ] + } + }) + + self.workflow.action_executions.create.side_effect = [ + rv, rv_provisioned + ] + with tempfile.NamedTemporaryFile() as inp: + inp.write(b'- name: Compute\n- name: Controller\n') + inp.flush() + argslist = ['--all', '--yes', inp.name] + verifylist = [('input', inp.name), ('yes', True), ('all', True)] + + parsed_args = self.check_parser(self.cmd, + argslist, verifylist) + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal_deploy.v1.undeploy_roles', + workflow_input={ + 'plan': 'overcloud', + 'roles': [{ + 'name': 'Unprovisioned', + 'count': 0, + 'instances': [ + {'hostname': u'compute-0', 'provisioned': False}, + {'hostname': u'controller-0', 'provisioned': False}, + {'hostname': u'compute-1', 'provisioned': False}, + {'hostname': u'controller-1', 'provisioned': False} + ] + }] + } ) diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index 1dc16d459..fc7f5f5d2 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -14,9 +14,12 @@ # import argparse +import collections import logging import os +import sys +from cliff.formatters import table from osc_lib import exceptions as oscexc from osc_lib.i18n import _ from osc_lib import utils @@ -530,6 +533,7 @@ class ProvisionNode(command.Command): output = baremetal.deploy_roles( self.app.client_manager, + plan=parsed_args.stack, roles=roles, ssh_keys=[ssh_key], ssh_user_name=parsed_args.overcloud_ssh_user) @@ -547,6 +551,20 @@ class UnprovisionNode(command.Command): def get_parser(self, prog_name): parser = super(UnprovisionNode, self).get_parser(prog_name) + parser.add_argument('--stack', dest='stack', + help=_('Name or ID of heat stack ' + '(default=Env: OVERCLOUD_STACK_NAME)'), + default=utils.env('OVERCLOUD_STACK_NAME', + default='overcloud')) + parser.add_argument('--all', + help=_('Unprovision every instance in the ' + 'deployment'), + default=False, + action="store_true") + parser.add_argument('-y', '--yes', + help=_('Skip yes/no prompt (assume yes)'), + default=False, + action="store_true") parser.add_argument('input', metavar='', help=_('Configuration file describing the ' @@ -559,10 +577,66 @@ class UnprovisionNode(command.Command): with open(parsed_args.input, 'r') as fp: roles = yaml.safe_load(fp) - # TODO(sbaker) call ExpandRolesAction to get a list of - # instances being unprovisioned to prompt for confirmation + nodes = [] + expanded = baremetal.expand_roles( + self.app.client_manager, + roles=roles, + stackname=parsed_args.stack, + provisioned=False) + nodes.extend(expanded.get('instances', [])) + + if parsed_args.all: + expanded = baremetal.expand_roles( + self.app.client_manager, + roles=roles, + stackname=parsed_args.stack, + provisioned=True) + nodes.extend(expanded.get('instances', [])) + + if not nodes: + print('No nodes to unprovision') + return + + self._print_nodes(nodes) + + if not parsed_args.yes: + confirm = oooutils.prompt_user_for_confirmation( + message=_("Are you sure you want to unprovision these %s " + "nodes [y/N]? ") % parsed_args.stack, + logger=self.log) + if not confirm: + raise oscexc.CommandError("Action not confirmed, exiting.") + unprovision_role = self._build_unprovision_role(nodes) + print('Unprovisioning %d nodes' % len(nodes)) baremetal.undeploy_roles( self.app.client_manager, - roles=roles) + roles=unprovision_role, + plan=parsed_args.stack) print('Unprovision complete') + + def _build_unprovision_role(self, nodes): + # build a fake role called Unprovisioned which has an instance + # entry for every node to be unprovisioned + instances = [{'hostname': n['hostname'], 'provisioned': False} + for n in nodes if 'hostname' in n] + return [{ + 'name': 'Unprovisioned', + 'count': 0, + 'instances': instances + }] + + def _print_nodes(self, nodes): + TableArgs = collections.namedtuple( + 'TableArgs', 'print_empty max_width fit_width') + args = TableArgs(print_empty=True, max_width=80, fit_width=True) + nodes_data = [(i.get('hostname', ''), + i.get('name', '')) for i in nodes] + + formatter = table.TableFormatter() + formatter.emit_list( + column_names=['hostname', 'name'], + data=nodes_data, + stdout=sys.stdout, + parsed_args=args + ) diff --git a/tripleoclient/workflows/baremetal.py b/tripleoclient/workflows/baremetal.py index b1e1c166d..c9a8a5a5f 100644 --- a/tripleoclient/workflows/baremetal.py +++ b/tripleoclient/workflows/baremetal.py @@ -568,3 +568,14 @@ def undeploy_roles(clients, **workflow_input): 'Error undeploying nodes: {}'.format(payload['message'])) return payload + + +def expand_roles(clients, roles, stackname, provisioned): + workflow_client = clients.workflow_engine + return base.call_action( + workflow_client, + 'tripleo.baremetal_deploy.expand_roles', + roles=roles, + stackname=stackname, + provisioned=provisioned + )