From 515d124316d11637331b6a851c79fa4f2cc45607 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 26 Dec 2016 16:42:48 -0800 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 5 + example/banutoo.json | 34 +++++ example/banutoo2.json | 34 +++++ example/zd.json | 19 +++ setup.py | 16 +++ zhypervisor/__init__.py | 1 + .../__pycache__/__init__.cpython-34.pyc | Bin 0 -> 160 bytes zhypervisor/clients/qmachine.py | 131 ++++++++++++++++++ zhypervisor/daemon.py | 130 +++++++++++++++++ zhypervisor/logging.py | 9 ++ zhypervisor/machine.py | 29 ++++ zhypervisor/tools/ifup.py | 19 +++ zhypervisor/util.py | 59 ++++++++ 14 files changed, 489 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example/banutoo.json create mode 100644 example/banutoo2.json create mode 100644 example/zd.json create mode 100644 setup.py create mode 100644 zhypervisor/__init__.py create mode 100644 zhypervisor/__pycache__/__init__.cpython-34.pyc create mode 100644 zhypervisor/clients/qmachine.py create mode 100644 zhypervisor/daemon.py create mode 100644 zhypervisor/logging.py create mode 100644 zhypervisor/machine.py create mode 100644 zhypervisor/tools/ifup.py create mode 100644 zhypervisor/util.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40730ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +dist +zhypervisor.egg-info diff --git a/README.md b/README.md new file mode 100644 index 0000000..1851893 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +zhypervisor +=========== + +A minimal hypervisor API based on QEMU. + diff --git a/example/banutoo.json b/example/banutoo.json new file mode 100644 index 0000000..bff9d44 --- /dev/null +++ b/example/banutoo.json @@ -0,0 +1,34 @@ +{ + "id": "banutoo", + "type": "q", + "spec": { + "options": { + "autostart": true, + "respawn": true + }, + "properties": { + "cores": 2, + "mem": 1024, + "drives": [ + { + "file": "banutoo.bin", + "datastore": "realm", + "index": 0, + "if": "virtio" + } + ], + "netifaces": [ + { + "type": "nic", + "vlan": 0, + "model": "e1000", + "macaddr": "82:25:60:41:D5:97" + }, + { + "type": "tap" + } + ], + "vnc": 5 + } + } +} diff --git a/example/banutoo2.json b/example/banutoo2.json new file mode 100644 index 0000000..3026945 --- /dev/null +++ b/example/banutoo2.json @@ -0,0 +1,34 @@ +{ + "id": "banutoo2", + "type": "q", + "spec": { + "options": { + "autostart": true, + "respawn": true + }, + "properties": { + "cores": 2, + "mem": 1024, + "drives": [ + { + "file": "banutoo2.bin", + "datastore": "realm", + "index": 0, + "if": "virtio" + } + ], + "netifaces": [ + { + "type": "nic", + "vlan": 0, + "model": "e1000", + "macaddr": "82:25:60:41:D5:98" + }, + { + "type": "tap" + } + ], + "vnc": 6 + } + } +} diff --git a/example/zd.json b/example/zd.json new file mode 100644 index 0000000..56eb481 --- /dev/null +++ b/example/zd.json @@ -0,0 +1,19 @@ +{ + "access": [ + [ + "root", + "toor", + 0 + ] + ], + "nodename": "examplenode", + "datastores": { + "default": { + "path": "/opt/datastore/" + }, + "realm": { + "path": "/media/realm/tmp/qemu-testin/banto/", + "init": true + } + } +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4a4dfea --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +from setuptools import setup +from zhypervisor import __version__ + + +setup(name='zhypervisor', + version=__version__, + description='python-based x86 hypervisor using qemu', + url='http://gitlab.xmopx.net/dave/zhypervisor', + author='dpedu', + author_email='dave@davepedu.com', + packages=['zhypervisor', 'zhypervisor.clients', 'zhypervisor.tools'], + entry_points={'console_scripts': ['zd = zhypervisor.daemon:main', + 'zd_ifup = zhypervisor.tools.ifup:main']}, + zip_safe=False) diff --git a/zhypervisor/__init__.py b/zhypervisor/__init__.py new file mode 100644 index 0000000..5fbd18e --- /dev/null +++ b/zhypervisor/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1-prealpha" diff --git a/zhypervisor/__pycache__/__init__.cpython-34.pyc b/zhypervisor/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f32b0f5e56fd9520c6b724136d737f94b6568c61 GIT binary patch literal 160 zcmaFI!^?GKWnzRp0|UcjAcg}*Aj<)Wi@AVA3IjtFkYr>C)?}*UGte{8Gt@08N=?iu z$Vl|lWW2>4A77SQRGgWg7azZpp@<2n1x)1KC~-#0&t2jw)RM literal 0 HcmV?d00001 diff --git a/zhypervisor/clients/qmachine.py b/zhypervisor/clients/qmachine.py new file mode 100644 index 0000000..a301cce --- /dev/null +++ b/zhypervisor/clients/qmachine.py @@ -0,0 +1,131 @@ +import os +import logging +import subprocess +from time import sleep +from threading import Thread + +from zhypervisor.util import TapDevice, Machine + + +class QMachine(Machine): + machine_type = "q" + + def __init__(self, spec): + Machine.__init__(self, spec) + self.proc = None + self.tap = TapDevice() + self.block_respawns = False + # TODO validate specs + + def start_machine(self): + if self.proc: + raise Exception("Machine already running!") + else: + qemu_args = self.get_args(tap=str(self.tap)) + logging.info("spawning qemu with: {}".format(' '.join(qemu_args))) + sleep(1) # anti-spin + self.proc = subprocess.Popen(qemu_args, preexec_fn=lambda: os.setpgrp(), stdin=subprocess.PIPE) + # TODO handle stdout/err - stream to logs? + Thread(target=self.wait_on_exit, args=[self.proc]).start() + + def wait_on_exit(self, proc): + proc.wait() + logging.info("qemu process has exited") + self.proc = None + if not self.block_respawns and self.spec.options.get("respawn", False): + self.start_machine() + + def stop_machine(self): + if self.proc: + logging.info("stopping machine...") + self.proc.stdin.write(b"system_powerdown\n") + self.proc.stdin.flush() + self.proc.wait() + self.proc = None + + def kill_machine(self): + if self.proc: + self.proc.terminate() + self.proc.wait() + self.proc = None + + def get_args(self, tap): + argv = ['qemu-system-x86_64'] + argv += self.get_args_system() + argv += self.get_args_drives() + argv += self.get_args_network(tap) + return argv + + def get_args_system(self): + """ + Return system-related args: + - Qemu meta args + - CPU core settings + - Mem amnt + - Boot device + """ + args = ["-monitor", "stdio", "-machine", "accel=kvm", "-smp"] + args.append("cpus={}".format(self.spec.properties.get("cores", 1))) # why doesn't this work: ,cores={} + args.append("-m") + args.append(str(self.spec.properties.get("mem", 256))) + args.append("-boot") + args.append("cd") + if self.spec.properties.get("vnc", False): + args.append("-vnc") + assert type(self.spec.properties.get("vnc")) == int, "VNC port should be an integer" + args.append(":{}".format(self.spec.properties.get("vnc"))) + return args + + def get_args_network(self, tap_name): + """ + Hard-coded for now + """ + args = [] + for iface in self.spec.properties.get("netifaces"): + iface_type = iface.get("type") + + if iface_type == "tap": + if "ifname" not in iface: + iface["ifname"] = tap_name + iface["script"] = "/root/zhypervisor/testenv/bin/zd_ifup" # TODO fixme + iface["downscript"] = "no" + + args.append("-net") + args.append(QMachine.format_args(iface)) + return args + + # return ['-net', 'nic,vlan=0,model=e1000,macaddr=82:25:60:41:D5:97', + # '-net', 'tap,ifname={},script=if_up.sh,downscript=no'.format(tap_name)] + + def get_args_drives(self): + """ + Inspect props.drives expecting a format like: {"file": "/tmp/ubuntu.qcow2", "index": 0, "if": "virtio"} + """ + drives = [] + for drive in self.spec.properties.get("drives", []): + drive_info = dict(drive) + drives.append("-drive") + + # translate datastore paths if neede + if "file" in drive_info: + drive_info["file"] = self.get_datastore_path(drive_info["datastore"], drive_info["file"]) + del drive_info["datastore"] + + drives.append(QMachine.format_args(drive_info)) + return drives + + @staticmethod + def format_args(d): + """ + Given a dictionary like: {"file": "/dev/zd0", "index": 0, "if", "virtio"} + Return a string like: file=/dev/zd0,index=0,if=virtio + """ + args = [] + for item, value in d.items(): + if item == "type": + args.insert(0, value) + else: + args.append("{}={}".format(item, value)) + if not args: + return None + return ','.join(args) diff --git a/zhypervisor/daemon.py b/zhypervisor/daemon.py new file mode 100644 index 0000000..65a6cec --- /dev/null +++ b/zhypervisor/daemon.py @@ -0,0 +1,130 @@ + +import os +import json +import signal +import logging +import argparse +from time import sleep +from concurrent.futures import ThreadPoolExecutor + +from zhypervisor.logging import setup_logging +from zhypervisor.machine import MachineSpec + +from pprint import pprint + + +class ZHypervisorDaemon(object): + def __init__(self, config): + self.config = config + self.datastores = {} + self.machines = {} + self.running = True + + self.init_datastores() + self.state = ZConfig(self.datastores["default"]) + + signal.signal(signal.SIGINT, self.signal_handler) # ctrl-c + signal.signal(signal.SIGTERM, self.signal_handler) # sigterm + + def init_datastores(self): + for name, info in self.config["datastores"].items(): + self.datastores[name] = ZDataStore(name, info["path"], info.get("init", False)) + + def init_machines(self): + for machine_info in self.state.get_machines(): + machine_id = machine_info["id"] + self.add_machine(machine_id, machine_info["type"], machine_info["spec"]) + + def add_machine(self, machine_id, machine_type, machine_spec): + machine = MachineSpec(self, machine_id, machine_type, machine_spec) + self.machines[machine_id] = machine + if machine.options.get("autostart", False): + machine.start() + + def signal_handler(self, signum, frame): + logging.critical("Got signal {}".format(signum)) + self.stop() + + def run(self): + # launch machines + self.init_machines() + + # start API + # TODO + + # Wait? + while self.running: + sleep(1) + + def stop(self): + self.running = False + + with ThreadPoolExecutor(10) as pool: + for machine_id, machine in self.machines.items(): + pool.submit(machine.stop) + + +class ZDataStore(object): + def __init__(self, name, root_path, init_ok=False): + self.name = name + self.root_path = root_path + os.makedirs(self.root_path, exist_ok=True) + try: + metainfo_path = self.get_filepath(".datastore.json") + assert os.path.exists(metainfo_path), "Datastore missing or not initialized! " \ + "File not found: {}".format(metainfo_path) + except: + if init_ok: + with open(metainfo_path, "w") as f: + json.dump({}, f) + else: + raise + logging.info("Initialized datastore %s at %s", name, self.root_path) + + def get_filepath(self, *paths): + return os.path.join(self.root_path, *paths) + + +class ZConfig(object): + def __init__(self, datastore): + self.datastore = datastore + + self.machine_data_dir = self.datastore.get_filepath("machines") + + for d in [self.machine_data_dir]: + os.makedirs(d, exist_ok=True) + + def get_machines(self): + machines = [] + logging.info("Looking for machines in {}".format(self.machine_data_dir)) + for mach_name in os.listdir(self.machine_data_dir): + with open(os.path.join(self.machine_data_dir, mach_name), "r") as f: + machines.append(json.load(f)) + return machines + + +def main(): + setup_logging() + + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--config", default="/etc/zd.json", help="Config file path") + args = parser.parse_args() + + if not os.path.exists(args.config): + logging.warning("Config does not exist, attempting to write default config") + with open(args.config, "w") as f: + json.dump({"nodename": "examplenode", + "access": [("root", "toor", 0)], + "state": "/opt/datastore/state/", + "datastores": { + "default": { + "path": "/opt/datastore/machines/" + } + }}, f, indent=4) + return + + with open(args.config) as f: + config = json.load(f) + + z = ZHypervisorDaemon(config) + z.run() diff --git a/zhypervisor/logging.py b/zhypervisor/logging.py new file mode 100644 index 0000000..b58c875 --- /dev/null +++ b/zhypervisor/logging.py @@ -0,0 +1,9 @@ + +import logging + + +def setup_logging(): + """ + Set up a standard logging level/format + """ + logging.basicConfig(level=logging.DEBUG) diff --git a/zhypervisor/machine.py b/zhypervisor/machine.py new file mode 100644 index 0000000..1c98c06 --- /dev/null +++ b/zhypervisor/machine.py @@ -0,0 +1,29 @@ +import logging + +from zhypervisor.clients.qmachine import QMachine + + +class MachineSpec(object): + def __init__(self, master, machine_id, machine_type, spec): + logging.info("Initting machine %s", machine_id) + self.master = master + self.machine_id = machine_id + self.machine_type = machine_type + + self.options = {} # hypervisor-level stuff like Autostart + self.properties = {} # machine level stuff like processor count + + # TODO replace if/else with better system + if machine_type == "q": + self.machine = QMachine(self) + self.options = spec["options"] + self.properties = spec["properties"] + else: + raise Exception("Unknown machine type: {}".format(machine_type)) + + def start(self): + self.machine.start_machine() + + def stop(self): + self.machine.block_respawns = True + self.machine.stop_machine() diff --git a/zhypervisor/tools/ifup.py b/zhypervisor/tools/ifup.py new file mode 100644 index 0000000..38652fc --- /dev/null +++ b/zhypervisor/tools/ifup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import sys +import logging +from subprocess import check_call + +from zhypervisor.logging import setup_logging + + +def main(): + setup_logging() + _, tap_name = sys.argv + logging.info("Enabling interface %s...", tap_name) + check_call(["brctl", "addif", "br0", tap_name]) + check_call(["ifconfig", tap_name, "up"]) + logging.info("Enabled interface %s", tap_name) + +if __name__ == '__main__': + main() diff --git a/zhypervisor/util.py b/zhypervisor/util.py new file mode 100644 index 0000000..e1e183d --- /dev/null +++ b/zhypervisor/util.py @@ -0,0 +1,59 @@ + +import os +from random import randint + + +class TapDevice(object): + """ + Utility class - adds/removes a tap device on the linux system. Can be used as a context manager. + """ + def __init__(self): + self.num = randint(0, 100000) + + def create(self): + os.system("ip tuntap add name {} mode tap".format(self)) + + def destroy(self): + os.system("ip link delete {}".format(self)) + + def __str__(self): + return "tap{}".format(self.num) + + def __enter__(self): + self.create() + return str(self) + + def __exit__(self, type, value, traceback): + self.destroy() + + +class Machine(object): + """ + All runnable types should subclass this + """ + def __init__(self, machine_spec): + self.spec = machine_spec + + def run_machine(self): + """ + Run the machine and block until it exits (or was killed) + """ + raise NotImplemented() + + def stop_machine(self): + """ + Ask the machine to stop nicely + """ + raise NotImplemented() + + def kill_machine(self): + """ + Stop the machine, brutally + """ + raise NotImplemented() + + def get_datastore_path(self, datastore_name, *paths): + """ + Resolve the filesystem path for a path in the given datastore + """ + return self.spec.master.datastores.get(datastore_name).get_filepath(*paths)