import os import shutil import pwd from base64 import b64decode from copy import deepcopy from subprocess import check_call, check_output from charmhelpers.fetch import ( apt_update, apt_upgrade, apt_install, ) from charmhelpers.core.host import ( adduser, add_group, add_user_to_group, mkdir, service_restart, lsb_release, write_file, ) from charmhelpers.core.hookenv import ( charm_dir, config, log, related_units, relation_ids, relation_get, DEBUG, INFO, ) from charmhelpers.core.templating import render from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute from charmhelpers.contrib.openstack import templating, context from charmhelpers.contrib.openstack.alternatives import install_alternative from charmhelpers.contrib.openstack.utils import ( configure_installation_source, get_os_codename_install_source, git_install_requested, git_clone_and_install, git_src_dir, os_release ) from nova_compute_context import ( CloudComputeContext, MetadataServiceContext, NovaComputeLibvirtContext, NovaComputeLibvirtOverrideContext, NovaComputeCephContext, NeutronComputeContext, InstanceConsoleContext, CEPH_CONF, ceph_config_file, HostIPContext, ) CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' TEMPLATES = 'templates/' BASE_PACKAGES = [ 'nova-compute', 'genisoimage', # was missing as a package dependency until raring. 'librbd1', # bug 1440953 'python-six', ] BASE_GIT_PACKAGES = [ 'libvirt-bin', 'libxml2-dev', 'libxslt1-dev', 'python-dev', 'python-pip', 'python-setuptools', 'zlib1g-dev', ] LATE_GIT_PACKAGES = [ 'bridge-utils', 'dnsmasq-base', 'dnsmasq-utils', 'ebtables', 'genisoimage', 'iptables', 'iputils-arping', 'kpartx', 'kvm', 'netcat', 'open-iscsi', 'parted', 'python-libvirt', 'qemu', 'qemu-system', 'qemu-utils', 'vlan', 'xen-system-amd64', ] # ubuntu packages that should not be installed when deploying from git GIT_PACKAGE_BLACKLIST = [ 'neutron-plugin-openvswitch', 'neutron-plugin-openvswitch-agent', 'neutron-server', 'nova-api', 'nova-api-metadata', 'nova-compute', 'nova-compute-kvm', 'nova-compute-lxc', 'nova-compute-qemu', 'nova-compute-uml', 'nova-compute-xen', 'nova-network', 'python-six', 'quantum-plugin-openvswitch', 'quantum-plugin-openvswitch-agent', 'quantum-server', ] NOVA_CONF_DIR = "/etc/nova" QEMU_CONF = '/etc/libvirt/qemu.conf' LIBVIRTD_CONF = '/etc/libvirt/libvirtd.conf' LIBVIRT_BIN = '/etc/default/libvirt-bin' LIBVIRT_BIN_OVERRIDES = '/etc/init/libvirt-bin.override' NOVA_CONF = '%s/nova.conf' % NOVA_CONF_DIR BASE_RESOURCE_MAP = { QEMU_CONF: { 'services': ['libvirt-bin'], 'contexts': [], }, LIBVIRTD_CONF: { 'services': ['libvirt-bin'], 'contexts': [NovaComputeLibvirtContext()], }, LIBVIRT_BIN: { 'services': ['libvirt-bin'], 'contexts': [NovaComputeLibvirtContext()], }, LIBVIRT_BIN_OVERRIDES: { 'services': ['libvirt-bin'], 'contexts': [NovaComputeLibvirtOverrideContext()], }, NOVA_CONF: { 'services': ['nova-compute'], 'contexts': [context.AMQPContext(ssl_dir=NOVA_CONF_DIR), context.SharedDBContext( relation_prefix='nova', ssl_dir=NOVA_CONF_DIR), context.PostgresqlDBContext(), context.ImageServiceContext(), context.OSConfigFlagContext(), CloudComputeContext(), NovaComputeLibvirtContext(), NovaComputeCephContext(), context.SyslogContext(), context.SubordinateConfigContext( interface='nova-ceilometer', service='nova', config_file=NOVA_CONF), InstanceConsoleContext(), context.ZeroMQContext(), context.NotificationDriverContext(), MetadataServiceContext(), HostIPContext(), context.LogLevelContext()], }, } CEPH_SECRET = '/etc/ceph/secret.xml' CEPH_RESOURCES = { CEPH_SECRET: { 'contexts': [NovaComputeCephContext()], 'services': [], } } QUANTUM_CONF_DIR = "/etc/quantum" QUANTUM_CONF = '%s/quantum.conf' % QUANTUM_CONF_DIR QUANTUM_RESOURCES = { QUANTUM_CONF: { 'services': [], 'contexts': [NeutronComputeContext(), context.AMQPContext(ssl_dir=QUANTUM_CONF_DIR), context.SyslogContext()], }, } NEUTRON_CONF_DIR = "/etc/neutron" NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR NEUTRON_RESOURCES = { NEUTRON_CONF: { 'services': [], 'contexts': [NeutronComputeContext(), context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR), context.SyslogContext()], }, } # Maps virt-type config to a compute package(s). VIRT_TYPES = { 'kvm': ['nova-compute-kvm'], 'qemu': ['nova-compute-qemu'], 'xen': ['nova-compute-xen'], 'uml': ['nova-compute-uml'], 'lxc': ['nova-compute-lxc'], } # Maps virt-type config to a libvirt URI. LIBVIRT_URIS = { 'kvm': 'qemu:///system', 'qemu': 'qemu:///system', 'xen': 'xen:///', 'uml': 'uml:///system', 'lxc': 'lxc:///', } def resource_map(): ''' Dynamically generate a map of resources that will be managed for a single hook execution. ''' # TODO: Cache this on first call? resource_map = deepcopy(BASE_RESOURCE_MAP) net_manager = network_manager() plugin = neutron_plugin() # Network manager gets set late by the cloud-compute interface. # FlatDHCPManager only requires some extra packages. if (net_manager in ['flatmanager', 'flatdhcpmanager'] and config('multi-host').lower() == 'yes'): resource_map[NOVA_CONF]['services'].extend( ['nova-api', 'nova-network'] ) # Neutron/quantum requires additional contexts, as well as new resources # depending on the plugin used. # NOTE(james-page): only required for ovs plugin right now if net_manager in ['neutron', 'quantum']: # This stanza supports the legacy case of ovs supported within # compute charm code (now moved to neutron-openvswitch subordinate) if manage_ovs(): if net_manager == 'quantum': nm_rsc = QUANTUM_RESOURCES if net_manager == 'neutron': nm_rsc = NEUTRON_RESOURCES resource_map.update(nm_rsc) conf = neutron_plugin_attribute(plugin, 'config', net_manager) svcs = neutron_plugin_attribute(plugin, 'services', net_manager) ctxts = (neutron_plugin_attribute(plugin, 'contexts', net_manager) or []) resource_map[conf] = {} resource_map[conf]['services'] = svcs resource_map[conf]['contexts'] = ctxts resource_map[conf]['contexts'].append(NeutronComputeContext()) # associate the plugin agent with main network manager config(s) [resource_map[nmc]['services'].extend(svcs) for nmc in nm_rsc] resource_map[NOVA_CONF]['contexts'].append(NeutronComputeContext()) if relation_ids('ceph'): CEPH_RESOURCES[ceph_config_file()] = { 'contexts': [NovaComputeCephContext()], 'services': ['nova-compute'] } resource_map.update(CEPH_RESOURCES) if enable_nova_metadata(): resource_map[NOVA_CONF]['services'].append('nova-api-metadata') return resource_map def restart_map(): ''' Constructs a restart map based on charm config settings and relation state. ''' return {k: v['services'] for k, v in resource_map().iteritems()} def services(): ''' Returns a list of services associate with this charm ''' _services = [] for v in restart_map().values(): _services = _services + v return list(set(_services)) def register_configs(): ''' Returns an OSTemplateRenderer object with all required configs registered. ''' release = os_release('nova-common') configs = templating.OSConfigRenderer(templates_dir=TEMPLATES, openstack_release=release) if relation_ids('ceph'): # Add charm ceph configuration to resources and # ensure directory actually exists mkdir(os.path.dirname(ceph_config_file())) mkdir(os.path.dirname(CEPH_CONF)) # Install ceph config as an alternative for co-location with # ceph and ceph-osd charms - nova-compute ceph.conf will be # lower priority that both of these but thats OK if not os.path.exists(ceph_config_file()): # touch file for pre-templated generation open(ceph_config_file(), 'w').close() install_alternative(os.path.basename(CEPH_CONF), CEPH_CONF, ceph_config_file()) for cfg, d in resource_map().iteritems(): configs.register(cfg, d['contexts']) return configs def determine_packages(): packages = [] + BASE_PACKAGES net_manager = network_manager() if (net_manager in ['flatmanager', 'flatdhcpmanager'] and config('multi-host').lower() == 'yes'): packages.extend(['nova-api', 'nova-network']) elif (net_manager in ['quantum', 'neutron'] and neutron_plugin_legacy_mode()): plugin = neutron_plugin() pkg_lists = neutron_plugin_attribute(plugin, 'packages', net_manager) for pkg_list in pkg_lists: packages.extend(pkg_list) if relation_ids('ceph'): packages.append('ceph-common') virt_type = config('virt-type') try: packages.extend(VIRT_TYPES[virt_type]) except KeyError: log('Unsupported virt-type configured: %s' % virt_type) raise if enable_nova_metadata(): packages.append('nova-api-metadata') if git_install_requested(): packages = list(set(packages)) packages.extend(BASE_GIT_PACKAGES) # don't include packages that will be installed from git for p in GIT_PACKAGE_BLACKLIST: if p in packages: packages.remove(p) return packages def migration_enabled(): # XXX: confirm juju-core bool behavior is the same. return config('enable-live-migration') def quantum_enabled(): manager = config('network-manager') if not manager: return False return manager.lower() == 'quantum' def _network_config(): ''' Obtain all relevant network configuration settings from nova-c-c via cloud-compute interface. ''' settings = ['network_manager', 'neutron_plugin', 'quantum_plugin'] net_config = {} for rid in relation_ids('cloud-compute'): for unit in related_units(rid): for setting in settings: value = relation_get(setting, rid=rid, unit=unit) if value: net_config[setting] = value return net_config def neutron_plugin(): return (_network_config().get('neutron_plugin') or _network_config().get('quantum_plugin')) def network_manager(): ''' Obtain the network manager advertised by nova-c-c, renaming to Quantum if required ''' manager = _network_config().get('network_manager') if manager: manager = manager.lower() if manager not in ['quantum', 'neutron']: return manager if os_release('nova-common') in ['folsom', 'grizzly']: return 'quantum' else: return 'neutron' return manager def public_ssh_key(user='root'): home = pwd.getpwnam(user).pw_dir try: with open(os.path.join(home, '.ssh', 'id_rsa.pub')) as key: return key.read().strip() except: return None def initialize_ssh_keys(user='root'): home_dir = pwd.getpwnam(user).pw_dir ssh_dir = os.path.join(home_dir, '.ssh') if not os.path.isdir(ssh_dir): os.mkdir(ssh_dir) priv_key = os.path.join(ssh_dir, 'id_rsa') if not os.path.isfile(priv_key): log('Generating new ssh key for user %s.' % user) cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048', '-f', priv_key] check_output(cmd) pub_key = '%s.pub' % priv_key if not os.path.isfile(pub_key): log('Generating missing ssh public key @ %s.' % pub_key) cmd = ['ssh-keygen', '-y', '-f', priv_key] p = check_output(cmd).strip() with open(pub_key, 'wb') as out: out.write(p) check_output(['chown', '-R', user, ssh_dir]) def import_authorized_keys(user='root', prefix=None): """Import SSH authorized_keys + known_hosts from a cloud-compute relation. Store known_hosts in user's $HOME/.ssh and authorized_keys in a path specified using authorized-keys-path config option. """ known_hosts = [] authorized_keys = [] if prefix: known_hosts_index = relation_get( '{}_known_hosts_max_index'.format(prefix)) if known_hosts_index: for index in range(0, int(known_hosts_index)): known_hosts.append(relation_get( '{}_known_hosts_{}'.format(prefix, index))) authorized_keys_index = relation_get( '{}_authorized_keys_max_index'.format(prefix)) if authorized_keys_index: for index in range(0, int(authorized_keys_index)): authorized_keys.append(relation_get( '{}_authorized_keys_{}'.format(prefix, index))) else: # XXX: Should this be managed via templates + contexts? known_hosts_index = relation_get('known_hosts_max_index') if known_hosts_index: for index in range(0, int(known_hosts_index)): known_hosts.append(relation_get( 'known_hosts_{}'.format(index))) authorized_keys_index = relation_get('authorized_keys_max_index') if authorized_keys_index: for index in range(0, int(authorized_keys_index)): authorized_keys.append(relation_get( 'authorized_keys_{}'.format(index))) # XXX: Should partial return of known_hosts or authorized_keys # be allowed ? if not len(known_hosts) or not len(authorized_keys): return homedir = pwd.getpwnam(user).pw_dir dest_auth_keys = config('authorized-keys-path').format( homedir=homedir, username=user) dest_known_hosts = os.path.join(homedir, '.ssh/known_hosts') log('Saving new known_hosts file to %s and authorized_keys file to: %s.' % (dest_known_hosts, dest_auth_keys)) with open(dest_known_hosts, 'wb') as _hosts: for index in range(0, int(known_hosts_index)): _hosts.write('{}\n'.format(known_hosts[index])) with open(dest_auth_keys, 'wb') as _keys: for index in range(0, int(authorized_keys_index)): _keys.write('{}\n'.format(authorized_keys[index])) def do_openstack_upgrade(): # NOTE(jamespage) horrible hack to make utils forget a cached value import charmhelpers.contrib.openstack.utils as utils utils.os_rel = None new_src = config('openstack-origin') new_os_rel = get_os_codename_install_source(new_src) log('Performing OpenStack upgrade to %s.' % (new_os_rel)) configure_installation_source(new_src) apt_update(fatal=True) dpkg_opts = [ '--option', 'Dpkg::Options::=--force-confnew', '--option', 'Dpkg::Options::=--force-confdef', ] apt_upgrade(options=dpkg_opts, fatal=True, dist=True) apt_install(determine_packages(), fatal=True) # Regenerate configs in full for new release configs = register_configs() configs.write_all() [service_restart(s) for s in services()] return configs def import_keystone_ca_cert(): """If provided, improt the Keystone CA cert that gets forwarded to compute nodes via the cloud-compute interface """ ca_cert = relation_get('ca_cert') if not ca_cert: return log('Writing Keystone CA certificate to %s' % CA_CERT_PATH) with open(CA_CERT_PATH, 'wb') as out: out.write(b64decode(ca_cert)) check_call(['update-ca-certificates']) def create_libvirt_secret(secret_file, secret_uuid, key): uri = LIBVIRT_URIS[config('virt-type')] if secret_uuid in check_output(['virsh', '-c', uri, 'secret-list']): old_key = check_output(['virsh', '-c', uri, 'secret-get-value', secret_uuid]) if old_key == key: log('Libvirt secret already exists for uuid %s.' % secret_uuid, level=DEBUG) return else: log('Libvirt secret changed for uuid %s.' % secret_uuid, level=INFO) log('Defining new libvirt secret for uuid %s.' % secret_uuid) cmd = ['virsh', '-c', uri, 'secret-define', '--file', secret_file] check_call(cmd) cmd = ['virsh', '-c', uri, 'secret-set-value', '--secret', secret_uuid, '--base64', key] check_call(cmd) def enable_shell(user): cmd = ['usermod', '-s', '/bin/bash', user] check_call(cmd) def disable_shell(user): cmd = ['usermod', '-s', '/bin/false', user] check_call(cmd) def fix_path_ownership(path, user='nova'): cmd = ['chown', user, path] check_call(cmd) def get_topics(): return ['compute'] def assert_charm_supports_ipv6(): """Check whether we are able to support charms ipv6.""" if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty": raise Exception("IPv6 is not supported in the charms for Ubuntu " "versions less than Trusty 14.04") def enable_nova_metadata(): ctxt = MetadataServiceContext()() return 'metadata_shared_secret' in ctxt def neutron_plugin_legacy_mode(): # If a charm is attatched to the neutron-plugin relation then its managing # neutron if relation_ids('neutron-plugin'): return False else: return config('manage-neutron-plugin-legacy-mode') def manage_ovs(): return neutron_plugin_legacy_mode() and neutron_plugin() == 'ovs' def git_install(projects_yaml): """Perform setup, and install git repos specified in yaml parameter.""" if git_install_requested(): git_pre_install() git_clone_and_install(projects_yaml, core_project='nova') git_post_install(projects_yaml) def git_pre_install(): """Perform pre-install setup.""" dirs = [ '/var/lib/nova', '/var/lib/nova/buckets', '/var/lib/nova/CA', '/var/lib/nova/CA/INTER', '/var/lib/nova/CA/newcerts', '/var/lib/nova/CA/private', '/var/lib/nova/CA/reqs', '/var/lib/nova/images', '/var/lib/nova/instances', '/var/lib/nova/keys', '/var/lib/nova/networks', '/var/lib/nova/tmp', '/var/log/nova', ] logs = [ '/var/log/nova/nova-api.log', '/var/log/nova/nova-compute.log', '/var/log/nova/nova-manage.log', '/var/log/nova/nova-network.log', ] adduser('nova', shell='/bin/bash', system_user=True) check_call(['usermod', '--home', '/var/lib/nova', 'nova']) add_group('nova', system_group=True) add_user_to_group('nova', 'nova') add_user_to_group('nova', 'libvirtd') for d in dirs: mkdir(d, owner='nova', group='nova', perms=0755, force=False) for l in logs: write_file(l, '', owner='nova', group='nova', perms=0644) def git_post_install(projects_yaml): """Perform post-install setup.""" src_etc = os.path.join(git_src_dir(projects_yaml, 'nova'), 'etc/nova') configs = [ {'src': src_etc, 'dest': '/etc/nova'}, ] for c in configs: if os.path.exists(c['dest']): shutil.rmtree(c['dest']) shutil.copytree(c['src'], c['dest']) virt_type = VIRT_TYPES[config('virt-type')][0] nova_compute_conf = 'git/{}.conf'.format(virt_type) render(nova_compute_conf, '/etc/nova/nova-compute.conf', {}, perms=0o644) render('git/nova_sudoers', '/etc/sudoers.d/nova_sudoers', {}, perms=0o440) service_name = 'nova-compute' nova_user = 'nova' start_dir = '/var/lib/nova' nova_conf = 'etc/nova/nova.conf' nova_api_metadata_context = { 'service_description': 'Nova Metadata API server', 'service_name': service_name, 'user_name': nova_user, 'start_dir': start_dir, 'process_name': 'nova-api-metadata', 'executable_name': '/usr/local/bin/nova-api-metadata', 'config_files': [nova_conf], } nova_api_context = { 'service_description': 'Nova API server', 'service_name': service_name, 'user_name': nova_user, 'start_dir': start_dir, 'process_name': 'nova-api', 'executable_name': '/usr/local/bin/nova-api', 'config_files': [nova_conf], } nova_compute_context = { 'service_description': 'Nova compute worker', 'service_name': service_name, 'user_name': nova_user, 'process_name': 'nova-compute', 'executable_name': '/usr/local/bin/nova-compute', 'config_files': [nova_conf, '/etc/nova/nova-compute.conf'], } nova_network_context = { 'service_description': 'Nova network worker', 'service_name': service_name, 'user_name': nova_user, 'start_dir': start_dir, 'process_name': 'nova-network', 'executable_name': '/usr/local/bin/nova-network', 'config_files': [nova_conf], } # NOTE(coreycb): Needs systemd support templates_dir = 'hooks/charmhelpers/contrib/openstack/templates' templates_dir = os.path.join(charm_dir(), templates_dir) render('git.upstart', '/etc/init/nova-api-metadata.conf', nova_api_metadata_context, perms=0o644, templates_dir=templates_dir) render('git.upstart', '/etc/init/nova-api.conf', nova_api_context, perms=0o644, templates_dir=templates_dir) render('git/upstart/nova-compute.upstart', '/etc/init/nova-compute.conf', nova_compute_context, perms=0o644) render('git.upstart', '/etc/init/nova-network.conf', nova_network_context, perms=0o644, templates_dir=templates_dir) apt_update() apt_install(LATE_GIT_PACKAGES, fatal=True)