diff --git a/service_runpod_control.sh b/service_runpod_control.sh new file mode 100755 index 0000000..694aa14 --- /dev/null +++ b/service_runpod_control.sh @@ -0,0 +1,705 @@ +#!/bin/bash +# +# RunPod Control Center - A Beautiful CLI Wrapper for runpodctl +# Manage RunPod GPU instances with style +# +# Usage: ./service_runpod_control.sh [COMMAND] [OPTIONS] +# +# Commands: +# create Create a new pod from configuration +# remove Remove a pod (by name or ID) +# get Get pod status (all pods) +# start Start a stopped pod +# stop Stop a running pod +# status Alias for 'get' +# help Show help message +# +# Options: +# -c, --config FILE Configuration file (default: $PWD/runpod.yml) +# -n, --dry-run Show what would be done without executing +# -h, --help Show help message +# + +set -euo pipefail + +# ============================================================================ +# COLOR PALETTE - GPU Computing Vibe (Orange/Yellow Theme) +# ============================================================================ + +RESET='\033[0m' + +# Foreground Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +ORANGE='\033[38;5;208m' + +# Bold +BOLD_RED='\033[1;31m' +BOLD_GREEN='\033[1;32m' +BOLD_YELLOW='\033[1;33m' +BOLD_BLUE='\033[1;34m' +BOLD_MAGENTA='\033[1;35m' +BOLD_CYAN='\033[1;36m' +BOLD_WHITE='\033[1;37m' +BOLD_ORANGE='\033[1;38;5;208m' + +# Styles +DIM='\033[2m' + +# ============================================================================ +# UNICODE CHARACTERS +# ============================================================================ + +CHECK_MARK="โœ“" +CROSS_MARK="โœ—" +ROCKET="๐Ÿš€" +GPU="๐ŸŽฎ" +SERVER="๐Ÿ–ฅ๏ธ" +POWER="โšก" +WARNING="โš ๏ธ" +INFO="โ„น๏ธ" +SPARKLES="โœจ" +MONEY="๐Ÿ’ฐ" +GLOBE="๐ŸŒ" +PLUG="๐Ÿ”Œ" +ID="๐Ÿ†”" +STATUS_DOT="โ—" +ARROW_RIGHT="โ†’" +BOX_LIGHT="โ”€" +BOX_DOUBLE="โ•" +BOX_VERT="โ”‚" +BOX_TL="โ•”" +BOX_TR="โ•—" +BOX_BL="โ•š" +BOX_BR="โ•" + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${PWD}/runpod.yml" +COMMAND="" +DRY_RUN=false +POD_ID="" + +# ============================================================================ +# LOGGING FUNCTIONS +# ============================================================================ + +print_banner() { + local width=70 + local title="$SERVER RunPod Control Center $POWER" + local title_plain=" RunPod Control Center " + local padding=$(( (width - ${#title_plain} - 4) / 2 )) + + echo "" + echo -e "${BOLD_ORANGE}${BOX_TL}$(printf '%*s' "$width" '' | tr ' ' "$BOX_DOUBLE")${BOX_TR}${RESET}" + echo -e "${BOLD_ORANGE}${BOX_VERT}$(printf '%*s' "$padding" '')${BOLD_WHITE}${title}${BOLD_ORANGE}$(printf '%*s' "$padding" '')${BOX_VERT}${RESET}" + echo -e "${BOLD_ORANGE}${BOX_BL}$(printf '%*s' "$width" '' | tr ' ' "$BOX_DOUBLE")${BOX_BR}${RESET}" + echo "" +} + +print_section() { + local title="$1" + echo -e "${BOLD_ORANGE}ยป ${BOLD_WHITE}${title}${RESET}" + echo -e "${DIM}$(printf '%*s' 70 '' | tr ' ' "$BOX_LIGHT")${RESET}" +} + +print_success() { + echo -e "${BOLD_GREEN}${CHECK_MARK}${RESET} $1" +} + +print_error() { + echo -e "${BOLD_RED}${CROSS_MARK}${RESET} $1" >&2 +} + +print_warning() { + echo -e "${BOLD_YELLOW}${WARNING}${RESET} $1" +} + +print_info() { + echo -e "${BOLD_CYAN}${INFO}${RESET} $1" +} + +print_detail() { + echo -e " ${DIM}${ARROW_RIGHT}${RESET} $1" +} + +print_field() { + local icon="$1" + local label="$2" + local value="$3" + printf " ${icon} ${BOLD_WHITE}%-12s${RESET} %s\n" "${label}:" "$value" +} + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +check_dependencies() { + local missing=() + + if ! command -v yq &>/dev/null; then + missing+=("yq") + fi + + if ! command -v runpodctl &>/dev/null; then + missing+=("runpodctl") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + print_error "Missing required dependencies: ${missing[*]}" + echo "" + echo "Install with:" + for dep in "${missing[@]}"; do + case "$dep" in + yq) + echo " brew install yq # or snap install yq" + ;; + runpodctl) + echo " See: https://github.com/runpod/runpodctl" + ;; + esac + done + exit 1 + fi +} + +load_config() { + if [[ ! -f "$CONFIG_FILE" ]]; then + print_error "Configuration file not found: $CONFIG_FILE" + exit 1 + fi + + # Validate YAML + if ! yq eval '.' "$CONFIG_FILE" &>/dev/null; then + print_error "Invalid YAML in configuration file: $CONFIG_FILE" + exit 1 + fi + + print_info "Using config: ${DIM}${CONFIG_FILE}${RESET}" +} + +get_pod_field() { + local field="$1" + local default="${2:-}" + local value + + value=$(yq eval ".$field // \"\"" "$CONFIG_FILE") + + if [[ -z "$value" || "$value" == "null" ]]; then + echo "$default" + else + echo "$value" + fi +} + +get_pod_array() { + local field="$1" + yq eval ".$field[]? // \"\"" "$CONFIG_FILE" 2>/dev/null | grep -v '^$' || true +} + +# Find pod ID by name from runpodctl get pod output +find_pod_id_by_name() { + local name="$1" + local output + + output=$(runpodctl get pod 2>/dev/null) || { + print_error "Failed to get pod list" + return 1 + } + + # Parse the output - runpodctl outputs a table + # Skip header line and find matching name + echo "$output" | tail -n +2 | while read -r line; do + local pod_id pod_name + pod_id=$(echo "$line" | awk '{print $1}') + pod_name=$(echo "$line" | awk '{print $2}') + + if [[ "$pod_name" == "$name" ]]; then + echo "$pod_id" + return 0 + fi + done +} + +get_status_color() { + local status="$1" + case "${status^^}" in + RUNNING) + echo -e "${BOLD_GREEN}${STATUS_DOT} RUNNING${RESET}" + ;; + EXITED|STOPPED) + echo -e "${BOLD_YELLOW}${STATUS_DOT} STOPPED${RESET}" + ;; + ERROR|FAILED) + echo -e "${BOLD_RED}${STATUS_DOT} ERROR${RESET}" + ;; + CREATING|STARTING) + echo -e "${BOLD_CYAN}${STATUS_DOT} STARTING${RESET}" + ;; + *) + echo -e "${DIM}${STATUS_DOT} ${status}${RESET}" + ;; + esac +} + +# ============================================================================ +# COMMAND IMPLEMENTATIONS +# ============================================================================ + +cmd_create() { + print_section "Creating Pod" + + load_config + + local name gpu_type gpu_count template_id network_volume_id + local container_disk_size volume_size volume_path mem vcpu + local secure_cloud community_cloud image_name + + # Required fields + name=$(get_pod_field "pod.name") + gpu_type=$(get_pod_field "pod.gpuType") + + if [[ -z "$name" ]]; then + print_error "Pod name is required in configuration" + exit 1 + fi + + if [[ -z "$gpu_type" ]]; then + print_error "GPU type is required in configuration" + exit 1 + fi + + # Optional fields + gpu_count=$(get_pod_field "pod.gpuCount" "1") + template_id=$(get_pod_field "pod.templateId") + network_volume_id=$(get_pod_field "pod.networkVolumeId") + container_disk_size=$(get_pod_field "pod.containerDiskSize" "20") + volume_size=$(get_pod_field "pod.volumeSize" "0") + volume_path=$(get_pod_field "pod.volumePath" "/runpod") + mem=$(get_pod_field "pod.mem") + vcpu=$(get_pod_field "pod.vcpu") + secure_cloud=$(get_pod_field "pod.secureCloud" "false") + community_cloud=$(get_pod_field "pod.communityCloud" "false") + image_name=$(get_pod_field "pod.imageName") + + # Display configuration + echo "" + print_field "$GPU" "Name" "$name" + print_field "$POWER" "GPU" "$gpu_type (x$gpu_count)" + [[ -n "$template_id" ]] && print_field "$SERVER" "Template" "$template_id" + [[ -n "$network_volume_id" ]] && print_field "$PLUG" "Network Vol" "$network_volume_id" + [[ -n "$image_name" ]] && print_field "$ROCKET" "Image" "$image_name" + + # Ports + local ports=() + while IFS= read -r port; do + [[ -n "$port" ]] && ports+=("$port") + done < <(get_pod_array "pod.ports") + + if [[ ${#ports[@]} -gt 0 ]]; then + print_field "$PLUG" "Ports" "${ports[*]}" + fi + + # Environment variables + local env_vars=() + while IFS= read -r env; do + [[ -n "$env" ]] && env_vars+=("$env") + done < <(get_pod_array "pod.env") + + echo "" + + # Build command arguments + local args=() + args+=(--name "$name") + args+=(--gpuType "$gpu_type") + args+=(--gpuCount "$gpu_count") + args+=(--containerDiskSize "$container_disk_size") + + [[ -n "$template_id" ]] && args+=(--templateId "$template_id") + [[ -n "$network_volume_id" ]] && args+=(--networkVolumeId "$network_volume_id") + [[ -n "$image_name" ]] && args+=(--imageName "$image_name") + [[ "$volume_size" != "0" ]] && args+=(--volumeSize "$volume_size") + [[ -n "$volume_path" ]] && args+=(--volumePath "$volume_path") + [[ -n "$mem" ]] && args+=(--mem "$mem") + [[ -n "$vcpu" ]] && args+=(--vcpu "$vcpu") + [[ "$secure_cloud" == "true" ]] && args+=(--secureCloud) + [[ "$community_cloud" == "true" ]] && args+=(--communityCloud) + + for port in "${ports[@]}"; do + args+=(--ports "$port") + done + + for env in "${env_vars[@]}"; do + args+=(--env "$env") + done + + if [[ "$DRY_RUN" == "true" ]]; then + print_warning "Dry run - would execute:" + echo "" + echo -e " ${DIM}runpodctl create pod ${args[*]}${RESET}" + echo "" + else + print_info "Executing: runpodctl create pod..." + echo "" + + local output + if output=$(runpodctl create pod "${args[@]}" 2>&1); then + print_success "Pod created successfully!" + echo "" + echo -e " ${DIM}${output}${RESET}" + echo "" + else + print_error "Failed to create pod" + echo -e " ${DIM}${output}${RESET}" + exit 1 + fi + fi +} + +cmd_get() { + print_section "Pod Status" + echo "" + + local output + if ! output=$(runpodctl get pod 2>&1); then + print_error "Failed to get pod status" + echo -e " ${DIM}${output}${RESET}" + exit 1 + fi + + # Check if empty + if [[ -z "$output" || "$output" == *"No pods found"* ]]; then + print_warning "No pods found" + return 0 + fi + + # Parse and display pods beautifully + local header_shown=false + local count=0 + + while IFS= read -r line; do + # Skip empty lines + [[ -z "$line" ]] && continue + + # Skip header line + if [[ "$header_shown" == "false" ]]; then + header_shown=true + continue + fi + + # Parse fields - runpodctl output format varies + # Typically: ID NAME STATUS GPU COST + local pod_id pod_name pod_status gpu_info cost_info + + pod_id=$(echo "$line" | awk '{print $1}') + pod_name=$(echo "$line" | awk '{print $2}') + pod_status=$(echo "$line" | awk '{print $3}') + gpu_info=$(echo "$line" | awk '{print $4}') + cost_info=$(echo "$line" | awk '{print $5}') + + [[ -z "$pod_id" ]] && continue + + ((count++)) + + if [[ $count -gt 1 ]]; then + echo "" + fi + + print_field "$GPU" "Name" "$pod_name" + print_field "$ID" "ID" "$pod_id" + echo -e " ${STATUS_DOT} ${BOLD_WHITE}Status:${RESET} $(get_status_color "$pod_status")" + [[ -n "$gpu_info" && "$gpu_info" != "-" ]] && print_field "$POWER" "GPU" "$gpu_info" + [[ -n "$cost_info" && "$cost_info" != "-" ]] && print_field "$MONEY" "Cost" "$cost_info" + + done <<< "$output" + + if [[ $count -eq 0 ]]; then + print_warning "No pods found" + else + echo "" + print_info "Total pods: $count" + fi + echo "" +} + +cmd_start() { + print_section "Starting Pod" + + load_config + + local name pod_id + name=$(get_pod_field "pod.name") + + if [[ -n "$POD_ID" ]]; then + pod_id="$POD_ID" + elif [[ -n "$name" ]]; then + print_info "Looking up pod by name: $name" + pod_id=$(find_pod_id_by_name "$name") + fi + + if [[ -z "$pod_id" ]]; then + print_error "Pod not found. Specify --id or configure pod.name in config." + exit 1 + fi + + echo "" + print_field "$ID" "Pod ID" "$pod_id" + echo "" + + if [[ "$DRY_RUN" == "true" ]]; then + print_warning "Dry run - would execute:" + echo "" + echo -e " ${DIM}runpodctl start pod $pod_id${RESET}" + echo "" + else + local output + if output=$(runpodctl start pod "$pod_id" 2>&1); then + print_success "Pod starting!" + echo -e " ${DIM}${output}${RESET}" + else + print_error "Failed to start pod" + echo -e " ${DIM}${output}${RESET}" + exit 1 + fi + fi + echo "" +} + +cmd_stop() { + print_section "Stopping Pod" + + load_config + + local name pod_id + name=$(get_pod_field "pod.name") + + if [[ -n "$POD_ID" ]]; then + pod_id="$POD_ID" + elif [[ -n "$name" ]]; then + print_info "Looking up pod by name: $name" + pod_id=$(find_pod_id_by_name "$name") + fi + + if [[ -z "$pod_id" ]]; then + print_error "Pod not found. Specify --id or configure pod.name in config." + exit 1 + fi + + echo "" + print_field "$ID" "Pod ID" "$pod_id" + echo "" + + if [[ "$DRY_RUN" == "true" ]]; then + print_warning "Dry run - would execute:" + echo "" + echo -e " ${DIM}runpodctl stop pod $pod_id${RESET}" + echo "" + else + local output + if output=$(runpodctl stop pod "$pod_id" 2>&1); then + print_success "Pod stopping!" + echo -e " ${DIM}${output}${RESET}" + else + print_error "Failed to stop pod" + echo -e " ${DIM}${output}${RESET}" + exit 1 + fi + fi + echo "" +} + +cmd_remove() { + print_section "Removing Pod" + + load_config + + local name pod_id + name=$(get_pod_field "pod.name") + + if [[ -n "$POD_ID" ]]; then + pod_id="$POD_ID" + elif [[ -n "$name" ]]; then + print_info "Looking up pod by name: $name" + pod_id=$(find_pod_id_by_name "$name") + fi + + if [[ -z "$pod_id" ]]; then + print_error "Pod not found. Specify --id or configure pod.name in config." + exit 1 + fi + + echo "" + print_field "$ID" "Pod ID" "$pod_id" + print_field "$GPU" "Name" "${name:-unknown}" + echo "" + + if [[ "$DRY_RUN" == "true" ]]; then + print_warning "Dry run - would execute:" + echo "" + echo -e " ${DIM}runpodctl remove pod $pod_id${RESET}" + echo "" + else + # Confirm removal + print_warning "This will permanently delete the pod!" + echo -n " Continue? [y/N] " + read -r confirm + + if [[ "${confirm,,}" != "y" ]]; then + print_info "Cancelled" + exit 0 + fi + + echo "" + local output + if output=$(runpodctl remove pod "$pod_id" 2>&1); then + print_success "Pod removed!" + echo -e " ${DIM}${output}${RESET}" + else + print_error "Failed to remove pod" + echo -e " ${DIM}${output}${RESET}" + exit 1 + fi + fi + echo "" +} + +cmd_help() { + echo -e "${BOLD_WHITE}USAGE${RESET}" + echo " service_runpod_control.sh [COMMAND] [OPTIONS]" + echo "" + + echo -e "${BOLD_WHITE}COMMANDS${RESET}" + echo -e " ${BOLD_ORANGE}create${RESET} Create a new pod from configuration" + echo -e " ${BOLD_ORANGE}remove${RESET} Remove a pod (by name or ID)" + echo -e " ${BOLD_ORANGE}get${RESET} Get pod status (all pods)" + echo -e " ${BOLD_ORANGE}start${RESET} Start a stopped pod" + echo -e " ${BOLD_ORANGE}stop${RESET} Stop a running pod" + echo -e " ${BOLD_ORANGE}status${RESET} Alias for 'get'" + echo -e " ${BOLD_ORANGE}help${RESET} Show this help message" + echo "" + + echo -e "${BOLD_WHITE}OPTIONS${RESET}" + echo " -c, --config FILE Configuration file (default: \$PWD/runpod.yml)" + echo " --id POD_ID Pod ID for start/stop/remove commands" + echo " -n, --dry-run Show what would be done without executing" + echo " -h, --help Show this help message" + echo "" + + echo -e "${BOLD_WHITE}CONFIGURATION${RESET}" + echo " Create a runpod.yml file with your pod configuration:" + echo "" + echo -e " ${DIM}pod:" + echo " name: \"my-gpu-pod\"" + echo " gpuType: \"NVIDIA GeForce RTX 4090\"" + echo " gpuCount: 1" + echo " templateId: \"your-template-id\"" + echo " networkVolumeId: \"your-volume-id\"" + echo " ports:" + echo -e " - \"22/tcp\"${RESET}" + echo "" + + echo -e "${BOLD_WHITE}EXAMPLES${RESET}" + echo " # Create a pod from config" + echo " ./service_runpod_control.sh create" + echo "" + echo " # Get status of all pods" + echo " ./service_runpod_control.sh get" + echo "" + echo " # Stop a pod by name (from config)" + echo " ./service_runpod_control.sh stop" + echo "" + echo " # Stop a specific pod by ID" + echo " ./service_runpod_control.sh stop --id abc123xyz" + echo "" +} + +# ============================================================================ +# ARGUMENT PARSING +# ============================================================================ + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + create|remove|get|start|stop|status|help) + COMMAND="$1" + shift + ;; + -c|--config) + CONFIG_FILE="$2" + shift 2 + ;; + --id) + POD_ID="$2" + shift 2 + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + COMMAND="help" + shift + ;; + *) + print_error "Unknown argument: $1" + echo "Use 'help' command for usage information" + exit 1 + ;; + esac + done +} + +# ============================================================================ +# MAIN +# ============================================================================ + +main() { + parse_args "$@" + + # Default to help if no command + if [[ -z "$COMMAND" ]]; then + cmd_help + exit 0 + fi + + # Check dependencies (except for help) + if [[ "$COMMAND" != "help" ]]; then + check_dependencies + fi + + print_banner + + case "$COMMAND" in + create) + cmd_create + ;; + remove) + cmd_remove + ;; + get|status) + cmd_get + ;; + start) + cmd_start + ;; + stop) + cmd_stop + ;; + help) + cmd_help + ;; + *) + print_error "Unknown command: $COMMAND" + exit 1 + ;; + esac +} + +main "$@"