E2 Phase 1: external-license-resources — xceesd daemon, extres role, EXTERNAL admission
Implements Phase 1 of .kiro/specs/external-license-resources/ (design.md): externally-reported license pools pushed by a dedicated daemon through the existing resval pipeline, with EXTERNAL-aware admission arithmetic. Wire & controller: - controller.capnp HostRole.extres (@5, appended); xce_join_req accepts it - extres.c: HostRole_extres join table (endpoint+name, no workermgr slot), leave cleanup; join.c/cluster.c dispatch - opupd.c do_update_resvals: extres branch runs the resource hooks and update_resval_list(c, NULL, ...) — host=="" entries route through update_val_res' SHARED branch into c->sharedinst; per-entry -EPERM for non-shared targets is logged, never fails the list - worker.c update_val_res: NULL worker only writes SHARED resources - cluster.c cluster_build_resources: rebuilt on resourceApplyDef (bug-1854) — EEXIST-tolerant so definitions added by an activation register on a RUNNING controller (bug-1973), and RES_APPLY_CHANGED updates definition fields (valtype/ordtype/flags/interval) in place so a corrected config heals a stale registration (bug-1974); seed_sharedinst_from_config skips EXTERNAL pools (daemon-reported free counts only; seeding would fabricate capacity while the daemon is down) Scheduler: - core_events.c RESVALS_DELTA: NULL/"" host marks a SHARED value and lands in core->sharedInst via dup_val (bug-1585 ownership rule), mirroring snapshot_load's host=="" precedent — previously WARN-skipped as unknown worker - resource.c unsatisfy_: EXTERNAL skips the inuse term (F >= uncomit + wanted; F is already net of our checkouts) xceesd (src/esd, new component): - script collectors polled per source, debounce on unchanged values, fail-open stale policy; pushes host=="" shared resvals over xcecli - reconnect gap fix (bug-1975): with a stable value the debounce pushed nothing and reconnects only fire inside a push — after a controller restart the daemon sat on a dead connection forever; the main loop now forces a full re-push when xcecli_is_connection_broken() (bug-1760 predicate) latches Config/mapping: - resources.yaml: extlic sample (location: [external]) - mapping.py: RESOURCE_FLAG_EXTERNAL (0x10) + location:["external"] mapping; xceadmin tests Tests (TDD, red->green per fix): test_extres_resvals.c (join+resval pipeline), test_config_resource_reconcile.c +2 (register past EEXIST / hot-apply changed definition), test_core_events.c +131 lines (shared DELTA landing), mapping tests. Suites: controller 1358/0, scheduler 100/0, esd 7/0, lib 1/0; check-no-fork covers src/esd. Live t1 E2E: config gen=15 push -> xceesd push extlic=4 lands in sharedinst -> rusage[extlic=3] job RUN; second job PENDs at free=1; release re-admits it; controller restart -> forced re-push restores the value and admission resumes. Full precommit job verification passed. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
ba42fdd82e
commit
2fcacac2cb
33 changed files with 2556 additions and 29 deletions
201
src/esd/testsuite/test_esd_core.c
Normal file
201
src/esd/testsuite/test_esd_core.c
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/* test_esd_core.c -- external-license-resources Phase 1, step 6 TDD: the
|
||||
* xceesd skeleton's pure core -- esd.yaml parsing and the script
|
||||
* collector's output parsing -- white-box #include of the sources under
|
||||
* test (same pattern as the worker's resource_sync_test /
|
||||
* controller_connector_test). The join/reconnect wiring lives in esd.c
|
||||
* against the live client library and is exercised by the t1 E2E, not
|
||||
* here; everything this file drives must be deterministic. */
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "esd.h"
|
||||
|
||||
/* White-box: reach the config schema/parse internals. */
|
||||
#include "../src/esd_config.c"
|
||||
/* White-box: the script collector's parse half (esd_parse_script_output);
|
||||
* the spawn half (esd_script_collect) is exercised by the t1 E2E with a
|
||||
* real fake-license script, not here. */
|
||||
#include "../src/esd_script.c"
|
||||
|
||||
static char g_tmpdir[256];
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/xce_esd_testXXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(g_tmpdir));
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
if (g_tmpdir[0] != '\0') {
|
||||
char path[300];
|
||||
|
||||
/* tests only write flat files inside g_tmpdir */
|
||||
snprintf(path, sizeof(path), "rm -rf %s", g_tmpdir);
|
||||
if (system(path) != 0) {
|
||||
/* best effort */
|
||||
}
|
||||
g_tmpdir[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void write_file(const char* dir, const char* name, const char* content)
|
||||
{
|
||||
char path[512];
|
||||
FILE* f;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s", dir, name);
|
||||
f = fopen(path, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ── esd_config_load ─────────────────────────────────────────────────── */
|
||||
|
||||
static void test_config_load_full_file(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
int rc;
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
write_file(g_tmpdir, "esd.yaml",
|
||||
"controller: 172.31.1.30:9990\n"
|
||||
"poll_interval: 30\n"
|
||||
"resources:\n"
|
||||
" - name: Verilog_XL\n"
|
||||
" collector: script\n"
|
||||
" command: /etc/xce/lic-report.sh\n"
|
||||
" interval: 15\n"
|
||||
" - name: cloud_gpu_quota\n"
|
||||
" collector: script\n"
|
||||
" command: /etc/xce/quota-report.sh\n");
|
||||
|
||||
rc = esd_config_load_path(g_tmpdir, "esd.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_STRING("172.31.1.30", cfg.controller_host);
|
||||
TEST_ASSERT_EQUAL_INT(9990, cfg.controller_port);
|
||||
TEST_ASSERT_EQUAL_INT(30, cfg.poll_interval);
|
||||
TEST_ASSERT_EQUAL_INT(2, cfg.nsrc);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(cfg.src);
|
||||
TEST_ASSERT_EQUAL_STRING("Verilog_XL", cfg.src[0].name);
|
||||
TEST_ASSERT_EQUAL_STRING("/etc/xce/lic-report.sh", cfg.src[0].command);
|
||||
TEST_ASSERT_EQUAL_INT(15, cfg.src[0].interval);
|
||||
TEST_ASSERT_EQUAL_STRING("cloud_gpu_quota", cfg.src[1].name);
|
||||
TEST_ASSERT_EQUAL_STRING("/etc/xce/quota-report.sh", cfg.src[1].command);
|
||||
TEST_ASSERT_EQUAL_INT(0, cfg.src[1].interval); /* no override */
|
||||
|
||||
esd_config_fini(&cfg);
|
||||
}
|
||||
|
||||
static void test_config_load_missing_file_is_enoent(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
int rc;
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
rc = esd_config_load_path(g_tmpdir, "no-such-file.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
||||
}
|
||||
|
||||
static void test_config_load_bad_shape_is_einval(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
int rc;
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
/* a source without a name is structurally invalid -- the controller
|
||||
* would refuse the push and the operator would have no idea which
|
||||
* entry died; fail loudly at load. */
|
||||
write_file(g_tmpdir, "esd.yaml",
|
||||
"controller: 172.31.1.30:9990\n"
|
||||
"resources:\n"
|
||||
" - collector: script\n"
|
||||
" command: /bin/true\n");
|
||||
|
||||
rc = esd_config_load_path(g_tmpdir, "esd.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
|
||||
/* a source without a command can never report -- same rule. */
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
write_file(g_tmpdir, "esd.yaml",
|
||||
"controller: 172.31.1.30:9990\n"
|
||||
"resources:\n"
|
||||
" - name: lic\n"
|
||||
" collector: script\n");
|
||||
|
||||
rc = esd_config_load_path(g_tmpdir, "esd.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
/* ── script collector output parsing (parse contract, not the spawn) ── */
|
||||
|
||||
static void test_parse_output_bare_number(void)
|
||||
{
|
||||
double v = esd_parse_script_output("4\n");
|
||||
|
||||
TEST_ASSERT_FALSE(isnan(v));
|
||||
TEST_ASSERT_EQUAL_FLOAT((float)4.0, (float)v);
|
||||
}
|
||||
|
||||
static void test_parse_output_name_value(void)
|
||||
{
|
||||
/* elim-style "<name> <value>" -- name may mismatch; the source it is
|
||||
* configured for is authoritative, the name is decorative. */
|
||||
double v = esd_parse_script_output("Verilog_XL 7\n");
|
||||
|
||||
TEST_ASSERT_FALSE(isnan(v));
|
||||
TEST_ASSERT_EQUAL_FLOAT((float)7.0, (float)v);
|
||||
}
|
||||
|
||||
static void test_parse_output_whitespace_and_negative(void)
|
||||
{
|
||||
double v = esd_parse_script_output(" \t 3.5 \n");
|
||||
|
||||
TEST_ASSERT_FALSE(isnan(v));
|
||||
TEST_ASSERT_EQUAL_FLOAT((float)3.5, (float)v);
|
||||
|
||||
v = esd_parse_script_output("lic -1\n");
|
||||
TEST_ASSERT_FALSE(isnan(v));
|
||||
TEST_ASSERT_EQUAL_FLOAT((float)-1.0, (float)v);
|
||||
}
|
||||
|
||||
static void test_parse_output_garbage_is_nan(void)
|
||||
{
|
||||
double v;
|
||||
|
||||
v = esd_parse_script_output("");
|
||||
TEST_ASSERT_TRUE(isnan(v));
|
||||
v = esd_parse_script_output("\n");
|
||||
TEST_ASSERT_TRUE(isnan(v));
|
||||
v = esd_parse_script_output("no digits here\n");
|
||||
TEST_ASSERT_TRUE(isnan(v));
|
||||
v = esd_parse_script_output(NULL);
|
||||
TEST_ASSERT_TRUE(isnan(v));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_config_load_full_file);
|
||||
RUN_TEST(test_config_load_missing_file_is_enoent);
|
||||
RUN_TEST(test_config_load_bad_shape_is_einval);
|
||||
RUN_TEST(test_parse_output_bare_number);
|
||||
RUN_TEST(test_parse_output_name_value);
|
||||
RUN_TEST(test_parse_output_whitespace_and_negative);
|
||||
RUN_TEST(test_parse_output_garbage_is_nan);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue