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