summaryrefslogtreecommitdiffstats
path: root/src/debputy/plugin/debputy/service_management.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/plugin/debputy/service_management.py')
-rw-r--r--src/debputy/plugin/debputy/service_management.py452
1 files changed, 452 insertions, 0 deletions
diff --git a/src/debputy/plugin/debputy/service_management.py b/src/debputy/plugin/debputy/service_management.py
new file mode 100644
index 0000000..1ec8c1b
--- /dev/null
+++ b/src/debputy/plugin/debputy/service_management.py
@@ -0,0 +1,452 @@
+import collections
+import dataclasses
+import os
+import textwrap
+from typing import Dict, List, Literal, Iterable, Sequence
+
+from debputy.packages import BinaryPackage
+from debputy.plugin.api.spec import (
+ ServiceRegistry,
+ VirtualPath,
+ PackageProcessingContext,
+ BinaryCtrlAccessor,
+ ServiceDefinition,
+)
+from debputy.util import _error, assume_not_none
+
+DPKG_ROOT = '"${DPKG_ROOT}"'
+EMPTY_DPKG_ROOT_CONDITION = '[ -z "${DPKG_ROOT}" ]'
+SERVICE_MANAGER_IS_SYSTEMD_CONDITION = "[ -d /run/systemd/system ]"
+
+
+@dataclasses.dataclass(slots=True)
+class SystemdServiceContext:
+ had_install_section: bool
+
+
+@dataclasses.dataclass(slots=True)
+class SystemdUnit:
+ path: VirtualPath
+ names: List[str]
+ type_of_service: str
+ service_scope: str
+ enable_by_default: bool
+ start_by_default: bool
+ had_install_section: bool
+
+
+def detect_systemd_service_files(
+ fs_root: VirtualPath,
+ service_registry: ServiceRegistry[SystemdServiceContext],
+ context: PackageProcessingContext,
+) -> None:
+ pkg = context.binary_package
+ systemd_units = _find_and_analyze_systemd_service_files(pkg, fs_root, "system")
+ for unit in systemd_units:
+ service_registry.register_service(
+ unit.path,
+ unit.names,
+ type_of_service=unit.type_of_service,
+ service_scope=unit.service_scope,
+ enable_by_default=unit.enable_by_default,
+ start_by_default=unit.start_by_default,
+ default_upgrade_rule="restart" if unit.start_by_default else "do-nothing",
+ service_context=SystemdServiceContext(
+ unit.had_install_section,
+ ),
+ )
+
+
+def generate_snippets_for_systemd_units(
+ services: Sequence[ServiceDefinition[SystemdServiceContext]],
+ ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ stop_in_prerm: List[str] = []
+ stop_then_start_scripts = []
+ on_purge = []
+ start_on_install = []
+ action_on_upgrade = collections.defaultdict(list)
+ assert services
+
+ for service_def in services:
+ if service_def.auto_enable_on_install:
+ template = """\
+ if deb-systemd-helper debian-installed {UNITFILE}; then
+ # The following line should be removed in trixie or trixie+1
+ deb-systemd-helper unmask {UNITFILE} >/dev/null || true
+
+ if deb-systemd-helper --quiet was-enabled {UNITFILE}; then
+ # Create new symlinks, if any.
+ deb-systemd-helper enable {UNITFILE} >/dev/null || true
+ fi
+ fi
+
+ # Update the statefile to add new symlinks (if any), which need to be cleaned
+ # up on purge. Also remove old symlinks.
+ deb-systemd-helper update-state {UNITFILE} >/dev/null || true
+ """
+ else:
+ template = """\
+ # The following line should be removed in trixie or trixie+1
+ deb-systemd-helper unmask {UNITFILE} >/dev/null || true
+
+ # was-enabled defaults to true, so new installations run enable.
+ if deb-systemd-helper --quiet was-enabled {UNITFILE}; then
+ # Enables the unit on first installation, creates new
+ # symlinks on upgrades if the unit file has changed.
+ deb-systemd-helper enable {UNITFILE} >/dev/null || true
+ else
+ # Update the statefile to add new symlinks (if any), which need to be
+ # cleaned up on purge. Also remove old symlinks.
+ deb-systemd-helper update-state {UNITFILE} >/dev/null || true
+ fi
+ """
+ service_name = service_def.name
+
+ if assume_not_none(service_def.service_context).had_install_section:
+ ctrl.maintscript.on_configure(
+ template.format(
+ UNITFILE=ctrl.maintscript.escape_shell_words(service_name),
+ )
+ )
+ on_purge.append(service_name)
+ elif service_def.auto_enable_on_install:
+ _error(
+ f'The service "{service_name}" cannot be enabled under "systemd" as'
+ f' it has no "[Install]" section. Please correct {service_def.definition_source}'
+ f' so that it does not enable the service or does not apply to "systemd"'
+ )
+
+ if service_def.auto_start_in_install:
+ start_on_install.append(service_name)
+ if service_def.on_upgrade == "stop-then-start":
+ stop_then_start_scripts.append(service_name)
+ elif service_def.on_upgrade in ("restart", "reload"):
+ action: str = service_def.on_upgrade
+ if not service_def.auto_start_in_install and action != "reload":
+ action = f"try-{action}"
+ action_on_upgrade[action].append(service_name)
+ elif service_def.on_upgrade != "do-nothing":
+ raise AssertionError(
+ f"Missing support for on_upgrade rule: {service_def.on_upgrade}"
+ )
+
+ if start_on_install or action_on_upgrade:
+ lines = [
+ "if {EMPTY_DPKG_ROOT_CONDITION} && {SERVICE_MANAGER_IS_SYSTEMD_CONDITION}; then".format(
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ SERVICE_MANAGER_IS_SYSTEMD_CONDITION=SERVICE_MANAGER_IS_SYSTEMD_CONDITION,
+ ),
+ " systemctl --system daemon-reload >/dev/null || true",
+ ]
+ if stop_then_start_scripts:
+ unit_files = ctrl.maintscript.escape_shell_words(*stop_then_start_scripts)
+ lines.append(
+ " deb-systemd-invoke start {UNITFILES} >/dev/null || true".format(
+ UNITFILES=unit_files,
+ )
+ )
+ if start_on_install:
+ lines.append(' if [ -z "$2" ]; then')
+ lines.append(
+ " deb-systemd-invoke start {UNITFILES} >/dev/null || true".format(
+ UNITFILES=ctrl.maintscript.escape_shell_words(*start_on_install),
+ )
+ )
+ lines.append(" fi")
+ if action_on_upgrade:
+ lines.append(' if [ -n "$2" ]; then')
+ for action, units in action_on_upgrade.items():
+ lines.append(
+ " deb-systemd-invoke {ACTION} {UNITFILES} >/dev/null || true".format(
+ ACTION=action,
+ UNITFILES=ctrl.maintscript.escape_shell_words(*units),
+ )
+ )
+ lines.append(" fi")
+ lines.append("fi")
+ combined = "".join(x if x.endswith("\n") else f"{x}\n" for x in lines)
+ ctrl.maintscript.on_configure(combined)
+
+ if stop_then_start_scripts:
+ ctrl.maintscript.unconditionally_in_script(
+ "preinst",
+ textwrap.dedent(
+ """\
+ if {EMPTY_DPKG_ROOT_CONDITION} && [ "$1" = upgrade ] && {SERVICE_MANAGER_IS_SYSTEMD_CONDITION} ; then
+ deb-systemd-invoke stop {UNIT_FILES} >/dev/null || true
+ fi
+ """.format(
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ SERVICE_MANAGER_IS_SYSTEMD_CONDITION=SERVICE_MANAGER_IS_SYSTEMD_CONDITION,
+ UNIT_FILES=ctrl.maintscript.escape_shell_words(
+ *stop_then_start_scripts
+ ),
+ )
+ ),
+ )
+
+ if stop_in_prerm:
+ ctrl.maintscript.on_before_removal(
+ """\
+ if {EMPTY_DPKG_ROOT_CONDITION} && {SERVICE_MANAGER_IS_SYSTEMD_CONDITION} ; then
+ deb-systemd-invoke stop {UNIT_FILES} >/dev/null || true
+ fi
+ """.format(
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ SERVICE_MANAGER_IS_SYSTEMD_CONDITION=SERVICE_MANAGER_IS_SYSTEMD_CONDITION,
+ UNIT_FILES=ctrl.maintscript.escape_shell_words(*stop_in_prerm),
+ )
+ )
+ if on_purge:
+ ctrl.maintscript.on_purge(
+ """\
+ if [ -x "/usr/bin/deb-systemd-helper" ]; then
+ deb-systemd-helper purge {UNITFILES} >/dev/null || true
+ fi
+ """.format(
+ UNITFILES=ctrl.maintscript.escape_shell_words(*stop_in_prerm),
+ )
+ )
+ ctrl.maintscript.on_removed(
+ textwrap.dedent(
+ """\
+ if {SERVICE_MANAGER_IS_SYSTEMD_CONDITION} ; then
+ systemctl --system daemon-reload >/dev/null || true
+ fi
+ """.format(
+ SERVICE_MANAGER_IS_SYSTEMD_CONDITION=SERVICE_MANAGER_IS_SYSTEMD_CONDITION
+ )
+ )
+ )
+
+
+def _remove_quote(v: str) -> str:
+ if v and v[0] == v[-1] and v[0] in ('"', "'"):
+ return v[1:-1]
+ return v
+
+
+def _find_and_analyze_systemd_service_files(
+ pkg: BinaryPackage,
+ fs_root: VirtualPath,
+ systemd_service_dir: Literal["system", "user"],
+) -> Iterable[SystemdUnit]:
+ service_dirs = [
+ f"./usr/lib/systemd/{systemd_service_dir}",
+ f"./lib/systemd/{systemd_service_dir}",
+ ]
+ had_install_sections = set()
+ aliases: Dict[str, List[str]] = collections.defaultdict(list)
+ seen = set()
+ all_files = []
+ expected_units = set()
+ expected_units_required_by = collections.defaultdict(list)
+
+ for d in service_dirs:
+ system_dir = fs_root.lookup(d)
+ if not system_dir:
+ continue
+ for child in system_dir.iterdir:
+ if child.is_symlink:
+ dest = os.path.basename(child.readlink())
+ aliases[dest].append(child.name)
+ elif child.is_file and child.name not in seen:
+ seen.add(child.name)
+ all_files.append(child)
+ if "@" in child.name:
+ # dh_installsystemd does not check the contents of templated services,
+ # and we match that.
+ continue
+ with child.open() as fd:
+ for line in fd:
+ line = line.strip()
+ line_lc = line.lower()
+ if line_lc == "[install]":
+ had_install_sections.add(child.name)
+ elif line_lc.startswith("alias="):
+ # This code assumes service names cannot contain spaces (as in
+ # if you copy-paste it for another field it might not work)
+ aliases[child.name].extend(
+ _remove_quote(x) for x in line[6:].split()
+ )
+ elif line_lc.startswith("also="):
+ # This code assumes service names cannot contain spaces (as in
+ # if you copy-paste it for another field it might not work)
+ for unit in (_remove_quote(x) for x in line[5:].split()):
+ expected_units_required_by[unit].append(child.absolute)
+ expected_units.add(unit)
+ for path in all_files:
+ if "@" in path.name:
+ # Match dh_installsystemd, which skips templated services
+ continue
+ names = aliases[path.name]
+ _, type_of_service = path.name.rsplit(".", 1)
+ expected_units.difference_update(names)
+ expected_units.discard(path.name)
+ names.extend(x[:-8] for x in list(names) if x.endswith(".service"))
+ names.insert(0, path.name)
+ if path.name.endswith(".service"):
+ names.insert(1, path.name[:-8])
+ yield SystemdUnit(
+ path,
+ names,
+ type_of_service,
+ systemd_service_dir,
+ # Bug (?) compat with dh_installsystemd. All units are started, but only
+ # enable those with an `[Install]` section.
+ # Possibly related bug #1055599
+ enable_by_default=path.name in had_install_sections,
+ start_by_default=True,
+ had_install_section=path.name in had_install_sections,
+ )
+
+ if expected_units:
+ for unit_name in expected_units:
+ required_by = expected_units_required_by[unit_name]
+ required_names = ", ".join(required_by)
+ _error(
+ f"The unit {unit_name} was required by {required_names} (via Also=...)"
+ f" but was not present in the package {pkg.name}"
+ )
+
+
+def generate_snippets_for_init_scripts(
+ services: Sequence[ServiceDefinition[None]],
+ ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ for service_def in services:
+ script_name = service_def.path.name
+ script_installed_path = service_def.path.absolute
+
+ update_rcd_params = (
+ "defaults" if service_def.auto_enable_on_install else "defaults-disabled"
+ )
+
+ ctrl.maintscript.unconditionally_in_script(
+ "preinst",
+ textwrap.dedent(
+ """\
+ if [ "$1" = "install" ] && [ -n "$2" ] && [ -x {DPKG_ROOT}{SCRIPT_PATH} ] ; then
+ chmod +x {DPKG_ROOT}{SCRIPT_PATH} >/dev/null || true
+ fi
+ """.format(
+ DPKG_ROOT=DPKG_ROOT,
+ SCRIPT_PATH=ctrl.maintscript.escape_shell_words(
+ script_installed_path
+ ),
+ )
+ ),
+ )
+
+ lines = [
+ "if {EMPTY_DPKG_ROOT_CONDITION} && [ -x {SCRIPT_PATH} ]; then",
+ " update-rc.d {SCRIPT_NAME} {UPDATE_RCD_PARAMS} >/dev/null || exit 1",
+ ]
+
+ if (
+ service_def.auto_start_in_install
+ and service_def.on_upgrade != "stop-then-start"
+ ):
+ lines.append(' if [ -z "$2" ]; then')
+ lines.append(
+ " invoke-rc.d --skip-systemd-native {SCRIPT_NAME} start >/dev/null || exit 1".format(
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ )
+ )
+ lines.append(" fi")
+
+ if service_def.on_upgrade in ("restart", "reload"):
+ lines.append(' if [ -n "$2" ]; then')
+ lines.append(
+ " invoke-rc.d --skip-systemd-native {SCRIPT_NAME} {ACTION} >/dev/null || exit 1".format(
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ ACTION=service_def.on_upgrade,
+ )
+ )
+ lines.append(" fi")
+ elif service_def.on_upgrade == "stop-then-start":
+ lines.append(
+ " invoke-rc.d --skip-systemd-native {SCRIPT_NAME} start >/dev/null || exit 1".format(
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ )
+ )
+ ctrl.maintscript.unconditionally_in_script(
+ "preinst",
+ textwrap.dedent(
+ """\
+ if {EMPTY_DPKG_ROOT_CONDITION} && [ "$1" = "upgrade" ] && [ -x {SCRIPT_PATH} ]; then
+ invoke-rc.d --skip-systemd-native {SCRIPT_NAME} stop > /dev/null || true
+ fi
+ """.format(
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ SCRIPT_PATH=ctrl.maintscript.escape_shell_words(
+ script_installed_path
+ ),
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ )
+ ),
+ )
+ elif service_def.on_upgrade != "do-nothing":
+ raise AssertionError(
+ f"Missing support for on_upgrade rule: {service_def.on_upgrade}"
+ )
+
+ lines.append("fi")
+ combined = "".join(x if x.endswith("\n") else f"{x}\n" for x in lines)
+ ctrl.maintscript.on_configure(
+ combined.format(
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ DPKG_ROOT=DPKG_ROOT,
+ UPDATE_RCD_PARAMS=update_rcd_params,
+ SCRIPT_PATH=ctrl.maintscript.escape_shell_words(script_installed_path),
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ )
+ )
+
+ ctrl.maintscript.on_removed(
+ textwrap.dedent(
+ """\
+ if [ -x {DPKG_ROOT}{SCRIPT_PATH} ]; then
+ chmod -x {DPKG_ROOT}{SCRIPT_PATH} > /dev/null || true
+ fi
+ """.format(
+ DPKG_ROOT=DPKG_ROOT,
+ SCRIPT_PATH=ctrl.maintscript.escape_shell_words(
+ script_installed_path
+ ),
+ )
+ )
+ )
+ ctrl.maintscript.on_purge(
+ textwrap.dedent(
+ """\
+ if {EMPTY_DPKG_ROOT_CONDITION} ; then
+ update-rc.d {SCRIPT_NAME} remove >/dev/null
+ fi
+ """.format(
+ SCRIPT_NAME=ctrl.maintscript.escape_shell_words(script_name),
+ EMPTY_DPKG_ROOT_CONDITION=EMPTY_DPKG_ROOT_CONDITION,
+ )
+ )
+ )
+
+
+def detect_sysv_init_service_files(
+ fs_root: VirtualPath,
+ service_registry: ServiceRegistry[None],
+ _context: PackageProcessingContext,
+) -> None:
+ etc_init = fs_root.lookup("/etc/init.d")
+ if not etc_init:
+ return
+ for path in etc_init.iterdir:
+ if path.is_dir or not path.is_executable:
+ continue
+
+ service_registry.register_service(
+ path,
+ path.name,
+ )