feat(config-upgrade-snapshot): add configcompare tool, extend snapverify (task 8.4)
Build configcompare_paths(), an offline sibling to snapdiff/walcompare that recomputes each member's active-config sha256 fresh from its own on-disk workdir/name/conf bytes and reports identical vs. divergent (Req 4.2). Wire it as a cmd_configcompare CLI subcommand in test.c, matching its siblings, after review caught it shipped as an unreachable library function in round 1. Extend snapverify to match the current on-disk checkpoint format: bump SNAP_FORMAT_VERSION to 2, replace the stale raw queue-frame decode with the capnp length-prefixed walk, add host-frame and config-bytes-section structural walks, and decode (not just presence-check) the header's cfgref tag/sha. Without this fix snapverify rejected every checkpoint written by the current codebase.
This commit is contained in:
parent
c5d8ecbd7d
commit
1d09631df0
10 changed files with 997 additions and 47 deletions
217
src/controller/testsuite/test_configcompare.c
Normal file
217
src/controller/testsuite/test_configcompare.c
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "configcompare.h"
|
||||
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_configcompare_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(g_dir));
|
||||
}
|
||||
|
||||
static void cleanup_dir(void)
|
||||
{
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir);
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
/* Creates <root>/<name>/conf/ (a member's `workdir/name` directory, mirroring
|
||||
* conf_dir = "%s/%s", myconf->workdir, myconf->name in persist.c /
|
||||
* config_repl.c) and writes the resulting path into out_path. Does NOT touch
|
||||
* config_repl.c or persist.c -- these are hand-written fixture files so the
|
||||
* test exercises configcompare.c's OWN resolve-then-read logic, not a
|
||||
* different code path's. */
|
||||
static void make_member_dir(const char* root, const char* name,
|
||||
char out_path[512])
|
||||
{
|
||||
snprintf(out_path, 512, "%s/%s", root, name);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(out_path, 0700));
|
||||
char conf_dir[560];
|
||||
snprintf(conf_dir, sizeof(conf_dir), "%s/conf", out_path);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(conf_dir, 0700));
|
||||
}
|
||||
|
||||
static void write_conf_current(const char* member_path, const char* hexsuffix)
|
||||
{
|
||||
char ptr_path[600];
|
||||
snprintf(ptr_path, sizeof(ptr_path), "%s/conf/current", member_path);
|
||||
FILE* f = fopen(ptr_path, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fprintf(f, "cluster.bin.%s\n", hexsuffix);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void write_conf_blob(const char* member_path, const char* hexsuffix,
|
||||
const uint8_t* bytes, size_t len)
|
||||
{
|
||||
char blob_path[600];
|
||||
snprintf(blob_path, sizeof(blob_path), "%s/conf/cluster.bin.%s",
|
||||
member_path, hexsuffix);
|
||||
FILE* f = fopen(blob_path, "wb");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
if (len > 0)
|
||||
TEST_ASSERT_EQUAL_INT((int)len, (int)fwrite(bytes, 1, len, f));
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Full happy-path member: dir + conf/current + the versioned blob it
|
||||
* points at. */
|
||||
static void setup_member_ok(const char* root, const char* name,
|
||||
const char* hexsuffix, const uint8_t* bytes,
|
||||
size_t len, char out_path[512])
|
||||
{
|
||||
make_member_dir(root, name, out_path);
|
||||
write_conf_current(out_path, hexsuffix);
|
||||
write_conf_blob(out_path, hexsuffix, bytes, len);
|
||||
}
|
||||
|
||||
static void test_two_members_identical_config_reports_identical(void)
|
||||
{
|
||||
fresh_dir();
|
||||
const uint8_t data[] = "identical config bytes for both members";
|
||||
|
||||
char m0[512], m1[512];
|
||||
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
||||
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data, sizeof(data) - 1, m1);
|
||||
|
||||
const char* paths[2] = { m0, m1 };
|
||||
configcompare_report_t rep;
|
||||
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.all_present);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.all_identical);
|
||||
TEST_ASSERT_EQUAL_INT(-1, rep.first_divergent_index);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_two_members_differing_config_reports_divergent(void)
|
||||
{
|
||||
fresh_dir();
|
||||
const uint8_t data0[] = "config bytes version A";
|
||||
const uint8_t data1[] = "config bytes version B, quite different";
|
||||
|
||||
char m0[512], m1[512];
|
||||
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data0, sizeof(data0) - 1, m0);
|
||||
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data1, sizeof(data1) - 1, m1);
|
||||
|
||||
const char* paths[2] = { m0, m1 };
|
||||
configcompare_report_t rep;
|
||||
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.all_present);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.all_identical);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.first_divergent_index);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* A member with no conf/current pointer file at all must be reported as
|
||||
* unreadable (read_rc != 0), never silently folded into "identical" or
|
||||
* "divergent" against the two members that DO agree. */
|
||||
static void test_missing_conf_current_reported_unreadable_not_divergent(void)
|
||||
{
|
||||
fresh_dir();
|
||||
const uint8_t data[] = "shared config bytes across the healthy members";
|
||||
|
||||
char m0[512], m1[512], m2[512];
|
||||
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
||||
/* m1: conf/ dir exists but conf/current was never written. */
|
||||
make_member_dir(g_dir, "m1", m1);
|
||||
setup_member_ok(g_dir, "m2", "cccccccccccc", data, sizeof(data) - 1, m2);
|
||||
|
||||
const char* paths[3] = { m0, m1, m2 };
|
||||
configcompare_report_t rep;
|
||||
int rc = configcompare_paths(paths, 3, stdout, &rep);
|
||||
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.read_rc[0]);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rep.read_rc[1]);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.all_present);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* A member whose conf/current points at a versioned file that does not
|
||||
* exist on disk gets the same "unreadable" treatment as a missing pointer
|
||||
* file -- neither is evidence of matching or mismatching configuration. */
|
||||
static void test_current_points_to_missing_versioned_file_reported_unreadable(void)
|
||||
{
|
||||
fresh_dir();
|
||||
const uint8_t data[] = "shared config bytes, dangling pointer case";
|
||||
|
||||
char m0[512], m1[512];
|
||||
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
||||
/* m1: conf/current points at cluster.bin.dddddddddddd, but that
|
||||
* versioned file was never written. */
|
||||
make_member_dir(g_dir, "m1", m1);
|
||||
write_conf_current(m1, "dddddddddddd");
|
||||
|
||||
const char* paths[2] = { m0, m1 };
|
||||
configcompare_report_t rep;
|
||||
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
||||
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.read_rc[0]);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rep.read_rc[1]);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* Basic determinism sanity check: comparing the same two byte-identical
|
||||
* files twice in a row yields the same hash both times. */
|
||||
static void test_repeated_comparison_of_identical_files_is_deterministic(void)
|
||||
{
|
||||
fresh_dir();
|
||||
const uint8_t data[] = "deterministic hash check bytes, fixed content";
|
||||
|
||||
char m0[512], m1[512];
|
||||
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
||||
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data, sizeof(data) - 1, m1);
|
||||
|
||||
const char* paths[2] = { m0, m1 };
|
||||
configcompare_report_t rep1, rep2;
|
||||
int rc1 = configcompare_paths(paths, 2, NULL, &rep1);
|
||||
int rc2 = configcompare_paths(paths, 2, NULL, &rep2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(0, memcmp(rep1.sha256[0], rep2.sha256[0], 32));
|
||||
TEST_ASSERT_EQUAL_INT(0, memcmp(rep1.sha256[1], rep2.sha256[1], 32));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* Fewer than two members means there is nothing to compare -- a negative
|
||||
* errno, not a degenerate "identical" verdict. */
|
||||
static void test_n_less_than_2_returns_negative_errno(void)
|
||||
{
|
||||
const char* one_path[1] = { "/tmp/xce_configcompare_irrelevant" };
|
||||
configcompare_report_t rep;
|
||||
|
||||
int rc0 = configcompare_paths(NULL, 0, NULL, &rep);
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc0);
|
||||
|
||||
int rc1 = configcompare_paths(one_path, 1, NULL, &rep);
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc1);
|
||||
}
|
||||
|
||||
void test_configcompare(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_two_members_identical_config_reports_identical);
|
||||
RUN_TEST(test_two_members_differing_config_reports_divergent);
|
||||
RUN_TEST(test_missing_conf_current_reported_unreadable_not_divergent);
|
||||
RUN_TEST(test_current_points_to_missing_versioned_file_reported_unreadable);
|
||||
RUN_TEST(test_repeated_comparison_of_identical_files_is_deterministic);
|
||||
RUN_TEST(test_n_less_than_2_returns_negative_errno);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue