]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/systemctl/systemctl-edit.c
Merge pull request #22791 from keszybz/bootctl-invert-order
[thirdparty/systemd.git] / src / systemctl / systemctl-edit.c
CommitLineData
db9ecf05 1/* SPDX-License-Identifier: LGPL-2.1-or-later */
daf71ef6
LP
2
3#include "bus-error.h"
4#include "copy.h"
85c5d313 5#include "fd-util.h"
6#include "fileio.h"
daf71ef6 7#include "fs-util.h"
35cd0ba5 8#include "mkdir-label.h"
daf71ef6
LP
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
85c5d313 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
32baf64d 25int verb_cat(int argc, char *argv[], void *userdata) {
b4c527f4 26 _cleanup_(hashmap_freep) Hashmap *cached_name_map = NULL, *cached_id_map = NULL;
daf71ef6
LP
27 _cleanup_(lookup_paths_free) LookupPaths lp = {};
28 _cleanup_strv_free_ char **names = NULL;
daf71ef6
LP
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
384c2c32 56 pager_open(arg_pager_flags);
daf71ef6
LP
57
58 STRV_FOREACH(name, names) {
59 _cleanup_free_ char *fragment_path = NULL;
60 _cleanup_strv_free_ char **dropin_paths = NULL;
61
b4c527f4 62 r = unit_find_paths(bus, *name, &lp, false, &cached_name_map, &cached_id_map, &fragment_path, &dropin_paths);
daf71ef6
LP
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
85c5d313 113static int create_edit_temp_file(const char *new_path, const char *original_path, char ** const original_unit_paths, char **ret_tmp_fn) {
daf71ef6
LP
114 _cleanup_free_ char *t = NULL;
115 int r;
116
117 assert(new_path);
daf71ef6
LP
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
85c5d313 128 if (original_path) {
129 r = mac_selinux_create_file_prepare(new_path, S_IFREG);
130 if (r < 0)
131 return r;
daf71ef6 132
85c5d313 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;
daf71ef6 147
85c5d313 148 r = mac_selinux_create_file_prepare(new_path, S_IFREG);
149 if (r < 0)
150 return r;
daf71ef6 151
85c5d313 152 f = fopen(t, "we");
daf71ef6 153 mac_selinux_create_file_clear();
85c5d313 154 if (!f)
155 return log_error_errno(errno, "Failed to open \"%s\": %m", t);
daf71ef6 156
85c5d313 157 r = fchmod(fileno(f), 0644);
daf71ef6 158 if (r < 0)
85c5d313 159 return log_error_errno(errno, "Failed to change mode of \"%s\": %m", t);
160
be81e45c 161 r = read_full_file(new_path, &new_contents, NULL);
85c5d313 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
be81e45c 182 r = read_full_file(*path, &contents, NULL);
85c5d313 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)) {
8f3e1b9d
DT
188 _cleanup_free_ char *commented_contents = NULL;
189
190 commented_contents = strreplace(strstrip(contents), "\n", "\n# ");
191 if (!commented_contents)
85c5d313 192 return log_oom();
8f3e1b9d 193 fprintf(f, "\n# %s", commented_contents);
85c5d313 194 }
195 }
daf71ef6 196
85c5d313 197 r = fflush_and_check(f);
daf71ef6 198 if (r < 0)
85c5d313 199 return log_error_errno(r, "Failed to create temporary file \"%s\": %m", t);
daf71ef6
LP
200 }
201
202 *ret_tmp_fn = TAKE_PTR(t);
203
204 return 0;
205}
206
207static 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
240static int unit_file_create_new(
241 const LookupPaths *paths,
242 const char *unit_name,
243 const char *suffix,
85c5d313 244 char ** const original_unit_paths,
daf71ef6
LP
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
85c5d313 261 r = create_edit_temp_file(new_path, NULL, original_unit_paths, &tmp_path);
daf71ef6
LP
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
271static 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
85c5d313 300 r = create_edit_temp_file(new_path, fragment_path, NULL, &tmp_path);
daf71ef6
LP
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
310static 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) {
de010b0b 319 char **editor_args = NULL;
daf71ef6
LP
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
377static int find_paths_to_edit(sd_bus *bus, char **names, char ***paths) {
b4c527f4 378 _cleanup_(hashmap_freep) Hashmap *cached_name_map = NULL, *cached_id_map = NULL;
daf71ef6 379 _cleanup_(lookup_paths_free) LookupPaths lp = {};
daf71ef6
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;
85c5d313 391 _cleanup_strv_free_ char **unit_paths = NULL;
daf71ef6
LP
392 const char *unit_name;
393
85c5d313 394 r = unit_find_paths(bus, *name, &lp, false, &cached_name_map, &cached_id_map, &path, &unit_paths);
daf71ef6
LP
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);
e4d22a9f 399 r = unit_find_paths(bus, *name, &lp, true, &cached_name_map, &cached_id_map, &path, &unit_paths);
daf71ef6
LP
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
98199724 406 if (!path) {
daf71ef6
LP
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",
85c5d313 419 NULL, &new_path, &tmp_path);
daf71ef6 420 } else {
daf71ef6
LP
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);
85c5d313 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 }
daf71ef6
LP
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
85c5d313 461static 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 */
be81e45c 469 r = read_full_file(path, &contents, NULL);
85c5d313 470 if (r < 0)
471 return log_error_errno(r, "Failed to read temporary file \"%s\": %m", path);
472
be81e45c
LP
473 size = strlen(contents);
474
85c5d313 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
32baf64d 497int verb_edit(int argc, char *argv[], void *userdata) {
daf71ef6
LP
498 _cleanup_(lookup_paths_free) LookupPaths lp = {};
499 _cleanup_strv_free_ char **names = NULL;
500 _cleanup_strv_free_ char **paths = NULL;
daf71ef6
LP
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");
ffcd6838
ZJS
525 if (strv_isempty(names))
526 return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No units matched the specified patterns.");
daf71ef6
LP
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. */
85c5d313 550 r = trim_edit_markers(*tmp);
551 if (r < 0)
552 continue;
553
daf71ef6
LP
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
623461c1
LP
568 if (!arg_no_reload && !install_client_side()) {
569 r = daemon_reload(ACTION_RELOAD, /* graceful= */ false);
570 if (r > 0)
571 r = 0;
572 }
daf71ef6
LP
573
574end:
575 STRV_FOREACH_PAIR(original, tmp, paths) {
576 (void) unlink(*tmp);
577
578 /* Removing empty dropin dirs */
579 if (!arg_full) {
c2b2df60 580 _cleanup_free_ char *dir = NULL;
daf71ef6
LP
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}