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 <verify_methods.h>
26
27 #include <actuator.h>
28 #include <eval_context.h>
29 #include <vars.h>
30 #include <expand.h>
31 #include <files_names.h>
32 #include <scope.h>
33 #include <unix.h>
34 #include <attributes.h>
35 #include <locks.h>
36 #include <generic_agent.h> // HashVariables
37 #include <fncall.h>
38 #include <rlist.h>
39 #include <ornaments.h>
40 #include <string_lib.h>
41
42 static void GetReturnValue(EvalContext *ctx, const Bundle *callee, const Promise *caller);
43
44 /*****************************************************************************/
45
46 /*
47 * This function should only be called from the evaluator so that methods promises
48 * never report REPAIRED compliance (the promises inside will do that already).
49 *
50 * Promise types that delegate to bundles (services and users) should call VerifyMethod,
51 * which maintains the REPAIRED compliance so that it bubbles up correctly to the parent
52 * promise type.
53 */
54
VerifyMethodsPromise(EvalContext * ctx,const Promise * pp)55 PromiseResult VerifyMethodsPromise(EvalContext *ctx, const Promise *pp)
56 {
57 Attributes a = GetMethodAttributes(ctx, pp);
58
59 const Constraint *cp;
60 Rval method_name;
61 bool destroy_name;
62
63 if ((cp = PromiseGetConstraint(pp, "usebundle")))
64 {
65 method_name = cp->rval;
66 destroy_name = false;
67 }
68 else
69 {
70 method_name = RvalNew(pp->promiser, RVAL_TYPE_SCALAR);
71 destroy_name = true;
72 }
73
74 PromiseResult result = VerifyMethod(ctx, method_name, &a, pp);
75
76 if (destroy_name)
77 {
78 RvalDestroy(method_name);
79 }
80
81 return result;
82 }
83
84 /*****************************************************************************/
85
VerifyMethod(EvalContext * ctx,const Rval call,const Attributes * a,const Promise * pp)86 PromiseResult VerifyMethod(EvalContext *ctx, const Rval call, const Attributes *a, const Promise *pp)
87 {
88 const Rlist *args = NULL;
89 Buffer *method_name = BufferNew();
90 switch (call.type)
91 {
92 case RVAL_TYPE_FNCALL:
93 {
94 const FnCall *fp = RvalFnCallValue(call);
95 ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, fp->name, method_name);
96 args = fp->args;
97 int arg_index = 0;
98 while (args)
99 {
100 ++arg_index;
101 args = args->next;
102 }
103 args = fp->args;
104 }
105 break;
106
107 case RVAL_TYPE_SCALAR:
108 {
109 ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name,
110 RvalScalarValue(call), method_name);
111 args = NULL;
112 }
113 break;
114
115 default:
116 BufferDestroy(method_name);
117 return PROMISE_RESULT_NOOP;
118 }
119
120 char lockname[CF_BUFSIZE];
121 GetLockName(lockname, "method", pp->promiser, args);
122
123 CfLock thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a->transaction.ifelapsed, a->transaction.expireafter, pp, false);
124 if (thislock.lock == NULL)
125 {
126 BufferDestroy(method_name);
127 return PROMISE_RESULT_SKIPPED;
128 }
129
130 PromiseBanner(ctx, pp);
131
132 const Bundle *bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), BufferData(method_name), "agent");
133
134 if (!bp)
135 {
136 bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), BufferData(method_name), "common");
137 }
138
139 PromiseResult result = PROMISE_RESULT_NOOP;
140
141 if (bp)
142 {
143 if (a->transaction.action == cfa_warn) // don't skip for dry-runs (ie ignore DONTDO)
144 {
145 result = PROMISE_RESULT_WARN;
146 cfPS(ctx, LOG_LEVEL_WARNING, result, pp, a, "Bundle '%s' should be invoked, but only a warning was promised!", BufferData(method_name));
147 }
148 else
149 {
150 BundleBanner(bp, args);
151 EvalContextSetBundleArgs(ctx, args);
152 EvalContextStackPushBundleFrame(ctx, bp, args, a->inherit);
153
154 /* Clear all array-variables that are already set in the sub-bundle.
155 Otherwise, array-data accumulates between multiple bundle evaluations.
156 Note: for bundles invoked multiple times via bundlesequence, array
157 data *does* accumulate. */
158 VariableTableIterator *iter = EvalContextVariableTableIteratorNew(ctx, bp->ns, bp->name, NULL);
159 Variable *var;
160 while ((var = VariableTableIteratorNext(iter)))
161 {
162 const VarRef *var_ref = VariableGetRef(var);
163 if (!var_ref->num_indices)
164 {
165 continue;
166 }
167 EvalContextVariableRemove(ctx, var_ref);
168 }
169 VariableTableIteratorDestroy(iter);
170
171 BundleResolve(ctx, bp);
172
173 result = ScheduleAgentOperations(ctx, bp);
174
175 GetReturnValue(ctx, bp, pp);
176
177 EvalContextStackPopFrame(ctx);
178 EvalContextSetBundleArgs(ctx, NULL);
179 switch (result)
180 {
181 case PROMISE_RESULT_SKIPPED:
182 // if a bundle returns 'skipped', meaning that all promises were locked in the bundle,
183 // we explicitly consider the method 'kept'
184 result = PROMISE_RESULT_NOOP;
185 // intentional fallthru
186
187 case PROMISE_RESULT_NOOP:
188 cfPS(ctx, LOG_LEVEL_VERBOSE, PROMISE_RESULT_NOOP, pp, a, "Method '%s' verified", bp->name);
189 break;
190
191 case PROMISE_RESULT_WARN:
192 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "Method '%s' invoked repairs, but only warnings promised", bp->name);
193 break;
194
195 case PROMISE_RESULT_CHANGE:
196 cfPS(ctx, LOG_LEVEL_VERBOSE, PROMISE_RESULT_CHANGE, pp, a, "Method '%s' invoked repairs", bp->name);
197 break;
198
199 case PROMISE_RESULT_FAIL:
200 case PROMISE_RESULT_DENIED:
201 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Method '%s' failed in some repairs", bp->name);
202 break;
203
204 default: // PROMISE_RESULT_INTERRUPTED, TIMEOUT
205 cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_FAIL, pp, a, "Method '%s' aborted in some repairs", bp->name);
206 break;
207 }
208 }
209
210 for (const Rlist *rp = bp->args; rp; rp = rp->next)
211 {
212 const char *lval = RlistScalarValue(rp);
213 VarRef *ref = VarRefParseFromBundle(lval, bp);
214 EvalContextVariableRemove(ctx, ref);
215 VarRefDestroy(ref);
216 }
217 }
218 else
219 {
220 if (IsCf3VarString(BufferData(method_name)))
221 {
222 Log(LOG_LEVEL_ERR,
223 "A variable seems to have been used for the name of the method. In this case, the promiser also needs to contain the unique name of the method");
224 }
225
226 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
227 "A method attempted to use a bundle '%s' that was apparently not defined",
228 BufferData(method_name));
229 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
230 }
231
232 YieldCurrentLock(thislock);
233 BufferDestroy(method_name);
234 EndBundleBanner(bp);
235
236 return result;
237 }
238
239 /***********************************************************************/
240
GetReturnValue(EvalContext * ctx,const Bundle * callee,const Promise * caller)241 static void GetReturnValue(EvalContext *ctx, const Bundle *callee, const Promise *caller)
242 {
243 char *result = PromiseGetConstraintAsRval(caller, "useresult", RVAL_TYPE_SCALAR);
244
245 if (result)
246 {
247 VarRef *ref = VarRefParseFromBundle("last-result", callee);
248 VariableTableIterator *iter = EvalContextVariableTableIteratorNew(ctx, ref->ns, ref->scope, ref->lval);
249 Variable *result_var = NULL;
250 while ((result_var = VariableTableIteratorNext(iter)))
251 {
252 const VarRef *result_var_ref = VariableGetRef(result_var);
253 assert(result_var_ref->num_indices == 1);
254 if (result_var_ref->num_indices != 1)
255 {
256 continue;
257 }
258
259 VarRef *new_ref = VarRefParseFromBundle(result, PromiseGetBundle(caller));
260 VarRefAddIndex(new_ref, result_var_ref->indices[0]);
261
262 Rval result_var_rval = VariableGetRval(result_var, true);
263 EvalContextVariablePut(ctx, new_ref, result_var_rval.item, VariableGetType(result_var), "source=bundle");
264
265 VarRefDestroy(new_ref);
266 }
267
268 VarRefDestroy(ref);
269 VariableTableIteratorDestroy(iter);
270 }
271
272 }
273