diff options
Diffstat (limited to 'dh_installsystemd')
-rwxr-xr-x | dh_installsystemd | 453 |
1 files changed, 453 insertions, 0 deletions
diff --git a/dh_installsystemd b/dh_installsystemd new file mode 100755 index 0000000..aba9d78 --- /dev/null +++ b/dh_installsystemd @@ -0,0 +1,453 @@ +#!/usr/bin/perl -w + +=head1 NAME + +dh_installsystemd - install systemd unit files + +=cut + +use strict; +use warnings; +use Debian::Debhelper::Dh_Lib; +use File::Find; +use Cwd qw(getcwd abs_path); + +our $VERSION = DH_BUILTIN_VERSION; + +=head1 SYNOPSIS + +B<dh_installsystemd> [S<I<debhelper options>>] [B<--restart-after-upgrade>] [B<--no-stop-on-upgrade>] [B<--no-enable>] [B<--no-start>] [B<--name=>I<name>] [S<I<unit file> ...>] + +=head1 DESCRIPTION + +B<dh_installsystemd> is a debhelper program that is responsible for +installing package maintainer supplied systemd unit files. + +It also finds the service files installed by a package and generates +F<preinst>, F<postinst>, and F<prerm> code blocks for enabling, +disabling, starting, stopping, and restarting the corresponding +systemd services, when the package is installed, updated, or +removed. These snippets are added to the maintainer scripts by +L<dh_installdeb(1)>. + +L<deb-systemd-helper(1)> is used to enable and disable systemd units, +thus it is not necessary that the machine actually runs systemd during +package installation time, enabling happens on all machines in order +to be able to switch from sysvinit to systemd and back. + +B<dh_installsystemd> operates on all unit files installed by a +package. For only generating blocks for specific unit files, pass them +as arguments, C<dh_installsystemd quota.service>. Specific unit files +can be excluded from processing using the B<-X> common L<debhelper(1)> +option. + +=head1 FILES + +=over 4 + +=item debian/I<package>.mount, + debian/I<package>.path, + debian/I<package>@.path, + debian/I<package>.service, + debian/I<package>@.service, + debian/I<package>.socket, + debian/I<package>@.socket, + debian/I<package>.target, + debian/I<package>@.target, + debian/I<package>.timer, + debian/I<package>@.timer + +If any of those files exists, they are installed into +F<lib/systemd/system/> in the package build directory. + +=item debian/I<package>.tmpfile + +Only used in compat 12 or earlier. In compat 13+, this file is +handled by L<dh_installtmpfiles(1)> instead. + +If this exists, it is installed into F<usr/lib/tmpfiles.d/> in the +package build directory. Note that the C<tmpfiles.d> mechanism is +currently only used by systemd. + +=back + +=head1 OPTIONS + +=over 4 + +=item B<--no-enable> + +Disable the service(s) on purge, but do not enable them on install. + +B<Note> that this option does not affect whether the services are +started. Please remember to also use B<--no-start> if the service +should not be started. + +=item B<--name=>I<name> + +This option controls several things. + +It changes the name that B<dh_installsystemd> uses when it looks for +maintainer provided systemd unit files as listed in the L</FILES> +section. As an example, B<dh_installsystemd --name foo> will look for +F<<< I<debian/package.>B<< I<foo> >>I<.service> >>> instead of +F<< I<debian/package.service> >>). These unit files are installed as F<< +I<name.unit-extension> >> (in the example, it would be installed as +F<<< B<< I<foo> >>I<.service> >>>). + +Furthermore, if no unit files are passed explicitly as command line +arguments, B<dh_installsystemd> will only act on unit files called +I<name> (rather than all unit files found in the package). + +=item B<--restart-after-upgrade> + +Do not stop the unit file until after the package upgrade has been completed. +This is the default behaviour in compat 10. + +In earlier compat levels the default was to stop the unit file in the +F<prerm>, and start it again in the F<postinst>. + +This can be useful for daemons that should not have a possibly long +downtime during upgrade. But you should make sure that the daemon will not +get confused by the package being upgraded while it's running before using +this option. + +=item B<--no-restart-after-upgrade> + +Undo a previous B<--restart-after-upgrade> (or the default of compat +10). If no other options are given, this will cause the service to be +stopped in the F<prerm> script and started again in the F<postinst> +script. + +=item B<-r>, B<--no-stop-on-upgrade>, B<--no-restart-on-upgrade> + +Do not stop service on upgrade. This has the side-effect of not +restarting the service as a part of the upgrade. + +If you want to restart the service with minimal downtime, please use +B<--restart-after-upgrade> (default in compat 10 or later). If you want +the service to be restarted but be stopped during the upgrade, then please +use B<--no-restart-after-upgrade> (note the "after-upgrade"). + +Note that the B<--no-restart-on-upgrade> alias is deprecated and will +be removed in compat 14. This is to avoid confusion with the +B<--no-restart-after-upgrade> option. + +=item B<--no-start> + +Do not start the unit file after upgrades and after initial installation (the +latter is only relevant for services without a corresponding init script). + +B<Note> that this option does not affect whether the services are +enabled. Please remember to also use B<--no-enable> if the services +should not be enabled. + +=item S<B<unit file> ...> + +Only process and generate maintscripts for the installed unit files +with the (base)name I<unit file>. + +Note: B<dh_installsystemd> will still install unit files from +F<debian/> but it will not generate any maintscripts for them unless +they are explicitly listed in S<B<unit file> ...> + +=back + +=head1 NOTES + +This command is not idempotent. L<dh_prep(1)> should be called between +invocations of this command (with the same arguments). Otherwise, it +may cause multiple instances of the same text to be added to +maintainer scripts. + +=cut + +$dh{RESTART_AFTER_UPGRADE} = ''; +$dh{NO_START} = ''; + +init(options => { + "no-enable" => \$dh{NO_ENABLE}, + "r" => \$dh{R_FLAG}, + 'no-stop-on-upgrade' => \$dh{R_FLAG}, + "no-restart-on-upgrade" => \$dh{R_FLAG}, + "no-start" => \$dh{NO_START}, + "R|restart-after-upgrade!" => \$dh{RESTART_AFTER_UPGRADE}, + "no-also" => \$dh{NO_ALSO}, # undocumented option +}); + +if ($dh{RESTART_AFTER_UPGRADE} eq '') { + $dh{RESTART_AFTER_UPGRADE} = 1 if not defined($dh{R_FLAG}) and $dh{NO_START} eq ''; +} + +sub quote { + # Add single quotes around the argument. + return '\'' . $_[0] . '\''; +} + +sub uniq { + my %seen; + return grep { !$seen{$_}++ } @_; +} + +sub contains_install_section { + my ($unit_path) = @_; + + open(my $fh, '<', $unit_path) or error("Cannot open($unit_path): $!"); + + while (my $line = <$fh>) { + chomp($line); + return 1 if $line =~ /^\s*\[Install\]$/i; + } + close($fh); + return 0; +} + +sub has_sysv_equivalent { + my ($tmpdir, $unit) = @_; + + $unit =~ s/\.(?:mount|service|socket|target|path)$//g; + return -f "$tmpdir/etc/init.d/$unit"; +} + +sub install_unit { + my ($package, $script, $pkgsuffix, $path, $installsuffix) = @_; + $installsuffix = $installsuffix || $pkgsuffix; + my $unit = pkgfile($package, $pkgsuffix); + return if $unit eq ''; + install_dir($path); + install_file($unit, "${path}/${script}.${installsuffix}"); +} + +# Extracts the directive values from a unit file. Handles repeated +# directives in the same unit file. Assumes values can only be +# composed of lists of unit names. This is good enough for the 'Also=' +# and 'Alias=' directives handled here. +sub extract_key { + my ($unit_path, $key) = @_; + my @values; + + open(my $fh, '<', $unit_path) or error("Cannot open($unit_path): $!"); + + while (my $line = <$fh>) { + chomp($line); + + # Since unit names can't have whitespace in systemd, simply + # use split and strip any leading/trailing quotes. See + # systemd-escape(1) for examples of valid unit names. + if ($line =~ /^\s*$key=(.+)$/i) { + for my $value (split(/\s+/, $1)) { + $value =~ s/^(["'])(.*)\g1$/$2/; + push @values, $value; + } + } + } + + close($fh); + return @values; +} + +sub list_installed_units { + my ($tmpdir, $aliases) = @_; + + my $lib_systemd_system = "$tmpdir/lib/systemd/system"; + my @installed; + + return unless -d $lib_systemd_system; + opendir(my $dh, "$lib_systemd_system") or error("Cannot opendir($lib_systemd_system): $!"); + + foreach my $name (readdir($dh)) { + my $path = "$lib_systemd_system/$name"; + next unless -f $path; + if (-l "$path") { + my $dest = basename(readlink($path)); + $aliases->{$dest} //= [ ]; + push @{$aliases->{$dest}}, $name; + } else { + push @installed, $name; + } + } + + closedir($dh); + return @installed; +} + + +# PROMISE: DH NOOP WITHOUT internal(bug#950723) tmp(lib/systemd/system) tmp(usr/lib/tmpfiles.d) tmp(etc/tmpfiles.d) mount path service socket target tmpfile timer cli-options() + + +# Install package maintainer supplied unit files +foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + + # Intall all unit files in the debian/ directory with names in the + # form $package.(service|target|socket|path|timer|mount|tmpfile) + # and their templated version when relevant. + + # This can be modified with the --name option to look for unit + # files with names in the form $package.$name.(service|...) and + # $name.(service|target|socket|path|timer|mount|tmpfile) and their + # templated version when relevant. + my $name = $dh{NAME} // $package; + + for my $type (qw(service target socket path timer)) { + install_unit($package, $name, $type, "$tmpdir/lib/systemd/system"); + install_unit("${package}@", "${name}@", $type, "$tmpdir/lib/systemd/system"); + } + + install_unit($package, $name, 'mount', "$tmpdir/lib/systemd/system"); + # In compat 13+, this is handled by dh_installtmpfiles + install_unit($package, $name, 'tmpfile', "$tmpdir/usr/lib/tmpfiles.d", 'conf') if compat(12); +} + + +if (compat(12)) { + # In compat 13+, this is handled by dh_installtmpfiles + # Add postinst code blocks to handle tmpfiles + foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + my @tmpfiles; + + my @dirs = grep { -d } map { "${tmpdir}/$_" } qw(usr/lib/tmpfiles.d etc/tmpfiles.d); + + find({ + wanted => sub { + my $name = $File::Find::name; + return if not -f $name or not $name =~ m{[.]conf$}; + push(@tmpfiles, basename($name)); }, + no_chdir => 1, + }, @dirs) if @dirs; + + if (@tmpfiles) { + autoscript($package, 'postinst', 'postinst-init-tmpfiles', { 'TMPFILES' => join(' ', sort @tmpfiles) }); + } + } +} + + +# Add postinst, prerm, and postrm code blocks to handle activation, +# deactivation, start and stopping of services when the package is +# installed, upgraded or removed. +foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + my (@args, @start_units, @enable_units, %aliases); + + my @installed_units = list_installed_units($tmpdir, \%aliases); + + # Handle either only the unit files which were passed as arguments + # or all unit files that are installed in this package. + if (@ARGV) { + @args = @ARGV; + } + elsif ($dh{NAME}) { + # Treat --name option as if the corresponding unit names were + # passed in the command line. + @args = grep /(^|\/)$dh{NAME}\.(mount|path|service|socket|target|timer)$/, @installed_units; + } + else { + @args = @installed_units; + } + + # Support excluding units via the -X debhelper common option. + foreach my $x (@{$dh{EXCLUDE}}) { + @args = grep !/(^|\/)$x$/, @args; + } + + # This hash prevents us from looping forever in the following + # while loop. An actual real-world example of such a loop is + # systemd's systemd-readahead-drop.service, which contains + # Also=systemd-readahead-collect.service, and that file in turn + # contains Also=systemd-readahead-drop.service, thus forming an + # endless loop. + my %seen; + + # Must use while and shift because the loop alters the list. + while (@args) { + my $unit = shift @args; + my $path = "${tmpdir}/lib/systemd/system/${unit}"; + + error("Package '$package' does not install unit '$unit'.") unless (-f $path); + + # Skip template service files. Enabling, disabling, starting + # or stopping those services without specifying the instance + # is not useful. + next if ($unit =~ /\@/); + + # Handle all unit files specified via Also= explicitly. This + # is not necessary for enabling, but for disabling, as we + # cannot read the unit file when disabling as it has already + # been deleted. The undocumented --no-also option disables + # handling of units linked via Also=. This option is provided + # only to suport a very specific use case in network-manager. + unless ($dh{NO_ALSO}) { + push @args, $_ for grep { !$seen{$_}++ } extract_key($path, 'Also'); + } + + # Extract unit aliases. + push @{$aliases{$unit}}, $_ for extract_key($path, 'Alias'); + + # In compat 11 (and earlier), dh_installinit will handle services with + # a sysv-equivalent service. In compat 12, dh_installsystemd will + # take care of it. + if (not compat(11) or not grep { has_sysv_equivalent($tmpdir, $_) } ($unit, @{$aliases{$unit}})) { + push @start_units, $unit; + } + + if (contains_install_section($path)) { + push @enable_units, $unit; + } + } + + @enable_units = map { quote($_) } uniq sort @enable_units; + @start_units = map { quote($_) } uniq sort @start_units; + + my %options = ('snippet-order' => 'service'); + + if (@enable_units) { + for my $unit (@enable_units) { + my $snippet = $dh{NO_ENABLE} ? 'postinst-systemd-dont-enable' : 'postinst-systemd-enable'; + autoscript($package, 'postinst', $snippet, { 'UNITFILE' => $unit }, \%options); + } + autoscript($package, 'postrm', 'postrm-systemd', {'UNITFILES' => join(' ', @enable_units) }); + } + + if (@start_units) { + my $replace = { 'UNITFILES' => join(' ', @start_units) }; + + if ($dh{RESTART_AFTER_UPGRADE}) { + my $snippet; + if ($dh{NO_START}) { + $snippet = 'postinst-systemd-restartnostart'; + $replace->{RESTART_ACTION} = 'try-restart'; + } else { + $snippet = 'postinst-systemd-restart'; + $replace->{RESTART_ACTION} = 'restart'; + } + autoscript($package, 'postinst', $snippet, $replace, \%options); + } elsif (!$dh{NO_START}) { + # (stop|start) service (before|after) upgrade + autoscript($package, 'postinst', 'postinst-systemd-start', $replace, \%options); + } + + # stop service before upgrade, if requested + autoscript($package, 'preinst', 'preinst-systemd-stop', $replace, \%options) + unless ($dh{R_FLAG} || $dh{RESTART_AFTER_UPGRADE}); + + # stop service only on remove + autoscript($package, 'prerm', 'prerm-systemd-restart', $replace, \%options) + unless ($dh{NO_START}); + + # Run this with "default" order so it is always after other + # service related autosnippets. + autoscript($package, 'postrm', 'postrm-systemd-reload-only', $replace); + } +} + +=head1 SEE ALSO + +L<debhelper(7)>, L<dh_installinit(1)>, L<deb-systemd-helper(1)> + +=head1 AUTHORS + +pkg-systemd-maintainers@lists.alioth.debian.org + +=cut |