#!/usr/bin/env python3 """Prepare only the syly2 UAT directory. Never prints credentials or dump rows.""" import hashlib, json, os, re, secrets, shutil, subprocess, sys, tarfile, zipfile from pathlib import Path os.umask(0o077) ROOT = Path('/home/ubuntu/apps/syly2-uat') assert not (ROOT/'evidence/restore-complete.json').exists(), 'Preparation is for a fresh deployment only; refusing to overwrite initialized UAT configuration' JDK = Path('/home/ubuntu/.local/opt/syly2-build/corretto-8.512.09.1') WAR = Path('/home/ubuntu/builds/syly2/20261029-01/artifacts/syly.war') def digest(path, alg='sha256'): h = hashlib.new(alg) with path.open('rb') as f: for chunk in iter(lambda: f.read(1024*1024), b''): h.update(chunk) return h.hexdigest() def write(path, text, mode=0o600): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) path.chmod(mode) assert digest(WAR) == '7c1e4236d32f2d2106df738d2523f08588d73eab7f70089382e3ae5e7c827e76' dump = ROOT/'incoming/sanya_ch_full.sql' assert digest(dump) == 'd6ec01e162f4b1c92350a0c97bd163b5deb633cdd187f96ff2aadb32e899c5d4' dump.chmod(0o600) mysql = ROOT/'downloads/mysql-5.7.44-runtime.tar.gz' app_only = '--app-only' in sys.argv if not app_only: mysql_manifest=json.loads((ROOT/'downloads/mysql-runtime-manifest.json').read_text()) assert mysql_manifest['source_md5_verified'] assert digest(mysql) == mysql_manifest['runtime_sha256'] tomcat = ROOT/'downloads/apache-tomcat-9.0.123.tar.gz' assert digest(tomcat, 'sha512') == (ROOT/'downloads/apache-tomcat-9.0.123.tar.gz.sha512').read_text().split()[0] ctx = ROOT/'image-context' ctx.mkdir(exist_ok=True) for archive, dest, allowed in [(mysql, ctx/'mysql', ('bin/', 'lib/', 'share/')), (tomcat, ctx/'tomcat', ('bin/', 'lib/', 'conf/'))]: if archive == mysql and app_only: continue if not dest.exists(): dest.mkdir() with tarfile.open(archive, 'r|gz') as tf: for m in tf: name = m.name.split('/',1)[1] if '/' in m.name else '' if name.startswith(allowed) and '/debug/' not in name and not name.endswith('-debug'): assert '..' not in Path(name).parts m.name = name tf.extract(m, dest) if not (ctx/'java').exists(): shutil.copytree(JDK, ctx/'java', symlinks=True) for f in ['Dockerfile.mysql','Dockerfile.app']: shutil.copy2(ROOT/'scripts'/f,ctx/f) for p in [ctx/'tomcat/webapps',ctx/'tomcat/work',ctx/'tomcat/temp',ctx/'tomcat/logs']: p.mkdir(exist_ok=True) p.chmod(0o755) # Image files must be traversable by the runtime uid. for base, dirs, files in os.walk(ctx): Path(base).chmod(0o755) for file in files: p = Path(base)/file if not p.is_symlink(): p.chmod(0o755 if p.stat().st_mode & 0o111 else 0o644) secret_file = ROOT/'secrets/credentials.json' if not secret_file.exists(): creds = {k:secrets.token_urlsafe(24) for k in ['mysql_root','mysql_app','uat_password']} creds['uat_username']='uat_admin' write(secret_file,json.dumps(creds,indent=2)) creds=json.loads(secret_file.read_text()) write(ROOT/'secrets/mysql-client.cnf','[client]\nuser=root\npassword='+creds['mysql_root']+'\nsocket=/tmp/mysql.sock\n') write(ROOT/'runtime/mysql.cnf','''[mysqld] basedir=/opt/mysql datadir=/var/lib/mysql socket=/tmp/mysql.sock pid-file=/tmp/mysql.pid port=3306 bind-address=0.0.0.0 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci lower_case_table_names=1 sql_mode=NO_ENGINE_SUBSTITUTION max_allowed_packet=512M innodb_buffer_pool_size=384M max_connections=60 event_scheduler=OFF local_infile=OFF secure_file_priv=NULL skip_name_resolve=ON skip_networking=ON ''') webapp=ROOT/'runtime/webapp' if not webapp.exists(): webapp.mkdir() with zipfile.ZipFile(WAR) as z: z.extractall(webapp) classes=webapp/'WEB-INF/classes' db='''hibernate.dialect=org.hibernate.dialect.MySQLDialect validationQuery.sqlserver=SELECT 1 jdbc.url=jdbc:mysql://db:3306/sanya_ch?useUnicode=true&characterEncoding=UTF-8&useSSL=false&connectTimeout=5000&socketTimeout=30000 jdbc.username=syly_uat jdbc.password='''+creds['mysql_app']+''' jdbc.dbType=mysql hibernate.hbm2ddl.auto=none scenicWebServiceBase=http://127.0.0.1:9 hotelWebServiceBase=http://127.0.0.1:9 hotelProviceurl=http://127.0.0.1:9 scenicProvice=http://127.0.0.1:9 hotelIp=127.0.0.1 ''' write(classes/'dbconfig.properties',db) write(classes/'spring-mvc-timeTask.xml',''' ''') write(classes/'log4j.properties','''log4j.rootLogger=WARN,A1 log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=[%p][%c]%m%n log4j.logger.org.hibernate.type.descriptor.sql.BasicBinder=OFF log4j.logger.org.hibernate.type.descriptor.sql.BasicExtractor=OFF log4j.logger.org.hibernate.engine.QueryParameters=OFF ''') p=classes/'spring-mvc-hibernate.xml';s=p.read_text();s=s.replace('true','false');s=re.sub(r'','',s);write(p,s) p=classes/'sysConfig.properties';s=p.read_text();s=re.sub(r'^mail\.[^=]+=.*$',lambda m: m.group().split('=')[0]+'=',s,flags=re.M);s=re.sub(r'^ck.userfiles=.*$', 'ck.userfiles=/syly/upload/',s,flags=re.M);write(p,s) # Remove alternate embedded database credentials in the runtime copy. for p in (classes/'zzc').glob('zzc_database.properties*'): write(p,'# Disabled in isolated UAT; runtime uses dbconfig.properties\n') p=webapp/'WEB-INF/web.xml';s=p.read_text() s=s.replace('*','/*') if 'uatGuard' not in s: start=s.index('>',s.index('uatGuarduat.UatGuardFilter uatGuard/*REQUEST\n'''+s[start:] # Eliminate the unused Druid servlet and its embedded credentials. s=re.sub(r'\s*druidStatView.*?','',s,flags=re.S) s=re.sub(r'\s*druidStatView.*?','',s,flags=re.S) write(p,s) login=webapp/'webpage/login/login.jsp' text=login.read_text() if 'isolated-uat-banner' not in text: text=text.replace('','<title>[UAT] ') text=re.sub(r'(<body[^>]*>)',r'\1<div id="isolated-uat-banner" style="position:fixed;top:0;left:0;right:0;z-index:9999;text-align:center;background:#fff3cd;color:#664d03;padding:6px">UAT 隔离测试环境 · 数据来自备份 · 外部上报已禁用</div>',text,count=1) write(login,text) cp=str(classes)+':'+str(webapp/'WEB-INF/lib/*')+':'+str(ctx/'tomcat/lib/servlet-api.jar') subprocess.run([str(JDK/'bin/javac'),'-encoding','UTF-8','-cp',cp,'-d',str(classes),str(ROOT/'scripts/UatGuardFilter.java')],check=True) write(ROOT/'runtime/server.xml','''<Server port="-1"><Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" maxThreads="80" /> <Engine name="Catalina" defaultHost="localhost"><Host name="localhost" appBase="webapps" unpackWARs="false" autoDeploy="false"> <Valve className="org.apache.catalina.valves.RemoteIpValve" internalProxies=".*" remoteIpHeader="x-forwarded-for" protocolHeader="x-forwarded-proto" portHeader="x-forwarded-port" hostHeader="x-forwarded-host"/> </Host></Engine></Service></Server>''') for p in [ROOT/'runtime/mysql-data', ROOT/'runtime/tomcat-work', ROOT/'runtime/tomcat-logs']: p.mkdir(exist_ok=True) tls=ROOT/'secrets/tls';tls.mkdir(exist_ok=True) if not (tls/'server.crt').exists(): subprocess.run(['openssl','req','-x509','-newkey','rsa:3072','-nodes','-keyout',str(tls/'server.key'),'-out',str(tls/'server.crt'),'-days','365','-subj','/CN=syly2-uat','-addext','subjectAltName=IP:192.168.68.223,DNS:localhost'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) write(ROOT/'evidence/runtime-manifest.json',json.dumps({'war_sha256':digest(WAR),'dump_sha256':digest(dump),'dump_bytes':dump.stat().st_size,'mysql_tar_sha256':None if app_only else digest(mysql),'mysql_vendor_md5_verified':not app_only,'tomcat_tar_sha512_verified':True,'tomcat':'9.0.123','java':'Corretto 8.512.09.1','mysql':'5.7.44'},indent=2)) print('Runtime prepared; hashes verified; credentials kept in private files.')