microstack/tools/init/init/main.py

279 lines
9.2 KiB
Python

"""Microstack Init
Initialize the databases and configuration files of a microstack
install.
We structure our init in the form of 'Question' classes, each of which
has an 'ask' routine, run in the order laid out in the
question_classes in the main function in this file.
.ask will either ask the user a question, and run the appropriate
routine in the Question class, or simply automatically run a routine
without input from the user (in the case of 'required' questions).
----------------------------------------------------------------------
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 argparse
import logging
import sys
import socket
import ipaddress
from functools import wraps
from init.config import log
from init.shell import (
default_network,
check,
check_output,
config_set,
fallback_source_address,
)
from init import questions
def requires_sudo(func):
@wraps(func)
def wrapper(*args, **kwargs):
if int(check_output('id', '-u')):
log.error("This script must be run with root privileges. "
"Please re-run with sudo.")
sys.exit(1)
return func(*args, **kwargs)
return wrapper
def check_file_size_positive(value):
ival = int(value)
if ival < 1:
raise argparse.ArgumentTypeError(
f'The file size for a loop device'
f' must be larger than 1GB, current: {value}')
return ival
def check_source_ip_address_valid(value):
try:
addr = ipaddress.ip_address(value)
except ValueError as e:
raise argparse.ArgumentTypeError(
'Invalid source IP address provided in as an argument.'
) from e
return addr
def parse_init_args():
parser = argparse.ArgumentParser()
parser.add_argument('--auto', '-a', action='store_true',
help='Run non interactively.')
parser.add_argument('--join', '-j',
dest='connection_string',
help='Pass a connection string generated by the'
' add-compute command at the control node'
' (required for compute nodes, unused for control'
' nodes).')
parser.add_argument('--compute', action='store_true')
parser.add_argument('--control', action='store_true')
parser.add_argument('--debug', action='store_true')
parser.add_argument(
'--setup-loop-based-cinder-lvm-backend',
default=False,
action='store_true',
help='(experimental) set up a loop device-backed'
' LVM backend for Cinder.'
)
parser.add_argument(
'--loop-device-file-size',
type=check_file_size_positive, default=32,
help=('File size in GB (10^9) of a file to be exposed as a loop'
' device for the Cinder LVM backend.')
)
parser.add_argument('--default-source-ip',
dest='default_source_ip',
type=check_source_ip_address_valid,
default=fallback_source_address(),
help='The IP address to be used by MicroStack'
' services as a source IP where possible. This'
' option can be useful for multi-homed nodes.')
args = parser.parse_args()
return args
def process_init_args(args):
"""Look through our args object and set the proper default config
values in our snap config, based on those args.
"""
if args.auto and not (args.control or args.compute):
raise ValueError('A role (--compute or --control) must be specified '
' when using --auto')
if args.compute or args.control:
config_set(**{'config.is-clustered': 'true'})
if args.compute:
config_set(**{'config.cluster.role': 'compute'})
if args.control:
# If both compute and control are passed for some reason, we
# wind up with the role of 'control', which is best, as a
# control node also serves as a compute node in our hyper
# converged architecture.
config_set(**{'config.cluster.role': 'control'})
if args.connection_string:
config_set(**{
'config.cluster.connection-string.raw': args.connection_string})
config_set(**{
'config.network.default-source-ip': args.default_source_ip})
if args.auto and not args.control and not args.connection_string:
raise ValueError('The connection string parameter must be specified'
' for compute nodes.')
if args.debug:
log.setLevel(logging.DEBUG)
config_set(**{
'config.cinder.setup-loop-based-cinder-lvm-backend':
f'{str(args.setup_loop_based_cinder_lvm_backend).lower()}',
'config.cinder.loop-device-file-size':
f'{args.loop_device_file_size}G',
})
return args.auto
@requires_sudo
def init() -> None:
args = parse_init_args()
auto = process_init_args(args)
# Do not ask about this if a CLI argument asking for it has been
# provided already.
cinder_lvm_question = questions.CinderVolumeLVMSetup()
if args.setup_loop_based_cinder_lvm_backend:
cinder_lvm_question.interactive = False
question_list = [
questions.DnsServers(),
questions.DnsDomain(),
questions.NetworkSettings(),
questions.OsPassword(), # TODO: turn this off if COMPUTE.
# The following are not yet implemented:
# questions.VmSwappiness(),
# questions.FileHandleLimits(),
questions.TlsCertificates(),
questions.DashboardAccess(),
questions.RabbitMq(),
questions.DatabaseSetup(),
questions.PlacementSetup(),
questions.NovaControlPlane(),
questions.NovaHypervisor(),
questions.NovaSpiceConsoleSetup(),
questions.NeutronControlPlane(),
questions.GlanceSetup(),
questions.SecurityRules(),
questions.CinderSetup(),
cinder_lvm_question,
questions.PostSetup(),
questions.ExtraServicesQuestion(),
]
clustering_question = questions.Clustering()
# If the connection string is specified we definitely
# want to set up clustering and we don't need to ask.
if args.connection_string:
if args.auto:
clustering_question.interactive = False
if args.control:
raise ValueError('Joining additional control nodes is'
' not supported.')
elif args.compute:
clustering_question.role_interactive = False
config_set(**{'config.is-clustered': True})
clustering_question.connection_string_interactive = False
clustering_question.yes(answer=True)
else:
if args.control or args.compute:
clustering_question.role_interactive = False
# The same code-path as for other questions will be executed.
question_list.insert(0, clustering_question)
for question in question_list:
if auto:
# Force all questions to be non-interactive if we passed --auto.
question.interactive = False
try:
question.ask()
except questions.ConfigError as e:
log.critical(e)
sys.exit(1)
def set_network_info() -> None:
"""Find and use the default network on a machine.
Helper to find the default network on a machine, and configure
MicroStack to use it in its default settings.
"""
try:
ip, gate, cidr = default_network()
except Exception:
# TODO: more specific exception handling.
log.exception(
'Could not determine default network info. '
'Falling back on 10.20.20.1')
return
check('snapctl', 'set', 'config.network.ext-gateway={}'.format(gate))
check('snapctl', 'set', 'config.network.ext-cidr={}'.format(cidr))
check('snapctl', 'set', 'config.network.control-ip={}'.format(ip))
check('snapctl', 'set',
'config.network.node-fqdn={}'.format(socket.getfqdn()))
@requires_sudo
def remove() -> None:
"""Helper to cleanly uninstall MicroStack."""
# Strip '--auto' out of the args passed to this command, as we
# need to check it, but also pass the other args off to the
# snapd's uninstall command. TODO: make this less hacky.
auto = False
if '--auto' in questions.uninstall.ARGS:
auto = True
questions.uninstall.ARGS = [
arg for arg in questions.uninstall.ARGS if 'auto' not in arg]
question_list = [
questions.uninstall.DeleteBridge(),
questions.uninstall.RemoveMicrostack(),
]
for question in question_list:
if auto:
question.interactive = False
question.ask()