From 8ab660cd38418d614c15205f86c21e9df08850ff Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Mon, 1 Jun 2026 21:55:48 +0900 Subject: [PATCH] feat: initial zioinfo-mail webmail system --- .claude/agents/_workspace/fix_report.json | 9 + .claude/agents/_workspace/infra-check.json | 11 + .claude/agents/_workspace/infra_check.py | 52 +++++ .claude/agents/_workspace/verify_report.json | 55 +++++ scripts/check/diff_company_jsx.py | 23 ++ scripts/check/e2e_pipeline_test.py | 64 ++++++ scripts/check/get_mail_error.py | 7 + scripts/check/trigger_jenkins_build.py | 54 +++++ scripts/check/verify_all_systems.py | 133 ++++++++++++ scripts/check/verify_guardia_itsm.py | 50 +++++ scripts/check/verify_sync.py | 31 +++ scripts/check/verify_www_deploy.py | 21 ++ scripts/check/wait_jenkins_build.py | 69 ++++++ scripts/deploy/patch_mail_backend.py | 74 +++++++ scripts/deploy/patch_mail_final.py | 90 ++++++++ scripts/deploy/verify_mail_final.py | 50 +++++ scripts/setup/fix_all_systems.py | 179 ++++++++++++++++ scripts/setup/fix_manager_www.py | 60 ++++++ scripts/setup/push_zioinfo_commits.py | 54 +++++ scripts/setup/setup_mail_cicd.py | 214 +++++++++++++++++++ scripts/setup/setup_mail_repo.py | 175 +++++++++++++++ workspace/zioinfo-mail/Jenkinsfile | 68 ++++++ 22 files changed, 1543 insertions(+) create mode 100644 .claude/agents/_workspace/fix_report.json create mode 100644 .claude/agents/_workspace/infra-check.json create mode 100644 .claude/agents/_workspace/infra_check.py create mode 100644 .claude/agents/_workspace/verify_report.json create mode 100644 scripts/check/diff_company_jsx.py create mode 100644 scripts/check/e2e_pipeline_test.py create mode 100644 scripts/check/get_mail_error.py create mode 100644 scripts/check/trigger_jenkins_build.py create mode 100644 scripts/check/verify_all_systems.py create mode 100644 scripts/check/verify_guardia_itsm.py create mode 100644 scripts/check/verify_sync.py create mode 100644 scripts/check/verify_www_deploy.py create mode 100644 scripts/check/wait_jenkins_build.py create mode 100644 scripts/deploy/patch_mail_backend.py create mode 100644 scripts/deploy/patch_mail_final.py create mode 100644 scripts/deploy/verify_mail_final.py create mode 100644 scripts/setup/fix_all_systems.py create mode 100644 scripts/setup/fix_manager_www.py create mode 100644 scripts/setup/push_zioinfo_commits.py create mode 100644 scripts/setup/setup_mail_cicd.py create mode 100644 scripts/setup/setup_mail_repo.py create mode 100644 workspace/zioinfo-mail/Jenkinsfile diff --git a/.claude/agents/_workspace/fix_report.json b/.claude/agents/_workspace/fix_report.json new file mode 100644 index 00000000..454a0608 --- /dev/null +++ b/.claude/agents/_workspace/fix_report.json @@ -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개 파일 반영" + } +} \ No newline at end of file diff --git a/.claude/agents/_workspace/infra-check.json b/.claude/agents/_workspace/infra-check.json new file mode 100644 index 00000000..9c9b833a --- /dev/null +++ b/.claude/agents/_workspace/infra-check.json @@ -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": "" +} \ No newline at end of file diff --git a/.claude/agents/_workspace/infra_check.py b/.claude/agents/_workspace/infra_check.py new file mode 100644 index 00000000..47c0ac90 --- /dev/null +++ b/.claude/agents/_workspace/infra_check.py @@ -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) diff --git a/.claude/agents/_workspace/verify_report.json b/.claude/agents/_workspace/verify_report.json new file mode 100644 index 00000000..5e2a7889 --- /dev/null +++ b/.claude/agents/_workspace/verify_report.json @@ -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" + ] +} \ No newline at end of file diff --git a/scripts/check/diff_company_jsx.py b/scripts/check/diff_company_jsx.py new file mode 100644 index 00000000..7ced9564 --- /dev/null +++ b/scripts/check/diff_company_jsx.py @@ -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() diff --git a/scripts/check/e2e_pipeline_test.py b/scripts/check/e2e_pipeline_test.py new file mode 100644 index 00000000..1582fa27 --- /dev/null +++ b/scripts/check/e2e_pipeline_test.py @@ -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() diff --git a/scripts/check/get_mail_error.py b/scripts/check/get_mail_error.py new file mode 100644 index 00000000..e197ff79 --- /dev/null +++ b/scripts/check/get_mail_error.py @@ -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() diff --git a/scripts/check/trigger_jenkins_build.py b/scripts/check/trigger_jenkins_build.py new file mode 100644 index 00000000..5d7ca4ed --- /dev/null +++ b/scripts/check/trigger_jenkins_build.py @@ -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() diff --git a/scripts/check/verify_all_systems.py b/scripts/check/verify_all_systems.py new file mode 100644 index 00000000..d055653d --- /dev/null +++ b/scripts/check/verify_all_systems.py @@ -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() diff --git a/scripts/check/verify_guardia_itsm.py b/scripts/check/verify_guardia_itsm.py new file mode 100644 index 00000000..e640b74e --- /dev/null +++ b/scripts/check/verify_guardia_itsm.py @@ -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() diff --git a/scripts/check/verify_sync.py b/scripts/check/verify_sync.py new file mode 100644 index 00000000..17048d4e --- /dev/null +++ b/scripts/check/verify_sync.py @@ -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() diff --git a/scripts/check/verify_www_deploy.py b/scripts/check/verify_www_deploy.py new file mode 100644 index 00000000..9f4e0867 --- /dev/null +++ b/scripts/check/verify_www_deploy.py @@ -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() diff --git a/scripts/check/wait_jenkins_build.py b/scripts/check/wait_jenkins_build.py new file mode 100644 index 00000000..84ec5daa --- /dev/null +++ b/scripts/check/wait_jenkins_build.py @@ -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() diff --git a/scripts/deploy/patch_mail_backend.py b/scripts/deploy/patch_mail_backend.py new file mode 100644 index 00000000..3703530a --- /dev/null +++ b/scripts/deploy/patch_mail_backend.py @@ -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() diff --git a/scripts/deploy/patch_mail_final.py b/scripts/deploy/patch_mail_final.py new file mode 100644 index 00000000..dedb37bb --- /dev/null +++ b/scripts/deploy/patch_mail_final.py @@ -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=== 완료 ===') diff --git a/scripts/deploy/verify_mail_final.py b/scripts/deploy/verify_mail_final.py new file mode 100644 index 00000000..fbe5c971 --- /dev/null +++ b/scripts/deploy/verify_mail_final.py @@ -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() diff --git a/scripts/setup/fix_all_systems.py b/scripts/setup/fix_all_systems.py new file mode 100644 index 00000000..8e7cfa02 --- /dev/null +++ b/scripts/setup/fix_all_systems.py @@ -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=== 수정 완료 ===') diff --git a/scripts/setup/fix_manager_www.py b/scripts/setup/fix_manager_www.py new file mode 100644 index 00000000..8bcf28ff --- /dev/null +++ b/scripts/setup/fix_manager_www.py @@ -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() diff --git a/scripts/setup/push_zioinfo_commits.py b/scripts/setup/push_zioinfo_commits.py new file mode 100644 index 00000000..67adccb6 --- /dev/null +++ b/scripts/setup/push_zioinfo_commits.py @@ -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=== 완료 ===') diff --git a/scripts/setup/setup_mail_cicd.py b/scripts/setup/setup_mail_cicd.py new file mode 100644 index 00000000..c89cad85 --- /dev/null +++ b/scripts/setup/setup_mail_cicd.py @@ -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""" + + GUARDiA zioinfo-mail Webmail CI/CD + false + + + {TOK} + + + + 2 + + + http://127.0.0.1:9003/zio/zioinfo-mail.git + gitea-zio + + + + */main + + false + + + + Jenkinsfile + true + + + + + + +""" + +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 구축 완료 ===') diff --git a/scripts/setup/setup_mail_repo.py b/scripts/setup/setup_mail_repo.py new file mode 100644 index 00000000..fdbf330c --- /dev/null +++ b/scripts/setup/setup_mail_repo.py @@ -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=== 완료 ===') diff --git a/workspace/zioinfo-mail/Jenkinsfile b/workspace/zioinfo-mail/Jenkinsfile new file mode 100644 index 00000000..6282dfc2 --- /dev/null +++ b/workspace/zioinfo-mail/Jenkinsfile @@ -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""" + } + } +}