Python UI to Monitor a Start9

Got a question about Start9 API’s. I coded a python script for monitoring the Start 9 vitals but I’m confused as to if I can call data from the Start9 API’s?.

# ---- CONFIGURE ----
SERVER_BASE_URL = "``http://192.168.1.100:8080``" # <-- change to your Start9 server IP:port
API_STATUS_ENDPOINT = "/api/status" # <-- change to the actual API path if different
POLL_INTERVAL_SECONDS = 30 REQUEST_TIMEOUT = 5 # -
# ---- CONFIGURE ----

Is it possible to pull the API routes? ie; server status, uptime, service list

You can use this one for reference (save as “vitals.py”):

#!/usr/bin/env python3
"""
Start9 Server Vitals
--------------------
Fetches server status, uptime, and service list from a Start9 (StartOS) server
via its JSON-RPC 2.0 API at POST /rpc/v1.

Usage:
    python vitals.py [--host HOST] [--json]

    The server password can be supplied via:
      - Environment variable:  START9_PASSWORD=yourpassword python vitals.py
      - Interactive prompt:    (default — typed securely, never echoed)

Options:
    --host HOST   Hostname or IP of the Start9 server
                  (default: adjective-noun.local)
    --json        Output raw JSON instead of pretty-printed table
    --no-verify   Skip SSL certificate verification (default: True, since
                  .local servers use self-signed certs)
"""

import argparse
import getpass
import json
import os
import sys
from datetime import timedelta

try:
    import requests
    from requests.packages.urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
    print("ERROR: 'requests' is not installed. Run:  pip install requests")
    sys.exit(1)


# ---------------------------------------------------------------------------
# RPC helpers
# ---------------------------------------------------------------------------

_rpc_id = 0


def _next_id() -> int:
    global _rpc_id
    _rpc_id += 1
    return _rpc_id


def rpc_call(session: requests.Session, base_url: str, method: str, params: dict) -> dict:
    """
    Send a single JSON-RPC 2.0 request.
    Returns the 'result' field on success, raises RuntimeError on RPC error.
    """
    payload = {
        "jsonrpc": "2.0",
        "id": _next_id(),
        "method": method,
        "params": params,
    }
    resp = session.post(f"{base_url}/rpc/v1", json=payload, verify=False, timeout=30)
    resp.raise_for_status()
    body = resp.json()
    if "error" in body:
        err = body["error"]
        raise RuntimeError(f"RPC error [{err.get('code')}]: {err.get('message')} — {err.get('data', {}).get('details', '')}")
    return body["result"]


# ---------------------------------------------------------------------------
# Data-fetching functions
# ---------------------------------------------------------------------------

def login(session: requests.Session, base_url: str, password: str) -> None:
    """Authenticate and store the session cookie."""
    rpc_call(session, base_url, "auth.login", {
        "password": password,
        "metadata": {"platforms": ["cli"]},
    })


def logout(session: requests.Session, base_url: str) -> None:
    """Cleanly end the session."""
    try:
        rpc_call(session, base_url, "auth.logout", {})
    except Exception:
        pass  # best-effort


def get_uptime(session: requests.Session, base_url: str) -> dict:
    """
    Returns server time info.
    Result shape: { "now": "<ISO8601>", "uptime": <seconds: float> }
    """
    return rpc_call(session, base_url, "server.time", {})


def get_metrics(session: requests.Session, base_url: str) -> dict:
    """
    Returns server hardware metrics (CPU, RAM, storage, etc.).
    """
    return rpc_call(session, base_url, "server.metrics", {})


def get_services(session: requests.Session, base_url: str) -> list:
    """
    Returns a list of installed services.
    Each item: { "id": str, "status": str, "version": str }
    """
    result = rpc_call(session, base_url, "package.list", {})
    # The API can return a list or a dict keyed by package id
    if isinstance(result, dict):
        services = []
        for pkg_id, pkg_data in result.items():
            entry = {"id": pkg_id}
            entry.update(pkg_data if isinstance(pkg_data, dict) else {})
            services.append(entry)
        return services
    return result if result else []


# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------

def format_uptime(seconds: float) -> str:
    td = timedelta(seconds=int(seconds))
    days = td.days
    hours, remainder = divmod(td.seconds, 3600)
    minutes, secs = divmod(remainder, 60)
    parts = []
    if days:
        parts.append(f"{days}d")
    if hours:
        parts.append(f"{hours}h")
    if minutes:
        parts.append(f"{minutes}m")
    parts.append(f"{secs}s")
    return " ".join(parts)


def _metric_value(metric_obj) -> str:
    """Extract a display value from a metric object like { value: X, unit: Y }."""
    if isinstance(metric_obj, dict):
        val = metric_obj.get("value", "")
        unit = metric_obj.get("unit", "")
        return f"{val} {unit}".strip() if unit else str(val)
    return str(metric_obj)


def print_vitals(host: str, time_data: dict, metrics: dict, services: list) -> None:
    uptime_secs = time_data.get("uptime", 0)
    server_time = time_data.get("now", "unknown")

    print()
    print("=" * 55)
    print(f"  Start9 Server Vitals — {host}")
    print("=" * 55)

    # --- Server status & uptime ---
    print(f"  Status  : Running ✓")
    print(f"  Uptime  : {format_uptime(uptime_secs)}")
    print(f"  Time    : {server_time}")

    # --- Metrics ---
    if metrics:
        print()
        print("  [ Hardware Metrics ]")
        # Flatten common metric paths the API returns
        def show(label: str, *keys):
            obj = metrics
            for k in keys:
                if isinstance(obj, dict):
                    obj = obj.get(k)
                else:
                    obj = None
                    break
            if obj is not None:
                print(f"  {label:<12}: {_metric_value(obj)}")

        # CPU — nested as metrics.cpu.percentage or metrics["cpu-load"]
        show("CPU Load",   "cpu", "percentage")
        show("CPU Load",   "cpu-load")
        # RAM
        show("RAM Used",   "memory", "percentage-used")
        show("RAM Used",   "ram",    "percentage-used")
        # Storage
        show("Disk Used",  "disk",   "used")
        show("Storage",    "storage","percentage-used")

        # If above keys didn't match, dump all top-level metric keys
        matched_any = any(
            isinstance(metrics.get(k), dict)
            for k in ("cpu", "memory", "disk", "ram", "storage", "cpu-load")
            if k in metrics
        )
        if not matched_any:
            for key, val in metrics.items():
                if isinstance(val, (dict, str, int, float)):
                    print(f"  {key:<12}: {_metric_value(val)}")

    # --- Services ---
    print()
    print("  [ Installed Services ]")
    if not services:
        print("  (none)")
    else:
        col_id  = max(len(s.get("id", "")) for s in services)
        col_id  = max(col_id, 4)
        col_ver = max(len(str(s.get("version", ""))) for s in services)
        col_ver = max(col_ver, 7)
        header  = f"  {'ID':<{col_id}}  {'VERSION':<{col_ver}}  STATUS"
        print(header)
        print("  " + "-" * (len(header) - 2))
        for svc in sorted(services, key=lambda s: s.get("id", "")):
            svc_id  = svc.get("id", "?")
            version = str(svc.get("version", ""))
            status  = svc.get("status", "unknown")
            # Add a simple visual indicator
            indicator = "●" if status == "installed" else "◌"
            print(f"  {svc_id:<{col_id}}  {version:<{col_ver}}  {indicator} {status}")

    print("=" * 55)
    print()


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Fetch vitals from a Start9 (StartOS) server.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument(
        "--host",
        default="adjective-noun.local",
        help="Hostname or IP of the Start9 server (default: adjective-noun.local)",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON",
    )
    parser.add_argument(
        "--no-verify",
        action="store_true",
        default=True,
        help="Skip SSL certificate verification (default: enabled for .local servers)",
    )
    args = parser.parse_args()

    base_url = f"https://{args.host}"

    # Get password
    password = os.environ.get("START9_PASSWORD")
    if not password:
        try:
            password = getpass.getpass(f"Password for {args.host}: ")
        except (KeyboardInterrupt, EOFError):
            print("\nAborted.")
            sys.exit(1)

    session = requests.Session()
    session.headers.update({"Content-Type": "application/json"})

    try:
        # Authenticate
        print(f"Connecting to {base_url} …", end="", flush=True)
        login(session, base_url, password)
        print(" OK")

        # Fetch data
        time_data = get_uptime(session, base_url)
        metrics   = get_metrics(session, base_url)
        services  = get_services(session, base_url)

        if args.json:
            output = {
                "server": {
                    "status": "running",
                    "uptime_seconds": time_data.get("uptime"),
                    "time": time_data.get("now"),
                },
                "metrics": metrics,
                "services": services,
            }
            print(json.dumps(output, indent=2))
        else:
            print_vitals(args.host, time_data, metrics, services)

    except requests.exceptions.ConnectionError as e:
        print(f"\nERROR: Could not connect to {base_url}")
        print(f"       Make sure the server is reachable and you're on the same network.")
        print(f"       Details: {e}")
        sys.exit(1)
    except RuntimeError as e:
        print(f"\nERROR: {e}")
        sys.exit(1)
    finally:
        logout(session, base_url)


if __name__ == "__main__":
    main()

Example Usage:

python vitals.py --host adjective-noun.local --json

For full usage help:

python vitals.py --help

How can I try to use this? Do I have to SSH into Start9 and then execute the python file?

This one just runs on another computer on the same local network.

Yep. Very helpful. I couldn’t get by the connection - darn password of all things. Sometimes I wonder about myself :zany_face: