]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
systemctl: add "edit --stdin"
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Sat, 2 Dec 2023 15:25:15 +0000 (16:25 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 5 Dec 2023 17:47:19 +0000 (18:47 +0100)
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.

man/systemctl.xml
src/shared/edit-util.c
src/shared/edit-util.h
src/systemctl/systemctl-edit.c
src/systemctl/systemctl.c
src/systemctl/systemctl.h

index c94dca55b9f2a76aecf42c51e3c18f485bb1f222..3fe455cb7ef1215f2da1d89089c05e1f0c4665a1 100644 (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>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>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 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" />
 
index dd77b0e20e3447c0fe775390f1aa0035409a2f51..2662b2bf1b39484bbdcb56484ab426a6b07f120b 100644 (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;
index 83b3df86839f018af81e757d7212cc9f1364321c..29ff81f4e7440affd61732d43fac9c01fe9d2def 100644 (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);
index 367afa20f7af44f0e4fc3d2df0ca0db17021b512..d967b1649ef83c9fb73411da23b80980e47d001f 100644 (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)
index dd6f6c9873e25e611ab703928c7ea7c79fb48c96..2a1d2d7967e0546bcf01f9b812b8cd1dfe64c19b 100644 (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;
 }
index e8ba8f76a07e9b8bc8edeaf0bf464d187c496504..9f63443fde9df7d476d36e7b978e51638d46b47d 100644 (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;