"""questions.py All of our subclasses of Question live here. We might break this file up into multiple pieces at some point, but for now, we're keeping things simple (if a big lengthy) ---------------------------------------------------------------------- Copyright 2019 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. """ import json from time import sleep from os import path from init.shell import (check, call, check_output, sql, nc_wait, log_wait, restart, download) from init.config import Env, log from init.questions.question import Question from init.questions import clustering, network, uninstall # noqa F401 _env = Env().get_env() class ConfigError(Exception): """Suitable error to raise in case there is an issue with the snapctl config or environment vars. """ class Clustering(Question): """Possibly setup clustering.""" _type = 'boolean' _question = 'Do you want to setup clustering?' config_key = 'config.clustered' interactive = True def yes(self, answer: bool): log.info('Configuring clustering ...') questions = [ clustering.Role(), clustering.Password(), clustering.ControlIp(), clustering.ComputeIp(), # Automagically skipped role='control' ] for question in questions: if not self.interactive: question.interactive = False question.ask() role = check_output('snapctl', 'get', 'config.cluster.role') control_ip = check_output('snapctl', 'get', 'config.network.control-ip') password = check_output('snapctl', 'get', 'config.cluster.password') log.debug('Role: {}, IP: {}, Password: {}'.format( role, control_ip, password)) # TODO: raise an exception if any of the above are None (can # happen if we're automatig and mess up our params.) if role == 'compute': log.info('I am a compute node.') # Gets config info and sets local env vals. check_output('microstack_join') # Set default question answers. check('snapctl', 'set', 'config.services.control-plane=false') check('snapctl', 'set', 'config.services.hypervisor=true') if role == 'control': log.info('I am a control node.') check('snapctl', 'set', 'config.services.control-plane=true') # We want to run a hypervisor on our control plane nodes # -- this is essentially a hyper converged cloud. check('snapctl', 'set', 'config.services.hypervisor=true') # TODO: if this is run after init has already been called, # need to restart services. # Write templates check('snap-openstack', 'setup') def no(self, answer: bool): # Turn off cluster server # TODO: it would be more secure to reverse this -- only enable # to service if we are doing clustering. check('snapctl', 'stop', '--disable', 'microstack.cluster-server') class ConfigQuestion(Question): """Question class that simply asks for and sets a config value. All we need to do is run 'snap-openstack setup' after we have saved off the value. The value to be set is specified by the name of the question class. """ def after(self, answer): """Our value has been saved. Run 'snap-openstack setup' to write it out, and load any changes to microstack.rc. # TODO this is a bit messy and redundant. Come up with a clean way of loading and writing config after the run of ConfigQuestions have been asked. """ check('snap-openstack', 'setup') # TODO: get rid of this? (I think that it has become redundant) mstackrc = '{SNAP_COMMON}/etc/microstack.rc'.format(**_env) with open(mstackrc, 'r') as rc_file: for line in rc_file.readlines(): if not line.startswith('export'): continue key, val = line[7:].split('=') _env[key.strip()] = val.strip() class Dns(Question): """Possibly override default dns.""" _type = 'string' _question = 'DNS to use' config_key = 'config.network.dns' def yes(self, answer: str): """Override the default dhcp_agent.ini file.""" file_path = '{SNAP_COMMON}/etc/neutron/dhcp_agent.ini'.format(**_env) with open(file_path, 'w') as f: f.write("""\ [DEFAULT] interface_driver = openvswitch dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq enable_isolated_metadata = True dnsmasq_dns_servers = {answer} """.format(answer=answer)) # Neutron is not actually started at this point, so we don't # need to restart. # TODO: This isn't idempotent, because it will behave # differently if we re-run this script when neutron *is* # started. Need to figure that out. class NetworkSettings(Question): """Write network settings, and """ _type = 'auto' _question = 'Network settings' def yes(self, answer): log.info('Configuring networking ...') # OpenvSwitch services may not have started up properly restart('ovsdb-server') restart('ovs-vswitchd') network.ExtGateway().ask() network.ExtCidr().ask() # Now that we have default or overriden values, setup the # bridge and write all the proper values into our config # files. check('setup-br-ex') check('snap-openstack', 'setup') network.IpForwarding().ask() class OsPassword(ConfigQuestion): _type = 'string' _question = 'Openstack Admin Password' config_key = 'config.credentials.os-password' def yes(self, answer): _env['ospassword'] = answer # TODO obfuscate the password! class ForceQemu(Question): _type = 'boolean' config_key = 'config.host.check-qemu' def yes(self, answer: str) -> None: """Possibly force us to use qemu emulation rather than kvm.""" cpuinfo = check_output('cat', '/proc/cpuinfo') if 'vmx' in cpuinfo or 'svm' in cpuinfo: # We have processor extensions installed. No need to Force # Qemu emulation. return _path = '{SNAP_COMMON}/etc/nova/nova.conf.d/hypervisor.conf'.format( **_env) with open(_path, 'w') as _file: _file.write("""\ [DEFAULT] compute_driver = libvirt.LibvirtDriver [workarounds] disable_rootwrap = True [libvirt] virt_type = qemu cpu_mode = host-model """) # TODO: restart nova services when re-running this after init. class VmSwappiness(Question): _type = 'boolean' _question = 'Do you wish to set vm swappiness to 1? (recommended)' def yes(self, answer: str) -> None: # TODO pass class FileHandleLimits(Question): _type = 'boolean' _question = 'Do you wish to increase file handle limits? (recommended)' def yes(self, answer: str) -> None: # TODO pass class DashboardAccess(ConfigQuestion): _type = 'string' _question = 'Dashboard allowed hosts.' config_key = 'config.network.dashboard-allowed-hosts' def yes(self, answer): log.info("Opening horizon dashboard up to {hosts}".format( hosts=answer)) class RabbitMq(Question): """Wait for Rabbit to start, then setup permissions.""" _type = 'boolean' config_key = 'config.services.control-plane' def _wait(self) -> None: restart('rabbitmq-server') # Restart server for plugs rabbit_port = check_output( 'snapctl', 'get', 'config.network.ports.rabbit') nc_wait(_env['control_ip'], rabbit_port) log_file = '{SNAP_COMMON}/log/rabbitmq/startup_log'.format(**_env) log_wait(log_file, 'completed') def _configure(self) -> None: """Configure RabbitMQ (actions may have already been run, in which case we fail silently). """ # Configure RabbitMQ check('{SNAP}/bin/setup-rabbit'.format(**_env)) def yes(self, answer: str) -> None: log.info('Waiting for RabbitMQ to start ...') self._wait() log.info('RabbitMQ started!') log.info('Configuring RabbitMQ ...') self._configure() log.info('RabbitMQ Configured!') def no(self, answer: str): log.info('Disabling local rabbit ...') check('snapctl', 'stop', '--disable', 'microstack.rabbitmq-server') class DatabaseSetup(Question): """Setup keystone permissions, then setup all databases.""" _type = 'boolean' config_key = 'config.services.control-plane' def _wait(self) -> None: mysql_port = check_output( 'snapctl', 'get', 'config.network.ports.mysql') nc_wait(_env['control_ip'], mysql_port) log_wait('{SNAP_COMMON}/log/mysql/error.log'.format(**_env), 'mysqld: ready for connections.') def _create_dbs(self) -> None: # TODO: actually use passwords here. for db in ('neutron', 'nova', 'nova_api', 'nova_cell0', 'cinder', 'glance', 'keystone'): sql("CREATE DATABASE IF NOT EXISTS {db};".format(db=db)) sql( "GRANT ALL PRIVILEGES ON {db}.* TO {db}@{control_ip} \ IDENTIFIED BY '{db}';".format(db=db, **_env)) # Grant nova user access to cell0 sql( "GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'{control_ip}' \ IDENTIFIED BY \'nova';".format(**_env)) def _bootstrap(self) -> None: if call('openstack', 'user', 'show', 'admin'): return bootstrap_url = 'http://{control_ip}:5000/v3/'.format(**_env) check('snap-openstack', 'launch', 'keystone-manage', 'bootstrap', '--bootstrap-password', _env['ospassword'], '--bootstrap-admin-url', bootstrap_url, '--bootstrap-internal-url', bootstrap_url, '--bootstrap-public-url', bootstrap_url) def yes(self, answer: str) -> None: """Setup Databases. Create all the MySQL databases we require, then setup the fernet keys and create the service project. """ log.info('Waiting for MySQL server to start ...') self._wait() log.info('Mysql server started! Creating databases ...') self._create_dbs() check('snapctl', 'set', 'database.ready=true') # Start keystone-uwsgi. We use snapctl, because systemd # doesn't yet know about the service. check('snapctl', 'start', 'microstack.nginx') check('snapctl', 'start', 'microstack.keystone-uwsgi') log.info('Configuring Keystone Fernet Keys ...') check('snap-openstack', 'launch', 'keystone-manage', 'fernet_setup', '--keystone-user', 'root', '--keystone-group', 'root') check('snap-openstack', 'launch', 'keystone-manage', 'db_sync') restart('keystone-uwsgi') log.info('Bootstrapping Keystone ...') self._bootstrap() log.info('Creating service project ...') if not call('openstack', 'project', 'show', 'service'): check('openstack', 'project', 'create', '--domain', 'default', '--description', 'Service Project', 'service') log.info('Keystone configured!') def no(self, answer: str): # We assume that the control node has a connection setup for us. check('snapctl', 'set', 'database.ready=true') log.info('Disabling local MySQL ...') check('snapctl', 'stop', '--disable', 'microstack.mysqld') class NovaHypervisor(Question): """Run the nova compute hypervisor.""" _type = 'boolean' config_key = 'config.services.hypervisor' def yes(self, answer): log.info('Configuring nova compute hypervisor ...') if not call('openstack', 'service', 'show', 'compute'): check('openstack', 'service', 'create', '--name', 'nova', '--description', '"Openstack Compute"', 'compute') # TODO make sure that we are the control plane before executing # TODO if control plane is not hypervisor, still create this for endpoint in ['public', 'internal', 'admin']: call('openstack', 'endpoint', 'create', '--region', 'microstack', 'compute', endpoint, 'http://{compute_ip}:8774/v2.1'.format(**_env)) check('snapctl', 'start', 'microstack.nova-compute') def no(self, answer): log.info('Disabling nova compute service ...') check('snapctl', 'stop', '--disable', 'microstack.nova-compute') class NovaControlPlane(Question): """Create all control plane nova users and services.""" _type = 'boolean' config_key = 'config.services.control-plane' def _flavors(self) -> None: """Create default flavors.""" if not call('openstack', 'flavor', 'show', 'm1.tiny'): check('openstack', 'flavor', 'create', '--id', '1', '--ram', '512', '--disk', '1', '--vcpus', '1', 'm1.tiny') if not call('openstack', 'flavor', 'show', 'm1.small'): check('openstack', 'flavor', 'create', '--id', '2', '--ram', '2048', '--disk', '20', '--vcpus', '1', 'm1.small') if not call('openstack', 'flavor', 'show', 'm1.medium'): check('openstack', 'flavor', 'create', '--id', '3', '--ram', '4096', '--disk', '20', '--vcpus', '2', 'm1.medium') if not call('openstack', 'flavor', 'show', 'm1.large'): check('openstack', 'flavor', 'create', '--id', '4', '--ram', '8192', '--disk', '20', '--vcpus', '4', 'm1.large') if not call('openstack', 'flavor', 'show', 'm1.xlarge'): check('openstack', 'flavor', 'create', '--id', '5', '--ram', '16384', '--disk', '20', '--vcpus', '8', 'm1.xlarge') def yes(self, answer: str) -> None: log.info('Configuring nova control plane services ...') if not call('openstack', 'user', 'show', 'nova'): check('openstack', 'user', 'create', '--domain', 'default', '--password', 'nova', 'nova') check('openstack', 'role', 'add', '--project', 'service', '--user', 'nova', 'admin') if not call('openstack', 'user', 'show', 'placement'): check('openstack', 'user', 'create', '--domain', 'default', '--password', 'placement', 'placement') check('openstack', 'role', 'add', '--project', 'service', '--user', 'placement', 'admin') if not call('openstack', 'service', 'show', 'placement'): check('openstack', 'service', 'create', '--name', 'placement', '--description', '"Placement API"', 'placement') for endpoint in ['public', 'internal', 'admin']: call('openstack', 'endpoint', 'create', '--region', 'microstack', 'placement', endpoint, 'http://{control_ip}:8778'.format(**_env)) # Use snapctl to start nova services. We need to call them # out manually, because systemd doesn't know about them yet. # TODO: parse the output of `snapctl services` to get this # list automagically. for service in [ 'microstack.nova-api', ]: check('snapctl', 'start', service) check('snap-openstack', 'launch', 'nova-manage', 'api_db', 'sync') if 'cell0' not in check_output('snap-openstack', 'launch', 'nova-manage', 'cell_v2', 'list_cells'): check('snap-openstack', 'launch', 'nova-manage', 'cell_v2', 'map_cell0') if 'cell1' not in check_output('snap-openstack', 'launch', 'nova-manage', 'cell_v2', 'list_cells'): check('snap-openstack', 'launch', 'nova-manage', 'cell_v2', 'create_cell', '--name=cell1', '--verbose') check('snap-openstack', 'launch', 'nova-manage', 'db', 'sync') restart('nova-api') restart('nova-compute') for service in [ 'microstack.nova-api-metadata', 'microstack.nova-conductor', 'microstack.nova-scheduler', 'microstack.nova-uwsgi', ]: check('snapctl', 'start', service) nc_wait(_env['compute_ip'], '8774') sleep(5) # TODO: log_wait log.info('Creating default flavors...') self._flavors() def no(self, answer): log.info('Disabling nova control plane services ...') for service in [ 'microstack.nova-uwsgi', 'microstack.nova-api', 'microstack.nova-conductor', 'microstack.nova-scheduler', 'microstack.nova-api-metadata']: check('snapctl', 'stop', '--disable', service) class NeutronControlPlane(Question): """Create all relevant neutron services and users.""" _type = 'boolean' config_key = 'config.services.control-plane' def yes(self, answer: str) -> None: log.info('Configuring Neutron') if not call('openstack', 'user', 'show', 'neutron'): check('openstack', 'user', 'create', '--domain', 'default', '--password', 'neutron', 'neutron') check('openstack', 'role', 'add', '--project', 'service', '--user', 'neutron', 'admin') if not call('openstack', 'service', 'show', 'network'): check('openstack', 'service', 'create', '--name', 'neutron', '--description', '"OpenStack Network"', 'network') for endpoint in ['public', 'internal', 'admin']: call('openstack', 'endpoint', 'create', '--region', 'microstack', 'network', endpoint, 'http://{control_ip}:9696'.format(**_env)) for service in [ 'microstack.neutron-api', 'microstack.neutron-dhcp-agent', 'microstack.neutron-l3-agent', 'microstack.neutron-metadata-agent', 'microstack.neutron-openvswitch-agent', ]: check('snapctl', 'start', service) check('snap-openstack', 'launch', 'neutron-db-manage', 'upgrade', 'head') for service in [ 'microstack.neutron-api', 'microstack.neutron-dhcp-agent', 'microstack.neutron-l3-agent', 'microstack.neutron-metadata-agent', 'microstack.neutron-openvswitch-agent', ]: check('snapctl', 'restart', service) nc_wait(_env['control_ip'], '9696') sleep(5) # TODO: log_wait if not call('openstack', 'network', 'show', 'test'): check('openstack', 'network', 'create', 'test') if not call('openstack', 'subnet', 'show', 'test-subnet'): check('openstack', 'subnet', 'create', '--network', 'test', '--subnet-range', '192.168.222.0/24', 'test-subnet') if not call('openstack', 'network', 'show', 'external'): check('openstack', 'network', 'create', '--external', '--provider-physical-network=physnet1', '--provider-network-type=flat', 'external') if not call('openstack', 'subnet', 'show', 'external-subnet'): check('openstack', 'subnet', 'create', '--network', 'external', '--subnet-range', _env['extcidr'], '--no-dhcp', 'external-subnet') if not call('openstack', 'router', 'show', 'test-router'): check('openstack', 'router', 'create', 'test-router') check('openstack', 'router', 'add', 'subnet', 'test-router', 'test-subnet') check('openstack', 'router', 'set', '--external-gateway', 'external', 'test-router') def no(self, answer): """Create endpoints pointed at control node if we're not setting up neutron on this machine. """ # Make sure that the agent is running. for service in [ 'microstack.neutron-openvswitch-agent', ]: check('snapctl', 'start', service) # Disable the other services. for service in [ 'microstack.neutron-api', 'microstack.neutron-dhcp-agent', 'microstack.neutron-metadata-agent', 'microstack.neutron-l3-agent', ]: check('snapctl', 'stop', '--disable', service) class GlanceSetup(Question): """Setup glance, and download an initial Cirros image.""" _type = 'boolean' config_key = 'config.services.control-plane' def _fetch_cirros(self) -> None: if call('openstack', 'image', 'show', 'cirros'): return log.info('Adding cirros image ...') env = dict(**_env) env['VER'] = '0.4.0' env['IMG'] = 'cirros-{VER}-x86_64-disk.img'.format(**env) cirros_path = '{SNAP_COMMON}/images/{IMG}'.format(**env) if not path.exists(cirros_path): check('mkdir', '-p', '{SNAP_COMMON}/images'.format(**env)) log.info('Downloading cirros image ...') download( 'http://download.cirros-cloud.net/{VER}/{IMG}'.format(**env), '{SNAP_COMMON}/images/{IMG}'.format(**env)) check('openstack', 'image', 'create', '--file', '{SNAP_COMMON}/images/{IMG}'.format(**env), '--public', '--container-format=bare', '--disk-format=qcow2', 'cirros') def yes(self, answer: str) -> None: log.info('Configuring Glance ...') if not call('openstack', 'user', 'show', 'glance'): check('openstack', 'user', 'create', '--domain', 'default', '--password', 'glance', 'glance') check('openstack', 'role', 'add', '--project', 'service', '--user', 'glance', 'admin') if not call('openstack', 'service', 'show', 'image'): check('openstack', 'service', 'create', '--name', 'glance', '--description', '"OpenStack Image"', 'image') for endpoint in ['internal', 'admin', 'public']: check('openstack', 'endpoint', 'create', '--region', 'microstack', 'image', endpoint, 'http://{compute_ip}:9292'.format(**_env)) for service in [ 'microstack.glance-api', 'microstack.registry', # TODO rename to glance-registery ]: check('snapctl', 'start', service) check('snap-openstack', 'launch', 'glance-manage', 'db_sync') restart('glance-api') restart('registry') nc_wait(_env['compute_ip'], '9292') sleep(5) # TODO: log_wait self._fetch_cirros() def no(self, answer): check('snapctl', 'stop', '--disable', 'microstack.glance-api') check('snapctl', 'stop', '--disable', 'microstack.registry') class SecurityRules(Question): """Setup default security rules.""" _type = 'boolean' config_key = 'config.network.security-rules' def yes(self, answer: str) -> None: # Create security group rules log.info('Creating security group rules ...') group_id = check_output('openstack', 'security', 'group', 'list', '--project', 'admin', '-f', 'value', '-c', 'ID') rules = check_output('openstack', 'security', 'group', 'rule', 'list', '--format', 'json') ping_rule = False ssh_rule = False for rule in json.loads(rules): if rule['Security Group'] == group_id: if rule['IP Protocol'] == 'icmp': ping_rule = True if rule['IP Protocol'] == 'tcp': ssh_rule = True if not ping_rule: check('openstack', 'security', 'group', 'rule', 'create', group_id, '--proto', 'icmp') if not ssh_rule: check('openstack', 'security', 'group', 'rule', 'create', group_id, '--proto', 'tcp', '--dst-port', '22') class PostSetup(Question): """Sneak in any additional cleanup, then set the initialized state.""" config_key = 'config.post-setup' def yes(self, answer: str) -> None: log.info('restarting libvirt and virtlogd ...') # This fixes an issue w/ logging not getting set. # TODO: fix issue. restart('libvirtd') restart('virtlogd') # Start horizon check('snapctl', 'start', 'microstack.horizon-uwsgi') check('snapctl', 'set', 'initialized=true') log.info('Complete. Marked microstack as initialized!') class SimpleServiceQuestion(Question): def yes(self, answer: str) -> None: log.info('enabling and starting ' + self.__class__.__name__) for service in self.services: check('snapctl', 'start', '--enable', service) log.info(self.__class__.__name__ + ' enabled') def no(self, answer): for service in self.services: check('snapctl', 'stop', '--disable', service) class ExtraServicesQuestion(Question): _type = 'boolean' _question = 'Do you want to setup extra services?' config_key = 'config.services.extra.enabled' interactive = True def yes(self, answer: bool): questions = [ Filebeat(), Telegraf(), Nrpe(), ] for question in questions: if not self.interactive: question.interactive = False question.ask() def no(self, answer: bool): pass class Filebeat(SimpleServiceQuestion): _type = 'boolean' _question = 'Do you want to enable Filebeat?' config_key = 'config.services.extra.filebeat' interactive = True @property def services(self): return [ '{SNAP_INSTANCE_NAME}.filebeat'.format(**_env) ] class Telegraf(SimpleServiceQuestion): _type = 'boolean' _question = 'Do you want to enable Telegraf?' config_key = 'config.services.extra.telegraf' interactive = True @property def services(self): return [ '{SNAP_INSTANCE_NAME}.telegraf'.format(**_env) ] class Nrpe(SimpleServiceQuestion): _type = 'boolean' _question = 'Do you want to enable NRPE?' config_key = 'config.services.extra.nrpe' interactive = True @property def services(self): return [ '{SNAP_INSTANCE_NAME}.nrpe'.format(**_env) ]