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" <