diff --git a/CMakeLists.txt b/CMakeLists.txt
index fe0a485..ef531f1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -158,12 +158,12 @@ if (requirements)
add_subdirectory(src/web)
if (NOT MSVC AND NOT WIN32 AND NOT (${CMAKE_SYSTEM_NAME} MATCHES "SunOS"))
add_subdirectory(src/power)
- add_subdirectory(src/virt)
endif()
endif()
if (SAI_BUILDER)
add_subdirectory(src/builder)
add_subdirectory(src/resource)
+ add_subdirectory(src/virt)
if (NOT MSVC AND NOT WIN32 AND NOT (${CMAKE_SYSTEM_NAME} MATCHES "SunOS"))
add_subdirectory(src/device)
diff --git a/src/virt/CMakeLists.txt b/src/virt/CMakeLists.txt
index a74deea..ac66ef4 100644
--- a/src/virt/CMakeLists.txt
+++ b/src/virt/CMakeLists.txt
@@ -4,6 +4,8 @@ set(CPACK_DEBIAN_VIRT_PACKAGE_NAME "sai-virt")
set(SRCS
v-sai.c
v-ws-server.c
+ v-libvirt.c
+ v-http-api.c
../common/c-utils.c
../common/struct-metadata.c
)
diff --git a/src/virt/v-http-api.c b/src/virt/v-http-api.c
new file mode 100644
index 0000000..e76e26a
--- /dev/null
+++ b/src/virt/v-http-api.c
@@ -0,0 +1,65 @@
+/*
+ * sai-virt - v-http-api.c
+ *
+ * Copyright (C) 2019 - 2026 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+
+#include "v-private.h"
+
+int
+callback_virt_http(struct lws *wsi, enum lws_callback_reasons reason,
+ void *user, void *in, size_t len)
+{
+ const char *path;
+ char vm_id[64];
+
+ switch (reason) {
+ case LWS_CALLBACK_HTTP:
+ path = (const char *)in;
+ if (len > 16 && !strncmp(path, "/auto-power-off/", 16)) {
+ lws_strncpy(vm_id, path + 16, sizeof(vm_id));
+ lwsl_notice("%s: Received auto-power-off for %s\n", __func__, vm_id);
+ if (virt.ops)
+ virt.ops->destroy(&virt, vm_id);
+
+ lws_return_http_status(wsi, HTTP_STATUS_OK, NULL);
+ return -1; /* hang up */
+ }
+
+ if (len > 6 && !strncmp(path, "/stay/", 6)) {
+ lws_strncpy(vm_id, path + 6, sizeof(vm_id));
+ lwsl_notice("%s: Received stay for %s\n", __func__, vm_id);
+ /* We never return stay = true for ephemeral VMs */
+ uint8_t stay_res = '0';
+ if (lws_write(wsi, &stay_res, 1, LWS_WRITE_HTTP) != 1)
+ return -1;
+ return -1; /* hang up */
+ }
+
+ lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
+ return -1;
+
+ default:
+ break;
+ }
+
+ return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+const struct lws_protocols virt_protocols[] = {
+ {
+ "http-only",
+ callback_virt_http,
+ 0,
+ 0,
+ },
+ { NULL, NULL, 0, 0 } /* terminator */
+};
diff --git a/src/virt/v-libvirt.c b/src/virt/v-libvirt.c
new file mode 100644
index 0000000..5cfa876
--- /dev/null
+++ b/src/virt/v-libvirt.c
@@ -0,0 +1,92 @@
+/*
+ * sai-virt - v-libvirt.c
+ *
+ * Copyright (C) 2019 - 2026 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "v-private.h"
+
+/*
+ * We use virsh via lws_spawn_piped to avoid a hard dependency on libvirt.so
+ * and to easily integrate with the asynchronous lws event loop.
+ */
+
+static void
+reap_virsh(void *opaque, const lws_spawn_resource_us_t *res, siginfo_t *si, int we_killed_him)
+{
+ lwsl_notice("%s: virsh exited with code %d\n", __func__, si->si_status);
+}
+
+static int
+spawn_virsh_command(struct sai_virt *virt, const char * const *exec_array)
+{
+ struct lws_spawn_piped_info info;
+ struct lws_spawn_piped *lsp;
+
+ memset(&info, 0, sizeof(info));
+ info.vh = virt->vhost;
+ info.exec_array = exec_array;
+ info.max_log_lines = 10;
+ info.reap_cb = reap_virsh;
+
+ lsp = lws_spawn_piped(&info);
+ if (!lsp) {
+ lwsl_err("%s: failed to spawn virsh\n", __func__);
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+ops_libvirt_init(struct sai_virt *virt)
+{
+ lwsl_notice("%s: libvirt ops initialized\n", __func__);
+ return 0;
+}
+
+static int
+ops_libvirt_spawn(struct sai_virt *virt, const char *platform)
+{
+ /*
+ * Mock implementation:
+ * Ideally, we would run:
+ * virsh virt-clone --original sai-template-<platform> --name sai-ephemeral-<uuid> --auto-clone
+ * virsh start sai-ephemeral-<uuid>
+ */
+ const char * const exec_array[] = {
+ "/usr/bin/virsh", "list", "--all", NULL
+ };
+
+ lwsl_notice("%s: Spawning ephemeral VM for platform: %s\n", __func__, platform);
+
+ return spawn_virsh_command(virt, exec_array);
+}
+
+static int
+ops_libvirt_destroy(struct sai_virt *virt, const char *vm_id)
+{
+ /*
+ * Mock implementation:
+ * virsh destroy <vm_id>
+ * virsh undefine <vm_id> --remove-all-storage
+ */
+ lwsl_notice("%s: Destroying ephemeral VM: %s\n", __func__, vm_id);
+ return 0;
+}
+
+const sai_virt_ops_t ops_libvirt = {
+ .name = "libvirt",
+ .init = ops_libvirt_init,
+ .spawn = ops_libvirt_spawn,
+ .destroy = ops_libvirt_destroy,
+};
diff --git a/src/virt/v-private.h b/src/virt/v-private.h
index ff914e8..c7fd1fd 100644
--- a/src/virt/v-private.h
+++ b/src/virt/v-private.h
@@ -15,7 +15,14 @@
#include "../common/include/private.h"
#include <pthread.h>
-struct saiv_server;
+struct sai_virt;
+
+typedef struct sai_virt_ops {
+ const char *name;
+ int (*init)(struct sai_virt *virt);
+ int (*spawn)(struct sai_virt *virt, const char *platform);
+ int (*destroy)(struct sai_virt *virt, const char *vm_id);
+} sai_virt_ops_t;
/*
* Represents the virt process state
@@ -25,6 +32,8 @@ struct sai_virt {
struct lws_context *context;
struct lws_vhost *vhost;
+ const sai_virt_ops_t *ops;
+
const char *bind; /* listen socket binding */
const char *perms; /* user:group */
const char *port; /* port we listen on */
@@ -48,6 +57,8 @@ LWS_SS_USER_TYPEDEF
extern struct sai_virt virt;
extern const lws_ss_info_t ssi_saiv_server_link_t;
+extern const sai_virt_ops_t ops_libvirt;
+extern const struct lws_protocols virt_protocols[];
int saiv_config(struct sai_virt *virt, const char *d);
diff --git a/src/virt/v-sai.c b/src/virt/v-sai.c
index 18c65b5..1113272 100644
--- a/src/virt/v-sai.c
+++ b/src/virt/v-sai.c
@@ -65,10 +65,16 @@ int main(int argc, const char **argv)
if (gethostname(virt.hostname, sizeof(virt.hostname) - 1))
lws_strncpy(virt.hostname, "unknown", sizeof(virt.hostname));
+ const struct lws_protocols *pprotocols[] = {
+ &virt_protocols[0],
+ NULL
+ };
+
memset(&info, 0, sizeof info);
- info.port = CONTEXT_PORT_NO_LISTEN;
+ info.port = 8000;
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
LWS_SERVER_OPTION_VALIDATE_UTF8;
+ info.pprotocols = pprotocols;
signal(SIGINT, sigint_handler);
@@ -80,6 +86,9 @@ int main(int argc, const char **argv)
return 1;
}
+ virt.ops = &ops_libvirt;
+ virt.ops->init(&virt);
+
virt.vhost = lws_create_vhost(virt.context, &info);
if (!virt.vhost) {
lwsl_err("lws init failed\n");
diff --git a/src/virt/v-ws-server.c b/src/virt/v-ws-server.c
index afc7276..87dbf8a 100644
--- a/src/virt/v-ws-server.c
+++ b/src/virt/v-ws-server.c
@@ -60,6 +60,9 @@ saiv_server_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
lws_start_foreach_dll(struct lws_dll2 *, p, pt->tasks.head) {
sai_platform_pending_task_t *t = lws_container_of(p, sai_platform_pending_task_t, list);
lwsl_notice(" - %s: %u pending\n", t->plat, t->pending);
+ if (t->pending > 0 && virt.ops) {
+ virt.ops->spawn(&virt, t->plat);
+ }
} lws_end_foreach_dll(p);
}