124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
"""VDSok provisioner — idempotent order + deploy for the KeyFarm VPS plane.
|
|
|
|
Reads the API key from tmp/vdsok_token.txt (never from argv/env literals).
|
|
Flow: /me -> /servers (reuse same-name box if exists) -> /account balance ->
|
|
POST /servers (Idempotency-Key, cheapest tariff by default) -> poll order ->
|
|
save creds to tmp/vdsok_server_<name>.txt -> optional deploy via kf_install.sh.
|
|
|
|
usage: python tmp/vdsok_provision.py [--name keyfarm-nl] [--tariff 16]
|
|
[--os ubuntu-24-04] [--months 1] [--shard 1] [--shards 4] [--deploy]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
import uuid
|
|
|
|
TOK_FILE = os.path.expanduser(r"~\tmp\vdsok_token.txt")
|
|
BASE = "https://vdsok.guru/api/v1"
|
|
|
|
|
|
def call(path, method="GET", body=None, idem=None, tok=None):
|
|
h = {"Authorization": "***" + tok, "Accept": "application/json"}
|
|
if idem:
|
|
h["Idempotency-Key"] = idem
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
if data:
|
|
h["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(BASE + path, method=method, headers=h, data=data)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
return r.status, json.loads(r.read().decode() or "null")
|
|
except urllib.error.HTTPError as e:
|
|
raw = e.read().decode(errors="replace")
|
|
try:
|
|
raw = json.loads(raw)
|
|
except Exception:
|
|
pass
|
|
return e.code, raw
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--name", default="keyfarm-hunter")
|
|
ap.add_argument("--tariff", type=int, default=0, help="0 = cheapest available")
|
|
ap.add_argument("--os", dest="os_slug", default="ubuntu-24-04")
|
|
ap.add_argument("--months", type=int, default=1)
|
|
ap.add_argument("--shard", type=int, default=0)
|
|
ap.add_argument("--shards", type=int, default=4)
|
|
ap.add_argument("--deploy", action="store_true")
|
|
a = ap.parse_args()
|
|
tok = open(TOK_FILE, encoding="utf-8").read().strip()
|
|
|
|
st, me = call("/me", tok=tok)
|
|
if st != 200:
|
|
sys.exit(f"/me failed: {st} {me}")
|
|
print(f"key ok: account_id={me.get('account_id')} sandbox={me.get('is_test')}")
|
|
|
|
st, servers = call("/servers?limit=100", tok=tok)
|
|
for s in (servers.get("data") or []):
|
|
if s.get("name") == a.name:
|
|
print(f"REUSE existing server id={s['id']} ip={s.get('primary_ip')} "
|
|
f"status={s.get('status')}")
|
|
save(a.name, s, None)
|
|
return
|
|
|
|
st, acct = call("/account", tok=tok)
|
|
bal = (acct.get("balance") or {}).get("amount") if isinstance(acct.get("balance"), dict) else acct.get("balance")
|
|
print(f"balance: {bal} {acct.get('currency')}")
|
|
|
|
st, tars = call("/catalog/tariffs?limit=100", tok=tok)
|
|
avail = [t for t in tars.get("data", []) if t.get("available")]
|
|
if not avail:
|
|
sys.exit("no available tariffs")
|
|
t = next((x for x in avail if x["id"] == a.tariff), None) if a.tariff else None
|
|
t = t or min(avail, key=lambda x: float(x["price_monthly"]))
|
|
price = float(t["price_monthly"]) * a.months
|
|
print(f"tariff: id={t['id']} {t['name']} {t['price_monthly']}/mo x{a.months} "
|
|
f"= {price:.2f} {t['currency']} | {t['vcpu']}vCPU "
|
|
f"{int(t['ram_mb'])//1024}GB {int(t['disk_mb'])//1024}GB {t['location']}")
|
|
if bal is not None and float(bal) < price:
|
|
sys.exit(f"INSUFFICIENT BALANCE {bal} < {price:.2f} — top up first")
|
|
|
|
st, res = call("/servers", "POST",
|
|
{"tariff_id": t["id"], "os": a.os_slug, "name": a.name,
|
|
"months": a.months},
|
|
idem=str(uuid.uuid4()), tok=tok)
|
|
if st not in (200, 201, 202):
|
|
sys.exit(f"order failed: {st} {res}")
|
|
srv = res.get("server", {})
|
|
pw = res.get("root_password")
|
|
print(f"ordered: id={srv.get('id')} charged={res.get('charged')} "
|
|
f"balance_after={res.get('balance_after')}")
|
|
# poll for IP
|
|
for i in range(24):
|
|
st, s2 = call(f"/servers/{srv.get('id')}", tok=tok)
|
|
ip = (s2 or {}).get("primary_ip")
|
|
if ip:
|
|
srv = s2
|
|
break
|
|
time.sleep(10)
|
|
save(a.name, srv, pw)
|
|
if a.deploy and srv.get("primary_ip") and pw:
|
|
print("deploy: scp tmp/kf_install.sh + tree, then "
|
|
f"bash install.sh {a.shard} {a.shards} (see handoff)")
|
|
|
|
|
|
def save(name, srv, pw):
|
|
out = os.path.expanduser(rf"~\tmp\vdsok_server_{name}.txt")
|
|
rec = {"server_id": srv.get("id"), "name": name,
|
|
"primary_ip": srv.get("primary_ip"), "status": srv.get("status"),
|
|
"tariff": (srv.get("tariff") or {}).get("name"),
|
|
"root_password": pw or "(shown once at order; see earlier record)"}
|
|
with open(out, "w", encoding="utf-8") as f:
|
|
json.dump(rec, f, indent=1)
|
|
os.chmod(out, 0o600)
|
|
print(f"saved: {out} ip={rec['primary_ip']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|