DevPlace CI / test (push) Failing after 44m45s
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>
165 lines
6.3 KiB
Python
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)
|