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 switch (rp->val.type)
399 {
400 case RVAL_TYPE_SCALAR:
401 break;
402
403 default:
404 // Cannot assign variable because value is a list containing a non-scalar item
405 VarRefDestroy(ref);
406 RvalDestroy(rval);
407 return PROMISE_RESULT_NOOP;
408 }
409 }
410 }
411
412 if (ref->num_indices > 0)
413 {
414 if (data_type == CF_DATA_TYPE_CONTAINER)
415 {
416 char *lval_str = VarRefToString(ref, true);
417 Log(LOG_LEVEL_ERR, "Cannot assign a container to an indexed variable name '%s'. Should be assigned to '%s' instead",
418 lval_str, ref->lval);
419 free(lval_str);
420 VarRefDestroy(ref);
421 RvalDestroy(rval);
422 return PROMISE_RESULT_NOOP;
423 }
424 else
425 {
426 DataType existing_type;
427 VarRef *base_ref = VarRefCopyIndexless(ref);
428 if (EvalContextVariableGet(ctx, ref, &existing_type) && existing_type == CF_DATA_TYPE_CONTAINER)
429 {
430 char *lval_str = VarRefToString(ref, true);
431 char *base_ref_str = VarRefToString(base_ref, true);
432 Log(LOG_LEVEL_ERR, "Cannot assign value to indexed variable name '%s', because a container is already assigned to the base name '%s'",
433 lval_str, base_ref_str);
434 free(lval_str);
435 free(base_ref_str);
436 VarRefDestroy(base_ref);
437 VarRefDestroy(ref);
438 RvalDestroy(rval);
439 return PROMISE_RESULT_NOOP;
440 }
441 VarRefDestroy(base_ref);
442 }
443 }
444
445
446 DataType required_datatype = DataTypeFromString(opts.cp_save->lval);
447 if (rval.type != DataTypeToRvalType(required_datatype))
448 {
449 char *ref_str = VarRefToString(ref, true);
450 char *value_str = RvalToString(rval);
451 Log(LOG_LEVEL_ERR, "Variable '%s' expected a variable of type '%s', but was given incompatible value '%s'",
452 ref_str, DataTypeToString(required_datatype), value_str);
453 PromiseRef(LOG_LEVEL_ERR, pp);
454
455 free(ref_str);
456 free(value_str);
457 VarRefDestroy(ref);
458 RvalDestroy(rval);
459 return PROMISE_RESULT_FAIL;
460 }
461
462 /* WRITE THE VARIABLE AT LAST. */
463 bool success = EvalContextVariablePut(ctx, ref, rval.item, required_datatype, "source=promise");
464
465 if (!success)
466 {
467 Log(LOG_LEVEL_VERBOSE,
468 "Unable to converge %s.%s value (possibly empty or infinite regression)",
469 ref->scope, pp->promiser);
470 PromiseRef(LOG_LEVEL_VERBOSE, pp);
471
472 VarRefDestroy(ref);
473 RvalDestroy(rval);
474 return PROMISE_RESULT_FAIL;
475 }
476
477 Rlist *promise_meta = PromiseGetConstraintAsList(ctx, "meta", pp);
478 if (promise_meta)
479 {
480 StringSet *class_meta = EvalContextVariableTags(ctx, ref);
481 Buffer *print;
482 for (const Rlist *rp = promise_meta; rp; rp = rp->next)
483 {
484 StringSetAdd(class_meta, xstrdup(RlistScalarValue(rp)));
485 print = StringSetToBuffer(class_meta, ',');
486 Log(LOG_LEVEL_DEBUG,
487 "Added tag %s to class %s, tags now [%s]",
488 RlistScalarValue(rp), pp->promiser, BufferData(print));
489 BufferDestroy(print);
490 }
491 }
492
493 result = PROMISE_RESULT_NOOP;
494 }
495 else
496 {
497 Log(LOG_LEVEL_ERR, "Variable %s has no promised value", pp->promiser);
498 Log(LOG_LEVEL_ERR, "Rule from %s at/before line %zu", PromiseGetBundle(pp)->source_path, opts.cp_save->offset.line);
499 result = PROMISE_RESULT_FAIL;
500 }
501
502 /*
503 * FIXME: Variable promise are exempt from normal evaluation logic still, so
504 * they are not pushed to evaluation stack before being evaluated. Due to
505 * this reason, we cannot call cfPS here to set classes, as it will error
506 * out with ProgrammingError.
507 *
508 * In order to support 'classes' body for variables as well, we call
509 * ClassAuditLog explicitly.
510 */
511 ClassAuditLog(ctx, pp, &a, result);
512
513 VarRefDestroy(ref);
514 RvalDestroy(rval);
515
516 return result;
517 }
518
CompareRval(const void * rval1_item,RvalType rval1_type,const void * rval2_item,RvalType rval2_type)519 static bool CompareRval(const void *rval1_item, RvalType rval1_type,
520 const void *rval2_item, RvalType rval2_type)
521 {
522 if (rval1_type != rval2_type)
523 {
524 return false;
525 }
526
527 switch (rval1_type)
528 {
529 case RVAL_TYPE_SCALAR:
530
531 if (IsCf3VarString(rval1_item) || IsCf3VarString(rval2_item))
532 {
533 return false; // inconclusive
534 }
535
536 if (strcmp(rval1_item, rval2_item) != 0)
537 {
538 return false;
539 }
540
541 break;
542
543 case RVAL_TYPE_LIST:
544 return RlistEqual(rval1_item, rval2_item);
545
546 case RVAL_TYPE_FNCALL:
547 return false;
548
549 default:
550 return false;
551 }
552
553 return true;
554 }
555
Epimenides(EvalContext * ctx,const char * ns,const char * scope,const char * var,Rval rval,int level)556 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level)
557 {
558 switch (rval.type)
559 {
560 case RVAL_TYPE_SCALAR:
561
562 if (StringContainsVar(RvalScalarValue(rval), var))
563 {
564 Log(LOG_LEVEL_ERR, "Scalar variable '%s' contains itself (non-convergent) '%s'", var, RvalScalarValue(rval));
565 return true;
566 }
567
568 if (IsCf3VarString(RvalScalarValue(rval)))
569 {
570 Buffer *exp = BufferNew();
571 ExpandScalar(ctx, ns, scope, RvalScalarValue(rval), exp);
572
573 if (strcmp(BufferData(exp), RvalScalarValue(rval)) == 0)
574 {
575 BufferDestroy(exp);
576 return false;
577 }
578
579 if (level > 3)
580 {
581 BufferDestroy(exp);
582 return false;
583 }
584
585 if (Epimenides(ctx, ns, scope, var, (Rval) { BufferGet(exp), RVAL_TYPE_SCALAR}, level + 1))
586 {
587 BufferDestroy(exp);
588 return true;
589 }
590
591 BufferDestroy(exp);
592 }
593
594 break;
595
596 case RVAL_TYPE_LIST:
597 for (const Rlist *rp = RvalRlistValue(rval); rp != NULL; rp = rp->next)
598 {
599 if (Epimenides(ctx, ns, scope, var, rp->val, level))
600 {
601 return true;
602 }
603 }
604 break;
605
606 case RVAL_TYPE_CONTAINER:
607 case RVAL_TYPE_FNCALL:
608 case RVAL_TYPE_NOPROMISEE:
609 return false;
610 }
611
612 return false;
613 }
614
615 /**
616 * @brief Collects variable constraints controlling how the promise should be converged
617 */
CollectConvergeVariableOptions(EvalContext * ctx,const Promise * pp)618 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp)
619 {
620 ConvergeVariableOptions opts;
621 opts.drop_undefined = false;
622 opts.cp_save = NULL; /* main variable value */
623 /* By default allow variable redefinition, use "policy" constraint
624 * to override. */
625 opts.ok_redefine = true;
626 /* Main return value: becomes true at the end of the function. */
627 opts.should_converge = false;
628
629 if (!IsDefinedClass(ctx, pp->classes))
630 {
631 return opts;
632 }
633
634 int num_values = 0;
635 for (size_t i = 0; i < SeqLength(pp->conlist); i++)
636 {
637 Constraint *cp = SeqAt(pp->conlist, i);
638
639 if (strcmp(cp->lval, "comment") == 0)
640 {
641 // Comments don't affect convergence
642 // Unclear why this is in the constraint list in the first place?
643 continue;
644 }
645 else if (cp->rval.item == NULL && cp->rval.type != RVAL_TYPE_LIST)
646 {
647 // No right value, considered empty
648 continue;
649 }
650 else if (strcmp(cp->lval, "ifvarclass") == 0 ||
651 strcmp(cp->lval, "if") == 0)
652 {
653 switch (cp->rval.type)
654 {
655 case RVAL_TYPE_SCALAR:
656 if (!IsDefinedClass(ctx, cp->rval.item))
657 {
658 return opts;
659 }
660
661 break;
662
663 case RVAL_TYPE_FNCALL:
664 {
665 bool excluded = false;
666
667 /* eval it: e.g. ifvarclass => not("a_class") */
668
669 Rval res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), cp->rval.item, pp).rval;
670
671 /* Don't continue unless function was evaluated properly */
672 if (res.type != RVAL_TYPE_SCALAR)
673 {
674 RvalDestroy(res);
675 return opts;
676 }
677
678 excluded = !IsDefinedClass(ctx, res.item);
679
680 RvalDestroy(res);
681
682 if (excluded)
683 {
684 return opts;
685 }
686 }
687 break;
688
689 default:
690 Log(LOG_LEVEL_ERR, "Invalid if/ifvarclass type '%c': should be string or function", cp->rval.type);
691 }
692 }
693 else if (strcmp(cp->lval, "policy") == 0)
694 {
695 if (strcmp(cp->rval.item, "ifdefined") == 0)
696 {
697 opts.drop_undefined = true;
698 }
699 else if (strcmp(cp->rval.item, "constant") == 0)
700 {
701 opts.ok_redefine = false;
702 }
703 }
704 else if (DataTypeFromString(cp->lval) != CF_DATA_TYPE_NONE)
705 {
706 num_values++;
707 opts.cp_save = cp;
708 }
709 }
710
711 if (opts.cp_save == NULL)
712 {
713 Log(LOG_LEVEL_WARNING, "Incomplete vars promise: %s",
714 pp->promiser);
715 PromiseRef(LOG_LEVEL_INFO, pp);
716 return opts;
717 }
718
719 if (num_values > 2)
720 {
721 Log(LOG_LEVEL_ERR,
722 "Variable '%s' breaks its own promise with multiple (%d) values",
723 pp->promiser, num_values);
724 PromiseRef(LOG_LEVEL_ERR, pp);
725 return opts;
726 }
727
728 /* All constraints look OK, and classes are defined. Move forward with
729 * this promise. */
730 opts.should_converge = true;
731
732 return opts;
733 }
734