The use of function maybe_do_policyd_overrides_on_config_changed is deprecated in favor of maybe_do_policyd_overrides. This would trigger deprecation warning messages not only in logs but also in actions such as seen in Octavia Charm The unittest covering the call has also been updated accordingly Closes-Bug: #1939840 Change-Id: Iec91d60727533e6291c3ec20442197ac44f4607c
542 lines
21 KiB
Python
542 lines
21 KiB
Python
# Copyright 2019 Canonical Ltd
|
|
#
|
|
# 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 collections
|
|
import enum
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
|
|
import charms_openstack.charm
|
|
from charms_openstack.charm.classes import SNAP_PATH_PREFIX_FORMAT
|
|
|
|
import charmhelpers.core as ch_core
|
|
import charmhelpers.contrib.openstack.context as ch_context
|
|
import charmhelpers.contrib.openstack.policyd as ch_policyd
|
|
|
|
|
|
class BaseOpenStackCephCharm(object):
|
|
"""Base class for Ceph classes.
|
|
|
|
Provided as a mixin so charm authors can compose the charm class
|
|
appropriate for their use case.
|
|
"""
|
|
# Ceph cluster name is used for naming of various configuration files and
|
|
# directories. It is also used by Ceph command line tools to interface
|
|
# with multiple distinct Ceph clusters from one place.
|
|
ceph_cluster_name = 'ceph'
|
|
|
|
# Both consumers and providers of Ceph services share a pattern of the
|
|
# need for a key and a keyring file on disk, they also share naming
|
|
# conventions.
|
|
# The most used key naming convention is for all instances of a service
|
|
# to share a key named after the service.
|
|
# Some services follow a different pattern with unique key names for each
|
|
# instance of a service. (e.g. RadosGW Multi-Site, RBD Mirroring)
|
|
ceph_key_per_unit_name = False
|
|
|
|
# Ceph service name and service type is used for sectioning of
|
|
# ``ceph.conf`, appropriate naming of keys and keyring files. By default
|
|
# ceph service name is determined from `application_name` property.
|
|
# If this does not fit your use case you can override.
|
|
ceph_service_name_override = ''
|
|
|
|
# Unless you are writing a charm providing Ceph mon|osd|mgr|mds services
|
|
# this should probably be left as-is.
|
|
|
|
class CephServiceType(enum.Enum):
|
|
"""Ceph service type."""
|
|
client = 'client'
|
|
mds = 'mds'
|
|
mgr = 'mgr'
|
|
mon = 'mon'
|
|
osd = 'osd'
|
|
|
|
def __str__(self):
|
|
"""Return string representation of value.
|
|
|
|
:returns: string representation of value.
|
|
:rtype: str
|
|
"""
|
|
return self.value
|
|
|
|
ceph_service_type = CephServiceType.client
|
|
|
|
# Path prefix to where the Ceph keyring should be stored.
|
|
ceph_keyring_path_prefix = '/etc/ceph'
|
|
|
|
@property
|
|
@ch_core.hookenv.cached
|
|
def application_name(self):
|
|
"""Provide the name this instance of the charm has in the Juju model.
|
|
|
|
:returns: Application name
|
|
:rtype: str
|
|
"""
|
|
return ch_core.hookenv.application_name()
|
|
|
|
@property
|
|
def snap_path_prefix(self, snap=None):
|
|
"""Provide the path prefix for a snap.
|
|
|
|
:param snap: (Optional) The snap you want to build a path prefix for
|
|
If not provided will attempt to build for the first snap
|
|
listed in self.snaps.
|
|
:type snap: str
|
|
:returns: Path prefix for snap or the empty string ('')
|
|
:rtype: str
|
|
"""
|
|
if snap:
|
|
return SNAP_PATH_PREFIX_FORMAT.format(snap)
|
|
elif self.snaps:
|
|
return SNAP_PATH_PREFIX_FORMAT.format(self.snaps[0])
|
|
else:
|
|
return ''
|
|
|
|
@property
|
|
def ceph_service_name(self):
|
|
"""Provide Ceph service name for use in config, key and keyrings.
|
|
|
|
:returns: Ceph service name
|
|
:rtype: str
|
|
"""
|
|
return (self.ceph_service_name_override or
|
|
self.application_name)
|
|
|
|
@property
|
|
def ceph_key_name(self):
|
|
"""Provide Ceph key name for the charm managed service.
|
|
|
|
:returns: Ceph key name
|
|
:rtype: str
|
|
"""
|
|
if self.ceph_service_type == self.CephServiceType.client:
|
|
base_key_name = '{}.{}'.format(
|
|
self.ceph_service_type,
|
|
self.ceph_service_name)
|
|
else:
|
|
base_key_name = self.ceph_service_name
|
|
|
|
if self.ceph_key_per_unit_name:
|
|
return '{}.{}'.format(
|
|
base_key_name,
|
|
socket.gethostname())
|
|
else:
|
|
return base_key_name
|
|
|
|
@property
|
|
def ceph_keyring_path(self):
|
|
"""Provide a path to where the Ceph keyring should be stored.
|
|
|
|
:returns: Path to directory
|
|
:rtype: str
|
|
"""
|
|
return os.path.join(self.snap_path_prefix,
|
|
self.ceph_keyring_path_prefix)
|
|
|
|
def ceph_keyring_absolute_path(self, cluster_name=None):
|
|
"""Provide absolute path to keyring file.
|
|
|
|
:param cluster_name: (Optional) Name of Ceph cluster to operate on.
|
|
Defaults to value of ``self.ceph_cluster_name``.
|
|
:type cluster_name: str
|
|
:returns: Absolute path to keyring file
|
|
:rtype: str
|
|
"""
|
|
if self.ceph_service_type == self.CephServiceType.client:
|
|
keyring_name = ('{}.{}.keyring'
|
|
.format(cluster_name or self.ceph_cluster_name,
|
|
self.ceph_key_name))
|
|
else:
|
|
keyring_name = 'keyring'
|
|
|
|
keyring_absolute_path = os.path.join(self.ceph_keyring_path,
|
|
keyring_name)
|
|
return keyring_absolute_path
|
|
|
|
def configure_ceph_keyring(self, key, cluster_name=None):
|
|
"""Creates or updates a Ceph keyring file.
|
|
|
|
:param key: Key data
|
|
:type key: str
|
|
:param cluster_name: (Optional) Name of Ceph cluster to operate on.
|
|
Defaults to value of ``self.ceph_cluster_name``.
|
|
:type cluster_name: str
|
|
:returns: Absolute path to keyring file
|
|
:rtype: str
|
|
:raises: subprocess.CalledProcessError, OSError
|
|
"""
|
|
if not os.path.isdir(self.ceph_keyring_path):
|
|
ch_core.host.mkdir(self.ceph_keyring_path,
|
|
owner=self.user, group=self.group, perms=0o750)
|
|
keyring_absolute_path = self.ceph_keyring_absolute_path(
|
|
cluster_name=cluster_name)
|
|
cmd = [
|
|
'ceph-authtool', keyring_absolute_path,
|
|
'--create-keyring', '--name={}'.format(self.ceph_key_name),
|
|
'--add-key', key, '--mode', '0600',
|
|
]
|
|
try:
|
|
subprocess.check_call(cmd)
|
|
except subprocess.CalledProcessError as cp:
|
|
if not cp.returncode == 1:
|
|
raise
|
|
# the version of ceph-authtool on the system does not have
|
|
# --mode command line argument
|
|
subprocess.check_call(cmd[:-2])
|
|
os.chmod(keyring_absolute_path, 0o600)
|
|
shutil.chown(keyring_absolute_path, user=self.user, group=self.group)
|
|
return keyring_absolute_path
|
|
|
|
def delete_ceph_keyring(self, cluster_name=None):
|
|
"""Deletes an existing Ceph keyring file.
|
|
|
|
:param cluster_name: (Optional) Name of Ceph cluster to operate on.
|
|
Defaults to value of ``self.ceph_cluster_name``.
|
|
:type cluster_name: str
|
|
:returns: Absolute path to the now removed keyring file or empty string
|
|
:rtype: str
|
|
"""
|
|
keyring_absolute_path = self.ceph_keyring_absolute_path(
|
|
cluster_name=cluster_name)
|
|
try:
|
|
os.remove(keyring_absolute_path)
|
|
return keyring_absolute_path
|
|
except OSError:
|
|
return ''
|
|
|
|
@staticmethod
|
|
def _get_bluestore_compression():
|
|
"""Get BlueStore Compression charm configuration if present.
|
|
|
|
:returns: Dictionary of options suitable for passing on as keyword
|
|
arguments or None.
|
|
:rtype: Optional[Dict[str,any]]
|
|
:raises: ValueError
|
|
"""
|
|
try:
|
|
bluestore_compression = (
|
|
ch_context.CephBlueStoreCompressionContext())
|
|
bluestore_compression.validate()
|
|
except KeyError:
|
|
# The charm does not have BlueStore Compression options defined
|
|
bluestore_compression = None
|
|
if bluestore_compression:
|
|
return bluestore_compression.get_kwargs()
|
|
|
|
def states_to_check(self, required_relations=None):
|
|
"""Augment states to check handling.
|
|
|
|
Validates Ceph specific configuration options and adds end user
|
|
feedback through juju status.
|
|
|
|
:param required_relations: List of relations which overrides
|
|
self.relations
|
|
:type required_relations: Optional[List[str]]
|
|
:returns: Map of relations and their states to check
|
|
:rtype: Dict[str,List[Tuple[str,str,str]]]
|
|
"""
|
|
states_to_check = super().states_to_check(
|
|
required_relations=required_relations)
|
|
try:
|
|
self._get_bluestore_compression()
|
|
except ValueError as e:
|
|
# we add a made up relation to have the library set the status for
|
|
# us.
|
|
states_to_check['charm.bluestore_compression'] = [
|
|
('charm.bluestore_compression',
|
|
'blocked',
|
|
'Invalid configuration: {}'.format(str(e))),
|
|
]
|
|
return states_to_check
|
|
|
|
def create_pool(self, ceph_interface, pool_name=None):
|
|
"""Request pool for service.
|
|
|
|
The pool created will be a replicated pool and will adopt standard
|
|
BlueStore compression options that are configured for the charm.
|
|
Charms that do not have provide BlueStore compression options will
|
|
create pools using the Ceph storage configured defaults.
|
|
|
|
:param ceph_interface: Ceph interface instance
|
|
:type ceph_interface: CephRequires
|
|
:param pool_name: (Optional) name of the pool to create.
|
|
Defaults to the name of the application deployed.
|
|
:type pool_name: str
|
|
"""
|
|
try:
|
|
bluestore_compression = self._get_bluestore_compression()
|
|
except ValueError as e:
|
|
# One or more of the values provided for the configuration options
|
|
# is invalid, do not attempt to create pool. The end user will be
|
|
# informed about the condition through juju status
|
|
# (see the ``states_to_check`` method above).
|
|
ch_core.hookenv.log('Invalid value(s) provided for Ceph BlueStore '
|
|
'compression: "{}"'
|
|
.format(str(e)))
|
|
return
|
|
kwargs = {
|
|
'name': pool_name or self.application_name,
|
|
}
|
|
if bluestore_compression:
|
|
kwargs.update(bluestore_compression)
|
|
ceph_interface.create_replicated_pool(**kwargs)
|
|
|
|
|
|
class CephCharm(BaseOpenStackCephCharm,
|
|
charms_openstack.charm.OpenStackCharm):
|
|
"""Class for charms deploying Ceph services.
|
|
|
|
It provides useful defaults to make release detection work when no
|
|
OpenStack packages are installed.
|
|
|
|
Ceph services also have different preferences for placement of keyring
|
|
files.
|
|
|
|
Code useful for and shared among charms deploying software that want to
|
|
consume Ceph services should be added to the BaseOpenStackCephCharm base
|
|
class.
|
|
"""
|
|
|
|
abstract_class = True
|
|
|
|
# Ubuntu Ceph packages are distributed along with the Ubuntu OpenStack
|
|
# packages, both for distro and UCA.
|
|
# Map OpenStack release to the Ceph release distributed with it.
|
|
package_codenames = {
|
|
'ceph-common': collections.OrderedDict([
|
|
('0', 'icehouse'), # 0.80 Firefly
|
|
('10', 'mitaka'), # 10.2.x Jewel
|
|
('12', 'pike'), # 12.2.x Luminous
|
|
('13', 'rocky'), # 13.2.x Mimic
|
|
('14', 'train'), # 14.2.x Nautilus
|
|
('15', 'ussuri'), # 15.2.x Octopus
|
|
]),
|
|
}
|
|
|
|
# Package to determine application version from
|
|
version_package = release_pkg = 'ceph-common'
|
|
|
|
# release = the first release in which this charm works. Refer to
|
|
# package_codenames variable above for table of OpenStack to Ceph releases.
|
|
release = 'icehouse'
|
|
|
|
# Python version used to execute installed workload
|
|
python_version = 3
|
|
|
|
# The name of the repository source configuration option.
|
|
# The ``ceph`` layer provides the ``config.yaml`` counterpart.
|
|
source_config_key = 'source'
|
|
|
|
# To make use of the CephRelationAdapter the derived charm class should
|
|
# define its own RelationAdapters class that inherits from
|
|
# ``adapters.OpenStackRelationAdapters`` or
|
|
# ``adapters.OpenStackAPIRelationAdapters``, whichever is most relevant.
|
|
#
|
|
# The custom RelationAdapters class should map the relation that provides
|
|
# the interface with a``mon_hosts`` property or function to the
|
|
# CephRelationAdapter by extending the ``relation_adapters`` dict.
|
|
#
|
|
# There is currently no standardization of relevant relation names among
|
|
# the Ceph providing or consuming charms, so it does currently not make
|
|
# sense to add this to the default relation adapters.
|
|
# adapters_class = MyCephCharmRelationAdapters
|
|
|
|
# Path prefix to where the Ceph keyring should be stored.
|
|
ceph_keyring_path_prefix = '/var/lib/ceph'
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Initialize class."""
|
|
super().__init__(**kwargs)
|
|
self.hostname = socket.gethostname()
|
|
|
|
@property
|
|
def ceph_keyring_path(self):
|
|
"""Provide a path to where the Ceph keyring should be stored.
|
|
|
|
:returns: Path to directory
|
|
:rtype: str
|
|
"""
|
|
keyring_path_components = [
|
|
self.snap_path_prefix,
|
|
self.ceph_keyring_path_prefix,
|
|
self.ceph_service_name]
|
|
|
|
if self.ceph_service_type != self.CephServiceType.client:
|
|
keyring_path_components.append(
|
|
'{}-{}'.format(self.ceph_cluster_name,
|
|
self.hostname))
|
|
|
|
return os.path.join(*keyring_path_components)
|
|
|
|
def configure_ceph_keyring(self, key, cluster_name=None):
|
|
"""Override parent method for Ceph service providing charms.
|
|
|
|
:param cluster_name: (Optional) Name of Ceph cluster to operate on.
|
|
Defaults to value of ``self.ceph_cluster_name``.
|
|
:type cluster_name: str
|
|
:raises: OSError
|
|
"""
|
|
keyring_absolute_path = super().configure_ceph_keyring(
|
|
key, cluster_name=cluster_name)
|
|
if self.ceph_service_type != self.CephServiceType.client:
|
|
return
|
|
# If the service is a client-type sevice (sych as RBD Mirror) add
|
|
# symlink to key in ``/etc/ceph``.
|
|
symlink_absolute_path = os.path.join(
|
|
'/etc/ceph',
|
|
os.path.basename(keyring_absolute_path))
|
|
if os.path.exists(symlink_absolute_path):
|
|
try:
|
|
if (os.readlink(symlink_absolute_path) !=
|
|
keyring_absolute_path):
|
|
os.remove(symlink_absolute_path)
|
|
else:
|
|
# Symlink exists and points to expected location
|
|
return
|
|
except OSError:
|
|
# We expected a symlink.
|
|
# Fall through and let os.symlink raise error.
|
|
pass
|
|
os.symlink(keyring_absolute_path, symlink_absolute_path)
|
|
|
|
def install(self):
|
|
"""Install packages related to this charm based on
|
|
contents of self.packages attribute, after first
|
|
configuring the installation source.
|
|
"""
|
|
self.configure_source()
|
|
super().install()
|
|
|
|
|
|
class PolicydOverridePlugin(object):
|
|
"""The PolicydOverridePlugin is provided to manage the policy.d overrides
|
|
to charms.openstack charms. It heavily leans on the
|
|
charmhelpers.contrib.openstack.policyd to provide the functionality. The
|
|
methods provided in this class simply use the functions from charm-helpers
|
|
so that charm authors can simply include this plugin class into the
|
|
inheritance list of the charm class.
|
|
|
|
It's very important that the PolicyOverridePlugin class appear FIRST in the
|
|
list of classes when declaring the charm class. This is to ensure that the
|
|
config_changed() method in this class gets called first, and it then calls
|
|
other classes. Otherwise, the config_changed method in the base class will
|
|
need to call the config_changed() method in this class manually. e.g. from
|
|
Designate:
|
|
|
|
class DesignateCharm(ch_plugins.PolicydOverridePlugin,
|
|
openstack_charm.HAOpenStackCharm):
|
|
|
|
Note that this feature is only available with OpenStack versions of
|
|
'queens' and later, and Ubuntu versions of 'bionic' and later. Prior to
|
|
those versions, the feature will not activate. This is checked in the
|
|
charm-helpers policyd implementation functions which are called from this
|
|
class' implementation.
|
|
|
|
This should be read in conjunction with the module
|
|
charmhelpers.contrib.openstack.policyd which provides further details on
|
|
the changes that need to be made to a charm to enable this feature.
|
|
|
|
Note that the metadata.yaml and config.yaml needs to be updated for the
|
|
charm to actually be able to use this class. See the
|
|
charmhelpers.contrib.openstack.policyd module for further details.
|
|
|
|
The following class variables are used to drive the plugin and should be
|
|
declared on the class:
|
|
|
|
policyd_service_name = str
|
|
policyd_blacklist_paths = Union[None, List[str]]
|
|
policyd_blacklist_keys = Union[None, List[str]]
|
|
policyd_template_function = Union[None, Callable[[str], str]]
|
|
policyd_restart_on_change = Union[None, bool]
|
|
|
|
These have the following meanings:
|
|
|
|
policyd_service_name:
|
|
This is the name of the payload that is having an override. e.g.
|
|
keystone. It is used to construct the policy.d directory:
|
|
/etc/keystone/policy.d/
|
|
|
|
policyd_blacklist_paths: (Optional)
|
|
These are other policyd overrides that exist in the above directory
|
|
that should not be touched. It is a list of the FULL path. e.g.
|
|
/etc/keystone/policy.d/charm-overrides.yaml
|
|
|
|
policyd_blacklist_keys: (Optional)
|
|
These are keys that should not appear in the YAML files. e.g. admin.
|
|
|
|
policyd_template_function: (Optional)
|
|
This is an callable that takes a string that returns another string
|
|
that tis then loaded as the yaml file. This is intended to allow a
|
|
charm to modify the proposed yaml file to allow substitution of rules
|
|
and values under the control of the charm. The charm needs to supply
|
|
the substitution function (and thus the variables that will be used).
|
|
|
|
policyd_restart_on_change: Optional
|
|
If set to True, then the service will be restarted using the charm
|
|
class' `restart_services` method.
|
|
"""
|
|
|
|
def _policyd_function_args(self):
|
|
"""Returns the parameters that need to be passed to the charm-helpers
|
|
policyd implemenation functions.
|
|
|
|
:returns: ([openstack_release, payload_name],
|
|
{blacklist_paths=...,
|
|
blacklist_keys=...,
|
|
template_function=...,
|
|
restart_handler=...,})
|
|
:rtype: Tuple[List[str,str], Dict[str,str]]
|
|
"""
|
|
blacklist_paths = getattr(self, 'policyd_blacklist_paths', None)
|
|
blacklist_keys = getattr(self, 'policyd_blacklist_keys', None)
|
|
template_function = getattr(self, 'policyd_template_function', None)
|
|
if getattr(self, 'policyd_restart_on_change', False):
|
|
restart_handler = self.restart_services
|
|
else:
|
|
restart_handler = None
|
|
return ([self.release, self.policyd_service_name],
|
|
dict(blacklist_paths=blacklist_paths,
|
|
blacklist_keys=blacklist_keys,
|
|
template_function=template_function,
|
|
restart_handler=restart_handler))
|
|
|
|
def _maybe_policyd_overrides(self):
|
|
args, kwargs = self._policyd_function_args()
|
|
ch_policyd.maybe_do_policyd_overrides(*args, **kwargs)
|
|
|
|
def install(self):
|
|
"""Hook into the install"""
|
|
super().install()
|
|
self._maybe_policyd_overrides()
|
|
|
|
def upgrade_charm(self):
|
|
"""Check the policyd during an upgrade_charm"""
|
|
super().upgrade_charm()
|
|
self._maybe_policyd_overrides()
|
|
|
|
def config_changed(self):
|
|
"""Note that this is usually a nop, and is only called from the default
|
|
handler. Please check that the charm implementation actually uses it.
|
|
"""
|
|
try:
|
|
super().config_changed()
|
|
except Exception:
|
|
pass
|
|
args, kwargs = self._policyd_function_args()
|
|
if 'config_changed' not in kwargs.keys():
|
|
kwargs['config_changed'] = True
|
|
ch_policyd.maybe_do_policyd_overrides(*args, **kwargs)
|