Initial commit

This commit is contained in:
dave 2016-12-26 16:42:48 -08:00
commit 515d124316
14 changed files with 489 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
dist
zhypervisor.egg-info

5
README.md Normal file
View File

@ -0,0 +1,5 @@
zhypervisor
===========
A minimal hypervisor API based on QEMU.

34
example/banutoo.json Normal file
View File

@ -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
}
}
}

34
example/banutoo2.json Normal file
View File

@ -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
}
}
}

19
example/zd.json Normal file
View File

@ -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
}
}
}

16
setup.py Normal file
View File

@ -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)

1
zhypervisor/__init__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.0.1-prealpha"

Binary file not shown.

View File

@ -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)

130
zhypervisor/daemon.py Normal file
View File

@ -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()

9
zhypervisor/logging.py Normal file
View File

@ -0,0 +1,9 @@
import logging
def setup_logging():
"""
Set up a standard logging level/format
"""
logging.basicConfig(level=logging.DEBUG)

29
zhypervisor/machine.py Normal file
View File

@ -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()

19
zhypervisor/tools/ifup.py Normal file
View File

@ -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()

59
zhypervisor/util.py Normal file
View File

@ -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)