mirror of
https://github.com/webmin/webmin.git
synced 2026-06-24 21:10:29 +01:00
This PR adds a standalone Systemd Services and Units module for managing systemd units across system and user scopes. The module keeps systemd-specific behavior separate from the legacy Bootup and Shutdown module and is implemented as standalone `strict`/`warnings` Perl code rather than depending on its existing init helpers. Those helpers intentionally smooth over multiple init systems, while this module keeps systemd-specific file handling, user-manager behavior, ACL checks, and control operations explicit, scoped, and easier to audit. It includes: - Tabbed views for services, timers, sockets, paths, targets, storage, resources, devices, and user units - Guided creation and editing for common unit types, with contextual fields, validation, and help - User-scoped unit management with linger support and safe handling of home-directory unit files - Runtime actions for start, stop, restart, enable, disable, status, logs, properties, dependencies, and system-unit mask/unmask - Drop-in override inventory plus create, edit, and delete flows - Manual unit-file editing with daemon reload reminders and actions - Configurable module behavior, visible tabs, display options, and post-create navigation - Comprehensive ACL controls for system/user scopes, actions, manual edits, drop-ins, linger, reload, backup, and user filters - Safe Webmin user support through a scoped safe ACL preset - Virtualmin integration for granting domain owners access to their own systemd user units - Tests for unit generation, safety checks, ACL behavior, user-unit handling, backup coverage, and Perl::Critic compatibility A companion Virtualmin PR adds template integration so domain owners can be granted scoped access to their own systemd user units when this module is installed.
459 lines
13 KiB
Perl
Executable File
459 lines
13 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Start, stop, inspect or enable a set of systemd units
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
require './systemd-lib.pl'; ## no critic
|
|
|
|
our (%access, %in, %text);
|
|
|
|
# Mass actions are POSTed from the index table or redirected from edit_unit.cgi.
|
|
ReadParse();
|
|
my @sel = split(/\0/, $in{'d'});
|
|
@sel || error($text{'mass_enone'});
|
|
|
|
# Work out whether selections target system or user managers.
|
|
my $user_scope = $in{'scope'} eq 'user' ? 1 : 0;
|
|
my $users_scope = $in{'scope'} eq 'users' ? 1 : 0;
|
|
my $unituser = clean_unit_value($in{'unituser'});
|
|
if ($user_scope) {
|
|
get_user_details($unituser) ||
|
|
error($text{'systemd_euser'});
|
|
}
|
|
if ($in{'return'}) {
|
|
valid_unit_name($in{'return'}) ||
|
|
error($text{'systemd_ename'});
|
|
}
|
|
|
|
# Convert raw checkbox values into validated action records.
|
|
my @units = mass_units(\@sel, $user_scope, $users_scope, $unituser);
|
|
foreach my $u (@units) {
|
|
systemd_can_view_scope(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error($u->{'user_scope'} ? 'pview_user' : 'pview');
|
|
}
|
|
|
|
# Convert submitted buttons into action flags.
|
|
my $start = $in{'start'} ? 1 : 0;
|
|
my $stop = $in{'stop'} ? 1 : 0;
|
|
my $restart = $in{'restart'} ? 1 : 0;
|
|
my $status = $in{'status'} ? 1 : 0;
|
|
my $props = $in{'props'} ? 1 : 0;
|
|
my $deps = $in{'deps'} ? 1 : 0;
|
|
my $logs = $in{'logs'} ? 1 : 0;
|
|
my $enable = $in{'addboot'} ? 1 : 0;
|
|
my $disable = $in{'delboot'} ? 1 : 0;
|
|
my $mask = $in{'mask'} ? 1 : 0;
|
|
my $unmask = $in{'unmask'} ? 1 : 0;
|
|
my $delete = $in{'delete'} ? 1 : 0;
|
|
my $printed_action_result = 0;
|
|
|
|
# Use an unbuffered page because long-running systemctl operations should show
|
|
# progress as each unit completes.
|
|
ui_print_unbuffered_header(undef, $logs ? $text{'systemd_logs'} :
|
|
$deps ? $text{'systemd_deps'} :
|
|
$props ? $text{'systemd_props'} :
|
|
$status ? $text{'systemd_statustitle'} :
|
|
$restart ? $text{'mass_urestart'} :
|
|
$start ? $text{'mass_ustart'} :
|
|
$stop ? $text{'mass_ustop'} :
|
|
$enable ? $text{'mass_usenable'} :
|
|
$disable ? $text{'mass_usdisable'} :
|
|
$mask ? $text{'mass_umask'} :
|
|
$unmask ? $text{'mass_uunmask'} :
|
|
$delete ? $text{'mass_udelete'} :
|
|
$text{'mass_ustop'}, "");
|
|
|
|
# Get status
|
|
if ($status) {
|
|
# Show full systemd status output for selected units.
|
|
foreach my $u (@units) {
|
|
systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pstatus');
|
|
my $s = $u->{'name'};
|
|
|
|
# Status command failures can still return useful output.
|
|
print_action_start(text('systemd_doingstatus',
|
|
mass_unit_label($u)));
|
|
my ($ok, $out) = $u->{'user_scope'} ?
|
|
status_user_unit($u->{'user'}, $s) :
|
|
status_unit($s);
|
|
print ui_tag('pre', html_escape($out)) if ($out);
|
|
print $text{'mass_failed'}, ui_p() if (!$out);
|
|
}
|
|
mass_log('status', \@units);
|
|
}
|
|
|
|
# Get properties
|
|
if ($props) {
|
|
# Show the exact property set systemd reports for selected units.
|
|
foreach my $u (@units) {
|
|
systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pstatus');
|
|
my $s = $u->{'name'};
|
|
|
|
# Properties are read from the selected system or user manager.
|
|
print_action_start(text('systemd_doingprops',
|
|
mass_unit_label($u)));
|
|
my ($ok, $out) = $u->{'user_scope'} ?
|
|
properties_user_unit($u->{'user'}, $s) :
|
|
properties_unit($s);
|
|
print ui_tag('pre', html_escape($out)) if ($out);
|
|
print $text{'mass_failed'}, ui_p() if (!$ok && !$out);
|
|
}
|
|
mass_log('props', \@units);
|
|
}
|
|
|
|
# Get dependencies
|
|
if ($deps) {
|
|
# Show the dependency tree for selected units.
|
|
foreach my $u (@units) {
|
|
systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pstatus');
|
|
my $s = $u->{'name'};
|
|
|
|
# Dependencies come from systemctl in the selected manager scope.
|
|
print_action_start(text('systemd_doingdeps',
|
|
mass_unit_label($u)));
|
|
my ($ok, $out) = $u->{'user_scope'} ?
|
|
dependencies_user_unit($u->{'user'}, $s) :
|
|
dependencies_unit($s);
|
|
print ui_tag('pre', html_escape($out)) if ($out);
|
|
print $text{'mass_failed'}, ui_p() if (!$ok && !$out);
|
|
}
|
|
mass_log('deps', \@units);
|
|
}
|
|
|
|
# Get logs
|
|
if ($logs) {
|
|
# Show recent journal output for selected units.
|
|
foreach my $u (@units) {
|
|
systemd_can_logs(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('plogs');
|
|
my $s = $u->{'name'};
|
|
|
|
# Logs are read through journalctl for both system and user units.
|
|
print_action_start(text('systemd_doinglogs',
|
|
mass_unit_label($u)));
|
|
my ($ok, $out) = $u->{'user_scope'} ?
|
|
logs_user_unit($u->{'user'}, $s) :
|
|
logs_unit($s);
|
|
print ui_tag('pre', html_escape($out)) if ($out);
|
|
print $text{'mass_failed'}, ui_p() if (!$ok && !$out);
|
|
}
|
|
mass_log('logs', \@units);
|
|
}
|
|
|
|
# Stop or restart before any later enable/start work.
|
|
if ($stop || $restart) {
|
|
# Webmin itself cannot be stopped here, but it can be restarted specially.
|
|
$SIG{'TERM'} = 'ignore'; # Restarting webmin may kill this script
|
|
foreach my $u (@units) {
|
|
systemd_can_runtime(\%access, $stop ? 'stop' : 'restart',
|
|
$u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error($stop ? 'pstop' : 'prestart');
|
|
my $s = $u->{'name'};
|
|
my ($ok, $out);
|
|
my $skipped = 0;
|
|
my $is_webmin = !$u->{'user_scope'} && $s eq 'webmin.service';
|
|
|
|
# Stop and restart are mutually exclusive submit actions.
|
|
if ($stop) {
|
|
print_action_start(text('mass_ustopping',
|
|
mass_unit_label($u)));
|
|
if (!$is_webmin) {
|
|
($ok, $out) = $u->{'user_scope'} ?
|
|
stop_user_unit($u->{'user'}, $s) :
|
|
stop_unit($s);
|
|
}
|
|
}
|
|
elsif ($restart) {
|
|
print_action_start(text('mass_urestarting',
|
|
mass_unit_label($u)));
|
|
if (!unit_restartable($s)) {
|
|
($ok, $out) = (1, $text{'mass_enorestart'});
|
|
$skipped = 1;
|
|
}
|
|
elsif (!$is_webmin) {
|
|
($ok, $out) = $u->{'user_scope'} ?
|
|
restart_user_unit($u->{'user'}, $s) :
|
|
restart_unit($s);
|
|
}
|
|
else {
|
|
restart_miniserv();
|
|
}
|
|
}
|
|
|
|
# Keep command output under the final per-unit result.
|
|
if ($is_webmin) {
|
|
print_action_result(1, text('mass_enoallow', $s), 1)
|
|
if ($stop);
|
|
print_action_result(1, undef, 0)
|
|
if ($restart);
|
|
}
|
|
else {
|
|
print_action_result($ok, $out, $skipped);
|
|
}
|
|
}
|
|
mass_log($stop ? 'massstop' : 'massrestart', \@units);
|
|
}
|
|
|
|
# Enable or disable
|
|
if ($enable || $disable) {
|
|
# Enable or disable startup for each selected unit.
|
|
foreach my $u (@units) {
|
|
systemd_can_boot(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pboot');
|
|
my $b = $u->{'name'};
|
|
my ($ok, $out) = (1, undef);
|
|
|
|
# User units use systemctl --user; system units use the system manager.
|
|
if ($enable) {
|
|
print_action_start(text('mass_uenable',
|
|
mass_unit_label($u)));
|
|
if ($u->{'user_scope'}) {
|
|
($ok, $out) =
|
|
enable_user_unit($u->{'user'}, $b);
|
|
}
|
|
else {
|
|
($ok, $out) = enable_unit($b);
|
|
}
|
|
}
|
|
else {
|
|
print_action_start(text('mass_udisable',
|
|
mass_unit_label($u)));
|
|
if ($u->{'user_scope'}) {
|
|
($ok, $out) =
|
|
disable_user_unit($u->{'user'}, $b);
|
|
}
|
|
else {
|
|
($ok, $out) = disable_unit($b);
|
|
}
|
|
}
|
|
|
|
# Keep command output under the final per-unit result.
|
|
print_action_result($ok, $out, startup_change_skipped($out));
|
|
|
|
}
|
|
mass_log($enable ? 'massenable' : 'massdisable', \@units);
|
|
}
|
|
|
|
# Mask or unmask
|
|
if ($mask || $unmask) {
|
|
# Masking prevents activation; unmasking restores normal start behavior.
|
|
foreach my $u (@units) {
|
|
systemd_can_mask(\%access, $u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pmask');
|
|
my $b = $u->{'name'};
|
|
my ($ok, $out);
|
|
|
|
# User units use systemctl --user; system units use the system manager.
|
|
if ($mask) {
|
|
print_action_start(text('mass_umasking',
|
|
mass_unit_label($u)));
|
|
($ok, $out) = $u->{'user_scope'} ?
|
|
mask_user_unit($u->{'user'}, $b) :
|
|
mask_unit($b);
|
|
}
|
|
else {
|
|
print_action_start(text('mass_uunmasking',
|
|
mass_unit_label($u)));
|
|
($ok, $out) = $u->{'user_scope'} ?
|
|
unmask_user_unit($u->{'user'}, $b) :
|
|
unmask_unit($b);
|
|
}
|
|
|
|
# Keep command output under the final per-unit result.
|
|
print_action_result($ok, $out, 0);
|
|
}
|
|
mass_log($mask ? 'massmask' : 'massunmask', \@units);
|
|
}
|
|
|
|
# Delete user units
|
|
if ($delete) {
|
|
# Bulk delete is intentionally limited to user units. System unit
|
|
# deletion stays on the per-unit edit page where the risk is clearer.
|
|
foreach my $u (@units) {
|
|
$u->{'user_scope'} || error($text{'mass_edelete_user'});
|
|
systemd_can_delete(\%access, 1, $u->{'user'}) ||
|
|
systemd_acl_error('pdelete_user');
|
|
my $s = $u->{'name'};
|
|
print_action_start(text('mass_udeleting',
|
|
mass_unit_label($u)));
|
|
disable_user_unit($u->{'user'}, $s);
|
|
stop_user_unit($u->{'user'}, $s);
|
|
my ($ok, $out) = delete_user_unit($u->{'user'}, $s);
|
|
print_action_result($ok, $out, 0);
|
|
}
|
|
mass_log('massdelete', \@units);
|
|
}
|
|
|
|
# Try to start at last
|
|
if ($start) {
|
|
# Start last, so "enable and start" first creates the wanted symlink.
|
|
foreach my $u (@units) {
|
|
systemd_can_runtime(\%access, 'start',
|
|
$u->{'user_scope'}, $u->{'user'}) ||
|
|
systemd_acl_error('pstart');
|
|
my $s = $u->{'name'};
|
|
my ($ok, $out);
|
|
|
|
# Each selected unit is started independently and reported inline.
|
|
print_action_start(text('mass_ustarting',
|
|
mass_unit_label($u)));
|
|
my $skipped = 0;
|
|
if (!unit_startable($s)) {
|
|
($ok, $out) = (1, $text{'mass_enostart'});
|
|
$skipped = 1;
|
|
}
|
|
else {
|
|
($ok, $out) = $u->{'user_scope'} ?
|
|
start_user_unit($u->{'user'}, $s) :
|
|
start_unit($s);
|
|
}
|
|
print_action_result($ok, $out, $skipped);
|
|
}
|
|
mass_log('massstart', \@units);
|
|
}
|
|
|
|
# Return to the unit page when it should still exist; otherwise return to its
|
|
# tab. Transient units can disappear after stop/restart actions.
|
|
if ($in{'return'} && !$in{'returnindex'}) {
|
|
my $dropin = $in{'returndropin'} ? "&dropin=1" : "";
|
|
my $dropfile = $dropin && $in{'returndropfile'} ?
|
|
"&dropfile=".urlize(clean_unit_value($in{'returndropfile'})) :
|
|
"";
|
|
my $return = $user_scope ?
|
|
"edit_unit.cgi?scope=user&unituser=".urlize($unituser).
|
|
"&name=".urlize($in{'return'}).$dropin.$dropfile :
|
|
"edit_unit.cgi?name=".urlize($in{'return'}).$dropin.$dropfile;
|
|
ui_print_footer($return,
|
|
$text{'systemd_return'});
|
|
}
|
|
else {
|
|
my $u = $units[0];
|
|
my $return = index_url($u->{'name'}, $u->{'user_scope'},
|
|
$user_scope ? $unituser : undef);
|
|
ui_print_footer($return, $text{'index_return'});
|
|
}
|
|
|
|
# print_action_start(message)
|
|
# Prints the first progress line for a unit action.
|
|
sub print_action_start
|
|
{
|
|
my ($msg) = @_;
|
|
if ($printed_action_result) {
|
|
print ui_tag('div', '', { 'class' => 'systemd-action-break',
|
|
'style' => 'height: 1em;' }), "\n";
|
|
$printed_action_result = 0;
|
|
}
|
|
print ui_tag('span', $msg, { 'data-first-print' => undef });
|
|
print ui_br(), "\n";
|
|
return;
|
|
}
|
|
|
|
# print_action_result(ok, output, skipped, html)
|
|
# Prints the final result line with command output folded underneath it.
|
|
sub print_action_result
|
|
{
|
|
my ($ok, $out, $skipped, $html) = @_;
|
|
my $status = $skipped ? $text{'mass_skipped'} :
|
|
$ok ? $text{'mass_ok'} : $text{'mass_failed'};
|
|
my $title = ui_tag('span', html_escape($status),
|
|
{ 'data-second-print' => undef });
|
|
if (!defined($out) || $out eq "") {
|
|
print $title, "\n";
|
|
$printed_action_result = 1;
|
|
return;
|
|
}
|
|
|
|
# Keep successful output quiet, but open failures for immediate diagnosis.
|
|
my $content = $out;
|
|
$content = $html ? $content :
|
|
ui_tag('pre', html_escape($content),
|
|
{ 'style' => 'margin-left: 10px;' });
|
|
print ui_details({
|
|
'html' => 1,
|
|
'title' => $title,
|
|
'content' => $content,
|
|
'class' => 'inline inlined',
|
|
}, !$ok && !$skipped);
|
|
print "\n";
|
|
$printed_action_result = 1;
|
|
return;
|
|
}
|
|
|
|
# mass_units(selected, user-scope, users-scope, user)
|
|
# Converts selected checkbox values into action records with optional owners.
|
|
sub mass_units
|
|
{
|
|
my ($selected, $user_scope, $users_scope, $unituser) = @_;
|
|
my @rv;
|
|
|
|
# The user-units tab packs owner and unit name into one checkbox value.
|
|
if ($users_scope) {
|
|
foreach my $raw (@$selected) {
|
|
my ($encuser, $encname) = split(/\t/, $raw, 2);
|
|
defined($encuser) && defined($encname) ||
|
|
error($text{'systemd_euser'});
|
|
my $user = clean_unit_value(un_urlize($encuser));
|
|
my $name = un_urlize($encname);
|
|
get_user_details($user) ||
|
|
error($text{'systemd_euser'});
|
|
valid_unit_name($name) ||
|
|
error($text{'systemd_ename'});
|
|
push(@rv, { 'name' => $name,
|
|
'user' => $user,
|
|
'user_scope' => 1 });
|
|
}
|
|
}
|
|
else {
|
|
# System-unit rows and single-user edit actions submit plain unit names.
|
|
foreach my $name (@$selected) {
|
|
valid_unit_name($name) ||
|
|
error($text{'systemd_ename'});
|
|
push(@rv, { 'name' => $name,
|
|
'user' => $unituser,
|
|
'user_scope' => $user_scope });
|
|
}
|
|
}
|
|
return @rv;
|
|
}
|
|
|
|
# mass_unit_label(unit)
|
|
# Returns escaped HTML for a unit name, including owner for user units.
|
|
sub mass_unit_label
|
|
{
|
|
my ($unit) = @_;
|
|
my $name = ui_tag('tt', html_escape($unit->{'name'}));
|
|
return $name if (!$unit->{'user_scope'});
|
|
return text('systemd_unit_for_user', $name,
|
|
ui_tag('tt', html_escape($unit->{'user'})));
|
|
}
|
|
|
|
# mass_log(action, units)
|
|
# Logs mixed system and user unit actions under the correct log type.
|
|
sub mass_log
|
|
{
|
|
my ($action, $units) = @_;
|
|
my @system;
|
|
my %users;
|
|
|
|
# Keep system and user actions separate so the log parser gets owner context.
|
|
foreach my $u (@$units) {
|
|
if ($u->{'user_scope'}) {
|
|
push(@{$users{$u->{'user'}}}, $u->{'name'});
|
|
}
|
|
else {
|
|
push(@system, $u->{'name'});
|
|
}
|
|
}
|
|
|
|
# Group user-unit records by owner to avoid one log line per unit.
|
|
webmin_log($action, 'systemd', join(" ", @system)) if (@system);
|
|
foreach my $user (sort keys %users) {
|
|
webmin_log($action, 'systemd-user', join(" ", @{$users{$user}}),
|
|
{ 'user' => $user });
|
|
}
|
|
}
|