commit
515d124316
14 changed files with 489 additions and 0 deletions
@ -0,0 +1,5 @@
|
||||
zhypervisor |
||||
=========== |
||||
|
||||
A minimal hypervisor API based on QEMU. |
||||
|
@ -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 |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
||||
} |
@ -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) |
Binary file not shown.
@ -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) |
@ -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() |
@ -0,0 +1,9 @@
|
||||
|
||||
import logging |
||||
|
||||
|
||||
def setup_logging(): |
||||
""" |
||||
Set up a standard logging level/format |
||||
""" |
||||
logging.basicConfig(level=logging.DEBUG) |
@ -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() |
@ -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() |
@ -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) |
Loading…
Reference in new issue