aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/daemon.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/daemon.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/daemon.py61
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()