# Copyright 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # vim: set ts=4:et from base64 import b64decode import os import json from charmhelpers.core.hookenv import ( config, relation_ids, related_units, relation_get, local_unit, log, WARNING, ERROR, ) from charmhelpers.core.strutils import bool_from_string from charmhelpers.contrib.openstack.context import ( OSContextGenerator, context_complete ) from charmhelpers.contrib.hahelpers.apache import ( get_ca_cert, get_cert, install_ca_cert, ) from charmhelpers.contrib.network.ip import ( get_ipv6_addr, format_ipv6_addr, get_relation_ip, ) from charmhelpers.core.host import pwgen VALID_ENDPOINT_TYPES = { 'PUBLICURL': 'publicURL', 'INTERNALURL': 'internalURL', 'ADMINURL': 'adminURL', } SSL_CERT_FILE = '/etc/apache2/ssl/horizon/cert_dashboard' SSL_KEY_FILE = '/etc/apache2/ssl/horizon/key_dashboard' class HorizonHAProxyContext(OSContextGenerator): def __call__(self): ''' Horizon specific HAProxy context; haproxy is used all the time in the openstack dashboard charm so a single instance just self refers ''' cluster_hosts = {} l_unit = local_unit().replace('/', '-') if config('prefer-ipv6'): cluster_hosts[l_unit] = get_ipv6_addr(exc_list=[config('vip')])[0] else: cluster_hosts[l_unit] = get_relation_ip('cluster') for rid in relation_ids('cluster'): for unit in related_units(rid): _unit = unit.replace('/', '-') addr = relation_get('private-address', rid=rid, unit=unit) cluster_hosts[_unit] = addr log('Ensuring haproxy enabled in /etc/default/haproxy.') with open('/etc/default/haproxy', 'w') as out: out.write('ENABLED=1\n') ctxt = { 'units': cluster_hosts, 'service_ports': { 'dash_insecure': [80, 70], 'dash_secure': [443, 433] }, 'prefer_ipv6': config('prefer-ipv6') } return ctxt # NOTE: this is a stripped-down version of # contrib.openstack.IdentityServiceContext class IdentityServiceContext(OSContextGenerator): interfaces = ['identity-service'] def normalize(self, endpoint_type): """Normalizes the endpoint type values. :param endpoint_type (string): the endpoint type to normalize. :raises: Exception if the endpoint type is not valid. :return (string): the normalized form of the endpoint type. """ normalized_form = VALID_ENDPOINT_TYPES.get(endpoint_type.upper(), None) if not normalized_form: msg = ('Endpoint type specified %s is not a valid' ' endpoint type' % endpoint_type) log(msg, ERROR) raise Exception(msg) return normalized_form def __call__(self): log('Generating template context for identity-service') ctxt = {} regions = set() for rid in relation_ids('identity-service'): for unit in related_units(rid): rdata = relation_get(rid=rid, unit=unit) serv_host = rdata.get('service_host') serv_host = format_ipv6_addr(serv_host) or serv_host region = rdata.get('region') local_ctxt = { 'service_port': rdata.get('service_port'), 'service_host': serv_host, 'service_protocol': rdata.get('service_protocol') or 'http', 'api_version': rdata.get('api_version', '2') } # If using keystone v3 the context is incomplete without the # admin domain id if local_ctxt['api_version'] == '3': if not config('default_domain'): local_ctxt['admin_domain_id'] = rdata.get( 'admin_domain_id') if not context_complete(local_ctxt): continue # Update the service endpoint and title for each available # region in order to support multi-region deployments if region is not None: endpoint = ("%(service_protocol)s://%(service_host)s" ":%(service_port)s/v2.0") % local_ctxt for reg in region.split(): regions.add((endpoint, reg)) if len(ctxt) == 0: ctxt = local_ctxt if len(regions) > 1: avail_regions = map(lambda r: {'endpoint': r[0], 'title': r[1]}, regions) ctxt['regions'] = sorted(avail_regions, key=lambda k: k['endpoint']) # Allow the endpoint types to be specified via a config parameter. # The config parameter accepts either: # 1. a single endpoint type to be specified, in which case the # primary endpoint is configured # 2. a list of endpoint types, in which case the primary endpoint # is taken as the first entry and the secondary endpoint is # taken as the second entry. All subsequent entries are ignored. ep_types = config('endpoint-type') if ep_types: ep_types = [self.normalize(e) for e in ep_types.split(',')] ctxt['primary_endpoint'] = ep_types[0] if len(ep_types) > 1: ctxt['secondary_endpoint'] = ep_types[1] return ctxt class HorizonContext(OSContextGenerator): def __call__(self): ''' Provide all configuration for Horizon ''' ctxt = { 'compress_offline': bool_from_string(config('offline-compression')), 'debug': bool_from_string(config('debug')), 'customization_module': config('customization-module'), 'default_role': config('default-role'), "webroot": config('webroot') or '/', "ubuntu_theme": bool_from_string(config('ubuntu-theme')), "default_theme": config('default-theme'), "custom_theme": config('custom-theme'), "secret": config('secret') or pwgen(), 'support_profile': config('profile') if config('profile') in ['cisco'] else None, "neutron_network_dvr": config("neutron-network-dvr"), "neutron_network_l3ha": config("neutron-network-l3ha"), "neutron_network_lb": config("neutron-network-lb"), "neutron_network_firewall": config("neutron-network-firewall"), "neutron_network_vpn": config("neutron-network-vpn"), "cinder_backup": config("cinder-backup"), "allow_password_autocompletion": config("allow-password-autocompletion"), "password_retrieve": config("password-retrieve"), 'default_domain': config('default-domain'), 'multi_domain': False if config('default-domain') else True, "default_create_volume": config("default-create-volume"), 'image_formats': config('image-formats'), 'api_result_limit': config('api-result-limit') or 1000, 'enable_fip_topology_check': config('enable-fip-topology-check'), 'session_timeout': config('session-timeout') } return ctxt class ApacheContext(OSContextGenerator): def __call__(self): ''' Grab cert and key from configuraton for SSL config ''' ctxt = { 'http_port': 70, 'https_port': 433, 'enforce_ssl': False, 'hsts_max_age_seconds': config('hsts-max-age-seconds'), "custom_theme": config('custom-theme'), } if config('enforce-ssl'): # NOTE(dosaboy): if ssl is not configured we shouldn't allow this if all(get_cert()): ctxt['enforce_ssl'] = True else: log("Enforce ssl redirect requested but ssl not configured - " "skipping redirect", level=WARNING) return ctxt class ApacheSSLContext(OSContextGenerator): def __call__(self): ''' Grab cert and key from configuration for SSL config ''' ctxt = {'ssl_configured': False} use_local_ca = True for rid in relation_ids('certificates'): if related_units(rid): use_local_ca = False if use_local_ca: ca_cert = get_ca_cert() if not ca_cert: return ctxt install_ca_cert(b64decode(ca_cert)) ssl_cert, ssl_key = get_cert() if all([ssl_cert, ssl_key]): with open('/etc/ssl/certs/dashboard.cert', 'wb') as cert_out: cert_out.write(b64decode(ssl_cert)) with open('/etc/ssl/private/dashboard.key', 'wb') as key_out: key_out.write(b64decode(ssl_key)) os.chmod('/etc/ssl/private/dashboard.key', 0o600) ctxt = { 'ssl_configured': True, 'ssl_cert': '/etc/ssl/certs/dashboard.cert', 'ssl_key': '/etc/ssl/private/dashboard.key', } else: if os.path.exists(SSL_CERT_FILE) and os.path.exists(SSL_KEY_FILE): ctxt = { 'ssl_configured': True, 'ssl_cert': SSL_CERT_FILE, 'ssl_key': SSL_KEY_FILE, } return ctxt class RouterSettingContext(OSContextGenerator): def __call__(self): ''' Enable/Disable Router Tab on horizon ''' ctxt = { 'disable_router': False if config('profile') in ['cisco'] else True } return ctxt class LocalSettingsContext(OSContextGenerator): def __call__(self): ''' Additional config stanzas to be appended to local_settings.py ''' relations = [] for rid in relation_ids("dashboard-plugin"): try: unit = related_units(rid)[0] except IndexError: pass else: rdata = relation_get(unit=unit, rid=rid) if set(('local-settings', 'priority')) <= set(rdata.keys()): relations.append((unit, rdata)) ctxt = { 'settings': [ '# {0}\n{1}'.format(u, rd['local-settings']) for u, rd in sorted(relations, key=lambda r: r[1]['priority'])] } return ctxt class WebSSOFIDServiceProviderContext(OSContextGenerator): interfaces = ['websso-fid-service-provider'] def __call__(self): websso_keys = ['protocol-name', 'idp-name', 'user-facing-name'] relations = [] for rid in relation_ids("websso-fid-service-provider"): try: # the first unit will do - the assumption is that all # of them should advertise the same data. This needs # refactoring if juju gets per-application relation data # support unit = related_units(rid)[0] except IndexError: pass else: rdata = relation_get(unit=unit, rid=rid) if set(rdata).issuperset(set(websso_keys)): relations.append({k: json.loads(rdata[k]) for k in websso_keys}) # populate the context with data from one or more # service providers ctxt = {'websso_data': relations} if relations else {} return ctxt