Files
devplacepy/devplacepy/cli/nodes.py
T
retoorandClaude Sonnet 5.5 02ab747cd0
DevPlace CI / test (push) Failing after 44m45s
Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:23:43 +02:00

165 lines
6.3 KiB
Python

# retoor <retoor@molodetz.nl>
import sys
from devplacepy.cli._shared import _audit_cli
LIST_LIMIT = 200
SUSPEND_DEFAULT_MINUTES = 60
SUSPEND_MAX_MINUTES = 7 * 24 * 60
def _prepare():
from devplacepy.database import init_db
init_db()
def cmd_nodes_list(args):
from devplacepy.database import get_table
from devplacepy.database.nodes import effective_status
_prepare()
rows = list(get_table("node_devices").find(deleted_at=None, order_by=["-created_at"], _limit=LIST_LIMIT))
if not rows:
print("No devices")
return
for row in rows:
print(
f"{row['uid']} {effective_status(row):<10} {row.get('kind') or '-':<9} "
f"{row.get('platform') or '-':<14} user={row['user_uid']} last={row.get('last_seen_at') or '-'}"
)
def cmd_nodes_stats(args):
from devplacepy.database.nodes import list_flags, public_stats
_prepare()
stats = public_stats()
print(f"donors: {stats['donors']} requests: {stats['requests']} bytes: {stats['bytes']}")
print(f"open flags: {len(list_flags())}")
def cmd_nodes_flags(args):
from devplacepy.database.nodes import list_flags
_prepare()
flags = list_flags()
if not flags:
print("No open flags")
return
for flag in flags:
print(f"{flag['uid']} {flag.get('kind') or '-':<22} device={flag.get('device_uid') or '-'} {flag.get('detail') or ''}")
def cmd_nodes_suspend(args):
from devplacepy.database.nodes import suspend_device
_prepare()
minutes = max(1, min(args.minutes, SUSPEND_MAX_MINUTES))
if suspend_device(args.device_uid, args.reason, minutes):
print(f"Suspended {args.device_uid} for {minutes} minutes")
_audit_cli(
"cli.nodes.suspend",
f"CLI suspended device {args.device_uid}",
target_type="node_device",
target_uid=args.device_uid,
metadata={"minutes": minutes, "reason": args.reason},
)
return
print(f"No active device {args.device_uid}")
sys.exit(1)
def cmd_nodes_unsuspend(args):
from devplacepy.database.nodes import unsuspend_device
_prepare()
if unsuspend_device(args.device_uid):
print(f"Lifted the suspension of {args.device_uid}")
_audit_cli(
"cli.nodes.unsuspend",
f"CLI lifted the suspension of device {args.device_uid}",
target_type="node_device",
target_uid=args.device_uid,
)
return
print(f"{args.device_uid} is not suspended")
sys.exit(1)
def cmd_nodes_prune(args):
from devplacepy.services.nodes.ledger import UsageLedger
_prepare()
counts = UsageLedger().prune(dry_run=args.dry_run)
label = "would remove" if args.dry_run else "removed"
for name, count in counts.items():
print(f"{label} {count} {name}")
def cmd_nodes_clear(args):
from devplacepy.database.nodes import clear_ephemeral
_prepare()
counts = clear_ephemeral(dry_run=not args.force)
label = "removed" if args.force else "would remove (pass --force)"
for name, count in counts.items():
print(f"{label} {count} {name}")
if args.force:
_audit_cli("cli.nodes.clear","CLI cleared the node job queue and event log", metadata=counts)
def cmd_nodes_geo_sync(args):
import asyncio
from devplacepy.services.nodes.geo import GeoError, geo_sync
try:
summary = asyncio.run(geo_sync(dry_run=args.dry_run, month=args.month))
except GeoError as exc:
print(f"geo-sync refused: {exc}")
sys.exit(1)
verb = "installed" if summary["installed"] else "verified, not installed (dry run)"
print(f"{verb}: month {summary['month']}, {summary['asn_rows']} ASN ranges, {summary['country_rows']} country ranges")
if summary["installed"]:
_audit_cli("cli.nodes.geo_sync", f"CLI installed the node address database for {summary['month']}", metadata={"month": summary["month"]})
def cmd_nodes_geo_status(args):
from devplacepy.services.nodes.geo import shared_geo
status = shared_geo().status()
if not status["available"]:
print(f"not installed at {status['path']}; every donor is classed unknown")
return
print(f"month {status.get('month')}, {status.get('asn_rows')} ASN ranges, {status.get('country_rows')} country ranges, loaded {status.get('loaded_at')}")
print(status["attribution"])
def register_nodes(subparsers):
nodes = subparsers.add_parser("nodes", help="Community donor node network")
sub = nodes.add_subparsers(title="action", dest="action")
sub.add_parser("list", help="List devices").set_defaults(func=cmd_nodes_list)
sub.add_parser("stats", help="Aggregate donation totals and open flags").set_defaults(func=cmd_nodes_stats)
sub.add_parser("flags", help="List open flags that need a human").set_defaults(func=cmd_nodes_flags)
suspend = sub.add_parser("suspend", help="Suspend one device for a while")
suspend.add_argument("device_uid")
suspend.add_argument("--minutes", type=int, default=SUSPEND_DEFAULT_MINUTES)
suspend.add_argument("--reason", default="suspended from the command line")
suspend.set_defaults(func=cmd_nodes_suspend)
unsuspend = sub.add_parser("unsuspend", help="Lift the suspension of a device")
unsuspend.add_argument("device_uid")
unsuspend.set_defaults(func=cmd_nodes_unsuspend)
prune = sub.add_parser("prune", help="Prune usage buckets, events and finished requests past their retention")
prune.add_argument("--dry-run", action="store_true", help="report what would be removed")
prune.set_defaults(func=cmd_nodes_prune)
clear = sub.add_parser("clear", help="Empty the job queue and the event log (never usage or donor totals)")
clear.add_argument("--force", action="store_true", help="actually delete; without it only counts are shown")
clear.set_defaults(func=cmd_nodes_clear)
geo_sync = sub.add_parser("geo-sync", help="Download the DB-IP address database used to class donor addresses")
geo_sync.add_argument("--dry-run", action="store_true", help="download and verify, do not install")
geo_sync.add_argument("--month", default="", help="YYYY-MM, default the newest published")
geo_sync.set_defaults(func=cmd_nodes_geo_sync)
sub.add_parser("geo-status", help="Show the installed address database").set_defaults(func=cmd_nodes_geo_status)