diff --git a/man/networkctl.xml b/man/networkctl.xml index 5e2126ff21..e47cf5895c 100644 --- a/man/networkctl.xml +++ b/man/networkctl.xml @@ -448,6 +448,9 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) systemd.link5 for more information. + If is specified, the new content will be read from standard input. + In this mode, the old content of the file is discarded. + @@ -608,6 +611,23 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) + + + + + When used with edit, the contents of the file will be read from standard + input and the editor will not be launched. In this mode, the old contents of the file are + automatically replaced. This is useful to "edit" configuration from scripts, especially so that + drop-in directories are created and populated in one go. + + Multiple drop-ins may be "edited" in this mode with , and + the same contents will be written to all of them. Otherwise exactly one main configuration file + is expected. + + + + + diff --git a/src/network/networkctl-config-file.c b/src/network/networkctl-config-file.c index 216e9d4954..1aa03619b7 100644 --- a/src/network/networkctl-config-file.c +++ b/src/network/networkctl-config-file.c @@ -396,23 +396,30 @@ static int reload_daemons(ReloadFlags flags) { } int verb_edit(int argc, char *argv[], void *userdata) { + char **args = ASSERT_PTR(strv_skip(argv, 1)); _cleanup_(edit_file_context_done) EditFileContext context = { .marker_start = DROPIN_MARKER_START, .marker_end = DROPIN_MARKER_END, .remove_parent = !!arg_drop_in, + .stdin = arg_stdin, }; _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; ReloadFlags reload = 0; int r; - if (!on_tty()) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit network config files if not on a tty."); + if (!on_tty() && !arg_stdin) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit network config files interactively if not on a tty."); + + /* Duplicating main configs makes no sense. This also mimics the behavior of systemctl. */ + if (arg_stdin && !arg_drop_in && strv_length(args) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "When 'edit --stdin' without '--drop-in=', exactly one config file for editing must be specified."); r = mac_selinux_init(); if (r < 0) return r; - STRV_FOREACH(name, strv_skip(argv, 1)) { + STRV_FOREACH(name, args) { _cleanup_strv_free_ char **dropins = NULL; _cleanup_free_ char *path = NULL; const char *link_config; diff --git a/src/network/networkctl.c b/src/network/networkctl.c index b51c7aa7e5..6c96a84c46 100644 --- a/src/network/networkctl.c +++ b/src/network/networkctl.c @@ -91,6 +91,7 @@ bool arg_all = false; bool arg_stats = false; bool arg_full = false; bool arg_runtime = false; +bool arg_stdin = false; unsigned arg_lines = 10; char *arg_drop_in = NULL; sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; @@ -3025,6 +3026,7 @@ static int help(void) { " after editing network config\n" " --drop-in=NAME Edit specified drop-in instead of main config file\n" " --runtime Edit runtime config files\n" + " --stdin Read new contents of edited file from stdin\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -3043,6 +3045,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_RELOAD, ARG_DROP_IN, ARG_RUNTIME, + ARG_STDIN, }; static const struct option options[] = { @@ -3058,6 +3061,7 @@ static int parse_argv(int argc, char *argv[]) { { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, { "drop-in", required_argument, NULL, ARG_DROP_IN }, { "runtime", no_argument, NULL, ARG_RUNTIME }, + { "stdin", no_argument, NULL, ARG_STDIN }, {} }; @@ -3092,6 +3096,10 @@ static int parse_argv(int argc, char *argv[]) { arg_runtime = true; break; + case ARG_STDIN: + arg_stdin = true; + break; + case ARG_DROP_IN: if (isempty(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty drop-in file name."); diff --git a/src/network/networkctl.h b/src/network/networkctl.h index 1765a8f83d..d44ee8173e 100644 --- a/src/network/networkctl.h +++ b/src/network/networkctl.h @@ -15,6 +15,7 @@ extern bool arg_all; extern bool arg_stats; extern bool arg_full; extern bool arg_runtime; +extern bool arg_stdin; extern unsigned arg_lines; extern char *arg_drop_in; extern sd_json_format_flags_t arg_json_format_flags; diff --git a/src/shared/edit-util.c b/src/shared/edit-util.c index cfb2828f4e..5dd8595a36 100644 --- a/src/shared/edit-util.c +++ b/src/shared/edit-util.c @@ -111,6 +111,9 @@ int edit_files_add( static int populate_edit_temp_file(EditFile *e, FILE *f, const char *filename) { assert(e); + assert(e->context); + assert(!e->context->stdin); + assert(e->path); assert(f); assert(filename); @@ -197,6 +200,7 @@ static int create_edit_temp_file(EditFile *e, const char *contents, size_t conte assert(e->path); assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end)); assert(contents || contents_size == 0); + assert(e->context->stdin == !!contents); if (e->temp) return 0; @@ -215,7 +219,7 @@ static int create_edit_temp_file(EditFile *e, const char *contents, size_t conte if (e->context->stdin) { if (fwrite(contents, 1, contents_size, f) != contents_size) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to copy input to temporary file '%s'.", temp); + "Failed to write stdin data to temporary file '%s'.", temp); } else { r = populate_edit_temp_file(e, f, temp); if (r < 0) @@ -236,23 +240,22 @@ static int run_editor_child(const EditFileContext *context) { const char *editor; int r; + assert(context); + assert(context->n_files >= 1); + /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL. * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute * well known editors. */ - editor = getenv("SYSTEMD_EDITOR"); - if (!editor) - editor = getenv("EDITOR"); - if (!editor) - editor = getenv("VISUAL"); + FOREACH_STRING(e, "SYSTEMD_EDITOR", "EDITOR", "VISUAL") { + editor = empty_to_null(getenv(e)); + if (editor) + break; + } - if (!isempty(editor)) { - _cleanup_strv_free_ char **editor_args = NULL; - - editor_args = strv_split(editor, WHITESPACE); - if (!editor_args) + if (editor) { + args = strv_split(editor, WHITESPACE); + if (!args) return log_oom(); - - args = TAKE_PTR(editor_args); } if (context->n_files == 1 && context->files[0].line > 1) { @@ -268,7 +271,7 @@ static int run_editor_child(const EditFileContext *context) { return log_oom(); } - if (!isempty(editor)) + if (editor) execvp(args[0], (char* const*) args); bool prepended = false; @@ -298,7 +301,7 @@ static int run_editor(const EditFileContext *context) { assert(context); - r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL); + r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG|FORK_LOG|FORK_WAIT, NULL); if (r < 0) return r; if (r == 0) { /* Child */ @@ -342,11 +345,8 @@ static int strip_edit_temp_file(EditFile *e) { } else stripped = strstrip(tmp); - if (isempty(stripped)) { - /* File is empty (has no real changes) */ - log_notice("%s: after editing, new contents are empty, not writing file.", e->path); - return 0; - } + if (isempty(stripped)) + return 0; /* File is empty (has no real changes) */ /* Trim prefix and suffix, but ensure suffixed by single newline */ new_contents = strjoin(stripped, "\n"); @@ -357,16 +357,77 @@ static int strip_edit_temp_file(EditFile *e) { return 1; /* Contents have real changes */ r = write_string_file(e->temp, new_contents, - WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE); + WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE); if (r < 0) return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp); return 1; /* Contents have real changes */ } +static int edit_file_install_one(EditFile *e) { + int r; + + assert(e); + assert(e->path); + assert(e->temp); + + r = strip_edit_temp_file(e); + if (r <= 0) + return r; + + r = RET_NERRNO(rename(e->temp, e->path)); + if (r < 0) + return log_error_errno(r, + "Failed to rename temporary file '%s' to target file '%s': %m", + e->temp, e->path); + e->temp = mfree(e->temp); + + return 1; +} + +static int edit_file_install_one_stdin(EditFile *e, const char *contents, size_t contents_size, int *fd) { + int r; + + assert(e); + assert(e->path); + assert(contents || contents_size == 0); + assert(fd); + + if (contents_size == 0) + return 0; + + if (*fd >= 0) { + r = mkdir_parents_label(e->path, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path); + + r = copy_file_atomic_at(*fd, NULL, AT_FDCWD, e->path, 0644, COPY_REFLINK|COPY_REPLACE|COPY_MAC_CREATE); + if (r < 0) + return log_error_errno(r, "Failed to copy stdin contents to '%s': %m", e->path); + + return 1; + } + + r = create_edit_temp_file(e, contents, contents_size); + if (r < 0) + return r; + + _cleanup_close_ int tfd = open(e->temp, O_PATH|O_CLOEXEC); + if (tfd < 0) + return log_error_errno(errno, "Failed to pin temporary file '%s': %m", e->temp); + + r = edit_file_install_one(e); + if (r <= 0) + return r; + + *fd = TAKE_FD(tfd); + + return 1; +} + int do_edit_files_and_install(EditFileContext *context) { - _cleanup_free_ char *data = NULL; - size_t data_size = 0; + _cleanup_free_ char *stdin_data = NULL; + size_t stdin_size = 0; int r; assert(context); @@ -375,38 +436,40 @@ int do_edit_files_and_install(EditFileContext *context) { return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit."); if (context->stdin) { - r = read_full_stream(stdin, &data, &data_size); + r = read_full_stream(stdin, &stdin_data, &stdin_size); if (r < 0) return log_error_errno(r, "Failed to read stdin: %m"); - } + } else { + FOREACH_ARRAY(editfile, context->files, context->n_files) { + r = create_edit_temp_file(editfile, /* contents = */ NULL, /* contents_size = */ 0); + if (r < 0) + return r; + } - FOREACH_ARRAY(editfile, context->files, context->n_files) { - r = create_edit_temp_file(editfile, data, data_size); - if (r < 0) - return r; - } - - if (!context->stdin) { r = run_editor(context); if (r < 0) return r; } + _cleanup_close_ int stdin_data_fd = -EBADF; + FOREACH_ARRAY(editfile, context->files, context->n_files) { - /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */ - r = strip_edit_temp_file(editfile); + if (context->stdin) { + r = edit_file_install_one_stdin(editfile, stdin_data, stdin_size, &stdin_data_fd); + if (r == 0) { + log_notice("Stripped stdin content is empty, not writing file."); + return 0; + } + } else { + r = edit_file_install_one(editfile); + if (r == 0) { + log_notice("%s: after editing, new contents are empty, not writing file.", + editfile->path); + continue; + } + } if (r < 0) return r; - if (r == 0) /* temp file doesn't carry actual changes, ignoring */ - continue; - - r = RET_NERRNO(rename(editfile->temp, editfile->path)); - if (r < 0) - return log_error_errno(r, - "Failed to rename temporary file '%s' to target file '%s': %m", - editfile->temp, - editfile->path); - editfile->temp = mfree(editfile->temp); log_info("Successfully installed edited file '%s'.", editfile->path); } diff --git a/src/systemctl/systemctl-edit.c b/src/systemctl/systemctl-edit.c index 15398f8364..b7bd3851fd 100644 --- a/src/systemctl/systemctl-edit.c +++ b/src/systemctl/systemctl-edit.c @@ -323,7 +323,7 @@ int verb_edit(int argc, char *argv[], void *userdata) { int r; if (!on_tty() && !arg_stdin) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units if not on a tty."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units interactively if not on a tty."); if (arg_transport != BUS_TRANSPORT_LOCAL) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units remotely."); diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 1e36455cf2..912c1d8301 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -347,7 +347,7 @@ static int systemctl_help(void) { " --drop-in=NAME Edit unit files using the specified drop-in file name\n" " --when=TIME Schedule halt/power-off/reboot/kexec action after\n" " a certain timestamp\n" - " --stdin Read contents of edited file from stdin\n" + " --stdin Read new contents of edited file from stdin\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, diff --git a/test/units/TEST-74-AUX-UTILS.networkctl.sh b/test/units/TEST-74-AUX-UTILS.networkctl.sh index 3e333a2eab..8c62de9155 100755 --- a/test/units/TEST-74-AUX-UTILS.networkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.networkctl.sh @@ -11,7 +11,7 @@ at_exit() { systemctl stop systemd-networkd if [[ -v NETWORK_NAME && -v NETDEV_NAME && -v LINK_NAME ]]; then - rm -fvr {/usr/lib,/etc,/run}/systemd/network/"$NETWORK_NAME" "/usr/lib/systemd/network/$NETDEV_NAME" \ + rm -fvr {/usr/lib,/etc,/run}/systemd/network/"$NETWORK_NAME" "/run/lib/systemd/network/$NETDEV_NAME" \ {/usr/lib,/etc}/systemd/network/"$LINK_NAME" "/etc/systemd/network/${NETWORK_NAME}.d" \ "new" "+4" fi @@ -75,13 +75,14 @@ cmp "+4" "/etc/systemd/network/${NETWORK_NAME}.d/test.conf" networkctl cat "$NETWORK_NAME" | grep '^# ' | cmp - <(printf '%s\n' "# /etc/systemd/network/$NETWORK_NAME" "# /etc/systemd/network/${NETWORK_NAME}.d/test.conf") -cat >"/usr/lib/systemd/network/$NETDEV_NAME" <"/usr/lib/systemd/network/$LINK_NAME" <