Add basic api
This commit is contained in:
parent
515d124316
commit
3a5f4ace3b
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "banutoo",
|
"machine_id": "banutoo",
|
||||||
"type": "q",
|
"machine_type": "q",
|
||||||
"spec": {
|
"spec": {
|
||||||
"options": {
|
"options": {
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "banutoo2",
|
"machine_id": "banutoo2",
|
||||||
"type": "q",
|
"machine_type": "q",
|
||||||
"spec": {
|
"spec": {
|
||||||
"options": {
|
"options": {
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -10,7 +10,7 @@ setup(name='zhypervisor',
|
||||||
url='http://gitlab.xmopx.net/dave/zhypervisor',
|
url='http://gitlab.xmopx.net/dave/zhypervisor',
|
||||||
author='dpedu',
|
author='dpedu',
|
||||||
author_email='dave@davepedu.com',
|
author_email='dave@davepedu.com',
|
||||||
packages=['zhypervisor', 'zhypervisor.clients', 'zhypervisor.tools'],
|
packages=['zhypervisor', 'zhypervisor.clients', 'zhypervisor.api', 'zhypervisor.tools'],
|
||||||
entry_points={'console_scripts': ['zd = zhypervisor.daemon:main',
|
entry_points={'console_scripts': ['zd = zhypervisor.daemon:main',
|
||||||
'zd_ifup = zhypervisor.tools.ifup:main']},
|
'zd_ifup = zhypervisor.tools.ifup:main']},
|
||||||
zip_safe=False)
|
zip_safe=False)
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
import cherrypy
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class Mountable(object):
|
||||||
|
"""
|
||||||
|
Macro for encapsulating a component's config and methods into one object.
|
||||||
|
:param conf: cherrypy config dict for use when mounting this component
|
||||||
|
"""
|
||||||
|
def __init__(self, conf=None):
|
||||||
|
self.conf = conf if conf else {'/': {}}
|
||||||
|
|
||||||
|
def mount(self, path):
|
||||||
|
"""
|
||||||
|
Mount this component into the cherrypy tree
|
||||||
|
:param path: where to mount it e.g. /v1
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
cherrypy.tree.mount(self, path, self.conf)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ZApi(object):
|
||||||
|
def __init__(self, master):
|
||||||
|
"""
|
||||||
|
Main component of the API service. Inits and assembles the various classes. Provides .run() and .stop() to
|
||||||
|
control it.
|
||||||
|
:param master: parent BastionController reference.
|
||||||
|
"""
|
||||||
|
self.master = master
|
||||||
|
self.app_v1 = ZApiV1(self).mount('/api/v1')
|
||||||
|
# self.app_root = BSApiRoot(self).mount('/api')
|
||||||
|
# self.ui = Mountable(conf={'/': {
|
||||||
|
# 'tools.staticdir.on': True,
|
||||||
|
# 'tools.staticdir.dir': os.getcwd() + '/ui/build', # TODO don't hardcode
|
||||||
|
# 'tools.staticdir.index': 'index.html'}}).mount('/ui')
|
||||||
|
|
||||||
|
cherrypy.config.update({
|
||||||
|
'sessionFilter.on': True,
|
||||||
|
'tools.sessions.on': True,
|
||||||
|
'tools.sessions.locking': 'explicit',
|
||||||
|
'tools.sessions.timeout': 525600,
|
||||||
|
'request.show_tracebacks': True,
|
||||||
|
'server.socket_port': 3000, # TODO configurable port
|
||||||
|
'server.thread_pool': 25,
|
||||||
|
'server.socket_host': '0.0.0.0',
|
||||||
|
'server.show_tracebacks': True,
|
||||||
|
'server.socket_timeout': 5,
|
||||||
|
'log.screen': False,
|
||||||
|
'engine.autoreload.on': False
|
||||||
|
})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
cherrypy.engine.start()
|
||||||
|
cherrypy.engine.block()
|
||||||
|
logging.info("API has shut down")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
cherrypy.engine.exit()
|
||||||
|
logging.info("API shutting down...")
|
||||||
|
|
||||||
|
|
||||||
|
class ZApiV1(Mountable):
|
||||||
|
"""
|
||||||
|
Provides the /v1/ api.
|
||||||
|
"""
|
||||||
|
def __init__(self, root):
|
||||||
|
super().__init__(conf={
|
||||||
|
"/machine": {'request.dispatch': cherrypy.dispatch.MethodDispatcher()},
|
||||||
|
# "/task": {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}, # @TODO this conf belongs in the child
|
||||||
|
# "/logs": {
|
||||||
|
# 'tools.staticdir.on': True,
|
||||||
|
# 'tools.staticdir.dir': root.master.log_path,
|
||||||
|
# 'tools.staticdir.content_types': {'log': 'text/plain'}
|
||||||
|
# }
|
||||||
|
})
|
||||||
|
self.root = root
|
||||||
|
self.machine = ZApiMachines(self.root)
|
||||||
|
# self.task = BSApiTask(self.root)
|
||||||
|
# self.control = BSApiControl(self.root)
|
||||||
|
# self.socket = ApiWebsockets(self.root)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
yield "It works!"
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def create_disk(self, datastore, name, size, fmt):
|
||||||
|
"""
|
||||||
|
WORKAROUND for creating qemu disks
|
||||||
|
TODO replace me
|
||||||
|
"""
|
||||||
|
assert fmt in ["qcow2", "raw"], "Disk format is invalid"
|
||||||
|
assert name.endswith(".bin"), "Disk must be named <something>.bin"
|
||||||
|
|
||||||
|
disk_path = self.root.master.datastores[datastore].get_filepath(name)
|
||||||
|
img_args = ["qemu-img", "create", "-f", fmt, disk_path, "{}M".format(int(size))]
|
||||||
|
logging.info("Creating disk with: %s", str(img_args))
|
||||||
|
subprocess.check_call(img_args)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.popargs("machine_id")
|
||||||
|
class ZApiMachineStop(object):
|
||||||
|
"""
|
||||||
|
Endpoint to stop running machines
|
||||||
|
"""
|
||||||
|
exposed = True
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def GET(self, machine_id):
|
||||||
|
"""
|
||||||
|
If the machine exists, stop it gracefully. This happens asynchronously.
|
||||||
|
"""
|
||||||
|
assert machine_id in self.root.master.machines
|
||||||
|
Thread(target=lambda: self.root.master.forceful_stop(machine_id)).start()
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.popargs("machine_id")
|
||||||
|
class ZApiMachineStart(object):
|
||||||
|
"""
|
||||||
|
Endpoint to start stopped machines
|
||||||
|
"""
|
||||||
|
exposed = True
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def GET(self, machine_id=None):
|
||||||
|
"""
|
||||||
|
Start the machine
|
||||||
|
"""
|
||||||
|
self.root.master.machines[machine_id].start()
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.popargs("machine_id")
|
||||||
|
class ZApiMachineRestart(object):
|
||||||
|
"""
|
||||||
|
Endpoint to restart machines
|
||||||
|
"""
|
||||||
|
exposed = True
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def GET(self, machine_id=None):
|
||||||
|
"""
|
||||||
|
Start the machine
|
||||||
|
TODO can we not repeat this from Stop/Start?
|
||||||
|
"""
|
||||||
|
assert machine_id in self.root.master.machines
|
||||||
|
self.root.master.forceful_stop(machine_id)
|
||||||
|
self.root.master.machines[machine_id].start()
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.popargs("machine_id")
|
||||||
|
class ZApiMachines():
|
||||||
|
"""
|
||||||
|
Endpoint for managing machines
|
||||||
|
"""
|
||||||
|
|
||||||
|
exposed = True
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
"""
|
||||||
|
Endpoint to modify machines. PUT and DELETE require the machine not be running, which can be managed with the
|
||||||
|
stop and start methods below
|
||||||
|
"""
|
||||||
|
self.root = root
|
||||||
|
self.stop = ZApiMachineStop(self.root)
|
||||||
|
self.start = ZApiMachineStart(self.root)
|
||||||
|
self.restart = ZApiMachineRestart(self.root)
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def GET(self, machine_id=None, action=None, summary=False):
|
||||||
|
"""
|
||||||
|
Get a list of all machines or specific one if passed
|
||||||
|
:param task_id: task to retrieve
|
||||||
|
"""
|
||||||
|
summary = summary in [True, 'True', 'true', 'yes', '1', 1]
|
||||||
|
|
||||||
|
machines = {}
|
||||||
|
for _machine_id, machine_spec in self.root.master.machines.items():
|
||||||
|
machine = {"machine_id": _machine_id,
|
||||||
|
"_status": machine_spec.machine.get_status()}
|
||||||
|
if not summary:
|
||||||
|
machine.update({"machine_type": machine_spec.machine_type,
|
||||||
|
"spec": machine_spec.serialize()})
|
||||||
|
|
||||||
|
machines[_machine_id] = machine
|
||||||
|
if machine_id is not None:
|
||||||
|
try:
|
||||||
|
return [machines[machine_id]]
|
||||||
|
except KeyError:
|
||||||
|
raise cherrypy.HTTPError(status=404)
|
||||||
|
else:
|
||||||
|
return list(machines.values())
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def PUT(self, machine_id, machine_type, machine_spec):
|
||||||
|
"""
|
||||||
|
Create a new machine or update an existing machine
|
||||||
|
:param machine_id: id of machine to create or modify
|
||||||
|
:param machine_type: set machine type (currently, only "q")
|
||||||
|
'param machine_spec: json dictionary describing the machine. see the 'spec' key of example/banutoo.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert machine_id not in self.root.master.machines or \
|
||||||
|
self.root.master.machines[machine_id].machine.get_status() == "stopped", \
|
||||||
|
"Machine must be stopped to modify"
|
||||||
|
|
||||||
|
machine_spec = json.loads(machine_spec)
|
||||||
|
self.root.master.add_machine(machine_id, machine_type, machine_spec, write=True)
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
def DELETE(self, machine_id):
|
||||||
|
"""
|
||||||
|
Delete a machine. Raises 404 if no machine exists. Raises error if machine is not stopped
|
||||||
|
:param machine_id: ID of machine to remove
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
assert self.root.master.machines[machine_id].machine.get_status() == "stopped", \
|
||||||
|
"Machine must be stopped to delete"
|
||||||
|
except KeyError:
|
||||||
|
raise cherrypy.HTTPError(status=404)
|
||||||
|
|
||||||
|
self.root.master.remove_machine(machine_id)
|
||||||
|
return machine_id
|
|
@ -17,7 +17,17 @@ class QMachine(Machine):
|
||||||
self.block_respawns = False
|
self.block_respawns = False
|
||||||
# TODO validate specs
|
# TODO validate specs
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""
|
||||||
|
Return string "stopped" or "running" depending on machine status
|
||||||
|
@TODO machine status consts
|
||||||
|
"""
|
||||||
|
return "stopped" if self.proc is None else "running"
|
||||||
|
|
||||||
def start_machine(self):
|
def start_machine(self):
|
||||||
|
"""
|
||||||
|
If needed, launch the machine.
|
||||||
|
"""
|
||||||
if self.proc:
|
if self.proc:
|
||||||
raise Exception("Machine already running!")
|
raise Exception("Machine already running!")
|
||||||
else:
|
else:
|
||||||
|
@ -29,6 +39,9 @@ class QMachine(Machine):
|
||||||
Thread(target=self.wait_on_exit, args=[self.proc]).start()
|
Thread(target=self.wait_on_exit, args=[self.proc]).start()
|
||||||
|
|
||||||
def wait_on_exit(self, proc):
|
def wait_on_exit(self, proc):
|
||||||
|
"""
|
||||||
|
Listener used by above start_machine to restart the machine if the machine exits
|
||||||
|
"""
|
||||||
proc.wait()
|
proc.wait()
|
||||||
logging.info("qemu process has exited")
|
logging.info("qemu process has exited")
|
||||||
self.proc = None
|
self.proc = None
|
||||||
|
@ -36,20 +49,30 @@ class QMachine(Machine):
|
||||||
self.start_machine()
|
self.start_machine()
|
||||||
|
|
||||||
def stop_machine(self):
|
def stop_machine(self):
|
||||||
|
"""
|
||||||
|
Send the powerdown signal to the running machine
|
||||||
|
"""
|
||||||
if self.proc:
|
if self.proc:
|
||||||
logging.info("stopping machine...")
|
logging.info("stopping machine %s", self.spec.machine_id)
|
||||||
self.proc.stdin.write(b"system_powerdown\n")
|
self.proc.stdin.write(b"system_powerdown\n")
|
||||||
self.proc.stdin.flush()
|
self.proc.stdin.flush()
|
||||||
self.proc.wait()
|
self.proc.wait()
|
||||||
self.proc = None
|
self.proc = None
|
||||||
|
|
||||||
def kill_machine(self):
|
def kill_machine(self):
|
||||||
|
"""
|
||||||
|
Forcefully kill the running machine
|
||||||
|
"""
|
||||||
|
print("Terminating {}".format(self.proc))
|
||||||
if self.proc:
|
if self.proc:
|
||||||
self.proc.terminate()
|
self.proc.terminate()
|
||||||
self.proc.wait()
|
self.proc.wait()
|
||||||
self.proc = None
|
self.proc = None
|
||||||
|
|
||||||
def get_args(self, tap):
|
def get_args(self, tap):
|
||||||
|
"""
|
||||||
|
Assemble the full argv array that will be executed for this machine
|
||||||
|
"""
|
||||||
argv = ['qemu-system-x86_64']
|
argv = ['qemu-system-x86_64']
|
||||||
argv += self.get_args_system()
|
argv += self.get_args_system()
|
||||||
argv += self.get_args_drives()
|
argv += self.get_args_drives()
|
||||||
|
@ -78,7 +101,7 @@ class QMachine(Machine):
|
||||||
|
|
||||||
def get_args_network(self, tap_name):
|
def get_args_network(self, tap_name):
|
||||||
"""
|
"""
|
||||||
Hard-coded for now
|
Return network related qemu args
|
||||||
"""
|
"""
|
||||||
args = []
|
args = []
|
||||||
for iface in self.spec.properties.get("netifaces"):
|
for iface in self.spec.properties.get("netifaces"):
|
||||||
|
@ -87,7 +110,7 @@ class QMachine(Machine):
|
||||||
if iface_type == "tap":
|
if iface_type == "tap":
|
||||||
if "ifname" not in iface:
|
if "ifname" not in iface:
|
||||||
iface["ifname"] = tap_name
|
iface["ifname"] = tap_name
|
||||||
iface["script"] = "/root/zhypervisor/testenv/bin/zd_ifup" # TODO fixme
|
iface["script"] = "/root/zhypervisor/testenv/bin/zd_ifup" # TODO don't hard code
|
||||||
iface["downscript"] = "no"
|
iface["downscript"] = "no"
|
||||||
|
|
||||||
args.append("-net")
|
args.append("-net")
|
||||||
|
|
|
@ -6,65 +6,133 @@ import logging
|
||||||
import argparse
|
import argparse
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from zhypervisor.logging import setup_logging
|
from zhypervisor.logging import setup_logging
|
||||||
from zhypervisor.machine import MachineSpec
|
from zhypervisor.machine import MachineSpec
|
||||||
|
from zhypervisor.api.api import ZApi
|
||||||
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
class ZHypervisorDaemon(object):
|
class ZHypervisorDaemon(object):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
"""
|
||||||
self.datastores = {}
|
Z Hypervisor main thread. Roles:
|
||||||
self.machines = {}
|
- Load and start machines and API on init
|
||||||
|
- Cleanup on shutdown
|
||||||
|
- Committing changes to machines to disk
|
||||||
|
- Primary interface to modify machines
|
||||||
|
"""
|
||||||
|
self.config = config # JSON config listing, mainly, datastore paths
|
||||||
|
self.datastores = {} # Mapping of datastore name -> objects
|
||||||
|
self.machines = {} # Mapping of machine name -> objects
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
|
# Set up datastores and use the default datastore for "State" storage
|
||||||
self.init_datastores()
|
self.init_datastores()
|
||||||
self.state = ZConfig(self.datastores["default"])
|
self.state = ZConfig(self.datastores["default"])
|
||||||
|
|
||||||
|
# start API
|
||||||
|
self.api = ZApi(self)
|
||||||
|
|
||||||
|
# Set up shutdown signal handlers
|
||||||
signal.signal(signal.SIGINT, self.signal_handler) # ctrl-c
|
signal.signal(signal.SIGINT, self.signal_handler) # ctrl-c
|
||||||
signal.signal(signal.SIGTERM, self.signal_handler) # sigterm
|
signal.signal(signal.SIGTERM, self.signal_handler) # sigterm
|
||||||
|
|
||||||
def init_datastores(self):
|
def init_datastores(self):
|
||||||
|
"""
|
||||||
|
Per datastore in the config, create a ZDataStore object
|
||||||
|
"""
|
||||||
for name, info in self.config["datastores"].items():
|
for name, info in self.config["datastores"].items():
|
||||||
self.datastores[name] = ZDataStore(name, info["path"], info.get("init", False))
|
self.datastores[name] = ZDataStore(name, info["path"], info.get("init", False))
|
||||||
|
|
||||||
def init_machines(self):
|
def init_machines(self):
|
||||||
|
"""
|
||||||
|
Per machine in the on-disk state, create a machine object
|
||||||
|
"""
|
||||||
for machine_info in self.state.get_machines():
|
for machine_info in self.state.get_machines():
|
||||||
machine_id = machine_info["id"]
|
machine_id = machine_info["machine_id"]
|
||||||
self.add_machine(machine_id, machine_info["type"], machine_info["spec"])
|
self.add_machine(machine_id, machine_info["machine_type"], machine_info["spec"])
|
||||||
|
|
||||||
def add_machine(self, machine_id, machine_type, machine_spec):
|
def add_machine(self, machine_id, machine_type, machine_spec, write=False):
|
||||||
machine = MachineSpec(self, machine_id, machine_type, machine_spec)
|
"""
|
||||||
self.machines[machine_id] = machine
|
Create or update a machine.
|
||||||
if machine.options.get("autostart", False):
|
:param machine_id: alphanumeric id of machine to modify/create
|
||||||
|
:param machine_type: runnable type e.g. "q"
|
||||||
|
:param machine_spec: dictionary of machine options - see example/ubuntu.json
|
||||||
|
:param write: commit machinge changes to on-disk state
|
||||||
|
"""
|
||||||
|
# Find / create the machine
|
||||||
|
if machine_id in self.machines:
|
||||||
|
machine = self.machines[machine_id]
|
||||||
|
machine.options = machine_spec["options"]
|
||||||
|
machine.properties = machine_spec["properties"]
|
||||||
|
else:
|
||||||
|
machine = MachineSpec(self, machine_id, machine_type, machine_spec)
|
||||||
|
self.machines[machine_id] = machine
|
||||||
|
|
||||||
|
# Update if necessary
|
||||||
|
if write:
|
||||||
|
self.state.write_machine(machine_id, machine_type, machine_spec)
|
||||||
|
|
||||||
|
# Launch if machine is an autostarted machine
|
||||||
|
if machine.options.get("autostart", False) and machine.machine.get_status() == "stopped":
|
||||||
machine.start()
|
machine.start()
|
||||||
|
|
||||||
def signal_handler(self, signum, frame):
|
def signal_handler(self, signum, frame):
|
||||||
|
"""
|
||||||
|
Handle signals sent to the daemon. On any, exit.
|
||||||
|
"""
|
||||||
logging.critical("Got signal {}".format(signum))
|
logging.critical("Got signal {}".format(signum))
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# launch machines
|
"""
|
||||||
|
Main loop of the daemon. Sets up & starts machines, runs api, and waits.
|
||||||
|
"""
|
||||||
self.init_machines()
|
self.init_machines()
|
||||||
|
self.api.run()
|
||||||
# start API
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
# Wait?
|
|
||||||
while self.running:
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""
|
||||||
|
SHut down the hypervisor. Stop the API then shut down machines
|
||||||
|
"""
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self.api.stop()
|
||||||
with ThreadPoolExecutor(10) as pool:
|
with ThreadPoolExecutor(10) as pool:
|
||||||
for machine_id, machine in self.machines.items():
|
for machine_id in self.machines.keys():
|
||||||
pool.submit(machine.stop)
|
pool.submit(self.forceful_stop, machine_id)
|
||||||
|
# Sequential shutdown code below is easier to debug
|
||||||
|
# for machine_id in self.machines.keys():
|
||||||
|
# self.forceful_stop(machine_id)
|
||||||
|
|
||||||
|
def forceful_stop(self, machine_id, timeout=10): # make this timeout longer?
|
||||||
|
"""
|
||||||
|
Gracefully stop a machine by asking it nicely, waiting some time, then forcefully killing it.
|
||||||
|
"""
|
||||||
|
machine_spec = self.machines[machine_id]
|
||||||
|
nice_stop = Thread(target=machine_spec.stop)
|
||||||
|
nice_stop.start()
|
||||||
|
nice_stop.join(timeout)
|
||||||
|
|
||||||
|
if nice_stop.is_alive():
|
||||||
|
logging.error("%s did not respond in %s seconds, killing", machine_id, timeout)
|
||||||
|
machine_spec.machine.kill_machine()
|
||||||
|
|
||||||
|
def remove_machine(self, machine_id):
|
||||||
|
"""
|
||||||
|
Remove a stopped machine from the system
|
||||||
|
"""
|
||||||
|
assert self.machines[machine_id].machine.get_status() == "stopped"
|
||||||
|
self.state.remove_machine(machine_id)
|
||||||
|
del self.machines[machine_id]
|
||||||
|
|
||||||
|
|
||||||
class ZDataStore(object):
|
class ZDataStore(object):
|
||||||
|
"""
|
||||||
|
Helper module representing a data storage location somewhere on disk
|
||||||
|
"""
|
||||||
def __init__(self, name, root_path, init_ok=False):
|
def __init__(self, name, root_path, init_ok=False):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.root_path = root_path
|
self.root_path = root_path
|
||||||
|
@ -86,6 +154,9 @@ class ZDataStore(object):
|
||||||
|
|
||||||
|
|
||||||
class ZConfig(object):
|
class ZConfig(object):
|
||||||
|
"""
|
||||||
|
The Z Hypervisor daemon's interface to the on-disk config
|
||||||
|
"""
|
||||||
def __init__(self, datastore):
|
def __init__(self, datastore):
|
||||||
self.datastore = datastore
|
self.datastore = datastore
|
||||||
|
|
||||||
|
@ -95,6 +166,9 @@ class ZConfig(object):
|
||||||
os.makedirs(d, exist_ok=True)
|
os.makedirs(d, exist_ok=True)
|
||||||
|
|
||||||
def get_machines(self):
|
def get_machines(self):
|
||||||
|
"""
|
||||||
|
Return config of all machines on disk
|
||||||
|
"""
|
||||||
machines = []
|
machines = []
|
||||||
logging.info("Looking for machines in {}".format(self.machine_data_dir))
|
logging.info("Looking for machines in {}".format(self.machine_data_dir))
|
||||||
for mach_name in os.listdir(self.machine_data_dir):
|
for mach_name in os.listdir(self.machine_data_dir):
|
||||||
|
@ -102,6 +176,28 @@ class ZConfig(object):
|
||||||
machines.append(json.load(f))
|
machines.append(json.load(f))
|
||||||
return machines
|
return machines
|
||||||
|
|
||||||
|
def write_machine(self, machine_id, machine_type, machine_spec):
|
||||||
|
"""
|
||||||
|
Write a machine's config to the disk. Params similar to elsewhere.
|
||||||
|
"""
|
||||||
|
with open(os.path.join(self.machine_data_dir, "{}.json".format(machine_id)), "w") as f:
|
||||||
|
json.dump({"machine_id": machine_id,
|
||||||
|
"machine_type": machine_type,
|
||||||
|
"spec": machine_spec}, f, indent=4)
|
||||||
|
|
||||||
|
def write_machine_o(self, machine_obj):
|
||||||
|
"""
|
||||||
|
Similar to write_machine, but accepts a MachineSpec object
|
||||||
|
"""
|
||||||
|
self.write_machine(machine_obj.machine_id, machine_obj.machine_type, machine_obj.serialize())
|
||||||
|
|
||||||
|
def remove_machine(self, machine_id):
|
||||||
|
"""
|
||||||
|
Remove a machine from the on disk state
|
||||||
|
"""
|
||||||
|
json_path = os.path.join(self.machine_data_dir, "{}.json".format(machine_id))
|
||||||
|
os.unlink(json_path)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
@ -118,7 +214,7 @@ def main():
|
||||||
"state": "/opt/datastore/state/",
|
"state": "/opt/datastore/state/",
|
||||||
"datastores": {
|
"datastores": {
|
||||||
"default": {
|
"default": {
|
||||||
"path": "/opt/datastore/machines/"
|
"path": "/opt/z/datastore/machines/"
|
||||||
}
|
}
|
||||||
}}, f, indent=4)
|
}}, f, indent=4)
|
||||||
return
|
return
|
||||||
|
@ -128,3 +224,4 @@ def main():
|
||||||
|
|
||||||
z = ZHypervisorDaemon(config)
|
z = ZHypervisorDaemon(config)
|
||||||
z.run()
|
z.run()
|
||||||
|
print("Z has been shut down")
|
||||||
|
|
|
@ -4,26 +4,44 @@ from zhypervisor.clients.qmachine import QMachine
|
||||||
|
|
||||||
|
|
||||||
class MachineSpec(object):
|
class MachineSpec(object):
|
||||||
|
"""
|
||||||
|
Represents a machine we may control
|
||||||
|
"""
|
||||||
def __init__(self, master, machine_id, machine_type, spec):
|
def __init__(self, master, machine_id, machine_type, spec):
|
||||||
|
"""
|
||||||
|
Initialize options and properties of the machine. More importantly, initialize the self.machine object which
|
||||||
|
should be a subclass of zhypervisor.util.Machine.
|
||||||
|
"""
|
||||||
logging.info("Initting machine %s", machine_id)
|
logging.info("Initting machine %s", machine_id)
|
||||||
self.master = master
|
self.master = master
|
||||||
self.machine_id = machine_id
|
self.machine_id = machine_id
|
||||||
self.machine_type = machine_type
|
self.machine_type = machine_type
|
||||||
|
|
||||||
self.options = {} # hypervisor-level stuff like Autostart
|
self.options = spec["options"]
|
||||||
self.properties = {} # machine level stuff like processor count
|
self.properties = spec["properties"]
|
||||||
|
|
||||||
# TODO replace if/else with better system
|
# TODO replace if/else with better system
|
||||||
if machine_type == "q":
|
if machine_type == "q":
|
||||||
self.machine = QMachine(self)
|
self.machine = QMachine(self)
|
||||||
self.options = spec["options"]
|
|
||||||
self.properties = spec["properties"]
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown machine type: {}".format(machine_type))
|
raise Exception("Unknown machine type: {}".format(machine_type))
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""
|
||||||
|
Start this machine (pass-through)
|
||||||
|
"""
|
||||||
self.machine.start_machine()
|
self.machine.start_machine()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stop this machine
|
||||||
|
"""
|
||||||
self.machine.block_respawns = True
|
self.machine.block_respawns = True
|
||||||
self.machine.stop_machine()
|
self.machine.stop_machine()
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
"""
|
||||||
|
Return a serializable form of this machine's specs
|
||||||
|
"""
|
||||||
|
return {"options": self.options,
|
||||||
|
"properties": self.properties}
|
||||||
|
|
|
@ -8,6 +8,10 @@ from zhypervisor.logging import setup_logging
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
"""
|
||||||
|
Helper script for dealing with QEMU network interfaces. When QEMU starts, it calls this script passing an interface
|
||||||
|
name when the virtual machine has been started with it. This needs to enable the interface.
|
||||||
|
"""
|
||||||
setup_logging()
|
setup_logging()
|
||||||
_, tap_name = sys.argv
|
_, tap_name = sys.argv
|
||||||
logging.info("Enabling interface %s...", tap_name)
|
logging.info("Enabling interface %s...", tap_name)
|
||||||
|
|
|
@ -52,6 +52,12 @@ class Machine(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplemented()
|
raise NotImplemented()
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""
|
||||||
|
Get the machine's status (return one of "running" or "stopped")
|
||||||
|
"""
|
||||||
|
raise NotImplemented()
|
||||||
|
|
||||||
def get_datastore_path(self, datastore_name, *paths):
|
def get_datastore_path(self, datastore_name, *paths):
|
||||||
"""
|
"""
|
||||||
Resolve the filesystem path for a path in the given datastore
|
Resolve the filesystem path for a path in the given datastore
|
||||||
|
|
Loading…
Reference in New Issue