mirror of
https://github.com/webmin/webmin.git
synced 2026-06-04 20:30:22 +01:00
475 lines
18 KiB
Perl
Executable File
475 lines
18 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Show a form for creating or editing a systemd unit
|
|
|
|
require './init-lib.pl';
|
|
$access{'bootup'} || &error($text{'edit_ecannot'});
|
|
&ReadParse();
|
|
|
|
# Work out whether this page is creating/editing a user-scoped unit. The
|
|
# function names below still say "service" for API compatibility, but the
|
|
# returned lists can contain services, timers, sockets and paths.
|
|
$unituser = &clean_systemd_unit_value($in{'unituser'} || $in{'user'});
|
|
$edit_user_scope = !$in{'new'} && $in{'scope'} eq 'user' ? 1 : 0;
|
|
$create_user_scope = $in{'new'} && $in{'scope'} eq 'user' ? 1 : 0;
|
|
|
|
# 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'}) {
|
|
&ui_print_header(undef, $text{'systemd_title1'}, "");
|
|
$u = { };
|
|
}
|
|
else {
|
|
&ui_print_header(undef, $edit_user_scope ? $text{'systemd_title2_user'} :
|
|
$text{'systemd_title2'}, "");
|
|
if ($edit_user_scope) {
|
|
&get_systemd_user_details($unituser) ||
|
|
&error($text{'systemd_euser'});
|
|
@systemds = &list_systemd_user_services($unituser);
|
|
}
|
|
else {
|
|
@systemds = &list_systemd_services();
|
|
}
|
|
($u) = grep { $_->{'name'} eq $in{'name'} } @systemds;
|
|
$u || &error($text{'systemd_egone'});
|
|
$u->{'legacy'} && &error($text{'systemd_elegacy'});
|
|
}
|
|
|
|
# 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_systemd.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'});
|
|
if ($in{'new'}) {
|
|
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.
|
|
@unittypes = map { [ $_, $text{'systemd_type_'.$_} || $_ ] }
|
|
&get_systemd_creatable_unit_types();
|
|
%creatable_types = map { $_, 1 } &get_systemd_creatable_unit_types();
|
|
$default_unittype = $creatable_types{$in{'unittype'}} ?
|
|
$in{'unittype'} : "service";
|
|
print &ui_table_row(&hlink($text{'systemd_type'}, "systemd_type"),
|
|
&ui_select("unittype", $default_unittype, \@unittypes,
|
|
1, 0, 0, 0));
|
|
print &ui_table_hr();
|
|
print &ui_table_row(&hlink($text{'systemd_name'}, "systemd_name"),
|
|
&ui_textbox("name", undef, 30).
|
|
&ui_tag('tt', ".$default_unittype",
|
|
{ 'id' => 'systemd_name_suffix' }));
|
|
|
|
# Description
|
|
print &ui_table_row(&hlink($text{'systemd_desc'}, "systemd_desc"),
|
|
&ui_textbox("desc", undef, 60));
|
|
|
|
# Start script
|
|
print &ui_table_row(&hlink($text{'systemd_start'}, "systemd_start"),
|
|
&ui_textarea("atstart", undef, 5, 80),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# Stop script
|
|
print &ui_table_row(&hlink($text{'systemd_stop'}, "systemd_stop"),
|
|
&ui_textarea("atstop", undef, 5, 80),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# Non-service type-specific settings
|
|
print &ui_table_row(&hlink($text{'systemd_unitconf'}, "systemd_unitconf"),
|
|
&ui_textarea("unitconf", undef, 8, 80, undef,
|
|
undef, "spellcheck='false'"),
|
|
1, undef, [ "data-systemd-nonservice='1' ".
|
|
"style='display:none'" ]);
|
|
|
|
# Start at boot?
|
|
print &ui_table_row(&hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
&ui_yesno_radio("boot", 1));
|
|
|
|
# User service controls
|
|
my $default_unituser = $unituser;
|
|
if (!$default_unituser) {
|
|
my $ruinfo = &get_systemd_user_details($remote_user);
|
|
$default_unituser = $ruinfo->{'user'}
|
|
if ($ruinfo && $ruinfo->{'uid'} != 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.
|
|
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();
|
|
print &ui_table_row(&hlink($text{'systemd_unituser'}, "systemd_unituser"),
|
|
&ui_textbox("unituser", $default_unituser, 20)." ".
|
|
&user_chooser_button("unituser"),
|
|
1, undef, [ "id='systemd_unituser_row'".
|
|
($create_user_scope ? "" : " style='display:none'") ]);
|
|
print &ui_table_row(&hlink($text{'systemd_linger'}, "systemd_linger"),
|
|
&ui_yesno_radio("linger", 1),
|
|
1, undef, [ "id='systemd_linger_row'".
|
|
($create_user_scope ? "" : " style='display:none'") ]);
|
|
|
|
print &ui_table_end();
|
|
|
|
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"),
|
|
&ui_textbox("before", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_after'}, "systemd_after"),
|
|
&ui_textbox("after", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_wants'}, "systemd_wants"),
|
|
&ui_textbox("wants", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_requires'}, "systemd_requires"),
|
|
&ui_textbox("requires", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_conflicts'}, "systemd_conflicts"),
|
|
&ui_textbox("conflicts", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_onfailure'}, "systemd_onfailure"),
|
|
&ui_textbox("onfailure", undef, 60));
|
|
print &ui_table_row(&hlink($text{'systemd_onsuccess'}, "systemd_onsuccess"),
|
|
&ui_textbox("onsuccess", undef, 60));
|
|
|
|
# Service options become irrelevant for timers, sockets, paths and targets;
|
|
# each row is marked so the JS type switch can hide it.
|
|
my @service_row = ( "data-systemd-service='1'" );
|
|
@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"),
|
|
&ui_filebox("pidfile", undef, 50),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_env'}, "systemd_env"),
|
|
&ui_textbox("env", undef, 60),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_envfile'}, "systemd_envfile"),
|
|
&ui_filebox("envfile", undef, 50),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_user'}, "systemd_user"),
|
|
&ui_textbox("user", undef, 20)." ".
|
|
&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"),
|
|
&ui_textbox("group", undef, 20)." ".
|
|
&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"),
|
|
&ui_filebox("workdir", undef, 50, undef,
|
|
undef, undef, 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", undef, \@restarts),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_restartsec'}, "systemd_restartsec"),
|
|
&ui_textbox("restartsec", undef, 10),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_watchdogsec'}, "systemd_watchdogsec"),
|
|
&ui_textbox("watchdogsec", undef, 10),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_timeout'}, "systemd_timeout"),
|
|
&ui_textbox("timeout", undef, 10),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_timeoutstop'}, "systemd_timeoutstop"),
|
|
&ui_textbox("timeoutstopsec", undef, 10),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_limitnofile'}, "systemd_limitnofile"),
|
|
&ui_textbox("limitnofile", undef, 10),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_logstd'}, "systemd_logstd"),
|
|
&ui_textbox("logstd", undef, 50),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_logerr'}, "systemd_logerr"),
|
|
&ui_textbox("logerr", undef, 50),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_syslogid'}, "systemd_syslogid"),
|
|
&ui_textbox("syslogid", undef, 30),
|
|
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"),
|
|
&ui_textbox("readwritepaths", undef, 60),
|
|
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_systemd_default_install_target($default_unittype,
|
|
$create_user_scope);
|
|
print &ui_table_row(&hlink($text{'systemd_wantedby'}, "systemd_wantedby"),
|
|
&ui_textbox("wantedby", $default_wantedby, 60));
|
|
|
|
# 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"),
|
|
&ui_textarea("startpre", undef, 3, 80),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_startpost'}, "systemd_startpost"),
|
|
&ui_textarea("startpost", undef, 3, 80),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_stoppost'}, "systemd_stoppost"),
|
|
&ui_textarea("stoppost", undef, 3, 80),
|
|
1, undef, \@service_row);
|
|
print &ui_table_row(&hlink($text{'systemd_reload'}, "systemd_reload"),
|
|
&ui_textarea("reload", undef, 3, 80),
|
|
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'
|
|
};
|
|
// 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'
|
|
},
|
|
user: {
|
|
service: 'default.target',
|
|
timer: 'timers.target',
|
|
socket: 'sockets.target',
|
|
path: 'paths.target',
|
|
target: 'default.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';
|
|
}
|
|
|
|
// 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 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';
|
|
const service = currentUnitType() == 'service';
|
|
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);
|
|
updateInstallTarget(enabled);
|
|
}
|
|
|
|
// Switches between service-specific rows and raw type-specific configuration.
|
|
function unitTypeChange()
|
|
{
|
|
const type = currentUnitType();
|
|
const service = type == 'service';
|
|
const suffix = document.getElementById('systemd_name_suffix');
|
|
if (suffix) {
|
|
suffix.textContent = systemdSuffixes[type] || '';
|
|
}
|
|
const serviceRows = document.querySelectorAll('[data-systemd-service]');
|
|
for (let i = 0; i < serviceRows.length; i++) {
|
|
serviceRows[i].style.display = service ? '' : 'none';
|
|
}
|
|
const nonServiceRows = document.querySelectorAll('[data-systemd-nonservice]');
|
|
for (let i = 0; i < nonServiceRows.length; i++) {
|
|
nonServiceRows[i].style.display = service ? '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 {
|
|
print &ui_table_start($text{'systemd_header'}, undef, 2);
|
|
|
|
# Unit name (non-editable)
|
|
print &ui_table_row(&hlink($text{'systemd_name'}, "systemd_name"),
|
|
&ui_tag('tt', &html_escape($in{'name'})));
|
|
|
|
# Config file and contents
|
|
print &ui_table_row(&hlink($text{'systemd_file'}, "systemd_file"),
|
|
&ui_tag('tt', &html_escape($u->{'file'})));
|
|
|
|
# User unit files are read through the privilege-dropping helper so a
|
|
# user-controlled path cannot make root follow symlinks in the home tree.
|
|
$conf = $edit_user_scope ?
|
|
&read_systemd_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));
|
|
|
|
# User-scope edits allow linger to be managed alongside the raw unit file.
|
|
if ($edit_user_scope) {
|
|
print &ui_table_row(&hlink($text{'systemd_unituser'}, "systemd_unituser"),
|
|
&ui_tag('tt', &html_escape($unituser)));
|
|
print &ui_table_row(&hlink($text{'systemd_linger'}, "systemd_linger"),
|
|
&ui_yesno_radio("linger",
|
|
&systemd_user_linger_enabled($unituser)));
|
|
}
|
|
|
|
# Current status
|
|
if ($u->{'boot'} != 2) {
|
|
print &ui_table_row(&hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
&ui_yesno_radio("boot", $u->{'boot'}));
|
|
}
|
|
print &ui_table_row(&hlink($text{'systemd_status'}, "systemd_status"),
|
|
$u->{'status'} == 1 && $u->{'pid'} ?
|
|
&text('systemd_status1', $u->{'pid'}) :
|
|
$u->{'status'} == 1 ?
|
|
$text{'systemd_status2'} :
|
|
$u->{'status'} == 0 ?
|
|
$text{'systemd_status0'} :
|
|
$text{'systemd_status3'});
|
|
|
|
print &ui_table_end();
|
|
}
|
|
|
|
if ($in{'new'}) {
|
|
print &ui_form_end([ [ undef, $text{'create'} ] ]);
|
|
}
|
|
else {
|
|
# Group save, runtime actions and delete separately in the button row.
|
|
my @save_buttons = ( [ undef, $text{'save'} ] );
|
|
my @control_buttons;
|
|
if (defined($u->{'status'}) && $u->{'status'} == 1) {
|
|
push(@control_buttons, [ 'restart', $text{'edit_restartnow'} ]);
|
|
push(@control_buttons, [ 'stop', $text{'edit_stopnow'} ])
|
|
if ($in{'name'} ne 'webmin.service');
|
|
}
|
|
elsif (defined($u->{'status'}) && $u->{'status'} == 0) {
|
|
push(@control_buttons, [ 'start', $text{'edit_startnow'} ]);
|
|
}
|
|
else {
|
|
push(@control_buttons, [ 'start', $text{'edit_startnow'} ],
|
|
[ 'restart', $text{'edit_restartnow'} ]);
|
|
push(@control_buttons, [ 'stop', $text{'edit_stopnow'} ])
|
|
if ($in{'name'} ne 'webmin.service');
|
|
}
|
|
push(@control_buttons, [ 'status', $text{'edit_statusnow'} ],
|
|
[ 'logs', $text{'edit_logsnow'} ]);
|
|
my @delete_buttons = (
|
|
$in{'name'} eq 'webmin.service' ? ( ) :
|
|
( [ 'delete', $text{'delete'} ] ),
|
|
);
|
|
print &ui_form_grouped_buttons([ [ \@save_buttons, \@control_buttons ],
|
|
\@delete_buttons ]);
|
|
print &ui_form_end();
|
|
}
|
|
|
|
# Return to the index tab that owns this unit when the type or scope is known.
|
|
$footer_url = $in{'new'} ?
|
|
&systemd_index_url(".".$default_unittype, $create_user_scope, $unituser) :
|
|
&systemd_index_url($in{'name'}, $edit_user_scope, $unituser);
|
|
&ui_print_footer($footer_url, $text{'index_return'});
|