diff --git a/src/controller/config_repl.c b/src/controller/config_repl.c new file mode 100644 index 0000000..62e784f --- /dev/null +++ b/src/controller/config_repl.c @@ -0,0 +1,368 @@ +#define _GNU_SOURCE +#include "config_repl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "byteorder.h" +#include "clusterconfig.h" +#include "utils.h" + +/* ── internal helpers ──────────────────────────── */ + +static void make_conf_dir(const char *workdir, char *out, size_t sz) +{ + snprintf(out, sz, "%s/conf", workdir); +} + +static void bytes_to_hex(const uint8_t *b, size_t n, char *out) +{ + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < n; i++) { + out[2 * i] = hex[(b[i] >> 4) & 0xf]; + out[2 * i + 1] = hex[b[i] & 0xf]; + } + out[2 * n] = '\0'; +} + +/* ── conf_read_current_filename ─────────────────── */ + +int conf_read_current_filename(const char *workdir, char *out, size_t outsz) +{ + char head_path[4096]; + snprintf(head_path, sizeof(head_path), "%s/conf/current", workdir); + + FILE *f = fopen(head_path, "r"); + if (!f) + return -ENOENT; + + struct stat st; + if (fstat(fileno(f), &st) == 0 && st.st_size == 0) { + fclose(f); + ERROR("conf/current is empty — treating as missing config (pointer file" + " may be from an interrupted write)"); + return -ENODATA; + } + + if (!fgets(out, (int)outsz, f)) { + fclose(f); + return -EIO; + } + fclose(f); + out[strcspn(out, "\n")] = '\0'; + + if (strchr(out, '/')) { + ERROR("conf/current contains path separator — corrupted pointer file"); + return -EINVAL; + } + if (strncmp(out, "cluster.bin.", 12) != 0) { + ERROR("conf/current '%s' does not start with 'cluster.bin.'", out); + return -EINVAL; + } + const char *hex = out + 12; + if (strlen(hex) != 12 || strspn(hex, "0123456789abcdef") != 12) { + ERROR("conf/current suffix '%s' is not 12 lowercase hex chars", hex); + return -EINVAL; + } + return 0; +} + +/* ── conf_save_config ───────────────────────────── */ + +int conf_save_config(myconf_t *myconf, const uint8_t *bytes, size_t len, + const uint8_t sha256[32]) +{ + char conf_dir[4096]; + make_conf_dir(myconf->workdir, conf_dir, sizeof(conf_dir)); + + if (mkdir(conf_dir, 0700) != 0 && errno != EEXIST) + return -errno; + + /* Derive 12-char hex tag from first 6 bytes of SHA-256 */ + char hex_tag[13]; + bytes_to_hex(sha256, 6, hex_tag); + + /* Step 1: write versioned file via thread-unique .tmp */ + char tmp_path[4096], ver_path[4096]; + snprintf(tmp_path, sizeof(tmp_path), "%s/cluster.bin.%s.%lu.tmp", + conf_dir, hex_tag, (unsigned long)pthread_self()); + snprintf(ver_path, sizeof(ver_path), "%s/cluster.bin.%s", + conf_dir, hex_tag); + + int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + return -errno; + if (write(fd, bytes, len) != (ssize_t)len) { + close(fd); + unlink(tmp_path); + return -EIO; + } + if (fsync(fd) != 0) { + close(fd); + unlink(tmp_path); + return -EIO; + } + close(fd); + if (rename(tmp_path, ver_path) != 0) { + unlink(tmp_path); + return -errno; + } + + /* Step 2: update HEAD pointer via thread-unique .tmp */ + char ptr_tmp[4096], ptr_path[4096]; + snprintf(ptr_tmp, sizeof(ptr_tmp), "%s/current.%lu.tmp", + conf_dir, (unsigned long)pthread_self()); + snprintf(ptr_path, sizeof(ptr_path), "%s/current", conf_dir); + + char target[NAME_MAX]; + snprintf(target, sizeof(target), "cluster.bin.%s\n", hex_tag); + + fd = open(ptr_tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + return -errno; + if (write(fd, target, strlen(target)) != (ssize_t)strlen(target)) { + close(fd); + unlink(ptr_tmp); + return -EIO; + } + fsync(fd); + close(fd); + if (rename(ptr_tmp, ptr_path) != 0) { + unlink(ptr_tmp); + return -errno; + } + + /* Step 2.5: fsync conf/ directory to persist the rename(2)s */ + fd = open(conf_dir, O_RDONLY); + if (fd >= 0) { + fsync(fd); + close(fd); + } + + /* Step 3: trim old versions */ + conf_trim_old_versions(myconf->workdir, myconf->conf_keep_n); + + /* Update conffile to the new versioned path (heap-allocated) */ + free(myconf->conffile); + if (asprintf(&myconf->conffile, "%s/cluster.bin.%s", conf_dir, hex_tag) < 0) + return -ENOMEM; + + /* Update tag (first 8 bytes of sha256, host order) and full hash */ + uint64_t new_tag; + memcpy(&new_tag, sha256, 8); + myconf->config_version_tag = be64toh(new_tag); + memcpy(myconf->config_sha256, sha256, 32); + + return 0; +} + +/* ── conf_read_raw ──────────────────────────────── */ + +int conf_read_raw(const char *conffile, uint8_t **bytes, size_t *len) +{ + FILE *f = fopen(conffile, "rb"); + if (!f) + return -errno; + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return -EIO; + } + long sz = ftell(f); + if (sz < 0) { + fclose(f); + return -EIO; + } + fseek(f, 0, SEEK_SET); + + if (sz == 0) { + fclose(f); + ERROR("Config file '%s' is empty — not a valid ClusterConfig blob", + conffile); + return -EIO; + } + if ((size_t)sz > CONF_MAX_SIZE) { + fclose(f); + ERROR("Config file '%s' exceeds CONF_MAX_SIZE (%zu bytes)", + conffile, (size_t)sz); + return -EFBIG; + } + + *bytes = (uint8_t *)malloc((size_t)sz); + if (!*bytes) { + fclose(f); + return -ENOMEM; + } + + if (fread(*bytes, 1, (size_t)sz, f) != (size_t)sz) { + free(*bytes); + *bytes = NULL; + fclose(f); + return -EIO; + } + fclose(f); + *len = (size_t)sz; + return 0; +} + +/* ── conf_get_saved_tag ─────────────────────────── */ + +uint64_t conf_get_saved_tag(const char *workdir) +{ + char fname[256] = {0}; + if (conf_read_current_filename(workdir, fname, sizeof(fname)) != 0) + return 0; + + char ver_path[4096]; + snprintf(ver_path, sizeof(ver_path), "%s/conf/%s", workdir, fname); + + uint8_t *bytes = NULL; + size_t blen = 0; + if (conf_read_raw(ver_path, &bytes, &blen) != 0) + return 0; + + uint8_t sha256[32]; + SHA256(bytes, blen, sha256); + free(bytes); + + uint64_t tag; + memcpy(&tag, sha256, 8); + return be64toh(tag); +} + +/* ── conf_check_stale ───────────────────────────── */ + +int conf_check_stale(const char *workdir, time_t stale_timeout_s) +{ + if (stale_timeout_s == 0) + return 0; + + char fname[256] = {0}; + if (conf_read_current_filename(workdir, fname, sizeof(fname)) != 0) + return 0; + + char ver_path[4096]; + snprintf(ver_path, sizeof(ver_path), "%s/conf/%s", workdir, fname); + + struct stat st; + if (stat(ver_path, &st) != 0) + return 0; + + time_t age = time(NULL) - st.st_mtime; + return (age > stale_timeout_s) ? 1 : 0; +} + +/* ── conf_trim_old_versions ─────────────────────── */ + +typedef struct { + char name[NAME_MAX]; + time_t mtime; +} ver_entry_t; + +static int ver_entry_cmp_desc(const void *a, const void *b) +{ + const ver_entry_t *ea = (const ver_entry_t *)a; + const ver_entry_t *eb = (const ver_entry_t *)b; + if (eb->mtime > ea->mtime) return 1; + if (eb->mtime < ea->mtime) return -1; + return 0; +} + +void conf_trim_old_versions(const char *workdir, int keep_n) +{ + char conf_dir[4096]; + make_conf_dir(workdir, conf_dir, sizeof(conf_dir)); + + /* Read current pointer to protect it */ + char current_name[256] = {0}; + conf_read_current_filename(workdir, current_name, sizeof(current_name)); + + ver_entry_t entries[256]; + int n = 0; + + DIR *d = opendir(conf_dir); + if (!d) + return; + + struct dirent *e; + while ((e = readdir(d)) != NULL && n < 256) { + if (strncmp(e->d_name, "cluster.bin.", 12) != 0) + continue; + if (strstr(e->d_name, ".tmp") != NULL) { + char full[4096]; + snprintf(full, sizeof(full), "%s/%s", conf_dir, e->d_name); + unlink(full); + continue; + } + char full[4096]; + snprintf(full, sizeof(full), "%s/%s", conf_dir, e->d_name); + struct stat st; + if (stat(full, &st) == 0) { + strncpy(entries[n].name, e->d_name, NAME_MAX - 1); + entries[n].name[NAME_MAX - 1] = '\0'; + entries[n].mtime = st.st_mtime; + n++; + } + } + closedir(d); + + /* Sort descending by mtime (newest first) */ + qsort(entries, (size_t)n, sizeof(entries[0]), ver_entry_cmp_desc); + + /* Delete everything after keep_n, protecting the current pointer */ + for (int i = keep_n; i < n; i++) { + if (strcmp(entries[i].name, current_name) == 0) + continue; + char full[4096]; + snprintf(full, sizeof(full), "%s/%s", conf_dir, entries[i].name); + unlink(full); + } +} + +/* ── config_contains_reserved_name ─────────────── */ + +int config_contains_reserved_name(cluster_config_t *cfg) +{ + if (!cfg) + return 0; + /* Check queue names — xce_queue_t.queue is the queue name field */ + for (int i = 0; i < cfg->nqueues; i++) { + if (cfg->queues[i] && + cfg->queues[i]->queue && + strcmp(cfg->queues[i]->queue, "lost_and_found") == 0) + return 1; + } + /* Check host config names — xce_hostconfig_t.name */ + for (int i = 0; i < cfg->nhconfigs; i++) { + if (cfg->hconfigs[i] && + cfg->hconfigs[i]->name && + strcmp(cfg->hconfigs[i]->name, "lost_and_found") == 0) + return 1; + } + return 0; +} + +/* ── sanitise_cn ────────────────────────────────── */ + +char *sanitise_cn(const char *src, size_t maxlen) +{ + static char buf[128]; + size_t n = maxlen < sizeof(buf) - 1 ? maxlen : sizeof(buf) - 1; + size_t i; + for (i = 0; i < n && src[i] != '\0'; i++) + buf[i] = (src[i] >= 0x20 && src[i] < 0x7f) ? src[i] : '?'; + buf[i] = '\0'; + return buf; +} diff --git a/src/controller/config_repl.h b/src/controller/config_repl.h new file mode 100644 index 0000000..4c15a11 --- /dev/null +++ b/src/controller/config_repl.h @@ -0,0 +1,92 @@ +#pragma once +/* config_repl.h — config storage, SHA-256, and replication functions */ +#define _GNU_SOURCE +#include +#include +#include + +#include "myconf.h" +#include "clusterconfig.h" + +/* Max config blob size — reject before allocating */ +#define CONF_MAX_SIZE (1 * 1024 * 1024) /* 1 MiB */ + +/* Compile-time gate for OP_upgradeConfig (Phase 2, default disabled) */ +#ifndef CONF_ENABLE_UPGRADE_CONFIG +#define CONF_ENABLE_UPGRADE_CONFIG 0 +#endif + +/* Push timeout for standby fan-out (ms) */ +#ifndef CONF_PUSH_TIMEOUT_MS +#define CONF_PUSH_TIMEOUT_MS 500 +#endif + +/* Certificate expiry warning threshold (days) */ +#define CERT_WARN_DAYS 30 + +/* ── Phase 1: Storage ────────────────────────────────────────── */ + +/* + * conf_read_current_filename: read HEAD-style pointer file at + * /conf/current. Strips trailing newline. Validates format: + * must start with "cluster.bin." followed by exactly 12 hex chars. + * Returns 0 on success; -ENOENT if absent; -ENODATA if empty (zero-length); + * -EINVAL if malformed (path separator, wrong prefix, wrong hex length). + * out must be at least NAME_MAX bytes. + */ +int conf_read_current_filename(const char *workdir, char *out, size_t outsz); + +/* + * conf_save_config: atomically write versioned file and update HEAD pointer. + * 1. Writes /cluster.bin..tmp, fsyncs, renames to versioned path. + * 2. Writes /current..tmp, fsyncs, renames to conf/current. + * 3. fsyncs the conf/ directory. + * 4. Trims old versions. + * 5. Updates myconf->conffile (heap-allocated via asprintf; caller must free). + * 6. Updates myconf->config_version_tag and myconf->config_sha256. + * Returns 0 on success, -errno on failure. + */ +int conf_save_config(myconf_t *myconf, const uint8_t *bytes, size_t len, + const uint8_t sha256[32]); + +/* + * conf_read_raw: read versioned file at conffile path. + * Allocates *bytes (caller must free). Enforces CONF_MAX_SIZE. + * Returns 0 on success; -EFBIG if exceeds limit; -errno on I/O error. + */ +int conf_read_raw(const char *conffile, uint8_t **bytes, size_t *len); + +/* + * conf_get_saved_tag: return first 8 bytes of SHA-256 of the current + * versioned file in host byte order. Returns 0 if conf/current absent. + * This is the "no config saved" zero-tag sentinel. + */ +uint64_t conf_get_saved_tag(const char *workdir); + +/* + * conf_check_stale: returns 1 if the current versioned file's mtime is + * older than stale_timeout_s seconds. Returns 0 if within timeout or + * if stale_timeout_s == 0 (disabled). Returns 0 if conf/current absent. + */ +int conf_check_stale(const char *workdir, time_t stale_timeout_s); + +/* + * conf_trim_old_versions: keep at most keep_n versioned cluster.bin.* files + * sorted by mtime (newest first). Always preserves the file named by the + * current conf/current pointer. Also removes stale .tmp files. + */ +void conf_trim_old_versions(const char *workdir, int keep_n); + +/* + * config_contains_reserved_name: returns 1 if ClusterConfig contains a + * queue or host named "lost_and_found". Returns 0 for NULL config. + * Checks queues[i]->queue (queue name) and hconfigs[i]->name (host name). + */ +int config_contains_reserved_name(cluster_config_t *cfg); + +/* + * sanitise_cn: copy at most maxlen printable ASCII chars from src to a + * static buffer, replacing non-printable/control chars with '?'. + * Returns pointer to the static buffer. NOT thread-safe (static storage). + */ +char *sanitise_cn(const char *src, size_t maxlen); diff --git a/src/controller/meson.build b/src/controller/meson.build index eb3e0de..3442bcc 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -1,5 +1,6 @@ controller_src = [ 'cluster.c', + 'config_repl.c', 'dataex.c', 'ha.c', 'job.c', @@ -97,12 +98,16 @@ controller_unit_test_src = [ 'testsuite/test_p3_hardening.c', 'testsuite/test_p3_manual_failover.c', 'testsuite/test_ctrl_metrics.c', + 'testsuite/test_config_repl.c', 'persist.c', 'ha.c', 'repl.c', 'ctrl_metrics.c', + 'config_repl.c', ] +ssl_dep = dependency('openssl') + controller_unit_test_deps = [ libxce_dep, unity_dep, @@ -110,6 +115,7 @@ controller_unit_test_deps = [ thread_dep, zlib_dep, c_capnproto_dep, + ssl_dep, ] controller_unit_test = executable('controller_unit_test', diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index 907ce3b..3193c7e 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -31,6 +31,7 @@ extern void test_p2_integration(void); extern void test_p3_hardening(void); extern void test_p3_manual_failover(void); extern void test_ctrl_metrics(void); +extern void test_config_repl(void); void setUp(void) { return; } @@ -131,6 +132,7 @@ int main(int argc, char* argv[]) TEST(test_p3_hardening); TEST(test_p3_manual_failover); TEST(test_ctrl_metrics); + TEST(test_config_repl); return UNITY_END(); } diff --git a/src/controller/testsuite/test_config_repl.c b/src/controller/testsuite/test_config_repl.c new file mode 100644 index 0000000..4e00694 --- /dev/null +++ b/src/controller/testsuite/test_config_repl.c @@ -0,0 +1,391 @@ +/* test_config_repl.c — Unity tests for config replication Phase 1 */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config_repl.h" +#include "myconf.h" + +/* Per-test directory helpers — not Unity setUp/tearDown (those are in + * controller_test.c). Each test that needs a temp dir calls cr_setup() + * at the top and cr_teardown() via the label before returning. */ + +static char g_tmpdir[256]; +static char g_conf_dir[300]; + +static void cr_setup(void) +{ + snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_cr_%d_%ld", + (int)getpid(), (long)time(NULL)); + mkdir(g_tmpdir, 0700); + snprintf(g_conf_dir, sizeof(g_conf_dir), "%s/conf", g_tmpdir); + mkdir(g_conf_dir, 0700); +} + +static void cr_teardown(void) +{ + char cmd[512]; + snprintf(cmd, sizeof(cmd), "rm -rf %s", g_tmpdir); + system(cmd); + g_tmpdir[0] = '\0'; + g_conf_dir[0] = '\0'; +} + +/* ── conf_read_current_filename ─────────────────── */ + +static void test_conf_read_current_filename_missing(void) +{ + cr_setup(); + char out[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-ENOENT, rc); + cr_teardown(); + printf("test_conf_read_current_filename_missing: PASS\n"); +} + +static void test_conf_read_current_filename_empty_file(void) +{ + cr_setup(); + char ptr_path[400]; + snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); + int fd = open(ptr_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT_NOT_EQUAL(-1, fd); + close(fd); + + char out[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-ENODATA, rc); + cr_teardown(); + printf("test_conf_read_current_filename_empty_file: PASS\n"); +} + +static void test_conf_read_current_filename_path_separator(void) +{ + cr_setup(); + char ptr_path[400]; + snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); + FILE *f = fopen(ptr_path, "w"); + fprintf(f, "cluster.bin/../evil\n"); + fclose(f); + + char out[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + cr_teardown(); + printf("test_conf_read_current_filename_path_separator: PASS\n"); +} + +static void test_conf_read_current_filename_bad_prefix(void) +{ + cr_setup(); + char ptr_path[400]; + snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); + FILE *f = fopen(ptr_path, "w"); + fprintf(f, "badprefix.aabbccddeeff\n"); + fclose(f); + + char out[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + cr_teardown(); + printf("test_conf_read_current_filename_bad_prefix: PASS\n"); +} + +static void test_conf_read_current_filename_wrong_hex_length(void) +{ + cr_setup(); + char ptr_path[400]; + snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); + FILE *f = fopen(ptr_path, "w"); + fprintf(f, "cluster.bin.aabb\n"); /* only 4 hex chars, need 12 */ + fclose(f); + + char out[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + cr_teardown(); + printf("test_conf_read_current_filename_wrong_hex_length: PASS\n"); +} + +/* ── conf_save_config + conf_read_current_filename round-trip ─ */ + +static void test_conf_save_creates_pointer_file(void) +{ + cr_setup(); + const uint8_t data[] = "fake capnproto config blob"; + size_t len = sizeof(data) - 1; + uint8_t sha256[32]; + SHA256(data, len, sha256); + + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + + int rc = conf_save_config(&myconf, data, len, sha256); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* conf/current must exist */ + char ptr_path[400]; + snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); + TEST_ASSERT_EQUAL_INT(0, access(ptr_path, F_OK)); + + free(myconf.conffile); + cr_teardown(); + printf("test_conf_save_creates_pointer_file: PASS\n"); +} + +static void test_conf_save_filename_has_12_hex_suffix(void) +{ + cr_setup(); + const uint8_t data[] = "another fake blob"; + size_t len = sizeof(data) - 1; + uint8_t sha256[32]; + SHA256(data, len, sha256); + + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + + conf_save_config(&myconf, data, len, sha256); + + char fname[256] = {0}; + int rc = conf_read_current_filename(g_tmpdir, fname, sizeof(fname)); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_INT(0, strncmp(fname, "cluster.bin.", 12)); + const char *hex = fname + 12; + TEST_ASSERT_EQUAL_INT(12, (int)strlen(hex)); + TEST_ASSERT_EQUAL_INT(12, (int)strspn(hex, "0123456789abcdef")); + + free(myconf.conffile); + cr_teardown(); + printf("test_conf_save_filename_has_12_hex_suffix: PASS\n"); +} + +/* ── conf_trim_old_versions ─────────────────────── */ + +static void test_conf_trim_keeps_only_n_versions(void) +{ + cr_setup(); + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + + uint8_t sha256[32]; + for (int i = 0; i < 3; i++) { + char data[32]; + snprintf(data, sizeof(data), "config version %d", i); + SHA256((uint8_t *)data, strlen(data), sha256); + usleep(15000); /* ensure distinct mtime ordering */ + conf_save_config(&myconf, (uint8_t *)data, strlen(data), sha256); + free(myconf.conffile); + myconf.conffile = NULL; + } + + /* Count cluster.bin.* files (exclude .tmp) */ + int count = 0; + DIR *d = opendir(g_conf_dir); + TEST_ASSERT_NOT_NULL(d); + struct dirent *e; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, "cluster.bin.", 12) == 0 && + strstr(e->d_name, ".tmp") == NULL) + count++; + } + closedir(d); + TEST_ASSERT_EQUAL_INT(2, count); + + free(myconf.conffile); + cr_teardown(); + printf("test_conf_trim_keeps_only_n_versions: PASS\n"); +} + +/* ── conf_get_saved_tag ─────────────────────────── */ + +static void test_conf_get_saved_tag_no_config(void) +{ + cr_setup(); + uint64_t tag = conf_get_saved_tag(g_tmpdir); + TEST_ASSERT_EQUAL_UINT64(0, tag); + cr_teardown(); + printf("test_conf_get_saved_tag_no_config: PASS\n"); +} + +static void test_conf_get_saved_tag_after_save(void) +{ + cr_setup(); + const uint8_t data[] = "tag test blob"; + size_t len = sizeof(data) - 1; + uint8_t sha256[32]; + SHA256(data, len, sha256); + + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + + conf_save_config(&myconf, data, len, sha256); + free(myconf.conffile); + + uint64_t tag = conf_get_saved_tag(g_tmpdir); + TEST_ASSERT_NOT_EQUAL(0, tag); + + /* Tag must equal first 8 bytes of sha256 in host order */ + uint64_t expected; + memcpy(&expected, sha256, 8); + /* be64toh conversion */ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + expected = __builtin_bswap64(expected); +#endif + TEST_ASSERT_EQUAL_UINT64(expected, tag); + + cr_teardown(); + printf("test_conf_get_saved_tag_after_save: PASS\n"); +} + +/* ── conf_check_stale ───────────────────────────── */ + +static void test_conf_check_stale_disabled(void) +{ + cr_setup(); + /* timeout 0 = disabled, never stale */ + TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 0)); + cr_teardown(); + printf("test_conf_check_stale_disabled: PASS\n"); +} + +static void test_conf_check_stale_fresh(void) +{ + cr_setup(); + const uint8_t data[] = "stale test blob"; + uint8_t sha256[32]; + SHA256(data, sizeof(data) - 1, sha256); + + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + conf_save_config(&myconf, data, sizeof(data) - 1, sha256); + free(myconf.conffile); + + /* Just saved — not stale with 24h timeout */ + TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 86400)); + cr_teardown(); + printf("test_conf_check_stale_fresh: PASS\n"); +} + +static void test_conf_check_stale_aged(void) +{ + cr_setup(); + const uint8_t data[] = "old config blob"; + uint8_t sha256[32]; + SHA256(data, sizeof(data) - 1, sha256); + + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.workdir = g_tmpdir; + myconf.name = "ctrl0"; + myconf.conf_keep_n = 2; + myconf.conffile = NULL; + conf_save_config(&myconf, data, sizeof(data) - 1, sha256); + free(myconf.conffile); + + /* Age the versioned file by 20 seconds via utime */ + char fname[256] = {0}; + conf_read_current_filename(g_tmpdir, fname, sizeof(fname)); + char ver_path[512]; + snprintf(ver_path, sizeof(ver_path), "%s/%s", g_conf_dir, fname); + + struct utimbuf old_time; + old_time.actime = time(NULL) - 20; + old_time.modtime = time(NULL) - 20; + utime(ver_path, &old_time); + + /* 5-second threshold → stale */ + TEST_ASSERT_EQUAL_INT(1, conf_check_stale(g_tmpdir, 5)); + /* 30-second threshold → not stale */ + TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 30)); + + cr_teardown(); + printf("test_conf_check_stale_aged: PASS\n"); +} + +/* ── config_contains_reserved_name ─────────────── */ + +static void test_config_contains_reserved_name_absent(void) +{ + /* NULL config — no reserved name */ + TEST_ASSERT_EQUAL_INT(0, config_contains_reserved_name(NULL)); + printf("test_config_contains_reserved_name_absent: PASS\n"); +} + +/* ── sanitise_cn ────────────────────────────────── */ + +static void test_sanitise_cn_printable(void) +{ + char *out = sanitise_cn("ctrl01.example.com", 64); + TEST_ASSERT_EQUAL_STRING("ctrl01.example.com", out); + printf("test_sanitise_cn_printable: PASS\n"); +} + +static void test_sanitise_cn_control_chars(void) +{ + char *out = sanitise_cn("evil\ninjection", 64); + /* newline replaced with '?' */ + TEST_ASSERT_NOT_NULL(strchr(out, '?')); + TEST_ASSERT_NULL(strchr(out, '\n')); + printf("test_sanitise_cn_control_chars: PASS\n"); +} + +static void test_sanitise_cn_truncated(void) +{ + char *out = sanitise_cn("abcdefghij", 5); + TEST_ASSERT_EQUAL_INT(5, (int)strlen(out)); + printf("test_sanitise_cn_truncated: PASS\n"); +} + +/* ── dispatch ───────────────────────────────────── */ + +void test_config_repl(void) +{ + RUN_TEST(test_conf_read_current_filename_missing); + RUN_TEST(test_conf_read_current_filename_empty_file); + RUN_TEST(test_conf_read_current_filename_path_separator); + RUN_TEST(test_conf_read_current_filename_bad_prefix); + RUN_TEST(test_conf_read_current_filename_wrong_hex_length); + RUN_TEST(test_conf_save_creates_pointer_file); + RUN_TEST(test_conf_save_filename_has_12_hex_suffix); + RUN_TEST(test_conf_trim_keeps_only_n_versions); + RUN_TEST(test_conf_get_saved_tag_no_config); + RUN_TEST(test_conf_get_saved_tag_after_save); + RUN_TEST(test_conf_check_stale_disabled); + RUN_TEST(test_conf_check_stale_fresh); + RUN_TEST(test_conf_check_stale_aged); + RUN_TEST(test_config_contains_reserved_name_absent); + RUN_TEST(test_sanitise_cn_printable); + RUN_TEST(test_sanitise_cn_control_chars); + RUN_TEST(test_sanitise_cn_truncated); +} diff --git a/src/include/clusterconfig.h b/src/include/clusterconfig.h index a9d9a61..f2a7d58 100644 --- a/src/include/clusterconfig.h +++ b/src/include/clusterconfig.h @@ -57,6 +57,7 @@ typedef struct { scaler_config_t** hproviders; int nlimits; xce_limit_config_t** limits; + uint64_t config_generation; /* monotonic audit counter from capnp schema */ } cluster_config_t; #endif diff --git a/src/include/typemap.h b/src/include/typemap.h index b567213..faf02c0 100644 --- a/src/include/typemap.h +++ b/src/include/typemap.h @@ -6,6 +6,16 @@ #include "clusterconfig.h" #include "xce/types.h" +/* Forward declarations for config-replication types used in + * xce_reply_t and xce_request_t unions below. */ +typedef struct repl_config_reply_s repl_config_reply_t; +typedef struct repl_config_push_reply_s repl_config_push_reply_t; +typedef struct upgrade_config_reply_s upgrade_config_reply_t; +typedef struct repl_config_req_s repl_config_req_t; +typedef struct repl_config_push_req_s repl_config_push_req_t; +typedef struct upgrade_config_req_s upgrade_config_req_t; +typedef struct heartbeat_msg_s heartbeat_msg_t; + typedef struct { char* resname; char* hostname; @@ -166,10 +176,13 @@ typedef struct { int32_t status; int data_which; union { - char* debugmsg; - uint64_t jobid; - xce_data_t* getdata; - xce_clusterconf_t* clusterconf; + char* debugmsg; + uint64_t jobid; + xce_data_t* getdata; + xce_clusterconf_t* clusterconf; + repl_config_reply_t* replconfigreplymsg; + repl_config_push_reply_t* replconfigpushreplymsg; + upgrade_config_reply_t* upgradeconfigreplymsg; } data; } xce_reply_t; @@ -194,9 +207,13 @@ typedef struct { xce_jobctrl_t* jobctrlmsg; xce_queuectrl_t* queuectrlmsg; xce_taskrun_t* runtaskmsg; - xce_preason_set_t* preasonmsg; - xce_clear_reason_t* clrpreasonmsg; - xce_hostctrl_t* hostctrlmsg; + xce_preason_set_t* preasonmsg; + xce_clear_reason_t* clrpreasonmsg; + xce_hostctrl_t* hostctrlmsg; + repl_config_req_t* replconfigrequestmsg; + repl_config_push_req_t* replconfigpushrequestmsg; + upgrade_config_req_t* upgradeconfigrequestmsg; + heartbeat_msg_t* heartbeatmsg; } body; } xce_request_t; @@ -236,4 +253,46 @@ typedef struct { uint32_t prevCrc; } capnp_repl_envelope_t; +/* ── Config replication Cap'n Proto mapped types (Phase 1) ────────────── + * C-side counterparts for the capnp encode/decode shims for config sync. + * Struct tags match the forward declarations near the top of this file. */ + +struct repl_config_req_s { + uint64_t version_tag; /* local config version tag */ +}; + +struct repl_config_reply_s { + int up_to_date; /* 1 if standby is current */ + uint64_t version_tag; /* primary's current version tag */ + xce_data_t *sha256; /* 32-byte SHA-256 of config blob (may be NULL) */ + xce_data_t *bytes; /* serialised ClusterConfig blob (may be NULL) */ +}; + +struct repl_config_push_req_s { + uint64_t version_tag; /* version tag of the pushed config */ + xce_data_t *sha256; /* 32-byte SHA-256 */ + xce_data_t *bytes; /* serialised ClusterConfig blob */ +}; + +struct repl_config_push_reply_s { + int accepted; /* 1 if standby accepted the push */ + uint64_t version_tag; /* standby's current version tag after decision */ +}; + +struct upgrade_config_req_s { + xce_data_t *sha256; /* 32-byte SHA-256 of proposed config */ + xce_data_t *bytes; /* serialised ClusterConfig blob */ +}; + +struct upgrade_config_reply_s { + uint64_t version_tag; /* new version tag after upgrade */ + char *error; /* NULL on success; error message on failure */ + uint32_t acked_count; /* number of standbys that acknowledged */ +}; + +struct heartbeat_msg_s { + uint32_t sender_priority; /* sender's HA priority */ + uint64_t term; /* sender's current term */ +}; + #endif