Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cf-agent/agent_operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
* EvalContextIsClassicOrder().
*
* Used by cf-agent (bundlesequence and methods promises) and cf-reactor
* (bundle in the 'then' attribute of an events promise). Can be called
* recursively through methods promises.
* (bundles in the 'then' attribute of an events promise, including reactor
* bundles without events promises, which have the promise types of agent
* bundles). Can be called recursively through methods promises.
*
* The caller is responsible for pushing the bundle frame
* (EvalContextStackPushBundleFrame()) before calling, and popping it after.
Expand Down
35 changes: 34 additions & 1 deletion cf-reactor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,39 @@

cf-reactor periodically reads the policy to set up the in-memory datastructures needed to react to events. It evaluates reactor bundles and parses events promises to create the corresponding `watchers`. These watchers stay active until the next policy read, when they are rebuilt.

Reactor bundles are of two mutually exclusive kinds:

- **With events promises**: they can only have `meta`, `vars`, `classes` and `events` promises. On every policy read, each of them without arguments is evaluated in that order, the events promises last, registering the watchers.
- **Without events promises**: they can have every promise type of agent bundles (built-in and custom). They are not evaluated on policy read, they are run from the `then` attribute of events promises, like cf-agent runs agent bundles (`ScheduleAgentOperations()`, in normal or top-down order).

This is checked when the policy is loaded (see `ReactorBundleCheck()`): having another promise type alongside events promises in a reactor bundle, or referring to a reactor bundle with events promises in a `then` attribute, is an error.

```cf3
bundle reactor set_up_events
{
vars:
"file" string => "/tmp/a";

events:
"recreate_a"
when => file_deleted("$(file)"),
then => { create_file("$(file)"), report_recreated };
}

bundle reactor create_file(file)
{
files:
"$(file)"
create => "true";
}

bundle reactor report_recreated
{
reports:
"Recreated the file";
}
```

Events promises are parsed into watcher instances, each holding: the event `key` from the events promise, an event `type` (e.g. file deletion), a `state` with whatever state the check needs (e.g. the name of the file being watched), and a `check function` that detects the event by comparing the current state to the state recorded at the last check (e.g. whether the file still exists). It returns `true` only on the specific transition being watched for, so it fires once per change and does not stay `true` afterwards.

### Events
Expand All @@ -22,7 +55,7 @@ On Linux, we could use inotify instead in the future, and let the kernel poll fo

### Running bundles

On dispatch, the bundle corresponding to an event is run, however it doesn't do a full agent run.
On dispatch, the bundles in the `then` attribute of the events promise are run in order, however it doesn't do a full agent run. They can be agent, common or reactor bundles without events promises (looked up in that order), so a policy for cf-reactor doesn't need agent bundles.

## Implementation details

Expand Down
66 changes: 39 additions & 27 deletions cf-reactor/reactor_transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,16 @@
#include <mod_custom.h> // Initialize/FinalizeCustomPromises()
#include <timeout.h> // SetReferenceTime()
#include <time_classes.h> // UpdateTimeClasses()
#include <mod_reactor.h> // REACTOR_INTERVAL_RANGE, REACTOR_FILE_DELETED
#include <mod_reactor.h> // REACTOR_INTERVAL_RANGE, REACTOR_FILE_DELETED, REACTOR_*_TYPES, BundleIsReactorEventsBundle()
#include <syntax.h> // CheckConstraintTypeMatch(), PromiseTypeSyntaxGet(), BodySyntaxGetConstraintSyntax()
#include <file_watcher.h>
#include <conversion.h>

/* Promise types evaluated within `bundle reactor NAME { ... }`. */
static const char *const REACTOR_TYPESEQUENCE[] =
{
"meta",
"vars",
"classes",
"events",
NULL
};

/* Resolves the agent or common bundle referred to by an entry of an events
* promise's 'then' attribute, either "name" or name(args). If args is not
* NULL, it is set to the bundle arguments (or NULL if there are none).
* Logs an error and returns NULL if the bundle is not found. */
/* Resolves the agent, common or reactor bundle (looked up in that order)
* referred to by an entry of an events promise's 'then' attribute, either
* "name" or name(args). If args is not NULL, it is set to the bundle
* arguments (or NULL if there are none). Logs an error and returns NULL if
* the bundle is not found, or is a reactor bundle with events promises. */
static const Bundle *ResolveThenBundle(
const EvalContext *ctx, const Promise *pp, Rval then_rval, const Rlist **args)
{
Expand All @@ -79,20 +70,25 @@ static const Bundle *ResolveThenBundle(
}

const Bundle *bp = NULL;
if (name != NULL)
for (size_t i = 0; name != NULL && bp == NULL && REACTOR_THEN_BUNDLE_TYPES[i] != NULL; i++)
{
bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name, "agent");
if (bp == NULL)
{
bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name, "common");
}
bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name,
REACTOR_THEN_BUNDLE_TYPES[i]);
}
if (bp == NULL)
{
Log(LOG_LEVEL_ERR, "Reactor events promise '%s' refers to unknown bundle '%s'",
pp->promiser, (name != NULL) ? name : "(invalid)");
return NULL;
}
if (BundleIsReactorEventsBundle(bp))
{
Log(LOG_LEVEL_ERR,
"Reactor events promise '%s' cannot run reactor bundle '%s', it has events promises "
"(reactor bundles with events promises are only evaluated when cf-reactor reads the policy)",
pp->promiser, bp->name);
return NULL;
}

if (args != NULL)
{
Expand Down Expand Up @@ -368,6 +364,9 @@ static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp)
return PROMISE_RESULT_NOOP;
}

/* Promise actuator for the promises of a reactor bundle with events promises
* evaluated on policy read: keeps its meta, vars and classes promises, and
* registers the watcher of each iteration of its events promises. */
static PromiseResult KeepReactorPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED void *param)
{
assert(param == NULL);
Expand All @@ -391,21 +390,31 @@ static PromiseResult KeepReactorPromise(EvalContext *ctx, const Promise *pp, ARG
return result;
}

/* Only the promise types of REACTOR_EVENTS_BUNDLE_PROMISE_TYPES are
* evaluated, see EvaluateReactorBundle() */
debug_abort_if_reached();
return PROMISE_RESULT_NOOP;
}

/* Keeps the promises of a reactor bundle with events promises on (re)read of
* the policy, in the order of REACTOR_EVENTS_BUNDLE_PROMISE_TYPES: the events
* promises are kept last, so that they see the variables and classes defined
* by the bundle. */
static void EvaluateReactorBundle(EvalContext *ctx, const Bundle *bp)
{
assert(bp != NULL);
assert(BundleIsReactorEventsBundle(bp));

BundleBanner(bp, NULL);
EvalContextStackPushBundleFrame(ctx, bp, NULL, false, NULL);

for (int i = 0; REACTOR_TYPESEQUENCE[i] != NULL; i++)
for (int i = 0; REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i] != NULL; i++)
{
const BundleSection *sp = BundleGetSection(bp, REACTOR_TYPESEQUENCE[i]);
const BundleSection *sp = BundleGetSection(bp, REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i]);
if (sp == NULL || SeqLength(sp->promises) == 0)
{
Log(LOG_LEVEL_DEBUG, "No promise type %s in bundle %s",
REACTOR_TYPESEQUENCE[i], bp->name);
REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i], bp->name);
continue;
}

Expand All @@ -415,10 +424,11 @@ static void EvaluateReactorBundle(EvalContext *ctx, const Bundle *bp)
const Promise *pp = SeqAt(sp->promises, j);
ExpandPromise(ctx, pp, KeepReactorPromise, NULL);
}
EvalContextStackPopFrame(ctx);
EvalContextStackPopFrame(ctx); /* bundle section */
}

EvalContextStackPopFrame(ctx);
EvalContextStackPopFrame(ctx); /* bundle */
EndBundleBanner(bp);
}

/* Runs the bundle referred to by an entry of the 'then' attribute of the
Expand Down Expand Up @@ -549,7 +559,9 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy)
for (size_t i = 0; i < SeqLength(policy->bundles); i++)
{
const Bundle *bp = SeqAt(policy->bundles, i);
if (!StringEqual(bp->type, CF_AGENTTYPES[AGENT_TYPE_REACTOR]))
/* The reactor bundles without events promises are only run from the
* 'then' attribute of events promises */
if (!BundleIsReactorEventsBundle(bp))
{
continue;
}
Expand Down
17 changes: 11 additions & 6 deletions cf-reactor/reactor_transform.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
#include <watcher.h> /* WatcherCheckResult */

/**
* @brief Evaluate every `bundle reactor NAME { ... }` in the given policy:
* keep its "meta", "vars" and "classes" promises, and register a watcher
* (see watcher.h) for each of its "events" promises.
* @brief Evaluate every `bundle reactor NAME { ... }` with events promises
* and without arguments in the given policy: keep its "meta", "vars" and
* "classes" promises, then register a watcher (see watcher.h) for each of
* its "events" promises. The reactor bundles without events promises are
* not evaluated, they are only run from the 'then' attribute of events
* promises (see BundleIsReactorEventsBundle()).
*
* Expects no watchers to be registered: on re-read of the policy, the
* watchers of the previous policy must be set aside with
Expand All @@ -43,9 +46,11 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy);

/**
* @brief Keep the events promise of a watcher that fired (see WatcherEventFn
* in watcher.h): run the bundle of its 'then' attribute, and record the
* outcome of the bundle as the outcome of the events promise. If the watcher
* failed to check for its event, record a failure instead.
* in watcher.h): run the agent, common or reactor bundles (without events
* promises) of its 'then' attribute like cf-agent does (see
* ScheduleAgentOperations()), and record the outcome of the bundles as the
* outcome of the events promise. If the watcher failed to check for its
* event, record a failure instead.
*
* @param pp the expanded events promise the watcher was registered with,
* kept as is: its class guard, if/unless and functions are not
Expand Down
2 changes: 1 addition & 1 deletion libpromises/cf3parse_logic.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ static bool RelevantBundle(const char *agent, const char *blocktype)
}

/* Here are some additional bundle types handled by cfAgent, and by
* cf-reactor for the agent bundles it runs */
* cf-reactor for the agent and reactor bundles it runs */

Item *ip = SplitString("edit_line,edit_xml", ',');

Expand Down
3 changes: 2 additions & 1 deletion libpromises/mod_methods.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ static const ConstraintSyntax CF_METHOD_BODIES[] =

static bool MethodsParseTreeCheck(const Promise *pp, Seq *errors)
{
return PromiseCheckBundleCallArity(pp, "usebundle", errors);
static const char *const bundle_types[] = { CF_AGENTC, CF_COMMONC, NULL };
return PromiseCheckBundleCallArity(pp, "usebundle", bundle_types, errors);
}

const PromiseTypeSyntax CF_METHOD_PROMISE_TYPES[] =
Expand Down
112 changes: 109 additions & 3 deletions libpromises/mod_reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
#include <syntax.h>
#include <policy.h>
#include <rlist.h>
#include <string_lib.h>
#include <fncall.h>
#include <class.h> /* ClassRefParse() */
#include <string_lib.h> /* StringEqual(), IsStrIn() */

static const char *const POLICY_ERROR_EVENTS_MISSING_ATTRIBUTE =
"Missing required attribute '%s' for promise type 'events'";
Expand Down Expand Up @@ -78,10 +80,103 @@ static const BodySyntax when_body = BodySyntaxNew("when", when_constraints, &Whe
static const ConstraintSyntax CF_EVENT_BODIES[] =
{
ConstraintSyntaxNewBody("when", &when_body, "Event to react to", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewBundleList("then", "Bundle or list of bundles to run in order on event", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewBundleList("then", "Agent, common or reactor bundle (without events promises) or list of bundles to run in order on event", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewNull()
};

const char *const REACTOR_THEN_BUNDLE_TYPES[] = { CF_AGENTC, CF_COMMONC, CF_REACTORC, NULL };

const char *const REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[] = { "meta", "vars", "classes", "events", NULL };

bool BundleIsReactorEventsBundle(const Bundle *bp)
{
assert(bp != NULL);
return StringEqual(bp->type, CF_REACTORC) && (BundleGetSection(bp, "events") != NULL);
}

static bool ReactorEventsBundleCheckSections(const Seq *sections, Seq *errors)
{
bool success = true;
for (size_t i = 0; i < SeqLength(sections); i++)
{
const BundleSection *sp = SeqAt(sections, i);
if (!IsStrIn(sp->promise_type, REACTOR_EVENTS_BUNDLE_PROMISE_TYPES))
{
SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_BUNDLE_SECTION, sp,
"Promise type '%s' not supported by reactor bundles with events promises, "
"which can only have meta, vars, classes and events promises "
"(the other promise types go in reactor bundles without events promises, "
"run from the 'then' attribute of events promises)",
sp->promise_type));
success = false;
}
}
return success;
}

bool ReactorBundleCheck(const Bundle *bp, Seq *errors)
{
assert(bp != NULL);
assert(StringEqual(bp->type, CF_REACTORC));

if (!BundleIsReactorEventsBundle(bp))
{
return true;
}

bool success = true;
success &= ReactorEventsBundleCheckSections(bp->sections, errors);
success &= ReactorEventsBundleCheckSections(bp->custom_sections, errors);
return success;
}

/* Checks that an entry of the 'then' attribute of the events promise, either
* "name" or name(args), does not refer to a reactor bundle with events
* promises, if the bundle is found. */
static bool ThenBundleCheck(const Promise *pp, const Constraint *cp, Rval rval, Seq *errors)
{
assert(pp != NULL);
assert(cp != NULL);

const char *name = NULL;
switch (rval.type)
{
case RVAL_TYPE_SCALAR:
name = RvalScalarValue(rval);
break;
case RVAL_TYPE_FNCALL:
name = RvalFnCallValue(rval)->name;
break;
default:
return true;
}

// HACK: exploiting the fact that class-references and call-references are similar
ClassRef ref = ClassRefParse(name);
if (!ClassRefIsQualified(ref))
{
ClassRefQualify(&ref, PromiseGetNamespace(pp));
}

const Bundle *callee = NULL;
for (size_t i = 0; callee == NULL && REACTOR_THEN_BUNDLE_TYPES[i] != NULL; i++)
{
callee = PolicyGetBundle(PromiseGetPolicy(pp), ref.ns, REACTOR_THEN_BUNDLE_TYPES[i], ref.name);
}

ClassRefDestroy(ref);

if (callee != NULL && BundleIsReactorEventsBundle(callee))
{
SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_CONSTRAINT, cp,
"Events promise cannot run reactor bundle '%s', it has events promises "
"(reactor bundles with events promises are only evaluated when cf-reactor reads the policy)",
name));
return false;
}
return true;
}

static bool EventsParseTreeCheck(const Promise *pp, Seq *errors)
{
bool success = true;
Expand All @@ -108,8 +203,19 @@ static bool EventsParseTreeCheck(const Promise *pp, Seq *errors)
POLICY_ERROR_EVENTS_EMPTY_THEN));
success = false;
}
else if (then->rval.type == RVAL_TYPE_LIST)
{
for (const Rlist *rp = RvalRlistValue(then->rval); rp != NULL; rp = rp->next)
{
success &= ThenBundleCheck(pp, then, rp->val, errors);
}
}
else
{
success &= ThenBundleCheck(pp, then, then->rval, errors);
}

success &= PromiseCheckBundleCallArity(pp, "then", errors);
success &= PromiseCheckBundleCallArity(pp, "then", REACTOR_THEN_BUNDLE_TYPES, errors);
return success;
}

Expand Down
Loading
Loading