mirror of
https://github.com/webmin/webmin.git
synced 2026-06-23 20:40:32 +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.
1137 lines
42 KiB
Perl
Executable File
1137 lines
42 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Show a form for creating, editing or viewing a systemd unit
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
require './systemd-lib.pl'; ## no critic
|
|
|
|
our (%access, %config, %in, %text, $remote_user);
|
|
|
|
# Returns safe extra attributes for create-form placeholders.
|
|
sub placeholder_tags
|
|
{
|
|
my ($text, $tags) = @_;
|
|
my $rv = "placeholder=\"".quote_escape($text)."\"";
|
|
$rv .= " ".$tags if ($tags);
|
|
return $rv;
|
|
}
|
|
|
|
# Returns a create-form text box with a short example placeholder.
|
|
sub placeholder_textbox
|
|
{
|
|
my ($name, $value, $size, $placeholder) = @_;
|
|
return ui_textbox($name, $value, $size, undef, undef,
|
|
placeholder_tags($placeholder));
|
|
}
|
|
|
|
# Returns a create-form file box with a path-style placeholder.
|
|
sub placeholder_filebox
|
|
{
|
|
my ($name, $value, $size, $placeholder, $dironly) = @_;
|
|
return ui_filebox($name, $value, $size, undef, undef,
|
|
placeholder_tags($placeholder), $dironly);
|
|
}
|
|
|
|
# Returns a create-form text area with a command or directive example.
|
|
sub placeholder_textarea
|
|
{
|
|
my ($name, $value, $rows, $cols, $placeholder, $tags) = @_;
|
|
return ui_textarea($name, $value, $rows, $cols, undef, undef,
|
|
placeholder_tags($placeholder, $tags));
|
|
}
|
|
|
|
# Returns example home and runtime paths for user-scope placeholders.
|
|
sub user_scope_example_paths
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = get_user_details($user);
|
|
my $home = $uinfo ? $uinfo->{'home'} : "/home/my-user";
|
|
my $runtime = $uinfo ? "/run/user/".$uinfo->{'uid'} : $home."/run";
|
|
return ($home, $runtime);
|
|
}
|
|
|
|
# Returns path-unit placeholders appropriate for system or user scope.
|
|
sub path_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = (
|
|
'exists' => "/run/my-app.ready",
|
|
'existsglob' => "/var/spool/my-app/*.job",
|
|
'changed' => "/etc/my-app.conf",
|
|
'modified' => "/etc/my-app.d",
|
|
'directorynotempty' => "/var/spool/my-app",
|
|
);
|
|
if ($user_scope) {
|
|
my ($home, $runtime) = user_scope_example_paths($user);
|
|
%rv = (
|
|
'exists' => $runtime."/my-app.ready",
|
|
'existsglob' => $home."/spool/my-app/*.job",
|
|
'changed' => $home."/.config/my-app.conf",
|
|
'modified' => $home."/.config/my-app.d",
|
|
'directorynotempty' => $home."/spool/my-app",
|
|
);
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
# Returns service-file placeholders appropriate for system or user scope.
|
|
sub service_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = (
|
|
'pidfile' => "/run/my-app.pid",
|
|
'envfile' => "-/etc/default/my-app",
|
|
'workdir' => "/srv/my-app",
|
|
'readwritepaths' => "/var/lib/my-app",
|
|
'startpre' => "/usr/bin/install -d /run/my-app",
|
|
'stoppost' => "/usr/bin/rm -f /run/my-app.pid",
|
|
);
|
|
if ($user_scope) {
|
|
my ($home, $runtime) = user_scope_example_paths($user);
|
|
%rv = (
|
|
'pidfile' => $runtime."/my-app.pid",
|
|
'envfile' => "-".$home."/.config/my-app/env",
|
|
'workdir' => $home."/my-app",
|
|
'readwritepaths' => $home."/my-app",
|
|
'startpre' => "/usr/bin/install -d ".$runtime."/my-app",
|
|
'stoppost' => "/usr/bin/rm -f ".$runtime."/my-app.pid",
|
|
);
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
# Returns socket placeholders appropriate for system or user scope.
|
|
sub socket_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = ( 'listenfifo' => "/run/my-app.fifo" );
|
|
if ($user_scope) {
|
|
my (undef, $runtime) = user_scope_example_paths($user);
|
|
$rv{'listenfifo'} = $runtime."/my-app.fifo";
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
ReadParse();
|
|
|
|
# Work out whether this page is creating or editing a user-scoped unit.
|
|
# User-scope units live in the selected Unix user's systemd manager.
|
|
my $unituser = clean_unit_value($in{'unituser'} || $in{'user'});
|
|
my $edit_user_scope = !$in{'new'} && $in{'scope'} eq 'user' ? 1 : 0;
|
|
my $create_default_user_scope = $in{'new'} && !defined($in{'scope'}) &&
|
|
$config{'default_create_scope'} eq 'user' ? 1 : 0;
|
|
my $create_user_scope = $in{'new'} &&
|
|
(($in{'scope'} || "") eq 'user' || $create_default_user_scope) ? 1 : 0;
|
|
my $edit_dropin = !$in{'new'} && $in{'dropin'} ? 1 : 0;
|
|
my $dropin_file = $edit_dropin ? clean_unit_value($in{'dropfile'}) : "";
|
|
my $dropin_info;
|
|
if ($in{'new'} && $create_user_scope && !$unituser) {
|
|
$unituser = systemd_acl_default_user(\%access) || "";
|
|
if (!$unituser) {
|
|
my $ruinfo = get_user_details($remote_user);
|
|
$unituser = $ruinfo->{'user'}
|
|
if ($ruinfo && $ruinfo->{'uid'} != 0);
|
|
}
|
|
}
|
|
if (!$in{'new'}) {
|
|
valid_unit_name($in{'name'}) ||
|
|
error($text{'systemd_ename'});
|
|
}
|
|
my ($u, $conf);
|
|
my (@units, @unittypes, @types, @killmodes, @restarts, @protects);
|
|
my (%creatable_types);
|
|
my $default_unittype = 'service';
|
|
my $unit_file_editable = 0;
|
|
my $can_save_unit = 0;
|
|
my $remote_uinfo = get_user_details($remote_user);
|
|
|
|
# New units start with an empty record. Existing units are looked up from the
|
|
# selected system or user scope so edits cannot cross scopes accidentally.
|
|
if ($in{'new'}) {
|
|
systemd_can_create(\%access, $create_user_scope,
|
|
$create_user_scope ? $unituser : undef) ||
|
|
systemd_acl_error($create_user_scope ?
|
|
'pcreate_user' : 'pcreate');
|
|
# The create form renders structured fields instead of raw unit contents.
|
|
ui_print_header(undef, $text{'systemd_title1'}, "");
|
|
$u = { };
|
|
}
|
|
else {
|
|
# Editing keeps the unit in its current system or user manager.
|
|
if ($edit_user_scope) {
|
|
# The owner must be a real Unix user before we inspect their units.
|
|
get_user_details($unituser) ||
|
|
error($text{'systemd_euser'});
|
|
systemd_can_view_scope(\%access, 1, $unituser) ||
|
|
systemd_acl_error('pview_user');
|
|
@units = list_user_units($unituser);
|
|
}
|
|
else {
|
|
# System-scope edits use the system unit list.
|
|
systemd_can_view_scope(\%access, 0) ||
|
|
systemd_acl_error('pview');
|
|
@units = list_units();
|
|
}
|
|
|
|
# Reject stale edit links after units have been deleted or renamed.
|
|
($u) = grep { $_->{'name'} eq $in{'name'} } @units;
|
|
$u || error($text{'systemd_egone'});
|
|
if ($edit_dropin && $dropin_file) {
|
|
$dropin_info = $edit_user_scope ?
|
|
user_dropin_config_file_info($unituser, $dropin_file) :
|
|
system_dropin_config_file_info($dropin_file);
|
|
$dropin_info && $dropin_info->{'unit'} eq $in{'name'} ||
|
|
error($text{'systemd_edropinfile'});
|
|
$dropin_file = $dropin_info->{'file'};
|
|
}
|
|
$unit_file_editable = unit_file_editable($u);
|
|
$can_save_unit = $edit_dropin ?
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser) :
|
|
systemd_can_edit(\%access, $edit_user_scope, $unituser);
|
|
|
|
# Runtime-managed units are inspect-only, so title them as views.
|
|
my $title_key = $unit_file_editable && $can_save_unit ?
|
|
($edit_user_scope ? 'systemd_title2_user' : 'systemd_title2') :
|
|
($edit_user_scope ? 'systemd_title2_view_user' :
|
|
'systemd_title2_view');
|
|
ui_print_header(undef, $text{$title_key}, "");
|
|
}
|
|
|
|
# The save script uses hidden scope fields to pick the correct control plane
|
|
# for later actions, including status/log redirects.
|
|
print ui_form_start("save_unit.cgi", "post");
|
|
print ui_hidden("new", $in{'new'});
|
|
print ui_hidden("scope", "user") if ($edit_user_scope);
|
|
print ui_hidden("unituser", $unituser) if ($edit_user_scope);
|
|
print ui_hidden("name", $in{'name'}) if (!$in{'new'});
|
|
print ui_hidden("dropin", 1) if ($edit_dropin);
|
|
print ui_hidden("dropfile", $dropin_file) if ($edit_dropin && $dropin_file);
|
|
if ($in{'new'}) {
|
|
# The first table contains the fields that almost every new unit needs.
|
|
print ui_table_start($text{'systemd_header'}, undef, 2);
|
|
|
|
# Unit type and name. The suffix is displayed separately, but the save
|
|
# script appends or validates it before writing the unit file.
|
|
my @creatable_unit_types = get_creatable_unit_types($create_user_scope);
|
|
@unittypes = map { [ $_, $text{'systemd_type_'.$_} || $_ ] }
|
|
@creatable_unit_types;
|
|
%creatable_types = map { $_, 1 } @creatable_unit_types;
|
|
$default_unittype = $creatable_types{$in{'unittype'}} ?
|
|
$in{'unittype'} : "service";
|
|
my $type_help = $create_user_scope ?
|
|
"systemd_type_user" : "systemd_type";
|
|
print ui_table_row(hlink($text{'systemd_type'}, $type_help),
|
|
ui_select("unittype", $default_unittype, \@unittypes,
|
|
1, 0, 0, 0));
|
|
print ui_table_hr();
|
|
print ui_table_row(hlink($text{'systemd_name'}, "systemd_name"),
|
|
placeholder_textbox("name", undef, 30, "my-app").
|
|
ui_tag('tt', ".$default_unittype",
|
|
{ 'id' => 'systemd_name_suffix' }));
|
|
|
|
# Every new unit needs a Description= line.
|
|
print ui_table_row(hlink($text{'systemd_desc'}, "systemd_desc"),
|
|
placeholder_textbox("desc", undef, 60,
|
|
"My app service"));
|
|
|
|
# Existing mount units can be paired with a new automount, so the user
|
|
# does not need to derive the automount name by hand.
|
|
my @mount_units;
|
|
if ($create_user_scope && get_user_details($unituser)) {
|
|
@mount_units = grep { $_->{'name'} =~ /\.mount$/ }
|
|
list_user_units($unituser);
|
|
}
|
|
elsif (!$create_user_scope) {
|
|
@mount_units = grep { $_->{'name'} =~ /\.mount$/ }
|
|
list_units();
|
|
}
|
|
my @automount_mounts = ( [ '', $text{'systemd_automountmount_none'} ] );
|
|
foreach my $mu (sort { $a->{'name'} cmp $b->{'name'} } @mount_units) {
|
|
my $where = mount_unit_where(
|
|
$mu, $create_user_scope ? $unituser : undef);
|
|
my $label = $mu->{'name'}.
|
|
($where ? " (".$where.")" : "");
|
|
push(@automount_mounts, [ $mu->{'name'}, $label ]);
|
|
}
|
|
|
|
# Service units use command fields rather than raw [Service] body text.
|
|
print ui_table_row(hlink($text{'systemd_start'}, "systemd_start"),
|
|
placeholder_textarea("atstart", undef, 5, 80,
|
|
"/usr/bin/my-app --foreground"),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# The stop command is optional; the save page can generate a default.
|
|
print ui_table_row(hlink($text{'systemd_stop'}, "systemd_stop"),
|
|
placeholder_textarea("atstop", undef, 5, 80,
|
|
"/bin/kill -TERM \$MAINPID"),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# Timer units can be created from common activation fields. More unusual
|
|
# timer directives remain available in the advanced body editor below.
|
|
my @timer_row = ( "data-systemd-timer='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_timeroncalendar'},
|
|
"systemd_timeroncalendar"),
|
|
placeholder_textbox("timer_oncalendar", undef, 40,
|
|
"daily"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeronbootsec'},
|
|
"systemd_timeronbootsec"),
|
|
placeholder_textbox("timer_onbootsec", undef, 10,
|
|
"5min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeronunitactivesec'},
|
|
"systemd_timeronunitactivesec"),
|
|
placeholder_textbox("timer_onunitactivesec", undef,
|
|
10, "1h"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerpersistent'},
|
|
"systemd_timerpersistent"),
|
|
ui_yesno_radio("timer_persistent", 0),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerrandomizeddelaysec'},
|
|
"systemd_timerrandomizeddelaysec"),
|
|
placeholder_textbox("timer_randomizeddelaysec", undef,
|
|
10, "10min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeraccuracysec'},
|
|
"systemd_timeraccuracysec"),
|
|
placeholder_textbox("timer_accuracysec", undef, 10,
|
|
"1min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerunit'},
|
|
"systemd_timerunit"),
|
|
placeholder_textbox("timer_unit", undef, 40,
|
|
"my-job.service"),
|
|
1, undef, \@timer_row);
|
|
|
|
# Socket units expose the usual listeners and ownership controls.
|
|
my @socket_row = ( "data-systemd-socket='1' style='display:none'" );
|
|
my $socket_placeholders =
|
|
socket_unit_placeholders($create_user_scope, $unituser);
|
|
print ui_table_row(hlink($text{'systemd_socketlistenstream'},
|
|
"systemd_socketlistenstream"),
|
|
placeholder_textbox("socket_listenstream", undef, 40,
|
|
"127.0.0.1:8080"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketlistendatagram'},
|
|
"systemd_socketlistendatagram"),
|
|
placeholder_textbox("socket_listendatagram", undef,
|
|
40, "10514"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketlistenfifo'},
|
|
"systemd_socketlistenfifo"),
|
|
placeholder_filebox("socket_listenfifo", undef, 50,
|
|
$socket_placeholders->{'listenfifo'}, 1),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketaccept'},
|
|
"systemd_socketaccept"),
|
|
ui_yesno_radio("socket_accept", 0),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketuser'},
|
|
"systemd_socketuser"),
|
|
placeholder_textbox("socket_user", undef, 20,
|
|
"appuser")." ".
|
|
user_chooser_button("socket_user"),
|
|
1, undef, [ "id='systemd_socket_user_row' ".
|
|
"data-systemd-socket='1' style='display:none'" ]);
|
|
print ui_table_row(hlink($text{'systemd_socketgroup'},
|
|
"systemd_socketgroup"),
|
|
placeholder_textbox("socket_group", undef, 20,
|
|
"appgroup")." ".
|
|
group_chooser_button("socket_group"),
|
|
1, undef, [ "id='systemd_socket_group_row' ".
|
|
"data-systemd-socket='1' style='display:none'" ]);
|
|
print ui_table_row(hlink($text{'systemd_socketmode'},
|
|
"systemd_socketmode"),
|
|
placeholder_textbox("socket_mode", undef, 10, "0660"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketservice'},
|
|
"systemd_socketservice"),
|
|
placeholder_textbox("socket_service", undef, 40,
|
|
"my-app.service"),
|
|
1, undef, \@socket_row);
|
|
|
|
# Path units watch files or directories and activate another unit.
|
|
my @path_row = ( "data-systemd-path='1' style='display:none'" );
|
|
my $path_placeholders =
|
|
path_unit_placeholders($create_user_scope, $unituser);
|
|
print ui_table_row(hlink($text{'systemd_pathexists'},
|
|
"systemd_pathexists"),
|
|
placeholder_filebox("path_exists", undef, 50,
|
|
$path_placeholders->{'exists'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathexistsglob'},
|
|
"systemd_pathexistsglob"),
|
|
placeholder_textbox("path_existsglob", undef, 50,
|
|
$path_placeholders->{'existsglob'}),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathchanged'},
|
|
"systemd_pathchanged"),
|
|
placeholder_filebox("path_changed", undef, 50,
|
|
$path_placeholders->{'changed'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathmodified'},
|
|
"systemd_pathmodified"),
|
|
placeholder_filebox("path_modified", undef, 50,
|
|
$path_placeholders->{'modified'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathdirectorynotempty'},
|
|
"systemd_pathdirectorynotempty"),
|
|
placeholder_filebox("path_directorynotempty", undef,
|
|
50,
|
|
$path_placeholders->{'directorynotempty'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathmakedirectory'},
|
|
"systemd_pathmakedirectory"),
|
|
ui_yesno_radio("path_makedirectory", 0),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathunit'},
|
|
"systemd_pathunit"),
|
|
placeholder_textbox("path_unit", undef, 40,
|
|
"my-app.service"),
|
|
1, undef, \@path_row);
|
|
|
|
# Mount units have a small, stable set of required fields. The unit name
|
|
# can be derived from Where= on save.
|
|
my @mount_row = ( "data-systemd-mount='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_mountwhat'}, "systemd_mountwhat"),
|
|
placeholder_textbox("mount_what", undef, 60,
|
|
"/dev/disk/by-uuid/abcd-1234"),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mountwhere'}, "systemd_mountwhere"),
|
|
placeholder_filebox("mount_where", undef, 50,
|
|
"/mnt/data", 1),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mounttype'}, "systemd_mounttype"),
|
|
placeholder_textbox("mount_type", undef, 20, "ext4"),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mountoptions'}, "systemd_mountoptions"),
|
|
placeholder_textbox("mount_options", undef, 60,
|
|
"defaults,noatime"),
|
|
1, undef, \@mount_row);
|
|
|
|
# Automount units activate a matching mount unit by path. Selecting an
|
|
# existing mount lets save_unit.cgi derive the automount name safely.
|
|
my @automount_row = (
|
|
"data-systemd-automount='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_automountmount'},
|
|
"systemd_automountmount"),
|
|
ui_select("automount_mount", undef,
|
|
\@automount_mounts),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountwhere'},
|
|
"systemd_automountwhere"),
|
|
placeholder_filebox("automount_where", undef, 50,
|
|
"/mnt/data", 1),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountidle'},
|
|
"systemd_automountidle"),
|
|
placeholder_textbox("automount_idle", undef, 10,
|
|
"5min"),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountmode'},
|
|
"systemd_automountmode"),
|
|
placeholder_textbox("automount_mode", undef, 10,
|
|
"0755"),
|
|
1, undef, \@automount_row);
|
|
|
|
# Swap and slice units have a few common directives worth exposing.
|
|
my @swap_row = ( "data-systemd-swap='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_swapwhat'}, "systemd_swapwhat"),
|
|
placeholder_filebox("swap_what", undef, 50,
|
|
"/swapfile", 1),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swappriority'},
|
|
"systemd_swappriority"),
|
|
placeholder_textbox("swap_priority", undef, 10, "10"),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swapoptions'},
|
|
"systemd_swapoptions"),
|
|
placeholder_textbox("swap_options", undef, 60,
|
|
"discard"),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swaptimeoutsec'},
|
|
"systemd_swaptimeoutsec"),
|
|
placeholder_textbox("swap_timeoutsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@swap_row);
|
|
my @slice_row = ( "data-systemd-slice='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_slicecpuweight'},
|
|
"systemd_slicecpuweight"),
|
|
placeholder_textbox("slice_cpuweight", undef, 10,
|
|
"200"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_slicememorymax'},
|
|
"systemd_slicememorymax"),
|
|
placeholder_textbox("slice_memorymax", undef, 10,
|
|
"512M"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_slicetasksmax'},
|
|
"systemd_slicetasksmax"),
|
|
placeholder_textbox("slice_tasksmax", undef, 10,
|
|
"500"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_sliceioweight'},
|
|
"systemd_sliceioweight"),
|
|
placeholder_textbox("slice_ioweight", undef, 10,
|
|
"200"),
|
|
1, undef, \@slice_row);
|
|
|
|
# Startup state is applied after the unit is created.
|
|
if (systemd_can_boot(\%access, $create_user_scope,
|
|
$create_user_scope ? $unituser : undef)) {
|
|
print ui_table_row(hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
ui_yesno_radio("boot", 1));
|
|
}
|
|
|
|
# Pick a safe default owner for new user units when possible.
|
|
my $default_unituser = $unituser;
|
|
my $acl_default_unituser;
|
|
if (!$default_unituser) {
|
|
$acl_default_unituser =
|
|
systemd_acl_default_user(\%access) || "";
|
|
$default_unituser = $acl_default_unituser;
|
|
if (!$default_unituser) {
|
|
$default_unituser = $remote_uinfo->{'user'}
|
|
if ($remote_uinfo &&
|
|
$remote_uinfo->{'uid'} != 0);
|
|
}
|
|
}
|
|
my $force_user_scope_create = $create_user_scope &&
|
|
!systemd_can_create(\%access, 0) &&
|
|
(($remote_uinfo && $remote_uinfo->{'uid'} != 0) ||
|
|
$acl_default_unituser) ? 1 : 0;
|
|
my $force_user_scope_owner = $force_user_scope_create &&
|
|
$default_unituser ? 1 : 0;
|
|
# User units live in the selected user's home and run under that user's
|
|
# systemd manager, so the service-level User=/Group= rows are hidden by JS.
|
|
if ($force_user_scope_create) {
|
|
print ui_hidden("userservice", 1);
|
|
}
|
|
else {
|
|
print ui_table_row(hlink($text{'systemd_userservice'},
|
|
"systemd_userservice"),
|
|
ui_radio("userservice",
|
|
$create_user_scope ? 1 : 0,
|
|
[ [ 1, $text{'yes'} ],
|
|
[ 0, $text{'no'} ] ]),
|
|
1, undef,
|
|
[ "id='systemd_userservice_row'" ]);
|
|
print ui_table_hr();
|
|
}
|
|
if ($force_user_scope_owner) {
|
|
print ui_hidden("unituser", $default_unituser);
|
|
}
|
|
else {
|
|
print ui_table_row(hlink($text{'systemd_unituser'},
|
|
"systemd_unituser"),
|
|
placeholder_textbox("unituser",
|
|
$default_unituser, 20,
|
|
"appuser")." ".
|
|
user_chooser_button("unituser"),
|
|
1, undef,
|
|
[ "id='systemd_unituser_row'".
|
|
($create_user_scope ? "" :
|
|
" style='display:none'") ]);
|
|
}
|
|
if (systemd_acl_bool(\%access, 'linger')) {
|
|
my $linger_text = $create_user_scope ?
|
|
$text{'systemd_linger_user'} : $text{'systemd_linger'};
|
|
my $linger_help = $create_user_scope ?
|
|
"systemd_linger_user" : "systemd_linger";
|
|
print ui_table_row(hlink($linger_text, $linger_help),
|
|
ui_yesno_radio("linger",
|
|
$config{'default_linger'} ? 1 : 0),
|
|
1, undef, [ "id='systemd_linger_row'".
|
|
($create_user_scope ? "" : " style='display:none'") ]);
|
|
}
|
|
|
|
print ui_table_end();
|
|
|
|
# Less common create-time settings are collapsed by default.
|
|
print ui_hidden_table_start($text{'systemd_advanced'}, undef, 2,
|
|
"advanced", 0);
|
|
|
|
# Unit relationships are shared by all creatable unit types and are written
|
|
# into the [Unit] section.
|
|
print ui_table_row(hlink($text{'systemd_before'}, "systemd_before"),
|
|
placeholder_textbox("before", undef, 60,
|
|
"network.target"));
|
|
print ui_table_row(hlink($text{'systemd_after'}, "systemd_after"),
|
|
placeholder_textbox("after", undef, 60,
|
|
"network-online.target"));
|
|
print ui_table_row(hlink($text{'systemd_wants'}, "systemd_wants"),
|
|
placeholder_textbox("wants", undef, 60,
|
|
"network-online.target"));
|
|
print ui_table_row(hlink($text{'systemd_requires'}, "systemd_requires"),
|
|
placeholder_textbox("requires", undef, 60,
|
|
"postgresql.service"));
|
|
print ui_table_row(hlink($text{'systemd_conflicts'}, "systemd_conflicts"),
|
|
placeholder_textbox("conflicts", undef, 60,
|
|
"old-app.service"));
|
|
print ui_table_row(hlink($text{'systemd_onfailure'}, "systemd_onfailure"),
|
|
placeholder_textbox("onfailure", undef, 60,
|
|
"notify@%n.service"));
|
|
print ui_table_row(hlink($text{'systemd_onsuccess'}, "systemd_onsuccess"),
|
|
placeholder_textbox("onsuccess", undef, 60,
|
|
"report.service"));
|
|
|
|
# Service options become irrelevant for all non-service unit types; each row
|
|
# is marked so the JS type switch can hide it.
|
|
my @service_row = ( "data-systemd-service='1'" );
|
|
my $service_placeholders =
|
|
service_unit_placeholders($create_user_scope, $unituser);
|
|
@types = ( [ '', $text{'default'} ], "simple", "exec", "forking",
|
|
"oneshot", "dbus", "notify", "idle" );
|
|
print ui_table_row(hlink($text{'systemd_servicetype'}, "systemd_servicetype"),
|
|
ui_select("type", undef, \@types),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_remain'}, "systemd_remain"),
|
|
ui_yesno_radio("remain", 0),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_pidfile'}, "systemd_pidfile"),
|
|
placeholder_filebox("pidfile", undef, 50,
|
|
$service_placeholders->{'pidfile'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_env'}, "systemd_env"),
|
|
placeholder_textbox("env", undef, 60,
|
|
"NODE_ENV=production PORT=8080"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_envfile'}, "systemd_envfile"),
|
|
placeholder_filebox("envfile", undef, 50,
|
|
$service_placeholders->{'envfile'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_user'}, "systemd_user"),
|
|
placeholder_textbox("user", undef, 20, "appuser")." ".
|
|
user_chooser_button("user"),
|
|
1, undef, [ "id='systemd_runas_user_row' ".
|
|
"data-systemd-service='1'".
|
|
($create_user_scope ? " style='display:none'" : "") ]);
|
|
print ui_table_row(hlink($text{'systemd_group'}, "systemd_group"),
|
|
placeholder_textbox("group", undef, 20, "appgroup")." ".
|
|
group_chooser_button("group"),
|
|
1, undef, [ "id='systemd_runas_group_row' ".
|
|
"data-systemd-service='1'".
|
|
($create_user_scope ? " style='display:none'" : "") ]);
|
|
@killmodes = ( [ '', $text{'default'} ], "control-group",
|
|
"process", "mixed", "none" );
|
|
print ui_table_row(hlink($text{'systemd_killmode'}, "systemd_killmode"),
|
|
ui_select("killmode", undef, \@killmodes),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_workdir'}, "systemd_workdir"),
|
|
placeholder_filebox("workdir", undef, 50,
|
|
$service_placeholders->{'workdir'}, 1),
|
|
1, undef, \@service_row);
|
|
@restarts = ( [ '', $text{'default'} ], "no", "on-success",
|
|
"on-failure", "on-abnormal", "on-watchdog",
|
|
"on-abort", "always" );
|
|
print ui_table_row(hlink($text{'systemd_restart'}, "systemd_restart"),
|
|
ui_select("restart_policy", undef, \@restarts),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_restartsec'}, "systemd_restartsec"),
|
|
placeholder_textbox("restartsec", undef, 10, "5s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_watchdogsec'}, "systemd_watchdogsec"),
|
|
placeholder_textbox("watchdogsec", undef, 10, "30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_timeout'}, "systemd_timeout"),
|
|
placeholder_textbox("timeoutstartsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_timeoutstop'}, "systemd_timeoutstop"),
|
|
placeholder_textbox("timeoutstopsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_limitnofile'}, "systemd_limitnofile"),
|
|
placeholder_textbox("limitnofile", undef, 10,
|
|
"65535"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_logstd'}, "systemd_logstd"),
|
|
placeholder_textbox("logstd", undef, 50,
|
|
"journal"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_logerr'}, "systemd_logerr"),
|
|
placeholder_textbox("logerr", undef, 50,
|
|
"journal"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_syslogid'}, "systemd_syslogid"),
|
|
placeholder_textbox("syslogid", undef, 30, "my-app"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_nonewprivs'}, "systemd_nonewprivs"),
|
|
ui_yesno_radio("nonewprivs", 0),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_privatetmp'}, "systemd_privatetmp"),
|
|
ui_yesno_radio("privatetmp", 0),
|
|
1, undef, \@service_row);
|
|
@protects = ( [ '', $text{'default'} ], "true", "full", "strict" );
|
|
print ui_table_row(hlink($text{'systemd_protectsystem'}, "systemd_protectsystem"),
|
|
ui_select("protectsystem", undef, \@protects),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_readwritepaths'}, "systemd_readwritepaths"),
|
|
placeholder_textbox("readwritepaths", undef, 60,
|
|
$service_placeholders->{'readwritepaths'}),
|
|
1, undef, \@service_row);
|
|
|
|
# Install options stay visible for all types. JS changes the default target
|
|
# when switching between system/user units or between unit types.
|
|
my $default_wantedby =
|
|
get_default_install_target($default_unittype,
|
|
$create_user_scope);
|
|
print ui_table_row(hlink($text{'systemd_wantedby'}, "systemd_wantedby"),
|
|
placeholder_textbox("wantedby", $default_wantedby,
|
|
60, "multi-user.target"));
|
|
|
|
# Extra non-service directives supplement the guided fields above. Only
|
|
# directive lines belong here; the renderer adds the correct section header.
|
|
print ui_table_row(hlink($text{'systemd_unitconf'}, "systemd_unitconf"),
|
|
placeholder_textarea("unitconf", undef, 8, 80,
|
|
"RuntimeMaxSec=1h", "spellcheck='false'"),
|
|
1, undef, [ "data-systemd-extra='1' ".
|
|
"style='display:none'" ]);
|
|
|
|
# Extra command hooks are service-only and are kept near the end because
|
|
# they are less commonly needed than the scalar service settings above.
|
|
print ui_table_row(hlink($text{'systemd_startpre'}, "systemd_startpre"),
|
|
placeholder_textarea("startpre", undef, 3, 80,
|
|
$service_placeholders->{'startpre'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_startpost'}, "systemd_startpost"),
|
|
placeholder_textarea("startpost", undef, 3, 80,
|
|
"/usr/bin/logger my-app started"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_stoppost'}, "systemd_stoppost"),
|
|
placeholder_textarea("stoppost", undef, 3, 80,
|
|
$service_placeholders->{'stoppost'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_reload'}, "systemd_reload"),
|
|
placeholder_textarea("reload", undef, 3, 80,
|
|
"/bin/kill -HUP \$MAINPID"),
|
|
1, undef, \@service_row);
|
|
|
|
print ui_hidden_table_end("advanced");
|
|
my $systemd_js = <<'EOF';
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Unit suffixes shown next to the editable base name.
|
|
const systemdSuffixes = {
|
|
service: '.service',
|
|
timer: '.timer',
|
|
socket: '.socket',
|
|
path: '.path',
|
|
target: '.target',
|
|
mount: '.mount',
|
|
automount: '.automount',
|
|
swap: '.swap',
|
|
slice: '.slice'
|
|
};
|
|
// Type-aware examples keep the create form from sounding service-only.
|
|
const systemdNamePlaceholders = {
|
|
service: 'my-app',
|
|
timer: 'nightly-backup',
|
|
socket: 'my-app',
|
|
path: 'config-watch',
|
|
target: 'app-stack',
|
|
mount: 'mnt-data',
|
|
automount: 'mnt-data',
|
|
swap: 'swapfile',
|
|
slice: 'app-workload'
|
|
};
|
|
const systemdDescPlaceholders = {
|
|
service: 'My app service',
|
|
timer: 'Nightly backup timer',
|
|
socket: 'My app socket',
|
|
path: 'Watch app config',
|
|
target: 'App stack target',
|
|
mount: 'Mount /mnt/data',
|
|
automount: 'Automount /mnt/data',
|
|
swap: 'Swap file',
|
|
slice: 'App resource slice'
|
|
};
|
|
const systemdExtraPlaceholders = {
|
|
timer: 'OnUnitInactiveSec=2min\nWakeSystem=no\nRemainAfterElapse=yes',
|
|
socket: 'Backlog=32\nKeepAlive=yes\nNoDelay=yes\nFileDescriptorName=my-app',
|
|
path: 'TriggerLimitIntervalSec=30s\nTriggerLimitBurst=10',
|
|
mount: 'TimeoutSec=30s\nLazyUnmount=yes',
|
|
automount: 'DirectoryMode=0755',
|
|
swap: 'TimeoutSec=30s',
|
|
slice: 'CPUQuota=50%\nMemoryHigh=256M'
|
|
};
|
|
// Default install targets mirror systemd's usual system and user unit targets.
|
|
const systemdInstallTargets = {
|
|
system: {
|
|
service: 'multi-user.target',
|
|
timer: 'timers.target',
|
|
socket: 'sockets.target',
|
|
path: 'paths.target',
|
|
target: 'multi-user.target',
|
|
mount: 'local-fs.target',
|
|
automount: 'local-fs.target',
|
|
swap: 'swap.target',
|
|
slice: 'slices.target'
|
|
},
|
|
user: {
|
|
service: 'default.target',
|
|
timer: 'timers.target',
|
|
socket: 'sockets.target',
|
|
path: 'paths.target',
|
|
target: 'default.target',
|
|
mount: 'default.target',
|
|
automount: 'default.target',
|
|
swap: 'default.target',
|
|
slice: 'slices.target'
|
|
}
|
|
};
|
|
|
|
// Returns the currently selected type, falling back to the service form.
|
|
function currentUnitType()
|
|
{
|
|
const field = document.querySelector('select[name="unittype"]');
|
|
return field && field.value ? field.value : 'service';
|
|
}
|
|
|
|
// Updates generic placeholders to match the currently selected unit type.
|
|
function updateTypePlaceholders()
|
|
{
|
|
const type = currentUnitType();
|
|
const nameField = document.querySelector('input[name="name"]');
|
|
const descField = document.querySelector('input[name="desc"]');
|
|
if (nameField && systemdNamePlaceholders[type]) {
|
|
nameField.setAttribute('placeholder', systemdNamePlaceholders[type]);
|
|
}
|
|
if (descField && systemdDescPlaceholders[type]) {
|
|
descField.setAttribute('placeholder', systemdDescPlaceholders[type]);
|
|
}
|
|
const extraField = document.querySelector('textarea[name="unitconf"]');
|
|
if (extraField) {
|
|
extraField.setAttribute('placeholder',
|
|
systemdExtraPlaceholders[type] || '');
|
|
}
|
|
}
|
|
|
|
// Detects defaults we own, so a custom WantedBy value is not overwritten.
|
|
function knownInstallTarget(value)
|
|
{
|
|
for (const scope in systemdInstallTargets) {
|
|
for (const type in systemdInstallTargets[scope]) {
|
|
if (systemdInstallTargets[scope][type] == value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Refreshes WantedBy only when it is blank or still one of our defaults.
|
|
function updateInstallTarget(userMode)
|
|
{
|
|
const field = document.querySelector('[name="wantedby"]');
|
|
if (!field) {
|
|
return;
|
|
}
|
|
const scope = userMode ? 'user' : 'system';
|
|
const target = systemdInstallTargets[scope][currentUnitType()];
|
|
if (target && (!field.value || knownInstallTarget(field.value))) {
|
|
field.value = target;
|
|
}
|
|
}
|
|
|
|
// Shows user-manager fields and hides service User=/Group= in user mode.
|
|
function userModeChange()
|
|
{
|
|
let checked = document.querySelector('input[name="userservice"]:checked');
|
|
const hidden = document.querySelector('input[name="userservice"][type="hidden"]');
|
|
const f = checked ? checked.form : null;
|
|
const userservice = f ? f.elements['userservice'] :
|
|
document.querySelectorAll('input[name="userservice"]');
|
|
if (!checked && userservice) {
|
|
for (let i = 0; i < userservice.length; i++) {
|
|
if (userservice[i].checked) {
|
|
checked = userservice[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const enabled = checked ? checked.value == '1' :
|
|
hidden ? hidden.value == '1' : false;
|
|
const service = currentUnitType() == 'service';
|
|
const socket = currentUnitType() == 'socket';
|
|
const showrow = function(id, show) {
|
|
const row = document.getElementById(id);
|
|
if (row) {
|
|
row.style.display = show ? '' : 'none';
|
|
}
|
|
};
|
|
showrow('systemd_unituser_row', enabled);
|
|
showrow('systemd_linger_row', enabled);
|
|
const userserviceHr =
|
|
document.querySelector('#systemd_userservice_row + tr');
|
|
if (userserviceHr) {
|
|
userserviceHr.style.display = enabled ? '' : 'none';
|
|
}
|
|
showrow('systemd_runas_user_row', !enabled && service);
|
|
showrow('systemd_runas_group_row', !enabled && service);
|
|
showrow('systemd_socket_user_row', !enabled && socket);
|
|
showrow('systemd_socket_group_row', !enabled && socket);
|
|
updateInstallTarget(enabled);
|
|
}
|
|
|
|
// Switches between service-specific rows and each unit type's guided fields.
|
|
function unitTypeChange()
|
|
{
|
|
const type = currentUnitType();
|
|
const service = type == 'service';
|
|
const extra = !service && type != 'target';
|
|
const mount = type == 'mount';
|
|
const automount = type == 'automount';
|
|
const typedRowSets = {
|
|
timer: document.querySelectorAll('[data-systemd-timer]'),
|
|
socket: document.querySelectorAll('[data-systemd-socket]'),
|
|
path: document.querySelectorAll('[data-systemd-path]'),
|
|
mount: document.querySelectorAll('[data-systemd-mount]'),
|
|
automount: document.querySelectorAll('[data-systemd-automount]'),
|
|
swap: document.querySelectorAll('[data-systemd-swap]'),
|
|
slice: document.querySelectorAll('[data-systemd-slice]')
|
|
};
|
|
const suffix = document.getElementById('systemd_name_suffix');
|
|
if (suffix) {
|
|
suffix.textContent = systemdSuffixes[type] || '';
|
|
}
|
|
updateTypePlaceholders();
|
|
const serviceRows = document.querySelectorAll('[data-systemd-service]');
|
|
for (let i = 0; i < serviceRows.length; i++) {
|
|
serviceRows[i].style.display = service ? '' : 'none';
|
|
}
|
|
for (const rowType in typedRowSets) {
|
|
const rows = typedRowSets[rowType];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = type == rowType ? '' : 'none';
|
|
}
|
|
}
|
|
const extraRows = document.querySelectorAll('[data-systemd-extra]');
|
|
for (let i = 0; i < extraRows.length; i++) {
|
|
extraRows[i].style.display = extra ? '' : 'none';
|
|
}
|
|
userModeChange();
|
|
}
|
|
|
|
// Authentic and Gray themes can render rows at different times, so initialize
|
|
// after DOM readiness and also bind explicit change handlers.
|
|
function initializeSystemdUnitForm()
|
|
{
|
|
const systemdUserServiceInputs =
|
|
document.querySelectorAll('input[name="userservice"]');
|
|
for (let i = 0; i < systemdUserServiceInputs.length; i++) {
|
|
systemdUserServiceInputs[i].addEventListener('change',
|
|
userModeChange);
|
|
}
|
|
const systemdUnitTypeInput = document.querySelector('select[name="unittype"]');
|
|
if (systemdUnitTypeInput) {
|
|
systemdUnitTypeInput.addEventListener('change',
|
|
unitTypeChange);
|
|
}
|
|
unitTypeChange();
|
|
}
|
|
|
|
if (document.readyState == 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializeSystemdUnitForm);
|
|
}
|
|
else {
|
|
initializeSystemdUnitForm();
|
|
}
|
|
})();
|
|
EOF
|
|
print ui_tag('script', $systemd_js,
|
|
{ 'type' => 'text/javascript' });
|
|
}
|
|
else {
|
|
# Existing units are edited as raw files to preserve unknown directives.
|
|
print ui_table_start($text{'systemd_header'}, undef, 2);
|
|
|
|
# Unit names are identifiers and cannot be renamed from the edit page.
|
|
print ui_table_row(hlink($text{'systemd_name'}, "systemd_name"),
|
|
ui_tag('tt', html_escape($in{'name'})));
|
|
|
|
# Show the resolved file path before the editable unit contents.
|
|
my $edit_file = $u->{'file'};
|
|
if ($edit_dropin) {
|
|
$edit_file = $dropin_file ||
|
|
($edit_user_scope ?
|
|
user_dropin_file($unituser, $in{'name'}) :
|
|
system_dropin_file($in{'name'}));
|
|
$edit_file || error($text{'systemd_edropinfile'});
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_file'}, "systemd_file"),
|
|
ui_tag('tt', html_escape($edit_file)));
|
|
|
|
# User files are read through privilege-dropping helpers so a path in the
|
|
# home tree cannot make root follow user-controlled symlinks.
|
|
if ($edit_dropin) {
|
|
$conf = $dropin_file && $edit_user_scope ?
|
|
read_user_dropin_config_file($unituser, $dropin_file) :
|
|
$dropin_file ?
|
|
read_system_dropin_config_file($dropin_file) :
|
|
$edit_user_scope ?
|
|
read_user_dropin_file($unituser, $in{'name'}) :
|
|
read_system_dropin_file($in{'name'});
|
|
defined($conf) || error($text{'systemd_edropinfile'});
|
|
}
|
|
else {
|
|
$conf = $edit_user_scope ?
|
|
read_user_unit_file($unituser, $u->{'file'}) :
|
|
read_file_contents($u->{'file'});
|
|
defined($conf) || error($text{'systemd_euserunitfile'});
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_conf'}, "systemd_conf"),
|
|
ui_textarea("data", $conf, 20, 80, undef,
|
|
undef, $unit_file_editable &&
|
|
$can_save_unit ? undef :
|
|
"readonly='readonly'"));
|
|
|
|
if ($edit_user_scope) {
|
|
# The owner is fixed for an existing user unit.
|
|
print ui_table_row(hlink($text{'systemd_unituser'}, "systemd_unituser"),
|
|
ui_tag('tt', html_escape($unituser)));
|
|
}
|
|
|
|
# Show systemd's own state model before editable policy toggles.
|
|
print ui_table_row(hlink($text{'systemd_runtime_state'},
|
|
"systemd_runtime_state"),
|
|
edit_runtime_state($u->{'runtime'}, $u->{'substate'}));
|
|
if (defined($u->{'pid'}) && $u->{'pid'} =~ /^\d+$/ && $u->{'pid'} > 0) {
|
|
print ui_table_row(hlink($text{'systemd_main_pid'},
|
|
"systemd_main_pid"),
|
|
ui_tag('tt', html_escape($u->{'pid'})));
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_unit_state'},
|
|
"systemd_unit_state"),
|
|
edit_state_value($u->{'unitstate'}));
|
|
|
|
# Only file-backed installable units can have their startup state changed.
|
|
if (boot_state_changeable($u->{'unitstate'}, $u->{'name'}) &&
|
|
systemd_can_boot(\%access, $edit_user_scope, $unituser)) {
|
|
print ui_table_row(hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
ui_yesno_radio("boot", $u->{'boot'}));
|
|
}
|
|
|
|
# User-scope edits allow linger to be managed alongside the raw unit file.
|
|
if ($edit_user_scope) {
|
|
my $linger_enabled = user_linger_enabled($unituser);
|
|
my $linger_field = systemd_can_linger(\%access, $unituser) ?
|
|
ui_yesno_radio("linger", $linger_enabled) :
|
|
html_escape($linger_enabled ? $text{'yes'} : $text{'no'});
|
|
print ui_table_row(hlink($text{'systemd_linger_user'},
|
|
"systemd_linger_user"),
|
|
$linger_field);
|
|
}
|
|
|
|
print ui_table_end();
|
|
}
|
|
|
|
if ($in{'new'}) {
|
|
# New units only need a create button; runtime actions appear after save.
|
|
print ui_form_end([ [ undef, $text{'create'} ] ]);
|
|
}
|
|
else {
|
|
# Keep save, override, runtime and inspection actions in nearby clusters;
|
|
# destructive actions stay isolated on the far side of the button row.
|
|
my @save_buttons = $unit_file_editable && $can_save_unit ?
|
|
( [ undef, $text{'save'} ] ) : ( );
|
|
my @control_buttons;
|
|
my @inspect_buttons = systemd_can_inspect(
|
|
\%access, $edit_user_scope, $unituser) ?
|
|
( [ 'status', $text{'edit_statusnow'} ],
|
|
[ 'props', $text{'edit_propsnow'} ],
|
|
[ 'deps', $text{'edit_depsnow'} ] ) : ( );
|
|
my @log_buttons = systemd_can_logs(
|
|
\%access, $edit_user_scope, $unituser) ?
|
|
( [ 'logs', $text{'edit_logsnow'} ] ) : ( );
|
|
|
|
# Running units can be stopped, but only restart units where systemd
|
|
# supports a restart job type. Some runtime units, such as scopes and
|
|
# devices, are externally managed and can only be inspected or stopped.
|
|
if (defined($u->{'status'}) && $u->{'status'} == 1) {
|
|
push(@control_buttons, [ 'restart', $text{'edit_restartnow'} ])
|
|
if (unit_restartable($in{'name'}) &&
|
|
systemd_can_runtime(
|
|
\%access, 'restart', $edit_user_scope,
|
|
$unituser));
|
|
push(@control_buttons, [ 'stop', $text{'edit_stopnow'} ])
|
|
if (systemd_can_runtime(
|
|
\%access, 'stop', $edit_user_scope,
|
|
$unituser));
|
|
}
|
|
elsif (unit_startable($in{'name'}) &&
|
|
systemd_can_runtime(\%access, 'start',
|
|
$edit_user_scope, $unituser)) {
|
|
push(@control_buttons, [ 'start', $text{'edit_startnow'} ]);
|
|
}
|
|
|
|
my @override_buttons;
|
|
if ($edit_dropin) {
|
|
push(@override_buttons,
|
|
[ 'stock_unit',
|
|
$text{'edit_stockunitnow'} || "Stock Unit" ]);
|
|
}
|
|
elsif ($unit_file_editable &&
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser)) {
|
|
my $override_text = dropin_exists($edit_user_scope,
|
|
$unituser, $in{'name'}) ?
|
|
($text{'edit_editoverridenow'} ||
|
|
"Edit Override") :
|
|
($text{'edit_overridenow'} ||
|
|
"Create Override");
|
|
push(@override_buttons, [ 'override', $override_text ]);
|
|
}
|
|
my @delete_buttons;
|
|
if ($edit_dropin && !$dropin_file && $unit_file_editable &&
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser)) {
|
|
push(@delete_buttons,
|
|
[ 'delete_override',
|
|
$text{'edit_deleteoverridenow'} || "Delete Override" ]);
|
|
}
|
|
elsif ($unit_file_editable && $in{'name'} ne 'webmin.service' &&
|
|
systemd_can_delete(\%access, $edit_user_scope, $unituser)) {
|
|
push(@delete_buttons, [ 'delete', $text{'delete'} ]);
|
|
}
|
|
|
|
print ui_form_grouped_buttons([ [ \@save_buttons,
|
|
\@override_buttons,
|
|
\@control_buttons,
|
|
\@inspect_buttons,
|
|
\@log_buttons ],
|
|
[ \@delete_buttons ] ]);
|
|
print ui_form_end();
|
|
}
|
|
|
|
# Return to the index tab that owns this unit when the type or scope is known.
|
|
my $footer_url = $in{'new'} ?
|
|
index_url(".".$default_unittype, $create_user_scope, $unituser) :
|
|
index_url($in{'name'}, $edit_user_scope, $unituser);
|
|
ui_print_footer($footer_url, $text{'index_return'});
|
|
|
|
# edit_runtime_state(active-state, sub-state)
|
|
# Returns a systemd-style runtime state value such as "Active (running)".
|
|
sub edit_runtime_state
|
|
{
|
|
my ($state, $substate) = @_;
|
|
my $value = edit_state_value($state);
|
|
if (defined($state) && $state ne "" &&
|
|
defined($substate) && $substate ne "" && $substate ne $state) {
|
|
$value .= " ".ui_tag('span',
|
|
"(".html_escape(lcfirst($substate)).")");
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
# edit_state_value(state)
|
|
# Returns a formatted systemd state value for the edit form.
|
|
sub edit_state_value
|
|
{
|
|
my ($state) = @_;
|
|
return ui_tag('i', $text{'index_unknown'})
|
|
if (!defined($state) || $state eq "");
|
|
return html_escape(ucfirst($state));
|
|
}
|