diff --git a/startvms.py b/startvms.py new file mode 100644 index 0000000..937e217 --- /dev/null +++ b/startvms.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +'''Start virtual machines when a network service becomes available + +Example OpenRC init script:: + + #!/sbin/runscript + + depend() { + need libvirtd + } + + start() { + ebegin "Starting additional virtual machines" + startvms -l ${STARTVMS_LOG:-/var/log/startvms.log} ${STARTVMS_ARGS} \ + ${VIRTUAL_MACHINES} + eend $? + } +''' + +import argparse +import datetime +import libvirt +import logging +import os +import signal +import socket +import sys +import time + + +LIBVIRT_DEFAULT_URI = os.environ.get('LIBVIRT_DEFAULT_URI', 'qemu:///session') +WAIT_FOR_HOST = "arcturus.pyrocufflink.jazz" +WAIT_FOR_PORT = 389 +WAIT_TIMEOUT = 300 # seconds + + +log = logging.getLogger('startvms') + + +class Timeout(Exception): + pass + + +class MaxLevelFilter(logging.Filter): + + def __init__(self, max_level=logging.WARNING, *args, **kwargs): + logging.Filter.__init__(self, *args, **kwargs) + self.max_level = max_level + + def filter(self, record): + return record.levelno <= self.max_level + + +def _parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--debug', action='store_true', default=False) + parser.add_argument('--foreground', '-F', action='store_true', + default=False, + help='Do not fork into the background before waiting') + parser.add_argument('--log-file', '-l', metavar='FILENAME', + help='Send log messages to FILENAME') + parser.add_argument('--connect', '-c', metavar='URI', dest='uri', + default=LIBVIRT_DEFAULT_URI, + help='libvirt connection URI') + parser.add_argument('--timeout', '-t', default=WAIT_TIMEOUT, type=int, + metavar='SECONDS', + help='Wait a maximum of SECONDS') + parser.add_argument('--port', '-p', default=WAIT_FOR_PORT, type=int, + help='Wait for PORT on remote host') + parser.add_argument('--host', '-H', default=WAIT_FOR_HOST, + help='Wait for HOST to come up before starting VMs') + parser.add_argument('vms', nargs='+', metavar='VMNAME', + help='Virtual machine(s) to start') + return parser.parse_args() + + +def _setup_logging(debug=False, log_file=None): + root_log = logging.getLogger() + root_log.setLevel(logging.NOTSET) + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setLevel(logging.ERROR) + root_log.addHandler(stderr_handler) + if log_file: + file_handler = logging.FileHandler(log_file) + if debug: + file_handler.setLevel(logging.DEBUG) + else: + file_handler.setLevel(logging.INFO) + file_formatter = logging.Formatter( + '%(asctime)s [%(name)s] %(levelname)s %(message)s' + ) + file_handler.setFormatter(file_formatter) + root_log.addHandler(file_handler) + else: + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.addFilter(MaxLevelFilter()) + if debug: + stdout_handler.setLevel(logging.DEBUG) + else: + stdout_handler.setLevel(logging.WARNING) + root_log.addHandler(stdout_handler) + + +def _daemonize(): + if os.fork(): + raise SystemExit(0) + signal.signal(signal.SIGHUP, signal.SIG_IGN) + os.setsid() + os.chdir('/') + if os.fork(): + raise SystemExit(0) + + +def wait_for(host, port, timeout=None): + log.info('Waiting {time} for port {port} on {host}'.format( + time='{0} seconds'.format(timeout) if timeout else 'indefinitely', + port=port, + host=host, + )) + if timeout is not None: + end = datetime.datetime.now() + datetime.timedelta(seconds=timeout) + keep_going = lambda: datetime.datetime.now() < end + else: + keep_going = lambda: True + while keep_going(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + try: + log.debug('Attempting connection to {0}:{1}'.format(host, port)) + s.connect((host, port)) + except: + log.debug('Connection failed') + time.sleep(1) + else: + log.debug('Connection succeeded') + s.shutdown(socket.SHUT_RDWR) + break + finally: + s.close() + else: + raise Timeout('Timed out waiting for port {port} on {host}'.format( + port=port, + host=host, + )) + + +def start_vm(conn, name): + domain = conn.lookupByName(name) + if domain.isActive(): + log.debug('Domain {0} is already active, skipping'.format(name)) + else: + log.info('Starting domain {0}'.format(name)) + domain.create() + + +def main(): + args = _parse_args() + if not args.foreground: + _daemonize() + _setup_logging(args.debug, args.log_file) + + try: + wait_for(args.host, args.port, args.timeout) + except Timeout as e: + log.error('{0}'.format(e)) + raise SystemExit(os.EX_NOHOST) + + try: + conn = libvirt.open(args.uri) + except libvirt.libvirtError as e: + log.error('{0}'.format(e)) + raise SystemExit(os.EX_UNAVAILABLE) + error = False + for vm in args.vms: + try: + start_vm(conn, vm) + except libvirt.libvirtError as e: + error = True + log.error('{0}'.format(e)) + continue + raise SystemExit(int(not error)) + + +if __name__ == '__main__': + main()