from charms import layer from charmhelpers.core.hookenv import unit_get from charmhelpers.core.hookenv import config from charmhelpers.core.hookenv import is_leader from charmhelpers.core.hookenv import leader_get, leader_set from charmhelpers.core import unitdata from charms.reactive import is_state from etcd_lib import get_ingress_address from etcd_lib import get_bind_address import string import random import os class EtcdDatabag: ''' This class represents a configuration object to ease configuration of an etcd unit during deployment and reconfiguration. The full dict of data when expanded looks like the following: {'public_address': '127.0.0.1', 'cluster_bind_address': '127.0.0.1', 'db_bind_address': '127.0.0.1', 'cluster_address': '127.0.0.1', 'db_address': '127.0.0.1', 'unit_name': 'etcd0', 'port': '2380', 'management_port': '2379', 'ca_certificate': '/etc/ssl/etcd/ca.crt', 'server_certificate': '/etc/ssl/etcd/server.crt', 'server_key': '/etc/ssl/etcd/server.key', 'token': '8XG27B', 'cluster_state': 'existing'} ''' def __init__(self): self.db = unitdata.kv() self.cluster_bind_address = self.get_bind_address('cluster') self.db_bind_address = self.get_bind_address('db') self.port = config('port') self.management_port = config('management_port') # Live polled properties self.public_address = unit_get('public-address') self.cluster_address = get_ingress_address('cluster') self.db_address = get_ingress_address('db') self.unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '') # Pull the TLS certificate paths from layer data tls_opts = layer.options('tls-client') ca_path = tls_opts['ca_certificate_path'] crt_path = tls_opts['server_certificate_path'] key_path = tls_opts['server_key_path'] # Pull the static etcd configuration from layer-data etcd_opts = layer.options('etcd') self.etcd_conf_dir = etcd_opts['etcd_conf_dir'] # This getter determines the current context of the storage path # depending on if durable storage is mounted. self.etcd_data_dir = self.storage_path() self.etcd_daemon = etcd_opts['etcd_daemon_process'] self.ca_certificate = ca_path self.server_certificate = crt_path self.server_key = key_path # Cluster concerns self.cluster = self.db.get('etcd.cluster', '') self.token = self.cluster_token() self.cluster_state = self.db.get('etcd.cluster-state', 'existing') def set_cluster(self, value): ''' Set the cluster string for peer registration ''' self.cluster = value self.db.set('etcd.cluster', value) def set_cluster_state(self, value): ''' Set the cluster state ''' self.cluster_state = value self.db.set('etcd.cluster-state', value) def cluster_token(self): ''' Getter to return the unique cluster token. ''' token = leader_get('token') if not token and is_leader(): token = self.id_generator() leader_set({'token': token}) return token def id_generator(self, size=6): ''' Return a random 6 character string for use in cluster init. @params size - The size of the string to return in characters ''' chars = string.ascii_uppercase + string.digits return ''.join(random.choice(chars) for _ in range(size)) def storage_path(self): ''' Storage mounts are limited in snap confinement. Default behavior is to version the database files in $SNAP_DATA. However the user can attach durable storage, which is mounted in /media. We need a common method to determine which storage path we are concerned with ''' etcd_opts = layer.options('etcd') if is_state('data.volume.attached'): return "/media/etcd/data" else: return etcd_opts['etcd_data_dir'] def get_bind_address(self, endpoint_name): ''' Returns the address that the service binds to. If the config parameter 'bind_to_all_interfaces' is set to true, it returns 0.0.0.0 If 'bind_to_all_interfaces' is set to false, it returns the bind address of the endpoint_name received as parameter @param endpoint_name name the endpoint from where the bind address is obtained ''' if bool(config('bind_to_all_interfaces')): return '0.0.0.0' return get_bind_address(endpoint_name)