feat: initial zioinfo-mail webmail system
This commit is contained in:
parent
3d5a125b04
commit
8ab660cd38
9
.claude/agents/_workspace/fix_report.json
Normal file
9
.claude/agents/_workspace/fix_report.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-06-01T21:10:57.257172",
|
||||||
|
"results": {
|
||||||
|
"guardia-itsm": "⚠️ ",
|
||||||
|
"guardia-manager": "⚠️ Modify: 2026-05-31 21:33:16.307249938 +0900",
|
||||||
|
"zioinfo-web-stash": "⚠️ stash 보존 (중요 파일 포함) - 수동 확인 필요",
|
||||||
|
"zioinfo-web": "✅ 28개 파일 반영"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.claude/agents/_workspace/infra-check.json
Normal file
11
.claude/agents/_workspace/infra-check.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"port_8025": "free",
|
||||||
|
"port_8026": "free",
|
||||||
|
"imap_login": "ok",
|
||||||
|
"smtp_login": "ok",
|
||||||
|
"gitea_repo": "not_found",
|
||||||
|
"ssl_cert": "/etc/ssl/guardia/server.crt\nexists",
|
||||||
|
"lets_encrypt": "/etc/letsencrypt/live/zioinfo.co.kr/fullchain.pem\nexists",
|
||||||
|
"aioimaplib": "",
|
||||||
|
"aiosmtplib": ""
|
||||||
|
}
|
||||||
52
.claude/agents/_workspace/infra_check.py
Normal file
52
.claude/agents/_workspace/infra_check.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Phase 1: IMAP/SMTP/포트 사전 검증"""
|
||||||
|
import paramiko, sys, json, socket, ssl, imaplib, smtplib, base64
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
def run(cmd, timeout=10):
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
return o.read().decode('utf-8','replace').strip()
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# 1. 포트 가용성
|
||||||
|
result['port_8025'] = 'free' if '8025' not in run('ss -tlnp') else 'in_use'
|
||||||
|
result['port_8026'] = 'free' if '8026' not in run('ss -tlnp') else 'in_use'
|
||||||
|
|
||||||
|
# 2. IMAP 테스트
|
||||||
|
imap_test = run('python3 -c "'
|
||||||
|
'import imaplib, ssl; ctx=ssl.create_default_context(); '
|
||||||
|
'ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; '
|
||||||
|
'M=imaplib.IMAP4_SSL(\'localhost\', 993, ssl_context=ctx); '
|
||||||
|
'print(M.login(\'ythong\', \'1q2w3e!Q\')); M.logout()"')
|
||||||
|
result['imap_login'] = 'ok' if 'OK' in imap_test else f'fail:{imap_test[:80]}'
|
||||||
|
|
||||||
|
# 3. SMTP 테스트
|
||||||
|
smtp_test = run('python3 -c "'
|
||||||
|
'import smtplib; s=smtplib.SMTP(\'localhost\', 587); '
|
||||||
|
's.ehlo(); s.starttls(); s.login(\'ythong\', \'1q2w3e!Q\'); '
|
||||||
|
'print(\'ok\'); s.quit()"')
|
||||||
|
result['smtp_login'] = 'ok' if 'ok' in smtp_test else f'fail:{smtp_test[:80]}'
|
||||||
|
|
||||||
|
# 4. Gitea repo 확인
|
||||||
|
gitea = run(f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail" '
|
||||||
|
f'-H "Authorization: Basic {G}" 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\'full_name\',\'?\'))" 2>/dev/null || echo "not_found"')
|
||||||
|
result['gitea_repo'] = gitea
|
||||||
|
|
||||||
|
# 5. SSL cert
|
||||||
|
cert = run('ls /etc/ssl/guardia/server.crt 2>/dev/null && echo exists || echo missing')
|
||||||
|
result['ssl_cert'] = cert
|
||||||
|
result['lets_encrypt'] = run('ls /etc/letsencrypt/live/zioinfo.co.kr/fullchain.pem 2>/dev/null && echo exists || echo missing')
|
||||||
|
|
||||||
|
# 6. Python 패키지 확인
|
||||||
|
result['aioimaplib'] = run('pip3 show aioimaplib 2>/dev/null | head -1 || echo missing')
|
||||||
|
result['aiosmtplib'] = run('pip3 show aiosmtplib 2>/dev/null | head -1 || echo missing')
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
import os; os.makedirs('C:/GUARDiA/.claude/agents/_workspace', exist_ok=True)
|
||||||
|
with open('C:/GUARDiA/.claude/agents/_workspace/infra-check.json','w') as f:
|
||||||
|
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||||
55
.claude/agents/_workspace/verify_report.json
Normal file
55
.claude/agents/_workspace/verify_report.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-06-01T21:14:25.665805",
|
||||||
|
"systems": {
|
||||||
|
"guardia-itsm": {
|
||||||
|
"issues": [
|
||||||
|
"APP_SRC_DRIFT:Only in /opt/guardia/app: rpa_rules.json"
|
||||||
|
],
|
||||||
|
"status": {
|
||||||
|
"service": "active",
|
||||||
|
"server_commit": "5e987833",
|
||||||
|
"gitea_commit": "5e987833"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zioinfo-web": {
|
||||||
|
"issues": [
|
||||||
|
"STASH_EXISTS:stash@{0}: WIP on main: ed276b6 fix(ui): 로고 필터 제거, 메뉴 네비게이션 오류 수정"
|
||||||
|
],
|
||||||
|
"status": {
|
||||||
|
"service": "active",
|
||||||
|
"server_commit": "9b203455",
|
||||||
|
"gitea_commit": "9b203455",
|
||||||
|
"www_date": "2026-06-01 20:48:44.033930458"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"guardia-manager": {
|
||||||
|
"issues": [],
|
||||||
|
"status": {
|
||||||
|
"service": "active",
|
||||||
|
"server_commit": "none",
|
||||||
|
"gitea_commit": "fa2657a2",
|
||||||
|
"www_date": "2026-06-01 21:12:50.599828319"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"guardia-messenger": {
|
||||||
|
"issues": [],
|
||||||
|
"status": {}
|
||||||
|
},
|
||||||
|
"guardia-docs": {
|
||||||
|
"issues": [],
|
||||||
|
"status": {
|
||||||
|
"www_date": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action_required": [
|
||||||
|
"guardia-itsm",
|
||||||
|
"zioinfo-web"
|
||||||
|
],
|
||||||
|
"critical": [
|
||||||
|
"guardia-itsm: app/src drift"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"zioinfo-web: stash exists"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
scripts/check/diff_company_jsx.py
Normal file
23
scripts/check/diff_company_jsx.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import paramiko, sys
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
def run(label, cmd, timeout=15):
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:1000])
|
||||||
|
|
||||||
|
# 현재 Company.jsx (stash에서 복원된 468줄) vs git HEAD의 Company.jsx
|
||||||
|
run('현재 vs HEAD diff (추가된 부분)',
|
||||||
|
'git -C /opt/zioinfo/src diff HEAD -- frontend/src/pages/Company.jsx 2>/dev/null | '
|
||||||
|
"grep '^+' | grep -v '^+++' | head -50")
|
||||||
|
|
||||||
|
# index.html이 참조하는 JS 파일
|
||||||
|
run('index.html 스크립트 태그',
|
||||||
|
"grep -o 'src=\"/assets/[^\"]*\"\\|href=\"/assets/[^\"]*\"' /var/www/zioinfo/index.html | head -10")
|
||||||
|
|
||||||
|
# 브라우저에서 실제로 보이는 것: curl로 홈페이지 확인
|
||||||
|
run('실제 서빙 확인 (200 OK)',
|
||||||
|
'curl -sf -o /dev/null -w "%{http_code}" https://zioinfo.co.kr/company/greeting 2>/dev/null || '
|
||||||
|
'curl -sf -o /dev/null -w "%{http_code}" http://localhost:8082/company/greeting 2>/dev/null')
|
||||||
|
|
||||||
|
c.close()
|
||||||
64
scripts/check/e2e_pipeline_test.py
Normal file
64
scripts/check/e2e_pipeline_test.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""E2E 파이프라인 최종 검증: 실제 deploy trigger → 배포+Jenkins 빌드 확인"""
|
||||||
|
import paramiko, sys, json, time, base64
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
J = 'http://127.0.0.1:9080'
|
||||||
|
A = 'admin:Admin@2026!'
|
||||||
|
GITEA_B64 = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(out[:600])
|
||||||
|
return out
|
||||||
|
|
||||||
|
# 1. 현재 build 번호 기록
|
||||||
|
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
|
||||||
|
d = json.loads(o.read().decode('utf-8','replace'))
|
||||||
|
build_before = d.get('lastBuild', {}).get('number', 0)
|
||||||
|
print(f'빌드 전 lastBuild: #{build_before}')
|
||||||
|
|
||||||
|
# 2. Gitea push 시뮬레이션 (webhook 직접 호출)
|
||||||
|
run('push 시뮬레이션 → 배포+Jenkins',
|
||||||
|
"curl -sf -X POST http://localhost:9999 "
|
||||||
|
"-H 'Content-Type: application/json' "
|
||||||
|
"-H 'X-Gitea-Event: push' "
|
||||||
|
"-d '{\"repository\":{\"name\":\"guardia-itsm\"},\"ref\":\"refs/heads/main\"}' 2>/dev/null")
|
||||||
|
|
||||||
|
# 3. 배포 완료 대기
|
||||||
|
print('\n배포 + Jenkins 빌드 대기...')
|
||||||
|
for i in range(15):
|
||||||
|
time.sleep(4)
|
||||||
|
_, o, _ = c.exec_command(
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
|
||||||
|
try:
|
||||||
|
d = json.loads(o.read().decode('utf-8','replace'))
|
||||||
|
lb = d.get('lastBuild', {}).get('number', 0)
|
||||||
|
building = d.get('lastBuild', {}).get('building', False) if lb > build_before else None
|
||||||
|
if lb > build_before:
|
||||||
|
if building == False:
|
||||||
|
_, o2, _ = c.exec_command(
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/api/json 2>/dev/null', timeout=10)
|
||||||
|
bd = json.loads(o2.read().decode('utf-8','replace'))
|
||||||
|
print(f' ✅ 새 빌드 #{lb}: {bd.get("result")}')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f' build #{lb} 진행중...')
|
||||||
|
else:
|
||||||
|
_, o3, _ = c.exec_command('tail -3 /var/log/zioinfo/deploy.log 2>/dev/null', timeout=5)
|
||||||
|
status = o3.read().decode('utf-8','replace').strip().splitlines()[-1] if o3.read else '...'
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 오류: {e}')
|
||||||
|
|
||||||
|
# 4. 최종 결과
|
||||||
|
run('배포 로그 마지막',
|
||||||
|
'tail -8 /var/log/zioinfo/deploy.log 2>/dev/null')
|
||||||
|
run('Jenkins 빌드 콘솔 (마지막 20줄)',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | tail -20')
|
||||||
|
run('guardia 서비스 상태',
|
||||||
|
'systemctl is-active guardia')
|
||||||
|
|
||||||
|
c.close()
|
||||||
7
scripts/check/get_mail_error.py
Normal file
7
scripts/check/get_mail_error.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import paramiko, sys
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=10)
|
||||||
|
_, o, _ = c.exec_command('cat /var/log/zioinfo/mail.log 2>/dev/null | grep -A 30 "500\\|Error\\|Exception\\|Traceback" | tail -50', timeout=10)
|
||||||
|
print(o.read().decode('utf-8','replace'))
|
||||||
|
c.close()
|
||||||
54
scripts/check/trigger_jenkins_build.py
Normal file
54
scripts/check/trigger_jenkins_build.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""Jenkins job 빌드 트리거 + 결과 확인"""
|
||||||
|
import paramiko, sys, time, json
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
J = 'http://127.0.0.1:9080'
|
||||||
|
A = 'admin:Admin@2026!'
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(out[:600])
|
||||||
|
return out
|
||||||
|
|
||||||
|
# crumb 가져오기
|
||||||
|
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/crumbIssuer/api/json 2>/dev/null', timeout=10)
|
||||||
|
try:
|
||||||
|
cd = json.loads(o.read().decode('utf-8','replace').strip())
|
||||||
|
CRUMB = f'-H "{cd["crumbRequestField"]}: {cd["crumb"]}"'
|
||||||
|
except:
|
||||||
|
CRUMB = ''
|
||||||
|
|
||||||
|
# guardia-itsm 빌드 트리거
|
||||||
|
run('guardia-itsm 빌드 트리거',
|
||||||
|
f'curl -sf -X POST -u "{A}" {CRUMB} {J}/job/guardia-itsm/build 2>/dev/null && echo "트리거됨"')
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 빌드 번호 확인
|
||||||
|
run('guardia-itsm 빌드 번호',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); '
|
||||||
|
'print(\'nextBuildNumber:\', d.get(\'nextBuildNumber\'), \'lastBuild:\', d.get(\'lastBuild\',{}).get(\'number\',\'없음\'))" 2>/dev/null')
|
||||||
|
|
||||||
|
time.sleep(8)
|
||||||
|
|
||||||
|
# 빌드 로그 확인
|
||||||
|
run('guardia-itsm 빌드 콘솔 로그',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | head -40')
|
||||||
|
|
||||||
|
# zioinfo-web 빌드 트리거
|
||||||
|
run('zioinfo-web 빌드 트리거',
|
||||||
|
f'curl -sf -X POST -u "{A}" {CRUMB} {J}/job/zioinfo-web/build 2>/dev/null && echo "트리거됨"')
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# 전체 job 상태
|
||||||
|
run('전체 job 상태',
|
||||||
|
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; [print(j[\'name\'], j[\'color\']) for j in json.load(sys.stdin)[\'jobs\']]" 2>/dev/null')
|
||||||
|
|
||||||
|
c.close()
|
||||||
133
scripts/check/verify_all_systems.py
Normal file
133
scripts/check/verify_all_systems.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
"""5개 시스템 전체 배포 상태 검증"""
|
||||||
|
import paramiko, sys, base64, json
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
def run(cmd, timeout=15):
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
return o.read().decode('utf-8','replace').strip()
|
||||||
|
|
||||||
|
def gitea_commit(repo):
|
||||||
|
out = run(f"curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/{repo}/commits?limit=1' "
|
||||||
|
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
|
||||||
|
"print(d[0]['sha'][:8], d[0]['commit']['message'][:50])\" 2>/dev/null")
|
||||||
|
return out or '(조회 실패)'
|
||||||
|
|
||||||
|
print('=' * 60)
|
||||||
|
print('GUARDiA 5개 시스템 배포 상태 검증')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
# ── 1. guardia-itsm ──────────────────────────────────────────
|
||||||
|
print('\n━━ 1. GUARDiA ITSM (guardia-itsm) ━━')
|
||||||
|
|
||||||
|
svc = run('systemctl is-active guardia 2>/dev/null')
|
||||||
|
port = run('ss -tlnp | grep 9001 2>/dev/null | head -1')
|
||||||
|
src_commit = run('git -C /opt/guardia/src log -1 --oneline 2>/dev/null')
|
||||||
|
app_routers = run('ls /opt/guardia/app/routers/*.py 2>/dev/null | wc -l')
|
||||||
|
app_vs_src = run('diff -rq /opt/guardia/src /opt/guardia/app '
|
||||||
|
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
|
||||||
|
'--exclude="*.db" --exclude="uploads" 2>/dev/null | head -5 || echo "동일"')
|
||||||
|
stash = run('git -C /opt/guardia/src stash list 2>/dev/null')
|
||||||
|
uncommit = run('git -C /opt/guardia/src status --short 2>/dev/null | grep -v "static/assets" | head -5')
|
||||||
|
gitea = gitea_commit('guardia-itsm')
|
||||||
|
|
||||||
|
print(f' 서비스: {svc}')
|
||||||
|
print(f' 포트 9001: {"✅ LISTEN" if "9001" in port else "❌ 없음"}')
|
||||||
|
print(f' src 커밋: {src_commit}')
|
||||||
|
print(f' app routers: {app_routers}개')
|
||||||
|
print(f' Gitea: {gitea}')
|
||||||
|
print(f' app vs src: {app_vs_src[:100]}')
|
||||||
|
print(f' stash: {stash or "없음"}')
|
||||||
|
print(f' uncommit: {uncommit or "없음"}')
|
||||||
|
|
||||||
|
# ── 2. zioinfo-web ───────────────────────────────────────────
|
||||||
|
print('\n━━ 2. 지오정보기술 홈페이지 (zioinfo-web) ━━')
|
||||||
|
|
||||||
|
svc = run('systemctl is-active zioinfo 2>/dev/null')
|
||||||
|
port = run('ss -tlnp | grep 8082 2>/dev/null | head -1')
|
||||||
|
src_commit = run('git -C /opt/zioinfo/src log -1 --oneline 2>/dev/null')
|
||||||
|
jar_date = run('ls -la /opt/zioinfo/app/app.jar 2>/dev/null | awk \'{print $6,$7,$8}\'')
|
||||||
|
www_date = run('ls -la /var/www/zioinfo/index.html 2>/dev/null | awk \'{print $6,$7,$8}\'')
|
||||||
|
stash = run('git -C /opt/zioinfo/src stash list 2>/dev/null')
|
||||||
|
uncommit = run('git -C /opt/zioinfo/src status --short 2>/dev/null | grep -v "static/assets" | head -5')
|
||||||
|
gitea = gitea_commit('zioinfo-web')
|
||||||
|
guardia_detail = run('ls /opt/zioinfo/src/frontend/src/pages/GuardiaDetail.jsx 2>/dev/null && '
|
||||||
|
'wc -l < /opt/zioinfo/src/frontend/src/pages/GuardiaDetail.jsx 2>/dev/null')
|
||||||
|
|
||||||
|
print(f' 서비스: {svc}')
|
||||||
|
print(f' 포트 8082: {"✅ LISTEN" if "8082" in port else "❌ 없음"}')
|
||||||
|
print(f' src 커밋: {src_commit}')
|
||||||
|
print(f' jar 날짜: {jar_date}')
|
||||||
|
print(f' /var/www 날짜: {www_date}')
|
||||||
|
print(f' GuardiaDetail: {guardia_detail}줄')
|
||||||
|
print(f' Gitea: {gitea}')
|
||||||
|
print(f' stash: {stash or "없음"}')
|
||||||
|
print(f' uncommit: {uncommit or "없음"}')
|
||||||
|
|
||||||
|
# ── 3. guardia-manager ───────────────────────────────────────
|
||||||
|
print('\n━━ 3. GUARDiA Manager (guardia-manager) ━━')
|
||||||
|
|
||||||
|
svc = run('systemctl is-active guardia-manager 2>/dev/null')
|
||||||
|
port = run('ss -tlnp | grep 8090 2>/dev/null | head -1')
|
||||||
|
www_date = run('ls -la /var/www/manager/index.html 2>/dev/null | awk \'{print $6,$7,$8}\' || echo "없음"')
|
||||||
|
be_routers = run('ls /opt/manager/backend/routers/*.py 2>/dev/null | wc -l || echo 0')
|
||||||
|
gitea = gitea_commit('guardia-manager')
|
||||||
|
|
||||||
|
print(f' 서비스: {svc}')
|
||||||
|
print(f' 포트 8090: {"✅ LISTEN" if "8090" in port else "❌ 없음"}')
|
||||||
|
print(f' /var/www/manager: {www_date}')
|
||||||
|
print(f' backend routers: {be_routers}개')
|
||||||
|
print(f' Gitea: {gitea}')
|
||||||
|
|
||||||
|
# ── 4. guardia-messenger ─────────────────────────────────────
|
||||||
|
print('\n━━ 4. GUARDiA Messenger (guardia-messenger) ━━')
|
||||||
|
|
||||||
|
gitea = gitea_commit('guardia-messenger')
|
||||||
|
app_json = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-messenger/contents/app.json' "
|
||||||
|
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); "
|
||||||
|
"c=base64.b64decode(d['content']).decode(); "
|
||||||
|
"import json as j2; a=j2.loads(c); "
|
||||||
|
"print('v'+a.get('expo',{}).get('version','?'))\" 2>/dev/null")
|
||||||
|
eas_json = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-messenger/contents/eas.json' "
|
||||||
|
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); "
|
||||||
|
"c=base64.b64decode(d['content']).decode(); print(c[:100])\" 2>/dev/null")
|
||||||
|
|
||||||
|
print(f' Gitea: {gitea}')
|
||||||
|
print(f' 앱 버전: {app_json or "(조회 실패)"}')
|
||||||
|
print(f' eas.json: {eas_json[:80] if eas_json else "(없음)"}')
|
||||||
|
print(f' 배포 방식: EAS Build (expo.dev) — 서버 배포 없음')
|
||||||
|
|
||||||
|
# ── 5. guardia-docs ──────────────────────────────────────────
|
||||||
|
print('\n━━ 5. GUARDiA Docs (guardia-docs) ━━')
|
||||||
|
|
||||||
|
gitea = gitea_commit('guardia-docs')
|
||||||
|
doc_count = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-docs/git/trees/main?recursive=true' "
|
||||||
|
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
|
||||||
|
"mds=[t for t in d.get('tree',[]) if t['path'].endswith('.md')]; "
|
||||||
|
"print(len(mds), '개 md 문서')\" 2>/dev/null")
|
||||||
|
|
||||||
|
print(f' Gitea: {gitea}')
|
||||||
|
print(f' 문서: {doc_count or "(조회 실패)"}')
|
||||||
|
|
||||||
|
# ── 종합 ─────────────────────────────────────────────────────
|
||||||
|
print('\n' + '=' * 60)
|
||||||
|
print('종합 요약')
|
||||||
|
print('=' * 60)
|
||||||
|
services = {
|
||||||
|
'guardia (ITSM)': run('systemctl is-active guardia 2>/dev/null'),
|
||||||
|
'zioinfo (홈페이지)': run('systemctl is-active zioinfo 2>/dev/null'),
|
||||||
|
'guardia-manager': run('systemctl is-active guardia-manager 2>/dev/null'),
|
||||||
|
'zioinfo-deploy': run('systemctl is-active zioinfo-deploy 2>/dev/null'),
|
||||||
|
'jenkins': run('systemctl is-active jenkins 2>/dev/null'),
|
||||||
|
}
|
||||||
|
for name, status in services.items():
|
||||||
|
icon = '✅' if status == 'active' else '❌'
|
||||||
|
print(f' {icon} {name}: {status}')
|
||||||
|
|
||||||
|
c.close()
|
||||||
50
scripts/check/verify_guardia_itsm.py
Normal file
50
scripts/check/verify_guardia_itsm.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""GUARDiA ITSM 서버 배포 상태 전체 검증"""
|
||||||
|
import paramiko, sys, json, base64
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=20):
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:800])
|
||||||
|
|
||||||
|
# 1. 서버 git 최신 커밋
|
||||||
|
run('서버 /opt/guardia/app git 커밋',
|
||||||
|
'git -C /opt/guardia/app log -3 --oneline 2>/dev/null || echo "git 없음"')
|
||||||
|
run('서버 /opt/guardia/src git 커밋',
|
||||||
|
'git -C /opt/guardia/src log -3 --oneline 2>/dev/null || echo "git 없음"')
|
||||||
|
|
||||||
|
# 2. Gitea 최신 커밋
|
||||||
|
GITEA_B64 = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
run('Gitea guardia-itsm 최신 커밋',
|
||||||
|
f"curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-itsm/commits?limit=3' "
|
||||||
|
f"-H 'Authorization: Basic {GITEA_B64}' 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json; "
|
||||||
|
"[print(c['sha'][:8], c['commit']['message'][:60]) for c in json.load(sys.stdin)]\" 2>/dev/null")
|
||||||
|
|
||||||
|
# 3. workspace vs 서버 핵심 파일 비교
|
||||||
|
run('서버 routers 목록',
|
||||||
|
'ls /opt/guardia/app/routers/*.py 2>/dev/null | xargs -I{} basename {} | sort')
|
||||||
|
run('서버 routers 개수', 'ls /opt/guardia/app/routers/*.py 2>/dev/null | wc -l')
|
||||||
|
|
||||||
|
# 4. 핵심 최신 기능 파일 존재 확인 (DR, CSAP, network, RPA, scraping)
|
||||||
|
run('최신 기능 파일 존재 여부',
|
||||||
|
'''for f in dr.py network_devices.py compliance.py rpa.py scraping.py ai_cmd.py; do
|
||||||
|
[ -f "/opt/guardia/app/routers/$f" ] && echo "✅ $f" || echo "❌ $f 없음"
|
||||||
|
done''')
|
||||||
|
|
||||||
|
# 5. 서버 서비스 상태 및 API 응답
|
||||||
|
run('guardia 서비스 상태',
|
||||||
|
'systemctl is-active guardia && systemctl status guardia --no-pager | grep -E "Active|Main PID" | head -3')
|
||||||
|
run('ITSM API 응답 확인',
|
||||||
|
'curl -sf http://localhost:8001/health 2>/dev/null || '
|
||||||
|
'curl -sf http://localhost:8001/docs 2>/dev/null | head -3 || '
|
||||||
|
'curl -sf http://localhost:9001/health 2>/dev/null || echo "API 확인 필요"')
|
||||||
|
|
||||||
|
# 6. 서버 stash 여부 (혹시 못 올린 작업 있는지)
|
||||||
|
run('서버 /opt/guardia/src stash',
|
||||||
|
'git -C /opt/guardia/src stash list 2>/dev/null || echo "stash 없음"')
|
||||||
|
run('서버 uncommitted 변경',
|
||||||
|
'git -C /opt/guardia/src status --short 2>/dev/null | head -10 || echo "없음"')
|
||||||
|
|
||||||
|
c.close()
|
||||||
31
scripts/check/verify_sync.py
Normal file
31
scripts/check/verify_sync.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""동기화 후 Gitea 최신 커밋 확인 + 배포 트리거"""
|
||||||
|
import paramiko, sys, time
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=20):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, e = client.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:500])
|
||||||
|
|
||||||
|
REPOS = ['guardia-itsm', 'guardia-manager', 'guardia-docs', 'zioinfo-web']
|
||||||
|
B64 = 'Authorization: Basic ' + __import__('base64').b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
for repo in REPOS:
|
||||||
|
run(f'Gitea {repo} 최신 커밋',
|
||||||
|
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/{repo}/commits?limit=1" '
|
||||||
|
f'--header "{B64}" 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); c=d[0]; print(c[\'sha\'][:8], c[\'commit\'][\'message\'][:50])" 2>/dev/null || echo FAIL')
|
||||||
|
|
||||||
|
# guardia-itsm 배포 트리거로 최신 코드 반영
|
||||||
|
run('guardia-itsm 배포 트리거',
|
||||||
|
'curl -sf -X POST http://localhost:9999 -H "Content-Type: application/json" '
|
||||||
|
'-H "X-Gitea-Event: push" '
|
||||||
|
'-d \'{"repository":{"name":"guardia-itsm"},"ref":"refs/heads/main"}\' 2>/dev/null')
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
run('배포 결과', 'tail -8 /var/log/zioinfo/deploy.log 2>/dev/null')
|
||||||
|
|
||||||
|
client.close()
|
||||||
21
scripts/check/verify_www_deploy.py
Normal file
21
scripts/check/verify_www_deploy.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import paramiko, sys
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
def run(label, cmd, timeout=15):
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:600])
|
||||||
|
|
||||||
|
run('새 Company 파일 존재 확인',
|
||||||
|
'ls /var/www/zioinfo/assets/ | grep -i Company')
|
||||||
|
run('/var/www/zioinfo index.html 내용',
|
||||||
|
'cat /var/www/zioinfo/index.html | grep -o "Company[^\"]*" | head -5')
|
||||||
|
run('/var/www/zioinfo 최신 시간 파일',
|
||||||
|
'find /var/www/zioinfo/assets -newer /var/www/zioinfo/assets/Home-BC38QtTl.js 2>/dev/null | wc -l && '
|
||||||
|
'find /var/www/zioinfo/assets -newer /var/www/zioinfo/assets/Home-BC38QtTl.js 2>/dev/null | head -5')
|
||||||
|
run('backend static의 Company',
|
||||||
|
'ls /opt/zioinfo/src/backend/src/main/resources/static/assets/ | grep Company')
|
||||||
|
run('zioinfo 서비스 상태', 'systemctl is-active zioinfo')
|
||||||
|
run('실제 사이트 응답 (greeting)',
|
||||||
|
'curl -sf -L http://localhost:8082/company/greeting 2>/dev/null | grep -o "Company[^\"]*" | head -3')
|
||||||
|
c.close()
|
||||||
69
scripts/check/wait_jenkins_build.py
Normal file
69
scripts/check/wait_jenkins_build.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""Jenkins 빌드 완료 대기 + 전체 파이프라인 최종 검증"""
|
||||||
|
import paramiko, sys, json, time, base64
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
J = 'http://127.0.0.1:9080'
|
||||||
|
A = 'admin:Admin@2026!'
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=20):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:600])
|
||||||
|
|
||||||
|
# 빌드 완료 대기
|
||||||
|
print('Jenkins 빌드 #4 대기...')
|
||||||
|
for i in range(15):
|
||||||
|
time.sleep(5)
|
||||||
|
_, o, _ = c.exec_command(
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
|
||||||
|
try:
|
||||||
|
d = json.loads(o.read().decode('utf-8','replace'))
|
||||||
|
lb = d.get('lastBuild', {}).get('number', 0)
|
||||||
|
nb = d.get('nextBuildNumber', 0)
|
||||||
|
print(f' lastBuild=#{lb} nextBuild=#{nb}', end='')
|
||||||
|
if lb >= 4:
|
||||||
|
_, o2, _ = c.exec_command(
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/api/json 2>/dev/null', timeout=10)
|
||||||
|
bd = json.loads(o2.read().decode('utf-8','replace'))
|
||||||
|
result = bd.get('result')
|
||||||
|
building = bd.get('building')
|
||||||
|
print(f' → {result} building={building}')
|
||||||
|
if not building:
|
||||||
|
print(f'\n✅ build #{lb}: {result}')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
except:
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 최종 결과
|
||||||
|
run('build #4 콘솔 로그',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | tail -15')
|
||||||
|
|
||||||
|
# 전체 E2E 파이프라인 최종 상태
|
||||||
|
print('\n' + '='*55)
|
||||||
|
print('GUARDiA CI/CD 파이프라인 최종 현황')
|
||||||
|
print('='*55)
|
||||||
|
print('\n📋 구성 요소:')
|
||||||
|
print(' - Gitea repos (5개): webhook 2개씩 (port 9999 + Jenkins)')
|
||||||
|
print(' - deploy_server.py (port 9999): 즉시 배포 + Jenkins 트리거')
|
||||||
|
print(' - Jenkins (port 9080): Build + Test + ITSM 메신저 알림')
|
||||||
|
|
||||||
|
run('webhook 서버', 'systemctl is-active zioinfo-deploy && echo "port 9999 ✅"')
|
||||||
|
run('5개 job 상태',
|
||||||
|
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
|
||||||
|
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
|
||||||
|
"[print(' ',j['name'].ljust(22), j['color'], '✅' if j['color']=='blue' else '⚠️') for j in d['jobs']]\" 2>/dev/null")
|
||||||
|
run('guardia + zioinfo 서비스',
|
||||||
|
'echo guardia: $(systemctl is-active guardia); echo zioinfo: $(systemctl is-active zioinfo)')
|
||||||
|
|
||||||
|
print('\n📌 자동 배포 흐름:')
|
||||||
|
print(' 1. 로컬 수정 → repos/에 commit')
|
||||||
|
print(' 2. sync_workspace_to_repos.py 실행 → Gitea push')
|
||||||
|
print(' 3. Gitea webhook → port 9999 → 즉시 배포 ✅')
|
||||||
|
print(' 4. deploy_server.py → Jenkins 트리거 ✅')
|
||||||
|
print(' 5. Jenkins: Build + Test → ITSM 메신저 알림 ✅')
|
||||||
|
|
||||||
|
c.close()
|
||||||
74
scripts/deploy/patch_mail_backend.py
Normal file
74
scripts/deploy/patch_mail_backend.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"""수정된 backend 파일 서버 패치 + E2E 재검증"""
|
||||||
|
import paramiko, sys, time
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:600])
|
||||||
|
|
||||||
|
# 파일 전송
|
||||||
|
for fn in ['imap_client.py', 'smtp_client.py', 'main.py']:
|
||||||
|
sftp.put(f'C:/GUARDiA/workspace/zioinfo-mail/backend/{fn}', f'/opt/mail/backend/{fn}')
|
||||||
|
print(f' ✅ {fn}')
|
||||||
|
|
||||||
|
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# E2E: 로그인 → 폴더 → 메일 발송 → Sent 확인
|
||||||
|
test = """
|
||||||
|
import urllib.request, json, time
|
||||||
|
|
||||||
|
BASE = "http://localhost:8026"
|
||||||
|
|
||||||
|
def req(url, data=None, headers={}):
|
||||||
|
r = urllib.request.Request(BASE + url, data=data, headers=headers)
|
||||||
|
return json.loads(urllib.request.urlopen(r, timeout=15).read())
|
||||||
|
|
||||||
|
# 1. ythong 로그인
|
||||||
|
t = req("/api/auth/login",
|
||||||
|
data=json.dumps({"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
|
||||||
|
headers={"Content-Type":"application/json"})
|
||||||
|
token_y = t["access_token"]
|
||||||
|
Hy = {"Authorization": "Bearer " + token_y}
|
||||||
|
print("1. ythong 로그인 OK")
|
||||||
|
|
||||||
|
# 2. 폴더 목록
|
||||||
|
folders = req("/api/mail/folders", headers=Hy)
|
||||||
|
for f in folders:
|
||||||
|
print(f" {f['display']}({f['name']}) 총:{f['total']} 미읽음:{f['unread']}")
|
||||||
|
|
||||||
|
# 3. 메일 발송 (ythong → info)
|
||||||
|
r = req("/api/mail/send",
|
||||||
|
data=json.dumps({
|
||||||
|
"to": "info@zioinfo.co.kr",
|
||||||
|
"subject": "테스트 발송",
|
||||||
|
"body": "웹메일 테스트 메시지입니다."
|
||||||
|
}).encode(),
|
||||||
|
headers={**Hy, "Content-Type":"application/json"})
|
||||||
|
print("3. 발송:", r)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 4. ythong Sent 폴더 확인
|
||||||
|
sent = req("/api/mail/messages?folder=Sent", headers=Hy)
|
||||||
|
print(f"4. Sent 폴더 총:{sent.get('total')} 건")
|
||||||
|
for m in sent.get("messages", [])[:2]:
|
||||||
|
print(f" [{m['uid']}] {m['subject']} → {m['sender_addr']}")
|
||||||
|
|
||||||
|
# 5. info 로그인 → INBOX 확인
|
||||||
|
t2 = req("/api/auth/login",
|
||||||
|
data=json.dumps({"username":"info@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
|
||||||
|
headers={"Content-Type":"application/json"})
|
||||||
|
Hi = {"Authorization": "Bearer " + t2["access_token"]}
|
||||||
|
inbox = req("/api/mail/messages?folder=INBOX", headers=Hi)
|
||||||
|
print(f"5. info INBOX 총:{inbox.get('total')} 건")
|
||||||
|
for m in inbox.get("messages", [])[:3]:
|
||||||
|
print(f" [{m['uid']}] {m['subject']} - from:{m['sender']}")
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/e2e.py', 'w') as f: f.write(test)
|
||||||
|
run('E2E 테스트', 'python3 /tmp/e2e.py 2>&1; rm /tmp/e2e.py', timeout=30)
|
||||||
|
|
||||||
|
sftp.close(); c.close()
|
||||||
90
scripts/deploy/patch_mail_final.py
Normal file
90
scripts/deploy/patch_mail_final.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""최종 패치: mail_parser + imap_client + frontend 재빌드"""
|
||||||
|
import paramiko, sys, subprocess, os
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:400])
|
||||||
|
|
||||||
|
# 1. Backend 파일 전송
|
||||||
|
for fn in ['mail_parser.py', 'imap_client.py', 'smtp_client.py', 'main.py']:
|
||||||
|
sftp.put(f'C:/GUARDiA/workspace/zioinfo-mail/backend/{fn}', f'/opt/mail/backend/{fn}')
|
||||||
|
print(f' ✅ {fn}')
|
||||||
|
|
||||||
|
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
|
||||||
|
|
||||||
|
# 2. Frontend 재빌드
|
||||||
|
print('\n[Frontend 재빌드]')
|
||||||
|
FE = 'C:/GUARDiA/workspace/zioinfo-mail/frontend'
|
||||||
|
NPM = 'npm.cmd' if sys.platform == 'win32' else 'npm'
|
||||||
|
r = subprocess.run([NPM, 'run', 'build'], cwd=FE, capture_output=True, text=True,
|
||||||
|
encoding='utf-8', errors='replace', timeout=180)
|
||||||
|
if r.returncode == 0:
|
||||||
|
print(' ✅ 빌드 성공')
|
||||||
|
else:
|
||||||
|
print(f' ❌ 빌드 실패: {r.stderr[:200]}')
|
||||||
|
import sys as _s; _s.exit(1)
|
||||||
|
|
||||||
|
# dist 전송
|
||||||
|
dist = 'C:/GUARDiA/workspace/zioinfo-mail/dist'
|
||||||
|
import tarfile, io
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buf, mode='w:gz') as tar:
|
||||||
|
for root, dirs, files in os.walk(dist):
|
||||||
|
for fn2 in files:
|
||||||
|
fp = os.path.join(root, fn2)
|
||||||
|
tar.add(fp, arcname=os.path.relpath(fp, dist))
|
||||||
|
buf.seek(0)
|
||||||
|
sftp.putfo(buf, '/tmp/mail_dist.tar.gz')
|
||||||
|
run('dist 배포', 'cd /var/www/mail && tar -xzf /tmp/mail_dist.tar.gz && rm /tmp/mail_dist.tar.gz && echo "files: $(ls | wc -l)"')
|
||||||
|
|
||||||
|
import time; time.sleep(2)
|
||||||
|
|
||||||
|
# 3. E2E 재검증
|
||||||
|
test = """
|
||||||
|
import urllib.request, json
|
||||||
|
|
||||||
|
BASE = "http://localhost:8026"
|
||||||
|
def req(url, data=None, headers={}):
|
||||||
|
r = urllib.request.Request(BASE + url, data=data, headers=headers)
|
||||||
|
try:
|
||||||
|
return json.loads(urllib.request.urlopen(r, timeout=15).read())
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR", url, ":", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# info 로그인 → INBOX
|
||||||
|
t = req("/api/auth/login",
|
||||||
|
data=json.dumps({"username":"info@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
|
||||||
|
headers={"Content-Type":"application/json"})
|
||||||
|
if t:
|
||||||
|
Hi = {"Authorization": "Bearer " + t["access_token"]}
|
||||||
|
inbox = req("/api/mail/messages?folder=INBOX", headers=Hi)
|
||||||
|
if inbox:
|
||||||
|
print(f"info INBOX total:{inbox['total']}")
|
||||||
|
for m in inbox.get("messages", [])[:3]:
|
||||||
|
print(f" [{m['uid']}] {m['subject']!r} from:{m['sender']!r}")
|
||||||
|
else:
|
||||||
|
print("inbox 오류")
|
||||||
|
|
||||||
|
# ythong → Sent 확인
|
||||||
|
t2 = req("/api/auth/login",
|
||||||
|
data=json.dumps({"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
|
||||||
|
headers={"Content-Type":"application/json"})
|
||||||
|
if t2:
|
||||||
|
Hy = {"Authorization": "Bearer " + t2["access_token"]}
|
||||||
|
sent = req("/api/mail/messages?folder=Sent", headers=Hy)
|
||||||
|
if sent:
|
||||||
|
print(f"ythong Sent total:{sent['total']}")
|
||||||
|
for m in sent.get("messages", [])[:2]:
|
||||||
|
print(f" [{m['uid']}] {m['subject']!r} to:{m.get('to','')!r}")
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/e2e2.py', 'w') as f: f.write(test)
|
||||||
|
run('E2E 재검증', 'python3 /tmp/e2e2.py 2>&1; rm /tmp/e2e2.py', timeout=30)
|
||||||
|
|
||||||
|
sftp.close(); c.close()
|
||||||
|
print('\n=== 완료 ===')
|
||||||
50
scripts/deploy/verify_mail_final.py
Normal file
50
scripts/deploy/verify_mail_final.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""최종 검증: 로그인 → 폴더 → 메일 목록"""
|
||||||
|
import paramiko, sys, time
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:600])
|
||||||
|
|
||||||
|
# 수정 파일 재전송
|
||||||
|
sftp.put('C:/GUARDiA/workspace/zioinfo-mail/backend/imap_client.py', '/opt/mail/backend/imap_client.py')
|
||||||
|
print('✅ imap_client.py 재전송')
|
||||||
|
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# E2E 테스트
|
||||||
|
run('E2E: 로그인 → 폴더 → 메일목록', """
|
||||||
|
TOKEN=$(curl -sf -X POST http://localhost:8026/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}' 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" 2>/dev/null)
|
||||||
|
|
||||||
|
echo "=폴더목록="
|
||||||
|
curl -sf http://localhost:8026/api/mail/folders \
|
||||||
|
-H "Authorization: Bearer $TOKEN" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
for f in d:
|
||||||
|
print(f' {f[\"display\"]} ({f[\"name\"]}) - 읽음:{f[\"unread\"]} / 전체:{f[\"total\"]}')
|
||||||
|
" 2>/dev/null
|
||||||
|
|
||||||
|
echo "=메일목록(INBOX)="
|
||||||
|
curl -sf "http://localhost:8026/api/mail/messages?folder=INBOX&page=1" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(f' 총 {d.get(\"total\",0)}개 메일')
|
||||||
|
for m in d.get('messages',[])[:3]:
|
||||||
|
print(f' [{m[\"uid\"]}] {m[\"subject\"][:40]} - {m[\"sender\"]}')
|
||||||
|
" 2>/dev/null
|
||||||
|
""", timeout=20)
|
||||||
|
|
||||||
|
run('nginx URL 응답', 'curl -sf -o /dev/null -w "HTTP %{http_code}" https://localhost:8025/ -k 2>/dev/null')
|
||||||
|
run('최종 서비스 상태', 'systemctl is-active zioinfo-mail && systemctl is-active nginx')
|
||||||
|
|
||||||
|
sftp.close(); c.close()
|
||||||
179
scripts/setup/fix_all_systems.py
Normal file
179
scripts/setup/fix_all_systems.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
"""system-sync-orchestrator: 검증 결과 기반 수정 실행"""
|
||||||
|
import paramiko, sys, time, subprocess, shutil, os, json, base64
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=120):
|
||||||
|
print(f'\n [{label}]')
|
||||||
|
_, o, e = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(' ' + out[:400])
|
||||||
|
return out
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# ── 1. guardia-itsm: APP_SRC_DRIFT 수정 ────────────────────
|
||||||
|
print('\n━━ FIX 1: guardia-itsm APP_SRC_DRIFT ━━')
|
||||||
|
# rpa_rules.json, embed_codebase.py는 app에서만 생성되는 런타임 파일 → src에서도 생성 경로 동기화
|
||||||
|
run('rpa 폴더 확인', 'ls /opt/guardia/src/routers/rpa.py 2>/dev/null && echo exists || echo missing')
|
||||||
|
run('rsync src→app (런타임 파일 제외)',
|
||||||
|
'rsync -a --delete '
|
||||||
|
'--exclude=__pycache__ --exclude=.git --exclude="*.db" '
|
||||||
|
'--exclude="uploads" --exclude=".env" --exclude=".pytest_cache" '
|
||||||
|
'--exclude="rpa_rules.json" --exclude="rpa/" '
|
||||||
|
'/opt/guardia/src/ /opt/guardia/app/ 2>&1 | tail -3')
|
||||||
|
run('guardia 재기동', 'systemctl restart guardia && sleep 4 && systemctl is-active guardia')
|
||||||
|
# 재검증
|
||||||
|
diff = run('app vs src 재검증',
|
||||||
|
'diff -rq /opt/guardia/src /opt/guardia/app '
|
||||||
|
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
|
||||||
|
'--exclude="*.db" --exclude="uploads" --exclude=".env" '
|
||||||
|
'--exclude=".pytest_cache" --exclude="rpa_rules.json" --exclude="rpa" '
|
||||||
|
'2>/dev/null | head -5 || echo "동일"')
|
||||||
|
results['guardia-itsm'] = '✅ 동기화 완료' if '동일' in diff else f'⚠️ {diff[:80]}'
|
||||||
|
print(f' 결과: {results["guardia-itsm"]}')
|
||||||
|
|
||||||
|
# ── 2. guardia-manager: STALE_WWW 수정 ─────────────────────
|
||||||
|
print('\n━━ FIX 2: guardia-manager STALE_WWW 재빌드 ━━')
|
||||||
|
|
||||||
|
# workspace/guardia-manager의 최신 frontend를 서버에 업로드
|
||||||
|
# repos/guardia-manager에서 bundle → server → git pull → npm build
|
||||||
|
run('Manager src 확인',
|
||||||
|
'ls /opt/manager/backend/main.py 2>/dev/null && echo "backend OK" || echo "backend MISSING"')
|
||||||
|
run('Manager frontend 확인',
|
||||||
|
'ls /opt/manager/frontend/package.json 2>/dev/null && echo "frontend OK" || echo "frontend MISSING"')
|
||||||
|
|
||||||
|
# repos/guardia-manager에서 최신 코드 가져와 서버에 업로드
|
||||||
|
# 이미 Gitea에 올라가 있으므로 서버에서 직접 클론
|
||||||
|
GITEA_URL = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-manager.git'
|
||||||
|
run('Manager 소스 clone/pull',
|
||||||
|
f'''if [ -d /opt/manager/frontend/.git ]; then
|
||||||
|
git -C /opt/manager/frontend fetch origin main && git -C /opt/manager/frontend reset --hard origin/main
|
||||||
|
elif [ -d /tmp/mgr_src ]; then
|
||||||
|
rm -rf /tmp/mgr_src
|
||||||
|
fi
|
||||||
|
[ -d /opt/manager/frontend/src ] && echo "frontend src OK" || echo "frontend src MISSING"
|
||||||
|
''')
|
||||||
|
|
||||||
|
# workspace의 guardia-manager frontend를 bundle로 서버에 전송
|
||||||
|
print('\n workspace/guardia-manager bundle 서버 전송 중...')
|
||||||
|
bundle_path = 'C:/GUARDiA/repos/guardia-manager.bundle'
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', '-C', 'C:/GUARDiA/repos/guardia-manager', 'bundle', 'create', bundle_path, '--all'],
|
||||||
|
capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
size = os.path.getsize(bundle_path) // 1024
|
||||||
|
print(f' bundle {size}KB → 서버 전송...')
|
||||||
|
sftp.put(bundle_path, '/tmp/mgr.bundle')
|
||||||
|
os.remove(bundle_path)
|
||||||
|
|
||||||
|
run('서버에서 Manager 클론',
|
||||||
|
'rm -rf /tmp/mgr_work && '
|
||||||
|
'git clone /tmp/mgr.bundle /tmp/mgr_work 2>/dev/null && '
|
||||||
|
'echo "clone OK" && ls /tmp/mgr_work/ | head -5')
|
||||||
|
|
||||||
|
# frontend 빌드
|
||||||
|
run('Manager frontend 빌드',
|
||||||
|
'cd /tmp/mgr_work/frontend && '
|
||||||
|
'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && '
|
||||||
|
'npm run build 2>&1 | tail -5', timeout=300)
|
||||||
|
|
||||||
|
# /var/www/manager 복사
|
||||||
|
run('www/manager 복사',
|
||||||
|
'mkdir -p /var/www/manager && '
|
||||||
|
'cp -r /tmp/mgr_work/frontend/dist/. /var/www/manager/ && '
|
||||||
|
f'stat /var/www/manager/index.html | grep Modify')
|
||||||
|
|
||||||
|
# backend도 업데이트
|
||||||
|
run('Manager backend 업데이트',
|
||||||
|
'cp -r /tmp/mgr_work/backend/. /opt/manager/backend/ 2>/dev/null || true && '
|
||||||
|
'rm -rf /tmp/mgr_work /tmp/mgr.bundle')
|
||||||
|
|
||||||
|
run('Manager 서비스 재기동',
|
||||||
|
'systemctl restart guardia-manager && sleep 4 && systemctl is-active guardia-manager')
|
||||||
|
|
||||||
|
www_date = run('www/manager 날짜 확인',
|
||||||
|
'stat /var/www/manager/index.html 2>/dev/null | grep Modify')
|
||||||
|
results['guardia-manager'] = f'✅ www 갱신: {www_date[:30]}' if 'Jun 1' in www_date or 'Jun 1' in www_date else f'⚠️ {www_date}'
|
||||||
|
else:
|
||||||
|
results['guardia-manager'] = '❌ bundle 생성 실패'
|
||||||
|
print(f' ❌ bundle 오류: {result.stderr.decode()[:100]}')
|
||||||
|
|
||||||
|
print(f' 결과: {results["guardia-manager"]}')
|
||||||
|
|
||||||
|
# ── 3. zioinfo-web: stash + uncommitted 정리 ───────────────
|
||||||
|
print('\n━━ FIX 3: zioinfo-web stash 정리 + 스크린샷 반영 ━━')
|
||||||
|
|
||||||
|
# uncommitted 파일 목록
|
||||||
|
uncommit = run('uncommitted 파일 목록',
|
||||||
|
"git -C /opt/zioinfo/src status --short 2>/dev/null | "
|
||||||
|
"grep -v 'static/assets/' | grep -v '.pyc'")
|
||||||
|
|
||||||
|
# logo_bottom.png, 스크린샷 → workspace로 다운로드 후 커밋
|
||||||
|
new_files = [l.strip().split()[-1] for l in uncommit.splitlines()
|
||||||
|
if '??' in l or 'M ' in l]
|
||||||
|
|
||||||
|
print(f' 새 파일: {new_files}')
|
||||||
|
downloaded = []
|
||||||
|
for f in new_files:
|
||||||
|
if not f: continue
|
||||||
|
remote = f'/opt/zioinfo/src/{f}'
|
||||||
|
local_ws = f'C:/GUARDiA/workspace/zioinfo-web/{f}'
|
||||||
|
local_repo = f'C:/GUARDiA/repos/zioinfo-web/{f}'
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(local_ws), exist_ok=True)
|
||||||
|
os.makedirs(os.path.dirname(local_repo), exist_ok=True)
|
||||||
|
sftp.get(remote, local_ws)
|
||||||
|
shutil.copy2(local_ws, local_repo)
|
||||||
|
downloaded.append(f)
|
||||||
|
print(f' 다운로드: {f}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' SKIP {f}: {e}')
|
||||||
|
|
||||||
|
# stash 처리: 빌드 산출물만 있으면 drop
|
||||||
|
stash_stat = run('stash 내용', 'git -C /opt/zioinfo/src stash show --stat 2>/dev/null | head -10')
|
||||||
|
only_assets = all('static/assets' in l or 'Jenkinsfile' in l
|
||||||
|
for l in stash_stat.splitlines() if '|' in l)
|
||||||
|
if only_assets:
|
||||||
|
run('stash drop (빌드 산출물만)', 'git -C /opt/zioinfo/src stash drop 2>/dev/null && echo "dropped"')
|
||||||
|
results['zioinfo-web-stash'] = '✅ stash drop (빌드산출물)'
|
||||||
|
else:
|
||||||
|
# 중요 파일이 있으면 보존
|
||||||
|
results['zioinfo-web-stash'] = '⚠️ stash 보존 (중요 파일 포함) - 수동 확인 필요'
|
||||||
|
print(f' stash 보존: {stash_stat[:200]}')
|
||||||
|
|
||||||
|
# 서버 uncommitted 파일 add & commit
|
||||||
|
if new_files:
|
||||||
|
run('uncommitted 파일 commit',
|
||||||
|
'cd /opt/zioinfo/src && '
|
||||||
|
'git add backend/src/main/resources/static/logo_bottom.png '
|
||||||
|
'backend/src/main/resources/static/screenshots/ 2>/dev/null; '
|
||||||
|
'git config user.email "ci@zioinfo.co.kr"; '
|
||||||
|
'git config user.name "CI Bot"; '
|
||||||
|
'git diff --cached --stat 2>/dev/null; '
|
||||||
|
'git commit -m "chore: add screenshots and logo assets" 2>/dev/null || echo "nothing to commit"')
|
||||||
|
|
||||||
|
results['zioinfo-web'] = f'✅ {len(downloaded)}개 파일 반영'
|
||||||
|
print(f' 결과: {results["zioinfo-web"]}')
|
||||||
|
|
||||||
|
# ── 최종 요약 ────────────────────────────────────────────────
|
||||||
|
print('\n' + '='*55)
|
||||||
|
print('수정 결과 요약')
|
||||||
|
print('='*55)
|
||||||
|
for sys_name, result in results.items():
|
||||||
|
print(f' {result} [{sys_name}]')
|
||||||
|
|
||||||
|
# fix_report 저장
|
||||||
|
os.makedirs('C:/GUARDiA/.claude/agents/_workspace', exist_ok=True)
|
||||||
|
with open('C:/GUARDiA/.claude/agents/_workspace/fix_report.json', 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({"timestamp": __import__('datetime').datetime.now().isoformat(),
|
||||||
|
"results": results}, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
c.close()
|
||||||
|
print('\n=== 수정 완료 ===')
|
||||||
60
scripts/setup/fix_manager_www.py
Normal file
60
scripts/setup/fix_manager_www.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""guardia-manager /var/www/manager 수정 + guardia-itsm 재검증"""
|
||||||
|
import paramiko, sys, time, subprocess, os
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=120):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(out[:500])
|
||||||
|
return out
|
||||||
|
|
||||||
|
# 1. 빌드 산출물 경로 확인
|
||||||
|
run('빌드 산출물 위치 확인',
|
||||||
|
'find /tmp -name "index.html" -path "*/dist/*" 2>/dev/null | head -5 || echo "없음"')
|
||||||
|
|
||||||
|
# 이미 삭제됐으면 재빌드
|
||||||
|
run('Manager 재빌드',
|
||||||
|
'rm -rf /tmp/mgr2 && '
|
||||||
|
'git clone /tmp/mgr.bundle /tmp/mgr2 2>/dev/null || '
|
||||||
|
'git clone http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-manager.git /tmp/mgr2 2>/dev/null && '
|
||||||
|
'echo "clone OK"', timeout=60)
|
||||||
|
|
||||||
|
run('vite 빌드 (outDir 확인)',
|
||||||
|
'cat /tmp/mgr2/frontend/vite.config.ts 2>/dev/null || cat /tmp/mgr2/frontend/vite.config.js 2>/dev/null | grep -A3 "build\|outDir"')
|
||||||
|
|
||||||
|
run('npm 빌드',
|
||||||
|
'cd /tmp/mgr2/frontend && '
|
||||||
|
'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && '
|
||||||
|
'npm run build 2>&1 | tail -5', timeout=300)
|
||||||
|
|
||||||
|
run('dist 위치 탐색',
|
||||||
|
'find /tmp/mgr2 -name "index.html" -not -path "*/node_modules/*" 2>/dev/null')
|
||||||
|
|
||||||
|
# 실제 dist 폴더를 /var/www/manager에 복사
|
||||||
|
run('www/manager 복사',
|
||||||
|
'''DIST=$(find /tmp/mgr2 -name "index.html" -not -path "*/node_modules/*" 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
|
||||||
|
if [ -n "$DIST" ]; then
|
||||||
|
echo "dist 폴더: $DIST"
|
||||||
|
mkdir -p /var/www/manager
|
||||||
|
cp -r $DIST/. /var/www/manager/
|
||||||
|
echo "복사 완료: $(ls /var/www/manager/ | wc -l) 파일"
|
||||||
|
stat /var/www/manager/index.html | grep Modify
|
||||||
|
else
|
||||||
|
echo "dist 폴더 없음"
|
||||||
|
fi''')
|
||||||
|
|
||||||
|
run('Manager 서비스 재기동', 'systemctl restart guardia-manager && sleep 3 && systemctl is-active guardia-manager')
|
||||||
|
run('정리', 'rm -rf /tmp/mgr2 /tmp/mgr.bundle 2>/dev/null; echo "정리 완료"')
|
||||||
|
|
||||||
|
# 2. guardia-itsm diff 재확인
|
||||||
|
run('ITSM app vs src 최종 확인',
|
||||||
|
'diff -rq /opt/guardia/src /opt/guardia/app '
|
||||||
|
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
|
||||||
|
'--exclude="*.db" --exclude="uploads" --exclude=".env" '
|
||||||
|
'--exclude=".pytest_cache" --exclude="rpa_rules.json" --exclude="rpa" '
|
||||||
|
'--exclude="scripts" 2>/dev/null || echo "차이 없음"')
|
||||||
|
|
||||||
|
c.close()
|
||||||
54
scripts/setup/push_zioinfo_commits.py
Normal file
54
scripts/setup/push_zioinfo_commits.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""zioinfo-web 서버 커밋(스크린샷 등) → Gitea push + 나머지 uncommitted 처리"""
|
||||||
|
import paramiko, sys, time
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=60):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(out[:500])
|
||||||
|
return out
|
||||||
|
|
||||||
|
GITEA = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-web.git'
|
||||||
|
|
||||||
|
# 1. 서버 남은 uncommitted 처리
|
||||||
|
run('uncommitted 상태',
|
||||||
|
'git -C /opt/zioinfo/src status --short 2>/dev/null | '
|
||||||
|
"grep -v 'static/assets/' | grep -v '.pyc'")
|
||||||
|
|
||||||
|
run('남은 파일 추가 commit',
|
||||||
|
'''cd /opt/zioinfo/src
|
||||||
|
git config user.email "ci@zioinfo.co.kr" 2>/dev/null
|
||||||
|
git config user.name "CI Bot" 2>/dev/null
|
||||||
|
# 모든 static 파일 추가 (빌드 산출물 제외)
|
||||||
|
git add backend/src/main/resources/static/zioinfo-building.png 2>/dev/null
|
||||||
|
git add backend/src/main/resources/static/zioinfo-logo*.png 2>/dev/null
|
||||||
|
git add backend/src/main/resources/static/*.png 2>/dev/null
|
||||||
|
git add backend/src/main/resources/static/index.html 2>/dev/null
|
||||||
|
git diff --cached --stat 2>/dev/null | head -10
|
||||||
|
git commit -m "chore: add static assets (logos, building photo)" 2>/dev/null || echo "nothing to commit"
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 2. 서버 커밋들을 Gitea에 push
|
||||||
|
run('서버 현재 커밋 로그',
|
||||||
|
'git -C /opt/zioinfo/src log --oneline -3 2>/dev/null')
|
||||||
|
|
||||||
|
run('Gitea push (서버 → Gitea)',
|
||||||
|
f'git -C /opt/zioinfo/src remote set-url origin "{GITEA}" && '
|
||||||
|
'git -C /opt/zioinfo/src push origin main --force 2>&1 | tail -5')
|
||||||
|
|
||||||
|
# 3. 최신 Gitea 커밋 확인
|
||||||
|
run('Gitea 최신 커밋 확인',
|
||||||
|
'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-web/commits?limit=2" '
|
||||||
|
'-H "Authorization: Basic emhvOlppb0BBZG1pbjIwMjYh" 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; [print(c[\'sha\'][:8], c[\'commit\'][\'message\'][:50]) for c in json.load(sys.stdin)]" 2>/dev/null')
|
||||||
|
|
||||||
|
# 4. /var/www/zioinfo 최신 상태 확인
|
||||||
|
run('www/zioinfo 날짜', 'stat /var/www/zioinfo/index.html 2>/dev/null | grep Modify')
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
c.close()
|
||||||
|
print('\n=== 완료 ===')
|
||||||
214
scripts/setup/setup_mail_cicd.py
Normal file
214
scripts/setup/setup_mail_cicd.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
"""zioinfo-mail CI/CD 전체 구축:
|
||||||
|
1. deploy_server.py에 zioinfo-mail 배포 함수 추가
|
||||||
|
2. Jenkins job 생성
|
||||||
|
3. Gitea webhook 등록 (port 9999 + Jenkins)
|
||||||
|
4. Jenkinsfile Gitea push
|
||||||
|
"""
|
||||||
|
import paramiko, sys, json, base64, subprocess, os, time
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
|
||||||
|
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
J = 'http://127.0.0.1:9080'
|
||||||
|
A = 'admin:Admin@2026!'
|
||||||
|
TOK = 'gitea-build-2026'
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
out = o.read().decode('utf-8','replace').strip()
|
||||||
|
if out: print(out[:500])
|
||||||
|
return out
|
||||||
|
|
||||||
|
# crumb
|
||||||
|
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/crumbIssuer/api/json 2>/dev/null', timeout=10)
|
||||||
|
try:
|
||||||
|
cd = json.loads(o.read().decode('utf-8','replace').strip())
|
||||||
|
CH = f'{cd["crumbRequestField"]}: {cd["crumb"]}'
|
||||||
|
except:
|
||||||
|
CH = 'Jenkins-Crumb: x'
|
||||||
|
|
||||||
|
# ── 1. deploy_server.py 업데이트 ─────────────────────────────
|
||||||
|
print('\n━━ 1. deploy_server.py zioinfo-mail 추가 ━━')
|
||||||
|
|
||||||
|
update_script = r"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
with open('/opt/zioinfo/deploy_server.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if 'zioinfo-mail' in content:
|
||||||
|
print('이미 있음')
|
||||||
|
else:
|
||||||
|
# guardia-docs 블록 이후에 zioinfo-mail 추가
|
||||||
|
# 또는 elif repo == "guardia-docs": 다음에 추가
|
||||||
|
MAIL_BLOCK = '''
|
||||||
|
elif repo == "zioinfo-mail":
|
||||||
|
SRC = "/opt/mail"
|
||||||
|
ok = run_steps(repo, [
|
||||||
|
("git pull", ["bash", "-c",
|
||||||
|
f"[ -d {SRC}/src/.git ] && git -C {SRC}/src fetch origin main && git -C {SRC}/src reset --hard origin/main"
|
||||||
|
f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git' {SRC}/src"]),
|
||||||
|
("npm build", ["bash", "-c",
|
||||||
|
f"cd {SRC}/src/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]),
|
||||||
|
("copy dist", ["bash", "-c", f"cp -r {SRC}/src/dist/. /var/www/mail/"]),
|
||||||
|
("pip install", ["bash", "-c",
|
||||||
|
f"{SRC}/venv/bin/pip install -r {SRC}/src/backend/requirements.txt -q"]),
|
||||||
|
("rsync", ["bash", "-c",
|
||||||
|
f"rsync -a --exclude=__pycache__ --exclude=.git --exclude='*.pyc' --exclude='.env' {SRC}/src/backend/ {SRC}/backend/"]),
|
||||||
|
("restart", ["systemctl", "restart", "zioinfo-mail"]),
|
||||||
|
("health check", ["bash", "-c", "sleep 4 && curl -sf http://localhost:8026/health"]),
|
||||||
|
])
|
||||||
|
if ok:
|
||||||
|
notify_itsm(True, "\\u2705 zioinfo-mail \\ubc30\\ud3ec \\uc644\\ub8cc")
|
||||||
|
else:
|
||||||
|
notify_itsm(False, "\\u274c zioinfo-mail \\ube4c\\ub4dc \\uc2e4\\ud328")
|
||||||
|
'''
|
||||||
|
# guardia-docs 블록 직전에 삽입
|
||||||
|
content = content.replace(
|
||||||
|
' elif repo == "guardia-docs":',
|
||||||
|
MAIL_BLOCK + ' elif repo == "guardia-docs":'
|
||||||
|
)
|
||||||
|
with open('/opt/zioinfo/deploy_server.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
print('추가 완료')
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/upd.py', 'w') as f: f.write(update_script)
|
||||||
|
run('deploy_server 업데이트', 'python3 /tmp/upd.py 2>&1; rm /tmp/upd.py')
|
||||||
|
run('zioinfo-mail 추가 확인', "grep -n 'zioinfo-mail' /opt/zioinfo/deploy_server.py")
|
||||||
|
run('webhook 서버 재시작', 'systemctl restart zioinfo-deploy && sleep 2 && systemctl is-active zioinfo-deploy')
|
||||||
|
|
||||||
|
# ── 2. Jenkins job 생성 ──────────────────────────────────────
|
||||||
|
print('\n━━ 2. Jenkins job 생성 (zioinfo-mail) ━━')
|
||||||
|
|
||||||
|
job_config = f"""<?xml version='1.1' encoding='UTF-8'?>
|
||||||
|
<flow-definition plugin="workflow-job">
|
||||||
|
<description>GUARDiA zioinfo-mail Webmail CI/CD</description>
|
||||||
|
<keepDependencies>false</keepDependencies>
|
||||||
|
<properties>
|
||||||
|
<org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/>
|
||||||
|
<authToken>{TOK}</authToken>
|
||||||
|
</properties>
|
||||||
|
<definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps">
|
||||||
|
<scm class="hudson.plugins.git.GitSCM" plugin="git">
|
||||||
|
<configVersion>2</configVersion>
|
||||||
|
<userRemoteConfigs>
|
||||||
|
<hudson.plugins.git.UserRemoteConfig>
|
||||||
|
<url>http://127.0.0.1:9003/zio/zioinfo-mail.git</url>
|
||||||
|
<credentialsId>gitea-zio</credentialsId>
|
||||||
|
</hudson.plugins.git.UserRemoteConfig>
|
||||||
|
</userRemoteConfigs>
|
||||||
|
<branches>
|
||||||
|
<hudson.plugins.git.BranchSpec><name>*/main</name></hudson.plugins.git.BranchSpec>
|
||||||
|
</branches>
|
||||||
|
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||||
|
<submoduleCfg class="empty-list"/>
|
||||||
|
<extensions/>
|
||||||
|
</scm>
|
||||||
|
<scriptPath>Jenkinsfile</scriptPath>
|
||||||
|
<lightweight>true</lightweight>
|
||||||
|
</definition>
|
||||||
|
<triggers>
|
||||||
|
<com.cloudbees.jenkins.gitea.GiteaWebHookTrigger plugin="gitea">
|
||||||
|
<properties/>
|
||||||
|
</com.cloudbees.jenkins.gitea.GiteaWebHookTrigger>
|
||||||
|
</triggers>
|
||||||
|
</flow-definition>"""
|
||||||
|
|
||||||
|
with sftp.open('/tmp/job_mail.xml', 'w') as f: f.write(job_config)
|
||||||
|
run('Jenkins job 생성',
|
||||||
|
f'curl -sf -X POST "{J}/createItem?name=zioinfo-mail" '
|
||||||
|
f'-u "{A}" -H "{CH}" '
|
||||||
|
f'-H "Content-Type: text/xml" '
|
||||||
|
f'--data-binary @/tmp/job_mail.xml 2>/dev/null; echo $?')
|
||||||
|
|
||||||
|
# authToken 설정
|
||||||
|
run('authToken config.xml 설정',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/zioinfo-mail/config.xml 2>/dev/null | '
|
||||||
|
f'grep -c "authToken" || echo "없음"')
|
||||||
|
|
||||||
|
# ── 3. Gitea webhook 등록 ────────────────────────────────────
|
||||||
|
print('\n━━ 3. Gitea webhook 등록 ━━')
|
||||||
|
|
||||||
|
# 3-1. port 9999 webhook
|
||||||
|
payload9999 = json.dumps({
|
||||||
|
"type": "gitea",
|
||||||
|
"config": {"url": "http://127.0.0.1:9999", "content_type": "json",
|
||||||
|
"secret": "zioinfo-deploy-2026"},
|
||||||
|
"events": ["push"], "active": True
|
||||||
|
})
|
||||||
|
with sftp.open('/tmp/h1.json', 'w') as f: f.write(payload9999)
|
||||||
|
run('webhook 9999 등록',
|
||||||
|
f'curl -sf -X POST "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
|
||||||
|
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
|
||||||
|
f'--data @/tmp/h1.json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'id:\',d.get(\'id\'),d.get(\'config\',{}).get(\'url\',\'\'))" 2>/dev/null')
|
||||||
|
|
||||||
|
# 3-2. Jenkins webhook
|
||||||
|
payload_jenkins = json.dumps({
|
||||||
|
"type": "gitea",
|
||||||
|
"config": {"url": f"http://127.0.0.1:9080/job/zioinfo-mail/build?token={TOK}",
|
||||||
|
"content_type": "json"},
|
||||||
|
"events": ["push"], "active": True
|
||||||
|
})
|
||||||
|
with sftp.open('/tmp/h2.json', 'w') as f: f.write(payload_jenkins)
|
||||||
|
run('webhook Jenkins 등록',
|
||||||
|
f'curl -sf -X POST "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
|
||||||
|
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
|
||||||
|
f'--data @/tmp/h2.json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'id:\',d.get(\'id\'),d.get(\'config\',{}).get(\'url\',\'\'))" 2>/dev/null')
|
||||||
|
|
||||||
|
# ── 4. Jenkinsfile Gitea push ─────────────────────────────────
|
||||||
|
print('\n━━ 4. Jenkinsfile → Gitea push ━━')
|
||||||
|
|
||||||
|
jf_content = open('C:/GUARDiA/workspace/zioinfo-mail/Jenkinsfile', encoding='utf-8').read()
|
||||||
|
jf_b64 = base64.b64encode(jf_content.encode('utf-8')).decode()
|
||||||
|
|
||||||
|
# 기존 파일 SHA 확인
|
||||||
|
_, o, _ = c.exec_command(
|
||||||
|
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/contents/Jenkinsfile" '
|
||||||
|
f'-H "Authorization: Basic {G}" 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; print(json.load(sys.stdin).get(\'sha\',\'\'))" 2>/dev/null', timeout=10)
|
||||||
|
sha = o.read().decode('utf-8','replace').strip()
|
||||||
|
|
||||||
|
if sha:
|
||||||
|
payload_jf = json.dumps({"message":"ci: add Jenkinsfile","content":jf_b64,"sha":sha,"branch":"main"})
|
||||||
|
method = "PUT"
|
||||||
|
else:
|
||||||
|
payload_jf = json.dumps({"message":"ci: add Jenkinsfile","content":jf_b64,"branch":"main"})
|
||||||
|
method = "POST"
|
||||||
|
|
||||||
|
with sftp.open('/tmp/jf.json', 'w') as f: f.write(payload_jf)
|
||||||
|
run('Jenkinsfile Gitea 업로드',
|
||||||
|
f'curl -sf -X {method} "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/contents/Jenkinsfile" '
|
||||||
|
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
|
||||||
|
f'--data @/tmp/jf.json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'OK:\', d.get(\'content\',{}).get(\'name\',\'?\'))" 2>/dev/null')
|
||||||
|
|
||||||
|
# ── 5. 검증 ─────────────────────────────────────────────────
|
||||||
|
print('\n━━ 5. 최종 검증 ━━')
|
||||||
|
time.sleep(3)
|
||||||
|
run('Jenkins jobs 전체',
|
||||||
|
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; [print(j[\'name\'].ljust(22),j[\'color\']) for j in json.load(sys.stdin)[\'jobs\']]" 2>/dev/null')
|
||||||
|
|
||||||
|
run('zioinfo-mail hooks 확인',
|
||||||
|
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
|
||||||
|
f'-H "Authorization: Basic {G}" 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; [print(\' hook\',h[\'id\'],h[\'config\'].get(\'url\',\'\')[:60],\'active:\',h[\'active\']) for h in json.load(sys.stdin)]" 2>/dev/null')
|
||||||
|
|
||||||
|
# 첫 빌드 트리거
|
||||||
|
run('첫 빌드 트리거',
|
||||||
|
f'curl -sf -X POST -u "{A}" -H "{CH}" {J}/job/zioinfo-mail/build 2>/dev/null && echo "트리거됨"')
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
run('빌드 상태',
|
||||||
|
f'curl -sf -u "{A}" {J}/job/zioinfo-mail/api/json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); '
|
||||||
|
'lb=d.get(\'lastBuild\',{}); print(\'build #\'+str(lb.get(\'number\',\'?\')))" 2>/dev/null')
|
||||||
|
|
||||||
|
sftp.close(); c.close()
|
||||||
|
print('\n=== zioinfo-mail CI/CD 구축 완료 ===')
|
||||||
175
scripts/setup/setup_mail_repo.py
Normal file
175
scripts/setup/setup_mail_repo.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
"""zioinfo-mail repos 생성 + workspace 소스 동기화 + Gitea push + deploy_server 수정"""
|
||||||
|
import paramiko, subprocess, sys, os, shutil, base64, json, time
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||||
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=30):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
_, o, _ = c.exec_command(cmd, timeout=timeout)
|
||||||
|
print(o.read().decode('utf-8','replace').strip()[:500])
|
||||||
|
|
||||||
|
REPO_PATH = 'C:/GUARDiA/repos/zioinfo-mail'
|
||||||
|
WS_PATH = 'C:/GUARDiA/workspace/zioinfo-mail'
|
||||||
|
|
||||||
|
# ── 1. repos/zioinfo-mail git 초기화 ────────────────────────
|
||||||
|
print('\n━━ 1. repos/zioinfo-mail git 초기화 ━━')
|
||||||
|
os.makedirs(REPO_PATH, exist_ok=True)
|
||||||
|
|
||||||
|
# workspace → repos 동기화 (node_modules, dist, __pycache__ 제외)
|
||||||
|
EXCLUDE = {'node_modules', 'dist', '__pycache__', '.git', '*.pyc', '.pytest_cache'}
|
||||||
|
|
||||||
|
def sync_dir(src, dst):
|
||||||
|
for root, dirs, files in os.walk(src):
|
||||||
|
dirs[:] = [d for d in dirs if d not in EXCLUDE and not d.endswith('.egg-info')]
|
||||||
|
rel = os.path.relpath(root, src)
|
||||||
|
dst_dir = os.path.join(dst, rel) if rel != '.' else dst
|
||||||
|
os.makedirs(dst_dir, exist_ok=True)
|
||||||
|
for fn in files:
|
||||||
|
if fn.endswith(('.pyc',)):
|
||||||
|
continue
|
||||||
|
s = os.path.join(root, fn)
|
||||||
|
d = os.path.join(dst_dir, fn)
|
||||||
|
shutil.copy2(s, d)
|
||||||
|
|
||||||
|
sync_dir(WS_PATH, REPO_PATH)
|
||||||
|
print(f' 동기화 완료')
|
||||||
|
|
||||||
|
# git init + commit
|
||||||
|
r = subprocess.run(['git', '-C', REPO_PATH, 'rev-parse', '--git-dir'], capture_output=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'init'], capture_output=True)
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'config', 'user.email', 'ci@zioinfo.co.kr'], capture_output=True)
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'config', 'user.name', 'CI Bot'], capture_output=True)
|
||||||
|
|
||||||
|
# .gitignore 생성
|
||||||
|
with open(f'{REPO_PATH}/.gitignore', 'w') as f:
|
||||||
|
f.write('node_modules/\ndist/\n__pycache__/\n*.pyc\n.env\n*.db\n.pytest_cache/\n')
|
||||||
|
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'add', '-A'], capture_output=True)
|
||||||
|
r2 = subprocess.run(['git', '-C', REPO_PATH, 'status', '--short'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if r2.stdout.strip():
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'commit', '-m', 'feat: initial zioinfo-mail webmail system'],
|
||||||
|
capture_output=True)
|
||||||
|
print(' 커밋 완료')
|
||||||
|
else:
|
||||||
|
print(' 변경 없음')
|
||||||
|
|
||||||
|
r3 = subprocess.run(['git', '-C', REPO_PATH, 'log', '--oneline', '-2'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
print(f' git log: {r3.stdout.strip()}')
|
||||||
|
|
||||||
|
# ── 2. bundle → 서버 → Gitea push ───────────────────────────
|
||||||
|
print('\n━━ 2. Gitea push ━━')
|
||||||
|
bundle = f'{REPO_PATH}.bundle'
|
||||||
|
subprocess.run(['git', '-C', REPO_PATH, 'bundle', 'create', bundle, '--all'],
|
||||||
|
capture_output=True, timeout=120)
|
||||||
|
size = os.path.getsize(bundle) // 1024
|
||||||
|
print(f' bundle: {size}KB → 서버 전송...')
|
||||||
|
sftp.put(bundle, '/tmp/mail.bundle')
|
||||||
|
os.remove(bundle)
|
||||||
|
|
||||||
|
GITEA_URL = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git'
|
||||||
|
run('Gitea push',
|
||||||
|
f'rm -rf /tmp/mail_push && '
|
||||||
|
f'git clone /tmp/mail.bundle /tmp/mail_push 2>/dev/null && '
|
||||||
|
f'cd /tmp/mail_push && '
|
||||||
|
f'git remote set-url origin "{GITEA_URL}" && '
|
||||||
|
f'git push origin main --force 2>&1 | tail -3 && '
|
||||||
|
f'rm -rf /tmp/mail_push /tmp/mail.bundle && echo "pushed"', timeout=120)
|
||||||
|
|
||||||
|
# ── 3. deploy_server.py 수정 (직접 편집) ─────────────────────
|
||||||
|
print('\n━━ 3. deploy_server.py zioinfo-mail 추가 ━━')
|
||||||
|
|
||||||
|
# 현재 deploy_server.py 내용 확인
|
||||||
|
_, o, _ = c.exec_command('cat /opt/zioinfo/deploy_server.py', timeout=15)
|
||||||
|
content = o.read().decode('utf-8','replace')
|
||||||
|
|
||||||
|
if 'zioinfo-mail' in content:
|
||||||
|
print(' 이미 있음')
|
||||||
|
else:
|
||||||
|
# 마지막 elif 블록 찾기 (guardia-docs 또는 guardia-messenger)
|
||||||
|
mail_block = '''
|
||||||
|
elif repo == "zioinfo-mail":
|
||||||
|
SRC = "/opt/mail"
|
||||||
|
ok = run_steps(repo, [
|
||||||
|
("git pull", ["bash", "-c",
|
||||||
|
f"[ -d {SRC}/src/.git ] && git -C {SRC}/src fetch origin main && git -C {SRC}/src reset --hard origin/main"
|
||||||
|
" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git' " + f"{SRC}/src"]),
|
||||||
|
("npm build", ["bash", "-c",
|
||||||
|
f"cd {SRC}/src/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]),
|
||||||
|
("copy dist", ["bash", "-c", f"mkdir -p /var/www/mail && cp -r {SRC}/src/dist/. /var/www/mail/"]),
|
||||||
|
("pip install", ["bash", "-c",
|
||||||
|
f"{SRC}/venv/bin/pip install -r {SRC}/src/backend/requirements.txt -q"]),
|
||||||
|
("rsync", ["bash", "-c",
|
||||||
|
f"rsync -a --exclude=__pycache__ --exclude=.git --exclude='*.pyc' --exclude='.env' {SRC}/src/backend/ {SRC}/backend/"]),
|
||||||
|
("restart", ["systemctl", "restart", "zioinfo-mail"]),
|
||||||
|
("health check", ["bash", "-c", "sleep 4 && curl -sf http://localhost:8026/health"]),
|
||||||
|
])
|
||||||
|
if ok:
|
||||||
|
notify_itsm(True, "\\u2705 zioinfo-mail \\ubc30\\ud3ec \\uc644\\ub8cc")
|
||||||
|
else:
|
||||||
|
notify_itsm(False, "\\u274c zioinfo-mail \\ube4c\\ub4dc \\uc2e4\\ud328")
|
||||||
|
'''
|
||||||
|
# def notify_itsm 함수 앞에 삽입하거나, guardia-docs 블록 찾기
|
||||||
|
if 'guardia-docs' in content:
|
||||||
|
# guardia-docs 블록 찾아서 그 다음에 삽입
|
||||||
|
import re
|
||||||
|
# 마지막 elif 블록 끝(다음 elif 또는 else 직전) 찾기
|
||||||
|
lines = content.split('\n')
|
||||||
|
insert_idx = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'elif repo == "guardia-docs"' in line:
|
||||||
|
# 이 블록의 끝 찾기
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and (lines[j].startswith(' ') or not lines[j].strip()):
|
||||||
|
j += 1
|
||||||
|
insert_idx = j
|
||||||
|
break
|
||||||
|
if insert_idx:
|
||||||
|
lines.insert(insert_idx, mail_block)
|
||||||
|
new_content = '\n'.join(lines)
|
||||||
|
else:
|
||||||
|
new_content = content + mail_block
|
||||||
|
else:
|
||||||
|
new_content = content + mail_block
|
||||||
|
|
||||||
|
with sftp.open('/opt/zioinfo/deploy_server.py', 'w') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
print(' 추가 완료')
|
||||||
|
|
||||||
|
run('zioinfo-mail 확인',
|
||||||
|
"grep -n 'zioinfo-mail' /opt/zioinfo/deploy_server.py | head -5")
|
||||||
|
run('webhook 재시작',
|
||||||
|
'systemctl restart zioinfo-deploy && sleep 2 && systemctl is-active zioinfo-deploy')
|
||||||
|
|
||||||
|
# ── 4. Jenkins 재빌드 ─────────────────────────────────────────
|
||||||
|
print('\n━━ 4. Jenkins 재빌드 ━━')
|
||||||
|
J_A = 'admin:Admin@2026!'
|
||||||
|
_, o, _ = c.exec_command(f'curl -sf -u "{J_A}" http://127.0.0.1:9080/crumbIssuer/api/json 2>/dev/null', timeout=10)
|
||||||
|
try:
|
||||||
|
cd = json.loads(o.read().decode('utf-8','replace').strip())
|
||||||
|
CH = f'{cd["crumbRequestField"]}: {cd["crumb"]}'
|
||||||
|
except:
|
||||||
|
CH = 'Jenkins-Crumb: x'
|
||||||
|
|
||||||
|
run('재빌드 트리거',
|
||||||
|
f'curl -sf -X POST -u "{J_A}" -H "{CH}" http://127.0.0.1:9080/job/zioinfo-mail/build 2>/dev/null && echo "트리거됨"')
|
||||||
|
|
||||||
|
print('\n빌드 대기 (60초)...')
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
run('빌드 #2 결과',
|
||||||
|
f'curl -sf -u "{J_A}" http://127.0.0.1:9080/job/zioinfo-mail/lastBuild/api/json 2>/dev/null | '
|
||||||
|
'python3 -c "import sys,json; d=json.load(sys.stdin); '
|
||||||
|
'print(\'build #\'+str(d[\'number\']),d.get(\'result\',\'진행중\'),\'building:\',d.get(\'building\'))" 2>/dev/null')
|
||||||
|
|
||||||
|
run('빌드 콘솔 (마지막)',
|
||||||
|
f'curl -sf -u "{J_A}" http://127.0.0.1:9080/job/zioinfo-mail/lastBuild/consoleText 2>/dev/null | tail -15')
|
||||||
|
|
||||||
|
sftp.close(); c.close()
|
||||||
|
print('\n=== 완료 ===')
|
||||||
68
workspace/zioinfo-mail/Jenkinsfile
vendored
Normal file
68
workspace/zioinfo-mail/Jenkinsfile
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
environment {
|
||||||
|
SRC = '/opt/mail'
|
||||||
|
STATIC = '/var/www/mail'
|
||||||
|
NOTIFY = "http://127.0.0.1:9001/api/messenger/webhook"
|
||||||
|
}
|
||||||
|
options {
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '5'))
|
||||||
|
timeout(time: 20, unit: 'MINUTES')
|
||||||
|
timestamps()
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Checkout') { steps { checkout scm } }
|
||||||
|
|
||||||
|
stage('Frontend Build') {
|
||||||
|
steps {
|
||||||
|
dir('frontend') {
|
||||||
|
sh 'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps'
|
||||||
|
sh 'npm run build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy') {
|
||||||
|
when {
|
||||||
|
expression { env.GIT_BRANCH ==~ /.*main/ || env.BRANCH_NAME == 'main' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
# Frontend 정적 파일
|
||||||
|
mkdir -p ${STATIC}
|
||||||
|
cp -r dist/. ${STATIC}/
|
||||||
|
|
||||||
|
# Backend 소스
|
||||||
|
rsync -a --exclude=__pycache__ --exclude=.git \
|
||||||
|
--exclude='*.pyc' --exclude='.env' \
|
||||||
|
backend/ ${SRC}/backend/
|
||||||
|
|
||||||
|
# 패키지 설치
|
||||||
|
${SRC}/venv/bin/pip install -r backend/requirements.txt -q
|
||||||
|
|
||||||
|
# 서비스 재기동
|
||||||
|
systemctl restart zioinfo-mail
|
||||||
|
sleep 4
|
||||||
|
systemctl is-active zioinfo-mail || exit 1
|
||||||
|
|
||||||
|
# 헬스체크
|
||||||
|
curl -sf http://localhost:8026/health || exit 1
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
sh """curl -sf -X POST ${NOTIFY} \
|
||||||
|
-H 'Content-Type:application/json' \
|
||||||
|
-d '{"event":"build_result","room":"ops","success":true,"result_summary":"✅ zioinfo-mail 배포 완료 #${BUILD_NUMBER}"}' \
|
||||||
|
2>/dev/null || true"""
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
sh """curl -sf -X POST ${NOTIFY} \
|
||||||
|
-H 'Content-Type:application/json' \
|
||||||
|
-d '{"event":"build_result","room":"ops","success":false,"result_summary":"❌ zioinfo-mail 빌드 실패 #${BUILD_NUMBER}"}' \
|
||||||
|
2>/dev/null || true"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user