From d25d7d980db868e114ad0a3814759fa79360c41b Mon Sep 17 00:00:00 2001 From: John Fulton Date: Wed, 9 Sep 2020 21:02:24 +0000 Subject: [PATCH] Add openstack overcloud export ceph Add a new command to export Ceph information from one or more Heat stacks to be used as input of another stack. Creates a valid YAML file with the CephExternalMultiConfig parameter populated. Also have export's export_password use yaml.safe_load in place of the deprecated yaml.load. Closes-Bug: #1895034 Change-Id: Ibdf9115e92c6b476b99d6df785b0c7e9f23991de (cherry picked from commit ac0dfb5e1e881cfce85b4426f29913fee6be9f77) --- ...vercloud-export-ceph-f36421e1685db302.yaml | 6 + setup.cfg | 1 + tripleoclient/export.py | 63 +++++++++- tripleoclient/tests/test_export.py | 56 +++++++++ .../tests/v1/test_overcloud_export_ceph.py | 66 ++++++++++ tripleoclient/v1/overcloud_export_ceph.py | 115 ++++++++++++++++++ 6 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/openstack-overcloud-export-ceph-f36421e1685db302.yaml create mode 100644 tripleoclient/tests/v1/test_overcloud_export_ceph.py create mode 100644 tripleoclient/v1/overcloud_export_ceph.py diff --git a/releasenotes/notes/openstack-overcloud-export-ceph-f36421e1685db302.yaml b/releasenotes/notes/openstack-overcloud-export-ceph-f36421e1685db302.yaml new file mode 100644 index 000000000..c4bea2377 --- /dev/null +++ b/releasenotes/notes/openstack-overcloud-export-ceph-f36421e1685db302.yaml @@ -0,0 +1,6 @@ +--- +features: + - A new command "openstack overcloud export ceph" is added. The command is + used to export the Ceph deployment data from one stack for use in another + stack with storage services which use that Ceph cluster when using the + multi-stack deployment feature. diff --git a/setup.cfg b/setup.cfg index e46c980c1..43c3e5cc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,6 +62,7 @@ openstack.tripleoclient.v2 = overcloud_credentials = tripleoclient.v1.overcloud_credentials:OvercloudCredentials overcloud_deploy = tripleoclient.v1.overcloud_deploy:DeployOvercloud overcloud_export = tripleoclient.v1.overcloud_export:ExportOvercloud + overcloud_export_ceph = tripleoclient.v1.overcloud_export_ceph:ExportOvercloudCeph overcloud_status = tripleoclient.v1.overcloud_deploy:GetDeploymentStatus overcloud_image_build = tripleoclient.v1.overcloud_image:BuildOvercloudImage overcloud_image_upload = tripleoclient.v1.overcloud_image:UploadOvercloudImage diff --git a/tripleoclient/export.py b/tripleoclient/export.py index ee64561e4..3ba4b9d58 100644 --- a/tripleoclient/export.py +++ b/tripleoclient/export.py @@ -42,7 +42,7 @@ def export_passwords(swift, stack, excludes=True): "file from swift: %s", str(e)) sys.exit(1) - data = yaml.load(content) + data = yaml.safe_load(content) # The "passwords" key in plan-environment.yaml are generated passwords, # they are not necessarily the actual password values used during the # deployment. @@ -141,3 +141,64 @@ def export_stack(heat, stack, should_filter=False, "No data returned to export %s from." % param) return data + + +def export_storage_ips(stack, config_download_dir=constants.DEFAULT_WORK_DIR): + inventory_file = "ceph-ansible/inventory.yml" + file = os.path.join(config_download_dir, stack, inventory_file) + with open(file, 'r') as ff: + try: + inventory_data = yaml.safe_load(ff) + except Exception as e: + LOG.error( + _('Could not read file %s') % file) + LOG.error(e) + mon_ips = [] + for mon_role in inventory_data['mons']['children'].keys(): + for hostname in inventory_data[mon_role]['hosts']: + ip = inventory_data[mon_role]['hosts'][hostname]['storage_ip'] + mon_ips.append(ip) + + return mon_ips + + +def export_ceph(stack, cephx, + config_download_dir=constants.DEFAULT_WORK_DIR, + mon_ips=[]): + # Return a map of ceph data for a list item in CephExternalMultiConfig + # by parsing files within the config_download_dir of a certain stack + + if len(mon_ips) == 0: + mon_ips = export_storage_ips(stack, config_download_dir) + + # Use ceph-ansible group_vars/all.yml to get remaining values + ceph_ansible_all = "ceph-ansible/group_vars/all.yml" + file = os.path.join(config_download_dir, stack, ceph_ansible_all) + with open(file, 'r') as ff: + try: + ceph_data = yaml.safe_load(ff) + except Exception as e: + LOG.error( + _('Could not read file %s') % file) + LOG.error(e) + + for key in ceph_data['keys']: + if key['name'] == 'client.' + str(cephx): + cephx_keys = [key] + + ceph_conf_overrides = {} + ceph_conf_overrides['client'] = {} + ceph_conf_overrides['client']['keyring'] = '/etc/ceph/' \ + + ceph_data['cluster'] \ + + '.client.' + cephx \ + + '.keyring' + # Combine extracted data into one map to return + data = {} + data['external_cluster_mon_ips'] = str(','.join(mon_ips)) + data['keys'] = cephx_keys + data['ceph_conf_overrides'] = ceph_conf_overrides + data['cluster'] = ceph_data['cluster'] + data['fsid'] = ceph_data['fsid'] + data['dashboard_enabled'] = False + + return data diff --git a/tripleoclient/tests/test_export.py b/tripleoclient/tests/test_export.py index b1862f516..c148975bc 100644 --- a/tripleoclient/tests/test_export.py +++ b/tripleoclient/tests/test_export.py @@ -42,6 +42,31 @@ class TestExport(TestCase): self.mock_stack.to_dict.return_value = dict(outputs=outputs) self.mock_open = mock.mock_open(read_data='{"an_key":"an_value"}') + ceph_inv = { + 'DistributedComputeHCI': { + 'hosts': { + 'dcn0-distributedcomputehci-0': { + 'storage_ip': '192.168.24.42' + } + } + }, + 'mons': { + 'children': { + 'DistributedComputeHCI': {} + } + } + } + self.mock_open_ceph_inv = mock.mock_open(read_data=str(ceph_inv)) + + ceph_all = { + 'cluster': 'dcn0', + 'fsid': 'a5a22d37-e01f-4fa0-a440-c72585c7487f', + 'keys': [ + {'name': 'client.openstack'} + ] + } + self.mock_open_ceph_all = mock.mock_open(read_data=str(ceph_all)) + @mock.patch('tripleoclient.utils.get_stack') def test_export_stack(self, mock_get_stack): heat = mock.Mock() @@ -151,3 +176,34 @@ class TestExport(TestCase): mock_passwords['passwords'].pop('CephRgwKey') self.assertEqual(mock_passwords['passwords'], data) + + def test_export_storage_ips(self): + with mock.patch('six.moves.builtins.open', self.mock_open_ceph_inv): + storage_ips = export.export_storage_ips('dcn0', + config_download_dir='/foo') + self.assertEqual(storage_ips, ['192.168.24.42']) + self.mock_open_ceph_inv.assert_called_once_with( + '/foo/dcn0/ceph-ansible/inventory.yml', 'r') + + def test_export_ceph(self): + expected = { + 'external_cluster_mon_ips': '192.168.24.42', + 'keys': [ + {'name': 'client.openstack'} + ], + 'ceph_conf_overrides': { + 'client': { + 'keyring': '/etc/ceph/dcn0.client.openstack.keyring' + } + }, + 'cluster': 'dcn0', + 'fsid': 'a5a22d37-e01f-4fa0-a440-c72585c7487f', + 'dashboard_enabled': False + } + with mock.patch('six.moves.builtins.open', self.mock_open_ceph_all): + data = export.export_ceph('dcn0', 'openstack', + config_download_dir='/foo', + mon_ips=['192.168.24.42']) + self.assertEqual(data, expected) + self.mock_open_ceph_all.assert_called_once_with( + '/foo/dcn0/ceph-ansible/group_vars/all.yml', 'r') diff --git a/tripleoclient/tests/v1/test_overcloud_export_ceph.py b/tripleoclient/tests/v1/test_overcloud_export_ceph.py new file mode 100644 index 000000000..c9cc501e7 --- /dev/null +++ b/tripleoclient/tests/v1/test_overcloud_export_ceph.py @@ -0,0 +1,66 @@ +# Copyright 2020 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 os + +import mock + +from osc_lib.tests import utils + +from tripleoclient.v1 import overcloud_export_ceph + + +class TestOvercloudExportCeph(utils.TestCommand): + + def setUp(self): + super(TestOvercloudExportCeph, self).setUp() + + self.cmd = overcloud_export_ceph.ExportOvercloudCeph(self.app, None) + self.tripleoclient = mock.Mock() + self.app.client_manager.tripleoclient = self.tripleoclient + self.mock_open = mock.mock_open() + + @mock.patch('os.path.exists') + @mock.patch('yaml.safe_dump') + @mock.patch('tripleoclient.export.export_ceph') + def test_export_ceph(self, mock_export_ceph, + mock_safe_dump, + mock_exists): + argslist = ['--stack', 'dcn0'] + verifylist = [('stack', 'dcn0')] + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + mock_exists.return_value = False + expected = { + 'external_cluster_mon_ips': '192.168.24.42', + 'keys': [ + {'name': 'client.openstack'} + ], + 'ceph_conf_overrides': { + 'client': { + 'keyring': '/etc/ceph/dcn0.client.openstack.keyring' + } + }, + 'cluster': 'dcn0', + 'fsid': 'a5a22d37-e01f-4fa0-a440-c72585c7487f', + 'dashboard_enabled': False + } + data = {} + data['parameter_defaults'] = {} + data['parameter_defaults']['CephExternalMultiConfig'] = [expected] + mock_export_ceph.return_value = expected + + with mock.patch('six.moves.builtins.open', self.mock_open): + self.cmd.take_action(parsed_args) + path = os.path.join(os.environ.get('HOME'), 'config-download') + mock_export_ceph.assert_called_once_with('dcn0', 'openstack', path) + self.assertEqual(data, mock_safe_dump.call_args[0][0]) diff --git a/tripleoclient/v1/overcloud_export_ceph.py b/tripleoclient/v1/overcloud_export_ceph.py new file mode 100644 index 000000000..f4f06916e --- /dev/null +++ b/tripleoclient/v1/overcloud_export_ceph.py @@ -0,0 +1,115 @@ +# 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. + +from datetime import datetime +import logging +import os.path +import yaml + +from osc_lib.i18n import _ +from osc_lib import utils + +from tripleoclient import command +from tripleoclient import export + + +class ExportOvercloudCeph(command.Command): + """Export Ceph information used as import of another stack + + Export Ceph information from one or more stacks to be used + as input of another stack. Creates a valid YAML file with + the CephExternalMultiConfig parameter populated. + """ + + log = logging.getLogger(__name__ + ".ExportOvercloudCeph") + now = datetime.now().strftime('%Y%m%d%H%M%S') + + def get_parser(self, prog_name): + parser = super(ExportOvercloudCeph, self).get_parser(prog_name) + parser.add_argument('--stack', + dest='stack', + metavar='', + help=_('Name of the overcloud stack(s) ' + 'to export Ceph information from. ' + 'If a comma delimited list of stacks is ' + 'passed, Ceph information for all stacks ' + 'will be exported into a single file. ' + '(default=Env: OVERCLOUD_STACK_NAME) '), + default=utils.env('OVERCLOUD_STACK_NAME', + default='overcloud')) + parser.add_argument('--cephx-key-client-name', '-k', + dest='cephx', + metavar='', + help=_('Name of the cephx client key to export. ' + '(default=openstack)'), + default='openstack') + parser.add_argument('--output-file', '-o', metavar='', + help=_('Name of the output file for the Ceph ' + 'data export. Defaults to ' + '"ceph-export-.yaml" if one ' + 'stack is provided. Defaults to ' + '"ceph-export--stacks.yaml" ' + 'if N stacks are provided.')) + parser.add_argument('--force-overwrite', '-f', action='store_true', + default=False, + help=_('Overwrite output file if it exists.')) + parser.add_argument('--config-download-dir', + action='store', + help=_('Directory to search for config-download ' + 'export data. Defaults to ' + '$HOME/config-download')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + stacks = parsed_args.stack.split(',') + stack_count = len(stacks) + if stack_count == 1: + name = parsed_args.stack + else: + name = str(stack_count) + '-stacks' + output_file = parsed_args.output_file or \ + 'ceph-export-%s.yaml' % name + + self.log.info('Running at %s with parameters %s', + self.now, + parsed_args) + + if os.path.exists(output_file) and not parsed_args.force_overwrite: + raise Exception( + "File '%s' already exists, not exporting." % output_file) + + if not parsed_args.config_download_dir: + config_download_dir = os.path.join(os.environ.get('HOME'), + 'config-download') + else: + config_download_dir = parsed_args.config_download_dir + + # extract ceph data for each stack into the cephs list + cephs = [] + for stack in stacks: + self.log.info('Exporting Ceph data from stack %s at %s', + stack, self.now) + cephs.append(export.export_ceph(stack, + parsed_args.cephx, + config_download_dir)) + data = {} + data['parameter_defaults'] = {} + data['parameter_defaults']['CephExternalMultiConfig'] = cephs + # write the exported data + with open(output_file, 'w') as f: + yaml.safe_dump(data, f, default_flow_style=False) + + print("Ceph information from %s stack(s) exported to %s." % + (len(cephs), output_file))