update 03-22 09:28

This commit is contained in:
2026-03-22 09:28:14 +09:00
commit 7f45211276
43 changed files with 9373 additions and 0 deletions

770
deploy.sh Normal file
View File

@@ -0,0 +1,770 @@
#!/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