From 855f3e2389bdf901e658acaa78c8e0bc3a028fdf Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 4 Jun 2026 14:58:03 -0500 Subject: [PATCH] Add timer_policy support to classes: promise type The timer_policy attribute (absolute vs reset) was available in classes bodies (outcome classes on other promise types) but missing from classes: type promises. As a result, VerifyClassPromise hardcoded CONTEXT_STATE_POLICY_RESET and persistent classes defined via classes: promises always reset their timer with no way to opt into preserve (absolute) behavior. - Add timer_policy option to CF_CLASSBODY syntax (mod_common.c) - Add PersistentClassPolicy timer field to ContextConstraint (cf3.defs.h) - Parse timer_policy in GetContextConstraints (attributes.c) - Pass parsed policy instead of hardcoded RESET (verify_classes.c) - Add unit test for persistent class timer policy behavior - Add acceptance test for timer_policy on classes: promises Ticket: CFE-4681 Changelog: Title --- libpromises/attributes.c | 16 +++- libpromises/cf3.defs.h | 1 + libpromises/mod_common.c | 1 + libpromises/verify_classes.c | 2 +- .../01_basic/persistent_timer_policy.cf | 77 +++++++++++++++++++ .../01_basic/persistent_timer_policy.cf.sub | 13 ++++ tests/unit/eval_context_test.c | 46 +++++++++++ 7 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf create mode 100644 tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf.sub diff --git a/libpromises/attributes.c b/libpromises/attributes.c index b9fe8674ba..7e92f1e329 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -1132,6 +1132,18 @@ ContextConstraint GetContextConstraints(const EvalContext *ctx, const Promise *p a.expression = NULL; a.persistent = PromiseGetConstraintAsInt(ctx, "persistence", pp); + { + const char *tp = PromiseGetConstraintAsRval(pp, "timer_policy", RVAL_TYPE_SCALAR); + if (tp != NULL && strncmp(tp, "abs", 3) == 0) + { + a.timer = CONTEXT_STATE_POLICY_PRESERVE; + } + else + { + a.timer = CONTEXT_STATE_POLICY_RESET; + } + } + { const char *context_scope = PromiseGetConstraintAsRval(pp, "scope", RVAL_TYPE_SCALAR); a.scope = ContextScopeFromString(context_scope); @@ -1143,7 +1155,9 @@ ContextConstraint GetContextConstraints(const EvalContext *ctx, const Promise *p for (int k = 0; CF_CLASSBODY[k].lval != NULL; k++) { - if (strcmp(cp->lval, "persistence") == 0 || strcmp(cp->lval, "scope") == 0) + if (strcmp(cp->lval, "persistence") == 0 || + strcmp(cp->lval, "scope") == 0 || + strcmp(cp->lval, "timer_policy") == 0) { continue; } diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index 71ca9f7f3b..7a25a2452a 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -1210,6 +1210,7 @@ typedef struct ContextScope scope; int nconstraints; int persistent; + PersistentClassPolicy timer; } ContextConstraint; /*************************************************************************/ diff --git a/libpromises/mod_common.c b/libpromises/mod_common.c index 3d41cd4270..7b1a59967a 100644 --- a/libpromises/mod_common.c +++ b/libpromises/mod_common.c @@ -226,6 +226,7 @@ const ConstraintSyntax CF_CLASSBODY[] = ConstraintSyntaxNewContext("not", "Evaluate the negation of string expression in normal form", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewContextList("select_class", "Select one of the named list of classes to define based on host identity. Default value: random_selection", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewContextList("xor", "Combine class sources with XOR", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewOption("timer_policy", "absolute,reset", "Whether a persistent class restarts its counter when rediscovered. Default value: reset", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; diff --git a/libpromises/verify_classes.c b/libpromises/verify_classes.c index 7f903e2eb6..69e9b8f566 100644 --- a/libpromises/verify_classes.c +++ b/libpromises/verify_classes.c @@ -131,7 +131,7 @@ PromiseResult VerifyClassPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED pp->promiser, a.context.persistent); Buffer *buf = StringSetToBuffer(tags, ','); EvalContextHeapPersistentSave(ctx, pp->promiser, a.context.persistent, - CONTEXT_STATE_POLICY_RESET, BufferData(buf)); + a.context.timer, BufferData(buf)); BufferDestroy(buf); } if (inserted && (comment != NULL)) diff --git a/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf b/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf new file mode 100644 index 0000000000..5310759554 --- /dev/null +++ b/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf @@ -0,0 +1,77 @@ +####################################################### +# +# CFE-4681: classes: promises should support timer_policy +# +# Test that timer_policy => "absolute" on a classes: promise +# causes the persistent class to be preserved (not reset) +# on subsequent agent runs. +# +####################################################### + +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent init +{ + vars: + "dflags" string => ifelse("EXTRA", "-DDEBUG,EXTRA", "-DDEBUG"); + + # Clean up any leftover persistent class from a previous test run + commands: + "$(G.echo)" classes => init_cancel_always; +} + +body classes init_cancel_always +{ + cancel_repaired => { "timer_policy_test_class" }; + cancel_notkept => { "timer_policy_test_class" }; + cancel_kept => { "timer_policy_test_class" }; +} + +bundle agent test +{ + vars: + "dflags" string => ifelse("EXTRA", "-DDEBUG,EXTRA", "-DDEBUG"); + + # First run: define the persistent class with timer_policy => "absolute" + commands: + "$(sys.cf_agent) -K $(dflags) -f $(this.promise_filename).sub" + classes => test_always("done_persisting"); +} + +body classes test_always(x) +{ + promise_repaired => { "$(x)" }; + promise_kept => { "$(x)" }; + repair_failed => { "$(x)" }; + repair_denied => { "$(x)" }; + repair_timeout => { "$(x)" }; +} + +bundle agent check +{ + vars: + "dflags" string => ifelse("EXTRA", "-DDEBUG,EXTRA", "-DDEBUG"); + + done_persisting:: + # Second run: the class should be in a preserved state + "subout" string => execresult("$(sys.cf_agent) -Kv $(dflags) -f $(this.promise_filename).sub 2>&1", "useshell"); + + classes: + done_persisting:: + "preserved" expression => regcmp(".*already in a preserved state.*", "$(subout)"); + + methods: + done_persisting:: + "" usebundle => dcs_passif("preserved", $(this.promise_filename)); + + reports: + DEBUG.done_persisting:: + "Agent output: $(subout)"; + !done_persisting:: + "$(this.promise_filename) FAIL (first run did not complete)"; +} diff --git a/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf.sub b/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf.sub new file mode 100644 index 0000000000..402d3ba3ef --- /dev/null +++ b/tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf.sub @@ -0,0 +1,13 @@ +body common control +{ + bundlesequence => { run }; +} + +bundle common run +{ + classes: + "timer_policy_test_class" + expression => "any", + persistence => "120", + timer_policy => "absolute"; +} diff --git a/tests/unit/eval_context_test.c b/tests/unit/eval_context_test.c index e47b6e1a4b..77234beda8 100644 --- a/tests/unit/eval_context_test.c +++ b/tests/unit/eval_context_test.c @@ -152,6 +152,51 @@ void test_eval_with_token_from_list(void) StringSetDestroy(time_classes); } +static void test_persistent_class_timer_policy(void) +{ + EvalContext *ctx = EvalContextNew(); + + /* Save a persistent class with PRESERVE policy, 60 minute TTL */ + EvalContextHeapPersistentSave(ctx, "timer_test", 60, + CONTEXT_STATE_POLICY_PRESERVE, "tag1"); + + /* Verify the class loads correctly after PRESERVE save */ + EvalContextHeapPersistentLoadAll(ctx); + + { + const Class *cls = EvalContextClassGet(ctx, "default", "timer_test"); + assert_true(cls != NULL); + assert_string_equal("timer_test", cls->name); + } + + /* Save again with PRESERVE -- the function should early-return + * (class is preserved, not expired, same tags), leaving the DB + * record unchanged. We verify by loading persistent classes and + * checking the class is still defined. */ + EvalContextHeapPersistentSave(ctx, "timer_test", 60, + CONTEXT_STATE_POLICY_PRESERVE, "tag1"); + + /* Class should still be defined after the second PRESERVE save */ + { + const Class *cls = EvalContextClassGet(ctx, "default", "timer_test"); + assert_true(cls != NULL); + assert_string_equal("timer_test", cls->name); + } + + /* Save with RESET policy -- the record SHOULD be overwritten. + * The class should still be loadable afterward. */ + EvalContextHeapPersistentSave(ctx, "timer_test", 60, + CONTEXT_STATE_POLICY_RESET, "tag1"); + + { + const Class *cls = EvalContextClassGet(ctx, "default", "timer_test"); + assert_true(cls != NULL); + assert_string_equal("timer_test", cls->name); + } + + EvalContextDestroy(ctx); +} + int main() { PRINT_TEST_BANNER(); @@ -160,6 +205,7 @@ int main() const UnitTest tests[] = { unit_test(test_class_persistence), + unit_test(test_persistent_class_timer_policy), unit_test(test_changes_chroot), unit_test(test_eval_with_token_from_list), };