diff --git a/CMakeLists.txt b/CMakeLists.txt
index ef531f1..2e0d0f9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,6 +3,7 @@ project(sai C)
find_package(libwebsockets CONFIG REQUIRED)
list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR})
include(CheckCSourceCompiles)
+include(CheckSymbolExists)
include(LwsCheckRequirements)
if (CMAKE_HOST_SYSTEM_NAME MATCHES "NetBSD" OR CMAKE_HOST_SYSTEM_NAME MATCHES "SunOS")
@@ -114,6 +115,11 @@ if (APPLE)
link_directories("/usr/local/lib")
endif()
+if (CMAKE_SYSTEM_NAME MATCHES "FreeBSD")
+ set(CMAKE_C_FLAGS "-I/usr/local/include ${CMAKE_C_FLAGS}")
+ link_directories("/usr/local/lib")
+endif()
+
if (LWS_OPENSSL_INCLUDE_DIRS)
set(CMAKE_C_FLAGS "-I\"${LWS_OPENSSL_INCLUDE_DIRS}\" ${CMAKE_C_FLAGS}")
endif()
@@ -132,6 +138,15 @@ CHECK_C_SOURCE_COMPILES("#include <libwebsockets.h>\nint
main(void) {\n#if defined(LWS_HAVE_LIBCAP)\n return
0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_LIBCAP)
+if (APPLE)
+ set(CMAKE_REQUIRED_LIBRARIES "-framework IOKit")
+ check_symbol_exists(kIOMainPortDefault "mach/mach.h;IOKit/IOKitLib.h" HAVE_KIOMAINPORTDEFAULT)
+ set(CMAKE_REQUIRED_LIBRARIES "")
+ if (HAVE_KIOMAINPORTDEFAULT)
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_KIOMAINPORTDEFAULT")
+ endif()
+endif()
+
#set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -v")
set (CMAKE_REQUIRED_INCLUDES "${CMAKE_REQUIRED_INCLUDES};${LIBWEBSOCKETS_INCLUDE_DIRS}")
diff --git a/assets/linux-fedora44.svg b/assets/linux-fedora44.svg
new file mode 100644
index 0000000..bfd8b52
--- /dev/null
+++ b/assets/linux-fedora44.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3.886 3.886" height="14.687" width="14.687"><defs><style>@font-face{font-family:"PT Sans Narrow";src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAiAAA8AAAAADDgAAgDFAAAAAAAAAAAAAAAAAAAAAAAAAABHUE9TAAABWAAAABAAAAAQABkADE9TLzIAAAFoAAAAVwAAAGBqFRzHY21hcAAAAcAAAABAAAABSgJJAehjdnQgAAACAAAAADAAAAAwCb4BwGZwZ20AAAIwAAABGAAAAZOeSOJvZ2FzcAAAA0gAAAAQAAAAEAAaAAlnbHlmAAADWAAAAjEAAAK8b2pFIWhlYWQAAAWMAAAANgAAADb7Wii/aGhlYQAABcQAAAAgAAAAJAW3AO9obXR4AAAF5AAAAAoAAAAKBOcAe2xvY2EAAAXwAAAACAAAAAgAvgFebWF4cAAABfgAAAAeAAAAIAIQAbpuYW1lAAAGGAAAAZcAAALjXFyVG3Bvc3QAAAewAAAAFAAAACD/uAA1cHJlcAAAB8QAAAC6AAABXCq2+IAAAQAAAAoADAAOAAAAAAAAeJxjYGE8xDiBgZmBgamLKYKBgcEbQjPGMRgx/OJgYuJmZWNiYmZiYmlgYFAXYECAAMcgRwYHoCpj5l//3jAwMP9iFFFgYJgOkmP8wrSHQQEIWQB5OA5WAHicY2BgYGaAYBkGRgYQcAHyGMF8FgYNIM0GpBkZmBiMGIz//wfywfT/C//PQdUDASMbAxmAiZGwmqEDAALwCS4AEQBBAEUAPAA3AEgASwBPAEAAVgAAAA7/OAAMAUAACAFxAAsB9AAOAg0ADAK8AA94nF2QPUvEQBCGZ008jZVgIyzCLiEWxx72VimSgKSJF4sdCz/gDsz9ByGFgmzhbxm7tcsfE2+yF664Zj7eGZ59dzyAqT0kjf0R4hu9+P/08HH1CwlEz08LD8IoVXYFiRdujgwLc81VZFRFUVYtbYrKKXe3cqpSb68rirOQebB2eKMIWttxfLCacpT7co14y5x45MSB45AJm4mwCQQG/PHSsakVRdeNvbfUF5LyAqXWqqShsTQUUiPy1mzvlPN7dzl5PmHPszkXpztKaymXBOjcrks19c5Jx/+Yeg/DgSDgUMgngS8xEqOs9KJvwqhPtRyFVKeafWLBbyembm3JTvXo9Gw8+cXSkvry5/BoPYDkw2IIiy2b33TjAAAAAwAIAAIAEwAB//8AA3icPZG7b9NAHMfvbMV5OCHOw84L241DHmqo3CQYU1S5QuVZAQsMjZARUoVYGBgCArEg1I0NFSok2KqgCgnOp8BCB0akLv4DKsTCxMDiGfj9LMry8e93d9/z3ecIJZf+RHRT2CNtmmeZ/jrX22KfE536TFeCVCKCD8sdcDNHff4FwPIhf52H5h3iIwKyPmsrrBCygs1yClNDfqBSf/ZZ3VcFn/+EmpVDvl+Gxc/r1B8TRs6ts6LdCIqaN44bCxor4405sYqF4lIg0ohX45JV7UAWI1ZUggKNFgc0PuezDGw2gZ35DuJ2XMHeM6mu1eGnGybOI57OATYQE8T0GB6AbxGoExDgPxAa4g4ufw/zfBeu5Bp0NPQE50SnT50Vmmw5TTVPVXdoCGr5iNCyOl2/NJhrLDSLZ04vT7ruG2o9MXTzVK9qOhd6La9yWc5qPbe5dlb/Pp9c1X/pglQbXFw8eX3JyMqEEpcQ+lj4SkzyLb4TyYp9llWYFH7alLakHUn0GVFYOmRpG4qgJEU4XQ9Z3f7nUAdt+qFDGRoZHAYpkJdNySgPXlGj8SvWQlazIXxo8IEIF32LeFUAdAyUA5gljYoBBrdxwAEls5fm1ISBihmbe4SRXUQCV7z4n6sYXcxhhE8RGODbAAq+UN1IbTpeAoy2LCnZ9cCvRu81RytGz/nwe680dEa9mr2gu+Wjjav9tStCy314/+78cTG9Olc5f82/dbM/WlYVp9q4Qchf23HDqAAAAAABAAAAAgDFlWBCPV8PPPUIGQPoAAAAAMk3jyAAAAAAyTlVOAA3//QBhALIAAAACQACAAAAAAAAeJxjYGRgYP717w0DA+MhBnMGO8YWBqAICmACAH5zBKkC7gAyAcIASQA3AAAAAAAAAL4BXnicY2BkYGBgZlBjANEMUJKbgYGJgXEyiAkAB9wAzgAAeJx9j0FKw0AUhv/RqriwiEsVGcGtaayCGNxosSAUDVp0KdGOaSQ0YSZaewLBC3gAVx7AC7jQc3gT/0xGsIJmmMz35v3/e28AzOMdAtW3xV2xQJ1RxROYwb7jSazi0HGNmhvHU1jAveNpzOHRcR0reKJL1GYZXePVscCyWHI8gbrYdjyJfXHguEbNg+MprIlnx9NYFB+O69gVn60sH+kk7hfy7UU2fX9HhpGOuqNcyU7R8+RemkqbN1Iro/Sd6nlhV55GAyOPIq2z4YmKb9NIf9voCuS4ImDhDX/87kxpk2QD2fR8f/NcHrc7YbdMr1fpflHkQaNhrnSSF8YzSeplOm5QdjFUl2ghQ44RNBLE6KOAxAt3Ez7XDilExGyELlU5FG86VPXgkfaQcskfbmMjxVPxvOO/VIZ0S5yyysAqjmxNzd5DnFAT45Z1yrvf3apegZ3j7xqBm3iD+z/dmZ3KcNqM2dLj2Xdu4pzRMdrsV/q/3etj7vJ9BacK0OAyuLLvzu2rPVs15ZnxNma+qnZBn8LlF/xJiBEAeJxjYGYAg/9bGYwYMAEzACzFAe14nGXOPw4BURDH8e/6t0skREQUEhuJahM0Co2IoNAQiUaj0ahRCjdwFK285h3BERzBEZhdExGbKT5vfpk3790gwOCQIhE4hrSUHKwwpU+HBmXACyyZWOLGEo8VE7o0v0mWIb2fGaw8tvwbSjCXsd8kGVuUYsOCAS1qn0WGnPzADW7gj4/b6shwJo+zM/ihe0NRKuwjpX9RwjkYClSi/Ko+1FmozJ1Uqz7Veqjcb6tr9aJa9R75Bl6+NzgAAA==) format("woff"); font-weight:normal;font-style:normal;}</style></defs><circle cx="1.943" cy="1.943" r="1.943" fill="#f9f9f9" fill-opacity=".12"/><path d="M.296 3.233C.334 2.69.23 2.125.362 1.598.492.966 1.027.458 1.664.353c.86-.22 1.744.512 1.877 1.35.06.583-.143 1.223-.662 1.544-.434.373-1.01.325-1.542.32-.363-.03-.907.152-1.04-.333z" fill="#51a2da" paint-order="markers fill stroke"/><path d="M1.814 1.75l.007.27-.33-.013c-.174.064-.357.247-.34.42.002.05-.013.104.025.144.056.08.128.208.222.214.12.035.236.05.342-.007.118-.095.195-.204.23-.34V1.334c.03-.192.127-.407.398-.427.077 0 .157-.016.23.015.12.03.2.12.264.222.042.093.093.19.082.295.006.114-.086.12-.166.147-.034-.032-.093-.046-.1-.098.004-.117-.023-.21-.097-.295-.123-.048-.19-.02-.312.027-.02.075-.064.146-.054.226l.004.293.225.001c.093.098.066.3-.067.273h-.16l.007.566c-.02.052-.026.11-.052.16-.185.235-.406.334-.673.358-.293-.06-.507-.243-.604-.498l-.038-.26a.79.79 0 0 1 .491-.566c.077-.015.153-.034.23-.026l.236.005z" fill="#fff" paint-order="markers fill stroke"/><text xml:space="preserve" x="-626.964" y="-109.218" font-size=".987" font-family="PT Sans Narrow" letter-spacing="0" word-spacing="0" writing-mode="lr-tb" direction="ltr" dominant-baseline="auto" baseline-shift="baseline"><tspan x="1.789" y="3.178" font-weight="600" font-family="A-OTF Shin Go Pro">32</tspan></text></svg>
diff --git a/assets/sai.css b/assets/sai.css
index 7075275..b060fba 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -647,7 +647,7 @@ div.ibuil {
display: inline-block;
vertical-align:top;
text-align:left;
- line-height: 50%;
+ line-height: normal;
float:left;
margin: 0.1em;
font-weight: normal;
@@ -659,7 +659,7 @@ div.ibuil {
user-select: none;
-webkit-user-select: none;
cursor: pointer;
- width: 80px;
+ width: 80px;
}
div.ibuil table {
@@ -1303,6 +1303,9 @@ canvas.power-graph {
.plat-peer-ip {
font-size: 0.8em;
color: #555;
+ display: block;
+ word-break: break-all;
+ margin-top: 2px;
}
.watchers-row {
display: flex;
diff --git a/assets/sai.js b/assets/sai.js
index 8dac6a9..f057d2e 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -1631,7 +1631,7 @@ function createBuilderDiv(plat) {
`<div class="res-bar"><div class="res-bar-inner res-bar-disk w-0"></div></div>` +
`</div>`;
if (authd && auth_is_admin && plat.peer_ip)
- innerHTML += `<span class="plat-peer-ip">${hsanitize(plat.peer_ip)}</span>`;
+ innerHTML += `<div class="plat-peer-ip">${hsanitize(plat.peer_ip)}</div>`;
innerHTML += `</td></tr></tbody></table>`;
platDiv.innerHTML = innerHTML;
diff --git a/src/builder/CMakeLists.txt b/src/builder/CMakeLists.txt
index f27d026..50f43b5 100644
--- a/src/builder/CMakeLists.txt
+++ b/src/builder/CMakeLists.txt
@@ -46,7 +46,7 @@ add_dependencies(${SUB} sai_git_hash)
if (SAI_BUILDER)
if (APPLE)
- target_link_libraries(sai-builder "-framework IOKit")
+ target_link_libraries(sai-builder "-framework IOKit" "-framework CoreFoundation")
endif()
endif()
@@ -80,7 +80,7 @@ add_dependencies(${SUB} sai_git_hash)
endif()
if (MSVC OR WIN32)
- target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)
+ target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib advapi32.lib)
endif()
if (MSVC OR WIN32)
diff --git a/src/builder/b-private.h b/src/builder/b-private.h
index 39d31ce..98f5375 100644
--- a/src/builder/b-private.h
+++ b/src/builder/b-private.h
@@ -77,7 +77,7 @@ struct saib_opaque_spawn {
};
#define SAI_LOAD_REPORT_US (1 * LWS_US_PER_SEC)
-#define SAI_IDLE_GRACE_US (30 * LWS_US_PER_SEC)
+#define SAI_IDLE_GRACE_US (builder.one_shot_active ? LWS_US_PER_SEC : (30 * LWS_US_PER_SEC))
#define SAI_STAY_POLL_US (20 * LWS_US_PER_SEC)
#define SAI_CLEANUP_JOBS_INTERVAL_US (60 * 60 * LWS_US_PER_SEC)
#define SAI_CLEANUP_JOB_DIR_MIN_AGE_SECS (24ull * 3600u)
@@ -162,6 +162,8 @@ struct sai_builder {
char event_affinity[65];
char event_affinity_active;
+ char one_shot_task_uuid[65];
+ char one_shot_active;
/* resource management */
diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c
index dff1381..03487a8 100644
--- a/src/builder/b-sai.c
+++ b/src/builder/b-sai.c
@@ -30,6 +30,7 @@
#include <signal.h>
#include <limits.h>
#include <stdlib.h>
+#include <fcntl.h>
#include <sys/types.h>
#if !defined(WIN32)
@@ -44,6 +45,8 @@
#if defined(__APPLE__)
#include <sys/stat.h> /* for mkdir() */
#include <mach-o/dyld.h>
+#include <IOKit/IOKitLib.h>
+#include <CoreFoundation/CoreFoundation.h>
#endif
#if defined(WIN32)
@@ -573,6 +576,11 @@ saib_app_run(int argc, const char **argv)
lwsl_notice("%s: event affinity mode enabled (ephemeral VM)\n", __func__);
}
+ if (lws_cmdline_option(argc, argv, "-O")) {
+ builder.one_shot_active = 1;
+ lwsl_notice("%s: one-shot mode enabled (single task ephemeral VM)\n", __func__);
+ }
+
#if defined(__NetBSD__) || defined(__OpenBSD__)
if (lws_cmdline_option(argc, argv, "-D")) {
if (lws_daemonize("/var/run/sai_builder.pid"))
@@ -626,6 +634,95 @@ saib_app_run(int argc, const char **argv)
return 1;
}
+#if defined(__linux__)
+ /*
+ * If running inside a VM spawned by sai-virt, we might have been
+ * passed a dynamic prefix via QEMU fw_cfg.
+ */
+ {
+ int fd = open("/sys/firmware/qemu_fw_cfg/by_name/opt/sai_builder_id/raw", O_RDONLY);
+ if (fd >= 0) {
+ char fw_id[128];
+ ssize_t fw_n = read(fd, fw_id, sizeof(fw_id) - 1);
+ if (fw_n > 0) {
+ fw_id[fw_n] = '\0';
+ /* Remove any trailing newline */
+ while (fw_n > 0 && (fw_id[fw_n - 1] == '\n' || fw_id[fw_n - 1] == '\r'))
+ fw_id[--fw_n] = '\0';
+
+ if (fw_n > 0) {
+ char compound[256];
+ lws_snprintf(compound, sizeof(compound), "%s-%s", fw_id, builder.host ? builder.host : "");
+
+ char *new_host = lwsac_use(&builder.conf_head, strlen(compound) + 1, 512);
+ if (new_host) {
+ strcpy(new_host, compound);
+ builder.host = new_host;
+ lwsl_notice("%s: Applied dynamic fw_cfg builder identity: %s\n", __func__, builder.host);
+ }
+ }
+ }
+ close(fd);
+ }
+ }
+#elif defined(WIN32)
+ {
+ HKEY hKey;
+ char fw_id[128];
+ DWORD dwType = REG_SZ;
+ DWORD dwSize = sizeof(fw_id);
+
+ if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\BIOS", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
+ if (RegQueryValueExA(hKey, "SystemSerialNumber", NULL, &dwType, (LPBYTE)fw_id, &dwSize) == ERROR_SUCCESS) {
+ if (!strncmp(fw_id, "sai_builder_id:", 15)) {
+ char *id = fw_id + 15;
+ char compound[256];
+ lws_snprintf(compound, sizeof(compound), "%s-%s", id, builder.host ? builder.host : "");
+
+ char *new_host = lwsac_use(&builder.conf_head, strlen(compound) + 1, 512);
+ if (new_host) {
+ strcpy(new_host, compound);
+ builder.host = new_host;
+ lwsl_notice("%s: Applied dynamic SMBIOS builder identity: %s\n", __func__, builder.host);
+ }
+ }
+ }
+ RegCloseKey(hKey);
+ }
+ }
+#elif defined(__APPLE__)
+ {
+#if defined(HAVE_KIOMAINPORTDEFAULT)
+ io_service_t platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
+#else
+ io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
+#endif
+ if (platformExpert) {
+ CFTypeRef serialNumberAsCFString = IORegistryEntryCreateCFProperty(platformExpert, CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);
+ if (serialNumberAsCFString) {
+ if (CFGetTypeID(serialNumberAsCFString) == CFStringGetTypeID()) {
+ char fw_id[128];
+ if (CFStringGetCString(serialNumberAsCFString, fw_id, sizeof(fw_id), kCFStringEncodingUTF8)) {
+ if (!strncmp(fw_id, "sai_builder_id:", 15)) {
+ char *id = fw_id + 15;
+ char compound[256];
+ lws_snprintf(compound, sizeof(compound), "%s-%s", id, builder.host ? builder.host : "");
+
+ char *new_host = lwsac_use(&builder.conf_head, strlen(compound) + 1, 512);
+ if (new_host) {
+ strcpy(new_host, compound);
+ builder.host = new_host;
+ lwsl_notice("%s: Applied dynamic SMBIOS builder identity: %s\n", __func__, builder.host);
+ }
+ }
+ }
+ }
+ CFRelease(serialNumberAsCFString);
+ }
+ IOObjectRelease(platformExpert);
+ }
+ }
+#endif
/*
* We need to sample the true uid / gid we should use inside
diff --git a/src/builder/b-task.c b/src/builder/b-task.c
index 9ec00f1..c4e31d2 100644
--- a/src/builder/b-task.c
+++ b/src/builder/b-task.c
@@ -177,6 +177,19 @@ saib_can_accept_task(sai_task_t *task, sai_plat_t *sp)
unsigned int executing = 0;
+ if (builder.one_shot_active) {
+ if (!builder.one_shot_task_uuid[0]) {
+ lws_strncpy(builder.one_shot_task_uuid, task->uuid,
+ sizeof(builder.one_shot_task_uuid));
+ lwsl_notice("%s: locked one-shot affinity to task %s\n",
+ __func__, builder.one_shot_task_uuid);
+ } else if (strcmp(builder.one_shot_task_uuid, task->uuid)) {
+ lwsl_notice("%s: reject task %s: one-shot affinity locked to %s\n",
+ __func__, task->uuid, builder.one_shot_task_uuid);
+ return 1;
+ }
+ }
+
if (builder.event_affinity_active) {
if (!builder.event_affinity[0]) {
lws_strncpy(builder.event_affinity, task->event_uuid,
diff --git a/src/common/c-utils.c b/src/common/c-utils.c
index 473d798..29cf97f 100644
--- a/src/common/c-utils.c
+++ b/src/common/c-utils.c
@@ -142,8 +142,6 @@ sai_ss_serialize_queue_helper(struct lws_ss_handle *h,
r = lws_struct_json_serialize(js, buf + LWS_PRE,
sizeof(buf) - LWS_PRE, &w);
- // lwsl_hexdump_err(buf + LWS_PRE, w);
-
sai_ss_queue_frag_on_buflist_REQUIRES_LWS_PRE(h, buflist,
buf + LWS_PRE, w, (unsigned int)((fi ? LWSSS_FLAG_SOM : 0) |
(r == LSJS_RESULT_FINISH ? LWSSS_FLAG_EOM : 0)));
diff --git a/src/power/p-sai.c b/src/power/p-sai.c
index fba4a73..def40ab 100644
--- a/src/power/p-sai.c
+++ b/src/power/p-sai.c
@@ -219,12 +219,12 @@ sul_pcon_check_cb(lws_sorted_usec_list_t *sul)
/* Rule 2: User Keep On -> Turn ON */
if (pc->flags & SAIP_PCON_F_MANUAL_STAY) {
target_on = 1;
- lwsl_warn("%s: PCON %s has user keep on -> Force ON\n", __func__, pc->name);
+ // lwsl_warn("%s: PCON %s has user keep on -> Force ON\n", __func__, pc->name);
}
/* Rule 3: Server Requested -> Turn ON */
else if (pc->flags & SAIP_PCON_F_NEEDED) {
target_on = 1;
- lwsl_warn("%s: PCON %s has server request -> Force ON\n", __func__, pc->name);
+ // lwsl_warn("%s: PCON %s has server request -> Force ON\n", __func__, pc->name);
}
lwsl_info("%s: PCON %s check: target=%d, current=%d (flags=0x%x)\n",
diff --git a/src/server/s-power.c b/src/server/s-power.c
index 1d77d5a..9f0cdc2 100644
--- a/src/server/s-power.c
+++ b/src/server/s-power.c
@@ -288,6 +288,7 @@ bail:
*/
struct pcon_lookup_ctx {
+ char *start;
char *p;
const char *end;
int *n;
@@ -304,6 +305,20 @@ cb_lookup_pcon(void *user, int cols, char **values, char **name)
m = strlen(values[0]);
+ if (ctx->start) {
+ const char *curr = ctx->start;
+ while (curr && *curr) {
+ const char *comma = strchr(curr, ',');
+ size_t len = comma ? (size_t)(comma - curr) : strlen(curr);
+ if (len == m && !strncmp(curr, values[0], m)) {
+ return 0;
+ }
+ if (!comma)
+ break;
+ curr = comma + 1;
+ }
+ }
+
if (*ctx->n)
*ctx->p++ = ',';
@@ -400,14 +415,17 @@ sais_power_tx(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl)
char q[256], query[256];
int r;
+ ctx.start = (char *)start;
ctx.p = (char *)p;
ctx.end = (const char *)end;
ctx.n = &n;
lws_sql_purify(q, pl->plat, sizeof(q));
lws_snprintf(query, sizeof(query),
- "SELECT DISTINCT pcon FROM builders WHERE platform = '%s'",
- q);
+ "SELECT DISTINCT pcon FROM builders WHERE platform = '%s' "
+ "UNION "
+ "SELECT DISTINCT pcon_name FROM pcon_builders WHERE builder_name = '%s'",
+ q, q);
r = sqlite3_exec(vhd->server.pdb, query, cb_lookup_pcon, &ctx, NULL);
diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c
index b665f1d..3045e10 100644
--- a/src/server/s-ws-web.c
+++ b/src/server/s-ws-web.c
@@ -219,34 +219,7 @@ sais_list_pcons(struct vhd *vhd)
sqlite3_finalize(pcon_stmt);
}
- /* Iterate PCONs and populate controlled builders */
- lws_start_foreach_dll(struct lws_dll2 *, d, pmb.power_controllers.head) {
- pc = lws_container_of(d, sai_power_controller_t, list);
- char query[256];
- sqlite3_stmt *stmt;
-
- /* Manually query builders mapped to this pcon to avoid LWS struct nested 0-row ac free bug */
- lws_snprintf(query, sizeof(query),
- "SELECT builder_name FROM pcon_builders WHERE pcon_name = '%s' "
- "ORDER BY builder_name LIMIT 100", pc->name);
-
- if (sqlite3_prepare_v2(vhd->server.pdb, query, -1, &stmt, NULL) == SQLITE_OK) {
- while (sqlite3_step(stmt) == SQLITE_ROW) {
- const char *bname = (const char *)sqlite3_column_text(stmt, 0);
- if (bname) {
- sai_controlled_builder_t *c =
- lwsac_use_zero(&ac, sizeof(*c), 2048);
- if (c) {
- lws_strncpy(c->name, bname, sizeof(c->name));
- lws_dll2_add_tail(&c->list,
- &pc->controlled_builders_owner);
- }
- }
- }
- sqlite3_finalize(stmt);
- }
- } lws_end_foreach_dll(d);
/* Serialize */
js = lws_struct_json_serialize_create(lsm_schema_power_managed_builders,
diff --git a/src/virt/CMakeLists.txt b/src/virt/CMakeLists.txt
index ac66ef4..489d49d 100644
--- a/src/virt/CMakeLists.txt
+++ b/src/virt/CMakeLists.txt
@@ -1,55 +1,66 @@
-set(SUB "sai-virt")
-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
-)
-
-set(requirements 1)
-require_lws_config(LWS_WITH_CLIENT 1 requirements)
-require_lws_config(LWS_WITH_STRUCT_JSON 1 requirements)
-require_lws_config(LWS_WITH_SECURE_STREAMS 1 requirements)
-
-if (requirements)
- add_executable(${SUB} ${SRCS})
-add_dependencies(${SUB} sai_git_hash)
- if (APPLE)
- set_property(TARGET ${SUB} PROPERTY MACOSX_RPATH YES)
- endif()
-
- if (SAI_LWS_INC_PATH)
- target_include_directories(${SUB} PRIVATE ${SAI_LWS_INC_PATH})
- endif()
-
- if (HAS_LIBCAP)
- find_library(CAP_LIB_PATH NAMES "cap")
- endif()
-
- target_link_libraries(${SUB} websockets ${SAI_LWS_LIB_PATH})
-
- message("LWS_OPENSSL_LIBRARIES ${SUB} '${LWS_OPENSSL_LIBRARIES}'")
- if (LWS_OPENSSL_LIBRARIES)
- target_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})
- endif()
-
- if (SAI_EXT_PTHREAD_LIBRARIES)
- target_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})
- endif()
- if (HAS_LIBCAP)
- target_link_libraries(${SUB} ${CAP_LIB_PATH})
- endif()
-
- if (MSVC OR WIN32)
- target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)
- endif()
-
- install(TARGETS "sai-virt"
- RUNTIME DESTINATION "${BIN_DIR}" COMPONENT virt)
-
-endif(requirements)
+find_library(VIRT_LIB_PATH NAMES "virt")
+
+if (VIRT_LIB_PATH)
+
+ set(SUB "sai-virt")
+ set(CPACK_DEBIAN_VIRT_PACKAGE_NAME "sai-virt")
+
+ set(SRCS
+ v-sai.c
+ v-ws-server.c
+ v-libvirt.c
+ v-http-api.c
+ v-conf.c
+ ../common/c-utils.c
+ ../common/struct-metadata.c
+ )
+
+ set(requirements 1)
+ require_lws_config(LWS_WITH_CLIENT 1 requirements)
+ require_lws_config(LWS_WITH_STRUCT_JSON 1 requirements)
+ require_lws_config(LWS_WITH_SECURE_STREAMS 1 requirements)
+
+ if (requirements)
+ add_executable(${SUB} ${SRCS})
+ add_dependencies(${SUB} sai_git_hash)
+ if (APPLE)
+ set_property(TARGET ${SUB} PROPERTY MACOSX_RPATH YES)
+ endif()
+
+ if (SAI_LWS_INC_PATH)
+ target_include_directories(${SUB} PRIVATE ${SAI_LWS_INC_PATH})
+ endif()
+
+ if (HAS_LIBCAP)
+ find_library(CAP_LIB_PATH NAMES "cap")
+ endif()
+
+ target_link_libraries(${SUB} websockets ${SAI_LWS_LIB_PATH} ${VIRT_LIB_PATH})
+
+ message("LWS_OPENSSL_LIBRARIES ${SUB} '${LWS_OPENSSL_LIBRARIES}'")
+ if (LWS_OPENSSL_LIBRARIES)
+ target_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})
+ endif()
+
+ if (SAI_EXT_PTHREAD_LIBRARIES)
+ target_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})
+ endif()
+ if (HAS_LIBCAP)
+ target_link_libraries(${SUB} ${CAP_LIB_PATH})
+ endif()
+
+ if (MSVC OR WIN32)
+ target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)
+ endif()
+
+ install(TARGETS "sai-virt"
+ RUNTIME DESTINATION "${BIN_DIR}" COMPONENT virt)
+
+ endif(requirements)
+
+else()
+ message("libvirt not found, skipping sai-virt")
+endif()
+
include(CPack)
+
diff --git a/src/virt/v-conf.c b/src/virt/v-conf.c
new file mode 100644
index 0000000..33a663e
--- /dev/null
+++ b/src/virt/v-conf.c
@@ -0,0 +1,203 @@
+/*
+ * sai-virt - v-conf.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 <fcntl.h>
+#include <unistd.h>
+
+#include "v-private.h"
+
+static const char * const paths_plat[] = {
+ "name",
+ "platform",
+ "base_image",
+ "overlay_size",
+};
+
+enum {
+ VJ_NAME,
+ VJ_PLATFORM,
+ VJ_BASE_IMAGE,
+ VJ_OVERLAY_SIZE,
+};
+
+struct v_conf_ctx {
+ saiv_plat_t *plat;
+};
+
+static signed char
+saiv_conf_plat_cb(struct lejp_ctx *ctx, char reason)
+{
+ struct v_conf_ctx *v = (struct v_conf_ctx *)ctx->user;
+
+ if (reason == LEJPCB_OBJECT_START && ctx->path_match == 0) {
+ v->plat = malloc(sizeof(*v->plat));
+ if (!v->plat)
+ return -1;
+ memset(v->plat, 0, sizeof(*v->plat));
+ return 0;
+ }
+
+ if (reason == LEJPCB_OBJECT_END && ctx->path_match == 0) {
+ if (v->plat && v->plat->name[0] && v->plat->base_image[0]) {
+ lws_dll2_add_tail(&v->plat->list, &virt.plat_owner);
+ lwsl_notice("Added platform %s (base %s)\n", v->plat->name, v->plat->base_image);
+ v->plat = NULL;
+ } else if (v->plat) {
+ lwsl_err("Platform definition missing name or base_image\n");
+ free(v->plat);
+ v->plat = NULL;
+ }
+ return 0;
+ }
+
+ if (reason == LEJPCB_VAL_STR_END) {
+ switch (ctx->path_match - 1) {
+ case VJ_NAME:
+ lws_strncpy(v->plat->name, ctx->buf, sizeof(v->plat->name));
+ break;
+ case VJ_PLATFORM:
+ lws_strncpy(v->plat->platform, ctx->buf, sizeof(v->plat->platform));
+ break;
+ case VJ_BASE_IMAGE:
+ lws_strncpy(v->plat->base_image, ctx->buf, sizeof(v->plat->base_image));
+ break;
+ case VJ_OVERLAY_SIZE:
+ lws_strncpy(v->plat->overlay_size, ctx->buf, sizeof(v->plat->overlay_size));
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int
+saiv_conf_dir_cb(const char *dirpath, void *user, struct lws_dir_entry *lde)
+{
+ char filepath[256];
+ struct lejp_ctx ctx;
+ struct v_conf_ctx vctx;
+ int fd, m = 0;
+ ssize_t n;
+ uint8_t buf[1024];
+
+ if (lde->name[0] == '.')
+ return 0;
+
+ lws_snprintf(filepath, sizeof(filepath), "%s/%s", dirpath, lde->name);
+
+ fd = open(filepath, O_RDONLY);
+ if (fd < 0) {
+ lwsl_err("Cannot open %s\n", filepath);
+ return 0;
+ }
+
+ memset(&vctx, 0, sizeof(vctx));
+ lejp_construct(&ctx, saiv_conf_plat_cb, &vctx, paths_plat, LWS_ARRAY_SIZE(paths_plat));
+
+ while ((n = read(fd, buf, sizeof(buf))) > 0) {
+ m = lejp_parse(&ctx, buf, (int)n);
+ if (m < 0 && m != LEJP_CONTINUE) {
+ lwsl_err("%s: JSON decode error %d\n", filepath, m);
+ break;
+ }
+ }
+ close(fd);
+ lejp_destruct(&ctx);
+
+ if (vctx.plat)
+ free(vctx.plat);
+
+ return 0;
+}
+
+int
+saiv_config(struct sai_virt *virt, const char *d)
+{
+ lwsl_notice("Parsing configuration directory %s\n", d);
+ return lws_dir(d, virt, saiv_conf_dir_cb);
+}
+
+static const char * const paths_global[] = {
+ "servers[].url",
+ "max_vms",
+};
+
+enum {
+ VJG_SERVER_URL,
+ VJG_MAX_VMS,
+};
+
+static signed char
+saiv_conf_global_cb(struct lejp_ctx *ctx, char reason)
+{
+ struct sai_virt *v = (struct sai_virt *)ctx->user;
+
+ if (reason == LEJPCB_VAL_STR_END) {
+ switch (ctx->path_match - 1) {
+ case VJG_SERVER_URL:
+ {
+ saiv_server_t *srv = malloc(sizeof(*srv));
+ if (srv) {
+ memset(srv, 0, sizeof(*srv));
+ srv->url = strdup(ctx->buf);
+ if (lws_ss_create(v->context, 0, &ssi_saiv_server_link_t,
+ srv, &srv->ss, NULL, NULL)) {
+ lwsl_err("%s: failed to create ss\n", __func__);
+ free((void *)srv->url);
+ free(srv);
+ } else {
+ lws_dll2_add_tail(&srv->list, &v->sai_server_owner);
+ lwsl_notice("Added server %s\n", srv->url);
+ }
+ }
+ break;
+ }
+ case VJG_MAX_VMS:
+ v->max_vms = atoi(ctx->buf);
+ lwsl_notice("Set max_vms to %d\n", v->max_vms);
+ break;
+ }
+ }
+ return 0;
+}
+
+int
+saiv_config_global(struct sai_virt *virt, const char *filepath)
+{
+ struct lejp_ctx ctx;
+ int fd, m = 0;
+ ssize_t n;
+ uint8_t buf[1024];
+
+ lwsl_notice("Parsing global configuration %s\n", filepath);
+
+ fd = open(filepath, O_RDONLY);
+ if (fd < 0) {
+ lwsl_err("Cannot open %s\n", filepath);
+ return 1;
+ }
+
+ lejp_construct(&ctx, saiv_conf_global_cb, virt, paths_global, LWS_ARRAY_SIZE(paths_global));
+
+ while ((n = read(fd, buf, sizeof(buf))) > 0) {
+ m = lejp_parse(&ctx, buf, (int)n);
+ if (m < 0 && m != LEJP_CONTINUE) {
+ lwsl_err("%s: JSON decode error %d\n", filepath, m);
+ break;
+ }
+ }
+ close(fd);
+ lejp_destruct(&ctx);
+
+ return 0;
+}
diff --git a/src/virt/v-http-api.c b/src/virt/v-http-api.c
index e0d3ae2..5a7ab55 100644
--- a/src/virt/v-http-api.c
+++ b/src/virt/v-http-api.c
@@ -63,6 +63,27 @@ callback_virt_http(struct lws *wsi, enum lws_callback_reasons reason,
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);
+
+ saiv_vm_t *found_vm = NULL;
+ lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
+ saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
+ lws_start_foreach_dll(struct lws_dll2 *, v, vp->vm_owner.head) {
+ saiv_vm_t *vm = lws_container_of(v, saiv_vm_t, list);
+ if (!strcmp(vm->name, vm_id)) {
+ found_vm = vm;
+ break;
+ }
+ } lws_end_foreach_dll(v);
+ if (found_vm)
+ break;
+ } lws_end_foreach_dll(d);
+
+ if (found_vm) {
+ /* Extend the safety timeout since the VM is alive and communicating */
+ lws_sul_schedule(virt.context, 0, &found_vm->sul_timeout,
+ saiv_vm_timeout_cb, 5 * 60 * LWS_US_PER_SEC);
+ }
+
/* We never return stay = true for ephemeral VMs */
uint8_t stay_res = '0';
if (lws_write(wsi, &stay_res, 1, LWS_WRITE_HTTP) != 1)
diff --git a/src/virt/v-libvirt.c b/src/virt/v-libvirt.c
index 605d9db..d31a844 100644
--- a/src/virt/v-libvirt.c
+++ b/src/virt/v-libvirt.c
@@ -12,44 +12,74 @@
#include <libwebsockets.h>
#include <string.h>
#include <stdio.h>
+#include <stdlib.h>
+#include <libvirt/libvirt.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.
- */
+virConnectPtr conn;
-static void
-reap_virsh(void *opaque, const lws_spawn_resource_us_t *res, siginfo_t *si, int we_killed_him)
+static char *
+replace_string(const char *orig, const char *rep, const char *with)
{
- lwsl_notice("%s: virsh exited with code %d\n", __func__, si->si_status);
+ char *result;
+ char *ins;
+ char *tmp;
+ size_t len_rep;
+ size_t len_with;
+ size_t len_front;
+ size_t count;
+
+ if (!orig || !rep)
+ return NULL;
+ len_rep = strlen(rep);
+ if (len_rep == 0)
+ return NULL;
+ if (!with)
+ with = "";
+ len_with = strlen(with);
+
+ ins = (char *)orig;
+ for (count = 0; (tmp = strstr(ins, rep)); ++count) {
+ ins = tmp + len_rep;
+ }
+
+ tmp = result = malloc(strlen(orig) + (count * len_with) + 1);
+ if (!result)
+ return NULL;
+
+ while (count--) {
+ ins = strstr(orig, rep);
+ len_front = lws_ptr_diff_size_t(ins, orig);
+ tmp = strncpy(tmp, orig, len_front) + len_front;
+ tmp = strcpy(tmp, with) + len_with;
+ orig += len_front + len_rep;
+ }
+ strcpy(tmp, orig);
+ return result;
}
-static int
-spawn_virsh_command(struct sai_virt *virt, const char * const *exec_array)
+static void
+strip_xml_tags(char *xml, const char *start_tag, const char *end_tag)
{
- 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;
+ char *start;
+ while ((start = strstr(xml, start_tag))) {
+ char *end = strstr(start, end_tag);
+ if (!end)
+ break;
+ end += strlen(end_tag);
+ memmove(start, end, strlen(end) + 1);
}
-
- return 0;
}
static int
ops_libvirt_init(struct sai_virt *virt)
{
+ conn = virConnectOpen("qemu:///system");
+ if (!conn) {
+ lwsl_err("Failed to open connection to qemu:///system\n");
+ return 1;
+ }
lwsl_notice("%s: libvirt ops initialized\n", __func__);
return 0;
}
@@ -57,19 +87,202 @@ ops_libvirt_init(struct sai_virt *virt)
static int
ops_libvirt_spawn(struct sai_virt *virt, struct saiv_vm *vm)
{
- const char * const exec_array[] = {
- "/usr/bin/virsh", "list", "--all", NULL
- };
+ virDomainPtr dom;
+ virStoragePoolPtr pool;
+ virStorageVolPtr vol;
+ char *xml, *xml2, *xml3;
+ char vol_xml[1024];
+ char overlay_path[256];
+ char orig_name_tag[128];
+ char new_name_tag[128];
+ char orig_source_tag[256];
+ char new_source_tag[256];
+ const char *shm_pool_xml = "<pool type='dir'><name>sai_shm</name><target><path>/dev/shm</path></target></pool>";
- lwsl_notice("%s: Spawning ephemeral VM %s for platform: %s\n", __func__, vm->name, vm->plat->name);
+ lwsl_notice("%s: Spawning ephemeral VM %s for platform: %s (base %s)\n",
+ __func__, vm->name, vm->plat->name, vm->plat->base_image);
- return spawn_virsh_command(virt, exec_array);
+ if (!conn)
+ return 1;
+
+ /* 1. Ensure the /dev/shm storage pool exists */
+ pool = virStoragePoolLookupByName(conn, "sai_shm");
+ if (!pool) {
+ pool = virStoragePoolCreateXML(conn, shm_pool_xml, 0);
+ if (!pool) {
+ lwsl_err("Failed to create transient shm storage pool\n");
+ return 1;
+ }
+ } else {
+ /* If it exists but is inactive, start it */
+ int active = virStoragePoolIsActive(pool);
+ if (active == 0)
+ virStoragePoolCreate(pool, 0);
+ }
+
+ long capacity_size = 20;
+ const char *capacity_unit = "G";
+ if (vm->plat->overlay_size[0]) {
+ char *p;
+ capacity_size = strtol(vm->plat->overlay_size, &p, 10);
+ if (p && *p)
+ capacity_unit = p;
+ }
+
+ /* 2. Create the overlay volume using libvirt API */
+ lws_snprintf(vol_xml, sizeof(vol_xml),
+ "<volume>"
+ " <name>%s.qcow2</name>"
+ " <capacity unit='%s'>%ld</capacity>"
+ " <target><format type='qcow2'/></target>"
+ " <backingStore>"
+ " <path>%s</path>"
+ " <format type='qcow2'/>"
+ " </backingStore>"
+ "</volume>",
+ vm->name,
+ capacity_unit, capacity_size,
+ vm->plat->base_image);
+
+ vol = virStorageVolCreateXML(pool, vol_xml, 0);
+ if (!vol) {
+ lwsl_err("Failed to create libvirt storage volume for overlay\n");
+ virStoragePoolFree(pool);
+ return 1;
+ }
+ virStorageVolFree(vol);
+ virStoragePoolFree(pool);
+
+ lws_snprintf(overlay_path, sizeof(overlay_path), "/dev/shm/%s.qcow2", vm->name);
+
+ /* 3. Get base domain XML and manipulate it */
+ dom = virDomainLookupByName(conn, vm->plat->name);
+ if (!dom) {
+ lwsl_err("Failed to find base domain %s\n", vm->plat->name);
+ return 1;
+ }
+
+ xml = virDomainGetXMLDesc(dom, 0);
+ virDomainFree(dom);
+
+ if (!xml) {
+ lwsl_err("Failed to get XML for base domain\n");
+ return 1;
+ }
+
+ /* Replace <name>base</name> with <name>vm->name</name> */
+ lws_snprintf(orig_name_tag, sizeof(orig_name_tag), "<name>%s</name>", vm->plat->name);
+ lws_snprintf(new_name_tag, sizeof(new_name_tag), "<name>%s</name>", vm->name);
+ xml2 = replace_string(xml, orig_name_tag, new_name_tag);
+ free(xml);
+
+ /* Replace <source file='base_image'/> with <source file='overlay_path'/> */
+ lws_snprintf(orig_source_tag, sizeof(orig_source_tag), "file='%s'", vm->plat->base_image);
+ lws_snprintf(new_source_tag, sizeof(new_source_tag), "file='%s'", overlay_path);
+ xml3 = replace_string(xml2, orig_source_tag, new_source_tag);
+ free(xml2);
+
+ if (!xml3) {
+ lwsl_err("Failed to manipulate XML\n");
+ return 1;
+ }
+
+ /* Inject qemu namespace into <domain> */
+ char *xml4 = replace_string(xml3, "<domain type=", "<domain xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' type=");
+ if (!xml4)
+ xml4 = strdup(xml3);
+ free(xml3);
+
+ /* Inject smbios mode='sysinfo' into <os> if missing */
+ char *xml4a = xml4;
+ if (!strstr(xml4, "<smbios mode='sysinfo'/>")) {
+ xml4a = replace_string(xml4, "</os>", " <smbios mode='sysinfo'/>\n </os>");
+ if (xml4a)
+ free(xml4);
+ else
+ xml4a = xml4;
+ }
+
+ /* Inject SMBIOS serial number and fw_cfg for builder identity */
+ char fw_cfg_tag[512];
+ lws_snprintf(fw_cfg_tag, sizeof(fw_cfg_tag),
+ " <sysinfo type='smbios'>\n"
+ " <system>\n"
+ " <entry name='serial'>sai_builder_id:%s</entry>\n"
+ " </system>\n"
+ " </sysinfo>\n"
+ " <qemu:commandline>\n"
+ " <qemu:arg value='-fw_cfg'/>\n"
+ " <qemu:arg value='name=opt/sai_builder_id,string=%s'/>\n"
+ " </qemu:commandline>\n"
+ "</domain>", vm->name, vm->name);
+
+ char *xml5 = replace_string(xml4a, "</domain>", fw_cfg_tag);
+ if (xml5) {
+ free(xml4a);
+ } else {
+ xml5 = xml4a;
+ }
+
+ if (!xml5) {
+ lwsl_err("Failed to manipulate XML\n");
+ return 1;
+ }
+
+ /* Remove UUID so libvirt generates a new one, avoiding conflicts with the base VM */
+ strip_xml_tags(xml5, "<uuid>", "</uuid>");
+ /* Remove MAC addresses so libvirt generates new ones, avoiding network conflicts */
+ strip_xml_tags(xml5, "<mac address=", "/>");
+
+ /* 4. Boot the transient domain */
+ dom = virDomainCreateXML(conn, xml5, 0);
+ free(xml5);
+
+ if (!dom) {
+ lwsl_err("Failed to create transient domain %s\n", vm->name);
+ return 1;
+ }
+
+ virDomainFree(dom);
+ lwsl_notice("Successfully spawned ephemeral VM %s\n", vm->name);
+
+ return 0;
}
static int
ops_libvirt_destroy(struct sai_virt *virt, struct saiv_vm *vm)
{
+ virDomainPtr dom;
+ virStoragePoolPtr pool;
+ virStorageVolPtr vol;
+ char vol_name[128];
+
lwsl_notice("%s: Destroying ephemeral VM: %s\n", __func__, vm->name);
+
+ if (!conn)
+ return 1;
+
+ dom = virDomainLookupByName(conn, vm->name);
+ if (dom) {
+ virDomainDestroy(dom);
+ virDomainFree(dom);
+ } else {
+ lwsl_warn("Domain %s not found during destroy\n", vm->name);
+ }
+
+ pool = virStoragePoolLookupByName(conn, "sai_shm");
+ if (pool) {
+ lws_snprintf(vol_name, sizeof(vol_name), "%s.qcow2", vm->name);
+ vol = virStorageVolLookupByName(pool, vol_name);
+ if (vol) {
+ virStorageVolDelete(vol, 0);
+ virStorageVolFree(vol);
+ } else {
+ lwsl_warn("Volume %s not found in pool sai_shm\n", vol_name);
+ }
+ virStoragePoolFree(pool);
+ }
+
return 0;
}
diff --git a/src/virt/v-private.h b/src/virt/v-private.h
index 273d685..7b57f7b 100644
--- a/src/virt/v-private.h
+++ b/src/virt/v-private.h
@@ -29,6 +29,9 @@ typedef struct sai_virt_ops {
typedef struct saiv_plat {
lws_dll2_t list;
char name[64];
+ char platform[128];
+ char base_image[128];
+ char overlay_size[32];
int wait_magnification;
int starting_vms;
@@ -84,5 +87,6 @@ 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);
+int saiv_config_global(struct sai_virt *virt, const char *filepath);
#endif
diff --git a/src/virt/v-sai.c b/src/virt/v-sai.c
index 5b0014f..84a6322 100644
--- a/src/virt/v-sai.c
+++ b/src/virt/v-sai.c
@@ -21,6 +21,9 @@ static int interrupted;
static const char * const default_ss_policy =
"{"
+ "\"release\": \"01234567\","
+ "\"product\": \"sai-virt\","
+ "\"schema-version\": 1,"
"\"retry\": ["
"{\"default\": {"
"\"backoff\": [1000, 2000, 3000, 5000, 10000],"
@@ -33,8 +36,11 @@ static const char * const default_ss_policy =
"\"s\": ["
"{\"sai_power_client\": {"
"\"endpoint\": \"${url}\","
+ "\"port\": 443,"
"\"protocol\": \"ws\","
- "\"ws_subprotocol\": \"com-warmcat-sai-builder\","
+ "\"tls\": true,"
+ "\"nailed_up\": true,"
+ "\"ws_subprotocol\": \"com-warmcat-sai\","
"\"http_url\": \"\","
"\"retry\": \"default\","
"\"metadata\": ["
@@ -75,7 +81,8 @@ int main(int argc, const char **argv)
memset(&info, 0, sizeof info);
info.port = 8000;
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
- LWS_SERVER_OPTION_VALIDATE_UTF8;
+ LWS_SERVER_OPTION_VALIDATE_UTF8 |
+ LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
info.pprotocols = pprotocols;
signal(SIGINT, sigint_handler);
@@ -97,30 +104,11 @@ int main(int argc, const char **argv)
return 1;
}
- /* We can spawn mac-m1, windows-10, etc. (Mocked for now) */
- const char *plats[] = {"windows-x86_64", "mac-m1"};
- for (size_t i = 0; i < LWS_ARRAY_SIZE(plats); i++) {
- saiv_plat_t *vp = malloc(sizeof(*vp));
- if (vp) {
- memset(vp, 0, sizeof(*vp));
- lws_strncpy(vp->name, plats[i], sizeof(vp->name));
- lws_dll2_add_tail(&vp->list, &virt.plat_owner);
- }
- }
+ /* Parse platforms from /etc/sai/virt/conf.d */
+ saiv_config(&virt, "/etc/sai/virt/conf.d");
- /* We create the server link manually for testing skeleton */
- saiv_server_t *srv = malloc(sizeof(*srv));
- if (srv) {
- memset(srv, 0, sizeof(*srv));
- srv->url = "warmcat.com"; /* example */
- if (lws_ss_create(virt.context, 0, &ssi_saiv_server_link_t,
- srv, &srv->ss, NULL, NULL)) {
- lwsl_err("%s: failed to create ss\n", __func__);
- free(srv);
- } else {
- lws_dll2_add_tail(&srv->list, &virt.sai_server_owner);
- }
- }
+ /* Parse global configuration from /etc/sai/virt/conf */
+ saiv_config_global(&virt, "/etc/sai/virt/conf");
while (!lws_service(virt.context, 0) && !interrupted)
;
@@ -129,11 +117,22 @@ int main(int argc, const char **argv)
saiv_server_t *s = lws_container_of(d, saiv_server_t, list);
lws_ss_destroy(&s->ss);
lws_dll2_remove(d);
+ free((void *)s->url);
free(s);
} lws_end_foreach_dll_safe(d, d1);
lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, virt.plat_owner.head) {
saiv_plat_t *p = lws_container_of(d, saiv_plat_t, list);
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, v, v1, p->vm_owner.head) {
+ saiv_vm_t *vm = lws_container_of(v, saiv_vm_t, list);
+ if (virt.ops)
+ virt.ops->destroy(&virt, vm);
+ lws_dll2_remove(v);
+ lws_sul_cancel(&vm->sul_timeout);
+ free(vm);
+ } lws_end_foreach_dll_safe(v, v1);
+
lws_dll2_remove(d);
free(p);
} lws_end_foreach_dll_safe(d, d1);
diff --git a/src/virt/v-ws-server.c b/src/virt/v-ws-server.c
index a9f1bdc..347810e 100644
--- a/src/virt/v-ws-server.c
+++ b/src/virt/v-ws-server.c
@@ -18,12 +18,16 @@
* When we connect, we masquerade as a builder/pcon and send our platforms
*/
static lws_ss_state_return_t
-saiv_server_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf,
- size_t *len, int *flags)
+saiv_server_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,
+ int *flags)
{
saiv_server_link_t *g = (saiv_server_link_t *)userobj;
+ lws_ss_state_return_t r;
- return sai_ss_tx_from_buflist_helper(g->ss, &g->bl_tx, buf, len, flags);
+ r = sai_ss_tx_from_buflist_helper(g->ss, &g->bl_tx, buf, len, flags);
+ if (r == LWSSSSRET_OK)
+ lwsl_notice("%s: Transmitted %zu bytes (flags=%d)\n", __func__, *len, *flags);
+ return r;
}
static void
@@ -87,46 +91,51 @@ saiv_server_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
sai_platform_pending_tasks_t *pt = (sai_platform_pending_tasks_t *)a.dest;
lwsl_notice("%s: Pending tasks for pcons: %s\n", __func__, pt->pcons);
- int total_wheel_weight = 0;
+ if (pt && pt->tasks.head) {
+ while (virt.running_vms < virt.max_vms) {
+ int total_wheel_weight = 0;
- /* Step 1: Calculate true demand and populate the wheel */
- 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);
+ /* Step 1: Count true demand */
+ 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);
- saiv_plat_t *found_vp = NULL;
- lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
- saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
- if (!strcmp(vp->name, t->plat)) {
- found_vp = vp;
- break;
- }
- } lws_end_foreach_dll(d);
+ saiv_plat_t *found_vp = NULL;
+ lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
+ saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
+ const char *pname = vp->platform[0] ? vp->platform : vp->name;
+ if (!strcmp(pname, t->plat)) {
+ found_vp = vp;
+ break;
+ }
+ } lws_end_foreach_dll(d);
- if (found_vp) {
- int true_demand = (int)t->pending - found_vp->starting_vms;
- if (true_demand > 0) {
- /* Apply wait magnification factor */
- total_wheel_weight += true_demand + found_vp->wait_magnification;
- }
- }
- } lws_end_foreach_dll(p);
+ if (found_vp) {
+ int true_demand = (int)t->pending - found_vp->starting_vms;
+ if (true_demand > 0) {
+ /* Apply wait magnification factor */
+ total_wheel_weight += true_demand + found_vp->wait_magnification;
+ }
+ }
+ } lws_end_foreach_dll(p);
- /* Step 2: Roll the dice if there is demand */
- if (total_wheel_weight > 0 && virt.running_vms < virt.max_vms) {
- /* LWS random */
- uint32_t r;
- lws_get_random(virt.context, &r, sizeof(r));
- int target = (int)(r % (uint32_t)total_wheel_weight);
+ if (total_wheel_weight == 0)
+ break;
+
+ /* Step 2: Roll the dice */
+ uint32_t r;
+ lws_get_random(virt.context, &r, sizeof(r));
+ int target = (int)(r % (uint32_t)total_wheel_weight);
- saiv_plat_t *winner = NULL;
+ saiv_plat_t *winner = NULL;
- lws_start_foreach_dll(struct lws_dll2 *, p, pt->tasks.head) {
+ 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);
saiv_plat_t *found_vp = NULL;
lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
- if (!strcmp(vp->name, t->plat)) {
+ const char *pname = vp->platform[0] ? vp->platform : vp->name;
+ if (!strcmp(pname, t->plat)) {
found_vp = vp;
break;
}
@@ -185,9 +194,13 @@ saiv_server_state(void *userobj, void *sh, lws_ss_constate_t state,
switch (state) {
case LWSSSCS_CREATING:
- lwsl_notice("%s: CREATING\n", __func__);
- /* We'd set metadata url here from the config, skipped for skeleton */
+ {
+ saiv_server_t *srv = (saiv_server_t *)g->opaque_data;
+ lwsl_notice("%s: CREATING (url: %s)\n", __func__, srv->url);
+ if (lws_ss_set_metadata(g->ss, "url", srv->url, strlen(srv->url)))
+ return LWSSSSRET_DESTROY_ME;
break;
+ }
case LWSSSCS_CONNECTED:
lwsl_notice("%s: Connected to sai-server\n", __func__);
@@ -199,18 +212,21 @@ saiv_server_state(void *userobj, void *sh, lws_ss_constate_t state,
sai_power_controller_t *pc = lwsac_use_zero(&ac, sizeof(*pc), 512);
if (pc) {
lws_strncpy(pc->name, virt.hostname, sizeof(pc->name));
+ lws_strncpy(pc->type, "virt", sizeof(pc->type));
+ pc->on = 1;
lws_dll2_add_tail(&pc->list, &pmb.power_controllers);
- }
- /* Register our platforms as the "builders" we manage */
- lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
- saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
- sai_power_managed_builder_t *bp = lwsac_use_zero(&ac, sizeof(*bp), 512);
- if (bp) {
- lws_strncpy(bp->name, vp->name, sizeof(bp->name));
- lws_dll2_add_tail(&bp->list, &pmb.builders);
- }
- } lws_end_foreach_dll(d);
+ /* Register our platforms as the "builders" we manage */
+ lws_start_foreach_dll(struct lws_dll2 *, d, virt.plat_owner.head) {
+ saiv_plat_t *vp = lws_container_of(d, saiv_plat_t, list);
+ sai_controlled_builder_t *c = lwsac_use_zero(&ac, sizeof(*c), 512);
+ if (c) {
+ const char *pname = vp->platform[0] ? vp->platform : vp->name;
+ lws_strncpy(c->name, pname, sizeof(c->name));
+ lws_dll2_add_tail(&c->list, &pc->controlled_builders_owner);
+ }
+ } lws_end_foreach_dll(d);
+ }
sai_ss_serialize_queue_helper(g->ss, &g->bl_tx,
lsm_schema_power_managed_builders,
diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt
index 8eb8e40..d1301a0 100644
--- a/src/web/CMakeLists.txt
+++ b/src/web/CMakeLists.txt
@@ -124,6 +124,7 @@ add_dependencies(${SUB} sai_git_hash)
../../assets/linux-centos-8.svg
../../assets/linux-fedora-32-riscv.svg
../../assets/linux-fedora-32.svg
+ ../../assets/linux-fedora44.svg
../../assets/linux-gentoo.svg
../../assets/linux-ubuntu-1804.svg
../../assets/linux-ubuntu-2004.svg
diff --git a/src/web/w-comms.c b/src/web/w-comms.c
index 9da21d6..e012760 100644
--- a/src/web/w-comms.c
+++ b/src/web/w-comms.c
@@ -238,8 +238,6 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
case LWS_CALLBACK_HTTP:
- lwsl_wsi_notice(wsi, "_HTTP");
-
if (!vhd) {
lwsl_err("%s: NULL vhd\n", __func__);
return -1;