feat: add config_repl Phase 1 — HEAD-file storage, SHA-256, trim, stale check, sanitise_cn

- config_repl.h/.c: atomic versioned file save, pointer file, trim, stale check
- test_config_repl.c: 17 Unity tests covering all Phase 1 storage functions
- typemap.h: add repl/heartbeat types + union members for xce_request_t/xce_reply_t
- clusterconfig.h: add config_generation field to cluster_config_t (capnp schema ref)
- meson.build: add config_repl.c and test_config_repl.c to build targets
- controller_test.c: register test_config_repl dispatch function
This commit is contained in:
Rongsong Shen 2026-05-13 15:45:28 +00:00
parent 1c4a103a66
commit 491c2550c2
7 changed files with 926 additions and 7 deletions

View file

@ -0,0 +1,368 @@
#define _GNU_SOURCE
#include "config_repl.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <openssl/sha.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <utime.h>
#include <unistd.h>
#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;
}

View file

@ -0,0 +1,92 @@
#pragma once
/* config_repl.h — config storage, SHA-256, and replication functions */
#define _GNU_SOURCE
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#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
* <workdir>/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 <conf_dir>/cluster.bin.<hex>.tmp, fsyncs, renames to versioned path.
* 2. Writes <conf_dir>/current.<tid>.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);

View file

@ -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',

View file

@ -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();
}

View file

@ -0,0 +1,391 @@
/* test_config_repl.c — Unity tests for config replication Phase 1 */
#define _GNU_SOURCE
#include <unity/unity.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <openssl/sha.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <utime.h>
#include <unistd.h>
#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);
}

View file

@ -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

View file

@ -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