Merge pull request #33979 from YHNdnzj/edit-util-no-duplicate-strip

edit-util: a few cleanups; support networkctl edit --stdin
This commit is contained in:
Luca Boccassi 2024-08-13 01:48:06 +02:00 committed by GitHub
commit 5936b4054a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 52 deletions

View File

@ -448,6 +448,9 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR)
<citerefentry><refentrytitle>systemd.link</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for more information.</para>
<para>If <option>--stdin</option> is specified, the new content will be read from standard input.
In this mode, the old content of the file is discarded.</para>
<xi:include href="version-info.xml" xpointer="v254"/></listitem>
</varlistentry>
@ -608,6 +611,23 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR)
</listitem>
</varlistentry>
<varlistentry>
<term><option>--stdin</option></term>
<listitem>
<para>When used with <command>edit</command>, 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.</para>
<para>Multiple drop-ins may be "edited" in this mode with <option>--drop-in=</option>, and
the same contents will be written to all of them. Otherwise exactly one main configuration file
is expected.</para>
<xi:include href="version-info.xml" xpointer="v257"/>
</listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="json" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />

View File

@ -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;

View File

@ -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.");

View File

@ -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;

View File

@ -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);
}

View File

@ -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.");

View File

@ -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,

View File

@ -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" <<EOF
networkctl edit --stdin --runtime "$NETDEV_NAME" <<EOF
[NetDev]
Name=test2
Kind=dummy
EOF
networkctl cat "$NETDEV_NAME"
networkctl cat "$NETDEV_NAME" | grep -v '^# ' |
cmp - <(printf '%s\n' "[NetDev]" "Name=test2" "Kind=dummy")
cat >"/usr/lib/systemd/network/$LINK_NAME" <<EOF
[Match]