Add systemd task shortcuts and simplify Grafana image setup
de739e7 parent: 9864974 modified
README.md +14 -2 | @@ -127,8 +127,8 @@ systemctl list-timers atop-modal-export.timer | ||
| 127 | 127 | To test it without waiting until midnight, run: |
| 128 | 128 | |
| 129 | 129 | ```sh |
| 130 | -run0 systemctl start atop-modal-export.service | |
| 131 | -journalctl -u atop-modal-export.service --no-pager -n 50 | |
| 130 | +just fire | |
| 131 | +just logs | |
| 132 | 132 | ``` |
| 133 | 133 | |
| 134 | 134 | If an export fails, `atop-modal-export-failure.service` writes a high-priority |
| @@ -139,6 +139,18 @@ Inspect failures with: | ||
| 139 | 139 | journalctl -u atop-modal-export.service --no-pager -n 100 |
| 140 | 140 | ``` |
| 141 | 141 | |
| 142 | +#### Task shortcuts | |
| 143 | + | |
| 144 | +The repository `justfile` keeps the routine commands in one place: | |
| 145 | + | |
| 146 | +```sh | |
| 147 | +just install # install/update units and enable the timer | |
| 148 | +just status # timer state and next run | |
| 149 | +just fire # run yesterday's scheduled export now | |
| 150 | +just test-today # manually export today's available samples | |
| 151 | +just logs # recent exporter output | |
| 152 | +``` | |
| 153 | + | |
| 142 | 154 | ### Grafana dashboard |
| 143 | 155 | |
| 144 | 156 | `modal_grafana.py` adds a browser-facing Grafana service with a provisioned |
| @@ -127,8 +127,8 @@ systemctl list-timers atop-modal-export.timer | |||
| 127 | To test it without waiting until midnight, run: | 127 | To test it without waiting until midnight, run: |
| 128 | 128 | ||
| 129 | ```sh | 129 | ```sh |
| 130 | -run0 systemctl start atop-modal-export.service | 130 | +just fire |
| 131 | -journalctl -u atop-modal-export.service --no-pager -n 50 | 131 | +just logs |
| 132 | ``` | 132 | ``` |
| 133 | 133 | ||
| 134 | If an export fails, `atop-modal-export-failure.service` writes a high-priority | 134 | If an export fails, `atop-modal-export-failure.service` writes a high-priority |
| @@ -139,6 +139,18 @@ Inspect failures with: | |||
| 139 | journalctl -u atop-modal-export.service --no-pager -n 100 | 139 | journalctl -u atop-modal-export.service --no-pager -n 100 |
| 140 | ``` | 140 | ``` |
| 141 | 141 | ||
| 142 | +#### Task shortcuts | ||
| 143 | + | ||
| 144 | +The repository `justfile` keeps the routine commands in one place: | ||
| 145 | + | ||
| 146 | +```sh | ||
| 147 | +just install # install/update units and enable the timer | ||
| 148 | +just status # timer state and next run | ||
| 149 | +just fire # run yesterday's scheduled export now | ||
| 150 | +just test-today # manually export today's available samples | ||
| 151 | +just logs # recent exporter output | ||
| 152 | +``` | ||
| 153 | + | ||
| 142 | ### Grafana dashboard | 154 | ### Grafana dashboard |
| 143 | 155 | ||
| 144 | `modal_grafana.py` adds a browser-facing Grafana service with a provisioned | 156 | `modal_grafana.py` adds a browser-facing Grafana service with a provisioned |
added
justfile +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] | |
| 2 | + | |
| 3 | +default: | |
| 4 | + @just --list | |
| 5 | + | |
| 6 | +# Install or update all systemd units and enable the daily export timer. | |
| 7 | +install: | |
| 8 | + run0 python3 systemd/install.py --enable | |
| 9 | + | |
| 10 | +# Show whether the systemd timer is enabled and when it will next run. | |
| 11 | +status: | |
| 12 | + run0 python3 systemd/install.py --status | |
| 13 | + | |
| 14 | +# Fire the same service used by the daily timer (exports yesterday's log). | |
| 15 | +fire: | |
| 16 | + run0 python3 systemd/install.py --fire | |
| 17 | + | |
| 18 | +# Export today's currently available atop history without waiting for midnight. | |
| 19 | +test-today: | |
| 20 | + modal run modal_atop_analyze.py --date "$$(date +%Y%m%d)" --initialize | |
| 21 | + | |
| 22 | +# Inspect the most recent scheduled-export output. | |
| 23 | +logs: | |
| 24 | + journalctl -u atop-modal-export.service --no-pager -n 100 | |
| 25 | + | |
| 26 | +# Show the Modal app's local-entrypoint arguments. | |
| 27 | +help: | |
| 28 | + modal run modal_atop_analyze.py --help | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] | ||
| 2 | + | ||
| 3 | +default: | ||
| 4 | + @just --list | ||
| 5 | + | ||
| 6 | +# Install or update all systemd units and enable the daily export timer. | ||
| 7 | +install: | ||
| 8 | + run0 python3 systemd/install.py --enable | ||
| 9 | + | ||
| 10 | +# Show whether the systemd timer is enabled and when it will next run. | ||
| 11 | +status: | ||
| 12 | + run0 python3 systemd/install.py --status | ||
| 13 | + | ||
| 14 | +# Fire the same service used by the daily timer (exports yesterday's log). | ||
| 15 | +fire: | ||
| 16 | + run0 python3 systemd/install.py --fire | ||
| 17 | + | ||
| 18 | +# Export today's currently available atop history without waiting for midnight. | ||
| 19 | +test-today: | ||
| 20 | + modal run modal_atop_analyze.py --date "$$(date +%Y%m%d)" --initialize | ||
| 21 | + | ||
| 22 | +# Inspect the most recent scheduled-export output. | ||
| 23 | +logs: | ||
| 24 | + journalctl -u atop-modal-export.service --no-pager -n 100 | ||
| 25 | + | ||
| 26 | +# Show the Modal app's local-entrypoint arguments. | ||
| 27 | +help: | ||
| 28 | + modal run modal_atop_analyze.py --help | ||
modified
modal_grafana.py +1 -7 | @@ -25,13 +25,7 @@ grafana_secret = modal.Secret.from_name( | ||
| 25 | 25 | ], |
| 26 | 26 | ) |
| 27 | 27 | image = ( |
| 28 | - modal.Image.from_registry( | |
| 29 | - "grafana/grafana:11.6.0", | |
| 30 | - add_python="3.12", | |
| 31 | - # Grafana's registry image runs as a non-root user. Modal injects and | |
| 32 | - # probes its Python runtime while preparing the Function image. | |
| 33 | - setup_dockerfile_commands=["USER root"], | |
| 34 | - ) | |
| 28 | + modal.Image.from_registry("grafana/grafana:11.6.0", add_python="3.12") | |
| 35 | 29 | .run_commands("grafana cli plugins install grafana-clickhouse-datasource") |
| 36 | 30 | .add_local_dir("grafana/provisioning", "/etc/grafana/provisioning") |
| 37 | 31 | ) |
| @@ -25,13 +25,7 @@ grafana_secret = modal.Secret.from_name( | |||
| 25 | ], | 25 | ], |
| 26 | ) | 26 | ) |
| 27 | image = ( | 27 | image = ( |
| 28 | - modal.Image.from_registry( | 28 | + modal.Image.from_registry("grafana/grafana:11.6.0", add_python="3.12") |
| 29 | - "grafana/grafana:11.6.0", | ||
| 30 | - add_python="3.12", | ||
| 31 | - # Grafana's registry image runs as a non-root user. Modal injects and | ||
| 32 | - # probes its Python runtime while preparing the Function image. | ||
| 33 | - setup_dockerfile_commands=["USER root"], | ||
| 34 | - ) | ||
| 35 | .run_commands("grafana cli plugins install grafana-clickhouse-datasource") | 29 | .run_commands("grafana cli plugins install grafana-clickhouse-datasource") |
| 36 | .add_local_dir("grafana/provisioning", "/etc/grafana/provisioning") | 30 | .add_local_dir("grafana/provisioning", "/etc/grafana/provisioning") |
| 37 | ) | 31 | ) |
modified
systemd/install.py +21 -10 | @@ -20,26 +20,37 @@ SYSTEMD_DIR = Path("/etc/systemd/system") | ||
| 20 | 20 | |
| 21 | 21 | def main() -> int: |
| 22 | 22 | parser = argparse.ArgumentParser(description="Install atop Modal-export systemd units.") |
| 23 | + parser.add_argument("--install", action="store_true", help="copy unit files and reload systemd") | |
| 23 | 24 | parser.add_argument("--enable", action="store_true", help="enable and start the timer") |
| 25 | + parser.add_argument("--fire", action="store_true", help="start the scheduled export now") | |
| 26 | + parser.add_argument("--status", action="store_true", help="show timer status and schedule") | |
| 24 | 27 | args = parser.parse_args() |
| 25 | 28 | |
| 26 | 29 | if os.geteuid() != 0: |
| 27 | 30 | parser.error("run this installer once with: run0 python3 systemd/install.py --enable") |
| 28 | 31 | |
| 29 | - source_dir = Path(__file__).resolve().parent | |
| 30 | - for name in UNITS: | |
| 31 | - source = source_dir / name | |
| 32 | - if not source.is_file(): | |
| 33 | - raise FileNotFoundError(f"missing unit file: {source}") | |
| 34 | - destination = SYSTEMD_DIR / name | |
| 35 | - shutil.copyfile(source, destination) | |
| 36 | - destination.chmod(0o644) | |
| 37 | - | |
| 38 | - subprocess.run(["systemctl", "daemon-reload"], check=True) | |
| 32 | + if not (args.install or args.enable or args.fire or args.status): | |
| 33 | + parser.error("choose at least one task") | |
| 34 | + | |
| 35 | + if args.install or args.enable: | |
| 36 | + source_dir = Path(__file__).resolve().parent | |
| 37 | + for name in UNITS: | |
| 38 | + source = source_dir / name | |
| 39 | + if not source.is_file(): | |
| 40 | + raise FileNotFoundError(f"missing unit file: {source}") | |
| 41 | + destination = SYSTEMD_DIR / name | |
| 42 | + shutil.copyfile(source, destination) | |
| 43 | + destination.chmod(0o644) | |
| 44 | + subprocess.run(["systemctl", "daemon-reload"], check=True) | |
| 39 | 45 | if args.enable: |
| 40 | 46 | subprocess.run( |
| 41 | 47 | ["systemctl", "enable", "--now", "atop-modal-export.timer"], check=True |
| 42 | 48 | ) |
| 49 | + if args.fire: | |
| 50 | + subprocess.run(["systemctl", "start", "atop-modal-export.service"], check=True) | |
| 51 | + if args.status: | |
| 52 | + subprocess.run(["systemctl", "status", "atop-modal-export.timer", "--no-pager"], check=True) | |
| 53 | + subprocess.run(["systemctl", "list-timers", "atop-modal-export.timer", "--no-pager"], check=True) | |
| 43 | 54 | return 0 |
| 44 | 55 | |
| 45 | 56 | |
| @@ -20,26 +20,37 @@ SYSTEMD_DIR = Path("/etc/systemd/system") | |||
| 20 | 20 | ||
| 21 | def main() -> int: | 21 | def main() -> int: |
| 22 | parser = argparse.ArgumentParser(description="Install atop Modal-export systemd units.") | 22 | parser = argparse.ArgumentParser(description="Install atop Modal-export systemd units.") |
| 23 | + parser.add_argument("--install", action="store_true", help="copy unit files and reload systemd") | ||
| 23 | parser.add_argument("--enable", action="store_true", help="enable and start the timer") | 24 | parser.add_argument("--enable", action="store_true", help="enable and start the timer") |
| 25 | + parser.add_argument("--fire", action="store_true", help="start the scheduled export now") | ||
| 26 | + parser.add_argument("--status", action="store_true", help="show timer status and schedule") | ||
| 24 | args = parser.parse_args() | 27 | args = parser.parse_args() |
| 25 | 28 | ||
| 26 | if os.geteuid() != 0: | 29 | if os.geteuid() != 0: |
| 27 | parser.error("run this installer once with: run0 python3 systemd/install.py --enable") | 30 | parser.error("run this installer once with: run0 python3 systemd/install.py --enable") |
| 28 | 31 | ||
| 29 | - source_dir = Path(__file__).resolve().parent | 32 | + if not (args.install or args.enable or args.fire or args.status): |
| 30 | - for name in UNITS: | 33 | + parser.error("choose at least one task") |
| 31 | - source = source_dir / name | 34 | + |
| 32 | - if not source.is_file(): | 35 | + if args.install or args.enable: |
| 33 | - raise FileNotFoundError(f"missing unit file: {source}") | 36 | + source_dir = Path(__file__).resolve().parent |
| 34 | - destination = SYSTEMD_DIR / name | 37 | + for name in UNITS: |
| 35 | - shutil.copyfile(source, destination) | 38 | + source = source_dir / name |
| 36 | - destination.chmod(0o644) | 39 | + if not source.is_file(): |
| 37 | - | 40 | + raise FileNotFoundError(f"missing unit file: {source}") |
| 38 | - subprocess.run(["systemctl", "daemon-reload"], check=True) | 41 | + destination = SYSTEMD_DIR / name |
| 42 | + shutil.copyfile(source, destination) | ||
| 43 | + destination.chmod(0o644) | ||
| 44 | + subprocess.run(["systemctl", "daemon-reload"], check=True) | ||
| 39 | if args.enable: | 45 | if args.enable: |
| 40 | subprocess.run( | 46 | subprocess.run( |
| 41 | ["systemctl", "enable", "--now", "atop-modal-export.timer"], check=True | 47 | ["systemctl", "enable", "--now", "atop-modal-export.timer"], check=True |
| 42 | ) | 48 | ) |
| 49 | + if args.fire: | ||
| 50 | + subprocess.run(["systemctl", "start", "atop-modal-export.service"], check=True) | ||
| 51 | + if args.status: | ||
| 52 | + subprocess.run(["systemctl", "status", "atop-modal-export.timer", "--no-pager"], check=True) | ||
| 53 | + subprocess.run(["systemctl", "list-timers", "atop-modal-export.timer", "--no-pager"], check=True) | ||
| 43 | return 0 | 54 | return 0 |
| 44 | 55 | ||
| 45 | 56 | ||