1
0
mirror of https://github.com/systemd/systemd synced 2025-10-05 16:03:15 +02:00

systemd-analyze: added the verb unit-shell to spawn and attach shell

This commit is contained in:
ZIHCO
2025-06-13 19:38:55 +01:00
committed by Luca Boccassi
parent 8a1d134144
commit 9a08000d18
7 changed files with 258 additions and 2 deletions

View File

@@ -70,6 +70,13 @@
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="plain">unit-paths</arg>
</cmdsynopsis>
<cmdsynopsis>
<command>systemd-analyze</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="plain">unit-shell</arg>
<arg choice="plain"><replaceable>SERVICE</replaceable></arg>
<arg choice="opt" rep="repeat"><replaceable>Command</replaceable></arg>
</cmdsynopsis>
<cmdsynopsis>
<command>systemd-analyze</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
@@ -1160,6 +1167,22 @@ LEGEND: M → sys_vendor (LENOVO) ┄ F → product_family (ThinkPad X1 Carbon G
<xi:include href="version-info.xml" xpointer="v258"/>
</refsect2>
<refsect2>
<title><command>systemd-analyze unit-shell <replaceable>SERVICE</replaceable> <optional><replaceable>command</replaceable>...</optional></command></title>
<para>The given command runs on the namespace of the specified running service. If no command is given,
spawn and attach a shell with the namespace to the service.</para>
<example>
<title>Example output</title>
<programlisting>$ systemd-analyze unit-shell systemd-resolved.service ls
bin dev etc home lib lib64 lost+found mnt proc run srv tmp var vmlinuz.old
boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlinuz work</programlisting>
</example>
<xi:include href="version-info.xml" xpointer="v258"/>
</refsect2>
</refsect1>
<refsect1>

View File

@@ -82,6 +82,7 @@ _systemd_analyze() {
[FDSTORE]='fdstore'
[CAPABILITY]='capability'
[TRANSIENT_SETTINGS]='transient-settings'
[UNIT_SHELL]='unit-shell'
)
local CONFIGS='locale.conf systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf
@@ -233,6 +234,13 @@ _systemd_analyze() {
else
comps="$(systemctl --no-legend --no-pager -t help)"
fi
elif __contains_word "$verb" ${VERBS[UNIT_SHELL]}; then
if [[ $cur = -* ]]; then
comps='--help --version'
else
comps=$( __get_services $mode )
fi
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )

View File

@@ -0,0 +1,123 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdio.h>
#include <stdlib.h>
#include "sd-bus.h"
#include "alloc-util.h"
#include "analyze.h"
#include "analyze-unit-shell.h"
#include "bus-error.h"
#include "bus-util.h"
#include "fd-util.h"
#include "log.h"
#include "namespace-util.h"
#include "process-util.h"
#include "runtime-scope.h"
#include "strv.h"
#include "unit-def.h"
#include "unit-name.h"
int verb_unit_shell(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_free_ char *unit = NULL;
int r;
if (arg_transport != BUS_TRANSPORT_LOCAL)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot spawn a unit shell for a remote service");
r = unit_name_mangle_with_suffix(argv[1], "as unit", UNIT_NAME_MANGLE_WARN, ".service", &unit);
if (r < 0)
return log_error_errno(r, "Failed to mangle name '%s': %m", argv[1]);
r = acquire_bus(&bus, /* use_full_bus= */ NULL);
if (r < 0)
return bus_log_connect_error(r, arg_transport, arg_runtime_scope);
_cleanup_free_ char *object = unit_dbus_path_from_name(unit);
if (!object)
return log_oom();
r = sd_bus_get_property(
bus,
"org.freedesktop.systemd1",
object,
"org.freedesktop.systemd1.Service",
"MainPID",
&error,
&reply,
"u");
if (r < 0)
return log_error_errno(r, "Failed to get the main PID of %s: %s", unit, bus_error_message(&error, r));
pid_t pid;
r = sd_bus_message_read(reply, "u", &pid);
if (r < 0)
return log_error_errno(r, "Failed to read the main PID of %s from reply: %m", unit);
_cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF, netns_fd = -EBADF, userns_fd = -EBADF;
r = namespace_open(
pid,
&pidns_fd,
&mntns_fd,
&netns_fd,
&userns_fd,
&root_fd);
if (r < 0)
return log_error_errno(r, "Failed to retrieve FDs of namespaces of %s: %m", unit);
_cleanup_strv_free_ char **args = NULL;
if (argc > 2) {
args = strv_copy(strv_skip(argv, 2));
if (!args)
return log_oom();
}
pid_t child;
r = namespace_fork(
"(unit-shell-ns)",
"(unit-shell)",
/* except_fds= */ NULL,
/* n_except_fds */ 0,
FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL,
pidns_fd,
mntns_fd,
netns_fd,
userns_fd,
root_fd,
&child);
if (r < 0)
return log_error_errno(r, "Failed to fork and enter the namespace of %s: %m", unit);
if (r == 0) {
if (args) {
r = execvp(args[0], args);
if (r < 0)
log_error_errno(errno, "Failed to execute '%s': %m", *args);
} else {
r = execl(DEFAULT_USER_SHELL, "-" DEFAULT_USER_SHELL_NAME, NULL);
if (r < 0)
log_debug_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', ignoring: %m");
if (!streq(DEFAULT_USER_SHELL, "/bin/bash")) {
r = execl("/bin/bash", "-bash", NULL);
if (r < 0)
log_debug_errno(errno, "Failed to execute '/bin/bash', ignoring: %m");
}
if (!streq(DEFAULT_USER_SHELL, "/bin/sh")) {
r = execl("/bin/sh", "-sh", NULL);
if (r < 0)
log_debug_errno(errno, "Failed to execute '/bin/sh', ignoring: %m");
}
log_error_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', '/bin/bash', and '/bin/sh': %m");
}
_exit(EXIT_FAILURE);
}
return wait_for_terminate_and_check(
"(unit-shell)",
child,
WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS);
}

View File

@@ -0,0 +1,4 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
int verb_unit_shell(int argc, char *argv[], void *userdata);

View File

@@ -44,6 +44,7 @@
#include "analyze-timestamp.h"
#include "analyze-unit-files.h"
#include "analyze-unit-paths.h"
#include "analyze-unit-shell.h"
#include "analyze-verify.h"
#include "analyze-verify-util.h"
#include "build.h"
@@ -241,6 +242,8 @@ static int help(int argc, char *argv[], void *userdata) {
" security [UNIT...] Analyze security of unit\n"
" fdstore SERVICE... Show file descriptor store contents of service\n"
" malloc [D-BUS SERVICE...] Dump malloc stats of a D-Bus service\n"
" unit-shell SERVICE [Command]\n"
" Run command on the namespace of the service\n"
"\n%3$sExecutable Analysis:%4$s\n"
" inspect-elf FILE... Parse and print ELF package metadata\n"
"\n%3$sTPM Operations:%4$s\n"
@@ -383,14 +386,68 @@ static int parse_argv(int argc, char *argv[]) {
{}
};
int r, c;
bool reorder = false;
int r, c, unit_shell = -1;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hqH:M:U:m", options, NULL)) >= 0)
/* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
* that checks for GNU extensions in optstring ('-' or '+; at the beginning). */
optind = 0;
for (;;) {
static const char option_string[] = "-hqH:M:U:m";
c = getopt_long(argc, argv, option_string + reorder, options, NULL);
if (c < 0)
break;
switch (c) {
case 1: /* getopt_long() returns 1 if "-" was the first character of the option string, and a
* non-option argument was discovered. */
assert(!reorder);
/* We generally are fine with the fact that getopt_long() reorders the command line, and looks
* for switches after the main verb. However, for "unit-shell" we really don't want that, since we
* want that switches specified after the service name are passed to the program to execute,
* and not processed by us. To make this possible, we'll first invoke getopt_long() with
* reordering disabled (i.e. with the "-" prefix in the option string), looking for the first
* non-option parameter. If it's the verb "unit-shell" we remember its position and continue
* processing options. In this case, as soon as we hit the next non-option argument we found
* the service name, and stop further processing. If the first non-option argument is any other
* verb than "unit-shell" we switch to normal reordering mode and continue processing arguments
* normally. */
if (unit_shell >= 0) {
optind--; /* don't processs this argument, go one step back */
goto done;
}
if (streq(optarg, "unit-shell"))
/* Remember the position of the "unit_shell" verb, and continue processing normally. */
unit_shell = optind - 1;
else {
int saved_optind;
/* Ok, this is some other verb. In this case, turn on reordering again, and continue
* processing normally. */
reorder = true;
/* We changed the option string. getopt_long() only looks at it again if we invoke it
* at least once with a reset option index. Hence, let's reset the option index here,
* then invoke getopt_long() again (ignoring what it has to say, after all we most
* likely already processed it), and the bump the option index so that we read the
* intended argument again. */
saved_optind = optind;
optind = 0;
(void) getopt_long(argc, argv, option_string + reorder, options, NULL);
optind = saved_optind - 1; /* go one step back, process this argument again */
}
break;
case 'h':
return help(0, NULL, NULL);
@@ -602,6 +659,22 @@ static int parse_argv(int argc, char *argv[]) {
default:
assert_not_reached();
}
}
done:
if (unit_shell >= 0) {
char *t;
/* We found the "unit-shell" verb while processing the argument list. Since we turned off reordering of the
* argument list initially let's readjust it now, and move the "unit-shell" verb to the back. */
optind -= 1; /* place the option index where the "unit-shell" verb will be placed */
t = argv[unit_shell];
for (int i = unit_shell; i < optind; i++)
argv[i] = argv[i+1];
argv[optind] = t;
}
if (arg_offline && !streq_ptr(argv[optind], "security"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -684,6 +757,7 @@ static int run(int argc, char *argv[]) {
{ "cat-config", 2, VERB_ANY, 0, verb_cat_config },
{ "unit-files", VERB_ANY, VERB_ANY, 0, verb_unit_files },
{ "unit-paths", 1, 1, 0, verb_unit_paths },
{ "unit-shell", 2, VERB_ANY, 0, verb_unit_shell },
{ "exit-status", VERB_ANY, VERB_ANY, 0, verb_exit_status },
{ "syscall-filter", VERB_ANY, VERB_ANY, 0, verb_syscall_filters },
{ "capability", VERB_ANY, VERB_ANY, 0, verb_capabilities },

View File

@@ -33,6 +33,7 @@ systemd_analyze_sources = files(
'analyze-timestamp.c',
'analyze-unit-files.c',
'analyze-unit-paths.c',
'analyze-unit-shell.c',
'analyze-verify.c',
'analyze.c',
)

View File

@@ -1116,6 +1116,29 @@ systemd-analyze transient-settings mount | grep CPUQuotaPeriodSec
(! systemd-analyze transient-settings service | grep ConditionKernelVersion )
(! systemd-analyze transient-settings service | grep AssertKernelVersion )
# check systemd-analyze unit-shell with a namespaced unit
UNIT_NAME="test-unit-shell.service"
UNIT_FILE="/run/systemd/system/$UNIT_NAME"
cat >"$UNIT_FILE" <<EOF
[Unit]
Description=Test unit for systemd-analyze unit-shell
[Service]
Type=notify
NotifyAccess=all
ExecStart=/bin/sh -c "echo 'Hello from test unit' >/tmp/testfile; systemd-notify --ready; sleep infinity"
PrivateTmp=disconnected
EOF
# Start the service
systemctl start "$UNIT_NAME"
# Wait for the service to be active
systemctl is-active --quiet "$UNIT_NAME"
# Verify the service is active and has a MainPID
MAIN_PID=$(systemctl show -p MainPID --value "$UNIT_NAME")
[ "$MAIN_PID" -gt 0 ]
# Test systemd-analyze unit-shell with a command (cat /tmp/testfile)
OUTPUT=$(systemd-analyze unit-shell "$UNIT_NAME" cat /tmp/testfile)
assert_in "Hello from test unit" "$OUTPUT"
systemd-analyze log-level info
touch /testok