Enable Database TLS
Bring the charms.openstack Database handling into line with classic charms and charm-helpers. * Write out the SSL key and certs from relation data * Point sqlalchamy at the files Change-Id: I78218aa972ead49f144bb19a988bd6f0bbf4a539
This commit is contained in:
parent
d0431be73d
commit
2ed7b212ef
@ -15,8 +15,10 @@
|
|||||||
"""Adapter classes and utilities for use with Reactive interfaces"""
|
"""Adapter classes and utilities for use with Reactive interfaces"""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import base64
|
||||||
import collections
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ import charmhelpers.core.host as ch_host
|
|||||||
import charms_openstack.ip as os_ip
|
import charms_openstack.ip as os_ip
|
||||||
|
|
||||||
ADDRESS_TYPES = sorted(os_ip.ADDRESS_MAP.keys(), reverse=True)
|
ADDRESS_TYPES = sorted(os_ip.ADDRESS_MAP.keys(), reverse=True)
|
||||||
|
CA_CERTS_DIR = "/usr/local/share/ca-certificates"
|
||||||
|
|
||||||
# handle declarative adapter properties using a decorator and simple functions
|
# handle declarative adapter properties using a decorator and simple functions
|
||||||
|
|
||||||
@ -94,7 +97,8 @@ class OpenStackRelationAdapter(object):
|
|||||||
The generic type of the interface the adapter is wrapping.
|
The generic type of the interface the adapter is wrapping.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, relation=None, accessors=None, relation_name=None):
|
def __init__(self, relation=None, accessors=None, relation_name=None,
|
||||||
|
charm_instance=None):
|
||||||
"""Class will usually be initialised using the 'relation' option to
|
"""Class will usually be initialised using the 'relation' option to
|
||||||
pass in an instance of a interface class. If there is no relation
|
pass in an instance of a interface class. If there is no relation
|
||||||
class yet available then 'relation_name' can be used instead.
|
class yet available then 'relation_name' can be used instead.
|
||||||
@ -102,6 +106,7 @@ class OpenStackRelationAdapter(object):
|
|||||||
:param relation: Instance of an interface class
|
:param relation: Instance of an interface class
|
||||||
:param accessors: List of accessible interfaces properties
|
:param accessors: List of accessible interfaces properties
|
||||||
:param relation_name: String name of relation
|
:param relation_name: String name of relation
|
||||||
|
:param charm_instance: Instantiation of charm class
|
||||||
"""
|
"""
|
||||||
self.relation = relation
|
self.relation = relation
|
||||||
if relation and relation_name:
|
if relation and relation_name:
|
||||||
@ -111,6 +116,7 @@ class OpenStackRelationAdapter(object):
|
|||||||
self._setup_properties()
|
self._setup_properties()
|
||||||
else:
|
else:
|
||||||
self._relation_name = relation_name
|
self._relation_name = relation_name
|
||||||
|
self.charm_instance = charm_instance
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relation_name(self):
|
def relation_name(self):
|
||||||
@ -443,13 +449,14 @@ class DatabaseRelationAdapter(OpenStackRelationAdapter):
|
|||||||
|
|
||||||
interface_type = "database"
|
interface_type = "database"
|
||||||
|
|
||||||
def __init__(self, relation):
|
def __init__(self, relation, ssl_dir=CA_CERTS_DIR, charm_instance=None):
|
||||||
# Note: These accessors need closer inspection and potentially need
|
# Note: These accessors need closer inspection and potentially need
|
||||||
# to be removed. The actual interface implements them as methods with
|
# to be removed. The actual interface implements them as methods with
|
||||||
# parameters. See bug: https://bugs.launchpad.net/bugs/1848216.
|
# parameters. See bug: https://bugs.launchpad.net/bugs/1848216.
|
||||||
add_accessors = ['password', 'username', 'database']
|
add_accessors = ['password', 'username', 'database']
|
||||||
super(DatabaseRelationAdapter, self).__init__(relation, add_accessors)
|
super(DatabaseRelationAdapter, self).__init__(
|
||||||
self.config = hookenv.config()
|
relation, add_accessors, charm_instance=charm_instance)
|
||||||
|
self.set_ssl_dir(ssl_dir)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
@ -469,18 +476,104 @@ class DatabaseRelationAdapter(OpenStackRelationAdapter):
|
|||||||
def type(self):
|
def type(self):
|
||||||
return 'mysql'
|
return 'mysql'
|
||||||
|
|
||||||
|
def set_ssl_dir(self, ssl_dir):
|
||||||
|
"""
|
||||||
|
Set the SSL dir to a non-default location. It may be desireble to
|
||||||
|
continue using:
|
||||||
|
/etc/apache2/ssl/<charm name>
|
||||||
|
|
||||||
|
:param ssl_dir: Directory to write out certificates/keys
|
||||||
|
:type ssl_dir: string
|
||||||
|
:returns: None
|
||||||
|
"rtype: None
|
||||||
|
"""
|
||||||
|
self.ssl_dir = ssl_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_ssl_ca(self):
|
||||||
|
"""
|
||||||
|
Database SSL Certificate Authority
|
||||||
|
|
||||||
|
Write out the CA to disk if it is on the relation.
|
||||||
|
|
||||||
|
:returns: Path to SSL Certificate Authority
|
||||||
|
:rtype: Union[string, None]
|
||||||
|
"""
|
||||||
|
if self.relation.ssl_ca():
|
||||||
|
ca_path = os.path.join(self.ssl_dir, 'db-client.ca')
|
||||||
|
# Note: self.charm_instance.group is used to set permissions.
|
||||||
|
# If for some reason the charm has not set the group it defaults
|
||||||
|
# to 'root' group ownership which may not be correct.
|
||||||
|
ch_host.write_file(
|
||||||
|
path=ca_path,
|
||||||
|
content=base64.b64decode(self.relation.ssl_ca()),
|
||||||
|
group=self.charm_instance.group,
|
||||||
|
perms=0o644)
|
||||||
|
|
||||||
|
return ca_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_ssl_cert(self):
|
||||||
|
"""
|
||||||
|
Database SSL Certificate
|
||||||
|
|
||||||
|
Write out the certificate to disk if it is on the relation.
|
||||||
|
|
||||||
|
:returns: Path to SSL Certificate
|
||||||
|
:rtype: Union[string, None]
|
||||||
|
"""
|
||||||
|
if self.relation.ssl_cert():
|
||||||
|
cert_path = os.path.join(self.ssl_dir, 'db-client.cert')
|
||||||
|
# Note: self.charm_instance.group is used to set permissions.
|
||||||
|
# If for some reason the charm has not set the group it defaults
|
||||||
|
# to 'root' group ownership which may not be correct.
|
||||||
|
ch_host.write_file(
|
||||||
|
path=cert_path,
|
||||||
|
content=base64.b64decode(self.relation.ssl_cert()),
|
||||||
|
group=self.charm_instance.group,
|
||||||
|
perms=0o644)
|
||||||
|
|
||||||
|
return cert_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_ssl_key(self):
|
||||||
|
"""
|
||||||
|
Database SSL Key
|
||||||
|
|
||||||
|
Write out the key to disk if it is on the relation.
|
||||||
|
|
||||||
|
:returns: Path to SSL Key
|
||||||
|
:rtype: Union[string, None]
|
||||||
|
"""
|
||||||
|
if self.relation.ssl_key():
|
||||||
|
key_path = os.path.join(self.ssl_dir, 'db-client.key')
|
||||||
|
# Note: self.charm_instance.group is used to set permissions.
|
||||||
|
# If for some reason the charm has not set the group it defaults
|
||||||
|
# to 'root' group ownership which may not be correct.
|
||||||
|
ch_host.write_file(
|
||||||
|
path=key_path,
|
||||||
|
content=base64.b64decode(self.relation.ssl_key()),
|
||||||
|
group=self.charm_instance.group,
|
||||||
|
perms=0o640)
|
||||||
|
|
||||||
|
return key_path
|
||||||
|
|
||||||
def get_password(self, prefix=None):
|
def get_password(self, prefix=None):
|
||||||
if prefix:
|
if prefix:
|
||||||
return self.relation.password(prefix=prefix)
|
return self.relation.password(prefix=prefix)
|
||||||
return self.password
|
return self.password
|
||||||
|
|
||||||
def get_uri(self, prefix=None):
|
@property
|
||||||
|
def driver(self):
|
||||||
driver = 'mysql'
|
driver = 'mysql'
|
||||||
release = ch_utils.get_os_codename_install_source(
|
release = ch_utils.get_os_codename_install_source(
|
||||||
self.config['openstack-origin'])
|
self.charm_instance.options.openstack_origin)
|
||||||
if (ch_utils.OPENSTACK_RELEASES.index(release) >=
|
if (ch_utils.OPENSTACK_RELEASES.index(release) >=
|
||||||
ch_utils.OPENSTACK_RELEASES.index('stein')):
|
ch_utils.OPENSTACK_RELEASES.index('stein')):
|
||||||
driver = 'mysql+pymysql'
|
driver = 'mysql+pymysql'
|
||||||
|
return driver
|
||||||
|
|
||||||
|
def get_uri(self, prefix=None):
|
||||||
if prefix:
|
if prefix:
|
||||||
username = self.relation.username(prefix=prefix)
|
username = self.relation.username(prefix=prefix)
|
||||||
password = self.relation.password(prefix=prefix)
|
password = self.relation.password(prefix=prefix)
|
||||||
@ -491,7 +584,7 @@ class DatabaseRelationAdapter(OpenStackRelationAdapter):
|
|||||||
database = self.database
|
database = self.database
|
||||||
if self.port:
|
if self.port:
|
||||||
uri = '{}://{}:{}@{}:{}/{}'.format(
|
uri = '{}://{}:{}@{}:{}/{}'.format(
|
||||||
driver,
|
self.driver,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
self.host,
|
self.host,
|
||||||
@ -501,21 +594,20 @@ class DatabaseRelationAdapter(OpenStackRelationAdapter):
|
|||||||
else:
|
else:
|
||||||
# defensive code if port is not passed
|
# defensive code if port is not passed
|
||||||
uri = '{}://{}:{}@{}/{}'.format(
|
uri = '{}://{}:{}@{}/{}'.format(
|
||||||
driver,
|
self.driver,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
self.host,
|
self.host,
|
||||||
database,
|
database,
|
||||||
)
|
)
|
||||||
try:
|
if self.database_ssl_ca:
|
||||||
if self.ssl_ca:
|
uri = '{}?ssl_ca={}'.format(uri, self.database_ssl_ca)
|
||||||
uri = '{}?ssl_ca={}'.format(uri, self.ssl_ca)
|
if self.database_ssl_cert:
|
||||||
if self.ssl_cert:
|
uri = ('{}&ssl_cert={}&ssl_key={}'
|
||||||
uri = ('{}&ssl_cert={}&ssl_key={}'
|
.format(
|
||||||
.format(uri, self.ssl_cert, self.ssl_key))
|
uri,
|
||||||
except AttributeError:
|
self.database_ssl_cert,
|
||||||
# ignore ssl_ca or ssl_cert if not available
|
self.database_ssl_key))
|
||||||
pass
|
|
||||||
return uri
|
return uri
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1203,9 +1295,13 @@ class OpenStackRelationAdapters(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
relation_name = relation.relation_name.replace('-', '_')
|
relation_name = relation.relation_name.replace('-', '_')
|
||||||
try:
|
try:
|
||||||
adapter = self._adapters[relation_name](relation)
|
cls = self._adapters[relation_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
adapter = OpenStackRelationAdapter(relation)
|
cls = OpenStackRelationAdapter
|
||||||
|
try:
|
||||||
|
adapter = cls(relation, charm_instance=self.charm_instance)
|
||||||
|
except TypeError:
|
||||||
|
adapter = cls(relation)
|
||||||
return relation_name, adapter
|
return relation_name, adapter
|
||||||
|
|
||||||
|
|
||||||
|
@ -385,12 +385,38 @@ class FakeDatabaseRelation():
|
|||||||
def database(self, prefix=''):
|
def database(self, prefix=''):
|
||||||
return 'database1{}'.format(prefix)
|
return 'database1{}'.format(prefix)
|
||||||
|
|
||||||
|
def ssl_ca(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ssl_cert(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ssl_key(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCharmInstance():
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.group = "group"
|
||||||
|
self.options = mock.MagicMock()
|
||||||
|
self.options.openstack_origin = "cloud:bionic-rocky"
|
||||||
|
|
||||||
|
|
||||||
class SSLDatabaseRelationAdapter(adapters.DatabaseRelationAdapter):
|
class SSLDatabaseRelationAdapter(adapters.DatabaseRelationAdapter):
|
||||||
|
|
||||||
ssl_ca = 'my-ca'
|
def __init__(self, relation, ssl_dir=None, charm_instance=None):
|
||||||
ssl_cert = 'my-cert'
|
relation.ssl_ca = lambda: (
|
||||||
ssl_key = 'my-key'
|
adapters.base64.b64encode('my-ca'.encode('UTF-8')))
|
||||||
|
relation.ssl_cert = lambda: (
|
||||||
|
adapters.base64.b64encode('my-cert'.encode('UTF-8')))
|
||||||
|
relation.ssl_key = lambda: (
|
||||||
|
adapters.base64.b64encode('my-key'.encode('UTF-8')))
|
||||||
|
if ssl_dir:
|
||||||
|
super().__init__(
|
||||||
|
relation, ssl_dir=ssl_dir, charm_instance=charm_instance)
|
||||||
|
else:
|
||||||
|
super().__init__(relation, charm_instance=charm_instance)
|
||||||
|
|
||||||
|
|
||||||
class TestDatabaseRelationAdapter(unittest.TestCase):
|
class TestDatabaseRelationAdapter(unittest.TestCase):
|
||||||
@ -400,7 +426,8 @@ class TestDatabaseRelationAdapter(unittest.TestCase):
|
|||||||
'get_os_codename_install_source',
|
'get_os_codename_install_source',
|
||||||
return_value='rocky'):
|
return_value='rocky'):
|
||||||
fake = FakeDatabaseRelation()
|
fake = FakeDatabaseRelation()
|
||||||
db = adapters.DatabaseRelationAdapter(fake)
|
db = adapters.DatabaseRelationAdapter(
|
||||||
|
fake, charm_instance=FakeCharmInstance())
|
||||||
self.assertEqual(db.host, 'host1')
|
self.assertEqual(db.host, 'host1')
|
||||||
self.assertEqual(db.port, 3306)
|
self.assertEqual(db.port, 3306)
|
||||||
self.assertEqual(db.type, 'mysql')
|
self.assertEqual(db.type, 'mysql')
|
||||||
@ -417,17 +444,21 @@ class TestDatabaseRelationAdapter(unittest.TestCase):
|
|||||||
db.get_password('x'),
|
db.get_password('x'),
|
||||||
'password1x')
|
'password1x')
|
||||||
# test the ssl feature of the base class
|
# test the ssl feature of the base class
|
||||||
db = SSLDatabaseRelationAdapter(fake)
|
db = SSLDatabaseRelationAdapter(
|
||||||
|
fake, charm_instance=FakeCharmInstance())
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
db.uri,
|
db.uri,
|
||||||
'mysql://username1:password1@host1:3306/database1'
|
'mysql://username1:password1@host1:3306/database1'
|
||||||
'?ssl_ca=my-ca'
|
'?ssl_ca=/usr/local/share/ca-certificates/db-client.ca'
|
||||||
'&ssl_cert=my-cert&ssl_key=my-key')
|
'&ssl_cert=/usr/local/share/ca-certificates/db-client.cert'
|
||||||
|
'&ssl_key=/usr/local/share/ca-certificates/db-client.key')
|
||||||
with mock.patch.object(adapters.ch_utils,
|
with mock.patch.object(adapters.ch_utils,
|
||||||
'get_os_codename_install_source',
|
'get_os_codename_install_source',
|
||||||
return_value='stein'):
|
return_value='stein'):
|
||||||
|
ssl_dir = '/ssl/path'
|
||||||
fake = FakeDatabaseRelation()
|
fake = FakeDatabaseRelation()
|
||||||
db = adapters.DatabaseRelationAdapter(fake)
|
db = adapters.DatabaseRelationAdapter(
|
||||||
|
fake, charm_instance=FakeCharmInstance())
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
db.uri,
|
db.uri,
|
||||||
'mysql+pymysql://username1:password1@host1:3306/database1')
|
'mysql+pymysql://username1:password1@host1:3306/database1')
|
||||||
@ -438,12 +469,14 @@ class TestDatabaseRelationAdapter(unittest.TestCase):
|
|||||||
db.get_password('x'),
|
db.get_password('x'),
|
||||||
'password1x')
|
'password1x')
|
||||||
# test the ssl feature of the base class
|
# test the ssl feature of the base class
|
||||||
db = SSLDatabaseRelationAdapter(fake)
|
db = SSLDatabaseRelationAdapter(
|
||||||
|
fake, ssl_dir=ssl_dir, charm_instance=FakeCharmInstance())
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
db.uri,
|
db.uri,
|
||||||
'mysql+pymysql://username1:password1@host1:3306/database1'
|
'mysql+pymysql://username1:password1@host1:3306/database1'
|
||||||
'?ssl_ca=my-ca'
|
'?ssl_ca={ssl_dir}/db-client.ca'
|
||||||
'&ssl_cert=my-cert&ssl_key=my-key')
|
'&ssl_cert={ssl_dir}/db-client.cert'
|
||||||
|
'&ssl_key={ssl_dir}/db-client.key'.format(ssl_dir=ssl_dir))
|
||||||
|
|
||||||
|
|
||||||
class TestConfigurationAdapter(unittest.TestCase):
|
class TestConfigurationAdapter(unittest.TestCase):
|
||||||
|
Loading…
Reference in New Issue
Block a user