Files
webmin/systemd/index.cgi

524 lines
17 KiB
Perl
Executable File

#!/usr/local/bin/perl
# Display systemd system and user units grouped by unit type.
use strict;
use warnings;
require './systemd-lib.pl'; ## no critic
our (%access, %config, %in, %text);
ReadParse();
# Read query params to restore the selected tab/user context on return links.
# The index is GET-addressable, so every value from %in below is either reduced
# to known values or validated before use, and none is printed raw.
has_command("systemctl") || error($text{'systemd_esystemctl'});
systemd_can_enter_module() || systemd_acl_error('penter');
# Print the page shell before building the tab contents.
ui_print_header(version_title(),
$text{'index_title'}, "", "intro", 1, 1, undef,
action_links());
print index_style();
# Query parameters only choose the active tab and optional user context.
my $scope = $in{'scope'} eq 'user' && tab_visible('user') ? 'user' : '';
my $unituser = clean_unit_value($in{'unituser'});
$unituser ||= systemd_acl_default_user() || "";
my $unituser_details = $unituser ?
get_user_details($unituser) : undef;
if ($scope eq 'user') {
$unituser_details || error($text{'systemd_euser'});
systemd_can_view_user_scope($unituser) ||
systemd_acl_error('pview_user');
}
$unituser = "" if (!$unituser_details);
# Load both system and user units so the visible tab set matches reality.
my @system_units = systemd_can_view_system() ? list_units() : ( );
my @user_units = tab_visible('user') &&
systemd_can_view_user_scope() ?
grep { systemd_acl_user_allowed($_->{'user'}) }
list_all_user_units() : ( );
my @tabs = index_tabs(\@system_units, \@user_units, $unituser);
# Pick a valid active tab. Invalid mode values fall back to the first tab.
my %valid_tabs = map { $_->{'id'}, 1 } @tabs;
my $requested = defined($in{'mode'}) ? $in{'mode'} : "";
my $mode = $requested && $valid_tabs{$requested} ? $requested :
$scope eq 'user' && $valid_tabs{'user'} ? 'user' :
$tabs[0]->{'id'};
my $formno = 0;
# When several unit groups exist, render Webmin tabs around each table.
if (@tabs > 1) {
my @uitabs = map { [ $_->{'id'}, $_->{'title'} ] } @tabs;
print ui_tabs_start(\@uitabs, "mode", $mode, 1);
foreach my $tab (@tabs) {
print ui_tabs_start_tab("mode", $tab->{'id'});
$formno++ if (print_index_tab($tab, $formno));
print ui_tabs_end_tab("mode", $tab->{'id'});
}
print ui_tabs_end(1);
}
else {
# A single unit group does not need tab chrome.
print_index_tab($tabs[0], $formno);
}
print_index_tools();
ui_print_footer("/", $text{'index'});
# version_title()
# Returns the first line of systemctl --version, or a plain fallback.
sub version_title
{
my $systemctl = has_command("systemctl");
return $text{'mode_systemd'} if (!$systemctl);
# Only the first line is useful as a subtitle, for example "systemd 252".
my $out = backquote_command(quotemeta($systemctl)." --version 2>/dev/null");
return $text{'mode_systemd'} if ($? || !defined($out) || $out eq "");
my ($first) = split(/\r?\n/, $out, 2);
$first = clean_unit_value($first);
return $first || $text{'mode_systemd'};
}
# index_style()
# Returns CSS used by the systemd index fragment.
sub index_style
{
# The style is emitted in the body so SPA theme navigation applies it too.
return ui_tag('style',
".systemd_linger_toggle { text-decoration: none; }\n".
".systemd_linger_toggle .ui_text_color { border-bottom: 1px dotted currentColor; }\n",
{ 'type' => 'text/css' });
}
# print_index_tools()
# Prints advanced module actions below the unit tables.
sub print_index_tools
{
my @buttons;
push(@buttons, [ "edit_manual.cgi",
$text{'index_edit_files'},
$text{'index_edit_filesdesc'} ])
if (systemd_acl_bool('manual') ||
systemd_acl_bool('manual_user'));
push(@buttons, [ "dropins.cgi",
$text{'index_dropins'},
$text{'index_dropinsdesc'} ])
if ($config{'show_dropin_inventory'} &&
systemd_can_enter_module());
push(@buttons, [ "restart.cgi",
$text{'index_reload'},
$text{'index_reloaddesc'} ])
if (systemd_can_reload());
return if (!@buttons);
print ui_hr();
print ui_buttons_start();
foreach my $button (@buttons) {
print ui_buttons_row(@$button);
}
print ui_buttons_end();
}
# index_tab_groups()
# Returns the system-unit tab layout and the unit suffixes each tab owns.
sub index_tab_groups
{
return (
{ 'id' => 'service', 'types' => [ 'service' ],
'create' => 'service' },
{ 'id' => 'timer', 'types' => [ 'timer' ],
'create' => 'timer' },
{ 'id' => 'socket', 'types' => [ 'socket' ],
'create' => 'socket' },
{ 'id' => 'path', 'types' => [ 'path' ],
'create' => 'path' },
{ 'id' => 'target', 'types' => [ 'target' ],
'create' => 'target' },
{ 'id' => 'storage', 'types' => [ 'mount', 'automount', 'swap' ],
'create' => 'mount', 'show_type' => 1 },
{ 'id' => 'resources', 'types' => [ 'slice', 'scope' ],
'create' => 'slice', 'show_type' => 1, 'inspect_only' => 1 },
{ 'id' => 'device', 'types' => [ 'device' ],
'inspect_only' => 1, 'selectable' => 0,
'show_unit_state' => 0 },
);
}
# index_type_tab(type)
# Returns the tab id that owns a unit type.
sub index_type_tab
{
my ($type) = @_;
foreach my $tab (index_tab_groups()) {
return $tab->{'id'} if (indexof($type, @{$tab->{'types'}}) >= 0);
}
return;
}
# index_tabs(system-units, user-units, [user])
# Builds tab metadata for system unit types plus one user-units tab.
sub index_tabs
{
my ($system_units, $user_units, $unituser) = @_;
my %by_tab;
# System units are grouped by suffix, with related low-level types combined.
foreach my $u (@$system_units) {
my ($display, $type) = index_name_type($u->{'name'});
$type = 'service' if (!$type);
next if (indexof($type, get_list_unit_types()) < 0);
next if (!unit_visible_on_index($u));
my $tabid = index_type_tab($type);
next if (!$tabid);
$u->{'_display'} = $config{'show_unit_suffixes'} ?
$u->{'name'} : $display;
$u->{'_type'} = $type;
push(@{$by_tab{$tabid}}, $u);
}
# Keep tab ordering stable even when some unit types are absent.
my @tabs;
foreach my $group (index_tab_groups()) {
next if (!tab_visible($group->{'id'}));
next if (!systemd_can_view_system());
my $units = $by_tab{$group->{'id'}} || [ ];
push(@tabs, { %$group,
'title' => index_tab_title($group->{'id'}),
'desc' => index_tab_desc($group->{'id'}),
'units' => $units });
}
# User units share one tab because the owner column distinguishes accounts.
my @visible_user_units;
foreach my $u (@$user_units) {
next if (!unit_visible_on_index($u));
my ($display, $type) = index_name_type($u->{'name'});
$u->{'_display'} = $config{'show_unit_suffixes'} ?
$u->{'name'} : $display;
$u->{'_type'} = $type || 'service';
push(@visible_user_units, $u);
}
$user_units = \@visible_user_units;
if (tab_visible('user') &&
systemd_can_view_user_scope($unituser)) {
push(@tabs, { 'id' => 'user',
'user' => 1,
'unituser' => $unituser,
'title' => $text{'systemd_tab_user'},
'desc' => $text{'systemd_tabdesc_user'},
'units' => $user_units });
}
return @tabs;
}
# print_index_tab(tab, form-number)
# Outputs one tab description and its mass-action table.
sub print_index_tab
{
my ($tab, $formno) = @_;
my $user_tab = $tab->{'user'} ? 1 : 0;
my $can_status = systemd_can_inspect($user_tab, $tab->{'unituser'});
my $can_logs = systemd_can_logs($user_tab, $tab->{'unituser'});
my $can_start = systemd_can_runtime('start',
$user_tab, $tab->{'unituser'});
my $can_stop = systemd_can_runtime('stop',
$user_tab, $tab->{'unituser'});
my $can_restart = systemd_can_runtime('restart',
$user_tab, $tab->{'unituser'});
my $can_boot = systemd_can_boot($user_tab, $tab->{'unituser'});
my $can_mask = $user_tab ? 0 :
systemd_can_mask($user_tab, $tab->{'unituser'});
my $can_delete = $user_tab ?
systemd_can_delete($user_tab, $tab->{'unituser'}) : 0;
my $selectable = exists($tab->{'selectable'}) ? $tab->{'selectable'} : 1;
$selectable &&= $can_status || $can_logs || $can_start || $can_stop ||
$can_restart || $can_boot || $can_mask || $can_delete ? 1 : 0;
my $show_unit_state = exists($tab->{'show_unit_state'}) ?
$tab->{'show_unit_state'} : 1;
my $show_type = !$config{'show_unit_suffixes'} &&
($user_tab || $tab->{'show_type'});
my %linger_cache;
# The create link inherits tab context so the create form opens with the right
# unit type or user-unit mode selected.
my $create_type = $user_tab ? 'service' : $tab->{'create'};
my $create_link = index_create_link($tab, $user_tab, $create_type);
my @links = $selectable ?
( select_all_link("d", $formno),
select_invert_link("d", $formno) ) :
( );
push(@links, $create_link) if ($create_link);
print ui_div($tab->{'desc'});
if (!@{$tab->{'units'}}) {
print ui_tag('p', index_empty_message($tab));
print ui_links_row([ $create_link ]) if ($create_link);
return 0;
}
# Start the mass-action form and keep scope in a hidden field for user units.
print ui_form_start("mass_units.cgi", "post");
print ui_links_row(\@links) if (@links);
print ui_hidden("scope", "users") if ($user_tab);
# Mixed-type tabs only need a type column when unit suffixes are hidden.
my @heads = $selectable ? ( "" ) : ( );
push(@heads, $text{'systemd_name'});
push(@heads, $text{'systemd_desc'}) if ($config{'desc'});
push(@heads, $text{'systemd_type'}) if ($show_type);
push(@heads, $text{'systemd_unit_state'}) if ($show_unit_state);
push(@heads, $text{'systemd_runtime_state'});
push(@heads, $text{'systemd_owner'}, $text{'systemd_linger_status'})
if ($user_tab);
print ui_columns_start(\@heads);
foreach my $u (@{$tab->{'units'}}) {
# Generated units without real files and masked units are shown read-only.
my $editable = $u->{'file'} && -f $u->{'file'} ? 1 : 0;
my $link = index_edit_url($u, $user_tab);
my $title = (!$editable ||
(defined($u->{'boot'}) && $u->{'boot'} == -1) ?
html_escape($u->{'_display'}) :
ui_link($link, html_escape($u->{'_display'})));
my $checkvalue = $user_tab ?
user_unit_selection_value($u->{'user'}, $u->{'name'}) :
$u->{'name'};
# Build the row from common columns first, then append user-only columns.
my @row = $selectable ?
( ui_checkbox("d", $checkvalue, undef) ) :
( );
push(@row, $title);
push(@row, html_escape($u->{'desc'})) if ($config{'desc'});
push(@row, html_escape(index_unit_type_title($u->{'_type'})))
if ($show_type);
push(@row, index_unit_state_column($u->{'unitstate'}))
if ($show_unit_state);
push(@row, index_runtime_state_column(
$u->{'runtime'}, $u->{'substate'}));
if ($user_tab) {
# Linger is per user, so cache it instead of calling loginctl per row.
if (!exists($linger_cache{$u->{'user'}})) {
$linger_cache{$u->{'user'}} =
user_linger_enabled($u->{'user'});
}
my $linger_html = systemd_can_linger($u->{'user'}) ?
linger_toggle_link(
$u->{'user'}, $linger_cache{$u->{'user'}}) :
html_escape($linger_cache{$u->{'user'}} ?
$text{'yes'} : $text{'no'});
push(@row, ui_tag('tt', html_escape($u->{'user'})),
$linger_html);
}
print ui_columns_row(\@row);
}
# Repeat row-selection links below the table for long lists.
print ui_columns_end();
if ($selectable) {
print ui_links_row(\@links);
my @runtime_buttons;
push(@runtime_buttons, [ "start", $text{'index_start'} ])
if ($can_start);
push(@runtime_buttons, [ "stop", $text{'index_stop'} ])
if ($can_stop);
push(@runtime_buttons, [ "restart", $text{'index_restart'} ])
if ($can_restart);
my @boot_buttons = $can_boot ?
( [ "addboot", $text{'index_addboot'} ],
[ "delboot", $text{'index_delboot'} ] ) : ( );
my @mask_buttons = $can_mask ?
( [ "mask", $text{'index_mask'} ],
[ "unmask", $text{'index_unmask'} ] ) : ( );
my @inspect_buttons;
push(@inspect_buttons, [ "status", $text{'index_statusnow'} ])
if ($can_status);
push(@inspect_buttons, [ "logs", $text{'index_logsnow'} ])
if ($can_logs);
my @delete_buttons = $can_delete ?
( [ "delete", $text{'index_delete'} ] ) : ( );
my @action_groups = $tab->{'inspect_only'} ?
grep { @$_ } ( \@inspect_buttons ) :
grep { @$_ } ( \@runtime_buttons, \@boot_buttons,
\@mask_buttons, \@inspect_buttons );
print ui_form_grouped_buttons([ [ @action_groups ],
[ \@delete_buttons ] ])
if (@action_groups || @delete_buttons);
}
print ui_form_end();
return 1;
}
# index_name_type(unit-name)
# Splits a full unit name into display name and unit type.
sub index_name_type
{
my ($name) = @_;
my $units_piped = join('|', map { quotemeta } get_unit_types());
# Only strip suffixes that systemd understands as unit types.
my ($type) = $name =~ /\.([^.]+)$/;
if (defined($type) && $type =~ /^(?:$units_piped)$/) {
my $display = $name;
$display =~ s/\.$type$//;
return ($display, $type);
}
return ($name, "");
}
# index_tab_title(type)
# Returns the plural tab title for a systemd unit type.
sub index_tab_title
{
my ($type) = @_;
return $text{'systemd_tab_'.$type} ||
$text{'systemd_type_'.$type} ||
ucfirst($type);
}
# index_tab_desc(type)
# Returns the explanatory text shown under a systemd unit tab.
sub index_tab_desc
{
my ($type) = @_;
return $text{'systemd_tabdesc_'.$type} || "";
}
# index_create_label(tab-id, create-type)
# Returns the tab-specific label for the create-unit link.
sub index_create_label
{
my ($tabid, $type) = @_;
return $text{'index_sadd_'.$tabid} ||
$text{'index_sadd_'.$type} ||
$text{'index_sadd'};
}
# index_create_link(tab, user-tab, create-type)
# Returns the create link for a tab, if allowed.
sub index_create_link
{
my ($tab, $user_tab, $create_type) = @_;
return "" if (!$create_type);
return "" if (!systemd_can_create($user_tab, $tab->{'unituser'}));
my $create_url = $user_tab && $tab->{'unituser'} ?
"edit_unit.cgi?new=1&scope=user&unittype=service&unituser=".
urlize($tab->{'unituser'}) :
$user_tab ? "edit_unit.cgi?new=1&scope=user&unittype=service" :
"edit_unit.cgi?new=1&unittype=".urlize($create_type);
return ui_link($create_url,
index_create_label($tab->{'id'}, $create_type));
}
# index_empty_message(tab)
# Returns the empty-state message for a unit tab.
sub index_empty_message
{
my ($tab) = @_;
if ($tab->{'user'}) {
return text('index_empty_user_owner',
ui_tag('tt', html_escape($tab->{'unituser'})))
if ($tab->{'unituser'});
return $text{'index_empty_user'};
}
return $text{'index_empty_'.$tab->{'id'}} || $text{'index_empty_units'};
}
# index_unit_type_title(type)
# Returns the display label for a single unit type.
sub index_unit_type_title
{
my ($type) = @_;
return $text{'systemd_type_'.$type} || $type;
}
# user_unit_selection_value(user, unit)
# Encodes a user-unit owner and name into one checkbox value for mass actions.
sub user_unit_selection_value
{
my ($user, $unit) = @_;
return urlize($user)."\t".urlize($unit);
}
# linger_toggle_link(user, enabled)
# Returns a link to toggle linger for a user-unit owner.
sub linger_toggle_link
{
my ($user, $enabled) = @_;
# The link flips the current state and lets set_linger.cgi validate again.
my $target = $enabled ? 0 : 1;
my $label = $enabled ? $text{'yes'} : $text{'no'};
my $type = $enabled ? 'success' : 'warn';
my $title = $enabled ? text('systemd_linger_disable', $user) :
text('systemd_linger_enable', $user);
my $url = "set_linger.cgi?user=".urlize($user)."&enabled=".$target;
return ui_tag('a', ui_text_color(html_escape($label), $type),
{ 'href' => $url,
'class' => 'systemd_linger_toggle',
'title' => $title });
}
# index_unit_state_column(state)
# Returns a formatted UnitFileState value for a unit row.
sub index_unit_state_column
{
my ($state) = @_;
return index_state_column($state, {
'enabled' => 'success',
'enabled-runtime' => 'success',
'disabled' => 'warn',
'masked' => 'danger',
'masked-runtime' => 'danger',
'bad' => 'danger',
});
}
# index_runtime_state_column(state, substate)
# Returns a formatted ActiveState value, with SubState when systemd reports it.
sub index_runtime_state_column
{
my ($state, $substate) = @_;
return index_state_column($state, {
'active' => 'success',
'inactive' => 'warn',
'activating' => 'warn',
'deactivating' => 'warn',
'failed' => 'danger',
}, $substate);
}
# index_state_column(state, colors, [substate])
# Returns a displayed systemd state value with light semantic coloring.
sub index_state_column
{
my ($state, $colors, $substate) = @_;
return ui_tag('i', $text{'index_unknown'})
if (!defined($state) || $state eq "");
my $label = ucfirst($state);
$label .= " (".$substate.")"
if (defined($substate) && $substate ne "" && $substate ne $state);
my $safe = html_escape($label);
my $color = $colors->{$state};
return $color ? ui_text_color($safe, $color) : $safe;
}
# index_edit_url(unit, user-tab)
# Returns the edit URL for a unit, preferring its safe override file.
sub index_edit_url
{
my ($unit, $user_tab) = @_;
my $url = $user_tab ?
"edit_unit.cgi?scope=user&unituser=".urlize($unit->{'user'}).
"&name=".urlize($unit->{'name'}) :
"edit_unit.cgi?name=".urlize($unit->{'name'});
if (dropin_exists($user_tab, $unit->{'user'}, $unit->{'name'})) {
$url .= "&dropin=1";
}
return $url;
}