mirror of
https://github.com/webmin/webmin.git
synced 2026-06-24 04:50:30 +01:00
855 lines
29 KiB
Perl
Executable File
855 lines
29 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Create, update or delete a systemd unit
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Symbol qw(gensym);
|
|
|
|
require './systemd-lib.pl'; ## no critic
|
|
|
|
our (%access, %config, %in, %text);
|
|
|
|
# All failures on this page should use systemd-specific wording.
|
|
error_setup($text{'systemd_err'});
|
|
ReadParse();
|
|
|
|
# Select system or user scope before loading units.
|
|
my $user_scope = $in{'new'} ? ($in{'userservice'} ? 1 : 0) :
|
|
($in{'scope'} eq 'user' ? 1 : 0);
|
|
my $unituser = clean_unit_value($in{'unituser'});
|
|
my $edit_dropin = !$in{'new'} && $in{'dropin'} ? 1 : 0;
|
|
my $dropin_file = $edit_dropin ? clean_unit_value($in{'dropfile'}) : "";
|
|
my $dropin_info;
|
|
my (@units, $u, $redirect);
|
|
if ($user_scope) {
|
|
# User units must always be tied to a real Unix account.
|
|
get_user_details($unituser) ||
|
|
error($text{'systemd_euser'});
|
|
systemd_can_view_scope(1, $unituser) ||
|
|
systemd_acl_error('pview_user');
|
|
@units = list_user_units($unituser);
|
|
}
|
|
else {
|
|
# System units are managed through the system manager.
|
|
systemd_can_view_scope(0) || systemd_acl_error('pview');
|
|
@units = list_units();
|
|
}
|
|
|
|
# Load the existing unit for edits and destructive actions.
|
|
if (!$in{'new'}) {
|
|
valid_unit_name($in{'name'}) ||
|
|
error($text{'systemd_ename'});
|
|
|
|
# The target unit must exist in the selected scope before it can be edited.
|
|
($u) = grep { $_->{'name'} eq $in{'name'} } @units;
|
|
$u || error($text{'systemd_egone'});
|
|
if ($edit_dropin && $dropin_file) {
|
|
$dropin_info = $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'};
|
|
}
|
|
}
|
|
|
|
if ($in{'stock_unit'}) {
|
|
# Leaving the override editor is navigation only; do not save form data.
|
|
redirect($user_scope ?
|
|
"edit_unit.cgi?scope=user&unituser=".urlize($unituser).
|
|
"&name=".urlize($in{'name'}) :
|
|
"edit_unit.cgi?name=".urlize($in{'name'}));
|
|
exit;
|
|
}
|
|
|
|
# Runtime actions do not save the form; they stream through mass_units.cgi.
|
|
if (!$in{'new'} &&
|
|
($in{'start'} || $in{'stop'} || $in{'restart'} || $in{'status'} ||
|
|
$in{'props'} || $in{'deps'} || $in{'logs'})) {
|
|
if ($in{'start'}) {
|
|
systemd_can_runtime('start',
|
|
$user_scope, $unituser) ||
|
|
systemd_acl_error('pstart');
|
|
}
|
|
elsif ($in{'stop'}) {
|
|
systemd_can_runtime('stop',
|
|
$user_scope, $unituser) ||
|
|
systemd_acl_error('pstop');
|
|
}
|
|
elsif ($in{'restart'}) {
|
|
systemd_can_runtime('restart',
|
|
$user_scope, $unituser) ||
|
|
systemd_acl_error('prestart');
|
|
}
|
|
elsif ($in{'logs'}) {
|
|
systemd_can_logs($user_scope, $unituser) ||
|
|
systemd_acl_error('plogs');
|
|
}
|
|
else {
|
|
systemd_can_inspect($user_scope, $unituser) ||
|
|
systemd_acl_error('pstatus');
|
|
}
|
|
# Stream runtime actions through mass_units.cgi.
|
|
my $scopeargs = $user_scope ? "&scope=user&unituser=".
|
|
urlize($unituser) : "";
|
|
my $dropinargs = $edit_dropin ? "&returndropin=1" : "";
|
|
$dropinargs .= "&returndropfile=".urlize($dropin_file)
|
|
if ($dropin_file);
|
|
my $returnindexargs =
|
|
(!$edit_dropin && !unit_file_editable($u) &&
|
|
($in{'stop'} || $in{'restart'})) ? "&returnindex=1" : "";
|
|
redirect("mass_units.cgi?d=".urlize($in{'name'})."&".
|
|
($in{'start'} ? "start=1" :
|
|
$in{'restart'} ? "restart=1" :
|
|
$in{'status'} ? "status=1" :
|
|
$in{'props'} ? "props=1" :
|
|
$in{'deps'} ? "deps=1" :
|
|
$in{'logs'} ? "logs=1" : "stop=1").
|
|
"&return=".urlize($in{'name'}).$scopeargs.$dropinargs.
|
|
$returnindexargs);
|
|
exit;
|
|
}
|
|
|
|
if ($in{'override'}) {
|
|
# Create the standard override file if needed, then open that drop-in.
|
|
systemd_can_dropin($user_scope, $unituser) ||
|
|
systemd_acl_error($user_scope ? 'pdropin_user' : 'pdropin');
|
|
unit_file_editable($u) || error($text{'systemd_ereadonly'});
|
|
my $base_data = $user_scope ?
|
|
read_user_unit_file($unituser, $u->{'file'}) :
|
|
read_file_contents($u->{'file'});
|
|
defined($base_data) ||
|
|
error($user_scope ? $text{'systemd_euserunitfile'} :
|
|
$text{'manual_eread'});
|
|
my $dropfile = $user_scope ?
|
|
user_dropin_file($unituser, $in{'name'}) :
|
|
system_dropin_file($in{'name'});
|
|
$dropfile || error($text{'systemd_edropinfile'});
|
|
|
|
# Existing override files are preserved; the button becomes an opener.
|
|
if ($user_scope) {
|
|
user_dropin_file_safe($unituser, $dropfile, 0) ||
|
|
error($text{'systemd_edropinfile'});
|
|
if (!-f $dropfile) {
|
|
my $template =
|
|
dropin_template($dropfile, $u->{'file'},
|
|
$base_data);
|
|
my ($ok, $out) = write_user_dropin_file(
|
|
$unituser, $in{'name'}, $template);
|
|
$ok || error($out);
|
|
webmin_log("override", "systemd-user", $in{'name'},
|
|
{ 'user' => $unituser });
|
|
}
|
|
$redirect = "edit_unit.cgi?scope=user&unituser=".
|
|
urlize($unituser)."&name=".urlize($in{'name'}).
|
|
"&dropin=1";
|
|
}
|
|
else {
|
|
my $dir = $dropfile;
|
|
$dir =~ s{/[^/]+$}{};
|
|
error($text{'systemd_edropinfile'})
|
|
if (-l $dir || (-e $dir && !-d $dir) ||
|
|
-l $dropfile || (-e $dropfile && !-f $dropfile));
|
|
if (!-f $dropfile) {
|
|
my $template =
|
|
dropin_template($dropfile, $u->{'file'},
|
|
$base_data);
|
|
my ($ok, $out) =
|
|
write_system_dropin_file($in{'name'},
|
|
$template);
|
|
$ok || error($out);
|
|
webmin_log("override", "systemd", $in{'name'});
|
|
}
|
|
$redirect = "edit_unit.cgi?name=".urlize($in{'name'}).
|
|
"&dropin=1";
|
|
}
|
|
}
|
|
elsif ($in{'delete_override'}) {
|
|
# Drop-in deletes are available only from the override editor.
|
|
systemd_can_dropin($user_scope, $unituser) ||
|
|
systemd_acl_error($user_scope ? 'pdropin_user' : 'pdropin');
|
|
$edit_dropin || error($text{'systemd_edropinfile'});
|
|
$dropin_file && error($text{'systemd_edropinfile'});
|
|
unit_file_editable($u) || error($text{'systemd_ereadonly'});
|
|
if ($user_scope) {
|
|
my ($ok, $out) =
|
|
delete_user_dropin_file($unituser, $in{'name'});
|
|
$ok || error($out);
|
|
($ok, $out) = reload_user_manager($unituser);
|
|
$ok || error_user_command($unituser, $out);
|
|
webmin_log("deleteoverride", "systemd-user", $in{'name'},
|
|
{ 'user' => $unituser });
|
|
$redirect = "edit_unit.cgi?scope=user&unituser=".
|
|
urlize($unituser)."&name=".urlize($in{'name'});
|
|
}
|
|
else {
|
|
my ($ok, $out) = delete_system_dropin_file($in{'name'});
|
|
$ok || error($out);
|
|
reload_manager();
|
|
webmin_log("deleteoverride", "systemd", $in{'name'});
|
|
$redirect = "edit_unit.cgi?name=".urlize($in{'name'});
|
|
}
|
|
}
|
|
elsif ($in{'delete'}) {
|
|
# Delete the unit after trying to stop it and remove it from startup.
|
|
systemd_can_delete($user_scope, $unituser) ||
|
|
systemd_acl_error($user_scope ? 'pdelete_user' : 'pdelete');
|
|
if ($user_scope) {
|
|
# User-unit deletion goes through helpers that drop to the owner.
|
|
disable_user_unit($unituser, $in{'name'});
|
|
stop_user_unit($unituser, $in{'name'});
|
|
my ($ok, $out) =
|
|
delete_user_unit($unituser, $in{'name'});
|
|
$ok || error_user_command($unituser, $out);
|
|
webmin_log("delete", "systemd-user", $in{'name'},
|
|
{ 'user' => $unituser });
|
|
}
|
|
else {
|
|
# Stop and disable are best-effort, but deletion must be reported.
|
|
disable_unit($in{'name'});
|
|
stop_unit($in{'name'});
|
|
my ($ok, $out) = delete_system_unit($in{'name'});
|
|
$ok || error($out);
|
|
webmin_log("delete", "systemd", $in{'name'});
|
|
}
|
|
$redirect = index_url($in{'name'}, $user_scope, $unituser);
|
|
}
|
|
elsif ($in{'new'}) {
|
|
systemd_can_create($user_scope, $user_scope ? $unituser : undef) ||
|
|
systemd_acl_error($user_scope ? 'pcreate_user' : 'pcreate');
|
|
# Normalize the unit name and suffix before checking for clashes.
|
|
my @creatable_unit_types = get_creatable_unit_types($user_scope);
|
|
my %creatable_types = map { $_, 1 } @creatable_unit_types;
|
|
my $unittype = $in{'unittype'} || 'service';
|
|
$creatable_types{$unittype} || error($text{'systemd_eunittype'});
|
|
$in{'name'} = clean_unit_value($in{'name'});
|
|
$in{'name'} = "" if (!defined($in{'name'}));
|
|
|
|
# Guided fields are rendered into the correct type-specific section.
|
|
my ($derived_name, $structured_body);
|
|
foreach my $o ('timer_oncalendar', 'timer_onbootsec',
|
|
'timer_onunitactivesec', 'timer_randomizeddelaysec',
|
|
'timer_accuracysec', 'timer_unit', 'timer_persistent',
|
|
'socket_listenstream', 'socket_listendatagram',
|
|
'socket_listenfifo', 'socket_user', 'socket_group',
|
|
'socket_mode', 'socket_service', 'socket_accept',
|
|
'path_exists', 'path_existsglob', 'path_changed',
|
|
'path_modified', 'path_directorynotempty',
|
|
'path_makedirectory', 'path_unit',
|
|
'mount_what', 'mount_where', 'mount_type',
|
|
'mount_options', 'automount_mount', 'automount_where',
|
|
'automount_idle', 'automount_mode',
|
|
'swap_what', 'swap_priority', 'swap_options',
|
|
'swap_timeoutsec', 'slice_cpuweight',
|
|
'slice_memorymax', 'slice_tasksmax', 'slice_ioweight') {
|
|
$in{$o} = clean_unit_value($in{$o});
|
|
$in{$o} = "" if (!defined($in{$o}));
|
|
}
|
|
my $raw_unitconf = clean_unit_body($in{'unitconf'});
|
|
$raw_unitconf = "" if (!defined($raw_unitconf));
|
|
$raw_unitconf =~ /^\s*\[/m &&
|
|
error($text{'systemd_eunitconfsection'});
|
|
|
|
if ($unittype eq 'timer') {
|
|
my %timer_labels = (
|
|
'timer_onbootsec' => $text{'systemd_timeronbootsec'},
|
|
'timer_onunitactivesec' =>
|
|
$text{'systemd_timeronunitactivesec'},
|
|
'timer_randomizeddelaysec' =>
|
|
$text{'systemd_timerrandomizeddelaysec'},
|
|
'timer_accuracysec' => $text{'systemd_timeraccuracysec'},
|
|
);
|
|
foreach my $o ('timer_onbootsec', 'timer_onunitactivesec',
|
|
'timer_randomizeddelaysec',
|
|
'timer_accuracysec') {
|
|
!$in{$o} || valid_duration($in{$o}) ||
|
|
error(text('systemd_eduration', $timer_labels{$o}));
|
|
}
|
|
!$in{'timer_unit'} || valid_unit_name($in{'timer_unit'}) ||
|
|
error($text{'systemd_etimerunit'});
|
|
my $has_timer = $in{'timer_oncalendar'} ||
|
|
$in{'timer_onbootsec'} ||
|
|
$in{'timer_onunitactivesec'} ||
|
|
$in{'timer_persistent'} ||
|
|
$in{'timer_randomizeddelaysec'} ||
|
|
$in{'timer_accuracysec'} ||
|
|
$in{'timer_unit'};
|
|
my $has_trigger = $in{'timer_oncalendar'} ||
|
|
$in{'timer_onbootsec'} ||
|
|
$in{'timer_onunitactivesec'};
|
|
$has_trigger || $raw_unitconf =~ /\S/ ||
|
|
error($text{'systemd_etimertrigger'});
|
|
$structured_body = render_timer_body({
|
|
'oncalendar' => $in{'timer_oncalendar'},
|
|
'onbootsec' => $in{'timer_onbootsec'},
|
|
'onunitactivesec' => $in{'timer_onunitactivesec'},
|
|
'persistent' => $in{'timer_persistent'},
|
|
'randomizeddelaysec' =>
|
|
$in{'timer_randomizeddelaysec'},
|
|
'accuracysec' => $in{'timer_accuracysec'},
|
|
'unit' => $in{'timer_unit'},
|
|
}) if ($has_timer);
|
|
}
|
|
elsif ($unittype eq 'socket') {
|
|
# User managers create filesystem sockets as the owning user.
|
|
if ($user_scope) {
|
|
$in{'socket_user'} = "";
|
|
$in{'socket_group'} = "";
|
|
}
|
|
foreach my $o ('socket_listenstream',
|
|
'socket_listendatagram') {
|
|
!$in{$o} || $in{$o} =~ /^\S+$/ ||
|
|
error($text{'systemd_esocketlisten'});
|
|
}
|
|
!$in{'socket_listenfifo'} ||
|
|
valid_path($in{'socket_listenfifo'}, 0, 0, 0) ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_socketlistenfifo'}));
|
|
!$in{'socket_mode'} || $in{'socket_mode'} =~ /^[0-7]{3,4}$/ ||
|
|
error($text{'systemd_esocketmode'});
|
|
!$in{'socket_service'} ||
|
|
(valid_unit_name($in{'socket_service'}) &&
|
|
$in{'socket_service'} =~ /\.service$/) ||
|
|
error($text{'systemd_esocketservice'});
|
|
my $has_listener = $in{'socket_listenstream'} ||
|
|
$in{'socket_listendatagram'} ||
|
|
$in{'socket_listenfifo'};
|
|
$has_listener || $raw_unitconf =~ /\S/ ||
|
|
error($text{'systemd_esocketlisten'});
|
|
my $has_socket = $has_listener || $in{'socket_accept'} ||
|
|
$in{'socket_user'} || $in{'socket_group'} ||
|
|
$in{'socket_mode'} || $in{'socket_service'};
|
|
$structured_body = render_socket_body({
|
|
'listenstream' => $in{'socket_listenstream'},
|
|
'listendatagram' => $in{'socket_listendatagram'},
|
|
'listenfifo' => $in{'socket_listenfifo'},
|
|
'accept' => $in{'socket_accept'},
|
|
'user' => $in{'socket_user'},
|
|
'group' => $in{'socket_group'},
|
|
'mode' => $in{'socket_mode'},
|
|
'service' => $in{'socket_service'},
|
|
}) if ($has_socket);
|
|
}
|
|
elsif ($unittype eq 'path') {
|
|
my %path_labels = (
|
|
'path_exists' => $text{'systemd_pathexists'},
|
|
'path_existsglob' => $text{'systemd_pathexistsglob'},
|
|
'path_changed' => $text{'systemd_pathchanged'},
|
|
'path_modified' => $text{'systemd_pathmodified'},
|
|
'path_directorynotempty' =>
|
|
$text{'systemd_pathdirectorynotempty'},
|
|
);
|
|
foreach my $o ('path_exists', 'path_existsglob',
|
|
'path_changed', 'path_modified',
|
|
'path_directorynotempty') {
|
|
!$in{$o} || valid_path($in{$o}, 0, 0, 0) ||
|
|
error(text('systemd_epath', $path_labels{$o}));
|
|
}
|
|
!$in{'path_unit'} || valid_unit_name($in{'path_unit'}) ||
|
|
error($text{'systemd_epathunit'});
|
|
my $has_path = $in{'path_exists'} || $in{'path_existsglob'} ||
|
|
$in{'path_changed'} ||
|
|
$in{'path_modified'} ||
|
|
$in{'path_directorynotempty'};
|
|
$has_path || $raw_unitconf =~ /\S/ ||
|
|
error($text{'systemd_epathtrigger'});
|
|
$structured_body = render_path_body({
|
|
'exists' => $in{'path_exists'},
|
|
'existsglob' => $in{'path_existsglob'},
|
|
'changed' => $in{'path_changed'},
|
|
'modified' => $in{'path_modified'},
|
|
'directorynotempty' => $in{'path_directorynotempty'},
|
|
'makedirectory' => $in{'path_makedirectory'},
|
|
'unit' => $in{'path_unit'},
|
|
}) if ($has_path || $in{'path_makedirectory'} ||
|
|
$in{'path_unit'});
|
|
}
|
|
elsif ($unittype eq 'mount' &&
|
|
($in{'mount_what'} || $in{'mount_where'} ||
|
|
$in{'mount_type'} || $in{'mount_options'})) {
|
|
$in{'mount_what'} =~ /\S/ ||
|
|
error($text{'systemd_emountwhat'});
|
|
valid_path($in{'mount_where'}, 0, 0, 0) ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_mountwhere'}));
|
|
$derived_name = path_unit_name($in{'mount_where'}, 'mount') ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_mountwhere'}));
|
|
$structured_body = render_mount_body(
|
|
$in{'mount_what'}, $in{'mount_where'},
|
|
$in{'mount_type'}, $in{'mount_options'});
|
|
}
|
|
elsif ($unittype eq 'automount' &&
|
|
($in{'automount_mount'} || $in{'automount_where'} ||
|
|
$in{'automount_idle'} || $in{'automount_mode'})) {
|
|
my $selected = $in{'automount_mount'};
|
|
if ($selected) {
|
|
valid_creatable_unit_name($selected, $user_scope) &&
|
|
$selected =~ /\.mount$/ ||
|
|
error($text{'systemd_eautomountmount'});
|
|
my ($mount) = grep { $_->{'name'} eq $selected } @units;
|
|
$mount || error($text{'systemd_eautomountmount'});
|
|
$in{'automount_where'} = mount_unit_where(
|
|
$mount, $user_scope ? $unituser : undef);
|
|
}
|
|
valid_path($in{'automount_where'}, 0, 0, 0) ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_automountwhere'}));
|
|
!$in{'automount_idle'} || valid_duration($in{'automount_idle'}) ||
|
|
error(text('systemd_eduration',
|
|
$text{'systemd_automountidle'}));
|
|
!$in{'automount_mode'} ||
|
|
$in{'automount_mode'} =~ /^[0-7]{3,4}$/ ||
|
|
error($text{'systemd_eautomountmode'});
|
|
my $mount_name =
|
|
path_unit_name($in{'automount_where'}, 'mount') ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_automountwhere'}));
|
|
my ($mount) = grep { $_->{'name'} eq $mount_name } @units;
|
|
$mount || error($text{'systemd_eautomountmount'});
|
|
$derived_name =
|
|
path_unit_name($in{'automount_where'}, 'automount') ||
|
|
error(text('systemd_epath',
|
|
$text{'systemd_automountwhere'}));
|
|
$structured_body = render_automount_body(
|
|
$in{'automount_where'}, $in{'automount_idle'},
|
|
$in{'automount_mode'});
|
|
}
|
|
elsif ($unittype eq 'swap') {
|
|
!$in{'swap_what'} || valid_path($in{'swap_what'}, 0, 0, 0) ||
|
|
error(text('systemd_epath', $text{'systemd_swapwhat'}));
|
|
!$in{'swap_priority'} || $in{'swap_priority'} =~ /^-?\d+$/ ||
|
|
error($text{'systemd_eswappriority'});
|
|
!$in{'swap_timeoutsec'} || valid_duration($in{'swap_timeoutsec'}) ||
|
|
error(text('systemd_eduration',
|
|
$text{'systemd_swaptimeoutsec'}));
|
|
$in{'swap_what'} || $raw_unitconf =~ /\S/ ||
|
|
error($text{'systemd_eswapwhat'});
|
|
$structured_body = render_swap_body({
|
|
'what' => $in{'swap_what'},
|
|
'priority' => $in{'swap_priority'},
|
|
'options' => $in{'swap_options'},
|
|
'timeoutsec' => $in{'swap_timeoutsec'},
|
|
}) if ($in{'swap_what'} || $in{'swap_priority'} ||
|
|
$in{'swap_options'} || $in{'swap_timeoutsec'});
|
|
}
|
|
elsif ($unittype eq 'slice') {
|
|
foreach my $o ('slice_cpuweight', 'slice_ioweight') {
|
|
!$in{$o} || ($in{$o} =~ /^\d+$/ &&
|
|
$in{$o} >= 1 && $in{$o} <= 10000) ||
|
|
error($text{'systemd_esliceweight'});
|
|
}
|
|
foreach my $o ('slice_memorymax', 'slice_tasksmax') {
|
|
!$in{$o} || $in{$o} =~ /^(infinity|\S+)$/ ||
|
|
error($text{'systemd_eslicelimit'});
|
|
}
|
|
$structured_body = render_slice_body({
|
|
'cpuweight' => $in{'slice_cpuweight'},
|
|
'memorymax' => $in{'slice_memorymax'},
|
|
'tasksmax' => $in{'slice_tasksmax'},
|
|
'ioweight' => $in{'slice_ioweight'},
|
|
}) if ($in{'slice_cpuweight'} ||
|
|
$in{'slice_memorymax'} ||
|
|
$in{'slice_tasksmax'} ||
|
|
$in{'slice_ioweight'});
|
|
}
|
|
$in{'name'} ||= $derived_name if ($derived_name);
|
|
|
|
# Users may type the suffix or choose it from the dropdown; keep them equal.
|
|
my $creatable_piped = join('|', map { quotemeta($_) }
|
|
@creatable_unit_types);
|
|
if ($in{'name'} =~ /\.($creatable_piped)$/i) {
|
|
lc($1) eq $unittype || error($text{'systemd_eunittype'});
|
|
$in{'name'} =~ s/\.($creatable_piped)$/\.$unittype/i;
|
|
}
|
|
else {
|
|
$in{'name'} .= ".".$unittype;
|
|
}
|
|
valid_creatable_unit_name($in{'name'}, $user_scope) ||
|
|
error($text{'systemd_ename'});
|
|
if ($derived_name && $in{'name'} ne $derived_name) {
|
|
error($unittype eq 'mount' ?
|
|
$text{'systemd_emountname'} :
|
|
$text{'systemd_eautomountname'});
|
|
}
|
|
|
|
# Refuse to overwrite an existing unit in the selected manager.
|
|
my ($clash) = grep { $_->{'name'} eq $in{'name'} } @units;
|
|
$clash && error($text{'systemd_eclash'});
|
|
$in{'desc'} || error($text{'systemd_edesc'});
|
|
|
|
# Services use explicit command fields; other unit types accept only the
|
|
# body of their type-specific section, which is wrapped server-side.
|
|
if ($unittype eq 'service') {
|
|
$in{'atstart'} =~ /\S/ || error($text{'systemd_estart'});
|
|
}
|
|
else {
|
|
$in{'unitconf'} = "";
|
|
if (defined($structured_body) && $structured_body =~ /\S/) {
|
|
$in{'unitconf'} = $structured_body;
|
|
$in{'unitconf'} .= "\n" if ($raw_unitconf =~ /\S/);
|
|
}
|
|
$in{'unitconf'} .= $raw_unitconf;
|
|
my %empty_ok = ( 'target' => 1, 'slice' => 1 );
|
|
$empty_ok{$unittype} || $in{'unitconf'} =~ /\S/ ||
|
|
error($text{'systemd_eunitconf'});
|
|
}
|
|
|
|
# Parse optional scalar settings into %opts.
|
|
my %opts;
|
|
$in{'restart'} = $in{'restart_policy'}
|
|
if ($in{'new'} && defined($in{'restart_policy'}));
|
|
|
|
# These options map to single-line unit directives, so line breaks collapse.
|
|
foreach my $o ('before', 'after', 'wants', 'requires', 'conflicts',
|
|
'onfailure', 'onsuccess', 'type', 'env', 'envfile',
|
|
'user', 'group', 'killmode', 'workdir', 'restart',
|
|
'restartsec', 'watchdogsec', 'timeout',
|
|
'timeoutstartsec',
|
|
'timeoutstopsec', 'limitnofile', 'logstd', 'logerr',
|
|
'syslogid', 'protectsystem', 'readwritepaths',
|
|
'wantedby') {
|
|
if (defined($in{$o})) {
|
|
$in{$o} =~ s/\r|\n/ /g;
|
|
$in{$o} =~ s/^\s+//;
|
|
$in{$o} =~ s/\s+$//;
|
|
$opts{$o} = $in{$o} if ($in{$o} =~ /\S/);
|
|
}
|
|
}
|
|
|
|
# Keep one command hook per input line.
|
|
foreach my $o ('startpre', 'startpost', 'stoppost') {
|
|
if (defined($in{$o})) {
|
|
$in{$o} =~ s/\r//g;
|
|
$in{$o} =~ s/^\s+//;
|
|
$in{$o} =~ s/\s+$//;
|
|
$opts{$o} = $in{$o} if ($in{$o} =~ /\S/);
|
|
}
|
|
}
|
|
|
|
# Reload and PID file use dedicated service fields rather than %opts.
|
|
foreach my $o ('reload', 'pidfile') {
|
|
$in{$o} = "" if (!defined($in{$o}));
|
|
$in{$o} =~ s/\r//g;
|
|
$in{$o} =~ s/\n/ /g if ($o eq 'pidfile');
|
|
$in{$o} =~ s/^\s+//;
|
|
$in{$o} =~ s/\s+$//;
|
|
}
|
|
|
|
# Boolean options are emitted only when enabled.
|
|
foreach my $o ('nonewprivs', 'privatetmp') {
|
|
$opts{$o} = 1 if ($in{$o});
|
|
}
|
|
my %duration_text = (
|
|
'restartsec' => $text{'systemd_restartsec'},
|
|
'watchdogsec' => $text{'systemd_watchdogsec'},
|
|
'timeout' => $text{'systemd_timeout'},
|
|
'timeoutstartsec' => $text{'systemd_timeout'},
|
|
'timeoutstopsec' => $text{'systemd_timeoutstop'},
|
|
);
|
|
|
|
# Service-only options are validated against systemd's expected value
|
|
# shapes, so invalid units fail on save instead of on daemon-reload.
|
|
if ($unittype eq 'service') {
|
|
# Validate duration-like values before writing them to the unit file.
|
|
foreach my $o ('restartsec', 'watchdogsec', 'timeout',
|
|
'timeoutstartsec', 'timeoutstopsec') {
|
|
!$opts{$o} || valid_duration($opts{$o}) ||
|
|
error(text('systemd_eduration', $duration_text{$o}));
|
|
}
|
|
!$in{'pidfile'} || valid_path($in{'pidfile'}, 0, 0) ||
|
|
error(text('systemd_epath', $text{'systemd_pidfile'}));
|
|
!$opts{'workdir'} || valid_path($opts{'workdir'}, 1, 1) ||
|
|
error(text('systemd_epath', $text{'systemd_workdir'}));
|
|
!$opts{'envfile'} || valid_path($opts{'envfile'}, 1, 0) ||
|
|
error(text('systemd_epath', $text{'systemd_envfile'}));
|
|
!$opts{'limitnofile'} ||
|
|
$opts{'limitnofile'} =~ /^(infinity|\d+)(:(infinity|\d+))?$/i ||
|
|
error($text{'systemd_elimitnofile'});
|
|
foreach my $o ('logstd', 'logerr') {
|
|
!$opts{$o} || valid_output($opts{$o}) ||
|
|
error(text('systemd_eoutput', $text{'systemd_'.$o}));
|
|
}
|
|
!$opts{'protectsystem'} ||
|
|
$opts{'protectsystem'} =~ /^(true|full|strict)$/ ||
|
|
error($text{'systemd_eprotectsystem'});
|
|
|
|
# ReadWritePaths can contain several shell-style path words.
|
|
if ($opts{'readwritepaths'}) {
|
|
foreach my $p (split_quoted_string($opts{'readwritepaths'})) {
|
|
valid_path($p, 1, 0, 1) ||
|
|
error(text('systemd_ereadwritepath', $p));
|
|
}
|
|
}
|
|
}
|
|
|
|
# User units already run as the owning user, so User=/Group= must not be
|
|
# written into the unit file.
|
|
if ($user_scope) {
|
|
delete($opts{'user'});
|
|
delete($opts{'group'});
|
|
}
|
|
$opts{'wantedby'} ||= get_default_install_target(
|
|
$unittype, $user_scope);
|
|
|
|
# Render the unit once, then write the same bytes to the selected scope.
|
|
my %unit = ( 'type' => $unittype,
|
|
'description' => $in{'desc'},
|
|
'options' => \%opts );
|
|
if ($unittype eq 'service') {
|
|
$unit{'service'} = { 'start' => $in{'atstart'},
|
|
'stop' => $in{'atstop'},
|
|
'reload' => $in{'reload'},
|
|
'pidfile' => $in{'pidfile'},
|
|
'remain' => $in{'remain'} };
|
|
}
|
|
else {
|
|
$unit{'body'} = $in{'unitconf'};
|
|
}
|
|
my $unit_data = render_unit(\%unit);
|
|
|
|
# Create the unit file in the selected scope. When requested, linger is
|
|
# enabled first so daemon-reload has a user manager to talk to.
|
|
if ($user_scope) {
|
|
# Linger is optional on create, but enabling it also starts the manager.
|
|
if ($in{'linger'}) {
|
|
systemd_can_linger($unituser) ||
|
|
systemd_acl_error('plinger');
|
|
my ($lok, $lout) = set_user_linger($unituser, 1);
|
|
$lok || error_user_command($unituser, $lout);
|
|
my ($mok, $mout) = start_user_manager($unituser);
|
|
$mok || error_user_command($unituser, $mout);
|
|
}
|
|
my ($ok, $out, $kind) = create_user_unit(
|
|
$unituser, $in{'name'}, $unit_data);
|
|
if (!$ok) {
|
|
$kind && $kind ne 'command' ?
|
|
error($out) : error_user_command($unituser, $out);
|
|
}
|
|
}
|
|
else {
|
|
# System-scope units are written under the local systemd unit root.
|
|
my ($ok, $out) = create_system_unit($in{'name'}, $unit_data);
|
|
$ok || error($out);
|
|
}
|
|
|
|
# Enable or disable startup after the unit has been written and reloaded.
|
|
if (defined($in{'boot'}) &&
|
|
systemd_can_boot($user_scope, $unituser)) {
|
|
if ($user_scope) {
|
|
my ($ok, $out);
|
|
|
|
# User enable/disable failures include the systemctl output.
|
|
if ($in{'boot'} == 0) {
|
|
($ok, $out) =
|
|
disable_user_unit($unituser,
|
|
$in{'name'});
|
|
}
|
|
else {
|
|
($ok, $out) =
|
|
enable_user_unit($unituser,
|
|
$in{'name'});
|
|
}
|
|
$ok || error_user_command($unituser, $out);
|
|
}
|
|
else {
|
|
# System enable/disable uses the existing Webmin error path.
|
|
if ($in{'boot'} == 0) {
|
|
my ($ok, $out) = disable_unit($in{'name'});
|
|
$ok || error($out);
|
|
}
|
|
else {
|
|
my ($ok, $out) = enable_unit($in{'name'});
|
|
$ok || error($out);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Log the create event, then return to the configured destination.
|
|
if ($user_scope) {
|
|
webmin_log("create", "systemd-user", $in{'name'},
|
|
{ 'user' => $unituser });
|
|
}
|
|
else {
|
|
webmin_log("create", "systemd", $in{'name'});
|
|
}
|
|
|
|
if ($config{'create_return_index'} eq '1') {
|
|
$redirect = index_url($in{'name'}, $user_scope, $unituser);
|
|
}
|
|
elsif ($user_scope) {
|
|
$redirect = "edit_unit.cgi?scope=user&unituser=".
|
|
urlize($unituser)."&name=".urlize($in{'name'});
|
|
}
|
|
else {
|
|
$redirect = "edit_unit.cgi?name=".urlize($in{'name'});
|
|
}
|
|
}
|
|
else {
|
|
# Save the raw unit file contents from the edit form.
|
|
my $can_save_unit = $edit_dropin ?
|
|
systemd_can_dropin($user_scope, $unituser) :
|
|
systemd_can_edit($user_scope, $unituser);
|
|
$can_save_unit ||
|
|
systemd_acl_error($edit_dropin ?
|
|
($user_scope ? 'pdropin_user' : 'pdropin') :
|
|
($user_scope ? 'pedit_user' : 'pedit'));
|
|
if (!unit_file_editable($u)) {
|
|
error($text{'systemd_ereadonly'});
|
|
}
|
|
$in{'data'} =~ /\S/ || error($text{'systemd_econf'});
|
|
$in{'data'} =~ s/\r//g;
|
|
my $save_data = $edit_dropin ?
|
|
dropin_effective_data($in{'data'}) : $in{'data'};
|
|
my $base_data;
|
|
my ($vok, $vout);
|
|
if ($edit_dropin) {
|
|
# Drop-ins are verified together with the base unit they override.
|
|
$base_data = $user_scope ?
|
|
read_user_unit_file($unituser, $u->{'file'}) :
|
|
read_file_contents($u->{'file'});
|
|
defined($base_data) ||
|
|
error($user_scope ? $text{'systemd_euserunitfile'} :
|
|
$text{'manual_eread'});
|
|
($vok, $vout) =
|
|
verify_dropin_data($u->{'file'}, $base_data,
|
|
$save_data, $user_scope,
|
|
$u->{'unitstate'}, $unituser);
|
|
}
|
|
else {
|
|
# Full unit edits are verified directly under their unit basename.
|
|
($vok, $vout) =
|
|
verify_unit_data($u->{'file'}, $save_data,
|
|
$user_scope, $unituser);
|
|
}
|
|
$vok || error($vout);
|
|
if ($user_scope) {
|
|
# User unit writes go through a privilege-dropped helper. Linger is
|
|
# disabled only after daemon-reload succeeds, so the reload is not cut
|
|
# off from the user manager.
|
|
my $disable_linger;
|
|
my ($wok, $wout) = $edit_dropin && $dropin_file ?
|
|
write_user_dropin_config_file($unituser, $dropin_file,
|
|
$save_data) :
|
|
$edit_dropin ?
|
|
write_user_dropin_file($unituser, $in{'name'},
|
|
$save_data) :
|
|
write_user_unit_file($unituser, $u->{'file'},
|
|
$save_data);
|
|
$wok || error($wout);
|
|
|
|
# Enabling linger happens before reload; disabling waits until after.
|
|
if (defined($in{'linger'})) {
|
|
systemd_can_linger($unituser) ||
|
|
systemd_acl_error('plinger');
|
|
if ($in{'linger'}) {
|
|
my ($lok, $lout) =
|
|
set_user_linger($unituser, 1);
|
|
$lok || error_user_command($unituser, $lout);
|
|
my ($mok, $mout) =
|
|
start_user_manager($unituser);
|
|
$mok || error_user_command($unituser, $mout);
|
|
}
|
|
else {
|
|
$disable_linger = 1;
|
|
}
|
|
}
|
|
my ($ok, $out) = reload_user_manager($unituser);
|
|
$ok || error_user_command($unituser, $out);
|
|
|
|
# Disable linger only after the user manager has accepted daemon-reload.
|
|
if ($disable_linger) {
|
|
my ($lok, $lout) = set_user_linger($unituser, 0);
|
|
$lok || error_user_command($unituser, $lout);
|
|
}
|
|
}
|
|
else {
|
|
# System units are root-owned and can be updated directly.
|
|
if ($edit_dropin && $dropin_file) {
|
|
my ($wok, $wout) =
|
|
write_system_dropin_config_file($dropin_file,
|
|
$save_data);
|
|
$wok || error($wout);
|
|
}
|
|
elsif ($edit_dropin) {
|
|
my ($wok, $wout) =
|
|
write_system_dropin_file($in{'name'},
|
|
$save_data);
|
|
$wok || error($wout);
|
|
}
|
|
else {
|
|
my $conf_fh = gensym();
|
|
open_lock_tempfile($conf_fh, ">$u->{'file'}");
|
|
print_tempfile($conf_fh, $save_data);
|
|
close_tempfile($conf_fh);
|
|
}
|
|
reload_manager();
|
|
}
|
|
|
|
# Apply startup state changes after saving the config.
|
|
if (defined($in{'boot'}) &&
|
|
boot_state_changeable($u->{'unitstate'}, $u->{'name'})) {
|
|
systemd_can_boot($user_scope, $unituser) ||
|
|
systemd_acl_error('pboot');
|
|
if ($user_scope) {
|
|
my ($ok, $out);
|
|
|
|
# Startup state is managed through the same scoped manager as edit.
|
|
if ($in{'boot'} == 0) {
|
|
($ok, $out) =
|
|
disable_user_unit(
|
|
$unituser, $in{'name'});
|
|
}
|
|
else {
|
|
($ok, $out) =
|
|
enable_user_unit(
|
|
$unituser, $in{'name'});
|
|
}
|
|
$ok || error_user_command($unituser, $out);
|
|
}
|
|
else {
|
|
# System-unit startup state is independent of the raw file write.
|
|
if ($in{'boot'} == 0) {
|
|
my ($ok, $out) =
|
|
disable_unit($in{'name'});
|
|
$ok || error($out);
|
|
}
|
|
else {
|
|
my ($ok, $out) =
|
|
enable_unit($in{'name'});
|
|
$ok || error($out);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Log the edit and return to the same scoped edit page.
|
|
if ($user_scope) {
|
|
webmin_log("modify", "systemd-user", $in{'name'},
|
|
{ 'user' => $unituser });
|
|
$redirect = "edit_unit.cgi?scope=user&unituser=".
|
|
urlize($unituser)."&name=".urlize($in{'name'}).
|
|
($edit_dropin ? "&dropin=1" : "").
|
|
($dropin_file ? "&dropfile=".urlize($dropin_file) : "");
|
|
}
|
|
else {
|
|
webmin_log("modify", "systemd", $in{'name'});
|
|
$redirect = "edit_unit.cgi?name=".urlize($in{'name'}).
|
|
($edit_dropin ? "&dropin=1" : "").
|
|
($dropin_file ? "&dropfile=".urlize($dropin_file) : "");
|
|
}
|
|
}
|
|
redirect($redirect || "");
|
|
|
|
# error_user_command(user, output)
|
|
# Shows a systemctl --user or loginctl failure with escaped command output.
|
|
sub error_user_command
|
|
{
|
|
my ($user, $out) = @_;
|
|
$out ||= $text{'systemd_euser'};
|
|
|
|
# Show command output as escaped preformatted text for easier diagnosis.
|
|
error(text('systemd_eusercmd',
|
|
ui_tag('tt', html_escape($user)),
|
|
ui_tag('pre', html_escape($out))));
|
|
}
|