Squash standalone deployed_ceph for pythonclient to Wallaby

This squash backports the standalone deployed_ceph feature to
Wallaby without introducing related bugs. It has 5 commits.

Depends-On: https://review.opendev.org/c/openstack/tripleo-ansible/+/845595

1. Add --mon-ip option to deployed ceph

Add --mon-ip option to 'openstack overcloud
ceph deploy', to easily override the variable
tripleo_cephadm_first_mon_ip from the CLI.

This option makes deployed ceph easier when
using standalone.

Change-Id: Id9a61356e8fd4618ce68faa2677f4ed30981b7d6
(cherry picked from commit 2ac3953410)

2. Make deployed ceph baremetal file optional

If the --ceph-spec option is passed to 'openstack
overcloud ceph deploy', then it is not necessary
for the path to the environment file output from
'openstack overcloud node provision' to be passed.

Add conditional to ensure that either --ceph-spec
or <deployed_baremetal.yaml> are used and add two
unit tests to cover all three scenarios: a. metal
is passed, b. spec is passed and c. neither are
passed so a failure is asserted.

Change-Id: Ibd350956f401fa48aaeae89fdd5e0bd9c28d0685
(cherry picked from commit 84055814f9)

3. Introduce "openstack overcloud ceph spec"

This command may be used to create a cephadm spec file as
a function of the output of metalsmith and a TripleO roles
file.

Calls cli-deployed-ceph.yaml with ceph_spec tag so users
may create the Ceph spec in a separate step from the Ceph
Deployment. The Ceph spec file may be then be passed to
"openstack overcloud ceph deploy".

Also, supports a --standalone feature which directly calls
the ceph_spec library in tripleo-common in the depends-on
chain. As Ansible is used less the tripleo-pythonclient can
use the ceph_spec library directly for more than just the
--standalone feature.

Change-Id: Icd18de08ebc818441e45ff7ce9f348fbc5813253
(cherry picked from commit 3701f2ac31)

4. Add --standalone to overcloud ceph user and deploy

Both 'openstack overcloud ceph user enable' and 'openstack
overcloud ceph deploy' currently use Ansible and require an
inventory with hosts matching those in the Ceph spec. The
inventory is created in the working directory when 'openstack
overcloud node provision' runs but for a standalone deployment
the user would need to create it manually. Abstract this detail
away by providing a --standalone option for single node deploys
which do not use metalsmith.

Change-Id: Icb785f015ee69d45dd513aef09fac625de421250
(cherry picked from commit 88ec87caf6)

5. Build standalone ceph_spec with Ansible

Instead of calling the ceph_spec library directly from
the python-tripleoclient call an ansible wrapper to it
instead so that the library does not need to live in
tripleo-common and bug 1961325 can be avoided.

Closes-Bug: 1961325
Change-Id: I7514c95b5c3c24a82e8703556d4205b272270511
(cherry picked from commit ec411b9329)
This commit is contained in:
John Fulton 2021-12-21 17:04:50 +00:00 committed by Francesco Pantano
parent b224e0b4f3
commit 46919b64db
5 changed files with 406 additions and 19 deletions

View File

@ -0,0 +1,14 @@
---
features:
- |
New command "openstack overcloud ceph spec" has been added. This command
may be used to create a cephadm spec file as a function of the output of
metalsmith and a TripleO roles file. For example, if metalsmith output a
file with multiple hosts of differing roles and each role contained various
Ceph services, then a cephadm spec file could parse these files and return
input compatible with cephadm. The ceph spec file may be then be passed to
"openstack overcloud ceph deploy" so that cephadm deploys only those Ceph
services on those hosts. This feature should save users from the need to
create two different files containing much of the same data and make it
easier and less error prone to include Ceph in a deployment without the
need to manually create the Ceph spec file.

View File

@ -43,6 +43,7 @@ openstack.tripleoclient.v2 =
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_ceph_spec = tripleoclient.v2.overcloud_ceph:OvercloudCephSpec
overcloud_ceph_user_disable = tripleoclient.v2.overcloud_ceph:OvercloudCephUserDisable
overcloud_ceph_user_enable = tripleoclient.v2.overcloud_ceph:OvercloudCephUserEnable
overcloud_config_download = tripleoclient.v1.overcloud_config:DownloadConfig

View File

@ -45,6 +45,7 @@ class TestOvercloudCephDeploy(fakes.FakePlaybookExecution):
'--stack', 'overcloud',
'--skip-user-create',
'--skip-hosts-config',
'--mon-ip', '127.0.0.1',
'--cephadm-ssh-user', 'jimmy',
'--output', 'deployed-ceph.yaml',
'--container-namespace', 'quay.io/ceph',
@ -66,8 +67,10 @@ class TestOvercloudCephDeploy(fakes.FakePlaybookExecution):
"deployed_ceph_tht_path": mock.ANY,
"working_dir": mock.ANY,
"stack_name": 'overcloud',
"tripleo_cephadm_standalone": False,
'tripleo_cephadm_ssh_user': 'jimmy',
'tripleo_cephadm_cluster': 'ceph',
'tripleo_cephadm_first_mon_ip': '127.0.0.1',
'tripleo_roles_path': mock.ANY,
'tripleo_cephadm_container_ns': 'quay.io/ceph',
'tripleo_cephadm_container_image': 'ceph',
@ -75,6 +78,53 @@ class TestOvercloudCephDeploy(fakes.FakePlaybookExecution):
}
)
@mock.patch('tripleoclient.utils.get_ceph_networks', autospect=True)
@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_deploy_ceph_spec(self, mock_playbook, mock_abspath,
mock_path_exists, mock_tempdirs,
mock_get_ceph_networks):
arglist = ['--yes',
'--stack', 'overcloud',
'--skip-user-create',
'--skip-hosts-config',
'--mon-ip', '127.0.0.1',
'--ceph-spec', 'ceph_spec.yaml',
'--cephadm-ssh-user', 'jimmy',
'--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,
skip_tags='cephadm_ssh_user',
reproduce_command=False,
extra_vars_file=mock.ANY,
extra_vars={
"deployed_ceph_tht_path": mock.ANY,
"working_dir": mock.ANY,
"stack_name": 'overcloud',
"tripleo_cephadm_standalone": False,
'tripleo_roles_path': mock.ANY,
'tripleo_cephadm_first_mon_ip': '127.0.0.1',
'tripleo_cephadm_cluster': 'ceph',
'dynamic_ceph_spec': False,
'ceph_spec_path': mock.ANY,
'tripleo_cephadm_container_ns': 'quay.io/ceph',
'tripleo_cephadm_container_image': 'ceph',
'tripleo_cephadm_container_tag': 'latest',
'tripleo_cephadm_ssh_user': 'jimmy',
}
)
@mock.patch('os.path.abspath', autospect=True)
@mock.patch('os.path.exists', autospect=True)
def test_overcloud_deploy_ceph_no_overwrite(self, mock_abspath,
@ -104,6 +154,16 @@ class TestOvercloudCephDeploy(fakes.FakePlaybookExecution):
self.assertRaises(osc_lib_exc.CommandError,
self.cmd.take_action, parsed_args)
@mock.patch('os.path.abspath', autospect=True)
@mock.patch('os.path.exists', autospect=True)
def test_overcloud_deploy_ceph_no_metal(self, mock_abspath,
mock_path_exists):
arglist = ['--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)
class TestOvercloudCephUserDisable(fakes.FakePlaybookExecution):
def setUp(self):
@ -265,3 +325,45 @@ class TestOvercloudCephUserEnable(fakes.FakePlaybookExecution):
"tripleo_cephadm_action": 'enable'
}
)
class TestOvercloudCephSpec(fakes.FakePlaybookExecution):
def setUp(self):
super(TestOvercloudCephSpec, 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.OvercloudCephSpec(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_ceph_spec(self, mock_playbook, mock_abspath,
mock_path_exists, mock_tempdirs):
arglist = ['deployed-metal.yaml', '--yes',
'--stack', 'overcloud',
'--roles-data', 'roles_data.yaml',
'--osd-spec', 'osd_spec.yaml',
'--output', 'ceph_spec.yaml']
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,
tags='ceph_spec',
reproduce_command=False,
extra_vars={
"baremetal_deployed_path": mock.ANY,
'tripleo_roles_path': mock.ANY,
'osd_spec_path': mock.ANY,
'ceph_spec_path': mock.ANY,
}
)

View File

@ -3222,6 +3222,29 @@ def get_host_groups_from_ceph_spec(ceph_spec_path, prefix='',
return hosts
def standalone_ceph_inventory(working_dir):
"""return an ansible inventory for deployed ceph standalone
:param working_dir: directory where inventory should be written
:return string: the path to the inventory
"""
host = get_hostname()
inv = \
{'Standalone':
{'hosts': {host: {},
'undercloud': {}},
'vars': {'ansible_connection': 'local',
'ansible_host': host,
'ansible_python_interpreter': sys.executable}},
'allovercloud':
{'children': {'Standalone': {}}}}
path = os.path.join(working_dir,
constants.TRIPLEO_STATIC_INVENTORY)
with open(path, 'w') as f:
f.write(yaml.safe_dump(inv))
return path
def cleanup_host_entry(entry):
# remove any tab or space excess
entry_stripped = re.sub('[ \t]+', ' ', str(entry).rstrip())

View File

@ -79,11 +79,13 @@ class OvercloudCephDeploy(command.Command):
def get_parser(self, prog_name):
parser = super(OvercloudCephDeploy, self).get_parser(prog_name)
parser.add_argument('baremetal_env',
parser.add_argument('baremetal_env', nargs='?',
metavar='<deployed_baremetal.yaml>',
help=_('Path to the environment file '
'output from "openstack '
'overcloud node provision".'))
'overcloud node provision". '
'This argument may be excluded '
'only if --ceph-spec is used.'))
parser.add_argument('-o', '--output', required=True,
metavar='<deployed_ceph.yaml>',
help=_('The path to the output environment '
@ -176,6 +178,14 @@ class OvercloudCephDeploy(command.Command):
"'ceph --cluster foo health' unless export "
"CEPH_ARGS='--cluster foo' is used."),
default='ceph')
parser.add_argument('--mon-ip',
help=_(
"IP address of the first Ceph monitor. "
"If not set, an IP from the Ceph "
"public_network of a server with the "
"mon label from the Ceph spec is used. "
"IP must already be active on server."),
default='')
parser.add_argument('--config',
help=_(
"Path to an existing ceph.conf with settings "
@ -247,10 +257,12 @@ class OvercloudCephDeploy(command.Command):
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 "
"Path to an existing Ceph spec file. If "
"not provided a spec will be generated "
"automatically based on --roles-data and "
"<deployed_baremetal.yaml>"),
"<deployed_baremetal.yaml>. The "
"<deployed_baremetal.yaml> parameter is "
"optional only if --ceph-spec is used."),
default=None)
spec_group.add_argument('--osd-spec',
help=_(
@ -270,6 +282,11 @@ class OvercloudCephDeploy(command.Command):
"Path to an existing crush hierarchy spec "
"file. "),
default=None)
parser.add_argument('--standalone', default=False,
action='store_true',
help=_("Use single host Ansible inventory. "
"Used only for development or testing "
"environments."))
parser.add_argument('--container-image-prepare',
help=_(
"Path to an alternative "
@ -327,14 +344,7 @@ class OvercloudCephDeploy(command.Command):
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(
@ -354,8 +364,11 @@ class OvercloudCephDeploy(command.Command):
working_dir = os.path.abspath(parsed_args.working_dir)
oooutils.makedirs(working_dir)
inventory = os.path.join(working_dir,
constants.TRIPLEO_STATIC_INVENTORY)
if parsed_args.standalone:
inventory = oooutils.standalone_ceph_inventory(working_dir)
else:
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: "
@ -365,13 +378,29 @@ class OvercloudCephDeploy(command.Command):
# 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,
"tripleo_cephadm_standalone": parsed_args.standalone
}
extra_vars_file = None
# optional paths to pass to playbook
if parsed_args.ceph_spec is None and \
parsed_args.baremetal_env is None:
raise oscexc.CommandError(
"Either <deployed_baremetal.yaml> "
"or --ceph-spec must be used.")
if parsed_args.baremetal_env:
baremetal_env_path = os.path.abspath(parsed_args.baremetal_env)
if not os.path.exists(baremetal_env_path):
raise oscexc.CommandError(
"Baremetal environment file does not exist:"
" %s" % parsed_args.baremetal_env)
else:
extra_vars['baremetal_deployed_path'] = \
os.path.abspath(parsed_args.baremetal_env)
if parsed_args.roles_data:
if not os.path.exists(parsed_args.roles_data):
raise oscexc.CommandError(
@ -406,6 +435,15 @@ class OvercloudCephDeploy(command.Command):
extra_vars['tripleo_cephadm_cluster'] = \
parsed_args.cluster
if parsed_args.mon_ip:
if not oooutils.is_valid_ip(parsed_args.mon_ip):
raise oscexc.CommandError(
"Invalid IP address '%s' passed to --mon-ip."
% parsed_args.mon_ip)
else:
extra_vars['tripleo_cephadm_first_mon_ip'] = \
parsed_args.mon_ip
if parsed_args.ceph_spec:
if not os.path.exists(parsed_args.ceph_spec):
raise oscexc.CommandError(
@ -658,6 +696,11 @@ class OvercloudCephUserDisable(command.Command):
metavar='<FSID>', required=True,
help=_("The FSID of the Ceph cluster to be "
"disabled. Required for disable option."))
parser.add_argument('--standalone', default=False,
action='store_true',
help=_("Use single host Ansible inventory. "
"Used only for development or testing "
"environments."))
return parser
@ -693,8 +736,11 @@ class OvercloudCephUserDisable(command.Command):
working_dir = os.path.abspath(parsed_args.working_dir)
oooutils.makedirs(working_dir)
inventory = os.path.join(working_dir,
constants.TRIPLEO_STATIC_INVENTORY)
if parsed_args.standalone:
inventory = oooutils.standalone_ceph_inventory(working_dir)
else:
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: "
@ -777,6 +823,11 @@ class OvercloudCephUserEnable(command.Command):
"so that cephadm will be re-enabled "
"for the Ceph cluster idenified "
"by the FSID."))
parser.add_argument('--standalone', default=False,
action='store_true',
help=_("Use single host Ansible inventory. "
"Used only for development or testing "
"environments."))
parser = arg_parse_common(parser)
return parser
@ -807,8 +858,11 @@ class OvercloudCephUserEnable(command.Command):
working_dir = os.path.abspath(parsed_args.working_dir)
oooutils.makedirs(working_dir)
inventory = os.path.join(working_dir,
constants.TRIPLEO_STATIC_INVENTORY)
if parsed_args.standalone:
inventory = oooutils.standalone_ceph_inventory(working_dir)
else:
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: "
@ -860,3 +914,196 @@ class OvercloudCephUserEnable(command.Command):
limit_hosts=ceph_hosts['_admin'][0],
reproduce_command=False,
)
class OvercloudCephSpec(command.Command):
log = logging.getLogger(__name__ + ".OvercloudCephSpec")
auth_required = False
def get_parser(self, prog_name):
parser = super(OvercloudCephSpec, self).get_parser(prog_name)
parser.add_argument('baremetal_env', nargs='?',
metavar='<deployed_baremetal.yaml>',
help=_('Path to the environment file '
'output from "openstack '
'overcloud node provision". '
'This argument may be excluded '
'only if --standalone is used.'))
parser.add_argument('-o', '--output', required=True,
metavar='<ceph_spec.yaml>',
help=_('The path to the output cephadm spec '
'file to pass to the "openstack '
'overcloud ceph deploy --ceph-spec '
'<ceph_spec.yaml>" command.'))
parser.add_argument('-y', '--yes', default=False, action='store_true',
help=_('Skip yes/no prompt before overwriting an '
'existing <ceph_spec.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))
parser.add_argument('--mon-ip',
help=_(
"IP address of the first Ceph monitor. "
"Only available with --standalone."),
default='')
parser.add_argument('--standalone', default=False,
action='store_true',
help=_("Create a spec file for a standalone "
"deployment. Used for single server "
"development or testing environments."))
spec_group = parser.add_mutually_exclusive_group()
spec_group.add_argument('--osd-spec',
help=_(
"Path to an existing OSD spec file. "
"When the Ceph spec file is generated "
"its OSD spec 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)
spec_group.add_argument('--crush-hierarchy',
help=_(
"Path to an existing crush hierarchy spec "
"file. "),
default=None)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
output_path = os.path.abspath(parsed_args.output)
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)
if parsed_args.standalone:
inventory = oooutils.standalone_ceph_inventory(working_dir)
else:
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 = {
'ceph_spec_path': output_path,
}
# optional paths to pass to playbook
if parsed_args.standalone is None and \
parsed_args.baremetal_env is None:
raise oscexc.CommandError(
"Either <deployed_baremetal.yaml> "
"or --standalone must be used.")
if parsed_args.baremetal_env:
baremetal_env_path = os.path.abspath(parsed_args.baremetal_env)
if not os.path.exists(baremetal_env_path):
raise oscexc.CommandError(
"Baremetal environment file does not exist:"
" %s" % parsed_args.baremetal_env)
else:
extra_vars['baremetal_deployed_path'] = \
os.path.abspath(parsed_args.baremetal_env)
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.mon_ip:
if not oooutils.is_valid_ip(parsed_args.mon_ip):
raise oscexc.CommandError(
"Invalid IP address '%s' passed to --mon-ip."
% parsed_args.mon_ip)
else:
if parsed_args.standalone:
extra_vars['tripleo_cephadm_first_mon_ip'] = \
parsed_args.mon_ip
else:
raise oscexc.CommandError(
"Option --mon-ip may only be "
"used with --standalone")
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)
if parsed_args.crush_hierarchy:
if not os.path.exists(parsed_args.crush_hierarchy):
raise oscexc.CommandError(
"Crush Hierarchy Spec file not found --crush-hierarchy %s."
% os.path.abspath(parsed_args.crush_hierarchy))
else:
extra_vars['crush_hierarchy_path'] = \
os.path.abspath(parsed_args.crush_hierarchy)
if parsed_args.standalone:
spec_playbook = 'cli-standalone-ceph-spec.yaml'
tags = ''
else:
spec_playbook = 'cli-deployed-ceph.yaml'
tags = 'ceph_spec'
with oooutils.TempDirs() as tmp:
oooutils.run_ansible_playbook(
playbook=spec_playbook,
inventory=inventory,
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=oooutils.playbook_verbosity(self=self),
extra_vars=extra_vars,
reproduce_command=False,
tags=tags,
)