systemctl: add "edit --stdin"

This is a fancy wrapper around "cat <<EOF", but:
- the user doesn't need to figure out the file name,
- parent directories are created automatically,
- daemon-reload is implied,
so it's a convenient way to create units or drop-ins.

Closes https://github.com/systemd/systemd/issues/21862.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2023-12-02 16:25:15 +01:00
parent 232f017b1a
commit 329050c5e2
6 changed files with 95 additions and 39 deletions

View file

@ -1177,38 +1177,40 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<term><command>edit <replaceable>UNIT</replaceable></command></term>
<listitem>
<para>Edit a drop-in snippet or a whole replacement file if
<option>--full</option> is specified, to extend or override the
specified unit.</para>
<para>Edit or replace a drop-in snippet or the main unit file, to extend or override the
definition of the specified unit.</para>
<para>Depending on whether <option>--system</option> (the default),
<option>--user</option>, or <option>--global</option> is specified,
this command creates a drop-in file for each unit either for the system,
for the calling user, or for all futures logins of all users. Then,
the editor (see the "Environment" section below) is invoked on
temporary files which will be written to the real location if the
editor exits successfully.</para>
<para>Depending on whether <option>--system</option> (the default), <option>--user</option>, or
<option>--global</option> is specified, this command will operate on the system unit files, unit
files for the calling user, or the unit files shared between all users.</para>
<para>The editor (see the "Environment" section below) is invoked on temporary files which will
be written to the real location if the editor exits successfully. After the editing is finished,
configuration is reloaded, equivalent to <command>systemctl daemon-reload --system</command> or
<command>systemctl daemon-reload --user</command>. For <command>edit --global</command>, the
reload is not performed and the edits will take effect only for subsequent logins (or after a
reload is requested in a different way).</para>
<para>If <option>--full</option> is specified, a replacement for the main unit file will be
created or edited. Otherwise, a drop-in file will be created or edited.</para>
<para>If <option>--drop-in=</option> is specified, the given drop-in file name
will be used instead of the default <filename>override.conf</filename>.</para>
<para>If <option>--full</option> is specified, this will copy the
original units instead of creating drop-in files.</para>
<para>If <option>--force</option> is specified and any units do
not already exist, new unit files will be opened for editing.</para>
<para>The unit must exist, i.e. its main unit file must be present. If <option>--force</option>
is specified, this requirement is ignored and a new unit may be created (with
<option>--full</option>), or a drop-in for a nonexistent unit may be crated.</para>
<para>If <option>--runtime</option> is specified, the changes will
be made temporarily in <filename>/run/</filename> and they will be
lost on the next reboot.</para>
<para>If <option>--stdin</option> is specified, the new contents will be read from standard
input. In this mode, the old contents of the file are discarded.</para>
<para>If the temporary file is empty upon exit, the modification of
the related unit is canceled.</para>
<para>After the units have been edited, systemd configuration is
reloaded (in a way that is equivalent to <command>daemon-reload</command>).
</para>
<para>Note that this command cannot be used to remotely edit units
and that you cannot temporarily edit units which are in
<filename>/etc/</filename>, since they take precedence over
@ -2764,6 +2766,27 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
</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
completely replaced. This is useful to "edit" unit files from scripts:</para>
<programlisting>$ systemctl edit --drop-in=limits.conf --stdin some-service.service &lt;&lt;EOF
[Unit]
AllowedCPUs=7,11
EOF
</programlisting>
<para>Multiple drop-ins may be "edited" in this mode; the same contents will be written to all of
them.</para>
<xi:include href="version-info.xml" xpointer="v256"/>
</listitem>
</varlistentry>
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="user-system-options.xml" xpointer="machine" />

View file

@ -178,7 +178,7 @@ static int populate_edit_temp_file(EditFile *e, FILE *f, const char *filename) {
return 0;
}
static int create_edit_temp_file(EditFile *e) {
static int create_edit_temp_file(EditFile *e, const char *contents, size_t contents_size) {
_cleanup_(unlink_and_freep) char *temp = NULL;
_cleanup_fclose_ FILE *f = NULL;
int r;
@ -187,6 +187,7 @@ static int create_edit_temp_file(EditFile *e) {
assert(e->context);
assert(e->path);
assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end));
assert(contents || contents_size == 0);
if (e->temp)
return 0;
@ -202,9 +203,15 @@ static int create_edit_temp_file(EditFile *e) {
if (fchmod(fileno(f), 0644) < 0)
return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp);
r = populate_edit_temp_file(e, f, temp);
if (r < 0)
return r;
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': %m", temp);
} else {
r = populate_edit_temp_file(e, f, temp);
if (r < 0)
return r;
}
r = fflush_and_check(f);
if (r < 0)
@ -310,7 +317,7 @@ static int strip_edit_temp_file(EditFile *e) {
if (!tmp)
return log_oom();
if (e->context->marker_start) {
if (e->context->marker_start && !e->context->stdin) {
/* Trim out the lines between the two markers */
char *contents_start, *contents_end;
@ -349,6 +356,8 @@ static int strip_edit_temp_file(EditFile *e) {
}
int do_edit_files_and_install(EditFileContext *context) {
_cleanup_free_ char *data = NULL;
size_t data_size = 0;
int r;
assert(context);
@ -356,33 +365,41 @@ int do_edit_files_and_install(EditFileContext *context) {
if (context->n_files == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
FOREACH_ARRAY(i, context->files, context->n_files) {
r = create_edit_temp_file(i);
if (context->stdin) {
r = read_full_stream(stdin, &data, &data_size);
if (r < 0)
return log_error_errno(r, "Failed to read stdin: %m");
}
FOREACH_ARRAY(editfile, context->files, context->n_files) {
r = create_edit_temp_file(editfile, data, data_size);
if (r < 0)
return r;
}
r = run_editor(context);
if (r < 0)
return r;
if (!context->stdin) {
r = run_editor(context);
if (r < 0)
return r;
}
FOREACH_ARRAY(i, context->files, context->n_files) {
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(i);
r = strip_edit_temp_file(editfile);
if (r < 0)
return r;
if (r == 0) /* temp file doesn't carry actual changes, ignoring */
continue;
r = RET_NERRNO(rename(i->temp, i->path));
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",
i->temp,
i->path);
i->temp = mfree(i->temp);
editfile->temp,
editfile->path);
editfile->temp = mfree(editfile->temp);
log_info("Successfully installed edited file '%s'.", i->path);
log_info("Successfully installed edited file '%s'.", editfile->path);
}
return 0;

View file

@ -24,7 +24,8 @@ struct EditFileContext {
const char *marker_start;
const char *marker_end;
bool remove_parent;
bool overwrite_with_origin; /* whether to always overwrite target with original file */
bool overwrite_with_origin; /* Always overwrite target with original file. */
bool stdin; /* Read contents from stdin instead of launching an editor. */
};
void edit_file_context_done(EditFileContext *context);

View file

@ -317,12 +317,13 @@ int verb_edit(int argc, char *argv[], void *userdata) {
.marker_end = DROPIN_MARKER_END,
.remove_parent = !arg_full,
.overwrite_with_origin = true,
.stdin = arg_stdin,
};
_cleanup_strv_free_ char **names = NULL;
sd_bus *bus;
int r;
if (!on_tty())
if (!on_tty() && !arg_stdin)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit units if not on a tty.");
if (arg_transport != BUS_TRANSPORT_LOCAL)
@ -342,6 +343,10 @@ int verb_edit(int argc, char *argv[], void *userdata) {
if (strv_isempty(names))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No units matched the specified patterns.");
if (arg_stdin && arg_full && strv_length(names) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"With 'edit --stdin --full', exactly one unit for editing must be specified.");
STRV_FOREACH(tmp, names) {
r = unit_is_masked(bus, *tmp);
if (r < 0)

View file

@ -103,6 +103,7 @@ bool arg_kill_value_set = false;
char *arg_root = NULL;
char *arg_image = NULL;
usec_t arg_when = 0;
bool arg_stdin = false;
const char *arg_reboot_argument = NULL;
enum action arg_action = ACTION_SYSTEMCTL;
BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
@ -335,6 +336,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"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
@ -461,6 +463,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
ARG_NO_WARN,
ARG_DROP_IN,
ARG_WHEN,
ARG_STDIN,
};
static const struct option options[] = {
@ -527,6 +530,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
{ "marked", no_argument, NULL, ARG_MARKED },
{ "drop-in", required_argument, NULL, ARG_DROP_IN },
{ "when", required_argument, NULL, ARG_WHEN },
{ "stdin", no_argument, NULL, ARG_STDIN },
{}
};
@ -1017,6 +1021,10 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
break;
case ARG_STDIN:
arg_stdin = true;
break;
case '.':
/* Output an error mimicking getopt, and print a hint afterwards */
log_error("%s: invalid option -- '.'", program_invocation_name);
@ -1067,7 +1075,8 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
}
if (arg_image && arg_root)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Please specify either --root= or --image=, the combination of both is not supported.");
return 1;
}

View file

@ -83,6 +83,7 @@ extern int arg_kill_value;
extern bool arg_kill_value_set;
extern char *arg_root;
extern usec_t arg_when;
extern bool arg_stdin;
extern const char *arg_reboot_argument;
extern enum action arg_action;
extern BusTransport arg_transport;