diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/daemon.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/daemon.py | 61 |
1 files changed, 60 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/daemon.py b/packages/meshbay-hub/src/meshbay_hub/daemon.py index ff66fee..4af26f5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/daemon.py +++ b/packages/meshbay-hub/src/meshbay_hub/daemon.py @@ -1,6 +1,7 @@ -"""Entry point for the meshbay-hub systemd service.""" +"""Entry point for the meshbay-hub systemd service, and its few CLI chores.""" import argparse +import asyncio import logging import sys from pathlib import Path @@ -15,6 +16,25 @@ def main() -> None: parser.add_argument("--config", type=Path, default=None) parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"]) + # Optional on purpose: the systemd unit runs `meshbay-hub --config …` with no + # subcommand and must go on starting the server. + sub = parser.add_subparsers(dest="command") + + prune = sub.add_parser( + "prune-groups", + help="delete groups no node ever hosted (meant for cron)") + prune.add_argument("--days", type=int, default=None, + help="grace period since creation (default 7)") + prune.add_argument("--dry-run", action="store_true", + help="list what would go, delete nothing") + # Accepted after the subcommand too. Everyone writes the cron line as + # `prune-groups --config …`, and argparse only takes an option before the + # subcommand unless the subparser declares it as well. SUPPRESS so that + # leaving it out here does not overwrite a value given before it. + prune.add_argument("--config", type=Path, default=argparse.SUPPRESS) + prune.add_argument("--log-level", default=argparse.SUPPRESS, + choices=["DEBUG", "INFO", "WARNING", "ERROR"]) + args = parser.parse_args() logging.basicConfig( @@ -24,6 +44,9 @@ def main() -> None: cfg = load_config(args.config) + if args.command == "prune-groups": + sys.exit(asyncio.run(_prune_groups(cfg, args.days, args.dry_run))) + uvicorn.run( "meshbay_hub.app:create_app", factory=True, @@ -34,5 +57,41 @@ def main() -> None: ) +async def _prune_groups(cfg, days: int | None, dry_run: bool) -> int: + """Collect groups that were created and never given a node. + + A group with no host has no files, no key and nothing to connect to, and is + invisible to everyone but its owner — so it is litter rather than data. The + grace period is counted from creation and `hosted_at` is never cleared, so a + node being offline today cannot make a live group look abandoned. + + Deliberately a command rather than a loop inside the server: deleting other + people's groups on a timer nobody asked for is the kind of thing an operator + should schedule knowingly, and `--dry-run` lets them see the list first. + """ + from meshbay_hub.db.engine import close_db, get_session_factory, init_db + from meshbay_hub.tasks.cleanup import UNHOSTED_GRACE_DAYS, prune_unhosted_groups + + grace = UNHOSTED_GRACE_DAYS if days is None else days + # init_db also runs create_all. That is what the server does at startup, so + # it adds no risk here — and it creates missing *tables* only, never a + # missing column, which is why deploys run alembic separately. + await init_db(cfg.db.url) + try: + async with get_session_factory()() as db: + gone = await prune_unhosted_groups(db, grace_days=grace, dry_run=dry_run) + finally: + await close_db() + + verb = "would delete" if dry_run else "deleted" + if not gone: + print(f"No group older than {grace} day(s) is still unhosted.") + return 0 + print(f"{verb} {len(gone)} group(s) unhosted for more than {grace} day(s):") + for gid, name in gone: + print(f" {gid} {name}") + return 0 + + if __name__ == "__main__": main() |