Hyper (Hyper v0.1.0)

Copy Markdown View Source

Hyper is a distrubuted elixir virtual machine orchestrator.

Summary

Functions

Create a new virtual machine from an image.

Run argv inside VM vm — a pid (from create_vm/1) or a Hyper.Vm.Id.t() binary — and return its captured output.

Fork the running VM vm_id: boot a child from a copy-on-write clone of its disk, colocated when the parent's node has room and re-placed cluster-wide otherwise. Returns the child VM. See Hyper.Vm.fork/1 for the semantics.

The vm id for a VM handle -- the pid returned by create_vm/1. Resolves on the pid's owning node, so a VM just placed on a remote node is found immediately rather than waiting for the routing CRDT to propagate. nil if unknown.

Stop and tear down vm_id, wherever it runs in the cluster.

Total CPU time vm has actually executed, metered from its cgroup: the sum of its flushed usage windows (Hyper.Metering.Usage) plus the owning node's not-yet-flushed remainder. An idle VM accrues (almost) nothing — this measures compute performed, not compute allocated, and is the billing counter.

Cluster-wide: which node currently runs vm_id? nil if unknown.

Functions

create_vm(spec)

@spec create_vm(Hyper.Vm.Spec.t()) :: {:ok, Hyper.Vm.t()} | {:error, term()}

Create a new virtual machine from an image.

Placement: scheduled onto the most available node (Hyper.Cluster.Scheduler), preferring nodes that already have the image's layers resident. On the chosen node a per-VM writable rootfs is built over the image and the guest is booted.

exec(vm, argv, opts \\ [])

@spec exec(Hyper.Vm.t() | Hyper.Vm.Id.t(), [String.t()], keyword()) ::
  {:ok, %{stdout: binary(), stderr: binary(), exit_code: integer()}}
  | {:error, term()}

Run argv inside VM vm — a pid (from create_vm/1) or a Hyper.Vm.Id.t() binary — and return its captured output.

Cluster-routed: a pid resolves to its owning node directly (node(pid)); a vm_id resolves via the routing registry (whereis/1). The command runs on whichever node hosts the VM, via that node's Hyper.Node.exec/3.

Returns {:ok, %{stdout: binary(), stderr: binary(), exit_code: integer()}}, or {:error, reason}:agent_unavailable if the relay/agent is not yet reachable, :timeout if the gRPC deadline expired, :not_found if the VM cannot be resolved (unknown vm_id, or a pid whose id/owning node is gone), :node_unreachable if the owning node cannot be reached over the cluster. opts: :env (map %{String.t() => String.t()}), :cwd (string), :timeout (ms, gRPC deadline forwarded to the relay; default 30 000).

argv is executed directly, not through a shell. Use an absolute argv head (["/bin/echo", "hi"]): the guest runs as PID 1 with a near-empty environment, and a non-empty :env replaces it entirely, so a bare command name has no PATH to resolve against and returns exit code 127.

fork_vm(vm_id)

@spec fork_vm(Hyper.Vm.Id.t()) :: {:ok, Hyper.Vm.t()} | {:error, term()}

Fork the running VM vm_id: boot a child from a copy-on-write clone of its disk, colocated when the parent's node has room and re-placed cluster-wide otherwise. Returns the child VM. See Hyper.Vm.fork/1 for the semantics.

id(pid)

@spec id(Hyper.Vm.t()) :: Hyper.Vm.Id.t() | nil

The vm id for a VM handle -- the pid returned by create_vm/1. Resolves on the pid's owning node, so a VM just placed on a remote node is found immediately rather than waiting for the routing CRDT to propagate. nil if unknown.

Returns nil (rather than crashing the caller) if the owning node is unreachable -- e.g. it died with a VM just placed on it. In that case the VM died with its host, so "unknown" is the truthful answer. Only :erpc's own transport failures are swallowed; a genuine fault in the lookup still raises.

stop_vm(vm_id)

@spec stop_vm(Hyper.Vm.Id.t()) :: :ok | {:error, :not_found | :machine_unreachable}

Stop and tear down vm_id, wherever it runs in the cluster.

Resolves the VM's supervisor through the routing registry and terminates it on its owning node (Hyper.Node.stop_image_vm/1), which unwinds the whole per-VM tree: guest killed, writable volume reclaimed, routing entry removed. Registry removal propagates via CRDT, so whereis/1 may briefly still resolve a just-stopped VM on other nodes.

Returns {:error, :not_found} when no such VM is registered anywhere, and {:error, :machine_unreachable} when the owning node cannot be reached.

usage(vm)

@spec usage(Hyper.Vm.t() | Hyper.Vm.Id.t()) ::
  {:ok, Unit.Time.t()} | {:error, :not_found}

Total CPU time vm has actually executed, metered from its cgroup: the sum of its flushed usage windows (Hyper.Metering.Usage) plus the owning node's not-yet-flushed remainder. An idle VM accrues (almost) nothing — this measures compute performed, not compute allocated, and is the billing counter.

Works for live and stopped VMs alike; a stopped VM reports its recorded lifetime total. Returns {:error, :not_found} only when vm is entirely unknown: not running anywhere and never metered. If the owning node is unreachable, the flushed total alone is returned — an under-count of at most one flush window (~60s), consistent with metering's crash guarantee.

whereis(vm_id)

@spec whereis(Hyper.Vm.Id.t()) :: node() | nil

Cluster-wide: which node currently runs vm_id? nil if unknown.