1 /*
2 Copyright 2021 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 #include <mod_custom.h>
26
27 #include <syntax.h>
28 #include <string_lib.h> // StringStartsWith()
29 #include <string_sequence.h> // SeqStrginFromString()
30 #include <policy.h> // Promise
31 #include <eval_context.h> // cfPS(), EvalContextVariableGet()
32 #include <attributes.h> // GetClassContextAttributes(), IsClassesBodyConstraint()
33 #include <expand.h> // ExpandScalar()
34 #include <var_expressions.h> // StringContainsUnresolved(), StringIsBareNonScalarRef()
35 #include <map.h> // Map*
36
37 static Map *custom_modules = NULL;
38
39 static const ConstraintSyntax promise_constraints[] = {
40 CONSTRAINT_SYNTAX_GLOBAL,
41 ConstraintSyntaxNewString(
42 "path", "", "Path to promise module", SYNTAX_STATUS_NORMAL),
43 ConstraintSyntaxNewString(
44 "interpreter", "", "Path to interpreter", SYNTAX_STATUS_NORMAL),
45 ConstraintSyntaxNewNull()};
46
47 const BodySyntax CUSTOM_PROMISE_BLOCK_SYNTAX =
48 BodySyntaxNew("promise", promise_constraints, NULL, SYNTAX_STATUS_NORMAL);
49
FindCustomPromiseType(const Promise * promise)50 Body *FindCustomPromiseType(const Promise *promise)
51 {
52 assert(promise != NULL);
53
54 const char *const promise_type = PromiseGetPromiseType(promise);
55 const Policy *const policy =
56 promise->parent_section->parent_bundle->parent_policy;
57 Seq *custom_promise_types = policy->custom_promise_types;
58 const size_t length = SeqLength(custom_promise_types);
59 for (size_t i = 0; i < length; ++i)
60 {
61 Body *current = SeqAt(custom_promise_types, i);
62 if (StringEqual(current->name, promise_type))
63 {
64 return current;
65 }
66 }
67 return NULL;
68 }
69
GetInterpreterAndPath(EvalContext * ctx,Body * promise_block,char ** interpreter_out,char ** path_out)70 static bool GetInterpreterAndPath(
71 EvalContext *ctx,
72 Body *promise_block,
73 char **interpreter_out,
74 char **path_out)
75 {
76 assert(promise_block != NULL);
77 assert(interpreter_out != NULL);
78 assert(path_out != NULL);
79
80 char *interpreter = NULL;
81 char *path = NULL;
82
83 const char *promise_type = promise_block->name;
84 Seq *promise_block_attributes = promise_block->conlist;
85 const size_t length = SeqLength(promise_block_attributes);
86
87 for (size_t i = 0; i < length; ++i)
88 {
89 Constraint *attribute = SeqAt(promise_block_attributes, i);
90 const char *name = attribute->lval;
91 const char *value = RvalScalarValue(attribute->rval);
92
93 if (StringEqual("interpreter", name))
94 {
95 free(interpreter);
96 interpreter = ExpandScalar(ctx, NULL, NULL, value, NULL);
97 }
98 else if (StringEqual("path", name))
99 {
100 free(path);
101 path = ExpandScalar(ctx, NULL, NULL, value, NULL);
102 }
103 else
104 {
105 debug_abort_if_reached();
106 }
107 }
108
109 if (path == NULL)
110 {
111 Log(LOG_LEVEL_ERR,
112 "Custom promise type '%s' missing path",
113 promise_type);
114 free(interpreter);
115 free(path);
116 return false;
117 }
118
119 *interpreter_out = interpreter;
120 *path_out = path;
121 return true;
122 }
123
PromiseModule_LogJson(JsonElement * object,const Promise * pp)124 static inline bool PromiseModule_LogJson(JsonElement *object, const Promise *pp)
125 {
126 const char *level_string = JsonObjectGetAsString(object, "level");
127 const char *message = JsonObjectGetAsString(object, "message");
128
129 assert(level_string != NULL && message != NULL);
130 const LogLevel level = LogLevelFromString(level_string);
131 assert(level != LOG_LEVEL_NOTHING);
132
133 if (pp != NULL)
134 {
135 /* Check if there is a log level specified for the particular promise. */
136 const char *value = PromiseGetConstraintAsRval(pp, "log_level", RVAL_TYPE_SCALAR);
137 if (value != NULL)
138 {
139 LogLevel specific = ActionAttributeLogLevelFromString(value);
140 if (specific < level)
141 {
142 /* Do not log messages that have a higher log level than the log
143 * level specified for the promise (e.g. 'info' messages when
144 * 'error' was requested for the promise). */
145 return false;
146 }
147 }
148 }
149
150 Log(level, "%s", message);
151
152 // We want to keep track of whether the module logged an error,
153 // it must log errors for not kept promises and validation errors.
154 const bool logged_error =
155 (level == LOG_LEVEL_ERR || level == LOG_LEVEL_CRIT);
156 return logged_error;
157 }
158
PromiseModule_ParseResultClasses(char * value)159 static inline JsonElement *PromiseModule_ParseResultClasses(char *value)
160 {
161 JsonElement *result_classes = JsonArrayCreate(1);
162 char *delim = strchr(value, ',');
163 while (delim != NULL)
164 {
165 *delim = '\0';
166 JsonArrayAppendString(result_classes, value);
167 value = delim + 1;
168 delim = strchr(value, ',');
169 }
170 JsonArrayAppendString(result_classes, value);
171 return result_classes;
172 }
173
PromiseModule_Receive(PromiseModule * module,const Promise * pp)174 static JsonElement *PromiseModule_Receive(PromiseModule *module, const Promise *pp)
175 {
176 assert(module != NULL);
177
178 bool line_based = !(module->json);
179
180 char *line = NULL;
181 size_t size = 0;
182 bool empty_line = false;
183 JsonElement *log_array = JsonArrayCreate(10);
184 JsonElement *response = NULL;
185
186 if (line_based)
187 {
188 response = JsonObjectCreate(10);
189 }
190
191 bool logged_error = false;
192
193 ssize_t bytes;
194 while (!empty_line
195 && ((bytes = getline(&line, &size, module->output)) > 0))
196 {
197 assert(bytes > 0);
198 assert(line != NULL);
199
200 assert(line[bytes] == '\0');
201 assert(line[bytes - 1] == '\n');
202 line[bytes - 1] = '\0';
203
204 // Log only non-empty lines:
205 if (bytes > 1)
206 {
207 Log(LOG_LEVEL_DEBUG, "Received line from module: '%s'", line);
208 }
209
210 if (line[0] == '\0')
211 {
212 empty_line = true;
213 }
214 else if (StringStartsWith(line, "log_"))
215 {
216 const char *const equal_sign = strchr(line, '=');
217 assert(equal_sign != NULL);
218 if (equal_sign == NULL)
219 {
220 Log(LOG_LEVEL_ERR,
221 "Promise module sent invalid log line: '%s'",
222 line);
223 // Skip this line but keep parsing
224 FREE_AND_NULL(line);
225 size = 0;
226 continue;
227 }
228 const char *const message = equal_sign + 1;
229 const char *const level_start = line + strlen("log_");
230 const size_t level_length = equal_sign - level_start;
231 char *const level = xstrndup(level_start, level_length);
232 assert(strlen(level) == level_length);
233
234 JsonElement *log_message = JsonObjectCreate(2);
235 JsonObjectAppendString(log_message, "level", level);
236 JsonObjectAppendString(log_message, "message", message);
237 logged_error |= PromiseModule_LogJson(log_message, pp);
238 JsonArrayAppendObject(log_array, log_message);
239
240 free(level);
241 }
242 else if (line_based)
243 {
244 const char *const equal_sign = strchr(line, '=');
245 assert(equal_sign != NULL);
246 if (equal_sign == NULL)
247 {
248 Log(LOG_LEVEL_ERR,
249 "Promise module sent invalid line: '%s'",
250 line);
251 }
252 else
253 {
254 const char *const value = equal_sign + 1;
255 const size_t key_length = equal_sign - line;
256 char *const key = xstrndup(line, key_length);
257 assert(strlen(key) == key_length);
258 if (StringEqual(key, "result_classes"))
259 {
260 char *result_classes_str = xstrdup(value);
261 JsonElement *result_classes = PromiseModule_ParseResultClasses(result_classes_str);
262 JsonObjectAppendArray(response, key, result_classes);
263 free(result_classes_str);
264 }
265 else
266 {
267 JsonObjectAppendString(response, key, value);
268 }
269 free(key);
270 }
271 }
272 else // JSON protocol:
273 {
274 assert(strlen(line) > 0);
275 assert(response == NULL); // Should be first and only line
276 const char *data = line; // JsonParse() moves this while parsing
277 JsonParseError err = JsonParse(&data, &response);
278 if (err != JSON_PARSE_OK)
279 {
280 assert(response == NULL);
281 Log(LOG_LEVEL_ERR,
282 "Promise module '%s' sent invalid JSON",
283 module->path);
284 free(line);
285 return NULL;
286 }
287 assert(response != NULL);
288 }
289
290 FREE_AND_NULL(line);
291 size = 0;
292 }
293
294 if (response == NULL)
295 {
296 // This can happen if using the JSON protocol, and the module sends
297 // nothing (newlines) or only log= lines.
298 assert(!line_based);
299 Log(LOG_LEVEL_ERR,
300 "The '%s' promise module sent an invalid/incomplete response with JSON based protocol",
301 module->path);
302 return NULL;
303 }
304
305 if (line_based)
306 {
307 JsonObjectAppendArray(response, "log", log_array);
308 log_array = NULL;
309 }
310 else
311 {
312 JsonElement *json_log_messages = JsonObjectGet(response, "log");
313
314 // Log messages inside JSON data haven't been printed yet,
315 // do it now:
316 if (json_log_messages != NULL)
317 {
318 size_t length = JsonLength(json_log_messages);
319 for (size_t i = 0; i < length; ++i)
320 {
321 logged_error |= PromiseModule_LogJson(
322 JsonArrayGet(json_log_messages, i), pp);
323 }
324 }
325
326 JsonElement *merged = NULL;
327 bool had_log_lines = (log_array != NULL && JsonLength(log_array) > 0);
328 if (json_log_messages == NULL && !had_log_lines)
329 {
330 // No log messages at all, no need to add anything to JSON
331 }
332 else if (!had_log_lines)
333 {
334 // No separate log lines before JSON data, leave JSON as is
335 }
336 else if (had_log_lines && (json_log_messages == NULL))
337 {
338 // Separate log lines, but no log messages in JSON data
339 JsonObjectAppendArray(response, "log", log_array);
340 log_array = NULL;
341 }
342 else
343 {
344 // both log messages as separate lines and in JSON, merge:
345 merged = JsonMerge(log_array, json_log_messages);
346 JsonObjectAppendArray(response, "log", merged);
347 // json_log_messages will be destroyed since we append over it
348 }
349 }
350 JsonDestroy(log_array);
351
352 assert(response != NULL);
353 JsonObjectAppendBool(response, "_logged_error", logged_error);
354 return response;
355 }
356
PromiseModule_SendMessage(PromiseModule * module,Seq * message)357 static void PromiseModule_SendMessage(PromiseModule *module, Seq *message)
358 {
359 assert(module != NULL);
360
361 const size_t length = SeqLength(message);
362 for (size_t i = 0; i < length; ++i)
363 {
364 const char *line = SeqAt(message, i);
365 const size_t line_length = strlen(line);
366 assert(line_length > 0 && memchr(line, '\n', line_length) == NULL);
367 fprintf(module->input, "%s\n", line);
368 }
369 fprintf(module->input, "\n");
370 fflush(module->input);
371 }
372
PromiseModule_ReceiveHeader(PromiseModule * module)373 static Seq *PromiseModule_ReceiveHeader(PromiseModule *module)
374 {
375 assert(module != NULL);
376
377 // Read header:
378 char *line = NULL;
379 size_t size = 0;
380 ssize_t bytes = getline(&line, &size, module->output);
381 if (bytes <= 0)
382 {
383 Log(LOG_LEVEL_ERR,
384 "Did not receive header from promise module '%s'",
385 module->path);
386 free(line);
387 return NULL;
388 }
389 if (line[bytes - 1] != '\n')
390 {
391 Log(LOG_LEVEL_ERR,
392 "Promise module '%s %s' sent an invalid header with no newline: '%s'",
393 module->interpreter,
394 module->path,
395 line);
396 free(line);
397 return NULL;
398 }
399 line[bytes - 1] = '\0';
400
401 Log(LOG_LEVEL_DEBUG, "Received header from promise module: '%s'", line);
402
403 Seq *header = SeqStringFromString(line, ' ');
404
405 FREE_AND_NULL(line);
406 size = 0;
407
408 // Read empty line:
409 bytes = getline(&line, &size, module->output);
410 if (bytes != 1 || line[0] != '\n')
411 {
412 Log(LOG_LEVEL_ERR,
413 "Promise module '%s %s' failed to send empty line after header: '%s'",
414 module->interpreter,
415 module->path,
416 line);
417 SeqDestroy(header);
418 free(line);
419 return NULL;
420 }
421
422 free(line);
423 return header;
424 }
425
426 // Internal function, use PromiseModule_Terminate instead
PromiseModule_DestroyInternal(PromiseModule * module)427 static void PromiseModule_DestroyInternal(PromiseModule *module)
428 {
429 assert(module != NULL);
430
431 free(module->path);
432 free(module->interpreter);
433
434 cf_pclose_full_duplex(&(module->fds));
435 free(module);
436 }
437
PromiseModule_Start(char * interpreter,char * path)438 static PromiseModule *PromiseModule_Start(char *interpreter, char *path)
439 {
440 assert(path != NULL);
441
442 if ((interpreter != NULL) && (access(interpreter, X_OK) != 0))
443 {
444 Log(LOG_LEVEL_ERR,
445 "Promise module interpreter '%s' is not an executable file",
446 interpreter);
447 return NULL;
448 }
449
450 if ((interpreter == NULL) && (access(path, X_OK) != 0))
451 {
452 Log(LOG_LEVEL_ERR,
453 "Promise module path '%s' is not an executable file",
454 path);
455 return NULL;
456 }
457
458 if (access(path, F_OK) != 0)
459 {
460 Log(LOG_LEVEL_ERR,
461 "Promise module '%s' does not exist",
462 path);
463 return NULL;
464 }
465
466 PromiseModule *module = xcalloc(1, sizeof(PromiseModule));
467
468 module->interpreter = interpreter;
469 module->path = path;
470
471 char command[CF_BUFSIZE];
472 if (interpreter == NULL)
473 {
474 snprintf(command, CF_BUFSIZE, "%s", path);
475 }
476 else
477 {
478 snprintf(command, CF_BUFSIZE, "%s %s", interpreter, path);
479 }
480
481 Log(LOG_LEVEL_VERBOSE, "Starting custom promise module '%s' with command '%s'",
482 path, command);
483 module->fds = cf_popen_full_duplex_streams(command, false, true);
484 module->output = module->fds.read_stream;
485 module->input = module->fds.write_stream;
486 module->message = NULL;
487
488 fprintf(module->input, "cf-agent %s v1\n\n", Version());
489 fflush(module->input);
490
491 Seq *header = PromiseModule_ReceiveHeader(module);
492
493 if (header == NULL)
494 {
495 // error logged in PromiseModule_ReceiveHeader()
496
497 /* Make sure 'path' and 'interpreter' are not free'd twice (the calling
498 * code frees them if it gets NULL). */
499 module->path = NULL;
500 module->interpreter = NULL;
501 PromiseModule_DestroyInternal(module);
502 return NULL;
503 }
504
505 assert(SeqLength(header) >= 3);
506 Seq *flags = SeqSplit(header, 3);
507 const size_t flags_length = SeqLength(flags);
508 for (size_t i = 0; i < flags_length; ++i)
509 {
510 const char *const flag = SeqAt(flags, i);
511 if (StringEqual(flag, "json_based"))
512 {
513 module->json = true;
514 }
515 else if (StringEqual(flag, "line_based"))
516 {
517 module->json = false;
518 }
519 }
520 SeqDestroy(flags);
521
522 SeqDestroy(header);
523
524 return module;
525 }
526
PromiseModule_AppendString(PromiseModule * module,const char * key,const char * value)527 static void PromiseModule_AppendString(
528 PromiseModule *module, const char *key, const char *value)
529 {
530 assert(module != NULL);
531
532 if (module->message == NULL)
533 {
534 module->message = JsonObjectCreate(10);
535 }
536 JsonObjectAppendString(module->message, key, value);
537 }
538
PromiseModule_AppendInteger(PromiseModule * module,const char * key,int64_t value)539 static void PromiseModule_AppendInteger(
540 PromiseModule *module, const char *key, int64_t value)
541 {
542 assert(module != NULL);
543
544 if (module->message == NULL)
545 {
546 module->message = JsonObjectCreate(10);
547 }
548 JsonObjectAppendInteger64(module->message, key, value);
549 }
550
PromiseModule_AppendAttribute(PromiseModule * module,const char * key,JsonElement * value)551 static void PromiseModule_AppendAttribute(
552 PromiseModule *module, const char *key, JsonElement *value)
553 {
554 assert(module != NULL);
555
556 if (module->message == NULL)
557 {
558 module->message = JsonObjectCreate(10);
559 }
560
561 JsonElement *attributes = JsonObjectGet(module->message, "attributes");
562 if (attributes == NULL)
563 {
564 attributes = JsonObjectCreate(10);
565 JsonObjectAppendObject(module->message, "attributes", attributes);
566 }
567
568 JsonObjectAppendElement(attributes, key, value);
569 }
570
PromiseModule_Send(PromiseModule * module)571 static void PromiseModule_Send(PromiseModule *module)
572 {
573 assert(module != NULL);
574
575 if (module->json)
576 {
577 Writer *w = FileWriter(module->input);
578 JsonWriteCompact(w, module->message);
579 FileWriterDetach(w);
580 DESTROY_AND_NULL(JsonDestroy, module->message);
581 fprintf(module->input, "\n\n");
582 fflush(module->input);
583 return;
584 }
585
586 Seq *message = SeqNew(10, free);
587
588 JsonIterator iter = JsonIteratorInit(module->message);
589 const char *key;
590 while ((key = JsonIteratorNextKey(&iter)) != NULL)
591 {
592 if (StringEqual("attributes", key))
593 {
594 JsonElement *attributes = JsonIteratorCurrentValue(&iter);
595 JsonIterator attr_iter = JsonIteratorInit(attributes);
596
597 const char *attr_name;
598 while ((attr_name = JsonIteratorNextKey(&attr_iter)) != NULL)
599 {
600 const char *attr_val = JsonPrimitiveGetAsString(
601 JsonIteratorCurrentValue(&attr_iter));
602 char *attr_line = NULL;
603 xasprintf(&attr_line, "attribute_%s=%s", attr_name, attr_val);
604 SeqAppend(message, attr_line);
605 }
606 }
607 else
608 {
609 const char *value =
610 JsonPrimitiveGetAsString(JsonIteratorCurrentValue(&iter));
611 char *line = NULL;
612 xasprintf(&line, "%s=%s", key, value);
613 SeqAppend(message, line);
614 }
615 }
616
617 PromiseModule_SendMessage(module, message);
618 SeqDestroy(message);
619 DESTROY_AND_NULL(JsonDestroy, module->message);
620 }
621
TryToGetContainerFromScalarRef(const EvalContext * ctx,const char * scalar,JsonElement ** out)622 static inline bool TryToGetContainerFromScalarRef(const EvalContext *ctx, const char *scalar, JsonElement **out)
623 {
624 if (StringIsBareNonScalarRef(scalar))
625 {
626 /* Resolve a potential 'data' variable reference. */
627 const size_t scalar_len = strlen(scalar);
628 char *var_ref_str = xstrndup(scalar + 2, scalar_len - 3);
629 VarRef *ref = VarRefParse(var_ref_str);
630
631 DataType type = CF_DATA_TYPE_NONE;
632 const void *val = EvalContextVariableGet(ctx, ref, &type);
633 free(var_ref_str);
634 VarRefDestroy(ref);
635
636 if ((val != NULL) && (type == CF_DATA_TYPE_CONTAINER))
637 {
638 if (out != NULL)
639 {
640 *out = JsonCopy(val);
641 }
642 return true;
643 }
644 }
645 return false;
646 }
647
PromiseModule_AppendAllAttributes(PromiseModule * module,const EvalContext * ctx,const Promise * pp)648 static void PromiseModule_AppendAllAttributes(
649 PromiseModule *module, const EvalContext *ctx, const Promise *pp)
650 {
651 assert(module != NULL);
652 assert(pp != NULL);
653
654 const size_t attributes = SeqLength(pp->conlist);
655 for (size_t i = 0; i < attributes; i++)
656 {
657 const Constraint *attribute = SeqAt(pp->conlist, i);
658 const char *const name = attribute->lval;
659 assert(!StringEqual(name, "ifvarclass")); // Not allowed by validation
660 if (IsClassesBodyConstraint(name)
661 || StringEqual(name, "if")
662 || StringEqual(name, "ifvarclass")
663 || StringEqual(name, "unless")
664 || StringEqual(name, "depends_on")
665 || StringEqual(name, "with"))
666 {
667 // Evaluated by agent and not sent to module, skip
668 continue;
669 }
670
671 if (StringEqual(attribute->lval, "log_level"))
672 {
673 /* Passed to the module as 'log_level' request field, not as an attribute. */
674 continue;
675 }
676
677 JsonElement *value = NULL;
678 if (attribute->rval.type == RVAL_TYPE_SCALAR)
679 {
680 /* Could be a '@(container)' reference. */
681 if (!TryToGetContainerFromScalarRef(ctx, RvalScalarValue(attribute->rval), &value))
682 {
683 /* Didn't resolve to a container value, let's just use the
684 * scalar value as-is. */
685 value = RvalToJson(attribute->rval);
686 }
687 }
688 if ((attribute->rval.type == RVAL_TYPE_LIST) ||
689 (attribute->rval.type == RVAL_TYPE_CONTAINER))
690 {
691 value = RvalToJson(attribute->rval);
692 }
693
694 if (value != NULL)
695 {
696 PromiseModule_AppendAttribute(module, name, value);
697 }
698 else
699 {
700 Log(LOG_LEVEL_VERBOSE,
701 "Unsupported type of the '%s' attribute (%c), cannot be sent to custom promise module",
702 name, attribute->rval.type);
703 }
704 }
705 }
706
CheckPrimitiveForUnexpandedVars(JsonElement * primitive,ARG_UNUSED void * data)707 static bool CheckPrimitiveForUnexpandedVars(JsonElement *primitive, ARG_UNUSED void *data)
708 {
709 assert(JsonGetElementType(primitive) == JSON_ELEMENT_TYPE_PRIMITIVE);
710
711 /* Stop the iteration if a variable expression is found. */
712 return (!StringContainsUnresolved(JsonPrimitiveGetAsString(primitive)));
713 }
714
CheckObjectForUnexpandedVars(JsonElement * object,ARG_UNUSED void * data)715 static bool CheckObjectForUnexpandedVars(JsonElement *object, ARG_UNUSED void *data)
716 {
717 assert(JsonGetType(object) == JSON_TYPE_OBJECT);
718
719 /* Stop the iteration if a variable expression is found among children
720 * keys. (elements inside the object are checked separately) */
721 JsonIterator iter = JsonIteratorInit(object);
722 while (JsonIteratorHasMore(&iter))
723 {
724 const char *key = JsonIteratorNextKey(&iter);
725 if (StringContainsUnresolved(key))
726 {
727 return false;
728 }
729 }
730 return true;
731 }
732
CustomPromise_IsFullyResolved(const EvalContext * ctx,const Promise * pp,bool nonscalars_allowed)733 static inline bool CustomPromise_IsFullyResolved(const EvalContext *ctx, const Promise *pp, bool nonscalars_allowed)
734 {
735 assert(pp != NULL);
736
737 if (StringContainsUnresolved(pp->promiser))
738 {
739 return false;
740 }
741 const size_t attributes = SeqLength(pp->conlist);
742 for (size_t i = 0; i < attributes; i++)
743 {
744 const Constraint *attribute = SeqAt(pp->conlist, i);
745 if (IsClassesBodyConstraint(attribute->lval))
746 {
747 /* Not passed to the modules, handled on the agent side. */
748 continue;
749 }
750 if (StringEqual(attribute->lval, "log_level"))
751 {
752 /* Passed to the module as 'log_level' request field, not as an attribute. */
753 continue;
754 }
755 if (StringEqual(attribute->lval, "unless"))
756 {
757 /* unless can actually have unresolved variables here,
758 it defaults to evaluate in case of unresolved variables,
759 to be the true opposite of if. (if would skip).*/
760 continue;
761 }
762 if ((attribute->rval.type == RVAL_TYPE_FNCALL) ||
763 (!nonscalars_allowed && (attribute->rval.type != RVAL_TYPE_SCALAR)))
764 {
765 return false;
766 }
767 if (attribute->rval.type == RVAL_TYPE_SCALAR)
768 {
769 const char *const value = RvalScalarValue(attribute->rval);
770 if (StringContainsUnresolved(value) && !TryToGetContainerFromScalarRef(ctx, value, NULL))
771 {
772 return false;
773 }
774 }
775 else if (attribute->rval.type == RVAL_TYPE_LIST)
776 {
777 assert(nonscalars_allowed);
778 for (Rlist *rl = RvalRlistValue(attribute->rval); rl != NULL; rl = rl->next)
779 {
780 assert(rl->val.type == RVAL_TYPE_SCALAR);
781 const char *const value = RvalScalarValue(rl->val);
782 if (StringContainsUnresolved(value))
783 {
784 return false;
785 }
786 }
787 }
788 else
789 {
790 assert(nonscalars_allowed);
791 assert(attribute->rval.type == RVAL_TYPE_CONTAINER);
792 JsonElement *attr_data = RvalContainerValue(attribute->rval);
793 return JsonWalk(attr_data, CheckObjectForUnexpandedVars, NULL,
794 CheckPrimitiveForUnexpandedVars, NULL);
795 }
796 }
797 return true;
798 }
799
800
HasResultAndResultIsValid(JsonElement * response)801 static inline bool HasResultAndResultIsValid(JsonElement *response)
802 {
803 const char *const result = JsonObjectGetAsString(response, "result");
804 return ((result != NULL) && StringEqual(result, "valid"));
805 }
806
LogLevelToRequestFromModule(const Promise * pp)807 static inline const char *LogLevelToRequestFromModule(const Promise *pp)
808 {
809 LogLevel log_level = LogGetGlobalLevel();
810
811 /* Check if there is a log level specified for the particular promise. */
812 const char *value = PromiseGetConstraintAsRval(pp, "log_level", RVAL_TYPE_SCALAR);
813 if (value != NULL)
814 {
815 LogLevel specific = ActionAttributeLogLevelFromString(value);
816
817 /* Promise-specific log level cannot go above the global log level
818 * (e.g. no 'info' messages for a particular promise if the global level
819 * is 'error'). */
820 log_level = MIN(log_level, specific);
821 }
822
823 // We will never request LOG_LEVEL_NOTHING or LOG_LEVEL_CRIT from the
824 // module:
825 if (log_level < LOG_LEVEL_ERR)
826 {
827 assert((log_level == LOG_LEVEL_NOTHING) || (log_level == LOG_LEVEL_CRIT));
828 return LogLevelToString(LOG_LEVEL_ERR);
829 }
830 return LogLevelToString(log_level);
831 }
832
PromiseModule_Validate(PromiseModule * module,const EvalContext * ctx,const Promise * pp)833 static bool PromiseModule_Validate(PromiseModule *module, const EvalContext *ctx, const Promise *pp)
834 {
835 assert(module != NULL);
836 assert(pp != NULL);
837
838 const char *const promise_type = PromiseGetPromiseType(pp);
839 const char *const promiser = pp->promiser;
840
841 PromiseModule_AppendString(module, "operation", "validate_promise");
842 PromiseModule_AppendString(module, "log_level", LogLevelToRequestFromModule(pp));
843 PromiseModule_AppendString(module, "promise_type", promise_type);
844 PromiseModule_AppendString(module, "promiser", promiser);
845 PromiseModule_AppendInteger(module, "line_number", pp->offset.line);
846 PromiseModule_AppendString(module, "filename", PromiseGetBundle(pp)->source_path);
847 PromiseModule_AppendAllAttributes(module, ctx, pp);
848 PromiseModule_Send(module);
849
850 // Prints errors / log messages from module:
851 JsonElement *response = PromiseModule_Receive(module, pp);
852
853 if (response == NULL)
854 {
855 // Error already printed in PromiseModule_Receive()
856 return false;
857 }
858
859 // TODO: const bool logged_error = JsonObjectGetAsBool(response, "_logged_error");
860 const bool logged_error = JsonPrimitiveGetAsBool(
861 JsonObjectGet(response, "_logged_error"));
862 const bool valid = HasResultAndResultIsValid(response);
863
864 JsonDestroy(response);
865
866 if (!valid)
867 {
868 // Detailed error messages from module should already have been printed
869 const char *const filename =
870 pp->parent_section->parent_bundle->source_path;
871 const size_t line = pp->offset.line;
872 Log(LOG_LEVEL_VERBOSE,
873 "%s promise with promiser '%s' failed validation (%s:%zu)",
874 promise_type,
875 promiser,
876 filename,
877 line);
878
879 if (!logged_error)
880 {
881 Log(LOG_LEVEL_CRIT,
882 "Bug in promise module - No error(s) logged for invalid %s promise with promiser '%s' (%s:%zu)",
883 promise_type,
884 promiser,
885 filename,
886 line);
887 }
888 }
889
890 return valid;
891 }
892
PromiseModule_Evaluate(PromiseModule * module,EvalContext * ctx,const Promise * pp)893 static PromiseResult PromiseModule_Evaluate(
894 PromiseModule *module, EvalContext *ctx, const Promise *pp)
895 {
896 assert(module != NULL);
897 assert(pp != NULL);
898
899 const char *const promise_type = PromiseGetPromiseType(pp);
900 const char *const promiser = pp->promiser;
901
902 PromiseModule_AppendString(module, "operation", "evaluate_promise");
903 PromiseModule_AppendString(
904 module, "log_level", LogLevelToRequestFromModule(pp));
905 PromiseModule_AppendString(module, "promise_type", promise_type);
906 PromiseModule_AppendString(module, "promiser", promiser);
907 PromiseModule_AppendInteger(module, "line_number", pp->offset.line);
908 PromiseModule_AppendString(module, "filename", PromiseGetBundle(pp)->source_path);
909
910 PromiseModule_AppendAllAttributes(module, ctx, pp);
911 PromiseModule_Send(module);
912
913 JsonElement *response = PromiseModule_Receive(module, pp);
914 if (response == NULL)
915 {
916 // Log from PromiseModule_Receive
917 return PROMISE_RESULT_FAIL;
918 }
919
920 JsonElement *result_classes = JsonObjectGetAsArray(response, "result_classes");
921 if (result_classes != NULL)
922 {
923 const size_t n_classes = JsonLength(result_classes);
924 for (size_t i = 0; i < n_classes; i++)
925 {
926 const char *class_name = JsonArrayGetAsString(result_classes, i);
927 assert(class_name != NULL);
928 EvalContextClassPutSoft(ctx, class_name, CONTEXT_SCOPE_BUNDLE, "source=promise-module");
929 }
930 }
931
932 PromiseResult result;
933 const char *const result_str = JsonObjectGetAsString(response, "result");
934 // TODO: const bool logged_error = JsonObjectGetAsBool(response, "_logged_error");
935 const bool logged_error = JsonPrimitiveGetAsBool(
936 JsonObjectGet(response, "_logged_error"));
937
938 /* Attributes needed for setting outcome classes etc. */
939 Attributes a = GetClassContextAttributes(ctx, pp);
940
941 if (result_str == NULL)
942 {
943 result = PROMISE_RESULT_FAIL;
944 cfPS(
945 ctx,
946 LOG_LEVEL_ERR,
947 result,
948 pp,
949 &a,
950 "Promise module did not return a result for promise evaluation (%s promise, promiser: '%s' module: '%s')",
951 promise_type,
952 promiser,
953 module->path);
954 }
955 else if (StringEqual(result_str, "kept"))
956 {
957 result = PROMISE_RESULT_NOOP;
958 cfPS(
959 ctx,
960 LOG_LEVEL_VERBOSE,
961 result,
962 pp,
963 &a,
964 "Promise with promiser '%s' was kept by promise module '%s'",
965 promiser,
966 module->path);
967 }
968 else if (StringEqual(result_str, "not_kept"))
969 {
970 result = PROMISE_RESULT_FAIL;
971 cfPS(
972 ctx,
973 LOG_LEVEL_VERBOSE,
974 result,
975 pp,
976 &a,
977 "Promise with promiser '%s' was not kept by promise module '%s'",
978 promiser,
979 module->path);
980 if (!logged_error)
981 {
982 const char *const filename =
983 pp->parent_section->parent_bundle->source_path;
984 const size_t line = pp->offset.line;
985 Log(LOG_LEVEL_CRIT,
986 "Bug in promise module - Failed to log errors for not kept %s promise with promiser '%s' (%s:%zu)",
987 promise_type,
988 promiser,
989 filename,
990 line);
991 }
992 }
993 else if (StringEqual(result_str, "repaired"))
994 {
995 result = PROMISE_RESULT_CHANGE;
996 cfPS(
997 ctx,
998 LOG_LEVEL_VERBOSE,
999 result,
1000 pp,
1001 &a,
1002 "Promise with promiser '%s' was repaired by promise module '%s'",
1003 promiser,
1004 module->path);
1005 }
1006 else if (StringEqual(result_str, "error"))
1007 {
1008 result = PROMISE_RESULT_FAIL;
1009 cfPS(
1010 ctx,
1011 LOG_LEVEL_ERR,
1012 result,
1013 pp,
1014 &a,
1015 "An unexpected error occured in promise module (%s promise, promiser: '%s' module: '%s')",
1016 promise_type,
1017 promiser,
1018 module->path);
1019 }
1020 else
1021 {
1022 result = PROMISE_RESULT_FAIL;
1023 cfPS(
1024 ctx,
1025 LOG_LEVEL_ERR,
1026 result,
1027 pp,
1028 &a,
1029 "Promise module returned unacceptable result: '%s' (%s promise, promiser: '%s' module: '%s')",
1030 result_str,
1031 promise_type,
1032 promiser,
1033 module->path);
1034 }
1035
1036 JsonDestroy(response);
1037 return result;
1038 }
1039
PromiseModule_Terminate(PromiseModule * module,const Promise * pp)1040 static void PromiseModule_Terminate(PromiseModule *module, const Promise *pp)
1041 {
1042 if (module != NULL)
1043 {
1044 PromiseModule_AppendString(module, "operation", "terminate");
1045 PromiseModule_Send(module);
1046
1047 JsonElement *response = PromiseModule_Receive(module, pp);
1048 JsonDestroy(response);
1049
1050 PromiseModule_DestroyInternal(module);
1051 }
1052 }
1053
PromiseModule_Terminate_untyped(void * data)1054 static void PromiseModule_Terminate_untyped(void *data)
1055 {
1056 PromiseModule *module = data;
1057 PromiseModule_Terminate(module, NULL);
1058 }
1059
InitializeCustomPromises()1060 bool InitializeCustomPromises()
1061 {
1062 /* module_path -> PromiseModule map */
1063 custom_modules = MapNew(StringHash_untyped,
1064 StringEqual_untyped,
1065 free,
1066 PromiseModule_Terminate_untyped);
1067 assert(custom_modules != NULL);
1068
1069 return (custom_modules != NULL);
1070 }
1071
FinalizeCustomPromises()1072 void FinalizeCustomPromises()
1073 {
1074 MapDestroy(custom_modules);
1075 }
1076
EvaluateCustomPromise(EvalContext * ctx,const Promise * pp)1077 PromiseResult EvaluateCustomPromise(EvalContext *ctx, const Promise *pp)
1078 {
1079 assert(ctx != NULL);
1080 assert(pp != NULL);
1081
1082 Body *promise_block = FindCustomPromiseType(pp);
1083 if (promise_block == NULL)
1084 {
1085 Log(LOG_LEVEL_ERR,
1086 "Undefined promise type '%s'",
1087 PromiseGetPromiseType(pp));
1088 return PROMISE_RESULT_FAIL;
1089 }
1090
1091 /* Attributes needed for setting outcome classes etc. */
1092 Attributes a = GetClassContextAttributes(ctx, pp);
1093
1094 char *interpreter = NULL;
1095 char *path = NULL;
1096
1097 bool success = GetInterpreterAndPath(ctx, promise_block, &interpreter, &path);
1098
1099 if (!success)
1100 {
1101 assert(interpreter == NULL && path == NULL);
1102 /* Details logged in GetInterpreterAndPath() */
1103 cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1104 return PROMISE_RESULT_FAIL;
1105 }
1106
1107 PromiseModule *module = MapGet(custom_modules, path);
1108 if (module == NULL)
1109 {
1110 module = PromiseModule_Start(interpreter, path);
1111 if (module != NULL)
1112 {
1113 MapInsert(custom_modules, xstrdup(path), module);
1114 }
1115 else
1116 {
1117 free(interpreter);
1118 free(path);
1119 // Error logged in PromiseModule_Start()
1120 cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1121 return PROMISE_RESULT_FAIL;
1122 }
1123 }
1124 else
1125 {
1126 if (!StringEqual(interpreter, module->interpreter))
1127 {
1128 Log(LOG_LEVEL_ERR, "Conflicting interpreter specifications for custom promise module '%s'"
1129 " (started with '%s' and '%s' requested for promise '%s' of type '%s')",
1130 path, module->interpreter, interpreter, pp->promiser, PromiseGetPromiseType(pp));
1131 free(interpreter);
1132 free(path);
1133 cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1134 return PROMISE_RESULT_FAIL;
1135 }
1136 free(interpreter);
1137 free(path);
1138 }
1139
1140 // TODO: Do validation earlier (cf-promises --full-check)
1141 bool valid = PromiseModule_Validate(module, ctx, pp);
1142
1143 if (valid)
1144 {
1145 valid = CustomPromise_IsFullyResolved(ctx, pp, module->json);
1146 if ((!valid) && (EvalContextGetPass(ctx) == CF_DONEPASSES - 1))
1147 {
1148 Log(LOG_LEVEL_ERR,
1149 "%s promise with promiser '%s' has unresolved/unexpanded variables",
1150 PromiseGetPromiseType(pp),
1151 pp->promiser);
1152 }
1153 }
1154
1155 PromiseResult result;
1156 if (valid)
1157 {
1158 result = PromiseModule_Evaluate(module, ctx, pp);
1159 }
1160 else
1161 {
1162 // PromiseModule_Validate() already printed an error
1163 Log(LOG_LEVEL_VERBOSE,
1164 "%s promise with promiser '%s' will be skipped because it failed validation",
1165 PromiseGetPromiseType(pp),
1166 pp->promiser);
1167 cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1168 result = PROMISE_RESULT_FAIL; // TODO: Investigate if DENIED is more
1169 // appropriate
1170 }
1171
1172 return result;
1173 }
1174