#!/bin/bash #==================================================================== # 솔메카 Smart Deploy v4.0 # - 서버 자동 스캔 (CPU/RAM/포트/기존 배포) # - 최적 서버 자동 선택 # - SSH + rsync + systemd 직접 배포 (Docker 없음) # - 배포 레지스트리 관리 (충돌 회피) # # 사용법: # deploy.sh . 현재 프로젝트 배포 # deploy.sh status 전체 상태 확인 # deploy.sh list 배포 목록 # deploy.sh stop <이름> 서비스 중지 # deploy.sh rm <이름> 서비스 삭제 # deploy.sh logs <이름> 로그 보기 #==================================================================== set -eo pipefail # ── 설정 ────────────────────────────────────────────────────────── SSH_KEY="$HOME/.ssh/id_ed25519" REGISTRY_FILE="C:/Users/User/.solmeca-deploy.json" PORT_RANGE_START=9100 PORT_RANGE_END=9300 # 서버 목록: name|ip|ssh_user|sudo_pass|cpus|has_gpu|tags SERVERS=( "kakao-main|100.125.85.86|kakao|2828fire!!|8|yes|web,bot,dashboard" "worker-ai|100.118.136.45|server|9220fire!!|20|yes|ai,ml,bot,dashboard,heavy" "worker-downsys|100.121.159.128|downsys|2828fire!!|8|no|web,bot,dashboard,light" "worker-kakao2|100.70.23.63|kakao|2828fire!!|12|yes|ai,gpu,bot,dashboard,automation" ) # ── 색상 ────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' log() { echo -e "${BLUE}$1${NC}"; } ok() { echo -e " ${GREEN}✓ $1${NC}"; } warn() { echo -e " ${YELLOW}! $1${NC}"; } err() { echo -e " ${RED}✗ $1${NC}"; } info() { echo -e " ${CYAN}$1${NC}"; } # ── SSH 헬퍼 ────────────────────────────────────────────────────── run_ssh() { local user=$1 ip=$2; shift 2 ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=no -o LogLevel=ERROR \ -i "$SSH_KEY" "${user}@${ip}" "$@" 2>/dev/null } # ── 레지스트리 관리 ─────────────────────────────────────────────── init_registry() { [ -f "$REGISTRY_FILE" ] || echo '{"deployments":{}}' > "$REGISTRY_FILE" } get_registry() { cat "$REGISTRY_FILE" } save_deployment() { local name=$1 server=$2 ip=$3 port=$4 type=$5 path=$6 user=$7 local now now=$(date -Iseconds) python3 -c " import json,sys reg = json.load(open('$REGISTRY_FILE')) reg['deployments']['$name'] = { 'server': '$server', 'ip': '$ip', 'port': $port, 'type': '$type', 'remote_path': '$path', 'ssh_user': '$user', 'deployed_at': '$now', 'status': 'running' } json.dump(reg, open('$REGISTRY_FILE','w'), indent=2, ensure_ascii=False) print('saved') " 2>/dev/null } remove_deployment() { local name=$1 python3 -c " import json reg = json.load(open('$REGISTRY_FILE')) if '$name' in reg['deployments']: del reg['deployments']['$name'] json.dump(reg, open('$REGISTRY_FILE','w'), indent=2, ensure_ascii=False) print('removed') else: print('not_found') " 2>/dev/null } update_md() { local md_file="C:/Users/User/Desktop/SERVER_ 운영/서버_배포_현황.md" python3 -c " import json from datetime import datetime reg = json.load(open('$REGISTRY_FILE')) deps = reg.get('deployments', {}) servers = {} for name, d in sorted(deps.items()): srv = d.get('server', '?') if srv not in servers: servers[srv] = [] port = d.get('port', 0) url = f\"http://{d['ip']}:{port}\" if port else '-' servers[srv].append({ 'name': name, 'type': d.get('type','?'), 'port': port, 'url': url, 'path': d.get('remote_path','?'), 'date': d.get('deployed_at','?')[:10] }) lines = ['# 서버 배포 현황', f'', f'> Updated: {datetime.now().strftime(\"%Y-%m-%d %H:%M\")}', f'> Total: {len(deps)} services', ''] for srv in ['worker-ai','worker-downsys','worker-kakao2','kakao-main']: if srv not in servers: continue lines.append(f'## {srv}') lines.append('') lines.append('| Name | Type | Port | URL | Path | Date |') lines.append('|------|------|------|-----|------|------|') for s in servers[srv]: lines.append(f\"| {s['name']} | {s['type']} | {s['port'] or '-'} | {s['url']} | {s['path']} | {s['date']} |\") lines.append('') with open('$md_file', 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) " 2>/dev/null } get_deployment_info() { local name=$1 python3 -c " import json reg = json.load(open('$REGISTRY_FILE')) d = reg['deployments'].get('$name') if d: print(f\"{d['server']}|{d['ip']}|{d['port']}|{d['ssh_user']}|{d['remote_path']}\") " 2>/dev/null } get_server_deployments() { local ip=$1 python3 -c " import json reg = json.load(open('$REGISTRY_FILE')) ports = [] for name, d in reg['deployments'].items(): if d['ip'] == '$ip': ports.append(str(d['port'])) print(' '.join(ports) if ports else '') " 2>/dev/null } # ── 프로젝트 타입 감지 ─────────────────────────────────────────── detect_project() { local dir=$1 local type="unknown" cmd="" install="" if [ -f "$dir/requirements.txt" ]; then type="python" install="pip install -r requirements.txt" # main 파일 찾기 if [ -f "$dir/main.py" ]; then if grep -q "uvicorn\|fastapi\|flask" "$dir/main.py" 2>/dev/null; then cmd="python3 -m uvicorn main:app --host 0.0.0.0 --port __PORT__" else cmd="python3 main.py" fi elif [ -f "$dir/app.py" ]; then cmd="python3 app.py" elif [ -f "$dir/bot.py" ]; then cmd="python3 bot.py" elif [ -f "$dir/dashboard.py" ]; then cmd="python3 dashboard.py --port __PORT__" else # 아무 .py 찾기 local pyfile pyfile=$(ls "$dir"/*.py 2>/dev/null | head -1) [ -n "$pyfile" ] && cmd="python3 $(basename "$pyfile")" fi elif [ -f "$dir/pyproject.toml" ]; then type="python" install="pip install ." cmd="python3 -m uvicorn main:app --host 0.0.0.0 --port __PORT__" elif [ -f "$dir/package.json" ]; then type="node" install="npm install" if grep -q '"start"' "$dir/package.json" 2>/dev/null; then cmd="npm start" elif [ -f "$dir/server.js" ]; then cmd="node server.js" elif [ -f "$dir/index.js" ]; then cmd="node index.js" elif [ -f "$dir/bot.js" ]; then cmd="node bot.js" else cmd="npm start" fi elif [ -f "$dir/go.mod" ]; then type="go" install="go build -o server ." cmd="./server" elif [ -f "$dir/Cargo.toml" ]; then type="rust" install="cargo build --release" cmd="./target/release/$(basename "$dir")" elif [ -f "$dir/index.html" ]; then type="static" install="" cmd="python3 -m http.server __PORT__" fi echo "${type}|${cmd}|${install}" } # ── 사용 가능한 포트 찾기 ───────────────────────────────────────── find_available_port() { local ssh_user=$1 ip=$2 # 서버의 사용 중 포트 local used_ports used_ports=$(run_ssh "$ssh_user" "$ip" "ss -tlnp | awk 'NR>1{print \$4}' | grep -oP ':\K[0-9]+' | sort -n | uniq" 2>/dev/null) # 레지스트리에 기록된 포트 local reg_ports reg_ports=$(get_server_deployments "$ip") local all_used="${used_ports} ${reg_ports}" for port in $(seq $PORT_RANGE_START $PORT_RANGE_END); do if ! echo "$all_used" | grep -qw "$port"; then echo "$port" return 0 fi done echo "0" return 1 } # ── 서버 리소스 조회 (병렬) ────────────────────────────────────── declare -A SERVER_LOADS=() fetch_all_server_loads() { local tmpdir tmpdir=$(mktemp -d) for server_info in "${SERVERS[@]}"; do IFS='|' read -r name ip ssh_user _ _ _ <<< "$server_info" ( result=$(run_ssh "$ssh_user" "$ip" ' read -r _ u1 n1 s1 i1 _ < /proc/stat; sleep 1; read -r _ u2 n2 s2 i2 _ < /proc/stat total=$(( (u2+n2+s2+i2) - (u1+n1+s1+i1) )); idle=$(( i2 - i1 )) [ "$total" -gt 0 ] && cpu_free=$(( idle * 100 / total )) || cpu_free=50 read -r mem_total mem_avail <<< $(awk "/MemTotal/{t=int(\$2/1024)} /MemAvailable/{a=int(\$2/1024)} END{print t,a}" /proc/meminfo) containers=$(docker ps -q 2>/dev/null | wc -l) procs=$(ps aux --no-heading 2>/dev/null | wc -l) disk_pct=$(df / | awk "NR==2{gsub(/%/,\"\",\$5); print \$5}") echo "$cpu_free $mem_total $mem_avail $containers $procs $disk_pct" ' 2>/dev/null || echo "50 0 0 0 0 0") echo "$result" > "${tmpdir}/${name}" ) & done wait for server_info in "${SERVERS[@]}"; do IFS='|' read -r name _ _ _ _ _ <<< "$server_info" [ -f "${tmpdir}/${name}" ] && SERVER_LOADS["$name"]=$(cat "${tmpdir}/${name}") || SERVER_LOADS["$name"]="50 0 0 0 0 0" done rm -rf "$tmpdir" } # ── 최적 서버 선택 ─────────────────────────────────────────────── select_best_server() { local app_type=$1 local best_server="" best_score=-999 fetch_all_server_loads for server_info in "${SERVERS[@]}"; do IFS='|' read -r name ip ssh_user sudo_pass cpus has_gpu tags <<< "$server_info" local cpu_free mem_total mem_avail containers procs disk_pct read -r cpu_free mem_total mem_avail containers procs disk_pct <<< "${SERVER_LOADS[$name]:-50 0 0 0 0 0}" local cpu_score=$((cpu_free)) local mem_score=50 [ "${mem_total:-0}" -gt 0 ] && mem_score=$((mem_avail * 100 / mem_total)) local disk_score=0 [ "${disk_pct:-0}" -gt 90 ] && disk_score=-50 [ "${disk_pct:-0}" -lt 50 ] && disk_score=10 # 배포 수 패널티 (이 서버에 이미 배포된 수) local deploy_count deploy_count=$(python3 -c " import json reg=json.load(open('$REGISTRY_FILE')) print(sum(1 for d in reg['deployments'].values() if d['ip']=='$ip')) " 2>/dev/null || echo 0) local deploy_penalty=$((deploy_count * 8)) # GPU 필요 시 local gpu_score=0 if [[ "$app_type" =~ ai|ml|gpu|cuda ]]; then [ "$has_gpu" = "no" ] && gpu_score=-200 || gpu_score=40 fi local score=$((cpu_score + mem_score + disk_score - deploy_penalty + gpu_score)) local gpu_label="--" [ "$has_gpu" = "yes" ] && gpu_label="GPU" printf " %-16s CPU=%3d%% RAM=%5dMB 배포=%d ${YELLOW}점수=%d${NC}\n" \ "$name" "$cpu_free" "$mem_avail" "$deploy_count" "$score" >&2 [ "$score" -gt "$best_score" ] && best_score=$score && best_server="$server_info" done echo "$best_server" } # ══════════════════════════════════════════════════════════════════ # 메인 배포 # ══════════════════════════════════════════════════════════════════ deploy() { local project_dir if [ "${1:-.}" = "." ]; then project_dir=$(pwd) else cd "$1" 2>/dev/null || { err "폴더 없음: $1"; return 1; } project_dir=$(pwd) fi local project_name project_name=$(basename "$project_dir") echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD}${CYAN} Deploy: ${project_name}${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" init_registry # ── [1] 프로젝트 감지 ── log "[1/6] 프로젝트 분석" local detect_result detect_result=$(detect_project "$project_dir") IFS='|' read -r proj_type proj_cmd proj_install <<< "$detect_result" if [ "$proj_type" = "unknown" ] || [ -z "$proj_cmd" ]; then err "프로젝트 타입을 감지할 수 없습니다" info "main.py, bot.py, package.json, index.html 등이 필요합니다" return 1 fi ok "타입: ${proj_type}" ok "실행: ${proj_cmd}" # ── [2] 기존 배포 확인 ── log "[2/6] 기존 배포 확인" local existing existing=$(get_deployment_info "$project_name") local srv_name srv_ip srv_user srv_pass srv_cpus srv_gpu srv_tags local port remote_path if [ -n "$existing" ]; then IFS='|' read -r old_server old_ip old_port old_user old_path <<< "$existing" warn "기존 배포 발견: ${old_server} 포트 ${old_port} → 업데이트합니다" # 기존 서버 정보 찾기 for s in "${SERVERS[@]}"; do IFS='|' read -r sn si su sp sc sg st <<< "$s" if [ "$si" = "$old_ip" ]; then srv_name=$sn; srv_ip=$si; srv_user=$su; srv_pass=$sp; srv_cpus=$sc; srv_gpu=$sg; srv_tags=$st break fi done port=$old_port remote_path=$old_path else # ── [3] 서버 선택 ── log "[3/6] 서버 선택" info "서버 스캔 중..." local selected selected=$(select_best_server "$proj_type") IFS='|' read -r srv_name srv_ip srv_user srv_pass srv_cpus srv_gpu srv_tags <<< "$selected" echo -e " ${GREEN}${BOLD}>>> ${srv_name} (${srv_ip})${NC}" # 포트 할당 port=$(find_available_port "$srv_user" "$srv_ip") if [ "$port" = "0" ]; then err "사용 가능한 포트가 없습니다" return 1 fi ok "포트: ${port}" remote_path="/opt/solmeca/${project_name}" fi # 실행 명령어에 포트 적용 proj_cmd="${proj_cmd//__PORT__/$port}" # 환경변수로 포트 전달 (PORT 환경변수) local env_port="PORT=${port}" # ── [4] 코드 전송 ── log "[4/6] 코드 전송" # 원격 디렉토리 생성 run_ssh "$srv_user" "$srv_ip" "mkdir -p '${remote_path}' 2>/dev/null || (echo '${srv_pass}' | sudo -S mkdir -p '${remote_path}' && echo '${srv_pass}' | sudo -S chown \$(whoami) '${remote_path}') 2>/dev/null" # tar로 묶어서 전송 (.git, node_modules 등 제외) tar czf /tmp/_solmeca_deploy.tar.gz \ --exclude='.git' --exclude='node_modules' --exclude='__pycache__' \ --exclude='.venv' --exclude='venv' --exclude='.env' \ --exclude='target' --exclude='dist' --exclude='build' \ -C "${project_dir}" . 2>/dev/null scp -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" \ /tmp/_solmeca_deploy.tar.gz "${srv_user}@${srv_ip}:/tmp/_solmeca_deploy.tar.gz" 2>/dev/null run_ssh "$srv_user" "$srv_ip" "cd '${remote_path}' && tar xzf /tmp/_solmeca_deploy.tar.gz && rm -f /tmp/_solmeca_deploy.tar.gz" rm -f /tmp/_solmeca_deploy.tar.gz ok "코드 전송 완료" # ── [5] 설치 + systemd 서비스 생성 ── log "[5/6] 설치 & 서비스 등록" # 서비스 이름 local service_name="solmeca-${project_name}" # ExecStart 명령어 결정 local exec_cmd="$proj_cmd" if [ "$proj_type" = "python" ]; then exec_cmd=$(echo "$proj_cmd" | sed "s|python3|${remote_path}/venv/bin/python3|") fi # 로컬에서 서비스 파일 생성 cat > /tmp/_solmeca_service.tmp << EOF [Unit] Description=Solmeca - ${project_name} After=network.target [Service] Type=simple WorkingDirectory=${remote_path} Environment=${env_port} ExecStart=${exec_cmd} Restart=on-failure RestartSec=5 StartLimitIntervalSec=300 StartLimitBurst=5 StandardOutput=journal StandardError=journal SyslogIdentifier=${service_name} User=${srv_user} [Install] WantedBy=multi-user.target EOF # 서비스 파일 전송 scp -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" \ /tmp/_solmeca_service.tmp "${srv_user}@${srv_ip}:/tmp/_solmeca_service.tmp" 2>/dev/null rm -f /tmp/_solmeca_service.tmp # 원격: 의존성 설치 + 서비스 등록 + 시작 run_ssh "$srv_user" "$srv_ip" " cd '${remote_path}' if [ '${proj_type}' = 'python' ]; then python3 -m venv venv 2>/dev/null || true . venv/bin/activate 2>/dev/null || true pip install -q -r requirements.txt 2>/dev/null || pip install -q . 2>/dev/null || true elif [ '${proj_type}' = 'node' ]; then npm install --omit=dev 2>/dev/null || true elif [ '${proj_type}' = 'go' ]; then go build -o server . 2>/dev/null || true fi echo '${srv_pass}' | sudo -S cp /tmp/_solmeca_service.tmp /etc/systemd/system/${service_name}.service 2>/dev/null rm -f /tmp/_solmeca_service.tmp echo '${srv_pass}' | sudo -S systemctl daemon-reload 2>/dev/null echo '${srv_pass}' | sudo -S systemctl enable ${service_name} 2>/dev/null echo '${srv_pass}' | sudo -S systemctl restart ${service_name} 2>/dev/null " # 서비스 상태 확인 sleep 2 local svc_status svc_status=$(run_ssh "$srv_user" "$srv_ip" "systemctl is-active ${service_name}" 2>/dev/null || echo "failed") if [ "$svc_status" = "active" ]; then ok "서비스 실행 중: ${service_name}" else warn "서비스 상태: ${svc_status}" info "로그 확인: deploy.sh logs ${project_name}" fi # ── [6] 레지스트리 기록 ── log "[6/6] 배포 기록" save_deployment "$project_name" "$srv_name" "$srv_ip" "$port" "$proj_type" "$remote_path" "$srv_user" update_md ok "기록 완료" # 배포 후 실시간 정보 수집 sleep 1 local proc_info proc_info=$(run_ssh "$srv_user" "$srv_ip" " PID=\$(systemctl show solmeca-${project_name} --property=MainPID --value 2>/dev/null) if [ -n \"\$PID\" ] && [ \"\$PID\" != '0' ]; then UPTIME=\$(ps -o etime= -p \$PID 2>/dev/null | tr -d ' ') MEM=\$(ps -o rss= -p \$PID 2>/dev/null | tr -d ' ') CPU=\$(ps -o %cpu= -p \$PID 2>/dev/null | tr -d ' ') echo \"\$PID|\${UPTIME:-0}|\${MEM:-0}|\${CPU:-0}\" else echo '0|0|0|0' fi " 2>/dev/null || echo "0|0|0|0") local p_pid p_uptime p_mem p_cpu IFS='|' read -r p_pid p_uptime p_mem p_cpu <<< "$proc_info" local mem_mb=0 [ "${p_mem:-0}" -gt 0 ] 2>/dev/null && mem_mb=$((p_mem / 1024)) echo -e "\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" if [ "$svc_status" = "active" ]; then echo -e "${GREEN}${BOLD} Deploy OK${NC}" else echo -e "${YELLOW}${BOLD} Deployed (starting...)${NC}" fi echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" printf " %-12s %s\n" "App" "${project_name}" printf " %-12s %s\n" "Type" "${proj_type}" printf " %-12s %s (%s)\n" "Server" "${srv_name}" "${srv_ip}" printf " %-12s %s\n" "Port" "${port}" printf " %-12s %s\n" "URL" "http://${srv_ip}:${port}" printf " %-12s %s\n" "Status" "${svc_status}" printf " %-12s %s\n" "PID" "${p_pid}" printf " %-12s %s\n" "Uptime" "${p_uptime}" printf " %-12s %s%%\n" "CPU" "${p_cpu}" printf " %-12s %sMB\n" "Memory" "${mem_mb}" printf " %-12s %s\n" "Path" "${remote_path}" echo "" echo -e " ${DIM}deploy.sh logs ${project_name} -- view logs${NC}" echo -e " ${DIM}deploy.sh stop ${project_name} -- stop service${NC}" echo -e " ${DIM}deploy.sh rm ${project_name} -- remove service${NC}" echo "" } # ── status 명령 ─────────────────────────────────────────────────── status_cmd() { init_registry echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD}${CYAN} 서버 & 배포 상태${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" log "서버 상태" fetch_all_server_loads printf " ${BOLD}%-16s %6s %10s %6s %4s${NC}\n" "서버" "CPU" "RAM여유" "디스크" "GPU" echo " ─────────────────────────────────────────────" for server_info in "${SERVERS[@]}"; do IFS='|' read -r name ip _ cpus has_gpu _ <<< "$server_info" local cpu_free mem_total mem_avail _ _ disk_pct read -r cpu_free mem_total mem_avail _ _ disk_pct <<< "${SERVER_LOADS[$name]:-0 0 0 0 0 0}" local gpu_label=" " [ "$has_gpu" = "yes" ] && gpu_label="GPU" local cpu_color=$GREEN [ "${cpu_free:-0}" -lt 30 ] && cpu_color=$YELLOW [ "${cpu_free:-0}" -lt 10 ] && cpu_color=$RED printf " %-16s ${cpu_color}%5d%%${NC} %5d/%4dMB %5d%% %4s\n" \ "$name" "$cpu_free" "$mem_avail" "$mem_total" "$disk_pct" "$gpu_label" done echo "" log "배포 목록" local deployments deployments=$(python3 -c " import json reg=json.load(open('$REGISTRY_FILE')) if not reg['deployments']: print(' (없음)') else: print(f' {\"Name\":<20s} {\"Server\":<16s} {\"Port\":<6s} {\"Type\":<8s} {\"URL\"}') print(' ' + '-' * 70) for name, d in sorted(reg['deployments'].items()): print(f' {name:<20s} {d[\"server\"]:<16s} {d[\"port\"]:<6d} {d[\"type\"]:<8s} http://{d[\"ip\"]}:{d[\"port\"]}') " 2>/dev/null) echo "$deployments" # 실시간 서비스 상세 정보 echo "" log "Service Details" local has_services=false for server_info in "${SERVERS[@]}"; do IFS='|' read -r sname sip suser _ _ _ _ <<< "$server_info" local services services=$(run_ssh "$suser" "$sip" "systemctl list-units 'solmeca-*' --no-pager --plain 2>/dev/null | grep solmeca | awk '{print \$1, \$3}'" 2>/dev/null) if [ -n "$services" ]; then has_services=true while IFS= read -r line; do local svc_name svc_status svc_name=$(echo "$line" | awk '{print $1}' | sed 's/solmeca-//;s/.service//') svc_status=$(echo "$line" | awk '{print $2}') # 프로세스 상세 정보 조회 local proc_detail proc_detail=$(run_ssh "$suser" "$sip" " PID=\$(systemctl show solmeca-${svc_name} --property=MainPID --value 2>/dev/null) if [ -n \"\$PID\" ] && [ \"\$PID\" != '0' ]; then UPTIME=\$(ps -o etime= -p \$PID 2>/dev/null | tr -d ' ') MEM=\$(ps -o rss= -p \$PID 2>/dev/null | tr -d ' ') CPU=\$(ps -o %cpu= -p \$PID 2>/dev/null | tr -d ' ') echo \"\$PID|\${UPTIME:-?}|\${MEM:-0}|\${CPU:-0}\" else echo '0|?|0|0' fi " 2>/dev/null || echo "0|?|0|0") local p_pid p_up p_mem p_cpu IFS='|' read -r p_pid p_up p_mem p_cpu <<< "$proc_detail" local mem_mb=0 [ "${p_mem:-0}" -gt 0 ] 2>/dev/null && mem_mb=$((p_mem / 1024)) # 레지스트리에서 포트 조회 local reg_port reg_port=$(python3 -c " import json reg=json.load(open('$REGISTRY_FILE')) d=reg['deployments'].get('$svc_name',{}) print(d.get('port','?')) " 2>/dev/null || echo "?") if [ "$svc_status" = "active" ]; then echo -e " ${GREEN}[ON]${NC} ${BOLD}${svc_name}${NC}" else echo -e " ${RED}[--]${NC} ${BOLD}${svc_name}${NC}" fi echo -e " Server: ${sname} (${sip})" echo -e " URL: ${CYAN}http://${sip}:${reg_port}${NC}" echo -e " PID: ${p_pid} Uptime: ${p_up} CPU: ${p_cpu}% RAM: ${mem_mb}MB" echo "" done <<< "$services" fi done if ! $has_services; then echo " (no services deployed)" fi echo "" } # ── list 명령 ───────────────────────────────────────────────────── list_cmd() { init_registry python3 -c " import json reg=json.load(open('$REGISTRY_FILE')) if not reg['deployments']: print('배포된 프로젝트가 없습니다.') else: for name, d in sorted(reg['deployments'].items()): print(f\"{name:20s} {d['server']:16s} 포트:{d['port']:<5d} {d['type']:8s} http://{d['ip']}:{d['port']}\") " 2>/dev/null } # ── stop 명령 ───────────────────────────────────────────────────── stop_cmd() { local name=$1 init_registry local info info=$(get_deployment_info "$name") if [ -z "$info" ]; then err "배포를 찾을 수 없습니다: $name" return 1 fi IFS='|' read -r _ ip _ user _ <<< "$info" # 서버 비밀번호 찾기 local pass="" for s in "${SERVERS[@]}"; do IFS='|' read -r _ si _ sp _ _ _ <<< "$s"; [ "$si" = "$ip" ] && pass=$sp && break; done run_ssh "$user" "$ip" "echo '${pass}' | sudo -S systemctl stop solmeca-${name}" 2>/dev/null ok "중지됨: ${name}" } # ── rm 명령 ─────────────────────────────────────────────────────── rm_cmd() { local name=$1 init_registry local info info=$(get_deployment_info "$name") if [ -z "$info" ]; then err "배포를 찾을 수 없습니다: $name" return 1 fi IFS='|' read -r _ ip _ user path <<< "$info" local pass="" for s in "${SERVERS[@]}"; do IFS='|' read -r _ si _ sp _ _ _ <<< "$s"; [ "$si" = "$ip" ] && pass=$sp && break; done run_ssh "$user" "$ip" " echo '${pass}' | sudo -S systemctl stop solmeca-${name} 2>/dev/null echo '${pass}' | sudo -S systemctl disable solmeca-${name} 2>/dev/null echo '${pass}' | sudo -S rm -f /etc/systemd/system/solmeca-${name}.service echo '${pass}' | sudo -S systemctl daemon-reload echo '${pass}' | sudo -S rm -rf '${path}' " 2>/dev/null remove_deployment "$name" update_md ok "삭제됨: ${name}" } # ── logs 명령 ───────────────────────────────────────────────────── logs_cmd() { local name=$1 init_registry local info info=$(get_deployment_info "$name") if [ -z "$info" ]; then err "배포를 찾을 수 없습니다: $name" return 1 fi IFS='|' read -r _ ip _ user _ <<< "$info" local pass="" for s in "${SERVERS[@]}"; do IFS='|' read -r _ si _ sp _ _ _ <<< "$s"; [ "$si" = "$ip" ] && pass=$sp && break; done run_ssh "$user" "$ip" "echo '${pass}' | sudo -S journalctl -u solmeca-${name} -n 50 --no-pager" } # ── 사용법 ──────────────────────────────────────────────────────── usage() { echo -e "${BOLD}${CYAN}솔메카 Deploy v4.0${NC}\n" echo "사용법:" echo " deploy.sh . 현재 프로젝트 배포" echo " deploy.sh status 서버/배포 상태" echo " deploy.sh list 배포 목록" echo " deploy.sh logs <이름> 로그 보기" echo " deploy.sh stop <이름> 서비스 중지" echo " deploy.sh rm <이름> 서비스 삭제" echo "" echo "지원 프로젝트:" echo " Python (main.py, bot.py, app.py + requirements.txt)" echo " Node.js (server.js, bot.js + package.json)" echo " Go (main.go + go.mod)" echo " Rust (src/main.rs + Cargo.toml)" echo " Static (index.html)" } # ── 실행 ────────────────────────────────────────────────────────── case "${1:---help}" in .) deploy "$(pwd)" ;; status|st) status_cmd ;; list|ls) list_cmd ;; stop) stop_cmd "${2:?이름을 입력하세요}" ;; rm|remove) rm_cmd "${2:?이름을 입력하세요}" ;; logs|log) logs_cmd "${2:?이름을 입력하세요}" ;; -h|--help|help) usage ;; *) deploy "$1" ;; esac