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