Files
syly2/ops/uat-deploy/prepare-runtime.py
T

153 lines
8.7 KiB
Python

#!/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','''<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"><property name="autoStartup" value="false"/></bean>
</beans>''')
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('<prop key="hibernate.show_sql">true</prop>','<prop key="hibernate.show_sql">false</prop>');s=re.sub(r'<property name="maxIdle" value="20"\s*/>','',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('<url-pattern>*</url-pattern>','<url-pattern>/*</url-pattern>')
if 'uatGuard' not in s:
start=s.index('>',s.index('<web-app'))+1
s=s[:start]+'''\n<filter><filter-name>uatGuard</filter-name><filter-class>uat.UatGuardFilter</filter-class></filter>
<filter-mapping><filter-name>uatGuard</filter-name><url-pattern>/*</url-pattern><dispatcher>REQUEST</dispatcher></filter-mapping>\n'''+s[start:]
# Eliminate the unused Druid servlet and its embedded credentials.
s=re.sub(r'<servlet>\s*<servlet-name>druidStatView</servlet-name>.*?</servlet>','',s,flags=re.S)
s=re.sub(r'<servlet-mapping>\s*<servlet-name>druidStatView</servlet-name>.*?</servlet-mapping>','',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>','<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.')