generate_unit.py

script

← Back to skill

Content hash: e4b438eaff3413154e706464b12e3a03df455378b732a4153c2f2e91d78b7b6f
#!/usr/bin/env python3
"""Generate a systemd unit file from a simple config.

Usage: python generate_unit.py --name my-app --exec /opt/app/venv/bin/app --user svc
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

UNIT_TEMPLATE = """[Unit]
Description={description}
After=network.target
{after_extra}
[Service]
Type={svc_type}
User={user}
Group={group}
WorkingDirectory={workdir}
ExecStart={exec_start}
Restart={restart}
RestartSec={restart_sec}
{env_file_line}
{extra_service}
[Install]
WantedBy=multi-user.target
"""


def generate(args: argparse.Namespace) -> str:
    env_file_line = ""
    if args.env_file:
        env_file_line = f"EnvironmentFile={args.env_file}"

    after_extra = ""
    if args.after_db:
        after_extra = "After=postgresql.service"

    extra_service = ""
    if args.svc_type == "notify":
        extra_service = "NotifyAccess=all\n"
    if args.limit_nofile:
        extra_service += f"LimitNOFILE={args.limit_nofile}\n"
    if args.timeout_sec:
        extra_service += f"TimeoutStopSec={args.timeout_sec}\n"

    return UNIT_TEMPLATE.format(
        description=args.description or args.name,
        after_extra=after_extra,
        svc_type=args.svc_type,
        user=args.user,
        group=args.group or args.user,
        workdir=args.workdir,
        exec_start=args.exec_start,
        restart=args.restart,
        restart_sec=args.restart_sec,
        env_file_line=env_file_line,
        extra_service=extra_service,
    )


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate a systemd unit file")
    parser.add_argument("--name", required=True, help="Service name")
    parser.add_argument("--description", help="Service description")
    parser.add_argument(
        "--exec-start", required=True, help="Full path to binary + args"
    )
    parser.add_argument("--user", required=True, help="Unprivileged user to run as")
    parser.add_argument("--group", help="Group (defaults to --user)")
    parser.add_argument("--workdir", default="/opt/app", help="WorkingDirectory")
    parser.add_argument("--env-file", help="Path to EnvironmentFile")
    parser.add_argument(
        "--svc-type",
        default="simple",
        choices=["simple", "notify", "forking", "oneshot"],
    )
    parser.add_argument(
        "--restart",
        default="on-failure",
        choices=["no", "on-failure", "always", "on-abnormal"],
    )
    parser.add_argument("--restart-sec", type=int, default=3)
    parser.add_argument(
        "--after-db", action="store_true", help="Add After=postgresql"
    )
    parser.add_argument("--limit-nofile", type=int, help="File descriptor limit")
    parser.add_argument("--timeout-sec", type=int, help="TimeoutStopSec")
    parser.add_argument("--output", help="Write to file instead of stdout")

    args = parser.parse_args()
    unit = generate(args)

    if args.output:
        Path(args.output).write_text(unit)
        print(f"Unit written to {args.output}")
    else:
        print(unit)

    # Reminders
    install_name = args.output.rsplit("/", 1)[-1] if args.output else args.name
    print("\n# After deploying, run:")
    print(f"#   sudo cp {args.output or '<file>'} /etc/systemd/system/{install_name}.service")
    print(f"#   sudo systemctl daemon-reload")
    print(f"#   sudo systemctl enable --now {install_name}")


if __name__ == "__main__":
    main()