]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/systemctl/systemctl-edit.c
Merge pull request #22791 from keszybz/bootctl-invert-order
[thirdparty/systemd.git] / src / systemctl / systemctl-edit.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include "bus-error.h"
4 #include "copy.h"
5 #include "fd-util.h"
6 #include "fileio.h"
7 #include "fs-util.h"
8 #include "mkdir-label.h"
9 #include "pager.h"
10 #include "path-util.h"
11 #include "pretty-print.h"
12 #include "process-util.h"
13 #include "selinux-util.h"
14 #include "stat-util.h"
15 #include "systemctl-daemon-reload.h"
16 #include "systemctl-edit.h"
17 #include "systemctl-util.h"
18 #include "systemctl.h"
19 #include "terminal-util.h"
20 #include "tmpfile-util.h"
21
22 #define EDIT_MARKER_START "### Anything between here and the comment below will become the new contents of the file"
23 #define EDIT_MARKER_END "### Lines below this comment will be discarded"
24
25 int verb_cat(int argc, char *argv[], void *userdata) {
26 _cleanup_(hashmap_freep) Hashmap *cached_name_map = NULL, *cached_id_map = NULL;
27 _cleanup_(lookup_paths_free) LookupPaths lp = {};
28 _cleanup_strv_free_ char **names = NULL;
29 sd_bus *bus;
30 bool first = true;
31 int r, rc = 0;
32
33 /* Include all units by default — i.e. continue as if the --all option was used */
34 if (strv_isempty(arg_states))
35 arg_all = true;
36
37 if (arg_transport != BUS_TRANSPORT_LOCAL)
38 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot remotely cat units.");
39
40 r = lookup_paths_init(&lp, arg_scope, 0, arg_root);
41 if (r < 0)
42 return log_error_errno(r, "Failed to determine unit paths: %m");
43
44 r = acquire_bus(BUS_MANAGER, &bus);
45 if (r < 0)
46 return r;
47
48 r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
49 if (r < 0)
50 return log_error_errno(r, "Failed to expand names: %m");
51
52 r = maybe_extend_with_unit_dependencies(bus, &names);
53 if (r < 0)
54 return r;
55
56 pager_open(arg_pager_flags);
57
58 STRV_FOREACH(name, names) {
59 _cleanup_free_ char *fragment_path = NULL;
60 _cleanup_strv_free_ char **dropin_paths = NULL;
61
62 r = unit_find_paths(bus, *name, &lp, false, &cached_name_map, &cached_id_map, &fragment_path, &dropin_paths);
63 if (r == -ERFKILL) {
64 printf("%s# Unit %s is masked%s.\n",
65 ansi_highlight_magenta(),
66 *name,
67 ansi_normal());
68 continue;
69 }
70 if (r == -EKEYREJECTED) {
71 printf("%s# Unit %s could not be loaded.%s\n",
72 ansi_highlight_magenta(),
73 *name,
74 ansi_normal());
75 continue;
76 }
77 if (r < 0)
78 return r;
79 if (r == 0) {
80 /* Skip units which have no on-disk counterpart, but propagate the error to the
81 * user */
82 rc = -ENOENT;
83 continue;
84 }
85
86 if (first)
87 first = false;
88 else
89 puts("");
90
91 if (need_daemon_reload(bus, *name) > 0) /* ignore errors (<0), this is informational output */
92 fprintf(stderr,
93 "%s# Warning: %s changed on disk, the version systemd has loaded is outdated.\n"
94 "%s# This output shows the current version of the unit's original fragment and drop-in files.\n"
95 "%s# If fragments or drop-ins were added or removed, they are not properly reflected in this output.\n"
96 "%s# Run 'systemctl%s daemon-reload' to reload units.%s\n",
97 ansi_highlight_red(),
98 *name,
99 ansi_highlight_red(),
100 ansi_highlight_red(),
101 ansi_highlight_red(),
102 arg_scope == UNIT_FILE_SYSTEM ? "" : " --user",
103 ansi_normal());
104
105 r = cat_files(fragment_path, dropin_paths, 0);
106 if (r < 0)
107 return r;
108 }
109
110 return rc;
111 }
112
113 static int create_edit_temp_file(const char *new_path, const char *original_path, char ** const original_unit_paths, char **ret_tmp_fn) {
114 _cleanup_free_ char *t = NULL;
115 int r;
116
117 assert(new_path);
118 assert(ret_tmp_fn);
119
120 r = tempfn_random(new_path, NULL, &t);
121 if (r < 0)
122 return log_error_errno(r, "Failed to determine temporary filename for \"%s\": %m", new_path);
123
124 r = mkdir_parents_label(new_path, 0755);
125 if (r < 0)
126 return log_error_errno(r, "Failed to create directories for \"%s\": %m", new_path);
127
128 if (original_path) {
129 r = mac_selinux_create_file_prepare(new_path, S_IFREG);
130 if (r < 0)
131 return r;
132
133 r = copy_file(original_path, t, 0, 0644, 0, 0, COPY_REFLINK);
134 if (r == -ENOENT) {
135 r = touch(t);
136 mac_selinux_create_file_clear();
137 if (r < 0)
138 return log_error_errno(r, "Failed to create temporary file \"%s\": %m", t);
139 } else {
140 mac_selinux_create_file_clear();
141 if (r < 0)
142 return log_error_errno(r, "Failed to create temporary file for \"%s\": %m", new_path);
143 }
144 } else if (original_unit_paths) {
145 _cleanup_free_ char *new_contents = NULL;
146 _cleanup_fclose_ FILE *f = NULL;
147
148 r = mac_selinux_create_file_prepare(new_path, S_IFREG);
149 if (r < 0)
150 return r;
151
152 f = fopen(t, "we");
153 mac_selinux_create_file_clear();
154 if (!f)
155 return log_error_errno(errno, "Failed to open \"%s\": %m", t);
156
157 r = fchmod(fileno(f), 0644);
158 if (r < 0)
159 return log_error_errno(errno, "Failed to change mode of \"%s\": %m", t);
160
161 r = read_full_file(new_path, &new_contents, NULL);
162 if (r < 0 && r != -ENOENT)
163 return log_error_errno(r, "Failed to read \"%s\": %m", new_path);
164
165 fprintf(f,
166 "### Editing %s\n"
167 EDIT_MARKER_START
168 "\n\n%s%s\n"
169 EDIT_MARKER_END,
170 new_path,
171 strempty(new_contents),
172 new_contents && endswith(new_contents, "\n") ? "" : "\n");
173
174 /* Add a comment with the contents of the original unit files */
175 STRV_FOREACH(path, original_unit_paths) {
176 _cleanup_free_ char *contents = NULL;
177
178 /* Skip the file that's being edited */
179 if (path_equal(*path, new_path))
180 continue;
181
182 r = read_full_file(*path, &contents, NULL);
183 if (r < 0)
184 return log_error_errno(r, "Failed to read \"%s\": %m", *path);
185
186 fprintf(f, "\n\n### %s", *path);
187 if (!isempty(contents)) {
188 _cleanup_free_ char *commented_contents = NULL;
189
190 commented_contents = strreplace(strstrip(contents), "\n", "\n# ");
191 if (!commented_contents)
192 return log_oom();
193 fprintf(f, "\n# %s", commented_contents);
194 }
195 }
196
197 r = fflush_and_check(f);
198 if (r < 0)
199 return log_error_errno(r, "Failed to create temporary file \"%s\": %m", t);
200 }
201
202 *ret_tmp_fn = TAKE_PTR(t);
203
204 return 0;
205 }
206
207 static int get_file_to_edit(
208 const LookupPaths *paths,
209 const char *name,
210 char **ret_path) {
211
212 _cleanup_free_ char *path = NULL, *run = NULL;
213
214 assert(name);
215 assert(ret_path);
216
217 path = path_join(paths->persistent_config, name);
218 if (!path)
219 return log_oom();
220
221 if (arg_runtime) {
222 run = path_join(paths->runtime_config, name);
223 if (!run)
224 return log_oom();
225 }
226
227 if (arg_runtime) {
228 if (access(path, F_OK) >= 0)
229 return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
230 "Refusing to create \"%s\" because it would be overridden by \"%s\" anyway.",
231 run, path);
232
233 *ret_path = TAKE_PTR(run);
234 } else
235 *ret_path = TAKE_PTR(path);
236
237 return 0;
238 }
239
240 static int unit_file_create_new(
241 const LookupPaths *paths,
242 const char *unit_name,
243 const char *suffix,
244 char ** const original_unit_paths,
245 char **ret_new_path,
246 char **ret_tmp_path) {
247
248 _cleanup_free_ char *new_path = NULL, *tmp_path = NULL;
249 const char *ending;
250 int r;
251
252 assert(unit_name);
253 assert(ret_new_path);
254 assert(ret_tmp_path);
255
256 ending = strjoina(unit_name, suffix);
257 r = get_file_to_edit(paths, ending, &new_path);
258 if (r < 0)
259 return r;
260
261 r = create_edit_temp_file(new_path, NULL, original_unit_paths, &tmp_path);
262 if (r < 0)
263 return r;
264
265 *ret_new_path = TAKE_PTR(new_path);
266 *ret_tmp_path = TAKE_PTR(tmp_path);
267
268 return 0;
269 }
270
271 static int unit_file_create_copy(
272 const LookupPaths *paths,
273 const char *unit_name,
274 const char *fragment_path,
275 char **ret_new_path,
276 char **ret_tmp_path) {
277
278 _cleanup_free_ char *new_path = NULL, *tmp_path = NULL;
279 int r;
280
281 assert(fragment_path);
282 assert(unit_name);
283 assert(ret_new_path);
284 assert(ret_tmp_path);
285
286 r = get_file_to_edit(paths, unit_name, &new_path);
287 if (r < 0)
288 return r;
289
290 if (!path_equal(fragment_path, new_path) && access(new_path, F_OK) >= 0) {
291 char response;
292
293 r = ask_char(&response, "yn", "\"%s\" already exists. Overwrite with \"%s\"? [(y)es, (n)o] ", new_path, fragment_path);
294 if (r < 0)
295 return r;
296 if (response != 'y')
297 return log_warning_errno(SYNTHETIC_ERRNO(EKEYREJECTED), "%s skipped.", unit_name);
298 }
299
300 r = create_edit_temp_file(new_path, fragment_path, NULL, &tmp_path);
301 if (r < 0)
302 return r;
303
304 *ret_new_path = TAKE_PTR(new_path);
305 *ret_tmp_path = TAKE_PTR(tmp_path);
306
307 return 0;
308 }
309
310 static int run_editor(char **paths) {
311 int r;
312
313 assert(paths);
314
315 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
316 if (r < 0)
317 return r;
318 if (r == 0) {
319 char **editor_args = NULL;
320 size_t n_editor_args = 0, i = 1, argc;
321 const char **args, *editor, *p;
322
323 argc = strv_length(paths)/2 + 1;
324
325 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL. If
326 * neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute well known
327 * editors. */
328 editor = getenv("SYSTEMD_EDITOR");
329 if (!editor)
330 editor = getenv("EDITOR");
331 if (!editor)
332 editor = getenv("VISUAL");
333
334 if (!isempty(editor)) {
335 editor_args = strv_split(editor, WHITESPACE);
336 if (!editor_args) {
337 (void) log_oom();
338 _exit(EXIT_FAILURE);
339 }
340 n_editor_args = strv_length(editor_args);
341 argc += n_editor_args - 1;
342 }
343
344 args = newa(const char*, argc + 1);
345
346 if (n_editor_args > 0) {
347 args[0] = editor_args[0];
348 for (; i < n_editor_args; i++)
349 args[i] = editor_args[i];
350 }
351
352 STRV_FOREACH_PAIR(original_path, tmp_path, paths)
353 args[i++] = *tmp_path;
354 args[i] = NULL;
355
356 if (n_editor_args > 0)
357 execvp(args[0], (char* const*) args);
358
359 FOREACH_STRING(p, "editor", "nano", "vim", "vi") {
360 args[0] = p;
361 execvp(p, (char* const*) args);
362 /* We do not fail if the editor doesn't exist because we want to try each one of them
363 * before failing. */
364 if (errno != ENOENT) {
365 log_error_errno(errno, "Failed to execute %s: %m", editor);
366 _exit(EXIT_FAILURE);
367 }
368 }
369
370 log_error("Cannot edit unit(s), no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
371 _exit(EXIT_FAILURE);
372 }
373
374 return 0;
375 }
376
377 static int find_paths_to_edit(sd_bus *bus, char **names, char ***paths) {
378 _cleanup_(hashmap_freep) Hashmap *cached_name_map = NULL, *cached_id_map = NULL;
379 _cleanup_(lookup_paths_free) LookupPaths lp = {};
380 int r;
381
382 assert(names);
383 assert(paths);
384
385 r = lookup_paths_init(&lp, arg_scope, 0, arg_root);
386 if (r < 0)
387 return r;
388
389 STRV_FOREACH(name, names) {
390 _cleanup_free_ char *path = NULL, *new_path = NULL, *tmp_path = NULL, *tmp_name = NULL;
391 _cleanup_strv_free_ char **unit_paths = NULL;
392 const char *unit_name;
393
394 r = unit_find_paths(bus, *name, &lp, false, &cached_name_map, &cached_id_map, &path, &unit_paths);
395 if (r == -EKEYREJECTED) {
396 /* If loading of the unit failed server side complete, then the server won't tell us
397 * the unit file path. In that case, find the file client side. */
398 log_debug_errno(r, "Unit '%s' was not loaded correctly, retrying client-side.", *name);
399 r = unit_find_paths(bus, *name, &lp, true, &cached_name_map, &cached_id_map, &path, &unit_paths);
400 }
401 if (r == -ERFKILL)
402 return log_error_errno(r, "Unit '%s' masked, cannot edit.", *name);
403 if (r < 0)
404 return r;
405
406 if (!path) {
407 if (!arg_force) {
408 log_info("Run 'systemctl edit%s --force --full %s' to create a new unit.",
409 arg_scope == UNIT_FILE_GLOBAL ? " --global" :
410 arg_scope == UNIT_FILE_USER ? " --user" : "",
411 *name);
412 return -ENOENT;
413 }
414
415 /* Create a new unit from scratch */
416 unit_name = *name;
417 r = unit_file_create_new(&lp, unit_name,
418 arg_full ? NULL : ".d/override.conf",
419 NULL, &new_path, &tmp_path);
420 } else {
421 unit_name = basename(path);
422 /* We follow unit aliases, but we need to propagate the instance */
423 if (unit_name_is_valid(*name, UNIT_NAME_INSTANCE) &&
424 unit_name_is_valid(unit_name, UNIT_NAME_TEMPLATE)) {
425 _cleanup_free_ char *instance = NULL;
426
427 r = unit_name_to_instance(*name, &instance);
428 if (r < 0)
429 return r;
430
431 r = unit_name_replace_instance(unit_name, instance, &tmp_name);
432 if (r < 0)
433 return r;
434
435 unit_name = tmp_name;
436 }
437
438 if (arg_full)
439 r = unit_file_create_copy(&lp, unit_name, path, &new_path, &tmp_path);
440 else {
441 r = strv_prepend(&unit_paths, path);
442 if (r < 0)
443 return log_oom();
444
445 r = unit_file_create_new(&lp, unit_name, ".d/override.conf", unit_paths, &new_path, &tmp_path);
446 }
447 }
448 if (r < 0)
449 return r;
450
451 r = strv_push_pair(paths, new_path, tmp_path);
452 if (r < 0)
453 return log_oom();
454
455 new_path = tmp_path = NULL;
456 }
457
458 return 0;
459 }
460
461 static int trim_edit_markers(const char *path) {
462 _cleanup_free_ char *contents = NULL;
463 char *contents_start = NULL;
464 const char *contents_end = NULL;
465 size_t size;
466 int r;
467
468 /* Trim out the lines between the two markers */
469 r = read_full_file(path, &contents, NULL);
470 if (r < 0)
471 return log_error_errno(r, "Failed to read temporary file \"%s\": %m", path);
472
473 size = strlen(contents);
474
475 contents_start = strstr(contents, EDIT_MARKER_START);
476 if (contents_start)
477 contents_start += strlen(EDIT_MARKER_START);
478 else
479 contents_start = contents;
480
481 contents_end = strstr(contents_start, EDIT_MARKER_END);
482 if (contents_end)
483 strshorten(contents_start, contents_end - contents_start);
484
485 contents_start = strstrip(contents_start);
486
487 /* Write new contents if the trimming actually changed anything */
488 if (strlen(contents) != size) {
489 r = write_string_file(path, contents_start, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
490 if (r < 0)
491 return log_error_errno(r, "Failed to modify temporary file \"%s\": %m", path);
492 }
493
494 return 0;
495 }
496
497 int verb_edit(int argc, char *argv[], void *userdata) {
498 _cleanup_(lookup_paths_free) LookupPaths lp = {};
499 _cleanup_strv_free_ char **names = NULL;
500 _cleanup_strv_free_ char **paths = NULL;
501 sd_bus *bus;
502 int r;
503
504 if (!on_tty())
505 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units if not on a tty.");
506
507 if (arg_transport != BUS_TRANSPORT_LOCAL)
508 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units remotely.");
509
510 r = lookup_paths_init(&lp, arg_scope, 0, arg_root);
511 if (r < 0)
512 return log_error_errno(r, "Failed to determine unit paths: %m");
513
514 r = mac_selinux_init();
515 if (r < 0)
516 return r;
517
518 r = acquire_bus(BUS_MANAGER, &bus);
519 if (r < 0)
520 return r;
521
522 r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
523 if (r < 0)
524 return log_error_errno(r, "Failed to expand names: %m");
525 if (strv_isempty(names))
526 return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No units matched the specified patterns.");
527
528 STRV_FOREACH(tmp, names) {
529 r = unit_is_masked(bus, &lp, *tmp);
530 if (r < 0)
531 return r;
532 if (r > 0)
533 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit %s: unit is masked.", *tmp);
534 }
535
536 r = find_paths_to_edit(bus, names, &paths);
537 if (r < 0)
538 return r;
539
540 if (strv_isempty(paths))
541 return -ENOENT;
542
543 r = run_editor(paths);
544 if (r < 0)
545 goto end;
546
547 STRV_FOREACH_PAIR(original, tmp, paths) {
548 /* If the temporary file is empty we ignore it. This allows the user to cancel the
549 * modification. */
550 r = trim_edit_markers(*tmp);
551 if (r < 0)
552 continue;
553
554 if (null_or_empty_path(*tmp)) {
555 log_warning("Editing \"%s\" canceled: temporary file is empty.", *original);
556 continue;
557 }
558
559 r = rename(*tmp, *original);
560 if (r < 0) {
561 r = log_error_errno(errno, "Failed to rename \"%s\" to \"%s\": %m", *tmp, *original);
562 goto end;
563 }
564 }
565
566 r = 0;
567
568 if (!arg_no_reload && !install_client_side()) {
569 r = daemon_reload(ACTION_RELOAD, /* graceful= */ false);
570 if (r > 0)
571 r = 0;
572 }
573
574 end:
575 STRV_FOREACH_PAIR(original, tmp, paths) {
576 (void) unlink(*tmp);
577
578 /* Removing empty dropin dirs */
579 if (!arg_full) {
580 _cleanup_free_ char *dir = NULL;
581
582 dir = dirname_malloc(*original);
583 if (!dir)
584 return log_oom();
585
586 /* No need to check if the dir is empty, rmdir does nothing if it is not the case. */
587 (void) rmdir(dir);
588 }
589 }
590
591 return r;
592 }