import cherrypy import logging import json 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', # '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': self.master.config.get("apiport", 3000), '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()}, "/disk": {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}, # "/task": {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}, # "/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.disk = ZApiDisks(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.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 """ 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("prop") class ZApiMachineProperty(object): """ Endpoint to modify machine properties """ exposed = True def __init__(self, root): self.root = root @cherrypy.tools.json_out() def GET(self, machine_id, prop): """ Fetch a property from a machine """ try: machine = self.root.master.machines[machine_id] return machine.properties[prop] except KeyError: raise cherrypy.HTTPError(status=404) @cherrypy.tools.json_out() def PUT(self, machine_id, prop, value): """ Set a property on a machine. """ value = json.loads(value) try: machine = self.root.master.machines[machine_id] assert machine.machine.get_status() == "stopped", "Machine must be stopped to modify" except KeyError: raise cherrypy.HTTPError(status=404) machine.properties[prop] = value machine.save() return [machine_id, prop, value] @cherrypy.tools.json_out() def DELETE(self, machine_id, prop): """ Remove a property on a machine. """ try: machine = self.root.master.machines[machine_id] assert machine.machine.get_status() == "stopped", "Machine must be stopped to modify" except KeyError: raise cherrypy.HTTPError(status=404) del machine.properties[prop] machine.save() return [machine_id, prop] @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) self.property = ZApiMachineProperty(self.root) @cherrypy.tools.json_out() def GET(self, machine_id=None, summary=False): """ Get a list of all machines or specific one if passed :param machine_id: machine 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({"properties": 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_spec): """ Create a new machine or update an existing machine :param machine_id: id of machine to create or modify '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_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 @cherrypy.popargs("disk_id") class ZApiDisks(): """ Endpoint for managing disks """ exposed = True def __init__(self, root): """ Endpoint to modify disks. PUT and DELETE require the disk not be attached. TODO how to attach/detach? """ self.root = root @cherrypy.tools.json_out() def GET(self, disk_id=None, summary=False): """ Get a list of disks or a specific one if passed :param disk_id: task to retrieve """ summary = summary in [True, 'True', 'true', 'yes', '1', 1] disks = {} for _disk_id, disk_spec in self.root.master.disks.items(): disk = {"disk_id": _disk_id} # "_status": machine_spec.machine.get_status()} attached / detached ? # if not summary: # machine.update({"machine_type": machine_spec.machine_type, # "spec": machine_spec.serialize()}) disk.update({"spec": disk_spec.serialize()}) disks[_disk_id] = disk if disk_id is not None: try: return [disks[disk_id]] except KeyError: raise cherrypy.HTTPError(status=404) else: return list(disks.values()) @cherrypy.tools.json_out() def PUT(self, disk_id, disk_spec): """ Create a new disk or update an existing disk :param disk_id: id of disk to create or modify 'param disk_spec: json dictionary describing the disk. see the 'spec' key of example/ubuntu-root.json """ disk_spec = json.loads(disk_spec) self.root.master.add_disk(disk_id, disk_spec, write=True) return disk_id def DELETE(self, disk_id): """ Delete a disk. Raises 404 if no such disk exists. Raises error if disk is not idle (detached) :param disk_id: ID of disk to remove """ self.root.master.remove_disk(disk_id) return disk_id