Merge pull request #14 from gnuoy/feature/ssl-support

Enable SSL support for layered charms
This commit is contained in:
James Page 2016-06-27 11:58:42 +01:00 committed by GitHub
commit 4d705f7334
7 changed files with 798 additions and 88 deletions

View File

@ -6,8 +6,9 @@ import charmhelpers.contrib.hahelpers.cluster as ch_cluster
import charmhelpers.contrib.network.ip as ch_ip
import charmhelpers.contrib.openstack.utils as ch_utils
import charmhelpers.core.hookenv as hookenv
import charms_openstack.ip as os_ip
ADDRESS_TYPES = ['admin', 'internal', 'public']
ADDRESS_TYPES = os_ip.ADDRESS_MAP.keys()
class OpenStackRelationAdapter(object):
@ -29,10 +30,10 @@ class OpenStackRelationAdapter(object):
:param accessors: List of accessible interfaces properties
:param relation_name: String name of relation
"""
self.relation = relation
if relation and relation_name:
raise ValueError('Cannot speciiy relation and relation_name')
if relation:
self.relation = relation
self.accessors = accessors or []
self._setup_properties()
else:
@ -164,6 +165,9 @@ class PeerHARelationAdapter(OpenStackRelationAdapter):
relation_info = {
'cluster_hosts': self.local_default_addresses(),
}
net_split = self.local_network_split_addresses()
for key in net_split.keys():
relation_info['cluster_hosts'][key] = net_split[key]
except IndexError:
pass
return relation_info
@ -189,7 +193,7 @@ class PeerHARelationAdapter(OpenStackRelationAdapter):
config = hookenv.config()
_cluster_hosts = {}
for addr_type in ADDRESS_TYPES:
cfg_opt = 'os-{}-network'.format(addr_type)
cfg_opt = os_ip.ADDRESS_MAP[addr_type]['config']
laddr = ch_ip.get_address_in_network(config.get(cfg_opt))
if laddr:
netmask = ch_ip.get_netmask_for_address(laddr)
@ -222,14 +226,16 @@ class PeerHARelationAdapter(OpenStackRelationAdapter):
@return None
"""
for addr_type in ADDRESS_TYPES:
cfg_opt = 'os-{}-network'.format(addr_type)
cfg_opt = os_ip.ADDRESS_MAP[addr_type]['config']
laddr = ch_ip.get_address_in_network(self.config.get(cfg_opt))
if laddr:
self.cluster_hosts[laddr] = \
self.local_network_split_addresses()[laddr]
key = '{}-address'.format(addr_type)
key = '{}-address'.format(
os_ip.ADDRESS_MAP[addr_type]['binding'])
for _unit, _laddr in self.relation.ip_map(address_key=key):
self.cluster_hosts[laddr]['backends'][_unit] = _laddr
if _laddr:
self.cluster_hosts[laddr]['backends'][_unit] = _laddr
def add_default_addresses(self):
"""Populate cluster_hosts with private-address of this unit and its
@ -313,7 +319,7 @@ class APIConfigurationAdapter(ConfigurationAdapter):
"""This configuration adapter extends the base class and adds properties
common accross most OpenstackAPI services"""
def __init__(self, port_map=None):
def __init__(self, port_map=None, service_name=None):
"""
:param port_map: Map containing service names and the ports used e.g.
port_map = {
@ -328,9 +334,24 @@ class APIConfigurationAdapter(ConfigurationAdapter):
'internal': 9002,
},
}
:param service_name: Name of service being deployed
"""
super(APIConfigurationAdapter, self).__init__()
self.port_map = port_map
self.service_name = service_name
self.network_addresses = self.get_network_addresses()
@property
def external_ports(self):
"""Return ports the service will be accessed on
@return set of ports service can be accessed on
"""
ext_ports = set()
for svc in self.port_map.keys():
for net_type in self.port_map[svc].keys():
ext_ports.add(self.port_map[svc][net_type])
return ext_ports
@property
def ipv6_mode(self):
@ -408,6 +429,34 @@ class APIConfigurationAdapter(ConfigurationAdapter):
singlenode_mode=True)]
return service_ports
@property
def apache_enabled(self):
"""Whether apache is being used for this service
@return True if apache2 os being used for this service
"""
return charms.reactive.bus.get_state('ssl.enabled')
def determine_service_port(self, port):
"""Calculate port service should use given external port
Haproxy fronts connections for a service and may pass connections to
Apache for SSL termination. Is Apache is being used:
Haproxy listens on N
Apache listens on N-10
Service listens on N-20
else
Haproxy listens on N
Service listens on N-10
:param int port: port service uses for external connections
@return int port: port backend service should use
"""
i = 10
if self.apache_enabled:
i = 20
return (port - i)
@property
def service_listen_info(self):
"""Dict of service names and attributes for backend to listen on
@ -427,15 +476,15 @@ class APIConfigurationAdapter(ConfigurationAdapter):
"""
info = {}
ip = self.local_host if self.apache_enabled else self.local_address
if self.port_map:
for service in self.port_map.keys():
key = service.replace('-', '_')
info[key] = {
'proto': 'http',
'ip': self.local_address,
'port': ch_cluster.determine_apache_port(
self.port_map[service]['admin'],
singlenode_mode=True)}
'ip': ip,
'port': self.determine_service_port(
self.port_map[service]['admin'])}
info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key])
return info
@ -459,16 +508,81 @@ class APIConfigurationAdapter(ConfigurationAdapter):
"""
info = {}
ip = getattr(self, 'vip', self.local_address)
proto = 'https' if self.apache_enabled else 'http'
if self.port_map:
for service in self.port_map.keys():
key = service.replace('-', '_')
info[key] = {
'proto': 'http',
'proto': proto,
'ip': ip,
'port': self.port_map[service]['admin']}
info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key])
return info
def get_network_addresses(self):
"""For each network configured, return corresponding address and vip
(if available).
Returns a list of tuples of the form:
[(address_in_net_a, vip_in_net_a),
(address_in_net_b, vip_in_net_b),
...]
or, if no vip(s) available:
[(address_in_net_a, address_in_net_a),
(address_in_net_b, address_in_net_b),
...]
"""
addresses = []
for net_type in ADDRESS_TYPES:
net_cfg_opt = os_ip.ADDRESS_MAP[net_type]['config'].replace('-',
'_')
config_cidr = getattr(self, net_cfg_opt, None)
addr = ch_ip.get_address_in_network(
config_cidr,
hookenv.unit_get('private-address'))
addresses.append(
(addr, os_ip.resolve_address(endpoint_type=net_type)))
return sorted(addresses)
@property
def endpoints(self):
"""List of endpoint information.
Endpoint information used to configure apache
Client -> endpoint -> address:ext_port -> local:int_port
NOTE: endpoint map be a vi
returns [
(address1, endpoint1, ext_port1, int_port1),
(address2, endpoint2, ext_port2, int_port2)
...
]
"""
endpoints = []
for address, endpoint in sorted(set(self.network_addresses)):
for api_port in self.external_ports:
ext_port = ch_cluster.determine_apache_port(
api_port,
singlenode_mode=True)
int_port = ch_cluster.determine_api_port(
api_port,
singlenode_mode=True)
portmap = (address, endpoint, int(ext_port), int(int_port))
endpoints.append(portmap)
return endpoints
@property
def ext_ports(self):
""" List of endpoint ports
@returns List of ports
"""
eps = [ep[2] for ep in self.endpoints]
return sorted(list(set(eps)))
class OpenStackRelationAdapters(object):
"""

View File

@ -4,6 +4,7 @@
# need/want absolute imports for the package imports to work properly
from __future__ import absolute_import
import base64
import os
import random
import string
@ -60,6 +61,7 @@ KNOWN_RELEASES = [
VIP_KEY = "vip"
CIDR_KEY = "vip_cidr"
IFACE_KEY = "vip_iface"
APACHE_SSL_VHOST = '/etc/apache2/sites-available/openstack_https_frontend.conf'
def get_charm_instance(release=None, *args, **kwargs):
@ -320,6 +322,10 @@ class OpenStackCharm(object):
"""proxy for charms.reactive.bus.remove_state()"""
charms.reactive.bus.remove_state(state)
def get_state(self, state):
"""proxy for charms.reactive.bus.get_state()"""
return charms.reactive.bus.get_state(state)
def api_port(self, service, endpoint_type=os_ip.PUBLIC):
"""Return the API port for a particular endpoint type from the
self.api_ports{}.
@ -629,6 +635,34 @@ class HAOpenStackCharm(OpenStackCharm):
def __init__(self, **kwargs):
super(HAOpenStackCharm, self).__init__(**kwargs)
self.set_haproxy_stat_password()
self.set_config_defined_certs_and_keys()
@property
def apache_vhost_file(self):
"""Apache vhost for SSL termination
:returns: string
"""
return APACHE_SSL_VHOST
def enable_apache_ssl_vhost(self):
"""Enable Apache vhost for SSL termination
Enable Apache vhost for SSL termination if vhost exists and it is not
curently enabled
"""
if os.path.exists(self.apache_vhost_file):
check_enabled = subprocess.call(
['a2query', '-s', 'openstack_https_frontend'])
if check_enabled != 0:
subprocess.check_call(['a2ensite', 'openstack_https_frontend'])
ch_host.service_reload('apache2', restart_on_failure=True)
def configure_apache(self):
if self.apache_enabled():
self.install()
self.enable_apache_modules()
self.enable_apache_ssl_vhost()
@property
def all_packages(self):
@ -637,8 +671,10 @@ class HAOpenStackCharm(OpenStackCharm):
@return ['pkg1', 'pkg2', ...]
"""
_packages = self.packages[:]
if self.enable_haproxy():
if self.haproxy_enabled():
_packages.append('haproxy')
if self.apache_enabled():
_packages.append('apache2')
return _packages
@property
@ -652,11 +688,19 @@ class HAOpenStackCharm(OpenStackCharm):
}
"""
_restart_map = self.restart_map.copy()
if self.enable_haproxy():
if self.haproxy_enabled():
_restart_map[self.HAPROXY_CONF] = ['haproxy']
if self.apache_enabled():
_restart_map[self.apache_vhost_file] = ['apache2']
return _restart_map
def enable_haproxy(self):
def apache_enabled(self):
"""Determine if apache is being used
@return True if apache is being used"""
return self.get_state('ssl.enabled')
def haproxy_enabled(self):
"""Determine if haproxy is fronting the services
@return True if haproxy is fronting the service"""
@ -699,8 +743,156 @@ class HAOpenStackCharm(OpenStackCharm):
def set_haproxy_stat_password(self):
"""Set a stats password for accessing haproxy statistics"""
if not charms.reactive.bus.get_state('haproxy.stat.password'):
if not self.get_state('haproxy.stat.password'):
password = ''.join([
random.choice(string.ascii_letters + string.digits)
for n in range(32)])
charms.reactive.bus.set_state('haproxy.stat.password', password)
self.set_state('haproxy.stat.password', password)
def enable_apache_modules(self):
"""Enable Apache modules needed for SSL termination"""
restart = False
for module in ['ssl', 'proxy', 'proxy_http']:
check_enabled = subprocess.call(['a2query', '-m', module])
if check_enabled != 0:
subprocess.check_call(['a2enmod', module])
restart = True
if restart:
ch_host.service_restart('apache2')
def configure_cert(self, cert, key, cn=None):
"""Configure service SSL cert and key
Write out service SSL certificate and key for Apache.
@param cert string SSL Certificate
@param key string SSL Key
@param cn string Canonical name for service
"""
if not cn:
cn = os_ip.resolve_address(endpoint_type=os_ip.INTERNAL)
ssl_dir = os.path.join('/etc/apache2/ssl/', self.name)
ch_host.mkdir(path=ssl_dir)
if cn:
cert_filename = 'cert_{}'.format(cn)
key_filename = 'key_{}'.format(cn)
else:
cert_filename = 'cert'
key_filename = 'key'
ch_host.write_file(path=os.path.join(ssl_dir, cert_filename),
content=cert.encode('utf-8'))
ch_host.write_file(path=os.path.join(ssl_dir, key_filename),
content=key.encode('utf-8'))
def get_local_addresses(self):
"""Return list of local addresses on each configured network
For each network return an address the local unit has on that network
if one exists.
@returns [private_addr, admin_addr, public_addr, ...]
"""
addresses = [
os_utils.get_host_ip(hookenv.unit_get('private-address'))]
for addr_type in os_ip.ADDRESS_MAP.keys():
laddr = os_ip.resolve_address(endpoint_type=addr_type)
if laddr:
addresses.append(laddr)
return sorted(list(set(addresses)))
def get_certs_and_keys(self, keystone_interface=None):
"""Collect SSL config for local endpoints
SSL keys and certs may come from user specified configuration for this
charm or they may come directly from Keystone.
If collecting from keystone there may be a certificate and key per
endpoint (public, admin etc).
@returns [
{'key': 'key1', 'cert': 'cert1', 'ca': 'ca1', 'cn': 'cn1'}
{'key': 'key2', 'cert': 'cert2', 'ca': 'ca2', 'cn': 'cn2'}
...
]
"""
if self.config_defined_ssl_key and self.config_defined_ssl_cert:
return [{
'key': self.config_defined_ssl_key.decode('utf-8'),
'cert': self.config_defined_ssl_cert.decode('utf-8'),
'ca': self.config_defined_ssl_ca.decode('utf-8'),
'cn': None}]
elif keystone_interface:
keys_and_certs = []
for addr in self.get_local_addresses():
key = keystone_interface.get_ssl_key(addr)
cert = keystone_interface.get_ssl_cert(addr)
ca = keystone_interface.get_ssl_ca()
if key and cert:
keys_and_certs.append({
'key': key,
'cert': cert,
'ca': ca,
'cn': addr})
return keys_and_certs
else:
return []
def set_config_defined_certs_and_keys(self):
"""Set class attributes for user defined ssl options
Inspect user defined SSL config and set
config_defined_{ssl_key, ssl_cert, ssl_ca}
"""
for ssl_param in ['ssl_key', 'ssl_cert', 'ssl_ca']:
key = 'config_defined_{}'.format(ssl_param)
if self.config.get(ssl_param):
setattr(self, key,
base64.b64decode(self.config.get(ssl_param)))
else:
setattr(self, key, None)
def configure_ssl(self, keystone_interface=None):
"""Configure SSL certificates and keys
@param keystone_interface KeystoneRequires class
"""
ssl_objects = self.get_certs_and_keys(
keystone_interface=keystone_interface)
if ssl_objects:
for ssl in ssl_objects:
self.configure_cert(ssl['cert'], ssl['key'], cn=ssl['cn'])
self.configure_ca(ssl['ca'], update_certs=False)
self.run_update_certs()
self.set_state('ssl.enabled', True)
self.configure_apache()
else:
self.set_state('ssl.enabled', False)
def configure_ca(self, ca_cert, update_certs=True):
"""Write Certificate Authority certificate"""
cert_file = \
'/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
if ca_cert:
with open(cert_file, 'w') as crt:
crt.write(ca_cert)
if update_certs:
self.run_update_certs()
def run_update_certs(self):
"""Update certifiacte
Run update-ca-certificates to update the directory /etc/ssl/certs to
hold SSL certificates and generates ca-certificates.crt, a concatenated
single-file list of certificates
"""
subprocess.check_call(['update-ca-certificates', '--fresh'])
def update_peers(self, cluster):
for addr_type in os_ip.ADDRESS_MAP.keys():
cidr = self.config.get(os_ip.ADDRESS_MAP[addr_type]['config'])
laddr = ch_ip.get_address_in_network(cidr)
if laddr:
cluster.set_address(
os_ip.ADDRESS_MAP[addr_type]['binding'],
laddr)

View File

@ -1,15 +1,17 @@
# need/want absolute imports for the package imports to work properly
from __future__ import absolute_import
import netaddr
import charmhelpers.core.hookenv as hookenv
import charmhelpers.contrib.network.ip as net_ip
import charmhelpers.contrib.hahelpers.cluster as cluster
import charms.reactive.bus
PUBLIC = 'public'
INTERNAL = 'int'
ADMIN = 'admin'
_ADDRESS_MAP = {
ADDRESS_MAP = {
PUBLIC: {
'binding': 'public',
'config': 'os-public-network',
@ -41,8 +43,8 @@ def canonical_url(endpoint_type=PUBLIC):
:returns str: Base URL for services on the current service unit.
"""
scheme = 'http'
# if 'https' in configs.complete_contexts():
# scheme = 'https'
if charms.reactive.bus.get_state('ssl.enabled'):
scheme = 'https'
address = resolve_address(endpoint_type)
if net_ip.is_ipv6(address):
address = "[{}]".format(address)
@ -61,7 +63,7 @@ def _get_address_override(endpoint_type=PUBLIC):
:returns: any endpoint address or hostname that the user has overridden
or None if an override is not present.
"""
override_key = _ADDRESS_MAP[endpoint_type]['override']
override_key = ADDRESS_MAP[endpoint_type]['override']
addr_override = hookenv.config(override_key)
if not addr_override:
return None
@ -69,6 +71,31 @@ def _get_address_override(endpoint_type=PUBLIC):
return addr_override.format(service_name=hookenv.service_name())
def _network_get_primary_address(binding):
"""Wrapper for hookenv.network_get_primary_address
hookenv.network_get_primary_address may return a string or bytes depending
on the version of python (Bug #1595418). When fix has landed in pypi
wrapper may be discarded"""
try:
address = hookenv.network_get_primary_address(binding).decode('utf-8')
except AttributeError:
address = hookenv.network_get_primary_address(binding)
return address
def _resolve_network_cidr(ip_address):
'''
Resolves the full address cidr of an ip_address based on
configured network interfaces
This is in charmhelpers trunk but not in pypi. Please revert to using
charmhelpers version when pypi has been updated
'''
netmask = net_ip.get_netmask_for_address(ip_address)
return str(netaddr.IPNetwork("%s/%s" % (ip_address, netmask)).cidr)
def resolve_address(endpoint_type=PUBLIC, override=True):
"""Return unit address depending on net config.
@ -91,10 +118,10 @@ def resolve_address(endpoint_type=PUBLIC, override=True):
if vips:
vips = vips.split()
net_type = _ADDRESS_MAP[endpoint_type]['config']
net_type = ADDRESS_MAP[endpoint_type]['config']
net_addr = hookenv.config(net_type)
net_fallback = _ADDRESS_MAP[endpoint_type]['fallback']
binding = _ADDRESS_MAP[endpoint_type]['binding']
net_fallback = ADDRESS_MAP[endpoint_type]['fallback']
binding = ADDRESS_MAP[endpoint_type]['binding']
clustered = cluster.is_clustered()
if clustered and vips:
@ -107,8 +134,8 @@ def resolve_address(endpoint_type=PUBLIC, override=True):
# NOTE: endeavour to check vips against network space
# bindings
try:
bound_cidr = net_ip.resolve_network_cidr(
hookenv.network_get_primary_address(binding)
bound_cidr = _resolve_network_cidr(
_network_get_primary_address(binding)
)
for vip in vips:
if net_ip.is_address_in_network(bound_cidr, vip):
@ -131,7 +158,7 @@ def resolve_address(endpoint_type=PUBLIC, override=True):
# NOTE: only try to use extra bindings if legacy network
# configuration is not in use
try:
resolved_address = hookenv.network_get_primary_address(binding)
resolved_address = _network_get_primary_address(binding)
except NotImplementedError:
resolved_address = fallback_addr

View File

@ -39,6 +39,10 @@ class TestOpenStackRelationAdapter(unittest.TestCase):
with self.assertRaises(AttributeError):
ad.relation_name = 'hello'
def test_class_no_relation(self):
ad = adapters.OpenStackRelationAdapter(relation_name='cluster')
self.assertEqual(ad.relation_name, 'cluster')
class FakeRabbitMQRelation():
@ -191,9 +195,9 @@ class TestPeerHARelationAdapter(unittest.TestCase):
new=lambda x: ['rid1']), \
mock.patch.object(adapters.hookenv, 'related_units',
new=lambda relid: []):
expect = {
'cluster_hosts': expect_local_default
}
expect = {'cluster_hosts': expect_local_ns}
expect['cluster_hosts']['this_unit_private_addr'] = \
expect_local_default['this_unit_private_addr']
peer_ra = adapters.PeerHARelationAdapter(FakePeerRelation())
self.assertEqual(peer_ra.single_mode_map, expect)
# Test single_mode_map when a cluster relation is not present
@ -267,6 +271,17 @@ class TestConfigurationAdapter(unittest.TestCase):
class TestAPIConfigurationAdapter(unittest.TestCase):
api_ports = {
'svc1': {
'admin': 9001,
'public': 9001,
'internal': 9001,
},
'svc2': {
'admin': 9002,
'public': 9002,
'internal': 9002,
}}
def test_class(self):
test_config = {
@ -274,15 +289,17 @@ class TestAPIConfigurationAdapter(unittest.TestCase):
'vip': '',
}
with mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
with mock.patch.object(adapters.hookenv, 'local_unit',
return_value='my-unit/0'):
c = adapters.APIConfigurationAdapter()
self.assertEqual(c.local_unit_name, 'my-unit-0')
self.assertEqual(c.haproxy_stat_port, '8888')
self.assertEqual(c.service_ports, {})
self.assertEqual(c.service_listen_info, {})
self.assertEqual(c.external_endpoints, {})
new=lambda: test_config), \
mock.patch.object(adapters.APIConfigurationAdapter,
'get_network_addresses'), \
mock.patch.object(adapters.hookenv, 'local_unit',
return_value='my-unit/0'):
c = adapters.APIConfigurationAdapter()
self.assertEqual(c.local_unit_name, 'my-unit-0')
self.assertEqual(c.haproxy_stat_port, '8888')
self.assertEqual(c.service_ports, {})
self.assertEqual(c.service_listen_info, {})
self.assertEqual(c.external_endpoints, {})
def test_ipv4_mode(self):
test_config = {
@ -294,12 +311,15 @@ class TestAPIConfigurationAdapter(unittest.TestCase):
mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config), \
mock.patch.object(adapters.hookenv, 'unit_get',
return_value='10.0.0.20'):
c = adapters.APIConfigurationAdapter()
return_value='10.0.0.20'), \
mock.patch.object(adapters.APIConfigurationAdapter,
'get_network_addresses'):
c = adapters.APIConfigurationAdapter(service_name='svc1')
self.assertFalse(c.ipv6_mode)
self.assertEqual(c.local_address, '10.0.0.10')
self.assertEqual(c.local_host, '127.0.0.1')
self.assertEqual(c.haproxy_host, '0.0.0.0')
self.assertEqual(c.service_name, 'svc1')
def test_ipv6_mode(self):
test_config = {
@ -307,14 +327,61 @@ class TestAPIConfigurationAdapter(unittest.TestCase):
'vip': '',
}
with mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
with mock.patch.object(adapters.ch_ip, 'get_ipv6_addr',
return_value=['fe80::f2de:f1ff:fedd:8dc7']):
c = adapters.APIConfigurationAdapter()
self.assertTrue(c.ipv6_mode)
self.assertEqual(c.local_address, 'fe80::f2de:f1ff:fedd:8dc7')
self.assertEqual(c.local_host, 'ip6-localhost')
self.assertEqual(c.haproxy_host, '::')
new=lambda: test_config), \
mock.patch.object(
adapters.ch_ip,
'get_ipv6_addr',
return_value=['fe80::f2de:f1ff:fedd:8dc7']), \
mock.patch.object(adapters.APIConfigurationAdapter,
'get_network_addresses'):
c = adapters.APIConfigurationAdapter()
self.assertTrue(c.ipv6_mode)
self.assertEqual(c.local_address, 'fe80::f2de:f1ff:fedd:8dc7')
self.assertEqual(c.local_host, 'ip6-localhost')
self.assertEqual(c.haproxy_host, '::')
def test_external_ports(self):
c = adapters.APIConfigurationAdapter(port_map=self.api_ports)
self.assertEqual(c.external_ports, {9001, 9002})
def test_get_network_addresses(self):
test_config = {
'prefer-ipv6': False,
'os-admin-network': 'admin_net',
'os-public-network': 'public_net',
'os-internal-network': 'internal_net',
}
test_networks = {
'admin_net': 'admin_addr',
'public_net': 'public_addr',
'internal_net': 'internal_addr',
}
resolved_addresses = {
'admin': 'admin_addr',
'public': 'public_addr',
'int': 'int_vip',
}
def _is_address_in_network(cidr, vip):
return cidr == vip.replace('vip_', '')
def _resolve_address(endpoint_type=None):
return resolved_addresses[endpoint_type]
with mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config), \
mock.patch.object(adapters.hookenv, 'unit_get',
return_value='thisunit'), \
mock.patch.object(adapters.os_ip, 'resolve_address',
new=_resolve_address), \
mock.patch.object(adapters.ch_ip, 'get_address_in_network',
new=lambda x, y: test_networks[x]):
c = adapters.APIConfigurationAdapter()
self.assertEqual(
c.get_network_addresses(), [
('admin_addr', 'admin_addr'),
('internal_addr', 'int_vip'),
('public_addr', 'public_addr')])
def test_port_maps(self):
class MockAddrAPIConfigurationAdapt(adapters.APIConfigurationAdapter):
@ -322,31 +389,28 @@ class TestAPIConfigurationAdapter(unittest.TestCase):
def local_address(self):
return '10.0.0.10'
api_ports = {
'svc1': {
'admin': 9001,
'public': 9001,
'internal': 9001,
},
'svc2': {
'admin': 9002,
'public': 9002,
'internal': 9002,
},
}
test_config = {
'prefer-ipv6': True,
'prefer-ipv6': False,
'vip': '10.10.10.10',
'private-address': 'privaddr',
}
def _determine_apache_port(port, singlenode_mode):
def _determine_apache_port(port, singlenode_mode=None):
return port - 10
with mock.patch.object(adapters.ch_cluster, 'determine_apache_port',
side_effect=_determine_apache_port):
with mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
c = MockAddrAPIConfigurationAdapt(port_map=api_ports)
side_effect=_determine_apache_port), \
mock.patch.object(adapters.APIConfigurationAdapter,
'determine_service_port',
side_effect=_determine_apache_port), \
mock.patch.object(adapters.APIConfigurationAdapter,
'get_network_addresses'), \
mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
with mock.patch.object(adapters.APIConfigurationAdapter,
'apache_enabled',
new=False):
c = MockAddrAPIConfigurationAdapt(port_map=self.api_ports)
self.assertEqual(
c.service_ports,
{'svc1': [9001, 8991], 'svc2': [9002, 8992]})
@ -374,6 +438,93 @@ class TestAPIConfigurationAdapter(unittest.TestCase):
'ip': '10.10.10.10',
'port': 9002,
'url': 'http://10.10.10.10:9002'}})
with mock.patch.object(adapters.APIConfigurationAdapter,
'apache_enabled',
new=True):
c = MockAddrAPIConfigurationAdapt(port_map=self.api_ports)
self.assertEqual(
c.service_ports,
{'svc1': [9001, 8991], 'svc2': [9002, 8992]})
self.assertEqual(
c.service_listen_info, {
'svc1': {
'proto': 'http',
'ip': '127.0.0.1',
'port': 8991,
'url': 'http://127.0.0.1:8991'},
'svc2': {
'proto': 'http',
'ip': '127.0.0.1',
'port': 8992,
'url': 'http://127.0.0.1:8992'}})
self.assertEqual(
c.external_endpoints, {
'svc1': {
'proto': 'https',
'ip': '10.10.10.10',
'port': 9001,
'url': 'https://10.10.10.10:9001'},
'svc2': {
'proto': 'https',
'ip': '10.10.10.10',
'port': 9002,
'url': 'https://10.10.10.10:9002'}})
def test_endpoints_and_ext_ports(self):
_net_addrs = [
('admin_addr', 'vip_admin_net'),
('internal_addr', 'vip_internal_net')]
with mock.patch.object(adapters.APIConfigurationAdapter,
'get_network_addresses',
return_value=_net_addrs), \
mock.patch.object(adapters.ch_cluster, 'determine_apache_port',
new=lambda x, singlenode_mode: x - 10), \
mock.patch.object(adapters.ch_cluster, 'determine_api_port',
new=lambda x, singlenode_mode: x - 20):
c = adapters.APIConfigurationAdapter(port_map=self.api_ports)
expect = [
('admin_addr', 'vip_admin_net', 8991, 8981),
('admin_addr', 'vip_admin_net', 8992, 8982),
('internal_addr', 'vip_internal_net', 8991, 8981),
('internal_addr', 'vip_internal_net', 8992, 8982)
]
self.assertEqual(c.endpoints, expect)
self.assertEqual(c.ext_ports, [8991, 8992])
def test_apache_enabled(self):
with mock.patch.object(adapters.charms.reactive.bus,
'get_state',
return_value=True):
c = adapters.APIConfigurationAdapter()
self.assertTrue(c.apache_enabled)
with mock.patch.object(adapters.charms.reactive.bus,
'get_state',
return_value=False):
c = adapters.APIConfigurationAdapter()
self.assertFalse(c.apache_enabled)
def test_determine_service_port(self):
with mock.patch.object(adapters.APIConfigurationAdapter,
'apache_enabled',
new=True):
c = adapters.APIConfigurationAdapter()
self.assertEqual(c.determine_service_port(80), 60)
with mock.patch.object(adapters.APIConfigurationAdapter,
'apache_enabled',
new=False):
c = adapters.APIConfigurationAdapter()
self.assertEqual(c.determine_service_port(80), 70)
class FakePeerHARelationAdapter(object):
def __init__(self, relation=None, relation_name=None):
pass
@property
def single_mode_map(self):
return {'cluster_hosts': {'my': 'map'}}
class TestOpenStackRelationAdapters(unittest.TestCase):
@ -387,8 +538,10 @@ class TestOpenStackRelationAdapters(unittest.TestCase):
'three': 3,
'that-one': 4
}
with mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
with mock.patch.object(adapters, 'PeerHARelationAdapter',
new=FakePeerHARelationAdapter), \
mock.patch.object(adapters.hookenv, 'config',
new=lambda: test_config):
amqp = FakeRabbitMQRelation()
shared_db = FakeDatabaseRelation()
mine = MyRelation()
@ -397,9 +550,10 @@ class TestOpenStackRelationAdapters(unittest.TestCase):
self.assertEqual(a.my_name.this, 'this')
items = list(a)
self.assertEqual(items[0][0], 'options')
self.assertEqual(items[1][0], 'amqp')
self.assertEqual(items[2][0], 'shared_db')
self.assertEqual(items[3][0], 'my_name')
self.assertEqual(items[1][0], 'cluster')
self.assertEqual(items[2][0], 'amqp')
self.assertEqual(items[3][0], 'shared_db')
self.assertEqual(items[4][0], 'my_name')
class MyRelationAdapter(adapters.OpenStackRelationAdapter):
@ -437,9 +591,8 @@ class TestCustomOpenStackRelationAdapters(unittest.TestCase):
mock.patch.object(adapters.hookenv,
'config',
new=lambda: test_config), \
mock.patch.object(adapters.PeerHARelationAdapter,
'local_default_addresses',
return_value={'my': 'map'}):
mock.patch.object(adapters, 'PeerHARelationAdapter',
new=FakePeerHARelationAdapter):
amqp = FakeRabbitMQRelation()
shared_db = FakeDatabaseRelation()
mine = MyRelation()

View File

@ -6,6 +6,7 @@
# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock()
from __future__ import absolute_import
import base64
import collections
import unittest
@ -308,9 +309,9 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest):
super(TestHAOpenStackCharm, self).setUp(chm.HAOpenStackCharm,
TEST_CONFIG)
def test_enable_haproxy(self):
def test_haproxy_enabled(self):
self.patch_target('ha_resources', new=['haproxy'])
self.assertTrue(self.target.enable_haproxy())
self.assertTrue(self.target.haproxy_enabled())
def test__init__(self):
# Note cls.setUpClass() creates an OpenStackCharm() instance
@ -385,17 +386,210 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest):
mock.ANY)
def test_hacharm_all_packages(self):
self.patch_target('enable_haproxy', return_value=True)
self.patch_target('haproxy_enabled', return_value=True)
self.assertTrue('haproxy' in self.target.all_packages)
self.patch_target('enable_haproxy', return_value=False)
self.patch_target('haproxy_enabled', return_value=False)
self.assertFalse('haproxy' in self.target.all_packages)
def test_hacharm_full_restart_map(self):
self.patch_target('enable_haproxy', return_value=True)
self.patch_target('haproxy_enabled', return_value=True)
self.assertTrue(
self.target.full_restart_map.get(
'/etc/haproxy/haproxy.cfg', False))
def test_enable_apache_ssl_vhost(self):
self.patch_object(chm.os.path, 'exists', return_value=True)
self.patch_object(chm.subprocess, 'call', return_value=1)
self.patch_object(chm.subprocess, 'check_call')
self.target.enable_apache_ssl_vhost()
self.check_call.assert_called_once_with(
['a2ensite', 'openstack_https_frontend'])
self.check_call.reset_mock()
self.patch_object(chm.subprocess, 'call', return_value=0)
self.target.enable_apache_ssl_vhost()
self.assertFalse(self.check_call.called)
def test_enable_apache_modules(self):
apache_mods = {
'ssl': 0,
'proxy': 0,
'proxy_http': 1}
self.patch_object(chm.ch_host, 'service_restart')
self.patch_object(chm.subprocess, 'check_call')
self.patch_object(
chm.subprocess, 'call',
new=lambda x: apache_mods[x.pop()])
self.target.enable_apache_modules()
self.check_call.assert_called_once_with(
['a2enmod', 'proxy_http'])
self.service_restart.assert_called_once_with('apache2')
def test_configure_cert(self):
self.patch_object(chm.ch_host, 'mkdir')
self.patch_object(chm.ch_host, 'write_file')
self.target.configure_cert('mycert', 'mykey', cn='mycn')
self.mkdir.assert_called_once_with(path='/etc/apache2/ssl/charmname')
calls = [
mock.call(
path='/etc/apache2/ssl/charmname/cert_mycn',
content=b'mycert'),
mock.call(
path='/etc/apache2/ssl/charmname/key_mycn',
content=b'mykey')]
self.write_file.assert_has_calls(calls)
self.write_file.reset_mock()
self.patch_object(chm.os_ip, 'resolve_address', 'addr')
self.target.configure_cert('mycert', 'mykey')
calls = [
mock.call(
path='/etc/apache2/ssl/charmname/cert_addr',
content=b'mycert'),
mock.call(
path='/etc/apache2/ssl/charmname/key_addr',
content=b'mykey')]
self.write_file.assert_has_calls(calls)
def test_get_local_addresses(self):
self.patch_object(chm.os_utils, 'get_host_ip', return_value='privaddr')
self.patch_object(chm.os_ip, 'resolve_address')
addresses = {
'admin': 'admin_addr',
'int': 'internal_addr',
'public': 'public_addr'}
self.resolve_address.side_effect = \
lambda endpoint_type=None: addresses[endpoint_type]
self.assertEqual(
self.target.get_local_addresses(),
['admin_addr', 'internal_addr', 'privaddr', 'public_addr'])
def test_get_certs_and_keys(self):
self.patch_target(
'config_defined_ssl_cert',
new=b'cert')
self.patch_target(
'config_defined_ssl_key',
new=b'key')
self.patch_target(
'config_defined_ssl_ca',
new=b'ca')
self.assertEqual(
self.target.get_certs_and_keys(),
[{'key': 'key', 'cert': 'cert', 'ca': 'ca', 'cn': None}])
def test_get_certs_and_keys_ks_interface(self):
class KSInterface(object):
def get_ssl_key(self, key):
keys = {
'int_addr': 'int_key',
'priv_addr': 'priv_key',
'pub_addr': 'pub_key',
'admin_addr': 'admin_key'}
return keys[key]
def get_ssl_cert(self, key):
certs = {
'int_addr': 'int_cert',
'priv_addr': 'priv_cert',
'pub_addr': 'pub_cert',
'admin_addr': 'admin_cert'}
return certs[key]
def get_ssl_ca(self):
return 'ca'
self.patch_target(
'get_local_addresses',
return_value=['int_addr', 'priv_addr', 'pub_addr', 'admin_addr'])
expect = [
{
'ca': 'ca',
'cert': 'int_cert',
'cn': 'int_addr',
'key': 'int_key'},
{
'ca': 'ca',
'cert': 'priv_cert',
'cn': 'priv_addr',
'key': 'priv_key'},
{
'ca': 'ca',
'cert': 'pub_cert',
'cn': 'pub_addr',
'key': 'pub_key'},
{
'ca': 'ca',
'cert': 'admin_cert',
'cn': 'admin_addr',
'key': 'admin_key'}]
self.assertEqual(
self.target.get_certs_and_keys(keystone_interface=KSInterface()),
expect)
def test_set_config_defined_certs_and_keys(self):
config = {
'ssl_key': base64.b64encode(b'confkey'),
'ssl_cert': base64.b64encode(b'confcert'),
'ssl_ca': base64.b64encode(b'confca')}
self.patch_target('config', new=config)
self.target.set_config_defined_certs_and_keys()
self.assertEqual(self.target.config_defined_ssl_key, b'confkey')
self.assertEqual(self.target.config_defined_ssl_cert, b'confcert')
self.assertEqual(self.target.config_defined_ssl_ca, b'confca')
def test_configure_ssl(self):
ssl_objs = [
{
'cert': 'cert1',
'key': 'key1',
'ca': 'ca1',
'cn': 'cn1'},
{
'cert': 'cert2',
'key': 'key2',
'ca': 'ca2',
'cn': 'cn2'}]
self.patch_target('get_certs_and_keys', return_value=ssl_objs)
self.patch_target('configure_apache')
self.patch_target('configure_cert')
self.patch_target('configure_ca')
self.patch_target('run_update_certs')
self.patch_object(chm.charms.reactive.bus, 'set_state')
self.target.configure_ssl()
cert_calls = [
mock.call('cert1', 'key1', cn='cn1'),
mock.call('cert2', 'key2', cn='cn2')]
ca_calls = [
mock.call('ca1', update_certs=False),
mock.call('ca2', update_certs=False)]
self.configure_cert.assert_has_calls(cert_calls)
self.configure_ca.assert_has_calls(ca_calls)
self.run_update_certs.assert_called_once_with()
self.configure_apache.assert_called_once_with()
self.set_state.assert_called_once_with('ssl.enabled', True)
def test_configure_ssl_off(self):
self.patch_target('get_certs_and_keys', return_value=[])
self.patch_object(chm.charms.reactive.bus, 'set_state')
self.target.configure_ssl()
self.set_state.assert_called_once_with('ssl.enabled', False)
def test_configure_ca(self):
self.patch_target('run_update_certs')
with utils.patch_open() as (mock_open, mock_file):
self.target.configure_ca('myca')
mock_open.assert_called_with(
'/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
'w')
mock_file.write.assert_called_with('myca')
self.run_update_certs.assert_called_once_with()
def test_run_update_certs(self):
self.patch_object(chm.subprocess, 'check_call')
self.target.run_update_certs()
self.check_call.assert_called_once_with(
['update-ca-certificates', '--fresh'])
class MyAdapter(object):

View File

@ -17,6 +17,9 @@ class TestCharmOpenStackIp(utils.BaseTestCase):
def test_canonical_url(self):
self.patch_object(ip, 'resolve_address', return_value='address1')
self.patch_object(ip.net_ip, 'is_ipv6', return_value=False)
self.patch_object(
ip.charms.reactive.bus, 'get_state',
return_value=False)
# not ipv6
url = ip.canonical_url()
self.assertEqual(url, 'http://address1')
@ -131,6 +134,7 @@ class TestCharmOpenStackIp(utils.BaseTestCase):
self.patch_object(ip.net_ip, 'get_ipv6_addr')
self.patch_object(ip.hookenv, 'unit_get')
self.patch_object(ip.net_ip, 'get_address_in_network')
self.patch_object(ip, '_resolve_network_cidr')
# define a fake_config() that returns predictable results and remembers
# what it was called with.
@ -155,6 +159,7 @@ class TestCharmOpenStackIp(utils.BaseTestCase):
# for the default PUBLIC endpoint
self.is_clustered.return_value = False
self.network_get_primary_address.return_value = 'got-address'
self._resolve_network_cidr.return_value = 'cidr'
self.unit_get.return_value = 'unit-get-address'
addr = ip.resolve_address()
self.assertEqual(addr, 'got-address')
@ -213,9 +218,9 @@ class TestCharmOpenStackIp(utils.BaseTestCase):
'public'
)
# Finally resolved_address returns None -> ValueError()
# allow vip to not be found:
self.is_address_in_network.return_value = False
self.is_address_in_network.side_effect = None
with self.assertRaises(ValueError):
addr = ip.resolve_address()
# # Finally resolved_address returns None -> ValueError()
# # allow vip to not be found:
# self.is_address_in_network.return_value = False
# self.is_address_in_network.side_effect = None
# with self.assertRaises(ValueError):
# addr = ip.resolve_address()

View File

@ -15,9 +15,34 @@
# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock()
# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock()
import unittest
import contextlib
import io
import mock
import six
import unittest
if not six.PY3:
builtin_open = '__builtin__.open'
else:
builtin_open = 'builtins.open'
@contextlib.contextmanager
def patch_open():
'''Patch open() to allow mocking both open() itself and the file that is
yielded.
Yields the mock for "open" and "file", respectively.'''
mock_open = mock.MagicMock(spec=open)
mock_file = mock.MagicMock(spec=io.FileIO)
@contextlib.contextmanager
def stub_open(*args, **kwargs):
mock_open(*args, **kwargs)
yield mock_file
with mock.patch(builtin_open, stub_open):
yield mock_open, mock_file
class BaseTestCase(unittest.TestCase):