charms.openstack/charms_openstack/plugins/classes.py

546 lines
20 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 base64
import collections
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.fetch as fetch
import charmhelpers.contrib.openstack.policyd as ch_policyd
import charms.reactive as reactive
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.
ceph_service_type = '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
"""
base_key_name = '{}.{}'.format(
self.ceph_service_type,
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
"""
keyring_name = ('{}.{}.keyring'
.format(cluster_name or self.ceph_cluster_name,
self.ceph_key_name))
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 ''
class CephCharm(charms_openstack.charm.OpenStackCharm,
BaseOpenStackCephCharm):
"""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
]),
}
# 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'
@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,
self.ceph_service_name)
def configure_ceph_keyring(self, key, cluster_name=None):
"""Override parent function to add symlink in ``/etc/ceph``."""
keyring_absolute_path = super().configure_ceph_keyring(
key, cluster_name=cluster_name)
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()
ch_policyd.maybe_do_policyd_overrides_on_config_changed(
*args, **kwargs)
TV_MOUNTS = "/var/triliovault-mounts"
class NFSShareNotMountedException(Exception):
"""Signal that the trilio nfs share is not mount"""
pass
class UnitNotLeaderException(Exception):
"""Signal that the unit is not the application leader"""
pass
class GhostShareAlreadyMountedException(Exception):
"""Signal that a ghost share is already mounted"""
pass
class TrilioVaultCharm(charms_openstack.charm.HAOpenStackCharm):
"""The TrilioVaultCharm class provides common specialisation of certain
functions for the Trilio charm set and is designed for use alongside
other base charms.openstack classes
"""
abstract_class = True
def __init__(self, **kwargs):
super(TrilioVaultCharm, self).__init__(**kwargs)
def configure_source(self):
"""Configure triliovault specific package sources in addition to
any general openstack package sources (via openstack-origin)
"""
with open(
"/etc/apt/sources.list.d/trilio-gemfury-sources.list", "w"
) as tsources:
tsources.write(ch_core.hookenv.config("triliovault-pkg-source"))
super().configure_source()
def install(self):
"""Install packages dealing with Trilio nuances for upgrades as well
Set the 'upgrade.triliovault' flag to ensure that any triliovault
packages are upgraded.
"""
self.configure_source()
packages = self.all_packages
if not reactive.is_flag_set("upgrade.triliovault"):
packages = fetch.filter_installed_packages(
self.all_packages)
if packages:
ch_core.hookenv.status_set('maintenance',
'Installing/upgrading packages')
fetch.apt_install(packages, fatal=True)
# AJK: we set this as charms can use it to detect installed state
self.set_state('{}-installed'.format(self.name))
self.update_api_ports()
# NOTE(jamespage): clear upgrade flag if set
if reactive.is_flag_set("upgrade.triliovault"):
reactive.clear_flag('upgrade.triliovault')
def series_upgrade_complete(self):
"""Re-configure sources post series upgrade"""
super().series_upgrade_complete()
self.configure_source()
def _encode_endpoint(self, backup_endpoint):
"""base64 encode an backup endpoint for cross mounting support"""
return base64.b64encode(backup_endpoint.encode()).decode()
def ghost_nfs_share(self, ghost_share):
"""Bind mount the local units nfs share to another sites location
:param ghost_share: NFS share URL to ghost
:type ghost_share: str
"""
nfs_share_path = os.path.join(
TV_MOUNTS,
self._encode_endpoint(ch_core.hookenv.config("nfs-shares"))
)
ghost_share_path = os.path.join(
TV_MOUNTS, self._encode_endpoint(ghost_share)
)
current_mounts = [mount[0] for mount in ch_core.host.mounts()]
if nfs_share_path not in current_mounts:
# Trilio has not mounted the NFS share so return
raise NFSShareNotMountedException(
"nfs-shares ({}) not mounted".format(
ch_core.hookenv.config("nfs-shares")
)
)
if ghost_share_path in current_mounts:
# bind mount already setup so return
raise GhostShareAlreadyMountedException(
"ghost mountpoint ({}) already bound".format(ghost_share_path)
)
if not os.path.exists(ghost_share_path):
os.mkdir(ghost_share_path)
ch_core.host.mount(nfs_share_path, ghost_share_path, options="bind")
class TrilioVaultSubordinateCharm(TrilioVaultCharm):
"""The TrilioVaultSubordinateCharm class provides common specialisation
of certain functions for the Trilio charm set and is designed for usei
alongside other base charms.openstack classes for subordinate charms
"""
abstract_class = True
def __init__(self, **kwargs):
super(TrilioVaultSubordinateCharm, self).__init__(**kwargs)
def configure_source(self):
"""Configure triliovault specific package sources in addition to
any general openstack package sources (via openstack-origin)
"""
super().configure_source()
fetch.apt_update(fatal=True)