From a3fd18b1b02f2f43e2f24f95b69f50e314c2d2fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 10:44:05 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=95=EB=A6=AC=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=9E=90=EB=8F=99=ED=99=94=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테스트 후 생성된 데이터를 쉽게 정리할 수 있도록 문서와 스크립트 추가 추가 파일: - CLEANUP_TEST_DATA.md: 상세한 정리 가이드 * farmq.db 약국 삭제 방법 * gateway.db 사용자 삭제 방법 * Headscale 노드 삭제 방법 * 백업 및 복구 가이드 - cleanup-test-data.sh: 대화형 정리 스크립트 * P0003 이후 약국 자동 삭제 * ID 5 이후 사용자 자동 삭제 * Headscale 노드 선택 삭제 * 백업 생성 옵션 * 안전 확인 프롬프트 변경 파일: - README.md: Headscale 섹션 업데이트 * 자동 등록 스크립트 설명 추가 * 테스트 데이터 정리 가이드 링크 추가 사용 예시: ```bash # 대화형 정리 bash cleanup-test-data.sh # 원격 실행 curl -fsSL https://.../cleanup-test-data.sh | bash ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLEANUP_TEST_DATA.md | 285 ++++++++++++++++ README.md | 50 ++- cleanup-test-data.sh | 257 +++++++++++++++ headscale-quick-install.sh.backup | 519 ++++++++++++++++++++++++++++++ 4 files changed, 1099 insertions(+), 12 deletions(-) create mode 100644 CLEANUP_TEST_DATA.md create mode 100755 cleanup-test-data.sh create mode 100755 headscale-quick-install.sh.backup diff --git a/CLEANUP_TEST_DATA.md b/CLEANUP_TEST_DATA.md new file mode 100644 index 0000000..8246a9b --- /dev/null +++ b/CLEANUP_TEST_DATA.md @@ -0,0 +1,285 @@ +# 테스트 데이터 정리 가이드 + +## 개요 + +headscale 자동 등록 스크립트 테스트 시 생성되는 데이터를 정리하는 방법 + +## 정리해야 할 데이터 + +1. **farmq.db**: 테스트 약국 데이터 (P0003 이후) +2. **gateway.db**: 테스트 사용자 계정 (ID 5 이후) +3. **Headscale**: 테스트 VPN 노드 + +--- + +## 1. farmq.db 테스트 약국 삭제 + +### 수동 삭제 (Python) + +```bash +cd /srv/headscale-tailscale-replacement/farmq-admin + +python3 << 'EOF' +import sqlite3 + +conn = sqlite3.connect('farmq.db') +cursor = conn.cursor() + +print('현재 약국 목록:') +cursor.execute('SELECT pharmacy_code, pharmacy_name, tailscale_ip FROM pharmacies') +for row in cursor.fetchall(): + print(f' {row[0]}: {row[1]} - {row[2]}') + +# P0003 이후 약국 삭제 (테스트 데이터) +print('\nP0003 이후 약국 삭제 중...') +cursor.execute("DELETE FROM pharmacies WHERE pharmacy_code >= 'P0003' AND pharmacy_code < 'P1000'") +deleted_count = cursor.rowcount + +conn.commit() + +print(f'✓ {deleted_count}개 약국 삭제 완료') + +print('\n남은 약국 목록:') +cursor.execute('SELECT pharmacy_code, pharmacy_name, tailscale_ip FROM pharmacies') +for row in cursor.fetchall(): + print(f' {row[0]}: {row[1]} - {row[2]}') + +conn.close() +EOF +``` + +### 특정 약국만 삭제 + +```bash +cd /srv/headscale-tailscale-replacement/farmq-admin + +python3 << 'EOF' +import sqlite3 + +conn = sqlite3.connect('farmq.db') +cursor = conn.cursor() + +# 삭제할 약국 코드 지정 +pharmacy_codes = ['P0003', 'P0004', 'P0005', 'P0006'] + +for code in pharmacy_codes: + cursor.execute('DELETE FROM pharmacies WHERE pharmacy_code = ?', (code,)) + print(f'✓ {code} 삭제 완료') + +conn.commit() +conn.close() +EOF +``` + +--- + +## 2. gateway.db 테스트 사용자 삭제 + +### 수동 삭제 (Python) + +```bash +cd /srv/pharmq-gateway + +python3 << 'EOF' +import sqlite3 + +conn = sqlite3.connect('gateway.db') +cursor = conn.cursor() + +print('현재 사용자 목록:') +cursor.execute('SELECT id, username, name, primary_pharmacy_code FROM users') +for row in cursor.fetchall(): + print(f' ID {row[0]}: {row[1]} ({row[2]}) - {row[3]}') + +# ID 5 이후 사용자 삭제 (테스트 데이터) +print('\nID 5 이후 사용자 삭제 중...') +cursor.execute('DELETE FROM pharmacy_members WHERE user_id >= 5') +cursor.execute('DELETE FROM users WHERE id >= 5') +deleted_count = cursor.rowcount + +conn.commit() + +print(f'✓ {deleted_count}명 사용자 삭제 완료') + +print('\n남은 사용자 목록:') +cursor.execute('SELECT id, username, name, primary_pharmacy_code FROM users') +for row in cursor.fetchall(): + print(f' ID {row[0]}: {row[1]} ({row[2]}) - {row[3]}') + +conn.close() +EOF +``` + +### 특정 사용자만 삭제 + +```bash +cd /srv/pharmq-gateway + +python3 << 'EOF' +import sqlite3 + +conn = sqlite3.connect('gateway.db') +cursor = conn.cursor() + +# 삭제할 사용자 ID 지정 +user_ids = [5, 6, 7, 8] + +for user_id in user_ids: + cursor.execute('DELETE FROM pharmacy_members WHERE user_id = ?', (user_id,)) + cursor.execute('DELETE FROM users WHERE id = ?', (user_id,)) + print(f'✓ ID {user_id} 삭제 완료') + +conn.commit() +conn.close() +EOF +``` + +--- + +## 3. Headscale 테스트 노드 삭제 + +### 노드 목록 확인 + +```bash +docker exec headscale headscale nodes list +``` + +### 특정 노드 삭제 (ID 기준) + +```bash +# 단일 노드 삭제 +docker exec headscale headscale nodes delete --identifier --force + +# 예시: ID 13 삭제 +docker exec headscale headscale nodes delete --identifier 13 --force +``` + +### 여러 노드 한번에 삭제 + +```bash +# 삭제할 노드 ID들 +NODE_IDS=(13 14 15 16 17 18) + +for id in "${NODE_IDS[@]}"; do + docker exec headscale headscale nodes delete --identifier $id --force + echo "✓ Node ID $id 삭제 완료" +done +``` + +### 특정 이름 패턴 노드 삭제 + +```bash +# pve-로 시작하는 테스트 노드 찾기 +docker exec headscale headscale nodes list | grep "pve-" + +# 해당 노드들의 ID를 확인한 후 수동으로 삭제 +docker exec headscale headscale nodes delete --identifier --force +``` + +--- + +## 자동화 스크립트 + +### 통합 정리 스크립트 실행 + +```bash +bash /srv/install_scripts/pve9-repo-fix/cleanup-test-data.sh +``` + +**또는 직접 다운로드하여 실행:** + +```bash +curl -fsSL https://raw.githubusercontent.com/thug0bin/pve9-repo-fix/main/cleanup-test-data.sh | bash +``` + +--- + +## 사용 시나리오 + +### 시나리오 1: 전체 테스트 데이터 정리 + +```bash +# 1. farmq.db 정리 (P0003 이후) +cd /srv/headscale-tailscale-replacement/farmq-admin +python3 -c "import sqlite3; conn = sqlite3.connect('farmq.db'); cursor = conn.cursor(); cursor.execute(\"DELETE FROM pharmacies WHERE pharmacy_code >= 'P0003' AND pharmacy_code < 'P1000'\"); conn.commit(); print(f'✓ {cursor.rowcount}개 약국 삭제'); conn.close()" + +# 2. gateway.db 정리 (ID 5 이후) +cd /srv/pharmq-gateway +python3 -c "import sqlite3; conn = sqlite3.connect('gateway.db'); cursor = conn.cursor(); cursor.execute('DELETE FROM pharmacy_members WHERE user_id >= 5'); cursor.execute('DELETE FROM users WHERE id >= 5'); conn.commit(); print(f'✓ {cursor.rowcount}명 사용자 삭제'); conn.close()" + +# 3. Headscale 노드 정리 (수동 확인 후) +docker exec headscale headscale nodes list +# 테스트 노드 ID 확인 후 삭제 +``` + +### 시나리오 2: 특정 약국 코드 범위만 정리 + +```bash +# P0005~P0010 범위만 삭제 +cd /srv/headscale-tailscale-replacement/farmq-admin +python3 -c "import sqlite3; conn = sqlite3.connect('farmq.db'); cursor = conn.cursor(); cursor.execute(\"DELETE FROM pharmacies WHERE pharmacy_code >= 'P0005' AND pharmacy_code <= 'P0010'\"); conn.commit(); print(f'✓ {cursor.rowcount}개 약국 삭제'); conn.close()" +``` + +--- + +## 주의사항 + +⚠️ **운영 데이터 보호** +- P001, P002, P0002는 운영 약국이므로 삭제하지 마세요 +- ID 1~4 사용자는 운영 계정이므로 삭제하지 마세요 +- 삭제 전 반드시 데이터를 확인하세요 + +⚠️ **백업 권장** +```bash +# farmq.db 백업 +cp /srv/headscale-tailscale-replacement/farmq-admin/farmq.db /srv/headscale-tailscale-replacement/farmq-admin/farmq.db.backup + +# gateway.db 백업 +cp /srv/pharmq-gateway/gateway.db /srv/pharmq-gateway/gateway.db.backup +``` + +⚠️ **Headscale 노드 삭제** +- offline 상태의 노드만 삭제 권장 +- online 노드 삭제 시 연결이 끊어집니다 + +--- + +## 트러블슈팅 + +### 문제: "database is locked" 에러 + +**원인**: Flask 서버가 DB를 사용 중 + +**해결**: +```bash +# farmq-admin 재시작 +cd /srv/headscale-tailscale-replacement/farmq-admin +pkill -f "flask run" +source venv/bin/activate +flask run --host=0.0.0.0 --port=5001 & + +# gateway 재시작 +cd /srv/pharmq-gateway +pkill -f "uvicorn" +source venv/bin/activate +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload & +``` + +### 문제: Python 스크립트 실행 안 됨 + +**해결**: 경로 확인 +```bash +# 올바른 경로로 이동 +cd /srv/headscale-tailscale-replacement/farmq-admin # farmq.db +cd /srv/pharmq-gateway # gateway.db +``` + +--- + +## 참고 + +- farmq.db 위치: `/srv/headscale-tailscale-replacement/farmq-admin/farmq.db` +- gateway.db 위치: `/srv/pharmq-gateway/gateway.db` +- Headscale: Docker 컨테이너 `headscale` +- 약국 코드 형식: P0001~P9999 (P + 4자리 숫자) +- 사용자 계정: pharmacy_code 소문자 (예: P0003 → p0003) diff --git a/README.md b/README.md index 4f187ab..f799f9d 100644 --- a/README.md +++ b/README.md @@ -172,27 +172,53 @@ claude-code # Claude Code 시작 claude-code --help # 도움말 보기 ``` -## 🌐 Headscale 빠른 설치 +## 🌐 Headscale VPN 등록 및 약국 자동 생성 -**Tailscale 대체 네트워크** 클라이언트 등록: +**Headscale VPN 등록 + 약국/계정 자동 생성** 올인원 스크립트: -### 빠른 설치 +### 자동 등록 스크립트 (권장) +```bash +curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/headscale-auto-register.sh | bash +``` + +**기능:** +- ✅ Headscale VPN 자동 등록 +- ✅ farmq.db에 약국 자동 생성 (P0003, P0004...) +- ✅ gateway.db에 관리자 계정 자동 생성 +- ✅ 즉시 프론트엔드 로그인 가능 +- ✅ 로그인 정보 자동 출력 + +### VPN만 등록 (계정 생성 없이) ```bash curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/headscale-quick-install.sh | bash ``` -### 수동 설치 -```bash -wget https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/headscale-quick-install.sh -chmod +x headscale-quick-install.sh -./headscale-quick-install.sh -``` - -### 설치 내용 -- ✅ **Headscale 클라이언트** 등록 +**기능:** +- ✅ Headscale 클라이언트 등록만 수행 - ✅ PBS 서버 등록 전 필수 네트워크 설정 - ✅ Proxmox 환경 통합 +### 테스트 데이터 정리 +스크립트 테스트 후 생성된 데이터 정리: + +```bash +curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/cleanup-test-data.sh | bash +``` + +**또는:** + +```bash +bash /srv/install_scripts/pve9-repo-fix/cleanup-test-data.sh +``` + +**정리 내용:** +- 🗑️ farmq.db에서 P0003 이후 테스트 약국 삭제 +- 🗑️ gateway.db에서 ID 5 이후 테스트 사용자 삭제 +- 🗑️ Headscale 테스트 노드 삭제 (선택) +- 💾 백업 생성 옵션 + +📖 **자세한 정리 가이드**: [CLEANUP_TEST_DATA.md](CLEANUP_TEST_DATA.md) + ## 💾 Proxmox Backup Server 올인원 설치 **PBS 서버 구축 및 Proxmox VE 통합**을 한 번에: diff --git a/cleanup-test-data.sh b/cleanup-test-data.sh new file mode 100755 index 0000000..fefcdd8 --- /dev/null +++ b/cleanup-test-data.sh @@ -0,0 +1,257 @@ +#!/bin/bash + +# ================================ +# 테스트 데이터 정리 스크립트 +# ================================ +# +# 용도: headscale 자동 등록 테스트 후 생성된 데이터 정리 +# - farmq.db: P0003 이후 약국 삭제 +# - gateway.db: ID 5 이후 사용자 삭제 +# - Headscale: 테스트 노드 삭제 (선택) +# + +set -e + +# ================================ +# 색상 정의 +# ================================ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +NC='\033[0m' # No Color + +# ================================ +# 헤더 출력 +# ================================ +print_header() { + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${WHITE}$1${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# ================================ +# 1. farmq.db 테스트 약국 삭제 +# ================================ +cleanup_farmq_db() { + print_header "1. farmq.db 테스트 약국 정리" + + FARMQ_DB="/srv/headscale-tailscale-replacement/farmq-admin/farmq.db" + + if [ ! -f "$FARMQ_DB" ]; then + echo -e "${RED}✗ farmq.db를 찾을 수 없습니다: $FARMQ_DB${NC}" + return 1 + fi + + echo -e "${BLUE}현재 약국 목록:${NC}" + python3 << EOF +import sqlite3 +conn = sqlite3.connect('$FARMQ_DB') +cursor = conn.cursor() +cursor.execute('SELECT pharmacy_code, pharmacy_name, tailscale_ip FROM pharmacies ORDER BY pharmacy_code') +for row in cursor.fetchall(): + print(f' {row[0]}: {row[1]} - {row[2]}') +conn.close() +EOF + + echo -e "\n${YELLOW}P0003 이후 약국을 삭제하시겠습니까? (y/N)${NC}" + read -p "> " -r response = 'P0003' AND pharmacy_code < 'P1000'") +deleted_count = cursor.rowcount +conn.commit() +conn.close() +print(f'✓ {deleted_count}개 약국 삭제 완료') +EOF + + echo -e "\n${GREEN}남은 약국 목록:${NC}" + python3 << EOF +import sqlite3 +conn = sqlite3.connect('$FARMQ_DB') +cursor = conn.cursor() +cursor.execute('SELECT pharmacy_code, pharmacy_name, tailscale_ip FROM pharmacies ORDER BY pharmacy_code') +for row in cursor.fetchall(): + print(f' {row[0]}: {row[1]} - {row[2]}') +conn.close() +EOF + else + echo -e "${YELLOW}건너뜀${NC}" + fi +} + +# ================================ +# 2. gateway.db 테스트 사용자 삭제 +# ================================ +cleanup_gateway_db() { + print_header "2. gateway.db 테스트 사용자 정리" + + GATEWAY_DB="/srv/pharmq-gateway/gateway.db" + + if [ ! -f "$GATEWAY_DB" ]; then + echo -e "${RED}✗ gateway.db를 찾을 수 없습니다: $GATEWAY_DB${NC}" + return 1 + fi + + echo -e "${BLUE}현재 사용자 목록:${NC}" + python3 << EOF +import sqlite3 +conn = sqlite3.connect('$GATEWAY_DB') +cursor = conn.cursor() +cursor.execute('SELECT id, username, name, primary_pharmacy_code FROM users ORDER BY id') +for row in cursor.fetchall(): + print(f' ID {row[0]}: {row[1]} ({row[2]}) - {row[3]}') +conn.close() +EOF + + echo -e "\n${YELLOW}ID 5 이후 사용자를 삭제하시겠습니까? (y/N)${NC}" + read -p "> " -r response = 5') +cursor.execute('DELETE FROM users WHERE id >= 5') +deleted_count = cursor.rowcount +conn.commit() +conn.close() +print(f'✓ {deleted_count}명 사용자 삭제 완료') +EOF + + echo -e "\n${GREEN}남은 사용자 목록:${NC}" + python3 << EOF +import sqlite3 +conn = sqlite3.connect('$GATEWAY_DB') +cursor = conn.cursor() +cursor.execute('SELECT id, username, name, primary_pharmacy_code FROM users ORDER BY id') +for row in cursor.fetchall(): + print(f' ID {row[0]}: {row[1]} ({row[2]}) - {row[3]}') +conn.close() +EOF + else + echo -e "${YELLOW}건너뜀${NC}" + fi +} + +# ================================ +# 3. Headscale 테스트 노드 삭제 +# ================================ +cleanup_headscale_nodes() { + print_header "3. Headscale 테스트 노드 정리" + + # Docker 컨테이너 확인 + if ! docker ps | grep -q headscale; then + echo -e "${RED}✗ Headscale 컨테이너를 찾을 수 없습니다${NC}" + return 1 + fi + + echo -e "${BLUE}현재 노드 목록:${NC}" + docker exec headscale headscale nodes list + + echo -e "\n${YELLOW}삭제할 노드 ID를 입력하세요 (공백으로 구분, Enter로 건너뛰기):${NC}" + echo -e "${YELLOW}예: 13 14 15 16${NC}" + read -p "> " -r node_ids /dev/null; then + echo -e "${GREEN}✓ Node ID $id 삭제 완료${NC}" + else + echo -e "${RED}✗ Node ID $id 삭제 실패${NC}" + fi + done + + echo -e "\n${GREEN}남은 노드 목록:${NC}" + docker exec headscale headscale nodes list +} + +# ================================ +# 4. 백업 생성 +# ================================ +create_backup() { + print_header "백업 생성 (선택)" + + echo -e "${YELLOW}데이터베이스 백업을 생성하시겠습니까? (y/N)${NC}" + read -p "> " -r response /dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + print_error "curl 또는 wget이 필요합니다." + exit 1 + fi + + # 네트워크 연결 확인 + if ! ping -c 1 8.8.8.8 >/dev/null 2>&1; then + print_warning "인터넷 연결을 확인해주세요." + fi + + print_success "시스템 요구사항 확인 완료" +} + +# ================================ +# Tailscale 설치 +# ================================ +install_tailscale() { + print_status "Tailscale 클라이언트 설치 중..." + + # 이미 설치되어 있는지 확인 + if command -v tailscale >/dev/null 2>&1; then + print_info "Tailscale이 이미 설치되어 있습니다." + TAILSCALE_VERSION=$(tailscale version | head -n1) + print_info "현재 버전: $TAILSCALE_VERSION" + return + fi + + case $OS in + ubuntu|debian) + print_info "Ubuntu/Debian용 Tailscale 설치 중..." + + # GPG 키 추가 + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list + + # 패키지 설치 + apt-get update -qq + apt-get install -y tailscale + ;; + + centos|rhel|rocky|almalinux) + print_info "CentOS/RHEL/Rocky용 Tailscale 설치 중..." + + # 리포지토리 추가 + curl -fsSL https://pkgs.tailscale.com/stable/rhel/tailscale.repo | tee /etc/yum.repos.d/tailscale.repo + + # 패키지 설치 + if command -v dnf >/dev/null 2>&1; then + dnf install -y tailscale + else + yum install -y tailscale + fi + ;; + + fedora) + print_info "Fedora용 Tailscale 설치 중..." + dnf install -y tailscale + ;; + + arch) + print_info "Arch Linux용 Tailscale 설치 중..." + pacman -S --noconfirm tailscale + ;; + + *) + print_warning "지원하지 않는 배포판입니다. 수동 설치를 시도합니다." + # Universal binary 다운로드 + ARCH=$(uname -m) + case $ARCH in + x86_64) TAILSCALE_ARCH="amd64" ;; + aarch64) TAILSCALE_ARCH="arm64" ;; + armv7l) TAILSCALE_ARCH="arm" ;; + *) + print_error "지원하지 않는 아키텍처: $ARCH" + exit 1 + ;; + esac + + # 최신 버전 다운로드 + TAILSCALE_VERSION=$(curl -s https://api.github.com/repos/tailscale/tailscale/releases/latest | grep '"tag_name"' | cut -d'"' -f4) + DOWNLOAD_URL="https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}.tgz" + + cd /tmp + curl -LO "$DOWNLOAD_URL" + tar xzf "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}.tgz" + + # 바이너리 복사 + cp "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}/tailscale" /usr/bin/ + cp "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}/tailscaled" /usr/sbin/ + + # 시스템 서비스 파일 생성 + cat > /etc/systemd/system/tailscaled.service << 'EOF' +[Unit] +Description=Tailscale node agent +Documentation=https://tailscale.com/kb/ +Wants=network-pre.target +After=network-pre.target NetworkManager.service systemd-resolved.service + +[Service] +EnvironmentFile=/etc/default/tailscaled +ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=$PORT $FLAGS +ExecStopPost=/usr/bin/tailscale logout +Restart=on-failure +RestartSec=5 + +Type=notify +RuntimeDirectory=tailscale +RuntimeDirectoryMode=0755 +StateDirectory=tailscale +StateDirectoryMode=0700 +CacheDirectory=tailscale +CacheDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target +EOF + + # 환경 설정 파일 + mkdir -p /etc/default + echo 'FLAGS=""' > /etc/default/tailscaled + echo 'PORT="41641"' >> /etc/default/tailscaled + + systemctl daemon-reload + ;; + esac + + print_success "Tailscale 설치 완료" + + # 버전 확인 + TAILSCALE_VERSION=$(tailscale version | head -n1) + print_info "설치된 버전: $TAILSCALE_VERSION" +} + +# ================================ +# Tailscale 서비스 시작 +# ================================ +start_tailscale() { + print_status "Tailscale 서비스 시작 중..." + + # systemd 서비스 활성화 및 시작 + systemctl enable tailscaled >/dev/null 2>&1 || true + systemctl start tailscaled >/dev/null 2>&1 || true + + # 서비스 상태 확인 + sleep 3 + if systemctl is-active --quiet tailscaled; then + print_success "Tailscaled 서비스가 실행 중입니다." + else + print_error "Tailscaled 서비스 시작에 실패했습니다." + print_info "수동으로 시작을 시도합니다..." + /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state & + sleep 5 + fi +} + +# ================================ +# Headscale 등록 +# ================================ +register_headscale() { + print_status "Headscale 서버에 등록 중..." + + # 기존 연결 확인 + if tailscale status >/dev/null 2>&1; then + print_warning "이미 Tailscale/Headscale에 연결되어 있습니다." + + # 현재 연결 상태 표시 + CURRENT_STATUS=$(tailscale status 2>/dev/null | head -5) + print_info "현재 연결 상태:" + echo "$CURRENT_STATUS" + + # 현재 서버 확인 + CURRENT_SERVER=$(tailscale status --json 2>/dev/null | grep -o '"CurrentTailnet":[^,]*' | cut -d'"' -f4 2>/dev/null || echo "알 수 없음") + TARGET_SERVER=$(echo "$HEADSCALE_SERVER" | sed 's|https\?://||' | sed 's|:[0-9]*||') + + print_info "현재 서버: $CURRENT_SERVER" + print_info "대상 서버: $TARGET_SERVER" + + # 강제 등록 옵션 확인 + if [ "$FORCE_REGISTER" = true ]; then + print_warning "강제 재등록 옵션이 활성화되었습니다." + print_info "기존 연결을 해제하고 재등록합니다..." + tailscale logout >/dev/null 2>&1 || true + sleep 3 + # 같은 서버인지 확인 + elif [[ "$CURRENT_SERVER" == *"$TARGET_SERVER"* ]] || [[ "$TARGET_SERVER" == *"$CURRENT_SERVER"* ]]; then + print_success "이미 올바른 Headscale 서버에 연결되어 있습니다!" + print_info "등록을 건너뜁니다." + return 0 + # 대화형 실행인지 확인 (터미널에서 직접 실행) + elif [ -t 0 ] && [ -t 1 ]; then + print_warning "다른 서버에 연결되어 있습니다." + echo -n "기존 연결을 해제하고 팜큐 Headscale로 등록하시겠습니까? (Y/n): " + read -r REPLY + + # 기본값을 Y로 변경 (엔터만 누르면 Y) + if [[ -z "$REPLY" ]] || [[ $REPLY =~ ^[Yy]$ ]]; then + print_info "기존 연결을 해제합니다..." + tailscale logout >/dev/null 2>&1 || true + sleep 3 + else + print_info "등록을 건너뜁니다." + return 0 + fi + else + # 파이프 실행 시 자동으로 재등록 (기본값: Y) + print_warning "다른 서버에 연결되어 있어 자동으로 팜큐 Headscale로 재등록합니다." + print_info "기존 연결을 해제합니다..." + tailscale logout >/dev/null 2>&1 || true + sleep 3 + fi + + # 추가 확인: 완전히 로그아웃되었는지 검증 + print_status "연결 해제 확인 중..." + for i in {1..10}; do + if ! tailscale status >/dev/null 2>&1; then + print_success "기존 연결이 완전히 해제되었습니다." + break + fi + print_info "로그아웃 대기 중... ($i/10)" + sleep 2 + + if [ $i -eq 10 ]; then + print_warning "로그아웃이 완료되지 않았지만 계속 진행합니다." + fi + done + fi + + print_info "Headscale 서버: $HEADSCALE_SERVER" + print_info "Pre-auth Key: ${PREAUTH_KEY:0:8}***************" + + # Headscale 등록 시도 + print_status "등록 명령 실행 중..." + + if tailscale up \ + --login-server="$HEADSCALE_SERVER" \ + --authkey="$PREAUTH_KEY" \ + --accept-routes \ + --accept-dns=true >/dev/null 2>&1; then + + print_success "Headscale 등록 성공!" + + else + print_error "자동 등록에 실패했습니다. 수동 등록을 진행합니다." + + # 수동 등록 모드 + print_info "다음 명령을 실행하여 수동 등록하세요:" + echo "" + echo "tailscale up --login-server=\"$HEADSCALE_SERVER\" --authkey=\"$PREAUTH_KEY\" --accept-routes --accept-dns=true" + echo "" + + # 등록 URL 시도 + REGISTER_URL=$(tailscale up --login-server="$HEADSCALE_SERVER" 2>&1 | grep -o 'https://[^[:space:]]*' | head -1) + if [ -n "$REGISTER_URL" ]; then + print_info "또는 다음 URL을 방문하여 등록하세요:" + echo "$REGISTER_URL" + fi + + return 1 + fi +} + +# ================================ +# 연결 상태 확인 +# ================================ +verify_connection() { + print_status "연결 상태 확인 중..." + + # 잠시 대기 (연결 안정화) + sleep 5 + + # Tailscale 상태 확인 + if ! tailscale status >/dev/null 2>&1; then + print_error "Tailscale 연결에 문제가 있습니다." + return 1 + fi + + # IP 주소 확인 + TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "N/A") + TAILSCALE_IP6=$(tailscale ip -6 2>/dev/null || echo "N/A") + + print_success "Headscale 네트워크 연결 완료!" + print_info "할당된 IPv4: $TAILSCALE_IP" + print_info "할당된 IPv6: $TAILSCALE_IP6" + + # 네트워크 테스트 + print_status "네트워크 연결 테스트 중..." + + if ping -c 3 -W 5 100.64.0.1 >/dev/null 2>&1; then + print_success "팜큐 네트워크($FARMQ_NETWORK) 연결 정상!" + else + print_warning "네트워크 테스트 실패. 방화벽을 확인해주세요." + fi + + # 연결된 노드 확인 + print_info "네트워크 상태:" + tailscale status | head -10 +} + +# ================================ +# 방화벽 설정 (선택사항) +# ================================ +configure_firewall() { + print_status "방화벽 설정 확인 중..." + + # UFW (Ubuntu/Debian) + if command -v ufw >/dev/null 2>&1; then + print_info "UFW 방화벽 감지됨" + if ufw status | grep -q "Status: active"; then + print_info "Tailscale 트래픽 허용 중..." + ufw allow in on tailscale0 >/dev/null 2>&1 || true + ufw allow 41641/udp comment "Tailscale" >/dev/null 2>&1 || true + fi + fi + + # firewalld (CentOS/RHEL/Fedora) + if command -v firewall-cmd >/dev/null 2>&1; then + print_info "firewalld 방화벽 감지됨" + if firewall-cmd --state >/dev/null 2>&1; then + print_info "Tailscale 트래픽 허용 중..." + firewall-cmd --permanent --add-service=tailscale >/dev/null 2>&1 || true + firewall-cmd --permanent --add-port=41641/udp >/dev/null 2>&1 || true + firewall-cmd --reload >/dev/null 2>&1 || true + fi + fi + + print_success "방화벽 설정 완료" +} + +# ================================ +# 정리 작업 +# ================================ +cleanup() { + print_status "정리 작업 수행 중..." + + # 임시 파일 정리 + rm -rf /tmp/tailscale_* >/dev/null 2>&1 || true + + # 시스템 정보 업데이트 + if command -v updatedb >/dev/null 2>&1; then + updatedb >/dev/null 2>&1 & + fi + + print_success "정리 작업 완료" +} + +# ================================ +# 최종 정보 출력 +# ================================ +show_final_info() { + print_header "팜큐 Headscale 설치 완료!" + + # 시스템 정보 + HOSTNAME=$(hostname) + TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "N/A") + + echo -e "${GREEN}🎉 설치가 성공적으로 완료되었습니다!${NC}\n" + + echo -e "${CYAN}📋 시스템 정보:${NC}" + echo -e " 호스트명: $HOSTNAME" + echo -e " Tailscale IP: $TAILSCALE_IP" + echo -e " OS: $OS $VERSION" + echo -e " Headscale 서버: $HEADSCALE_SERVER" + + echo -e "\n${YELLOW}🔧 유용한 명령어:${NC}" + echo -e " tailscale status # 연결 상태 확인" + echo -e " tailscale ip # 할당된 IP 확인" + echo -e " tailscale ping # 다른 노드와 연결 테스트" + echo -e " tailscale logout # 네트워크에서 해제" + + echo -e "\n${PURPLE}🌐 팜큐 관리자 페이지:${NC}" + echo -e " http://192.168.0.151:5002" + echo -e " http://192.168.0.151:5002/vms (VM 관리)" + + echo -e "\n${WHITE}문제가 있을 경우 로그를 확인하세요:${NC}" + echo -e " journalctl -u tailscaled -f" + + print_header "설치 완료 - 팜큐 네트워크를 사용할 수 있습니다!" +} + +# ================================ +# 메인 함수 +# ================================ +main() { + print_header "팜큐(FARMQ) Headscale 원클릭 설치" + + # 사전 체크 + detect_os + check_requirements + + # 설치 과정 + install_tailscale + start_tailscale + register_headscale + + # 사후 설정 + configure_firewall + verify_connection + + # 정리 및 완료 + cleanup + show_final_info +} + +# ================================ +# 에러 핸들링 +# ================================ +trap 'echo -e "\n❌ 설치 중 오류가 발생했습니다. 로그를 확인해주세요."; exit 1' ERR + +# 스크립트 실행 +main "$@" \ No newline at end of file