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 <platform.h>
26
27 #include <stdlib.h>
28 #include <stdio.h>
29
30 #include <logging.h>
31 #include <eval_context.h> /* ToChangesChroot() */
32 #include <file_lib.h> /* IsAbsoluteFileName() */
33 #include <dir.h> /* DirOpen(),...*/
34 #include <string_lib.h> /* StringEqual() */
35 #include <string_sequence.h> /* ReadLenPrefixedString() */
36 #include <changes_chroot.h> /* CHROOT_CHANGES_LIST_FILE */
37 #include <known_dirs.h> /* GetBinDir() */
38 #include <files_names.h> /* JoinPaths() */
39 #include <pipes.h> /* cf_popen(), cf_pclose() */
40 #include <set.h> /* StringSet */
41 #include <map.h> /* StringMap */
42 #include <csv_parser.h> /* GetCsvLineNext() */
43
44 #include <simulate_mode.h>
45
46 #define DELIM_CHAR '='
47
48 /* Taken from coreutils/lib/stat-macros.h. */
49 #define CHMOD_MODE_BITS (S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)
50
PrintDelimiter()51 static inline void PrintDelimiter()
52 {
53 char *columns = getenv("COLUMNS");
54 int n_columns = 0;
55 if (columns != NULL)
56 {
57 n_columns = atoi(columns);
58 }
59 n_columns = MAX(n_columns, 80) - 5;
60 for (int i = n_columns; i > 0; i--)
61 {
62 putchar(DELIM_CHAR);
63 }
64 putchar('\n');
65 }
66
67 #ifndef __MINGW32__
ManifestStatInfo(const struct stat * st)68 static void ManifestStatInfo(const struct stat *st)
69 {
70 assert(st != NULL);
71
72 /* Inspired by the output from the 'stat' command */
73 char mode_str[10] = {
74 st->st_mode & S_IRUSR ? 'r' : '-',
75 st->st_mode & S_IWUSR ? 'w' : '-',
76 (st->st_mode & S_ISUID
77 ? (st->st_mode & S_IXUSR ? 's' : 'S')
78 : (st->st_mode & S_IXUSR ? 'x' : '-')),
79 st->st_mode & S_IRGRP ? 'r' : '-',
80 st->st_mode & S_IWGRP ? 'w' : '-',
81 (st->st_mode & S_ISGID
82 ? (st->st_mode & S_IXGRP ? 's' : 'S')
83 : (st->st_mode & S_IXGRP ? 'x' : '-')),
84 st->st_mode & S_IROTH ? 'r' : '-',
85 st->st_mode & S_IWOTH ? 'w' : '-',
86 (st->st_mode & S_ISVTX
87 ? (st->st_mode & S_IXOTH ? 't' : 'T')
88 : (st->st_mode & S_IXOTH ? 'x' : '-')),
89 '\0'
90 };
91 printf("Size: %ju\n", (uintmax_t) st->st_size);
92 printf("Access: (%04o/%s) ", st->st_mode & CHMOD_MODE_BITS, mode_str);
93
94 errno = 0;
95 struct passwd *owner = getpwuid(st->st_uid);
96 if (owner != NULL)
97 {
98 printf("Uid: (%ju/%s) ", (uintmax_t) st->st_uid, owner->pw_name);
99 }
100 else
101 {
102 if (errno == 0)
103 {
104 Log(LOG_LEVEL_ERR, "Failed to get file owner name: non-existent user id '%ju'",
105 (uintmax_t) st->st_uid);
106 }
107 else
108 {
109 Log(LOG_LEVEL_ERR, "Failed to get file owner name: %s",
110 GetErrorStr());
111 }
112 }
113
114 errno = 0;
115 struct group *group = getgrgid(st->st_gid);
116 if (group != NULL) {
117 printf("Gid: (%ju/%s)\n", (uintmax_t) st->st_gid, group->gr_name);
118 }
119 else
120 {
121 if (errno == 0)
122 {
123 Log(LOG_LEVEL_ERR, "Failed to get file group name: non-existent group id '%ju'",
124 (uintmax_t) st->st_gid);
125 }
126 else
127 {
128 Log(LOG_LEVEL_ERR, "Failed to get file group name: %s",
129 GetErrorStr());
130 }
131 }
132
133 #define MAX_TIMESTAMP_SIZE (sizeof("2020-10-05 12:56:18 +0200"))
134 char buf[MAX_TIMESTAMP_SIZE] = {0};
135
136 size_t ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
137 localtime((time_t*) &(st->st_atime)));
138 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
139 printf("Access: %s\n", buf);
140
141 ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
142 localtime((time_t*) &(st->st_mtime)));
143 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
144 printf("Modify: %s\n", buf);
145
146 ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
147 localtime((time_t*) &(st->st_ctime)));
148 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
149 printf("Change: %s\n", buf);
150 }
151 #else /* !__MINGW32__ */
ManifestStatInfo(const struct stat * st)152 static void ManifestStatInfo(const struct stat *st)
153 {
154 assert(st != NULL);
155
156 /* Inspired by the output from the 'stat' command */
157 char mode_str[10] = {
158 st->st_mode & S_IRUSR ? 'r' : '-',
159 st->st_mode & S_IWUSR ? 'w' : '-',
160 st->st_mode & S_IXUSR ? 'x' : '-',
161 st->st_mode & S_IRGRP ? 'r' : '-',
162 st->st_mode & S_IWGRP ? 'w' : '-',
163 st->st_mode & S_IXGRP ? 'x' : '-',
164 st->st_mode & S_IROTH ? 'r' : '-',
165 st->st_mode & S_IWOTH ? 'w' : '-',
166 st->st_mode & S_IXOTH ? 'x' : '-',
167 '\0'
168 };
169 printf("Size: %ju\n", (uintmax_t) st->st_size);
170 printf("Access: (%04o/%s) ", st->st_mode, mode_str);
171 printf("Uid: %ju ", (uintmax_t)st->st_uid);
172 printf("Gid: %ju\n", (uintmax_t)st->st_gid);
173
174 #define MAX_TIMESTAMP_SIZE (sizeof("2020-10-05 12:56:18 +0200"))
175 char buf[MAX_TIMESTAMP_SIZE] = {0};
176
177 size_t ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
178 localtime((time_t*) &(st->st_atime)));
179 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
180 printf("Access: %s\n", buf);
181
182 ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
183 localtime((time_t*) &(st->st_mtime)));
184 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
185 printf("Modify: %s\n", buf);
186
187 ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
188 localtime((time_t*) &(st->st_ctime)));
189 assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
190 printf("Change: %s\n", buf);
191 }
192 #endif /* !__MINGW32__ */
193
194 #ifndef __MINGW32__
ManifestLinkTarget(const char * link_path,bool chrooted)195 static inline void ManifestLinkTarget(const char *link_path, bool chrooted)
196 {
197 char target[PATH_MAX] = {0};
198 if (readlink(link_path, target, sizeof(target)) > 0)
199 {
200 const char *real_target = target;
201 if (chrooted && IsAbsoluteFileName(target))
202 {
203 real_target = ToNormalRoot(target);
204 }
205 printf("Target: '%s'\n", real_target);
206 }
207 else
208 {
209 printf("Invalid target\n");
210 }
211 }
212 #endif /* !__MINGW32__ */
213
ManifestFileContents(const char * path)214 static inline void ManifestFileContents(const char *path)
215 {
216 FILE *f = fopen(path, "r");
217 if (f != NULL)
218 {
219 puts("Contents of the file:");
220 }
221 else
222 {
223 Log(LOG_LEVEL_ERR, "Failed to open file for reading: %s", GetErrorStr());
224 return;
225 }
226
227 bool binary = false;
228 char last_char = '\n';
229
230 char buf[CF_BUFSIZE];
231 size_t n_read;
232 bool done = false;
233 while (!done && ((n_read = fread(buf, 1, sizeof(buf), f)) > 0))
234 {
235 bool is_ascii = true;
236 for (size_t i = 0; is_ascii && (i < n_read); i++)
237 {
238 is_ascii = isascii(buf[i]);
239 }
240 last_char = buf[n_read - 1];
241 if (is_ascii)
242 {
243 size_t offset = 0;
244 size_t to_write = n_read;
245 while (to_write > 0)
246 {
247 size_t n_written = fwrite(buf + offset, 1, to_write, stdout);
248 if (n_written > 0)
249 {
250 to_write -= n_written;
251 offset += n_written;
252 }
253 else
254 {
255 Log(LOG_LEVEL_ERR, "Failed to print contents of the file");
256 break;
257 }
258 }
259 }
260 else
261 {
262 puts("File contains non-ASCII data");
263 binary = true;
264 done = true;
265 }
266 }
267 if (!binary && (last_char != '\n'))
268 {
269 puts("\n\\no newline at the end of file");
270 }
271 if (!done && !feof(f))
272 {
273 Log(LOG_LEVEL_ERR, "Failed to print contents of the file");
274 }
275 fclose(f);
276 }
277
ManifestDirectoryListing(const char * path)278 static inline void ManifestDirectoryListing(const char *path)
279 {
280 Dir *dir = DirOpen(path);
281 if (dir == NULL)
282 {
283 Log(LOG_LEVEL_ERR, "Failed to open the directory: %s", GetErrorStr());
284 return;
285 }
286 else
287 {
288 puts("Directory contents:");
289 }
290 for (const struct dirent *dir_p = DirRead(dir);
291 dir_p != NULL;
292 dir_p = DirRead(dir))
293 {
294 if (StringEqual(dir_p->d_name, ".") ||
295 StringEqual(dir_p->d_name, ".."))
296 {
297 continue;
298 }
299 else
300 {
301 puts(dir_p->d_name);
302 }
303 }
304 DirClose(dir);
305 }
306
GetFileTypeDescription(mode_t st_mode)307 static inline const char *GetFileTypeDescription(mode_t st_mode)
308 {
309 switch (st_mode & S_IFMT) {
310 case S_IFBLK:
311 return "block device";
312 case S_IFCHR:
313 return "character device";
314 case S_IFDIR:
315 return "directory";
316 case S_IFIFO:
317 return "FIFO/pipe";
318 #ifndef __MINGW32__
319 case S_IFLNK:
320 return "symbolic link";
321 #endif
322 case S_IFREG:
323 return "regular file";
324 case S_IFSOCK:
325 return "socket";
326 default:
327 debug_abort_if_reached();
328 return "unknown";
329 }
330 }
331
ManifestFileDetails(const char * path,struct stat * st,bool chrooted)332 static inline void ManifestFileDetails(const char *path, struct stat *st, bool chrooted)
333 {
334 assert(st != NULL);
335
336 switch (st->st_mode & S_IFMT) {
337 case S_IFREG:
338 puts(""); /* blank line */
339 ManifestFileContents(path);
340 break;
341 #ifndef __MINGW32__
342 case S_IFLNK:
343 puts(""); /* blank line */
344 ManifestLinkTarget(path, chrooted);
345 break;
346 #endif
347 case S_IFDIR:
348 puts(""); /* blank line */
349 ManifestDirectoryListing(path);
350 break;
351 default:
352 /* nothing to do for other types */
353 break;
354 }
355 }
356
ManifestFile(const char * path,bool chrooted)357 bool ManifestFile(const char *path, bool chrooted)
358 {
359 PrintDelimiter();
360 const char *real_path = path;
361 if (chrooted)
362 {
363 real_path = ToChangesChroot(path);
364 }
365
366 /* TODO: handle renames */
367 struct stat st;
368 if (lstat(real_path, &st) == -1)
369 {
370 printf("'%s' no longer exists\n", path);
371 return true;
372 }
373
374 printf("'%s' is a %s\n", path, GetFileTypeDescription(st.st_mode));
375 ManifestStatInfo(&st);
376 ManifestFileDetails(real_path, &st, chrooted);
377
378 return true;
379 }
380
ManifestRename(const char * orig_name,const char * new_name)381 bool ManifestRename(const char *orig_name, const char *new_name)
382 {
383 PrintDelimiter();
384 printf("'%s' is the new name of '%s'\n", new_name, orig_name);
385 return true;
386 }
387
RunDiff(const char * path1,const char * path2)388 static bool RunDiff(const char *path1, const char *path2)
389 {
390 char diff_path[PATH_MAX];
391 strncpy(diff_path, GetBinDir(), sizeof(diff_path));
392 JoinPaths(diff_path, sizeof(diff_path), "diff");
393
394 /* We use the '--label' option to override the paths in the output, for example:
395 * --- original /etc/motd.d/cfengine
396 * +++ changed /etc/motd.d/cfengine
397 * @@ -1,1 +1,1 @@
398 * -One line
399 * +New line
400 */
401 char *command;
402 int ret = xasprintf(&command, "%s -u --label 'original %s' --label 'changed %s' '%s' '%s'",
403 diff_path, path1, path1, path1, path2);
404 assert(ret != -1); /* should never happen */
405
406 FILE *f = cf_popen(command, "r", true);
407
408 char buf[CF_BUFSIZE];
409 size_t n_read;
410 bool failure = false;
411 while (!failure && ((n_read = fread(buf, 1, sizeof(buf), f)) > 0))
412 {
413 size_t offset = 0;
414 size_t to_write = n_read;
415 while (to_write > 0)
416 {
417 size_t n_written = fwrite(buf + offset, 1, to_write, stdout);
418 if (n_written > 0)
419 {
420 to_write -= n_written;
421 offset += n_written;
422 }
423 else
424 {
425 Log(LOG_LEVEL_ERR, "Failed to print results from 'diff' for '%s' and '%s'",
426 path1, path2);
427 failure = true;
428 break;
429 }
430 }
431 }
432 if (!feof(f))
433 {
434 Log(LOG_LEVEL_ERR, "Failed to read output from the 'diff' utility");
435 cf_pclose(f);
436 free(command);
437 return false;
438 }
439 ret = cf_pclose(f);
440 free(command);
441 if (ret == 2)
442 {
443 Log(LOG_LEVEL_ERR, "'diff -u %s %s' failed", path1, path2);
444 return false;
445 }
446 return !failure;
447 }
448
DiffFile(const char * path)449 static bool DiffFile(const char *path)
450 {
451 const char *chrooted_path = ToChangesChroot(path);
452
453 struct stat st_orig;
454 struct stat st_chrooted;
455 if (lstat(path, &st_orig) == -1)
456 {
457 /* Original final doesn't exist, must be a new file in the changes
458 * chroot, let's just manifest it instead of running 'diff' on it. */
459 ManifestFile(path, true);
460 return true;
461 }
462
463 PrintDelimiter();
464 if (lstat(chrooted_path, &st_chrooted) == -1)
465 {
466 /* TODO: should this do print info about the original file? */
467 printf("'%s' no longer exists\n", path);
468 return true;
469 }
470
471 if ((st_orig.st_mode & S_IFMT) != (st_chrooted.st_mode & S_IFMT))
472 {
473 /* File type changed. */
474 printf("'%s' changed type from %s to %s\n", path,
475 GetFileTypeDescription(st_orig.st_mode),
476 GetFileTypeDescription(st_chrooted.st_mode));
477 ManifestStatInfo(&st_chrooted);
478 ManifestFileDetails(path, &st_chrooted, true);
479 return true;
480 }
481 else
482 {
483 switch (st_chrooted.st_mode & S_IFMT) {
484 case S_IFREG:
485 case S_IFDIR:
486 return RunDiff(path, chrooted_path);
487 default:
488 printf("'%s' is a %s\n", path, GetFileTypeDescription(st_chrooted.st_mode));
489 ManifestStatInfo(&st_chrooted);
490 ManifestFileDetails(path, &st_chrooted, true);
491 return true;
492 }
493 }
494 }
495
ManifestRenamedFiles()496 bool ManifestRenamedFiles()
497 {
498 bool success = true;
499
500 const char *renamed_files_file = ToChangesChroot(CHROOT_RENAMES_LIST_FILE);
501 if (access(renamed_files_file, F_OK) == 0)
502 {
503 int fd = safe_open(renamed_files_file, O_RDONLY);
504 if (fd == -1)
505 {
506 Log(LOG_LEVEL_ERR, "Failed to open the file with list of renamed files: %s", GetErrorStr());
507 success = false;
508 }
509
510 Log(LOG_LEVEL_INFO, "Manifesting renamed files (in the changes chroot)");
511 bool done = false;
512 while (!done)
513 {
514 /* The CHROOT_RENAMES_LIST_FILE contains lines where two consecutive
515 * lines represent the original and the new name of a file (see
516 * RecordFileRenamedInChroot(). */
517
518 /* TODO: read into a PATH_MAX buffers */
519 char *orig_name;
520 int ret = ReadLenPrefixedString(fd, &orig_name);
521 if (ret > 0)
522 {
523 char *new_name;
524 ret = ReadLenPrefixedString(fd, &new_name);
525 if (ret > 0)
526 {
527 success = (success && ManifestRename(orig_name, new_name));
528 free(new_name);
529 }
530 else
531 {
532 /* If there was the line with the original name, there
533 * must be a line with the new name. */
534 Log(LOG_LEVEL_ERR, "Invalid data about renamed files");
535 success = false;
536 done = true;
537 }
538 free(orig_name);
539 }
540 else if (ret == 0)
541 {
542 /* EOF */
543 done = true;
544 }
545 else
546 {
547 Log(LOG_LEVEL_ERR, "Failed to read the list of changed files");
548 success = false;
549 done = true;
550 }
551 }
552 close(fd);
553 }
554 return success;
555 }
556
AuditChangedFiles(EvalMode mode,StringSet ** audited_files)557 bool AuditChangedFiles(EvalMode mode, StringSet **audited_files)
558 {
559 assert((mode == EVAL_MODE_SIMULATE_MANIFEST) ||
560 (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL) ||
561 (mode == EVAL_MODE_SIMULATE_DIFF));
562
563 bool success = true;
564
565 /* Renames are part of the EVAL_MODE_SIMULATE_MANIFEST audit output which
566 * shows changes. */
567 if (mode != EVAL_MODE_SIMULATE_MANIFEST_FULL)
568 {
569 success = ManifestRenamedFiles();
570 }
571
572 const char *action;
573 const char *action_ing;
574 const char *file_category;
575 const char *files_list_file;
576 if (mode == EVAL_MODE_SIMULATE_MANIFEST)
577 {
578 action = "manifest";
579 action_ing = "Manifesting";
580 file_category = "changed";
581 files_list_file = ToChangesChroot(CHROOT_CHANGES_LIST_FILE);
582 }
583 else if (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL)
584 {
585 action = "manifest";
586 action_ing = "Manifesting";
587 file_category = "unmodified";
588 files_list_file = ToChangesChroot(CHROOT_KEPT_LIST_FILE);
589 }
590 else
591 {
592 action = "show diff for";
593 action_ing = "Showing diff for";
594 file_category = "changed";
595 files_list_file = ToChangesChroot(CHROOT_CHANGES_LIST_FILE);
596 }
597
598 /* If the file doesn't exist, there were no changes recorded. */
599 if (access(files_list_file, F_OK) != 0)
600 {
601 Log(LOG_LEVEL_INFO, "No %s files to %s", file_category, action);
602 return true;
603 }
604
605 int fd = safe_open(files_list_file, O_RDONLY);
606 if (fd == -1)
607 {
608 Log(LOG_LEVEL_ERR, "Failed to open the file with list of %s files: %s", file_category, GetErrorStr());
609 return false;
610 }
611
612 Log(LOG_LEVEL_INFO, "%s %s files (in the changes chroot)", action_ing, file_category);
613 if (*audited_files == NULL)
614 {
615 *audited_files = StringSetNew();
616 }
617 bool done = false;
618 while (!done)
619 {
620 /* TODO: read into a PATH_MAX buffer */
621 char *path;
622 int ret = ReadLenPrefixedString(fd, &path);
623 if (ret > 0)
624 {
625 /* Each file should only be audited once. */
626 if (!StringSetContains(*audited_files, path))
627 {
628 if ((mode == EVAL_MODE_SIMULATE_MANIFEST) ||
629 (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL))
630 {
631 success = (success && ManifestFile(path, true));
632 }
633 else
634 {
635 success = (success && DiffFile(path));
636 }
637 StringSetAdd(*audited_files, path);
638 }
639 else
640 {
641 free(path);
642 }
643 }
644 else if (ret == 0)
645 {
646 /* EOF */
647 done = true;
648 }
649 else
650 {
651 Log(LOG_LEVEL_ERR, "Failed to read the list of %s files", file_category);
652 success = false;
653 done = true;
654 }
655 }
656 close(fd);
657 return success;
658 }
659
ManifestChangedFiles(StringSet ** audited_files)660 bool ManifestChangedFiles(StringSet **audited_files)
661 {
662 return AuditChangedFiles(EVAL_MODE_SIMULATE_MANIFEST, audited_files);
663 }
664
ManifestAllFiles(StringSet ** audited_files)665 bool ManifestAllFiles(StringSet **audited_files)
666 {
667 return AuditChangedFiles(EVAL_MODE_SIMULATE_MANIFEST_FULL, audited_files);
668 }
669
DiffChangedFiles(StringSet ** audited_files)670 bool DiffChangedFiles(StringSet **audited_files)
671 {
672 return AuditChangedFiles(EVAL_MODE_SIMULATE_DIFF, audited_files);
673 }
674
675
676 typedef struct PkgOperationRecord_ {
677 char *msg;
678 char *pkg_ver;
679 } PkgOperationRecord;
680
PkgOperationRecordNew(char * msg,char * pkg_ver)681 static PkgOperationRecord *PkgOperationRecordNew(char *msg, char *pkg_ver)
682 {
683 PkgOperationRecord *ret = xmalloc(sizeof(PkgOperationRecord));
684 ret->msg = msg;
685 ret->pkg_ver = pkg_ver;
686
687 return ret;
688 }
689
PkgOperationRecordDestroy(PkgOperationRecord * pkg_op)690 static void PkgOperationRecordDestroy(PkgOperationRecord *pkg_op)
691 {
692 if (pkg_op != NULL)
693 {
694 free(pkg_op->msg);
695 free(pkg_op->pkg_ver);
696 free(pkg_op);
697 }
698 }
699
PkgVersionIsGreater(const char * ver1,const char * ver2)700 static inline bool PkgVersionIsGreater(const char *ver1, const char *ver2)
701 {
702 /* Empty/missing versions should be handled separately based on the
703 * operations they appear in */
704 assert(!NULL_OR_EMPTY(ver1) && !NULL_OR_EMPTY(ver2));
705
706 /* "latest" is greater than any version */
707 if (StringEqual(ver1, "latest"))
708 {
709 return true;
710 }
711
712 /* TODO: do real version comparison */
713 return (StringSafeCompare(ver1, ver2) == 1);
714 }
715
GetPkgOperationMsg(ChrootPkgOperationCode op,const char * pkg_name,const char * pkg_arch,const char * pkg_ver)716 static inline char *GetPkgOperationMsg(ChrootPkgOperationCode op, const char *pkg_name, const char *pkg_arch, const char *pkg_ver)
717 {
718 const char *op_str = "";
719 switch (op)
720 {
721 case CHROOT_PKG_OPERATION_CODE_INSTALL:
722 op_str = "installed";
723 break;
724 case CHROOT_PKG_OPERATION_CODE_REMOVE:
725 op_str = "removed";
726 break;
727 case CHROOT_PKG_OPERATION_CODE_PRESENT:
728 op_str = "present";
729 break;
730 case CHROOT_PKG_OPERATION_CODE_ABSENT:
731 op_str = "absent";
732 break;
733 default:
734 debug_abort_if_reached();
735 }
736
737 char *msg;
738 if (!NULL_OR_EMPTY(pkg_arch) && !NULL_OR_EMPTY(pkg_ver))
739 {
740 xasprintf(&msg, "Package '%s-%s [%s]' would be %s\n", pkg_name, pkg_arch, pkg_ver, op_str);
741 }
742 else if (!NULL_OR_EMPTY(pkg_arch))
743 {
744 xasprintf(&msg, "Package '%s-%s' would be %s\n", pkg_name, pkg_arch, op_str);
745 }
746 else if (!NULL_OR_EMPTY(pkg_ver))
747 {
748 xasprintf(&msg, "Package '%s [%s]' would be %s\n", pkg_name, pkg_ver, op_str);
749 }
750 else
751 {
752 xasprintf(&msg, "Package '%s' would be %s\n", pkg_name, op_str);
753 }
754
755 return msg;
756 }
757
DiffPkgOperations()758 bool DiffPkgOperations()
759 {
760 const char *pkgs_ops_csv_file = ToChangesChroot(CHROOT_PKGS_OPS_FILE);
761 if (access(pkgs_ops_csv_file, F_OK) != 0)
762 {
763 Log(LOG_LEVEL_INFO, "No package operations done by the agent run");
764 return true;
765 }
766
767 FILE *csv_file = safe_fopen(pkgs_ops_csv_file, "r");
768
769 if (csv_file == NULL)
770 {
771 Log(LOG_LEVEL_ERR, "Failed to open the file with package operations records");
772 return false;
773 }
774
775 Map *installed = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
776 Map *removed = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
777 char *line;
778 while ((line = GetCsvLineNext(csv_file)) != NULL)
779 {
780 Seq *fields = SeqParseCsvString(line);
781 free(line);
782 if ((fields == NULL) || (SeqLength(fields) != 4))
783 {
784 Log(LOG_LEVEL_ERR, "Invalid package operation record: '%s'", line);
785 SeqDestroy(fields);
786 continue;
787 }
788
789 /* See RecordPkgOperationInChroot() */
790 const char *op = SeqAt(fields, 0);
791 const char *pkg_name = SeqAt(fields, 1);
792 const char *pkg_ver = SeqAt(fields, 2);
793 const char *pkg_arch = SeqAt(fields, 3);
794
795 /* These two must always be set properly. */
796 assert(!NULL_OR_EMPTY(op));
797 assert(!NULL_OR_EMPTY(pkg_name));
798
799 /* We need to have a key for the map encoding package name and
800 * architecture. Let's use the sequence "-_-" as a separator to avoid
801 * collisions with possible weird package names. */
802 char *name_arch;
803 xasprintf(&name_arch, "%s-_-%s", pkg_name, pkg_arch ? pkg_arch : "");
804
805 if (*op == CHROOT_PKG_OPERATION_CODE_PRESENT)
806 {
807 /* 'present' operation means that the package was requested to be present and it already
808 * was, so installation didn't happen. However, package operations are not properly
809 * simulated, so the package may have been simulate-removed before and the 'present'
810 * operation would in reality mean the package would be installed back. It's only
811 * recorded as a 'present' operation because the removal doesn't happen in simulate mode
812 * and so the package is still seen as present in the system (package cache).
813 *
814 * This means that a 'present' operation after 'remove' operation results in no
815 * difference (the package would be installed back) so the potential message about the
816 * removal should be removed. */
817 MapRemove(removed, name_arch);
818 }
819 else if (*op == CHROOT_PKG_OPERATION_CODE_ABSENT)
820 {
821 /* The same logic as above applies here for an originally absent package that is
822 * installed and then reported as absent again. No diff to report, just remove the
823 * message about the package installation. */
824
825 /* However, if a different specific version is reported as absent than the version that
826 * would have been installed, this removal would not remove the installed package
827 * (because of version mismatch). */
828 if (NULL_OR_EMPTY(pkg_ver))
829 {
830 MapRemove(installed, name_arch);
831 }
832 else
833 {
834 PkgOperationRecord *record = MapGet(installed, name_arch);
835 if ((record != NULL) && StringEqual(pkg_ver, record->pkg_ver))
836 {
837 /* Matching version being removed -> cancel the installation */
838 MapRemove(installed, name_arch);
839 }
840 }
841 }
842 else if (*op == CHROOT_PKG_OPERATION_CODE_INSTALL)
843 {
844 /* Package would be installed if there is no previous 'install' operation record with a
845 * higher version.
846 * OR
847 * Package would be removed and now it would be installed. However, if the 'install'
848 * operation had the same version as what is already present in the system (remove in
849 * simulation mode doesn't remove the package), it would be reported as 'present'
850 * operation. So 'install' operation must mean a newer version than what's present would
851 * be installed. */
852
853 PkgOperationRecord *prev_record = MapGet(installed, name_arch);
854 if ((prev_record == NULL) || PkgVersionIsGreater(pkg_ver, prev_record->pkg_ver))
855 {
856 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_INSTALL,
857 pkg_name, pkg_arch, pkg_ver);
858 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
859 MapInsert(installed, name_arch, record);
860 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
861 }
862
863 /* Package installation cancels a previous removal (if any). */
864 MapRemove(removed, name_arch);
865 }
866 else
867 {
868 assert(*op == CHROOT_PKG_OPERATION_CODE_REMOVE); /* The only option not covered above. */
869
870 /* If there is a previous 'remove' operation record with version specified, prefer that
871 * message over a new message without version specification as the net result would be
872 * the package being removed, in the version that was pressent. */
873 PkgOperationRecord *prev_record = MapGet(removed, name_arch);
874 bool insert_new_msg = ((prev_record == NULL) || (NULL_OR_EMPTY(prev_record->pkg_ver)));
875
876 /* If there is a previous 'install' operation and now there is a 'remove' operation it
877 * means that the package was initially present, then updated by the 'install' operation
878 * and now it is attempted to be removed. If the 'remove' operation specifies a version
879 * then in case the versions match, the net result would be no change (install and
880 * remove). If there is a mismatch between the versions, the installation would happen,
881 * but the removal would fail. If no version is specified for the 'remove' operation. it
882 * would remove the installed package. */
883 if (NULL_OR_EMPTY(pkg_ver))
884 {
885 /* No version specified, remove the installation message (if any). */
886 MapRemove(installed, name_arch);
887 }
888 else
889 {
890 PkgOperationRecord *inst_record = MapGet(installed, name_arch);
891 if ((inst_record != NULL) && (StringEqual(pkg_ver, inst_record->pkg_ver)))
892 {
893 MapRemove(installed, name_arch);
894 }
895 else
896 {
897 /* Keeping the install message, the removal would make no change. */
898 insert_new_msg = false;
899 }
900 }
901
902 if (insert_new_msg)
903 {
904 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_REMOVE,
905 pkg_name, pkg_arch, pkg_ver);
906 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
907 MapInsert(removed, name_arch, record);
908 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
909 }
910 }
911 SeqDestroy(fields);
912 free(name_arch);
913 }
914 fclose(csv_file);
915
916 if ((MapSize(installed) == 0) && (MapSize(removed) == 0))
917 {
918 Log(LOG_LEVEL_INFO, "No differences in installed packages to report");
919
920 MapDestroy(installed);
921 MapDestroy(removed);
922
923 return true;
924 }
925
926 Log(LOG_LEVEL_INFO, "Showing differences in installed packages");
927 MapIterator i = MapIteratorInit(installed);
928 MapKeyValue *item;
929 while ((item = MapIteratorNext(&i)))
930 {
931 PkgOperationRecord *value = item->value;
932 const char *msg = value->msg;
933 puts(msg);
934 }
935 i = MapIteratorInit(removed);
936 while ((item = MapIteratorNext(&i)))
937 {
938 PkgOperationRecord *value = item->value;
939 const char *msg = value->msg;
940 puts(msg);
941 }
942
943 MapDestroy(installed);
944 MapDestroy(removed);
945
946 return true;
947 }
948
ManifestPkgOperations()949 bool ManifestPkgOperations()
950 {
951 const char *pkgs_ops_csv_file = ToChangesChroot(CHROOT_PKGS_OPS_FILE);
952 if (access(pkgs_ops_csv_file, F_OK) != 0)
953 {
954 Log(LOG_LEVEL_INFO, "No package operations done by the agent run");
955 return true;
956 }
957
958 FILE *csv_file = safe_fopen(pkgs_ops_csv_file, "r");
959
960 if (csv_file == NULL)
961 {
962 Log(LOG_LEVEL_ERR, "Failed to open the file with package operations records");
963 return false;
964 }
965
966 Map *present = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
967 Map *absent = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
968 char *line;
969 while ((line = GetCsvLineNext(csv_file)) != NULL)
970 {
971 Seq *fields = SeqParseCsvString(line);
972 free(line);
973 if ((fields == NULL) || (SeqLength(fields) != 4))
974 {
975 Log(LOG_LEVEL_ERR, "Invalid package operation record: '%s'", line);
976 SeqDestroy(fields);
977 continue;
978 }
979
980 /* See RecordPkgOperationInChroot() */
981 const char *op = SeqAt(fields, 0);
982 const char *pkg_name = SeqAt(fields, 1);
983 const char *pkg_ver = SeqAt(fields, 2);
984 const char *pkg_arch = SeqAt(fields, 3);
985
986 /* These two must always be set properly. */
987 assert(!NULL_OR_EMPTY(op));
988 assert(!NULL_OR_EMPTY(pkg_name));
989
990 /* We need to have a key for the map encoding package name and
991 * architecture. Let's use the sequence "-_-" as a separator to avoid
992 * collisions with possible weird package names. */
993 char *name_arch;
994 xasprintf(&name_arch, "%s-_-%s", pkg_name, pkg_arch ? pkg_arch : "");
995
996 if ((*op == CHROOT_PKG_OPERATION_CODE_INSTALL) ||
997 (*op == CHROOT_PKG_OPERATION_CODE_PRESENT))
998 {
999 /* If there is a previous install/present operation, we want to choose the message with
1000 * the higher version or the message which has a specific version (if any). */
1001 PkgOperationRecord *prev_record = MapGet(present, name_arch);
1002 if ((prev_record == NULL) ||
1003 (NULL_OR_EMPTY(prev_record->pkg_ver) && !NULL_OR_EMPTY(pkg_ver)) ||
1004 (!NULL_OR_EMPTY(pkg_ver) && !NULL_OR_EMPTY(prev_record->pkg_ver) &&
1005 PkgVersionIsGreater(pkg_ver, prev_record->pkg_ver)))
1006 {
1007 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_PRESENT,
1008 pkg_name, pkg_arch, pkg_ver);
1009 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
1010 MapInsert(present, name_arch, record);
1011 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
1012 }
1013
1014 /* Cancels any previous remove/absent message. */
1015 MapRemove(absent, name_arch);
1016 }
1017 else
1018 {
1019 assert((*op == CHROOT_PKG_OPERATION_CODE_REMOVE) ||
1020 (*op == CHROOT_PKG_OPERATION_CODE_ABSENT));
1021
1022 /* If there is a 'present' message with a different version than the version specified
1023 * here (if any), we know that the removal would fail and so it should not be
1024 * reported. */
1025 PkgOperationRecord *present_record = MapGet(present, name_arch);
1026 if ((present_record == NULL) ||
1027 NULL_OR_EMPTY(present_record->pkg_ver) || NULL_OR_EMPTY(pkg_ver) ||
1028 StringEqual(present_record->pkg_ver, pkg_ver))
1029 {
1030 /* Remove the 'present' message now that we know the removal would/should work. */
1031 MapRemove(present, name_arch);
1032
1033 /* If there is a previous 'absent' message, we want to use the message with no
1034 * version specification (if any) because it's more generic. */
1035 PkgOperationRecord *prev_record = MapGet(absent, name_arch);
1036 if ((prev_record == NULL) ||
1037 (!NULL_OR_EMPTY(prev_record->pkg_ver) && NULL_OR_EMPTY(pkg_ver)))
1038 {
1039 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_ABSENT,
1040 pkg_name, pkg_arch, pkg_ver);
1041 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
1042 MapInsert(absent, name_arch, record);
1043 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
1044 }
1045 }
1046 }
1047 SeqDestroy(fields);
1048 free(name_arch);
1049 }
1050 fclose(csv_file);
1051
1052 /* If there were package operations (the file with the records exists, which is checked above),
1053 * there must be something to manifest. Otherwise, there's a flaw in the logic above,
1054 * manipulating the maps. */
1055 assert((MapSize(present) != 0) || (MapSize(absent) != 0));
1056
1057 Log(LOG_LEVEL_INFO, "Manifesting present and absent packages");
1058 MapIterator i = MapIteratorInit(present);
1059 MapKeyValue *item;
1060 while ((item = MapIteratorNext(&i)))
1061 {
1062 PkgOperationRecord *value = item->value;
1063 const char *msg = value->msg;
1064 puts(msg);
1065 }
1066 i = MapIteratorInit(absent);
1067 while ((item = MapIteratorNext(&i)))
1068 {
1069 PkgOperationRecord *value = item->value;
1070 const char *msg = value->msg;
1071 puts(msg);
1072 }
1073
1074 MapDestroy(present);
1075 MapDestroy(absent);
1076
1077 return true;
1078 }
1079