import os import subprocess import ConfigParser from base64 import b64encode from collections import OrderedDict from copy import deepcopy from charmhelpers.contrib.openstack import templating, context from charmhelpers.contrib.openstack.utils import ( get_os_codename_package, save_script_rc as _save_script_rc, ) from charmhelpers.core.hookenv import ( config, log, relation_ids, remote_unit, ERROR, ) import nova_cc_context TEMPLATES = 'templates/' CLUSTER_RES = 'res_nova_vip' # removed from original: python-mysqldb python-keystone charm-helper-sh BASE_PACKAGES = [ 'apache2', 'haproxy', 'uuid', ] BASE_SERVICES = [ 'nova-api-ec2', 'nova-api-os-compute', 'nova-objectstore', 'nova-cert', 'nova-scheduler', ] API_PORTS = { 'nova-api-ec2': 8773, 'nova-api-os-compute': 8774, 'nova-api-os-volume': 8776, 'nova-objectstore': 3333, 'quantum-server': 9696, } BASE_RESOURCE_MAP = OrderedDict([ ('/etc/nova/nova.conf', { 'services': BASE_SERVICES, 'contexts': [context.AMQPContext(), context.SharedDBContext(), context.ImageServiceContext(), nova_cc_context.VolumeServiceContext()], }), ('/etc/nova/api-paste.ini', { 'services': [s for s in BASE_SERVICES if 'api' in s], 'contexts': [context.IdentityServiceContext()], }), ('/etc/quantum/quantum.conf', { 'services': ['quantum-server'], 'contexts': [], }), ('/etc/quantum/api-paste.ini', { 'services': ['quantum-server'], 'contexts': [], }), ('/etc/haproxy/haproxy.cfg', { 'contexts': [context.HAProxyContext(), nova_cc_context.HAProxyContext()], 'services': ['haproxy'], }), ('/etc/apache2/sites-available/openstack_https_frontend', { 'contexts': [], 'contexts': [nova_cc_context.ApacheSSLContext()], 'services': ['apache2'], }), ]) CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' NOVA_SSH_DIR = '/etc/nova/compute_ssh/' def resource_map(): ''' Dynamically generate a map of resources that will be managed for a single hook execution. ''' resource_map = deepcopy(BASE_RESOURCE_MAP) if relation_ids('nova-volume-service'): # if we have a relation to a nova-volume service, we're # also managing the nova-volume API endpoint (legacy) resource_map['/etc/nova/nova.conf']['services'].append( 'nova-api-os-volume') if config('network-manager').lower() not in ['quantum', 'quantum']: # pop out quantum resources if not deploying it. easier to # remove it from the base ordered dict than add it in later # and still preserve ordering for restart_map(). [resource_map.pop(k) for k in list(resource_map.iterkeys()) if 'quantum' in k] return resource_map def register_configs(): release = get_os_codename_package('nova-common', fatal=False) or 'essex' configs = templating.OSConfigRenderer(templates_dir=TEMPLATES, openstack_release=release) for cfg, rscs in resource_map.iteritems(): configs.register(cfg, rscs['contexts']) return configs def restart_map(): return {k: v['services'] for k, v in resource_map().iteritems()} def determine_ports(): '''Assemble a list of API ports for services we are managing''' ports = [] for cfg, services in restart_map().iteritems(): for service in services: try: ports.append(API_PORTS[service]) except KeyError: pass return ports def network_manager(): pass def determine_packages(): # currently all packages match service names packages = [] + BASE_PACKAGES for k, v in resource_map().iteritems(): packages.extend(v['services']) return list(set(packages)) def save_script_rc(): env_vars = { 'OPENSTACK_PORT_MCASTPORT': config('ha-mcastport'), 'OPENSTACK_SERVICE_API_EC2': 'nova-api-ec2', 'OPENSTACK_SERVICE_API_OS_COMPUTE': 'nova-api-os-compute', 'OPENSTACK_SERVICE_CERT': 'nova-cert', 'OPENSTACK_SERVICE_CONDUCTOR': 'nova-conductor', 'OPENSTACK_SERVICE_OBJECTSTORE': 'nova-objectstore', 'OPENSTACK_SERVICE_SCHEDULER': 'nova-scheduler', } if relation_ids('nova-volume-service'): env_vars['OPENSTACK_SERVICE_API_OS_VOL'] = 'nova-api-os-volume' if config('network-manager').lower() in ['quantum', 'quantum']: env_vars['OPENSTACK_SERVICE_API_QUANTUM'] = 'quantum-server' _save_script_rc(**env_vars) def do_openstack_upgrade(): # TODO pass def quantum_plugin(): plugin = config('quantum-plugin').lower() if not plugin: return config('quantum-plugin').lower() def volume_service(): '''Specifies correct volume API for specific OS release''' os_vers = get_os_codename_package('nova-common') if os_vers == 'essex': return 'nova-volume' elif os_vers == 'folsom': # support both drivers in folsom. if not relation_ids('cinder-volume-service'): return 'nova-volume' return 'cinder' def migrate_database(): '''Runs nova-manage to initialize a new database or migrate existing''' cmd = ['nova-manage', 'db', 'sync'] subprocess.check_call(cmd) def auth_token_config(setting): ''' Returns currently configured value for setting in api-paste.ini's authtoken section, or None. ''' config = ConfigParser.RawConfigParser() config.read('/etc/nova/api-paste.ini') try: value = config.get('filter:authtoken', setting) except: return None if value.startswith('%'): return None return value def keystone_ca_cert_b64(): '''Returns the local Keystone-provided CA cert if it exists, or None.''' if not os.path.isfile(CA_CERT_PATH): return None with open(CA_CERT_PATH) as _in: return b64encode(_in.read()) def ssh_directory_for_unit(): remote_service = remote_unit().split('/')[0] d = os.path.join(NOVA_SSH_DIR, remote_service) if not os.path.isdir(d): os.mkdir(d) return d def known_hosts(): return os.path.join(ssh_directory_for_unit(), 'known_hosts') def authorized_keys(): return os.path.join(ssh_directory_for_unit(), 'authorized_keys') def ssh_known_host_key(host): cmd = ['ssh-keygen', '-f', known_hosts(), '-H', '-F', host] return subprocess.check_output(cmd).strip() def remove_known_host(host): log('Removing SSH known host entry for compute host at %s' % host) cmd = ['ssh-kegen', '-f', known_hosts(), '-R', host] subprocess.check_call(cmd) def add_known_host(host): '''Add variations of host to a known hosts file.''' cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host] try: remote_key = subprocess.check_output(cmd).strip() except Exception as e: log('Could not obtain SSH host key from %s' % host, level=ERROR) raise e current_key = ssh_known_host_key(host) if current_key: if remote_key == current_key: log('Known host key for compute host %s up to date.' % host) return else: remove_known_host(host) log('Adding SSH host key to known hosts for compute node at %s.' % host) with open(known_hosts(), 'a') as out: out.write(remote_key + '\n') def ssh_authorized_key_exists(public_key): with open(authorized_keys()) as keys: return (' %s ' % public_key) in keys.read() def add_authorized_key(public_key): with open(authorized_keys(), 'a') as keys: keys.write(public_key + '\n') def ssh_compute_add(public_key, host): if not ssh_known_host_key(host): add_known_host(host) if not ssh_authorized_key_exists(public_key): log('Saving SSH authorized key for compute host at %s.' % host) add_authorized_key(host) def ssh_known_hosts_b64(): with open(known_hosts()) as hosts: return b64encode(hosts.read()) def ssh_authorized_keys_b64(): with open(authorized_keys()) as keys: return b64encode(keys.read()) def ssh_compute_remove(): if not (os.path.isfile(authorized_keys()) or os.path.isfile(known_hosts())): return # NOTE: compute names its ssh key as ${service}-{$unit_num}. we dont # have access to relation settings from departed hooks, so # we need to remove key based on keyname only. key_name = remote_unit().replace('/', '-') with open(authorized_keys()) as _keys: keys = _keys.readlines() print keys [keys.remove(key) for key in keys if key_name in key] with open(authorized_keys(), 'w') as _keys: _keys.write('\n'.join(keys)) def determine_endpoints(): pass