diff --git a/config.yaml b/config.yaml index d8780fe..533dac6 100644 --- a/config.yaml +++ b/config.yaml @@ -59,3 +59,18 @@ options: . Valid options are "cephx" and "none". If "none" is specified, keys will still be created and deployed so that it can be enabled later. + user-keys: + type: string + default: "" + description: | + A space-separated list of : pairs used to + lookup authentication keys for a specific user instead of trying to + create a user and a key via ceph-mon. + admin-user: + type: string + default: "client.admin" + description: | + A configurable admin user name. Used for scenarios where pools are + pre-created and the user given to charm-ceph-proxy simply needs to + check the existence of a given pool and error out if one does not + exist. Can be used in conjunction with user-keys. diff --git a/hooks/ceph.py b/hooks/ceph.py index f352f53..de1e48e 100644 --- a/hooks/ceph.py +++ b/hooks/ceph.py @@ -32,6 +32,7 @@ from charmhelpers.core.hookenv import ( cached, status_set, WARNING, + config, ) from charmhelpers.fetch import ( apt_cache @@ -371,14 +372,30 @@ def get_upgrade_key(): return get_named_key('upgrade-osd', _upgrade_caps) +def _config_user_key(name): + user_keys_list = config('user-keys') + if user_keys_list: + for ukpair in user_keys_list.split(' '): + uk = ukpair.split(':') + if len(uk) == 2: + user_type, k = uk + t, u = user_type.split('.') + if u == name: + return k + + def get_named_key(name, caps=None): + config_user_key = _config_user_key(name) + if config_user_key: + return config_user_key + caps = caps or _default_caps cmd = [ "sudo", "-u", ceph_user(), 'ceph', - '--name', 'client.admin', + '--name', config('admin-user'), '--keyring', '/var/lib/ceph/mon/ceph-{}/keyring'.format( get_unit_hostname() diff --git a/hooks/ceph_hooks.py b/hooks/ceph_hooks.py index 960eeba..21a9d98 100755 --- a/hooks/ceph_hooks.py +++ b/hooks/ceph_hooks.py @@ -86,11 +86,16 @@ def emit_cephconf(): render('ceph.conf', charm_ceph_conf, cephcontext, perms=0o644) install_alternative('ceph.conf', '/etc/ceph/ceph.conf', charm_ceph_conf, 100) - keyring = 'ceph.client.admin.keyring' + + keyring_template = 'ceph.keyring' + keyring = 'ceph.{}.keyring'.format(config('admin-user')) keyring_path = '/etc/ceph/' + keyring - ctx = {'admin_key': config('admin-key')} + ctx = { + 'admin_key': config('admin-key'), + 'admin_user': config('admin-user'), + } user = ceph.ceph_user() - render(keyring, keyring_path, ctx, owner=user, perms=0o600) + render(keyring_template, keyring_path, ctx, owner=user, perms=0o600) keyring = 'keyring' keyring_path = ( diff --git a/templates/ceph.client.admin.keyring b/templates/ceph.keyring similarity index 53% rename from templates/ceph.client.admin.keyring rename to templates/ceph.keyring index ce0a4da..30832f9 100644 --- a/templates/ceph.client.admin.keyring +++ b/templates/ceph.keyring @@ -1,3 +1,3 @@ -[client.admin] +[{{ admin_user }}] key = {{admin_key}} diff --git a/templates/mon.keyring b/templates/mon.keyring index 567c2ea..b8aa5bc 100644 --- a/templates/mon.keyring +++ b/templates/mon.keyring @@ -1,3 +1,3 @@ -[client.admin] +[{{ admin_user }}] key = {{admin_key}} diff --git a/unit_tests/test_ceph.py b/unit_tests/test_ceph.py new file mode 100644 index 0000000..9ed36c0 --- /dev/null +++ b/unit_tests/test_ceph.py @@ -0,0 +1,95 @@ +import unittest + +import mock + +import ceph + + +class CephTestCase(unittest.TestCase): + def setUp(self): + super(CephTestCase, self).setUp() + + @staticmethod + def populated_config_side_effect(key): + return { + 'user-keys': + 'client.cinder-ceph:AQAij2tbMNjMOhAAqInpXQLFrltDgmYid6KXbg== ' + 'client.glance:AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g== ' + 'client.gnocchi:AQDk7qJb0csAFRAAQqPU6HchVW3PT6ymgXdI/A== ' + 'client.nova-compute-kvm:' + 'AQBkjmtb1hWxLxAA3UhxSblgFSCtHVoZ8W6rNQ== ' + 'client.radosgw.gateway:' + 'AQBljmtb65mrHhAAGy9VRkfsatWVLb9EpoWDfw==', + 'admin-user': 'client.myadmin' + }[key] + + @staticmethod + def empty_config_side_effect(key): + return { + 'user-keys': '', + 'admin-user': 'client.myadmin' + }[key] + + @mock.patch('ceph.config') + def test_config_user_key_populated(self, mock_config): + user_name = 'glance' + user_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g==' + + mock_config.side_effect = self.populated_config_side_effect + named_key = ceph._config_user_key(user_name) + self.assertEqual(user_key, named_key) + + @mock.patch('ceph.config') + def test_config_empty_user_key(self, mock_config): + user_name = 'cinder-ceph' + + mock_config.side_effect = self.empty_config_side_effect + named_key = ceph._config_user_key(user_name) + self.assertEqual(named_key, None) + + @mock.patch('ceph.config') + def test_get_named_key_populated(self, mock_config): + user_name = 'glance' + user_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g==' + + mock_config.side_effect = self.populated_config_side_effect + named_key = ceph.get_named_key(user_name) + + self.assertEqual(user_key, named_key) + + @mock.patch('subprocess.check_output') + @mock.patch('ceph.get_unit_hostname') + @mock.patch('ceph.ceph_user') + @mock.patch('ceph.config') + def test_get_named_key_empty(self, mock_config, mock_ceph_user, + mock_get_unit_hostname, mock_check_output): + user_name = 'cinder-ceph' + user_type = 'client' + admin_user = 'client.myadmin' + user_spec = '{}.{}'.format(user_type, user_name) + expected_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g==' + expected_output = ('[client.testuser]\n key = {}' + .format(expected_key)) + caps = { + 'mon': ['allow rw'], + 'osd': ['allow rwx'] + } + ceph_user = 'ceph' + ceph_proxy_host = 'cephproxy' + mock_get_unit_hostname.return_value = ceph_proxy_host + + def check_output_side_effect(cmd): + return { + ' '.join(['sudo', '-u', ceph_user, 'ceph', '--name', + admin_user, + '--keyring', + '/var/lib/ceph/mon/ceph-{}/keyring'.format( + ceph_proxy_host), + 'auth', 'get-or-create', user_spec, 'mon', + 'allow rw', 'osd', 'allow rwx']): expected_output + }[' '.join(cmd)] + mock_check_output.side_effect = check_output_side_effect + mock_config.side_effect = self.empty_config_side_effect + mock_ceph_user.return_value = ceph_user + named_key = ceph.get_named_key(user_name, caps) + self.assertEqual(named_key, expected_key) diff --git a/unit_tests/test_ceph_hooks.py b/unit_tests/test_ceph_hooks.py index 802fce9..4f65596 100644 --- a/unit_tests/test_ceph_hooks.py +++ b/unit_tests/test_ceph_hooks.py @@ -76,6 +76,7 @@ class TestHooks(test_utils.CharmTestCase): self.test_config.set('monitor-hosts', '127.0.0.1:1234') self.test_config.set('fsid', 'abc123') self.test_config.set('admin-key', 'key123') + self.test_config.set('admin-user', 'client.myadmin') def c(k): x = {'radosgw': ['rados:1'], @@ -105,10 +106,15 @@ class TestHooks(test_utils.CharmTestCase): '/etc/ceph/ceph.conf', '%s/ceph.conf' % dirname, 100) - keyring = 'ceph.client.admin.keyring' - context = {'admin_key': self.test_config.get('admin-key')} - self.render.assert_any_call(keyring, - '/etc/ceph/' + keyring, + keyring_template = 'ceph.keyring' + keyring_name = 'ceph.{}.keyring'.format( + self.test_config.get('admin-user')) + context = { + 'admin_key': self.test_config.get('admin-key'), + 'admin_user': self.test_config.get('admin-user'), + } + self.render.assert_any_call(keyring_template, + '/etc/ceph/' + keyring_name, context, owner='ceph-user', perms=0o600) mock_rgw_rel.assert_called_with(relid='rados:1', unit='rados/1')