mirror of
https://github.com/hanzoskill/komodo.git
synced 2026-10-30 21:03:23 +00:00
521 lines
15 KiB
Python
521 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Komodo CLI - interact with Komodo Core API.
|
|
|
|
Environment variables:
|
|
KOMODO_ADDRESS - Komodo Core URL (e.g., https://komodo.example.com)
|
|
KOMODO_API_KEY - API key (starts with K-)
|
|
KOMODO_API_SECRET - API secret (starts with S-)
|
|
|
|
Usage:
|
|
python komodo.py <command> [args...]
|
|
|
|
Commands:
|
|
# Read operations
|
|
servers - List all servers with status
|
|
deployments - List all deployments
|
|
stacks - List all stacks
|
|
builds - List all builds
|
|
procedures - List all procedures
|
|
repos - List all repos
|
|
|
|
# Server details
|
|
server <name> - Get server details and stats
|
|
server-stats <name> - Get server CPU/memory/disk stats
|
|
|
|
# Deployment operations
|
|
deployment <name> - Get deployment details
|
|
deploy <name> - Deploy/redeploy a deployment
|
|
start <name> - Start a deployment
|
|
stop <name> - Stop a deployment
|
|
restart <name> - Restart a deployment
|
|
logs <name> [lines] - Get container logs (default 100 lines)
|
|
|
|
# Stack operations
|
|
stack <name> - Get stack details
|
|
deploy-stack <name> - Deploy a stack
|
|
start-stack <name> - Start a stack
|
|
stop-stack <name> - Stop a stack
|
|
restart-stack <name> - Restart a stack
|
|
|
|
# Build operations
|
|
build <name> - Get build details
|
|
run-build <name> - Run a build
|
|
|
|
# Procedure operations
|
|
procedure <name> - Get procedure details
|
|
run-procedure <name> - Run a procedure
|
|
|
|
# Create operations
|
|
create-stack <name> <server> <compose_file> [env_file]
|
|
- Create a new stack from compose file
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import Any
|
|
|
|
KOMODO_ADDRESS = os.environ.get("KOMODO_ADDRESS", "").rstrip("/")
|
|
KOMODO_API_KEY = os.environ.get("KOMODO_API_KEY", "")
|
|
KOMODO_API_SECRET = os.environ.get("KOMODO_API_SECRET", "")
|
|
|
|
|
|
def api_call(endpoint: str, payload: dict | None = None, method: str = "POST") -> Any:
|
|
"""Make an API call to Komodo Core."""
|
|
if not KOMODO_ADDRESS:
|
|
print("Error: KOMODO_ADDRESS not set", file=sys.stderr)
|
|
sys.exit(1)
|
|
if not KOMODO_API_KEY or not KOMODO_API_SECRET:
|
|
print("Error: KOMODO_API_KEY or KOMODO_API_SECRET not set", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
url = f"{KOMODO_ADDRESS}/{endpoint}"
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": KOMODO_API_KEY,
|
|
"X-Api-Secret": KOMODO_API_SECRET,
|
|
}
|
|
|
|
data = json.dumps(payload or {}).encode("utf-8")
|
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
return json.loads(response.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as e:
|
|
error_body = e.read().decode("utf-8")
|
|
print(f"HTTP Error {e.code}: {error_body}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except urllib.error.URLError as e:
|
|
print(f"URL Error: {e.reason}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def read(request_type: str, payload: dict | None = None) -> Any:
|
|
"""Read operation."""
|
|
return api_call(f"read/{request_type}", payload)
|
|
|
|
|
|
def execute(request_type: str, payload: dict | None = None) -> Any:
|
|
"""Execute operation."""
|
|
return api_call(f"execute/{request_type}", payload)
|
|
|
|
|
|
def format_state(state: str) -> str:
|
|
"""Format state with emoji."""
|
|
states = {
|
|
"Ok": "🟢",
|
|
"Running": "🟢",
|
|
"Stopped": "🔴",
|
|
"NotDeployed": "⚪",
|
|
"Unknown": "❓",
|
|
"Unhealthy": "🟡",
|
|
"Restarting": "🔄",
|
|
"Building": "🔨",
|
|
"Pending": "⏳",
|
|
}
|
|
emoji = states.get(state, "❓")
|
|
return f"{emoji} {state}"
|
|
|
|
|
|
def cmd_servers():
|
|
"""List all servers."""
|
|
servers = read("ListServers")
|
|
if not servers:
|
|
print("No servers found.")
|
|
return
|
|
|
|
print("=== Servers ===\n")
|
|
for s in servers:
|
|
info = s.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
version = info.get("version", "?")
|
|
print(f"{state} {s['name']}")
|
|
print(f" Version: {version}")
|
|
print(f" Address: {info.get('address', 'N/A')}")
|
|
print()
|
|
|
|
|
|
def cmd_server(name: str):
|
|
"""Get server details."""
|
|
servers = read("ListServers")
|
|
server = next((s for s in servers if s["name"].lower() == name.lower()), None)
|
|
if not server:
|
|
print(f"Server '{name}' not found.")
|
|
return
|
|
|
|
info = server.get("info", {})
|
|
print(f"=== Server: {server['name']} ===\n")
|
|
print(f"ID: {server['id']}")
|
|
print(f"State: {format_state(info.get('state', 'Unknown'))}")
|
|
print(f"Version: {info.get('version', 'N/A')}")
|
|
print(f"Address: {info.get('address', 'N/A')}")
|
|
print(f"External: {info.get('external_address', 'N/A')}")
|
|
print(f"Region: {info.get('region', 'N/A')}")
|
|
|
|
|
|
def cmd_server_stats(name: str):
|
|
"""Get server stats."""
|
|
stats = read("GetSystemStats", {"server": name})
|
|
if not stats:
|
|
print(f"No stats for server '{name}'.")
|
|
return
|
|
|
|
print(f"=== Server Stats: {name} ===\n")
|
|
|
|
# CPU
|
|
cpu = stats.get("cpu", {})
|
|
cpu_usage = cpu.get("usage", 0)
|
|
print(f"CPU: {cpu_usage:.1f}%")
|
|
|
|
# Memory
|
|
mem = stats.get("memory", {})
|
|
mem_used = mem.get("used_gb", 0)
|
|
mem_total = mem.get("total_gb", 0)
|
|
mem_pct = (mem_used / mem_total * 100) if mem_total > 0 else 0
|
|
print(f"Memory: {mem_used:.1f} / {mem_total:.1f} GB ({mem_pct:.1f}%)")
|
|
|
|
# Disks
|
|
disks = stats.get("disks", [])
|
|
for disk in disks:
|
|
mount = disk.get("mount", "?")
|
|
used = disk.get("used_gb", 0)
|
|
total = disk.get("total_gb", 0)
|
|
pct = (used / total * 100) if total > 0 else 0
|
|
print(f"Disk {mount}: {used:.1f} / {total:.1f} GB ({pct:.1f}%)")
|
|
|
|
|
|
def cmd_deployments():
|
|
"""List all deployments."""
|
|
deployments = read("ListDeployments")
|
|
if not deployments:
|
|
print("No deployments found.")
|
|
return
|
|
|
|
print("=== Deployments ===\n")
|
|
for d in deployments:
|
|
info = d.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
image = info.get("image", "N/A")
|
|
server = info.get("server_name", "N/A")
|
|
print(f"{state} {d['name']}")
|
|
print(f" Image: {image}")
|
|
print(f" Server: {server}")
|
|
print()
|
|
|
|
|
|
def cmd_deployment(name: str):
|
|
"""Get deployment details."""
|
|
deployments = read("ListDeployments")
|
|
dep = next((d for d in deployments if d["name"].lower() == name.lower()), None)
|
|
if not dep:
|
|
print(f"Deployment '{name}' not found.")
|
|
return
|
|
|
|
info = dep.get("info", {})
|
|
print(f"=== Deployment: {dep['name']} ===\n")
|
|
print(f"ID: {dep['id']}")
|
|
print(f"State: {format_state(info.get('state', 'Unknown'))}")
|
|
print(f"Image: {info.get('image', 'N/A')}")
|
|
print(f"Server: {info.get('server_name', 'N/A')}")
|
|
|
|
|
|
def cmd_deploy(name: str):
|
|
"""Deploy a deployment."""
|
|
result = execute("Deploy", {"deployment": name})
|
|
print(f"Deploy '{name}': {result}")
|
|
|
|
|
|
def cmd_start(name: str):
|
|
"""Start a deployment."""
|
|
result = execute("StartDeployment", {"deployment": name})
|
|
print(f"Start '{name}': {result}")
|
|
|
|
|
|
def cmd_stop(name: str):
|
|
"""Stop a deployment."""
|
|
result = execute("StopDeployment", {"deployment": name})
|
|
print(f"Stop '{name}': {result}")
|
|
|
|
|
|
def cmd_restart(name: str):
|
|
"""Restart a deployment."""
|
|
result = execute("RestartDeployment", {"deployment": name})
|
|
print(f"Restart '{name}': {result}")
|
|
|
|
|
|
def cmd_logs(name: str, lines: int = 100):
|
|
"""Get container logs."""
|
|
result = read("GetLog", {"deployment": name, "tail": lines})
|
|
if isinstance(result, dict) and "log" in result:
|
|
print(result["log"])
|
|
else:
|
|
print(result)
|
|
|
|
|
|
def cmd_stacks():
|
|
"""List all stacks."""
|
|
stacks = read("ListStacks")
|
|
if not stacks:
|
|
print("No stacks found.")
|
|
return
|
|
|
|
print("=== Stacks ===\n")
|
|
for s in stacks:
|
|
info = s.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
server = info.get("server_name", "N/A")
|
|
print(f"{state} {s['name']}")
|
|
print(f" Server: {server}")
|
|
print()
|
|
|
|
|
|
def cmd_stack(name: str):
|
|
"""Get stack details."""
|
|
stacks = read("ListStacks")
|
|
stack = next((s for s in stacks if s["name"].lower() == name.lower()), None)
|
|
if not stack:
|
|
print(f"Stack '{name}' not found.")
|
|
return
|
|
|
|
info = stack.get("info", {})
|
|
print(f"=== Stack: {stack['name']} ===\n")
|
|
print(f"ID: {stack['id']}")
|
|
print(f"State: {format_state(info.get('state', 'Unknown'))}")
|
|
print(f"Server: {info.get('server_name', 'N/A')}")
|
|
|
|
|
|
def cmd_deploy_stack(name: str):
|
|
"""Deploy a stack."""
|
|
result = execute("DeployStack", {"stack": name})
|
|
print(f"Deploy stack '{name}': {result}")
|
|
|
|
|
|
def cmd_start_stack(name: str):
|
|
"""Start a stack."""
|
|
result = execute("StartStack", {"stack": name})
|
|
print(f"Start stack '{name}': {result}")
|
|
|
|
|
|
def cmd_stop_stack(name: str):
|
|
"""Stop a stack."""
|
|
result = execute("StopStack", {"stack": name})
|
|
print(f"Stop stack '{name}': {result}")
|
|
|
|
|
|
def cmd_restart_stack(name: str):
|
|
"""Restart a stack."""
|
|
result = execute("RestartStack", {"stack": name})
|
|
print(f"Restart stack '{name}': {result}")
|
|
|
|
|
|
def cmd_builds():
|
|
"""List all builds."""
|
|
builds = read("ListBuilds")
|
|
if not builds:
|
|
print("No builds found.")
|
|
return
|
|
|
|
print("=== Builds ===\n")
|
|
for b in builds:
|
|
info = b.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
version = info.get("version", "N/A")
|
|
print(f"{state} {b['name']}")
|
|
print(f" Version: {version}")
|
|
print()
|
|
|
|
|
|
def cmd_build(name: str):
|
|
"""Get build details."""
|
|
builds = read("ListBuilds")
|
|
build = next((b for b in builds if b["name"].lower() == name.lower()), None)
|
|
if not build:
|
|
print(f"Build '{name}' not found.")
|
|
return
|
|
|
|
info = build.get("info", {})
|
|
print(f"=== Build: {build['name']} ===\n")
|
|
print(f"ID: {build['id']}")
|
|
print(f"State: {format_state(info.get('state', 'Unknown'))}")
|
|
print(f"Version: {info.get('version', 'N/A')}")
|
|
|
|
|
|
def cmd_run_build(name: str):
|
|
"""Run a build."""
|
|
result = execute("RunBuild", {"build": name})
|
|
print(f"Run build '{name}': {result}")
|
|
|
|
|
|
def cmd_procedures():
|
|
"""List all procedures."""
|
|
procedures = read("ListProcedures")
|
|
if not procedures:
|
|
print("No procedures found.")
|
|
return
|
|
|
|
print("=== Procedures ===\n")
|
|
for p in procedures:
|
|
info = p.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
print(f"{state} {p['name']}")
|
|
print()
|
|
|
|
|
|
def cmd_procedure(name: str):
|
|
"""Get procedure details."""
|
|
procedures = read("ListProcedures")
|
|
proc = next((p for p in procedures if p["name"].lower() == name.lower()), None)
|
|
if not proc:
|
|
print(f"Procedure '{name}' not found.")
|
|
return
|
|
|
|
print(f"=== Procedure: {proc['name']} ===\n")
|
|
print(f"ID: {proc['id']}")
|
|
|
|
|
|
def cmd_run_procedure(name: str):
|
|
"""Run a procedure."""
|
|
result = execute("RunProcedure", {"procedure": name})
|
|
print(f"Run procedure '{name}': {result}")
|
|
|
|
|
|
def cmd_repos():
|
|
"""List all repos."""
|
|
repos = read("ListRepos")
|
|
if not repos:
|
|
print("No repos found.")
|
|
return
|
|
|
|
print("=== Repos ===\n")
|
|
for r in repos:
|
|
info = r.get("info", {})
|
|
state = format_state(info.get("state", "Unknown"))
|
|
print(f"{state} {r['name']}")
|
|
print()
|
|
|
|
|
|
def cmd_create_stack(name: str, server: str, compose_file: str, env_file: str | None = None):
|
|
"""Create a new stack from compose file."""
|
|
# Read compose file
|
|
try:
|
|
with open(compose_file, "r") as f:
|
|
compose_contents = f.read()
|
|
except FileNotFoundError:
|
|
print(f"Error: Compose file '{compose_file}' not found.")
|
|
sys.exit(1)
|
|
|
|
# Read env file if provided
|
|
env_vars = {}
|
|
if env_file:
|
|
try:
|
|
with open(env_file, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith("#") and "=" in line:
|
|
key, _, value = line.partition("=")
|
|
env_vars[key.strip()] = value.strip()
|
|
except FileNotFoundError:
|
|
print(f"Error: Env file '{env_file}' not found.")
|
|
sys.exit(1)
|
|
|
|
# First, create the stack
|
|
create_payload = {
|
|
"name": name,
|
|
"config": {
|
|
"server_id": server,
|
|
"file_contents": compose_contents,
|
|
"environment": "\n".join(f"{k}={v}" for k, v in env_vars.items()) if env_vars else "",
|
|
}
|
|
}
|
|
|
|
result = api_call("write/CreateStack", create_payload)
|
|
print(f"Created stack '{name}': {result.get('id', result)}")
|
|
print(f"\nTo deploy, run: python komodo.py deploy-stack {name}")
|
|
|
|
|
|
def cmd_delete_stack(name: str):
|
|
"""Delete a stack."""
|
|
result = execute("DeleteStack", {"stack": name})
|
|
print(f"Delete stack '{name}': {result}")
|
|
|
|
|
|
def cmd_stack_logs(name: str, service: str | None = None):
|
|
"""Get stack service logs."""
|
|
payload = {"stack": name}
|
|
if service:
|
|
payload["service"] = service
|
|
result = read("GetStackServiceLog", payload)
|
|
if isinstance(result, dict) and "log" in result:
|
|
print(result["log"])
|
|
else:
|
|
print(result)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(__doc__)
|
|
sys.exit(0)
|
|
|
|
cmd = sys.argv[1].lower()
|
|
args = sys.argv[2:]
|
|
|
|
commands = {
|
|
"servers": (cmd_servers, 0),
|
|
"server": (cmd_server, 1),
|
|
"server-stats": (cmd_server_stats, 1),
|
|
"deployments": (cmd_deployments, 0),
|
|
"deployment": (cmd_deployment, 1),
|
|
"deploy": (cmd_deploy, 1),
|
|
"start": (cmd_start, 1),
|
|
"stop": (cmd_stop, 1),
|
|
"restart": (cmd_restart, 1),
|
|
"logs": (cmd_logs, 1),
|
|
"stacks": (cmd_stacks, 0),
|
|
"stack": (cmd_stack, 1),
|
|
"deploy-stack": (cmd_deploy_stack, 1),
|
|
"start-stack": (cmd_start_stack, 1),
|
|
"stop-stack": (cmd_stop_stack, 1),
|
|
"restart-stack": (cmd_restart_stack, 1),
|
|
"create-stack": (cmd_create_stack, 3),
|
|
"delete-stack": (cmd_delete_stack, 1),
|
|
"stack-logs": (cmd_stack_logs, 1),
|
|
"builds": (cmd_builds, 0),
|
|
"build": (cmd_build, 1),
|
|
"run-build": (cmd_run_build, 1),
|
|
"procedures": (cmd_procedures, 0),
|
|
"procedure": (cmd_procedure, 1),
|
|
"run-procedure": (cmd_run_procedure, 1),
|
|
"repos": (cmd_repos, 0),
|
|
}
|
|
|
|
if cmd not in commands:
|
|
print(f"Unknown command: {cmd}")
|
|
print("Run without arguments for help.")
|
|
sys.exit(1)
|
|
|
|
func, min_args = commands[cmd]
|
|
if len(args) < min_args:
|
|
print(f"Command '{cmd}' requires {min_args} argument(s).")
|
|
sys.exit(1)
|
|
|
|
if cmd == "logs" and len(args) >= 2:
|
|
func(args[0], int(args[1]))
|
|
elif cmd == "create-stack":
|
|
# create-stack <name> <server> <compose_file> [env_file]
|
|
env_file = args[3] if len(args) >= 4 else None
|
|
func(args[0], args[1], args[2], env_file)
|
|
elif cmd == "stack-logs" and len(args) >= 2:
|
|
func(args[0], args[1])
|
|
elif min_args > 0:
|
|
func(args[0])
|
|
else:
|
|
func()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|