z-hypervisor/zhypervisor/api/api.py

240 lines
7.6 KiB
Python

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