diff --git a/README.md b/README.md index ad00a34..b586e27 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ The ceph-proxy charm deploys a proxy that acts as a [ceph-mon][ceph-mon-charm] application for an external Ceph cluster. It joins a non-charmed Ceph cluster to a Juju model. +The charm works with traditional Ceph charm clients (e.g. cinder, glance, +nova-compute) but it also supports the [ceph-radosgw][ceph-radosgw-charm] and +[ceph-fs][ceph-fs-charm] charms. + # Usage ## Configuration @@ -66,7 +70,9 @@ For general charm questions refer to the [OpenStack Charm Guide][cg]. [ceph-upstream]: https://ceph.io [cg]: https://docs.openstack.org/charm-guide -[ceph-mon-charm]: https://jaas.ai/ceph-mon -[juju-docs-actions]: https://jaas.ai/docs/actions +[ceph-mon-charm]: https://charmhub.io/ceph-mon +[ceph-fs-charm]: https://charmbui.io/ceph-fs +[ceph-radosgw-charm]: https://charmbui.io/ceph-radosgw +[juju-docs-actions]: https://charmbui.io/docs/actions [juju-docs-config-apps]: https://juju.is/docs/configuring-applications [lp-bugs-charm-ceph-proxy]: https://bugs.launchpad.net/charm-ceph-proxy/+filebug diff --git a/hooks/ceph.py b/hooks/ceph.py index d6213a7..7e57155 100644 --- a/hooks/ceph.py +++ b/hooks/ceph.py @@ -351,6 +351,12 @@ def get_radosgw_key(name='radosgw.gateway'): return get_named_key(name, _radosgw_caps) +def get_mds_key(name): + return get_named_entity_key(entity='mds', + name=name, + caps=mds_caps) + + _default_caps = collections.OrderedDict([ ('mon', ['allow r', 'allow command "osd blacklist"']), @@ -363,6 +369,12 @@ admin_caps = { 'osd': ['allow *'] } +mds_caps = collections.OrderedDict([ + ('osd', ['allow *']), + ('mds', ['allow']), + ('mon', ['allow rwx']), +]) + osd_upgrade_caps = { 'mon': ['allow command "config-key"', 'allow command "osd tree"', @@ -390,15 +402,17 @@ def _config_user_key(name): return k -def get_named_key(name, caps=None, pool_list=None): +def get_named_entity_key(name, caps=None, pool_list=None, + entity='client'): """Retrieve a specific named cephx key. - :param name: String Name of key to get. - :param pool_list: The list of pools to give access to + :param name: String Name of key to get. EXACT MATCH :param caps: dict of cephx capabilities + :param pool_list: The list of pools to give access to + :param entity: String Name of type to get. :returns: Returns a cephx key """ - key_name = 'client.{}'.format(name) + key_name = '{}.{}'.format(entity, name) try: # Does the key already exist? output = str(subprocess.check_output( @@ -424,7 +438,8 @@ def get_named_key(name, caps=None, pool_list=None): return parse_key(output) except subprocess.CalledProcessError: # Couldn't get the key, time to create it! - log("Creating new key for {}".format(name), level=DEBUG) + log("Creating new key for {}".format(key_name), level=DEBUG) + caps = caps or _default_caps cmd = [ "sudo", @@ -455,6 +470,17 @@ def get_named_key(name, caps=None, pool_list=None): .strip()) # IGNORE:E1103 +def get_named_key(name, caps=None, pool_list=None): + """Retrieve a specific named cephx key. + + :param name: String Name of key to get. + :param caps: dict of cephx capabilities + :param pool_list: The list of pools to give access to + :returns: Returns a cephx key + """ + return get_named_entity_key(name, caps, pool_list, entity='client') + + def upgrade_key_caps(key, caps, pool_list=None): """ Upgrade key to have capabilities caps """ if not is_leader(): diff --git a/hooks/ceph_hooks.py b/hooks/ceph_hooks.py index 0c72f29..682c7b3 100755 --- a/hooks/ceph_hooks.py +++ b/hooks/ceph_hooks.py @@ -31,6 +31,7 @@ import ceph from charmhelpers.core.hookenv import ( log, DEBUG, + INFO, config, is_leader, relation_ids, @@ -137,6 +138,7 @@ def emit_cephconf(): notify_radosgws() notify_client() + notify_cephfs_mds() @hooks.hook('config-changed') @@ -160,6 +162,12 @@ def notify_client(): client_relation_joined(relid=relid, unit=unit) +def notify_cephfs_mds(): + for relid in relation_ids('mds'): + for unit in related_units(relid): + mds_relation_joined(relid=relid, unit=unit) + + @hooks.hook('radosgw-relation-changed') @hooks.hook('radosgw-relation-joined') def radosgw_relation(relid=None, unit=None): @@ -203,6 +211,41 @@ def radosgw_relation(relid=None, unit=None): log('FSID or admin key not provided, please configure them') +@hooks.hook('mds-relation-joined') +@hooks.hook('mds-relation-changed') +def mds_relation_joined(relid=None, unit=None): + if not ready(): + log('MDS: FSID or admin key not provided, please configure them', + level=INFO) + return + + log('ceph-proxy config ok - providing mds client with keys') + if not unit: + unit = remote_unit() + + mds_name = relation_get(attribute='mds-name', + rid=relid, unit=unit) + ceph_addrs = config('monitor-hosts') + data = { + 'fsid': config('fsid'), + 'auth': config('auth-supported'), + 'ceph-public-address': ceph_addrs, + } + if mds_name: + data['{}_mds_key'.format(mds_name)] = ( + ceph.get_mds_key(name=mds_name) + ) + + settings = relation_get(rid=relid, unit=unit) or {} + if 'broker_req' in settings: + rsp = process_requests(settings['broker_req']) + unit_id = unit.replace('/', '-') + unit_response_key = 'broker-rsp-' + unit_id + data[unit_response_key] = rsp + log('MDS: relation_set (%s): %s' % (relid, str(data)), level=DEBUG) + relation_set(relation_id=relid, relation_settings=data) + + @hooks.hook('client-relation-joined') def client_relation_joined(relid=None, unit=None): if ready(): diff --git a/hooks/mds-relation-changed b/hooks/mds-relation-changed new file mode 120000 index 0000000..52d9663 --- /dev/null +++ b/hooks/mds-relation-changed @@ -0,0 +1 @@ +ceph_hooks.py \ No newline at end of file diff --git a/hooks/mds-relation-joined b/hooks/mds-relation-joined new file mode 120000 index 0000000..52d9663 --- /dev/null +++ b/hooks/mds-relation-joined @@ -0,0 +1 @@ +ceph_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 7854a2d..24f04fc 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -21,3 +21,5 @@ provides: interface: ceph-client radosgw: interface: ceph-radosgw + mds: + interface: ceph-mds diff --git a/tests/bundles/jammy-antelope.yaml b/tests/bundles/jammy-antelope.yaml index 76c08a8..1e4e54e 100644 --- a/tests/bundles/jammy-antelope.yaml +++ b/tests/bundles/jammy-antelope.yaml @@ -25,6 +25,9 @@ machines: '13': '14': '15': + '16': + '17': + '18': applications: @@ -146,6 +149,19 @@ applications: - '15' channel: latest/edge + ubuntu: # used to test mounts + charm: ch:ubuntu + num_units: 2 + to: + - '16' + - '17' + + ceph-fs: + charm: ch:ceph-fs + channel: latest/edge + num_units: 1 + to: + - '18' relations: @@ -193,3 +209,6 @@ relations: - - 'nova-compute:amqp' - 'rabbitmq-server:amqp' + + - - 'ceph-proxy:mds' + - 'ceph-fs:ceph-mds' diff --git a/tests/bundles/jammy-yoga.yaml b/tests/bundles/jammy-yoga.yaml index 87d4b5a..12ee621 100644 --- a/tests/bundles/jammy-yoga.yaml +++ b/tests/bundles/jammy-yoga.yaml @@ -25,6 +25,9 @@ machines: '13': '14': '15': + '16': + '17': + '18': applications: @@ -146,6 +149,19 @@ applications: - '15' channel: latest/edge + ubuntu: # used to test mounts + charm: ch:ubuntu + num_units: 2 + to: + - '16' + - '17' + + ceph-fs: + charm: ch:ceph-fs + channel: latest/edge + num_units: 1 + to: + - '18' relations: @@ -193,3 +209,6 @@ relations: - - 'nova-compute:amqp' - 'rabbitmq-server:amqp' + + - - 'ceph-proxy:mds' + - 'ceph-fs:ceph-mds' diff --git a/tests/bundles/jammy-zed.yaml b/tests/bundles/jammy-zed.yaml index 94e65f8..80969e4 100644 --- a/tests/bundles/jammy-zed.yaml +++ b/tests/bundles/jammy-zed.yaml @@ -25,6 +25,9 @@ machines: '13': '14': '15': + '16': + '17': + '18': applications: @@ -146,6 +149,19 @@ applications: - '15' channel: latest/edge + ubuntu: # used to test mounts + charm: ch:ubuntu + num_units: 2 + to: + - '16' + - '17' + + ceph-fs: + charm: ch:ceph-fs + channel: latest/edge + num_units: 1 + to: + - '18' relations: @@ -193,3 +209,6 @@ relations: - - 'nova-compute:amqp' - 'rabbitmq-server:amqp' + + - - 'ceph-proxy:mds' + - 'ceph-fs:ceph-mds' diff --git a/tests/bundles/kinetic-zed.yaml b/tests/bundles/kinetic-zed.yaml index 4d03114..376648a 100644 --- a/tests/bundles/kinetic-zed.yaml +++ b/tests/bundles/kinetic-zed.yaml @@ -25,6 +25,9 @@ machines: '13': '14': '15': + '16': + '17': + '18': applications: @@ -146,6 +149,19 @@ applications: - '15' channel: latest/edge + ubuntu: # used to test mounts + charm: ch:ubuntu + num_units: 2 + to: + - '16' + - '17' + + ceph-fs: + charm: ch:ceph-fs + channel: latest/edge + num_units: 1 + to: + - '18' relations: @@ -193,3 +209,6 @@ relations: - - 'nova-compute:amqp' - 'rabbitmq-server:amqp' + + - - 'ceph-proxy:mds' + - 'ceph-fs:ceph-mds' diff --git a/tests/bundles/lunar-antelope.yaml b/tests/bundles/lunar-antelope.yaml index a7b7de2..e6cdff9 100644 --- a/tests/bundles/lunar-antelope.yaml +++ b/tests/bundles/lunar-antelope.yaml @@ -25,6 +25,9 @@ machines: '13': '14': '15': + '16': + '17': + '18': applications: @@ -146,6 +149,19 @@ applications: - '15' channel: latest/edge + ubuntu: # used to test mounts + charm: ch:ubuntu + num_units: 2 + to: + - '16' + - '17' + + ceph-fs: + charm: ch:ceph-fs + channel: latest/edge + num_units: 1 + to: + - '18' relations: @@ -193,3 +209,6 @@ relations: - - 'nova-compute:amqp' - 'rabbitmq-server:amqp' + + - - 'ceph-proxy:mds' + - 'ceph-fs:ceph-mds' diff --git a/tests/tests.yaml b/tests/tests.yaml index ee84d3e..bfa452d 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -7,6 +7,7 @@ configure: tests: - zaza.openstack.charm_tests.ceph.tests.CephProxyTest + - zaza.openstack.charm_tests.ceph.fs.tests.CephFSWithCephProxyTests - erasure-coded: - zaza.openstack.charm_tests.ceph.tests.CephProxyTest - zaza.openstack.charm_tests.ceph.tests.CheckPoolTypes @@ -33,22 +34,28 @@ smoke_bundles: target_deploy_status: ceph-proxy: workload-status: blocked - workload-status-message: Ensure FSID and admin-key are set + workload-status-message-prefix: "Ensure FSID and admin-key are set" ceph-radosgw: workload-status: waiting - workload-status-message: "Incomplete relations: mon" + workload-status-message-prefix: "Incomplete relations: mon" keystone: workload-status: active - workload-status-message: "Unit is ready" - nova-compute: - workload-status: waiting - workload-status-message: "Incomplete relations: storage-backend" + workload-status-message-prefix: "Unit is ready" cinder-ceph: workload-status: waiting - workload-status-message: "Ceph broker request incomplete" + workload-status-message-prefix: "Ceph broker request incomplete" + ceph-fs: + workload-status: waiting + workload-status-message-prefix: "'ceph-mds' incomplete" + nova-compute: + workload-status: waiting + workload-status-message-prefix: "Incomplete relations: storage-backend" glance: workload-status: waiting - workload-status-message: "Incomplete relations: storage-backend" + workload-status-message-prefix: "Incomplete relations: storage-backend" + ubuntu: + workload-status: active + workload-status-message-prefix: '' tests_options: force_deploy: diff --git a/unit_tests/test_ceph_hooks.py b/unit_tests/test_ceph_hooks.py index 2af966a..8706b4c 100644 --- a/unit_tests/test_ceph_hooks.py +++ b/unit_tests/test_ceph_hooks.py @@ -74,10 +74,11 @@ class TestHooks(test_utils.CharmTestCase): mock_apt_install.assert_called_with(packages=[]) @mock.patch('ceph.ceph_user') + @mock.patch.object(hooks, 'mds_relation_joined', autospec=True) @mock.patch.object(hooks, 'radosgw_relation') @mock.patch.object(hooks, 'client_relation_joined') def test_emit_cephconf(self, mock_client_rel, mock_rgw_rel, - mock_ceph_user): + mock_mds_rel, mock_ceph_user): mock_ceph_user.return_value = 'ceph-user' self.test_config.set('monitor-hosts', '127.0.0.1:1234') self.test_config.set('fsid', 'abc123') @@ -89,6 +90,8 @@ class TestHooks(test_utils.CharmTestCase): 'client': ['client:1'], 'rados:1': ['rados/1'], 'client:1': ['client/1'], + 'mds': ['mds:2'], + 'mds:2': ['mds/3'], } return x[k] @@ -127,6 +130,7 @@ class TestHooks(test_utils.CharmTestCase): mock_rgw_rel.assert_called_with(relid='rados:1', unit='rados/1') mock_client_rel.assert_called_with(relid='client:1', unit='client/1') + mock_mds_rel.assert_called_with(relid='mds:2', unit='mds/3') @mock.patch.object(hooks.ceph, 'ceph_user') @mock.patch('subprocess.check_output') @@ -162,6 +166,56 @@ class TestHooks(test_utils.CharmTestCase): mock_package_install.assert_not_called() mock_emit_cephconf.assert_any_call() + @mock.patch('subprocess.check_output', autospec=True) + @mock.patch('ceph.config', autospec=True) + @mock.patch('ceph.get_mds_key', autospec=True) + @mock.patch('ceph.ceph_user', autospec=True) + def test_mds_relation_joined(self, ceph_user, get_mds_key, ceph_config, + check_output): + my_mds_key = '1234-key' + mds_name = 'adjusted-mayfly' + rid = 'mds:1' + ceph_user.return_value = 'ceph' + get_mds_key.return_value = my_mds_key + ceph_config.side_effect = self.test_config.get + + settings = {'ceph-public-address': '127.0.0.1:1234 [::1]:4321', + 'auth': 'cephx', + 'fsid': 'some-fsid'} + + rel_data_get = {'broker_req': 'my-uuid', + 'mds-name': mds_name} + rel_data_set = {'broker-rsp-client-0': 'foobar', + '%s_mds_key' % mds_name: my_mds_key} + rel_data_set.update(settings) + + def fake_relation_get(attribute=None, rid=None, unit=None): + if attribute: + return rel_data_get[attribute] + else: + return rel_data_get + + self.relation_get.side_effect = fake_relation_get + + # unconfigured ceph-proxy + with mock.patch.object(hooks, 'log') as log: + hooks.mds_relation_joined() + log.assert_called_with( + 'MDS: FSID or admin key not provided, please configure them', + level='INFO') + + # Configure ceph-proxy with the ceph details. + self.test_config.set('monitor-hosts', settings['ceph-public-address']) + self.test_config.set('fsid', settings['fsid']) + self.test_config.set('admin-key', 'some-admin-key') + + with mock.patch.object(hooks, 'process_requests') as process_requests: + process_requests.return_value = 'foobar' + hooks.mds_relation_joined(relid=rid) + process_requests.assert_called_with('my-uuid') + self.relation_set.assert_called_with( + relation_id=rid, relation_settings=rel_data_set) + @mock.patch('ceph_hooks.emit_cephconf') @mock.patch('ceph_hooks.package_install') def test_update_apt_source(self, mock_package_install, mock_emit_cephconf):