[hopem,r=]
Fix SSL injection for ca/cert/key provided by config.
This commit is contained in:
parent
3c3faa8728
commit
7a2cc794cd
@ -1,4 +1,4 @@
|
||||
branch: lp:charm-helpers
|
||||
branch: lp:~hopem/charm-helpers/fix-ssl-install-from-config
|
||||
destination: hooks/charmhelpers
|
||||
include:
|
||||
- core
|
||||
|
@ -1,4 +1,4 @@
|
||||
branch: lp:charm-helpers
|
||||
branch: lp:~hopem/charm-helpers/fix-ssl-install-from-config
|
||||
destination: tests/charmhelpers
|
||||
include:
|
||||
- contrib.amulet
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from base64 import b64decode
|
||||
from subprocess import check_call
|
||||
@ -48,6 +49,8 @@ from charmhelpers.core.hookenv import (
|
||||
from charmhelpers.core.sysctl import create as sysctl_create
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
list_nics,
|
||||
get_nic_hwaddr,
|
||||
mkdir,
|
||||
write_file,
|
||||
)
|
||||
@ -65,12 +68,18 @@ from charmhelpers.contrib.hahelpers.apache import (
|
||||
from charmhelpers.contrib.openstack.neutron import (
|
||||
neutron_plugin_attribute,
|
||||
)
|
||||
from charmhelpers.contrib.openstack.ip import (
|
||||
resolve_address,
|
||||
INTERNAL,
|
||||
)
|
||||
from charmhelpers.contrib.network.ip import (
|
||||
get_address_in_network,
|
||||
get_ipv4_addr,
|
||||
get_ipv6_addr,
|
||||
get_netmask_for_address,
|
||||
format_ipv6_addr,
|
||||
is_address_in_network,
|
||||
is_bridge_member,
|
||||
)
|
||||
from charmhelpers.contrib.openstack.utils import get_host_ip
|
||||
|
||||
@ -727,7 +736,14 @@ class ApacheSSLContext(OSContextGenerator):
|
||||
'endpoints': [],
|
||||
'ext_ports': []}
|
||||
|
||||
for cn in self.canonical_names():
|
||||
cns = self.canonical_names()
|
||||
if cns:
|
||||
for cn in cns:
|
||||
self.configure_cert(cn)
|
||||
else:
|
||||
# Expect cert/key provided in config (currently assumed that ca
|
||||
# uses ip for cn)
|
||||
cn = resolve_address(endpoint_type=INTERNAL)
|
||||
self.configure_cert(cn)
|
||||
|
||||
addresses = self.get_network_addresses()
|
||||
@ -883,6 +899,48 @@ class NeutronContext(OSContextGenerator):
|
||||
return ctxt
|
||||
|
||||
|
||||
class NeutronPortContext(OSContextGenerator):
|
||||
NIC_PREFIXES = ['eth', 'bond']
|
||||
|
||||
def resolve_ports(self, ports):
|
||||
"""Resolve NICs not yet bound to bridge(s)
|
||||
|
||||
If hwaddress provided then returns resolved hwaddress otherwise NIC.
|
||||
"""
|
||||
if not ports:
|
||||
return None
|
||||
|
||||
hwaddr_to_nic = {}
|
||||
hwaddr_to_ip = {}
|
||||
for nic in list_nics(self.NIC_PREFIXES):
|
||||
hwaddr = get_nic_hwaddr(nic)
|
||||
hwaddr_to_nic[hwaddr] = nic
|
||||
addresses = get_ipv4_addr(nic, fatal=False)
|
||||
addresses += get_ipv6_addr(iface=nic, fatal=False)
|
||||
hwaddr_to_ip[hwaddr] = addresses
|
||||
|
||||
resolved = []
|
||||
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
|
||||
for entry in ports:
|
||||
if re.match(mac_regex, entry):
|
||||
# NIC is in known NICs and does NOT hace an IP address
|
||||
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
|
||||
# If the nic is part of a bridge then don't use it
|
||||
if is_bridge_member(hwaddr_to_nic[entry]):
|
||||
continue
|
||||
|
||||
# Entry is a MAC address for a valid interface that doesn't
|
||||
# have an IP address assigned yet.
|
||||
resolved.append(hwaddr_to_nic[entry])
|
||||
else:
|
||||
# If the passed entry is not a MAC address, assume it's a valid
|
||||
# interface, and that the user put it there on purpose (we can
|
||||
# trust it to be the real external network).
|
||||
resolved.append(entry)
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
class OSConfigFlagContext(OSContextGenerator):
|
||||
"""Provides support for user-defined config flags.
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
# Various utilies for dealing with Neutron and the renaming from Quantum.
|
||||
|
||||
import six
|
||||
from subprocess import check_output
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -237,3 +238,72 @@ def network_manager():
|
||||
else:
|
||||
# ensure accurate naming for all releases post-H
|
||||
return 'neutron'
|
||||
|
||||
|
||||
def parse_mappings(mappings):
|
||||
parsed = {}
|
||||
if mappings:
|
||||
mappings = mappings.split(' ')
|
||||
for m in mappings:
|
||||
p = m.partition(':')
|
||||
if p[1] == ':':
|
||||
parsed[p[0].strip()] = p[2].strip()
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_bridge_mappings(mappings):
|
||||
"""Parse bridge mappings.
|
||||
|
||||
Mappings must be a space-delimited list of provider:bridge mappings.
|
||||
|
||||
Returns dict of the form {provider:bridge}.
|
||||
"""
|
||||
return parse_mappings(mappings)
|
||||
|
||||
|
||||
def parse_data_port_mappings(mappings, default_bridge='br-data'):
|
||||
"""Parse data port mappings.
|
||||
|
||||
Mappings must be a space-delimited list of bridge:port mappings.
|
||||
|
||||
Returns dict of the form {bridge:port}.
|
||||
"""
|
||||
_mappings = parse_mappings(mappings)
|
||||
if not _mappings:
|
||||
if not mappings:
|
||||
return {}
|
||||
|
||||
# For backwards-compatibility we need to support port-only provided in
|
||||
# config.
|
||||
_mappings = {default_bridge: mappings.split(' ')[0]}
|
||||
|
||||
bridges = _mappings.keys()
|
||||
ports = _mappings.values()
|
||||
if len(set(bridges)) != len(bridges):
|
||||
raise Exception("It is not allowed to have more than one port "
|
||||
"configured on the same bridge")
|
||||
|
||||
if len(set(ports)) != len(ports):
|
||||
raise Exception("It is not allowed to have the same port configured "
|
||||
"on more than one bridge")
|
||||
|
||||
return _mappings
|
||||
|
||||
|
||||
def parse_vlan_range_mappings(mappings):
|
||||
"""Parse vlan range mappings.
|
||||
|
||||
Mappings must be a space-delimited list of provider:start:end mappings.
|
||||
|
||||
Returns dict of the form {provider: (start, end)}.
|
||||
"""
|
||||
_mappings = parse_mappings(mappings)
|
||||
if not _mappings:
|
||||
return {}
|
||||
|
||||
mappings = {}
|
||||
for p, r in six.iteritems(_mappings):
|
||||
mappings[p] = tuple(r.split(':'))
|
||||
|
||||
return mappings
|
||||
|
@ -566,3 +566,29 @@ class Hooks(object):
|
||||
def charm_dir():
|
||||
"""Return the root directory of the current charm"""
|
||||
return os.environ.get('CHARM_DIR')
|
||||
|
||||
|
||||
@cached
|
||||
def action_get(key=None):
|
||||
"""Gets the value of an action parameter, or all key/value param pairs"""
|
||||
cmd = ['action-get']
|
||||
if key is not None:
|
||||
cmd.append(key)
|
||||
cmd.append('--format=json')
|
||||
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
|
||||
return action_data
|
||||
|
||||
|
||||
def action_set(values):
|
||||
"""Sets the values to be returned after the action finishes"""
|
||||
cmd = ['action-set']
|
||||
for k, v in list(values.items()):
|
||||
cmd.append('{}={}'.format(k, v))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def action_fail(message):
|
||||
"""Sets the action status to failed and sets the error message.
|
||||
|
||||
The results set by action_set are preserved."""
|
||||
subprocess.check_call(['action-fail', message])
|
||||
|
@ -339,12 +339,16 @@ def lsb_release():
|
||||
def pwgen(length=None):
|
||||
"""Generate a random pasword."""
|
||||
if length is None:
|
||||
# A random length is ok to use a weak PRNG
|
||||
length = random.choice(range(35, 45))
|
||||
alphanumeric_chars = [
|
||||
l for l in (string.ascii_letters + string.digits)
|
||||
if l not in 'l0QD1vAEIOUaeiou']
|
||||
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
|
||||
# actual password
|
||||
random_generator = random.SystemRandom()
|
||||
random_chars = [
|
||||
random.choice(alphanumeric_chars) for _ in range(length)]
|
||||
random_generator.choice(alphanumeric_chars) for _ in range(length)]
|
||||
return(''.join(random_chars))
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from charmhelpers.core.hookenv import config
|
||||
from base64 import b64decode
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
mkdir,
|
||||
@ -17,6 +17,7 @@ from charmhelpers.contrib.hahelpers.cluster import (
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
log,
|
||||
DEBUG,
|
||||
INFO,
|
||||
@ -31,6 +32,13 @@ from charmhelpers.contrib.hahelpers.apache import install_ca_cert
|
||||
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
||||
|
||||
|
||||
def is_cert_provided_in_config():
|
||||
ca = config('ssl_ca')
|
||||
cert = config('ssl_cert')
|
||||
key = config('ssl_key')
|
||||
return bool(ca and cert and key)
|
||||
|
||||
|
||||
class ApacheSSLContext(context.ApacheSSLContext):
|
||||
|
||||
interfaces = ['https']
|
||||
@ -71,12 +79,8 @@ class ApacheSSLContext(context.ApacheSSLContext):
|
||||
get_ca,
|
||||
ensure_permissions,
|
||||
is_ssl_cert_master,
|
||||
is_ssl_enabled,
|
||||
)
|
||||
|
||||
if not is_ssl_enabled():
|
||||
return
|
||||
|
||||
# Ensure ssl dir exists whether master or not
|
||||
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
|
||||
perms = 0o755
|
||||
@ -85,15 +89,23 @@ class ApacheSSLContext(context.ApacheSSLContext):
|
||||
ensure_permissions(ssl_dir, user=SSH_USER, group='keystone',
|
||||
perms=perms)
|
||||
|
||||
if not is_ssl_cert_master():
|
||||
if not is_cert_provided_in_config() and not is_ssl_cert_master():
|
||||
log("Not ssl-cert-master - skipping apache cert config until "
|
||||
"master is elected", level=INFO)
|
||||
return
|
||||
|
||||
log("Creating apache ssl certs in %s" % (ssl_dir), level=INFO)
|
||||
|
||||
ca = get_ca(user=SSH_USER)
|
||||
cert, key = ca.get_cert_and_key(common_name=cn)
|
||||
cert = config('ssl_cert')
|
||||
key = config('ssl_key')
|
||||
|
||||
if not (cert and key):
|
||||
ca = get_ca(user=SSH_USER)
|
||||
cert, key = ca.get_cert_and_key(common_name=cn)
|
||||
else:
|
||||
cert = b64decode(cert)
|
||||
key = b64decode(key)
|
||||
|
||||
write_file(path=os.path.join(ssl_dir, 'cert_{}'.format(cn)),
|
||||
content=cert, owner=SSH_USER, group='keystone', perms=0o644)
|
||||
write_file(path=os.path.join(ssl_dir, 'key_{}'.format(cn)),
|
||||
@ -105,20 +117,22 @@ class ApacheSSLContext(context.ApacheSSLContext):
|
||||
get_ca,
|
||||
ensure_permissions,
|
||||
is_ssl_cert_master,
|
||||
is_ssl_enabled,
|
||||
)
|
||||
|
||||
if not is_ssl_enabled():
|
||||
return
|
||||
|
||||
if not is_ssl_cert_master():
|
||||
if not is_cert_provided_in_config() and not is_ssl_cert_master():
|
||||
log("Not ssl-cert-master - skipping apache ca config until "
|
||||
"master is elected", level=INFO)
|
||||
return
|
||||
|
||||
ca = get_ca(user=SSH_USER)
|
||||
install_ca_cert(ca.get_ca_bundle())
|
||||
ca_cert = config('ssl_ca')
|
||||
if ca_cert is None:
|
||||
ca = get_ca(user=SSH_USER)
|
||||
ca_cert = ca.get_ca_bundle()
|
||||
else:
|
||||
ca_cert = b64decode(ca_cert)
|
||||
|
||||
# Ensure accessible by keystone ssh user and group (unison)
|
||||
install_ca_cert(ca_cert)
|
||||
ensure_permissions(CA_CERT_PATH, user=SSH_USER, group='keystone',
|
||||
perms=0o0644)
|
||||
|
||||
|
@ -6,8 +6,10 @@ from test_utils import (
|
||||
)
|
||||
|
||||
TO_PATCH = [
|
||||
'config',
|
||||
'determine_apache_port',
|
||||
'determine_api_port',
|
||||
'is_cert_provided_in_config',
|
||||
]
|
||||
|
||||
|
||||
@ -16,6 +18,7 @@ class TestKeystoneContexts(CharmTestCase):
|
||||
def setUp(self):
|
||||
super(TestKeystoneContexts, self).setUp(context, TO_PATCH)
|
||||
|
||||
@patch.object(context, 'is_cert_provided_in_config')
|
||||
@patch.object(context, 'mkdir')
|
||||
@patch('keystone_utils.get_ca')
|
||||
@patch('keystone_utils.ensure_permissions')
|
||||
@ -30,7 +33,9 @@ class TestKeystoneContexts(CharmTestCase):
|
||||
mock_determine_ports,
|
||||
mock_ensure_permissions,
|
||||
mock_get_ca,
|
||||
mock_mkdir):
|
||||
mock_mkdir,
|
||||
mock_cert_provided_in_config):
|
||||
mock_cert_provided_in_config.return_value = False
|
||||
mock_is_ssl_enabled.return_value = True
|
||||
mock_is_ssl_cert_master.return_value = False
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user