diff --git a/example/docker-disk.json b/example/docker-disk.json new file mode 100644 index 0000000..a2a7ce3 --- /dev/null +++ b/example/docker-disk.json @@ -0,0 +1,7 @@ +{ + "disk_id": "dockerdata", + "properties": { + "type": "dockerdisk", + "datastore": "default" + } +} diff --git a/example/docker.json b/example/docker.json new file mode 100644 index 0000000..c0c7ab2 --- /dev/null +++ b/example/docker.json @@ -0,0 +1,9 @@ +{ + "machine_id": "dockertest", + "properties": { + "ports": [ + [1234, 80] + ], + "image": "dpedu/nginx" + } +} diff --git a/zhypervisor/clients/dockermachine.py b/zhypervisor/clients/dockermachine.py new file mode 100644 index 0000000..92df502 --- /dev/null +++ b/zhypervisor/clients/dockermachine.py @@ -0,0 +1,106 @@ +import os +import logging +import subprocess +from time import sleep +from threading import Thread +from zhypervisor.util import ZDisk +from zhypervisor.util import Machine + + +class DockerMachine(Machine): + machine_type = "docker" + + def __init__(self, spec): + Machine.__init__(self, spec) + self.proc = None + self.block_respawns = False + + def get_status(self): + """ + Return string "stopped" or "running" depending on machine status + """ + return "stopped" if self.proc is None else "running" + + def start_machine(self): + """ + If needed, launch the machine. + """ + if self.proc: + raise Exception("Machine already running!") + else: + docker_args = self.get_args() + logging.info("spawning docker with: {}".format(' '.join(docker_args))) + sleep(1) # anti-spin + self.proc = subprocess.Popen(docker_args, preexec_fn=lambda: os.setpgrp()) + # TODO handle stdout/err - stream to logs? + Thread(target=self.wait_on_exit, args=[self.proc]).start() + + def wait_on_exit(self, proc): + """ + Listener used by above start_machine to restart the machine if the machine exits + """ + proc.wait() + logging.info("docker process has exited") + self.proc = None + if not self.block_respawns and self.spec.properties.get("respawn", False): + self.start_machine() + + def stop_machine(self): + """ + Send the powerdown signal to the running machine + """ + if self.proc: + logging.info("stopping machine %s", self.spec.machine_id) + subprocess.check_call(["docker", "stop", self.spec.machine_id]) + self.proc.wait() + self.proc = None + + def kill_machine(self): + """ + Forcefully kill the running machine + """ + print("Terminating {}".format(self.proc)) + if self.proc: + subprocess.check_call(["docker", "kill", self.spec.machine_id]) + try: + self.proc.wait(5) + except subprocess.TimeoutError: + self.proc.kill() + self.proc.wait() + self.proc = None + + def get_args(self): + """ + Assemble the full argv array that will be executed for this machine + """ + argv = ['docker', 'run', '--rm', '--name', self.spec.machine_id, + '--hostname', self.spec.properties.get("hostname", self.spec.machine_id)] + + for hostport, containerport in self.spec.properties.get("ports", []): + argv.append("-p") + argv.append("{}:{}".format(int(hostport), int(containerport))) + + for volume in self.spec.properties.get("volumes", []): + disk_ob = self.spec.master.disks[volume["disk"]] + + volpath = disk_ob.get_path() + argv.append("-v") + argv.append("{}:{}".format(volpath, volume.get("mountpoint"))) + + if self.spec.properties.get("stopsignal", False): + argv += ['--stop-signal', int(self.spec.properties.get("stopsignal"))] + + argv += ['--stop-timeout', int(self.spec.properties.get("timeout", 25))] + + argv.append("{}".format(self.spec.properties.get("image"))) + if self.spec.properties.get("cmd", False): + argv.append("{}".format(self.spec.properties.get("cmd"))) + + return [str(arg) for arg in argv] + + +class DockerDisk(ZDisk): + + def validate(self): + pass + # alphanumeric only? underscores? diff --git a/zhypervisor/daemon.py b/zhypervisor/daemon.py index 6366a66..8ae25aa 100644 --- a/zhypervisor/daemon.py +++ b/zhypervisor/daemon.py @@ -12,6 +12,7 @@ from concurrent.futures import ThreadPoolExecutor from zhypervisor.logging import setup_logging from zhypervisor.machine import MachineSpec from zhypervisor.clients.qmachine import QDisk, IsoDisk +from zhypervisor.clients.dockermachine import DockerDisk from zhypervisor.util import ZDisk from zhypervisor.api.api import ZApi @@ -113,6 +114,8 @@ class ZHypervisorDaemon(object): disk = QDisk(datastore, disk_id, disk_spec) elif disk_type == "iso": disk = IsoDisk(datastore, disk_id, disk_spec) + elif disk_type == "dockerdisk": + disk = DockerDisk(datastore, disk_id, disk_spec) else: raise Exception("Unknown disk type: {}".format(disk_type)) disk = ZDisk(datastore, disk_id, disk_spec) diff --git a/zhypervisor/machine.py b/zhypervisor/machine.py index 9bec820..12092f7 100644 --- a/zhypervisor/machine.py +++ b/zhypervisor/machine.py @@ -1,6 +1,9 @@ import logging from zhypervisor.clients.qmachine import QMachine +from zhypervisor.clients.dockermachine import DockerMachine + +MACHINETYPES = {"q": QMachine, "docker": DockerMachine} class MachineSpec(object): @@ -18,11 +21,12 @@ class MachineSpec(object): self.properties = spec - # TODO replace if/else with better system - if self.properties["type"] == "q": - self.machine = QMachine(self) - else: - raise Exception("Unknown machine type: {}".format(self.properties["type"])) + try: + machine_type = MACHINETYPES[self.properties.get("type", None)] + except KeyError: + raise Exception("Unknown or missing machine type: {}".format(self.properties.get("type", None))) + + self.machine = machine_type(self) def start(self): """ diff --git a/zhypervisor/util.py b/zhypervisor/util.py index db8e3c7..042a14b 100644 --- a/zhypervisor/util.py +++ b/zhypervisor/util.py @@ -35,7 +35,7 @@ class Machine(object): def __init__(self, machine_spec): self.spec = machine_spec - def run_machine(self): + def start_machine(self): """ Run the machine and block until it exits (or was killed) """