diff --git a/hooks/rabbitmq_context.py b/hooks/rabbitmq_context.py index 858e9c97..9b687a86 100644 --- a/hooks/rabbitmq_context.py +++ b/hooks/rabbitmq_context.py @@ -48,9 +48,9 @@ except ImportError: import psutil -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" +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" RABBITMQ_CTL = '/usr/sbin/rabbitmqctl' ENV_CONF = '/etc/rabbitmq/rabbitmq-env.conf' @@ -90,9 +90,9 @@ class RabbitMQSSLContext(object): 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)): + (ssl_key, SSL_KEY_FILE), + (ssl_cert, SSL_CERT_FILE), + (ssl_ca, SSL_CA_FILE)): if not contents: continue @@ -100,20 +100,26 @@ class RabbitMQSSLContext(object): with open(path, 'w') as fh: fh.write(contents) - os.chmod(path, 0o640) + if path == SSL_CA_FILE: + # the CA can be world readable and it will allow clients to + # verify the certificate offered by rabbit. + os.chmod(path, 0o644) + else: + 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_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 + data["ssl_ca_file"] = SSL_CA_FILE return data diff --git a/hooks/rabbitmq_server_relations.py b/hooks/rabbitmq_server_relations.py index 0cd29c45..ce8cf72f 100755 --- a/hooks/rabbitmq_server_relations.py +++ b/hooks/rabbitmq_server_relations.py @@ -42,6 +42,7 @@ except ImportError: import rabbit_utils as rabbit import ssl_utils +from rabbitmq_context import SSL_CA_FILE from lib.utils import ( chown, chmod, @@ -634,12 +635,33 @@ def update_nrpe_checks(): rabbit.grant_permissions(user, vhost) nrpe_compat = nrpe.NRPE(hostname=hostname) - nrpe_compat.add_check( - shortname=rabbit.RABBIT_USER, - description='Check RabbitMQ {%s}' % myunit, - check_cmd='{}/check_rabbitmq.py --user {} --password {} --vhost {}' - ''.format(NAGIOS_PLUGINS, user, password, vhost) - ) + if config('ssl') in ['off', 'on']: + cmd = ('{plugins_dir}/check_rabbitmq.py --user {user} ' + '--password {password} --vhost {vhost}') + cmd = cmd.format(plugins_dir=NAGIOS_PLUGINS, user=user, + password=password, vhost=vhost) + nrpe_compat.add_check( + shortname=rabbit.RABBIT_USER, + description='Check RabbitMQ {%s}' % myunit, + check_cmd=cmd + ) + if config('ssl') in ['only', 'on']: + log('Adding rabbitmq SSL check', level=DEBUG) + cmd = ('{plugins_dir}/check_rabbitmq.py --user {user} ' + '--password {password} --vhost {vhost} ' + '--ssl --ssl-ca {ssl_ca} --port {port}') + cmd = cmd.format(plugins_dir=NAGIOS_PLUGINS, + user=user, + password=password, + port=int(config('ssl_port')), + vhost=vhost, + ssl_ca=SSL_CA_FILE) + nrpe_compat.add_check( + shortname=rabbit.RABBIT_USER + "_ssl", + description='Check RabbitMQ (SSL) {%s}' % myunit, + check_cmd=cmd + ) + if config('queue_thresholds'): cmd = "" # If value of queue_thresholds is incorrect we want the hook to fail @@ -726,7 +748,17 @@ def config_changed(): if rabbit.archive_upgrade_available(): rabbit.install_or_upgrade_packages() - open_port(5672) + if config('ssl') == 'off': + open_port(5672) + close_port(int(config('ssl_port'))) + elif config('ssl') == 'on': + open_port(5672) + open_port(int(config('ssl_port'))) + elif config('ssl') == 'only': + close_port(5672) + open_port(int(config('ssl_port'))) + else: + log("Unknown ssl config value: '%s'" % config('ssl'), level=ERROR) chown(RABBIT_DIR, rabbit.RABBIT_USER, rabbit.RABBIT_USER) chmod(RABBIT_DIR, 0o775) diff --git a/scripts/check_rabbitmq.py b/scripts/check_rabbitmq.py index 44d74ae0..73f44d4c 100755 --- a/scripts/check_rabbitmq.py +++ b/scripts/check_rabbitmq.py @@ -38,14 +38,18 @@ def alarm_handler(signum, frame): os._exit(1) -def get_connection(host_port, user, password, vhost): +def get_connection(host_port, user, password, vhost, ssl, ssl_ca): """ connect to the amqp service """ if options.verbose: print "Connection to %s requested" % host_port try: - ret = amqp.Connection(host=host_port, userid=user, - password=password, virtual_host=vhost, - insist=False) + params = {'host': host_port, 'userid': user, 'password': password, + 'virtual_host': vhost, 'insist': False} + if ssl: + params['ssl'] = {'ca_certs': ssl_ca} + + ret = amqp.Connection(**params) + except (socket.error, TypeError), e: print "ERROR: Could not connect to RabbitMQ server %s:%d" % ( options.host, options.port) @@ -53,9 +57,9 @@ def get_connection(host_port, user, password, vhost): print e raise sys.exit(2) - except: - print "ERROR: Unknown error connecting to RabbitMQ server %s:%d" % ( - options.host, options.port) + except Exception as ex: + print("ERROR: Unknown error connecting to RabbitMQ server %s:%d: %s" + % (options.host, options.port, ex)) if options.verbose: raise sys.exit(3) @@ -174,12 +178,12 @@ def main_loop(conn, exname): return consumer.loop(timeout=options.timeout) -def main(host, port, exname, extype, user, password, vhost): +def main(host, port, exname, extype, user, password, vhost, ssl, ssl_ca): """ setup the connection and the communication channel """ sys.stdout = os.fdopen(os.dup(1), "w", 0) host_port = "%s:%s" % (host, port) - conn = get_connection(host_port, user, password, vhost) - chan = conn.channel() + conn = get_connection(host_port, user, password, vhost, ssl, ssl_ca) + if setup_exchange(conn, exname, extype): if options.verbose: print "Created %s exchange of type %s" % (exname, extype) @@ -187,8 +191,13 @@ def main(host, port, exname, extype, user, password, vhost): if options.verbose: print "Reusing existing exchange %s of type %s" % (exname, extype) ret = main_loop(conn, exname) - chan.close() - conn.close() + + try: + conn.close() + except socket.error: + # when using SSL socket.shutdown() fails inside amqplib. + pass + return ret if __name__ == '__main__': @@ -222,6 +231,10 @@ if __name__ == '__main__': parser.add_option("--vhost", dest="vhost", default="/", help="RabbitMQ vhost [default=%default]", metavar="VHOST") + parser.add_option("--ssl", dest="ssl", default=False, action="store_true", + help="Connect using SSL") + parser.add_option("--ssl-ca", metavar="FILE", dest="ssl_ca", + help="SSL CA certificate path") (options, args) = parser.parse_args() if options.verbose: @@ -229,7 +242,8 @@ if __name__ == '__main__': Using AMQP setup: host:port=%s:%d exchange_name=%s exchange_type=%s """ % (options.host, options.port, options.exchange, options.type) ret = main(options.host, options.port, options.exchange, options.type, - options.user, options.password, options.vhost) + options.user, options.password, options.vhost, options.ssl, + options.ssl_ca) if ret: print "Ok: sent and received %d test messages" % options.messages sys.exit(0) diff --git a/unit_tests/test_rabbitmq_server_relations.py b/unit_tests/test_rabbitmq_server_relations.py index 22d62fad..6b1f9ccb 100644 --- a/unit_tests/test_rabbitmq_server_relations.py +++ b/unit_tests/test_rabbitmq_server_relations.py @@ -14,6 +14,7 @@ import os import shutil +import subprocess import sys import tempfile @@ -34,6 +35,7 @@ with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec: mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f: lambda *args, **kwargs: f(*args, **kwargs)) import rabbitmq_server_relations + import rabbit_utils TO_PATCH = [ # charmhelpers.core.hookenv @@ -48,6 +50,11 @@ class RelationUtil(CharmTestCase): self.fake_repo = {} super(RelationUtil, self).setUp(rabbitmq_server_relations, TO_PATCH) + self.tmp_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + super(RelationUtil, self).tearDown() @patch('rabbitmq_server_relations.rabbit.leader_node_is_ready') @patch('rabbitmq_server_relations.peer_store_and_set') @@ -278,3 +285,71 @@ class RelationUtil(CharmTestCase): finally: if os.path.exists(tmpdir): shutil.rmtree(tmpdir) + + @patch('rabbitmq_server_relations.local_unit') + @patch('charmhelpers.contrib.charmsupport.nrpe.NRPE.add_check') + @patch('subprocess.check_call') + @patch('rabbit_utils.get_rabbit_password_on_disk') + @patch('charmhelpers.contrib.charmsupport.nrpe.relation_ids') + @patch('charmhelpers.contrib.charmsupport.nrpe.config') + @patch('charmhelpers.contrib.charmsupport.nrpe.get_nagios_unit_name') + @patch('charmhelpers.contrib.charmsupport.nrpe.get_nagios_hostname') + @patch('os.fchown') + @patch('rabbitmq_server_relations.charm_dir') + @patch('subprocess.check_output') + @patch('rabbitmq_server_relations.config') + def test_update_nrpe_checks(self, mock_config, mock_check_output, + mock_charm_dir, mock_fchown, + mock_get_nagios_hostname, + mock_get_nagios_unit_name, mock_config2, + mock_nrpe_relation_ids, + mock_get_rabbit_password_on_disk, + mock_check_call, mock_add_check, + mock_local_unit): + + self.test_config.set('ssl', 'on') + + mock_charm_dir.side_effect = lambda: self.tmp_dir + mock_config.side_effect = self.test_config + mock_config2.side_effect = self.test_config + rabbitmq_server_relations.STATS_CRONFILE = os.path.join( + self.tmp_dir, "rabbitmq-stats") + mock_get_nagios_hostname.return_value = "foo-0" + mock_get_nagios_unit_name.return_value = "bar-0" + mock_get_rabbit_password_on_disk.return_value = "qwerty" + mock_nrpe_relation_ids.side_effect = lambda x: [ + 'nrpe-external-master:1'] + mock_local_unit.return_value = 'unit/0' + + rabbitmq_server_relations.update_nrpe_checks() + + mock_check_output.assert_any_call( + ['/usr/bin/rsync', '-r', '--delete', '--executability', + '%s/scripts/collect_rabbitmq_stats.sh' % self.tmp_dir, + '/usr/local/bin/collect_rabbitmq_stats.sh'], + stderr=subprocess.STDOUT) + + # regular check on 5672 + cmd = ('{plugins_dir}/check_rabbitmq.py --user {user} ' + '--password {password} --vhost {vhost}').format( + plugins_dir=rabbitmq_server_relations.NAGIOS_PLUGINS, + user='nagios-unit-0', vhost='nagios-unit-0', + password='qwerty') + + mock_add_check.assert_any_call( + shortname=rabbit_utils.RABBIT_USER, + description='Check RabbitMQ {%s}' % 'bar-0', check_cmd=cmd) + + # check on ssl port 5671 + cmd = ('{plugins_dir}/check_rabbitmq.py --user {user} ' + '--password {password} --vhost {vhost} ' + '--ssl --ssl-ca {ssl_ca} --port {port}').format( + plugins_dir=rabbitmq_server_relations.NAGIOS_PLUGINS, + user='nagios-unit-0', + password='qwerty', + port=int(self.test_config['ssl_port']), + vhost='nagios-unit-0', + ssl_ca=rabbitmq_server_relations.SSL_CA_FILE) + mock_add_check.assert_any_call( + shortname=rabbit_utils.RABBIT_USER + "_ssl", + description='Check RabbitMQ (SSL) {%s}' % 'bar-0', check_cmd=cmd) diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py index c06a73c5..cebe7a33 100644 --- a/unit_tests/test_utils.py +++ b/unit_tests/test_utils.py @@ -117,6 +117,15 @@ class TestConfig(object): def __getitem__(self, key): return self.get(key) + def __contains__(self, key): + return key in self.config + + def __call__(self, key=None): + if key: + return self.get(key) + else: + return self + class TestRelation(object):