Initial commit
This commit is contained in:
commit
515d124316
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build
|
||||
dist
|
||||
zhypervisor.egg-info
|
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
zhypervisor
|
||||
===========
|
||||
|
||||
A minimal hypervisor API based on QEMU.
|
||||
|
34
example/banutoo.json
Normal file
34
example/banutoo.json
Normal 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
34
example/banutoo2.json
Normal 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
19
example/zd.json
Normal 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
16
setup.py
Normal 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
1
zhypervisor/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = "0.0.1-prealpha"
|
BIN
zhypervisor/__pycache__/__init__.cpython-34.pyc
Normal file
BIN
zhypervisor/__pycache__/__init__.cpython-34.pyc
Normal file
Binary file not shown.
131
zhypervisor/clients/qmachine.py
Normal file
131
zhypervisor/clients/qmachine.py
Normal 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
130
zhypervisor/daemon.py
Normal 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
9
zhypervisor/logging.py
Normal 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
29
zhypervisor/machine.py
Normal 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
19
zhypervisor/tools/ifup.py
Normal 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
59
zhypervisor/util.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user