Rebase to follow existing Openstack charm style
This commit is contained in:
parent
1cab4616c3
commit
0f07f772c6
|
@ -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')
|
Loading…
Reference in New Issue