1 /*
2 Copyright 2020 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 <verify_vars.h>
26
27 #include <actuator.h>
28 #include <attributes.h>
29 #include <regex.h> /* CompileRegex,StringMatchFullWithPrecompiledRegex */
30 #include <buffer.h>
31 #include <misc_lib.h>
32 #include <fncall.h>
33 #include <rlist.h>
34 #include <conversion.h>
35 #include <expand.h>
36 #include <scope.h>
37 #include <promises.h>
38 #include <vars.h>
39 #include <matching.h>
40 #include <syntax.h>
41 #include <audit.h>
42 #include <string_lib.h>
43 #include <cleanup.h>
44
45 typedef struct
46 {
47 bool should_converge;
48 bool ok_redefine;
49 bool drop_undefined;
50 Constraint *cp_save; // e.g. string => "foo"
51 } ConvergeVariableOptions;
52
53
54 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp);
55 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level);
56 static bool CompareRval(const void *rval1_item, RvalType rval1_type, const void *rval2_item, RvalType rval2_type);
57
IsLegalVariableName(EvalContext * ctx,const Promise * pp)58 static bool IsLegalVariableName(EvalContext *ctx, const Promise *pp)
59 {
60 const char *var_name = pp->promiser;
61
62 /* TODO: remove at some point (global, leaked), but for now
63 * this offers an attractive speedup. */
64 static pcre *rx = NULL;
65 if (!rx)
66 {
67 /* \200-\377 is there for multibyte unicode characters */
68 rx = CompileRegex("[a-zA-Z0-9_\200-\377.]+(\\[.+\\])*"); /* Known leak, see TODO. */
69 }
70
71 if (!StringMatchFullWithPrecompiledRegex(rx, var_name))
72 {
73 return false;
74 }
75
76 char *bracket = strchr(var_name, '[');
77 char *dot = strchr(var_name, '.');
78 /* we only care about variable name prefix, not dots inside array keys */
79 if ((dot != NULL) && ((bracket == NULL) || (dot < bracket)))
80 {
81 /* detect and prevent remote bundle variable injection (CFE-1915) */
82 char *prefix = xstrndup(var_name, dot - var_name);
83 const Bundle *cur_bundle = PromiseGetBundle(pp);
84
85 if (!StringEqual(prefix, cur_bundle->name))
86 {
87 Log(LOG_LEVEL_VERBOSE,
88 "Variable '%s' may be attempted to be injected into a remote bundle",
89 var_name);
90 if (StringSetContains(EvalContextGetBundleNames(ctx), prefix))
91 {
92 Log(LOG_LEVEL_ERR, "Remote bundle variable injection detected!");
93 free(prefix);
94 return false;
95 }
96 /* the conflicting bundle may be defined later, we need to remember
97 * this promise as potentially dangerous */
98 EvalContextPushRemoteVarPromise(ctx, prefix, pp->org_pp);
99 }
100 free(prefix);
101 }
102
103 return true;
104 }
105
106 // TODO why not printing that new definition is skipped?
107 // TODO what with ifdefined?
108
VerifyVarPromise(EvalContext * ctx,const Promise * pp,ARG_UNUSED void * param)109 PromiseResult VerifyVarPromise(EvalContext *ctx, const Promise *pp,
110 ARG_UNUSED void *param)
111 {
112 ConvergeVariableOptions opts = CollectConvergeVariableOptions(ctx, pp);
113
114 Log(LOG_LEVEL_DEBUG, "Evaluating vars promise: %s", pp->promiser);
115 LogDebug(LOG_MOD_VARS,
116 "ok_redefine=%d, drop_undefined=%d, should_converge=%d",
117 opts.ok_redefine, opts.drop_undefined, opts.should_converge);
118
119 if (!opts.should_converge)
120 {
121 LogDebug(LOG_MOD_VARS,
122 "Skipping vars promise because should_converge=false");
123 return PROMISE_RESULT_NOOP;
124 }
125
126 // opts.drop_undefined = true; /* always remove @{unresolved_list} */
127
128 Attributes a = ZeroAttributes;
129 // More consideration needs to be given to using these
130 //a.transaction = GetTransactionConstraints(pp);
131
132 /* Warn if promise locking was used with a promise that doesn't support it
133 * (which applies to all of 'vars', 'meta' and 'defaults' promises handled
134 * by this code).
135 * (Only do this in the first pass in cf-promises, we don't have to repeat
136 * the warning over and over.) */
137 if (EvalContextGetPass(ctx) == 0 && THIS_AGENT_TYPE == AGENT_TYPE_COMMON)
138 {
139 int ifelapsed = PromiseGetConstraintAsInt(ctx, "ifelapsed", pp);
140 if (ifelapsed != CF_NOINT)
141 {
142 Log(LOG_LEVEL_WARNING,
143 "ifelapsed attribute specified in action body for %s promise '%s',"
144 " but %s promises do not support promise locking",
145 pp->parent_section->promise_type, pp->promiser,
146 pp->parent_section->promise_type);
147 }
148 int expireafter = PromiseGetConstraintAsInt(ctx, "expireafter", pp);
149 if (expireafter != CF_NOINT)
150 {
151 Log(LOG_LEVEL_WARNING,
152 "expireafter attribute specified in action body for %s promise '%s',"
153 " but %s promises do not support promise locking",
154 pp->parent_section->promise_type, pp->promiser,
155 pp->parent_section->promise_type);
156 }
157 }
158
159 a.classes = GetClassDefinitionConstraints(ctx, pp);
160
161 VarRef *ref = VarRefParseFromBundle(pp->promiser, PromiseGetBundle(pp));
162 if (strcmp("meta", pp->parent_section->promise_type) == 0)
163 {
164 VarRefSetMeta(ref, true);
165 }
166
167 DataType existing_value_type = CF_DATA_TYPE_NONE;
168 const void *existing_value;
169 if (IsExpandable(pp->promiser))
170 {
171 existing_value = NULL;
172 }
173 else
174 {
175 existing_value = EvalContextVariableGet(ctx, ref, &existing_value_type);
176 }
177
178 Rval rval = opts.cp_save->rval;
179 PromiseResult result;
180
181 if (rval.item != NULL || rval.type == RVAL_TYPE_LIST)
182 {
183 DataType data_type = DataTypeFromString(opts.cp_save->lval);
184
185 if (opts.cp_save->rval.type == RVAL_TYPE_FNCALL)
186 {
187 FnCall *fp = RvalFnCallValue(rval);
188 const FnCallType *fn = FnCallTypeGet(fp->name);
189 if (!fn)
190 {
191 assert(false && "Canary: should have been caught before this point");
192 FatalError(ctx, "While setting variable '%s' in bundle '%s', unknown function '%s'",
193 pp->promiser, PromiseGetBundle(pp)->name, fp->name);
194 }
195
196 if (fn->dtype != DataTypeFromString(opts.cp_save->lval))
197 {
198 FatalError(ctx, "While setting variable '%s' in bundle '%s', variable declared type '%s' but function '%s' returns type '%s'",
199 pp->promiser, PromiseGetBundle(pp)->name, opts.cp_save->lval,
200 fp->name, DataTypeToString(fn->dtype));
201 }
202
203 if (existing_value_type != CF_DATA_TYPE_NONE)
204 {
205 // Already did this
206 VarRefDestroy(ref);
207 return PROMISE_RESULT_NOOP;
208 }
209
210 FnCallResult res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), fp, pp);
211
212 if (res.status == FNCALL_FAILURE)
213 {
214 /* We do not assign variables to failed fn calls */
215 if (EvalContextGetPass(ctx) == CF_DONEPASSES-1) {
216 // If we still fail at last pass, make a log
217 Log(LOG_LEVEL_VERBOSE, "While setting variable '%s' in bundle '%s', function '%s' failed - skipping",
218 pp->promiser, PromiseGetBundle(pp)->name, fp->name);
219 }
220 RvalDestroy(res.rval);
221 VarRefDestroy(ref);
222 return PROMISE_RESULT_NOOP;
223 }
224 else
225 {
226 rval = res.rval;
227 }
228 }
229 else
230 {
231 Buffer *conv = BufferNew();
232 bool malformed = false, misprint = false;
233
234 if (strcmp(opts.cp_save->lval, "int") == 0)
235 {
236 long int asint = IntFromString(opts.cp_save->rval.item);
237 if (asint == CF_NOINT)
238 {
239 malformed = true;
240 }
241 else if (0 > BufferPrintf(conv, "%ld", asint))
242 {
243 misprint = true;
244 }
245 else
246 {
247 rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
248 }
249 }
250 else if (strcmp(opts.cp_save->lval, "real") == 0)
251 {
252 double real_value;
253 if (!DoubleFromString(opts.cp_save->rval.item, &real_value))
254 {
255 malformed = true;
256 }
257 else if (0 > BufferPrintf(conv, "%lf", real_value))
258 {
259 misprint = true;
260 }
261 else
262 {
263 rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
264 }
265 }
266 else
267 {
268 rval = RvalCopy(opts.cp_save->rval);
269 }
270 BufferDestroy(conv);
271
272 if (malformed)
273 {
274 /* Arises when opts->cp_save->rval.item isn't yet expanded. */
275 /* Has already been logged by *FromString */
276 VarRefDestroy(ref);
277 return PROMISE_RESULT_FAIL;
278 }
279 else if (misprint)
280 {
281 /* Even though no problems with memory allocation can
282 * get here, there might be other problems. */
283 UnexpectedError("Problems writing to buffer");
284 VarRefDestroy(ref);
285 return PROMISE_RESULT_FAIL;
286 }
287 else if (rval.type == RVAL_TYPE_LIST)
288 {
289 Rlist *rval_list = RvalRlistValue(rval);
290 RlistFlatten(ctx, &rval_list);
291 rval.item = rval_list;
292 }
293 }
294
295 if (Epimenides(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, pp->promiser, rval, 0))
296 {
297 Log(LOG_LEVEL_ERR, "Variable '%s' contains itself indirectly - an unkeepable promise", pp->promiser);
298 DoCleanupAndExit(EXIT_FAILURE);
299 }
300 else
301 {
302 /* See if the variable needs recursively expanding again */
303
304 Rval returnval = EvaluateFinalRval(ctx, PromiseGetPolicy(pp), ref->ns, ref->scope, rval, true, pp);
305
306 RvalDestroy(rval);
307
308 // freed before function exit
309 rval = returnval;
310 }
311
312 /* If variable did resolve but we're not allowed to modify it. */
313 /* ok_redefine: only on second iteration, else we ignore broken promises. TODO wat? */
314 if (existing_value_type != CF_DATA_TYPE_NONE &&
315 !opts.ok_redefine)
316 {
317 if (!CompareRval(existing_value, DataTypeToRvalType(existing_value_type),
318 rval.item, rval.type))
319 {
320 switch (rval.type)
321 {
322 /* TODO redefinition shouldn't be mentioned. Maybe handle like normal variable definition/ */
323 case RVAL_TYPE_SCALAR:
324 Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant scalar '%s': from '%s' to '%s'",
325 pp->promiser, (const char *)existing_value, RvalScalarValue(rval));
326 PromiseRef(LOG_LEVEL_VERBOSE, pp);
327 break;
328
329 case RVAL_TYPE_LIST:
330 {
331 Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant list '%s'", pp->promiser);
332 Writer *w = StringWriter();
333 RlistWrite(w, existing_value);
334 char *oldstr = StringWriterClose(w);
335 Log(LOG_LEVEL_DEBUG, "Old value: %s", oldstr);
336 free(oldstr);
337
338 w = StringWriter();
339 RlistWrite(w, rval.item);
340 char *newstr = StringWriterClose(w);
341 Log(LOG_LEVEL_DEBUG, "Skipped new value: %s", newstr);
342 free(newstr);
343
344 PromiseRef(LOG_LEVEL_VERBOSE, pp);
345 }
346 break;
347
348 case RVAL_TYPE_CONTAINER:
349 case RVAL_TYPE_FNCALL:
350 case RVAL_TYPE_NOPROMISEE:
351 break;
352 }
353 }
354
355 RvalDestroy(rval);
356 VarRefDestroy(ref);
357 return PROMISE_RESULT_NOOP;
358 }
359
360 if (IsCf3VarString(pp->promiser))
361 {
362 // Unexpanded variables, we don't do anything with
363 RvalDestroy(rval);
364 VarRefDestroy(ref);
365 return PROMISE_RESULT_NOOP;
366 }
367
368 if (!IsLegalVariableName(ctx, pp))
369 {
370 Log(LOG_LEVEL_ERR, "Variable identifier '%s' is not legal", pp->promiser);
371 PromiseRef(LOG_LEVEL_ERR, pp);
372 RvalDestroy(rval);
373 VarRefDestroy(ref);
374 return PROMISE_RESULT_NOOP;
375 }
376
377 if (rval.type == RVAL_TYPE_LIST)
378 {
379 if (opts.drop_undefined)
380 {
381 Rlist *stripped = RvalRlistValue(rval);
382 Rlist *entry = stripped;
383 while (entry)
384 {
385 Rlist *delete_me = NULL;
386 if (IsNakedVar(RlistScalarValue(entry), '@'))
387 {
388 delete_me = entry;
389 }
390 entry = entry->next;
391 RlistDestroyEntry(&stripped, delete_me);
392 }
393 rval.item = stripped;
394 }
395
396 for (const Rlist *rp = RvalRlistValue(rval); rp; rp = rp->next)
397 {
398 if (rp->val.type != RVAL_TYPE_SCALAR)
399 {
400 // Cannot assign variable because value is a list containing a non-scalar item
401 VarRefDestroy(ref);
402 RvalDestroy(rval);
403 return PROMISE_RESULT_NOOP;
404 }
405 }
406 }
407
408 if (ref->num_indices > 0)
409 {
410 if (data_type == CF_DATA_TYPE_CONTAINER)
411 {
412 char *lval_str = VarRefToString(ref, true);
413 Log(LOG_LEVEL_ERR, "Cannot assign a container to an indexed variable name '%s'. Should be assigned to '%s' instead",
414 lval_str, ref->lval);
415 free(lval_str);
416 VarRefDestroy(ref);
417 RvalDestroy(rval);
418 return PROMISE_RESULT_NOOP;
419 }
420 else
421 {
422 DataType existing_type;
423 VarRef *base_ref = VarRefCopyIndexless(ref);
424 if (EvalContextVariableGet(ctx, ref, &existing_type) && existing_type == CF_DATA_TYPE_CONTAINER)
425 {
426 char *lval_str = VarRefToString(ref, true);
427 char *base_ref_str = VarRefToString(base_ref, true);
428 Log(LOG_LEVEL_ERR, "Cannot assign value to indexed variable name '%s', because a container is already assigned to the base name '%s'",
429 lval_str, base_ref_str);
430 free(lval_str);
431 free(base_ref_str);
432 VarRefDestroy(base_ref);
433 VarRefDestroy(ref);
434 RvalDestroy(rval);
435 return PROMISE_RESULT_NOOP;
436 }
437 VarRefDestroy(base_ref);
438 }
439 }
440
441
442 DataType required_datatype = DataTypeFromString(opts.cp_save->lval);
443 if (rval.type != DataTypeToRvalType(required_datatype))
444 {
445 char *ref_str = VarRefToString(ref, true);
446 char *value_str = RvalToString(rval);
447 Log(LOG_LEVEL_ERR, "Variable '%s' expected a variable of type '%s', but was given incompatible value '%s'",
448 ref_str, DataTypeToString(required_datatype), value_str);
449 PromiseRef(LOG_LEVEL_ERR, pp);
450
451 free(ref_str);
452 free(value_str);
453 VarRefDestroy(ref);
454 RvalDestroy(rval);
455 return PROMISE_RESULT_FAIL;
456 }
457
458 /* WRITE THE VARIABLE AT LAST. */
459 bool success = EvalContextVariablePut(ctx, ref, rval.item, required_datatype, "source=promise");
460
461 if (!success)
462 {
463 Log(LOG_LEVEL_VERBOSE,
464 "Unable to converge %s.%s value (possibly empty or infinite regression)",
465 ref->scope, pp->promiser);
466 PromiseRef(LOG_LEVEL_VERBOSE, pp);
467
468 VarRefDestroy(ref);
469 RvalDestroy(rval);
470 return PROMISE_RESULT_FAIL;
471 }
472
473 Rlist *promise_meta = PromiseGetConstraintAsList(ctx, "meta", pp);
474 if (promise_meta)
475 {
476 StringSet *class_meta = EvalContextVariableTags(ctx, ref);
477 Buffer *print;
478 for (const Rlist *rp = promise_meta; rp; rp = rp->next)
479 {
480 StringSetAdd(class_meta, xstrdup(RlistScalarValue(rp)));
481 print = StringSetToBuffer(class_meta, ',');
482 Log(LOG_LEVEL_DEBUG,
483 "Added tag %s to class %s, tags now [%s]",
484 RlistScalarValue(rp), pp->promiser, BufferData(print));
485 BufferDestroy(print);
486 }
487 }
488
489 result = PROMISE_RESULT_NOOP;
490 }
491 else
492 {
493 Log(LOG_LEVEL_ERR, "Variable %s has no promised value", pp->promiser);
494 Log(LOG_LEVEL_ERR, "Rule from %s at/before line %zu", PromiseGetBundle(pp)->source_path, opts.cp_save->offset.line);
495 result = PROMISE_RESULT_FAIL;
496 }
497
498 /*
499 * FIXME: Variable promise are exempt from normal evaluation logic still, so
500 * they are not pushed to evaluation stack before being evaluated. Due to
501 * this reason, we cannot call cfPS here to set classes, as it will error
502 * out with ProgrammingError.
503 *
504 * In order to support 'classes' body for variables as well, we call
505 * ClassAuditLog explicitly.
506 */
507 ClassAuditLog(ctx, pp, &a, result);
508
509 VarRefDestroy(ref);
510 RvalDestroy(rval);
511
512 return result;
513 }
514
CompareRval(const void * rval1_item,RvalType rval1_type,const void * rval2_item,RvalType rval2_type)515 static bool CompareRval(const void *rval1_item, RvalType rval1_type,
516 const void *rval2_item, RvalType rval2_type)
517 {
518 if (rval1_type != rval2_type)
519 {
520 return false;
521 }
522
523 switch (rval1_type)
524 {
525 case RVAL_TYPE_SCALAR:
526
527 if (IsCf3VarString(rval1_item) || IsCf3VarString(rval2_item))
528 {
529 return false; // inconclusive
530 }
531
532 if (strcmp(rval1_item, rval2_item) != 0)
533 {
534 return false;
535 }
536
537 break;
538
539 case RVAL_TYPE_LIST:
540 return RlistEqual(rval1_item, rval2_item);
541
542 case RVAL_TYPE_FNCALL:
543 return false;
544
545 default:
546 return false;
547 }
548
549 return true;
550 }
551
Epimenides(EvalContext * ctx,const char * ns,const char * scope,const char * var,Rval rval,int level)552 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level)
553 {
554 switch (rval.type)
555 {
556 case RVAL_TYPE_SCALAR:
557
558 if (StringContainsVar(RvalScalarValue(rval), var))
559 {
560 Log(LOG_LEVEL_ERR, "Scalar variable '%s' contains itself (non-convergent) '%s'", var, RvalScalarValue(rval));
561 return true;
562 }
563
564 if (IsCf3VarString(RvalScalarValue(rval)))
565 {
566 Buffer *exp = BufferNew();
567 ExpandScalar(ctx, ns, scope, RvalScalarValue(rval), exp);
568
569 if (strcmp(BufferData(exp), RvalScalarValue(rval)) == 0)
570 {
571 BufferDestroy(exp);
572 return false;
573 }
574
575 if (level > 3)
576 {
577 BufferDestroy(exp);
578 return false;
579 }
580
581 if (Epimenides(ctx, ns, scope, var, (Rval) { BufferGet(exp), RVAL_TYPE_SCALAR}, level + 1))
582 {
583 BufferDestroy(exp);
584 return true;
585 }
586
587 BufferDestroy(exp);
588 }
589
590 break;
591
592 case RVAL_TYPE_LIST:
593 for (const Rlist *rp = RvalRlistValue(rval); rp != NULL; rp = rp->next)
594 {
595 if (Epimenides(ctx, ns, scope, var, rp->val, level))
596 {
597 return true;
598 }
599 }
600 break;
601
602 case RVAL_TYPE_CONTAINER:
603 case RVAL_TYPE_FNCALL:
604 case RVAL_TYPE_NOPROMISEE:
605 return false;
606 }
607
608 return false;
609 }
610
611 /**
612 * @brief Collects variable constraints controlling how the promise should be converged
613 */
CollectConvergeVariableOptions(EvalContext * ctx,const Promise * pp)614 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp)
615 {
616 ConvergeVariableOptions opts;
617 opts.drop_undefined = false;
618 opts.cp_save = NULL; /* main variable value */
619 /* By default allow variable redefinition, use "policy" constraint
620 * to override. */
621 opts.ok_redefine = true;
622 /* Main return value: becomes true at the end of the function. */
623 opts.should_converge = false;
624
625 if (!IsDefinedClass(ctx, pp->classes))
626 {
627 return opts;
628 }
629
630 int num_values = 0;
631 for (size_t i = 0; i < SeqLength(pp->conlist); i++)
632 {
633 Constraint *cp = SeqAt(pp->conlist, i);
634
635 if (strcmp(cp->lval, "comment") == 0)
636 {
637 // Comments don't affect convergence
638 // Unclear why this is in the constraint list in the first place?
639 continue;
640 }
641 else if (cp->rval.item == NULL && cp->rval.type != RVAL_TYPE_LIST)
642 {
643 // No right value, considered empty
644 continue;
645 }
646 else if (strcmp(cp->lval, "ifvarclass") == 0 ||
647 strcmp(cp->lval, "if") == 0)
648 {
649 switch (cp->rval.type)
650 {
651 case RVAL_TYPE_SCALAR:
652 if (!IsDefinedClass(ctx, cp->rval.item))
653 {
654 return opts;
655 }
656
657 break;
658
659 case RVAL_TYPE_FNCALL:
660 {
661 bool excluded = false;
662
663 /* eval it: e.g. ifvarclass => not("a_class") */
664
665 Rval res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), cp->rval.item, pp).rval;
666
667 /* Don't continue unless function was evaluated properly */
668 if (res.type != RVAL_TYPE_SCALAR)
669 {
670 RvalDestroy(res);
671 return opts;
672 }
673
674 excluded = !IsDefinedClass(ctx, res.item);
675
676 RvalDestroy(res);
677
678 if (excluded)
679 {
680 return opts;
681 }
682 }
683 break;
684
685 default:
686 Log(LOG_LEVEL_ERR, "Invalid if/ifvarclass type '%c': should be string or function", cp->rval.type);
687 }
688 }
689 else if (strcmp(cp->lval, "policy") == 0)
690 {
691 if (strcmp(cp->rval.item, "ifdefined") == 0)
692 {
693 opts.drop_undefined = true;
694 }
695 else if (strcmp(cp->rval.item, "constant") == 0)
696 {
697 opts.ok_redefine = false;
698 }
699 }
700 else if (DataTypeFromString(cp->lval) != CF_DATA_TYPE_NONE)
701 {
702 num_values++;
703 opts.cp_save = cp;
704 }
705 }
706
707 if (opts.cp_save == NULL)
708 {
709 Log(LOG_LEVEL_WARNING, "Incomplete vars promise: %s",
710 pp->promiser);
711 PromiseRef(LOG_LEVEL_INFO, pp);
712 return opts;
713 }
714
715 if (num_values > 2)
716 {
717 Log(LOG_LEVEL_ERR,
718 "Variable '%s' breaks its own promise with multiple (%d) values",
719 pp->promiser, num_values);
720 PromiseRef(LOG_LEVEL_ERR, pp);
721 return opts;
722 }
723
724 /* All constraints look OK, and classes are defined. Move forward with
725 * this promise. */
726 opts.should_converge = true;
727
728 return opts;
729 }
730