Use ApacheSSLContext to enable SSL object storage

Enable SSL object storage using ApacheSSLContext.

Change-Id: Id044afc8c07696a5447eb9dc4836470203372090
Closes-Bug: #1690826
Closes-Bug: #1708464
This commit is contained in:
David Ames 2017-08-25 16:30:02 -07:00
parent 30f7d5fec9
commit 2226518612
6 changed files with 164 additions and 91 deletions

View File

@ -41,6 +41,15 @@ from charmhelpers.contrib.network.ip import (
from charmhelpers.contrib.storage.linux.ceph import CephConfContext
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
service_namespace = 'ceph-radosgw'
def __call__(self):
self.external_ports = [config('port')]
return super(ApacheSSLContext, self).__call__()
class HAProxyContext(context.HAProxyContext):
def __call__(self):
@ -163,7 +172,7 @@ class MonContext(context.CephContext):
if config('prefer-ipv6'):
ensure_host_resolvable_v6(host)
port = determine_apache_port(config('port'), singlenode_mode=True)
port = determine_api_port(config('port'), singlenode_mode=True)
if config('prefer-ipv6'):
port = "[::]:%s" % (port)

View File

@ -44,6 +44,7 @@ from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.core.host import (
cmp_pkgrevno,
is_container,
service_reload,
)
from charmhelpers.contrib.network.ip import (
get_relation_ip,
@ -78,6 +79,7 @@ from utils import (
services,
assess_status,
setup_keystone_certs,
disable_unused_apache_sites,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.contrib.hardening.harden import harden
@ -96,11 +98,11 @@ PACKAGES = [
# since python-keystoneclient does not pull in icehouse
# version
'radosgw',
'apache2'
]
APACHE_PACKAGES = [
'libapache2-mod-fastcgi',
'apache2',
]
@ -114,6 +116,7 @@ def install_packages():
status_set('maintenance', 'Installing radosgw packages')
apt_install(PACKAGES, fatal=True)
apt_purge(APACHE_PACKAGES)
disable_unused_apache_sites()
@hooks.hook('install.real')
@ -136,6 +139,7 @@ def install():
@harden()
def config_changed():
install_packages()
disable_unused_apache_sites()
if config('prefer-ipv6'):
status_set('maintenance', 'configuring ipv6')
@ -154,6 +158,7 @@ def config_changed():
mon_relation(r_id, unit)
CONFIGS.write_all()
configure_https()
update_nrpe_config()
@ -205,11 +210,11 @@ def identity_joined(relid=None):
sys.exit(1)
port = config('port')
admin_url = '%s:%i/swift' % (canonical_url(None, ADMIN), port)
admin_url = '%s:%i/swift' % (canonical_url(CONFIGS, ADMIN), port)
internal_url = '%s:%s/swift/v1' % \
(canonical_url(None, INTERNAL), port)
(canonical_url(CONFIGS, INTERNAL), port)
public_url = '%s:%s/swift/v1' % \
(canonical_url(None, PUBLIC), port)
(canonical_url(CONFIGS, PUBLIC), port)
relation_set(service='swift',
region=config('region'),
public_url=public_url, internal_url=internal_url,
@ -217,12 +222,6 @@ 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']})
@ -231,6 +230,7 @@ def identity_changed(relid=None):
CONFIGS.write_all()
if not is_unit_paused_set():
restart()
configure_https()
@hooks.hook('cluster-relation-joined')
@ -351,6 +351,32 @@ def update_nrpe_config():
nrpe_setup.write()
def configure_https():
'''Enables SSL API Apache config if appropriate and kicks
identity-service and image-service with any required
updates
'''
CONFIGS.write_all()
if 'https' in CONFIGS.complete_contexts():
cmd = ['a2ensite', 'openstack_https_frontend']
subprocess.check_call(cmd)
else:
cmd = ['a2dissite', 'openstack_https_frontend']
try:
subprocess.check_call(cmd)
except subprocess.CalledProcessError:
# The site is not yet enabled or
# https is not configured
pass
# TODO: improve this by checking if local CN certs are available
# first then checking reload status (see LP #1433114).
if not is_unit_paused_set():
service_reload('apache2', restart_on_failure=True)
setup_keystone_certs(CONFIGS)
@hooks.hook('update-status')
@harden()
def update_status():

View File

@ -28,6 +28,7 @@ from charmhelpers.core.hookenv import (
INFO,
relation_get,
relation_ids,
related_units,
application_version_set,
)
from charmhelpers.contrib.network.ip import (
@ -46,7 +47,10 @@ from charmhelpers.contrib.openstack.utils import (
from charmhelpers.contrib.openstack.keystone import (
format_endpoint,
)
from charmhelpers.contrib.hahelpers.cluster import get_hacluster_config
from charmhelpers.contrib.hahelpers.cluster import (
get_hacluster_config,
https,
)
from charmhelpers.core.host import (
cmp_pkgrevno,
lsb_release,
@ -104,6 +108,12 @@ CEPH_CONF = '/etc/ceph/ceph.conf'
VERSION_PACKAGE = 'radosgw'
UNUSED_APACHE_SITE_FILES = ["/etc/apache2/sites-available/000-default.conf"]
APACHE_PORTS_FILE = "/etc/apache2/ports.conf"
APACHE_SITE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
APACHE_SITE_24_CONF = '/etc/apache2/sites-available/' \
'openstack_https_frontend.conf'
BASE_RESOURCE_MAP = OrderedDict([
(HAPROXY_CONF, {
'contexts': [context.HAProxyContext(singlenode_mode=True),
@ -114,6 +124,14 @@ BASE_RESOURCE_MAP = OrderedDict([
'contexts': [ceph_radosgw_context.MonContext()],
'services': ['radosgw'],
}),
(APACHE_SITE_CONF, {
'contexts': [ceph_radosgw_context.ApacheSSLContext()],
'services': ['apache2'],
}),
(APACHE_SITE_24_CONF, {
'contexts': [ceph_radosgw_context.ApacheSSLContext()],
'services': ['apache2'],
}),
])
@ -131,6 +149,12 @@ def resource_map():
These will be managed for a single hook execution.
"""
resource_map = deepcopy(BASE_RESOURCE_MAP)
if os.path.exists('/etc/apache2/conf-available'):
resource_map.pop(APACHE_SITE_CONF)
else:
resource_map.pop(APACHE_SITE_24_CONF)
return resource_map
@ -156,7 +180,10 @@ def services():
_services = []
for v in BASE_RESOURCE_MAP.values():
_services.extend(v.get('services', []))
return list(set(_services))
_set_services = set(_services)
if not https():
_set_services.remove('apache2')
return list(_set_services)
def enable_pocket(pocket):
@ -370,7 +397,9 @@ def get_ks_ca_cert(admin_token, auth_endpoint, certs_path):
:param certs_path: Path to local certs store
:returns: None
"""
ksclient = client.Client(token=admin_token, endpoint=auth_endpoint)
ksclient = keystoneclient.httpclient.HTTPClient(token=admin_token,
endpoint=auth_endpoint)
ca_cert = get_ks_cert(ksclient, auth_endpoint, 'ca')
if ca_cert:
try:
@ -428,7 +457,7 @@ def get_ks_signing_cert(admin_token, auth_endpoint, certs_path):
@defer_if_unavailable(['keystoneclient'])
def setup_keystone_certs(unit=None, rid=None):
def setup_keystone_certs(CONFIGS):
"""
Get CA and signing certs from Keystone used to decrypt revoked token list.
@ -440,14 +469,20 @@ def setup_keystone_certs(unit=None, rid=None):
if not os.path.exists(certs_path):
mkdir(certs_path)
rdata = relation_get(unit=unit, rid=rid)
required = ['admin_token', 'auth_host', 'auth_port', 'api_version']
settings = {key: rdata.get(key) for key in required}
if not all(settings.values()):
log("Missing relation settings ({}) - deferring cert setup".format(
', '.join([k for k in settings if not settings[k]])),
# Do not continue until identity-relation is complete
if 'identity-service' not in CONFIGS.complete_contexts():
log("Missing relation settings - deferring cert setup",
level=DEBUG)
return
rdata = {}
for relid in relation_ids('identity-service'):
for unit in related_units(relid):
rdata = relation_get(unit=unit, rid=relid)
if rdata:
break
required = ['admin_token', 'auth_host', 'auth_port', 'api_version']
settings = {key: rdata.get(key) for key in required}
auth_protocol = rdata.get('auth_protocol', 'http')
if is_ipv6(settings.get('auth_host')):
@ -463,3 +498,21 @@ def setup_keystone_certs(unit=None, rid=None):
get_ks_signing_cert(settings['admin_token'], auth_endpoint, certs_path)
except KSCertSetupException as e:
log("Keystone certs setup incomplete - {}".format(e), level=INFO)
def disable_unused_apache_sites():
"""Ensure that unused apache configurations are disabled to prevent them
from conflicting with the charm-provided version.
"""
for apache_site_file in UNUSED_APACHE_SITE_FILES:
apache_site = apache_site_file.split('/')[-1].split('.')[0]
if os.path.exists(apache_site_file):
try:
# Try it cleanly
subprocess.check_call(['a2dissite', apache_site])
except subprocess.CalledProcessError:
# Remove the file
os.remove(apache_site_file)
with open(APACHE_PORTS_FILE, 'w') as ports:
ports.write("")

View File

@ -29,6 +29,7 @@ TO_PATCH = [
'cmp_pkgrevno',
'socket',
'unit_public_ip',
'determine_api_port'
]
@ -54,6 +55,7 @@ class HAProxyContextTests(CharmTestCase):
_haconfig.side_effect = self.test_config.get
_harelation_ids.return_value = []
haproxy_context = context.HAProxyContext()
self.determine_api_port.return_value = 70
expect = {
'cephradosgw_bind_port': 70,
'service_ports': {'cephradosgw-server': [80, 70]}
@ -190,6 +192,7 @@ class MonContextTest(CharmTestCase):
self.relation_get.side_effect = _relation_get
self.relation_ids.return_value = ['mon:6']
self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2']
self.determine_api_port.return_value = 70
expect = {
'auth_supported': 'cephx',
'hostname': 'testhost',
@ -229,6 +232,7 @@ class MonContextTest(CharmTestCase):
self.relation_get.side_effect = _relation_get
self.relation_ids.return_value = ['mon:6']
self.related_units.return_value = ['ceph-proxy/0']
self.determine_api_port.return_value = 70
expect = {
'auth_supported': 'cephx',
'hostname': 'testhost',
@ -277,6 +281,7 @@ class MonContextTest(CharmTestCase):
self.relation_get.side_effect = _relation_get
self.relation_ids.return_value = ['mon:6']
self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2']
self.determine_api_port.return_value = 70
expect = {
'auth_supported': 'none',
'hostname': 'testhost',
@ -307,6 +312,7 @@ class MonContextTest(CharmTestCase):
self.relation_get.side_effect = _relation_get
self.relation_ids.return_value = ['mon:6']
self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2']
self.determine_api_port.return_value = 70
expect = {
'auth_supported': 'cephx',
'hostname': 'testhost',

View File

@ -12,8 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
from mock import (
call,
patch,
@ -29,6 +27,7 @@ TO_PATCH = [
'application_version_set',
'get_upstream_version',
'format_endpoint',
'https',
]
@ -90,8 +89,11 @@ class CephRadosGWUtilTests(CharmTestCase):
# ports=None whilst port checks are disabled.
f.assert_called_once_with('assessor', services='s1', ports=None)
@patch.object(utils, 'related_units')
@patch.object(utils, 'relation_ids')
@patch.dict('sys.modules', {'requests': MagicMock(),
'keystoneclient': MagicMock()})
'keystoneclient': MagicMock(),
'httpclient': MagicMock()})
@patch.object(utils, 'is_ipv6', lambda addr: False)
@patch.object(utils, 'get_ks_signing_cert')
@patch.object(utils, 'get_ks_ca_cert')
@ -99,93 +101,71 @@ class CephRadosGWUtilTests(CharmTestCase):
@patch.object(utils, 'mkdir')
def test_setup_keystone_certs(self, mock_mkdir, mock_relation_get,
mock_get_ks_ca_cert,
mock_get_ks_signing_cert):
mock_get_ks_signing_cert,
mock_relation_ids, mock_related_units):
auth_host = 'foo/bar'
auth_port = 80
admin_token = '666'
auth_url = 'http://%s:%s/v2.0' % (auth_host, auth_port)
self.format_endpoint.return_value = auth_url
configs = MagicMock()
configs.complete_contexts.return_value = ['identity-service']
mock_relation_ids.return_value = ['identity-service:5']
mock_related_units.return_value = ['keystone/1']
mock_relation_get.return_value = {'auth_host': auth_host,
'auth_port': auth_port,
'admin_token': admin_token,
'api_version': '2'}
utils.setup_keystone_certs()
utils.setup_keystone_certs(configs)
mock_get_ks_signing_cert.assert_has_calls([call(admin_token, auth_url,
'/var/lib/ceph/nss')])
mock_get_ks_ca_cert.assert_has_calls([call(admin_token, auth_url,
'/var/lib/ceph/nss')])
def test_get_ks_signing_cert(self):
@patch.object(utils, 'get_ks_cert')
@patch.object(utils.subprocess, 'Popen')
@patch.object(utils.subprocess, 'check_output')
def test_get_ks_signing_cert(self, mock_check_output, mock_Popen,
mock_get_ks_cert):
auth_host = 'foo/bar'
auth_port = 80
admin_token = '666'
auth_url = 'http://%s:%s/v2.0' % (auth_host, auth_port)
mock_ksclient = MagicMock
m = mock_open()
with patch.dict('sys.modules',
{'requests': MagicMock(),
'keystoneclient': mock_ksclient,
'keystoneclient.exceptions': MagicMock(),
'keystoneclient.exceptions.ConnectionRefused':
MagicMock(),
'keystoneclient.exceptions.Forbidden': MagicMock(),
'keystoneclient.v2_0': MagicMock(),
'keystoneclient.v2_0.client': MagicMock()}):
# Reimport
del sys.modules['utils']
import utils
with patch.object(utils, 'subprocess') as mock_subprocess:
with patch.object(utils, 'open', m, create=True):
mock_certificates = MagicMock()
mock_ksclient.certificates = mock_certificates
mock_certificates.get_signing_certificate.return_value = \
'signing_cert_data'
utils.get_ks_signing_cert(admin_token, auth_url,
'/foo/bar')
mock_certificates.get_signing_certificate.return_value = \
None
self.assertRaises(utils.KSCertSetupException,
utils.get_ks_signing_cert, admin_token,
auth_url, '/foo/bar')
with patch.object(utils, 'open', m, create=True):
c = ['openssl', 'x509', '-in',
'/foo/bar/signing_certificate.pem',
'-pubkey']
mock_subprocess.check_output.assert_called_with(c)
mock_get_ks_cert.return_value = 'signing_cert_data'
utils.get_ks_signing_cert(admin_token, auth_url, '/foo/bar')
def test_get_ks_ca_cert(self):
mock_get_ks_cert.return_value = None
with self.assertRaises(utils.KSCertSetupException):
utils.get_ks_signing_cert(admin_token, auth_url, '/foo/bar')
c = ['openssl', 'x509', '-in',
'/foo/bar/signing_certificate.pem',
'-pubkey']
mock_check_output.assert_called_with(c)
@patch.object(utils, 'get_ks_cert')
@patch.object(utils.subprocess, 'Popen')
@patch.object(utils.subprocess, 'check_output')
def test_get_ks_ca_cert(self, mock_check_output, mock_Popen,
mock_get_ks_cert):
auth_host = 'foo/bar'
auth_port = 80
admin_token = '666'
auth_url = 'http://%s:%s/v2.0' % (auth_host, auth_port)
mock_ksclient = MagicMock
m = mock_open()
with patch.dict('sys.modules',
{'requests': MagicMock(),
'keystoneclient': mock_ksclient,
'keystoneclient.exceptions': MagicMock(),
'keystoneclient.exceptions.ConnectionRefused':
MagicMock(),
'keystoneclient.exceptions.Forbidden': MagicMock(),
'keystoneclient.v2_0': MagicMock(),
'keystoneclient.v2_0.client': MagicMock()}):
# Reimport
del sys.modules['utils']
import utils
with patch.object(utils, 'subprocess') as mock_subprocess:
with patch.object(utils, 'open', m, create=True):
mock_certificates = MagicMock()
mock_ksclient.certificates = mock_certificates
mock_certificates.get_ca_certificate.return_value = \
'ca_cert_data'
utils.get_ks_ca_cert(admin_token, auth_url, '/foo/bar')
mock_certificates.get_ca_certificate.return_value = None
self.assertRaises(utils.KSCertSetupException,
utils.get_ks_ca_cert, admin_token,
auth_url, '/foo/bar')
with patch.object(utils, 'open', m, create=True):
mock_get_ks_cert.return_value = 'ca_cert_data'
utils.get_ks_ca_cert(admin_token, auth_url, '/foo/bar')
c = ['openssl', 'x509', '-in', '/foo/bar/ca.pem',
'-pubkey']
mock_subprocess.check_output.assert_called_with(c)
mock_get_ks_cert.return_value = None
with self.assertRaises(utils.KSCertSetupException):
utils.get_ks_ca_cert(admin_token, auth_url, '/foo/bar')
c = ['openssl', 'x509', '-in', '/foo/bar/ca.pem',
'-pubkey']
mock_check_output.assert_called_with(c)

View File

@ -53,6 +53,9 @@ TO_PATCH = [
'get_hacluster_config',
'update_dns_ha_resource_params',
'get_relation_ip',
'disable_unused_apache_sites',
'service_reload',
'setup_keystone_certs',
]
@ -69,8 +72,7 @@ class CephRadosGWTests(CharmTestCase):
ceph_hooks.install_packages()
self.add_source.assert_called_with('distro', 'secretkey')
self.assertTrue(self.apt_update.called)
self.apt_purge.assert_called_with(['libapache2-mod-fastcgi',
'apache2'])
self.apt_purge.assert_called_with(['libapache2-mod-fastcgi'])
def test_install(self):
_install_packages = self.patch('install_packages')
@ -144,15 +146,12 @@ 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,
mock_setup_keystone_certs):
def test_identity_joined_early_version(self, _config):
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',