diff --git a/config.yaml b/config.yaml index 7881fdf1..fa40f7e1 100644 --- a/config.yaml +++ b/config.yaml @@ -212,14 +212,24 @@ options: type: int default: description: | - Restrict the rbd features used to the specified level. If set, this will - inform clients that they should set the config value `rbd default - features`, for example: - . - rbd default features = 1 - . - This needs to be set to 1 when deploying a cloud with the nova-lxd - hypervisor. + Default RBD Features to use when creating new images. The value of this + configuration option will be shared with consumers of the ``ceph-client`` + interface and client charms may choose to add this to the Ceph + configuration file on the units they manage. + + Example: + + rbd default features = 1 + + NOTE: If you have clients using the kernel RBD driver you must set this + configuration option to a value corrensponding to the features the driver + in your kernel supports. The kernel RBD driver tends to be multiple + cycles behind the userspace driver available for libvirt/qemu. Nova LXD + is among the clients depending on the kernel RBD driver. + + NOTE: If you want to use the RBD Mirroring feature you must either let + this configuration option be the default or make sure the value you set + includes the ``exclusive-lock`` and ``journaling`` features. no-bootstrap: type: boolean default: False diff --git a/hooks/ceph_hooks.py b/hooks/ceph_hooks.py index 43647703..355e01b3 100755 --- a/hooks/ceph_hooks.py +++ b/hooks/ceph_hooks.py @@ -81,10 +81,11 @@ from charmhelpers.core.templating import render from charmhelpers.contrib.storage.linux.ceph import ( CephConfContext) from utils import ( + assert_charm_supports_ipv6, + get_cluster_addr, get_networks, get_public_addr, - get_cluster_addr, - assert_charm_supports_ipv6 + get_rbd_features, ) from charmhelpers.contrib.charmsupport import nrpe @@ -184,8 +185,9 @@ def get_ceph_context(): cephcontext['public_addr'] = get_public_addr() cephcontext['cluster_addr'] = get_cluster_addr() - if config('default-rbd-features'): - cephcontext['rbd_features'] = config('default-rbd-features') + rbd_features = get_rbd_features() + if rbd_features: + cephcontext['rbd_features'] = rbd_features if config('disable-pg-max-object-skew'): cephcontext['disable_object_skew'] = config( diff --git a/hooks/utils.py b/hooks/utils.py index f4bc81e2..46063d3d 100644 --- a/hooks/utils.py +++ b/hooks/utils.py @@ -12,27 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -import socket import re +import socket +import subprocess + from charmhelpers.core.hookenv import ( - unit_get, + DEBUG, cached, config, - status_set, - network_get_primary_address, + goal_state, log, - DEBUG, + network_get_primary_address, + related_units, + relation_ids, + status_set, + unit_get, ) from charmhelpers.fetch import ( apt_install, filter_installed_packages ) - from charmhelpers.core.host import ( lsb_release, CompareHostReleases, ) - from charmhelpers.contrib.network.ip import ( get_address_in_network, get_ipv6_addr @@ -152,3 +155,56 @@ def assert_charm_supports_ipv6(): if CompareHostReleases(_release) < "trusty": raise Exception("IPv6 is not supported in the charms for Ubuntu " "versions less than Trusty 14.04") + + +def has_rbd_mirrors(): + """Determine if we have or will have ``rbd-mirror`` charms related. + + :returns: True or False + :rtype: bool + """ + try: + # NOTE(fnordahl): This optimization will not be useful until we get a + # resolution on LP: #1818245 + raise NotImplementedError + gs = goal_state() + return 'rbd-mirror' in gs.get('relations', {}) + except NotImplementedError: + for relid in relation_ids('rbd-mirror'): + if related_units(relid): + return True + + +def get_default_rbd_features(): + """Get default value for ``rbd_default_features``. + + This is retrieved by asking the installed Ceph binary to show its runtime + config when using a empty configuration file. + + :returns: Installed Ceph's Default vaule for ``rbd_default_features`` + :rtype: int + :raises: subprocess.CalledProcessError + """ + output = subprocess.check_output( + ['ceph', '-c', '/dev/null', '--show-config'], + universal_newlines=True) + for line in output.splitlines(): + if 'rbd_default_features' in line: + return int(line.split('=')[1].lstrip().rstrip()) + + +def get_rbd_features(): + """Determine if we should set, and what the rbd default features should be. + + :returns: None or the apropriate value to use + :rtype: Option[int, None] + """ + RBD_FEATURE_EXCLUSIVE_LOCK = 4 + RBD_FEATURE_JOURNALING = 64 + + rbd_feature_config = config('default-rbd-features') + if rbd_feature_config: + return int(rbd_feature_config) + elif has_rbd_mirrors(): + return (get_default_rbd_features() | + RBD_FEATURE_EXCLUSIVE_LOCK | RBD_FEATURE_JOURNALING) diff --git a/unit_tests/test_ceph_hooks.py b/unit_tests/test_ceph_hooks.py index 6173e2cd..e7515307 100644 --- a/unit_tests/test_ceph_hooks.py +++ b/unit_tests/test_ceph_hooks.py @@ -57,6 +57,7 @@ class CephHooksTestCase(unittest.TestCase): def setUp(self): super(CephHooksTestCase, self).setUp() + @patch.object(ceph_hooks, 'get_rbd_features', return_value=None) @patch.object(ceph_hooks, 'get_public_addr', lambda *args: "10.0.0.1") @patch.object(ceph_hooks, 'get_cluster_addr', lambda *args: "10.1.0.1") @patch.object(ceph_hooks, 'cmp_pkgrevno', lambda *args: 1) @@ -66,7 +67,8 @@ class CephHooksTestCase(unittest.TestCase): @patch.object(ceph_hooks, 'leader_get', lambda *args: '1234') @patch.object(ceph, 'config') @patch.object(ceph_hooks, 'config') - def test_get_ceph_context(self, mock_config, mock_config2): + def test_get_ceph_context(self, mock_config, mock_config2, + _get_rbd_features): config = copy.deepcopy(CHARM_CONFIG) mock_config.side_effect = lambda key: config[key] mock_config2.side_effect = lambda key: config[key] @@ -84,6 +86,7 @@ class CephHooksTestCase(unittest.TestCase): 'use_syslog': 'true'} self.assertEqual(ctxt, expected) + @patch.object(ceph_hooks, 'get_rbd_features', return_value=1) @patch.object(ceph_hooks, 'get_public_addr', lambda *args: "10.0.0.1") @patch.object(ceph_hooks, 'get_cluster_addr', lambda *args: "10.1.0.1") @patch.object(ceph_hooks, 'cmp_pkgrevno', @@ -94,9 +97,9 @@ class CephHooksTestCase(unittest.TestCase): @patch.object(ceph_hooks, 'leader_get', lambda *args: '1234') @patch.object(ceph, 'config') @patch.object(ceph_hooks, 'config') - def test_get_ceph_context_rbd_features(self, mock_config, mock_config2): + def test_get_ceph_context_rbd_features(self, mock_config, mock_config2, + _get_rbd_features): config = copy.deepcopy(CHARM_CONFIG) - config['default-rbd-features'] = 1 mock_config.side_effect = lambda key: config[key] mock_config2.side_effect = lambda key: config[key] ctxt = ceph_hooks.get_ceph_context() @@ -114,6 +117,7 @@ class CephHooksTestCase(unittest.TestCase): 'rbd_features': 1} self.assertEqual(ctxt, expected) + @patch.object(ceph_hooks, 'get_rbd_features', return_value=None) @patch.object(ceph_hooks, 'get_public_addr', lambda *args: "10.0.0.1") @patch.object(ceph_hooks, 'get_cluster_addr', lambda *args: "10.1.0.1") @patch.object(ceph_hooks, 'cmp_pkgrevno', lambda *args: 1) @@ -123,7 +127,8 @@ class CephHooksTestCase(unittest.TestCase): @patch.object(ceph_hooks, 'leader_get', lambda *args: '1234') @patch.object(ceph, 'config') @patch.object(ceph_hooks, 'config') - def test_get_ceph_context_w_config_flags(self, mock_config, mock_config2): + def test_get_ceph_context_w_config_flags(self, mock_config, mock_config2, + _get_rbd_features): config = copy.deepcopy(CHARM_CONFIG) config['config-flags'] = '{"mon": {"mon sync max retries": 10}}' mock_config.side_effect = lambda key: config[key] @@ -143,6 +148,7 @@ class CephHooksTestCase(unittest.TestCase): 'use_syslog': 'true'} self.assertEqual(ctxt, expected) + @patch.object(ceph_hooks, 'get_rbd_features', return_value=None) @patch.object(ceph_hooks, 'get_public_addr', lambda *args: "10.0.0.1") @patch.object(ceph_hooks, 'get_cluster_addr', lambda *args: "10.1.0.1") @patch.object(ceph_hooks, 'cmp_pkgrevno', lambda *args: 1) @@ -153,7 +159,8 @@ class CephHooksTestCase(unittest.TestCase): @patch.object(ceph, 'config') @patch.object(ceph_hooks, 'config') def test_get_ceph_context_w_config_flags_invalid(self, mock_config, - mock_config2): + mock_config2, + _get_rbd_features): config = copy.deepcopy(CHARM_CONFIG) config['config-flags'] = ('{"mon": {"mon sync max retries": 10},' '"foo": "bar"}') diff --git a/unit_tests/test_ceph_utils.py b/unit_tests/test_ceph_utils.py new file mode 100644 index 00000000..23013afd --- /dev/null +++ b/unit_tests/test_ceph_utils.py @@ -0,0 +1,65 @@ +# 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 mock + +import test_utils + +from hooks import utils + + +class CephUtilsTestCase(test_utils.CharmTestCase): + + def setUp(self): + super().setUp() + + @mock.patch.object(utils, 'related_units') + @mock.patch.object(utils, 'relation_ids') + def test_has_rbd_mirrors(self, _relation_ids, _related_units): + # NOTE(fnordahl): This optimization will not be useful until we get a + # resolution on LP: #1818245 + # _goal_state.return_value = {'relations': {'rbd-mirror': None}} + # self.assertTrue(utils.has_rbd_mirrors()) + # _goal_state.assert_called_once_with() + # _goal_state.side_effect = NotImplementedError + _relation_ids.return_value = ['arelid'] + _related_units.return_value = ['aunit/0'] + self.assertTrue(utils.has_rbd_mirrors()) + _relation_ids.assert_called_once_with('rbd-mirror') + _related_units.assert_called_once_with('arelid') + + @mock.patch.object(utils.subprocess, 'check_output') + def test_get_default_rbd_features(self, _check_output): + _check_output.return_value = ('a = b\nrbd_default_features = 61\n' + 'c = d\n') + self.assertEquals( + utils.get_default_rbd_features(), + 61) + _check_output.assert_called_once_with( + ['ceph', '-c', '/dev/null', '--show-config'], + universal_newlines=True) + + @mock.patch.object(utils, 'get_default_rbd_features') + @mock.patch.object(utils, 'has_rbd_mirrors') + @mock.patch.object(utils, 'config') + def test_get_rbd_features(self, _config, _has_rbd_mirrors, + _get_default_rbd_features): + _config.side_effect = \ + lambda key: {'default-rbd-features': 42}.get(key, None) + self.assertEquals(utils.get_rbd_features(), 42) + _has_rbd_mirrors.return_value = True + _get_default_rbd_features.return_value = 61 + _config.side_effect = lambda key: {}.get(key, None) + self.assertEquals(utils.get_rbd_features(), 125) + _has_rbd_mirrors.return_value = False + self.assertEquals(utils.get_rbd_features(), None)