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 <files_operators.h>
26
27 #include <actuator.h>
28 #include <eval_context.h>
29 #include <promises.h>
30 #include <dir.h>
31 #include <dbm_api.h>
32 #include <files_names.h>
33 #include <files_interfaces.h>
34 #include <hash.h>
35 #include <files_copy.h>
36 #include <vars.h>
37 #include <item_lib.h>
38 #include <conversion.h>
39 #include <expand.h>
40 #include <scope.h>
41 #include <matching.h>
42 #include <attributes.h>
43 #include <client_code.h>
44 #include <pipes.h>
45 #include <locks.h>
46 #include <string_lib.h>
47 #include <files_repository.h>
48 #include <files_lib.h>
49 #include <buffer.h>
50
51
MoveObstruction(EvalContext * ctx,char * from,const Attributes * attr,const Promise * pp,PromiseResult * result)52 bool MoveObstruction(EvalContext *ctx, char *from, const Attributes *attr, const Promise *pp, PromiseResult *result)
53 {
54 assert(attr != NULL);
55 struct stat sb;
56 char stamp[CF_BUFSIZE], saved[CF_BUFSIZE];
57 time_t now_stamp = time((time_t *) NULL);
58
59 const char *changes_from = from;
60 if (ChrootChanges())
61 {
62 changes_from = ToChangesChroot(from);
63 }
64
65 if (lstat(from, &sb) == 0)
66 {
67 if (!attr->move_obstructions)
68 {
69 RecordFailure(ctx, pp, attr, "Object '%s' is obstructing promise", from);
70 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
71 return false;
72 }
73
74 if (!S_ISDIR(sb.st_mode))
75 {
76 if (!MakingChanges(ctx, pp, attr, result, "move aside object '%s' obstructing promise", from))
77 {
78 return false;
79 }
80
81 saved[0] = '\0';
82 strlcpy(saved, changes_from, sizeof(saved));
83
84 if (attr->copy.backup == BACKUP_OPTION_TIMESTAMP || attr->edits.backup == BACKUP_OPTION_TIMESTAMP)
85 {
86 snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&now_stamp)));
87 strlcat(saved, stamp, sizeof(saved));
88 }
89
90 strlcat(saved, CF_SAVED, sizeof(saved));
91
92 if (rename(changes_from, saved) == -1)
93 {
94 RecordFailure(ctx, pp, attr,
95 "Can't rename '%s' to '%s'. (rename: %s)", from, saved, GetErrorStr());
96 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
97 return false;
98 }
99 RecordChange(ctx, pp, attr, "Moved obstructing object '%s' to '%s'", from, saved);
100 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
101
102 if (ArchiveToRepository(saved, attr))
103 {
104 RecordChange(ctx, pp, attr, "Archived '%s'", saved);
105 unlink(saved);
106 }
107
108 return true;
109 }
110
111 if (S_ISDIR(sb.st_mode))
112 {
113 if (!MakingChanges(ctx, pp, attr, result, "move aside directory '%s' obstructing", from))
114 {
115 return false;
116 }
117
118 saved[0] = '\0';
119 strlcpy(saved, changes_from, sizeof(saved));
120
121 snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&now_stamp)));
122 strlcat(saved, stamp, sizeof(saved));
123 strlcat(saved, CF_SAVED, sizeof(saved));
124 strlcat(saved, ".dir", sizeof(saved));
125
126 if (stat(saved, &sb) != -1)
127 {
128 RecordFailure(ctx, pp, attr,
129 "Couldn't move directory '%s' aside, since '%s' exists already",
130 from, saved);
131 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
132 return false;
133 }
134
135 if (rename(changes_from, saved) == -1)
136 {
137 RecordFailure(ctx, pp, attr, "Can't rename '%s' to '%s'. (rename: %s)",
138 from, saved, GetErrorStr());
139 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
140 return false;
141 }
142 RecordChange(ctx, pp, attr, "Moved directory '%s' to '%s%s'", from, from, CF_SAVED);
143 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
144 }
145 }
146
147 return true;
148 }
149
150 /*********************************************************************/
151
SaveAsFile(SaveCallbackFn callback,void * param,const char * file,const Attributes * a,NewLineMode new_line_mode)152 bool SaveAsFile(SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode)
153 {
154 assert(a != NULL);
155 struct stat statbuf;
156 char new[CF_BUFSIZE], backup[CF_BUFSIZE];
157 char stamp[CF_BUFSIZE];
158 time_t stamp_now;
159 Buffer *deref_file = BufferNewFrom(file, strlen(file));
160 Buffer *pretty_file = BufferNew();
161 bool ret = false;
162
163 BufferPrintf(pretty_file, "'%s'", file);
164
165 stamp_now = time((time_t *) NULL);
166
167 while (1)
168 {
169 if (lstat(BufferData(deref_file), &statbuf) == -1)
170 {
171 Log(LOG_LEVEL_ERR, "Can no longer access file %s, which needed editing. (lstat: %s)", BufferData(pretty_file), GetErrorStr());
172 goto end;
173 }
174 #ifndef __MINGW32__
175 if (S_ISLNK(statbuf.st_mode))
176 {
177 char buf[statbuf.st_size + 1];
178 // Careful. readlink() doesn't add '\0' byte.
179 ssize_t linksize = readlink(BufferData(deref_file), buf, statbuf.st_size);
180 if (linksize == 0)
181 {
182 Log(LOG_LEVEL_WARNING, "readlink() failed with 0 bytes. Should not happen (bug?).");
183 goto end;
184 }
185 else if (linksize < 0)
186 {
187 Log(LOG_LEVEL_ERR, "Could not read link %s. (readlink: %s)", BufferData(pretty_file), GetErrorStr());
188 goto end;
189 }
190 buf[linksize] = '\0';
191 if (!IsAbsPath(buf))
192 {
193 char dir[BufferSize(deref_file) + 1];
194 strcpy(dir, BufferData(deref_file));
195 ChopLastNode(dir);
196 BufferPrintf(deref_file, "%s/%s", dir, buf);
197 }
198 else
199 {
200 BufferSet(deref_file, buf, linksize);
201 }
202 BufferPrintf(pretty_file, "'%s' (from symlink '%s')", BufferData(deref_file), file);
203 }
204 else
205 #endif
206 {
207 break;
208 }
209 }
210
211 strcpy(backup, BufferData(deref_file));
212
213 if (a->edits.backup == BACKUP_OPTION_TIMESTAMP)
214 {
215 snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&stamp_now)));
216 strcat(backup, stamp);
217 }
218
219 strcat(backup, ".cf-before-edit");
220
221 strcpy(new, BufferData(deref_file));
222 strcat(new, ".cf-after-edit");
223 unlink(new); /* Just in case of races */
224
225 if ((*callback)(new, param, new_line_mode) == false)
226 {
227 goto end;
228 }
229
230 if (!CopyFilePermissionsDisk(BufferData(deref_file), new))
231 {
232 Log(LOG_LEVEL_ERR, "Can't copy file permissions from %s to '%s' - so promised edits could not be moved into place.",
233 BufferData(pretty_file), new);
234 goto end;
235 }
236
237 unlink(backup);
238 #ifndef __MINGW32__
239 if (link(BufferData(deref_file), backup) == -1)
240 {
241 Log(LOG_LEVEL_VERBOSE, "Can't link %s to '%s' - falling back to copy. (link: %s)",
242 BufferData(pretty_file), backup, GetErrorStr());
243 #else
244 /* No hardlinks on Windows, go straight to copying */
245 {
246 #endif
247 if (!CopyRegularFileDisk(BufferData(deref_file), backup))
248 {
249 Log(LOG_LEVEL_ERR, "Can't copy %s to '%s' - so promised edits could not be moved into place.",
250 BufferData(pretty_file), backup);
251 goto end;
252 }
253 if (!CopyFilePermissionsDisk(BufferData(deref_file), backup))
254 {
255 Log(LOG_LEVEL_ERR, "Can't copy permissions %s to '%s' - so promised edits could not be moved into place.",
256 BufferData(pretty_file), backup);
257 goto end;
258 }
259 }
260
261 if (a->edits.backup == BACKUP_OPTION_ROTATE)
262 {
263 RotateFiles(backup, a->edits.rotate);
264 unlink(backup);
265 }
266
267 if (a->edits.backup != BACKUP_OPTION_NO_BACKUP)
268 {
269 if (ArchiveToRepository(backup, a))
270 {
271 unlink(backup);
272 }
273 }
274
275 else
276 {
277 unlink(backup);
278 }
279
280 if (rename(new, BufferData(deref_file)) == -1)
281 {
282 Log(LOG_LEVEL_ERR, "Can't rename '%s' to %s - so promised edits could not be moved into place. (rename: %s)",
283 new, BufferData(pretty_file), GetErrorStr());
284 goto end;
285 }
286
287 ret = true;
288
289 end:
290 BufferDestroy(pretty_file);
291 BufferDestroy(deref_file);
292 return ret;
293 }
294
295 /*********************************************************************/
296
297 static bool SaveItemListCallback(const char *dest_filename, void *param, NewLineMode new_line_mode)
298 {
299 Item *liststart = param, *ip;
300
301 //saving list to file
302 FILE *fp = safe_fopen(
303 dest_filename, (new_line_mode == NewLineMode_Native) ? "wt" : "w");
304 if (fp == NULL)
305 {
306 Log(LOG_LEVEL_ERR, "Unable to open destination file '%s' for writing. (fopen: %s)",
307 dest_filename, GetErrorStr());
308 return false;
309 }
310
311 for (ip = liststart; ip != NULL; ip = ip->next)
312 {
313 if (fprintf(fp, "%s\n", ip->name) < 0)
314 {
315 Log(LOG_LEVEL_ERR, "Unable to write into destination file '%s'. (fprintf: %s)",
316 dest_filename, GetErrorStr());
317 fclose(fp);
318 return false;
319 }
320 }
321
322 if (fclose(fp) == -1)
323 {
324 Log(LOG_LEVEL_ERR, "Unable to close file '%s' after writing. (fclose: %s)",
325 dest_filename, GetErrorStr());
326 return false;
327 }
328
329 return true;
330 }
331
332 /*********************************************************************/
333
334 bool SaveItemListAsFile(Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode)
335 {
336 assert(a != NULL);
337 return SaveAsFile(&SaveItemListCallback, liststart, file, a, new_line_mode);
338 }
339
340 // Some complex logic here to enable warnings of diffs to be given
341
342 static Item *NextItem(const Item *ip)
343 {
344 if (ip)
345 {
346 return ip->next;
347 }
348 else
349 {
350 return NULL;
351 }
352 }
353
354 static bool ItemListsEqual(EvalContext *ctx, const Item *list1, const Item *list2, int warnings,
355 const Attributes *a, const Promise *pp, PromiseResult *result)
356 {
357 assert(a != NULL);
358 bool retval = true;
359
360 const Item *ip1 = list1;
361 const Item *ip2 = list2;
362
363 while (true)
364 {
365 if ((ip1 == NULL) && (ip2 == NULL))
366 {
367 return retval;
368 }
369
370 if ((ip1 == NULL) || (ip2 == NULL))
371 {
372 if (warnings)
373 {
374 if ((ip1 == list1) || (ip2 == list2))
375 {
376 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "File content wants to change from from/to full/empty but only a warning promised");
377 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
378 }
379 else
380 {
381 if (ip1 != NULL)
382 {
383 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, " ! edit_line change warning promised: (remove) %s",
384 ip1->name);
385 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
386 }
387
388 if (ip2 != NULL)
389 {
390 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, " ! edit_line change warning promised: (add) %s", ip2->name);
391 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
392 }
393 }
394 }
395
396 if (warnings)
397 {
398 if (ip1 || ip2)
399 {
400 retval = false;
401 ip1 = NextItem(ip1);
402 ip2 = NextItem(ip2);
403 continue;
404 }
405 }
406
407 return false;
408 }
409
410 if (strcmp(ip1->name, ip2->name) != 0)
411 {
412 if (!warnings)
413 {
414 // No need to wait
415 return false;
416 }
417 else
418 {
419 // If we want to see warnings, we need to scan the whole file
420
421 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "edit_line warning promised: - %s", ip1->name);
422 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
423 retval = false;
424 }
425 }
426
427 ip1 = NextItem(ip1);
428 ip2 = NextItem(ip2);
429 }
430
431 return retval;
432 }
433
434 /* returns true if file on disk is identical to file in memory */
435
436 bool CompareToFile(
437 EvalContext *ctx,
438 const Item *liststart,
439 const char *file,
440 const Attributes *a,
441 const Promise *pp,
442 PromiseResult *result)
443 {
444 assert(a != NULL);
445 struct stat statbuf;
446 Item *cmplist = NULL;
447
448 if (stat(file, &statbuf) == -1)
449 {
450 return false;
451 }
452
453 if ((liststart == NULL) && (statbuf.st_size == 0))
454 {
455 return true;
456 }
457
458 if (liststart == NULL)
459 {
460 return false;
461 }
462
463 if (!LoadFileAsItemList(&cmplist, file, a->edits))
464 {
465 return false;
466 }
467
468 if (!ItemListsEqual(ctx, cmplist, liststart, (a->transaction.action == cfa_warn), a, pp, result))
469 {
470 DeleteItemList(cmplist);
471 return false;
472 }
473
474 DeleteItemList(cmplist);
475 return (true);
476 }
477