# Copyright 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import grp import os import pwd import re import sys import ssl_utils from charmhelpers.contrib.ssl.service import ServiceCA from charmhelpers.core.host import is_container from charmhelpers.fetch import apt_install from charmhelpers.core.hookenv import ( open_port, close_port, config, log, service_name, relation_ids, DEBUG, WARNING, ERROR, ) # python-six in ensured by charmhelpers import so we put this here. import six try: import psutil except ImportError: if six.PY2: apt_install('python-psutil', fatal=True) else: apt_install('python3-psutil', fatal=True) 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" RABBITMQ_CTL = '/usr/sbin/rabbitmqctl' ENV_CONF = '/etc/rabbitmq/rabbitmq-env.conf' # Rabbimq docs recommend min. 12 threads per core (see LP: #1693561) # NOTE(hopem): these defaults give us roughly the same as the default shipped # with the version of rabbitmq in ubuntu xenial (3.5.7) - see # https://tinyurl.com/rabbitmq-3-5-7 for exact value. Note that # this default has increased with newer versions so we should # track this and keep the charm up-to-date. DEFAULT_MULTIPLIER = 24 MAX_DEFAULT_THREADS = DEFAULT_MULTIPLIER * 2 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): if not os.path.exists(RABBITMQ_CTL): log('Deferring SSL configuration, RabbitMQ not yet installed') return {} 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) 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_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): ctxt = {'cluster_partition_handling': config('cluster-partition-handling')} if config('connection-backlog'): ctxt['connection_backlog'] = config('connection-backlog') return ctxt class RabbitMQEnvContext(object): def calculate_threads(self): """ Determine the number of erl vm threads in pool based in cpu resources available. Number of threads will be limited to MAX_DEFAULT_WORKERS in container environments where no worker-multipler configuration option been set. @returns int: number of io threads to allocate """ try: num_cpus = psutil.cpu_count() except AttributeError: num_cpus = psutil.NUM_CPUS multiplier = (config('erl-vm-io-thread-multiplier') or DEFAULT_MULTIPLIER) log("Calculating erl vm io thread pool size based on num_cpus={} and " "multiplier={}".format(num_cpus, multiplier), DEBUG) count = int(num_cpus * multiplier) if multiplier > 0 and count == 0: count = 1 if config('erl-vm-io-thread-multiplier') is None and is_container(): # NOTE(hopem): Limit unconfigured erl-vm-io-thread-multiplier # to MAX_DEFAULT_THREADS to avoid insane pool # configuration in LXD containers on large servers. count = min(count, MAX_DEFAULT_THREADS) log("erl vm io thread pool size = {} (capped={})" .format(count, is_container()), DEBUG) return count def __call__(self): """Write rabbitmq-env.conf according to charm config. We never overwrite RABBITMQ_NODENAME to ensure that we don't break clustered rabbitmq. """ blacklist = ['RABBITMQ_NODENAME'] context = {'settings': {}} key = 'RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS' context['settings'][key] = "'+A {}'".format(self.calculate_threads()) if config('prefer-ipv6'): key = 'RABBITMQ_SERVER_START_ARGS' context['settings'][key] = "'-proto_dist inet6_tcp'" # TODO: this is legacy HA and should be removed since it is now # deprecated. if relation_ids('ha'): if not config('ha-vip-only'): # TODO: do we need to remove this setting if it already exists # and the above is false? context['settings']['RABBITMQ_NODENAME'] = \ '{}@localhost'.format(service_name()) if os.path.exists(ENV_CONF): for line in open(ENV_CONF).readlines(): if re.search('^\s*#', line) or not line.strip('\n'): # ignore commented or blank lines continue _line = line.partition("=") key = _line[0].strip() val = _line[2].strip() if _line[1] != "=": log("Unable to parse line '{}' from {}".format(line, ENV_CONF), WARNING) continue if key in blacklist: # Keep original log("Leaving {} setting untouched".format(key), DEBUG) context['settings'][key] = val return context