153 lines
8.7 KiB
Python
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.')
|