From 552fa0e1ec2144bb38d684aaf9e2f4025e5add46 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Tue, 5 Feb 2019 14:01:17 +0100 Subject: [PATCH] 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 --- charms_openstack/adapters.py | 62 +++-- charms_openstack/charm/classes.py | 3 +- charms_openstack/charm/core.py | 6 +- charms_openstack/plugins/__init__.py | 28 ++ charms_openstack/plugins/adapters.py | 53 ++++ charms_openstack/plugins/classes.py | 251 ++++++++++++++++++ .../charms_openstack/plugins/__init__.py | 0 .../charms_openstack/plugins/test_adapters.py | 50 ++++ .../charms_openstack/plugins/test_classes.py | 152 +++++++++++ unit_tests/test_charms_openstack_adapters.py | 31 ++- 10 files changed, 613 insertions(+), 23 deletions(-) create mode 100644 charms_openstack/plugins/__init__.py create mode 100644 charms_openstack/plugins/adapters.py create mode 100644 charms_openstack/plugins/classes.py create mode 100644 unit_tests/charms_openstack/plugins/__init__.py create mode 100644 unit_tests/charms_openstack/plugins/test_adapters.py create mode 100644 unit_tests/charms_openstack/plugins/test_classes.py diff --git a/charms_openstack/adapters.py b/charms_openstack/adapters.py index b0061e4..3e8508b 100644 --- a/charms_openstack/adapters.py +++ b/charms_openstack/adapters.py @@ -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): diff --git a/charms_openstack/charm/classes.py b/charms_openstack/charm/classes.py index dbf244f..2cca72e 100644 --- a/charms_openstack/charm/classes.py +++ b/charms_openstack/charm/classes.py @@ -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, diff --git a/charms_openstack/charm/core.py b/charms_openstack/charm/core.py index 1edbf54..d1b01f8 100644 --- a/charms_openstack/charm/core.py +++ b/charms_openstack/charm/core.py @@ -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) diff --git a/charms_openstack/plugins/__init__.py b/charms_openstack/plugins/__init__.py new file mode 100644 index 0000000..bcb253c --- /dev/null +++ b/charms_openstack/plugins/__init__.py @@ -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", +) diff --git a/charms_openstack/plugins/adapters.py b/charms_openstack/plugins/adapters.py new file mode 100644 index 0000000..7b7acb0 --- /dev/null +++ b/charms_openstack/plugins/adapters.py @@ -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 '' diff --git a/charms_openstack/plugins/classes.py b/charms_openstack/plugins/classes.py new file mode 100644 index 0000000..329b3ba --- /dev/null +++ b/charms_openstack/plugins/classes.py @@ -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() diff --git a/unit_tests/charms_openstack/plugins/__init__.py b/unit_tests/charms_openstack/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unit_tests/charms_openstack/plugins/test_adapters.py b/unit_tests/charms_openstack/plugins/test_adapters.py new file mode 100644 index 0000000..38c0ddc --- /dev/null +++ b/unit_tests/charms_openstack/plugins/test_adapters.py @@ -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') diff --git a/unit_tests/charms_openstack/plugins/test_classes.py b/unit_tests/charms_openstack/plugins/test_classes.py new file mode 100644 index 0000000..7d26be3 --- /dev/null +++ b/unit_tests/charms_openstack/plugins/test_classes.py @@ -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() diff --git a/unit_tests/test_charms_openstack_adapters.py b/unit_tests/test_charms_openstack_adapters.py index 96f1d9f..0f53c7d 100644 --- a/unit_tests/test_charms_openstack_adapters.py +++ b/unit_tests/test_charms_openstack_adapters.py @@ -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():