diff --git a/install_loki_debian13.py b/install_loki_debian13.py new file mode 100644 index 0000000..e29f735 --- /dev/null +++ b/install_loki_debian13.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Installer & updater for Grafana Loki (Debian 13 / trixie). +Default run uses: + --deb-url latest from GitHub + --config /etc/loki/custom-config.yml + --data-dir /data/loki + +--force: wykonuje wszystkie kroki od nowa i nadpisuje pliki/katalogi/konfigi. +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.request +from urllib.error import URLError, HTTPError + +GITHUB_LATEST_API = "https://api.github.com/repos/grafana/loki/releases/latest" + +PKG_NAME = "loki" +BIN_PATH = "/usr/bin/loki" + +SYSTEMD_UNIT = "/lib/systemd/system/loki.service" +SYSTEMD_OVERRIDE_DIR = "/etc/systemd/system/loki.service.d" +SYSTEMD_OVERRIDE_PATH = os.path.join(SYSTEMD_OVERRIDE_DIR, "override.conf") + +DEFAULT_USER = "loki" +DEFAULT_GROUP = "loki" +DEFAULT_CONFIG_DIR = "/etc/loki" +DEFAULT_CONFIG_PATH = os.path.join(DEFAULT_CONFIG_DIR, "custom-config.yml") +DEFAULT_DATA_DIR = "/data/loki" +DEFAULT_LOG_DIR = "/var/log/loki" + +CUSTOM_CONFIG = """ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + log_level: debug + grpc_server_max_concurrent_streams: 1000 + +common: + instance_addr: 0.0.0.0 + path_prefix: /data/loki + storage: + filesystem: + chunks_directory: /data/loki/chunks + rules_directory: /data/loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /data/loki/db + +pattern_ingester: + enabled: true + metric_aggregation: + loki_address: 0.0.0.0:3100 + +ruler: + alertmanager_url: http://10.87.2.22:9093 + +frontend: + encoding: protobuf + +query_scheduler: + max_outstanding_requests_per_tenant: 2048 + +limits_config: + ingestion_rate_mb: 1024 + ingestion_burst_size_mb: 1024 + max_query_lookback: 168h + max_query_series: 10000 + retention_period: 336h + +compactor: + working_directory: /data/loki/compactor + compaction_interval: 10m + retention_enabled: true + retention_delete_delay: 2h + retention_delete_worker_count: 150 + delete_request_store: filesystem +""" + +# ---------------- helpers ---------------- + +def log(msg: str, quiet: bool): + if not quiet: + print(msg) + +def run(cmd, check=True, quiet=False): + p = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if check and p.returncode != 0: + if quiet: + sys.stderr.write(p.stderr or p.stdout) + else: + print(p.stdout) + print(p.stderr, file=sys.stderr) + raise SystemExit(p.returncode) + return p.returncode, p.stdout.strip(), p.stderr.strip() + +def ensure_user(user=DEFAULT_USER, group=DEFAULT_GROUP, quiet=False): + code, _, _ = run(f"getent group {group}", check=False, quiet=quiet) + if code != 0: + log(f"Creating group: {group}", quiet) + run(f"groupadd --system {group}") + code, _, _ = run(f"id -u {user}", check=False, quiet=quiet) + if code != 0: + log(f"Creating user: {user}", quiet) + run( + "useradd --system --no-create-home --shell /usr/sbin/nologin " + f"--gid {group} {user}" + ) + +def ensure_dirs(paths, owner=(DEFAULT_USER, DEFAULT_GROUP), mode=0o750, quiet=False, force=False): + for p in paths: + if force and os.path.isdir(p): + shutil.rmtree(p) + if not os.path.isdir(p): + log(f"Creating dir: {p}", quiet) + os.makedirs(p, exist_ok=True) + os.chmod(p, mode) + run(f"chown -R {owner[0]}:{owner[1]} {p}") + +def write_config(path, content, mode=0o640, owner=(DEFAULT_USER, DEFAULT_GROUP), quiet=False, force=False): + if force or not os.path.exists(path): + base = os.path.dirname(path) + if base and not os.path.isdir(base): + os.makedirs(base, exist_ok=True) + log(f"Writing config: {path}", quiet) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + os.chmod(path, mode) + run(f"chown {owner[0]}:{owner[1]} {path}") + +def download(url, dest, quiet=False): + log(f"Downloading: {url}", quiet) + req = urllib.request.Request(url, headers={"User-Agent": "loki-installer"}) + with urllib.request.urlopen(req) as r, open(dest, "wb") as f: + shutil.copyfileobj(r, f) + +def get_installed_version(quiet=False): + code, out, _ = run(f"dpkg-query -W -f='${{Version}}\n' {PKG_NAME}", check=False, quiet=quiet) + return out.strip() if code == 0 else None + +def semver_tuple(v: str): + m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", v) + if not m: + return (0, 0, 0) + parts = [int(x) if x else 0 for x in m.groups()] + while len(parts) < 3: + parts.append(0) + return tuple(parts) + +def find_latest_loki_deb_asset(quiet=False): + req = urllib.request.Request(GITHUB_LATEST_API, headers={"User-Agent": "loki-installer", "Accept": "application/vnd.github+json"}) + with urllib.request.urlopen(req) as r: + data = json.loads(r.read().decode("utf-8")) + for a in data.get("assets", []): + name = a.get("name", "") + url = a.get("browser_download_url") + if name.startswith("loki_") and name.endswith("_amd64.deb"): + ver = name.split("_")[1] + return ver, url + return None, None + +def install_deb_from_url(url, quiet=False, force=False): + with tempfile.TemporaryDirectory() as td: + deb_path = os.path.join(td, os.path.basename(url) or "loki.deb") + download(url, deb_path, quiet=quiet) + log("Installing package…", quiet) + run("apt-get update", check=False, quiet=quiet) + run(f"DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" -y install {deb_path}") + log("Installed loki", quiet) + +def ensure_systemd(config_path=DEFAULT_CONFIG_PATH, user=DEFAULT_USER, group=DEFAULT_GROUP, quiet=False, force=False): + unit_path = "/etc/systemd/system/loki.service" if not os.path.isfile(SYSTEMD_UNIT) else None + if force and os.path.exists("/etc/systemd/system/loki.service"): + os.remove("/etc/systemd/system/loki.service") + if force and os.path.exists(SYSTEMD_OVERRIDE_PATH): + os.remove(SYSTEMD_OVERRIDE_PATH) + if unit_path: + unit = f""" +[Unit] +Description=Grafana Loki +After=network.target + +[Service] +Type=simple +ExecStart={BIN_PATH} -config.file={config_path} +User={user} +Group={group} +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +""" + with open(unit_path, "w", encoding="utf-8") as f: + f.write(unit.lstrip()) + else: + os.makedirs(SYSTEMD_OVERRIDE_DIR, exist_ok=True) + override = f""" +[Service] +User={user} +Group={group} +ExecStart= +ExecStart={BIN_PATH} -config.file={config_path} +""" + with open(SYSTEMD_OVERRIDE_PATH, "w", encoding="utf-8") as f: + f.write(override.lstrip()) + + run("systemctl daemon-reload") + run("systemctl enable --now loki") + +def main(): + p = argparse.ArgumentParser(description="Install/Update Grafana Loki (Debian 13)") + p.add_argument("--deb-url", default="latest", help="URL to loki amd64 .deb or 'latest'") + p.add_argument("--update", action="store_true", help="Silent update from GitHub") + p.add_argument("--force", action="store_true", help="Redo all steps, overwrite config and dirs") + p.add_argument("--user", default=DEFAULT_USER, help="Service user") + p.add_argument("--group", default=DEFAULT_GROUP, help="Service group") + p.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="Path to Loki config") + p.add_argument("--data-dir", default=DEFAULT_DATA_DIR, help="Loki data dir") + p.add_argument("--log-dir", default=DEFAULT_LOG_DIR, help="Loki log dir") + p.add_argument("--quiet", action="store_true", help="Less output") + args = p.parse_args() + + quiet = args.quiet or args.update + + if os.geteuid() != 0: + print("Run as root.", file=sys.stderr) + return 1 + + ensure_user(args.user, args.group, quiet=quiet) + + ensure_dirs([ + os.path.dirname(args.config) or DEFAULT_CONFIG_DIR, + args.data_dir, + os.path.join(args.data_dir, "chunks"), + os.path.join(args.data_dir, "rules"), + os.path.join(args.data_dir, "db"), + os.path.join(args.data_dir, "compactor"), + args.log_dir, + ], owner=(args.user, args.group), quiet=quiet, force=args.force) + + write_config(args.config, CUSTOM_CONFIG, owner=(args.user, args.group), quiet=quiet, force=args.force) + + installed = get_installed_version(quiet=quiet) + deb_url = args.deb_url + if deb_url == "latest": + try: + latest_ver, latest_url = find_latest_loki_deb_asset(quiet=quiet) + except (URLError, HTTPError): + return 0 + if not latest_ver or not latest_url: + return 0 + deb_url = latest_url + if args.update or args.force: + try: + latest_ver, latest_url = find_latest_loki_deb_asset(quiet=quiet) + except (URLError, HTTPError): + return 0 + if not latest_ver or not latest_url: + return 0 + if args.force or not installed or semver_tuple(installed) < semver_tuple(latest_ver): + try: + install_deb_from_url(latest_url, quiet=True, force=args.force) + except SystemExit: + return 1 + else: + if not installed: + install_deb_from_url(deb_url, quiet=quiet) + + ensure_systemd(config_path=args.config, user=args.user, group=args.group, quiet=quiet, force=args.force) + + run(f"chown -R {args.user}:{args.group} {os.path.dirname(args.config)} {args.data_dir} {args.log_dir}") + + log("OK.", quiet) + return 0 + +if __name__ == "__main__": + sys.exit(main())