diff --git a/mod_core_list.txt b/mod_core_list.txt
index fd8c059c9..5108b09a8 100644
--- a/mod_core_list.txt
+++ b/mod_core_list.txt
@@ -1 +1 @@
-acl apache authentic-theme backup-config bind8 change-user cron dovecot fail2ban fdisk filemin firewalld fsdump gray-theme htaccess-htpasswd init logrotate logviewer lvm mailboxes mailcap mount mysql net package-updates passwd phpini postfix proc procmail proftpd quota servers software spam sshd status system-status time updown useradmin usermin webmin webmincron webminlog xterm
\ No newline at end of file
+acl apache authentic-theme backup-config bind8 change-user cron dovecot fail2ban fdisk filemin firewalld fsdump gray-theme htaccess-htpasswd init systemd logrotate logviewer lvm mailboxes mailcap mount mysql net package-updates passwd phpini postfix proc procmail proftpd quota servers software spam sshd status system-status time updown useradmin usermin webmin webmincron webminlog xterm
\ No newline at end of file
diff --git a/mod_full_list.txt b/mod_full_list.txt
index a5f8fb4cf..619767c54 100644
--- a/mod_full_list.txt
+++ b/mod_full_list.txt
@@ -1 +1 @@
-acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 heartbeat hpuxexports htaccess-htpasswd idmapd inetd init inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones
+acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 heartbeat hpuxexports htaccess-htpasswd idmapd inetd init systemd inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones
diff --git a/systemd/acl_security.pl b/systemd/acl_security.pl
new file mode 100644
index 000000000..d076e78ec
--- /dev/null
+++ b/systemd/acl_security.pl
@@ -0,0 +1,81 @@
+use strict;
+use warnings;
+no warnings 'redefine';
+
+require 'systemd-lib.pl'; ## no critic
+
+our (%in, %text);
+
+# acl_security_form(options)
+# Outputs ACL controls for granting access to systemd unit management.
+sub acl_security_form
+{
+my ($o) = @_;
+my $m = $o->{'mode'} || 0;
+
+print ui_table_span(ui_tag('b', html_escape($text{'acl_section_users'})));
+print ui_table_row($text{'acl_users'},
+ ui_radio("mode", $m,
+ [ [ 0, "$text{'acl_all'}
" ],
+ [ 3, "$text{'acl_this'}
" ],
+ [ 1, $text{'acl_only'}." ".
+ ui_textbox("userscan",
+ $m == 1 ? $o->{'users'} : "", 40)." ".
+ user_chooser_button("userscan", 1)."
" ],
+ [ 2, $text{'acl_except'}." ".
+ ui_textbox("userscannot",
+ $m == 2 ? $o->{'users'} : "", 40)." ".
+ user_chooser_button("userscannot", 1)."
" ],
+ [ 5, $text{'acl_gid'}." ".
+ ui_textbox("gid",
+ $m == 5 ? scalar(getgrgid($o->{'users'})) : "", 13)." ".
+ group_chooser_button("gid", 0)."
" ],
+ [ 4, $text{'acl_uid'}." ".
+ ui_textbox("uidmin", $o->{'uidmin'}, 6)." - ".
+ ui_textbox("uidmax", $o->{'uidmax'}, 6)."
" ],
+ ]), 3, undef, undef, 1);
+print ui_table_hr();
+
+print ui_table_span(ui_tag('b', html_escape($text{'acl_section_view'})));
+foreach my $a (qw(view view_user status status_user logs logs_user)) {
+ print ui_table_row($text{'acl_'.$a},
+ ui_yesno_radio($a, $o->{$a}), 3);
+ }
+print ui_table_hr();
+
+print ui_table_span(ui_tag('b', html_escape($text{'acl_section_runtime'})));
+foreach my $a (qw(start start_user stop stop_user restart restart_user
+ boot boot_user mask mask_user reload linger)) {
+ print ui_table_row($text{'acl_'.$a},
+ ui_yesno_radio($a, $o->{$a}), 3);
+ }
+print ui_table_hr();
+
+print ui_table_span(ui_tag('b', html_escape($text{'acl_section_change'})));
+foreach my $a (qw(create create_user edit edit_user delete delete_user
+ dropin dropin_user manual manual_user backup)) {
+ print ui_table_row($text{'acl_'.$a},
+ ui_yesno_radio($a, $o->{$a}), 3);
+ }
+}
+
+# acl_security_save(options)
+# Saves systemd ACL settings from the submitted form.
+sub acl_security_save
+{
+my ($o) = @_;
+
+my $mode = defined($in{'mode'}) && $in{'mode'} =~ /^[0-5]$/ ?
+ $in{'mode'} : 0;
+$o->{'mode'} = $mode;
+$o->{'users'} = $mode == 0 || $mode == 3 || $mode == 4 ? "" :
+ $mode == 5 ? scalar(getgrnam($in{'gid'} || "")) || "" :
+ $mode == 1 ? $in{'userscan'} || "" : $in{'userscannot'} || "";
+$o->{'uidmin'} = $mode == 4 ? $in{'uidmin'} || "" : "";
+$o->{'uidmax'} = $mode == 4 ? $in{'uidmax'} || "" : "";
+foreach my $a (systemd_acl_keys()) {
+ $o->{$a} = $in{$a} || 0;
+ }
+}
+
+1;
diff --git a/systemd/backup_config.pl b/systemd/backup_config.pl
new file mode 100644
index 000000000..425c6b4b1
--- /dev/null
+++ b/systemd/backup_config.pl
@@ -0,0 +1,83 @@
+use strict;
+use warnings;
+
+require 'systemd-lib.pl'; ## no critic
+
+our %access;
+
+# backup_config_files()
+# Returns local system and user unit files that should be included in backups.
+sub backup_config_files
+{
+my @rv;
+my %seen;
+return @rv if (!systemd_acl_bool(\%access, 'backup'));
+my $add_file = sub {
+ my ($file) = @_;
+ push(@rv, $file) if ($file && !$seen{$file}++);
+ };
+
+# System unit backups should only include locally managed files under /etc.
+foreach my $u (list_units()) {
+ $add_file->($u->{'file'})
+ if ($u->{'file'} && $u->{'file'} =~ m!^/etc/systemd/system/!);
+ my $name = backup_unit_name($u);
+ if ($name && dropin_exists(0, undef, $name)) {
+ $add_file->(system_dropin_file($name));
+ }
+ }
+
+# User units live under home directories and are safe to include by path.
+foreach my $u (list_all_user_units()) {
+ next if (!systemd_acl_user_allowed(\%access, $u->{'user'}));
+ $add_file->($u->{'file'}) if ($u->{'file'});
+ my $name = backup_unit_name($u);
+ if ($name && dropin_exists(1, $u->{'user'}, $name)) {
+ $add_file->(user_dropin_file($u->{'user'}, $name));
+ }
+ }
+return @rv;
+}
+
+# backup_unit_name(unit)
+# Returns the safe unit name from a listed unit row.
+sub backup_unit_name
+{
+my ($u) = @_;
+return $u->{'name'} if ($u->{'name'} && valid_unit_name($u->{'name'}));
+if ($u->{'file'} && $u->{'file'} =~ m{/([^/]+)$} &&
+ valid_unit_name($1)) {
+ return $1;
+ }
+return;
+}
+
+# pre_backup()
+# No preparation is needed before Webmin copies systemd unit files.
+sub pre_backup
+{
+return;
+}
+
+# post_backup()
+# No cleanup is needed after Webmin copies systemd unit files.
+sub post_backup
+{
+return;
+}
+
+# pre_restore()
+# No preparation is needed before Webmin restores systemd unit files.
+sub pre_restore
+{
+return;
+}
+
+# post_restore()
+# Reloads systemd after restored unit files are back on disk.
+sub post_restore
+{
+reload_manager();
+}
+
+1;
diff --git a/systemd/config b/systemd/config
new file mode 100644
index 000000000..a316c83e5
--- /dev/null
+++ b/systemd/config
@@ -0,0 +1,11 @@
+desc=1
+logs_lines=200
+logs_current_boot=0
+visible_tabs=service,timer,socket,path,target,storage,resources,device,user
+show_runtime_units=1
+default_create_scope=system
+manual_vendor_units=1
+default_linger=1
+show_unit_suffixes=0
+show_dropin_inventory=1
+create_return_index=0
diff --git a/systemd/config.info b/systemd/config.info
new file mode 100644
index 000000000..dd1407c7e
--- /dev/null
+++ b/systemd/config.info
@@ -0,0 +1,11 @@
+desc=Display unit descriptions,1,1-Yes,0-No
+logs_lines=Number of journal log lines to show,0
+logs_current_boot=Journal log scope,1,1-Current boot only,0-All available logs
+visible_tabs=Tabs to show on the index page,15,visible_tabs
+show_runtime_units=Show generated and transient units,1,1-Yes,0-No
+default_create_scope=Default scope for new units,1,system-System units,user-User units
+manual_vendor_units=Include vendor unit files in the manual editor,1,1-Yes,0-No
+default_linger=Enable linger by default for new user units,1,1-Yes,0-No
+show_unit_suffixes=Show full unit names with type suffixes,1,1-Yes,0-No
+show_dropin_inventory=Show drop-in override inventory,1,1-Yes,0-No
+create_return_index=Return to index after creating a new unit,1,1-Yes,0-No
diff --git a/systemd/config_info.pl b/systemd/config_info.pl
new file mode 100644
index 000000000..e5e454486
--- /dev/null
+++ b/systemd/config_info.pl
@@ -0,0 +1,34 @@
+use strict;
+use warnings;
+
+require './systemd-lib.pl'; ## no critic
+
+our (%in, %text);
+
+# show_visible_tabs(value)
+# Returns checkbox controls for choosing which index tabs are visible.
+sub show_visible_tabs
+{
+my ($value) = @_;
+my %enabled = map { $_, 1 } split(/\s*,\s*/, $value || default_visible_tabs());
+my @fields;
+foreach my $tab (get_index_tab_ids()) {
+ my $label = $text{'systemd_tab_'.$tab} || ucfirst($tab);
+ push(@fields, ui_checkbox(
+ "visible_tabs", $tab, $label, $enabled{$tab}));
+ }
+return join(" ", @fields);
+}
+
+# parse_visible_tabs(value)
+# Returns selected tab IDs, and rejects saving with every tab hidden.
+sub parse_visible_tabs
+{
+my @tabs = split(/\0/, $in{'visible_tabs'} || "");
+my %valid = map { $_, 1 } get_index_tab_ids();
+@tabs = grep { $valid{$_} } @tabs;
+@tabs || error($text{'systemd_evisibletabs'});
+return join(",", @tabs);
+}
+
+1;
diff --git a/systemd/defaultacl b/systemd/defaultacl
new file mode 100644
index 000000000..6ba026d80
--- /dev/null
+++ b/systemd/defaultacl
@@ -0,0 +1,34 @@
+view=1
+view_user=1
+status=1
+status_user=1
+logs=1
+logs_user=1
+start=1
+start_user=1
+stop=1
+stop_user=1
+restart=1
+restart_user=1
+boot=1
+boot_user=1
+mask=1
+mask_user=1
+create=1
+create_user=1
+edit=1
+edit_user=1
+delete=1
+delete_user=1
+dropin=1
+dropin_user=1
+manual=1
+manual_user=1
+reload=1
+linger=1
+backup=1
+mode=0
+users=
+uidmin=
+uidmax=
+noconfig=0
diff --git a/systemd/dropins.cgi b/systemd/dropins.cgi
new file mode 100755
index 000000000..735814772
--- /dev/null
+++ b/systemd/dropins.cgi
@@ -0,0 +1,122 @@
+#!/usr/local/bin/perl
+# Show an inventory of discovered systemd drop-in override files.
+
+use strict;
+use warnings;
+
+require './systemd-lib.pl'; ## no critic
+
+our (%access, %config, %text);
+
+ReadParse();
+
+has_command("systemctl") || error($text{'systemd_esystemctl'});
+systemd_can_enter_module(\%access) || systemd_acl_error('penter');
+$config{'show_dropin_inventory'} || error($text{'dropins_disabled'});
+
+my $can_system = systemd_can_view_system(\%access);
+my $can_user = systemd_can_view_user_scope(\%access);
+
+my @system_units = $can_system ? list_units() : ( );
+my %system_units = map { $_->{'name'}, $_ } @system_units;
+my @user_units = $can_user ?
+ grep { systemd_acl_user_allowed(\%access, $_->{'user'}) }
+ list_all_user_units() : ( );
+my %user_units = map { $_->{'user'}."\t".$_->{'name'}, $_ } @user_units;
+
+my @dropins;
+push(@dropins, list_system_dropin_override_files()) if ($can_system);
+if ($can_user) {
+ foreach my $dropin (list_all_user_dropin_override_files()) {
+ next if (!systemd_acl_user_allowed(\%access, $dropin->{'user'}));
+ push(@dropins, $dropin);
+ }
+ }
+@dropins = sort { $a->{'scope'} cmp $b->{'scope'} ||
+ ($a->{'user'} || "") cmp ($b->{'user'} || "") ||
+ $a->{'unit'} cmp $b->{'unit'} ||
+ $a->{'file'} cmp $b->{'file'} } @dropins;
+
+ui_print_header(undef, $text{'dropins_title'}, "", "intro", undef, 1,
+ undef, action_links());
+
+print ui_tag('p', $text{'dropins_desc'});
+if (!@dropins) {
+ print ui_tag('p', $text{'dropins_empty'});
+ }
+else {
+ print_dropin_table(\@dropins, \%system_units, \%user_units);
+ }
+
+ui_print_footer("index.cgi", $text{'index_return'});
+
+# print_dropin_table(dropins, system-units, user-units)
+# Outputs the discovered drop-in inventory table.
+sub print_dropin_table
+{
+my ($dropins, $system_units, $user_units) = @_;
+print ui_columns_start([
+ $text{'systemd_name'},
+ $text{'dropins_scope'},
+ $text{'systemd_owner'},
+ $text{'dropins_file'},
+ $text{'dropins_actions'},
+ ]);
+foreach my $dropin (@$dropins) {
+ print ui_columns_row([
+ ui_tag('tt', html_escape($dropin->{'unit'})),
+ html_escape(dropin_scope_label($dropin)),
+ $dropin->{'scope'} eq 'user' ?
+ ui_tag('tt', html_escape($dropin->{'user'})) :
+ html_escape("-"),
+ ui_tag('tt', html_escape($dropin->{'file'})),
+ dropin_action_link($dropin, $system_units, $user_units),
+ ]);
+ }
+print ui_columns_end();
+}
+
+# dropin_scope_label(dropin)
+# Returns a human-readable scope label for a drop-in descriptor.
+sub dropin_scope_label
+{
+my ($dropin) = @_;
+return $dropin->{'scope'} eq 'user' ?
+ $text{'dropins_scope_user'} : $text{'dropins_scope_system'};
+}
+
+# dropin_action_link(dropin, system-units, user-units)
+# Returns an edit action when the discovered drop-in belongs to a known unit.
+sub dropin_action_link
+{
+my ($dropin, $system_units, $user_units) = @_;
+if ($dropin->{'scope'} eq 'user') {
+ my $user = $dropin->{'user'};
+ my $unit = $dropin->{'unit'};
+ return ui_tag('i', html_escape($text{'dropins_unit_missing'}))
+ if (!$user_units->{$user."\t".$unit});
+ return ui_tag('i', html_escape($text{'dropins_view_only'}))
+ if (!systemd_can_dropin(\%access, 1, $user));
+ my $url = "edit_unit.cgi?scope=user&unituser=".urlize($user).
+ "&name=".urlize($unit)."&dropin=1".
+ dropin_file_arg($dropin);
+ return ui_link($url, $text{'dropins_edit'});
+ }
+my $unit = $dropin->{'unit'};
+return ui_tag('i', html_escape($text{'dropins_unit_missing'}))
+ if (!$system_units->{$unit});
+return ui_tag('i', html_escape($text{'dropins_view_only'}))
+ if (!systemd_can_dropin(\%access, 0));
+return ui_link("edit_unit.cgi?name=".urlize($unit)."&dropin=1".
+ dropin_file_arg($dropin),
+ $text{'dropins_edit'});
+}
+
+# dropin_file_arg(dropin)
+# Returns an exact drop-in file query argument for non-standard drop-ins.
+sub dropin_file_arg
+{
+my ($dropin) = @_;
+return "" if ($dropin->{'standard'});
+return "&dropfile=".urlize($dropin->{'file'});
+}
diff --git a/systemd/edit_manual.cgi b/systemd/edit_manual.cgi
new file mode 100755
index 000000000..1ae201149
--- /dev/null
+++ b/systemd/edit_manual.cgi
@@ -0,0 +1,70 @@
+#!/usr/local/bin/perl
+# Show a page for manually editing discovered systemd unit files.
+
+use strict;
+use warnings;
+
+require './systemd-lib.pl'; ## no critic
+
+our (%access, %in, %text);
+
+ReadParse();
+error_setup($text{'manual_edit_err'});
+
+systemd_acl_bool(\%access, 'manual') ||
+ systemd_acl_bool(\%access, 'manual_user') ||
+ systemd_acl_error('pmanual');
+
+# File choices are constrained to discovered system and local user unit files.
+my @files = grep { systemd_can_manual(\%access, $_) } list_manual_unit_files();
+@files || error(manual_empty_message());
+my %allowed = map { $_->{'file'}, $_ } @files;
+my $info = $allowed{$in{'file'}} || $files[0];
+my $file = $info->{'file'};
+my $data = read_manual_unit_file($info);
+defined($data) || error($text{'manual_eread'});
+
+ui_print_header(undef, $text{'manual_title'}, "");
+my $desc = $info->{'scope'} eq 'user' ?
+ text('manual_desc_user',
+ ui_tag('tt', html_escape($info->{'user'}))) :
+ $text{'manual_desc'};
+print ui_div($desc);
+
+# Keep the selector separate so changing files does not submit edits.
+print ui_form_start("edit_manual.cgi");
+print ui_tag('b', html_escape($text{'manual_select'}));
+print ui_select("file", $file,
+ [ map { [ $_->{'file'}, manual_unit_file_label($_) ] } @files ]);
+print " ", ui_submit($text{'manual_ok'});
+print ui_form_end();
+
+# The editor preserves raw unit text; validation is limited to the file path.
+print ui_form_start("save_manual.cgi", "form-data");
+print ui_hidden("file", $file);
+print ui_table_start(undef, undef, 2);
+print ui_table_row(undef, ui_textarea("data", $data, 35, 120), 2);
+print ui_table_end();
+print ui_form_end([ [ "save", $text{'save'} ] ]);
+
+ui_print_footer("index.cgi", $text{'index_return'});
+
+# manual_unit_file_label(info)
+# Returns the selector label for a manual-edit unit file.
+sub manual_unit_file_label
+{
+my ($info) = @_;
+return html_escape($info->{'file'});
+}
+
+# manual_empty_message()
+# Returns an empty-state message for the current manual-edit ACL scope.
+sub manual_empty_message
+{
+my $user = systemd_acl_default_user(\%access);
+return text('manual_enone_user',
+ ui_tag('tt', html_escape($user)))
+ if ($user && systemd_acl_bool(\%access, 'manual_user') &&
+ !systemd_acl_bool(\%access, 'manual'));
+return $text{'manual_enone'};
+}
diff --git a/systemd/edit_unit.cgi b/systemd/edit_unit.cgi
new file mode 100755
index 000000000..dbee6eb9a
--- /dev/null
+++ b/systemd/edit_unit.cgi
@@ -0,0 +1,1136 @@
+#!/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));
+}
diff --git a/systemd/help/config_create_return_index.html b/systemd/help/config_create_return_index.html
new file mode 100644
index 000000000..3b4fd1a5d
--- /dev/null
+++ b/systemd/help/config_create_return_index.html
@@ -0,0 +1,4 @@
+
Controls where the module goes after successfully creating a unit. When set +to yes, it returns to the matching index tab. When set to no, it opens the +new unit's edit page for immediate review and follow-up actions.
diff --git a/systemd/help/config_default_create_scope.html b/systemd/help/config_default_create_scope.html new file mode 100644 index 000000000..67427f8d0 --- /dev/null +++ b/systemd/help/config_default_create_scope.html @@ -0,0 +1,4 @@ +Sets the default scope used when the create-unit form is opened without an +explicit system or user context. Links from index tabs still keep their own +context, such as opening user-unit creation from the User units tab.
diff --git a/systemd/help/config_default_linger.html b/systemd/help/config_default_linger.html new file mode 100644 index 000000000..461ca05ad --- /dev/null +++ b/systemd/help/config_default_linger.html @@ -0,0 +1,4 @@ +Controls the default linger choice when creating new user units. Linger +allows the user's systemd manager and enabled user units to run without an +active login session.
diff --git a/systemd/help/config_desc.html b/systemd/help/config_desc.html new file mode 100644 index 000000000..cf4107823 --- /dev/null +++ b/systemd/help/config_desc.html @@ -0,0 +1,3 @@ +Controls whether unit descriptions are shown in the index tables. Disable +this for a denser table when names and states are enough.
diff --git a/systemd/help/config_logs_current_boot.html b/systemd/help/config_logs_current_boot.html new file mode 100644 index 000000000..e588cbf32 --- /dev/null +++ b/systemd/help/config_logs_current_boot.html @@ -0,0 +1,4 @@ +Controls whether the Logs action reads entries from the current boot only +or from all journal history available on the system. Current-boot logs are +usually faster and avoid mixing output from previous starts of the machine.
diff --git a/systemd/help/config_logs_lines.html b/systemd/help/config_logs_lines.html new file mode 100644 index 000000000..534362fa9 --- /dev/null +++ b/systemd/help/config_logs_lines.html @@ -0,0 +1,4 @@ +The number of recent journal lines to read for each selected unit when using +the Logs action. Larger values provide more history but can make log pages +longer or slower.
diff --git a/systemd/help/config_manual_vendor_units.html b/systemd/help/config_manual_vendor_units.html new file mode 100644 index 000000000..179cc71a4 --- /dev/null +++ b/systemd/help/config_manual_vendor_units.html @@ -0,0 +1,5 @@ +Controls whether the manual file editor includes packaged unit files from +vendor directories such as /usr/lib/systemd/system. These files are +useful for inspection, but normal customizations should usually be made with +drop-in overrides or local units under /etc/systemd/system.
diff --git a/systemd/help/config_show_dropin_inventory.html b/systemd/help/config_show_dropin_inventory.html new file mode 100644 index 000000000..e4ab2ef7e --- /dev/null +++ b/systemd/help/config_show_dropin_inventory.html @@ -0,0 +1,8 @@ +Controls whether the module index displays the drop-in overrides inventory +action view.
+ +The inventory lists discovered drop-in override files for scopes the +current Webmin user can view. Edit links are shown for safe drop-ins attached +to known units only when the matching system or user drop-in ACL permits +management of those overrides.
diff --git a/systemd/help/config_show_runtime_units.html b/systemd/help/config_show_runtime_units.html new file mode 100644 index 000000000..8c4bf86af --- /dev/null +++ b/systemd/help/config_show_runtime_units.html @@ -0,0 +1,4 @@ +Controls whether index tabs include units created dynamically by systemd, +such as transient scopes and generated units. Hiding them keeps the index +focused on persistent unit files that administrators normally edit.
diff --git a/systemd/help/config_show_unit_suffixes.html b/systemd/help/config_show_unit_suffixes.html new file mode 100644 index 000000000..6413e42c9 --- /dev/null +++ b/systemd/help/config_show_unit_suffixes.html @@ -0,0 +1,4 @@ +Controls whether index tables display full unit names such as +sshd.service or shorter base names such as sshd. When +suffixes are hidden, mixed tabs add a Unit type column where needed.
diff --git a/systemd/help/config_visible_tabs.html b/systemd/help/config_visible_tabs.html new file mode 100644 index 000000000..8eea77e00 --- /dev/null +++ b/systemd/help/config_visible_tabs.html @@ -0,0 +1,4 @@ +Selects which unit groups appear on the index page. At least one tab must +remain enabled. Hiding a tab only removes it from the index view; it does not +delete, disable, or mask any units.
diff --git a/systemd/help/intro.html b/systemd/help/intro.html new file mode 100644 index 000000000..4bf049088 --- /dev/null +++ b/systemd/help/intro.html @@ -0,0 +1,12 @@ +This module manages units controlled by systemd, including services, timers, +sockets, paths, targets, storage units, resource-control units, devices and user +units. The index groups units by type and shows both the unit file state, such +as enabled, disabled, static or masked, and the runtime state reported by +systemd.
+Use the unit tables to start, stop, restart, enable, disable, mask, inspect +status or read logs. Existing units can be edited directly when their unit files +are writable, or customized with drop-in override files when packaged base units +should be left intact. User units are managed through the owning user's systemd +manager, with linger controls for units that should keep running without an +active login.
diff --git a/systemd/help/systemd_after.html b/systemd/help/systemd_after.html new file mode 100644 index 000000000..925548778 --- /dev/null +++ b/systemd/help/systemd_after.html @@ -0,0 +1,6 @@ +Units that should be started before this unit. This writes +After= in the [Unit] section.
+Enter space-separated unit names, such as network-online.target +postgresql.service. This only controls ordering when both units are being +started. It does not by itself start the listed units.
diff --git a/systemd/help/systemd_automountidle.html b/systemd/help/systemd_automountidle.html new file mode 100644 index 000000000..0a84da9a1 --- /dev/null +++ b/systemd/help/systemd_automountidle.html @@ -0,0 +1,4 @@ +Optional systemd duration after which an idle automount is unmounted, such as +30s, 5min, or 1h. Leave blank to use systemd's +default behavior.
diff --git a/systemd/help/systemd_automountmode.html b/systemd/help/systemd_automountmode.html new file mode 100644 index 000000000..6932758ad --- /dev/null +++ b/systemd/help/systemd_automountmode.html @@ -0,0 +1,3 @@ +Optional octal mode used if systemd creates the automount directory, such as +0755 or 0700.
diff --git a/systemd/help/systemd_automountmount.html b/systemd/help/systemd_automountmount.html new file mode 100644 index 000000000..822aeb1f8 --- /dev/null +++ b/systemd/help/systemd_automountmount.html @@ -0,0 +1,6 @@ +Select a matching .mount unit for this automount. The automount +unit name and path will be derived from the selected mount unit. For example, +mnt-data.mount pairs with mnt-data.automount.
+If no matching mount is selected, enter an automount path instead. A matching +.mount unit for that path must already exist.
diff --git a/systemd/help/systemd_automountwhere.html b/systemd/help/systemd_automountwhere.html new file mode 100644 index 000000000..0f315884b --- /dev/null +++ b/systemd/help/systemd_automountwhere.html @@ -0,0 +1,4 @@ +The absolute path watched by the automount unit. Accessing this path causes +systemd to activate the matching .mount unit. The matching mount must +use the same path in its Where= setting.
diff --git a/systemd/help/systemd_before.html b/systemd/help/systemd_before.html new file mode 100644 index 000000000..20cedecb3 --- /dev/null +++ b/systemd/help/systemd_before.html @@ -0,0 +1,7 @@ +Units that should be ordered after this unit. This writes +Before= in the [Unit] section.
+Enter space-separated unit names, such as nginx.service +myapp.target. Ordering alone does not cause the other unit to start; pair +it with Wants= or Requires= when this unit should also +pull that unit into the same start transaction.
diff --git a/systemd/help/systemd_boot.html b/systemd/help/systemd_boot.html new file mode 100644 index 000000000..a26ae9cc7 --- /dev/null +++ b/systemd/help/systemd_boot.html @@ -0,0 +1,9 @@ +Controls whether the unit is enabled. System units are enabled for the +selected install target; user units are enabled in the selected user's systemd +manager.
+For user units, enabling the unit is separate from linger. Enable linger +when the unit should be able to start at boot or keep running after the user +logs out.
+This option is not shown for unit types or unit file states that systemd +cannot enable directly, such as transient, generated, scope, or device units.
diff --git a/systemd/help/systemd_conf.html b/systemd/help/systemd_conf.html new file mode 100644 index 000000000..8855afe13 --- /dev/null +++ b/systemd/help/systemd_conf.html @@ -0,0 +1,9 @@ +The raw unit file or drop-in override contents. For editable units, changes +made here are saved directly to the selected file.
+Use this for options that are not exposed by the form. After saving, the +system or user systemd manager is reloaded as appropriate so it sees the +updated unit definition.
+Runtime-managed units, such as transient scope units and generated units, are +shown read-only because systemd creates those files dynamically. Use the status, +properties, dependencies and log buttons to inspect them.
diff --git a/systemd/help/systemd_conflicts.html b/systemd/help/systemd_conflicts.html new file mode 100644 index 000000000..35e5783a9 --- /dev/null +++ b/systemd/help/systemd_conflicts.html @@ -0,0 +1,8 @@ +Units that cannot run at the same time as this unit. Starting one side +causes systemd to stop the other.
+Enter space-separated unit names. This is useful for mutually exclusive +implementations, such as two services that bind the same port or manage the +same resource.
+Use After= or Before= as well if the stop/start order +matters when switching between conflicting units.
diff --git a/systemd/help/systemd_desc.html b/systemd/help/systemd_desc.html new file mode 100644 index 000000000..942428a26 --- /dev/null +++ b/systemd/help/systemd_desc.html @@ -0,0 +1,5 @@ +A short human-readable unit description written as Description= +in the unit's [Unit] section.
+This text appears in commands such as systemctl status. It is only +a label and does not change ordering, startup behavior, or logging.
diff --git a/systemd/help/systemd_env.html b/systemd/help/systemd_env.html new file mode 100644 index 000000000..b4621d0ce --- /dev/null +++ b/systemd/help/systemd_env.html @@ -0,0 +1,9 @@ +Environment variables to pass to the service, written as +Environment=. Use systemd's normal assignment syntax, such as +NAME=value.
+Enter one or more assignments separated by spaces, for example +NODE_ENV=production PORT=3000. Quote values that contain spaces, such +as APP_NAME="My App".
+For larger or secret-bearing sets of values, prefer an environment file and +set permissions on that file carefully.
diff --git a/systemd/help/systemd_envfile.html b/systemd/help/systemd_envfile.html new file mode 100644 index 000000000..6ae5edce1 --- /dev/null +++ b/systemd/help/systemd_envfile.html @@ -0,0 +1,11 @@ +Absolute path to a file containing environment variables for the service. +This writes EnvironmentFile=. Prefix the path with - to +ignore a missing file.
+Each line in the file should normally be a shell-style assignment such as +NAME=value. Common examples are /etc/default/myapp, +/etc/sysconfig/myapp, or a private file under the application's +directory.
+For user units, use an absolute path to a file readable by the selected +user, typically below that user's home directory such as +/home/example/.config/myapp/environment.
diff --git a/systemd/help/systemd_file.html b/systemd/help/systemd_file.html new file mode 100644 index 000000000..66404d7ee --- /dev/null +++ b/systemd/help/systemd_file.html @@ -0,0 +1,8 @@ +The path to the systemd unit file or drop-in override file currently being +edited.
+System units are normally stored under a system unit directory. User +units created here are stored below the selected user's +~/.config/systemd/user directory.
+Packaged vendor unit files may be shown for inspection, but normal local +changes should usually be made with a drop-in override or a local unit file.
diff --git a/systemd/help/systemd_group.html b/systemd/help/systemd_group.html new file mode 100644 index 000000000..66aedcd93 --- /dev/null +++ b/systemd/help/systemd_group.html @@ -0,0 +1,5 @@ +For system services, writes Group= so the service process runs with +the selected Unix group. This option is hidden for user units.
+Leave this empty to use the selected user's default group. Set it only when +the service needs a specific primary group for file or socket access.
diff --git a/systemd/help/systemd_killmode.html b/systemd/help/systemd_killmode.html new file mode 100644 index 000000000..75132a41a --- /dev/null +++ b/systemd/help/systemd_killmode.html @@ -0,0 +1,11 @@ +Controls how systemd terminates processes belonging to the service. This +writes KillMode=.
+control-group is the default and safest choice: systemd stops the +main process and any remaining child processes in the service cgroup. +mixed sends the first termination signal only to the main process, +then later kills remaining cgroup processes if needed.
+process stops only the main process and can leave child processes +behind. none makes systemd run the stop command but not kill service +processes. Avoid process and none unless you know the +application manages its own process tree safely.
diff --git a/systemd/help/systemd_limitnofile.html b/systemd/help/systemd_limitnofile.html new file mode 100644 index 000000000..0d385487c --- /dev/null +++ b/systemd/help/systemd_limitnofile.html @@ -0,0 +1,6 @@ +Sets the service file descriptor limit with LimitNOFILE=.
+Enter a number such as 65535, infinity, or a +soft:hard pair such as 4096:65535. This is commonly needed by busy +web servers, proxies, databases, and applications that keep many sockets or +files open.
diff --git a/systemd/help/systemd_linger.html b/systemd/help/systemd_linger.html new file mode 100644 index 000000000..0c90e523f --- /dev/null +++ b/systemd/help/systemd_linger.html @@ -0,0 +1,8 @@ +Controls systemd linger, implemented by +loginctl enable-linger for the selected user. This allows the user's +systemd manager and enabled user units to run after the user logs out and to +start at boot.
+Without linger, a user unit normally requires an active login session or an +already-running user manager. This is fine for desktop/session units, but +server-style user units usually need linger enabled.
diff --git a/systemd/help/systemd_linger_user.html b/systemd/help/systemd_linger_user.html new file mode 100644 index 000000000..37c260fad --- /dev/null +++ b/systemd/help/systemd_linger_user.html @@ -0,0 +1,7 @@ +Controls whether this user's systemd manager is allowed to continue running +without an active login session. This is useful for user units that should keep +running after the user logs out or start at boot.
+This is systemd linger for the unit owner. Enabling a user unit at boot and +allowing it to run without login are separate settings; server-style user units +usually need both.
diff --git a/systemd/help/systemd_logerr.html b/systemd/help/systemd_logerr.html new file mode 100644 index 000000000..1ff733783 --- /dev/null +++ b/systemd/help/systemd_logerr.html @@ -0,0 +1,11 @@ +Destination for the service standard error stream, written as +StandardError=. Common values include journal, +null, inherit, journal+console, +file:/path/to/file, append:/path/to/file, and +truncate:/path/to/file.
+Advanced systemd targets such as kmsg, tty, +socket, and fd:name are also accepted.
+If you enter an absolute file path, it will be written as +append:/path/to/file. Use journal to keep errors in +journalctl, or leave empty to inherit the systemd default.
diff --git a/systemd/help/systemd_logstd.html b/systemd/help/systemd_logstd.html new file mode 100644 index 000000000..cac99ae09 --- /dev/null +++ b/systemd/help/systemd_logstd.html @@ -0,0 +1,11 @@ +Destination for the service standard output stream, written as +StandardOutput=. Common values include journal, +null, inherit, journal+console, +file:/path/to/file, append:/path/to/file, and +truncate:/path/to/file.
+Advanced systemd targets such as kmsg, tty, +socket, and fd:name are also accepted.
+If you enter an absolute file path, it will be written as +append:/path/to/file so output is appended instead of replacing the +file. Leave empty to use the systemd default.
diff --git a/systemd/help/systemd_main_pid.html b/systemd/help/systemd_main_pid.html new file mode 100644 index 000000000..f3645ccf2 --- /dev/null +++ b/systemd/help/systemd_main_pid.html @@ -0,0 +1,6 @@ +The main process ID reported by systemd for this unit. It is shown only when +systemd reports a positive process ID.
+ +Use the status and properties buttons for the full process and unit state +reported by systemd.
diff --git a/systemd/help/systemd_mountoptions.html b/systemd/help/systemd_mountoptions.html new file mode 100644 index 000000000..9d593187f --- /dev/null +++ b/systemd/help/systemd_mountoptions.html @@ -0,0 +1,4 @@ +Optional comma-separated mount options, such as defaults, +noatime, ro, or rw,nosuid,nodev. These are written +as the Options= directive in the [Mount] section.
diff --git a/systemd/help/systemd_mounttype.html b/systemd/help/systemd_mounttype.html new file mode 100644 index 000000000..03fdd6217 --- /dev/null +++ b/systemd/help/systemd_mounttype.html @@ -0,0 +1,4 @@ +The optional filesystem type passed to systemd, such as ext4, +xfs, nfs, or tmpfs. Leave this blank when systemd +or the mount helper can determine the type automatically.
diff --git a/systemd/help/systemd_mountwhat.html b/systemd/help/systemd_mountwhat.html new file mode 100644 index 000000000..1e03e1e36 --- /dev/null +++ b/systemd/help/systemd_mountwhat.html @@ -0,0 +1,5 @@ +The filesystem, block device, network export, or other source to mount. +Examples include UUID=01234567-89ab-cdef-0123-456789abcdef, +/dev/disk/by-label/data, /dev/mapper/vg0-data, +server:/export/path, or tmpfs.
diff --git a/systemd/help/systemd_mountwhere.html b/systemd/help/systemd_mountwhere.html new file mode 100644 index 000000000..1eebe96fc --- /dev/null +++ b/systemd/help/systemd_mountwhere.html @@ -0,0 +1,4 @@ +The absolute path where this filesystem will be mounted, such as +/mnt/data. The systemd mount unit name is derived from this path, so +/mnt/data becomes mnt-data.mount.
diff --git a/systemd/help/systemd_name.html b/systemd/help/systemd_name.html new file mode 100644 index 000000000..94e8969d3 --- /dev/null +++ b/systemd/help/systemd_name.html @@ -0,0 +1,10 @@ +The systemd unit name to create or edit. When creating a new unit, the suffix +for the selected unit type is appended if it is not already included.
+Use a unit name such as myapp.service, myjob.timer, or +myapp.socket, not a filesystem path. +This is also the name other unit fields refer to, for example in +After= or WantedBy=.
+For mount and automount units, the name can be left blank when a mount path +is entered or an existing mount unit is selected. The expected systemd name +will be derived from the mount path.
diff --git a/systemd/help/systemd_nonewprivs.html b/systemd/help/systemd_nonewprivs.html new file mode 100644 index 000000000..fc4f3c80f --- /dev/null +++ b/systemd/help/systemd_nonewprivs.html @@ -0,0 +1,6 @@ +Writes NoNewPrivileges=yes, preventing the service and its child +processes from gaining additional privileges.
+This is a low-risk hardening option for many services. Avoid it only when +the application intentionally relies on setuid helpers or other privilege +elevation after startup.
diff --git a/systemd/help/systemd_onfailure.html b/systemd/help/systemd_onfailure.html new file mode 100644 index 000000000..d43306a9f --- /dev/null +++ b/systemd/help/systemd_onfailure.html @@ -0,0 +1,8 @@ +Units to activate when this unit enters a failed state. This writes +OnFailure=.
+Enter space-separated unit names, not commands. For example, +alert-admin@%n.service can start a separate templated service and pass +this unit's name as %n.
+For services with a restart policy, the failure unit is normally activated +only after systemd gives up and the service becomes failed.
diff --git a/systemd/help/systemd_onsuccess.html b/systemd/help/systemd_onsuccess.html new file mode 100644 index 000000000..74525c8b6 --- /dev/null +++ b/systemd/help/systemd_onsuccess.html @@ -0,0 +1,8 @@ +Units to activate when this unit finishes successfully. This writes +OnSuccess=.
+Enter space-separated unit names, not commands. This is mainly useful for +oneshot jobs that should trigger a follow-up unit after completing normally, +such as publish-report.service.
+This directive was added in systemd 249, so older distributions may ignore +it or log a warning.
diff --git a/systemd/help/systemd_pathchanged.html b/systemd/help/systemd_pathchanged.html new file mode 100644 index 000000000..d03bb15b9 --- /dev/null +++ b/systemd/help/systemd_pathchanged.html @@ -0,0 +1,7 @@ +An absolute path for PathChanged=. The path unit activates its target +unit when the file is closed after being written, or when a watched directory +changes.
+For user units, this path is watched by the selected user's systemd manager. +It should be a path that user can access, typically below the user's home +directory or runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_pathdirectorynotempty.html b/systemd/help/systemd_pathdirectorynotempty.html new file mode 100644 index 000000000..e37c858ee --- /dev/null +++ b/systemd/help/systemd_pathdirectorynotempty.html @@ -0,0 +1,6 @@ +An absolute directory path for DirectoryNotEmpty=. The path unit +activates its target unit when this directory contains at least one entry.
+For user units, this directory is watched by the selected user's systemd +manager. It should be a directory that user can access, typically below the +user's home directory or runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_pathexists.html b/systemd/help/systemd_pathexists.html new file mode 100644 index 000000000..5a462644c --- /dev/null +++ b/systemd/help/systemd_pathexists.html @@ -0,0 +1,6 @@ +An absolute path for PathExists=. The path unit activates its target +unit when this file or directory exists.
+For user units, this path is evaluated by the selected user's systemd +manager. It should be a path that user can access, typically below the user's +home directory or runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_pathexistsglob.html b/systemd/help/systemd_pathexistsglob.html new file mode 100644 index 000000000..cf0418dd7 --- /dev/null +++ b/systemd/help/systemd_pathexistsglob.html @@ -0,0 +1,7 @@ +An absolute shell-style glob for PathExistsGlob=, such as +/var/spool/app/*.ready. The path unit activates its target unit when at +least one matching file or directory exists.
+For user units, the glob is evaluated by the selected user's systemd +manager. It should match paths that user can access, typically below the user's +home directory or runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_pathmakedirectory.html b/systemd/help/systemd_pathmakedirectory.html new file mode 100644 index 000000000..da8ac89f2 --- /dev/null +++ b/systemd/help/systemd_pathmakedirectory.html @@ -0,0 +1,7 @@ +When enabled, MakeDirectory=yes is written. systemd will create the +watched directory if it does not already exist. This is useful for watched +directory paths, not for watched files or glob patterns.
+For user units, the directory is created by the selected user's systemd +manager, with that user's permissions. It cannot create directories that the +user would not otherwise be allowed to create.
diff --git a/systemd/help/systemd_pathmodified.html b/systemd/help/systemd_pathmodified.html new file mode 100644 index 000000000..20959d8da --- /dev/null +++ b/systemd/help/systemd_pathmodified.html @@ -0,0 +1,6 @@ +An absolute path for PathModified=. The path unit activates its +target unit when the file or directory is modified.
+For user units, this path is watched by the selected user's systemd manager. +It should be a path that user can access, typically below the user's home +directory or runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_pathunit.html b/systemd/help/systemd_pathunit.html new file mode 100644 index 000000000..86ab75fd0 --- /dev/null +++ b/systemd/help/systemd_pathunit.html @@ -0,0 +1,4 @@ +The unit activated by this path unit, written as Unit=. Include the +full unit name and suffix, such as reload-config.service. If omitted, +systemd uses the matching service name.
diff --git a/systemd/help/systemd_pidfile.html b/systemd/help/systemd_pidfile.html new file mode 100644 index 000000000..d8dcbe6d9 --- /dev/null +++ b/systemd/help/systemd_pidfile.html @@ -0,0 +1,7 @@ +Path to a PID file for forking services. This writes +PIDFile=.
+Use an absolute path to the file written by the daemon after it forks, such +as /run/myapp.pid. This helps systemd identify the main process.
+For user units, use a path writable by the selected user, typically below +the user's runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_privatetmp.html b/systemd/help/systemd_privatetmp.html new file mode 100644 index 000000000..a6cbc4c33 --- /dev/null +++ b/systemd/help/systemd_privatetmp.html @@ -0,0 +1,6 @@ +Writes PrivateTmp=yes, giving the service a private view of +temporary directories such as /tmp.
+This helps isolate temporary files from the rest of the system. Do not use +it when the service must share files through /tmp with other +processes.
diff --git a/systemd/help/systemd_protectsystem.html b/systemd/help/systemd_protectsystem.html new file mode 100644 index 000000000..79aaade77 --- /dev/null +++ b/systemd/help/systemd_protectsystem.html @@ -0,0 +1,10 @@ +Restricts write access to system directories using +ProtectSystem=. Stronger values provide stricter filesystem +protection.
+true makes core system directories such as /usr and +/boot read-only. full also protects /etc. +strict makes the filesystem broadly read-only except for API +filesystems and paths explicitly made writable.
+Use ReadWritePaths= for directories the service still needs to +modify.
diff --git a/systemd/help/systemd_readwritepaths.html b/systemd/help/systemd_readwritepaths.html new file mode 100644 index 000000000..703e9e22d --- /dev/null +++ b/systemd/help/systemd_readwritepaths.html @@ -0,0 +1,11 @@ +Paths that remain writable when filesystem protection is enabled. This +writes ReadWritePaths=.
+Enter space-separated absolute paths, such as /var/lib/myapp +/var/log/myapp. This is typically used with ProtectSystem=full +or ProtectSystem=strict.
+Advanced systemd path prefixes such as - for optional paths are +accepted when needed.
+For user units, these should be paths the selected user can access, +typically below that user's home directory. They cannot make system +directories writable to the user.
diff --git a/systemd/help/systemd_reload.html b/systemd/help/systemd_reload.html new file mode 100644 index 000000000..4eca2d736 --- /dev/null +++ b/systemd/help/systemd_reload.html @@ -0,0 +1,7 @@ +Commands run when the service is reloaded. These are written as +ExecReload= entries.
+Enter one command per line. Use this for a real application reload, such as +sending HUP to the main process or running the daemon's own reload +command. Leave empty if the service cannot reload configuration without a +restart.
diff --git a/systemd/help/systemd_remain.html b/systemd/help/systemd_remain.html new file mode 100644 index 000000000..5d92e5d40 --- /dev/null +++ b/systemd/help/systemd_remain.html @@ -0,0 +1,5 @@ +Writes RemainAfterExit=yes. This is most useful for +oneshot services whose command exits after changing system state.
+When enabled, systemd keeps the service in the active state after the start +command exits. This lets a later stop command undo the action.
diff --git a/systemd/help/systemd_requires.html b/systemd/help/systemd_requires.html new file mode 100644 index 000000000..e8d2ae2b7 --- /dev/null +++ b/systemd/help/systemd_requires.html @@ -0,0 +1,6 @@ +Strong dependencies for this unit. If a required unit fails to start or is +stopped, this unit is also affected.
+Enter space-separated unit names. Use this when the unit cannot run +without the listed units. Add After= as well when the dependency must +be fully started before this unit starts.
diff --git a/systemd/help/systemd_restart.html b/systemd/help/systemd_restart.html new file mode 100644 index 000000000..6a3b385bc --- /dev/null +++ b/systemd/help/systemd_restart.html @@ -0,0 +1,9 @@ +Controls when systemd restarts the service after it exits. This writes +Restart=.
+no disables automatic restarts. on-failure restarts after +non-zero exits, signals, timeouts, and watchdog failures. always +restarts after almost any exit except an explicit stop by systemd.
+on-success, on-abnormal, on-abort, and +on-watchdog are narrower policies. For ordinary server processes, +on-failure is usually the practical choice.
diff --git a/systemd/help/systemd_restartsec.html b/systemd/help/systemd_restartsec.html new file mode 100644 index 000000000..dfcebb3cb --- /dev/null +++ b/systemd/help/systemd_restartsec.html @@ -0,0 +1,6 @@ +Delay before systemd restarts the service, written as +RestartSec=. Values may use systemd time syntax such as 5s +or 1min.
+Use this with a restart policy to avoid tight restart loops, for example +5s or 30s.
diff --git a/systemd/help/systemd_runtime_state.html b/systemd/help/systemd_runtime_state.html new file mode 100644 index 000000000..233e71a0f --- /dev/null +++ b/systemd/help/systemd_runtime_state.html @@ -0,0 +1,6 @@ +The active state and sub-state reported by systemd, such as +active (running), active (exited), inactive (dead) +or failed.
+ +Use the status and log buttons for the full systemd and journal output.
diff --git a/systemd/help/systemd_servicetype.html b/systemd/help/systemd_servicetype.html new file mode 100644 index 000000000..90b7e9d9f --- /dev/null +++ b/systemd/help/systemd_servicetype.html @@ -0,0 +1,10 @@ +The systemd Type= value. Leave as default for ordinary long-running +commands unless the service needs another startup protocol such as +forking, oneshot, or notify.
+simple treats the started process as the service immediately. +exec is similar but waits until the command has been executed. +forking is for daemons that background themselves and often needs a +PID file. oneshot is for short tasks that exit. dbus and +notify wait for D-Bus ownership or systemd readiness notification. +idle delays execution until other jobs are dispatched.
diff --git a/systemd/help/systemd_slicecpuweight.html b/systemd/help/systemd_slicecpuweight.html new file mode 100644 index 000000000..b64b58bc7 --- /dev/null +++ b/systemd/help/systemd_slicecpuweight.html @@ -0,0 +1,6 @@ +An optional CPUWeight= value from 1 to 10000. +Higher values give this slice a larger share of CPU time when there is +contention.
+For user units, this applies only within the selected user's systemd +manager and cannot grant CPU share beyond that user's parent cgroup.
diff --git a/systemd/help/systemd_sliceioweight.html b/systemd/help/systemd_sliceioweight.html new file mode 100644 index 000000000..c54d7cfc8 --- /dev/null +++ b/systemd/help/systemd_sliceioweight.html @@ -0,0 +1,6 @@ +An optional IOWeight= value from 1 to 10000. +Higher values give this slice a larger share of I/O bandwidth when there is +contention.
+For user units, this applies only within the selected user's systemd +manager and cannot grant I/O share beyond that user's parent cgroup.
diff --git a/systemd/help/systemd_slicememorymax.html b/systemd/help/systemd_slicememorymax.html new file mode 100644 index 000000000..8c271a817 --- /dev/null +++ b/systemd/help/systemd_slicememorymax.html @@ -0,0 +1,5 @@ +An optional MemoryMax= limit for the slice, such as 512M, +2G, or infinity.
+For user units, this applies only within the selected user's systemd +manager and cannot raise limits imposed by the user's parent cgroup.
diff --git a/systemd/help/systemd_slicetasksmax.html b/systemd/help/systemd_slicetasksmax.html new file mode 100644 index 000000000..282ce8ba0 --- /dev/null +++ b/systemd/help/systemd_slicetasksmax.html @@ -0,0 +1,5 @@ +An optional TasksMax= limit for the slice, such as 500 or +infinity.
+For user units, this applies only within the selected user's systemd +manager and cannot raise limits imposed by the user's parent cgroup.
diff --git a/systemd/help/systemd_socketaccept.html b/systemd/help/systemd_socketaccept.html new file mode 100644 index 000000000..752af66aa --- /dev/null +++ b/systemd/help/systemd_socketaccept.html @@ -0,0 +1,7 @@ +When enabled, Accept=yes is written and systemd starts one service +instance for each incoming connection. Leave disabled for the common model where +one service handles all traffic.
+Per-connection sockets normally activate a template-style service that can +handle instances. Use Accept=no when a single service should receive +all accepted connections from the socket.
diff --git a/systemd/help/systemd_socketgroup.html b/systemd/help/systemd_socketgroup.html new file mode 100644 index 000000000..d1564ceb2 --- /dev/null +++ b/systemd/help/systemd_socketgroup.html @@ -0,0 +1,5 @@ +The Unix group name for SocketGroup=. This controls group ownership +of the socket node when systemd creates filesystem sockets or FIFOs.
+For user units, filesystem sockets and FIFOs are created by the selected +user's systemd manager. This guided field is used only for system units.
diff --git a/systemd/help/systemd_socketlistendatagram.html b/systemd/help/systemd_socketlistendatagram.html new file mode 100644 index 000000000..b3526598b --- /dev/null +++ b/systemd/help/systemd_socketlistendatagram.html @@ -0,0 +1,4 @@ +A ListenDatagram= endpoint for UDP or datagram sockets. Examples +include 514, 127.0.0.1:10514, or an absolute filesystem +socket path.
diff --git a/systemd/help/systemd_socketlistenfifo.html b/systemd/help/systemd_socketlistenfifo.html new file mode 100644 index 000000000..36cb4e28f --- /dev/null +++ b/systemd/help/systemd_socketlistenfifo.html @@ -0,0 +1,6 @@ +An absolute path for ListenFIFO=. systemd creates or opens this FIFO +and activates the matching service when data is written to it.
+For user units, the FIFO is created by the selected user's systemd manager. +Use a path that user can create, typically below the user's home directory or +runtime directory such as /run/user/UID.
diff --git a/systemd/help/systemd_socketlistenstream.html b/systemd/help/systemd_socketlistenstream.html new file mode 100644 index 000000000..05ca58cd9 --- /dev/null +++ b/systemd/help/systemd_socketlistenstream.html @@ -0,0 +1,3 @@ +A ListenStream= endpoint for TCP or stream sockets. Examples include +8080, 127.0.0.1:8080, or an absolute filesystem socket path.
diff --git a/systemd/help/systemd_socketmode.html b/systemd/help/systemd_socketmode.html new file mode 100644 index 000000000..c6717189f --- /dev/null +++ b/systemd/help/systemd_socketmode.html @@ -0,0 +1,4 @@ +The file mode for SocketMode=, such as 0660 or +0600. This applies to filesystem sockets and FIFOs created by +systemd.
diff --git a/systemd/help/systemd_socketservice.html b/systemd/help/systemd_socketservice.html new file mode 100644 index 000000000..750f9249b --- /dev/null +++ b/systemd/help/systemd_socketservice.html @@ -0,0 +1,8 @@ +The service unit activated by this socket, written as Service=. +Include the full service name, such as example.service. If omitted, +systemd uses the matching service name.
+For the common Accept=no model, this is usually a normal service. +When Accept=yes is enabled, use a service that is designed to run as +one instance per connection, typically a template such as +example@.service.
diff --git a/systemd/help/systemd_socketuser.html b/systemd/help/systemd_socketuser.html new file mode 100644 index 000000000..638827daa --- /dev/null +++ b/systemd/help/systemd_socketuser.html @@ -0,0 +1,6 @@ +The Unix user name for SocketUser=. This controls ownership of the +socket node when systemd creates filesystem sockets or FIFOs.
+For user units, filesystem sockets and FIFOs are created by the selected +user's systemd manager and are owned by that user. This guided field is used +only for system units.
diff --git a/systemd/help/systemd_start.html b/systemd/help/systemd_start.html new file mode 100644 index 000000000..c95336350 --- /dev/null +++ b/systemd/help/systemd_start.html @@ -0,0 +1,10 @@ +Commands run by systemd when a service starts. These are written as +ExecStart= entries in the unit file and apply only to +.service units.
+Enter one command per line. For ordinary long-running daemons this is +usually a single absolute command, for example /usr/bin/node +/home/app/server.js.
+Multiple start commands are best used with Type=oneshot. For other +service types, multiple commands are combined through a shell command so +systemd still has one main process to track.
diff --git a/systemd/help/systemd_startpost.html b/systemd/help/systemd_startpost.html new file mode 100644 index 000000000..572e92558 --- /dev/null +++ b/systemd/help/systemd_startpost.html @@ -0,0 +1,5 @@ +Commands run after the main start command. These are written as +ExecStartPost= entries.
+Enter one command per line. These run only after systemd considers the main +start command successful according to the selected service type.
diff --git a/systemd/help/systemd_startpre.html b/systemd/help/systemd_startpre.html new file mode 100644 index 000000000..38fda4184 --- /dev/null +++ b/systemd/help/systemd_startpre.html @@ -0,0 +1,6 @@ +Commands run before the main start command. These are written as +ExecStartPre= entries.
+Enter one command per line. Use this for quick setup checks, migrations, or +directory preparation. Long-running background processes should not be started +from pre-start commands.
diff --git a/systemd/help/systemd_status.html b/systemd/help/systemd_status.html new file mode 100644 index 000000000..79b0f6853 --- /dev/null +++ b/systemd/help/systemd_status.html @@ -0,0 +1,11 @@ +The unit file state and runtime state reported by systemd. The unit file +state shows whether the unit is enabled, disabled, static, masked or in another +systemd state.
+The runtime state shows the active state and sub-state, such as +active (running), active (exited), inactive (dead) or +failed. If systemd reports a main process ID, it is shown separately. +Use the status and log buttons for the full systemd and journal output.
+Units with transient or generated file state are managed by systemd at +runtime. Their contents may be shown for inspection, but they are read-only in +the editor.
diff --git a/systemd/help/systemd_stop.html b/systemd/help/systemd_stop.html new file mode 100644 index 000000000..263b86530 --- /dev/null +++ b/systemd/help/systemd_stop.html @@ -0,0 +1,7 @@ +Optional commands run when a service is stopped. These are written as +ExecStop= entries in the unit file and apply only to +.service units.
+Use this when the application has its own graceful shutdown command. If this +is left empty, systemd stops the service using its normal signal and +KillMode= behavior.
diff --git a/systemd/help/systemd_stoppost.html b/systemd/help/systemd_stoppost.html new file mode 100644 index 000000000..6eb482cd7 --- /dev/null +++ b/systemd/help/systemd_stoppost.html @@ -0,0 +1,6 @@ +Commands run after the service stops. These are written as +ExecStopPost= entries.
+Enter one command per line. These can be used for cleanup, notification, or +removing temporary files, including cases where the service exited +unexpectedly.
diff --git a/systemd/help/systemd_swapoptions.html b/systemd/help/systemd_swapoptions.html new file mode 100644 index 000000000..149c6d148 --- /dev/null +++ b/systemd/help/systemd_swapoptions.html @@ -0,0 +1,3 @@ +Optional comma-separated swap options for Options=. These are passed +to the swap activation tools.
diff --git a/systemd/help/systemd_swappriority.html b/systemd/help/systemd_swappriority.html new file mode 100644 index 000000000..a19d374a3 --- /dev/null +++ b/systemd/help/systemd_swappriority.html @@ -0,0 +1,3 @@ +An optional numeric Priority= value. Higher-priority swap areas are +used first. Negative values are allowed by systemd.
diff --git a/systemd/help/systemd_swaptimeoutsec.html b/systemd/help/systemd_swaptimeoutsec.html new file mode 100644 index 000000000..ef3005315 --- /dev/null +++ b/systemd/help/systemd_swaptimeoutsec.html @@ -0,0 +1,3 @@ +An optional TimeoutSec= value for swap activation, such as +30s or 2min.
diff --git a/systemd/help/systemd_swapwhat.html b/systemd/help/systemd_swapwhat.html new file mode 100644 index 000000000..839a4abca --- /dev/null +++ b/systemd/help/systemd_swapwhat.html @@ -0,0 +1,3 @@ +The swap device or swap file for What=. Use an absolute path such as +/swapfile or a stable device path under /dev/disk/by-uuid.
diff --git a/systemd/help/systemd_syslogid.html b/systemd/help/systemd_syslogid.html new file mode 100644 index 000000000..bbc1625aa --- /dev/null +++ b/systemd/help/systemd_syslogid.html @@ -0,0 +1,5 @@ +Name used to identify log messages from this service in the systemd journal +or syslog. This writes the systemd SyslogIdentifier= directive.
+Use a short stable name, such as myapp. This makes log filtering +easier when multiple commands or wrappers write to the journal.
diff --git a/systemd/help/systemd_timeout.html b/systemd/help/systemd_timeout.html new file mode 100644 index 000000000..738c97c6f --- /dev/null +++ b/systemd/help/systemd_timeout.html @@ -0,0 +1,7 @@ +Maximum time systemd waits for the service to start. This writes +TimeoutStartSec=.
+Use systemd time syntax such as 30s, 2min, or 0 +to disable the timeout. This matters most for service types where systemd waits +for readiness, such as forking, notify, and +oneshot.
diff --git a/systemd/help/systemd_timeoutstop.html b/systemd/help/systemd_timeoutstop.html new file mode 100644 index 000000000..551ec4657 --- /dev/null +++ b/systemd/help/systemd_timeoutstop.html @@ -0,0 +1,6 @@ +Maximum time systemd waits for the service to stop cleanly. This writes +TimeoutStopSec=.
+After this timeout, systemd may force termination according to the service +kill settings. Increase it for applications that need time to flush data or +shut down cleanly.
diff --git a/systemd/help/systemd_timeraccuracysec.html b/systemd/help/systemd_timeraccuracysec.html new file mode 100644 index 000000000..5da565ff4 --- /dev/null +++ b/systemd/help/systemd_timeraccuracysec.html @@ -0,0 +1,4 @@ +An optional AccuracySec= value. systemd may coalesce timer events +within this window to reduce wakeups. Smaller values are more exact; larger +values are more power-efficient.
diff --git a/systemd/help/systemd_timeronbootsec.html b/systemd/help/systemd_timeronbootsec.html new file mode 100644 index 000000000..045dd2048 --- /dev/null +++ b/systemd/help/systemd_timeronbootsec.html @@ -0,0 +1,4 @@ +A monotonic delay for OnBootSec=. The timer will fire this long +after the system boots, for example 5min, 1h, or +30s.
diff --git a/systemd/help/systemd_timeroncalendar.html b/systemd/help/systemd_timeroncalendar.html new file mode 100644 index 000000000..b34367265 --- /dev/null +++ b/systemd/help/systemd_timeroncalendar.html @@ -0,0 +1,4 @@ +A calendar expression for OnCalendar=. Use values such as +hourly, daily, weekly, or a full systemd calendar +expression such as Mon..Fri 02:30.
diff --git a/systemd/help/systemd_timeronunitactivesec.html b/systemd/help/systemd_timeronunitactivesec.html new file mode 100644 index 000000000..1396972d5 --- /dev/null +++ b/systemd/help/systemd_timeronunitactivesec.html @@ -0,0 +1,4 @@ +A monotonic delay for OnUnitActiveSec=. The timer will run again +this long after the activated unit last became active, for example +15min or 1h.
diff --git a/systemd/help/systemd_timerpersistent.html b/systemd/help/systemd_timerpersistent.html new file mode 100644 index 000000000..a668605d6 --- /dev/null +++ b/systemd/help/systemd_timerpersistent.html @@ -0,0 +1,6 @@ +When enabled, Persistent=yes is written. For calendar timers, this +lets systemd run the timer once when it becomes active again if a scheduled run +was missed while the manager was stopped or the system was off.
+This does not catch up monotonic timers such as OnBootSec= or +OnUnitActiveSec=.
diff --git a/systemd/help/systemd_timerrandomizeddelaysec.html b/systemd/help/systemd_timerrandomizeddelaysec.html new file mode 100644 index 000000000..2743f3059 --- /dev/null +++ b/systemd/help/systemd_timerrandomizeddelaysec.html @@ -0,0 +1,4 @@ +An optional RandomizedDelaySec= value. systemd will delay each run by +a random amount up to this value, which helps avoid many timers starting at the +same instant.
diff --git a/systemd/help/systemd_timerunit.html b/systemd/help/systemd_timerunit.html new file mode 100644 index 000000000..7a1fc6f90 --- /dev/null +++ b/systemd/help/systemd_timerunit.html @@ -0,0 +1,4 @@ +The unit activated by this timer, written as Unit=. Include the full +unit name and suffix, such as backup.service. If omitted, systemd uses +the matching service name.
diff --git a/systemd/help/systemd_type.html b/systemd/help/systemd_type.html new file mode 100644 index 000000000..d83d70cec --- /dev/null +++ b/systemd/help/systemd_type.html @@ -0,0 +1,18 @@ +The type of system unit to create. The selected type controls the filename +suffix and the type-specific section written to the unit file. Created system +units are written below /etc/systemd/system.
+service creates a guided [Service] unit for starting and +supervising a process. timer creates a [Timer] unit that +activates another unit on a schedule. socket creates a +[Socket] unit for socket activation. path creates a +[Path] unit that reacts to filesystem changes. target +creates a grouping or synchronization point for other units.
+mount creates a [Mount] unit for a filesystem mount, and +automount creates a matching [Automount] unit that activates +a mount on demand. swap creates a [Swap] unit for swap space. +slice creates a [Slice] unit for resource-control grouping +and cgroup policy.
+Scope and device units are normally created by systemd, udev, or other +programs at runtime. They can be inspected when listed by systemd, but they are +not created as persistent unit files here.
diff --git a/systemd/help/systemd_type_user.html b/systemd/help/systemd_type_user.html new file mode 100644 index 000000000..ca865fedb --- /dev/null +++ b/systemd/help/systemd_type_user.html @@ -0,0 +1,21 @@ +The type of user unit to create. The selected type controls the filename +suffix and the type-specific section written to the unit file. Created user +units are written below the selected user's +~/.config/systemd/user directory.
+service creates a guided [Service] unit for starting and +supervising a process as this Unix user. timer creates a +[Timer] unit that activates another user unit on a schedule. +socket creates a [Socket] unit for user-level socket +activation. path creates a [Path] unit that reacts to +filesystem changes visible to this user. target creates a grouping or +synchronization point for other user units.
+slice creates a [Slice] unit for grouping this user's own +units and applying resource-control settings such as CPUWeight=, +MemoryMax=, TasksMax=, or IOWeight=. These limits +apply only within the user's systemd manager and cannot grant control over +system slices or other users.
+Mount, automount and swap units are system-manager unit types and are not +available when creating user units. Scope and device units are created at +runtime by systemd or other programs and are not created as persistent unit +files here.
diff --git a/systemd/help/systemd_unit_state.html b/systemd/help/systemd_unit_state.html new file mode 100644 index 000000000..39c053616 --- /dev/null +++ b/systemd/help/systemd_unit_state.html @@ -0,0 +1,7 @@ +The unit file state reported by systemd. It shows whether the unit is +enabled, disabled, static, masked or in another systemd state.
+ +Units with transient or generated file state are managed by systemd at +runtime. Their contents may be shown for inspection, but they are read-only in +the editor.
diff --git a/systemd/help/systemd_unitconf.html b/systemd/help/systemd_unitconf.html new file mode 100644 index 000000000..fcc15d770 --- /dev/null +++ b/systemd/help/systemd_unitconf.html @@ -0,0 +1,31 @@ +Directives for the selected non-service unit type. Enter directives only, +without the section header; the correct section will be written, such as +[Timer], [Socket], [Path], [Mount], +[Automount], [Swap], or [Slice].
+Mount and automount units have dedicated fields for their common settings. +Use this field only for uncommon directives that are not shown elsewhere in +the form.
+For a timer, examples include OnCalendar=daily, +Persistent=true, and Unit=myjob.service. If Unit= +is omitted, systemd activates the service with the same base name, such as +myjob.service for myjob.timer.
+For a socket, examples include ListenStream=8080, +ListenStream=/run/myapp.sock, Accept=false, and +Service=myapp.service. For user units, filesystem socket paths should +be below a location the selected user can write, such as +/run/user/UID. If Service= is omitted, systemd uses the +service with the same base name.
+For a path unit, examples include PathChanged=/srv/myapp, +PathExists=/var/run/myapp.ready, and Unit=myjob.service. +For user units, watched paths should normally be below the selected user's +home directory or runtime directory.
+Target units do not need a type-specific settings body in this form. +Dependencies such as Wants=, Requires=, Before=, +and After= are set in the common advanced options.
+For a swap unit, examples include What=/swapfile and +Priority=10. For a slice unit, use the dedicated fields for +CPU weight, memory maximum, task maximum, and I/O weight; this field can be +used for additional resource controls such as CPUQuota=50% or +MemoryHigh=256M. For user units, slice resource controls apply only +within the selected user's systemd manager.
diff --git a/systemd/help/systemd_unituser.html b/systemd/help/systemd_unituser.html new file mode 100644 index 000000000..bdf9f76ae --- /dev/null +++ b/systemd/help/systemd_unituser.html @@ -0,0 +1,5 @@ +The Unix user whose home directory and systemd user manager own the user +unit. The account is resolved with the local password database.
+The unit file is written below this user's home directory in +~/.config/systemd/user and is owned by that account.
diff --git a/systemd/help/systemd_user.html b/systemd/help/systemd_user.html new file mode 100644 index 000000000..9be7b1e4c --- /dev/null +++ b/systemd/help/systemd_user.html @@ -0,0 +1,7 @@ +For system services, writes User= so the service process runs as +the selected Unix user. This option is hidden for user units.
+Use this when creating a system service that should be managed by the system +manager but should drop privileges before running the application process.
+This is different from creating a user unit. A user unit is owned and +managed by the user's systemd manager and does not need User=.
diff --git a/systemd/help/systemd_userservice.html b/systemd/help/systemd_userservice.html new file mode 100644 index 000000000..81133c321 --- /dev/null +++ b/systemd/help/systemd_userservice.html @@ -0,0 +1,10 @@ +Creates the unit under the selected user's ~/.config/systemd/user +directory and manages it with systemctl --user.
+User units run inside the user's systemd manager. For service units, +User= and Group= are not written because the user manager +already runs as that user.
+Choose this for applications that belong to a user account and should use +that user's home directory, environment, and user unit lifecycle. Choose +No for a normal system unit, even if a service unit later uses +User= to drop privileges.
diff --git a/systemd/help/systemd_wantedby.html b/systemd/help/systemd_wantedby.html new file mode 100644 index 000000000..e34a224f4 --- /dev/null +++ b/systemd/help/systemd_wantedby.html @@ -0,0 +1,8 @@ +The target that should want this unit when it is enabled. This writes +WantedBy= in the [Install] section.
+For system services, multi-user.target is the usual default. For +user service units, default.target is normally used so the service starts +with the user's default manager target. Timers, sockets, and paths usually use +timers.target, sockets.target, and paths.target +respectively.
diff --git a/systemd/help/systemd_wants.html b/systemd/help/systemd_wants.html new file mode 100644 index 000000000..44a7f35ee --- /dev/null +++ b/systemd/help/systemd_wants.html @@ -0,0 +1,7 @@ +Weak dependencies for this unit. When this unit is started, systemd also +tries to start the units listed in Wants=.
+Enter space-separated unit names. Use this when the listed unit is helpful +but not strictly required. If it fails, this unit may still start.
+A common pattern is to use both Wants=other.service and +After=other.service so the other unit is pulled in and ordered first.
diff --git a/systemd/help/systemd_watchdogsec.html b/systemd/help/systemd_watchdogsec.html new file mode 100644 index 000000000..6197f0134 --- /dev/null +++ b/systemd/help/systemd_watchdogsec.html @@ -0,0 +1,8 @@ +Enables the systemd watchdog for services that send watchdog notifications. +This writes WatchdogSec=.
+Only use this for applications that support systemd watchdog pings, usually +through sd_notify(). If the service does not send keep-alive +notifications before the timeout, systemd treats it as failed.
+This pairs naturally with Type=notify and +Restart=on-watchdog or Restart=on-failure.
diff --git a/systemd/help/systemd_workdir.html b/systemd/help/systemd_workdir.html new file mode 100644 index 000000000..40993f962 --- /dev/null +++ b/systemd/help/systemd_workdir.html @@ -0,0 +1,10 @@ +Directory used as the service process current working directory. This writes +WorkingDirectory=.
+Use an absolute path, such as /srv/myapp. A path beginning with +~ is also accepted by systemd for the service user's home directory, +and a leading - makes a missing directory non-fatal.
+This is useful for applications that load relative configuration files or +assets.
+For user units, use a directory that belongs to the selected user, typically +below that user's home directory.
diff --git a/systemd/images/icon.gif b/systemd/images/icon.gif new file mode 100644 index 000000000..7f61a9bc9 Binary files /dev/null and b/systemd/images/icon.gif differ diff --git a/systemd/index.cgi b/systemd/index.cgi new file mode 100755 index 000000000..f5bc07c87 --- /dev/null +++ b/systemd/index.cgi @@ -0,0 +1,522 @@ +#!/usr/local/bin/perl +# Display systemd system and user units grouped by unit type. + +use strict; +use warnings; + +require './systemd-lib.pl'; ## no critic + +our (%access, %config, %in, %text); + +ReadParse(); + +# The index can be reached by GET, so all query input below is either reduced +# to known values or validated before it is used. +has_command("systemctl") || error($text{'systemd_esystemctl'}); +systemd_can_enter_module(\%access) || systemd_acl_error('penter'); + +# Print the page shell before building the tab contents. +ui_print_header(version_title(), + $text{'index_title'}, "", "intro", 1, 1, undef, + action_links()); +print index_style(); + +# Query parameters only choose the active tab and optional user context. +my $scope = $in{'scope'} eq 'user' && tab_visible('user') ? 'user' : ''; +my $unituser = clean_unit_value($in{'unituser'}); +$unituser ||= systemd_acl_default_user(\%access) || ""; +my $unituser_details = $unituser ? + get_user_details($unituser) : undef; +if ($scope eq 'user') { + $unituser_details || error($text{'systemd_euser'}); + systemd_can_view_user_scope(\%access, $unituser) || + systemd_acl_error('pview_user'); + } +$unituser = "" if (!$unituser_details); + +# Load both system and user units so the visible tab set matches reality. +my @system_units = systemd_can_view_system(\%access) ? list_units() : ( ); +my @user_units = tab_visible('user') && + systemd_can_view_user_scope(\%access) ? + grep { systemd_acl_user_allowed(\%access, $_->{'user'}) } + list_all_user_units() : ( ); +my @tabs = index_tabs(\@system_units, \@user_units, $unituser); + +# Pick a valid active tab. Invalid mode values fall back to the first tab. +my %valid_tabs = map { $_->{'id'}, 1 } @tabs; +my $requested = defined($in{'mode'}) ? $in{'mode'} : ""; +my $mode = $requested && $valid_tabs{$requested} ? $requested : + $scope eq 'user' && $valid_tabs{'user'} ? 'user' : + $tabs[0]->{'id'}; +my $formno = 0; + +# When several unit groups exist, render Webmin tabs around each table. +if (@tabs > 1) { + my @uitabs = map { [ $_->{'id'}, $_->{'title'} ] } @tabs; + print ui_tabs_start(\@uitabs, "mode", $mode, 1); + foreach my $tab (@tabs) { + print ui_tabs_start_tab("mode", $tab->{'id'}); + $formno++ if (print_index_tab($tab, $formno)); + print ui_tabs_end_tab("mode", $tab->{'id'}); + } + print ui_tabs_end(1); + } +else { + # A single unit group does not need tab chrome. + print_index_tab($tabs[0], $formno); + } + +print_index_tools(); +ui_print_footer("/", $text{'index'}); + +# version_title() +# Returns the first line of systemctl --version, or a plain fallback. +sub version_title +{ +my $systemctl = has_command("systemctl"); +return $text{'mode_systemd'} if (!$systemctl); + +# Only the first line is useful as a subtitle, for example "systemd 252". +my $out = backquote_command(quotemeta($systemctl)." --version 2>/dev/null"); +return $text{'mode_systemd'} if ($? || !defined($out) || $out eq ""); +my ($first) = split(/\r?\n/, $out, 2); +$first = clean_unit_value($first); +return $first || $text{'mode_systemd'}; +} + +# index_style() +# Returns CSS used by the systemd index fragment. +sub index_style +{ +# The style is emitted in the body so SPA theme navigation applies it too. +return ui_tag('style', + ".systemd_linger_toggle { text-decoration: none; }\n". + ".systemd_linger_toggle .ui_text_color { border-bottom: 1px dotted currentColor; }\n", + { 'type' => 'text/css' }); +} + +# print_index_tools() +# Prints advanced module actions below the unit tables. +sub print_index_tools +{ +my @buttons; +push(@buttons, [ "edit_manual.cgi", + $text{'index_edit_files'}, + $text{'index_edit_filesdesc'} ]) + if (systemd_acl_bool(\%access, 'manual') || + systemd_acl_bool(\%access, 'manual_user')); +push(@buttons, [ "dropins.cgi", + $text{'index_dropins'}, + $text{'index_dropinsdesc'} ]) + if ($config{'show_dropin_inventory'} && + systemd_can_enter_module(\%access)); +push(@buttons, [ "restart.cgi", + $text{'index_reload'}, + $text{'index_reloaddesc'} ]) + if (systemd_can_reload(\%access)); +return if (!@buttons); +print ui_hr(); +print ui_buttons_start(); +foreach my $button (@buttons) { + print ui_buttons_row(@$button); + } +print ui_buttons_end(); +} + +# index_tab_groups() +# Returns the system-unit tab layout and the unit suffixes each tab owns. +sub index_tab_groups +{ +return ( + { 'id' => 'service', 'types' => [ 'service' ], + 'create' => 'service' }, + { 'id' => 'timer', 'types' => [ 'timer' ], + 'create' => 'timer' }, + { 'id' => 'socket', 'types' => [ 'socket' ], + 'create' => 'socket' }, + { 'id' => 'path', 'types' => [ 'path' ], + 'create' => 'path' }, + { 'id' => 'target', 'types' => [ 'target' ], + 'create' => 'target' }, + { 'id' => 'storage', 'types' => [ 'mount', 'automount', 'swap' ], + 'create' => 'mount', 'show_type' => 1 }, + { 'id' => 'resources', 'types' => [ 'slice', 'scope' ], + 'create' => 'slice', 'show_type' => 1, 'inspect_only' => 1 }, + { 'id' => 'device', 'types' => [ 'device' ], + 'inspect_only' => 1, 'selectable' => 0, + 'show_unit_state' => 0 }, + ); +} + +# index_type_tab(type) +# Returns the tab id that owns a unit type. +sub index_type_tab +{ +my ($type) = @_; +foreach my $tab (index_tab_groups()) { + return $tab->{'id'} if (indexof($type, @{$tab->{'types'}}) >= 0); + } +return; +} + +# index_tabs(system-units, user-units, [user]) +# Builds tab metadata for system unit types plus one user-units tab. +sub index_tabs +{ +my ($system_units, $user_units, $unituser) = @_; +my %by_tab; + +# System units are grouped by suffix, with related low-level types combined. +foreach my $u (@$system_units) { + my ($display, $type) = index_name_type($u->{'name'}); + $type = 'service' if (!$type); + next if (indexof($type, get_list_unit_types()) < 0); + next if (!unit_visible_on_index($u)); + my $tabid = index_type_tab($type); + next if (!$tabid); + $u->{'_display'} = $config{'show_unit_suffixes'} ? + $u->{'name'} : $display; + $u->{'_type'} = $type; + push(@{$by_tab{$tabid}}, $u); + } + +# Keep tab ordering stable even when some unit types are absent. +my @tabs; +foreach my $group (index_tab_groups()) { + next if (!tab_visible($group->{'id'})); + next if (!systemd_can_view_system(\%access)); + my $units = $by_tab{$group->{'id'}} || [ ]; + push(@tabs, { %$group, + 'title' => index_tab_title($group->{'id'}), + 'desc' => index_tab_desc($group->{'id'}), + 'units' => $units }); + } + +# User units share one tab because the owner column distinguishes accounts. +my @visible_user_units; +foreach my $u (@$user_units) { + next if (!unit_visible_on_index($u)); + my ($display, $type) = index_name_type($u->{'name'}); + $u->{'_display'} = $config{'show_unit_suffixes'} ? + $u->{'name'} : $display; + $u->{'_type'} = $type || 'service'; + push(@visible_user_units, $u); + } +$user_units = \@visible_user_units; +if (tab_visible('user') && + systemd_can_view_user_scope(\%access, $unituser)) { + push(@tabs, { 'id' => 'user', + 'user' => 1, + 'unituser' => $unituser, + 'title' => $text{'systemd_tab_user'}, + 'desc' => $text{'systemd_tabdesc_user'}, + 'units' => $user_units }); + } +return @tabs; +} + +# print_index_tab(tab, form-number) +# Outputs one tab description and its mass-action table. +sub print_index_tab +{ +my ($tab, $formno) = @_; +my $user_tab = $tab->{'user'} ? 1 : 0; +my $can_status = systemd_can_inspect(\%access, $user_tab, $tab->{'unituser'}); +my $can_logs = systemd_can_logs(\%access, $user_tab, $tab->{'unituser'}); +my $can_start = systemd_can_runtime(\%access, 'start', + $user_tab, $tab->{'unituser'}); +my $can_stop = systemd_can_runtime(\%access, 'stop', + $user_tab, $tab->{'unituser'}); +my $can_restart = systemd_can_runtime(\%access, 'restart', + $user_tab, $tab->{'unituser'}); +my $can_boot = systemd_can_boot(\%access, $user_tab, $tab->{'unituser'}); +my $can_mask = $user_tab ? 0 : + systemd_can_mask(\%access, $user_tab, $tab->{'unituser'}); +my $can_delete = $user_tab ? + systemd_can_delete(\%access, $user_tab, $tab->{'unituser'}) : 0; +my $selectable = exists($tab->{'selectable'}) ? $tab->{'selectable'} : 1; +$selectable &&= $can_status || $can_logs || $can_start || $can_stop || + $can_restart || $can_boot || $can_mask || $can_delete ? 1 : 0; +my $show_unit_state = exists($tab->{'show_unit_state'}) ? + $tab->{'show_unit_state'} : 1; +my $show_type = !$config{'show_unit_suffixes'} && + ($user_tab || $tab->{'show_type'}); +my %linger_cache; + +# The create link inherits tab context so the create form opens with the right +# unit type or user-unit mode selected. +my $create_type = $user_tab ? 'service' : $tab->{'create'}; +my $create_link = index_create_link($tab, $user_tab, $create_type); +my @links = $selectable ? + ( select_all_link("d", $formno), + select_invert_link("d", $formno) ) : + ( ); +push(@links, $create_link) if ($create_link); + +print ui_div($tab->{'desc'}); +if (!@{$tab->{'units'}}) { + print ui_tag('p', index_empty_message($tab)); + print ui_links_row([ $create_link ]) if ($create_link); + return 0; + } + +# Start the mass-action form and keep scope in a hidden field for user units. +print ui_form_start("mass_units.cgi", "post"); +print ui_links_row(\@links) if (@links); +print ui_hidden("scope", "users") if ($user_tab); + +# Mixed-type tabs only need a type column when unit suffixes are hidden. +my @heads = $selectable ? ( "" ) : ( ); +push(@heads, $text{'systemd_name'}); +push(@heads, $text{'systemd_desc'}) if ($config{'desc'}); +push(@heads, $text{'systemd_type'}) if ($show_type); +push(@heads, $text{'systemd_unit_state'}) if ($show_unit_state); +push(@heads, $text{'systemd_runtime_state'}); +push(@heads, $text{'systemd_owner'}, $text{'systemd_linger_status'}) + if ($user_tab); +print ui_columns_start(\@heads); +foreach my $u (@{$tab->{'units'}}) { + # Generated units without real files and masked units are shown read-only. + my $editable = $u->{'file'} && -f $u->{'file'} ? 1 : 0; + my $link = index_edit_url($u, $user_tab); + my $title = (!$editable || + (defined($u->{'boot'}) && $u->{'boot'} == -1) ? + html_escape($u->{'_display'}) : + ui_link($link, html_escape($u->{'_display'}))); + my $checkvalue = $user_tab ? + user_unit_selection_value($u->{'user'}, $u->{'name'}) : + $u->{'name'}; + + # Build the row from common columns first, then append user-only columns. + my @row = $selectable ? + ( ui_checkbox("d", $checkvalue, undef) ) : + ( ); + push(@row, $title); + push(@row, html_escape($u->{'desc'})) if ($config{'desc'}); + push(@row, html_escape(index_unit_type_title($u->{'_type'}))) + if ($show_type); + push(@row, index_unit_state_column($u->{'unitstate'})) + if ($show_unit_state); + push(@row, index_runtime_state_column( + $u->{'runtime'}, $u->{'substate'})); + if ($user_tab) { + # Linger is per user, so cache it instead of calling loginctl per row. + if (!exists($linger_cache{$u->{'user'}})) { + $linger_cache{$u->{'user'}} = + user_linger_enabled($u->{'user'}); + } + my $linger_html = systemd_can_linger(\%access, $u->{'user'}) ? + linger_toggle_link( + $u->{'user'}, $linger_cache{$u->{'user'}}) : + html_escape($linger_cache{$u->{'user'}} ? + $text{'yes'} : $text{'no'}); + push(@row, ui_tag('tt', html_escape($u->{'user'})), + $linger_html); + } + print ui_columns_row(\@row); + } + +# Repeat row-selection links below the table for long lists. +print ui_columns_end(); +if ($selectable) { + print ui_links_row(\@links); + my @runtime_buttons; + push(@runtime_buttons, [ "start", $text{'index_start'} ]) + if ($can_start); + push(@runtime_buttons, [ "stop", $text{'index_stop'} ]) + if ($can_stop); + push(@runtime_buttons, [ "restart", $text{'index_restart'} ]) + if ($can_restart); + my @boot_buttons = $can_boot ? + ( [ "addboot", $text{'index_addboot'} ], + [ "delboot", $text{'index_delboot'} ] ) : ( ); + my @mask_buttons = $can_mask ? + ( [ "mask", $text{'index_mask'} ], + [ "unmask", $text{'index_unmask'} ] ) : ( ); + my @inspect_buttons; + push(@inspect_buttons, [ "status", $text{'index_statusnow'} ]) + if ($can_status); + push(@inspect_buttons, [ "logs", $text{'index_logsnow'} ]) + if ($can_logs); + my @delete_buttons = $can_delete ? + ( [ "delete", $text{'index_delete'} ] ) : ( ); + my @action_groups = $tab->{'inspect_only'} ? + grep { @$_ } ( \@inspect_buttons ) : + grep { @$_ } ( \@runtime_buttons, \@boot_buttons, + \@mask_buttons, \@inspect_buttons ); + print ui_form_grouped_buttons([ [ @action_groups ], + [ \@delete_buttons ] ]) + if (@action_groups || @delete_buttons); + } +print ui_form_end(); +return 1; +} + +# index_name_type(unit-name) +# Splits a full unit name into display name and unit type. +sub index_name_type +{ +my ($name) = @_; +my $units_piped = join('|', map { quotemeta } get_unit_types()); + +# Only strip suffixes that systemd understands as unit types. +my ($type) = $name =~ /\.([^.]+)$/; +if (defined($type) && $type =~ /^(?:$units_piped)$/) { + my $display = $name; + $display =~ s/\.$type$//; + return ($display, $type); + } +return ($name, ""); +} + +# index_tab_title(type) +# Returns the plural tab title for a systemd unit type. +sub index_tab_title +{ +my ($type) = @_; +return $text{'systemd_tab_'.$type} || + $text{'systemd_type_'.$type} || + ucfirst($type); +} + +# index_tab_desc(type) +# Returns the explanatory text shown under a systemd unit tab. +sub index_tab_desc +{ +my ($type) = @_; +return $text{'systemd_tabdesc_'.$type} || ""; +} + +# index_create_label(tab-id, create-type) +# Returns the tab-specific label for the create-unit link. +sub index_create_label +{ +my ($tabid, $type) = @_; +return $text{'index_sadd_'.$tabid} || + $text{'index_sadd_'.$type} || + $text{'index_sadd'}; +} + +# index_create_link(tab, user-tab, create-type) +# Returns the create link for a tab, if allowed. +sub index_create_link +{ +my ($tab, $user_tab, $create_type) = @_; +return "" if (!$create_type); +return "" if (!systemd_can_create(\%access, $user_tab, $tab->{'unituser'})); +my $create_url = $user_tab && $tab->{'unituser'} ? + "edit_unit.cgi?new=1&scope=user&unittype=service&unituser=". + urlize($tab->{'unituser'}) : + $user_tab ? "edit_unit.cgi?new=1&scope=user&unittype=service" : + "edit_unit.cgi?new=1&unittype=".urlize($create_type); +return ui_link($create_url, + index_create_label($tab->{'id'}, $create_type)); +} + +# index_empty_message(tab) +# Returns the empty-state message for a unit tab. +sub index_empty_message +{ +my ($tab) = @_; +if ($tab->{'user'}) { + return text('index_empty_user_owner', + ui_tag('tt', html_escape($tab->{'unituser'}))) + if ($tab->{'unituser'}); + return $text{'index_empty_user'}; + } +return $text{'index_empty_'.$tab->{'id'}} || $text{'index_empty_units'}; +} + +# index_unit_type_title(type) +# Returns the display label for a single unit type. +sub index_unit_type_title +{ +my ($type) = @_; +return $text{'systemd_type_'.$type} || $type; +} + +# user_unit_selection_value(user, unit) +# Encodes a user-unit owner and name into one checkbox value for mass actions. +sub user_unit_selection_value +{ +my ($user, $unit) = @_; +return urlize($user)."\t".urlize($unit); +} + +# linger_toggle_link(user, enabled) +# Returns a link to toggle linger for a user-unit owner. +sub linger_toggle_link +{ +my ($user, $enabled) = @_; + +# The link flips the current state and lets set_linger.cgi validate again. +my $target = $enabled ? 0 : 1; +my $label = $enabled ? $text{'yes'} : $text{'no'}; +my $type = $enabled ? 'success' : 'warn'; +my $title = $enabled ? text('systemd_linger_disable', $user) : + text('systemd_linger_enable', $user); +my $url = "set_linger.cgi?user=".urlize($user)."&enabled=".$target; +return ui_tag('a', ui_text_color(html_escape($label), $type), + { 'href' => $url, + 'class' => 'systemd_linger_toggle', + 'title' => $title }); +} + +# index_unit_state_column(state) +# Returns a formatted UnitFileState value for a unit row. +sub index_unit_state_column +{ +my ($state) = @_; +return index_state_column($state, { + 'enabled' => 'success', + 'enabled-runtime' => 'success', + 'disabled' => 'warn', + 'masked' => 'danger', + 'masked-runtime' => 'danger', + 'bad' => 'danger', + }); +} + +# index_runtime_state_column(state, substate) +# Returns a formatted ActiveState value, with SubState when systemd reports it. +sub index_runtime_state_column +{ +my ($state, $substate) = @_; +return index_state_column($state, { + 'active' => 'success', + 'inactive' => 'warn', + 'activating' => 'warn', + 'deactivating' => 'warn', + 'failed' => 'danger', + }, $substate); +} + +# index_state_column(state, colors, [substate]) +# Returns a displayed systemd state value with light semantic coloring. +sub index_state_column +{ +my ($state, $colors, $substate) = @_; +return ui_tag('i', $text{'index_unknown'}) + if (!defined($state) || $state eq ""); +my $label = ucfirst($state); +$label .= " (".$substate.")" + if (defined($substate) && $substate ne "" && $substate ne $state); +my $safe = html_escape($label); +my $color = $colors->{$state}; +return $color ? ui_text_color($safe, $color) : $safe; +} + +# index_edit_url(unit, user-tab) +# Returns the edit URL for a unit, preferring its safe override file. +sub index_edit_url +{ +my ($unit, $user_tab) = @_; +my $url = $user_tab ? + "edit_unit.cgi?scope=user&unituser=".urlize($unit->{'user'}). + "&name=".urlize($unit->{'name'}) : + "edit_unit.cgi?name=".urlize($unit->{'name'}); +if (dropin_exists($user_tab, $unit->{'user'}, $unit->{'name'})) { + $url .= "&dropin=1"; + } +return $url; +} diff --git a/systemd/install_check.pl b/systemd/install_check.pl new file mode 100644 index 000000000..695065afb --- /dev/null +++ b/systemd/install_check.pl @@ -0,0 +1,19 @@ +#!/usr/local/bin/perl +# Decides whether the standalone systemd module should be shown. + +use strict; +use warnings; +use lib ".."; + +use WebminCore; + +# is_installed(mode) +# Returns Webmin's install-check code for systems with systemctl available. +sub is_installed +{ +# Mode 0 is a boolean probe; mode 1 asks whether the module should be visible. +return 0 if (!has_command("systemctl")); +return $_[0] ? 2 : 1; +} + +1; diff --git a/systemd/lang/en b/systemd/lang/en new file mode 100644 index 000000000..36c768f5f --- /dev/null +++ b/systemd/lang/en @@ -0,0 +1,400 @@ +index_title=Systemd Services and Units +index=Module Index +index_return=systemd services and units +index_stop=Stop +index_start=Start +index_restart=Restart +index_addboot=Enable +index_delboot=Disable +index_mask=Mask +index_unmask=Unmask +index_delete=Delete +index_statusnow=Status +index_logsnow=Logs +index_unknown=Unknown +index_sadd=Create a new systemd unit +index_sadd_service=Create a new service unit +index_sadd_timer=Create a new timer unit +index_sadd_socket=Create a new socket unit +index_sadd_path=Create a new path unit +index_sadd_target=Create a new target unit +index_sadd_storage=Create a new storage unit +index_sadd_resources=Create a new slice unit +index_sadd_user=Create a new user unit +index_empty_units=No units were found in this section. +index_empty_service=No service units were found. +index_empty_timer=No timer units were found. +index_empty_socket=No socket units were found. +index_empty_path=No path units were found. +index_empty_target=No target units were found. +index_empty_storage=No mount, automount or swap units were found. +index_empty_resources=No slice or scope units were found. +index_empty_device=No device units were found. +index_empty_user=No user units were found. +index_empty_user_owner=No user units were found for $1. +index_edit_files=Edit Unit Files +index_edit_filesdesc=Inspect or manually edit discovered system and user unit files from systemd unit directories. +index_dropins=View Drop-in Overrides +index_dropinsdesc=Inspect discovered system and user drop-in override files and open safe drop-ins for editing. +index_reload=Reload Daemon +index_reloaddesc=Run systemctl daemon-reload so the system manager re-reads changed unit files. +index_reload_user=Reload User Manager +index_reload_userdesc=Run systemctl --user daemon-reload so the user's systemd manager re-reads changed unit files. +edit_ecannot=You are not allowed to access systemd units +edit_startnow=Start +edit_stopnow=Stop +edit_restartnow=Restart +edit_statusnow=Status +edit_propsnow=Properties +edit_depsnow=Dependencies +edit_logsnow=Logs +edit_overridenow=Create Override +edit_editoverridenow=Edit Override +edit_stockunitnow=Edit Base Unit +edit_deleteoverridenow=Delete Override +ss_ecannot=You are not allowed to start or stop systemd units +acl_section_users=User unit owner restrictions +acl_users=Can manage user units for +acl_all=All users +acl_this=Current Webmin user +acl_only=Only users +acl_except=All except users +acl_uid=Users with UID in range +acl_gid=Users with primary group +acl_section_view=Read access +acl_section_runtime=Runtime control +acl_section_change=Change and advanced access +acl_view=View system units +acl_view_user=View user units +acl_status=View system unit status, properties and dependencies +acl_status_user=View user unit status, properties and dependencies +acl_logs=View system unit logs +acl_logs_user=View user unit logs +acl_start=Start system units +acl_start_user=Start user units +acl_stop=Stop system units +acl_stop_user=Stop user units +acl_restart=Restart system units +acl_restart_user=Restart user units +acl_boot=Enable or disable system units at boot +acl_boot_user=Enable or disable user units at boot +acl_mask=Mask or unmask system units +acl_mask_user=Mask or unmask user units +acl_reload=Reload the systemd manager +acl_linger=Manage user linger state +acl_create=Create system units +acl_create_user=Create user units +acl_edit=Edit system unit files +acl_edit_user=Edit user unit files +acl_delete=Delete system units +acl_delete_user=Delete user units +acl_dropin=Manage system unit drop-ins +acl_dropin_user=Manage user unit drop-ins +acl_manual=Manually edit discovered system unit files +acl_manual_user=Manually edit discovered user unit files +acl_backup=Include systemd unit files in backups +eacl_np=Access denied: +eacl_penter=Access to the systemd module is not permitted. +eacl_pview=Viewing system units is not permitted. +eacl_pview_user=Viewing systemd user units for this user is not permitted. +eacl_pstatus=Viewing status, properties or dependencies is not permitted. +eacl_plogs=Viewing systemd unit logs is not permitted. +eacl_pstart=Starting systemd units is not permitted. +eacl_pstop=Stopping systemd units is not permitted. +eacl_prestart=Restarting systemd units is not permitted. +eacl_pboot=Changing unit startup state is not permitted. +eacl_pmask=Masking or unmasking systemd units is not permitted. +eacl_pcreate=Creating system units is not permitted. +eacl_pcreate_user=Creating user units for this user is not permitted. +eacl_pedit=Editing system unit files is not permitted. +eacl_pedit_user=Editing user unit files for this user is not permitted. +eacl_pdelete=Deleting system units is not permitted. +eacl_pdelete_user=Deleting user units for this user is not permitted. +eacl_pdropin=Managing system unit drop-ins is not permitted. +eacl_pdropin_user=Managing user unit drop-ins for this user is not permitted. +eacl_pmanual=Manually editing system unit files is not permitted. +eacl_pmanual_user=Manually editing user unit files for this user is not permitted. +eacl_preload=Reloading the systemd manager is not permitted. +eacl_plinger=Managing linger for this user is not permitted. +eacl_pbackup=Backing up systemd unit files is not permitted. +log_modify=Modified unit $1 +log_create=Created unit $1 +log_delete=Deleted unit $1 +log_override=Created drop-in override for unit $1 +log_deleteoverride=Deleted drop-in override for unit $1 +log_status=Fetched status of unit $1 +log_props=Fetched properties of unit $1 +log_deps=Listed dependencies of unit $1 +log_logs=Fetched logs for unit $1 +log_massstart=Started units $1 +log_massstop=Stopped units $1 +log_massrestart=Restarted units $1 +log_massenable=Enabled units $1 +log_massdisable=Disabled units $1 +log_massmask=Masked units $1 +log_massunmask=Unmasked units $1 +log_user_modify=Modified user unit $1 for $2 +log_user_create=Created user unit $1 for $2 +log_user_delete=Deleted user unit $1 for $2 +log_user_override=Created drop-in override for user unit $1 for $2 +log_user_deleteoverride=Deleted drop-in override for user unit $1 for $2 +log_user_status=Fetched status of user unit $1 for $2 +log_user_props=Fetched properties of user unit $1 for $2 +log_user_deps=Listed dependencies of user unit $1 for $2 +log_user_logs=Fetched logs for user unit $1 for $2 +log_user_massstart=Started user units $1 for $2 +log_user_massstop=Stopped user units $1 for $2 +log_user_massrestart=Restarted user units $1 for $2 +log_user_massenable=Enabled user units $1 for $2 +log_user_massdisable=Disabled user units $1 for $2 +log_user_massmask=Masked user units $1 for $2 +log_user_massunmask=Unmasked user units $1 for $2 +log_user_massdelete=Deleted user units $1 for $2 +log_user_linger=Set linger for user $1 to $2 +log_manual=Manually edited unit file $1 +log_reload=Reloaded systemd daemon +log_user_reload=Reloaded systemd user manager for $1 +log_user_manual=Manually edited user unit file $1 for $2 +mass_enone=No units selected +mass_enoallow=cannot stop $1 +mass_failed=.. failed +mass_skipped=.. skipped +mass_ok=.. done +mass_ustart=Starting Units +mass_urestart=Restarting Units +mass_ustop=Stopping Units +mass_usenable=Enabling Units +mass_usdisable=Disabling Units +mass_umask=Masking Units +mass_uunmask=Unmasking Units +mass_udelete=Deleting User Units +mass_uenable=Enabling unit $1. +mass_udisable=Disabling unit $1. +mass_umasking=Masking unit $1 .. +mass_uunmasking=Unmasking unit $1 .. +mass_udeleting=Deleting unit $1 .. +mass_ustarting=Starting unit $1 .. +mass_ustopping=Stopping unit $1 .. +mass_urestarting=Restarting unit $1 .. +mass_edelete_user=Mass delete is only available for user units +mass_enostart=Start is not applicable for this unit type +mass_enorestart=Restart is not applicable for this unit type +manual_title=Edit Systemd Unit Files +manual_desc=This editor is limited to discovered system unit files and drop-in override files. Packaged files under vendor directories are shown for inspection and advanced edits; prefer local overrides for normal customization. +manual_desc_user=This editor is limited to discovered user unit files and drop-in override files owned by $1. Changes affect that user's systemd manager; save, then reload the user manager from the user units page. +manual_select=Editing config file +manual_ok=Show +manual_edit_err=Failed to edit systemd unit files +manual_err=Failed to save systemd unit file +manual_enone=No editable systemd unit files were found. Create a unit first, or enable manual editing for an existing local unit file. +manual_enone_user=No editable user unit files were found for $1. Create a user unit first, or return to the user units page. +manual_efile=Missing or invalid systemd unit file +manual_eread=Failed to read systemd unit file +manual_ewrite=Failed to write systemd unit file +dropins_title=Drop-in Override Files +dropins_desc=This inventory lists discovered drop-in override files under local system and user unit directories. Drop-ins for known units can be opened for editing when permitted by ACLs. +dropins_empty=No drop-in override files were found. +dropins_disabled=The drop-in override inventory is disabled in the module configuration. +dropins_scope=Scope +dropins_scope_system=System +dropins_scope_user=User +dropins_file=Drop-in file +dropins_actions=Actions +dropins_edit=Edit +dropins_not_editable=Not editable +dropins_view_only=View only +dropins_unit_missing=Unit not found +reload_title=Reloading Systemd Daemon +reload_doing=Reloading the systemd daemon .. +reload_err=Failed to reload systemd daemon +reload_user_title=Reloading Systemd User Manager +reload_user_doing=Reloading systemd user manager for $1 .. +reload_user_err=Failed to reload systemd user manager +mode_systemd=Systemd +systemd_title1=Create Systemd Unit +systemd_title2=Edit Systemd Unit +systemd_title2_user=Edit Systemd User Unit +systemd_title2_view=View Systemd Unit +systemd_title2_view_user=View Systemd User Unit +systemd_egone=Unit no longer exists! +systemd_header=Systemd unit details +systemd_name=Unit name +systemd_type=Unit type +systemd_file=Configuration file +systemd_type_service=Service +systemd_type_timer=Timer +systemd_type_socket=Socket +systemd_type_path=Path +systemd_type_target=Target +systemd_type_mount=Mount +systemd_type_automount=Automount +systemd_type_swap=Swap +systemd_type_slice=Slice +systemd_type_scope=Scope +systemd_type_device=Device +systemd_desc=Unit description +systemd_start=Commands to run on startup +systemd_stop=Commands to run on shutdown +systemd_unitconf=Type-specific settings +systemd_timeroncalendar=Calendar schedule +systemd_timeronbootsec=Delay after boot +systemd_timeronunitactivesec=Delay after activation +systemd_timerpersistent=Catch up missed runs? +systemd_timerrandomizeddelaysec=Randomized delay +systemd_timeraccuracysec=Timer accuracy +systemd_timerunit=Unit to activate +systemd_socketlistenstream=Stream listener +systemd_socketlistendatagram=Datagram listener +systemd_socketlistenfifo=FIFO listener +systemd_socketaccept=Accept each connection? +systemd_socketuser=Socket owner +systemd_socketgroup=Socket group +systemd_socketmode=Socket mode +systemd_socketservice=Service to activate +systemd_pathexists=Path exists +systemd_pathexistsglob=Path exists glob +systemd_pathchanged=Path changed +systemd_pathmodified=Path modified +systemd_pathdirectorynotempty=Directory not empty +systemd_pathmakedirectory=Create watched directory? +systemd_pathunit=Unit to activate +systemd_mountwhat=Mount source +systemd_mountwhere=Mount point +systemd_mounttype=Filesystem type +systemd_mountoptions=Mount options +systemd_automountmount=Existing mount unit +systemd_automountmount_none=Select manually +systemd_automountwhere=Automount path +systemd_automountidle=Idle timeout +systemd_automountmode=Directory mode +systemd_swapwhat=Swap device or file +systemd_swappriority=Swap priority +systemd_swapoptions=Swap options +systemd_swaptimeoutsec=Swap timeout +systemd_slicecpuweight=CPU weight +systemd_slicememorymax=Memory maximum +systemd_slicetasksmax=Tasks maximum +systemd_sliceioweight=I/O weight +systemd_conf=Systemd unit configuration +systemd_boot=Start at boot time? +systemd_status=Status details +systemd_unit_state=Unit file state +systemd_runtime_state=Runtime state +systemd_main_pid=Main PID +systemd_err=Failed to save systemd unit +systemd_linger_err=Failed to change systemd user linger setting +systemd_ename=Missing or invalid systemd unit name +systemd_eunittype=Missing, invalid or mismatched systemd unit type +systemd_eclash=A unit with the same name already exists +systemd_edesc=Missing unit description +systemd_eunitconf=Missing type-specific unit settings +systemd_eunitconfsection=Type-specific settings must contain directives only, without section headers +systemd_etimertrigger=Enter at least one timer trigger +systemd_etimerunit=Missing or invalid unit to activate +systemd_esocketlisten=Enter at least one socket listener +systemd_esocketmode=Invalid socket mode +systemd_esocketservice=Missing or invalid service to activate +systemd_epathtrigger=Enter at least one watched path +systemd_epathunit=Missing or invalid unit to activate +systemd_emountwhat=Missing mount source +systemd_emountname=Mount unit name must match the mount point +systemd_eautomountmount=Missing or invalid matching .mount unit +systemd_eautomountname=Automount unit name must match the mount point +systemd_eautomountmode=Invalid automount directory mode +systemd_eswapwhat=Missing or invalid swap device or file +systemd_eswappriority=Invalid swap priority +systemd_esliceweight=Invalid systemd resource weight +systemd_eslicelimit=Invalid systemd resource limit +systemd_return=systemd unit +systemd_econf=No systemd unit configuration entered +systemd_estart=Missing commands to run on startup +systemd_euser=Missing or invalid user for systemd user unit +systemd_euserhome=Failed to create systemd user unit directory +systemd_euserunitfile=Unsafe or invalid systemd user unit file +systemd_euserunitdir=Unsafe or wrongly-owned systemd user unit directory +systemd_edropinfile=Unsafe or invalid systemd drop-in override file +systemd_ereadonly=This systemd unit file is managed at runtime and cannot be edited directly. +systemd_eloginctl=The loginctl command is not available on your system +systemd_esystemctl=The systemctl command is not available on your system +systemd_eusercmd=Failed to run systemctl --user for $1: $2 +systemd_elinger=Missing or invalid linger state +systemd_everify=Failed to verify systemd unit file: $1 +systemd_userservice=Create as user unit? +systemd_unituser=User service owner +systemd_linger=Enable linger for this user? +systemd_linger_user=Allow user units to run without login? +systemd_owner=User +systemd_linger_status=Linger +systemd_linger_enable=Enable linger for $1 +systemd_linger_disable=Disable linger for $1 +systemd_unit_for_user=$1 for user $2 +systemd_tab_service=Services +systemd_tab_timer=Timers +systemd_tab_socket=Sockets +systemd_tab_path=Paths +systemd_tab_target=Targets +systemd_tab_storage=Storage +systemd_tab_resources=Resources +systemd_tab_device=Devices +systemd_tab_user=User units +systemd_tabdesc_service=Services run long-lived, short-lived or one-shot commands under the system-level systemd manager, with dependencies, restart policy, logging and enablement handled by the unit file. +systemd_tabdesc_timer=Timers activate services or other units from calendar schedules, monotonic intervals or boot-relative delays, and are useful for dependency-aware scheduled jobs. +systemd_tabdesc_socket=Sockets listen on network, filesystem or IPC endpoints and activate matching services only when traffic arrives, keeping idle services stopped until needed. +systemd_tabdesc_path=Paths watch files or directories and activate matching units when paths appear, change, become non-empty or meet other filesystem conditions. +systemd_tabdesc_target=Targets collect related units into named synchronization points such as boot states, maintenance modes and dependency groups. +systemd_tabdesc_storage=Mount, automount and swap units define filesystems, on-demand mounts and swap resources that systemd can order, activate and track. +systemd_tabdesc_resources=Slice and scope units show systemd resource-control groups. Slices define persistent cgroup policy; scopes usually represent externally-created process groups. +systemd_tabdesc_device=Device units represent kernel devices exposed to systemd by udev. They are runtime objects for dependency tracking and inspection. +systemd_tabdesc_user=User units live under users' home directories and are controlled by each user's systemd manager. Linger controls whether a user's manager can run without an active login. +systemd_evisibletabs=At least one tab must be selected to show on the index page +systemd_statustitle=Unit Status +systemd_doingstatus=Requesting status of unit $1 .. +systemd_props=Unit Properties +systemd_doingprops=Reading properties for unit $1 .. +systemd_deps=Unit Dependencies +systemd_doingdeps=Listing dependencies for unit $1 .. +systemd_logs=Unit Logs +systemd_doinglogs=Reading logs for unit $1 .. +systemd_ejournal=The journalctl command is not available on your system +systemd_eduration=Missing or invalid time for $1 +systemd_epath=Missing or invalid path for $1 +systemd_elimitnofile=Missing or invalid open files limit +systemd_eoutput=Missing or invalid setting for $1 +systemd_eprotectsystem=Invalid protect system setting +systemd_ereadwritepath=Invalid writable path $1 +systemd_advanced=Advanced options +systemd_startpre=Commands to run before startup +systemd_startpost=Commands to run after startup +systemd_stoppost=Commands to run after shutdown +systemd_reload=Commands to run on reload +systemd_before=Start before units +systemd_after=Start after units +systemd_wants=Wants units +systemd_requires=Requires units +systemd_conflicts=Conflicts with units +systemd_onfailure=On failure units +systemd_onsuccess=On success units +systemd_servicetype=Service type +systemd_remain=Remain active after command exits? +systemd_pidfile=PID file +systemd_env=Environment variables +systemd_envfile=Environment file +systemd_user=Run as user +systemd_group=Run as group +systemd_killmode=Kill mode +systemd_workdir=Working directory +systemd_restart=Restart policy +systemd_restartsec=Restart delay +systemd_watchdogsec=Watchdog timeout +systemd_timeout=Startup timeout +systemd_timeoutstop=Shutdown timeout +systemd_limitnofile=Open files limit +systemd_logstd=Standard output +systemd_logerr=Standard error +systemd_syslogid=Log identifier +systemd_nonewprivs=Prevent gaining new privileges? +systemd_privatetmp=Use private temporary directory? +systemd_protectsystem=Protect system files +systemd_readwritepaths=Writable paths +systemd_wantedby=Install target +syslog_journalctl=Systemd journal diff --git a/systemd/log_parser.pl b/systemd/log_parser.pl new file mode 100755 index 000000000..143fa80d9 --- /dev/null +++ b/systemd/log_parser.pl @@ -0,0 +1,100 @@ +# log_parser.pl +# Functions for parsing this module's logs + +use strict; +use warnings; + +require 'systemd-lib.pl'; ## no critic + +our %text; + +# parse_webmin_log(user, script, action, type, object, params) +# Converts logged information from this module into escaped HTML fragments. +sub parse_webmin_log +{ +my ($user, $script, $action, $type, $object, $p) = @_; + +# This parser returns HTML fragments, so escape log values before wrapping them +# in UI tags or translated strings. +if ($type eq 'systemd-user' && + ($action eq 'modify' || $action eq 'create' || $action eq 'delete' || + $action eq 'override' || $action eq 'deleteoverride' || + $action eq 'status' || $action eq 'props' || $action eq 'deps' || + $action eq 'logs' || + $action eq 'massstart' || $action eq 'massstop' || + $action eq 'massrestart' || $action eq 'massenable' || + $action eq 'massdisable' || $action eq 'massmask' || + $action eq 'massunmask' || $action eq 'massdelete' || + $action eq 'linger' || + $action eq 'manual' || $action eq 'reload')) { + + # Linger logs describe the user rather than one or more unit names. + if ($action eq 'linger') { + return text('log_user_linger', + ui_tag('tt', html_escape($p->{'user'})), + $p->{'enabled'} ? $text{'yes'} : $text{'no'}); + } + if ($action eq 'manual') { + return text('log_user_manual', + ui_tag('tt', html_escape($object)), + ui_tag('tt', html_escape($p->{'user'}))); + } + if ($action eq 'reload') { + return text('log_user_reload', + ui_tag('tt', html_escape($p->{'user'}))); + } + + # User-unit actions include both escaped unit names and the owner. + return text('log_user_'.$action, + join(", ", map { ui_tag('tt', html_escape($_)) } + split(/\s+/, $object)), + ui_tag('tt', html_escape($p->{'user'}))); + } + +# System-unit messages use the same escaping as user units because unit names +# can be shown directly in the Webmin log viewer. +elsif ($type eq 'systemd' && $action eq 'modify') { + return text('log_modify', ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'create') { + return text('log_create', ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'delete') { + return text('log_delete', ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'override') { + return text('log_override', ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'deleteoverride') { + return text('log_deleteoverride', + ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'manual') { + return text('log_manual', ui_tag('tt', html_escape($object))); + } +elsif ($type eq 'systemd' && $action eq 'reload') { + return $text{'log_reload'}; + } +elsif ($type eq 'systemd' && + ($action eq 'status' || $action eq 'props' || $action eq 'deps' || + $action eq 'logs')) { + return text('log_'.$action, + join(", ", map { ui_tag('tt', html_escape($_)) } + split(/\s+/, $object))); + } + +# Mass-action logs contain space-separated unit names. +elsif ($type eq 'systemd' && + ($action eq 'massstart' || $action eq 'massstop' || + $action eq 'massrestart' || + $action eq 'massenable' || $action eq 'massdisable' || + $action eq 'massmask' || $action eq 'massunmask')) { + return text('log_'.$action, + join(", ", map { ui_tag('tt', html_escape($_)) } + split(/\s+/, $object))); + } +else { + # Unknown log records fall back to Webmin's default rendering. + return; + } +} diff --git a/systemd/mass_units.cgi b/systemd/mass_units.cgi new file mode 100755 index 000000000..0919555aa --- /dev/null +++ b/systemd/mass_units.cgi @@ -0,0 +1,458 @@ +#!/usr/local/bin/perl +# Start, stop, inspect or enable a set of systemd units + +use strict; +use warnings; + +require './systemd-lib.pl'; ## no critic + +our (%access, %in, %text); + +# Mass actions are POSTed from the index table or redirected from edit_unit.cgi. +ReadParse(); +my @sel = split(/\0/, $in{'d'}); +@sel || error($text{'mass_enone'}); + +# Work out whether selections target system or user managers. +my $user_scope = $in{'scope'} eq 'user' ? 1 : 0; +my $users_scope = $in{'scope'} eq 'users' ? 1 : 0; +my $unituser = clean_unit_value($in{'unituser'}); +if ($user_scope) { + get_user_details($unituser) || + error($text{'systemd_euser'}); + } +if ($in{'return'}) { + valid_unit_name($in{'return'}) || + error($text{'systemd_ename'}); + } + +# Convert raw checkbox values into validated action records. +my @units = mass_units(\@sel, $user_scope, $users_scope, $unituser); +foreach my $u (@units) { + systemd_can_view_scope(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error($u->{'user_scope'} ? 'pview_user' : 'pview'); + } + +# Convert submitted buttons into action flags. +my $start = $in{'start'} ? 1 : 0; +my $stop = $in{'stop'} ? 1 : 0; +my $restart = $in{'restart'} ? 1 : 0; +my $status = $in{'status'} ? 1 : 0; +my $props = $in{'props'} ? 1 : 0; +my $deps = $in{'deps'} ? 1 : 0; +my $logs = $in{'logs'} ? 1 : 0; +my $enable = $in{'addboot'} ? 1 : 0; +my $disable = $in{'delboot'} ? 1 : 0; +my $mask = $in{'mask'} ? 1 : 0; +my $unmask = $in{'unmask'} ? 1 : 0; +my $delete = $in{'delete'} ? 1 : 0; +my $printed_action_result = 0; + +# Use an unbuffered page because long-running systemctl operations should show +# progress as each unit completes. +ui_print_unbuffered_header(undef, $logs ? $text{'systemd_logs'} : + $deps ? $text{'systemd_deps'} : + $props ? $text{'systemd_props'} : + $status ? $text{'systemd_statustitle'} : + $restart ? $text{'mass_urestart'} : + $start ? $text{'mass_ustart'} : + $stop ? $text{'mass_ustop'} : + $enable ? $text{'mass_usenable'} : + $disable ? $text{'mass_usdisable'} : + $mask ? $text{'mass_umask'} : + $unmask ? $text{'mass_uunmask'} : + $delete ? $text{'mass_udelete'} : + $text{'mass_ustop'}, ""); + +# Get status +if ($status) { + # Show full systemd status output for selected units. + foreach my $u (@units) { + systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pstatus'); + my $s = $u->{'name'}; + + # Status command failures can still return useful output. + print_action_start(text('systemd_doingstatus', + mass_unit_label($u))); + my ($ok, $out) = $u->{'user_scope'} ? + status_user_unit($u->{'user'}, $s) : + status_unit($s); + print ui_tag('pre', html_escape($out)) if ($out); + print $text{'mass_failed'}, ui_p() if (!$out); + } + mass_log('status', \@units); + } + +# Get properties +if ($props) { + # Show the exact property set systemd reports for selected units. + foreach my $u (@units) { + systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pstatus'); + my $s = $u->{'name'}; + + # Properties are read from the selected system or user manager. + print_action_start(text('systemd_doingprops', + mass_unit_label($u))); + my ($ok, $out) = $u->{'user_scope'} ? + properties_user_unit($u->{'user'}, $s) : + properties_unit($s); + print ui_tag('pre', html_escape($out)) if ($out); + print $text{'mass_failed'}, ui_p() if (!$ok && !$out); + } + mass_log('props', \@units); + } + +# Get dependencies +if ($deps) { + # Show the dependency tree for selected units. + foreach my $u (@units) { + systemd_can_inspect(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pstatus'); + my $s = $u->{'name'}; + + # Dependencies come from systemctl in the selected manager scope. + print_action_start(text('systemd_doingdeps', + mass_unit_label($u))); + my ($ok, $out) = $u->{'user_scope'} ? + dependencies_user_unit($u->{'user'}, $s) : + dependencies_unit($s); + print ui_tag('pre', html_escape($out)) if ($out); + print $text{'mass_failed'}, ui_p() if (!$ok && !$out); + } + mass_log('deps', \@units); + } + +# Get logs +if ($logs) { + # Show recent journal output for selected units. + foreach my $u (@units) { + systemd_can_logs(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('plogs'); + my $s = $u->{'name'}; + + # Logs are read through journalctl for both system and user units. + print_action_start(text('systemd_doinglogs', + mass_unit_label($u))); + my ($ok, $out) = $u->{'user_scope'} ? + logs_user_unit($u->{'user'}, $s) : + logs_unit($s); + print ui_tag('pre', html_escape($out)) if ($out); + print $text{'mass_failed'}, ui_p() if (!$ok && !$out); + } + mass_log('logs', \@units); + } + +# Stop or restart before any later enable/start work. +if ($stop || $restart) { + # Webmin itself cannot be stopped here, but it can be restarted specially. + $SIG{'TERM'} = 'ignore'; # Restarting webmin may kill this script + foreach my $u (@units) { + systemd_can_runtime(\%access, $stop ? 'stop' : 'restart', + $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error($stop ? 'pstop' : 'prestart'); + my $s = $u->{'name'}; + my ($ok, $out); + my $skipped = 0; + my $is_webmin = !$u->{'user_scope'} && $s eq 'webmin.service'; + + # Stop and restart are mutually exclusive submit actions. + if ($stop) { + print_action_start(text('mass_ustopping', + mass_unit_label($u))); + if (!$is_webmin) { + ($ok, $out) = $u->{'user_scope'} ? + stop_user_unit($u->{'user'}, $s) : + stop_unit($s); + } + } + elsif ($restart) { + print_action_start(text('mass_urestarting', + mass_unit_label($u))); + if (!unit_restartable($s)) { + ($ok, $out) = (1, $text{'mass_enorestart'}); + $skipped = 1; + } + elsif (!$is_webmin) { + ($ok, $out) = $u->{'user_scope'} ? + restart_user_unit($u->{'user'}, $s) : + restart_unit($s); + } + else { + restart_miniserv(); + } + } + + # Keep command output under the final per-unit result. + if ($is_webmin) { + print_action_result(1, text('mass_enoallow', $s), 1) + if ($stop); + print_action_result(1, undef, 0) + if ($restart); + } + else { + print_action_result($ok, $out, $skipped); + } + } + mass_log($stop ? 'massstop' : 'massrestart', \@units); + } + +# Enable or disable +if ($enable || $disable) { + # Enable or disable startup for each selected unit. + foreach my $u (@units) { + systemd_can_boot(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pboot'); + my $b = $u->{'name'}; + my ($ok, $out) = (1, undef); + + # User units use systemctl --user; system units use the system manager. + if ($enable) { + print_action_start(text('mass_uenable', + mass_unit_label($u))); + if ($u->{'user_scope'}) { + ($ok, $out) = + enable_user_unit($u->{'user'}, $b); + } + else { + ($ok, $out) = enable_unit($b); + } + } + else { + print_action_start(text('mass_udisable', + mass_unit_label($u))); + if ($u->{'user_scope'}) { + ($ok, $out) = + disable_user_unit($u->{'user'}, $b); + } + else { + ($ok, $out) = disable_unit($b); + } + } + + # Keep command output under the final per-unit result. + print_action_result($ok, $out, startup_change_skipped($out)); + + } + mass_log($enable ? 'massenable' : 'massdisable', \@units); + } + +# Mask or unmask +if ($mask || $unmask) { + # Masking prevents activation; unmasking restores normal start behavior. + foreach my $u (@units) { + systemd_can_mask(\%access, $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pmask'); + my $b = $u->{'name'}; + my ($ok, $out); + + # User units use systemctl --user; system units use the system manager. + if ($mask) { + print_action_start(text('mass_umasking', + mass_unit_label($u))); + ($ok, $out) = $u->{'user_scope'} ? + mask_user_unit($u->{'user'}, $b) : + mask_unit($b); + } + else { + print_action_start(text('mass_uunmasking', + mass_unit_label($u))); + ($ok, $out) = $u->{'user_scope'} ? + unmask_user_unit($u->{'user'}, $b) : + unmask_unit($b); + } + + # Keep command output under the final per-unit result. + print_action_result($ok, $out, 0); + } + mass_log($mask ? 'massmask' : 'massunmask', \@units); + } + +# Delete user units +if ($delete) { + # Bulk delete is intentionally limited to user units. System unit + # deletion stays on the per-unit edit page where the risk is clearer. + foreach my $u (@units) { + $u->{'user_scope'} || error($text{'mass_edelete_user'}); + systemd_can_delete(\%access, 1, $u->{'user'}) || + systemd_acl_error('pdelete_user'); + my $s = $u->{'name'}; + print_action_start(text('mass_udeleting', + mass_unit_label($u))); + disable_user_unit($u->{'user'}, $s); + stop_user_unit($u->{'user'}, $s); + my ($ok, $out) = delete_user_unit($u->{'user'}, $s); + print_action_result($ok, $out, 0); + } + mass_log('massdelete', \@units); + } + +# Try to start at last +if ($start) { + # Start last, so "enable and start" first creates the wanted symlink. + foreach my $u (@units) { + systemd_can_runtime(\%access, 'start', + $u->{'user_scope'}, $u->{'user'}) || + systemd_acl_error('pstart'); + my $s = $u->{'name'}; + my ($ok, $out); + + # Each selected unit is started independently and reported inline. + print_action_start(text('mass_ustarting', + mass_unit_label($u))); + my $skipped = 0; + if (!unit_startable($s)) { + ($ok, $out) = (1, $text{'mass_enostart'}); + $skipped = 1; + } + else { + ($ok, $out) = $u->{'user_scope'} ? + start_user_unit($u->{'user'}, $s) : + start_unit($s); + } + print_action_result($ok, $out, $skipped); + } + mass_log('massstart', \@units); + } + +# Return to the unit page when it should still exist; otherwise return to its +# tab. Transient units can disappear after stop/restart actions. +if ($in{'return'} && !$in{'returnindex'}) { + my $dropin = $in{'returndropin'} ? "&dropin=1" : ""; + my $dropfile = $dropin && $in{'returndropfile'} ? + "&dropfile=".urlize(clean_unit_value($in{'returndropfile'})) : + ""; + my $return = $user_scope ? + "edit_unit.cgi?scope=user&unituser=".urlize($unituser). + "&name=".urlize($in{'return'}).$dropin.$dropfile : + "edit_unit.cgi?name=".urlize($in{'return'}).$dropin.$dropfile; + ui_print_footer($return, + $text{'systemd_return'}); + } +else { + my $u = $units[0]; + my $return = index_url($u->{'name'}, $u->{'user_scope'}, + $user_scope ? $unituser : undef); + ui_print_footer($return, $text{'index_return'}); + } + +# print_action_start(message) +# Prints the first progress line for a unit action. +sub print_action_start +{ +my ($msg) = @_; +if ($printed_action_result) { + print ui_tag('div', '', { 'class' => 'systemd-action-break', + 'style' => 'height: 1em;' }), "\n"; + $printed_action_result = 0; + } +print ui_tag('span', $msg, { 'data-first-print' => undef }); +print ui_br(), "\n"; +return; +} + +# print_action_result(ok, output, skipped, html) +# Prints the final result line with command output folded underneath it. +sub print_action_result +{ +my ($ok, $out, $skipped, $html) = @_; +my $status = $skipped ? $text{'mass_skipped'} : + $ok ? $text{'mass_ok'} : $text{'mass_failed'}; +my $title = ui_tag('span', html_escape($status), + { 'data-second-print' => undef }); +if (!defined($out) || $out eq "") { + print $title, "\n"; + $printed_action_result = 1; + return; + } + +# Keep successful output quiet, but open failures for immediate diagnosis. +my $content = $out; +$content = $html ? $content : + ui_tag('pre', html_escape($content), + { 'style' => 'margin-left: 10px;' }); +print ui_details({ + 'html' => 1, + 'title' => $title, + 'content' => $content, + 'class' => 'inline inlined', + }, !$ok && !$skipped); +print "\n"; +$printed_action_result = 1; +return; +} + +# mass_units(selected, user-scope, users-scope, user) +# Converts selected checkbox values into action records with optional owners. +sub mass_units +{ +my ($selected, $user_scope, $users_scope, $unituser) = @_; +my @rv; + +# The user-units tab packs owner and unit name into one checkbox value. +if ($users_scope) { + foreach my $raw (@$selected) { + my ($encuser, $encname) = split(/\t/, $raw, 2); + defined($encuser) && defined($encname) || + error($text{'systemd_euser'}); + my $user = clean_unit_value(un_urlize($encuser)); + my $name = un_urlize($encname); + get_user_details($user) || + error($text{'systemd_euser'}); + valid_unit_name($name) || + error($text{'systemd_ename'}); + push(@rv, { 'name' => $name, + 'user' => $user, + 'user_scope' => 1 }); + } + } +else { + # System-unit rows and single-user edit actions submit plain unit names. + foreach my $name (@$selected) { + valid_unit_name($name) || + error($text{'systemd_ename'}); + push(@rv, { 'name' => $name, + 'user' => $unituser, + 'user_scope' => $user_scope }); + } + } +return @rv; +} + +# mass_unit_label(unit) +# Returns escaped HTML for a unit name, including owner for user units. +sub mass_unit_label +{ +my ($unit) = @_; +my $name = ui_tag('tt', html_escape($unit->{'name'})); +return $name if (!$unit->{'user_scope'}); +return text('systemd_unit_for_user', $name, + ui_tag('tt', html_escape($unit->{'user'}))); +} + +# mass_log(action, units) +# Logs mixed system and user unit actions under the correct log type. +sub mass_log +{ +my ($action, $units) = @_; +my @system; +my %users; + +# Keep system and user actions separate so the log parser gets owner context. +foreach my $u (@$units) { + if ($u->{'user_scope'}) { + push(@{$users{$u->{'user'}}}, $u->{'name'}); + } + else { + push(@system, $u->{'name'}); + } + } + +# Group user-unit records by owner to avoid one log line per unit. +webmin_log($action, 'systemd', join(" ", @system)) if (@system); +foreach my $user (sort keys %users) { + webmin_log($action, 'systemd-user', join(" ", @{$users{$user}}), + { 'user' => $user }); + } +} diff --git a/systemd/module.info b/systemd/module.info new file mode 100644 index 000000000..731558d70 --- /dev/null +++ b/systemd/module.info @@ -0,0 +1,7 @@ +name=Systemd +category=system +os_support=*-linux +desc=Systemd Services and Units +longdesc=Manage systemd system and user units, including services, timers, sockets, paths and targets. +readonly=1 +syslog=1 diff --git a/systemd/restart.cgi b/systemd/restart.cgi new file mode 100755 index 000000000..34023fcd8 --- /dev/null +++ b/systemd/restart.cgi @@ -0,0 +1,31 @@ +#!/usr/local/bin/perl +# Reload the system systemd manager after unit-file changes. + +use strict; +use warnings; + +require './systemd-lib.pl'; ## no critic + +our (%access, %text); + +ReadParse(); +error_setup($text{'reload_err'}); +systemd_can_reload(\%access) || systemd_acl_error('preload'); + +ui_print_unbuffered_header(undef, $text{'reload_title'}, ""); + +# Run daemon-reload directly so command output can be shown to the admin. +my $systemctl = has_command("systemctl"); +$systemctl || error($text{'systemd_esystemctl'}); +print $text{'reload_doing'}, ui_br(), "\n"; +my $out = backquote_logged( + quotemeta($systemctl)." daemon-reload 2>&1 {'user'}) || + systemd_acl_error('pmanual_user'); + +ui_print_unbuffered_header(undef, $text{'reload_user_title'}, ""); + +print text('reload_user_doing', + ui_tag('tt', html_escape($uinfo->{'user'}))), ui_br(), "\n"; +my ($ok, $out) = reload_user_manager($uinfo->{'user'}); +print ui_tag('pre', html_escape($out)) if ($out); +print($ok ? $text{'mass_ok'} : $text{'mass_failed'}, ui_p()); +if ($ok) { + mark_user_daemon_reloaded($uinfo->{'user'}); + webmin_log("reload", "systemd-user", $uinfo->{'user'}, + { 'user' => $uinfo->{'user'} }); + } + +ui_print_footer("index.cgi?scope=user&unituser=".urlize($uinfo->{'user'}), + $text{'index_return'}); diff --git a/systemd/safeacl b/systemd/safeacl new file mode 100644 index 000000000..d07ef302e --- /dev/null +++ b/systemd/safeacl @@ -0,0 +1,34 @@ +noconfig=1 +mode=3 +users= +uidmin= +uidmax= +view=0 +view_user=1 +status=0 +status_user=1 +logs=0 +logs_user=1 +start=0 +start_user=1 +stop=0 +stop_user=1 +restart=0 +restart_user=1 +boot=0 +boot_user=1 +mask=0 +mask_user=0 +create=0 +create_user=1 +edit=0 +edit_user=1 +delete=0 +delete_user=1 +dropin=0 +dropin_user=1 +manual=0 +manual_user=1 +reload=0 +linger=1 +backup=0 diff --git a/systemd/save_manual.cgi b/systemd/save_manual.cgi new file mode 100755 index 000000000..6dd3aa89c --- /dev/null +++ b/systemd/save_manual.cgi @@ -0,0 +1,34 @@ +#!/usr/local/bin/perl +# Save a raw systemd unit file selected by edit_manual.cgi. + +use strict; +use warnings; + +require './systemd-lib.pl'; ## no critic + +our (%access, %in, %text); + +ReadParseMime(); +error_setup($text{'manual_err'}); + +# The posted path must still be in the discovered allowlist at save time. +my $info = manual_unit_file($in{'file'}); +$info || error($text{'manual_efile'}); +systemd_can_manual(\%access, $info) || + systemd_acl_error($info->{'scope'} eq 'user' ? + 'pmanual_user' : 'pmanual'); +my ($ok, $err) = write_manual_unit_file($info, $in{'data'}); +$ok || error($err || $text{'manual_ewrite'}); + +# User-unit edits include the owner so the log parser can render context. +if ($info->{'scope'} eq 'user') { + mark_user_units_changed($info->{'user'}); + webmin_log("manual", "systemd-user", $info->{'file'}, + { 'user' => $info->{'user'} }); + redirect("index.cgi?scope=user&unituser=".urlize($info->{'user'})); + } +else { + mark_units_changed(); + webmin_log("manual", "systemd", $info->{'file'}); + redirect(""); + } diff --git a/systemd/save_unit.cgi b/systemd/save_unit.cgi new file mode 100755 index 000000000..8b1b945bf --- /dev/null +++ b/systemd/save_unit.cgi @@ -0,0 +1,854 @@ +#!/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)))); +} diff --git a/systemd/set_linger.cgi b/systemd/set_linger.cgi new file mode 100755 index 000000000..99606afd5 --- /dev/null +++ b/systemd/set_linger.cgi @@ -0,0 +1,50 @@ +#!/usr/local/bin/perl +# Toggle linger for a systemd user manager + +use strict; +use warnings; + +require './systemd-lib.pl'; ## no critic + +our (%access, %in, %text); + +# This page is reached from the index linger toggle links. +error_setup($text{'systemd_linger_err'}); +ReadParse(); + +# Validate the requested user and linger state before calling loginctl. +my $user = clean_unit_value($in{'user'}); +my $enabled = $in{'enabled'}; +get_user_details($user) || error($text{'systemd_euser'}); +systemd_can_linger(\%access, $user) || systemd_acl_error('plinger'); +if (!defined($enabled) || $enabled !~ /^[01]$/) { + error($text{'systemd_elinger'}); + } + +# Apply the requested linger state through loginctl. +my ($ok, $out) = set_user_linger($user, $enabled); +$ok || error_linger_command($user, $out); + +# Enabling linger should also bring up the user manager immediately. +if ($enabled) { + ($ok, $out) = start_user_manager($user); + $ok || error_linger_command($user, $out); + } + +# Record the change and return to the User units tab for the same owner. +webmin_log("linger", "systemd-user", $user, + { 'user' => $user, 'enabled' => $enabled }); +redirect(index_url(undef, 1, $user)); + +# error_linger_command(user, output) +# Shows escaped loginctl or systemctl output from a failed operation. +sub error_linger_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)))); +} diff --git a/systemd/syslog_logs.pl b/systemd/syslog_logs.pl new file mode 100644 index 000000000..6b7cfdd8b --- /dev/null +++ b/systemd/syslog_logs.pl @@ -0,0 +1,24 @@ +# Supplies the System Logs module with a journalctl-backed log source. + +use strict; +use warnings; + +require 'systemd-lib.pl'; ## no critic + +our %text; + +# syslog_getlogs() +# Returns a journalctl log source if journalctl is installed. +sub syslog_getlogs +{ +if (has_command("journalctl")) { + # Let the System Logs module run journalctl when rendering entries. + return ( { 'cmd' => "journalctl -n 1000", + 'desc' => $text{'syslog_journalctl'}, + 'active' => 1, } ); + } +else { + # Without journalctl there is no useful systemd log source to add. + return ( ); + } +} diff --git a/systemd/systemd-lib.pl b/systemd/systemd-lib.pl new file mode 100644 index 000000000..cd003e504 --- /dev/null +++ b/systemd/systemd-lib.pl @@ -0,0 +1,3938 @@ +=head1 systemd-lib.pl + +Common functions for listing, creating and managing systemd system and user units. + +=cut + +use strict; +use warnings; +use lib ".."; +use Cwd qw(abs_path); +use Symbol qw(gensym); + +use WebminCore; + +our (%access, %config, %gconfig, %in, %text, @list_units_cache, $remote_user, + $module_var_directory); +our ($unit_config_change_flag, $daemon_reload_time_flag); + +init_config(); +%access = get_module_acl(); +$config{"desc"} = 1 if (!defined($config{"desc"})); +$config{"logs_lines"} = 200 + if (!defined($config{"logs_lines"}) || + $config{"logs_lines"} !~ /^\d+$/ || + $config{"logs_lines"} < 1); +$config{"logs_current_boot"} = 0 + if (!defined($config{"logs_current_boot"})); +$config{"show_runtime_units"} = 1 + if (!defined($config{"show_runtime_units"})); +$config{"default_create_scope"} = "system" + if (!defined($config{"default_create_scope"}) || + $config{"default_create_scope"} !~ /^(system|user)$/); +$config{"manual_vendor_units"} = 1 + if (!defined($config{"manual_vendor_units"})); +$config{"default_linger"} = 1 + if (!defined($config{"default_linger"})); +$config{"show_unit_suffixes"} = 0 + if (!defined($config{"show_unit_suffixes"})); +$config{"show_dropin_inventory"} = 1 + if (!defined($config{"show_dropin_inventory"})); +$config{"create_return_index"} = 0 + if (!defined($config{"create_return_index"}) || + $config{"create_return_index"} !~ /^[01]$/); +$config{"visible_tabs"} ||= default_visible_tabs(); +$unit_config_change_flag = $module_var_directory."/unit-change-flag"; +$daemon_reload_time_flag = $module_var_directory."/daemon-reload-flag"; + +=head2 systemd_acl_keys() + +Returns all boolean ACL keys understood by this module. + +=cut +sub systemd_acl_keys +{ +return (qw(view view_user status status_user logs logs_user + start start_user stop stop_user restart restart_user + boot boot_user mask mask_user + create create_user edit edit_user delete delete_user + dropin dropin_user manual manual_user reload linger backup)); +} + +=head2 systemd_user_unit_acl(user) + +Returns a safe ACL hash for managing one Unix user's systemd user units, +without granting any system-unit access. + +=cut +sub systemd_user_unit_acl +{ +my ($user) = @_; +my %acl = map { $_, 0 } systemd_acl_keys(); +foreach my $key (qw(view_user status_user logs_user start_user stop_user + restart_user boot_user create_user edit_user + delete_user dropin_user manual_user linger)) { + $acl{$key} = 1; + } +$acl{'noconfig'} = 1; +$acl{'mode'} = 1; +$acl{'users'} = defined($user) ? $user : ""; +$acl{'uidmin'} = ""; +$acl{'uidmax'} = ""; +return %acl; +} + +=head2 systemd_acl_bool(&acl, key) + +Returns a boolean ACL value. + +=cut +sub systemd_acl_bool +{ +my ($acl, $key) = @_; +$acl ||= \%access; +return $acl->{$key} ? 1 : 0 if (exists($acl->{$key})); +return 0; +} + +=head2 systemd_acl_error(reason-key) + +Throws a standardized ACL denial error. The key should be the suffix after +C