From 797bed8b4e480592b874ea8c7c1b9e53e58a1dbb Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Thu, 3 Jul 2025 14:28:52 +0200 Subject: [PATCH 1/4] Allow top-down evaluation of promises Ticket: ENT-10184 Signed-off-by: Victor Moene --- cf-agent/cf-agent.c | 49 ++++++++++++++++++++++++++++++++++++++ libpromises/cf3.defs.h | 1 + libpromises/eval_context.h | 1 + libpromises/expand.c | 9 +++++++ libpromises/mod_common.c | 1 + libpromises/policy.c | 4 ++++ libpromises/policy.h | 1 + libpromises/prototypes3.h | 2 ++ 8 files changed, 68 insertions(+) diff --git a/cf-agent/cf-agent.c b/cf-agent/cf-agent.c index 2009183361..92dcce36c9 100644 --- a/cf-agent/cf-agent.c +++ b/cf-agent/cf-agent.c @@ -1559,6 +1559,15 @@ static void AllClassesReport(const EvalContext *ctx) PromiseResult ScheduleAgentOperations(EvalContext *ctx, const Bundle *bp) // NB - this function can be called recursively through "methods" +{ + if (EvalContextGetEvalOption(ctx, EVAL_OPTION_NORMAL_EVALUATION)) + { + return ScheduleAgentOperationsNormalOrder(ctx, bp); + } + return ScheduleAgentOperationsTopDownOrder(ctx, bp); +} + +PromiseResult ScheduleAgentOperationsNormalOrder(EvalContext *ctx, const Bundle *bp) { assert(bp != NULL); @@ -1652,6 +1661,46 @@ PromiseResult ScheduleAgentOperations(EvalContext *ctx, const Bundle *bp) return result; } +PromiseResult ScheduleAgentOperationsTopDownOrder(EvalContext *ctx, const Bundle *bp) +{ + assert(bp != NULL); + + int save_pr_kept = PR_KEPT; + int save_pr_repaired = PR_REPAIRED; + int save_pr_notkept = PR_NOTKEPT; + struct timespec start = BeginMeasure(); + + if (PROCESSREFRESH == NULL || (PROCESSREFRESH && IsRegexItemIn(ctx, PROCESSREFRESH, bp->name))) + { + ClearProcessTable(); + } + + PromiseResult result = PROMISE_RESULT_SKIPPED; + for (int pass = 1; pass < CF_DONEPASSES; pass++) + { + for (size_t ppi = 0; ppi < SeqLength(bp->all_promises); ppi++) + { + Promise *pp = SeqAt(bp->all_promises, ppi); + BundleSection *parent_section = pp->parent_section; + + EvalContextStackPushBundleSectionFrame(ctx, parent_section); + + PromiseResult promise_result = ExpandPromise(ctx, pp, KeepAgentPromise, NULL); + result = PromiseResultUpdate(result, promise_result); + if (EvalAborted(ctx) || BundleAbort(ctx)) + { + EvalContextStackPopFrame(ctx); + NoteBundleCompliance(bp, save_pr_kept, save_pr_repaired, save_pr_notkept, start); + return result; + } + EvalContextStackPopFrame(ctx); + } + } + + NoteBundleCompliance(bp, save_pr_kept, save_pr_repaired, save_pr_notkept, start); + return result; +} + /*********************************************************************/ #ifdef __MINGW32__ diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index 039afee5c8..18dd73592d 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -441,6 +441,7 @@ typedef enum COMMON_CONTROL_TLS_MIN_VERSION, COMMON_CONTROL_PACKAGE_INVENTORY, COMMON_CONTROL_PACKAGE_MODULE, + COMMON_CONTROL_EVALUATION_ORDER, COMMON_CONTROL_MAX } CommonControl; diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index 6ad09c08b3..ed7fede076 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -108,6 +108,7 @@ typedef enum EVAL_OPTION_EVAL_FUNCTIONS = 1 << 0, EVAL_OPTION_CACHE_SYSTEM_FUNCTIONS = 1 << 1, + EVAL_OPTION_NORMAL_EVALUATION = 1 << 2, EVAL_OPTION_FULL = 0xFFFFFFFF } EvalContextOption; diff --git a/libpromises/expand.c b/libpromises/expand.c index 1a5fdbc1a6..681999f220 100644 --- a/libpromises/expand.c +++ b/libpromises/expand.c @@ -1025,6 +1025,15 @@ static void ResolveControlBody(EvalContext *ctx, GenericAgentConfig *config, /* Ignored */ } + if (strcmp(lval, CFG_CONTROLBODY[COMMON_CONTROL_EVALUATION_ORDER].lval) == 0) + { + Log(LOG_LEVEL_VERBOSE, "SET evaluation %s", + RvalScalarValue(evaluated_rval)); + + bool is_normal = (StringEqual(RvalScalarValue(evaluated_rval), "normal")); + EvalContextSetEvalOption(ctx, EVAL_OPTION_NORMAL_EVALUATION, is_normal); + } + RvalDestroy(evaluated_rval); } diff --git a/libpromises/mod_common.c b/libpromises/mod_common.c index 3e8cbe902b..5af4f123a1 100644 --- a/libpromises/mod_common.c +++ b/libpromises/mod_common.c @@ -268,6 +268,7 @@ const ConstraintSyntax CFG_CONTROLBODY[COMMON_CONTROL_MAX + 1] = ConstraintSyntaxNewString("tls_min_version", "", "Minimum acceptable TLS version for outgoing connections, defaults to OpenSSL's default", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewStringList("package_inventory", ".*", "Name of the package manager used for software inventory management", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewString("package_module", ".*", "Name of the default package manager", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString("evaluation_order", "(normal|top_down)", "Order of evaluation of promises", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; diff --git a/libpromises/policy.c b/libpromises/policy.c index 1a79eb2351..4eab215ec0 100644 --- a/libpromises/policy.c +++ b/libpromises/policy.c @@ -1322,6 +1322,7 @@ Bundle *PolicyAppendBundle(Policy *policy, bundle->source_path = SafeStringDuplicate(source_path); bundle->sections = SeqNew(10, BundleSectionDestroy); bundle->custom_sections = SeqNew(10, BundleSectionDestroy); + bundle->all_promises = SeqNew(10, NULL); return bundle; } @@ -1437,6 +1438,7 @@ Promise *BundleSectionAppendPromise(BundleSection *section, const char *promiser { assert(promiser && "Missing promiser"); assert(section != NULL && "Missing promise type"); + assert(section->parent_bundle != NULL); Promise *pp = xcalloc(1, sizeof(Promise)); @@ -1452,6 +1454,7 @@ Promise *BundleSectionAppendPromise(BundleSection *section, const char *promiser } SeqAppend(section->promises, pp); + SeqAppend(section->parent_bundle->all_promises, pp); pp->parent_section = section; @@ -1479,6 +1482,7 @@ static void BundleDestroy(Bundle *bundle) RlistDestroy(bundle->args); SeqDestroy(bundle->sections); SeqDestroy(bundle->custom_sections); + SeqDestroy(bundle->all_promises); free(bundle); } diff --git a/libpromises/policy.h b/libpromises/policy.h index 9f9304faef..cbd75a8849 100644 --- a/libpromises/policy.h +++ b/libpromises/policy.h @@ -79,6 +79,7 @@ struct Bundle_ Seq *sections; Seq *custom_sections; + Seq *all_promises; char *source_path; SourceOffset offset; diff --git a/libpromises/prototypes3.h b/libpromises/prototypes3.h index 1ba16e97d0..8a5334a730 100644 --- a/libpromises/prototypes3.h +++ b/libpromises/prototypes3.h @@ -44,6 +44,8 @@ void yyerror(const char *s); /* agent.c */ PromiseResult ScheduleAgentOperations(EvalContext *ctx, const Bundle *bp); +PromiseResult ScheduleAgentOperationsNormalOrder(EvalContext *ctx, const Bundle *bp); +PromiseResult ScheduleAgentOperationsTopDownOrder(EvalContext *ctx, const Bundle *bp); /* Only for agent.c */ From 36548c484ffc9e2c7d5a258f3fb543e25d3e70ff Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Thu, 3 Jul 2025 16:31:44 +0200 Subject: [PATCH 2/4] Added better verbose output Signed-off-by: Victor Moene --- cf-agent/cf-agent.c | 8 ++++++++ libpromises/ornaments.c | 14 ++++++++++++++ libpromises/ornaments.h | 1 + 3 files changed, 23 insertions(+) diff --git a/cf-agent/cf-agent.c b/cf-agent/cf-agent.c index 92dcce36c9..17251b4157 100644 --- a/cf-agent/cf-agent.c +++ b/cf-agent/cf-agent.c @@ -1678,11 +1678,19 @@ PromiseResult ScheduleAgentOperationsTopDownOrder(EvalContext *ctx, const Bundle PromiseResult result = PROMISE_RESULT_SKIPPED; for (int pass = 1; pass < CF_DONEPASSES; pass++) { + char *last_promise_type = ""; for (size_t ppi = 0; ppi < SeqLength(bp->all_promises); ppi++) { + EvalContextSetPass(ctx, pass); Promise *pp = SeqAt(bp->all_promises, ppi); BundleSection *parent_section = pp->parent_section; + if (!StringEqual(last_promise_type, parent_section->promise_type)) + { + SpecialTypeBannerFromString(parent_section->promise_type, pass); + } + last_promise_type = parent_section->promise_type; + EvalContextStackPushBundleSectionFrame(ctx, parent_section); PromiseResult promise_result = ExpandPromise(ctx, pp, KeepAgentPromise, NULL); diff --git a/libpromises/ornaments.c b/libpromises/ornaments.c index ca37e98d88..c0d4d67dc8 100644 --- a/libpromises/ornaments.c +++ b/libpromises/ornaments.c @@ -130,6 +130,20 @@ void SpecialTypeBanner(TypeSequence type, int pass) } } +void SpecialTypeBannerFromString(char *type, int pass) +{ + if (StringEqual(type, "classes")) + { + Log(LOG_LEVEL_VERBOSE, "C: ........................................................."); + Log(LOG_LEVEL_VERBOSE, "C: BEGIN classes / conditions (pass %d)", pass); + } + if (StringEqual(type, "vars")) + { + Log(LOG_LEVEL_VERBOSE, "V: ........................................................."); + Log(LOG_LEVEL_VERBOSE, "V: BEGIN variables (pass %d)", pass); + } +} + void PromiseBanner(EvalContext *ctx, const Promise *pp) { char handle[CF_MAXVARSIZE]; diff --git a/libpromises/ornaments.h b/libpromises/ornaments.h index 2688127ce9..37d6106f97 100644 --- a/libpromises/ornaments.h +++ b/libpromises/ornaments.h @@ -34,6 +34,7 @@ #include void SpecialTypeBanner(TypeSequence type, int pass); +void SpecialTypeBannerFromString(char *type, int pass); void PromiseBanner(EvalContext *ctx, const Promise *pp); void Banner(const char *s); void Legend(); From 452264d5a281775b16c2578747466b5031e79a85 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 7 Jul 2025 12:30:57 +0200 Subject: [PATCH 3/4] Changed from normal to classic Signed-off-by: Victor Moene --- cf-agent/cf-agent.c | 2 +- libpromises/eval_context.h | 2 +- libpromises/expand.c | 4 ++-- libpromises/mod_common.c | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cf-agent/cf-agent.c b/cf-agent/cf-agent.c index 17251b4157..58c5324dd7 100644 --- a/cf-agent/cf-agent.c +++ b/cf-agent/cf-agent.c @@ -1560,7 +1560,7 @@ static void AllClassesReport(const EvalContext *ctx) PromiseResult ScheduleAgentOperations(EvalContext *ctx, const Bundle *bp) // NB - this function can be called recursively through "methods" { - if (EvalContextGetEvalOption(ctx, EVAL_OPTION_NORMAL_EVALUATION)) + if (EvalContextGetEvalOption(ctx, EVAL_OPTION_CLASSIC_EVALUATION)) { return ScheduleAgentOperationsNormalOrder(ctx, bp); } diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index ed7fede076..713de4596e 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -108,7 +108,7 @@ typedef enum EVAL_OPTION_EVAL_FUNCTIONS = 1 << 0, EVAL_OPTION_CACHE_SYSTEM_FUNCTIONS = 1 << 1, - EVAL_OPTION_NORMAL_EVALUATION = 1 << 2, + EVAL_OPTION_CLASSIC_EVALUATION = 1 << 2, EVAL_OPTION_FULL = 0xFFFFFFFF } EvalContextOption; diff --git a/libpromises/expand.c b/libpromises/expand.c index 681999f220..dbd87afd43 100644 --- a/libpromises/expand.c +++ b/libpromises/expand.c @@ -1030,8 +1030,8 @@ static void ResolveControlBody(EvalContext *ctx, GenericAgentConfig *config, Log(LOG_LEVEL_VERBOSE, "SET evaluation %s", RvalScalarValue(evaluated_rval)); - bool is_normal = (StringEqual(RvalScalarValue(evaluated_rval), "normal")); - EvalContextSetEvalOption(ctx, EVAL_OPTION_NORMAL_EVALUATION, is_normal); + bool is_classic = (StringEqual(RvalScalarValue(evaluated_rval), "classic")); + EvalContextSetEvalOption(ctx, EVAL_OPTION_CLASSIC_EVALUATION, is_classic); } RvalDestroy(evaluated_rval); diff --git a/libpromises/mod_common.c b/libpromises/mod_common.c index 5af4f123a1..16b68589b2 100644 --- a/libpromises/mod_common.c +++ b/libpromises/mod_common.c @@ -268,7 +268,7 @@ const ConstraintSyntax CFG_CONTROLBODY[COMMON_CONTROL_MAX + 1] = ConstraintSyntaxNewString("tls_min_version", "", "Minimum acceptable TLS version for outgoing connections, defaults to OpenSSL's default", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewStringList("package_inventory", ".*", "Name of the package manager used for software inventory management", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewString("package_module", ".*", "Name of the default package manager", SYNTAX_STATUS_NORMAL), - ConstraintSyntaxNewString("evaluation_order", "(normal|top_down)", "Order of evaluation of promises", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString("evaluation_order", "(classic|top_down)", "Order of evaluation of promises", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; From 1dcceca6ae0c971b90e1e383af169f2d23853d57 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 8 Jul 2025 11:50:52 +0200 Subject: [PATCH 4/4] Added tests for top down evaluation Signed-off-by: Victor Moene --- .../15_control/01_common/top_down/cfengine.py | 466 ++++++++++++++++++ .../01_common/top_down/dummy_promise_type.py | 26 + .../top_down_evaluation_complex_policy.cf | 48 ++ ...op_down_evaluation_custom_promise_types.cf | 59 +++ .../top_down_evaluation_long_policy.cf | 103 ++++ 5 files changed, 702 insertions(+) create mode 100644 tests/acceptance/15_control/01_common/top_down/cfengine.py create mode 100644 tests/acceptance/15_control/01_common/top_down/dummy_promise_type.py create mode 100644 tests/acceptance/15_control/01_common/top_down_evaluation_complex_policy.cf create mode 100644 tests/acceptance/15_control/01_common/top_down_evaluation_custom_promise_types.cf create mode 100644 tests/acceptance/15_control/01_common/top_down_evaluation_long_policy.cf diff --git a/tests/acceptance/15_control/01_common/top_down/cfengine.py b/tests/acceptance/15_control/01_common/top_down/cfengine.py new file mode 100644 index 0000000000..5f82cf6274 --- /dev/null +++ b/tests/acceptance/15_control/01_common/top_down/cfengine.py @@ -0,0 +1,466 @@ +import sys +import json +import traceback +from copy import copy +from collections import OrderedDict + +_LOG_LEVELS = { + level: idx + for idx, level in enumerate( + ("critical", "error", "warning", "notice", "info", "verbose", "debug") + ) +} + + +def _skip_until_empty_line(file): + while True: + line = file.readline().strip() + if not line: + break + + +def _get_request(file, record_file=None): + line = file.readline() + blank_line = file.readline() + if record_file is not None: + record_file.write("< " + line) + record_file.write("< " + blank_line) + + return json.loads(line.strip()) + + +def _put_response(data, file, record_file=None): + data = json.dumps(data) + file.write(data + "\n\n") + file.flush() + + if record_file is not None: + record_file.write("> " + data + "\n") + record_file.write("> \n") + + +def _would_log(level_set, msg_level): + if msg_level not in _LOG_LEVELS: + # uknown level, assume it would be logged + return True + + return _LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set] + + +def _cfengine_type(typing): + if typing is str: + return "string" + if typing is int: + return "int" + if typing in (list, tuple): + return "slist" + if typing is dict: + return "data container" + if typing is bool: + return "true/false" + return "Error in promise module" + + +class AttributeObject(object): + def __init__(self, d): + for key, value in d.items(): + setattr(self, key, value) + + def __repr__(self): + return "{}({})".format( + self.__class__.__qualname__, + ", ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), + ) + + +class ValidationError(Exception): + def __init__(self, message): + self.message = message + + +class ProtocolError(Exception): + def __init__(self, message): + self.message = message + + +class Result: + # Promise evaluation outcomes, can reveal "real" problems with system: + KEPT = "kept" # Satisfied already, no change + REPAIRED = "repaired" # Not satisfied before , but fixed + NOT_KEPT = "not_kept" # Not satisfied before , not fixed + + # Validation only, can reveal problems in CFEngine policy: + VALID = "valid" # Validation successful + INVALID = "invalid" # Validation failed, error in cfengine policy + + # Generic succes / fail for init / terminate requests: + SUCCESS = "success" + FAILURE = "failure" + + # Unexpected, can reveal problems in promise module: + ERROR = "error" # Something went wrong in module / protocol + + +class PromiseModule: + def __init__( + self, name="default_module_name", version="0.0.1", record_file_path=None + ): + self.name = name + self.version = version + # Note: The class doesn't expose any way to set protocol version + # or flags, because that should be abstracted away from the + # user (module author). + self._validator_attributes = OrderedDict() + self._result_classes = None + + # File to record all the incoming and outgoing communication + self._record_file = open(record_file_path, "a") if record_file_path else None + + def start(self, in_file=None, out_file=None): + self._in = in_file or sys.stdin + self._out = out_file or sys.stdout + + first_line = self._in.readline() + if self._record_file is not None: + self._record_file.write("< " + first_line) + + header = first_line.strip().split(" ") + name = header[0] + version = header[1] + protocol_version = header[2] + # flags = header[3:] -- unused for now + + assert len(name) > 0 # cf-agent + assert version.startswith("3.") # 3.18.0 + assert protocol_version[0] == "v" # v1 + + _skip_until_empty_line(self._in) + + header_reply = "{name} {version} v1 json_based\n\n".format( + name=self.name, version=self.version + ) + self._out.write(header_reply) + self._out.flush() + + if self._record_file is not None: + self._record_file.write("> " + header_reply.strip() + "\n") + self._record_file.write(">\n") + + while True: + self._response = {} + self._result = None + request = _get_request(self._in, self._record_file) + self._handle_request(request) + + def _convert_types(self, promiser, attributes): + # Will only convert types if module has typing information: + if not self._has_validation_attributes: + return promiser, attributes + + replacements = {} + for name, value in attributes.items(): + if type(value) is not str: + # If something is not string, assume it is correct type + continue + if name not in self._validator_attributes: + # Unknown attribute, this will cause a validation error later + continue + # "true"/"false" -> True/False + if self._validator_attributes[name]["typing"] is bool: + if value == "true": + replacements[name] = True + elif value == "false": + replacements[name] = False + # "int" -> int() + elif self._validator_attributes[name]["typing"] is int: + try: + replacements[name] = int(value) + except ValueError: + pass + + # Don't edit dict while iterating over it, after instead: + attributes.update(replacements) + + return (promiser, attributes) + + def _handle_request(self, request): + if not request: + sys.exit("Error: Empty/invalid request or EOF reached") + + operation = request["operation"] + self._log_level = request.get("log_level", "info") + self._response["operation"] = operation + + # Agent will never request log level critical + assert self._log_level in [ + "error", + "warning", + "notice", + "info", + "verbose", + "debug", + ] + + if operation in ["validate_promise", "evaluate_promise"]: + promiser = request["promiser"] + attributes = request.get("attributes", {}) + promiser, attributes = self._convert_types(promiser, attributes) + promiser, attributes = self.prepare_promiser_and_attributes( + promiser, attributes + ) + self._response["promiser"] = promiser + self._response["attributes"] = attributes + + if operation == "init": + self._handle_init() + elif operation == "validate_promise": + self._handle_validate(promiser, attributes, request) + elif operation == "evaluate_promise": + self._handle_evaluate(promiser, attributes, request) + elif operation == "terminate": + self._handle_terminate() + else: + self._log_level = None + raise ProtocolError( + "Unknown operation: '{operation}'".format(operation=operation) + ) + + self._log_level = None + + def _add_result(self): + self._response["result"] = self._result + + def _add_result_classes(self): + if self._result_classes: + self._response["result_classes"] = self._result_classes + + def _add_traceback_to_response(self): + if self._log_level != "debug": + return + + trace = traceback.format_exc() + logs = self._response.get("log", []) + logs.append({"level": "debug", "message": trace}) + self._response["log"] = logs + + def add_attribute( + self, + name, + typing, + default=None, + required=False, + default_to_promiser=False, + validator=None, + ): + attribute = OrderedDict() + attribute["name"] = name + attribute["typing"] = typing + attribute["default"] = default + attribute["required"] = required + attribute["default_to_promiser"] = default_to_promiser + attribute["validator"] = validator + self._validator_attributes[name] = attribute + + @property + def _has_validation_attributes(self): + return bool(self._validator_attributes) + + def create_attribute_dict(self, promiser, attributes): + + # Check for missing required attributes: + for name, attribute in self._validator_attributes.items(): + if attribute["required"] and name not in attributes: + raise ValidationError( + "Missing required attribute '{name}'".format(name=name) + ) + + # Check for unknown attributes: + for name in attributes: + if name not in self._validator_attributes: + raise ValidationError("Unknown attribute '{name}'".format(name=name)) + + # Check typings and run custom validator callbacks: + for name, value in attributes.items(): + expected = _cfengine_type(self._validator_attributes[name]["typing"]) + found = _cfengine_type(type(value)) + if found != expected: + raise ValidationError( + "Wrong type for attribute '{name}', requires '{expected}', not '{value}'({found})".format( + name=name, expected=expected, value=value, found=found + ) + ) + if self._validator_attributes[name]["validator"]: + # Can raise ValidationError: + self._validator_attributes[name]["validator"](value) + + attribute_dict = OrderedDict() + + # Copy attributes specified by user policy: + for key, value in attributes.items(): + attribute_dict[key] = value + + # Set defaults based on promise module validation hints: + for name, value in self._validator_attributes.items(): + if value.get("default_to_promiser", False): + attribute_dict.setdefault(name, promiser) + elif value.get("default", None) is not None: + attribute_dict.setdefault(name, copy(value["default"])) + else: + attribute_dict.setdefault(name, None) + + return attribute_dict + + def create_attribute_object(self, promiser, attributes): + attribute_dict = self.create_attribute_dict(promiser, attributes) + return AttributeObject(attribute_dict) + + def _validate_attributes(self, promiser, attributes): + if not self._has_validation_attributes: + # Can only validate attributes if module + # provided typings for attributes + return + self.create_attribute_object(promiser, attributes) + return # Only interested in exceptions, return None + + def _handle_init(self): + self._result = self.protocol_init(None) + self._add_result() + _put_response(self._response, self._out, self._record_file) + + def _handle_validate(self, promiser, attributes, request): + meta = {"promise_type": request.get("promise_type")} + try: + self.validate_attributes(promiser, attributes, meta) + returned = self.validate_promise(promiser, attributes, meta) + if returned is None: + # Good, expected + self._result = Result.VALID + else: + # Bad, validate method shouldn't return anything else + self.log_critical( + "Bug in promise module {name} - validate_promise() should not return anything".format( + name=self.name + ) + ) + self._result = Result.ERROR + except ValidationError as e: + message = str(e) + if "promise_type" in request: + message += " for {request_promise_type} promise with promiser '{promiser}'".format( + request_promise_type=request["promise_type"], promiser=promiser + ) + else: + message += " for promise with promiser '{promiser}'".format( + promiser=promiser + ) + if "filename" in request and "line_number" in request: + message += " ({request_filename}:{request_line_number})".format( + request_filename=request["filename"], + request_line_number=request["line_number"], + ) + + self.log_error(message) + self._result = Result.INVALID + except Exception as e: + self.log_critical( + "{error_type}: {error}".format(error_type=type(e).__name__, error=e) + ) + self._add_traceback_to_response() + self._result = Result.ERROR + self._add_result() + _put_response(self._response, self._out, self._record_file) + + def _handle_evaluate(self, promiser, attributes, request): + self._result_classes = None + meta = {"promise_type": request.get("promise_type")} + try: + results = self.evaluate_promise(promiser, attributes, meta) + + # evaluate_promise should return either a result or a (result, result_classes) pair + if type(results) == str: + self._result = results + else: + assert len(results) == 2 + self._result = results[0] + self._result_classes = results[1] + except Exception as e: + self.log_critical( + "{error_type}: {error}".format(error_type=type(e).__name__, error=e) + ) + self._add_traceback_to_response() + self._result = Result.ERROR + self._add_result() + self._add_result_classes() + _put_response(self._response, self._out, self._record_file) + + def _handle_terminate(self): + self._result = self.protocol_terminate() + self._add_result() + _put_response(self._response, self._out, self._record_file) + sys.exit(0) + + def _log(self, level, message): + if self._log_level is not None and not _would_log(self._log_level, level): + return + + # Message can be str or an object which implements __str__() + # for example an exception: + message = str(message).replace("\n", r"\n") + assert "\n" not in message + self._out.write("log_{level}={message}\n".format(level=level, message=message)) + self._out.flush() + + if self._record_file is not None: + self._record_file.write( + "log_{level}={message}\n".format(level=level, message=message) + ) + + def log_critical(self, message): + self._log("critical", message) + + def log_error(self, message): + self._log("error", message) + + def log_warning(self, message): + self._log("warning", message) + + def log_notice(self, message): + self._log("notice", message) + + def log_info(self, message): + self._log("info", message) + + def log_verbose(self, message): + self._log("verbose", message) + + def log_debug(self, message): + self._log("debug", message) + + def _log_traceback(self): + trace = traceback.format_exc().split("\n") + for line in trace: + self.log_debug(line) + + # Functions to override in subclass: + + def protocol_init(self, version): + return Result.SUCCESS + + def prepare_promiser_and_attributes(self, promiser, attributes): + """Override if you want to modify promiser or attributes before validate or evaluate""" + return (promiser, attributes) + + def validate_attributes(self, promiser, attributes, meta): + """Override this if you want to prevent automatic validation""" + return self._validate_attributes(promiser, attributes) + + def validate_promise(self, promiser, attributes, meta): + """Must override this or use validation through self.add_attribute()""" + if not self._has_validation_attributes: + raise NotImplementedError("Promise module must implement validate_promise") + + def evaluate_promise(self, promiser, attributes, meta): + raise NotImplementedError("Promise module must implement evaluate_promise") + + def protocol_terminate(self): + return Result.SUCCESS diff --git a/tests/acceptance/15_control/01_common/top_down/dummy_promise_type.py b/tests/acceptance/15_control/01_common/top_down/dummy_promise_type.py new file mode 100644 index 0000000000..9e7be71d17 --- /dev/null +++ b/tests/acceptance/15_control/01_common/top_down/dummy_promise_type.py @@ -0,0 +1,26 @@ +from cfengine import ( + PromiseModule, + Result, +) + + +class DummyPromiseType(PromiseModule): + + def __init__(self, **kwargs): + super(DummyPromiseType, self).__init__( + name="dummy_promise_module", version="0.0.0", **kwargs + ) + + def validate_promise(self, promiser, attributes, meta): + pass + + def evaluate_promise(self, promiser, attributes, metadata): + + with open(promiser, "a") as f: + f.write("test") + + return Result.KEPT + + +if __name__ == "__main__": + DummyPromiseType().start() diff --git a/tests/acceptance/15_control/01_common/top_down_evaluation_complex_policy.cf b/tests/acceptance/15_control/01_common/top_down_evaluation_complex_policy.cf new file mode 100644 index 0000000000..9377b89464 --- /dev/null +++ b/tests/acceptance/15_control/01_common/top_down_evaluation_complex_policy.cf @@ -0,0 +1,48 @@ +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { "test" }; + evaluation_order => "top_down"; +} +bundle agent test +{ + classes: + "first_class" expression => "any"; + + vars: + first_class:: + "first_var" string => "hello"; + !first_class:: + "first_var" string => "bye"; + + classes: + "second_class" expression => strcmp("$(first_var)", "hello"); + + vars: + second_class:: + "second_var" string => "foo"; + !second_class:: + "second_var" string => "bar"; + + classes: + "third_class" expression => strcmp("$(second_var)", "foo"); + + vars: + third_class:: + "third_var" string => "faz"; + !third_class:: + "thid_var" string => "baz"; + + classes: + "ok" expression => strcmp("$(third_var)", "faz"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + + DEBUG:: + "$(third_var)"; + +} diff --git a/tests/acceptance/15_control/01_common/top_down_evaluation_custom_promise_types.cf b/tests/acceptance/15_control/01_common/top_down_evaluation_custom_promise_types.cf new file mode 100644 index 0000000000..13eeec6fc4 --- /dev/null +++ b/tests/acceptance/15_control/01_common/top_down_evaluation_custom_promise_types.cf @@ -0,0 +1,59 @@ +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { "test" }; + evaluation_order => "top_down"; +} + +promise agent dummy +{ + path => "$(this.promise_dirname)/top_down/dummy_promise_type.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent init +{ + files: + "/tmp/dummyfile" + create => "true"; +} + +bundle agent test +{ + + dummy: + "/tmp/dummyfile" + smth => "hello"; + + vars: + "content" + string => readfile("/tmp/dummyfile"); + + files: + "/tmp/dummyfile" + content => "ok"; + + vars: + "content" + string => readfile("/tmp/dummyfile"); + + classes: + "ok" + expression => strcmp("$(content)", "ok"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + + DEBUG:: + "$(content)"; +} + +bundle agent cleanup +{ + files: + "/tmp/dummyfile" + delete => tidy; +} \ No newline at end of file diff --git a/tests/acceptance/15_control/01_common/top_down_evaluation_long_policy.cf b/tests/acceptance/15_control/01_common/top_down_evaluation_long_policy.cf new file mode 100644 index 0000000000..b4aab8fdbf --- /dev/null +++ b/tests/acceptance/15_control/01_common/top_down_evaluation_long_policy.cf @@ -0,0 +1,103 @@ +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { "init", "test", "cleanup" }; + evaluation_order => "top_down"; +} + +bundle agent init +{ + files: + "/tmp/example" + create => "true"; + "/tmp/otherfile" + create => "true"; +} + +bundle agent test +{ + vars: + "result" + string => "First"; + "foo" + string => "second"; + "result" + string => concat("$(result)", ", $(foo)"); + "sixth" + string => "sixth"; + + classes: + "bar" + if => "any"; + + vars: + "foo" + string => "other"; + bar:: + "baz" + string => "third"; + + "result" + string => concat("$(result)", ", $(baz)"); + + files: + "/tmp/example" + content => "fourth"; + + vars: + "content" + string => readfile("/tmp/example"); + "result" + string => concat("$(result)", ", $(content)"); + + classes: + "test" + expression => strcmp("$(content)", "fourth"); + + vars: + test:: + "result" + string => concat("$(result)", ", fifth"); + + + files: + "/tmp/otherfile" + content => "something"; + + vars: + "othercontent" + string => readfile("/tmp/otherfile"); + + files: + "/tmp/otherfile" + delete => tidy; + + classes: + "myclass" + expression => strcmp("$(othercontent)", "something"); + + vars: + myclass:: + "result" + string => concat("$(result)", ", $(sixth)"); + + classes: + "ok" + expression => strcmp("$(result)", "First, second, third, fourth, fifth, sixth"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + + DEBUG:: + "$(result)"; +} + +bundle agent cleanup +{ + files: + "/tmp/example" + delete => tidy; +} \ No newline at end of file