Initial commit
This commit is contained in:
commit
515d124316
|
@ -0,0 +1,3 @@
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
zhypervisor.egg-info
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.0.1-prealpha"
|
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