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:
parent
5951a20650
commit
51b0cc4c13
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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())
|
||||
|
Loading…
x
Reference in New Issue
Block a user