diff --git a/hooks/ceph_radosgw_context.py b/hooks/ceph_radosgw_context.py index 1e079904..839731e0 100644 --- a/hooks/ceph_radosgw_context.py +++ b/hooks/ceph_radosgw_context.py @@ -13,6 +13,7 @@ from charmhelpers.core.hookenv import ( relation_get, unit_get, ) +import os import socket import dns.resolver @@ -101,6 +102,12 @@ class MonContext(context.OSContextGenerator): 'embedded_webserver': config('use-embedded-webserver'), } + certs_path = '/var/lib/ceph/nss' + paths = [os.path.join(certs_path, 'ca.pem'), + os.path.join(certs_path, 'signing_certificate.pem')] + if all([os.path.isfile(p) for p in paths]): + ctxt['cms'] = True + if self.context_complete(ctxt): return ctxt diff --git a/hooks/hooks.py b/hooks/hooks.py index 8898706b..91c43bd0 100755 --- a/hooks/hooks.py +++ b/hooks/hooks.py @@ -13,14 +13,19 @@ import sys import glob import os import ceph + from charmhelpers.core.hookenv import ( relation_get, relation_ids, + related_units, config, unit_get, open_port, relation_set, - log, ERROR, + log, + DEBUG, + WARNING, + ERROR, Hooks, UnregisteredHookError, status_set, ) @@ -43,9 +48,11 @@ from utils import ( REQUIRED_INTERFACES, check_optional_relations, ) - from charmhelpers.payload.execd import execd_preinstall -from charmhelpers.core.host import cmp_pkgrevno +from charmhelpers.core.host import ( + cmp_pkgrevno, + mkdir, +) from charmhelpers.contrib.network.ip import ( get_iface_for_address, @@ -89,6 +96,11 @@ PACKAGES = [ 'radosgw', 'ntp', 'haproxy', + 'libnss3-tools', + 'python-keystoneclient', + 'python-six', # Ensures correct version is installed for precise + # since python-keystoneclient does not pull in icehouse + # version ] APACHE_PACKAGES = [ @@ -155,6 +167,99 @@ def apache_ports(): shutil.copy('files/ports.conf', '/etc/apache2/ports.conf') +def setup_keystone_certs(unit=None, rid=None): + """ + Get CA and signing certs from Keystone used to decrypt revoked token list. + """ + import requests + try: + # Kilo and newer + from keystoneclient.exceptions import ConnectionRefused + except ImportError: + # Juno and older + from keystoneclient.exceptions import ConnectionError as \ + ConnectionRefused + + from keystoneclient.v2_0 import client + + certs_path = '/var/lib/ceph/nss' + mkdir(certs_path) + + rdata = relation_get(unit=unit, rid=rid) + auth_protocol = rdata.get('auth_protocol', 'http') + + required_keys = ['admin_token', 'auth_host', 'auth_port'] + settings = {} + for key in required_keys: + settings[key] = rdata.get(key) + + if not all(settings.values()): + log("Missing relation settings (%s) - skipping cert setup" % + (', '.join([k for k in settings.keys() if not settings[k]])), + level=DEBUG) + return + + auth_endpoint = "%s://%s:%s/v2.0" % (auth_protocol, settings['auth_host'], + settings['auth_port']) + keystone = client.Client(token=settings['admin_token'], + endpoint=auth_endpoint) + + # CA + try: + # Kilo and newer + ca_cert = keystone.certificates.get_ca_certificate() + except AttributeError: + # Juno and older + ca_cert = requests.request('GET', auth_endpoint + + '/certificates/ca').text + except ConnectionRefused: + log("Error connecting to keystone - skipping ca/signing cert setup", + level=WARNING) + return + + if ca_cert: + log("Updating ca cert from keystone", level=DEBUG) + ca = os.path.join(certs_path, 'ca.pem') + with open(ca, 'w') as fd: + fd.write(ca_cert) + + out = subprocess.check_output(['openssl', 'x509', '-in', ca, + '-pubkey']) + p = subprocess.Popen(['certutil', '-d', certs_path, '-A', '-n', 'ca', + '-t', 'TCu,Cu,Tuw'], stdin=subprocess.PIPE) + p.communicate(out) + else: + log("No ca cert available from keystone", level=DEBUG) + + # Signing cert + try: + # Kilo and newer + signing_cert = keystone.certificates.get_signing_certificate() + except AttributeError: + # Juno and older + signing_cert = requests.request('GET', auth_endpoint + + '/certificates/signing').text + except ConnectionRefused: + log("Error connecting to keystone - skipping ca/signing cert setup", + level=WARNING) + return + + if signing_cert: + log("Updating signing cert from keystone", level=DEBUG) + signing_cert_path = os.path.join(certs_path, 'signing_certificate.pem') + with open(signing_cert_path, 'w') as fd: + fd.write(signing_cert) + + out = subprocess.check_output(['openssl', 'x509', '-in', + signing_cert_path, '-pubkey']) + p = subprocess.Popen(['certutil', '-A', '-d', certs_path, '-n', + 'signing_cert', '-t', 'P,P,P'], + stdin=subprocess.PIPE) + p.communicate(out) + else: + log("No signing cert available from keystone", level=DEBUG) + + @hooks.hook('upgrade-charm', 'config-changed') @restart_on_change({'/etc/ceph/ceph.conf': ['radosgw'], @@ -170,8 +275,9 @@ def config_changed(): apache_modules() apache_ports() apache_reload() + for r_id in relation_ids('identity-service'): - identity_joined(relid=r_id) + identity_changed(relid=r_id) @hooks.hook('mon-relation-departed', @@ -225,10 +331,17 @@ def identity_joined(relid=None): requested_roles=config('operator-roles'), relation_id=relid) + if relid: + for unit in related_units(relid): + setup_keystone_certs(unit=unit, rid=relid) + else: + setup_keystone_certs() + @hooks.hook('identity-service-relation-changed') @restart_on_change({'/etc/ceph/ceph.conf': ['radosgw']}) -def identity_changed(): +def identity_changed(relid=None): + identity_joined(relid) CONFIGS.write_all() restart() diff --git a/templates/ceph.conf b/templates/ceph.conf index e1c95fce..a4626d58 100644 --- a/templates/ceph.conf +++ b/templates/ceph.conf @@ -30,5 +30,7 @@ rgw keystone accepted roles = {{ user_roles }} rgw keystone token cache size = {{ cache_size }} rgw keystone revocation interval = {{ revocation_check_interval }} rgw s3 auth use keystone = true -#nss db path = /var/lib/ceph/nss +{% if cms -%} +nss db path = /var/lib/ceph/nss +{% endif %} {% endif %} diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py index 9b17cd10..fecc8b71 100644 --- a/unit_tests/test_hooks.py +++ b/unit_tests/test_hooks.py @@ -43,6 +43,7 @@ TO_PATCH = [ 'relation_ids', 'relation_set', 'relation_get', + 'related_units', 'render_template', 'shutil', 'status_set', @@ -108,9 +109,8 @@ class CephRadosGWTests(CharmTestCase): self.add_source.assert_called_with('distro', 'secretkey') self.assertTrue(self.apt_update.called) self.assertFalse(_install_packages.called) - self.apt_install.assert_called_with(['radosgw', - 'ntp', - 'haproxy'], fatal=True) + self.apt_install.assert_called_with(ceph_hooks.PACKAGES, + fatal=True) self.apt_purge.assert_called_with(['libapache2-mod-fastcgi', 'apache2']) @@ -167,6 +167,7 @@ class CephRadosGWTests(CharmTestCase): ] self.subprocess.call.assert_has_calls(calls) + @patch.object(ceph_hooks, 'mkdir', lambda *args: None) def test_config_changed(self): _install_packages = self.patch('install_packages') _emit_apacheconf = self.patch('emit_apacheconf') @@ -221,12 +222,15 @@ class CephRadosGWTests(CharmTestCase): cmd = ['service', 'radosgw', 'restart'] self.subprocess.call.assert_called_with(cmd) + @patch.object(ceph_hooks, 'setup_keystone_certs') @patch('charmhelpers.contrib.openstack.ip.service_name', lambda *args: 'ceph-radosgw') @patch('charmhelpers.contrib.openstack.ip.config') - def test_identity_joined_early_version(self, _config): + def test_identity_joined_early_version(self, _config, + mock_setup_keystone_certs): self.cmp_pkgrevno.return_value = -1 ceph_hooks.identity_joined() + self.assertTrue(mock_setup_keystone_certs.called) self.sys.exit.assert_called_with(1) @patch('charmhelpers.contrib.openstack.ip.service_name', @@ -234,6 +238,7 @@ class CephRadosGWTests(CharmTestCase): @patch('charmhelpers.contrib.openstack.ip.resolve_address') @patch('charmhelpers.contrib.openstack.ip.config') def test_identity_joined(self, _config, _resolve_address): + self.related_units = ['unit/0'] self.cmp_pkgrevno.return_value = 1 _resolve_address.return_value = 'myserv' _config.side_effect = self.test_config.get @@ -257,6 +262,7 @@ class CephRadosGWTests(CharmTestCase): @patch('charmhelpers.contrib.openstack.ip.config') def test_identity_joined_public_name(self, _config, _unit_get, _is_clustered): + self.related_units = ['unit/0'] _config.side_effect = self.test_config.get self.test_config.set('os-public-hostname', 'files.example.com') _unit_get.return_value = 'myserv' @@ -271,11 +277,13 @@ class CephRadosGWTests(CharmTestCase): relation_id='rid', admin_url='http://myserv:80/swift') - def test_identity_changed(self): + @patch.object(ceph_hooks, 'identity_joined') + def test_identity_changed(self, mock_identity_joined): _restart = self.patch('restart') ceph_hooks.identity_changed() self.CONFIGS.write_all.assert_called_with() self.assertTrue(_restart.called) + self.assertTrue(mock_identity_joined.called) @patch('charmhelpers.contrib.openstack.ip.is_clustered') @patch('charmhelpers.contrib.openstack.ip.unit_get')