Compare commits
77 Commits
b661f79ecd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
788346f2ae | ||
|
|
4621fdcb6d | ||
|
|
02ebc89fc1 | ||
|
|
3dad99747b | ||
|
|
b37b1281a5 | ||
|
|
05b01e111b | ||
|
|
16fd28662c | ||
|
|
3359ca04f6 | ||
|
|
1f2939cca4 | ||
|
|
8419c4271c | ||
|
|
4d1845c6bc | ||
|
|
9a662c1251 | ||
|
|
566280f4c5 | ||
|
|
dd53e869d0 | ||
|
|
36fdae3eb1 | ||
|
|
f89bbecdcd | ||
|
|
394a0b0a7c | ||
|
|
f0900204fb | ||
|
|
8d76dc8da2 | ||
|
|
a0e94682bf | ||
|
|
6834612deb | ||
|
|
d08ff19470 | ||
|
|
985ec18651 | ||
|
|
d0cd2b1137 | ||
|
|
93a2313d37 | ||
|
|
59a10f48e5 | ||
|
|
85c5f7f930 | ||
|
|
aede1e9197 | ||
|
|
545ad63b50 | ||
|
|
5fa7812009 | ||
|
|
d975723268 | ||
|
|
eb4097e66e | ||
|
|
1841d72ac3 | ||
|
|
86e34d6916 | ||
|
|
05a063eb1e | ||
|
|
267d262eb3 | ||
|
|
fbea9419c3 | ||
|
|
86eb7ea806 | ||
|
|
d02883256f | ||
|
|
e7d5dd02d2 | ||
|
|
1977d21a9b | ||
|
|
2bf3754d62 | ||
|
|
41e9fa1056 | ||
|
|
9c952449d9 | ||
|
|
85c5e1ec29 | ||
|
|
13b1da4ee0 | ||
|
|
ccd34c7f54 | ||
|
|
d0c8b26138 | ||
|
|
4934b0a8f9 | ||
| c6919abf1c | |||
| 1f926d6b35 | |||
| f5a7c7f695 | |||
|
|
4f7929fc61 | ||
| 02137c50a8 | |||
|
|
1c6e6dcf56 | ||
|
|
269350c1d2 | ||
|
|
d8ce36b8a4 | ||
|
|
8a391cabd8 | ||
|
|
987662f95b | ||
|
|
ae1782a6de | ||
|
|
e7485983cc | ||
|
|
c2b810c6fc | ||
|
|
a3fd18b1b0 | ||
|
|
38c6257180 | ||
|
|
c0973c622a | ||
|
|
9c9a25218e | ||
|
|
7020339867 | ||
|
|
9d4142ddb6 | ||
|
|
60be9daff4 | ||
| 7a793ea77d | |||
| ffa955a64f | |||
| 912410ba90 | |||
| a45be35543 | |||
| 187c90e29e | |||
| e309524709 | |||
| 1baba8626b | |||
| 2780b5cf14 |
285
CLEANUP_TEST_DATA.md
Normal file
285
CLEANUP_TEST_DATA.md
Normal file
@@ -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~P9999 약국 삭제 (P001, P002, P0001, P0002는 보호)
|
||||
print('\nP0003 이후 약국 삭제 중...')
|
||||
cursor.execute("DELETE FROM pharmacies WHERE pharmacy_code >= 'P0003' AND pharmacy_code <= 'P9999' AND LENGTH(pharmacy_code) = 5")
|
||||
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 <NODE_ID> --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 <ID> --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~P9999, P001/P002 보호)
|
||||
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 <= 'P9999' AND LENGTH(pharmacy_code) = 5\"); 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)
|
||||
538
FARMQ_ADMIN_INTEGRATION_ANALYSIS.md
Normal file
538
FARMQ_ADMIN_INTEGRATION_ANALYSIS.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# farmq-admin 통합 분석 및 개선 방안
|
||||
|
||||
## 📅 작성일
|
||||
**2025년 11월 14일**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 발견 사항
|
||||
|
||||
### farmq-admin이 이미 존재하고 실행 중!
|
||||
|
||||
```bash
|
||||
프로세스: /srv/headscale-tailscale-replacement/farmq-admin/venv/bin/python app.py
|
||||
포트: 5001
|
||||
상태: 실행 중 (Nov 05부터 계속 실행)
|
||||
```
|
||||
|
||||
**중요:** farmq-admin은 Flask 기반 웹 애플리케이션으로, **farmq.db를 관리하는 API를 이미 제공**하고 있습니다!
|
||||
|
||||
---
|
||||
|
||||
## 📊 farmq-admin 구조 분석
|
||||
|
||||
### 1. 실행 정보
|
||||
```bash
|
||||
위치: /srv/headscale-tailscale-replacement/farmq-admin/
|
||||
메인: app.py (Flask 애플리케이션)
|
||||
포트: 5001
|
||||
DB: farmq.db (SQLAlchemy ORM 사용)
|
||||
```
|
||||
|
||||
### 2. 주요 API 엔드포인트
|
||||
|
||||
#### 약국 관리 API
|
||||
```python
|
||||
POST /api/pharmacy # 새 약국 생성 ✨
|
||||
GET /api/pharmacy/<id> # 약국 정보 조회
|
||||
PUT /api/pharmacy/<id> # 약국 정보 수정
|
||||
DELETE /api/pharmacy/<id> # 약국 삭제
|
||||
```
|
||||
|
||||
### 3. 약국 생성 API 상세 (`POST /api/pharmacy`)
|
||||
|
||||
**요청 예시:**
|
||||
```json
|
||||
POST http://localhost:5001/api/pharmacy
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"pharmacy_name": "행복약국",
|
||||
"owner_name": "홍길동",
|
||||
"owner_phone": "010-1234-5678",
|
||||
"owner_email": "happy@pharmq.kr",
|
||||
"phone": "02-1234-5678",
|
||||
"address": "서울시 강남구...",
|
||||
"api_port": 8082
|
||||
}
|
||||
```
|
||||
|
||||
**응답 예시:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "약국 \"행복약국\" (코드: P004) 생성 완료",
|
||||
"pharmacy": {
|
||||
"id": 4,
|
||||
"pharmacy_code": "P004",
|
||||
"pharmacy_name": "행복약국",
|
||||
"tailscale_ip": null,
|
||||
"api_port": 8082,
|
||||
"status": "active",
|
||||
"owner_name": "홍길동",
|
||||
"owner_phone": "010-1234-5678",
|
||||
"owner_email": "happy@pharmq.kr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 자동 생성 로직 (이미 구현됨!)
|
||||
|
||||
```python
|
||||
# app.py Line 418-433
|
||||
# pharmacy_code 자동 생성 (P001~P999)
|
||||
last_pharmacy = farmq_session.query(PharmacyInfo)\
|
||||
.filter(PharmacyInfo.pharmacy_code.like('P%'))\
|
||||
.order_by(PharmacyInfo.pharmacy_code.desc())\
|
||||
.first()
|
||||
|
||||
if last_pharmacy and last_pharmacy.pharmacy_code:
|
||||
try:
|
||||
last_num = int(last_pharmacy.pharmacy_code[1:])
|
||||
new_num = last_num + 1
|
||||
except:
|
||||
new_num = 1
|
||||
else:
|
||||
new_num = 1
|
||||
|
||||
pharmacy_code = f"P{new_num:03d}" # P001, P002, P003...
|
||||
```
|
||||
|
||||
**결과:** 마지막 약국 코드를 찾아서 자동으로 +1 증가!
|
||||
|
||||
---
|
||||
|
||||
## 🔄 개선된 통합 방안
|
||||
|
||||
### 기존 계획 vs 새로운 발견
|
||||
|
||||
#### ❌ 기존 계획 (불필요)
|
||||
```
|
||||
Python 스크립트로 직접 farmq.db INSERT
|
||||
→ SQL 쿼리 작성
|
||||
→ 에러 핸들링 직접 구현
|
||||
→ 검증 로직 직접 구현
|
||||
```
|
||||
|
||||
#### ✅ 새로운 방안 (API 활용)
|
||||
```
|
||||
farmq-admin API 호출
|
||||
→ 이미 모든 로직 구현됨
|
||||
→ 검증, 에러 핸들링 완료
|
||||
→ pharmacy_code 자동 생성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 최종 개선 방안
|
||||
|
||||
### 방법 1: farmq-admin API 활용 (권장 ⭐)
|
||||
|
||||
#### 장점
|
||||
- ✅ 이미 구현된 API 활용
|
||||
- ✅ pharmacy_code 자동 생성
|
||||
- ✅ 검증 로직 포함
|
||||
- ✅ 에러 핸들링 완료
|
||||
- ✅ 유지보수 용이
|
||||
|
||||
#### 구현
|
||||
```bash
|
||||
# headscale-quick-install.sh에서 호출
|
||||
register_to_farmq_api() {
|
||||
print_status "farmq-admin API를 통해 약국 등록 중..."
|
||||
|
||||
# JSON 데이터 생성
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
"owner_name": "$OWNER_NAME",
|
||||
"owner_phone": "$OWNER_PHONE",
|
||||
"owner_email": "$OWNER_EMAIL",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"address": "$PHARMACY_ADDRESS",
|
||||
"api_port": 8082
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# API 호출
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
http://localhost:5001/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# 응답 확인
|
||||
SUCCESS=$(echo "$RESPONSE" | grep -o '"success"[[:space:]]*:[[:space:]]*true')
|
||||
|
||||
if [ -n "$SUCCESS" ]; then
|
||||
# pharmacy_code 추출
|
||||
PHARMACY_CODE=$(echo "$RESPONSE" | grep -o '"pharmacy_code"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
print_success "약국 등록 완료! 코드: $PHARMACY_CODE"
|
||||
|
||||
# VPN IP 업데이트 (별도 API 호출 필요)
|
||||
update_pharmacy_vpn_ip "$PHARMACY_CODE" "$TAILSCALE_IP"
|
||||
|
||||
return 0
|
||||
else
|
||||
print_error "약국 등록 실패"
|
||||
echo "$RESPONSE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### VPN IP 업데이트
|
||||
```bash
|
||||
update_pharmacy_vpn_ip() {
|
||||
local PHARMACY_CODE=$1
|
||||
local VPN_IP=$2
|
||||
|
||||
# pharmacy_id 조회 필요 (또는 UPDATE API 수정)
|
||||
# 현재는 PUT /api/pharmacy/<id>만 있음
|
||||
|
||||
# 임시 방안: 직접 DB 업데이트
|
||||
python3 << EOF
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/srv/headscale-tailscale-replacement/farmq-admin/farmq.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE pharmacies
|
||||
SET tailscale_ip = ?
|
||||
WHERE pharmacy_code = ?
|
||||
""", ("$VPN_IP", "$PHARMACY_CODE"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("VPN IP 업데이트 완료: $PHARMACY_CODE → $VPN_IP")
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
### 방법 2: 하이브리드 방식 (대안)
|
||||
|
||||
```bash
|
||||
register_pharmacy_hybrid() {
|
||||
# 1. farmq-admin API로 기본 정보 등록
|
||||
PHARMACY_CODE=$(call_farmq_api_create)
|
||||
|
||||
# 2. VPN IP는 직접 업데이트 (API에 없는 필드)
|
||||
update_vpn_ip_direct "$PHARMACY_CODE" "$TAILSCALE_IP"
|
||||
|
||||
# 3. gateway.db는 Python 스크립트로 생성
|
||||
create_gateway_user "$PHARMACY_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 수정된 전체 흐름
|
||||
|
||||
### 새로운 스크립트 흐름
|
||||
```
|
||||
1. OS 감지
|
||||
2. Tailscale 설치
|
||||
3. Tailscale 서비스 시작
|
||||
4. Headscale 등록 (preauth key 사용)
|
||||
5. VPN IP 할당 받음 (예: 100.64.0.15)
|
||||
6. 연결 확인
|
||||
7. ✨ 약국 정보 입력 받기 (대화형)
|
||||
8. ✨ farmq-admin API 호출 → farmq.db 등록
|
||||
- pharmacy_code 자동 생성 (API가 처리)
|
||||
- 기본 정보 저장
|
||||
9. ✨ VPN IP 업데이트 (직접 DB 또는 API 개선)
|
||||
10. ✨ gateway.db에 사용자 생성 (Python 스크립트)
|
||||
11. ✨ 로그인 정보 출력
|
||||
12. 종료 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 필요한 개선 사항
|
||||
|
||||
### farmq-admin API 개선 (선택사항)
|
||||
|
||||
#### 1. VPN IP 업데이트 API 추가
|
||||
```python
|
||||
# app.py에 추가
|
||||
@app.route('/api/pharmacy/<pharmacy_code>/vpn-ip', methods=['PUT'])
|
||||
def api_update_pharmacy_vpn_ip(pharmacy_code):
|
||||
"""약국 VPN IP 업데이트"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
vpn_ip = data.get('vpn_ip', '').strip()
|
||||
|
||||
if not vpn_ip:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'VPN IP는 필수입니다.'
|
||||
}), 400
|
||||
|
||||
farmq_session = get_farmq_session()
|
||||
try:
|
||||
pharmacy = farmq_session.query(PharmacyInfo).filter(
|
||||
PharmacyInfo.pharmacy_code == pharmacy_code
|
||||
).first()
|
||||
|
||||
if not pharmacy:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '약국을 찾을 수 없습니다.'
|
||||
}), 404
|
||||
|
||||
pharmacy.tailscale_ip = vpn_ip
|
||||
farmq_session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'VPN IP 업데이트 완료: {pharmacy_code} → {vpn_ip}'
|
||||
})
|
||||
|
||||
finally:
|
||||
farmq_session.close()
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
```
|
||||
|
||||
**사용:**
|
||||
```bash
|
||||
curl -X PUT http://localhost:5001/api/pharmacy/P004/vpn-ip \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"vpn_ip": "100.64.0.15"}'
|
||||
```
|
||||
|
||||
#### 2. pharmacy_code로 조회 API 추가
|
||||
```python
|
||||
@app.route('/api/pharmacy/code/<pharmacy_code>', methods=['GET'])
|
||||
def api_get_pharmacy_by_code(pharmacy_code):
|
||||
"""약국 코드로 약국 정보 조회"""
|
||||
try:
|
||||
farmq_session = get_farmq_session()
|
||||
try:
|
||||
pharmacy = farmq_session.query(PharmacyInfo).filter(
|
||||
PharmacyInfo.pharmacy_code == pharmacy_code
|
||||
).first()
|
||||
|
||||
if not pharmacy:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '약국을 찾을 수 없습니다.'
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'pharmacy': pharmacy.to_dict()
|
||||
})
|
||||
|
||||
finally:
|
||||
farmq_session.close()
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 비교표
|
||||
|
||||
| 항목 | 직접 DB 접근 | farmq-admin API |
|
||||
|------|-------------|-----------------|
|
||||
| 구현 난이도 | 중 | 쉬움 ⭐ |
|
||||
| pharmacy_code 생성 | 직접 구현 필요 | 자동 처리 ✅ |
|
||||
| 검증 로직 | 직접 구현 필요 | 이미 구현됨 ✅ |
|
||||
| 에러 핸들링 | 직접 구현 필요 | 이미 구현됨 ✅ |
|
||||
| 유지보수 | 어려움 | 쉬움 ✅ |
|
||||
| VPN IP 업데이트 | 직접 가능 | API 개선 필요 |
|
||||
| 의존성 | SQLite3만 | curl 필요 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 최종 권장 사항
|
||||
|
||||
### Phase 1: 최소 변경 (즉시 적용 가능)
|
||||
|
||||
```bash
|
||||
# headscale-quick-install.sh 수정
|
||||
|
||||
register_pharmacy() {
|
||||
print_status "약국 등록 중..."
|
||||
|
||||
# 1. farmq-admin API로 약국 생성
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
http://localhost:5001/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"pharmacy_name\": \"$PHARMACY_NAME\",
|
||||
\"owner_name\": \"$OWNER_NAME\",
|
||||
\"owner_phone\": \"$OWNER_PHONE\",
|
||||
\"owner_email\": \"$OWNER_EMAIL\",
|
||||
\"api_port\": 8082
|
||||
}")
|
||||
|
||||
# 2. pharmacy_code 추출
|
||||
PHARMACY_CODE=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('pharmacy',{}).get('pharmacy_code',''))")
|
||||
|
||||
# 3. VPN IP 업데이트 (직접 DB)
|
||||
python3 << EOF
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/srv/headscale-tailscale-replacement/farmq-admin/farmq.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE pharmacies SET tailscale_ip = ? WHERE pharmacy_code = ?",
|
||||
("$TAILSCALE_IP", "$PHARMACY_CODE"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
EOF
|
||||
|
||||
# 4. gateway.db 사용자 생성 (별도 스크립트)
|
||||
python3 /srv/pharmq-gateway/scripts/create_gateway_user.py \
|
||||
--pharmacy-code "$PHARMACY_CODE" \
|
||||
--owner "$OWNER_NAME" \
|
||||
--phone "$OWNER_PHONE" \
|
||||
--email "$OWNER_EMAIL"
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: farmq-admin API 개선 (선택)
|
||||
|
||||
app.py에 다음 추가:
|
||||
1. `PUT /api/pharmacy/<code>/vpn-ip` - VPN IP 업데이트
|
||||
2. `GET /api/pharmacy/code/<code>` - 코드로 조회
|
||||
|
||||
### Phase 3: 완전 통합
|
||||
|
||||
gateway.db 생성도 API로 통합 (gateway API 서버에서)
|
||||
|
||||
---
|
||||
|
||||
## 📝 구현 예시 (최소 변경)
|
||||
|
||||
### 1. gateway 사용자 생성 스크립트
|
||||
|
||||
**파일:** `/srv/pharmq-gateway/scripts/create_gateway_user.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
import string
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
|
||||
def generate_password(length=8):
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def create_gateway_user(args):
|
||||
username = f"{args.pharmacy_code.lower()}_admin"
|
||||
password = generate_password(8)
|
||||
password_hash = pbkdf2_sha256.hash(password)
|
||||
|
||||
conn = sqlite3.connect(args.gateway_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# users 테이블
|
||||
cursor.execute("""
|
||||
INSERT INTO users (
|
||||
username, email, password_hash, name, phone,
|
||||
primary_pharmacy_code, role, status,
|
||||
failed_login_attempts, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
username,
|
||||
args.email,
|
||||
password_hash,
|
||||
args.owner,
|
||||
args.phone,
|
||||
args.pharmacy_code,
|
||||
'admin',
|
||||
'active',
|
||||
0,
|
||||
datetime.now().isoformat(),
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
# pharmacy_members 테이블
|
||||
cursor.execute("""
|
||||
INSERT INTO pharmacy_members (
|
||||
pharmacy_code, user_id, position, access_level,
|
||||
can_view_sales, can_view_inventory, can_manage_staff,
|
||||
is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
args.pharmacy_code, user_id, '대표약사', 'owner',
|
||||
True, True, True, True,
|
||||
datetime.now().isoformat(),
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 결과 출력 (JSON)
|
||||
import json
|
||||
print(json.dumps({
|
||||
'success': True,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'pharmacy_code': args.pharmacy_code
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
import json
|
||||
print(json.dumps({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}))
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--pharmacy-code', required=True)
|
||||
parser.add_argument('--owner', required=True)
|
||||
parser.add_argument('--phone', required=True)
|
||||
parser.add_argument('--email', required=True)
|
||||
parser.add_argument('--gateway-db',
|
||||
default='/srv/pharmq-gateway/gateway.db')
|
||||
|
||||
args = parser.parse_args()
|
||||
create_gateway_user(args)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 결론
|
||||
|
||||
### 핵심 발견
|
||||
**farmq-admin이 이미 약국 생성 API를 제공하고 있음!**
|
||||
- pharmacy_code 자동 생성 ✅
|
||||
- 검증 로직 완료 ✅
|
||||
- 에러 핸들링 완료 ✅
|
||||
|
||||
### 개선 방안
|
||||
1. **farmq.db**: farmq-admin API 활용 (POST /api/pharmacy)
|
||||
2. **VPN IP**: 직접 DB 업데이트 (또는 API 추가)
|
||||
3. **gateway.db**: Python 스크립트로 생성
|
||||
|
||||
### 장점
|
||||
- ✅ 기존 시스템 재사용
|
||||
- ✅ 중복 코드 방지
|
||||
- ✅ 유지보수 용이
|
||||
- ✅ 빠른 구현 가능
|
||||
|
||||
---
|
||||
|
||||
**작성일:** 2025년 11월 14일
|
||||
**작성자:** Claude Code
|
||||
**버전:** farmq-admin Integration Analysis v1.0
|
||||
838
HEADSCALE_AUTO_REGISTER_PLAN.md
Normal file
838
HEADSCALE_AUTO_REGISTER_PLAN.md
Normal file
@@ -0,0 +1,838 @@
|
||||
# Headscale 자동 등록 및 DB 생성 개선 기획서
|
||||
|
||||
## 📅 작성일
|
||||
**2025년 11월 14일**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 목표
|
||||
|
||||
Headscale 설치 스크립트(`headscale-quick-install.sh`)를 개선하여:
|
||||
1. ✅ Headscale VPN 등록 (현재 완료)
|
||||
2. ✅ **farmq.db 자동 생성** (신규)
|
||||
3. ✅ **gateway.db 자동 생성** (신규)
|
||||
4. ✅ **즉시 프론트엔드 로그인 가능** (신규)
|
||||
|
||||
**최종 결과:** 스크립트 실행 → 즉시 React 프론트엔드에서 로그인하여 사용 가능
|
||||
|
||||
---
|
||||
|
||||
## 📊 현재 상태 분석
|
||||
|
||||
### 현재 스크립트 흐름
|
||||
```
|
||||
1. OS 감지
|
||||
2. Tailscale 설치
|
||||
3. Tailscale 서비스 시작
|
||||
4. Headscale 등록 (preauth key 사용)
|
||||
5. VPN IP 할당 받음 (예: 100.64.0.15)
|
||||
6. 연결 확인
|
||||
7. 종료 ❌ (DB 생성 없음)
|
||||
```
|
||||
|
||||
### 문제점
|
||||
- ❌ farmq.db에 약국 정보 없음
|
||||
- ❌ gateway.db에 사용자 계정 없음
|
||||
- ❌ 프론트엔드에서 로그인 불가능
|
||||
- ❌ 수동으로 DB 수정 필요
|
||||
|
||||
---
|
||||
|
||||
## 🔄 개선된 흐름
|
||||
|
||||
### 새로운 스크립트 흐름
|
||||
```
|
||||
1. OS 감지
|
||||
2. Tailscale 설치
|
||||
3. Tailscale 서비스 시작
|
||||
4. Headscale 등록 (preauth key 사용)
|
||||
5. VPN IP 할당 받음 (예: 100.64.0.15)
|
||||
6. 연결 확인
|
||||
7. ✨ 약국 정보 입력 받기 (대화형)
|
||||
8. ✨ farmq.db에 약국 등록
|
||||
9. ✨ gateway.db에 사용자 생성
|
||||
10. ✨ 로그인 정보 출력
|
||||
11. 종료 ✅ (완전 자동화)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 데이터베이스 맵핑 구조
|
||||
|
||||
### 3계층 DB 구조
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ gateway.db │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ users │ │
|
||||
│ │ - username: "약국코드_admin" (예: P0004_admin) │ │
|
||||
│ │ - password_hash: (자동 생성된 비밀번호) │ │
|
||||
│ │ - primary_pharmacy_code: "P0004" ◄───────┐ │ │
|
||||
│ │ - role: "admin" │ │ │
|
||||
│ │ - email: "p0004@pharmq.kr" │ │ │
|
||||
│ └───────────────────────────────────────────┼───────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────────────┼───────┐ │
|
||||
│ │ pharmacy_members │ │ │
|
||||
│ │ - user_id: (위에서 생성된 ID) │ │ │
|
||||
│ │ - pharmacy_code: "P0004" ◄───────────────┘ │ │
|
||||
│ │ - position: "대표약사" │ │
|
||||
│ │ - access_level: "owner" │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ pharmacy_code
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ farmq.db │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ pharmacies │ │
|
||||
│ │ - pharmacy_code: "P0004" ◄─────────────────┐ │ │
|
||||
│ │ - pharmacy_name: "사용자 입력" │ │ │
|
||||
│ │ - tailscale_ip: "100.64.0.15" (Headscale) │ │ │
|
||||
│ │ - api_port: 8082 │ │ │
|
||||
│ │ - status: "active" │ │ │
|
||||
│ │ - owner_name: "사용자 입력" │ │ │
|
||||
│ │ - owner_phone: "사용자 입력" │ │ │
|
||||
│ │ - headscale_user_name: "default" │ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ vpn_ip
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ db.sqlite (Headscale - 읽기전용) │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ nodes │ │
|
||||
│ │ - ipv4: "100.64.0.15" │ │
|
||||
│ │ - last_seen: "실시간" │ │
|
||||
│ │ - user_id: 1 (default) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 사용자 입력 항목
|
||||
|
||||
### 필수 입력 항목
|
||||
```bash
|
||||
1. 약국 이름 (pharmacy_name)
|
||||
예: "행복약국", "새서울약국"
|
||||
|
||||
2. 대표약사 이름 (owner_name)
|
||||
예: "홍길동"
|
||||
|
||||
3. 대표약사 전화번호 (owner_phone)
|
||||
예: "010-1234-5678"
|
||||
|
||||
4. 이메일 (owner_email) - 선택
|
||||
예: "happy@pharmq.kr"
|
||||
```
|
||||
|
||||
### 자동 생성 항목
|
||||
```bash
|
||||
1. pharmacy_code
|
||||
로직: farmq.db에서 가장 큰 번호 + 1
|
||||
예: 마지막이 P0002 → P0003 생성
|
||||
|
||||
2. VPN IP (tailscale_ip)
|
||||
로직: tailscale ip -4 명령으로 자동 획득
|
||||
예: 100.64.0.15
|
||||
|
||||
3. username
|
||||
로직: {pharmacy_code}_admin
|
||||
예: P0003 → "p0003_admin"
|
||||
|
||||
4. password
|
||||
로직: 랜덤 8자리 생성 (영문+숫자)
|
||||
예: "aB3xK9mP"
|
||||
|
||||
5. email (입력 없을 시)
|
||||
로직: {pharmacy_code}@pharmq.kr
|
||||
예: "p0003@pharmq.kr"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 기술 구현 방안
|
||||
|
||||
### 1. DB 접근 방법
|
||||
|
||||
#### Option A: SQLite CLI (간단)
|
||||
```bash
|
||||
sqlite3 /srv/headscale-tailscale-replacement/farmq-admin/farmq.db \
|
||||
"INSERT INTO pharmacies (pharmacy_code, pharmacy_name, ...) VALUES (...);"
|
||||
```
|
||||
|
||||
**장점:**
|
||||
- ✅ 추가 의존성 없음
|
||||
- ✅ 스크립트에 바로 통합 가능
|
||||
|
||||
**단점:**
|
||||
- ❌ 복잡한 쿼리 작성 어려움
|
||||
- ❌ 에러 핸들링 제한적
|
||||
|
||||
#### Option B: Python 스크립트 (권장 ⭐)
|
||||
```bash
|
||||
python3 /srv/pharmq-gateway/scripts/register_pharmacy.py \
|
||||
--name "행복약국" \
|
||||
--owner "홍길동" \
|
||||
--phone "010-1234-5678" \
|
||||
--vpn-ip "100.64.0.15"
|
||||
```
|
||||
|
||||
**장점:**
|
||||
- ✅ 복잡한 로직 구현 가능
|
||||
- ✅ 에러 핸들링 우수
|
||||
- ✅ 검증 로직 추가 가능
|
||||
- ✅ 비밀번호 해싱 자동
|
||||
|
||||
**단점:**
|
||||
- ❌ Python 의존성 필요 (대부분 시스템에 기본 설치됨)
|
||||
|
||||
### 2. 스크립트 구조
|
||||
|
||||
```bash
|
||||
# headscale-quick-install.sh 개선안
|
||||
|
||||
main() {
|
||||
# 기존 코드 (1-6단계)
|
||||
detect_os
|
||||
check_requirements
|
||||
install_tailscale
|
||||
start_tailscale
|
||||
register_headscale
|
||||
verify_connection
|
||||
|
||||
# ✨ 신규 추가 (7-10단계)
|
||||
collect_pharmacy_info # 약국 정보 입력 받기
|
||||
register_to_farmq_db # farmq.db 등록
|
||||
create_gateway_user # gateway.db 사용자 생성
|
||||
show_login_info # 로그인 정보 출력
|
||||
|
||||
# 기존 마무리
|
||||
cleanup
|
||||
show_final_info
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 상세 구현 계획
|
||||
|
||||
### Step 7: 약국 정보 수집 (대화형)
|
||||
|
||||
```bash
|
||||
collect_pharmacy_info() {
|
||||
print_header "약국 정보 입력"
|
||||
|
||||
# VPN IP 자동 획득
|
||||
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null)
|
||||
print_info "할당된 VPN IP: $TAILSCALE_IP"
|
||||
|
||||
# 다음 약국 코드 자동 생성
|
||||
NEXT_CODE=$(get_next_pharmacy_code)
|
||||
print_info "새 약국 코드: $NEXT_CODE"
|
||||
|
||||
# 사용자 입력
|
||||
read -p "약국 이름을 입력하세요: " PHARMACY_NAME
|
||||
read -p "대표약사 이름을 입력하세요: " OWNER_NAME
|
||||
read -p "대표약사 전화번호 (010-XXXX-XXXX): " OWNER_PHONE
|
||||
read -p "이메일 (선택, Enter로 건너뛰기): " OWNER_EMAIL
|
||||
|
||||
# 기본값 설정
|
||||
if [ -z "$OWNER_EMAIL" ]; then
|
||||
OWNER_EMAIL="${NEXT_CODE,,}@pharmq.kr" # 소문자로 변환
|
||||
fi
|
||||
|
||||
# 확인
|
||||
print_info "입력 정보 확인:"
|
||||
echo " 약국 코드: $NEXT_CODE"
|
||||
echo " 약국 이름: $PHARMACY_NAME"
|
||||
echo " 대표약사: $OWNER_NAME"
|
||||
echo " 전화번호: $OWNER_PHONE"
|
||||
echo " 이메일: $OWNER_EMAIL"
|
||||
echo " VPN IP: $TAILSCALE_IP"
|
||||
|
||||
read -p "위 정보로 등록하시겠습니까? (Y/n): " CONFIRM
|
||||
if [[ ! $CONFIRM =~ ^[Yy]$ ]] && [ -n "$CONFIRM" ]; then
|
||||
print_error "등록이 취소되었습니다."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: farmq.db 등록
|
||||
|
||||
#### 8-1. Python 헬퍼 스크립트 생성
|
||||
**파일:** `/srv/pharmq-gateway/scripts/register_pharmacy.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
약국 자동 등록 스크립트
|
||||
Headscale 설치 후 farmq.db와 gateway.db에 약국 정보 자동 생성
|
||||
"""
|
||||
import sqlite3
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
import string
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
|
||||
def generate_password(length=8):
|
||||
"""랜덤 비밀번호 생성"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def get_next_pharmacy_code(farmq_db_path):
|
||||
"""다음 약국 코드 생성 (P0001, P0002, ...)"""
|
||||
conn = sqlite3.connect(farmq_db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT pharmacy_code FROM pharmacies
|
||||
WHERE pharmacy_code LIKE 'P%'
|
||||
ORDER BY pharmacy_code DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result:
|
||||
# P0002 → 2 → 3 → P0003
|
||||
last_num = int(result[0][1:])
|
||||
next_num = last_num + 1
|
||||
else:
|
||||
next_num = 1
|
||||
|
||||
return f"P{next_num:04d}"
|
||||
|
||||
def register_pharmacy(args):
|
||||
"""farmq.db에 약국 등록"""
|
||||
print("=" * 60)
|
||||
print("📋 farmq.db 약국 등록")
|
||||
print("=" * 60)
|
||||
|
||||
# pharmacy_code 생성
|
||||
if not args.code:
|
||||
args.code = get_next_pharmacy_code(args.farmq_db)
|
||||
|
||||
print(f"\n약국 코드: {args.code}")
|
||||
print(f"약국 이름: {args.name}")
|
||||
print(f"VPN IP: {args.vpn_ip}")
|
||||
|
||||
conn = sqlite3.connect(args.farmq_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO pharmacies (
|
||||
pharmacy_code,
|
||||
pharmacy_name,
|
||||
tailscale_ip,
|
||||
api_port,
|
||||
status,
|
||||
owner_name,
|
||||
owner_phone,
|
||||
owner_email,
|
||||
headscale_user_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
args.code,
|
||||
args.name,
|
||||
args.vpn_ip,
|
||||
8082, # 기본 API 포트
|
||||
'active',
|
||||
args.owner,
|
||||
args.phone,
|
||||
args.email or f"{args.code.lower()}@pharmq.kr",
|
||||
'default', # Headscale user
|
||||
datetime.now().isoformat(),
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✅ farmq.db 등록 완료 (pharmacy_code: {args.code})")
|
||||
|
||||
except sqlite3.IntegrityError as e:
|
||||
print(f"\n❌ 오류: {e}")
|
||||
print(f"약국 코드 '{args.code}'가 이미 존재합니다.")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return args.code
|
||||
|
||||
def create_gateway_user(args, pharmacy_code):
|
||||
"""gateway.db에 사용자 생성"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🔐 gateway.db 사용자 생성")
|
||||
print("=" * 60)
|
||||
|
||||
# username 생성
|
||||
username = f"{pharmacy_code.lower()}_admin"
|
||||
|
||||
# 랜덤 비밀번호 생성
|
||||
password = generate_password(8)
|
||||
password_hash = pbkdf2_sha256.hash(password)
|
||||
|
||||
print(f"\nUsername: {username}")
|
||||
print(f"Password: {password}")
|
||||
|
||||
conn = sqlite3.connect(args.gateway_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# users 테이블에 삽입
|
||||
cursor.execute("""
|
||||
INSERT INTO users (
|
||||
username,
|
||||
email,
|
||||
password_hash,
|
||||
name,
|
||||
phone,
|
||||
primary_pharmacy_code,
|
||||
role,
|
||||
status,
|
||||
failed_login_attempts,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
username,
|
||||
args.email or f"{pharmacy_code.lower()}@pharmq.kr",
|
||||
password_hash,
|
||||
args.owner,
|
||||
args.phone,
|
||||
pharmacy_code,
|
||||
'admin',
|
||||
'active',
|
||||
0,
|
||||
datetime.now().isoformat(),
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
# pharmacy_members 테이블에 삽입
|
||||
cursor.execute("""
|
||||
INSERT INTO pharmacy_members (
|
||||
pharmacy_code,
|
||||
user_id,
|
||||
position,
|
||||
access_level,
|
||||
can_view_sales,
|
||||
can_view_inventory,
|
||||
can_manage_staff,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
pharmacy_code,
|
||||
user_id,
|
||||
'대표약사',
|
||||
'owner',
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
datetime.now().isoformat(),
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✅ gateway.db 사용자 생성 완료 (user_id: {user_id})")
|
||||
|
||||
except sqlite3.IntegrityError as e:
|
||||
print(f"\n❌ 오류: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return username, password
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='약국 자동 등록')
|
||||
parser.add_argument('--name', required=True, help='약국 이름')
|
||||
parser.add_argument('--owner', required=True, help='대표약사 이름')
|
||||
parser.add_argument('--phone', required=True, help='전화번호')
|
||||
parser.add_argument('--vpn-ip', required=True, help='VPN IP 주소')
|
||||
parser.add_argument('--email', help='이메일 (선택)')
|
||||
parser.add_argument('--code', help='약국 코드 (자동 생성)')
|
||||
parser.add_argument('--farmq-db',
|
||||
default='/srv/headscale-tailscale-replacement/farmq-admin/farmq.db',
|
||||
help='farmq.db 경로')
|
||||
parser.add_argument('--gateway-db',
|
||||
default='/srv/pharmq-gateway/gateway.db',
|
||||
help='gateway.db 경로')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. farmq.db 등록
|
||||
pharmacy_code = register_pharmacy(args)
|
||||
|
||||
# 2. gateway.db 사용자 생성
|
||||
username, password = create_gateway_user(args, pharmacy_code)
|
||||
|
||||
# 3. 최종 정보 출력
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 등록 완료!")
|
||||
print("=" * 60)
|
||||
print(f"\n📋 약국 정보:")
|
||||
print(f" 약국 코드: {pharmacy_code}")
|
||||
print(f" 약국 이름: {args.name}")
|
||||
print(f" VPN IP: {args.vpn_ip}")
|
||||
print(f" API 포트: 8082")
|
||||
|
||||
print(f"\n🔑 로그인 정보:")
|
||||
print(f" Username: {username}")
|
||||
print(f" Password: {password}")
|
||||
print(f" 이메일: {args.email or f'{pharmacy_code.lower()}@pharmq.kr'}")
|
||||
|
||||
print(f"\n🌐 프론트엔드 접속:")
|
||||
print(f" https://pharmq.kr")
|
||||
print(f" 또는")
|
||||
print(f" https://dev.pharmq.kr")
|
||||
|
||||
print("\n⚠️ 비밀번호를 안전한 곳에 보관하세요!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
#### 8-2. Bash 스크립트에서 호출
|
||||
|
||||
```bash
|
||||
register_to_farmq_and_gateway() {
|
||||
print_status "약국 정보 등록 중..."
|
||||
|
||||
# Python 스크립트 실행
|
||||
REGISTER_OUTPUT=$(python3 /srv/pharmq-gateway/scripts/register_pharmacy.py \
|
||||
--name "$PHARMACY_NAME" \
|
||||
--owner "$OWNER_NAME" \
|
||||
--phone "$OWNER_PHONE" \
|
||||
--email "$OWNER_EMAIL" \
|
||||
--vpn-ip "$TAILSCALE_IP" 2>&1)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "등록 완료!"
|
||||
echo "$REGISTER_OUTPUT"
|
||||
|
||||
# 로그인 정보 추출
|
||||
USERNAME=$(echo "$REGISTER_OUTPUT" | grep "Username:" | awk '{print $2}')
|
||||
PASSWORD=$(echo "$REGISTER_OUTPUT" | grep "Password:" | awk '{print $2}')
|
||||
PHARMACY_CODE=$(echo "$REGISTER_OUTPUT" | grep "약국 코드:" | awk '{print $3}')
|
||||
|
||||
# 환경 변수 저장
|
||||
export PHARMACY_CODE
|
||||
export USERNAME
|
||||
export PASSWORD
|
||||
else
|
||||
print_error "등록 실패!"
|
||||
echo "$REGISTER_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 최종 사용자 경험
|
||||
|
||||
### 실행 예시
|
||||
```bash
|
||||
# 1. 스크립트 실행
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/headscale-quick-install.sh | bash
|
||||
|
||||
# 2. Headscale 등록 자동 진행...
|
||||
# 3. VPN IP 할당: 100.64.0.15
|
||||
|
||||
# 4. 약국 정보 입력
|
||||
============================================
|
||||
약국 정보 입력
|
||||
============================================
|
||||
|
||||
할당된 VPN IP: 100.64.0.15
|
||||
새 약국 코드: P0004
|
||||
|
||||
약국 이름을 입력하세요: 행복약국
|
||||
대표약사 이름을 입력하세요: 홍길동
|
||||
대표약사 전화번호 (010-XXXX-XXXX): 010-1234-5678
|
||||
이메일 (선택, Enter로 건너뛰기):
|
||||
|
||||
입력 정보 확인:
|
||||
약국 코드: P0004
|
||||
약국 이름: 행복약국
|
||||
대표약사: 홍길동
|
||||
전화번호: 010-1234-5678
|
||||
이메일: p0004@pharmq.kr
|
||||
VPN IP: 100.64.0.15
|
||||
|
||||
위 정보로 등록하시겠습니까? (Y/n): Y
|
||||
|
||||
# 5. 자동 등록 진행...
|
||||
|
||||
============================================
|
||||
설치 완료!
|
||||
============================================
|
||||
|
||||
✅ Headscale VPN 연결 완료
|
||||
✅ farmq.db 약국 등록 완료
|
||||
✅ gateway.db 사용자 생성 완료
|
||||
|
||||
📋 약국 정보:
|
||||
약국 코드: P0004
|
||||
약국 이름: 행복약국
|
||||
VPN IP: 100.64.0.15
|
||||
API 포트: 8082
|
||||
|
||||
🔑 로그인 정보:
|
||||
Username: p0004_admin
|
||||
Password: aB3xK9mP
|
||||
이메일: p0004@pharmq.kr
|
||||
|
||||
🌐 프론트엔드 접속:
|
||||
https://pharmq.kr
|
||||
또는
|
||||
https://dev.pharmq.kr
|
||||
|
||||
⚠️ 비밀번호를 안전한 곳에 보관하세요!
|
||||
|
||||
============================================
|
||||
```
|
||||
|
||||
### 프론트엔드 로그인
|
||||
```
|
||||
1. https://pharmq.kr 접속
|
||||
2. Username: p0004_admin
|
||||
3. Password: aB3xK9mP
|
||||
4. 로그인 → 즉시 사용 가능! ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 구현 우선순위
|
||||
|
||||
### Phase 1: Python 스크립트 생성 ⭐ (최우선)
|
||||
```bash
|
||||
/srv/pharmq-gateway/scripts/register_pharmacy.py
|
||||
```
|
||||
- farmq.db INSERT
|
||||
- gateway.db users, pharmacy_members INSERT
|
||||
- 비밀번호 해싱
|
||||
- 에러 핸들링
|
||||
|
||||
### Phase 2: Bash 스크립트 통합 ⭐
|
||||
```bash
|
||||
headscale-quick-install.sh 수정
|
||||
```
|
||||
- collect_pharmacy_info() 함수 추가
|
||||
- register_to_farmq_and_gateway() 함수 추가
|
||||
- show_login_info() 함수 추가
|
||||
|
||||
### Phase 3: 테스트 ⭐
|
||||
```bash
|
||||
1. 새 VM에서 스크립트 실행
|
||||
2. DB 확인
|
||||
3. 프론트엔드 로그인 테스트
|
||||
4. API 통신 테스트
|
||||
```
|
||||
|
||||
### Phase 4: 문서화
|
||||
```markdown
|
||||
- README 업데이트
|
||||
- 스크립트 주석 추가
|
||||
- 트러블슈팅 가이드
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 보안 고려사항
|
||||
|
||||
### 1. 비밀번호 생성
|
||||
- ✅ 8자리 이상
|
||||
- ✅ 영문 대소문자 + 숫자 조합
|
||||
- ✅ `secrets` 모듈 사용 (암호학적으로 안전)
|
||||
|
||||
### 2. 비밀번호 해싱
|
||||
- ✅ pbkdf2_sha256 사용
|
||||
- ✅ Salt 자동 생성
|
||||
- ✅ Rainbow table 공격 방지
|
||||
|
||||
### 3. DB 접근 권한
|
||||
- ✅ root 권한으로 스크립트 실행 필요
|
||||
- ✅ DB 파일 권한 확인
|
||||
- ✅ SQLite injection 방지 (parameterized query)
|
||||
|
||||
### 4. 로그인 정보 노출
|
||||
- ⚠️ 스크립트 실행 화면에 비밀번호 표시됨
|
||||
- ✅ 일회용 비밀번호 생성
|
||||
- ✅ 초기 로그인 후 비밀번호 변경 권장
|
||||
|
||||
---
|
||||
|
||||
## 📊 데이터 흐름 다이어그램
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 사용자 (터미널) │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
│ 1. 스크립트 실행
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ headscale-quick-install.sh │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. OS 감지 │
|
||||
│ 2. Tailscale 설치 │
|
||||
│ 3. Headscale 등록 │
|
||||
│ 4. VPN IP 획득: 100.64.0.15 │
|
||||
│ 5. 약국 정보 입력 받기 ────────────┐ │
|
||||
│ │ │
|
||||
│ 6. Python 스크립트 호출 ───────────┼──────────┐ │
|
||||
└─────────────────────────────────────┼──────────┼────────────┘
|
||||
│ │
|
||||
┌───────────────────┘ │
|
||||
│ │
|
||||
↓ ↓
|
||||
┌─────────────────────────────────┐ ┌─────────────────────────┐
|
||||
│ register_pharmacy.py │ │ register_pharmacy.py │
|
||||
├─────────────────────────────────┤ ├─────────────────────────┤
|
||||
│ 1. 다음 코드 생성: P0004 │ │ 1. username 생성 │
|
||||
│ 2. farmq.db INSERT │ │ p0004_admin │
|
||||
│ - pharmacy_code: P0004 │ │ 2. password 생성 │
|
||||
│ - pharmacy_name: 행복약국 │ │ aB3xK9mP │
|
||||
│ - tailscale_ip: 100.64.0.15 │ │ 3. password 해싱 │
|
||||
│ - api_port: 8082 │ │ 4. gateway.db INSERT │
|
||||
│ - status: active │ │ - users 테이블 │
|
||||
│ 3. 성공 반환 │ │ - pharmacy_members │
|
||||
└─────────────────┬───────────────┘ └──────────┬──────────────┘
|
||||
│ │
|
||||
│ │
|
||||
└──────────┬──────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 최종 정보 출력 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 약국 코드: P0004 │
|
||||
│ 약국 이름: 행복약국 │
|
||||
│ VPN IP: 100.64.0.15 │
|
||||
│ Username: p0004_admin │
|
||||
│ Password: aB3xK9mP │
|
||||
│ 프론트엔드: https://pharmq.kr │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ 사용자가 로그인 정보 복사
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ React 프론트엔드 (https://pharmq.kr) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. 로그인 페이지 │
|
||||
│ 2. Username: p0004_admin 입력 │
|
||||
│ 3. Password: aB3xK9mP 입력 │
|
||||
│ 4. 로그인 버튼 클릭 │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
│ POST /api/auth/login
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Gateway API (gateway.pharmq.kr:8000) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. gateway.db users 조회 │
|
||||
│ 2. 비밀번호 검증 (pbkdf2_sha256) │
|
||||
│ 3. JWT 토큰 생성 │
|
||||
│ payload: { │
|
||||
│ username: "p0004_admin", │
|
||||
│ pharmacy_code: "P0004", │
|
||||
│ role: "admin" │
|
||||
│ } │
|
||||
│ 4. 토큰 반환 │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
│ JWT Token
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ React 프론트엔드 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ✅ 로그인 성공! │
|
||||
│ ✅ 대시보드 페이지 진입 │
|
||||
│ ✅ API 요청 가능 (JWT 토큰 사용) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 성공 기준
|
||||
|
||||
### 기능적 요구사항
|
||||
- ✅ 스크립트 실행 후 5분 이내 완료
|
||||
- ✅ farmq.db에 약국 정보 자동 생성
|
||||
- ✅ gateway.db에 사용자 계정 자동 생성
|
||||
- ✅ 프론트엔드에서 즉시 로그인 가능
|
||||
- ✅ API 통신 정상 작동
|
||||
|
||||
### 비기능적 요구사항
|
||||
- ✅ 에러 발생 시 롤백
|
||||
- ✅ 중복 등록 방지
|
||||
- ✅ 입력 검증 (전화번호 형식 등)
|
||||
- ✅ 로그 파일 생성 (추후)
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 자료
|
||||
|
||||
### 관련 문서
|
||||
- [GATEWAY_ARCHITECTURE_EXPLAINED.md](GATEWAY_ARCHITECTURE_EXPLAINED.md)
|
||||
- [DATABASE_MAPPING_EXPLAINED.md](DATABASE_MAPPING_EXPLAINED.md)
|
||||
|
||||
### 필요한 Python 라이브러리
|
||||
```bash
|
||||
# 대부분 기본 설치되어 있음
|
||||
python3 -c "import sqlite3, secrets, string, argparse" # 기본 라이브러리
|
||||
|
||||
# passlib만 추가 설치 필요
|
||||
pip3 install passlib
|
||||
```
|
||||
|
||||
### DB 경로
|
||||
```bash
|
||||
farmq.db: /srv/headscale-tailscale-replacement/farmq-admin/farmq.db
|
||||
gateway.db: /srv/pharmq-gateway/gateway.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 체크리스트
|
||||
|
||||
### 개발
|
||||
- [ ] register_pharmacy.py 스크립트 작성
|
||||
- [ ] headscale-quick-install.sh 수정
|
||||
- [ ] 입력 검증 로직 추가
|
||||
- [ ] 에러 핸들링 추가
|
||||
|
||||
### 테스트
|
||||
- [ ] 로컬 테스트 (farmq.db, gateway.db)
|
||||
- [ ] 새 VM에서 전체 플로우 테스트
|
||||
- [ ] 프론트엔드 로그인 테스트
|
||||
- [ ] API 통신 테스트
|
||||
|
||||
### 문서화
|
||||
- [ ] README 업데이트
|
||||
- [ ] 주석 추가
|
||||
- [ ] 트러블슈팅 가이드 작성
|
||||
|
||||
### 배포
|
||||
- [ ] Gitea에 커밋
|
||||
- [ ] 기존 스크립트 백업
|
||||
- [ ] 프로덕션 배포
|
||||
|
||||
---
|
||||
|
||||
**작성일:** 2025년 11월 14일
|
||||
**작성자:** Claude Code
|
||||
**버전:** Headscale Auto-Register Plan v1.0
|
||||
160
RDP/RDP_TOGGLE_API.md
Normal file
160
RDP/RDP_TOGGLE_API.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# RDP Toggle API Documentation
|
||||
|
||||
## 개요
|
||||
Proxmox VE 호스트에서 RDP/Shell 모드를 API로 전환할 수 있는 시스템입니다.
|
||||
|
||||
## 구성 요소
|
||||
|
||||
### 1. **rdp-toggle-api.py**
|
||||
- FastAPI 기반 REST API 서버
|
||||
- 포트: 8080
|
||||
- RDP/Shell 모드 전환 제어
|
||||
|
||||
### 2. **rdp-toggle-web.html**
|
||||
- 웹 기반 컨트롤 패널
|
||||
- 실시간 상태 모니터링
|
||||
- 설정 변경 인터페이스
|
||||
|
||||
### 3. **install-rdp-api.sh**
|
||||
- 자동 설치 스크립트
|
||||
- systemd 서비스 설정
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
### GET /status
|
||||
현재 상태 확인
|
||||
```bash
|
||||
curl http://localhost:8080/status
|
||||
```
|
||||
|
||||
### POST /toggle
|
||||
모드 전환 (rdp/shell)
|
||||
```bash
|
||||
# RDP 모드로 전환
|
||||
curl -X POST http://localhost:8080/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}'
|
||||
|
||||
# Shell 모드로 전환
|
||||
curl -X POST http://localhost:8080/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}'
|
||||
```
|
||||
|
||||
### GET /config
|
||||
현재 설정 확인
|
||||
```bash
|
||||
curl http://localhost:8080/config
|
||||
```
|
||||
|
||||
### PUT /config
|
||||
설정 업데이트
|
||||
```bash
|
||||
curl -X PUT http://localhost:8080/config \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"rdp_server": "192.168.0.229",
|
||||
"rdp_username": "0bin",
|
||||
"rdp_password": "trajet6640"
|
||||
}'
|
||||
```
|
||||
|
||||
## 테스트 환경 설정
|
||||
|
||||
### RDP 서버 정보
|
||||
- **서버 주소**: 192.168.0.229
|
||||
- **사용자명**: 0bin
|
||||
- **비밀번호**: trajet6640
|
||||
- **로컬 사용자**: rdpuser
|
||||
|
||||
## 설치 방법
|
||||
|
||||
```bash
|
||||
# 1. 설치 스크립트 실행
|
||||
chmod +x install-rdp-api.sh
|
||||
./install-rdp-api.sh
|
||||
|
||||
# 2. 서비스 상태 확인
|
||||
systemctl status rdp-toggle-api
|
||||
|
||||
# 3. 웹 인터페이스 접속
|
||||
# 브라우저에서 http://[PROXMOX_IP]:8080 접속
|
||||
```
|
||||
|
||||
## 사용 시나리오
|
||||
|
||||
### 1. 초기 설정
|
||||
```bash
|
||||
# RDP 설정 구성
|
||||
curl -X PUT http://localhost:8080/config \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"rdp_server": "192.168.0.229",
|
||||
"rdp_username": "0bin",
|
||||
"rdp_password": "trajet6640"
|
||||
}'
|
||||
```
|
||||
|
||||
### 2. RDP 모드 활성화
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}'
|
||||
```
|
||||
|
||||
### 3. Shell 모드로 복귀
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}'
|
||||
```
|
||||
|
||||
## 동작 원리
|
||||
|
||||
### RDP 모드 활성화 시
|
||||
1. getty@tty1 자동 로그인 설정
|
||||
2. X Window System 자동 시작
|
||||
3. FreeRDP3 전체화면 실행
|
||||
4. RDP 연결 자동 수립
|
||||
|
||||
### Shell 모드 활성화 시
|
||||
1. RDP 프로세스 종료
|
||||
2. X Window 종료
|
||||
3. 자동 로그인 해제
|
||||
4. 일반 TTY 로그인 화면 복원
|
||||
|
||||
## 상태 파일
|
||||
- 상태 저장: `/var/lib/rdp-toggle/state.json`
|
||||
- 설정 저장: `/var/lib/rdp-toggle/config.json`
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### API 서버가 시작되지 않을 때
|
||||
```bash
|
||||
# 로그 확인
|
||||
journalctl -u rdp-toggle-api -f
|
||||
|
||||
# Python 패키지 재설치
|
||||
pip3 install --upgrade fastapi uvicorn
|
||||
```
|
||||
|
||||
### RDP 연결이 실패할 때
|
||||
```bash
|
||||
# 현재 상태 확인
|
||||
curl http://localhost:8080/status
|
||||
|
||||
# RDP 프로세스 확인
|
||||
ps aux | grep xfreerdp3
|
||||
```
|
||||
|
||||
### Shell 모드로 전환이 안 될 때
|
||||
```bash
|
||||
# 수동으로 RDP 종료
|
||||
pkill -u rdpuser
|
||||
systemctl restart getty@tty1
|
||||
```
|
||||
|
||||
## 보안 고려사항
|
||||
- API는 기본적으로 모든 IP에서 접근 가능 (0.0.0.0:8080)
|
||||
- 프로덕션 환경에서는 방화벽 설정 권장
|
||||
- 비밀번호는 평문으로 저장됨 (향후 암호화 필요)
|
||||
304
RDP/README.md
Normal file
304
RDP/README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Proxmox RDP 자동화 시스템
|
||||
|
||||
Proxmox VE 호스트에서 **RDP 초기 설정** 및 **RDP/Shell 모드 API 전환**을 지원하는 통합 솔루션
|
||||
|
||||
## 개요
|
||||
|
||||
이 시스템은 두 가지 주요 기능을 제공합니다:
|
||||
|
||||
1. **Proxmox RDP 초기 설정** - 부팅 시 자동으로 원격 Windows PC에 RDP 연결
|
||||
2. **RDP Toggle API** - 외부에서 API 호출로 RDP/Shell 모드 실시간 전환
|
||||
|
||||
프론트엔드 또는 curl 명령으로 Proxmox 물리 화면을 Shell ↔ RDP 모드로 즉시 전환 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 빠른 설치
|
||||
|
||||
### 1️⃣ Proxmox RDP 초기 설정 (자동 부팅 연결)
|
||||
|
||||
Proxmox 호스트가 부팅 시 자동으로 RDP 연결하도록 설정:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/proxmox-auto-rdp-setup.sh | bash
|
||||
```
|
||||
|
||||
**설치 내용:**
|
||||
- ✅ X Window + Openbox 윈도우 매니저
|
||||
- ✅ FreeRDP3 클라이언트 설치
|
||||
- ✅ 자동 로그인 및 X 시작 설정
|
||||
- ✅ RDP 서버 연결 정보 구성
|
||||
- ✅ 풀스크린 RDP 자동 실행
|
||||
|
||||
### 2️⃣ RDP Toggle API 설치 (원격 제어)
|
||||
|
||||
API를 통해 RDP/Shell 모드를 원격으로 전환:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/install-rdp-api.sh | bash
|
||||
```
|
||||
|
||||
**설치 내용:**
|
||||
- ✅ FastAPI 기반 REST API 서버
|
||||
- ✅ Python venv 환경 구성
|
||||
- ✅ systemd 서비스 자동 시작
|
||||
- ✅ 포트 8090에서 API 실행
|
||||
|
||||
---
|
||||
|
||||
## 📖 사용 방법
|
||||
|
||||
### RDP Toggle API 사용
|
||||
|
||||
#### 서비스 확인
|
||||
```bash
|
||||
systemctl status rdp-toggle-api
|
||||
```
|
||||
|
||||
#### API 테스트
|
||||
```bash
|
||||
# 현재 상태 확인
|
||||
curl http://localhost:8090/status
|
||||
|
||||
# RDP 모드로 전환
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}'
|
||||
|
||||
# Shell 모드로 전환
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 API 엔드포인트
|
||||
|
||||
### GET /status
|
||||
현재 RDP/Shell 모드 상태 확인
|
||||
```bash
|
||||
curl http://localhost:8090/status
|
||||
```
|
||||
|
||||
**응답 예시:**
|
||||
```json
|
||||
{
|
||||
"current_mode": "shell",
|
||||
"rdp_active": false,
|
||||
"last_changed": "2025-11-17T10:30:00",
|
||||
"config": {
|
||||
"rdp_server": "192.168.0.229",
|
||||
"rdp_username": "user",
|
||||
"local_user": "rdpuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /toggle
|
||||
RDP/Shell 모드 전환
|
||||
```bash
|
||||
# RDP 모드 활성화
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}'
|
||||
|
||||
# Shell 모드로 전환
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}'
|
||||
```
|
||||
|
||||
### GET /config
|
||||
현재 RDP 연결 설정 조회
|
||||
```bash
|
||||
curl http://localhost:8090/config
|
||||
```
|
||||
|
||||
### PUT /config
|
||||
RDP 연결 설정 업데이트
|
||||
```bash
|
||||
curl -X PUT http://localhost:8090/config \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"rdp_server": "new-server.example.com:3389",
|
||||
"rdp_username": "newuser",
|
||||
"rdp_password": "newpassword"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 프론트엔드 연동
|
||||
|
||||
### React 예시
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const RDPToggle = () => {
|
||||
const [status, setStatus] = useState(null);
|
||||
const API_URL = 'http://your-proxmox-ip:8090';
|
||||
|
||||
// 상태 확인
|
||||
const fetchStatus = async () => {
|
||||
const res = await fetch(`${API_URL}/status`);
|
||||
const data = await res.json();
|
||||
setStatus(data);
|
||||
};
|
||||
|
||||
// 모드 전환
|
||||
const toggleMode = async (mode) => {
|
||||
await fetch(`${API_URL}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode })
|
||||
});
|
||||
fetchStatus(); // 상태 갱신
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>현재 모드: {status?.current_mode}</p>
|
||||
<button onClick={() => toggleMode('rdp')}>RDP 모드</button>
|
||||
<button onClick={() => toggleMode('shell')}>Shell 모드</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 웹 기반 컨트롤 패널
|
||||
|
||||
간단한 HTML 기반 컨트롤 패널도 제공됩니다:
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/rdp-toggle-web.html -o rdp-control.html
|
||||
```
|
||||
|
||||
브라우저에서 `rdp-control.html` 파일을 열어 사용하세요.
|
||||
|
||||
---
|
||||
|
||||
## 📂 구성 파일
|
||||
|
||||
### 설치 위치
|
||||
- **API 서버**: `/opt/rdp-toggle-api/`
|
||||
- **Python 가상환경**: `/opt/rdp-toggle-api/venv/`
|
||||
- **systemd 서비스**: `/etc/systemd/system/rdp-toggle-api.service`
|
||||
|
||||
### 구성 파일 목록
|
||||
- `rdp-toggle-api.py` - FastAPI 기반 REST API 서버
|
||||
- `install-rdp-api.sh` - 자동 설치 스크립트 (curl 실행 가능)
|
||||
- `proxmox-auto-rdp-setup.sh` - Proxmox RDP 초기 설정 스크립트
|
||||
- `rdp-toggle-web.html` - 웹 기반 컨트롤 패널
|
||||
- `requirements.txt` - Python 패키지 의존성
|
||||
|
||||
---
|
||||
|
||||
## ✨ 주요 기능
|
||||
|
||||
- ✅ **RDP ↔ Shell 모드 즉시 전환**
|
||||
- ✅ **실시간 상태 모니터링** (프로세스 확인)
|
||||
- ✅ **CORS 지원** (외부 프론트엔드 접근 가능)
|
||||
- ✅ **Python venv 환경** (시스템 패키지 충돌 방지)
|
||||
- ✅ **systemd 서비스** (자동 시작, 재시작)
|
||||
- ✅ **설정 동적 변경** (API로 RDP 서버 정보 변경)
|
||||
- ✅ **curl 원라이너 설치** (웹에서 바로 복사해서 실행)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 서비스 관리
|
||||
|
||||
```bash
|
||||
# 서비스 상태 확인
|
||||
systemctl status rdp-toggle-api
|
||||
|
||||
# 서비스 재시작
|
||||
systemctl restart rdp-toggle-api
|
||||
|
||||
# 서비스 중지
|
||||
systemctl stop rdp-toggle-api
|
||||
|
||||
# 서비스 시작
|
||||
systemctl start rdp-toggle-api
|
||||
|
||||
# 로그 확인
|
||||
journalctl -u rdp-toggle-api -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 네트워크 설정
|
||||
|
||||
- **기본 포트**: `8090`
|
||||
- **바인드 주소**: `0.0.0.0` (모든 인터페이스)
|
||||
- **CORS**: 모든 origin 허용 (`allow_origins=["*"]`)
|
||||
|
||||
외부에서 접근하려면 방화벽에서 포트 8090을 허용하세요:
|
||||
```bash
|
||||
# UFW 사용 시
|
||||
ufw allow 8090/tcp
|
||||
|
||||
# iptables 사용 시
|
||||
iptables -A INPUT -p tcp --dport 8090 -j ACCEPT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 문서
|
||||
|
||||
- [RDP_TOGGLE_API.md](RDP_TOGGLE_API.md) - API 상세 문서
|
||||
- [proxmox_auto_rdp_setup_korean.md](proxmox_auto_rdp_setup_korean.md) - RDP 초기 설정 가이드
|
||||
|
||||
---
|
||||
|
||||
## 🐛 문제 해결
|
||||
|
||||
### API 서버가 시작되지 않는 경우
|
||||
```bash
|
||||
# 로그 확인
|
||||
journalctl -u rdp-toggle-api -n 50
|
||||
|
||||
# Python 가상환경 재설치
|
||||
rm -rf /opt/rdp-toggle-api/venv
|
||||
python3 -m venv /opt/rdp-toggle-api/venv
|
||||
/opt/rdp-toggle-api/venv/bin/pip install fastapi uvicorn python-multipart pydantic
|
||||
systemctl restart rdp-toggle-api
|
||||
```
|
||||
|
||||
### RDP 모드 전환이 작동하지 않는 경우
|
||||
```bash
|
||||
# RDP 초기 설정이 완료되었는지 확인
|
||||
ls -la /home/rdpuser/.xinitrc
|
||||
cat /etc/systemd/system/getty@tty1.service.d/override.conf
|
||||
|
||||
# FreeRDP3 설치 확인
|
||||
which xfreerdp3
|
||||
```
|
||||
|
||||
### 포트 8090이 이미 사용 중인 경우
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
ss -tulpn | grep 8090
|
||||
|
||||
# API 서비스 파일에서 포트 변경
|
||||
nano /opt/rdp-toggle-api/rdp-toggle-api.py
|
||||
# 마지막 줄: uvicorn.run(app, host="0.0.0.0", port=8090)
|
||||
# 포트 번호를 원하는 값으로 변경 후 저장
|
||||
|
||||
systemctl restart rdp-toggle-api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 라이선스
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
**작성자**: thug0bin
|
||||
**리포지토리**: https://git.0bin.in/thug0bin/pve9-repo-fix
|
||||
188
RDP/install-rdp-api.sh
Executable file
188
RDP/install-rdp-api.sh
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
|
||||
# RDP Toggle API 설치 스크립트
|
||||
# curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/install-rdp-api.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
# IP 주소 가져오기 (Headscale 우선, 없으면 로컬 IP)
|
||||
get_primary_ip() {
|
||||
# Headscale VPN IP 확인 (100.64.x.x 대역)
|
||||
local headscale_ip=$(hostname -I | tr ' ' '\n' | grep '^100\.64\.' | head -n1)
|
||||
|
||||
if [ -n "$headscale_ip" ]; then
|
||||
echo "$headscale_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Headscale IP가 없으면 로컬 IP (첫 번째)
|
||||
local local_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
if [ -n "$local_ip" ]; then
|
||||
echo "$local_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 그것도 없으면 localhost
|
||||
echo "127.0.0.1"
|
||||
}
|
||||
|
||||
# 테스트 메뉴 함수
|
||||
show_test_menu() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "RDP API를 테스트하시겠습니까?"
|
||||
echo " 1) RDP 모드로 전환 테스트"
|
||||
echo " 2) Shell 모드로 전환 테스트"
|
||||
echo " 3) 현재 상태 확인"
|
||||
echo " 4) 종료"
|
||||
echo ""
|
||||
echo -n "선택 [1/2/3/4]: "
|
||||
read -r test_choice </dev/tty
|
||||
|
||||
case "$test_choice" in
|
||||
1)
|
||||
echo ""
|
||||
echo "RDP 모드로 전환 중..."
|
||||
if curl -s -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}' > /dev/null 2>&1; then
|
||||
echo "✅ RDP 모드로 전환 완료!"
|
||||
echo ""
|
||||
echo "현재 상태:"
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
else
|
||||
echo "❌ RDP 모드 전환 실패. 서비스 상태를 확인하세요:"
|
||||
echo " systemctl status rdp-toggle-api"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
echo ""
|
||||
echo "Shell 모드로 전환 중..."
|
||||
if curl -s -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}' > /dev/null 2>&1; then
|
||||
echo "✅ Shell 모드로 전환 완료!"
|
||||
echo ""
|
||||
echo "현재 상태:"
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
else
|
||||
echo "❌ Shell 모드 전환 실패. 서비스 상태를 확인하세요:"
|
||||
echo " systemctl status rdp-toggle-api"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
echo "현재 상태 확인 중..."
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
;;
|
||||
4)
|
||||
echo ""
|
||||
echo "종료합니다."
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
echo "잘못된 선택입니다."
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 설치 디렉토리 설정
|
||||
INSTALL_DIR="/opt/rdp-toggle-api"
|
||||
VENV_DIR="$INSTALL_DIR/venv"
|
||||
GITEA_BASE_URL="https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP"
|
||||
|
||||
# 이미 설치되어 있는지 확인
|
||||
if systemctl is-active --quiet rdp-toggle-api.service && [ -f "$INSTALL_DIR/rdp-toggle-api.py" ]; then
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ RDP Toggle API가 이미 설치되어 실행 중입니다!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📍 API 서버: http://$(get_primary_ip):8090"
|
||||
echo "📁 설치 위치: $INSTALL_DIR"
|
||||
echo ""
|
||||
|
||||
# 바로 테스트 메뉴 보여주기
|
||||
show_test_menu
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "완료!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "RDP Toggle API 설치 시작..."
|
||||
|
||||
# Python 및 venv 설치
|
||||
echo "Python 및 필수 패키지 설치 중..."
|
||||
apt update
|
||||
apt install -y python3 python3-venv python3-pip curl
|
||||
|
||||
# 설치 디렉토리 생성
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# 가상환경 생성
|
||||
echo "가상환경 생성 중..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
# 가상환경에서 패키지 설치
|
||||
echo "Python 패키지 설치 중..."
|
||||
"$VENV_DIR/bin/pip" install --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install fastapi==0.115.5 uvicorn==0.32.1 python-multipart==0.0.20 pydantic==2.10.3
|
||||
|
||||
# API 파일 다운로드
|
||||
echo "API 서버 파일 다운로드 중..."
|
||||
curl -fsSL "$GITEA_BASE_URL/rdp-toggle-api.py" -o "$INSTALL_DIR/rdp-toggle-api.py"
|
||||
chmod +x "$INSTALL_DIR/rdp-toggle-api.py"
|
||||
|
||||
# systemd 서비스 생성
|
||||
echo "systemd 서비스 생성 중..."
|
||||
cat > /etc/systemd/system/rdp-toggle-api.service << EOF
|
||||
[Unit]
|
||||
Description=RDP Toggle API Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$VENV_DIR/bin/python $INSTALL_DIR/rdp-toggle-api.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 서비스 활성화 및 시작
|
||||
echo "서비스 활성화 및 시작 중..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable rdp-toggle-api.service
|
||||
systemctl start rdp-toggle-api.service
|
||||
|
||||
# 잠시 대기 후 상태 확인
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "RDP Toggle API 설치 완료!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📍 API 서버: http://$(get_primary_ip):8090"
|
||||
echo "📁 설치 위치: $INSTALL_DIR"
|
||||
echo ""
|
||||
|
||||
# 테스트 메뉴 보여주기
|
||||
show_test_menu
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "설치 및 설정이 완료되었습니다!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
919
RDP/proxmox-auto-rdp-setup.sh
Executable file
919
RDP/proxmox-auto-rdp-setup.sh
Executable file
@@ -0,0 +1,919 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Proxmox Auto RDP Setup Script
|
||||
# 자동으로 Proxmox 호스트를 RDP VM에 연결하는 설정을 수행합니다
|
||||
# 사용법: bash -c "$(curl -fsSL [스크립트 URL])"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 색상 코드 정의
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
msg_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
msg_ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
msg_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
msg_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
# 헤더 출력
|
||||
print_header() {
|
||||
clear
|
||||
echo -e "${PURPLE}"
|
||||
echo "═══════════════════════════════════════════════════════════════════"
|
||||
echo " Proxmox Auto RDP Setup Script v1.0"
|
||||
echo "═══════════════════════════════════════════════════════════════════"
|
||||
echo -e "${NC}"
|
||||
echo "이 스크립트는 Proxmox VE 호스트가 부팅 시 자동으로 RDP 연결하도록 설정합니다."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Proxmox 버전 확인
|
||||
check_proxmox_version() {
|
||||
msg_info "Proxmox VE 버전 확인 중..."
|
||||
|
||||
# pveversion 명령어 확인
|
||||
if ! command -v pveversion > /dev/null 2>&1; then
|
||||
msg_warn "pveversion 명령을 찾을 수 없습니다. 버전 확인을 건너뜁니다."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 버전 추출 (여러 형식 지원)
|
||||
local pve_version=$(pveversion 2>/dev/null | head -n1 | grep -oP '\d+\.\d+' | head -n1 | cut -d'.' -f1)
|
||||
|
||||
# 버전 번호를 추출할 수 없으면 경고만 하고 계속 진행
|
||||
if [ -z "$pve_version" ]; then
|
||||
msg_warn "Proxmox VE 버전을 확인할 수 없습니다. 계속 진행합니다."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 숫자인지 확인
|
||||
if ! [[ "$pve_version" =~ ^[0-9]+$ ]]; then
|
||||
msg_warn "Proxmox VE 버전 형식이 올바르지 않습니다 ($pve_version). 계속 진행합니다."
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "Proxmox VE $pve_version.x 버전 확인됨"
|
||||
}
|
||||
|
||||
# IP 주소 가져오기 (Headscale 우선, 없으면 로컬 IP)
|
||||
get_primary_ip() {
|
||||
# Headscale VPN IP 확인 (100.64.x.x 대역)
|
||||
local headscale_ip=$(hostname -I | tr ' ' '\n' | grep '^100\.64\.' | head -n1)
|
||||
|
||||
if [ -n "$headscale_ip" ]; then
|
||||
echo "$headscale_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Headscale IP가 없으면 로컬 IP (첫 번째)
|
||||
local local_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
if [ -n "$local_ip" ]; then
|
||||
echo "$local_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 그것도 없으면 localhost
|
||||
echo "127.0.0.1"
|
||||
}
|
||||
|
||||
# 루트 권한 확인
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
msg_error "이 스크립트는 root 권한으로 실행해야 합니다. sudo를 사용하세요."
|
||||
fi
|
||||
}
|
||||
|
||||
# 입력 검증 함수
|
||||
validate_rdp_server() {
|
||||
local server="$1"
|
||||
|
||||
# 기본적인 형식 검증 (호스트:포트 또는 호스트만)
|
||||
if [[ ! "$server" =~ ^[a-zA-Z0-9.-]+(:([0-9]{1,5}))?$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 포트 범위 검증
|
||||
if [[ "$server" =~ :([0-9]+)$ ]]; then
|
||||
local port="${BASH_REMATCH[1]}"
|
||||
if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_username() {
|
||||
local username="$1"
|
||||
|
||||
# 사용자명 길이 및 문자 검증
|
||||
if [ ${#username} -lt 1 ] || [ ${#username} -gt 32 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 허용된 문자만 포함 확인
|
||||
if [[ ! "$username" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 사용자 입력 받기
|
||||
get_user_input() {
|
||||
echo -e "${CYAN}RDP 연결 정보를 입력해주세요:${NC}"
|
||||
echo ""
|
||||
|
||||
# RDP 서버 정보 (검증 포함)
|
||||
while true; do
|
||||
read -p "RDP 서버 주소 (예: example.com:3389): " RDP_SERVER </dev/tty
|
||||
if [ -z "$RDP_SERVER" ]; then
|
||||
msg_error "RDP 서버 주소는 필수입니다."
|
||||
elif ! validate_rdp_server "$RDP_SERVER"; then
|
||||
msg_warn "잘못된 서버 주소 형식입니다. 다시 입력해주세요."
|
||||
echo " 예시: example.com 또는 example.com:3389"
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 사용자명 (검증 포함)
|
||||
while true; do
|
||||
read -p "RDP 사용자명: " RDP_USERNAME </dev/tty
|
||||
if [ -z "$RDP_USERNAME" ]; then
|
||||
msg_error "RDP 사용자명은 필수입니다."
|
||||
elif ! validate_username "$RDP_USERNAME"; then
|
||||
msg_warn "잘못된 사용자명 형식입니다. 영문, 숫자, ., _, - 만 사용 가능합니다."
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 패스워드 (재확인 포함)
|
||||
while true; do
|
||||
echo -n "RDP 패스워드: "
|
||||
read -s RDP_PASSWORD </dev/tty
|
||||
echo ""
|
||||
if [ -z "$RDP_PASSWORD" ]; then
|
||||
msg_error "RDP 패스워드는 필수입니다."
|
||||
fi
|
||||
|
||||
echo -n "패스워드 확인: "
|
||||
read -s password_confirm </dev/tty
|
||||
echo ""
|
||||
|
||||
if [ "$RDP_PASSWORD" != "$password_confirm" ]; then
|
||||
msg_warn "패스워드가 일치하지 않습니다. 다시 입력해주세요."
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 로컬 사용자명 (선택사항, 검증 포함)
|
||||
while true; do
|
||||
read -p "로컬 사용자명 [rdpuser]: " LOCAL_USER </dev/tty
|
||||
LOCAL_USER=${LOCAL_USER:-rdpuser}
|
||||
|
||||
if ! validate_username "$LOCAL_USER"; then
|
||||
msg_warn "잘못된 로컬 사용자명 형식입니다. 영문, 숫자, ., _, - 만 사용 가능합니다."
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}입력된 정보:${NC}"
|
||||
echo " RDP 서버: $RDP_SERVER"
|
||||
echo " RDP 사용자: $RDP_USERNAME"
|
||||
echo " 로컬 사용자: $LOCAL_USER"
|
||||
echo ""
|
||||
|
||||
# 최종 확인
|
||||
local attempts=0
|
||||
while [ $attempts -lt 3 ]; do
|
||||
read -p "설정을 계속하시겠습니까? [y/N]: " confirm </dev/tty
|
||||
case $confirm in
|
||||
[yY]|[yY][eE][sS])
|
||||
return 0
|
||||
;;
|
||||
[nN]|[nN][oO]|"")
|
||||
msg_error "설정이 취소되었습니다."
|
||||
;;
|
||||
*)
|
||||
msg_warn "y 또는 n으로 답해주세요."
|
||||
((attempts++))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
msg_error "너무 많은 잘못된 입력으로 설정이 취소되었습니다."
|
||||
}
|
||||
|
||||
# 백업 생성
|
||||
create_backup() {
|
||||
msg_info "기존 설정 백업 중..."
|
||||
|
||||
local backup_dir="/root/proxmox-rdp-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
# 기존 설정 백업
|
||||
if [ -f /etc/systemd/system/getty@tty1.service.d/override.conf ]; then
|
||||
cp -r /etc/systemd/system/getty@tty1.service.d "$backup_dir/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if id "$LOCAL_USER" &>/dev/null; then
|
||||
cp -r "/home/$LOCAL_USER" "$backup_dir/home-$LOCAL_USER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg_ok "백업 완료: $backup_dir"
|
||||
}
|
||||
|
||||
# 네트워크 연결 확인
|
||||
check_network() {
|
||||
msg_info "네트워크 연결 확인 중..."
|
||||
|
||||
# 인터넷 연결 확인
|
||||
if ! ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; then
|
||||
msg_error "인터넷 연결을 확인할 수 없습니다. 네트워크 설정을 확인해주세요."
|
||||
fi
|
||||
|
||||
# RDP 서버 연결 확인
|
||||
local server_host="${RDP_SERVER%:*}"
|
||||
local server_port="${RDP_SERVER#*:}"
|
||||
|
||||
# 포트가 지정되지 않았으면 기본값 3389 사용
|
||||
if [ "$server_port" = "$server_host" ]; then
|
||||
server_port="3389"
|
||||
fi
|
||||
|
||||
msg_info "RDP 서버 연결 확인 중... ($server_host:$server_port)"
|
||||
|
||||
if command -v timeout > /dev/null; then
|
||||
if ! timeout 10 bash -c "</dev/tcp/$server_host/$server_port" > /dev/null 2>&1; then
|
||||
msg_warn "RDP 서버에 연결할 수 없습니다. 서버 주소와 포트를 확인해주세요."
|
||||
read -p "계속 진행하시겠습니까? [y/N]: " continue_anyway </dev/tty
|
||||
case $continue_anyway in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) msg_error "설정이 취소되었습니다." ;;
|
||||
esac
|
||||
else
|
||||
msg_ok "RDP 서버 연결 확인됨"
|
||||
fi
|
||||
else
|
||||
msg_warn "연결 테스트를 건너뜁니다 (timeout 명령어 없음)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 필수 패키지 설치
|
||||
install_packages() {
|
||||
msg_info "필수 패키지 설치 중..."
|
||||
|
||||
# 패키지 목록 업데이트
|
||||
msg_info " - 패키지 목록 업데이트 중..."
|
||||
if ! apt update > /dev/null 2>&1; then
|
||||
msg_error "패키지 목록 업데이트에 실패했습니다."
|
||||
fi
|
||||
|
||||
# 필수 패키지들
|
||||
local packages="xorg openbox unclutter freerdp3-x11"
|
||||
|
||||
for package in $packages; do
|
||||
msg_info " - $package 설치 중..."
|
||||
|
||||
# 패키지가 이미 설치되어 있는지 확인
|
||||
if dpkg -l | grep -q "^ii $package "; then
|
||||
msg_ok " $package 이미 설치됨"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 패키지 설치 시도
|
||||
local retry_count=0
|
||||
while [ $retry_count -lt 3 ]; do
|
||||
if apt install -y "$package" > /dev/null 2>&1; then
|
||||
msg_ok " $package 설치 완료"
|
||||
break
|
||||
else
|
||||
((retry_count++))
|
||||
if [ $retry_count -lt 3 ]; then
|
||||
msg_warn " $package 설치 실패, 재시도 중... ($retry_count/3)"
|
||||
sleep 2
|
||||
else
|
||||
msg_error "$package 설치에 실패했습니다. 네트워크 연결이나 패키지 저장소를 확인해주세요."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# 설치 확인 (실제 명령어 존재 여부로 확인)
|
||||
msg_info "설치된 패키지 확인 중..."
|
||||
|
||||
# 각 패키지의 주요 실행 파일 확인
|
||||
local check_commands="startx openbox xfreerdp3 unclutter"
|
||||
local all_ok=true
|
||||
|
||||
for cmd in $check_commands; do
|
||||
if command -v "$cmd" > /dev/null 2>&1; then
|
||||
msg_ok " $cmd 설치 확인됨"
|
||||
else
|
||||
msg_warn " $cmd를 찾을 수 없습니다"
|
||||
all_ok=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$all_ok" = false ]; then
|
||||
msg_warn "일부 패키지가 정상적으로 설치되지 않았을 수 있습니다. 계속 진행합니다."
|
||||
else
|
||||
msg_ok "모든 패키지 설치 완료"
|
||||
fi
|
||||
}
|
||||
|
||||
# 사용자 계정 생성
|
||||
setup_user() {
|
||||
msg_info "사용자 계정 설정 중..."
|
||||
|
||||
if ! id "$LOCAL_USER" &>/dev/null; then
|
||||
useradd -m -s /bin/bash "$LOCAL_USER"
|
||||
msg_ok "사용자 '$LOCAL_USER' 생성됨"
|
||||
else
|
||||
msg_warn "사용자 '$LOCAL_USER'가 이미 존재합니다."
|
||||
fi
|
||||
|
||||
# 홈 디렉토리 권한 설정
|
||||
chown -R "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER"
|
||||
}
|
||||
|
||||
# 자동 로그인 설정
|
||||
setup_autologin() {
|
||||
msg_info "자동 로그인 설정 중..."
|
||||
|
||||
# getty@tty1 서비스 override 디렉토리 생성
|
||||
mkdir -p /etc/systemd/system/getty@tty1.service.d
|
||||
|
||||
# override.conf 파일 생성
|
||||
cat > /etc/systemd/system/getty@tty1.service.d/override.conf << EOF
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin $LOCAL_USER --noclear %I \$TERM
|
||||
Type=idle
|
||||
EOF
|
||||
|
||||
# systemd 리로드
|
||||
systemctl daemon-reload
|
||||
|
||||
msg_ok "자동 로그인 설정 완료"
|
||||
}
|
||||
|
||||
# X 자동 시작 설정
|
||||
setup_x_autostart() {
|
||||
msg_info "X Window 자동 시작 설정 중..."
|
||||
|
||||
# .bash_profile 생성
|
||||
cat > "/home/$LOCAL_USER/.bash_profile" << 'EOF'
|
||||
# tty1에서만 X 자동 시작
|
||||
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
|
||||
startx
|
||||
logout
|
||||
fi
|
||||
EOF
|
||||
|
||||
chown "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.bash_profile"
|
||||
|
||||
msg_ok "X Window 자동 시작 설정 완료"
|
||||
}
|
||||
|
||||
# RDP 연결 설정
|
||||
setup_rdp_connection() {
|
||||
msg_info "RDP 연결 설정 중..."
|
||||
|
||||
# .xinitrc 파일 생성
|
||||
cat > "/home/$LOCAL_USER/.xinitrc" << EOF
|
||||
#!/bin/bash
|
||||
|
||||
# 화면 절전 모드 비활성화
|
||||
xset -dpms
|
||||
xset s off
|
||||
xset s noblank
|
||||
|
||||
# 마우스 커서 숨기기
|
||||
unclutter -idle 0.1 -root &
|
||||
|
||||
# Openbox 윈도우 매니저 시작
|
||||
openbox-session &
|
||||
|
||||
# 잠시 대기 (X 완전 초기화)
|
||||
sleep 2
|
||||
|
||||
# FreeRDP3를 사용한 직접 RDP 연결 (풀스크린)
|
||||
xfreerdp3 \\
|
||||
/v:$RDP_SERVER \\
|
||||
/u:$RDP_USERNAME \\
|
||||
/p:"$RDP_PASSWORD" \\
|
||||
+f \\
|
||||
/cert:ignore \\
|
||||
+dynamic-resolution \\
|
||||
/sound:sys:alsa \\
|
||||
+clipboard
|
||||
|
||||
# RDP 종료 시 X 세션도 종료
|
||||
pkill -SIGTERM Xorg
|
||||
EOF
|
||||
|
||||
# 실행 권한 및 소유권 설정
|
||||
chmod +x "/home/$LOCAL_USER/.xinitrc"
|
||||
chown "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.xinitrc"
|
||||
|
||||
msg_ok "RDP 연결 설정 완료"
|
||||
}
|
||||
|
||||
# Openbox 설정
|
||||
setup_openbox() {
|
||||
msg_info "Openbox 윈도우 매니저 설정 중..."
|
||||
|
||||
# Openbox 설정 디렉토리 생성
|
||||
mkdir -p "/home/$LOCAL_USER/.config/openbox"
|
||||
|
||||
# rc.xml 설정 파일 생성
|
||||
cat > "/home/$LOCAL_USER/.config/openbox/rc.xml" << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openbox_config xmlns="http://openbox.org/3.4/rc">
|
||||
<applications>
|
||||
<application class="*">
|
||||
<decor>no</decor>
|
||||
<maximized>yes</maximized>
|
||||
</application>
|
||||
</applications>
|
||||
</openbox_config>
|
||||
EOF
|
||||
|
||||
# 소유권 설정
|
||||
chown -R "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.config"
|
||||
|
||||
msg_ok "Openbox 설정 완료"
|
||||
}
|
||||
|
||||
# RDP Toggle API 설치
|
||||
install_rdp_api() {
|
||||
msg_info "RDP Toggle API 설치 중..."
|
||||
|
||||
local INSTALL_DIR="/opt/rdp-toggle-api"
|
||||
local VENV_DIR="$INSTALL_DIR/venv"
|
||||
local GITEA_BASE_URL="https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP"
|
||||
|
||||
# Python 및 venv 설치
|
||||
msg_info "Python 및 필수 패키지 설치 중..."
|
||||
apt install -y python3 python3-venv python3-pip curl > /dev/null 2>&1
|
||||
|
||||
# 설치 디렉토리 생성
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# 가상환경 생성
|
||||
msg_info "Python 가상환경 생성 중..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
# 가상환경에서 패키지 설치
|
||||
msg_info "FastAPI 패키지 설치 중..."
|
||||
"$VENV_DIR/bin/pip" install --upgrade pip > /dev/null 2>&1
|
||||
"$VENV_DIR/bin/pip" install fastapi==0.115.5 uvicorn==0.32.1 python-multipart==0.0.20 pydantic==2.10.3 > /dev/null 2>&1
|
||||
|
||||
# API 파일 다운로드
|
||||
msg_info "API 서버 파일 다운로드 중..."
|
||||
curl -fsSL "$GITEA_BASE_URL/rdp-toggle-api.py" -o "$INSTALL_DIR/rdp-toggle-api.py"
|
||||
chmod +x "$INSTALL_DIR/rdp-toggle-api.py"
|
||||
|
||||
# systemd 서비스 생성
|
||||
msg_info "systemd 서비스 생성 중..."
|
||||
cat > /etc/systemd/system/rdp-toggle-api.service << EOF
|
||||
[Unit]
|
||||
Description=RDP Toggle API Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$VENV_DIR/bin/python $INSTALL_DIR/rdp-toggle-api.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# RDP 설정을 API에 저장
|
||||
msg_info "RDP 설정 저장 중..."
|
||||
mkdir -p /var/lib/rdp-toggle
|
||||
cat > /var/lib/rdp-toggle/config.json << EOF
|
||||
{
|
||||
"rdp_server": "$RDP_SERVER",
|
||||
"rdp_username": "$RDP_USERNAME",
|
||||
"rdp_password": "$RDP_PASSWORD",
|
||||
"local_user": "$LOCAL_USER"
|
||||
}
|
||||
EOF
|
||||
|
||||
# 서비스 활성화 및 시작
|
||||
msg_info "API 서비스 활성화 및 시작 중..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable rdp-toggle-api.service > /dev/null 2>&1
|
||||
systemctl start rdp-toggle-api.service
|
||||
|
||||
# API 서비스 시작 대기
|
||||
sleep 3
|
||||
|
||||
# API 서비스 확인
|
||||
if systemctl is-active rdp-toggle-api.service > /dev/null 2>&1; then
|
||||
msg_ok "RDP Toggle API 설치 완료 (포트 8090)"
|
||||
return 0
|
||||
else
|
||||
msg_warn "API 서비스가 정상적으로 시작되지 않았습니다."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 설정 테스트
|
||||
test_configuration() {
|
||||
msg_info "설정 테스트 중..."
|
||||
|
||||
# systemd 서비스 상태 확인
|
||||
if systemctl is-enabled getty@tty1.service > /dev/null 2>&1; then
|
||||
msg_ok "getty@tty1 서비스가 활성화되어 있습니다."
|
||||
else
|
||||
msg_warn "getty@tty1 서비스가 비활성화되어 있습니다."
|
||||
fi
|
||||
|
||||
# 필수 파일들 존재 확인
|
||||
local files=(
|
||||
"/etc/systemd/system/getty@tty1.service.d/override.conf"
|
||||
"/home/$LOCAL_USER/.bash_profile"
|
||||
"/home/$LOCAL_USER/.xinitrc"
|
||||
"/home/$LOCAL_USER/.config/openbox/rc.xml"
|
||||
)
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
msg_ok "설정 파일 존재: $file"
|
||||
else
|
||||
msg_error "설정 파일 누락: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
# FreeRDP3 설치 확인
|
||||
if command -v xfreerdp3 > /dev/null 2>&1; then
|
||||
msg_ok "FreeRDP3 설치 확인됨"
|
||||
else
|
||||
msg_error "FreeRDP3가 설치되어 있지 않습니다."
|
||||
fi
|
||||
}
|
||||
|
||||
# 완료 메시지
|
||||
print_completion() {
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} 설정이 완료되었습니다!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}설정 요약:${NC}"
|
||||
echo " - RDP 서버: $RDP_SERVER"
|
||||
echo " - RDP 사용자: $RDP_USERNAME"
|
||||
echo " - 로컬 사용자: $LOCAL_USER"
|
||||
echo " - 자동 로그인: 활성화됨 (tty1)"
|
||||
echo " - 풀스크린 RDP: 활성화됨"
|
||||
echo " - RDP Toggle API: 설치됨 (포트 8090)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}다음 단계:${NC}"
|
||||
echo " 1. RDP 연결을 활성화하세요 (즉시 적용 또는 재부팅)"
|
||||
echo " 2. 자동으로 RDP 연결이 시작됩니다"
|
||||
echo " 3. 문제 발생 시 Ctrl+Alt+F2로 다른 터미널에 접근 가능합니다"
|
||||
echo ""
|
||||
echo -e "${CYAN}RDP 연결을 어떻게 활성화하시겠습니까?${NC}"
|
||||
echo " 1) 즉시 적용 (getty@tty1 서비스 재시작, 권장)"
|
||||
echo " 2) 시스템 재부팅"
|
||||
echo " 3) 나중에 수동으로 적용"
|
||||
echo ""
|
||||
echo -n "선택 [1/2/3]: "
|
||||
read -r apply_choice </dev/tty
|
||||
|
||||
case $apply_choice in
|
||||
1)
|
||||
msg_info "API를 통해 RDP 모드로 전환합니다..."
|
||||
systemctl daemon-reload
|
||||
sleep 1
|
||||
|
||||
# API를 통해 RDP 모드 활성화
|
||||
local api_response=$(curl -s -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}' 2>/dev/null)
|
||||
|
||||
if echo "$api_response" | grep -q '"status":"success"'; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ RDP 모드가 API를 통해 활성화되었습니다!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}참고:${NC}"
|
||||
echo " - 물리 모니터(tty1)에서 RDP 연결이 시작됩니다"
|
||||
echo " - SSH 세션은 계속 사용 가능합니다"
|
||||
echo " - RDP Toggle API: http://$(get_primary_ip):8090"
|
||||
echo ""
|
||||
echo -e "${CYAN}API 사용 예시:${NC}"
|
||||
echo " Shell 모드로 전환: curl -X POST http://localhost:8090/toggle -H 'Content-Type: application/json' -d '{\"mode\":\"shell\"}'"
|
||||
echo " 상태 확인: curl http://localhost:8090/status"
|
||||
echo ""
|
||||
else
|
||||
msg_warn "API를 통한 RDP 활성화에 실패했습니다. 수동으로 활성화합니다..."
|
||||
systemctl restart getty@tty1.service
|
||||
sleep 1
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ RDP 연결이 tty1에서 활성화되었습니다!${NC}"
|
||||
echo ""
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
msg_info "시스템을 재부팅합니다..."
|
||||
sleep 2
|
||||
reboot
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
echo -e "${GREEN}설정이 완료되었습니다.${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}수동 적용 방법:${NC}"
|
||||
echo " API로 RDP 활성화: curl -X POST http://localhost:8090/toggle -H 'Content-Type: application/json' -d '{\"mode\":\"rdp\"}'"
|
||||
echo " 또는 직접 활성화: systemctl daemon-reload && systemctl restart getty@tty1"
|
||||
echo " 또는 재부팅: reboot"
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# RDP 토글 테스트 메뉴
|
||||
show_rdp_toggle_menu() {
|
||||
while true; do
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "RDP 제어 메뉴"
|
||||
echo " 1) RDP 모드로 전환"
|
||||
echo " 2) Shell 모드로 전환"
|
||||
echo " 3) 현재 상태 확인"
|
||||
echo " 4) 종료"
|
||||
echo ""
|
||||
echo -n "선택 [1/2/3/4]: "
|
||||
read -r choice </dev/tty
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
echo ""
|
||||
msg_info "RDP 모드로 전환 중..."
|
||||
if curl -s -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}' > /dev/null 2>&1; then
|
||||
msg_ok "RDP 모드로 전환 완료!"
|
||||
echo ""
|
||||
echo "현재 상태:"
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
else
|
||||
msg_error "RDP 모드 전환 실패. 서비스 상태를 확인하세요: systemctl status rdp-toggle-api"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
echo ""
|
||||
msg_info "Shell 모드로 전환 중..."
|
||||
if curl -s -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}' > /dev/null 2>&1; then
|
||||
msg_ok "Shell 모드로 전환 완료!"
|
||||
echo ""
|
||||
echo "현재 상태:"
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
else
|
||||
msg_error "Shell 모드 전환 실패. 서비스 상태를 확인하세요: systemctl status rdp-toggle-api"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
msg_info "현재 상태 확인 중..."
|
||||
curl -s http://localhost:8090/status | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8090/status
|
||||
;;
|
||||
4)
|
||||
echo ""
|
||||
msg_ok "종료합니다."
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
msg_warn "잘못된 선택입니다."
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
# 메인 함수
|
||||
main() {
|
||||
print_header
|
||||
|
||||
# 사전 검사
|
||||
check_root
|
||||
check_proxmox_version
|
||||
|
||||
# 이미 설치되어 있는지 확인
|
||||
if systemctl is-active --quiet rdp-toggle-api.service && \
|
||||
[ -f "/opt/rdp-toggle-api/rdp-toggle-api.py" ] && \
|
||||
[ -f "/etc/systemd/system/getty@tty1.service.d/override.conf" ]; then
|
||||
|
||||
# 현재 설정 로드
|
||||
if [ -f "/var/lib/rdp-toggle/config.json" ]; then
|
||||
CURRENT_RDP_SERVER=$(python3 -c "import json; print(json.load(open('/var/lib/rdp-toggle/config.json'))['rdp_server'])" 2>/dev/null || echo "")
|
||||
CURRENT_RDP_USERNAME=$(python3 -c "import json; print(json.load(open('/var/lib/rdp-toggle/config.json'))['rdp_username'])" 2>/dev/null || echo "")
|
||||
CURRENT_LOCAL_USER=$(python3 -c "import json; print(json.load(open('/var/lib/rdp-toggle/config.json'))['local_user'])" 2>/dev/null || echo "rdpuser")
|
||||
else
|
||||
CURRENT_RDP_SERVER=""
|
||||
CURRENT_RDP_USERNAME=""
|
||||
CURRENT_LOCAL_USER="rdpuser"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}=========================================="
|
||||
echo "✅ RDP 자동화 시스템이 이미 설치되어 있습니다!"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}📍 API 서버: http://$(get_primary_ip):8090${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}현재 설정:${NC}"
|
||||
echo " RDP 서버: ${CURRENT_RDP_SERVER:-'설정되지 않음'}"
|
||||
echo " RDP 사용자: ${CURRENT_RDP_USERNAME:-'설정되지 않음'}"
|
||||
echo " 로컬 사용자: $CURRENT_LOCAL_USER"
|
||||
echo ""
|
||||
echo "다음 중 선택하세요:"
|
||||
echo " 1) RDP 토글 메뉴 (RDP ↔ Shell 전환)"
|
||||
echo " 2) RDP 설정 수정"
|
||||
echo " 3) 재설치 (기존 설정 삭제 후 새로 설치)"
|
||||
echo " 4) 종료"
|
||||
echo ""
|
||||
echo -n "선택 [1/2/3/4]: "
|
||||
read -r reinstall_choice </dev/tty
|
||||
|
||||
case "$reinstall_choice" in
|
||||
1)
|
||||
# RDP 토글 메뉴 실행
|
||||
show_rdp_toggle_menu
|
||||
exit 0
|
||||
;;
|
||||
2)
|
||||
# RDP 설정 수정
|
||||
echo ""
|
||||
echo -e "${CYAN}=========================================="
|
||||
echo "RDP 설정 수정"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# 새로운 RDP 정보 입력
|
||||
if [ -n "$CURRENT_RDP_SERVER" ]; then
|
||||
echo -n "RDP 서버 주소 (현재: $CURRENT_RDP_SERVER, Enter=유지): "
|
||||
else
|
||||
echo -n "RDP 서버 주소 (예: 192.168.0.201:3389): "
|
||||
fi
|
||||
read -r NEW_RDP_SERVER </dev/tty
|
||||
[ -z "$NEW_RDP_SERVER" ] && NEW_RDP_SERVER="$CURRENT_RDP_SERVER"
|
||||
|
||||
if [ -n "$CURRENT_RDP_USERNAME" ]; then
|
||||
echo -n "RDP 사용자명 (현재: $CURRENT_RDP_USERNAME, Enter=유지): "
|
||||
else
|
||||
echo -n "RDP 사용자명: "
|
||||
fi
|
||||
read -r NEW_RDP_USERNAME </dev/tty
|
||||
[ -z "$NEW_RDP_USERNAME" ] && NEW_RDP_USERNAME="$CURRENT_RDP_USERNAME"
|
||||
|
||||
echo -n "RDP 비밀번호: "
|
||||
read -s NEW_RDP_PASSWORD </dev/tty
|
||||
echo ""
|
||||
|
||||
# 로컬 사용자는 기본 rdpuser로 자동 설정
|
||||
NEW_LOCAL_USER="${CURRENT_LOCAL_USER:-rdpuser}"
|
||||
|
||||
echo ""
|
||||
echo "새로운 설정:"
|
||||
echo " RDP 서버: $NEW_RDP_SERVER"
|
||||
echo " RDP 사용자: $NEW_RDP_USERNAME"
|
||||
echo " 로컬 사용자: $NEW_LOCAL_USER (자동 설정)"
|
||||
echo ""
|
||||
echo -n "이 설정으로 업데이트하시겠습니까? [y/N]: "
|
||||
read -r confirm </dev/tty
|
||||
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
# 설정 파일 업데이트
|
||||
cat > /var/lib/rdp-toggle/config.json << EOF
|
||||
{
|
||||
"rdp_server": "$NEW_RDP_SERVER",
|
||||
"rdp_username": "$NEW_RDP_USERNAME",
|
||||
"rdp_password": "$NEW_RDP_PASSWORD",
|
||||
"local_user": "$NEW_LOCAL_USER"
|
||||
}
|
||||
EOF
|
||||
|
||||
# RDP 설정 파일도 업데이트
|
||||
RDP_SERVER="$NEW_RDP_SERVER"
|
||||
RDP_USERNAME="$NEW_RDP_USERNAME"
|
||||
RDP_PASSWORD="$NEW_RDP_PASSWORD"
|
||||
LOCAL_USER="$NEW_LOCAL_USER"
|
||||
|
||||
# .xinitrc 업데이트
|
||||
user_home="/home/$LOCAL_USER"
|
||||
if [ -f "$user_home/.xinitrc" ]; then
|
||||
cat > "$user_home/.xinitrc" << EOF
|
||||
#!/bin/bash
|
||||
xset -dpms
|
||||
xset s off
|
||||
xset s noblank
|
||||
unclutter -idle 0.1 -root &
|
||||
openbox-session &
|
||||
sleep 2
|
||||
xfreerdp3 /v:$RDP_SERVER /u:$RDP_USERNAME /p:"$RDP_PASSWORD" +f /cert:ignore +dynamic-resolution /sound:sys:alsa +clipboard
|
||||
pkill -SIGTERM Xorg
|
||||
EOF
|
||||
chmod +x "$user_home/.xinitrc"
|
||||
chown "$LOCAL_USER:$LOCAL_USER" "$user_home/.xinitrc"
|
||||
fi
|
||||
|
||||
msg_ok "RDP 설정이 업데이트되었습니다!"
|
||||
echo ""
|
||||
echo "변경사항을 적용하려면 RDP 모드를 다시 활성화하세요."
|
||||
echo ""
|
||||
|
||||
# 토글 메뉴로 이동
|
||||
show_rdp_toggle_menu
|
||||
else
|
||||
msg_warn "설정 변경이 취소되었습니다."
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
msg_warn "기존 설정을 삭제하고 재설치를 시작합니다..."
|
||||
# 기존 설정 삭제는 하지 않고 덮어쓰기로 진행
|
||||
echo ""
|
||||
;;
|
||||
4)
|
||||
echo ""
|
||||
msg_ok "종료합니다."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
msg_warn "잘못된 선택입니다. 종료합니다."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 사용자 입력
|
||||
get_user_input
|
||||
|
||||
# 네트워크 확인
|
||||
check_network
|
||||
|
||||
# 백업 생성
|
||||
create_backup
|
||||
|
||||
# 설정 수행
|
||||
install_packages
|
||||
setup_user
|
||||
setup_autologin
|
||||
setup_x_autostart
|
||||
setup_rdp_connection
|
||||
setup_openbox
|
||||
|
||||
# RDP Toggle API 설치
|
||||
install_rdp_api
|
||||
|
||||
# 테스트 및 완료
|
||||
test_configuration
|
||||
print_completion
|
||||
}
|
||||
|
||||
# 에러 핸들링
|
||||
trap 'msg_error "스크립트 실행 중 오류가 발생했습니다."' ERR
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
212
RDP/proxmox_auto_rdp_setup_korean.md
Normal file
212
RDP/proxmox_auto_rdp_setup_korean.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Proxmox 9.0 자동 RDP 연결 설정 가이드
|
||||
|
||||
## 개요
|
||||
Proxmox VE 9.0 (Debian 13 기반) 호스트가 부팅될 때 자동으로 Windows VM에 RDP로 풀스크린 연결하는 설정 가이드입니다.
|
||||
|
||||
**목표**: CLI 화면을 보지 않고 부팅 후 바로 RDP 화면이 풀스크린으로 표시
|
||||
|
||||
## 환경 정보
|
||||
- **OS**: Proxmox VE 9.0.5 (Debian 13 기반)
|
||||
- **RDP 대상**: ysleadersos.com:6642
|
||||
- **인증정보**: doctor-03 / @flejtm301
|
||||
|
||||
## 전체 설정 과정
|
||||
|
||||
### 1단계: 필수 패키지 설치
|
||||
|
||||
```bash
|
||||
# X 윈도우 시스템 및 관련 패키지 설치
|
||||
apt update
|
||||
apt install -y xorg openbox unclutter freerdp3-x11
|
||||
|
||||
# 설치된 패키지 확인
|
||||
dpkg -l | grep -E "(xorg|openbox|freerdp)"
|
||||
```
|
||||
|
||||
### 2단계: 사용자 계정 생성 및 설정
|
||||
|
||||
```bash
|
||||
# rdpuser 계정 생성 (이미 존재한다면 건너뛰기)
|
||||
useradd -m -s /bin/bash rdpuser
|
||||
passwd rdpuser
|
||||
|
||||
# 사용자 홈 디렉토리 권한 설정
|
||||
chown -R rdpuser:rdpuser /home/rdpuser
|
||||
```
|
||||
|
||||
### 3단계: systemd 자동 로그인 설정
|
||||
|
||||
```bash
|
||||
# getty@tty1 서비스 override 디렉토리 생성
|
||||
mkdir -p /etc/systemd/system/getty@tty1.service.d
|
||||
|
||||
# override.conf 파일 생성
|
||||
cat > /etc/systemd/system/getty@tty1.service.d/override.conf << 'EOF'
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin rdpuser --noclear %I $TERM
|
||||
Type=idle
|
||||
EOF
|
||||
|
||||
# systemd 설정 리로드
|
||||
systemctl daemon-reload
|
||||
systemctl restart getty@tty1.service
|
||||
```
|
||||
|
||||
### 4단계: 자동 X 시작 설정
|
||||
|
||||
```bash
|
||||
# rdpuser의 .bash_profile 생성
|
||||
cat > /home/rdpuser/.bash_profile << 'EOF'
|
||||
# tty1에서만 X 자동 시작
|
||||
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
|
||||
startx
|
||||
logout
|
||||
fi
|
||||
EOF
|
||||
|
||||
# 파일 소유권 설정
|
||||
chown rdpuser:rdpuser /home/rdpuser/.bash_profile
|
||||
```
|
||||
|
||||
### 5단계: X 세션 설정 (.xinitrc)
|
||||
|
||||
```bash
|
||||
# .xinitrc 파일 생성
|
||||
cat > /home/rdpuser/.xinitrc << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# 화면 절전 모드 비활성화
|
||||
xset -dpms
|
||||
xset s off
|
||||
xset s noblank
|
||||
|
||||
# 마우스 커서 숨기기
|
||||
unclutter -idle 0.1 -root &
|
||||
|
||||
# Openbox 윈도우 매니저 시작
|
||||
openbox-session &
|
||||
|
||||
# 잠시 대기 (X 완전 초기화)
|
||||
sleep 2
|
||||
|
||||
# FreeRDP3를 사용한 직접 RDP 연결 (풀스크린)
|
||||
xfreerdp3 \
|
||||
/v:ysleadersos.com:6642 \
|
||||
/u:doctor-03 \
|
||||
/p:"@flejtm301" \
|
||||
+f \
|
||||
/cert:ignore \
|
||||
+dynamic-resolution \
|
||||
/sound:sys:alsa \
|
||||
+clipboard
|
||||
|
||||
# RDP 종료 시 X 세션도 종료
|
||||
pkill -SIGTERM Xorg
|
||||
EOF
|
||||
|
||||
# 실행 권한 및 소유권 설정
|
||||
chmod +x /home/rdpuser/.xinitrc
|
||||
chown rdpuser:rdpuser /home/rdpuser/.xinitrc
|
||||
```
|
||||
|
||||
### 6단계: Openbox 설정 (풀스크린 최적화)
|
||||
|
||||
```bash
|
||||
# Openbox 설정 디렉토리 생성
|
||||
mkdir -p /home/rdpuser/.config/openbox
|
||||
|
||||
# rc.xml 설정 파일 생성 (윈도우 장식 제거, 풀스크린 강제)
|
||||
cat > /home/rdpuser/.config/openbox/rc.xml << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openbox_config xmlns="http://openbox.org/3.4/rc">
|
||||
<applications>
|
||||
<application class="*">
|
||||
<decor>no</decor>
|
||||
<maximized>yes</maximized>
|
||||
</application>
|
||||
</applications>
|
||||
</openbox_config>
|
||||
EOF
|
||||
|
||||
# 디렉토리 및 파일 소유권 설정
|
||||
chown -R rdpuser:rdpuser /home/rdpuser/.config
|
||||
```
|
||||
|
||||
## 주요 문제 해결 과정
|
||||
|
||||
### 문제 1: 초기 Remmina 사용 시 연결 실패
|
||||
- **증상**: 부팅 후 화면 깜빡임, RDP 연결되지 않음
|
||||
- **원인**: Remmina가 자동 실행 환경에서 불안정
|
||||
- **해결**: Remmina를 FreeRDP3로 교체
|
||||
|
||||
### 문제 2: .bash_profile의 exec startx 문제
|
||||
- **증상**: 로그인/로그아웃 반복 루프
|
||||
- **원인**: `exec startx`로 인한 세션 교체 문제
|
||||
- **해결**: `exec startx`를 `startx`로 변경하고 `logout` 추가
|
||||
|
||||
### 문제 3: FreeRDP3 명령어 문법 오류
|
||||
- **증상**: "Unexpected keyword" 오류
|
||||
- **해결**: 올바른 FreeRDP3 문법 적용
|
||||
- `/cert-ignore` → `/cert:ignore`
|
||||
- `/f` → `+f`
|
||||
- `/dynamic-resolution` → `+dynamic-resolution`
|
||||
- `/clipboard` → `+clipboard`
|
||||
|
||||
## 설정 파일 요약
|
||||
|
||||
### 핵심 설정 파일들:
|
||||
1. `/etc/systemd/system/getty@tty1.service.d/override.conf` - 자동 로그인
|
||||
2. `/home/rdpuser/.bash_profile` - X 자동 시작
|
||||
3. `/home/rdpuser/.xinitrc` - RDP 연결 실행
|
||||
4. `/home/rdpuser/.config/openbox/rc.xml` - 풀스크린 최적화
|
||||
|
||||
## 동작 흐름
|
||||
|
||||
1. **부팅 완료** → systemd가 tty1에서 rdpuser 자동 로그인
|
||||
2. **로그인** → .bash_profile이 tty1에서 startx 실행
|
||||
3. **X 시작** → .xinitrc가 실행됨
|
||||
4. **Openbox 실행** → 윈도우 매니저 시작
|
||||
5. **FreeRDP3 실행** → 풀스크린 RDP 연결
|
||||
6. **RDP 종료시** → X 세션도 함께 종료
|
||||
|
||||
## 테스트 및 확인
|
||||
|
||||
### 설정 확인 명령어:
|
||||
```bash
|
||||
# 자동 로그인 서비스 상태 확인
|
||||
systemctl status getty@tty1.service
|
||||
|
||||
# X 서버 실행 확인
|
||||
ps aux | grep Xorg
|
||||
|
||||
# RDP 연결 테스트 (수동)
|
||||
su - rdpuser -c "DISPLAY=:0 xfreerdp3 /v:ysleadersos.com:6642 /u:doctor-03 /p:'@flejtm301' +f /cert:ignore"
|
||||
```
|
||||
|
||||
### 로그 확인:
|
||||
```bash
|
||||
# systemd 로그 확인
|
||||
journalctl -u getty@tty1.service -f
|
||||
|
||||
# X 서버 로그 확인
|
||||
cat /home/rdpuser/.local/share/xorg/Xorg.0.log
|
||||
```
|
||||
|
||||
## 최종 결과
|
||||
|
||||
설정 완료 후 Proxmox 호스트를 재부팅하면:
|
||||
- ✅ CLI 화면을 보지 않고 바로 RDP 화면이 표시됨
|
||||
- ✅ 풀스크린 모드로 Windows VM에 자동 연결
|
||||
- ✅ 사용자 개입 없이 완전 자동화된 부팅-RDP 연결
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **보안**: 패스워드가 설정 파일에 평문으로 저장됨 (운영 환경에서는 보안 강화 필요)
|
||||
2. **네트워크**: RDP 대상 서버가 접근 가능한 상태여야 함
|
||||
3. **백업**: 설정 변경 전 기존 설정 백업 권장
|
||||
4. **권한**: 모든 설정 파일의 소유권이 rdpuser로 설정되어야 함
|
||||
|
||||
---
|
||||
*생성일: 2025-08-24*
|
||||
*작성자: Claude Code Assistant*
|
||||
276
RDP/rdp-toggle-api.py
Normal file
276
RDP/rdp-toggle-api.py
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RDP Toggle API Server
|
||||
Control RDP/Shell display mode via REST API
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="RDP Toggle API", version="1.0.0")
|
||||
|
||||
# CORS 설정 (외부에서 접근 가능)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 상태 저장 파일
|
||||
STATE_FILE = "/var/lib/rdp-toggle/state.json"
|
||||
CONFIG_FILE = "/var/lib/rdp-toggle/config.json"
|
||||
|
||||
# 기본 설정
|
||||
DEFAULT_CONFIG = {
|
||||
"rdp_server": "192.168.0.229",
|
||||
"rdp_username": "0bin",
|
||||
"rdp_password": "trajet6640",
|
||||
"local_user": "rdpuser"
|
||||
}
|
||||
|
||||
class ToggleRequest(BaseModel):
|
||||
mode: str # "rdp" or "shell"
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
rdp_server: Optional[str] = None
|
||||
rdp_username: Optional[str] = None
|
||||
rdp_password: Optional[str] = None
|
||||
local_user: Optional[str] = None
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
current_mode: str
|
||||
rdp_active: bool
|
||||
last_changed: str
|
||||
config: dict
|
||||
|
||||
def ensure_directories():
|
||||
"""필요한 디렉토리 생성"""
|
||||
os.makedirs("/var/lib/rdp-toggle", exist_ok=True)
|
||||
|
||||
def load_state():
|
||||
"""현재 상태 로드"""
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
return {
|
||||
"current_mode": "shell",
|
||||
"rdp_active": False,
|
||||
"last_changed": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def save_state(state):
|
||||
"""상태 저장"""
|
||||
ensure_directories()
|
||||
with open(STATE_FILE, 'w') as f:
|
||||
json.dump(state, f)
|
||||
|
||||
def load_config():
|
||||
"""설정 로드"""
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
return DEFAULT_CONFIG
|
||||
|
||||
def save_config(config):
|
||||
"""설정 저장"""
|
||||
ensure_directories()
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f)
|
||||
|
||||
def enable_rdp():
|
||||
"""RDP 모드 활성화"""
|
||||
config = load_config()
|
||||
|
||||
# 자동 로그인 설정
|
||||
subprocess.run([
|
||||
"mkdir", "-p", "/etc/systemd/system/getty@tty1.service.d"
|
||||
], check=False)
|
||||
|
||||
override_content = f"""[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin {config['local_user']} --noclear %I $TERM
|
||||
Type=idle"""
|
||||
|
||||
with open("/etc/systemd/system/getty@tty1.service.d/override.conf", "w") as f:
|
||||
f.write(override_content)
|
||||
|
||||
# X 자동 시작 스크립트
|
||||
bash_profile = f"""if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
|
||||
startx
|
||||
logout
|
||||
fi"""
|
||||
|
||||
user_home = f"/home/{config['local_user']}"
|
||||
with open(f"{user_home}/.bash_profile", "w") as f:
|
||||
f.write(bash_profile)
|
||||
|
||||
# .xinitrc 업데이트
|
||||
xinitrc_content = f"""#!/bin/bash
|
||||
xset -dpms
|
||||
xset s off
|
||||
xset s noblank
|
||||
unclutter -idle 0.1 -root &
|
||||
openbox-session &
|
||||
sleep 2
|
||||
xfreerdp3 /v:{config['rdp_server']} /u:{config['rdp_username']} /p:"{config['rdp_password']}" +f /cert:ignore +dynamic-resolution /sound:sys:alsa +clipboard
|
||||
pkill -SIGTERM Xorg"""
|
||||
|
||||
with open(f"{user_home}/.xinitrc", "w") as f:
|
||||
f.write(xinitrc_content)
|
||||
|
||||
subprocess.run(["chmod", "+x", f"{user_home}/.xinitrc"])
|
||||
subprocess.run(["chown", f"{config['local_user']}:{config['local_user']}",
|
||||
f"{user_home}/.bash_profile", f"{user_home}/.xinitrc"])
|
||||
|
||||
# systemd 리로드 및 getty 재시작
|
||||
subprocess.run(["systemctl", "daemon-reload"])
|
||||
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
|
||||
|
||||
return True
|
||||
|
||||
def disable_rdp():
|
||||
"""Shell 모드로 전환 (RDP 비활성화)"""
|
||||
config = load_config()
|
||||
|
||||
# RDP 프로세스 종료
|
||||
subprocess.run(["pkill", "-u", config['local_user'], "xfreerdp3"], check=False)
|
||||
subprocess.run(["pkill", "-u", config['local_user'], "-f", "xinit|Xorg|openbox"], check=False)
|
||||
|
||||
# 자동 로그인 설정 제거
|
||||
subprocess.run(["rm", "-f", "/etc/systemd/system/getty@tty1.service.d/override.conf"], check=False)
|
||||
|
||||
# 자동 시작 스크립트 제거
|
||||
user_home = f"/home/{config['local_user']}"
|
||||
subprocess.run(["rm", "-f", f"{user_home}/.bash_profile"], check=False)
|
||||
|
||||
# systemd 리로드 및 getty 재시작
|
||||
subprocess.run(["systemctl", "daemon-reload"])
|
||||
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
|
||||
|
||||
# TTY1으로 전환
|
||||
subprocess.run(["chvt", "1"], check=False)
|
||||
|
||||
return True
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""API 정보"""
|
||||
return {
|
||||
"name": "RDP Toggle API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"GET /status": "현재 상태 확인",
|
||||
"POST /toggle": "모드 전환 (rdp/shell)",
|
||||
"GET /config": "현재 설정 확인",
|
||||
"PUT /config": "설정 업데이트"
|
||||
}
|
||||
}
|
||||
|
||||
@app.get("/status", response_model=StatusResponse)
|
||||
async def get_status():
|
||||
"""현재 상태 반환"""
|
||||
state = load_state()
|
||||
config = load_config()
|
||||
|
||||
# 실제 프로세스 확인
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "xfreerdp3"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
rdp_running = result.returncode == 0
|
||||
state["rdp_active"] = rdp_running
|
||||
state["current_mode"] = "rdp" if rdp_running else "shell"
|
||||
except:
|
||||
pass
|
||||
|
||||
return StatusResponse(
|
||||
current_mode=state["current_mode"],
|
||||
rdp_active=state["rdp_active"],
|
||||
last_changed=state["last_changed"],
|
||||
config=config
|
||||
)
|
||||
|
||||
@app.post("/toggle")
|
||||
async def toggle_mode(request: ToggleRequest):
|
||||
"""모드 전환"""
|
||||
if request.mode not in ["rdp", "shell"]:
|
||||
raise HTTPException(status_code=400, detail="Mode must be 'rdp' or 'shell'")
|
||||
|
||||
state = load_state()
|
||||
|
||||
try:
|
||||
if request.mode == "rdp":
|
||||
success = enable_rdp()
|
||||
if success:
|
||||
state["current_mode"] = "rdp"
|
||||
state["rdp_active"] = True
|
||||
else: # shell
|
||||
success = disable_rdp()
|
||||
if success:
|
||||
state["current_mode"] = "shell"
|
||||
state["rdp_active"] = False
|
||||
|
||||
state["last_changed"] = datetime.now().isoformat()
|
||||
save_state(state)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": request.mode,
|
||||
"message": f"Switched to {request.mode} mode"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config():
|
||||
"""현재 설정 반환"""
|
||||
return load_config()
|
||||
|
||||
@app.put("/config")
|
||||
async def update_config(update: ConfigUpdate):
|
||||
"""설정 업데이트"""
|
||||
config = load_config()
|
||||
|
||||
if update.rdp_server:
|
||||
config["rdp_server"] = update.rdp_server
|
||||
if update.rdp_username:
|
||||
config["rdp_username"] = update.rdp_username
|
||||
if update.rdp_password is not None:
|
||||
config["rdp_password"] = update.rdp_password
|
||||
if update.local_user:
|
||||
config["local_user"] = update.local_user
|
||||
|
||||
save_config(config)
|
||||
|
||||
# 현재 RDP 모드인 경우 재시작
|
||||
state = load_state()
|
||||
if state["rdp_active"]:
|
||||
disable_rdp()
|
||||
enable_rdp()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"config": config
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Root 권한 확인
|
||||
if os.geteuid() != 0:
|
||||
print("This script must be run as root")
|
||||
exit(1)
|
||||
|
||||
ensure_directories()
|
||||
|
||||
# 서버 시작
|
||||
uvicorn.run(app, host="0.0.0.0", port=8090)
|
||||
480
RDP/rdp-toggle-web.html
Normal file
480
RDP/rdp-toggle-web.html
Normal file
@@ -0,0 +1,480 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RDP Toggle Control</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #f7f8fc;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.toggle-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
background: #ccc;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-slider {
|
||||
transform: translateX(40px);
|
||||
}
|
||||
|
||||
.mode-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: #f7f8fc;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
flex: 0 0 120px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #cbd5e0;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🖥️ RDP Toggle Control</h1>
|
||||
|
||||
<div class="alert" id="alert"></div>
|
||||
|
||||
<div class="status-card">
|
||||
<div class="status-row">
|
||||
<span class="status-label">연결 상태</span>
|
||||
<span class="status-value">
|
||||
<span class="status-indicator" id="status-indicator"></span>
|
||||
<span id="connection-status">확인 중...</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">현재 모드</span>
|
||||
<span class="status-value" id="current-mode">-</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">마지막 변경</span>
|
||||
<span class="status-value" id="last-changed">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-section">
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-label">
|
||||
<span>Shell</span>
|
||||
<div class="toggle-switch" id="toggle-switch">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<span>RDP</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="config-title">RDP 설정</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">서버 주소:</span>
|
||||
<input type="text" class="config-value" id="rdp-server" placeholder="예: 192.168.0.150">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">사용자명:</span>
|
||||
<input type="text" class="config-value" id="rdp-username" placeholder="예: administrator">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">비밀번호:</span>
|
||||
<input type="password" class="config-value" id="rdp-password" placeholder="비밀번호">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="refreshStatus()">새로고침</button>
|
||||
<button class="btn btn-primary" onclick="saveConfig()">설정 저장</button>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p style="margin-top: 10px; color: #666;">처리 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = window.location.hostname === 'localhost'
|
||||
? 'http://localhost:8090'
|
||||
: `http://${window.location.hostname}:8090`;
|
||||
|
||||
let currentMode = 'shell';
|
||||
let isUpdating = false;
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/status`);
|
||||
if (!response.ok) throw new Error('API 연결 실패');
|
||||
|
||||
const data = await response.json();
|
||||
updateUI(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Status fetch error:', error);
|
||||
showAlert('API 서버에 연결할 수 없습니다.', 'error');
|
||||
document.getElementById('connection-status').textContent = '오프라인';
|
||||
document.getElementById('status-indicator').className = 'status-indicator inactive';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(data) {
|
||||
currentMode = data.current_mode;
|
||||
|
||||
// 상태 업데이트
|
||||
document.getElementById('connection-status').textContent = '온라인';
|
||||
document.getElementById('status-indicator').className = 'status-indicator active';
|
||||
document.getElementById('current-mode').textContent =
|
||||
currentMode === 'rdp' ? 'RDP 모드' : 'Shell 모드';
|
||||
|
||||
// 시간 포맷
|
||||
const lastChanged = new Date(data.last_changed);
|
||||
document.getElementById('last-changed').textContent =
|
||||
lastChanged.toLocaleString('ko-KR');
|
||||
|
||||
// 토글 스위치 업데이트
|
||||
const toggleSwitch = document.getElementById('toggle-switch');
|
||||
if (currentMode === 'rdp') {
|
||||
toggleSwitch.classList.add('active');
|
||||
} else {
|
||||
toggleSwitch.classList.remove('active');
|
||||
}
|
||||
|
||||
// 설정 값 업데이트
|
||||
if (data.config) {
|
||||
document.getElementById('rdp-server').value = data.config.rdp_server || '';
|
||||
document.getElementById('rdp-username').value = data.config.rdp_username || '';
|
||||
document.getElementById('rdp-password').value = data.config.rdp_password || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleMode() {
|
||||
if (isUpdating) return;
|
||||
|
||||
isUpdating = true;
|
||||
showLoading(true);
|
||||
|
||||
const newMode = currentMode === 'rdp' ? 'shell' : 'rdp';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ mode: newMode })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('모드 전환 실패');
|
||||
|
||||
const data = await response.json();
|
||||
showAlert(`${newMode === 'rdp' ? 'RDP' : 'Shell'} 모드로 전환되었습니다.`, 'success');
|
||||
|
||||
// 상태 새로고침
|
||||
setTimeout(() => fetchStatus(), 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Toggle error:', error);
|
||||
showAlert('모드 전환에 실패했습니다.', 'error');
|
||||
// 토글 스위치 원래대로
|
||||
const toggleSwitch = document.getElementById('toggle-switch');
|
||||
if (currentMode === 'rdp') {
|
||||
toggleSwitch.classList.add('active');
|
||||
} else {
|
||||
toggleSwitch.classList.remove('active');
|
||||
}
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
showLoading(true);
|
||||
|
||||
const config = {
|
||||
rdp_server: document.getElementById('rdp-server').value,
|
||||
rdp_username: document.getElementById('rdp-username').value,
|
||||
rdp_password: document.getElementById('rdp-password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/config`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('설정 저장 실패');
|
||||
|
||||
showAlert('설정이 저장되었습니다.', 'success');
|
||||
fetchStatus();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Config save error:', error);
|
||||
showAlert('설정 저장에 실패했습니다.', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
fetchStatus();
|
||||
showAlert('상태를 새로고침했습니다.', 'success');
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = `alert ${type}`;
|
||||
alert.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
alert.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 토글 스위치 이벤트
|
||||
document.getElementById('toggle-switch').addEventListener('click', toggleMode);
|
||||
|
||||
// 초기 로드
|
||||
fetchStatus();
|
||||
|
||||
// 주기적 상태 업데이트 (10초마다)
|
||||
setInterval(fetchStatus, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
4
RDP/requirements.txt
Normal file
4
RDP/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.5
|
||||
uvicorn==0.32.1
|
||||
python-multipart==0.0.20
|
||||
pydantic==2.10.3
|
||||
207
README.md
207
README.md
@@ -114,21 +114,21 @@ grep -r "enterprise.proxmox.com" /etc/apt/sources.list.d/
|
||||
```
|
||||
## 💻 개발 환경 설정 (code-server)
|
||||
|
||||
팜큐 네트워크에 연결된 서버에서 **웹 기반 VS Code 개발 환경**을 빠르게 구축:
|
||||
서버에서 **웹 기반 VS Code 개발 환경**을 빠르게 구축:
|
||||
|
||||
### 한 줄 설치 (권장)
|
||||
### 기본 설치 (포트 8080)
|
||||
```bash
|
||||
# 기본 포트 8080으로 설치
|
||||
curl -fsSL https://git.0bin.in/thug0bin/headscale-tailscale-replacement/raw/branch/live/pharmq-headscale-production/docs/code-server.sh | bash
|
||||
|
||||
# 포트 지정 설치 (예: 8443)
|
||||
curl -fsSL https://git.0bin.in/thug0bin/headscale-tailscale-replacement/raw/branch/live/pharmq-headscale-production/docs/code-server.sh | PORT=8443 bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/code-server.sh | bash
|
||||
```
|
||||
|
||||
### 무인 설치 (비밀번호 환경변수 설정)
|
||||
### 포트 지정 설치
|
||||
```bash
|
||||
# 비밀번호를 환경변수로 전달
|
||||
curl -fsSL https://git.0bin.in/thug0bin/headscale-tailscale-replacement/raw/branch/live/pharmq-headscale-production/docs/code-server.sh | PASSWORD="your-secure-password" SKIP_CONFIRM=1 bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/code-server.sh | PORT=8443 bash
|
||||
```
|
||||
|
||||
### 무인 설치 (비밀번호 환경변수)
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/code-server.sh | PASSWORD="your-secure-password" SKIP_CONFIRM=1 bash
|
||||
```
|
||||
|
||||
### 자동 설치 기능
|
||||
@@ -142,6 +142,193 @@ curl -fsSL https://git.0bin.in/thug0bin/headscale-tailscale-replacement/raw/bran
|
||||
```bash
|
||||
# 브라우저에서 접속
|
||||
http://<서버IP>:8080
|
||||
```
|
||||
|
||||
## 🤖 Claude Code CLI 설치
|
||||
|
||||
**AI 기반 개발 도구**를 서버에 설치:
|
||||
|
||||
### 기본 설치
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/install-claude-code.sh | bash
|
||||
```
|
||||
|
||||
### 수동 설치 (스크립트 확인 후 실행)
|
||||
```bash
|
||||
wget https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/install-claude-code.sh
|
||||
chmod +x install-claude-code.sh
|
||||
./install-claude-code.sh
|
||||
```
|
||||
|
||||
### 설치 내용
|
||||
- ✅ **Node.js 20.x LTS** 자동 설치
|
||||
- ✅ **npm** 포함 설치
|
||||
- ✅ **Claude Code CLI** 글로벌 설치
|
||||
- ✅ 설치 확인 및 버전 출력
|
||||
|
||||
### 사용 방법
|
||||
```bash
|
||||
claude-code # Claude Code 시작
|
||||
claude-code --help # 도움말 보기
|
||||
```
|
||||
|
||||
## 🌐 Headscale VPN 등록 및 약국 자동 생성
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**기능:**
|
||||
- ✅ 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 통합**을 한 번에:
|
||||
|
||||
### 빠른 설치
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pbs_allinone.sh | bash
|
||||
```
|
||||
|
||||
### 수동 설치
|
||||
```bash
|
||||
wget https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pbs_allinone.sh
|
||||
chmod +x pbs_allinone.sh
|
||||
./pbs_allinone.sh
|
||||
```
|
||||
|
||||
### 설치 내용
|
||||
- ✅ **PBS 저장소 설정** (no-subscription)
|
||||
- ✅ **PBS 패키지 설치**
|
||||
- ✅ **백업 저장소 생성 및 마운트**
|
||||
- ✅ **방화벽 설정** (포트 8007)
|
||||
- ✅ **Proxmox VE PBS 통합**
|
||||
|
||||
### 설치 후 접속
|
||||
```bash
|
||||
# 브라우저에서 PBS 웹 인터페이스 접속
|
||||
https://<서버IP>:8007
|
||||
```
|
||||
|
||||
## 🏷️ Proxmox VE 호스트명/FQDN 변경
|
||||
|
||||
**Proxmox 설치 후 안전하게 호스트명 및 FQDN 변경**:
|
||||
|
||||
### 빠른 실행
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pve-host-changer.sh | bash
|
||||
```
|
||||
|
||||
### 수동 실행
|
||||
```bash
|
||||
wget https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pve-host-changer.sh
|
||||
chmod +x pve-host-changer.sh
|
||||
./pve-host-changer.sh
|
||||
```
|
||||
|
||||
### 주요 기능
|
||||
- ✅ **현재 설정 확인** 및 백업
|
||||
- ✅ **호스트명/도메인 입력 검증**
|
||||
- ✅ **자동 설정 파일 수정** (/etc/hostname, /etc/hosts)
|
||||
- ✅ **Proxmox 서비스 재시작**
|
||||
- ✅ **인증서 자동 재발급**
|
||||
- ✅ **변경사항 검증** 및 롤백 가이드
|
||||
|
||||
### 주의사항
|
||||
- 단독 노드(Single node)에서는 안전하게 변경 가능
|
||||
- 클러스터 구성 시 신중하게 진행 필요
|
||||
- 변경 후 시스템 재부팅 권장
|
||||
|
||||
## 🖥️ Proxmox RDP 자동화
|
||||
|
||||
**Proxmox 호스트에서 RDP 자동 연결 및 원격 제어**:
|
||||
|
||||
### RDP 올인원 설정 (권장)
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/proxmox-auto-rdp-setup.sh | bash
|
||||
```
|
||||
|
||||
**설치 내용:**
|
||||
- ✅ X Window + Openbox 윈도우 매니저
|
||||
- ✅ FreeRDP3 클라이언트 설치
|
||||
- ✅ 자동 로그인 및 X 시작 설정
|
||||
- ✅ RDP 서버 연결 정보 구성
|
||||
- ✅ **RDP Toggle API 자동 설치** (포트 8090)
|
||||
- ✅ **API를 통한 RDP 모드 활성화**
|
||||
|
||||
**특징:**
|
||||
- 🔒 **API 작동 = RDP 제어 가능 보장**
|
||||
- 🚀 **즉시 적용 옵션** (재부팅 불필요)
|
||||
- 🎛️ **원격 Shell/RDP 모드 전환 가능**
|
||||
|
||||
### RDP Toggle API 단독 설치
|
||||
이미 RDP 초기 설정이 완료된 경우에만 사용:
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/RDP/install-rdp-api.sh | bash
|
||||
```
|
||||
|
||||
**기능:**
|
||||
- ✅ **REST API로 RDP/Shell 모드 전환**
|
||||
- ✅ FastAPI 기반 서버 (포트 8090)
|
||||
- ✅ 프론트엔드 통합 가능 (CORS 지원)
|
||||
- ✅ 실시간 상태 모니터링
|
||||
- ✅ 설정 동적 변경 (API로 RDP 서버 정보 변경)
|
||||
|
||||
### API 사용 예시
|
||||
```bash
|
||||
# 현재 상태 확인
|
||||
curl http://localhost:8090/status
|
||||
|
||||
# RDP 모드로 전환
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"rdp"}'
|
||||
|
||||
# Shell 모드로 전환
|
||||
curl -X POST http://localhost:8090/toggle \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"shell"}'
|
||||
```
|
||||
|
||||
📖 **자세한 가이드**: [RDP/README.md](RDP/README.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
|
||||
263
SCRIPT_IMPROVEMENT_PLAN.md
Normal file
263
SCRIPT_IMPROVEMENT_PLAN.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# headscale-quick-install.sh 개선 계획
|
||||
|
||||
## 목표
|
||||
Headscale VPN 등록 시 **farmq.db와 gateway.db에 자동으로 약국 및 관리자 계정 생성**하여
|
||||
스크립트 실행만으로 **즉시 프론트엔드 로그인 가능**하게 만들기
|
||||
|
||||
## 자동 생성 플로우
|
||||
|
||||
```
|
||||
1. Headscale VPN 등록 → VPN IP 부여 (예: 100.64.0.25)
|
||||
2. farmq-admin API 호출 → farmq.db에 약국 생성
|
||||
- pharmacy_code: P0005 (자동 증가)
|
||||
- pharmacy_name: 사용자 입력
|
||||
- tailscale_ip: 100.64.0.25 (VPN IP)
|
||||
- hira_code: 사용자 입력 (선택)
|
||||
- api_port: 8082 (기본값)
|
||||
|
||||
3. gateway API 호출 → gateway.db에 admin 계정 생성
|
||||
- username: p0005 (pharmacy_code 소문자)
|
||||
- password: 1234 (기본 비밀번호)
|
||||
- email: p0005@pharmq.internal
|
||||
- name: {pharmacy_name} 관리자
|
||||
- role: admin
|
||||
- primary_pharmacy_code: P0005
|
||||
- pharmacy_members에도 자동 매핑됨
|
||||
|
||||
4. 로그인 정보 출력
|
||||
```
|
||||
|
||||
## 추가할 함수들
|
||||
|
||||
### 1. `collect_pharmacy_info()`
|
||||
약국 기본 정보를 사용자로부터 입력받음
|
||||
|
||||
```bash
|
||||
collect_pharmacy_info() {
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${WHITE}약국 정보 입력${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
# 약국명 입력 (필수)
|
||||
while [ -z "$PHARMACY_NAME" ]; do
|
||||
read -p "약국명을 입력하세요: " PHARMACY_NAME
|
||||
done
|
||||
|
||||
# 요양기관부호 입력 (선택)
|
||||
read -p "요양기관부호 (선택, Enter로 건너뛰기): " HIRA_CODE
|
||||
|
||||
# 약국 주소 입력 (선택)
|
||||
read -p "약국 주소 (선택): " PHARMACY_ADDRESS
|
||||
|
||||
# 약국장 이름 입력 (선택)
|
||||
read -p "약국장 이름 (선택): " OWNER_NAME
|
||||
|
||||
# 연락처 입력 (선택)
|
||||
read -p "약국 연락처 (선택): " PHARMACY_PHONE
|
||||
|
||||
echo -e "${GREEN}✓ 약국 정보 입력 완료${NC}"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `get_assigned_vpn_ip()`
|
||||
Headscale에서 부여받은 VPN IP 가져오기
|
||||
|
||||
```bash
|
||||
get_assigned_vpn_ip() {
|
||||
echo -e "${BLUE}VPN IP 확인 중...${NC}"
|
||||
|
||||
# tailscale status로 IP 추출
|
||||
VPN_IP=$(tailscale status --json 2>/dev/null | grep -oP '"TailscaleIPs":\["(\d+\.\d+\.\d+\.\d+)"' | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
|
||||
|
||||
if [ -z "$VPN_IP" ]; then
|
||||
echo -e "${RED}✗ VPN IP를 가져올 수 없습니다${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ VPN IP: $VPN_IP${NC}"
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `create_pharmacy_via_api()`
|
||||
farmq-admin API를 호출하여 약국 생성
|
||||
|
||||
```bash
|
||||
create_pharmacy_via_api() {
|
||||
echo -e "${BLUE}약국 등록 중 (farmq.db)...${NC}"
|
||||
|
||||
# JSON 데이터 구성
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
"vpn_ip": "$VPN_IP",
|
||||
"hira_code": "$HIRA_CODE",
|
||||
"address": "$PHARMACY_ADDRESS",
|
||||
"owner_name": "$OWNER_NAME",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"api_port": 8082
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# API 호출 (외부 도메인)
|
||||
RESPONSE=$(curl -s -X POST https://demo.pharmq.kr/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# pharmacy_code 추출
|
||||
PHARMACY_CODE=$(echo "$RESPONSE" | grep -oP '"pharmacy_code":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$PHARMACY_CODE" ]; then
|
||||
echo -e "${RED}✗ 약국 생성 실패${NC}"
|
||||
echo "$RESPONSE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ 약국 생성 완료: $PHARMACY_CODE${NC}"
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
### 4. `create_gateway_user_via_api()`
|
||||
gateway API를 호출하여 관리자 계정 생성
|
||||
|
||||
```bash
|
||||
create_gateway_user_via_api() {
|
||||
echo -e "${BLUE}관리자 계정 생성 중 (gateway.db)...${NC}"
|
||||
|
||||
# username: pharmacy_code 소문자 (P0005 → p0005)
|
||||
USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')
|
||||
PASSWORD="1234" # 기본 비밀번호
|
||||
EMAIL="${USERNAME}@pharmq.internal"
|
||||
|
||||
# JSON 데이터 구성
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"username": "$USERNAME",
|
||||
"email": "$EMAIL",
|
||||
"password": "$PASSWORD",
|
||||
"name": "${PHARMACY_NAME} 관리자",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"primary_pharmacy_code": "$PHARMACY_CODE",
|
||||
"role": "admin"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# API 호출 (외부 도메인)
|
||||
RESPONSE=$(curl -s -X POST https://gateway.pharmq.kr/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# 성공 여부 확인
|
||||
if echo "$RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✓ 관리자 계정 생성 완료${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ 관리자 계정 생성 실패${NC}"
|
||||
echo "$RESPONSE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `display_login_credentials()`
|
||||
생성된 로그인 정보 출력
|
||||
|
||||
```bash
|
||||
display_login_credentials() {
|
||||
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${WHITE}🎉 설치 및 등록 완료!${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
echo -e "\n${GREEN}약국 정보:${NC}"
|
||||
echo -e " 약국 코드: ${WHITE}$PHARMACY_CODE${NC}"
|
||||
echo -e " 약국명: ${WHITE}$PHARMACY_NAME${NC}"
|
||||
echo -e " VPN IP: ${WHITE}$VPN_IP${NC}"
|
||||
|
||||
echo -e "\n${GREEN}프론트엔드 로그인 정보:${NC}"
|
||||
echo -e " URL: ${WHITE}https://pharmq.kr${NC}"
|
||||
echo -e " 아이디: ${WHITE}$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')${NC}"
|
||||
echo -e " 비밀번호: ${WHITE}1234${NC}"
|
||||
echo -e " ${YELLOW}⚠ 최초 로그인 후 비밀번호를 변경하세요!${NC}"
|
||||
|
||||
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
```
|
||||
|
||||
## main() 함수 수정
|
||||
|
||||
```bash
|
||||
main() {
|
||||
print_header "팜큐(FARMQ) Headscale 원클릭 설치"
|
||||
|
||||
# 사전 체크
|
||||
detect_os
|
||||
check_requirements
|
||||
|
||||
# 설치 과정
|
||||
install_tailscale
|
||||
start_tailscale
|
||||
register_headscale
|
||||
|
||||
# VPN IP 확인
|
||||
sleep 3 # Headscale에서 IP 할당 대기
|
||||
get_assigned_vpn_ip || exit 1
|
||||
|
||||
# 약국 정보 수집
|
||||
collect_pharmacy_info
|
||||
|
||||
# 약국 및 계정 생성
|
||||
create_pharmacy_via_api || exit 1
|
||||
create_gateway_user_via_api || exit 1
|
||||
|
||||
# 사후 설정
|
||||
configure_firewall
|
||||
verify_connection
|
||||
|
||||
# 정리 및 완료
|
||||
cleanup
|
||||
display_login_credentials
|
||||
}
|
||||
```
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
| API | URL | 용도 | 접근 방식 |
|
||||
|-----|-----|------|----------|
|
||||
| farmq-admin | https://demo.pharmq.kr/api/pharmacy | 약국 생성 | 외부 도메인 (HTTPS) |
|
||||
| gateway | https://gateway.pharmq.kr/api/auth/register | 사용자 생성 | 외부 도메인 (HTTPS) |
|
||||
|
||||
**장점**:
|
||||
- ✅ 모든 API가 외부 도메인으로 통일되어 있음
|
||||
- ✅ HTTPS로 보안 통신
|
||||
- ✅ 내부 네트워크 접근 불필요
|
||||
- ✅ VPN 망 내부/외부 어디서든 실행 가능
|
||||
|
||||
## 보안 고려사항
|
||||
|
||||
1. **기본 비밀번호 1234**: 간단하지만 프론트엔드에서 강제 변경 유도 필요
|
||||
2. **API 키 인증**: 현재는 public API지만 나중에 인증 토큰 추가 고려
|
||||
3. **HTTPS**: 현재 HTTP이지만 production에서는 HTTPS 사용 권장
|
||||
|
||||
## 에러 처리
|
||||
|
||||
- VPN IP 할당 실패 시 스크립트 중단
|
||||
- 약국 생성 실패 시 스크립트 중단
|
||||
- 사용자 생성 실패 시 경고만 출력 (약국은 이미 생성됨)
|
||||
|
||||
## 테스트 시나리오
|
||||
|
||||
1. ✅ 정상 플로우: 모든 정보 입력 → 약국 및 계정 생성 → 로그인 성공
|
||||
2. ✅ 선택 정보 생략: 약국명만 입력 → 약국 및 계정 생성 → 로그인 성공
|
||||
3. ✅ VPN IP 할당 실패: 에러 메시지 출력 후 종료
|
||||
4. ✅ 약국 생성 실패: 에러 메시지 출력 후 종료
|
||||
5. ✅ 사용자 생성 실패: 경고 출력, 수동 생성 안내
|
||||
|
||||
## 다음 단계
|
||||
|
||||
1. ✅ farmq-admin API 개선 (VPN IP, hira_code 지원) - 완료
|
||||
2. ✅ gateway API 개선 (pharmacy 검증, pharmacy_members 자동 추가) - 완료
|
||||
3. ⏳ headscale-quick-install.sh 수정 - 진행 중
|
||||
4. ⏳ 통합 테스트
|
||||
217
Tailscale_TUN_Fix_25_11_02.md
Normal file
217
Tailscale_TUN_Fix_25_11_02.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Tailscale TUN 디바이스 설정 성공 보고서
|
||||
|
||||
**작성일시**: 2025-11-02 17:21 KST
|
||||
**대상 컨테이너**: Ubuntu24 LXC (VMID 101)
|
||||
**작업 목적**: LXC 컨테이너에서 Tailscale 실행을 위한 TUN 디바이스 설정
|
||||
|
||||
---
|
||||
|
||||
## 문제 상황
|
||||
|
||||
### 초기 증상
|
||||
- Tailscale 데몬(tailscaled.service) 실행 실패
|
||||
- 상태: `failed (Result: exit-code)`
|
||||
- 재시작 시도 5회 후 중단
|
||||
|
||||
### 원인 분석
|
||||
```
|
||||
Nov 02 16:22:09 Ubuntu24 tailscaled[299184]: wgengine.NewUserspaceEngine(tun "tailscale0") error:
|
||||
tstun.New("tailscale0"): CreateTUN("tailscale0") failed; /dev/net/tun does not exist
|
||||
```
|
||||
|
||||
**핵심 문제**: LXC 컨테이너 내부에 `/dev/net/tun` 디바이스가 존재하지 않음
|
||||
|
||||
### 기술적 배경
|
||||
- Tailscale은 VPN 터널링을 위해 TUN 네트워크 디바이스가 필요
|
||||
- LXC 컨테이너는 기본적으로 보안을 위해 디바이스 접근을 제한
|
||||
- TUN 디바이스는 문자 디바이스(character device) `c 10:200`
|
||||
|
||||
---
|
||||
|
||||
## 해결 과정
|
||||
|
||||
### 1. 사전 백업
|
||||
```bash
|
||||
# 스냅샷 생성
|
||||
pct snapshot 101 tailscale_fix_before -description "Tailscale TUN 설정 전 백업 - 2025-11-02 17:16"
|
||||
|
||||
# 설정 파일 백업
|
||||
cp /etc/pve/lxc/101.conf /etc/pve/lxc/101.conf.backup
|
||||
```
|
||||
|
||||
**백업 항목**:
|
||||
- LXC 전체 시스템 스냅샷
|
||||
- 설정 파일 백업
|
||||
- 실행 중인 서비스 상태 리포트 작성
|
||||
|
||||
### 2. 컨테이너 중지
|
||||
```bash
|
||||
pct stop 101
|
||||
```
|
||||
|
||||
**영향받은 서비스**:
|
||||
- code-server (포트 8680, 8080)
|
||||
- gp9.service (약국 크롤링)
|
||||
- prescription-monitoring.service (처방전 모니터링)
|
||||
- mosquitto (MQTT 브로커)
|
||||
- samba (파일 공유)
|
||||
|
||||
### 3. LXC 설정 파일 수정
|
||||
|
||||
**수정 파일**: `/etc/pve/lxc/101.conf`
|
||||
|
||||
**추가된 설정**:
|
||||
```conf
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net dev/net none bind,create=dir
|
||||
```
|
||||
|
||||
**설정 설명**:
|
||||
- `lxc.cgroup2.devices.allow: c 10:200 rwm`
|
||||
- cgroup2를 통해 문자 디바이스 10:200 (TUN) 접근 허용
|
||||
- rwm = read, write, mknod 권한
|
||||
|
||||
- `lxc.mount.entry: /dev/net dev/net none bind,create=dir`
|
||||
- 호스트의 `/dev/net` 디렉토리를 컨테이너에 바인드 마운트
|
||||
- `create=dir`: 디렉토리가 없으면 자동 생성
|
||||
|
||||
**설정 위치**: 메인 컨테이너 설정 블록 끝 (unprivileged: 0 다음)
|
||||
|
||||
### 4. 컨테이너 재시작
|
||||
```bash
|
||||
pct start 101
|
||||
```
|
||||
|
||||
**재시작 완료**: 약 5초 소요
|
||||
|
||||
### 5. TUN 디바이스 확인
|
||||
```bash
|
||||
pct exec 101 -- ls -la /dev/net/tun
|
||||
```
|
||||
|
||||
**결과**:
|
||||
```
|
||||
crw-rw-rw- 1 root root 10, 200 Sep 18 22:34 /dev/net/tun
|
||||
```
|
||||
✅ TUN 디바이스 정상 생성 확인
|
||||
|
||||
### 6. Tailscale 서비스 시작
|
||||
```bash
|
||||
pct exec 101 -- systemctl start tailscaled
|
||||
pct exec 101 -- systemctl status tailscaled
|
||||
```
|
||||
|
||||
**결과**:
|
||||
```
|
||||
● tailscaled.service - Tailscale node agent
|
||||
Loaded: loaded (/usr/lib/systemd/system/tailscaled.service; enabled; preset: enabled)
|
||||
Active: active (running) since Sun 2025-11-02 17:20:55 KST
|
||||
Main PID: 256 (tailscaled)
|
||||
Status: "Needs login: "
|
||||
Tasks: 11
|
||||
Memory: 50.7M
|
||||
CPU: 602ms
|
||||
```
|
||||
✅ Tailscale 데몬 정상 실행 확인
|
||||
|
||||
---
|
||||
|
||||
## 최종 결과
|
||||
|
||||
### 성공 확인
|
||||
- TUN 디바이스: **정상 작동** (`/dev/net/tun` 존재)
|
||||
- Tailscale 데몬: **active (running)**
|
||||
- 서비스 상태: "Needs login" (정상 - 로그인 대기 상태)
|
||||
- 에러 로그: **없음**
|
||||
|
||||
### 추가 작업 필요
|
||||
Tailscale 네트워크 연결을 위해 로그인 필요:
|
||||
```bash
|
||||
pct exec 101 -- tailscale up
|
||||
```
|
||||
위 명령 실행 후 제공되는 인증 URL로 접속하여 Tailscale 계정 인증
|
||||
|
||||
---
|
||||
|
||||
## 기술 요약
|
||||
|
||||
### 적용된 LXC 설정
|
||||
| 설정 항목 | 값 | 용도 |
|
||||
|----------|-----|------|
|
||||
| lxc.cgroup2.devices.allow | c 10:200 rwm | TUN 디바이스 접근 권한 |
|
||||
| lxc.mount.entry | /dev/net dev/net none bind,create=dir | TUN 디바이스 마운트 |
|
||||
|
||||
### 디바이스 정보
|
||||
- **디바이스 타입**: Character device
|
||||
- **Major number**: 10
|
||||
- **Minor number**: 200
|
||||
- **권한**: crw-rw-rw- (읽기/쓰기)
|
||||
- **경로**: /dev/net/tun
|
||||
|
||||
---
|
||||
|
||||
## 복원 방법
|
||||
|
||||
문제 발생 시 이전 상태로 복원:
|
||||
|
||||
### 방법 1: 스냅샷 복원
|
||||
```bash
|
||||
pct rollback 101 tailscale_fix_before
|
||||
pct start 101
|
||||
```
|
||||
|
||||
### 방법 2: 설정 파일 복원
|
||||
```bash
|
||||
pct stop 101
|
||||
cp /etc/pve/lxc/101.conf.backup /etc/pve/lxc/101.conf
|
||||
pct start 101
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
### LXC에서 Tailscale 실행하기
|
||||
- Proxmox LXC는 기본적으로 보안을 위해 장치 접근 제한
|
||||
- VPN 솔루션(Tailscale, WireGuard 등)은 TUN/TAP 디바이스 필요
|
||||
- unprivileged 컨테이너에서는 추가 설정 필요할 수 있음
|
||||
|
||||
### 관련 문서
|
||||
- Tailscale 공식 문서: https://tailscale.com/kb/
|
||||
- Proxmox LXC 문서: https://pve.proxmox.com/wiki/Linux_Container
|
||||
- Linux TUN/TAP: https://www.kernel.org/doc/Documentation/networking/tuntap.txt
|
||||
|
||||
---
|
||||
|
||||
## 작업 이력
|
||||
|
||||
| 시간 | 작업 내용 | 상태 |
|
||||
|------|-----------|------|
|
||||
| 17:16 | 스냅샷 생성 (tailscale_fix_before) | ✅ 완료 |
|
||||
| 17:16 | 리포트 작성 (Ubuntu24_LXC_Status_Report.md) | ✅ 완료 |
|
||||
| 17:17 | LXC 컨테이너 중지 | ✅ 완료 |
|
||||
| 17:18 | TUN 디바이스 설정 추가 | ✅ 완료 |
|
||||
| 17:19 | LXC 컨테이너 시작 | ✅ 완료 |
|
||||
| 17:20 | TUN 디바이스 확인 | ✅ 정상 |
|
||||
| 17:20 | Tailscale 서비스 시작 | ✅ 정상 |
|
||||
| 17:21 | Tailscale 상태 확인 | ✅ 정상 |
|
||||
|
||||
---
|
||||
|
||||
## 결론
|
||||
|
||||
LXC 컨테이너에서 Tailscale을 실행하기 위해 필요한 TUN 디바이스 설정을 성공적으로 완료했습니다.
|
||||
|
||||
**주요 성과**:
|
||||
1. ✅ TUN 디바이스 접근 권한 설정
|
||||
2. ✅ Tailscale 데몬 정상 실행
|
||||
3. ✅ 모든 기존 서비스 정상 작동
|
||||
4. ✅ 백업 및 복원 방법 확보
|
||||
|
||||
**다음 단계**: Tailscale 계정 인증 후 네트워크 연결
|
||||
|
||||
---
|
||||
|
||||
**작성자**: Claude Code
|
||||
**문서 버전**: 1.0
|
||||
**최종 수정**: 2025-11-02 17:21 KST
|
||||
341
UNIFIED_INSTALL_DESIGN.md
Normal file
341
UNIFIED_INSTALL_DESIGN.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# PharmQ PVE 원클릭 통합 설치 스크립트 설계
|
||||
|
||||
## 목표
|
||||
|
||||
**PVE host 1대에서 스크립트 1번 실행으로 모든 것을 완료:**
|
||||
1. PVE 구독 리포 수정 (no-subscription)
|
||||
2. PVE host에 Tailscale 설치 + Headscale 등록
|
||||
3. PBS 등록 + Windows VM(팜IT3000) 복원
|
||||
4. Ubuntu CT 자동 생성 (API 서버용)
|
||||
5. CT 내부에 Tailscale 설치 + Headscale 등록 + API 환경 구축
|
||||
6. farmq.db에 약국 + 장비 2개 등록
|
||||
7. gateway.db에 관리자 계정 생성
|
||||
8. 로그인 정보 출력
|
||||
|
||||
## 실행 방법
|
||||
|
||||
```bash
|
||||
# PVE host의 shell에서 실행
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pharmq-setup.sh | bash
|
||||
```
|
||||
|
||||
## Phase 구조 (총 8단계)
|
||||
|
||||
### Phase 1: PVE Repository Fix (자동, ~30초)
|
||||
- proxmox-archive-keyring 확인
|
||||
- no-subscription 리포 설정 (deb822)
|
||||
- enterprise 리포 비활성화
|
||||
- `apt-get update`
|
||||
|
||||
### Phase 2: PVE Host Tailscale 등록 (자동, ~20초)
|
||||
- Tailscale 패키지 설치 (이미 있으면 스킵)
|
||||
- tailscaled 서비스 시작
|
||||
- `tailscale up --login-server=http://head.pharmq.kr --authkey=... --hostname=PXXXX-pve-pharmq`
|
||||
- PVE host VPN IP 획득 → `$PVE_VPN_IP`
|
||||
- **주의**: hostname은 Phase 3에서 약국 코드 확정 후 `tailscale set --hostname=...`으로 변경
|
||||
|
||||
### Phase 3: 약국 정보 수집 (대화형, 사용자 입력)
|
||||
- 약국명 (필수)
|
||||
- 요양기관부호 (선택)
|
||||
- 주소, 약국장, 연락처 (선택)
|
||||
- MSSQL 서버 IP (기본: `192.168.0.201\PM2014` — Windows VM 복원 후 변경될 수 있음)
|
||||
|
||||
**참고**: `curl | bash` 파이프 실행 시 `read`는 `/dev/tty`에서 읽음
|
||||
|
||||
### Phase 4: PBS 등록 + Windows VM 복원 (반자동, ~5분)
|
||||
|
||||
PBS에서 팜IT3000 Windows VM을 복원하는 단계.
|
||||
|
||||
#### 4-1. PBS 스토리지 등록 (자동)
|
||||
```bash
|
||||
pvesm add pbs "PBS-Auto" \
|
||||
--server "100.64.0.10" \
|
||||
--port "8007" \
|
||||
--datastore "PBS-DVA" \
|
||||
--username "0bin@pbs" \
|
||||
--password "@Trajet6640" \
|
||||
--fingerprint "24:42:c6:..." \
|
||||
--namespace "PQ"
|
||||
```
|
||||
- 이미 등록돼있으면 스킵
|
||||
|
||||
#### 4-2. PBS API 인증 + 백업 목록 조회 (자동)
|
||||
```bash
|
||||
# PBS API 로그인 → ticket 획득
|
||||
# GET /api2/json/admin/datastore/PBS-DVA/groups?ns=PQ
|
||||
# VM 백업 목록 표시
|
||||
```
|
||||
|
||||
#### 4-3. 복원할 VM 선택 (사용자 입력)
|
||||
```
|
||||
=== VM 백업 ===
|
||||
1. VM 100 - 3개 백업 (최근: 2026-04-05 14:00)
|
||||
└─ 팜IT3000 Windows Server 템플릿
|
||||
2. VM 200 - 2개 백업 (최근: 2026-04-03 10:00)
|
||||
└─ 기본 Windows 10
|
||||
|
||||
복원할 VM 번호: 1
|
||||
복원 VMID [기본: 200]: 200
|
||||
저장 스토리지 [기본: local-lvm]: local-lvm
|
||||
```
|
||||
|
||||
#### 4-4. VM 복원 실행 (자동)
|
||||
```bash
|
||||
# 최신 스냅샷 자동 선택
|
||||
qmrestore "PBS-Auto:backup/vm/$TEMPLATE_VMID/$LATEST_SNAPSHOT" $TARGET_VMID \
|
||||
--storage local-lvm --unique 1
|
||||
```
|
||||
|
||||
#### 4-5. Windows VM 시작 (자동)
|
||||
```bash
|
||||
qm start $TARGET_VMID
|
||||
sleep 30 # Windows 부팅 대기
|
||||
```
|
||||
- VM의 LAN IP → `$MSSQL_SERVER` 로 사용 (기본 `192.168.0.201\PM2014`)
|
||||
|
||||
### Phase 5: Ubuntu CT 생성 (자동, ~2분)
|
||||
|
||||
#### 5-1. 템플릿 준비
|
||||
```bash
|
||||
pveam update
|
||||
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst
|
||||
# 이미 있으면 스킵
|
||||
```
|
||||
|
||||
#### 5-2. VMID 자동 선택
|
||||
- 200~299 범위에서 사용 가능한 VMID 자동 탐색 (VM과 겹치지 않게)
|
||||
- `pct list` + `qm list`로 기존 VM/CT 확인
|
||||
|
||||
#### 5-3. LAN IP 자동 선택
|
||||
- 192.168.0.100~199 범위에서 사용 가능한 IP 탐색
|
||||
- `ping -c1 -W1`로 충돌 확인
|
||||
|
||||
#### 5-4. CT 생성
|
||||
```bash
|
||||
pct create $CT_VMID local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
|
||||
--hostname ubuntu-api \
|
||||
--cores 4 --memory 8192 \
|
||||
--rootfs local-lvm:30 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=192.168.0.$X/24,gw=192.168.0.1 \
|
||||
--nameserver 8.8.8.8 \
|
||||
--password 'trajet6640' \
|
||||
--unprivileged 1 \
|
||||
--features nesting=1
|
||||
```
|
||||
|
||||
#### 5-5. TUN 디바이스 설정 (Tailscale 필수)
|
||||
```bash
|
||||
echo 'lxc.cgroup2.devices.allow: c 10:200 rwm' >> /etc/pve/lxc/$CT_VMID.conf
|
||||
echo 'lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file' >> /etc/pve/lxc/$CT_VMID.conf
|
||||
```
|
||||
|
||||
#### 5-6. CT 시작
|
||||
```bash
|
||||
pct start $CT_VMID
|
||||
sleep 5 # 부팅 대기
|
||||
```
|
||||
|
||||
### Phase 6: CT 내부 환경 구축 (자동, ~3분)
|
||||
|
||||
모든 명령은 `pct exec $CT_VMID -- bash -c '...'`로 실행
|
||||
|
||||
#### 6-1. 기본 패키지
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y curl git python3-pip python3-dev python3-venv \
|
||||
pkg-config unixodbc unixodbc-dev build-essential
|
||||
```
|
||||
|
||||
#### 6-2. Microsoft ODBC Driver 18 (FreeTDS 아님!)
|
||||
```bash
|
||||
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" > /etc/apt/sources.list.d/mssql-release.list
|
||||
apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql18
|
||||
# tdsodbc 절대 설치하지 않음 — named instance 접속 불가
|
||||
```
|
||||
|
||||
#### 6-3. Tailscale 설치 + Headscale 등록
|
||||
```bash
|
||||
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 && apt-get install -y tailscale
|
||||
systemctl enable --now tailscaled
|
||||
sleep 3
|
||||
tailscale up --login-server=http://head.pharmq.kr \
|
||||
--authkey=$PREAUTH_KEY \
|
||||
--hostname=$PHARMACY_CODE-ubuntu-api \
|
||||
--accept-routes --accept-dns=false
|
||||
```
|
||||
- CT VPN IP 획득 → `$CT_VPN_IP`
|
||||
|
||||
#### 6-4. API 서버 코드 클론 + 설치
|
||||
```bash
|
||||
git clone https://git.0bin.in/thug0bin/person-lookup-web.git /srv/person-lookup-web-local
|
||||
cd /srv/person-lookup-web-local
|
||||
pip3 install -r requirements.txt --break-system-packages
|
||||
# 추가 누락 패키지 (P0019 freeze 기준)
|
||||
pip3 install brother_ql pytz qrcode netifaces pydantic anthropic httpx PyYAML rich --break-system-packages
|
||||
```
|
||||
|
||||
#### 6-5. .env 파일 생성
|
||||
```bash
|
||||
cat > /srv/person-lookup-web-local/.env << EOF
|
||||
MSSQL_SERVER=$MSSQL_SERVER
|
||||
MSSQL_USER=sa
|
||||
MSSQL_PASSWORD=tmddls214!%(
|
||||
PHARMACY_CODE=$PHARMACY_CODE
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 6-6. systemd 서비스 등록
|
||||
```bash
|
||||
cat > /etc/systemd/system/pharmq-api.service << EOF
|
||||
[Unit]
|
||||
Description=PharmQ API Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/srv/person-lookup-web-local
|
||||
ExecStart=/usr/bin/python3 app.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=FLASK_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now pharmq-api
|
||||
```
|
||||
|
||||
### Phase 7: 약국 + 장비 등록 (자동, ~5초)
|
||||
|
||||
#### 7-1. farmq.db — 약국 생성 (CT를 primary 장비로)
|
||||
```bash
|
||||
curl -s -X POST https://demo.pharmq.kr/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
"vpn_ip": "$CT_VPN_IP",
|
||||
"device_type": "linux_server",
|
||||
"device_name": "$PHARMACY_CODE-ubuntu-api",
|
||||
"hostname": "ubuntu-api",
|
||||
"hira_code": "$HIRA_CODE",
|
||||
"address": "$ADDRESS",
|
||||
"owner_name": "$OWNER_NAME",
|
||||
"phone": "$PHONE",
|
||||
"api_port": 8082
|
||||
}'
|
||||
# → pharmacy_code 응답 (P0022 등)
|
||||
```
|
||||
|
||||
#### 7-2. farmq.db — PVE host 장비 추가 등록 ✅ NEW API
|
||||
```bash
|
||||
curl -s -X POST https://demo.pharmq.kr/api/pharmacy/$PHARMACY_CODE/device \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tailscale_ip": "$PVE_VPN_IP",
|
||||
"device_name": "$PHARMACY_CODE-pve-pharmq",
|
||||
"device_type": "proxmox_host",
|
||||
"device_role": "main_server",
|
||||
"hostname": "$PVE_HOSTNAME",
|
||||
"is_primary": false
|
||||
}'
|
||||
```
|
||||
|
||||
#### 7-3. gateway.db — 관리자 계정 생성
|
||||
```bash
|
||||
curl -s -X POST https://gateway.pharmq.kr/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "$PHARMACY_CODE_LOWER",
|
||||
"email": "$PHARMACY_CODE_LOWER@pharmq.internal",
|
||||
"password": "12341234",
|
||||
"name": "$PHARMACY_NAME 관리자",
|
||||
"phone": "$PHONE",
|
||||
"primary_pharmacy_code": "$PHARMACY_CODE",
|
||||
"role": "admin"
|
||||
}'
|
||||
```
|
||||
|
||||
### Phase 8: 검증 + 결과 출력 (자동, ~10초)
|
||||
|
||||
1. CT API 서버 헬스체크: `curl http://$CT_VPN_IP:8082/api/status`
|
||||
2. Windows VM 상태 확인: `qm status $VM_VMID`
|
||||
3. PVE → CT 네트워크 확인: `ping -c3 $CT_VPN_IP`
|
||||
4. 최종 결과 출력:
|
||||
|
||||
```
|
||||
============================================
|
||||
PharmQ 원클릭 설치 완료!
|
||||
============================================
|
||||
|
||||
약국 코드: P0022
|
||||
약국명: OO약국
|
||||
|
||||
[Windows VM - 팜IT3000]
|
||||
VMID: 200
|
||||
MSSQL: 192.168.0.201\PM2014
|
||||
|
||||
[PVE Host]
|
||||
VPN IP: 100.64.0.XX
|
||||
hostname: P0022-pve-pharmq
|
||||
|
||||
[Ubuntu API CT]
|
||||
VPN IP: 100.64.0.YY
|
||||
hostname: P0022-ubuntu-api
|
||||
CT VMID: 204
|
||||
LAN IP: 192.168.0.103
|
||||
API: http://100.64.0.YY:8082
|
||||
|
||||
로그인 정보:
|
||||
URL: https://pharmq.kr
|
||||
ID: p0022
|
||||
PW: 12341234
|
||||
|
||||
⚠ 최초 로그인 후 비밀번호를 변경하세요!
|
||||
============================================
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **ODBC Driver**: FreeTDS(tdsodbc) 절대 설치 금지. MSSQL named instance 접속 불가
|
||||
2. **Preauth Key**: 현재 키 `b4692...` 만료일 2026-09-22 (충분)
|
||||
3. **CT VMID**: 200~299 자동 탐색, VM과 겹치지 않게
|
||||
4. **DNS**: CT에 `--nameserver 8.8.8.8` 필수 (Headscale DNS만 쓰면 apt 불가)
|
||||
5. **TUN 디바이스**: CT 생성 후 start 전에 설정 필수
|
||||
6. **중복 실행**: 같은 PVE에서 다시 돌리면 새 약국 코드 생성됨 (기존 것 덮어쓰지 않음)
|
||||
7. **PBS 접근**: VPN 연결 후에만 PBS(100.64.0.10) 접근 가능 → Phase 2 이후 실행
|
||||
|
||||
## API 엔드포인트 (완료)
|
||||
|
||||
| 엔드포인트 | 용도 | 상태 |
|
||||
|-----------|------|------|
|
||||
| `POST /api/pharmacy` | 약국 생성 + 1번째 장비 등록 | 기존 |
|
||||
| `POST /api/pharmacy/<code>/device` | 추가 장비 등록 | ✅ 신규 추가 |
|
||||
| `POST /api/auth/register` | 관리자 계정 생성 | 기존 (gateway) |
|
||||
|
||||
## PBS 설정 (하드코딩)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| PBS 서버 | 100.64.0.10:8007 |
|
||||
| 사용자 | 0bin@pbs |
|
||||
| 비밀번호 | @Trajet6640 |
|
||||
| 데이터스토어 | PBS-DVA |
|
||||
| 네임스페이스 | PQ |
|
||||
| Fingerprint | 24:42:c6:0f:a8:... |
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
pve9-repo-fix/
|
||||
├── pharmq-setup.sh ← 통합 스크립트 (메인)
|
||||
├── fix-pve9-repos.sh ← 기존 Phase 1 (참고용, 통합에 흡수)
|
||||
├── headscale-auto-register.sh ← 기존 (참고용, 통합에 흡수)
|
||||
├── pbs_allinone.sh ← 기존 PBS (참고용, Phase 4에 흡수)
|
||||
├── UNIFIED_INSTALL_DESIGN.md ← 이 문서
|
||||
└── README.md
|
||||
```
|
||||
436
VNC/README.md
Normal file
436
VNC/README.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# PharmQ noVNC 자동 설치 스크립트
|
||||
|
||||
각 약국 Proxmox Host에 noVNC over WebSocket을 자동으로 설치하는 대화형 스크립트입니다.
|
||||
|
||||
## 📦 개요
|
||||
|
||||
이 스크립트는 Proxmox VE 환경에서 실행 중인 VM의 VNC 화면을 웹 브라우저로 제공하는 시스템을 자동으로 설치합니다.
|
||||
|
||||
**주요 기능:**
|
||||
- ✅ 대화형 설치 (VM 자동 감지 및 선택)
|
||||
- ✅ Python 가상환경 자동 구성
|
||||
- ✅ systemd 서비스 자동 등록
|
||||
- ✅ 방화벽 설정 (Tailscale VPN 전용)
|
||||
- ✅ 헬스체크 및 상태 확인
|
||||
- ✅ 재설치 및 업데이트 지원
|
||||
|
||||
## 🚀 빠른 설치
|
||||
|
||||
### 원클릭 설치 (curl)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh | bash
|
||||
```
|
||||
|
||||
### 수동 다운로드 후 설치
|
||||
|
||||
```bash
|
||||
wget https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh
|
||||
chmod +x pharmq-novnc-setup.sh
|
||||
./pharmq-novnc-setup.sh
|
||||
```
|
||||
|
||||
## 📋 사전 요구사항
|
||||
|
||||
### 필수 조건
|
||||
- ✅ Proxmox VE 환경
|
||||
- ✅ Root 권한
|
||||
- ✅ 실행 중인 VM (VNC 활성화)
|
||||
- ✅ 인터넷 연결
|
||||
|
||||
### 자동 설치되는 패키지
|
||||
- `python3`, `python3-venv`, `python3-pip`
|
||||
- `git`, `curl`
|
||||
- `websockify`
|
||||
- Flask 및 관련 Python 패키지
|
||||
|
||||
## 🎯 설치 과정
|
||||
|
||||
### 1단계: 스크립트 실행
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh | bash
|
||||
```
|
||||
|
||||
### 2단계: VM 선택
|
||||
|
||||
스크립트가 자동으로 실행 중인 VM 목록을 표시합니다:
|
||||
|
||||
```
|
||||
실행 중인 VM 목록:
|
||||
|
||||
1) VM 201 - PharmQ Server (VM 201) (VNC: 5988)
|
||||
2) VM 202 - PharmQ Client (VM 202) (VNC: 5989)
|
||||
|
||||
VM1 선택 (번호 입력): 1
|
||||
VM2를 추가하시겠습니까? [y/N]: y
|
||||
VM2 선택 (번호 입력): 2
|
||||
```
|
||||
|
||||
### 3단계: 약국 정보 입력
|
||||
|
||||
```
|
||||
약국 코드 (예: P0014): P0014
|
||||
약국 이름 (예: 늘기쁨약국): 늘기쁨약국
|
||||
Proxmox 호스트 IP [192.168.0.200]: (Enter)
|
||||
Proxmox 사용자명 [root@pam]: (Enter)
|
||||
Proxmox 비밀번호: ********
|
||||
```
|
||||
|
||||
### 4단계: 자동 설치
|
||||
|
||||
- ✅ 패키지 설치
|
||||
- ✅ pharmq-novnc 다운로드
|
||||
- ✅ Python 가상환경 구성
|
||||
- ✅ 설정 파일 생성
|
||||
- ✅ systemd 서비스 등록
|
||||
- ✅ 서비스 시작
|
||||
|
||||
### 5단계: 완료
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
설치가 완료되었습니다!
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
설정 요약:
|
||||
약국 코드: P0014
|
||||
약국 이름: 늘기쁨약국
|
||||
설치 경로: /srv/pharmq-novnc
|
||||
|
||||
VM 설정:
|
||||
VM1: 201 - PharmQ Server (VM 201)
|
||||
- VNC 포트: 5988
|
||||
- WebSocket 포트: 6085
|
||||
VM2: 202 - PharmQ Client (VM 202)
|
||||
- VNC 포트: 5989
|
||||
- WebSocket 포트: 6086
|
||||
|
||||
접속 URL:
|
||||
로컬 헬스체크: http://localhost:6000/health
|
||||
VM1 noVNC: http://localhost:6000/
|
||||
VM2 noVNC: http://localhost:6000/vnc2
|
||||
|
||||
Gateway를 통한 접속 (외부):
|
||||
https://gateway.pharmq.kr/api/novnc/P0014/vnc1
|
||||
https://gateway.pharmq.kr/api/novnc/P0014/vnc2
|
||||
```
|
||||
|
||||
## 🔧 설치 후 확인
|
||||
|
||||
### 서비스 상태 확인
|
||||
|
||||
```bash
|
||||
systemctl status pharmq-vnc-app.service
|
||||
systemctl status pharmq-websockify-vnc1.service
|
||||
systemctl status pharmq-websockify-vnc2.service
|
||||
```
|
||||
|
||||
### 포트 리스닝 확인
|
||||
|
||||
```bash
|
||||
ss -tlnp | grep -E "6000|6085|6086"
|
||||
```
|
||||
|
||||
### 헬스체크
|
||||
|
||||
```bash
|
||||
curl http://localhost:6000/health
|
||||
```
|
||||
|
||||
**정상 응답 예시:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"pharmacy_id": "P0014",
|
||||
"pharmacy_name": "늘기쁨약국",
|
||||
"vms": [
|
||||
{
|
||||
"id": 201,
|
||||
"name": "PharmQ Server (VM 201)",
|
||||
"vnc_connected": true
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"name": "PharmQ Client (VM 202)",
|
||||
"vnc_connected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🌐 접속 방법
|
||||
|
||||
### 1. 로컬 접속 (Proxmox Host 내부)
|
||||
|
||||
```
|
||||
http://localhost:6000/ # VM1
|
||||
http://localhost:6000/vnc2 # VM2
|
||||
http://localhost:6000/health # 헬스체크
|
||||
```
|
||||
|
||||
### 2. Tailscale VPN을 통한 직접 접속
|
||||
|
||||
```
|
||||
http://100.64.0.24:6000/ # VM1 (약국 VPN IP 사용)
|
||||
http://100.64.0.24:6000/vnc2 # VM2
|
||||
```
|
||||
|
||||
### 3. Gateway를 통한 접속 (권장)
|
||||
|
||||
```
|
||||
https://gateway.pharmq.kr/api/novnc/P0014/vnc1 # VM1
|
||||
https://gateway.pharmq.kr/api/novnc/P0014/vnc2 # VM2
|
||||
```
|
||||
|
||||
## 🔒 보안 설정
|
||||
|
||||
스크립트 실행 중 방화벽 설정 옵션:
|
||||
|
||||
```
|
||||
방화벽 설정을 진행하시겠습니까? [y/N]: y
|
||||
```
|
||||
|
||||
이 옵션을 선택하면:
|
||||
- ✅ Tailscale VPN(`tailscale0`)에서만 접근 허용
|
||||
- ✅ 외부 인터넷에서 직접 접근 차단
|
||||
- ✅ SSH 접속은 영향 없음
|
||||
|
||||
### 수동 방화벽 설정
|
||||
|
||||
```bash
|
||||
# Tailscale VPN 인터페이스에서만 허용
|
||||
ufw allow in on tailscale0 to any port 6000,6085,6086 proto tcp
|
||||
|
||||
# 외부 접근 차단
|
||||
ufw deny 6000/tcp
|
||||
ufw deny 6085/tcp
|
||||
ufw deny 6086/tcp
|
||||
|
||||
# 방화벽 활성화
|
||||
ufw enable
|
||||
```
|
||||
|
||||
## 🔄 재설치 및 업데이트
|
||||
|
||||
### 재설치
|
||||
|
||||
스크립트를 다시 실행하면 기존 설치를 감지하고 옵션을 제공합니다:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh | bash
|
||||
```
|
||||
|
||||
```
|
||||
✅ PharmQ noVNC가 이미 설치되어 있습니다!
|
||||
|
||||
현재 설정:
|
||||
약국 코드: P0014
|
||||
약국 이름: 늘기쁨약국
|
||||
|
||||
다음 중 선택하세요:
|
||||
1) 상태 확인
|
||||
2) 재설치 (기존 설정 백업 후 새로 설치)
|
||||
3) 종료
|
||||
|
||||
선택 [1/2/3]:
|
||||
```
|
||||
|
||||
### 서비스 재시작
|
||||
|
||||
```bash
|
||||
# 전체 서비스 재시작
|
||||
systemctl restart pharmq-websockify-vnc1.service
|
||||
systemctl restart pharmq-websockify-vnc2.service
|
||||
systemctl restart pharmq-vnc-app.service
|
||||
```
|
||||
|
||||
### 설정 파일 수동 편집
|
||||
|
||||
```bash
|
||||
nano /srv/pharmq-novnc/config.json
|
||||
systemctl restart pharmq-vnc-app.service
|
||||
```
|
||||
|
||||
## 📊 관리 명령어
|
||||
|
||||
### 상태 확인
|
||||
|
||||
```bash
|
||||
/srv/pharmq-novnc/scripts/check-status.sh
|
||||
```
|
||||
|
||||
### 로그 확인
|
||||
|
||||
```bash
|
||||
# 실시간 로그
|
||||
journalctl -u pharmq-vnc-app.service -f
|
||||
journalctl -u pharmq-websockify-vnc1.service -f
|
||||
|
||||
# 최근 로그
|
||||
journalctl -u pharmq-vnc-app.service -n 100
|
||||
```
|
||||
|
||||
### 서비스 중지
|
||||
|
||||
```bash
|
||||
systemctl stop pharmq-vnc-app.service
|
||||
systemctl stop pharmq-websockify-vnc1.service
|
||||
systemctl stop pharmq-websockify-vnc2.service
|
||||
```
|
||||
|
||||
### 서비스 비활성화
|
||||
|
||||
```bash
|
||||
systemctl disable pharmq-vnc-app.service
|
||||
systemctl disable pharmq-websockify-vnc1.service
|
||||
systemctl disable pharmq-websockify-vnc2.service
|
||||
```
|
||||
|
||||
## 🐛 트러블슈팅
|
||||
|
||||
### 문제: 서비스가 시작되지 않음
|
||||
|
||||
```bash
|
||||
# 로그 확인
|
||||
journalctl -xeu pharmq-vnc-app.service
|
||||
|
||||
# 수동 테스트
|
||||
source /srv/pharmq-novnc/venv/bin/activate
|
||||
cd /srv/pharmq-novnc
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### 문제: VNC 화면이 검은색
|
||||
|
||||
```bash
|
||||
# VM 상태 확인
|
||||
qm status 201
|
||||
qm status 202
|
||||
|
||||
# VNC 포트 확인
|
||||
nc -zv localhost 5988
|
||||
nc -zv localhost 5989
|
||||
|
||||
# Websockify 재시작
|
||||
systemctl restart pharmq-websockify-vnc1.service
|
||||
systemctl restart pharmq-websockify-vnc2.service
|
||||
```
|
||||
|
||||
### 문제: Gateway에서 502 에러
|
||||
|
||||
```bash
|
||||
# 약국 서버 헬스체크
|
||||
curl http://localhost:6000/health
|
||||
|
||||
# Tailscale 연결 확인
|
||||
tailscale status
|
||||
|
||||
# Flask App 재시작
|
||||
systemctl restart pharmq-vnc-app.service
|
||||
```
|
||||
|
||||
### 문제: 포트 충돌
|
||||
|
||||
```bash
|
||||
# 포트 사용 중인 프로세스 확인
|
||||
ss -tlnp | grep 6000
|
||||
ss -tlnp | grep 6085
|
||||
ss -tlnp | grep 6086
|
||||
|
||||
# 프로세스 종료
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
## 📁 디렉토리 구조
|
||||
|
||||
```
|
||||
/srv/pharmq-novnc/
|
||||
├── app.py # Flask 애플리케이션
|
||||
├── config.json # 설정 파일
|
||||
├── requirements.txt # Python 패키지 목록
|
||||
├── venv/ # Python 가상환경
|
||||
├── static/ # 정적 파일 (CSS, JS)
|
||||
├── templates/ # HTML 템플릿
|
||||
├── systemd/ # systemd 서비스 파일
|
||||
│ ├── pharmq-vnc-app.service
|
||||
│ ├── pharmq-websockify-vnc1.service
|
||||
│ └── pharmq-websockify-vnc2.service
|
||||
└── scripts/
|
||||
├── check-status.sh # 상태 확인 스크립트
|
||||
└── restart-all.sh # 전체 재시작 스크립트
|
||||
```
|
||||
|
||||
## 🔗 관련 문서
|
||||
|
||||
- **pharmq-novnc 리포지토리**: https://git.0bin.in/thug0bin/pharmq-novnc
|
||||
- **Gateway 통합 문서**: /srv/docs/VNCoverVPN.md
|
||||
- **Frontend 통합 가이드**: pharmq_on CloudBillingService 컴포넌트
|
||||
|
||||
## 📞 지원
|
||||
|
||||
설치 중 문제 발생 시:
|
||||
|
||||
1. 스크립트 로그 저장:
|
||||
```bash
|
||||
script /tmp/install-log.txt
|
||||
curl -fsSL https://git.0bin.in/.../pharmq-novnc-setup.sh | bash
|
||||
exit
|
||||
```
|
||||
|
||||
2. 서비스 로그 수집:
|
||||
```bash
|
||||
journalctl -u pharmq-vnc-app.service -n 100 > /tmp/vnc-app.log
|
||||
journalctl -u pharmq-websockify-vnc1.service -n 100 > /tmp/vnc1.log
|
||||
```
|
||||
|
||||
3. PharmQ 개발팀에 문의
|
||||
|
||||
## 🎯 setupScripts.ts 통합
|
||||
|
||||
프론트엔드 [setupScripts.ts](pharmq_on/src/constants/setupScripts.ts)에 추가:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'pharmq-novnc-setup',
|
||||
title: '5단계: PharmQ noVNC 설치',
|
||||
emoji: '🖥️',
|
||||
description: 'VM VNC 화면을 웹 브라우저로 제공 (대화형 설치)',
|
||||
command: 'curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh | bash',
|
||||
category: 'core',
|
||||
step: 5,
|
||||
details: {
|
||||
prerequisite: '⚠️ 1단계(Repository Fix) + 2단계(VPN 등록) 완료 필수',
|
||||
problemDescription: [
|
||||
'VM VNC 화면에 직접 접근하기 어려움',
|
||||
'중앙 관리자가 약국 PC 화면을 원격으로 확인 필요',
|
||||
'VNC 프로토콜을 웹 브라우저에서 사용 불가'
|
||||
],
|
||||
features: [
|
||||
'VM 자동 감지 및 선택',
|
||||
'noVNC over WebSocket 설치',
|
||||
'Proxmox API 통합 (마우스 리셋)',
|
||||
'systemd 서비스 자동 등록',
|
||||
'방화벽 설정 (Tailscale VPN 전용)',
|
||||
'Gateway 연동 (https://gateway.pharmq.kr)'
|
||||
],
|
||||
verification: [
|
||||
'설치 완료 후 표시되는 URL 확인',
|
||||
'curl http://localhost:6000/health',
|
||||
'https://gateway.pharmq.kr/api/novnc/P0014/vnc1 접속'
|
||||
],
|
||||
warnings: [
|
||||
'1단계 + 2단계 완료 후 실행',
|
||||
'Proxmox VE 환경 필수',
|
||||
'Root 권한 필요',
|
||||
'실행 중인 VM이 있어야 함'
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**버전**: 1.0
|
||||
**작성일**: 2025-11-21
|
||||
**작성자**: PharmQ Development Team
|
||||
802
VNC/pharmq-novnc-setup.sh
Executable file
802
VNC/pharmq-novnc-setup.sh
Executable file
@@ -0,0 +1,802 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PharmQ noVNC Setup Script
|
||||
# Ubuntu VM에서 실행하여 Proxmox Host의 VM VNC를 noVNC로 제공
|
||||
# 사용법: curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/VNC/pharmq-novnc-setup.sh | bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# 명령행 인자 처리 (통합 스크립트에서 호출 시 사용)
|
||||
ARG_PVE_HOST=""
|
||||
ARG_PVE_USER=""
|
||||
ARG_PVE_PASSWORD=""
|
||||
ARG_PHARMACY_CODE=""
|
||||
ARG_PHARMACY_NAME=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--pve-host) ARG_PVE_HOST="$2"; shift 2 ;;
|
||||
--pve-user) ARG_PVE_USER="$2"; shift 2 ;;
|
||||
--pve-password) ARG_PVE_PASSWORD="$2"; shift 2 ;;
|
||||
--pharmacy-code) ARG_PHARMACY_CODE="$2"; shift 2 ;;
|
||||
--pharmacy-name) ARG_PHARMACY_NAME="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 색상 코드 정의
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
msg_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
msg_ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
msg_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
msg_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
# 헤더 출력
|
||||
print_header() {
|
||||
clear
|
||||
echo -e "${PURPLE}"
|
||||
echo "═══════════════════════════════════════════════════════════════════"
|
||||
echo " PharmQ noVNC Setup Script v1.0"
|
||||
echo "═══════════════════════════════════════════════════════════════════"
|
||||
echo -e "${NC}"
|
||||
echo "이 스크립트는 Ubuntu VM에서 Proxmox Host의 VM VNC를 웹으로 제공합니다."
|
||||
echo "각 약국의 VM VNC 화면을 웹 브라우저로 제공합니다."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 환경 감지 (Proxmox Host vs Ubuntu VM)
|
||||
detect_environment() {
|
||||
msg_info "실행 환경 감지 중..."
|
||||
|
||||
if command -v pveversion > /dev/null 2>&1; then
|
||||
INSTALL_ENV="proxmox"
|
||||
msg_warn "Proxmox VE Host에서 실행 중입니다."
|
||||
msg_warn "이 스크립트는 Proxmox 내부의 Ubuntu VM에서 실행되어야 합니다."
|
||||
read -p "계속 진행하시겠습니까? [y/N]: " continue_anyway </dev/tty
|
||||
case $continue_anyway in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) msg_error "설치가 취소되었습니다." ;;
|
||||
esac
|
||||
else
|
||||
INSTALL_ENV="ubuntu"
|
||||
msg_ok "Ubuntu 환경에서 실행 중입니다."
|
||||
fi
|
||||
}
|
||||
|
||||
# 루트 권한 확인
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
msg_error "이 스크립트는 root 권한으로 실행해야 합니다. sudo를 사용하세요."
|
||||
fi
|
||||
}
|
||||
|
||||
# Proxmox API를 통해 VM 목록 가져오기
|
||||
get_vm_list_from_api() {
|
||||
local pve_host=$1
|
||||
local pve_user=$2
|
||||
local pve_password=$3
|
||||
|
||||
msg_info "Proxmox API를 통해 VM 목록 가져오는 중..."
|
||||
|
||||
# API 로그인
|
||||
local ticket_response=$(curl -k -s -X POST \
|
||||
"https://${pve_host}:8006/api2/json/access/ticket" \
|
||||
-d "username=${pve_user}&password=${pve_password}")
|
||||
|
||||
if [ -z "$ticket_response" ]; then
|
||||
msg_error "Proxmox API 로그인 실패: 응답 없음"
|
||||
fi
|
||||
|
||||
local ticket=$(echo "$ticket_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['ticket'])" 2>/dev/null || echo "")
|
||||
local csrf_token=$(echo "$ticket_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['CSRFPreventionToken'])" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$ticket" ]; then
|
||||
msg_error "Proxmox API 로그인 실패: 티켓을 가져올 수 없습니다."
|
||||
fi
|
||||
|
||||
# 노드 목록 가져오기
|
||||
local nodes_response=$(curl -k -s -X GET \
|
||||
"https://${pve_host}:8006/api2/json/nodes" \
|
||||
--cookie "PVEAuthCookie=${ticket}")
|
||||
|
||||
local node_name=$(echo "$nodes_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data'][0]['node'])" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$node_name" ]; then
|
||||
msg_error "Proxmox 노드를 찾을 수 없습니다."
|
||||
fi
|
||||
|
||||
msg_ok "Proxmox 노드: $node_name"
|
||||
|
||||
# VM 목록 가져오기
|
||||
local vms_response=$(curl -k -s -X GET \
|
||||
"https://${pve_host}:8006/api2/json/nodes/${node_name}/qemu" \
|
||||
--cookie "PVEAuthCookie=${ticket}")
|
||||
|
||||
# VM 목록 파싱 (running 상태만, vmid 순서로 정렬)
|
||||
VM_LIST=$(echo "$vms_response" | python3 -c "
|
||||
import sys, json
|
||||
vms = json.load(sys.stdin)['data']
|
||||
running_vms = [vm for vm in vms if vm.get('status') == 'running']
|
||||
# vmid 순서로 정렬
|
||||
running_vms.sort(key=lambda x: x['vmid'])
|
||||
for vm in running_vms:
|
||||
print(f\"{vm['vmid']}:{vm.get('name', 'VM-' + str(vm['vmid']))}\")
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$VM_LIST" ]; then
|
||||
msg_warn "실행 중인 VM이 없습니다."
|
||||
return 1
|
||||
fi
|
||||
|
||||
msg_ok "VM 목록 가져오기 완료"
|
||||
return 0
|
||||
}
|
||||
|
||||
# VNC 포트 가져오기 (Proxmox API)
|
||||
get_vnc_port_from_api() {
|
||||
local pve_host=$1
|
||||
local pve_user=$2
|
||||
local pve_password=$3
|
||||
local vmid=$4
|
||||
|
||||
# API 로그인
|
||||
local ticket_response=$(curl -k -s -X POST \
|
||||
"https://${pve_host}:8006/api2/json/access/ticket" \
|
||||
-d "username=${pve_user}&password=${pve_password}")
|
||||
|
||||
local ticket=$(echo "$ticket_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['ticket'])" 2>/dev/null || echo "")
|
||||
|
||||
# 노드 가져오기
|
||||
local nodes_response=$(curl -k -s -X GET \
|
||||
"https://${pve_host}:8006/api2/json/nodes" \
|
||||
--cookie "PVEAuthCookie=${ticket}")
|
||||
|
||||
local node_name=$(echo "$nodes_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data'][0]['node'])" 2>/dev/null || echo "")
|
||||
|
||||
# VM 설정 가져오기
|
||||
local vm_config=$(curl -k -s -X GET \
|
||||
"https://${pve_host}:8006/api2/json/nodes/${node_name}/qemu/${vmid}/config" \
|
||||
--cookie "PVEAuthCookie=${ticket}")
|
||||
|
||||
# VNC 포트 추출
|
||||
local vnc_port=$(echo "$vm_config" | python3 -c "
|
||||
import sys, json
|
||||
config = json.load(sys.stdin)['data']
|
||||
# VNC display 번호로부터 포트 계산: 5900 + display
|
||||
for key, value in config.items():
|
||||
if 'vnc' in key or 'vga' in key:
|
||||
if 'vnc' in str(value):
|
||||
# vnc 설정에서 display 번호 추출 (예: 'vnc,:1')
|
||||
parts = str(value).split(',')
|
||||
for part in parts:
|
||||
if part.startswith(':'):
|
||||
display_num = part[1:]
|
||||
if display_num.isdigit():
|
||||
print(5900 + int(display_num))
|
||||
sys.exit(0)
|
||||
# VNC display를 찾지 못한 경우 기본값
|
||||
print(5900 + $vmid % 100)
|
||||
" 2>/dev/null || echo "5900")
|
||||
|
||||
echo "$vnc_port"
|
||||
}
|
||||
|
||||
# VM 선택 및 설정
|
||||
configure_vms() {
|
||||
# 인자가 있으면 사용, 없으면 대화형
|
||||
if [ -n "$ARG_PVE_HOST" ] && [ -n "$ARG_PVE_PASSWORD" ]; then
|
||||
PVE_HOST="$ARG_PVE_HOST"
|
||||
PVE_USER="${ARG_PVE_USER:-root@pam}"
|
||||
PVE_PASSWORD="$ARG_PVE_PASSWORD"
|
||||
msg_ok "PVE 접속 정보 자동 설정: $PVE_HOST ($PVE_USER)"
|
||||
else
|
||||
echo -e "${CYAN}Proxmox API 접속 정보 입력:${NC}"
|
||||
echo ""
|
||||
|
||||
read -p "Proxmox 호스트 IP [192.168.0.200]: " PVE_HOST </dev/tty
|
||||
PVE_HOST=${PVE_HOST:-192.168.0.200}
|
||||
|
||||
read -p "Proxmox 사용자명 [root@pam]: " PVE_USER </dev/tty
|
||||
PVE_USER=${PVE_USER:-root@pam}
|
||||
|
||||
echo -n "Proxmox 비밀번호: "
|
||||
read -s PVE_PASSWORD </dev/tty
|
||||
echo ""
|
||||
|
||||
if [ -z "$PVE_PASSWORD" ]; then
|
||||
msg_error "Proxmox 비밀번호는 필수입니다."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_info "Proxmox API 연결 테스트 중..."
|
||||
|
||||
# VM 목록 가져오기
|
||||
if ! get_vm_list_from_api "$PVE_HOST" "$PVE_USER" "$PVE_PASSWORD"; then
|
||||
msg_error "VM 목록을 가져올 수 없습니다."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "실행 중인 VM 목록:"
|
||||
echo ""
|
||||
|
||||
declare -A VM_INFO
|
||||
local idx=1
|
||||
|
||||
while IFS=: read -r vmid vm_name; do
|
||||
echo " $idx) VM $vmid - $vm_name"
|
||||
VM_INFO[$idx]="$vmid:$vm_name"
|
||||
((idx++))
|
||||
done <<< "$VM_LIST"
|
||||
|
||||
if [ ${#VM_INFO[@]} -eq 0 ]; then
|
||||
msg_error "실행 중인 VM이 없습니다."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "noVNC로 제공할 VM을 최대 2개까지 선택하세요."
|
||||
echo "(예: 201-SERVER를 선택하려면 '1' 입력)"
|
||||
echo ""
|
||||
|
||||
# VM1 선택
|
||||
while true; do
|
||||
read -p "VM1 선택 (목록 번호 1, 2, 3 중 선택): " vm1_choice </dev/tty
|
||||
if [[ -n "${VM_INFO[$vm1_choice]:-}" ]]; then
|
||||
IFS=':' read -r VM1_ID VM1_NAME <<< "${VM_INFO[$vm1_choice]}"
|
||||
break
|
||||
else
|
||||
msg_warn "잘못된 선택입니다. 다시 입력해주세요."
|
||||
fi
|
||||
done
|
||||
|
||||
msg_info "VM1 VNC 포트 조회 중..."
|
||||
VM1_VNC_PORT=$(get_vnc_port_from_api "$PVE_HOST" "$PVE_USER" "$PVE_PASSWORD" "$VM1_ID")
|
||||
msg_ok "VM1: $VM1_ID - $VM1_NAME (VNC: $VM1_VNC_PORT)"
|
||||
|
||||
# VM2 선택 (선택사항)
|
||||
echo ""
|
||||
read -p "VM2를 추가하시겠습니까? [y/N]: " add_vm2 </dev/tty
|
||||
|
||||
VM2_ENABLED=false
|
||||
case $add_vm2 in
|
||||
[yY]|[yY][eE][sS])
|
||||
while true; do
|
||||
read -p "VM2 선택 (목록 번호 입력, VM1과 다른 번호): " vm2_choice </dev/tty
|
||||
if [[ -n "${VM_INFO[$vm2_choice]:-}" ]]; then
|
||||
if [ "$vm2_choice" = "$vm1_choice" ]; then
|
||||
msg_warn "VM1과 다른 VM을 선택해주세요."
|
||||
continue
|
||||
fi
|
||||
IFS=':' read -r VM2_ID VM2_NAME <<< "${VM_INFO[$vm2_choice]}"
|
||||
VM2_ENABLED=true
|
||||
msg_info "VM2 VNC 포트 조회 중..."
|
||||
VM2_VNC_PORT=$(get_vnc_port_from_api "$PVE_HOST" "$PVE_USER" "$PVE_PASSWORD" "$VM2_ID")
|
||||
msg_ok "VM2: $VM2_ID - $VM2_NAME (VNC: $VM2_VNC_PORT)"
|
||||
break
|
||||
else
|
||||
msg_warn "잘못된 선택입니다. 다시 입력해주세요."
|
||||
fi
|
||||
done
|
||||
;;
|
||||
*)
|
||||
msg_info "VM2 설정을 건너뜁니다."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 약국 정보 입력
|
||||
get_pharmacy_info() {
|
||||
# 인자가 있으면 사용
|
||||
if [ -n "$ARG_PHARMACY_CODE" ] && [ -n "$ARG_PHARMACY_NAME" ]; then
|
||||
PHARMACY_CODE="$ARG_PHARMACY_CODE"
|
||||
PHARMACY_NAME="$ARG_PHARMACY_NAME"
|
||||
msg_ok "약국 정보 자동 설정: $PHARMACY_CODE ($PHARMACY_NAME)"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${CYAN}약국 정보를 입력해주세요:${NC}"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
read -p "약국 코드 (예: P0014): " PHARMACY_CODE </dev/tty
|
||||
if [ -z "$PHARMACY_CODE" ]; then
|
||||
msg_warn "약국 코드는 필수입니다."
|
||||
continue
|
||||
fi
|
||||
if [[ ! "$PHARMACY_CODE" =~ ^P[0-9]{4}$ ]]; then
|
||||
msg_warn "약국 코드는 P0001 형식이어야 합니다."
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
read -p "약국 이름 (예: 늘기쁨약국): " PHARMACY_NAME </dev/tty
|
||||
if [ -z "$PHARMACY_NAME" ]; then
|
||||
msg_warn "약국 이름은 필수입니다."
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
fi
|
||||
|
||||
# WebSocket 포트
|
||||
WS_PORT1=6085
|
||||
WS_PORT2=6086
|
||||
FLASK_PORT=6000
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}입력된 정보:${NC}"
|
||||
echo " 약국 코드: $PHARMACY_CODE"
|
||||
echo " 약국 이름: $PHARMACY_NAME"
|
||||
echo " Proxmox 호스트: $PVE_HOST"
|
||||
echo " VM1: $VM1_ID - $VM1_NAME (VNC: $VM1_VNC_PORT, WebSocket: $WS_PORT1)"
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
echo " VM2: $VM2_ID - $VM2_NAME (VNC: $VM2_VNC_PORT, WebSocket: $WS_PORT2)"
|
||||
fi
|
||||
echo " Flask 포트: $FLASK_PORT"
|
||||
echo ""
|
||||
|
||||
read -p "이 설정으로 계속하시겠습니까? [y/N]: " confirm </dev/tty
|
||||
case $confirm in
|
||||
[yY]|[yY][eE][sS])
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
msg_error "설치가 취소되었습니다."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 네트워크 연결 확인
|
||||
check_network() {
|
||||
msg_info "네트워크 연결 확인 중..."
|
||||
|
||||
if ! ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; then
|
||||
msg_error "인터넷 연결을 확인할 수 없습니다. 네트워크 설정을 확인해주세요."
|
||||
fi
|
||||
|
||||
# Proxmox 서버 연결 확인
|
||||
msg_info "Proxmox 서버 연결 확인 중... ($PVE_HOST:8006)"
|
||||
|
||||
if command -v timeout > /dev/null; then
|
||||
if ! timeout 10 bash -c "</dev/tcp/$PVE_HOST/8006" > /dev/null 2>&1; then
|
||||
msg_warn "Proxmox 서버에 연결할 수 없습니다. 설정을 확인해주세요."
|
||||
read -p "계속 진행하시겠습니까? [y/N]: " continue_anyway </dev/tty
|
||||
case $continue_anyway in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) msg_error "설치가 취소되었습니다." ;;
|
||||
esac
|
||||
else
|
||||
msg_ok "Proxmox 서버 연결 확인됨"
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_ok "네트워크 연결 확인됨"
|
||||
}
|
||||
|
||||
# 필수 패키지 설치
|
||||
install_packages() {
|
||||
msg_info "필수 패키지 설치 중..."
|
||||
|
||||
msg_info " - 패키지 목록 업데이트 중..."
|
||||
if ! apt update > /dev/null 2>&1; then
|
||||
msg_error "패키지 목록 업데이트에 실패했습니다."
|
||||
fi
|
||||
|
||||
local packages="python3 python3-venv python3-pip git curl websockify"
|
||||
|
||||
for package in $packages; do
|
||||
if dpkg -l | grep -q "^ii $package "; then
|
||||
msg_ok " $package 이미 설치됨"
|
||||
continue
|
||||
fi
|
||||
|
||||
msg_info " - $package 설치 중..."
|
||||
if apt install -y "$package" > /dev/null 2>&1; then
|
||||
msg_ok " $package 설치 완료"
|
||||
else
|
||||
msg_error "$package 설치에 실패했습니다."
|
||||
fi
|
||||
done
|
||||
|
||||
msg_ok "모든 패키지 설치 완료"
|
||||
}
|
||||
|
||||
# pharmq-novnc 다운로드 및 설치
|
||||
install_pharmq_novnc() {
|
||||
msg_info "pharmq-novnc 애플리케이션 설치 중..."
|
||||
|
||||
local INSTALL_DIR="/srv/pharmq-novnc"
|
||||
local REPO_URL="https://git.0bin.in/thug0bin/pharmq-novnc.git"
|
||||
|
||||
# 기존 설치 확인
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
msg_warn "기존 설치 디렉토리가 발견되었습니다: $INSTALL_DIR"
|
||||
read -p "기존 설치를 덮어쓰시겠습니까? [y/N]: " overwrite </dev/tty
|
||||
case $overwrite in
|
||||
[yY]|[yY][eE][sS])
|
||||
msg_info "기존 설치 백업 중..."
|
||||
mv "$INSTALL_DIR" "${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
;;
|
||||
*)
|
||||
msg_error "설치가 취소되었습니다."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Git clone
|
||||
msg_info "pharmq-novnc 리포지토리 다운로드 중..."
|
||||
if git clone "$REPO_URL" "$INSTALL_DIR" > /dev/null 2>&1; then
|
||||
msg_ok "다운로드 완료"
|
||||
else
|
||||
msg_error "리포지토리 다운로드에 실패했습니다."
|
||||
fi
|
||||
|
||||
# Python 가상환경 생성
|
||||
msg_info "Python 가상환경 생성 중..."
|
||||
cd "$INSTALL_DIR"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip > /dev/null 2>&1
|
||||
pip install -r requirements.txt > /dev/null 2>&1
|
||||
deactivate
|
||||
msg_ok "Python 가상환경 설정 완료"
|
||||
}
|
||||
|
||||
# config.json 생성
|
||||
create_config() {
|
||||
msg_info "설정 파일 생성 중..."
|
||||
|
||||
local INSTALL_DIR="/srv/pharmq-novnc"
|
||||
local CONFIG_FILE="$INSTALL_DIR/config.json"
|
||||
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
{
|
||||
"pharmacy_id": "$PHARMACY_CODE",
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
|
||||
"vnc_host": "$PVE_HOST",
|
||||
|
||||
"proxmox": {
|
||||
"host": "$PVE_HOST",
|
||||
"port": 8006,
|
||||
"username": "$PVE_USER",
|
||||
"password": "$PVE_PASSWORD",
|
||||
"verify_ssl": false
|
||||
},
|
||||
|
||||
"vms": [
|
||||
{
|
||||
"id": $VM1_ID,
|
||||
"name": "$VM1_NAME",
|
||||
"vnc_port": $VM1_VNC_PORT,
|
||||
"websockify_port": $WS_PORT1
|
||||
},
|
||||
{
|
||||
"id": $VM2_ID,
|
||||
"name": "$VM2_NAME",
|
||||
"vnc_port": $VM2_VNC_PORT,
|
||||
"websockify_port": $WS_PORT2
|
||||
}
|
||||
],
|
||||
|
||||
"flask": {
|
||||
"host": "0.0.0.0",
|
||||
"port": $FLASK_PORT
|
||||
},
|
||||
|
||||
"notes": "vnc_host는 Proxmox VNC 서버 주소입니다. Flask는 Ubuntu VM에서 실행되며 Proxmox API를 통해 원격 VM을 제어합니다."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
{
|
||||
"pharmacy_id": "$PHARMACY_CODE",
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
|
||||
"vnc_host": "$PVE_HOST",
|
||||
|
||||
"proxmox": {
|
||||
"host": "$PVE_HOST",
|
||||
"port": 8006,
|
||||
"username": "$PVE_USER",
|
||||
"password": "$PVE_PASSWORD",
|
||||
"verify_ssl": false
|
||||
},
|
||||
|
||||
"vms": [
|
||||
{
|
||||
"id": $VM1_ID,
|
||||
"name": "$VM1_NAME",
|
||||
"vnc_port": $VM1_VNC_PORT,
|
||||
"websockify_port": $WS_PORT1
|
||||
}
|
||||
],
|
||||
|
||||
"flask": {
|
||||
"host": "0.0.0.0",
|
||||
"port": $FLASK_PORT
|
||||
},
|
||||
|
||||
"notes": "vnc_host는 Proxmox VNC 서버 주소입니다. Flask는 Ubuntu VM에서 실행되며 Proxmox API를 통해 원격 VM을 제어합니다."
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
msg_ok "설정 파일 생성 완료: $CONFIG_FILE"
|
||||
}
|
||||
|
||||
# systemd 서비스 설치
|
||||
setup_systemd_services() {
|
||||
msg_info "systemd 서비스 설치 중..."
|
||||
|
||||
local INSTALL_DIR="/srv/pharmq-novnc"
|
||||
|
||||
# 서비스 파일 복사
|
||||
cp "$INSTALL_DIR/systemd/"*.service /etc/systemd/system/
|
||||
|
||||
# systemd 리로드
|
||||
systemctl daemon-reload
|
||||
|
||||
msg_ok "systemd 서비스 등록 완료"
|
||||
}
|
||||
|
||||
# 서비스 시작
|
||||
start_services() {
|
||||
msg_info "서비스 시작 중..."
|
||||
|
||||
local services=("pharmq-websockify-vnc1" "pharmq-vnc-app")
|
||||
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
services+=("pharmq-websockify-vnc2")
|
||||
fi
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
msg_info " - $service.service 시작 중..."
|
||||
systemctl enable "$service.service" > /dev/null 2>&1
|
||||
systemctl restart "$service.service"
|
||||
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active "$service.service" > /dev/null 2>&1; then
|
||||
msg_ok " $service: 실행 중"
|
||||
else
|
||||
msg_warn " $service: 시작 실패"
|
||||
msg_info " 로그 확인: journalctl -xeu $service.service"
|
||||
fi
|
||||
done
|
||||
|
||||
msg_ok "모든 서비스 시작 완료"
|
||||
}
|
||||
|
||||
# 방화벽 설정 (선택사항)
|
||||
configure_firewall() {
|
||||
echo ""
|
||||
echo -e "${CYAN}방화벽 설정 (선택사항)${NC}"
|
||||
echo ""
|
||||
echo "Tailscale VPN으로만 접근하도록 방화벽을 설정하시겠습니까?"
|
||||
echo "이 설정은 외부에서의 직접 접근을 차단하고 VPN을 통한 접근만 허용합니다."
|
||||
echo ""
|
||||
read -p "방화벽 설정을 진행하시겠습니까? [y/N]: " setup_fw </dev/tty
|
||||
|
||||
case $setup_fw in
|
||||
[yY]|[yY][eE][sS])
|
||||
msg_info "방화벽 설정 중..."
|
||||
|
||||
if ! command -v ufw > /dev/null 2>&1; then
|
||||
msg_info "ufw 설치 중..."
|
||||
apt install -y ufw > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Tailscale 인터페이스 확인
|
||||
if ip link show tailscale0 > /dev/null 2>&1; then
|
||||
ufw allow in on tailscale0 to any port $FLASK_PORT,$WS_PORT1,$WS_PORT2 proto tcp
|
||||
ufw deny $FLASK_PORT/tcp
|
||||
ufw deny $WS_PORT1/tcp
|
||||
ufw deny $WS_PORT2/tcp
|
||||
|
||||
msg_ok "방화벽 설정 완료 (Tailscale VPN만 허용)"
|
||||
else
|
||||
msg_warn "Tailscale 인터페이스를 찾을 수 없습니다. 방화벽 설정을 건너뜁니다."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
msg_info "방화벽 설정을 건너뜁니다."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 설치 테스트
|
||||
test_installation() {
|
||||
msg_info "설치 테스트 중..."
|
||||
|
||||
# 포트 리스닝 확인
|
||||
sleep 3
|
||||
|
||||
if ss -tlnp 2>/dev/null | grep -q ":$FLASK_PORT "; then
|
||||
msg_ok "Flask 앱이 포트 $FLASK_PORT에서 실행 중입니다."
|
||||
else
|
||||
msg_warn "Flask 앱이 포트 $FLASK_PORT에서 실행되지 않습니다."
|
||||
fi
|
||||
|
||||
if ss -tlnp 2>/dev/null | grep -q ":$WS_PORT1 "; then
|
||||
msg_ok "WebSocket 1이 포트 $WS_PORT1에서 실행 중입니다."
|
||||
else
|
||||
msg_warn "WebSocket 1이 포트 $WS_PORT1에서 실행되지 않습니다."
|
||||
fi
|
||||
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
if ss -tlnp 2>/dev/null | grep -q ":$WS_PORT2 "; then
|
||||
msg_ok "WebSocket 2가 포트 $WS_PORT2에서 실행 중입니다."
|
||||
else
|
||||
msg_warn "WebSocket 2가 포트 $WS_PORT2에서 실행되지 않습니다."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 헬스체크
|
||||
if curl -s -f "http://localhost:$FLASK_PORT/health" > /dev/null 2>&1; then
|
||||
msg_ok "헬스체크 성공"
|
||||
else
|
||||
msg_warn "헬스체크 실패 (서비스 시작 대기 중일 수 있습니다)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 완료 메시지
|
||||
print_completion() {
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} 설치가 완료되었습니다!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}설정 요약:${NC}"
|
||||
echo " 약국 코드: $PHARMACY_CODE"
|
||||
echo " 약국 이름: $PHARMACY_NAME"
|
||||
echo " 설치 경로: /srv/pharmq-novnc"
|
||||
echo " Proxmox 호스트: $PVE_HOST"
|
||||
echo ""
|
||||
echo -e "${CYAN}VM 설정:${NC}"
|
||||
echo " VM1: $VM1_ID - $VM1_NAME"
|
||||
echo " - VNC 포트: $VM1_VNC_PORT"
|
||||
echo " - WebSocket 포트: $WS_PORT1"
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
echo " VM2: $VM2_ID - $VM2_NAME"
|
||||
echo " - VNC 포트: $VM2_VNC_PORT"
|
||||
echo " - WebSocket 포트: $WS_PORT2"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}접속 URL:${NC}"
|
||||
echo " 로컬 헬스체크: http://localhost:$FLASK_PORT/health"
|
||||
echo " VM1 noVNC: http://localhost:$FLASK_PORT/"
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
echo " VM2 noVNC: http://localhost:$FLASK_PORT/vnc2"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}Gateway를 통한 접속 (외부):${NC}"
|
||||
echo " https://gateway.pharmq.kr/api/novnc/$PHARMACY_CODE/vnc1"
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
echo " https://gateway.pharmq.kr/api/novnc/$PHARMACY_CODE/vnc2"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}관리 명령어:${NC}"
|
||||
echo " 상태 확인: /srv/pharmq-novnc/scripts/check-status.sh"
|
||||
echo " 전체 재시작: systemctl restart pharmq-websockify-vnc1 pharmq-vnc-app"
|
||||
if [ "$VM2_ENABLED" = true ]; then
|
||||
echo " systemctl restart pharmq-websockify-vnc2"
|
||||
fi
|
||||
echo " 로그 확인: journalctl -u pharmq-vnc-app.service -f"
|
||||
echo ""
|
||||
echo -e "${YELLOW}다음 단계:${NC}"
|
||||
echo " 1. 헬스체크 테스트: curl http://localhost:$FLASK_PORT/health"
|
||||
echo " 2. Gateway에서 접속 테스트"
|
||||
echo " 3. 브라우저에서 noVNC 화면 확인"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 메인 함수
|
||||
main() {
|
||||
print_header
|
||||
|
||||
# 사전 검사
|
||||
check_root
|
||||
detect_environment
|
||||
|
||||
# 기존 설치 확인
|
||||
if [ -f "/srv/pharmq-novnc/config.json" ] && \
|
||||
systemctl is-active --quiet pharmq-vnc-app.service; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=========================================="
|
||||
echo "✅ PharmQ noVNC가 이미 설치되어 있습니다!"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
|
||||
CURRENT_PHARMACY_CODE=$(python3 -c "import json; print(json.load(open('/srv/pharmq-novnc/config.json', encoding='utf-8'))['pharmacy_id'])" 2>/dev/null || echo "")
|
||||
CURRENT_PHARMACY_NAME=$(python3 -c "import json; print(json.load(open('/srv/pharmq-novnc/config.json', encoding='utf-8'))['pharmacy_name'])" 2>/dev/null || echo "")
|
||||
|
||||
echo -e "${CYAN}현재 설정:${NC}"
|
||||
echo " 약국 코드: ${CURRENT_PHARMACY_CODE:-'설정되지 않음'}"
|
||||
echo " 약국 이름: ${CURRENT_PHARMACY_NAME:-'설정되지 않음'}"
|
||||
echo ""
|
||||
echo "다음 중 선택하세요:"
|
||||
echo " 1) 상태 확인"
|
||||
echo " 2) 재설치 (기존 설정 백업 후 새로 설치)"
|
||||
echo " 3) 종료"
|
||||
echo ""
|
||||
echo -n "선택 [1/2/3]: "
|
||||
read -r choice </dev/tty
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
echo ""
|
||||
msg_info "서비스 상태 확인 중..."
|
||||
if [ -x "/srv/pharmq-novnc/scripts/check-status.sh" ]; then
|
||||
/srv/pharmq-novnc/scripts/check-status.sh
|
||||
else
|
||||
systemctl status pharmq-vnc-app.service pharmq-websockify-vnc1.service
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
2)
|
||||
msg_warn "재설치를 시작합니다..."
|
||||
echo ""
|
||||
;;
|
||||
3)
|
||||
msg_ok "종료합니다."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
msg_warn "잘못된 선택입니다. 종료합니다."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# VM 설정
|
||||
configure_vms
|
||||
|
||||
# 약국 정보 입력
|
||||
get_pharmacy_info
|
||||
|
||||
# 네트워크 확인
|
||||
check_network
|
||||
|
||||
# 설치 진행
|
||||
install_packages
|
||||
install_pharmq_novnc
|
||||
create_config
|
||||
setup_systemd_services
|
||||
start_services
|
||||
|
||||
# 방화벽 설정 (선택사항)
|
||||
configure_firewall
|
||||
|
||||
# 테스트 및 완료
|
||||
test_installation
|
||||
print_completion
|
||||
}
|
||||
|
||||
# 에러 핸들링
|
||||
trap 'msg_error "스크립트 실행 중 오류가 발생했습니다."' ERR
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
258
cleanup-test-data.sh
Executable file
258
cleanup-test-data.sh
Executable file
@@ -0,0 +1,258 @@
|
||||
#!/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 </dev/tty
|
||||
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
python3 << EOF
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('$FARMQ_DB')
|
||||
cursor = conn.cursor()
|
||||
# P0003~P9999만 삭제 (P001, P002, P0001, P0002는 보호)
|
||||
cursor.execute("DELETE FROM pharmacies WHERE pharmacy_code >= 'P0003' AND pharmacy_code <= 'P9999' AND LENGTH(pharmacy_code) = 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('$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 </dev/tty
|
||||
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
python3 << EOF
|
||||
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')
|
||||
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/tty
|
||||
|
||||
if [ -z "$node_ids" ]; then
|
||||
echo -e "${YELLOW}건너뜀${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "\n${BLUE}노드 삭제 중...${NC}"
|
||||
for id in $node_ids; do
|
||||
if docker exec headscale headscale nodes delete --identifier "$id" --force 2>/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/tty
|
||||
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# farmq.db 백업
|
||||
if [ -f "/srv/headscale-tailscale-replacement/farmq-admin/farmq.db" ]; then
|
||||
cp /srv/headscale-tailscale-replacement/farmq-admin/farmq.db \
|
||||
/srv/headscale-tailscale-replacement/farmq-admin/farmq.db.backup_$TIMESTAMP
|
||||
echo -e "${GREEN}✓ farmq.db 백업 생성: farmq.db.backup_$TIMESTAMP${NC}"
|
||||
fi
|
||||
|
||||
# gateway.db 백업
|
||||
if [ -f "/srv/pharmq-gateway/gateway.db" ]; then
|
||||
cp /srv/pharmq-gateway/gateway.db \
|
||||
/srv/pharmq-gateway/gateway.db.backup_$TIMESTAMP
|
||||
echo -e "${GREEN}✓ gateway.db 백업 생성: gateway.db.backup_$TIMESTAMP${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}백업 건너뜀${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 메인 함수
|
||||
# ================================
|
||||
main() {
|
||||
print_header "팜큐(FARMQ) 테스트 데이터 정리"
|
||||
|
||||
echo -e "${CYAN}이 스크립트는 다음 작업을 수행합니다:${NC}"
|
||||
echo -e " 1. farmq.db에서 P0003 이후 약국 삭제"
|
||||
echo -e " 2. gateway.db에서 ID 5 이후 사용자 삭제"
|
||||
echo -e " 3. Headscale 테스트 노드 삭제 (선택)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ 주의: 운영 데이터는 삭제되지 않습니다${NC}"
|
||||
echo -e " - 보호되는 약국: P001, P002, P0002"
|
||||
echo -e " - 보호되는 사용자: ID 1~4"
|
||||
echo ""
|
||||
|
||||
read -p "계속하시겠습니까? (y/N) " -r response </dev/tty
|
||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}취소되었습니다${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 백업 생성 (선택)
|
||||
create_backup
|
||||
|
||||
echo ""
|
||||
|
||||
# 1. farmq.db 정리
|
||||
cleanup_farmq_db
|
||||
|
||||
echo ""
|
||||
|
||||
# 2. gateway.db 정리
|
||||
cleanup_gateway_db
|
||||
|
||||
echo ""
|
||||
|
||||
# 3. Headscale 노드 정리
|
||||
cleanup_headscale_nodes
|
||||
|
||||
echo ""
|
||||
print_header "정리 완료!"
|
||||
echo -e "${GREEN}모든 작업이 완료되었습니다.${NC}"
|
||||
}
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
128
code-server.sh
Executable file
128
code-server.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup-code-server.sh
|
||||
# - code-server 미설치 시 자동 설치
|
||||
# - 최초 1회 실행해 ~/.config/code-server/config.yaml 생성
|
||||
# - config.yaml을 0.0.0.0:<PORT> + 지정 비밀번호로 갱신
|
||||
# - 기존에 떠있는 code-server(수동/비-systemd) 프로세스 정리
|
||||
# - systemd 미사용: nohup으로 백그라운드 실행
|
||||
#
|
||||
# 환경변수:
|
||||
# PORT=8080 # 바인드 포트 (기본 8080)
|
||||
# PASSWORD= # 비밀번호(무인 실행용)
|
||||
# SKIP_CONFIRM=0/1 # 비밀번호 확인 입력 생략
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PORT:-8080}"
|
||||
CONFIG_DIR="${HOME}/.config/code-server"
|
||||
CONFIG_FILE="${CONFIG_DIR}/config.yaml"
|
||||
LOG_FILE="${HOME}/code-server.log"
|
||||
|
||||
say() { echo -e "$@"; }
|
||||
die() { echo -e "❌ $@" >&2; exit 1; }
|
||||
|
||||
# 0) 필수 도구 준비 (curl/timeout/pgrep 등)
|
||||
if ! command -v curl >/dev/null 2>&1 || ! command -v timeout >/dev/null 2>&1; then
|
||||
say "📦 필요 패키지 설치 중 (curl, coreutils, procps 등)..."
|
||||
if command -v apt >/dev/null 2>&1; then
|
||||
apt update -y >/dev/null 2>&1 || true
|
||||
apt install -y curl ca-certificates coreutils procps >/dev/null 2>&1
|
||||
else
|
||||
die "apt 환경이 아닙니다. curl/timeout/pgrep가 필요합니다."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1) code-server 설치 확인 및 자동 설치
|
||||
if ! command -v code-server >/dev/null 2>&1; then
|
||||
say "📦 code-server 미설치 상태 → 설치 진행..."
|
||||
bash <(curl -fsSL https://code-server.dev/install.sh)
|
||||
command -v code-server >/dev/null 2>&1 || die "code-server 설치 실패"
|
||||
say "✅ code-server 설치 완료"
|
||||
else
|
||||
say "✅ code-server 이미 설치됨"
|
||||
fi
|
||||
|
||||
# 2) config.yaml 생성 (없으면 최초 1회 3~5초 실행)
|
||||
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||
say "📝 config.yaml 이 없어 최초 1회 실행으로 생성합니다..."
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
timeout 5s code-server >/dev/null 2>&1 || true
|
||||
[ -f "${CONFIG_FILE}" ] || die "config.yaml 생성 실패"
|
||||
say "✅ 기본 config.yaml 생성됨: ${CONFIG_FILE}"
|
||||
else
|
||||
say "ℹ️ 기존 config.yaml 감지: ${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
# 3) 비밀번호 입력/확정
|
||||
if [ "${PASSWORD-}" = "" ]; then
|
||||
read -rsp "🔐 code-server 접속 비밀번호 입력: " PASS; echo
|
||||
if [ "${SKIP_CONFIRM-0}" != "1" ]; then
|
||||
read -rsp "🔐 비밀번호 확인 입력: " PASS2; echo
|
||||
[ "$PASS" = "$PASS2" ] || die "비밀번호 불일치"
|
||||
fi
|
||||
else
|
||||
PASS="$PASSWORD"
|
||||
fi
|
||||
[ -n "$PASS" ] || die "비밀번호는 비어 있을 수 없습니다."
|
||||
|
||||
# 4) 기존 파일 백업 후 config.yaml 갱신
|
||||
ts="$(date +%Y%m%d%H%M%S)"
|
||||
if [ -f "${CONFIG_FILE}" ]; then
|
||||
cp -a "${CONFIG_FILE}" "${CONFIG_FILE}.bak.${ts}"
|
||||
say "🗂 백업 생성: ${CONFIG_FILE}.bak.${ts}"
|
||||
fi
|
||||
|
||||
cat > "${CONFIG_FILE}" <<EOF
|
||||
bind-addr: 0.0.0.0:${PORT}
|
||||
auth: password
|
||||
password: ${PASS}
|
||||
cert: false
|
||||
EOF
|
||||
|
||||
say "✅ config.yaml 갱신됨 (bind-addr=0.0.0.0:${PORT})"
|
||||
|
||||
# 5) systemd로 떠있다면 중지(원하시는 게 '비-systemd' 운영이므로)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl is-active --quiet "code-server@${USER}"; then
|
||||
say "⏹ systemd 서비스(code-server@${USER}) 중지"
|
||||
systemctl stop "code-server@${USER}" || true
|
||||
fi
|
||||
if [ "$EUID" -eq 0 ] && systemctl is-active --quiet "code-server@root"; then
|
||||
say "⏹ systemd 서비스(code-server@root}) 중지"
|
||||
systemctl stop "code-server@root" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# 6) 수동/기존 실행 프로세스 정리 (부모/자식 순서 종료)
|
||||
say "🧹 기존 code-server 수동 프로세스 정리..."
|
||||
# 부모 엔트리(메인/entry) TERM
|
||||
pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)"
|
||||
if [ -n "${pids}" ]; then
|
||||
for p in $pids; do
|
||||
pkill -TERM -P "$p" 2>/dev/null || true
|
||||
kill -TERM "$p" 2>/dev/null || true
|
||||
done
|
||||
sleep 2
|
||||
fi
|
||||
# 남아있으면 KILL
|
||||
pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)"
|
||||
[ -n "${pids}" ] && kill -9 $pids 2>/dev/null || true
|
||||
|
||||
# 보조 호스트/터미널 프로세스 잔여물 정리(있어도 없어도 무방)
|
||||
pkill -f "vscode/out/bootstrap-fork --type=ptyHost" 2>/dev/null || true
|
||||
pkill -f "vscode/out/bootstrap-fork --type=extensionHost" 2>/dev/null || true
|
||||
pkill -f "shellIntegration-bash.sh" 2>/dev/null || true
|
||||
|
||||
# 7) 비-systemd 백그라운드 실행
|
||||
say "🚀 code-server 일반 실행(nohup 백그라운드) 시작..."
|
||||
nohup code-server > "${LOG_FILE}" 2>&1 &
|
||||
pid=$!
|
||||
disown || true
|
||||
sleep 1
|
||||
|
||||
say "✅ 실행됨 (PID: ${pid})"
|
||||
say "📄 로그 보기: tail -f ${LOG_FILE}"
|
||||
say "🌐 접속 URL: http://<서버IP>:${PORT}"
|
||||
say "🔑 비밀번호: (방금 설정한 값)"
|
||||
say "🔒 보안 권장: 역프록시(Caddy/Nginx) + HTTPS 사용 시 config는 127.0.0.1로 바꾸세요."
|
||||
758
headscale-auto-register.sh
Executable file
758
headscale-auto-register.sh
Executable file
@@ -0,0 +1,758 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 팜큐(FARMQ) Headscale 원클릭 설치 및 등록 스크립트
|
||||
# 사용법: curl -fsSL https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# 또는: wget -qO- https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# root 계정: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash
|
||||
# 강제 재등록: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash -s -- --force
|
||||
|
||||
set -e
|
||||
|
||||
# ================================
|
||||
# 설정 (필요시 수정)
|
||||
# ================================
|
||||
HEADSCALE_SERVER="http://head.pharmq.kr" # Headscale 서버 주소
|
||||
PREAUTH_KEY="b46923995afeaec90e588168f2e1bf99801775e8657ce003" # 7일간 재사용 가능한 키
|
||||
FARMQ_NETWORK="100.64.0.0/10" # 팜큐 네트워크 대역
|
||||
|
||||
# 명령행 옵션 처리
|
||||
FORCE_REGISTER=false
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force|-f)
|
||||
FORCE_REGISTER=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "사용법: $0 [옵션]"
|
||||
echo "옵션:"
|
||||
echo " --force, -f 기존 연결을 강제로 해제하고 재등록"
|
||||
echo " --help, -h 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# 알 수 없는 옵션 무시
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ================================
|
||||
# 색상 출력 함수
|
||||
# ================================
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
WHITE='\033[1;37m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${PURPLE}============================================${NC}"
|
||||
echo -e "${WHITE}$1${NC}"
|
||||
echo -e "${PURPLE}============================================${NC}\n"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
echo -e "\n${BLUE}🔧 $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\n${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\n${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "\n${CYAN}📋 $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "\n${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 운영체제 감지
|
||||
# ================================
|
||||
detect_os() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
CODENAME=$VERSION_CODENAME
|
||||
else
|
||||
print_error "지원하지 않는 운영체제입니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "감지된 OS: $OS $VERSION ($CODENAME)"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 시스템 요구사항 확인
|
||||
# ================================
|
||||
check_requirements() {
|
||||
print_status "시스템 요구사항 확인 중..."
|
||||
|
||||
# Root 권한 확인
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "이 스크립트는 root 권한으로 실행해야 합니다."
|
||||
print_info "다음 중 하나의 방법으로 다시 실행해주세요:"
|
||||
print_info "1. sudo가 있는 경우: curl ... | sudo bash"
|
||||
print_info "2. root 계정인 경우: curl ... | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# curl 또는 wget 확인
|
||||
if ! command -v curl >/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=false >/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=false"
|
||||
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 <node> # 다른 노드와 연결 테스트"
|
||||
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 "설치 완료 - 팜큐 네트워크를 사용할 수 있습니다!"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 약국 자동 등록 함수들
|
||||
# ================================
|
||||
|
||||
# 장비 타입 및 정보 자동 감지
|
||||
detect_device_info() {
|
||||
echo -e "${BLUE}장비 정보 감지 중...${NC}"
|
||||
|
||||
# hostname 수집
|
||||
DEVICE_HOSTNAME=$(hostname)
|
||||
|
||||
# Proxmox 호스트 감지
|
||||
if [ -f /etc/pve/local/pve-ssl.pem ] || command -v pveversion >/dev/null 2>&1; then
|
||||
DEVICE_TYPE="proxmox_host"
|
||||
DEVICE_NAME="${PHARMACY_NAME} Proxmox Host"
|
||||
echo -e "${GREEN}✓ Proxmox VE 호스트 감지됨${NC}"
|
||||
|
||||
# Windows (WSL 환경)
|
||||
elif grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
DEVICE_TYPE="windows_pc"
|
||||
DEVICE_NAME="${PHARMACY_NAME} Windows PC"
|
||||
echo -e "${GREEN}✓ Windows PC (WSL) 감지됨${NC}"
|
||||
|
||||
# macOS
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
DEVICE_TYPE="mac"
|
||||
DEVICE_NAME="${PHARMACY_NAME} Mac"
|
||||
echo -e "${GREEN}✓ macOS 감지됨${NC}"
|
||||
|
||||
# Linux 서버 (일반)
|
||||
else
|
||||
DEVICE_TYPE="linux_server"
|
||||
DEVICE_NAME="${PHARMACY_NAME} Linux Server"
|
||||
echo -e "${GREEN}✓ Linux 서버 감지됨${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${CYAN} 장비명: $DEVICE_NAME${NC}"
|
||||
echo -e "${CYAN} 장비 타입: $DEVICE_TYPE${NC}"
|
||||
echo -e "${CYAN} 호스트명: $DEVICE_HOSTNAME${NC}"
|
||||
}
|
||||
|
||||
# 약국 정보 수집
|
||||
collect_pharmacy_info() {
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${WHITE}약국 정보 입력${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
# 약국명 입력 (필수)
|
||||
while [ -z "$PHARMACY_NAME" ]; do
|
||||
read -p "약국명을 입력하세요: " PHARMACY_NAME </dev/tty
|
||||
done
|
||||
|
||||
# 요양기관부호 입력 (선택)
|
||||
read -p "요양기관부호 (선택, Enter로 건너뛰기): " HIRA_CODE </dev/tty
|
||||
|
||||
# 약국 주소 입력 (선택)
|
||||
read -p "약국 주소 (선택): " PHARMACY_ADDRESS </dev/tty
|
||||
|
||||
# 약국장 이름 입력 (선택)
|
||||
read -p "약국장 이름 (선택): " OWNER_NAME </dev/tty
|
||||
|
||||
# 연락처 입력 (선택)
|
||||
read -p "약국 연락처 (선택): " PHARMACY_PHONE </dev/tty
|
||||
|
||||
echo -e "${GREEN}✓ 약국 정보 입력 완료${NC}"
|
||||
}
|
||||
|
||||
# VPN IP 확인
|
||||
get_assigned_vpn_ip() {
|
||||
echo -e "${BLUE}VPN IP 확인 중...${NC}"
|
||||
|
||||
# tailscale ip -4로 IP 추출 (최대 10초 대기)
|
||||
for i in {1..10}; do
|
||||
VPN_IP=$(tailscale ip -4 2>/dev/null)
|
||||
|
||||
if [ -n "$VPN_IP" ]; then
|
||||
echo -e "${GREEN}✓ VPN IP: $VPN_IP${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}⏳ VPN IP 할당 대기 중... ($i/10)${NC}"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo -e "${RED}✗ VPN IP를 가져올 수 없습니다${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# farmq-admin API 호출하여 약국 생성
|
||||
create_pharmacy_via_api() {
|
||||
echo -e "${BLUE}약국 등록 중 (farmq.db)...${NC}"
|
||||
|
||||
# JSON 데이터 구성 (장비 정보 포함)
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
"vpn_ip": "$VPN_IP",
|
||||
"hira_code": "$HIRA_CODE",
|
||||
"address": "$PHARMACY_ADDRESS",
|
||||
"owner_name": "$OWNER_NAME",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"api_port": 8082,
|
||||
"device_type": "$DEVICE_TYPE",
|
||||
"device_name": "$DEVICE_NAME",
|
||||
"hostname": "$DEVICE_HOSTNAME"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# API 호출 (외부 도메인)
|
||||
RESPONSE=$(curl -s -X POST https://demo.pharmq.kr/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# 성공 여부 먼저 확인 (공백/줄바꿈 허용)
|
||||
if ! echo "$RESPONSE" | grep -q '"success"' || ! echo "$RESPONSE" | grep -q 'true'; then
|
||||
echo -e "${RED}✗ 약국 생성 API 실패${NC}"
|
||||
echo -e "${YELLOW}[응답 내용]${NC}"
|
||||
echo "$RESPONSE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# pharmacy_code 추출 (순수 bash - grep 사용)
|
||||
# 방법: grep으로 pharmacy_code 라인 찾고, sed로 값 추출
|
||||
PHARMACY_CODE=$(echo "$RESPONSE" | grep '"pharmacy_code"' | sed 's/.*"pharmacy_code":[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
|
||||
# 추출 실패 시 디버깅 정보 출력
|
||||
if [ -z "$PHARMACY_CODE" ]; then
|
||||
echo -e "${RED}✗ pharmacy_code 추출 실패${NC}"
|
||||
echo -e "${YELLOW}[원인] JSON 파싱 에러${NC}"
|
||||
echo -e "${YELLOW}[응답 내용]${NC}"
|
||||
echo "$RESPONSE"
|
||||
echo -e "${YELLOW}[추출 시도 결과]${NC}"
|
||||
echo "$RESPONSE" | grep '"pharmacy_code"'
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ 약국 생성 완료: $PHARMACY_CODE${NC}"
|
||||
|
||||
# 장비 등록 여부 확인
|
||||
if echo "$RESPONSE" | grep -q '"device_registered"' && echo "$RESPONSE" | grep -q 'true'; then
|
||||
echo -e "${GREEN}✓ 장비 등록 완료: $DEVICE_TYPE${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ 장비 등록 실패 (약국은 정상 생성됨)${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# gateway API 호출하여 관리자 계정 생성
|
||||
create_gateway_user_via_api() {
|
||||
echo -e "${BLUE}관리자 계정 생성 중 (gateway.db)...${NC}"
|
||||
|
||||
# username: pharmacy_code 소문자 (P0005 → p0005)
|
||||
USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')
|
||||
PASSWORD="12341234" # 기본 비밀번호 (최소 8자)
|
||||
EMAIL="${USERNAME}@pharmq.internal"
|
||||
|
||||
# JSON 데이터 구성
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"username": "$USERNAME",
|
||||
"email": "$EMAIL",
|
||||
"password": "$PASSWORD",
|
||||
"name": "${PHARMACY_NAME} 관리자",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"primary_pharmacy_code": "$PHARMACY_CODE",
|
||||
"role": "admin"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# 디버깅: 전송할 데이터 출력
|
||||
echo -e "${CYAN}[DEBUG] 전송 데이터:${NC}"
|
||||
echo "$JSON_DATA"
|
||||
|
||||
# API 호출 (외부 도메인)
|
||||
RESPONSE=$(curl -s -X POST https://gateway.pharmq.kr/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# 디버깅: 응답 출력
|
||||
echo -e "${CYAN}[DEBUG] API 응답:${NC}"
|
||||
echo "$RESPONSE"
|
||||
|
||||
# 성공 여부 확인
|
||||
if echo "$RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✓ 관리자 계정 생성 완료${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ 관리자 계정 생성 실패${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 로그인 정보 출력
|
||||
display_login_credentials() {
|
||||
USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${WHITE}🎉 설치 및 등록 완료!${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
echo -e "\n${GREEN}=== 결과 요약 ===${NC}"
|
||||
echo -e "약국 코드: ${WHITE}$PHARMACY_CODE${NC}"
|
||||
echo -e "약국명: ${WHITE}$PHARMACY_NAME${NC}"
|
||||
echo -e "생성된 계정: ${WHITE}$USERNAME${NC}"
|
||||
echo -e "로그인 URL: ${WHITE}https://pharmq.kr${NC}"
|
||||
echo -e "비밀번호: ${WHITE}12341234${NC}"
|
||||
|
||||
echo -e "\n${GREEN}VPN 정보:${NC}"
|
||||
echo -e " VPN IP: ${WHITE}$VPN_IP${NC}"
|
||||
[ -n "$HIRA_CODE" ] && echo -e " 요양기관부호: ${WHITE}$HIRA_CODE${NC}"
|
||||
[ -n "$PHARMACY_ADDRESS" ] && echo -e " 주소: ${WHITE}$PHARMACY_ADDRESS${NC}"
|
||||
[ -n "$PHARMACY_PHONE" ] && echo -e " 전화번호: ${WHITE}$PHARMACY_PHONE${NC}"
|
||||
|
||||
echo -e "\n${GREEN}장비 정보:${NC}"
|
||||
echo -e " 장비명: ${WHITE}$DEVICE_NAME${NC}"
|
||||
echo -e " 장비 타입: ${WHITE}$DEVICE_TYPE${NC}"
|
||||
echo -e " 호스트명: ${WHITE}$DEVICE_HOSTNAME${NC}"
|
||||
|
||||
echo -e "\n${YELLOW}⚠ 최초 로그인 후 비밀번호를 변경하세요!${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 메인 함수
|
||||
# ================================
|
||||
main() {
|
||||
print_header "팜큐(FARMQ) Headscale 원클릭 설치 및 약국 등록"
|
||||
|
||||
# 사전 체크
|
||||
detect_os
|
||||
check_requirements
|
||||
|
||||
# 설치 과정
|
||||
install_tailscale
|
||||
start_tailscale
|
||||
register_headscale
|
||||
|
||||
# VPN IP 확인 (Headscale 등록 직후)
|
||||
sleep 3 # Headscale에서 IP 할당 대기
|
||||
get_assigned_vpn_ip || exit 1
|
||||
|
||||
# 약국 정보 수집
|
||||
collect_pharmacy_info
|
||||
|
||||
# 장비 정보 자동 감지
|
||||
detect_device_info
|
||||
|
||||
# 약국 및 계정 생성 (장비 등록 포함)
|
||||
create_pharmacy_via_api || exit 1
|
||||
create_gateway_user_via_api || exit 1
|
||||
|
||||
# 사후 설정
|
||||
configure_firewall
|
||||
verify_connection
|
||||
|
||||
# 정리 및 완료
|
||||
cleanup
|
||||
display_login_credentials
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 에러 핸들링
|
||||
# ================================
|
||||
trap 'echo -e "\n❌ 설치 중 오류가 발생했습니다. 로그를 확인해주세요."; exit 1' ERR
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
519
headscale-quick-install.sh
Executable file
519
headscale-quick-install.sh
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 팜큐(FARMQ) Headscale 원클릭 설치 및 등록 스크립트
|
||||
# 사용법: curl -fsSL https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# 또는: wget -qO- https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# root 계정: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash
|
||||
# 강제 재등록: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash -s -- --force
|
||||
|
||||
set -e
|
||||
|
||||
# ================================
|
||||
# 설정 (필요시 수정)
|
||||
# ================================
|
||||
HEADSCALE_SERVER="http://head.pharmq.kr" # Headscale 서버 주소
|
||||
PREAUTH_KEY="b46923995afeaec90e588168f2e1bf99801775e8657ce003" # 7일간 재사용 가능한 키
|
||||
FARMQ_NETWORK="100.64.0.0/10" # 팜큐 네트워크 대역
|
||||
|
||||
# 명령행 옵션 처리
|
||||
FORCE_REGISTER=false
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force|-f)
|
||||
FORCE_REGISTER=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "사용법: $0 [옵션]"
|
||||
echo "옵션:"
|
||||
echo " --force, -f 기존 연결을 강제로 해제하고 재등록"
|
||||
echo " --help, -h 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# 알 수 없는 옵션 무시
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ================================
|
||||
# 색상 출력 함수
|
||||
# ================================
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
WHITE='\033[1;37m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${PURPLE}============================================${NC}"
|
||||
echo -e "${WHITE}$1${NC}"
|
||||
echo -e "${PURPLE}============================================${NC}\n"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
echo -e "\n${BLUE}🔧 $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\n${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\n${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "\n${CYAN}📋 $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "\n${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 운영체제 감지
|
||||
# ================================
|
||||
detect_os() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
CODENAME=$VERSION_CODENAME
|
||||
else
|
||||
print_error "지원하지 않는 운영체제입니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "감지된 OS: $OS $VERSION ($CODENAME)"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 시스템 요구사항 확인
|
||||
# ================================
|
||||
check_requirements() {
|
||||
print_status "시스템 요구사항 확인 중..."
|
||||
|
||||
# Root 권한 확인
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "이 스크립트는 root 권한으로 실행해야 합니다."
|
||||
print_info "다음 중 하나의 방법으로 다시 실행해주세요:"
|
||||
print_info "1. sudo가 있는 경우: curl ... | sudo bash"
|
||||
print_info "2. root 계정인 경우: curl ... | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# curl 또는 wget 확인
|
||||
if ! command -v curl >/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 <node> # 다른 노드와 연결 테스트"
|
||||
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 "$@"
|
||||
519
headscale-quick-install.sh.backup
Executable file
519
headscale-quick-install.sh.backup
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 팜큐(FARMQ) Headscale 원클릭 설치 및 등록 스크립트
|
||||
# 사용법: curl -fsSL https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# 또는: wget -qO- https://git.0bin.in/.../quick-install.sh | sudo bash
|
||||
# root 계정: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash
|
||||
# 강제 재등록: curl -fsSL https://git.0bin.in/.../quick-install.sh | bash -s -- --force
|
||||
|
||||
set -e
|
||||
|
||||
# ================================
|
||||
# 설정 (필요시 수정)
|
||||
# ================================
|
||||
HEADSCALE_SERVER="http://head.pharmq.kr" # Headscale 서버 주소
|
||||
PREAUTH_KEY="b46923995afeaec90e588168f2e1bf99801775e8657ce003" # 7일간 재사용 가능한 키
|
||||
FARMQ_NETWORK="100.64.0.0/10" # 팜큐 네트워크 대역
|
||||
|
||||
# 명령행 옵션 처리
|
||||
FORCE_REGISTER=false
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force|-f)
|
||||
FORCE_REGISTER=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "사용법: $0 [옵션]"
|
||||
echo "옵션:"
|
||||
echo " --force, -f 기존 연결을 강제로 해제하고 재등록"
|
||||
echo " --help, -h 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# 알 수 없는 옵션 무시
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ================================
|
||||
# 색상 출력 함수
|
||||
# ================================
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
WHITE='\033[1;37m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${PURPLE}============================================${NC}"
|
||||
echo -e "${WHITE}$1${NC}"
|
||||
echo -e "${PURPLE}============================================${NC}\n"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
echo -e "\n${BLUE}🔧 $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\n${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\n${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "\n${CYAN}📋 $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "\n${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 운영체제 감지
|
||||
# ================================
|
||||
detect_os() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
CODENAME=$VERSION_CODENAME
|
||||
else
|
||||
print_error "지원하지 않는 운영체제입니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "감지된 OS: $OS $VERSION ($CODENAME)"
|
||||
}
|
||||
|
||||
# ================================
|
||||
# 시스템 요구사항 확인
|
||||
# ================================
|
||||
check_requirements() {
|
||||
print_status "시스템 요구사항 확인 중..."
|
||||
|
||||
# Root 권한 확인
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "이 스크립트는 root 권한으로 실행해야 합니다."
|
||||
print_info "다음 중 하나의 방법으로 다시 실행해주세요:"
|
||||
print_info "1. sudo가 있는 경우: curl ... | sudo bash"
|
||||
print_info "2. root 계정인 경우: curl ... | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# curl 또는 wget 확인
|
||||
if ! command -v curl >/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 <node> # 다른 노드와 연결 테스트"
|
||||
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 "$@"
|
||||
629
pbs_allinone.sh
Normal file
629
pbs_allinone.sh
Normal file
@@ -0,0 +1,629 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# PBS 올인원 스크립트 - Proxmox 호스트에서 실행
|
||||
# 작성일: 2025-11-04
|
||||
# 용도: PBS 등록 → 백업 조회 → 복구를 한 번에
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 색상
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m'
|
||||
|
||||
# PBS 서버 설정 (자동 입력)
|
||||
PBS_SERVER="100.64.0.10"
|
||||
PBS_PORT="8007"
|
||||
PBS_USERNAME="0bin@pbs"
|
||||
PBS_PASSWORD="@Trajet6640"
|
||||
PBS_DATASTORE="PBS-DVA"
|
||||
PBS_FINGERPRINT="24:42:c6:0f:a8:1b:93:32:32:44:84:be:6a:c5:71:97:e4:4d:61:fc:a4:48:12:0c:97:3b:9f:1f:cc:b2:54:e8"
|
||||
PBS_STORAGE_NAME="PBS-Auto"
|
||||
|
||||
# 복구 설정
|
||||
TEMPLATE_VMID=""
|
||||
TARGET_VMID=""
|
||||
TARGET_STORAGE="local-lvm"
|
||||
BACKUP_TYPE=""
|
||||
LATEST_SNAPSHOT=""
|
||||
|
||||
# 로그 함수
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_step() { echo -e "${CYAN}╰─► ${NC}$1"; }
|
||||
|
||||
# 배너
|
||||
print_banner() {
|
||||
clear
|
||||
echo -e "${MAGENTA}"
|
||||
cat << "EOF"
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ PBS 올인원 스크립트 ║
|
||||
║ 등록 → 조회 → 복구 한 번에! ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Proxmox 환경 확인
|
||||
check_proxmox() {
|
||||
if [ ! -f /etc/pve/storage.cfg ]; then
|
||||
log_error "Proxmox VE가 설치되어 있지 않습니다."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Proxmox VE 환경 확인 완료"
|
||||
}
|
||||
|
||||
# PBS 스토리지 등록
|
||||
register_pbs() {
|
||||
log_step "PBS 스토리지 등록 중..."
|
||||
|
||||
# 이미 등록되어 있는지 확인
|
||||
if pvesm status | grep -q "^${PBS_STORAGE_NAME} "; then
|
||||
log_warning "PBS 스토리지가 이미 등록되어 있습니다."
|
||||
read -p "$(echo -e ${YELLOW}재등록하시겠습니까?${NC}) (y/N): " reregister < /dev/tty
|
||||
|
||||
if [[ "$reregister" =~ ^[Yy]$ ]]; then
|
||||
log_info "기존 PBS 스토리지 제거 중..."
|
||||
pvesm remove "${PBS_STORAGE_NAME}"
|
||||
sleep 1
|
||||
else
|
||||
log_info "기존 PBS 스토리지 사용"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# PBS 등록
|
||||
log_info "PBS 서버 등록 중..."
|
||||
pvesm add pbs "${PBS_STORAGE_NAME}" \
|
||||
--server "${PBS_SERVER}" \
|
||||
--port "${PBS_PORT}" \
|
||||
--datastore "${PBS_DATASTORE}" \
|
||||
--username "${PBS_USERNAME}" \
|
||||
--password "${PBS_PASSWORD}" \
|
||||
--fingerprint "${PBS_FINGERPRINT}" \
|
||||
--namespace "PQ" 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_success "PBS 스토리지 등록 완료!"
|
||||
else
|
||||
log_error "PBS 스토리지 등록 실패"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 연결 테스트
|
||||
log_info "PBS 연결 테스트 중..."
|
||||
if pvesm status -storage "${PBS_STORAGE_NAME}" &>/dev/null; then
|
||||
log_success "PBS 연결 성공!"
|
||||
echo ""
|
||||
pvesm status -storage "${PBS_STORAGE_NAME}"
|
||||
echo ""
|
||||
else
|
||||
log_error "PBS 연결 테스트 실패"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# PBS API 인증
|
||||
pbs_login() {
|
||||
log_info "PBS API 인증 중..."
|
||||
|
||||
curl -k -s -X POST "https://${PBS_SERVER}:${PBS_PORT}/api2/json/access/ticket" \
|
||||
-d "username=${PBS_USERNAME}" \
|
||||
-d "password=${PBS_PASSWORD}" > /tmp/pbs_auth.json
|
||||
|
||||
if [ ! -s /tmp/pbs_auth.json ]; then
|
||||
log_error "PBS API 응답 없음. 서버 연결을 확인하세요."
|
||||
return 1
|
||||
fi
|
||||
|
||||
PBS_TICKET=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["ticket"])' 2>/dev/null)
|
||||
PBS_CSRF=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["CSRFPreventionToken"])' 2>/dev/null)
|
||||
|
||||
if [ -z "$PBS_TICKET" ]; then
|
||||
log_error "PBS API 인증 실패. 응답:"
|
||||
cat /tmp/pbs_auth.json | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "PBS API 인증 완료"
|
||||
}
|
||||
|
||||
# 백업 그룹 목록 조회 및 선택
|
||||
list_and_select_backup() {
|
||||
log_step "PBS에서 백업 목록 조회 중..."
|
||||
echo ""
|
||||
|
||||
# 응답을 파일에 저장 (PQ 네임스페이스 명시)
|
||||
curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/groups?ns=PQ" \
|
||||
-H "Cookie: PBSAuthCookie=${PBS_TICKET}" \
|
||||
-H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_groups.json
|
||||
|
||||
# 디버깅: 응답 확인
|
||||
if [ ! -s /tmp/pbs_groups.json ]; then
|
||||
log_error "PBS API 응답이 비어있습니다"
|
||||
log_info "PBS_TICKET: ${PBS_TICKET:0:50}..."
|
||||
log_info "PBS_CSRF: ${PBS_CSRF:0:50}..."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 디버깅: 응답 길이 확인
|
||||
log_info "API 응답 수신 ($(stat -c%s /tmp/pbs_groups.json) bytes)"
|
||||
|
||||
# PBS 인증 정보를 환경 변수로 export
|
||||
export PBS_TICKET
|
||||
export PBS_CSRF
|
||||
|
||||
python3 << PYEOF
|
||||
import sys, json, subprocess, os
|
||||
|
||||
# PBS 인증 정보 가져오기
|
||||
PBS_TICKET = os.environ.get('PBS_TICKET', '')
|
||||
PBS_CSRF = os.environ.get('PBS_CSRF', '')
|
||||
PBS_SERVER = "${PBS_SERVER}"
|
||||
PBS_PORT = "${PBS_PORT}"
|
||||
PBS_DATASTORE = "${PBS_DATASTORE}"
|
||||
|
||||
def get_snapshot_comment(backup_type, backup_id):
|
||||
"""최신 스냅샷의 comment를 가져오기"""
|
||||
try:
|
||||
cmd = [
|
||||
'curl', '-k', '-s', '-X', 'GET',
|
||||
f'https://{PBS_SERVER}:{PBS_PORT}/api2/json/admin/datastore/{PBS_DATASTORE}/snapshots?backup-type={backup_type}&backup-id={backup_id}&ns=PQ',
|
||||
'-H', f'Cookie: PBSAuthCookie={PBS_TICKET}',
|
||||
'-H', f'CSRFPreventionToken: {PBS_CSRF}'
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
data = json.loads(result.stdout)
|
||||
snapshots = data.get("data", [])
|
||||
|
||||
if snapshots:
|
||||
# 가장 최근 스냅샷 찾기
|
||||
latest = max(snapshots, key=lambda x: x.get("backup-time", 0))
|
||||
return latest.get("comment", "")
|
||||
except:
|
||||
pass
|
||||
return ""
|
||||
|
||||
try:
|
||||
with open('/tmp/pbs_groups.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
groups = data.get("data", [])
|
||||
|
||||
vm_groups = [g for g in groups if g.get("backup-type") == "vm"]
|
||||
ct_groups = [g for g in groups if g.get("backup-type") == "ct"]
|
||||
|
||||
if vm_groups:
|
||||
print("\033[1;36m=== VM 백업 ===\033[0m")
|
||||
for i, group in enumerate(vm_groups, 1):
|
||||
backup_id = group.get("backup-id", "N/A")
|
||||
backup_count = group.get("backup-count", 0)
|
||||
last_backup = group.get("last-backup", 0)
|
||||
|
||||
# groups에서 comment 가져오기 (없으면 스냅샷에서 조회)
|
||||
comment = group.get("comment", "")
|
||||
if not comment:
|
||||
comment = get_snapshot_comment("vm", backup_id)
|
||||
|
||||
from datetime import datetime
|
||||
if last_backup > 0:
|
||||
last_str = datetime.fromtimestamp(last_backup).strftime("%Y-%m-%d %H:%M")
|
||||
else:
|
||||
last_str = "N/A"
|
||||
|
||||
if comment:
|
||||
print(f" {i}. VM {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})")
|
||||
print(f" \033[0;90m└─ {comment}\033[0m")
|
||||
else:
|
||||
print(f" {i}. VM {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})")
|
||||
|
||||
if ct_groups:
|
||||
print("\n\033[1;36m=== CT 백업 ===\033[0m")
|
||||
for i, group in enumerate(ct_groups, 1):
|
||||
backup_id = group.get("backup-id", "N/A")
|
||||
backup_count = group.get("backup-count", 0)
|
||||
last_backup = group.get("last-backup", 0)
|
||||
|
||||
# groups에서 comment 가져오기 (없으면 스냅샷에서 조회)
|
||||
comment = group.get("comment", "")
|
||||
if not comment:
|
||||
comment = get_snapshot_comment("ct", backup_id)
|
||||
|
||||
from datetime import datetime
|
||||
if last_backup > 0:
|
||||
last_str = datetime.fromtimestamp(last_backup).strftime("%Y-%m-%d %H:%M")
|
||||
else:
|
||||
last_str = "N/A"
|
||||
|
||||
if comment:
|
||||
print(f" {i}. CT {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})")
|
||||
print(f" \033[0;90m└─ {comment}\033[0m")
|
||||
else:
|
||||
print(f" {i}. CT {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})")
|
||||
|
||||
print("")
|
||||
|
||||
except Exception as e:
|
||||
print(f"오류: {e}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
|
||||
# 백업 선택
|
||||
echo ""
|
||||
while true; do
|
||||
read -p "$(echo -e ${CYAN}백업 타입${NC}) (vm/ct): " BACKUP_TYPE < /dev/tty
|
||||
if [[ "$BACKUP_TYPE" =~ ^(vm|ct)$ ]]; then
|
||||
break
|
||||
fi
|
||||
log_error "vm 또는 ct를 입력하세요"
|
||||
done
|
||||
|
||||
read -p "$(echo -e ${CYAN}백업 ID${NC}): " TEMPLATE_VMID < /dev/tty
|
||||
if [ -z "$TEMPLATE_VMID" ]; then
|
||||
log_error "백업 ID는 필수입니다"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 최신 스냅샷 찾기
|
||||
log_info "최신 백업 조회 중..."
|
||||
if get_latest_snapshot "$BACKUP_TYPE" "$TEMPLATE_VMID"; then
|
||||
log_success "최신 백업: ${LATEST_SNAPSHOT}"
|
||||
else
|
||||
log_error "${BACKUP_TYPE} ${TEMPLATE_VMID}의 백업을 찾을 수 없습니다"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 최신 스냅샷 조회
|
||||
get_latest_snapshot() {
|
||||
local backup_type="$1"
|
||||
local backup_id="$2"
|
||||
|
||||
curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/snapshots?backup-type=${backup_type}&backup-id=${backup_id}&ns=PQ" \
|
||||
-H "Cookie: PBSAuthCookie=${PBS_TICKET}" \
|
||||
-H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_snapshots.json
|
||||
|
||||
LATEST_SNAPSHOT=$(python3 << 'PYEOF'
|
||||
import sys, json
|
||||
try:
|
||||
with open('/tmp/pbs_snapshots.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
snapshots = data.get("data", [])
|
||||
|
||||
if not snapshots:
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
# 가장 최근 백업 선택
|
||||
latest = max(snapshots, key=lambda x: x.get("backup-time", 0))
|
||||
backup_time = latest.get("backup-time", 0)
|
||||
|
||||
from datetime import datetime
|
||||
backup_time_str = datetime.utcfromtimestamp(backup_time).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
print(backup_time_str)
|
||||
|
||||
except Exception as e:
|
||||
print("", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
|
||||
if [ -z "$LATEST_SNAPSHOT" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 사용 가능한 스토리지 목록 표시
|
||||
list_available_storage() {
|
||||
echo ""
|
||||
log_step "사용 가능한 스토리지 목록"
|
||||
echo ""
|
||||
|
||||
# VM/CT 이미지를 저장할 수 있는 스토리지만 필터링
|
||||
pvesm status -content images,rootdir 2>/dev/null | tail -n +2 | while read -r line; do
|
||||
storage_name=$(echo "$line" | awk '{print $1}')
|
||||
storage_type=$(echo "$line" | awk '{print $2}')
|
||||
total_mb=$(echo "$line" | awk '{print $4}')
|
||||
used_mb=$(echo "$line" | awk '{print $5}')
|
||||
avail_mb=$(echo "$line" | awk '{print $6}')
|
||||
usage_pct=$(echo "$line" | awk '{print $7}')
|
||||
|
||||
# MB를 GB로 변환
|
||||
total_gb=$(echo "scale=2; $total_mb / 1024 / 1024" | bc)
|
||||
avail_gb=$(echo "scale=2; $avail_mb / 1024 / 1024" | bc)
|
||||
|
||||
echo -e " ${GREEN}●${NC} ${CYAN}${storage_name}${NC}"
|
||||
echo -e " 타입: ${storage_type} | 용량: ${total_gb}GB | 여유: ${avail_gb}GB | 사용률: ${usage_pct}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 복구 설정 입력
|
||||
get_restore_config() {
|
||||
echo ""
|
||||
log_step "복구 설정을 입력하세요"
|
||||
echo ""
|
||||
|
||||
# 복구할 VM/CT ID
|
||||
read -p "$(echo -e ${CYAN}복구할 VM/CT ID${NC}) [기본값: ${TEMPLATE_VMID}]: " TARGET_VMID < /dev/tty
|
||||
TARGET_VMID=${TARGET_VMID:-$TEMPLATE_VMID}
|
||||
|
||||
# 사용 가능한 스토리지 목록 표시
|
||||
list_available_storage
|
||||
|
||||
# 스토리지 선택
|
||||
while true; do
|
||||
read -p "$(echo -e ${CYAN}저장 스토리지 이름${NC}) [기본값: local-lvm]: " input_storage < /dev/tty
|
||||
TARGET_STORAGE=${input_storage:-local-lvm}
|
||||
|
||||
# 입력한 스토리지가 존재하는지 확인
|
||||
if pvesm status -storage "${TARGET_STORAGE}" &>/dev/null; then
|
||||
# VM/CT 이미지를 저장할 수 있는지 확인
|
||||
if pvesm status -storage "${TARGET_STORAGE}" -content images &>/dev/null || \
|
||||
pvesm status -storage "${TARGET_STORAGE}" -content rootdir &>/dev/null; then
|
||||
log_success "스토리지 '${TARGET_STORAGE}' 선택됨"
|
||||
break
|
||||
else
|
||||
log_error "스토리지 '${TARGET_STORAGE}'는 VM/CT 이미지를 저장할 수 없습니다"
|
||||
fi
|
||||
else
|
||||
log_error "스토리지 '${TARGET_STORAGE}'를 찾을 수 없습니다"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo ""
|
||||
log_info "복구 요약:"
|
||||
echo " 백업 소스: ${BACKUP_TYPE}/${TEMPLATE_VMID}"
|
||||
echo " 백업 시점: ${LATEST_SNAPSHOT}"
|
||||
echo " 복구 ID: ${TARGET_VMID}"
|
||||
echo " 저장 위치: ${TARGET_STORAGE}"
|
||||
echo ""
|
||||
|
||||
read -p "$(echo -e ${YELLOW}이 설정으로 복구하시겠습니까?${NC}) (y/N): " confirm < /dev/tty
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
log_info "작업 취소됨"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# VM/CT 복구
|
||||
restore_backup() {
|
||||
log_step "백업 복구 중... (시간이 걸릴 수 있습니다)"
|
||||
echo ""
|
||||
|
||||
# 백업 경로 구성
|
||||
BACKUP_PATH="${PBS_STORAGE_NAME}:backup/${BACKUP_TYPE}/${TEMPLATE_VMID}/${LATEST_SNAPSHOT}"
|
||||
|
||||
log_info "백업 경로: ${BACKUP_PATH}"
|
||||
|
||||
# 기존 VM/CT 확인
|
||||
if qm status ${TARGET_VMID} 2>/dev/null || pct status ${TARGET_VMID} 2>/dev/null; then
|
||||
log_warning "VM/CT ${TARGET_VMID}가 이미 존재합니다"
|
||||
read -p "$(echo -e ${YELLOW}삭제하고 복구하시겠습니까?${NC}) (y/N): " delete_confirm < /dev/tty
|
||||
|
||||
if [[ "$delete_confirm" =~ ^[Yy]$ ]]; then
|
||||
log_info "기존 VM/CT 삭제 중..."
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
qm stop ${TARGET_VMID} --skiplock || true
|
||||
sleep 2
|
||||
qm destroy ${TARGET_VMID} --purge --skiplock || true
|
||||
else
|
||||
pct stop ${TARGET_VMID} || true
|
||||
sleep 2
|
||||
pct destroy ${TARGET_VMID} --purge || true
|
||||
fi
|
||||
sleep 2
|
||||
log_success "기존 VM/CT 삭제 완료"
|
||||
else
|
||||
log_error "복구 취소됨"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 복구 실행
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
log_info "VM 복구 시작..."
|
||||
echo ""
|
||||
|
||||
qmrestore "${BACKUP_PATH}" ${TARGET_VMID} \
|
||||
--storage ${TARGET_STORAGE} \
|
||||
--unique 1 2>&1 | tee /tmp/pbs-restore.log
|
||||
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
echo ""
|
||||
log_success "VM 복구 완료!"
|
||||
else
|
||||
echo ""
|
||||
log_error "VM 복구 실패. 로그: /tmp/pbs-restore.log"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_info "CT 복구 시작..."
|
||||
echo ""
|
||||
|
||||
pct restore ${TARGET_VMID} "${BACKUP_PATH}" \
|
||||
--storage ${TARGET_STORAGE} \
|
||||
--unprivileged 1 2>&1 | tee /tmp/pbs-restore.log
|
||||
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
echo ""
|
||||
log_success "CT 복구 완료!"
|
||||
else
|
||||
echo ""
|
||||
log_error "CT 복구 실패. 로그: /tmp/pbs-restore.log"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# VM/CT 시작
|
||||
start_vm() {
|
||||
echo ""
|
||||
read -p "$(echo -e ${YELLOW}VM/CT를 바로 시작하시겠습니까?${NC}) (y/N): " start_confirm < /dev/tty
|
||||
|
||||
if [[ "$start_confirm" =~ ^[Yy]$ ]]; then
|
||||
log_info "VM/CT 시작 중..."
|
||||
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
qm start ${TARGET_VMID}
|
||||
else
|
||||
pct start ${TARGET_VMID}
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_success "VM/CT 시작 완료!"
|
||||
|
||||
sleep 3
|
||||
log_info "상태 확인 중..."
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
qm status ${TARGET_VMID}
|
||||
else
|
||||
pct status ${TARGET_VMID}
|
||||
fi
|
||||
else
|
||||
log_error "시작 실패"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_info "수동으로 시작하세요:"
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
echo " qm start ${TARGET_VMID}"
|
||||
else
|
||||
echo " pct start ${TARGET_VMID}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 완료 메시지
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo -e "${GREEN}"
|
||||
cat << "EOF"
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ ✅ 복구 완료! ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
|
||||
log_info "복구 정보:"
|
||||
echo " 백업 소스: ${BACKUP_TYPE}/${TEMPLATE_VMID}"
|
||||
echo " 백업 시점: ${LATEST_SNAPSHOT}"
|
||||
echo " 복구 ID: ${TARGET_VMID}"
|
||||
echo " 스토리지: ${TARGET_STORAGE}"
|
||||
echo ""
|
||||
|
||||
log_info "Proxmox Web UI:"
|
||||
local pve_ip=$(hostname -I | awk '{print $1}')
|
||||
echo " https://${pve_ip}:8006"
|
||||
echo ""
|
||||
|
||||
if [ "$BACKUP_TYPE" = "vm" ]; then
|
||||
log_info "유용한 명령어:"
|
||||
echo " VM 상태: qm status ${TARGET_VMID}"
|
||||
echo " VM 시작: qm start ${TARGET_VMID}"
|
||||
echo " VM 중지: qm stop ${TARGET_VMID}"
|
||||
echo " VM 정보: qm config ${TARGET_VMID}"
|
||||
else
|
||||
log_info "유용한 명령어:"
|
||||
echo " CT 상태: pct status ${TARGET_VMID}"
|
||||
echo " CT 시작: pct start ${TARGET_VMID}"
|
||||
echo " CT 중지: pct stop ${TARGET_VMID}"
|
||||
echo " CT 접속: pct enter ${TARGET_VMID}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
log_info "PBS 스토리지:"
|
||||
echo " 이름: ${PBS_STORAGE_NAME}"
|
||||
echo " 제거: pvesm remove ${PBS_STORAGE_NAME}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
print_banner
|
||||
|
||||
# Proxmox 환경 확인
|
||||
check_proxmox
|
||||
echo ""
|
||||
|
||||
# PBS 스토리지 등록
|
||||
register_pbs || exit 1
|
||||
echo ""
|
||||
|
||||
# PBS API 인증
|
||||
pbs_login || exit 1
|
||||
echo ""
|
||||
|
||||
# 백업 목록 조회 및 선택
|
||||
list_and_select_backup
|
||||
echo ""
|
||||
|
||||
# 복구 설정 입력
|
||||
get_restore_config
|
||||
echo ""
|
||||
|
||||
# 백업 복구
|
||||
restore_backup || exit 1
|
||||
echo ""
|
||||
|
||||
# VM/CT 시작
|
||||
start_vm
|
||||
echo ""
|
||||
|
||||
# 완료 메시지
|
||||
print_summary
|
||||
}
|
||||
|
||||
# 도움말
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "PBS 올인원 스크립트 - Proxmox 호스트에서 실행"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "사용법: $0"
|
||||
echo ""
|
||||
echo "이 스크립트는:"
|
||||
echo " 1. PBS 스토리지를 자동 등록"
|
||||
echo " 2. API로 백업 목록 조회"
|
||||
echo " 3. 선택한 백업을 복구"
|
||||
echo ""
|
||||
echo "실행 예시:"
|
||||
echo " bash $0"
|
||||
echo ""
|
||||
echo "사전 준비:"
|
||||
echo " - Proxmox VE 설치 완료"
|
||||
echo " - PBS 서버 (100.64.0.10) 접근 가능"
|
||||
echo " - python3 설치 (보통 기본 설치됨)"
|
||||
echo ""
|
||||
echo "모든 설정(IP, 비밀번호 등)은 자동 입력됩니다!"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Root 권한 확인
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "이 스크립트는 root 권한이 필요합니다."
|
||||
log_info "다음 명령으로 실행하세요: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 스크립트 실행
|
||||
main
|
||||
471
pbs_auto_restore_103_to_203.sh
Executable file
471
pbs_auto_restore_103_to_203.sh
Executable file
@@ -0,0 +1,471 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# PBS 자동 복구 스크립트 - VM 103 → 203
|
||||
# 작성일: 2025-11-21
|
||||
# 용도: PBS 등록 → VM 103 백업 자동 복구 → VM 203으로 설치
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 색상
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m'
|
||||
|
||||
# PBS 서버 설정 (자동 입력)
|
||||
PBS_SERVER="100.64.0.10"
|
||||
PBS_PORT="8007"
|
||||
PBS_USERNAME="0bin@pbs"
|
||||
PBS_PASSWORD="@Trajet6640"
|
||||
PBS_DATASTORE="PBS-DVA"
|
||||
PBS_FINGERPRINT="24:42:c6:0f:a8:1b:93:32:32:44:84:be:6a:c5:71:97:e4:4d:61:fc:a4:48:12:0c:97:3b:9f:1f:cc:b2:54:e8"
|
||||
PBS_STORAGE_NAME="PBS-Auto"
|
||||
|
||||
# 고정 복구 설정
|
||||
SOURCE_VMID="103" # 백업 소스 VM ID
|
||||
TARGET_VMID="203" # 복구할 VM ID
|
||||
TARGET_STORAGE="local-lvm" # 저장 스토리지
|
||||
BACKUP_TYPE="vm" # 백업 타입 (vm 고정)
|
||||
LATEST_SNAPSHOT="" # 최신 스냅샷 (자동 조회)
|
||||
|
||||
# 로그 함수
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_step() { echo -e "${CYAN}╰─► ${NC}$1"; }
|
||||
|
||||
# 배너
|
||||
print_banner() {
|
||||
clear
|
||||
echo -e "${MAGENTA}"
|
||||
cat << "EOF"
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ PBS 자동 복구 스크립트 ║
|
||||
║ VM 103 → VM 203 자동 복구 ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Proxmox 환경 확인
|
||||
check_proxmox() {
|
||||
if [ ! -f /etc/pve/storage.cfg ]; then
|
||||
log_error "Proxmox VE가 설치되어 있지 않습니다."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Proxmox VE 환경 확인 완료"
|
||||
}
|
||||
|
||||
# PBS 스토리지 등록
|
||||
register_pbs() {
|
||||
log_step "PBS 스토리지 등록 중..."
|
||||
|
||||
# 이미 등록되어 있는지 확인
|
||||
if pvesm status | grep -q "^${PBS_STORAGE_NAME} "; then
|
||||
log_warning "PBS 스토리지가 이미 등록되어 있습니다."
|
||||
log_info "기존 PBS 스토리지 제거 중..."
|
||||
pvesm remove "${PBS_STORAGE_NAME}" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# PBS 등록
|
||||
log_info "PBS 서버 등록 중..."
|
||||
pvesm add pbs "${PBS_STORAGE_NAME}" \
|
||||
--server "${PBS_SERVER}" \
|
||||
--port "${PBS_PORT}" \
|
||||
--datastore "${PBS_DATASTORE}" \
|
||||
--username "${PBS_USERNAME}" \
|
||||
--password "${PBS_PASSWORD}" \
|
||||
--fingerprint "${PBS_FINGERPRINT}" \
|
||||
--namespace "PQ" 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_success "PBS 스토리지 등록 완료!"
|
||||
else
|
||||
log_error "PBS 스토리지 등록 실패"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 연결 테스트
|
||||
log_info "PBS 연결 테스트 중..."
|
||||
if pvesm status -storage "${PBS_STORAGE_NAME}" &>/dev/null; then
|
||||
log_success "PBS 연결 성공!"
|
||||
echo ""
|
||||
pvesm status -storage "${PBS_STORAGE_NAME}"
|
||||
echo ""
|
||||
else
|
||||
log_error "PBS 연결 테스트 실패"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# PBS API 인증
|
||||
pbs_login() {
|
||||
log_info "PBS API 인증 중..."
|
||||
|
||||
curl -k -s -X POST "https://${PBS_SERVER}:${PBS_PORT}/api2/json/access/ticket" \
|
||||
-d "username=${PBS_USERNAME}" \
|
||||
-d "password=${PBS_PASSWORD}" > /tmp/pbs_auth.json
|
||||
|
||||
if [ ! -s /tmp/pbs_auth.json ]; then
|
||||
log_error "PBS API 응답 없음. 서버 연결을 확인하세요."
|
||||
return 1
|
||||
fi
|
||||
|
||||
PBS_TICKET=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["ticket"])' 2>/dev/null)
|
||||
PBS_CSRF=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["CSRFPreventionToken"])' 2>/dev/null)
|
||||
|
||||
if [ -z "$PBS_TICKET" ]; then
|
||||
log_error "PBS API 인증 실패. 응답:"
|
||||
cat /tmp/pbs_auth.json | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "PBS API 인증 완료"
|
||||
}
|
||||
|
||||
# VM 103 백업 확인
|
||||
check_backup_exists() {
|
||||
log_step "VM ${SOURCE_VMID} 백업 확인 중..."
|
||||
|
||||
curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/groups?ns=PQ" \
|
||||
-H "Cookie: PBSAuthCookie=${PBS_TICKET}" \
|
||||
-H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_groups.json
|
||||
|
||||
# VM 103 백업이 있는지 확인
|
||||
local has_backup=$(python3 << PYEOF
|
||||
import sys, json
|
||||
try:
|
||||
with open('/tmp/pbs_groups.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
groups = data.get("data", [])
|
||||
|
||||
for group in groups:
|
||||
if group.get("backup-type") == "vm" and group.get("backup-id") == "${SOURCE_VMID}":
|
||||
backup_count = group.get("backup-count", 0)
|
||||
last_backup = group.get("last-backup", 0)
|
||||
|
||||
from datetime import datetime
|
||||
if last_backup > 0:
|
||||
last_str = datetime.fromtimestamp(last_backup).strftime("%Y-%m-%d %H:%M")
|
||||
else:
|
||||
last_str = "N/A"
|
||||
|
||||
print(f"VM {group.get('backup-id')} - {backup_count}개 백업 (최근: {last_str})")
|
||||
sys.exit(0)
|
||||
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print("")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_success "백업 발견: ${has_backup}"
|
||||
return 0
|
||||
else
|
||||
log_error "VM ${SOURCE_VMID} 백업을 찾을 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 최신 스냅샷 조회
|
||||
get_latest_snapshot() {
|
||||
log_info "최신 백업 조회 중..."
|
||||
|
||||
curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/snapshots?backup-type=${BACKUP_TYPE}&backup-id=${SOURCE_VMID}&ns=PQ" \
|
||||
-H "Cookie: PBSAuthCookie=${PBS_TICKET}" \
|
||||
-H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_snapshots.json
|
||||
|
||||
LATEST_SNAPSHOT=$(python3 << 'PYEOF'
|
||||
import sys, json
|
||||
try:
|
||||
with open('/tmp/pbs_snapshots.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
snapshots = data.get("data", [])
|
||||
|
||||
if not snapshots:
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
# 가장 최근 백업 선택
|
||||
latest = max(snapshots, key=lambda x: x.get("backup-time", 0))
|
||||
backup_time = latest.get("backup-time", 0)
|
||||
|
||||
from datetime import datetime
|
||||
backup_time_str = datetime.utcfromtimestamp(backup_time).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
print(backup_time_str)
|
||||
|
||||
except Exception as e:
|
||||
print("", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
|
||||
if [ -z "$LATEST_SNAPSHOT" ]; then
|
||||
log_error "최신 백업을 찾을 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "최신 백업: ${LATEST_SNAPSHOT}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 스토리지 확인
|
||||
check_storage() {
|
||||
log_step "저장 스토리지 확인 중..."
|
||||
|
||||
if pvesm status -storage "${TARGET_STORAGE}" &>/dev/null; then
|
||||
# VM 이미지를 저장할 수 있는지 확인
|
||||
if pvesm status -storage "${TARGET_STORAGE}" -content images &>/dev/null; then
|
||||
log_success "스토리지 '${TARGET_STORAGE}' 확인 완료"
|
||||
|
||||
# 스토리지 정보 표시
|
||||
local storage_info=$(pvesm status -storage "${TARGET_STORAGE}" 2>/dev/null | tail -n +2)
|
||||
local total_mb=$(echo "$storage_info" | awk '{print $4}')
|
||||
local avail_mb=$(echo "$storage_info" | awk '{print $6}')
|
||||
local usage_pct=$(echo "$storage_info" | awk '{print $7}')
|
||||
|
||||
if [ -n "$total_mb" ]; then
|
||||
local total_gb=$(echo "scale=2; $total_mb / 1024 / 1024" | bc 2>/dev/null || echo "N/A")
|
||||
local avail_gb=$(echo "scale=2; $avail_mb / 1024 / 1024" | bc 2>/dev/null || echo "N/A")
|
||||
log_info "스토리지 정보: ${total_gb}GB 총용량 | ${avail_gb}GB 여유 | ${usage_pct} 사용률"
|
||||
fi
|
||||
|
||||
return 0
|
||||
else
|
||||
log_error "스토리지 '${TARGET_STORAGE}'는 VM 이미지를 저장할 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "스토리지 '${TARGET_STORAGE}'를 찾을 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 복구 설정 확인
|
||||
show_restore_config() {
|
||||
echo ""
|
||||
log_info "복구 설정:"
|
||||
echo " 백업 소스: VM ${SOURCE_VMID}"
|
||||
echo " 백업 시점: ${LATEST_SNAPSHOT}"
|
||||
echo " 복구 ID: VM ${TARGET_VMID}"
|
||||
echo " 저장 위치: ${TARGET_STORAGE}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# VM 복구
|
||||
restore_backup() {
|
||||
log_step "백업 복구 중... (시간이 걸릴 수 있습니다)"
|
||||
echo ""
|
||||
|
||||
# 백업 경로 구성
|
||||
BACKUP_PATH="${PBS_STORAGE_NAME}:backup/${BACKUP_TYPE}/${SOURCE_VMID}/${LATEST_SNAPSHOT}"
|
||||
|
||||
log_info "백업 경로: ${BACKUP_PATH}"
|
||||
|
||||
# 기존 VM 확인
|
||||
if qm status ${TARGET_VMID} 2>/dev/null; then
|
||||
log_warning "VM ${TARGET_VMID}가 이미 존재합니다"
|
||||
log_info "기존 VM 삭제 중..."
|
||||
|
||||
qm stop ${TARGET_VMID} --skiplock 2>/dev/null || true
|
||||
sleep 2
|
||||
qm destroy ${TARGET_VMID} --purge --skiplock 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
log_success "기존 VM 삭제 완료"
|
||||
fi
|
||||
|
||||
# 복구 실행
|
||||
log_info "VM 복구 시작..."
|
||||
echo ""
|
||||
|
||||
qmrestore "${BACKUP_PATH}" ${TARGET_VMID} \
|
||||
--storage ${TARGET_STORAGE} \
|
||||
--unique 1 2>&1 | tee /tmp/pbs-restore.log
|
||||
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
echo ""
|
||||
log_success "VM 복구 완료!"
|
||||
return 0
|
||||
else
|
||||
echo ""
|
||||
log_error "VM 복구 실패. 로그: /tmp/pbs-restore.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# VM 자동 시작 (선택)
|
||||
start_vm() {
|
||||
echo ""
|
||||
log_info "VM ${TARGET_VMID}를 바로 시작하시겠습니까?"
|
||||
echo -e "${YELLOW}10초 내 입력하세요 (y/N):${NC} "
|
||||
|
||||
if read -t 10 start_confirm 2>/dev/null; then
|
||||
:
|
||||
else
|
||||
start_confirm="N"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ "$start_confirm" =~ ^[Yy]$ ]]; then
|
||||
log_info "VM 시작 중..."
|
||||
|
||||
qm start ${TARGET_VMID}
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_success "VM 시작 완료!"
|
||||
|
||||
sleep 3
|
||||
log_info "상태 확인 중..."
|
||||
qm status ${TARGET_VMID}
|
||||
else
|
||||
log_error "시작 실패"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_info "VM을 수동으로 시작하세요: qm start ${TARGET_VMID}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 완료 메시지
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo -e "${GREEN}"
|
||||
cat << "EOF"
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ ✅ 복구 완료! ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
|
||||
log_info "복구 정보:"
|
||||
echo " 백업 소스: VM ${SOURCE_VMID}"
|
||||
echo " 백업 시점: ${LATEST_SNAPSHOT}"
|
||||
echo " 복구 ID: VM ${TARGET_VMID}"
|
||||
echo " 스토리지: ${TARGET_STORAGE}"
|
||||
echo ""
|
||||
|
||||
log_info "Proxmox Web UI:"
|
||||
local pve_ip=$(hostname -I | awk '{print $1}')
|
||||
echo " https://${pve_ip}:8006"
|
||||
echo ""
|
||||
|
||||
log_info "유용한 명령어:"
|
||||
echo " VM 상태: qm status ${TARGET_VMID}"
|
||||
echo " VM 시작: qm start ${TARGET_VMID}"
|
||||
echo " VM 중지: qm stop ${TARGET_VMID}"
|
||||
echo " VM 정보: qm config ${TARGET_VMID}"
|
||||
echo ""
|
||||
|
||||
log_info "PBS 스토리지:"
|
||||
echo " 이름: ${PBS_STORAGE_NAME}"
|
||||
echo " 제거: pvesm remove ${PBS_STORAGE_NAME}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
print_banner
|
||||
|
||||
# Proxmox 환경 확인
|
||||
check_proxmox
|
||||
echo ""
|
||||
|
||||
# PBS 스토리지 등록
|
||||
register_pbs || exit 1
|
||||
echo ""
|
||||
|
||||
# PBS API 인증
|
||||
pbs_login || exit 1
|
||||
echo ""
|
||||
|
||||
# VM 103 백업 확인
|
||||
check_backup_exists || exit 1
|
||||
echo ""
|
||||
|
||||
# 최신 스냅샷 조회
|
||||
get_latest_snapshot || exit 1
|
||||
echo ""
|
||||
|
||||
# 스토리지 확인
|
||||
check_storage || exit 1
|
||||
echo ""
|
||||
|
||||
# 복구 설정 표시
|
||||
show_restore_config
|
||||
|
||||
# 자동 복구 시작 (3초 대기)
|
||||
log_warning "3초 후 자동으로 복구를 시작합니다..."
|
||||
sleep 3
|
||||
echo ""
|
||||
|
||||
# 백업 복구
|
||||
restore_backup || exit 1
|
||||
echo ""
|
||||
|
||||
# VM 시작 (선택)
|
||||
start_vm
|
||||
echo ""
|
||||
|
||||
# 완료 메시지
|
||||
print_summary
|
||||
}
|
||||
|
||||
# 도움말
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "PBS 자동 복구 스크립트 - VM 103 → VM 203"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "사용법: $0"
|
||||
echo ""
|
||||
echo "이 스크립트는:"
|
||||
echo " 1. PBS 스토리지를 자동 등록"
|
||||
echo " 2. VM 103 백업 자동 조회"
|
||||
echo " 3. VM 203으로 자동 복구"
|
||||
echo ""
|
||||
echo "고정 설정:"
|
||||
echo " 소스 VM ID: 103"
|
||||
echo " 복구 VM ID: 203"
|
||||
echo " 스토리지: local-lvm"
|
||||
echo " PBS 서버: 100.64.0.10"
|
||||
echo ""
|
||||
echo "실행 예시:"
|
||||
echo " bash $0"
|
||||
echo ""
|
||||
echo "사전 준비:"
|
||||
echo " - Proxmox VE 설치 완료"
|
||||
echo " - PBS 서버 (100.64.0.10) 접근 가능"
|
||||
echo " - python3 설치 (보통 기본 설치됨)"
|
||||
echo ""
|
||||
echo "모든 설정이 자동화되어 있어 사용자 입력이 최소화됩니다!"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Root 권한 확인
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "이 스크립트는 root 권한이 필요합니다."
|
||||
log_info "다음 명령으로 실행하세요: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 스크립트 실행
|
||||
main
|
||||
1222
pharmq-setup-v2.sh
Normal file
1222
pharmq-setup-v2.sh
Normal file
File diff suppressed because it is too large
Load Diff
1247
pharmq-setup.sh
Normal file
1247
pharmq-setup.sh
Normal file
File diff suppressed because it is too large
Load Diff
383
pve-host-changer.sh
Normal file
383
pve-host-changer.sh
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Proxmox VE 호스트명/FQDN 수정 스크립트
|
||||
# 작성일: 2025-11-14
|
||||
# 용도: Proxmox 설치 후 안전하게 호스트명 및 FQDN 변경
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 색상 정의
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 설정 변수
|
||||
NEW_HOSTNAME=""
|
||||
NEW_DOMAIN=""
|
||||
NEW_FQDN=""
|
||||
HOST_IP=""
|
||||
|
||||
# 로그 함수
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}╰─► ${NC}$1"
|
||||
}
|
||||
|
||||
# 배너 출력
|
||||
print_banner() {
|
||||
clear
|
||||
echo -e "${MAGENTA}"
|
||||
cat << "EOF"
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Proxmox VE 호스트명/FQDN 변경 스크립트 ║
|
||||
║ 안전하게 호스트명을 변경합니다 ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Root 권한 확인
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "이 스크립트는 root 권한이 필요합니다."
|
||||
log_info "다음 명령으로 실행하세요: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Proxmox VE 확인
|
||||
check_proxmox() {
|
||||
if [ ! -f /etc/pve/storage.cfg ]; then
|
||||
log_error "Proxmox VE가 설치되어 있지 않습니다."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Proxmox VE 확인 완료"
|
||||
}
|
||||
|
||||
# 현재 설정 표시
|
||||
show_current_config() {
|
||||
log_info "현재 호스트명 설정:"
|
||||
echo ""
|
||||
echo " FQDN: $(hostname -f)"
|
||||
echo " 호스트명: $(hostname)"
|
||||
echo " IP 주소: $(hostname -I | awk '{print $1}')"
|
||||
echo ""
|
||||
|
||||
log_info "현재 /etc/hostname 내용:"
|
||||
cat /etc/hostname | sed 's/^/ /'
|
||||
echo ""
|
||||
|
||||
log_info "현재 /etc/hosts 내용:"
|
||||
cat /etc/hosts | sed 's/^/ /'
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 사용자 입력 받기
|
||||
get_user_input() {
|
||||
echo ""
|
||||
log_info "새로운 호스트명 정보를 입력하세요."
|
||||
echo ""
|
||||
|
||||
# 새 호스트명 입력
|
||||
while true; do
|
||||
read -p "$(echo -e ${CYAN}새 호스트명${NC}) (예: p0001-pve): " NEW_HOSTNAME
|
||||
if [ -n "$NEW_HOSTNAME" ]; then
|
||||
# 호스트명 검증 (알파벳, 숫자, 하이픈만 허용)
|
||||
if [[ "$NEW_HOSTNAME" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
||||
break
|
||||
else
|
||||
log_error "호스트명은 알파벳, 숫자, 하이픈(-)만 사용 가능합니다."
|
||||
fi
|
||||
else
|
||||
log_error "호스트명은 필수입니다."
|
||||
fi
|
||||
done
|
||||
|
||||
# 도메인 입력
|
||||
while true; do
|
||||
read -p "$(echo -e ${CYAN}도메인${NC}) (예: pharmq.kr): " NEW_DOMAIN
|
||||
if [ -n "$NEW_DOMAIN" ]; then
|
||||
# 도메인 검증 (기본적인 형식 체크)
|
||||
if [[ "$NEW_DOMAIN" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||
break
|
||||
else
|
||||
log_error "올바른 도메인 형식이 아닙니다. (예: example.com)"
|
||||
fi
|
||||
else
|
||||
log_error "도메인은 필수입니다."
|
||||
fi
|
||||
done
|
||||
|
||||
# FQDN 구성
|
||||
NEW_FQDN="${NEW_HOSTNAME}.${NEW_DOMAIN}"
|
||||
|
||||
# IP 주소 자동 감지
|
||||
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||
log_info "감지된 IP 주소: ${HOST_IP}"
|
||||
|
||||
read -p "$(echo -e ${CYAN}IP 주소 변경${NC}) [엔터로 ${HOST_IP} 사용]: " input_ip
|
||||
if [ -n "$input_ip" ]; then
|
||||
HOST_IP="$input_ip"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "변경 요약:"
|
||||
echo " 현재 FQDN: $(hostname -f)"
|
||||
echo " 새 FQDN: ${NEW_FQDN}"
|
||||
echo " 현재 호스트명: $(hostname)"
|
||||
echo " 새 호스트명: ${NEW_HOSTNAME}"
|
||||
echo " IP 주소: ${HOST_IP}"
|
||||
echo ""
|
||||
|
||||
log_warning "⚠️ 주의사항:"
|
||||
echo " 1. 클러스터 구성된 노드는 신중하게 변경하세요"
|
||||
echo " 2. DNS에 ${NEW_FQDN} → ${HOST_IP} 레코드 등록 권장"
|
||||
echo " 3. 인증서가 자동으로 재발급됩니다"
|
||||
echo ""
|
||||
|
||||
read -p "$(echo -e ${YELLOW}이 설정으로 변경하시겠습니까?${NC}) (y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
log_info "작업 취소됨"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 백업 생성
|
||||
create_backup() {
|
||||
log_step "현재 설정 백업 중..."
|
||||
|
||||
BACKUP_DIR="/root/hostname_backup_$(date +%Y%m%d_%H%M%S)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
cp /etc/hostname "$BACKUP_DIR/hostname.bak"
|
||||
cp /etc/hosts "$BACKUP_DIR/hosts.bak"
|
||||
|
||||
log_success "백업 완료: ${BACKUP_DIR}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# /etc/hostname 수정
|
||||
update_hostname_file() {
|
||||
log_step "/etc/hostname 수정 중..."
|
||||
|
||||
echo "$NEW_HOSTNAME" > /etc/hostname
|
||||
|
||||
log_success "/etc/hostname 수정 완료"
|
||||
}
|
||||
|
||||
# /etc/hosts 수정
|
||||
update_hosts_file() {
|
||||
log_step "/etc/hosts 수정 중..."
|
||||
|
||||
# 기존 127.0.1.1 라인 제거 및 새 설정 추가
|
||||
cat > /etc/hosts << EOF
|
||||
127.0.0.1 localhost.localdomain localhost
|
||||
${HOST_IP} ${NEW_FQDN} ${NEW_HOSTNAME}
|
||||
|
||||
# The following lines are desirable for IPv6 capable hosts
|
||||
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00::0 ip6-localnet
|
||||
ff00::0 ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
ff02::3 ip6-allhosts
|
||||
EOF
|
||||
|
||||
log_success "/etc/hosts 수정 완료"
|
||||
}
|
||||
|
||||
# hostnamectl 적용
|
||||
apply_hostnamectl() {
|
||||
log_step "hostnamectl로 호스트명 적용 중..."
|
||||
|
||||
hostnamectl set-hostname "$NEW_HOSTNAME"
|
||||
|
||||
log_success "hostnamectl 적용 완료"
|
||||
}
|
||||
|
||||
# Proxmox 서비스 재시작
|
||||
restart_proxmox_services() {
|
||||
log_step "Proxmox 서비스 재시작 중..."
|
||||
|
||||
systemctl restart pveproxy
|
||||
systemctl restart pvedaemon
|
||||
|
||||
log_success "Proxmox 서비스 재시작 완료"
|
||||
}
|
||||
|
||||
# 인증서 재발급
|
||||
renew_certificate() {
|
||||
log_step "인증서 재발급 중..."
|
||||
echo ""
|
||||
|
||||
# 기존 인증서 백업
|
||||
if [ -f /etc/pve/local/pveproxy-ssl.pem ]; then
|
||||
cp /etc/pve/local/pveproxy-ssl.pem "$BACKUP_DIR/pveproxy-ssl.pem.bak" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f /etc/pve/local/pveproxy-ssl.key ]; then
|
||||
cp /etc/pve/local/pveproxy-ssl.key "$BACKUP_DIR/pveproxy-ssl.key.bak" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 인증서 재발급
|
||||
pvecm updatecerts -f 2>/dev/null || pvenode cert renew 2>&1
|
||||
|
||||
sleep 2
|
||||
systemctl restart pveproxy
|
||||
|
||||
echo ""
|
||||
log_success "인증서 재발급 완료"
|
||||
}
|
||||
|
||||
# 변경 사항 확인
|
||||
verify_changes() {
|
||||
log_step "변경 사항 확인 중..."
|
||||
echo ""
|
||||
|
||||
CURRENT_FQDN=$(hostname -f)
|
||||
CURRENT_HOSTNAME=$(hostname)
|
||||
|
||||
if [ "$CURRENT_FQDN" = "$NEW_FQDN" ] && [ "$CURRENT_HOSTNAME" = "$NEW_HOSTNAME" ]; then
|
||||
log_success "호스트명 변경 성공!"
|
||||
echo " FQDN: ${CURRENT_FQDN}"
|
||||
echo " 호스트명: ${CURRENT_HOSTNAME}"
|
||||
else
|
||||
log_warning "호스트명이 완전히 적용되지 않았습니다."
|
||||
echo " 예상 FQDN: ${NEW_FQDN}"
|
||||
echo " 현재 FQDN: ${CURRENT_FQDN}"
|
||||
echo " 예상 호스트명: ${NEW_HOSTNAME}"
|
||||
echo " 현재 호스트명: ${CURRENT_HOSTNAME}"
|
||||
echo ""
|
||||
log_info "시스템 재부팅 후 완전히 적용됩니다."
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 다음 단계 안내
|
||||
print_next_steps() {
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ 호스트명 변경 완료! ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
log_info "다음 단계:"
|
||||
echo ""
|
||||
echo "1. 시스템 재부팅 (권장):"
|
||||
echo " reboot"
|
||||
echo ""
|
||||
echo "2. DNS에 A 레코드 등록 (선택사항):"
|
||||
echo " ${NEW_FQDN} → ${HOST_IP}"
|
||||
echo ""
|
||||
echo "3. Proxmox Web UI 접속:"
|
||||
echo " https://${HOST_IP}:8006"
|
||||
echo " https://${NEW_FQDN}:8006"
|
||||
echo ""
|
||||
echo "4. 인증서 경고 발생 시:"
|
||||
echo " 브라우저에서 예외 추가 또는 Let's Encrypt 인증서 발급"
|
||||
echo " pvenode acme account register default mail@example.com"
|
||||
echo " pvenode acme cert order"
|
||||
echo ""
|
||||
echo "5. 설정 롤백 (필요시):"
|
||||
echo " cp ${BACKUP_DIR}/hostname.bak /etc/hostname"
|
||||
echo " cp ${BACKUP_DIR}/hosts.bak /etc/hosts"
|
||||
echo " reboot"
|
||||
echo ""
|
||||
log_info "백업 위치: ${BACKUP_DIR}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
print_banner
|
||||
|
||||
# 권한 확인
|
||||
check_root
|
||||
|
||||
# Proxmox 확인
|
||||
check_proxmox
|
||||
echo ""
|
||||
|
||||
# 현재 설정 표시
|
||||
show_current_config
|
||||
|
||||
# 사용자 입력
|
||||
get_user_input
|
||||
|
||||
# 백업 생성
|
||||
create_backup
|
||||
|
||||
# 호스트명 변경
|
||||
update_hostname_file
|
||||
update_hosts_file
|
||||
apply_hostnamectl
|
||||
|
||||
echo ""
|
||||
|
||||
# Proxmox 서비스 재시작
|
||||
restart_proxmox_services
|
||||
|
||||
echo ""
|
||||
|
||||
# 인증서 재발급
|
||||
renew_certificate
|
||||
|
||||
echo ""
|
||||
|
||||
# 변경 사항 확인
|
||||
verify_changes
|
||||
|
||||
# 다음 단계 안내
|
||||
print_next_steps
|
||||
}
|
||||
|
||||
# 도움말
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Proxmox VE 호스트명/FQDN 변경 스크립트"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "사용법: $0"
|
||||
echo ""
|
||||
echo "이 스크립트는:"
|
||||
echo " 1. 현재 호스트명/FQDN 설정 백업"
|
||||
echo " 2. /etc/hostname, /etc/hosts 수정"
|
||||
echo " 3. hostnamectl로 호스트명 적용"
|
||||
echo " 4. Proxmox 서비스 재시작"
|
||||
echo " 5. 인증서 자동 재발급"
|
||||
echo ""
|
||||
echo "실행 예시:"
|
||||
echo " bash $0"
|
||||
echo ""
|
||||
echo "주의사항:"
|
||||
echo " - 단독 노드(Single node)에서는 안전하게 변경 가능"
|
||||
echo " - 클러스터 구성 시 신중하게 진행 필요"
|
||||
echo " - 변경 후 시스템 재부팅 권장"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 스크립트 실행
|
||||
main
|
||||
Reference in New Issue
Block a user