2025-12-23 19:23:49 +08:00

331 lines
9.0 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Annotated
import typer
from rich.console import Console
from fc.commands.utils import get_doctype, validate_server_name
from fc.printer import Print, print_full_plan_details, print_plan_details, show_usage
if TYPE_CHECKING:
from fc.authentication.session import CloudSession
server = typer.Typer(help="Server Commands")
console = Console()
@server.command(help="Show live usage for a server")
def usage(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name")],
):
session: CloudSession = ctx.obj
try:
usage_data = session.post(
"press.api.server.usage",
json={"name": name},
message=f"[bold green]Fetching usage for {name}...",
)
if not isinstance(usage_data, dict) or not usage_data:
Print.info(console, "No usage data returned.")
return
vcpu = usage_data.get("vcpu")
disk_gb = usage_data.get("disk") # GB
mem_mb = usage_data.get("memory") # MB
free_mem_bytes = usage_data.get("free_memory") # bytes (avg 10m)
show_usage(
vcpu=vcpu,
mem_mb=mem_mb,
disk_gb=disk_gb,
free_mem_bytes=free_mem_bytes,
console=console,
)
except Exception as e:
Print.error(console, e)
@server.command(help="Show details about a specific plan for a server")
def show_plan(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name")],
plan: Annotated[str, typer.Option("--plan", help="Plan name")],
):
session: CloudSession = ctx.obj
try:
doctype = get_doctype(name)
payload = {"name": doctype, "cluster": "Mumbai", "platform": "arm64"}
plans = session.post(
"press.api.server.plans", json=payload, message="[bold green]Fetching available server plans..."
)
selected_plan = next((p for p in plans if p.get("name") == plan), None)
if not selected_plan:
Print.error(console, f"Plan '{plan}' not found for server '{name}'")
return
print_plan_details(selected_plan, console)
except Exception as e:
Print.error(console, e)
@server.command(help="Shows the current plan for a server")
def server_plan(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name")],
):
session: CloudSession = ctx.obj
try:
doctype = get_doctype(name)
payload = {
"doctype": doctype,
"name": name,
"fields": ["current_plan"],
"debug": 0,
}
response = session.post(
"press.api.client.get", json=payload, message="[bold green]Getting server details..."
)
if not response or "current_plan" not in response:
Print.error(console, f"{doctype} '{name}' or its current plan not found.")
return
plan = response["current_plan"]
print_full_plan_details(plan, console)
except Exception as e:
Print.error(console, e)
@server.command(help="Increase storage for a server")
def increase_storage(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name")],
increment: Annotated[int, typer.Option("--increment", help="Increment size in GB")],
force: Annotated[
bool,
typer.Option("--force", "-f", is_flag=True, help="Skip confirmation"),
] = False,
):
session: CloudSession = ctx.obj
is_valid, err = validate_server_name(name)
if not is_valid:
Print.error(console, err)
return
try:
doctype = get_doctype(name)
if not _should_proceed(
f"Increase storage for server '{name}' by {increment} GB? This action may be irreversible.",
force,
):
Print.info(console, "Operation cancelled.")
return
payload = {
"dt": doctype,
"dn": name,
"method": "increase_disk_size_for_server",
"args": {"server": name, "increment": increment},
}
response = session.post(
"press.api.client.run_pg_method",
json=payload,
message=f"[bold green]Increasing storage for {name} by {increment}GB...",
)
if response and response.get("success") is False:
Print.error(console, f"Failed to increase storage: {response.get('message', 'Unknown error')}")
return
Print.success(
console,
f"Storage increased for server {name} by {increment} GB",
)
except Exception as e:
Print.error(console, e)
@server.command(help="Show current plan and choose available server plans")
def choose_plan(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name")],
plan: Annotated[str, typer.Option("--plan", help="Plan name")],
force: Annotated[
bool,
typer.Option("--force", "-f", is_flag=True, help="Skip confirmation"),
] = False,
):
session: CloudSession = ctx.obj
try:
doctype = get_doctype(name)
payload = {"name": doctype, "cluster": "Mumbai", "platform": "arm64"}
plans = session.post(
"press.api.server.plans", json=payload, message="[bold green]Fetching available server plans..."
)
selected_plan = next((p for p in plans if p.get("name") == plan), None)
if not selected_plan:
Print.error(console, f"Plan '{plan}' not found for server '{name}'")
return
current_plan_name = _get_current_plan_name(session, doctype, name)
if current_plan_name and current_plan_name == selected_plan.get("name"):
Print.info(
console,
f"Plan '{current_plan_name}' is already active for server '{name}'. Choose a different plan to change.",
)
return
if not _should_proceed(
f"Change plan for server '{name}' to '{selected_plan.get('name')}'?",
force,
):
Print.info(console, "Operation cancelled.")
return
change_payload = {
"dt": doctype,
"dn": name,
"method": "change_plan",
"args": {"plan": selected_plan.get("name")},
}
response = session.post(
"press.api.client.run_pg_method",
json=change_payload,
message=f"[bold green]Changing plan for {name} to {selected_plan.get('name')}...",
)
if response and response.get("success") is False:
Print.error(console, f"Failed to change plan: {response.get('message', 'Unknown error')}")
return
previous_plan = current_plan_name or "(unknown)"
Print.success(
console,
f"Plan changed for server '{name}': {previous_plan} -> {selected_plan.get('name')}",
)
print_plan_details(selected_plan, console)
except Exception as e:
Print.error(console, e)
@server.command(help="Create a new server")
def create_server(
ctx: typer.Context,
title: Annotated[str, typer.Argument(help="Server title")],
cluster: Annotated[str, typer.Option("--cluster", help="Cluster name")] = "",
app_plan: Annotated[str, typer.Option("--app-plan", help="App server plan name")] = "",
db_plan: Annotated[str, typer.Option("--db-plan", help="Database server plan name")] = "",
auto_increase_storage: Annotated[
bool, typer.Option("--auto-increase-storage", is_flag=True, help="Auto increase storage")
] = False,
):
session: CloudSession = ctx.obj
try:
server_payload = {
"cluster": cluster,
"title": title,
"app_plan": app_plan,
"db_plan": db_plan,
"auto_increase_storage": auto_increase_storage,
}
response = session.post(
"press.api.server.new",
json={"server": server_payload},
message=f"[bold green]Creating server '{title}' in cluster '{cluster}'...",
)
if not response or not response.get("server"):
Print.error(
console,
f"Failed to create server: {response.get('message', 'Unknown error') if response else 'No response from backend.'}",
)
return
Print.success(console, f"Successfully created server: {response['server']}")
if response.get("job"):
Print.info(console, f"Server creation job started: {response['job']}")
except Exception as e:
Print.error(console, e)
@server.command(help="Delete a server (archive)")
def delete_server(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="Server name to delete")],
force: Annotated[
bool,
typer.Option("--force", "-f", is_flag=True, help="Skip confirmation"),
] = False,
):
session: CloudSession = ctx.obj
try:
if not _should_proceed(
f"Are you sure you want to archive server '{name}'? This action may be irreversible.",
force,
):
Print.info(console, "Operation cancelled.")
return
response = session.post(
"press.api.server.archive",
json={"name": name},
message=f"[bold red]Archiving server '{name}'...",
)
if response and response.get("exc_type"):
Print.error(console, f"Failed to delete server: {response.get('exception', 'Unknown error')}")
return
Print.success(console, f"Successfully deleted (archived) server: {name}")
except Exception as e:
Print.error(console, e)
def _should_proceed(message: str, confirm_token: str | None) -> bool:
if isinstance(confirm_token, bool) and confirm_token:
return True
if isinstance(confirm_token, str) and confirm_token.lower() in {"force", "yes", "y"}:
return True
return typer.confirm(message, default=False)
def _get_current_plan_name(session: "CloudSession", doctype: str, name: str) -> str | None:
resp = session.post(
"press.api.client.get",
json={
"doctype": doctype,
"name": name,
"fields": ["current_plan"],
"debug": 0,
},
message="[bold green]Checking current server plan...",
)
if isinstance(resp, dict) and resp.get("current_plan"):
cp = resp["current_plan"]
if isinstance(cp, dict):
return cp.get("name")
if isinstance(cp, str):
return cp
return None