erplibre/script/systemd/install_daemon.py
Mathieu Benoit da2dc63a19 [FIX] outillage : désimbriquer les f-strings que seul 3.12 sait lire
Cinq fichiers employaient des f-strings PEP 701 — guillemets imbriqués du même
type, backslash dans l'expression, expression sur plusieurs lignes — que
Python refuse avant 3.12. Or les hooks de script/git/hooks portent
« #!/usr/bin/env python3 » et tournent donc sur l'interpréteur du système,
qu'une distribution livre encore en 3.10 : la SyntaxError tombe au chargement,
sans qu'aucun garde puisse nommer la commande à taper. Les chaînes traduites
gardent leur texte à l'octet près, sans quoi leur clé se perdrait. Les
commentaires de ces fichiers passent du récit au présent, comme la règle le
demande de ce qu'on touche.
Vérifié : tout l'arbre Python parse sous 3.10, 3.11, 3.12 et 3.14.

--- EN ---

Five files used PEP 701 f-strings — quotes of the same kind nested, a backslash
in the expression, an expression spanning lines — which Python refuses before
3.12. The hooks in script/git/hooks carry "#!/usr/bin/env python3" and so run
on the system interpreter, which a distribution still ships as 3.10: the
SyntaxError lands at load, before any guard can name the command to type.
Translated strings keep their text byte for byte, else their key would be lost.
The comments of those files move from tale to present tense, as the rule asks
of what one touches.
Checked: the whole Python tree parses under 3.10, 3.11, 3.12 and 3.14.

Assisted-by: Claude Opus 5
2026-09-24 13:56:57 -04:00

197 lines
6.1 KiB
Python
Executable file

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import argparse
import os
import stat
import subprocess
import sys
from pathlib import Path
# « ExecStart=/bin/bash …/run.sh » et non le script seul.
#
# Lancé seul, systemd doit EXÉCUTER le fichier, et l'échoue en « 203/EXEC »
# dans quatre cas au moins : bit x absent, shebang qui ne résout pas, /home
# monté noexec, SELinux refusant l'execve. L'échec est alors MUET : le
# processus meurt avant d'entrer dans le script, sans une ligne de sortie, et
# 203 ressemble à une erreur d'application — le diagnostic part donc dans la
# mauvaise direction.
#
# Passé à bash, run.sh n'est plus qu'une DONNÉE lue : les quatre causes
# disparaissent ensemble, y compris noexec et SELinux, qui ne portent que sur
# l'execve. Rien n'est perdu au passage — le shebang du script désigne déjà
# bash. C'est aussi ce que font install_daemon.sh et le générateur de todo.py,
# les trois écrivant la même unité.
UNIT_TEMPLATE = """[Unit]
Description=ERPLibre for {user}
Requires=postgresql.service
After=network.target network-online.target postgresql.service
[Service]
Type=simple
SyslogIdentifier={user}
PermissionsStartOnly=true
User={user}
Group={user}
Restart=always
RestartSec=5
PIDFile={home_erplibre}/.venv.erplibre/service.pid
# bash explicite : voir install_daemon.py (evite 203/EXEC).
ExecStart=/bin/bash {home_erplibre}/run.sh{EXEC_PARAM}
WorkingDirectory={home_erplibre}
StandardOutput=journal+console
[Install]
WantedBy=multi-user.target
"""
def ensure_root():
if os.geteuid() != 0:
sys.exit("❌ This script must be run as root (use sudo or root).")
def run(cmd: list[str]) -> None:
subprocess.run(cmd, check=True)
def w_cmd(param: str, arg: str = None) -> str:
if not arg:
if type(arg) is str:
return ""
return param
return f"{param} {arg}"
def main():
parser = argparse.ArgumentParser(
description="Create and enable a SystemD service for ERPLibre (Python equivalent of the bash script)."
)
# Exposed variables as configurable parameters
parser.add_argument(
"--user",
required=True,
help="Linux user running the service (e.g. erplibre)",
)
parser.add_argument(
"--home-erplibre",
help="Path to erplibre directory, workspace root path.",
)
parser.add_argument("--port", help="Application port (for information).")
parser.add_argument(
"--database",
help="Database name to run only with it (for information).",
)
parser.add_argument(
"--config-name",
help="Service name (and systemd file name without .service). Default: user",
)
# Utility flags
parser.add_argument(
"--no-enable",
action="store_true",
help="Do not enable service on boot.",
)
parser.add_argument(
"--no-restart",
action="store_true",
help="Do not restart the service after setup.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without changing the system.",
)
args = parser.parse_args()
ensure_root()
# Default paths and fallback values (same logic as in the bash script)
el_user = args.user
el_home_erplibre = args.home_erplibre or os.getcwd()
el_config_name = (
args.config_name
or f"erplibre_{el_user}_{os.path.basename(os.getcwd())}"
)
db_param = w_cmd("-d", args.database or "")
port_param = w_cmd("-p", args.port or "")
exec_param = " " + f" {db_param} {port_param}".strip()
# Render the systemd service file content
unit_content = UNIT_TEMPLATE.format(
user=el_user,
home_erplibre=el_home_erplibre,
EXEC_PARAM=exec_param,
)
unit_dest_path = Path(f"/etc/systemd/system/{el_config_name}.service")
print("* Final systemd file location:", unit_dest_path)
if args.dry_run:
print("\n--- GENERATED CONTENT ---\n")
print(unit_content)
print("\n--- END ---")
print("\n(dry-run) No files written, no systemctl commands executed.")
return
# Write temporary unit file
unit_dest_path.write_text(unit_content, encoding="utf-8")
# chmod 755
print("* Setting permissions to 755")
unit_dest_path.chmod(
stat.S_IRUSR
| stat.S_IWUSR
| stat.S_IXUSR
| stat.S_IRGRP
| stat.S_IXGRP
| stat.S_IROTH
| stat.S_IXOTH
)
# chown root:root
print("* Changing owner to root:root")
os.chown(unit_dest_path, 0, 0)
# Sanity checks
if not Path(el_home_erplibre).exists():
print(f"⚠️ Warning: {el_home_erplibre} does not exist.")
if not Path(el_home_erplibre, "run.sh").exists():
print(f"⚠️ Warning: {el_home_erplibre}/run.sh does not exist.")
# Reload systemd and enable service
print("* Reloading systemd daemon")
run(["systemctl", "daemon-reload"])
if not args.no_enable:
print(f"* Enabling service at boot: {el_config_name}.service")
run(["systemctl", "enable", f"{el_config_name}.service"])
else:
print("* (skip) enable step")
if not args.no_restart:
print(f"* Restarting service: {el_config_name}.service")
run(["systemctl", "restart", f"{el_config_name}.service"])
else:
print("* (skip) restart step")
# Summary (equivalent of bash echo block)
print("\n-----------------------------------------------------------")
print("Done! ERPLibre service created and started. Details:")
print(f"Port: {args.port}")
print(f"Service user: {el_user}")
print(f"Systemd file: {unit_dest_path}")
print(f"Start: systemctl start {el_config_name}")
print(f"Stop: systemctl stop {el_config_name}")
print(f"Restart: systemctl restart {el_config_name}")
print(f"Status: systemctl status {el_config_name}")
print(f"Logs: journalctl -feu {el_config_name}")
print("-----------------------------------------------------------")
if __name__ == "__main__":
main()