New command "overcloud node extract provisioned"

As a first step to upgrade to a nova-less undercloud, the baremetal
deployment yaml which represents the current overcloud needs to be
built.

This command builds the yaml for an existing overcloud using the
heat stack output AnsibleHostVarsMap and a list of all baremetal
nodes. When the generated yaml is used to run "openstack overcloud
node provision" no changes will be made to any nodes.

Blueprint: nova-less-deploy
Change-Id: I47c885697bf36b999fd0ad9bd99a23990440ed62
This commit is contained in:
Steve Baker 2019-08-01 03:58:31 +00:00
parent 5951a20650
commit 51b0cc4c13
3 changed files with 207 additions and 0 deletions

View File

@ -76,6 +76,7 @@ openstack.tripleoclient.v2 =
overcloud_node_bios_reset = tripleoclient.v1.overcloud_bios:ResetBIOS
overcloud_node_provision = tripleoclient.v2.overcloud_node:ProvisionNode
overcloud_node_unprovision = tripleoclient.v2.overcloud_node:UnprovisionNode
overcloud_node_extract_provisioned = tripleoclient.v1.overcloud_node:ExtractProvisionedNode
overcloud_parameters_set = tripleoclient.v1.overcloud_parameters:SetParameters
overcloud_plan_create = tripleoclient.v1.overcloud_plan:CreatePlan
overcloud_plan_delete = tripleoclient.v1.overcloud_plan:DeletePlan

View File

@ -749,3 +749,123 @@ class TestDiscoverNode(fakes.TestOvercloudNode):
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
self.cmd.take_action(parsed_args)
class TestExtractProvisionedNode(test_utils.TestCommand):
def setUp(self):
super(TestExtractProvisionedNode, self).setUp()
self.orchestration = mock.Mock()
self.app.client_manager.orchestration = self.orchestration
self.baremetal = mock.Mock()
self.app.client_manager.baremetal = self.baremetal
self.cmd = overcloud_node.ExtractProvisionedNode(self.app, None)
self.extract_file = tempfile.NamedTemporaryFile(
mode='w', delete=False, suffix='.yaml')
self.extract_file.close()
self.addCleanup(os.unlink, self.extract_file.name)
def test_extract(self):
stack_dict = {
'parameters': {
'ComputeHostnameFormat': '%stackname%-novacompute-%index%',
'ControllerHostnameFormat': '%stackname%-controller-%index%'
},
'outputs': [{
'output_key': 'AnsibleHostVarsMap',
'output_value': {
'Compute': [
'overcloud-novacompute-0'
],
'Controller': [
'overcloud-controller-0',
'overcloud-controller-1',
'overcloud-controller-2'
],
}
}]
}
stack = mock.Mock()
stack.to_dict.return_value = stack_dict
self.orchestration.stacks.get.return_value = stack
nodes = [
mock.Mock(),
mock.Mock(),
mock.Mock(),
mock.Mock()
]
nodes[0].name = 'bm-0'
nodes[1].name = 'bm-1'
nodes[2].name = 'bm-2'
nodes[3].name = 'bm-3'
nodes[0].instance_info = {'display_name': 'overcloud-controller-0'}
nodes[1].instance_info = {'display_name': 'overcloud-controller-1'}
nodes[2].instance_info = {'display_name': 'overcloud-controller-2'}
nodes[3].instance_info = {'display_name': 'overcloud-novacompute-0'}
self.baremetal.node.list.return_value = nodes
argslist = ['--output', self.extract_file.name, '--yes']
self.app.command_options = argslist
verifylist = [('output', self.extract_file.name), ('yes', True)]
parsed_args = self.check_parser(self.cmd,
argslist, verifylist)
self.cmd.take_action(parsed_args)
result = self.cmd.app.stdout.make_string()
self.assertEqual([{
'name': 'Compute',
'count': 1,
'hostname_format': '%stackname%-novacompute-%index%',
'instances': [{
'hostname': 'overcloud-novacompute-0',
'name': 'bm-3'
}],
}, {
'name': 'Controller',
'count': 3,
'hostname_format': '%stackname%-controller-%index%',
'instances': [{
'hostname': 'overcloud-controller-0',
'name': 'bm-0'
}, {
'hostname': 'overcloud-controller-1',
'name': 'bm-1'
}, {
'hostname': 'overcloud-controller-2',
'name': 'bm-2'
}],
}], yaml.safe_load(result))
with open(self.extract_file.name) as f:
self.assertEqual(yaml.safe_load(result), yaml.safe_load(f))
def test_extract_empty(self):
stack_dict = {
'parameters': {},
'outputs': []
}
stack = mock.Mock()
stack.to_dict.return_value = stack_dict
self.orchestration.stacks.get.return_value = stack
nodes = []
self.baremetal.node.list.return_value = nodes
argslist = []
self.app.command_options = argslist
verifylist = []
parsed_args = self.check_parser(self.cmd,
argslist, verifylist)
self.cmd.take_action(parsed_args)
result = self.cmd.app.stdout.make_string()
self.assertIsNone(yaml.safe_load(result))

View File

@ -14,9 +14,11 @@
#
import collections
import datetime
import json
import logging
import os
import sys
from cliff.formatters import table
from osc_lib import exceptions as oscexc
@ -439,3 +441,87 @@ class DiscoverNode(command.Command):
baremetal.provide(self.app.client_manager,
node_uuids=nodes_uuids
)
class ExtractProvisionedNode(command.Command):
log = logging.getLogger(__name__ + ".ExtractProvisionedNode")
def _setup_clients(self):
self.clients = self.app.client_manager
self.orchestration_client = self.clients.orchestration
self.baremetal_client = self.clients.baremetal
def get_parser(self, prog_name):
parser = super(ExtractProvisionedNode, 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('-o', '--output',
metavar='<baremetal_deployment.yaml>',
help=_('The output file path describing the '
'baremetal deployment'))
parser.add_argument('-y', '--yes', default=False, action='store_true',
help=_('Skip yes/no prompt for existing files '
'(assume yes).'))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
self._setup_clients()
stack = oooutils.get_stack(self.orchestration_client,
parsed_args.stack)
host_vars = oooutils.get_stack_output_item(
stack, 'AnsibleHostVarsMap') or {}
parameters = stack.to_dict().get('parameters', {})
# list all baremetal nodes and map hostname to node name
node_details = self.baremetal_client.node.list(detail=True)
hostname_node_map = {}
for node in node_details:
hostname = node.instance_info.get('display_name')
if hostname and node.name:
hostname_node_map[hostname] = node.name
role_data = six.StringIO()
role_data.write('# Generated with the following on %s\n#\n' %
datetime.datetime.now().isoformat())
role_data.write('# openstack %s\n#\n\n' %
' '.join(self.app.command_options))
for role, entries in host_vars.items():
role_count = len(entries)
# skip zero count roles
if not role_count:
continue
role_data.write('- name: %s\n' % role)
role_data.write(' count: %s\n' % role_count)
hostname_format = parameters.get('%sHostnameFormat' % role)
if hostname_format:
role_data.write(' hostname_format: "%s"\n' % hostname_format)
role_data.write(' instances:\n')
for entry in sorted(entries):
role_data.write(' - hostname: %s\n' % entry)
if entry in hostname_node_map:
role_data.write(' name: %s\n' %
hostname_node_map[entry])
if parsed_args.output:
if (os.path.exists(parsed_args.output)
and not parsed_args.yes and sys.stdin.isatty()):
prompt_response = six.moves.input(
('Overwrite existing file %s [y/N]?' % parsed_args.output)
).lower()
if not prompt_response.startswith('y'):
raise oscexc.CommandError(
"Will not overwrite existing file:"
" %s" % parsed_args.output)
with open(parsed_args.output, 'w+') as fp:
fp.write(role_data.getvalue())
self.app.stdout.write(role_data.getvalue())