Files
charm-interface-mysql-shared/requires.py
David Ames b2ed9aa73c Enable MySQL TLS
Enable passing the certificate authority on the relation in order to
enable TLS communication to the MySQL DB.

In order to enable MySQL to use TLS for client connections all that
is required is the CA certificate. A certificate and key may be used
for client certificate authentication. However, since almost all
deployments will not use certificate authentication, focusing on the CA as SSL
data complete makes sense.

Change-Id: I785afe7f64cb57caa857178d529e3cabdcf63517
2020-06-30 15:24:52 -07:00

214 lines
7.0 KiB
Python

from charmhelpers.core import hookenv
from charms.reactive import RelationBase
from charms.reactive import hook
from charms.reactive import scopes
class MySQLSharedRequires(RelationBase):
scope = scopes.GLOBAL
# These remote data fields will be automatically mapped to accessors
# with a basic documentation string provided.
auto_accessors = ['access-network', 'db_host', 'db_port',
'ssl_ca', 'ssl_cert', 'ssl_key',
'cluster-series-upgrading', 'wait_timeout']
@hook('{requires:mysql-shared}-relation-joined')
def joined(self):
self.set_state('{relation_name}.connected')
@hook('{requires:mysql-shared}-relation-changed')
def changed(self):
if self.cluster_series_upgrading() == 'True':
self.remove_state('{relation_name}.available')
self.remove_state('{relation_name}.available.access_network')
self.remove_state('{relation_name}.available.ssl')
else:
if self.base_data_complete() and self.unit_allowed_all_dbs():
self.set_state('{relation_name}.available')
if self.access_network_data_complete():
self.set_state('{relation_name}.available.access_network')
if self.ssl_data_complete():
self.set_state('{relation_name}.available.ssl')
@hook('{requires:mysql-shared}-relation-{broken,departed}')
def departed(self):
# Clear state
self.remove_state('{relation_name}.connected')
self.remove_state('{relation_name}.available')
self.remove_state('{relation_name}.available.access_network')
self.remove_state('{relation_name}.available.ssl')
# Check if this is the last unit
for conversation in self.conversations():
for rel_id in conversation.relation_ids:
if len(hookenv.related_units(rel_id)) > 0:
# This is not the last unit so reevaluate state
self.joined()
self.changed()
def configure(self, database, username, hostname=None, prefix=None):
"""
Called by charm layer that uses this interface to configure a database.
"""
if not hostname:
conversation = self.conversation()
try:
hostname = hookenv.network_get_primary_address(
conversation.relation_name
)
except NotImplementedError:
hostname = hookenv.unit_private_ip()
if prefix:
relation_info = {
prefix + '_database': database,
prefix + '_username': username,
prefix + '_hostname': hostname,
}
self.set_prefix(prefix)
else:
relation_info = {
'database': database,
'username': username,
'hostname': hostname,
}
self.set_remote(**relation_info)
self.set_local(**relation_info)
def set_prefix(self, prefix):
"""
Store all of the database prefixes in a list.
"""
prefixes = self.get_local('prefixes')
if prefixes:
if prefix not in prefixes:
self.set_local('prefixes', prefixes + [prefix])
else:
self.set_local('prefixes', [prefix])
def get_prefixes(self):
"""
Return the list of saved prefixes.
"""
return self.get_local('prefixes')
def database(self, prefix=None):
"""
Return a configured database name.
"""
if prefix:
return self.get_local(prefix + '_database')
return self.get_local('database')
def username(self, prefix=None):
"""
Return a configured username.
"""
if prefix:
return self.get_local(prefix + '_username')
return self.get_local('username')
def hostname(self, prefix=None):
"""
Return a configured hostname.
"""
if prefix:
return self.get_local(prefix + '_hostname')
return self.get_local('hostname')
def password(self, prefix=None):
"""
Return a database password.
"""
if prefix:
return self.get_remote(prefix + '_password')
return self.get_remote('password')
def allowed_units(self, prefix=None):
"""
Return a database's allowed_units.
"""
if prefix:
return self.get_remote(prefix + '_allowed_units')
return self.get_remote('allowed_units')
def base_data_complete(self):
"""
Check if required base data is complete.
"""
data = {
'db_host': self.db_host(),
}
if self.get_prefixes():
suffixes = ['_password', '_allowed_units']
for prefix in self.get_prefixes():
for suffix in suffixes:
key = prefix + suffix
data[key] = self.get_remote(key)
else:
data['password'] = self.get_remote('password')
data['allowed_units'] = self.get_remote('allowed_units')
if all(data.values()):
return True
return False
def unit_allowed_db(self, prefix=None):
""""
Check unit can access requested database.
:param prefix: Prefix used to distinguish multiple db requests.
:type prefix: str
:returns: Whether db acl has been setup.
:rtype: bool
"""
allowed = False
allowed_units = self.allowed_units(prefix=prefix) or ''
hookenv.log("Checking {} is in {}".format(
hookenv.local_unit(),
allowed_units.split()))
if allowed_units and hookenv.local_unit() in allowed_units.split():
allowed = True
hookenv.log("Unit allowed: {}".format(allowed))
return allowed
def unit_allowed_all_dbs(self):
""""
Check unit can access all requested databases.
:returns: Whether db acl has been setup for all dbs.
:rtype: bool
"""
if self.get_prefixes():
_allowed = [self.unit_allowed_db(prefix=p)
for p in self.get_prefixes()]
else:
_allowed = [self.unit_allowed_db()]
hookenv.log("Allowed: {}".format(_allowed))
if all(_allowed):
hookenv.log("Returning unit_allowed_all_dbs True")
return True
hookenv.log("Returning unit_allowed_all_dbs False")
return False
def access_network_data_complete(self):
"""
Check if optional access network data provided by mysql is complete.
"""
data = {
'access-network': self.access_network(),
}
if all(data.values()):
return True
return False
def ssl_data_complete(self):
"""
Check if optional ssl data provided by mysql is complete.
"""
data = {
'ssl_ca': self.ssl_ca(),
}
if all(data.values()):
return True
return False