- Added the cluster_partition_handling option fixes LP: #1442443

- Refactored the ssl handling code.
- Added a generic config loader
This commit is contained in:
Jorge Niedbalski 2015-04-09 23:34:40 -03:00
parent fc72a580b8
commit 6ab6c5e2ca
9 changed files with 368 additions and 152 deletions

View File

@ -8,7 +8,7 @@ options:
type: string
default: "off"
description: |
Enable SSL connections on rabbitmq, valid values are 'off', 'on', 'only'. If ssl_key,
Enable SSL connections on rabbitmq, valid values are 'off', 'on', 'only'. If ssl_key,
ssl_cert, ssl_ca are provided then then those values will be used. Otherwise
the service will act as its own certificate authority and pass its ca cert to clients.
For HA or clustered rabbits ssl key/cert must be provided.
@ -92,6 +92,12 @@ options:
description: |
When set to true the 'ha-mode: all' policy is applied to all the exchages
that match the expression '^(?!amq\.).*'
cluster-partition-handling:
type: string
default: ignore
description: |
How to respond to cluster partitions.
See http://www.rabbitmq.com/partitions.html for further details.
rbd-size:
type: string
default: 5G

View File

@ -1,6 +1,4 @@
import os
import pwd
import grp
import re
import socket
import sys
@ -8,6 +6,13 @@ import subprocess
import glob
import tempfile
from rabbitmq_context import (
RabbitMQSSLContext,
RabbitMQClusterContext
)
from charmhelpers.core.templating import render
from charmhelpers.contrib.openstack.utils import (
get_hostname,
)
@ -30,8 +35,6 @@ from charmhelpers.core.host import (
cmp_pkgrevno
)
from charmhelpers.core.templating import render
from charmhelpers.contrib.peerstorage import (
peer_store,
peer_retrieve
@ -57,7 +60,10 @@ _named_passwd = '/var/lib/charm/{}/{}.passwd'
# the charm doesn't concern itself with template specifics etc.
CONFIG_FILES = OrderedDict([
(RABBITMQ_CONF, {
'hook_contexts': None,
'hook_contexts': [
RabbitMQSSLContext(),
RabbitMQClusterContext(),
],
'services': ['rabbitmq-server']
}),
(ENV_CONF, {
@ -71,6 +77,32 @@ CONFIG_FILES = OrderedDict([
])
class ConfigRenderer():
def __init__(self, config):
self.config_data = {}
for config_path, data in config.items():
if 'hook_contexts' in data and data['hook_contexts']:
ctxt = {}
for svc_context in data['hook_contexts']:
ctxt.update(svc_context())
self.config_data[config_path] = ctxt
def write(self, config_path):
data = self.config_data.get(config_path, None)
if data:
log("writing config file: %s , data: %s" % (config_path,
str(data)), level='DEBUG')
render(os.path.basename(config_path), config_path,
data, perms=0o644)
def write_all(self):
for service in self.config_data.keys():
self.write(service)
class RabbitmqError(Exception):
pass
@ -390,40 +422,6 @@ def enable_plugin(plugin):
def disable_plugin(plugin):
_manage_plugin(plugin, 'disable')
ssl_key_file = "/etc/rabbitmq/rabbit-server-privkey.pem"
ssl_cert_file = "/etc/rabbitmq/rabbit-server-cert.pem"
ssl_ca_file = "/etc/rabbitmq/rabbit-server-ca.pem"
def enable_ssl(ssl_key, ssl_cert, ssl_port,
ssl_ca=None, ssl_only=False, ssl_client=None):
uid = pwd.getpwnam("root").pw_uid
gid = grp.getgrnam("rabbitmq").gr_gid
for contents, path in (
(ssl_key, ssl_key_file),
(ssl_cert, ssl_cert_file),
(ssl_ca, ssl_ca_file)):
if not contents:
continue
with open(path, 'w') as fh:
fh.write(contents)
os.chmod(path, 0o640)
os.chown(path, uid, gid)
data = {
"ssl_port": ssl_port,
"ssl_cert_file": ssl_cert_file,
"ssl_key_file": ssl_key_file,
"ssl_client": ssl_client,
"ssl_ca_file": "",
"ssl_only": ssl_only}
if ssl_ca:
data["ssl_ca_file"] = ssl_ca_file
render(os.path.basename(RABBITMQ_CONF), RABBITMQ_CONF, data, perms=0o644)
def execute(cmd, die=False, echo=False):
""" Executes a command

125
hooks/rabbitmq_context.py Normal file
View File

@ -0,0 +1,125 @@
from charmhelpers.contrib.ssl.service import ServiceCA
from charmhelpers.core.hookenv import (
open_port,
close_port,
config,
log,
ERROR,
)
import sys
import pwd
import grp
import os
import base64
import ssl_utils
ssl_key_file = "/etc/rabbitmq/rabbit-server-privkey.pem"
ssl_cert_file = "/etc/rabbitmq/rabbit-server-cert.pem"
ssl_ca_file = "/etc/rabbitmq/rabbit-server-ca.pem"
def convert_from_base64(v):
# Rabbit originally supported pem encoded key/cert in config, play
# nice on upgrades as we now expect base64 encoded key/cert/ca.
if not v:
return v
if v.startswith('-----BEGIN'):
return v
try:
return base64.b64decode(v)
except TypeError:
return v
class RabbitMQSSLContext(object):
def enable_ssl(self, ssl_key, ssl_cert, ssl_port,
ssl_ca=None, ssl_only=False, ssl_client=None):
uid = pwd.getpwnam("root").pw_uid
gid = grp.getgrnam("rabbitmq").gr_gid
for contents, path in (
(ssl_key, ssl_key_file),
(ssl_cert, ssl_cert_file),
(ssl_ca, ssl_ca_file)):
if not contents:
continue
with open(path, 'w') as fh:
fh.write(contents)
os.chmod(path, 0o640)
os.chown(path, uid, gid)
data = {
"ssl_port": ssl_port,
"ssl_cert_file": ssl_cert_file,
"ssl_key_file": ssl_key_file,
"ssl_client": ssl_client,
"ssl_ca_file": "",
"ssl_only": ssl_only
}
if ssl_ca:
data["ssl_ca_file"] = ssl_ca_file
return data
def __call__(self):
"""
The legacy config support adds some additional complications.
ssl_enabled = True, ssl = off -> ssl enabled
ssl_enabled = False, ssl = on -> ssl enabled
"""
ssl_mode, external_ca = ssl_utils.get_ssl_mode()
ctxt = {
'ssl_mode': ssl_mode,
}
if ssl_mode == 'off':
close_port(config('ssl_port'))
ssl_utils.reconfigure_client_ssl()
return ctxt
ssl_key = convert_from_base64(config('ssl_key'))
ssl_cert = convert_from_base64(config('ssl_cert'))
ssl_ca = convert_from_base64(config('ssl_ca'))
ssl_port = config('ssl_port')
# If external managed certs then we need all the fields.
if (
ssl_mode in ('on', 'only') and
any((ssl_key, ssl_cert)) and
not all((ssl_key, ssl_cert))
):
log('If ssl_key or ssl_cert are specified both are required.',
level=ERROR)
sys.exit(1)
if not external_ca:
ssl_cert, ssl_key, ssl_ca = ServiceCA.get_service_cert()
ctxt.update(self.enable_ssl(
ssl_key, ssl_cert, ssl_port, ssl_ca,
ssl_only=(ssl_mode == "only"), ssl_client=False
))
ssl_utils.reconfigure_client_ssl(True)
open_port(ssl_port)
return ctxt
class RabbitMQClusterContext(object):
def __call__(self):
return {
'cluster_partition_handling': config('cluster-partition-handling'),
}

View File

@ -1,5 +1,5 @@
#!/usr/bin/python
import base64
import os
import shutil
import sys
@ -8,6 +8,8 @@ import glob
import socket
import rabbit_utils as rabbit
import ssl_utils
from lib.utils import (
chown, chmod,
is_newer,
@ -60,7 +62,6 @@ from charmhelpers.core.host import (
service_restart,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.contrib.ssl.service import ServiceCA
from charmhelpers.contrib.peerstorage import (
peer_echo,
@ -175,7 +176,7 @@ def amqp_changed(relation_id=None, remote_unit=None):
get_address_in_network(config('access-network'),
unit_get('private-address'))
configure_client_ssl(relation_settings)
ssl_utils.configure_client_ssl(relation_settings)
if is_clustered():
relation_settings['clustered'] = 'true'
@ -512,103 +513,10 @@ def upgrade_charm():
MAN_PLUGIN = 'rabbitmq_management'
def configure_client_ssl(relation_data):
"""Configure client with ssl
"""
ssl_mode, external_ca = _get_ssl_mode()
if ssl_mode == 'off':
return
relation_data['ssl_port'] = config('ssl_port')
if external_ca:
if config('ssl_ca'):
relation_data['ssl_ca'] = base64.b64encode(
config('ssl_ca'))
return
ca = ServiceCA.get_ca()
relation_data['ssl_ca'] = base64.b64encode(ca.get_ca_bundle())
def _get_ssl_mode():
ssl_mode = config('ssl')
external_ca = False
# Legacy config boolean option
ssl_on = config('ssl_enabled')
if ssl_mode == 'off' and ssl_on is False:
ssl_mode = 'off'
elif ssl_mode == 'off' and ssl_on:
ssl_mode = 'on'
ssl_key = config('ssl_key')
ssl_cert = config('ssl_cert')
if all((ssl_key, ssl_cert)):
external_ca = True
return ssl_mode, external_ca
def _convert_from_base64(v):
# Rabbit originally supported pem encoded key/cert in config, play
# nice on upgrades as we now expect base64 encoded key/cert/ca.
if not v:
return v
if v.startswith('-----BEGIN'):
return v
try:
return base64.b64decode(v)
except TypeError:
return v
def reconfigure_client_ssl(ssl_enabled=False):
ssl_config_keys = set(('ssl_key', 'ssl_cert', 'ssl_ca'))
for rid in relation_ids('amqp'):
rdata = relation_get(rid=rid, unit=os.environ['JUJU_UNIT_NAME'])
if not ssl_enabled and ssl_config_keys.intersection(rdata):
# No clean way to remove entirely, but blank them.
relation_set(relation_id=rid, ssl_key='', ssl_cert='', ssl_ca='')
elif ssl_enabled and not ssl_config_keys.intersection(rdata):
configure_client_ssl(rdata)
relation_set(relation_id=rid, **rdata)
def configure_rabbit_ssl():
"""
The legacy config support adds some additional complications.
ssl_enabled = True, ssl = off -> ssl enabled
ssl_enabled = False, ssl = on -> ssl enabled
"""
ssl_mode, external_ca = _get_ssl_mode()
if ssl_mode == 'off':
if os.path.exists(rabbit.RABBITMQ_CONF):
os.remove(rabbit.RABBITMQ_CONF)
close_port(config('ssl_port'))
reconfigure_client_ssl()
return
ssl_key = _convert_from_base64(config('ssl_key'))
ssl_cert = _convert_from_base64(config('ssl_cert'))
ssl_ca = _convert_from_base64(config('ssl_ca'))
ssl_port = config('ssl_port')
# If external managed certs then we need all the fields.
if (ssl_mode in ('on', 'only') and any((ssl_key, ssl_cert)) and
not all((ssl_key, ssl_cert))):
log('If ssl_key or ssl_cert are specified both are required.',
level=ERROR)
sys.exit(1)
if not external_ca:
ssl_cert, ssl_key, ssl_ca = ServiceCA.get_service_cert()
rabbit.enable_ssl(
ssl_key, ssl_cert, ssl_port, ssl_ca,
ssl_only=(ssl_mode == "only"), ssl_client=False)
reconfigure_client_ssl(True)
open_port(ssl_port)
@hooks.hook('config-changed')
@restart_on_change(rabbit.restart_map())
def config_changed():
if config('prefer-ipv6'):
rabbit.assert_charm_supports_ipv6()
@ -638,9 +546,9 @@ def config_changed():
rabbit.disable_plugin(MAN_PLUGIN)
close_port(55672)
configure_rabbit_ssl()
rabbit.set_all_mirroring_queues(config('mirroring-queues'))
rabbit.ConfigRenderer(
rabbit.CONFIG_FILES).write_all()
if is_relation_made("ha"):
ha_is_active_active = config("ha-vip-only")

55
hooks/ssl_utils.py Normal file
View File

@ -0,0 +1,55 @@
from charmhelpers.contrib.ssl.service import ServiceCA
from charmhelpers.core.hookenv import (
config,
relation_ids,
relation_set,
relation_get,
)
import base64
import os
def get_ssl_mode():
ssl_mode = config('ssl')
external_ca = False
# Legacy config boolean option
ssl_on = config('ssl_enabled')
if ssl_mode == 'off' and ssl_on is False:
ssl_mode = 'off'
elif ssl_mode == 'off' and ssl_on:
ssl_mode = 'on'
ssl_key = config('ssl_key')
ssl_cert = config('ssl_cert')
if all((ssl_key, ssl_cert)):
external_ca = True
return ssl_mode, external_ca
def configure_client_ssl(relation_data):
"""Configure client with ssl
"""
ssl_mode, external_ca = get_ssl_mode()
if ssl_mode == 'off':
return
relation_data['ssl_port'] = config('ssl_port')
if external_ca:
if config('ssl_ca'):
relation_data['ssl_ca'] = base64.b64encode(
config('ssl_ca'))
return
ca = ServiceCA.get_ca()
relation_data['ssl_ca'] = base64.b64encode(ca.get_ca_bundle())
def reconfigure_client_ssl(ssl_enabled=False):
ssl_config_keys = set(('ssl_key', 'ssl_cert', 'ssl_ca'))
for rid in relation_ids('amqp'):
rdata = relation_get(rid=rid, unit=os.environ['JUJU_UNIT_NAME'])
if not ssl_enabled and ssl_config_keys.intersection(rdata):
# No clean way to remove entirely, but blank them.
relation_set(relation_id=rid, ssl_key='', ssl_cert='', ssl_ca='')
elif ssl_enabled and not ssl_config_keys.intersection(rdata):
configure_client_ssl(rdata)
relation_set(relation_id=rid, **rdata)

View File

@ -1,21 +1,35 @@
[
{rabbit, [
{% if ssl_only %}
{rabbit, [
{% if ssl_only %}
{tcp_listeners, []},
{% else %}
{tcp_listeners, [5672]},
{% endif %}
{ssl_listeners, [{{ ssl_port }}]},
{% if ssl_port %}
{ssl_listeners, [{{ ssl_port }}]},
{% endif %}
{% if ssl_mode == "on" %}
{ssl_options, [
{verify, verify_peer},
{% if ssl_client %}
{fail_if_no_peer_cert, true},
{% if ssl_client %}
{fail_if_no_peer_cert, true},
{% else %}
{fail_if_no_peer_cert, false},
{% endif %}{% if ssl_ca_file %}
{cacertfile, "{{ ssl_ca_file }}"}, {% endif %}
{certfile, "{{ ssl_cert_file }}"},
{keyfile, "{{ ssl_key_file }}"}
]}
{% endif %}
{% if ssl_ca_file %}
{cacertfile, "{{ ssl_ca_file }}"},
{% endif %}
{% if ssl_cert_file %}
{certfile, "{{ ssl_cert_file }}"},
{% endif %}
{% if ssl_key_file %}
{keyfile, "{{ ssl_key_file }}"}
{% endif %}
]},
{% endif %}
{% if cluster_partition_handling %}
{cluster_partition_handling, {{ cluster_partition_handling }}}
{% endif %}
]}
].
].

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
#
# This Amulet test deploys rabbitmq-server
#
# Note: We use python2, because pika doesn't support python3
import amulet
# The number of seconds to wait for the environment to setup.
seconds = 1200
d = amulet.Deployment(series="trusty")
d.add('rabbitmq-server', units=1)
# Create a configuration.
configuration = {'cluster-partition-handling': "autoheal"}
d.configure('rabbitmq-server', configuration)
d.expose('rabbitmq-server')
try:
d.setup(timeout=seconds)
d.sentry.wait(seconds)
except amulet.helpers.TimeoutError:
message = 'The environment did not setup in %d seconds.' % seconds
amulet.raise_status(amulet.SKIP, msg=message)
except:
raise
rabbit_unit = d.sentry.unit['rabbitmq-server/0']
output, code = rabbit_unit.run("grep autoheal /etc/rabbitmq/rabbitmq.conf")
if code != 0 or output == "":
amulet.raise_status(amulet.FAIL, msg=message)

View File

@ -0,0 +1,79 @@
import rabbitmq_context
import mock
import unittest
class TestRabbitMQSSLContext(unittest.TestCase):
@mock.patch("rabbitmq_context.config")
@mock.patch("rabbitmq_context.close_port")
@mock.patch("rabbitmq_context.ssl_utils.reconfigure_client_ssl")
@mock.patch("rabbitmq_context.ssl_utils.get_ssl_mode")
def test_context_ssl_off(self, get_ssl_mode, reconfig_ssl, close_port,
config):
get_ssl_mode.return_value = ("off", "off")
self.assertEqual(rabbitmq_context.RabbitMQSSLContext().__call__(), {
"ssl_mode": "off"
})
close_port.assert_called_once()
reconfig_ssl.assert_called_once()
@mock.patch("rabbitmq_context.open_port")
@mock.patch("rabbitmq_context.os.chmod")
@mock.patch("rabbitmq_context.os.chown")
@mock.patch("rabbitmq_context.pwd.getpwnam")
@mock.patch("rabbitmq_context.grp.getgrnam")
@mock.patch("rabbitmq_context.config")
@mock.patch("rabbitmq_context.close_port")
@mock.patch("rabbitmq_context.ssl_utils.reconfigure_client_ssl")
@mock.patch("rabbitmq_context.ssl_utils.get_ssl_mode")
def test_context_ssl_on(self, get_ssl_mode, reconfig_ssl, close_port,
config, gr, pw, chown, chmod, open_port):
get_ssl_mode.return_value = ("on", "on")
def config_get(n):
return None
config.side_effect = config_get
def pw(name):
class Uid(object):
pw_uid = 1
gr_gid = 100
return Uid()
pw.side_effect = pw
gr.side_effect = pw
m = mock.mock_open()
with mock.patch('rabbitmq_context.open', m, create=True):
self.assertEqual(
rabbitmq_context.RabbitMQSSLContext().__call__(), {
"ssl_port": None,
"ssl_cert_file": "/etc/rabbitmq/rabbit-server-cert.pem",
"ssl_key_file": '/etc/rabbitmq/rabbit-server-privkey.pem',
"ssl_client": False,
"ssl_ca_file": "",
"ssl_only": False,
"ssl_mode": "on",
})
reconfig_ssl.assert_called_once()
open_port.assert_called_once()
class TestRabbitMQClusterContext(unittest.TestCase):
@mock.patch("rabbitmq_context.config")
def test_context_ssl_off(self, config):
config.return_value = "ignore"
self.assertEqual(
rabbitmq_context.RabbitMQClusterContext().__call__(), {
'cluster_partition_handling': "ignore"
})
config.assert_called_once_with("cluster-partition-handling")

View File

@ -37,7 +37,7 @@ class RelationUtil(TestCase):
@patch('rabbitmq_server_relations.relation_set')
@patch('apt_pkg.Cache')
@patch('rabbitmq_server_relations.is_clustered')
@patch('rabbitmq_server_relations.configure_client_ssl')
@patch('rabbitmq_server_relations.ssl_utils.configure_client_ssl')
@patch('rabbitmq_server_relations.unit_get')
@patch('rabbitmq_server_relations.relation_get')
@patch('rabbitmq_server_relations.is_elected_leader')
@ -87,7 +87,7 @@ class RelationUtil(TestCase):
@patch('rabbitmq_server_relations.relation_set')
@patch('apt_pkg.Cache')
@patch('rabbitmq_server_relations.is_clustered')
@patch('rabbitmq_server_relations.configure_client_ssl')
@patch('rabbitmq_server_relations.ssl_utils.configure_client_ssl')
@patch('rabbitmq_server_relations.unit_get')
@patch('rabbitmq_server_relations.relation_get')
@patch('rabbitmq_server_relations.is_elected_leader')