Introduce "openstack overcloud ceph deploy"

The new "openstack overcloud ceph deploy" is used to deploy
Ceph after the hardware has been provisioned with networking
and before the overcloud is deployed. The command takes the
output of "openstack overcloud node provision" as input and
returns a Heat enviornment file, e.g. deployed_ceph.yaml,
as output. The deployed_ceph.yaml file may then be passed
to the "openstack overcloud deploy" command as input.

Change-Id: Ie0032190f0c07fd47a36a1915c02f0ba1a9ae2a4
This commit is contained in:
John Fulton 2021-07-11 20:41:54 +00:00 committed by Francesco Pantano
parent 63d3e637dc
commit 61e4f4c788
7 changed files with 483 additions and 0 deletions

View File

@ -0,0 +1,11 @@
---
features:
- |
A new command "openstack overcloud ceph deploy" is added. The command is
used to deploy Ceph after the hardware has been provisioned with networking
and before the overcloud is deployed. The command takes the output of
"openstack overcloud node provision" as input and returns a Heat enviornment
file, e.g. deployed_ceph.yaml, as output. The deployed_ceph.yaml file may then
be passed to the "openstack overcloud deploy" command as input. During overcloud
deployment the Ceph cluster is then configured to host OpenStack. E.g. cephx keys
and pools are still created on the Ceph cluster by "openstack overcloud deploy".

View File

@ -42,6 +42,7 @@ openstack.tripleoclient.v2 =
overcloud_admin_authorize = tripleoclient.v1.overcloud_admin:Authorize
overcloud_netenv_validate = tripleoclient.v1.overcloud_netenv_validate:ValidateOvercloudNetenv
overcloud_cell_export = tripleoclient.v1.overcloud_cell:ExportCell
overcloud_ceph_deploy = tripleoclient.v2.overcloud_ceph:OvercloudCephDeploy
overcloud_config_download = tripleoclient.v1.overcloud_config:DownloadConfig
overcloud_container_image_upload = tripleoclient.v1.container_image:UploadImage
overcloud_container_image_build = tripleoclient.v1.container_image:BuildImage

View File

@ -2295,3 +2295,41 @@ class TestProhibitedOverrides(base.TestCommand):
environment)
resource_registry.pop("neutron")
self.assertIsNone(utils.check_neutron_resources(environment))
class TestParseContainerImagePrepare(TestCase):
fake_env = {'parameter_defaults': {'ContainerImagePrepare':
[{'push_destination': True, 'set':
{'ceph_image': 'ceph',
'ceph_namespace': 'quay.io:443/ceph',
'ceph_tag': 'latest'}}],
'ContainerImageRegistryCredentials':
{'quay.io:443': {'quay_username':
'quay_password'}}}}
def test_parse_container_image_prepare(self):
key = 'ContainerImagePrepare'
keys = ['ceph_namespace', 'ceph_image', 'ceph_tag']
reg_expected = {'ceph_image': 'ceph',
'ceph_namespace': 'quay.io:443/ceph',
'ceph_tag': 'latest'}
with tempfile.NamedTemporaryFile(mode='w') as cfgfile:
yaml.safe_dump(self.fake_env, cfgfile)
reg_actual = \
utils.parse_container_image_prepare(key, keys,
cfgfile.name)
self.assertEqual(reg_actual, reg_expected)
def test_parse_container_image_prepare_credentials(self):
key = 'ContainerImageRegistryCredentials'
keys = ['quay.io:443/ceph']
reg_expected = {'registry_url': 'quay.io:443',
'registry_username': 'quay_username',
'registry_password': 'quay_password'}
with tempfile.NamedTemporaryFile(mode='w') as cfgfile:
yaml.safe_dump(self.fake_env, cfgfile)
reg_actual = \
utils.parse_container_image_prepare(key, keys,
cfgfile.name)
self.assertEqual(reg_actual, reg_expected)

View File

@ -0,0 +1,77 @@
# Copyright 2021 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 osc_lib import exceptions as osc_lib_exc
from tripleoclient.tests import fakes
from tripleoclient.v2 import overcloud_ceph
class TestOvercloudCephDeploy(fakes.FakePlaybookExecution):
def setUp(self):
super(TestOvercloudCephDeploy, self).setUp()
# Get the command object to test
app_args = mock.Mock()
app_args.verbose_level = 1
self.app.options = fakes.FakeOptions()
self.cmd = overcloud_ceph.OvercloudCephDeploy(self.app,
app_args)
@mock.patch('tripleoclient.utils.TempDirs', autospect=True)
@mock.patch('os.path.abspath', autospect=True)
@mock.patch('os.path.exists', autospect=True)
@mock.patch('tripleoclient.utils.run_ansible_playbook', autospec=True)
def test_overcloud_deploy_ceph(self, mock_playbook, mock_abspath,
mock_path_exists, mock_tempdirs):
arglist = ['deployed-metal.yaml', '--yes',
'--stack', 'overcloud',
'--output', 'deployed-ceph.yaml',
'--container-namespace', 'quay.io/ceph',
'--container-image', 'ceph',
'--container-tag', 'latest']
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
mock_playbook.assert_called_once_with(
playbook='cli-deployed-ceph.yaml',
inventory=mock.ANY,
workdir=mock.ANY,
playbook_dir=mock.ANY,
verbosity=3,
extra_vars={
"baremetal_deployed_path": mock.ANY,
"deployed_ceph_tht_path": mock.ANY,
"working_dir": mock.ANY,
"stack_name": 'overcloud',
'tripleo_roles_path': mock.ANY,
'tripleo_cephadm_container_ns': 'quay.io/ceph',
'tripleo_cephadm_container_image': 'ceph',
'tripleo_cephadm_container_tag': 'latest',
}
)
@mock.patch('os.path.abspath', autospect=True)
@mock.patch('os.path.exists', autospect=True)
def test_overcloud_deploy_ceph_no_overwrite(self, mock_abspath,
mock_path_exists):
arglist = ['deployed-metal.yaml',
'--stack', 'overcloud',
'--output', 'deployed-ceph.yaml']
parsed_args = self.check_parser(self.cmd, arglist, [])
self.assertRaises(osc_lib_exc.CommandError,
self.cmd.take_action, parsed_args)

View File

@ -59,6 +59,7 @@ from six.moves import configparser
from heatclient import exc as hc_exc
from six.moves.urllib import error as url_error
from six.moves.urllib import parse as url_parse
from six.moves.urllib import request
from tenacity import retry
@ -2968,3 +2969,86 @@ def check_prohibited_overrides(protected_overrides, user_environments):
if found_conflict:
raise exceptions.DeploymentError(msg)
def parse_container_image_prepare(tht_key='ContainerImagePrepare',
keys=[], source=None):
"""Extracts key/value pairs from list of keys in source file
If keys=[foo,bar] and source is the following,
then return {foo: 1, bar: 2}
parameter_defaults:
ContainerImagePrepare:
- tag_from_label: grault
set:
foo: 1
bar: 2
namespace: quay.io/garply
ContainerImageRegistryCredentials:
'quay.io': {'quay_username': 'quay_password'}
Alternatively, if tht_key='ContainerImageRegistryCredentials' and
keys=['quay.io/garply'] for the above, then return the following:
{'registry_url': 'quay.io',
'registry_username': 'quay_username',
'registry_password': 'quay_password'}
If the tht_key is not found, return an empty dictionary
:param tht_key: string of a THT parameter (only 2 options)
:param keys: list of keys to extract
:param source: (string) path to container_image_prepare_defaults.yaml
:return: dictionary
"""
image_map = {}
if source is None:
source = kolla_builder.DEFAULT_PREPARE_FILE
if not os.path.exists(source):
raise RuntimeError(
"Path to container image prepare defaults file "
"not found: %s." % os.path.abspath(source))
with open(source, 'r') as stream:
try:
images = yaml.safe_load(stream)
except yaml.YAMLError as exc:
raise RuntimeError(
"yaml.safe_load(%s) returned '%s'" % (source, exc))
if tht_key == 'ContainerImagePrepare':
try:
tag_list = images['parameter_defaults'][tht_key]
for key in keys:
for tag in tag_list:
if 'set' in tag:
if key in tag['set']:
image_map[key] = tag['set'][key]
except KeyError:
raise RuntimeError(
"The expected parameter_defaults and %s are not "
"defined in data file: %s" % (tht_key, source))
elif tht_key == 'ContainerImageRegistryCredentials':
try:
tag_list = images['parameter_defaults'][tht_key]
for key in keys:
for tag in tag_list:
registry = url_parse.urlparse(key).netloc
if len(registry) == 0:
registry = url_parse.urlparse('//' + key).netloc
if tag == registry:
if isinstance(tag_list[registry], collections.Mapping):
credentials = tag_list[registry].popitem()
image_map['registry_username'] = credentials[0]
image_map['registry_password'] = credentials[1]
image_map['registry_url'] = registry
except KeyError:
LOG.info("Unable to parse %s from %s. "
"Assuming the container registry does not "
"require authentication or that the "
"registry URL, username and password "
"will be passed another way."
% (tht_key, source))
else:
raise RuntimeError("Unsupported tht_key: %s" % tht_key)
return image_map

View File

@ -0,0 +1,272 @@
# Copyright 2021 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
import os
from osc_lib import exceptions as oscexc
from osc_lib.i18n import _
from osc_lib import utils
from tripleoclient import command
from tripleoclient import constants
from tripleoclient import utils as oooutils
class OvercloudCephDeploy(command.Command):
log = logging.getLogger(__name__ + ".OvercloudCephDeploy")
def get_parser(self, prog_name):
parser = super(OvercloudCephDeploy, self).get_parser(prog_name)
parser.add_argument('baremetal_env',
metavar='<deployed_baremetal.yaml>',
help=_('Path to the environment file '
'output from "openstack '
'overcloud node provision".'))
parser.add_argument('-o', '--output', required=True,
metavar='<deployed_ceph.yaml>',
help=_('The path to the output environment '
'file describing the Ceph deployment '
' to pass to the overcloud deployment.'))
parser.add_argument('-y', '--yes', default=False, action='store_true',
help=_('Skip yes/no prompt before overwriting an '
'existing <deployed_ceph.yaml> output file '
'(assume yes).'))
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(
'--working-dir', action='store',
help=_('The working directory for the deployment where all '
'input, output, and generated files will be stored.\n'
'Defaults to "$HOME/overcloud-deploy/<stack>"'))
parser.add_argument('--roles-data',
help=_(
"Path to an alternative roles_data.yaml. "
"Used to decide which node gets which "
"Ceph mon, mgr, or osd service "
"based on the node's role in "
"<deployed_baremetal.yaml>."),
default=os.path.join(
constants.TRIPLEO_HEAT_TEMPLATES,
constants.OVERCLOUD_ROLES_FILE))
spec_group = parser.add_mutually_exclusive_group()
spec_group.add_argument('--ceph-spec',
help=_(
"Path to an existing Ceph spec file. "
"If not provided a spec will be generated "
"automatically based on --roles-data and "
"<deployed_baremetal.yaml>"),
default=None)
spec_group.add_argument('--osd-spec',
help=_(
"Path to an existing OSD spec file. "
"Mutually exclusive with --ceph-spec. "
"If the Ceph spec file is generated "
"automatically, then the OSD spec "
"in the Ceph spec file defaults to "
"{data_devices: {all: true}} "
"for all service_type osd. "
"Use --osd-spec to override the "
"data_devices value inside the "
"Ceph spec file."),
default=None)
parser.add_argument('--container-image-prepare',
help=_(
"Path to an alternative "
"container_image_prepare_defaults.yaml. "
"Used to control which Ceph container is "
"pulled by cephadm via the ceph_namespace, "
"ceph_image, and ceph_tag variables in "
"addition to registry authentication via "
"ContainerImageRegistryCredentials."
),
default=None)
container_group = parser.add_argument_group("container-image-prepare "
"overrides",
"The following options "
"may be used to override "
"individual values "
"set via "
"--container-image-prepare"
". If the example "
"variables below were "
"set the image would be "
"concatenated into "
"quay.io/ceph/ceph:latest "
"and a custom registry "
"login would be used."
)
container_group.add_argument('--container-namespace',
required=False,
help='e.g. quay.io/ceph')
container_group.add_argument('--container-image',
required=False,
help='e.g. ceph')
container_group.add_argument('--container-tag',
required=False,
help='e.g. latest')
container_group.add_argument('--registry-url',
required=False,
help='')
container_group.add_argument('--registry-username',
required=False,
help='')
container_group.add_argument('--registry-password',
required=False,
help='')
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
baremetal_env_path = os.path.abspath(parsed_args.baremetal_env)
output_path = os.path.abspath(parsed_args.output)
if not os.path.exists(baremetal_env_path):
raise oscexc.CommandError(
"Baremetal environment file does not exist:"
" %s" % parsed_args.baremetal_env)
overwrite = parsed_args.yes
if (os.path.exists(output_path) and not overwrite
and not oooutils.prompt_user_for_confirmation(
'Overwrite existing file %s [y/N]?' % parsed_args.output,
self.log)):
raise oscexc.CommandError("Will not overwrite existing file:"
" %s. See the --yes parameter to "
"override this behavior. " %
parsed_args.output)
else:
overwrite = True
if not parsed_args.working_dir:
working_dir = oooutils.get_default_working_dir(
parsed_args.stack)
else:
working_dir = os.path.abspath(parsed_args.working_dir)
oooutils.makedirs(working_dir)
inventory = os.path.join(working_dir,
constants.TRIPLEO_STATIC_INVENTORY)
if not os.path.exists(inventory):
raise oscexc.CommandError(
"Inventory file not found in working directory: "
"%s. It should have been created by "
"'openstack overcloud node provision'."
% inventory)
# mandatory extra_vars are now set, add others conditionally
extra_vars = {
"baremetal_deployed_path": baremetal_env_path,
"deployed_ceph_tht_path": output_path,
"working_dir": working_dir,
"stack_name": parsed_args.stack,
}
# optional paths to pass to playbook
if parsed_args.roles_data:
if not os.path.exists(parsed_args.roles_data):
raise oscexc.CommandError(
"Roles Data file not found --roles-data %s."
% os.path.abspath(parsed_args.roles_data))
else:
extra_vars['tripleo_roles_path'] = \
os.path.abspath(parsed_args.roles_data)
if parsed_args.ceph_spec:
if not os.path.exists(parsed_args.ceph_spec):
raise oscexc.CommandError(
"Ceph Spec file not found --ceph-spec %s."
% os.path.abspath(parsed_args.ceph_spec))
else:
extra_vars['dynamic_ceph_spec'] = False
extra_vars['ceph_spec_path'] = \
os.path.abspath(parsed_args.ceph_spec)
if parsed_args.osd_spec:
if not os.path.exists(parsed_args.osd_spec):
raise oscexc.CommandError(
"OSD Spec file not found --osd-spec %s."
% os.path.abspath(parsed_args.osd_spec))
else:
extra_vars['osd_spec_path'] = \
os.path.abspath(parsed_args.osd_spec)
# optional container vars to pass to playbook
keys = ['ceph_namespace', 'ceph_image', 'ceph_tag']
key = 'ContainerImagePrepare'
container_dict = \
oooutils.parse_container_image_prepare(key, keys,
parsed_args.
container_image_prepare)
extra_vars['tripleo_cephadm_container_ns'] = \
parsed_args.container_namespace or \
container_dict['ceph_namespace']
extra_vars['tripleo_cephadm_container_image'] = \
parsed_args.container_image or \
container_dict['ceph_image']
extra_vars['tripleo_cephadm_container_tag'] = \
parsed_args.container_tag or \
container_dict['ceph_tag']
# optional container registry vars to pass to playbook
if 'tripleo_cephadm_container_ns' in extra_vars:
keys = [extra_vars['tripleo_cephadm_container_ns']]
key = 'ContainerImageRegistryCredentials'
registry_dict = \
oooutils.parse_container_image_prepare(key, keys,
parsed_args.
container_image_prepare)
# It's valid for the registry_dict to be empty so
# we cannot default to it with an 'or' like we can
# for ceph_{namespace,image,tag} as above.
if 'registry_url' in registry_dict:
extra_vars['tripleo_cephadm_registry_url'] = \
registry_dict['registry_url']
if 'registry_password' in registry_dict:
extra_vars['tripleo_cephadm_registry_password'] = \
registry_dict['registry_password']
if 'registry_username' in registry_dict:
extra_vars['tripleo_cephadm_registry_username'] = \
registry_dict['registry_username']
# Whether registry vars came out of --container-image-prepare
# or not, we need either to set them (as above) or override
# them if they were passed via the CLI (as follows)
if parsed_args.registry_url:
extra_vars['tripleo_cephadm_registry_url'] = \
parsed_args.registry_url
if parsed_args.registry_password:
extra_vars['tripleo_cephadm_registry_password'] = \
parsed_args.registry_password
if parsed_args.registry_username:
extra_vars['tripleo_cephadm_registry_username'] = \
parsed_args.registry_username
# call the playbook
with oooutils.TempDirs() as tmp:
oooutils.run_ansible_playbook(
playbook='cli-deployed-ceph.yaml',
inventory=inventory,
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=oooutils.playbook_verbosity(self=self),
extra_vars=extra_vars,
)