#!/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(\%access, 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(\%access, 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(\%access, 'start', $user_scope, $unituser) || systemd_acl_error('pstart'); } elsif ($in{'stop'}) { systemd_can_runtime(\%access, 'stop', $user_scope, $unituser) || systemd_acl_error('pstop'); } elsif ($in{'restart'}) { systemd_can_runtime(\%access, 'restart', $user_scope, $unituser) || systemd_acl_error('prestart'); } elsif ($in{'logs'}) { systemd_can_logs(\%access, $user_scope, $unituser) || systemd_acl_error('plogs'); } else { systemd_can_inspect(\%access, $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(\%access, $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(\%access, $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(\%access, $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(\%access, $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(\%access, $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(\%access, $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(\%access, $user_scope, $unituser) : systemd_can_edit(\%access, $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(\%access, $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(\%access, $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)))); }