"""CI/CD 전체 구축: 플러그인 → Credential → Jenkinsfile → Job 생성""" import paramiko, sys, time, json 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=30) sftp = client.open_sftp() JENKINS = 'http://127.0.0.1:9080' AUTH = 'admin:Admin@2026!' def run(label, cmd, timeout=120): print(f'\n[{label}]') _, o, e = client.exec_command(cmd, timeout=timeout) out = o.read().decode('utf-8', errors='replace').strip() if out: print(out[:400]) return out def jenkins_script(label, script, timeout=30): """Jenkins Script Console 실행""" print(f'\n[Script: {label}]') # crumb 가져오기 _, o, _ = client.exec_command( f'curl -sf "{JENKINS}/crumbIssuer/api/json" -u {AUTH} 2>/dev/null', timeout=10) crumb_data = o.read().decode('utf-8', errors='replace').strip() try: crumb = json.loads(crumb_data)['crumb'] crumb_field = json.loads(crumb_data)['crumbRequestField'] except: crumb = '' crumb_field = 'Jenkins-Crumb' escaped = script.replace("'", "'\\''") cmd = f"""curl -sf -X POST "{JENKINS}/scriptText" -u {AUTH} \ -H "{crumb_field}: {crumb}" \ --data-urlencode 'script={escaped}' 2>/dev/null""" _, o, _ = client.exec_command(cmd, timeout=timeout) out = o.read().decode('utf-8', errors='replace').strip() if out: print(f' {out[:200]}') return out # ── 1. 플러그인 설치 ────────────────────────────────────────────────────────── run('필수 플러그인 설치', f""" java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} install-plugin \ git workflow-aggregator pipeline-stage-view credentials-binding \ ssh-agent nodejs timestamper ansicolor http_request junit \ 2>&1 | tail -3 echo "플러그인 설치 요청" """) # ── 2. Credential 등록 ──────────────────────────────────────────────────────── jenkins_script('Gitea Credential 등록', """ import jenkins.model.* import com.cloudbees.plugins.credentials.* import com.cloudbees.plugins.credentials.domains.* import com.cloudbees.plugins.credentials.impl.* def store = Jenkins.instance.getExtensionList( "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0]?.getStore() if (store) { // 기존 삭제 def existing = store.getCredentials(Domain.global()).find { it.id == "gitea-zio" } if (existing) store.removeCredentials(Domain.global(), existing) def cred = new UsernamePasswordCredentialsImpl( CredentialsScope.GLOBAL, "gitea-zio", "Gitea zio account", "zio", "Zio@Admin2026!" ) store.addCredentials(Domain.global(), cred) println "gitea-zio credential 등록 완료" } else { println "Store 없음 - credentials 플러그인 미설치" } """) # ── 3. 글로벌 환경변수 설정 ─────────────────────────────────────────────────── jenkins_script('글로벌 환경변수', """ import jenkins.model.* import hudson.slaves.* def instance = Jenkins.instance def globalProps = instance.globalNodeProperties def existing = globalProps.getAll(EnvironmentVariablesNodeProperty.class) EnvironmentVariablesNodeProperty prop if (existing) { prop = existing[0] } else { prop = new EnvironmentVariablesNodeProperty() globalProps.add(prop) } def env = prop.getEnvVars() env.put("ITSM_BASE_URL", "http://127.0.0.1:9001") env.put("GITEA_URL", "http://127.0.0.1:9003") env.put("SERVER_HOST", "101.79.17.164") instance.save() println "환경변수 설정 완료" """) # ── 4. Jenkinsfile 작성 ─────────────────────────────────────────────────────── print('\n[Jenkinsfile 작성]') # zioinfo-web Jenkinsfile (이미 있으면 업데이트) ZIOINFO_JF = '''pipeline { agent any environment { SRC = '/opt/zioinfo/src' JAR_DIR = '/opt/zioinfo/app' STATIC = '/var/www/zioinfo' MVN = '/usr/bin/mvn' NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" } options { buildDiscarder(logRotator(numToKeepStr: '5')) timeout(time: 20, unit: 'MINUTES') timestamps() } stages { stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: scm.branches, userRemoteConfigs: scm.userRemoteConfigs, extensions: [[$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'frontend'], [path: 'backend']] ]] ]) } } stage('Frontend Build') { steps { dir('frontend') { sh 'npm ci --legacy-peer-deps --prefer-offline 2>/dev/null || npm install --legacy-peer-deps' sh 'npm run build' } } } stage('Backend Build') { steps { dir('backend') { sh "${MVN} clean package -DskipTests -q" } } } stage('Deploy') { when { branch 'main' } steps { sh """ cp backend/target/*.jar ${JAR_DIR}/app.jar cp -r backend/src/main/resources/static/. ${STATIC}/ systemctl restart zioinfo sleep 4 systemctl is-active zioinfo || 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-web 배포 완료 #${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-web 빌드 실패 #${BUILD_NUMBER}\\"}' 2>/dev/null || true" } } }''' ITSM_JF = '''pipeline { agent any environment { APP = '/opt/guardia/app' VENV = '/opt/guardia/venv' NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" } options { buildDiscarder(logRotator(numToKeepStr: '5')) timeout(time: 15, unit: 'MINUTES') timestamps() } stages { stage('Checkout') { steps { checkout scm } } stage('Install') { steps { sh "${VENV}/bin/pip install -r requirements.txt -q" } } stage('Test') { when { expression { fileExists('tests/') } } steps { sh "${VENV}/bin/pytest tests/ -q --tb=short --junitxml=test-results.xml || true" junit allowEmptyResults: true, testResults: 'test-results.xml' } } stage('Deploy') { when { branch 'main' } steps { sh """ rsync -a --exclude=__pycache__ --exclude=.git \\ --exclude=rpa_rules.json --exclude='*.pyc' \\ . ${APP}/ systemctl restart guardia sleep 4 systemctl is-active guardia || 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\\":\\"✅ guardia-itsm 배포 완료 #${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\\":\\"❌ guardia-itsm 빌드 실패 #${BUILD_NUMBER}\\"}' 2>/dev/null || true" } } }''' MANAGER_JF = '''pipeline { agent any environment { STATIC = '/var/www/manager' NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" } options { buildDiscarder(logRotator(numToKeepStr: '5')); timeout(time: 15, unit: 'MINUTES') } stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { dir('frontend') { sh 'npm ci 2>/dev/null || npm install' sh 'npm run build' } } } stage('Deploy') { when { branch 'main' } steps { sh """ cp -r frontend/dist/. ${STATIC}/ systemctl restart guardia-manager 2>/dev/null || true """ } } } post { success { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\\"event\\":\\"build_result\\",\\"room\\":\\"ops\\",\\"success\\":true,\\"result_summary\\":\\"✅ guardia-manager 배포 완료\\"}' 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\\":\\"❌ guardia-manager 빌드 실패\\"}' 2>/dev/null || true" } } }''' DOCS_JF = '''pipeline { agent any environment { DOCS = '/var/www/docs' } options { buildDiscarder(logRotator(numToKeepStr: '3')) } stages { stage('Checkout') { steps { checkout scm } } stage('Deploy') { when { branch 'main' } steps { sh 'mkdir -p ${DOCS} && cp -r . ${DOCS}/' } } } }''' # 서버의 독립 repo에 Jenkinsfile 작성 REPO_FILES = [ ('/opt/zioinfo/src/Jenkinsfile', ZIOINFO_JF), ('/opt/guardia/app/Jenkinsfile', ITSM_JF), ('/opt/manager/Jenkinsfile', MANAGER_JF), ('/opt/docs/Jenkinsfile', DOCS_JF), ] for path, content in REPO_FILES: dir_path = '/'.join(path.split('/')[:-1]) client.exec_command(f'mkdir -p {dir_path}') time.sleep(0.05) try: with sftp.open(path, 'w') as f: f.write(content) print(f' OK: {path}') except Exception as ex: print(f' SKIP {path}: {ex}') # 로컬 workspace에도 Jenkinsfile 저장 import os JF_MAP = { r'C:\GUARDiA\workspace\zioinfo-web\Jenkinsfile': ZIOINFO_JF, r'C:\GUARDiA\workspace\guardia-itsm\Jenkinsfile': ITSM_JF, r'C:\GUARDiA\workspace\guardia-manager\Jenkinsfile': MANAGER_JF, r'C:\GUARDiA\workspace\guardia-docs\Jenkinsfile': DOCS_JF, } for path, content in JF_MAP.items(): with open(path, 'w', encoding='utf-8') as f: f.write(content) print(f' local OK: {path.split(chr(92))[-2]}/Jenkinsfile') # ── 5. Jenkins Job 생성 ─────────────────────────────────────────────────────── REPOS = { 'zioinfo-web': ('http://127.0.0.1:9003/zio/zioinfo-web.git', 'Jenkinsfile'), 'guardia-itsm': ('http://127.0.0.1:9003/zio/guardia-itsm.git', 'Jenkinsfile'), 'guardia-manager': ('http://127.0.0.1:9003/zio/guardia-manager.git', 'Jenkinsfile'), 'guardia-docs': ('http://127.0.0.1:9003/zio/guardia-docs.git', 'Jenkinsfile'), } for job_name, (git_url, jf_path) in REPOS.items(): job_xml = f''' {job_name} CI/CD Pipeline false {git_url} gitea-zio */main {jf_path} true ''' # Job 생성 r = run(f'Job 생성: {job_name}', f""" echo '{job_xml}' | java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} \ create-job {job_name} 2>/dev/null \ || java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} \ update-job {job_name} << 'EOF' {job_xml} EOF """) run('Jenkins 플러그인 재시작', f""" java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} restart 2>/dev/null || true sleep 5 systemctl is-active jenkins """) sftp.close() client.close() print('\n=== CI/CD 파이프라인 구축 완료 ===')