Add Ceph base class and relation adapter

Our Ceph packages are distributed along with our OpenStack
packages both for distro and UCA.

Reactive Ceph charms can thus reuse a large portion of the
``charms.openstack`` library, both for basic package and
service management and for default reactive handlers.

The new classes are placed in a ``plugins`` directory.

First consumer of the ``CephCharm`` class is the
``ceph-rbd-mirror`` charm accompanied by the
``charm-layer-ceph`` layer.

Existing reactive charms that consume or provide Ceph services
should also be ported to use the base functionality now added
to the ``OpenStackCharm`` and ``CephCharm`` base classes
(e.g. ``ceph-fs``, ``gnocchi``).

Adds support to OpenStackRelationAdapter for passing on
properties from Endpoint based interfaces.

Change-Id: I86bdd35b301fa39504c5d5af9a2b7d01bfd38768
This commit is contained in:
Frode Nordahl 2019-02-05 14:01:17 +01:00
parent 341b879cc1
commit 552fa0e1ec
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
10 changed files with 613 additions and 23 deletions

View File

@ -124,28 +124,54 @@ class OpenStackRelationAdapter(object):
def _setup_properties(self):
"""
Setup property based accessors for an interfaces
auto accessors
Setup property based accessors for interface.
For charms.reactive.Endpoint interfaces a list of properties is built
by looking for type(property) attributes added by the interface class.
For charms.reactive.RelationBase interfaces the auto_accessors list is
used to determine which properties to set.
Note that the accessor is dynamic as each access calls the underlying
getattr() for each property access.
"""
try:
self.accessors.extend(self.relation.auto_accessors)
except AttributeError:
self.accessors = []
for field in self.accessors:
meth_name = field.replace('-', '_')
# Get the relation property dynamically
# Note the additional lambda name: is to create a closure over
# meth_name so that a new 'name' gets created for each loop,
# otherwise the same variable meth_name is referenced in each of
# the internal lambdas. i.e. this is (lambda x: ...)(value)
setattr(self.__class__,
meth_name,
(lambda name: property(
lambda self: getattr(
self.relation, name)()))(meth_name))
if isinstance(self.relation, charms.reactive.Endpoint):
# Get names of properties the interface class instance has,
# remove the properties inherited from charms.reactive.Endpoint
# base class
interface_instance_names = dir(self.relation)
base_class_names = dir(charms.reactive.Endpoint)
property_names = [
p for p in interface_instance_names if isinstance(
getattr(type(self.relation), p, None), property) and
p not in base_class_names]
for name in property_names:
# The double lamda trick is necessary to ensure we get fresh
# data from the interface class property at every call to the
# new property. Without it we would store the value that was
# there at instantiation of this class.
setattr(self.__class__,
name,
(lambda name: property(
lambda self: getattr(
self.relation, name)))(name))
else:
try:
self.accessors.extend(self.relation.auto_accessors)
except AttributeError:
self.accessors = []
for field in self.accessors:
meth_name = field.replace('-', '_')
# Get the relation property dynamically
# Note the additional lambda name: is to create a closure over
# meth_name so that a new 'name' gets created for each loop,
# otherwise the same variable meth_name is referenced in each
# of the internal lambdas. i.e. this is (lambda x: ...)(value)
setattr(self.__class__,
meth_name,
(lambda name: property(
lambda self: getattr(
self.relation, name)()))(meth_name))
class MemcacheRelationAdapter(OpenStackRelationAdapter):

View File

@ -36,7 +36,8 @@ IFACE_KEY = "vip_iface"
DNSHA_KEY = "dns-ha"
APACHE_SSL_VHOST = '/etc/apache2/sites-available/openstack_https_frontend.conf'
SYSTEM_CA_CERTS = '/etc/ssl/certs/ca-certificates.crt'
SNAP_CA_CERTS = '/var/snap/{}/common/etc/ssl/certs/ca-certificates.crt'
SNAP_PATH_PREFIX_FORMAT = '/var/snap/{}/common'
SNAP_CA_CERTS = SNAP_PATH_PREFIX_FORMAT + '/etc/ssl/certs/ca-certificates.crt'
class OpenStackCharm(BaseOpenStackCharm,

View File

@ -931,7 +931,7 @@ class BaseOpenStackCharmActions(object):
:returns: None
"""
if self.openstack_upgrade_available(self.release_pkg):
if self.config['action-managed-upgrade']:
if self.config.get('action-managed-upgrade', False):
hookenv.log('Not performing OpenStack upgrade as '
'action-managed-upgrade is enabled')
else:
@ -1007,7 +1007,9 @@ class BaseOpenStackCharmActions(object):
:returns: None
"""
if hookenv.is_leader():
if not self.sync_cmd:
return
elif hookenv.is_leader():
subprocess.check_call(self.sync_cmd)
else:
hookenv.log("Deferring DB sync to leader", level=hookenv.INFO)

View File

@ -0,0 +1,28 @@
# 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.
# Pull in helpers that 'charms_openstack.plugins' will export
from charms_openstack.plugins.adapters import (
CephRelationAdapter,
)
from charms_openstack.plugins.classes import (
BaseOpenStackCephCharm,
CephCharm,
)
__all__ = (
"BaseOpenStackCephCharm",
"CephCharm",
"CephRelationAdapter",
)

View File

@ -0,0 +1,53 @@
# 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 charms_openstack.adapters
class CephRelationAdapter(charms_openstack.adapters.OpenStackRelationAdapter):
"""
Adapter class for Ceph interfaces.
"""
# NOTE(fnordahl): the ``interface_type`` variable holds informational value
# only. This relation adapter can be used with any interface that
# provides the properties or functions referenced in this class.
interface_type = "ceph-mon"
@property
def monitors(self):
"""
Provide comma separated list of hosts that should be used
to access Ceph.
The mon_hosts function in Ceph interfaces tend to return a list or
generator object.
We need a comma separated string for use in our configuration
templates.
The sorting is important to avoid service restarts just because
of entries changing order in the returned data.
NOTE(fnordahl): Adapted from jamesapage's adapter in ``charm-gnocchi``
:returns: comma separated string with Ceph monitor hosts
:rtype: str
"""
hosts = sorted(self.relation.mon_hosts())
if len(hosts) > 0:
return ','.join(hosts)
else:
return ''

View File

@ -0,0 +1,251 @@
# 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 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
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 configure_ceph_keyring(self, interface,
cluster_name=None):
"""Creates or updates a Ceph keyring file.
:param interface: Interface with ``key`` property.
:type interface: Any class that has a property named ``key``.
: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_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)
subprocess.check_call([
'ceph-authtool', keyring_absolute_path,
'--create-keyring', '--name={}'.format(self.ceph_key_name),
'--add-key', interface.key, '--mode', '0600'])
shutil.chown(keyring_absolute_path, user=self.user, group=self.group)
return keyring_absolute_path
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, interface, cluster_name=None):
"""Override parent function to add symlink in ``/etc/ceph``."""
keyring_absolute_path = super().configure_ceph_keyring(
interface, 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()

View File

@ -0,0 +1,50 @@
# Copyright 2016 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.
# Note that the unit_tests/__init__.py has the following lines to stop
# side effects from the imorts from charm helpers.
# sys.path.append('./lib')
# mock out some charmhelpers libraries as they have apt install side effects
# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock()
# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock()
import unittest
import mock
import charms_openstack.adapters as c_adapters
import charms_openstack.plugins.adapters as pl_adapters
class FakeCephClientRelation():
relation_name = 'storage-ceph'
def mon_hosts(self):
return ['c', 'b', 'a']
class TestCephRelationAdapter(unittest.TestCase):
def test_class(self):
test_config = {}
with mock.patch.object(c_adapters.hookenv, 'related_units',
return_value=[]), \
mock.patch.object(c_adapters.hookenv,
'config',
new=lambda: test_config):
interface_ceph = FakeCephClientRelation()
adapter_ceph = pl_adapters.CephRelationAdapter(
relation=interface_ceph)
self.assertEqual(adapter_ceph.monitors, 'a,b,c')

View File

@ -0,0 +1,152 @@
import mock
import os
from unit_tests.charms_openstack.charm.utils import BaseOpenStackCharmTest
import charms_openstack.charm.classes as chm
import charms_openstack.plugins.classes as cpl
TEST_CONFIG = {'config': True,
'openstack-origin': None}
class FakeOpenStackCephConsumingCharm(
chm.OpenStackCharm,
cpl.BaseOpenStackCephCharm):
abstract_class = True
class TestOpenStackCephConsumingCharm(BaseOpenStackCharmTest):
def setUp(self):
super(TestOpenStackCephConsumingCharm, self).setUp(
FakeOpenStackCephConsumingCharm, TEST_CONFIG)
def test_application_name(self):
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='svc1')
self.assertEqual(self.target.application_name, 'svc1')
def test_ceph_service_name(self):
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='charmname')
self.assertEqual(
self.target.ceph_service_name,
'charmname')
self.target.ceph_service_name_override = 'override'
self.assertEqual(
self.target.ceph_service_name,
'override')
def test_ceph_key_name(self):
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='charmname')
self.assertEqual(
self.target.ceph_key_name,
'client.charmname')
self.patch_object(cpl.socket, 'gethostname', return_value='hostname')
self.target.ceph_key_per_unit_name = True
self.assertEqual(
self.target.ceph_key_name,
'client.charmname.hostname')
def test_ceph_keyring_path(self):
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='charmname')
self.assertEqual(
self.target.ceph_keyring_path,
'/etc/ceph')
self.target.snaps = ['gnocchi']
self.assertEqual(
self.target.ceph_keyring_path,
os.path.join(cpl.SNAP_PATH_PREFIX_FORMAT.format('gnocchi'),
'/etc/ceph'))
def test_configure_ceph_keyring(self):
self.patch_object(cpl.os.path, 'isdir', return_value=False)
self.patch_object(cpl.ch_core.host, 'mkdir')
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='sarepta')
self.patch_object(cpl.subprocess, 'check_call')
self.patch_object(cpl.shutil, 'chown')
interface = mock.MagicMock()
interface.key = 'KEY'
self.assertEqual(self.target.configure_ceph_keyring(interface),
'/etc/ceph/ceph.client.sarepta.keyring')
self.isdir.assert_called_with('/etc/ceph')
self.mkdir.assert_called_with('/etc/ceph',
owner='root', group='root', perms=0o750)
self.check_call.assert_called_with([
'ceph-authtool',
'/etc/ceph/ceph.client.sarepta.keyring',
'--create-keyring', '--name=client.sarepta', '--add-key', 'KEY',
'--mode', '0600',
])
self.target.user = 'ceph'
self.target.group = 'ceph'
self.target.configure_ceph_keyring(interface)
self.chown.assert_called_with(
'/etc/ceph/ceph.client.sarepta.keyring',
user='ceph', group='ceph')
class TestCephCharm(BaseOpenStackCharmTest):
def setUp(self):
super(TestCephCharm, self).setUp(cpl.CephCharm, {'source': None})
def test_ceph_keyring_path(self):
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='charmname')
self.assertEqual(
self.target.ceph_keyring_path,
'/var/lib/ceph/charmname')
self.target.snaps = ['gnocchi']
self.assertEqual(
self.target.ceph_keyring_path,
os.path.join(cpl.SNAP_PATH_PREFIX_FORMAT.format('gnocchi'),
'/var/lib/ceph/charmname'))
def test_configure_ceph_keyring(self):
self.patch_object(cpl.os.path, 'isdir', return_value=False)
self.patch_object(cpl.ch_core.host, 'mkdir')
self.patch_object(cpl.ch_core.hookenv, 'application_name',
return_value='sarepta')
self.patch_object(cpl.subprocess, 'check_call')
self.patch_object(cpl.shutil, 'chown')
self.patch_object(cpl.os, 'symlink')
interface = mock.MagicMock()
interface.key = 'KEY'
self.patch_object(cpl.os.path, 'exists', return_value=True)
self.patch_object(cpl.os, 'readlink')
self.patch_object(cpl.os, 'remove')
self.readlink.side_effect = OSError
self.target.configure_ceph_keyring(interface)
self.isdir.assert_called_with('/var/lib/ceph/sarepta')
self.mkdir.assert_called_with('/var/lib/ceph/sarepta',
owner='root', group='root', perms=0o750)
self.check_call.assert_called_with([
'ceph-authtool',
'/var/lib/ceph/sarepta/ceph.client.sarepta.keyring',
'--create-keyring', '--name=client.sarepta', '--add-key', 'KEY',
'--mode', '0600',
])
self.exists.assert_called_with(
'/etc/ceph/ceph.client.sarepta.keyring')
self.readlink.assert_called_with(
'/etc/ceph/ceph.client.sarepta.keyring')
assert not self.remove.called
self.symlink.assert_called_with(
'/var/lib/ceph/sarepta/ceph.client.sarepta.keyring',
'/etc/ceph/ceph.client.sarepta.keyring')
self.readlink.side_effect = None
self.readlink.return_value = '/some/where/else'
self.target.configure_ceph_keyring(interface)
self.remove.assert_called_with('/etc/ceph/ceph.client.sarepta.keyring')
def test_install(self):
self.patch_object(cpl.subprocess, 'check_output', return_value=b'\n')
self.patch_target('configure_source')
self.target.install()
self.target.configure_source.assert_called()
self.check_output.assert_called()

View File

@ -24,6 +24,8 @@ import copy
import unittest
import mock
import charms.reactive as reactive
import charms_openstack.adapters as adapters
@ -54,9 +56,10 @@ class MyRelation(object):
auto_accessors = ['this', 'that']
relation_name = 'my-name'
value = 'this'
def this(self):
return 'this'
return self.value
def that(self):
return 'that'
@ -65,16 +68,31 @@ class MyRelation(object):
return 'thing'
class MyEndpointRelation(reactive.Endpoint):
value = 'has value in config rendering'
def a_function(self):
return 'value is not for config rendering'
@property
def a_property(self):
return self.value
class TestOpenStackRelationAdapter(unittest.TestCase):
def test_class(self):
ad = adapters.OpenStackRelationAdapter(MyRelation(), ['some'])
r = MyRelation()
ad = adapters.OpenStackRelationAdapter(r, ['some'])
self.assertEqual(ad.this, 'this')
self.assertEqual(ad.that, 'that')
self.assertEqual(ad.some, 'thing')
self.assertEqual(ad.relation_name, 'my-name')
with self.assertRaises(AttributeError):
ad.relation_name = 'hello'
r.value = 'changed'
self.assertEqual(ad.this, 'changed')
def test_class_no_relation(self):
ad = adapters.OpenStackRelationAdapter(relation_name='cluster')
@ -101,6 +119,15 @@ class TestOpenStackRelationAdapter(unittest.TestCase):
self.assertIsInstance(i, FakeRelation)
self.assertEqual(i.b, 4)
def test_class_with_endpoint_relation(self):
er = MyEndpointRelation('my-name')
ad = adapters.OpenStackRelationAdapter(er)
self.assertEqual(ad.a_property, 'has value in config rendering')
er.value = 'can change after instantiation'
self.assertEqual(ad.a_property, 'can change after instantiation')
with self.assertRaises(AttributeError):
self.assertFalse(ad.a_function)
class FakeMemcacheRelation():