# 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 import json from charmhelpers.core.hookenv import ( config, relation_ids, related_units, relation_get, local_unit, log, ERROR, WARNING, ) from charmhelpers.core.strutils import bool_from_string from charmhelpers.contrib.openstack import context from charmhelpers.contrib.openstack.context import ( OSContextGenerator, context_complete ) from charmhelpers.contrib.hahelpers.cluster import ( https, ) from charmhelpers.contrib.network.ip import ( get_ipv6_addr, format_ipv6_addr, get_relation_ip, ) import charmhelpers.contrib.openstack.policyd as policyd 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'), 'haproxy_expose_stats': config('haproxy-expose-stats') } return ctxt 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) default_role = config('default-role') lc_default_role = config('default-role').lower() for role in rdata.get('created_roles', '').split(','): if role.lower() == lc_default_role: default_role = role serv_host = rdata.get('service_host') serv_host = format_ipv6_addr(serv_host) or serv_host internal_host = rdata.get('internal_host') internal_host = (format_ipv6_addr(internal_host) or internal_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'), 'default_role': default_role } # If using keystone v3 the context is incomplete without the # admin domain id if local_ctxt['api_version'] == '3': local_ctxt['ks_endpoint_path'] = 'v3' if not config('default_domain'): local_ctxt['admin_domain_id'] = rdata.get( 'admin_domain_id') else: local_ctxt['ks_endpoint_path'] = 'v2.0' if not context_complete(local_ctxt): continue # internal_* keys will be treated as optional, since the user # could be upgrading the openstack-dashboard charm before # keystone, so we add them to `local_ctxt` after calling # `context_complete()`. local_ctxt.update({ 'internal_port': rdata.get('internal_port'), 'internal_host': internal_host, 'internal_protocol': rdata.get('internal_protocol') or 'http', }) # if the use configured the charm to use internal endpoints, # but the keystone charm didn't provide the internal_host key # in the relation we fallback to use the service_host. if config("use-internal-endpoints") and internal_host: log("Using internal endpoints to configure horizon") local_ctxt["ks_protocol"] = local_ctxt["internal_protocol"] local_ctxt["ks_host"] = local_ctxt["internal_host"] local_ctxt["ks_port"] = local_ctxt["internal_port"] else: log("Using service host to configure horizon") local_ctxt["ks_protocol"] = local_ctxt["service_protocol"] local_ctxt["ks_host"] = local_ctxt["service_host"] local_ctxt["ks_port"] = local_ctxt["service_port"] # Update the service endpoint and title for each available # region in order to support multi-region deployments if region is not None: if config("use-internal-endpoints") and internal_host: endpoint = ( "{internal_protocol}://{internal_host}" ":{internal_port}/{ks_endpoint_path}").format( **local_ctxt) else: endpoint = ( "{service_protocol}://{service_host}" ":{service_port}/{ks_endpoint_path}").format( **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'), "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').strip() if config('secret') else 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"), 'hide_create_volume': config('hide-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'), 'dropdown_max_items': config('dropdown-max-items'), 'enable_consistency_groups': config('enable-consistency-groups'), 'disable_instance_snapshot': bool( config('disable-instance-snapshot')), 'disable_password_reveal': config('disable-password-reveal'), 'enforce_password_check': config('enforce-password-check'), 'site_branding': config('site-branding'), 'site_branding_link': config('site-branding-link'), 'help_url': config('help-url'), 'create_instance_flavor_sort_key': config('create-instance-flavor-sort-key'), 'create_instance_flavor_sort_reverse': config('create-instance-flavor-sort-reverse'), } return ctxt class PolicydContext(OSContextGenerator): def __init__(self, policyd_extract_policy_dirs_fn): self.policyd_extract_policy_dirs_fn = policyd_extract_policy_dirs_fn def __call__(self): """Policyd variables for the local_settings.py configuration file. :returns: The context to help set vars in the localsettings. :rtype: Dict[str, ANY] """ activated = (config('use-policyd-override') and policyd.is_policy_success_file_set()) if activated: return { 'policyd_overrides_activated': activated, 'policy_dirs': self.policyd_extract_policy_dirs_fn(), } else: return { 'policyd_overrides_activated': activated } 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'): if https(): ctxt['enforce_ssl'] = True else: log("Enforce ssl redirect requested but ssl not configured - " "skipping redirect", level=WARNING) return ctxt class ApacheSSLContext(context.ApacheSSLContext): interfaces = ['https'] external_ports = [443] service_namespace = 'horizon' def __call__(self): return super(ApacheSSLContext, self).__call__() 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