diff --git a/mc-vm-manager/VMHelper.py b/mc-vm-manager/VMHelper.py index 2c743b3..c717604 100644 --- a/mc-vm-manager/VMHelper.py +++ b/mc-vm-manager/VMHelper.py @@ -1,12 +1,21 @@ import json import subprocess import os, errno +import threading +import time +from qmp import QEMUMonitorProtocol class VMHelper: - def __init__(self): - self.config = json.load(open("/root/tmp/config.json")) + def __init__(self, filename): + self.config = json.load(open(filename)) - def getPid(self,vmid): + def getVmIds(self): + if ('VMs' in self.config): + return [x for x , y in self.config['VMs'].items()] + else: + return [] + + def getPid(self,vmid) -> "int or None": result = None try: with open(self.config['kvm']['pidfile'].replace("$VMID", vmid)) as pidfd: @@ -17,23 +26,44 @@ class VMHelper: return result - def process_running(self,vmid): + def process_running(self,vmid) -> bool: pid = self.getPid(vmid) if pid is None: return False - result = None try: os.kill(pid,0) except OSError as e: - result = e.errno != errno.ESRCH + return e.errno != errno.ESRCH else: - result = True + return True + def getQMPStatus(self,vmid): + # cmd doku http://git.qemu.org/?p=qemu.git;a=blob;f=qmp-commands.hx;h=1e0e11ee32571209e2dfce41b5c18f01d6ad3880;hb=HEAD + proto = QEMUMonitorProtocol(self.config['kvm']['qmpsocket'].replace("$VMID", vmid)) + proto.connect() + + result = {} + + queryKvm = proto.command("query-kvm") + result["kvm-enabled"] = queryKvm["enabled"] + result["kvm-present"] = queryKvm["present"] + + result["status"] = proto.command("query-status")["status"] + + proto.close() return result + def autostartVMs(self): + if ('VMs' in self.config): + for vmid, vmcfg in self.config['VMs'].items(): + if "autostart" in vmcfg and vmcfg["autostart"] and not self.process_running(vmid): + self.startVM(vmid) + else: + raise Exception("Missing VMs config section!") + def startVM(self, vmid): self.setupNetwork(vmid) cmd = [] @@ -46,12 +76,77 @@ class VMHelper: cmd.append("-qmp") cmd.append("unix:" + self.config['kvm']['qmpsocket'].replace("$VMID", vmid) + ",server,nowait") + cmd += ["-name", vmid] + default_args = self.config['kvm']['default_args'].replace("$VMID", vmid) cmd += default_args.split() cmd += self.createArguments(vmid).split() #print(" ".join(cmd)) subprocess.Popen(cmd, stdout=open("/dev/null"), stderr=open("/dev/null")) - #subprocess.call(cmd) + #subprocess.call(cmd + + def shutdownVMs(self,timeout,parallel=True, statusCallback=lambda vmid, st : None): + threads = [] + for vmid in self.getVmIds(): + if self.process_running(vmid): + # i=vmid: strange workaround for strange problem the it uses the wrong vmid... + def stopCallback(st, i=vmid): statusCallback(i, st) + + thread = threading.Thread(target=lambda : self.stopVM(vmid, timeout, stopCallback)) + thread.start() + if parallel: + threads.append(thread) + else: + thread.join() + + for thread in threads: + thread.join() + + def stopVM(self, vmid, timeout=None, statusCallback=lambda st : None, wait=False): + proto = QEMUMonitorProtocol(self.config['kvm']['qmpsocket'].replace("$VMID", vmid)) + proto.connect() + + statusCallback("send_powerdown") + proto.cmd("system_powerdown") + + if timeout is None and not wait: + proto.close() + return + + timeoutEvent = threading.Event() + + def waitForShutdown(): + shutDown = False + while not timeoutEvent.is_set() and not shutDown : + event = proto.pull_event(wait=True) + shutDown = event is not None and event["event"] == 'SHUTDOWN' + + eventWaitThread = threading.Thread(target=waitForShutdown) + eventWaitThread.start() + + if(timeout is None and wait): + eventWaitThread.join() + proto.close() + return + + eventWaitThread.join(timeout) + timeoutEvent.set() + if eventWaitThread.is_alive(): + statusCallback("send_quit") + proto.cmd("quit") + time.sleep(1) + if(self.process_running(vmid)): + statusCallback("kill_vm") + self.killVm(vmid, 9) #kill it with fire! + + proto.close() + + def killVm(self, vmid, signal=15): + pid = self.getPid(vmid) + + if pid is not None: + os.kill(pid,signal) + def createArguments(self, vmid): if ('VMs' in self.config) and (vmid in self.config['VMs']): diff --git a/mc-vm-manager/config.json b/mc-vm-manager/config.json index a9b742e..0a08122 100644 --- a/mc-vm-manager/config.json +++ b/mc-vm-manager/config.json @@ -28,7 +28,8 @@ "keyboard" : "de", "kernel": "/home/markus/kernel-3.8.5", "append": "root=/dev/vda", - "owner": "markus" + "owner": "markus", + "autostart" : false }, "bar": { "cpu": "kvm64", @@ -45,7 +46,28 @@ "display": 2 }, "keyboard" : "de", - "owner": "markus" + "owner": "markus", + + "autostart" : true + }, + "baz": { + "cpu": "kvm64", + "smp": 2, + "memory": 2048, + "cdrom": "/root/tmp/install-amd64-minimal-20130207.iso", + "network": { + "hw": "virtio", + "dev": "tap-baz", + "mac": "54:52:00:00:01:03", + "ip": ["192.0.2.26", "192.0.2.27"] + }, + "vnc": { + "display": 3 + }, + "keyboard" : "de", + "owner": "markus", + + "autostart" : true } } } diff --git a/mc-vm-manager/manager.py b/mc-vm-manager/manager.py index bcdacd0..214f5fd 100755 --- a/mc-vm-manager/manager.py +++ b/mc-vm-manager/manager.py @@ -2,33 +2,107 @@ import argparse import sys +import time from VMHelper import VMHelper -helper = None + +#### Constants ### + +CONFIG_FILENAME_DEFAULT = "/root/tmp/config.json" +SHUTDOWN_TIMEOUT = 180 + +#### --------- ### + +helper = VMHelper(CONFIG_FILENAME_DEFAULT) +stopStatusToMessage = { + "send_powerdown" : "Sending ACPI poweroff Event.", + "send_quit" : "Shoutdown has timed out! Quitting forcefully!", + "kill_vm" : "Killing process!" + } + +def vmm_list(args): + print("Available VMs:") + print() + for vmid in helper.getVmIds(): + s = "ID: {0} -- {1}" + state = None + if helper.process_running(vmid): + state = helper.getQMPStatus(vmid)["status"] + else : + state = "stopped" + print(s.format(vmid,state)) + def vmm_start(args): + if(helper.process_running(args.vmid)): + print("VM {0} is already running!".format(args.vmid)) + return + print('Starting VM {0.vmid}.'.format(args)) helper.startVM(args.vmid) + #print("Successfully started: " + ("yes" if helper.process_running(args.vmid) else "no")) +def vmm_shutdown(args): + timeout = SHUTDOWN_TIMEOUT if args.t is None or not args.t.isdigit() else int(args.t) + stCallback = lambda vmid, st : print("VM {0}: {1}".format(vmid, stopStatusToMessage[st])) + + helper.shutdownVMs(timeout, args.s is None or args.s < 1, statusCallback=stCallback) def vmm_stop(args): - print('Stopping VM {0.vmid}.'.format(args)) - helper.stopVM(args['vmid']) + if(not helper.process_running(args.vmid)): + print("VM {0} is not running!".format(args.vmid)) + return -def vmm_status(args): + if args.k is not None and args.k >= 1: + helper.killVm(args.vmid) + time.sleep(0.3) + if helper.process_running(args.vmid): + time.sleep(2) + if helper.process_running(args.vmid): + helper.killVm(args.vmid , 9) + print("VM killed successfully: " + ("yes" if not helper.process_running(args.vmid) else "no")) + return + + if args.t is not None and not args.t.isdigit(): + print ("timeout must be positve integer but is " + args.t + "!") + return + + timeout = int(args.t) if args.t is not None else None + + print('Stopping VM {0.vmid} with timeout {1}.'.format(args,timeout)) + + + helper.stopVM(args.vmid, timeout, lambda x: print(stopStatusToMessage[x]), wait=args.w is not None) + +def vmm_status(args): if args.v >= 1: print('Gathering status information for VM {0.vmid}.'.format(args)) - + + proc_running = helper.process_running(args.vmid) + print("VM ID: {0}".format(args.vmid)) - print("Qemu process is running: " + ("yes (pid: {0})".format(helper.getPid(args.vmid)) if helper.process_running(args.vmid) else "no")) + print("Qemu process is running: " + ("yes (pid: {0})".format(helper.getPid(args.vmid)) if proc_running else "no")) + #TODO write some decoder with status descriptions plaintext + + for key,val in helper.getQMPStatus(args.vmid).items(): + print("{0}: {1}".format(key,val)) def vmm_cleanup(args): + if(helper.process_running(args.vmid)): + print("VM {0} is running, nothing to clean up!".format(args.vmid)) + return + print('Cleaning up for VM {0.vmid}.'.format(args)) helper.teardownNetwork(args.vmid) + #TODO remove pidfile and qmpfile + +def vmm_version(args): + print("MC VM Manager v0.1\nCopyright (c) M. Hauschild and P. Dahlberg 2013.\n") + +def vmm_autostart(args): + helper.autostartVMs() def main(): - global helper - - print("MC VM Manager v0.1\nCopyright (c) M. Hauschild and P. Dahlberg 2013.\n") + #maybe we need to create a lockfile parser = argparse.ArgumentParser(prog='mcvmm', description='Manages VMs') subparsers = parser.add_subparsers(title='subcommands') @@ -38,10 +112,18 @@ def main(): parser_start.add_argument('vmid', action='store', help='the ID of the VM') parser_start.set_defaults(func=vmm_start) - parser_stop = subparsers.add_parser('stop', help='stop a VM') + parser_stop = subparsers.add_parser('stop', help='Shutdown VM with ACPI poweroff') parser_stop.add_argument('vmid', action='store', help='the ID of the VM') + parser_stop.add_argument('-t', action='store', help='forcefully quit after given timeout value (signed integer), implies -w') + parser_stop.add_argument('-k', action='count', help='Kill the qemu process') + parser_stop.add_argument('-w', action='count', help='wait until the VM has stopped') parser_stop.set_defaults(func=vmm_stop) + parser_shutdown = subparsers.add_parser('shutdown', help='Shutdown all VMs ACPI poweroff and quit forcefully after timeout') + parser_shutdown.add_argument('-t', action='store', help='forcefully quit after given timeout value (signed integer, default: {0}'.format(SHUTDOWN_TIMEOUT)) + parser_shutdown.add_argument('-s', action='count', help='sequencial mode, timeout for each vm') + parser_shutdown.set_defaults(func=vmm_shutdown) + parser_status = subparsers.add_parser('status', help='status a VM') parser_status.add_argument('-v', action='count', help='increase verbosity') parser_status.add_argument('vmid', action='store', default=0, help='the ID of the VM') @@ -51,11 +133,21 @@ def main(): parser_cleanup.add_argument('vmid', action='store', default=0, help='the ID of the VM') parser_cleanup.set_defaults(func=vmm_cleanup) - helper = VMHelper() - - args = parser.parse_args() + parser_list = subparsers.add_parser('list', help='list available vms') + parser_list.set_defaults(func=vmm_list) - #TODO abfrage ob vmid existiert + parser_version = subparsers.add_parser('version', help='Prints version and authors') + parser_version.set_defaults(func=vmm_version) + + parser_autostart = subparsers.add_parser('autostart', help='Start VMs configured with autostart') + parser_autostart.set_defaults(func=vmm_autostart) + + args = parser.parse_args() + + if "vmid" in args and args.vmid not in helper.getVmIds(): + print("VM {0} does not exist!".format(args.vmid)) + return + args.func(args) if __name__ == '__main__': diff --git a/mc-vm-manager/qmp.py b/mc-vm-manager/qmp.py index 773fb91..d067b2a 100644 --- a/mc-vm-manager/qmp.py +++ b/mc-vm-manager/qmp.py @@ -5,7 +5,7 @@ # Authors: # Luiz Capitulino # -# This work is licensed under the terms of the GNU GPL, version 2. See +# This work is licensed under the terms of the GNU GPL, version 2. See # the COPYING file in the top-level directory. import json @@ -13,178 +13,182 @@ import errno import socket class QMPError(Exception): - pass + pass class QMPConnectError(QMPError): - pass + pass class QMPCapabilitiesError(QMPError): - pass + pass class QEMUMonitorProtocol: - def __init__(self, address, server=False): - """ - Create a QEMUMonitorProtocol class. + def __init__(self, address, server=False): + """ + Create a QEMUMonitorProtocol class. - @param address: QEMU address, can be either a unix socket path (string) - or a tuple in the form ( address, port ) for a TCP - connection - @param server: server mode listens on the socket (bool) - @raise socket.error on socket connection errors - @note No connection is established, this is done by the connect() or - accept() methods - """ - self.__events = [] - self.__address = address - self.__sock = self.__get_sock() - if server: - self.__sock.bind(self.__address) - self.__sock.listen(1) + @param address: QEMU address, can be either a unix socket path (string) + or a tuple in the form ( address, port ) for a TCP + connection + @param server: server mode listens on the socket (bool) + @raise socket.error on socket connection errors + @note No connection is established, this is done by the connect() or + accept() methods + """ + self.__events = [] + self.__address = address + self.__sock = self.__get_sock() + if server: + self.__sock.bind(self.__address) + self.__sock.listen(1) - def __get_sock(self): - if isinstance(self.__address, tuple): - family = socket.AF_INET - else: - family = socket.AF_UNIX - return socket.socket(family, socket.SOCK_STREAM) + def __get_sock(self): + if isinstance(self.__address, tuple): + family = socket.AF_INET + else: + family = socket.AF_UNIX + return socket.socket(family, socket.SOCK_STREAM) - def __negotiate_capabilities(self): - greeting = self.__json_read() - if greeting is None or 'QMP' not in greeting: - raise QMPConnectError - # Greeting seems ok, negotiate capabilities - resp = self.cmd('qmp_capabilities') - if "return" in resp: - return greeting - raise QMPCapabilitiesError + def __negotiate_capabilities(self): + greeting = self.__json_read() + if greeting is None or 'QMP' not in greeting: + raise QMPConnectError + # Greeting seems ok, negotiate capabilities + resp = self.cmd('qmp_capabilities') + if "return" in resp: + return greeting + raise QMPCapabilitiesError - def __json_read(self, only_event=False): - while True: - data = self.__sockfile.readline() - if not data: - return - resp = json.loads(data) - if 'event' in resp: - self.__events.append(resp) - if not only_event: - continue - return resp + def __json_read(self, only_event=False): + while True: + data = self.__sockfile.readline() + if not data: + return + resp = json.loads(data) + if 'event' in resp: + self.__events.append(resp) + if not only_event: + continue + return resp - error = socket.error + error = socket.error - def connect(self, negotiate=True): - """ - Connect to the QMP Monitor and perform capabilities negotiation. + def connect(self, negotiate=True): + """ + Connect to the QMP Monitor and perform capabilities negotiation. - @return QMP greeting dict - @raise socket.error on socket connection errors - @raise QMPConnectError if the greeting is not received - @raise QMPCapabilitiesError if fails to negotiate capabilities - """ - self.__sock.connect(self.__address) - self.__sockfile = self.__sock.makefile() - if negotiate: - return self.__negotiate_capabilities() + @return QMP greeting dict + @raise socket.error on socket connection errors + @raise QMPConnectError if the greeting is not received + @raise QMPCapabilitiesError if fails to negotiate capabilities + """ + self.__sock.connect(self.__address) + self.__sockfile = self.__sock.makefile() + if negotiate: + return self.__negotiate_capabilities() - def accept(self): - """ - Await connection from QMP Monitor and perform capabilities negotiation. + def accept(self): + """ + Await connection from QMP Monitor and perform capabilities negotiation. - @return QMP greeting dict - @raise socket.error on socket connection errors - @raise QMPConnectError if the greeting is not received - @raise QMPCapabilitiesError if fails to negotiate capabilities - """ - self.__sock, _ = self.__sock.accept() - self.__sockfile = self.__sock.makefile() - return self.__negotiate_capabilities() + @return QMP greeting dict + @raise socket.error on socket connection errors + @raise QMPConnectError if the greeting is not received + @raise QMPCapabilitiesError if fails to negotiate capabilities + """ + self.__sock, _ = self.__sock.accept() + self.__sockfile = self.__sock.makefile() + return self.__negotiate_capabilities() - def cmd_obj(self, qmp_cmd): - """ - Send a QMP command to the QMP Monitor. + def cmd_obj(self, qmp_cmd): + """ + Send a QMP command to the QMP Monitor. - @param qmp_cmd: QMP command to be sent as a Python dict - @return QMP response as a Python dict or None if the connection has - been closed - """ - try: - self.__sock.sendall(bytes(json.dumps(qmp_cmd), 'UTF-8')) - except socket.error as err: - if err[0] == errno.EPIPE: - return - raise socket.error(err) - return self.__json_read() + @param qmp_cmd: QMP command to be sent as a Python dict + @return QMP response as a Python dict or None if the connection has + been closed + """ + try: + self.__sock.sendall(bytes(json.dumps(qmp_cmd), 'UTF-8')) + except socket.error as err: + if err[0] == errno.EPIPE: + return + raise socket.error(err) + return self.__json_read() - def cmd(self, name, args=None, id=None): - """ - Build a QMP command and send it to the QMP Monitor. + def cmd(self, name, args=None, id=None): + """ + Build a QMP command and send it to the QMP Monitor. - @param name: command name (string) - @param args: command arguments (dict) - @param id: command id (dict, list, string or int) - """ - qmp_cmd = { 'execute': name } - if args: - qmp_cmd['arguments'] = args - if id: - qmp_cmd['id'] = id - return self.cmd_obj(qmp_cmd) + @param name: command name (string) + @param args: command arguments (dict) + @param id: command id (dict, list, string or int) + """ + qmp_cmd = { 'execute': name } + if args: + qmp_cmd['arguments'] = args + if id: + qmp_cmd['id'] = id + return self.cmd_obj(qmp_cmd) - def command(self, cmd, **kwds): - ret = self.cmd(cmd, kwds) - if 'error' in ret: - raise Exception(ret['error']['desc']) - return ret['return'] + def command(self, cmd, **kwds): + ret = self.cmd(cmd, kwds) + if 'error' in ret: + raise Exception(ret['error']['desc']) + return ret['return'] - def pull_event(self, wait=False): - """ - Get and delete the first available QMP event. + def pull_event(self, wait=False): + """ + Get and delete the first available QMP event. - @param wait: block until an event is available (bool) - """ - self.__sock.setblocking(0) - try: - self.__json_read() - except socket.error as err: - if err[0] == errno.EAGAIN: - # No data available - pass - self.__sock.setblocking(1) - if not self.__events and wait: - self.__json_read(only_event=True) - event = self.__events[0] - del self.__events[0] - return event + @param wait: block until an event is available (bool) + """ + self.__sock.setblocking(0) + try: + self.__json_read() + except socket.error as err: + if err[0] == errno.EAGAIN: + # No data available + pass + self.__sock.setblocking(1) + if not self.__events and wait: + self.__json_read(only_event=True) - def get_events(self, wait=False): - """ - Get a list of available QMP events. + if len(self.__events) == 0: + return None + else: + event = self.__events[0] + del self.__events[0] + return event - @param wait: block until an event is available (bool) - """ - self.__sock.setblocking(0) - try: - self.__json_read() - except socket.error as err: - if err[0] == errno.EAGAIN: - # No data available - pass - self.__sock.setblocking(1) - if not self.__events and wait: - self.__json_read(only_event=True) - return self.__events + def get_events(self, wait=False): + """ + Get a list of available QMP events. - def clear_events(self): - """ - Clear current list of pending events. - """ - self.__events = [] + @param wait: block until an event is available (bool) + """ + self.__sock.setblocking(0) + try: + self.__json_read() + except socket.error as err: + if err[0] == errno.EAGAIN: + # No data available + pass + self.__sock.setblocking(1) + if not self.__events and wait: + self.__json_read(only_event=True) + return self.__events - def close(self): - self.__sock.close() - self.__sockfile.close() + def clear_events(self): + """ + Clear current list of pending events. + """ + self.__events = [] - timeout = socket.timeout + def close(self): + self.__sock.close() + self.__sockfile.close() - def settimeout(self, timeout): - self.__sock.settimeout(timeout) + timeout = socket.timeout + + def settimeout(self, timeout): + self.__sock.settimeout(timeout)