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:
parent
341b879cc1
commit
552fa0e1ec
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
)
|
|
@ -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 ''
|
|
@ -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()
|
|
@ -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')
|
|
@ -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()
|
|
@ -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():
|
||||
|
||||
|
|
Loading…
Reference in New Issue