From 0f07f772c62cc26e51af8ec8594e5afbf72a6f57 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 4 Jul 2016 10:23:45 +0000 Subject: [PATCH] Rebase to follow existing Openstack charm style --- charm/lib/charm/openstack/designate_bind.py | 421 ++++++++++++++++++ ...ate-bind.py => designate_bind_handlers.py} | 0 2 files changed, 421 insertions(+) create mode 100644 charm/lib/charm/openstack/designate_bind.py rename charm/reactive/{designate-bind.py => designate_bind_handlers.py} (100%) diff --git a/charm/lib/charm/openstack/designate_bind.py b/charm/lib/charm/openstack/designate_bind.py new file mode 100644 index 0000000..1cace26 --- /dev/null +++ b/charm/lib/charm/openstack/designate_bind.py @@ -0,0 +1,421 @@ +import glob +import os +import time +import subprocess +import hmac +import hashlib +import base64 + +import charms_openstack.charm as openstack_charm +import charms_openstack.adapters as adapters +import charms.reactive as reactive +import charmhelpers.core.decorators as ch_decorators +import charmhelpers.core.hookenv as hookenv +import charmhelpers.core.host as host + + +LEADERDB_SECRET_KEY = 'rndc_key' +LEADERDB_SYNC_SRC_KEY = 'sync_src' +LEADERDB_SYNC_TIME_KEY = 'sync_time' +CLUSTER_SYNC_KEY = 'sync_request' +WWW_DIR = '/var/www/html' +ZONE_DIR = '/var/cache/bind/' + + +def install(): + """Use the singleton from the DesignateBindCharm to install the packages + on the unit + + :returns: None + """ + DesignateBindCharm.singleton.install() + + +def set_apparmor(): + """Use the singleton from the DesignateBindCharm to setup apparmor + + :returns: None + """ + DesignateBindCharm.singleton.set_apparmor() + + +def init_rndckey(): + """Use the singleton from the DesignateBindCharm to initalise the rndc key + if possible and not already done + + :returns: str or None. Secret if available, None if not. + """ + return DesignateBindCharm.singleton.init_rndckey() + + +def get_rndc_secret(): + """Use the singleton from the DesignateBindCharm to retrieve the RNDC + secret + + :returns: str or None. Secret if available, None if not. + """ + return DesignateBindCharm.singleton.get_rndc_secret() + + +def get_rndc_algorithm(): + """Use the singleton from the DesignateBindCharm to retrieve the RNDC + algorithm + + :returns: str or None. Algorithm if available, None if not. + """ + return DesignateBindCharm.singleton.get_rndc_algorithm() + + +def get_sync_time(): + """Use the singleton from the DesignateBindCharm to retrieve the time of + the published zone zync target + + :returns: str or None. Current sync target creation time if available, None + if not. + """ + return DesignateBindCharm.singleton.get_sync_time() + + +def setup_sync(): + """Use the singleton from the DesignateBindCharm to create a zone sync + target + + :returns: None + """ + DesignateBindCharm.singleton.setup_sync() + + +def retrieve_zones(): + """Use the singleton from the DesignateBindCharm to retrieve the zone + information and install it + + :returns: None + """ + DesignateBindCharm.singleton.retrieve_zones() + + +def request_sync(hacluster): + """Use the singleton from the DesignateBindCharm to request the leader + creates a sync target + + :param hacluster: OpenstackHAPeers() interface class + :returns: None + """ + DesignateBindCharm.singleton.request_sync(hacluster) + + +def process_requests(hacluster): + """Use the singleton from the DesignateBindCharm setup a sync target if + requested + + :returns: None + """ + DesignateBindCharm.singleton.process_requests(hacluster) + + +def render_all_configs(interfaces_list): + """Use the singleton from the DesignateBindCharm to render configurations + and restart services as needed + + :param interfaces_list: List of instances of interface classes. + :returns: None + """ + DesignateBindCharm.singleton.render_with_interfaces(interfaces_list) + + +class DNSAdapter(adapters.OpenStackRelationAdapter): + + def __init__(self, relation): + super(DNSAdapter, self).__init__(relation) + + @property + def control_listen_ip(self): + """IP local rndc service listens on + + :returns: str: IP local rndc listens on + """ + return hookenv.unit_private_ip() + + @property + def control_ips(self): + """Comma delimited list of rndc client IPs + + :returns: str: Comma delimited list of rndc client IPs + """ + return ';'.join(self.relation.client_ips()) + + @property + def algorithm(self): + """Algorithm used to encode rndc secret + + :returns: str: Algorithm used to encode rndc secret + """ + return DesignateBindCharm.get_rndc_algorithm() + + @property + def secret(self): + """RNDC Secret + + :returns: str: rndc secret + """ + return DesignateBindCharm.get_rndc_secret() + + +class BindAdapters(adapters.OpenStackRelationAdapters): + """ + Adapters class for the DesignateBind charm. + """ + relation_adapters = { + 'dns_backend': DNSAdapter, + } + + def __init__(self, relations): + super(BindAdapters, self).__init__( + relations) + + +class DesignateBindCharm(openstack_charm.OpenStackCharm): + + name = 'designate_bind' + packages = ['bind9', 'apache2'] + + services = ['bind9'] + + required_relations = ['dns-backend'] + + restart_map = { + '/etc/bind/named.conf.options': services, + '/etc/bind/named.conf': services, + '/etc/bind/rndc.key': services, + } + service_type = 'designate_bind' + default_service = 'bind9' + adapters_class = BindAdapters + release = 'icehouse' + + def __init__(self, release=None, **kwargs): + super(DesignateBindCharm, self).__init__(release='icehouse', **kwargs) + + @staticmethod + def get_rndc_algorithm(): + """Algorithm used to encode rndc secret + + :returns: str: Algorithm used to encode rndc secret + """ + return 'hmac-md5' + + @staticmethod + def get_rndc_secret(): + """rndc secret + + :returns: str: rndc secret + """ + return hookenv.leader_get(attribute=LEADERDB_SECRET_KEY) + + @staticmethod + def get_sync_src(): + """URL published zone file can be retrieved from + + :returns: str: URL published zone file can be retrieved from + """ + return hookenv.leader_get(attribute=LEADERDB_SYNC_SRC_KEY) + + @staticmethod + def get_sync_time(): + """Epoch seconds when published sync was created + + :returns: str: Epoch seconds when published sync was created + """ + return hookenv.leader_get(attribute=LEADERDB_SYNC_TIME_KEY) + + def process_requests(self, hacluster): + """Check for sync requests and respond + + This should only be called by an application leader. + Check to see if a peer has requested a sync. If so check if the time + the request was created is more recent that then published sync target. + If so, setup a new sync target. When the target is setup the leader db + is updated with the new sync request time and URL. this will trigger a + leader-*changed hook on the requesting unit allowing that unit to pick + up the new file. + + :param hacluster: OpenstackHAPeers() interface class + :returns: None + """ + hookenv.log('Processing sync requests', level=hookenv.DEBUG) + sync_requests = hacluster.retrieve_remote(CLUSTER_SYNC_KEY) + max_time = 0 + for req in sync_requests: + if float(req) > max_time: + max_time = float(req) + hookenv.log('Newest sync request: {}'.format(max_time), + level=hookenv.DEBUG) + if max_time > float(self.get_sync_time()): + self.setup_sync() + + def set_sync_info(self, sync_time, sync_file): + """Update leader DB with sync information + + :param sync_time: str Time sync was created in epoch seconds + :param sync_file: str Local file containing zone information + :returns: None + """ + sync_info = { + LEADERDB_SYNC_SRC_KEY: 'http://{}:80/zone-syncs/{}'.format( + hookenv.unit_private_ip(), sync_file), + LEADERDB_SYNC_TIME_KEY: sync_time, + } + hookenv.leader_set(sync_info) + + def generate_rndc_key(self): + """Generate a RNDC key + + :returns: str Base64 encoded hmac-md5 digest + """ + key = os.urandom(10) + dig = hmac.new(key, msg=b'RNDC Secret', digestmod=hashlib.md5).digest() + return base64.b64encode(dig).decode() + + def init_rndckey(self): + """Create a RNDC key if needed + + Return the rndc key from the leader DB or if one is not present + generate a new one. + + :returns: str: rndc key + """ + secret = DesignateBindCharm.get_rndc_secret() + hookenv.log('Retrieving secret', level=hookenv.DEBUG) + if not secret: + hookenv.log('Secret not found in leader db', level=hookenv.DEBUG) + if hookenv.is_leader(): + hookenv.log('Creating new secret as leader', + level=hookenv.DEBUG) + secret = self.generate_rndc_key(self) + hookenv.leader_set({LEADERDB_SECRET_KEY: secret}) + return secret + + def create_zone_tarball(self, tarfile): + """Create a tar ball of zone files + + :param tarfile: str Location of tar ball to be created. + :returns: None + """ + zone_files = [] + for re in ['juju*', 'slave*', '*nzf']: + for _file in glob.glob('{}/{}'.format(ZONE_DIR, re)): + zone_files.append(os.path.basename(_file)) + cmd = ['tar', 'zcvf', tarfile] + cmd.extend(zone_files) + subprocess.check_call(cmd, cwd=ZONE_DIR) + + def setup_sync(self): + """Setup a sync target + + Stop bind and tar up zone files, and start bind. Then update leaderdb + with details of new sync. + + :returns: None + """ + hookenv.log('Setting up zone info for collection', level=hookenv.DEBUG) + sync_time = str(time.time()) + sync_dir = '{}/zone-syncs'.format(WWW_DIR, sync_time) + try: + os.mkdir(sync_dir, 0o755) + except os.FileExistsError: + os.chmod(sync_dir, 0o755) + unit_name = hookenv.local_unit().replace('/', '_') + touch_file = '{}/juju-zone-src-{}'.format(ZONE_DIR, unit_name) + open(touch_file, 'w+').close() + # FIXME Try freezing DNS rather than stopping bind + self.service_control('stop', ['bind9']) + tar_file = '{}/{}.tar.gz'.format(sync_dir, sync_time) + self.create_zone_tarball(tar_file) + self.service_control('start', ['bind9']) + self.set_sync_info(sync_time, '{}.tar.gz'.format(sync_time)) + + def service_control(self, cmd, services): + """Control listed services + + :param cmd: str Action to take on service (stop, start, restart) + :returns: None + """ + + cmds = { + 'stop': host.service_stop, + 'start': host.service_start, + 'restart': host.service_restart, + } + for service in self.services: + cmds[cmd](service) + + def request_sync(self, hacluster): + """Request peer sets up a sync target + + Send a request via the cluster relation asking for a sync target to be + setup. + + :param hacluster: OpenstackHAPeers() interface class + :returns: None + """ + request_time = str(time.time()) + hacluster.send_all({CLUSTER_SYNC_KEY: request_time}, store_local=True) + reactive.set_state('sync.request.sent') + + @ch_decorators.retry_on_exception(3, base_delay=2, + exc_type=subprocess.CalledProcessError) + def wget_file(self, url, target_dir): + """Retireve file from url into target_dir + + :param url: str Retrieve file from this url + :param target_dir: Place file in this directory + :returns: None + """ + cmd = ['wget', url, '--retry-connrefused', '-t', '10'] + subprocess.check_call(cmd, cwd=target_dir) + + def retrieve_zones(self, cluster_relation=None): + """Retrieve and install zones file + + Check if published sync target was created after this units sync + request was sent, if it was install the zones file. Alternatively if + no peer relation was set then assume the current sync target is to be + used regardless of when it was created. + + :param cluster_relation: OpenstackHAPeers() interface class + :returns: None + """ + + if cluster_relation: + request_time = cluster_relation.retrieve_local(CLUSTER_SYNC_KEY) + sync_time = DesignateBindCharm.get_sync_time() + if request_time and request_time > sync_time: + hookenv.log(('Request for sync sent but remote sync time is too' + ' old, defering until a more up-to-date target is ' + 'available'), + level=hookenv.WARNING) + else: + self.service_control('stop') + url = DesignateBindCharm.get_sync_src() + self.wget_file(url, ZONE_DIR) + tar_file = url.split('/')[-1] + subprocess.check_call(['tar', 'xf', tar_file], cwd=ZONE_DIR) + os.remove('{}/{}'.format(ZONE_DIR, tar_file)) + self.service_control('start') + reactive.remove_state('sync.request.sent') + reactive.set_state('zones.initialised') + + def set_apparmor(self): + """Disbale apparmor for named + + This is currently specified in the designate documentation + http://docs.openstack.org/developer/designate/getting-started.html + + TODO: Check this is *really* needed + + :returns: None + """ + apparmor_file = '/etc/apparmor.d/disable/usr.sbin.named' + if not os.path.isfile(apparmor_file): + open(apparmor_file, 'w').close() + host.service_reload('apparmor') diff --git a/charm/reactive/designate-bind.py b/charm/reactive/designate_bind_handlers.py similarity index 100% rename from charm/reactive/designate-bind.py rename to charm/reactive/designate_bind_handlers.py