chore: import verified syly2 maintenance baseline without embedded credentials
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
> 历史记录:以下说明对应 2026-10-29 原始 UAT 构建及部署,当前仓库的构建入口和密钥配置请看根目录 README。
|
||||
|
||||
# syly2 UAT 编译工具
|
||||
|
||||
目标主机:`ubuntu@192.168.68.223`。这些工具用于隔离的源码编译,不包含数据库初始化或应用发布步骤。
|
||||
|
||||
## 本次目录
|
||||
|
||||
- 工具链:`/home/ubuntu/.local/opt/syly2-build`
|
||||
- Maven 专用仓库:`/home/ubuntu/.local/share/syly2-build/m2`
|
||||
- 构建记录:`/home/ubuntu/builds/syly2/20261029-01`
|
||||
- 编译副本:上述构建目录下的 `source`
|
||||
- 构建日志:上述构建目录下的 `logs`
|
||||
- 本地传输包、原始文件哈希清单及回收证据:项目 `.local/uat-build/20261029-01`
|
||||
|
||||
## 工具职责
|
||||
|
||||
1. `prepare-toolchain.py`:安装固定版本 Corretto 8.512.09.1 与 Maven 3.9.11,验证官方 SHA-256/SHA-512 校验值;只写入当前用户的专用目录,不修改系统 Java 或 shell 配置。
|
||||
2. `install-private-libs.py RUN_DIRECTORY`:将交接包中的 6 个旧 JAR 注册到专用 Maven 仓库,记录原文件哈希。JODConverter 使用原项目声明的坐标和交接包中的 JAR,其原嵌入坐标不同,未据此升级或替换组件。
|
||||
3. `apply-compatibility.py RUN_DIRECTORY`:校验原始文件哈希,备份并修改编译副本中的三个文件:POM 的 Windows bootclasspath 分号改为 `${path.separator}`,两个统计 Controller 各删除一条未使用的 JDK 内部类 import。保留原换行符,并输出完整 `compatibility.patch`。`pom-linux.patch` 单独展示第一项差异。
|
||||
4. `run-build.sh RUN_DIRECTORY PHASE`:在专用 JDK/仓库下执行 `clean package`,限制 JVM 内存、降低 CPU 调度优先级并记录退出码;显式跳过测试编译及执行。
|
||||
5. `verify-build.py RUN_DIRECTORY PHASE`:校验构建退出码、WAR CRC、关键条目和 Java 字节码版本,核对源码差异,保存产物与哈希,并比较构建前后的共享 UAT 容器状态。需要构建目录中已有 `containers-before.json` 基线记录。
|
||||
|
||||
本次只打包外层工程的 `pom.xml`、`src`、`jar`,不带历史 `target` 或内层重复工程。传输包与展开后的 5,302 个文件均核验了 SHA-256。
|
||||
|
||||
## 重新编译当前 UAT 副本
|
||||
|
||||
```sh
|
||||
ssh ubuntu@192.168.68.223 'bash /home/ubuntu/builds/syly2/20261029-01/scripts/run-build.sh /home/ubuntu/builds/syly2/20261029-01 repeat'
|
||||
```
|
||||
|
||||
`repeat` 对应日志文件名;需要保留每次日志时请使用不同的阶段名。
|
||||
|
||||
仅使用编译工具链时:
|
||||
|
||||
```sh
|
||||
source /home/ubuntu/.local/opt/syly2-build/env.sh
|
||||
java -version
|
||||
mvn -version
|
||||
```
|
||||
|
||||
## 在新的构建目录复现
|
||||
|
||||
先建立当前用户所有、权限为 700 的独立目录,创建 `source`、`logs`、`artifacts`、`scripts` 子目录,复制并核验源码包、原始 `source-manifest.json` 与这些工具。然后执行:
|
||||
|
||||
```sh
|
||||
python3 scripts/prepare-toolchain.py
|
||||
python3 scripts/install-private-libs.py /absolute/path/to/build-run
|
||||
python3 scripts/apply-compatibility.py /absolute/path/to/build-run
|
||||
bash scripts/run-build.sh /absolute/path/to/build-run linux-build
|
||||
```
|
||||
|
||||
源码包包含原有配置,构建目录、传输包和产物均应按受限资料管理。编译通过不代表应用启动、数据库恢复、JSP 运行或省级接口联调通过。
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply only the three verified compilation fixes to an isolated source copy."""
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
os.umask(0o077)
|
||||
run = Path(sys.argv[1]).resolve()
|
||||
source = run / 'source'
|
||||
manifest = json.loads((run / 'source-manifest.json').read_text())
|
||||
changes = {
|
||||
'pom.xml': (
|
||||
b'${java.home}/lib/rt.jar;${java.home}/lib/jce.jar',
|
||||
b'${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar',
|
||||
),
|
||||
'src/main/java/com/zzc/web/htoal/controller/HotelQuarterStatisticController.java': (
|
||||
b'import com.sun.org.apache.regexp.internal.recompile;\r\n', b'',
|
||||
),
|
||||
'src/main/java/com/zzc/web/travel/controller/InlandStatisticPageController.java': (
|
||||
b'import com.sun.org.apache.regexp.internal.recompile;\r\n', b'',
|
||||
),
|
||||
}
|
||||
patches = []
|
||||
for relative, (before, after) in changes.items():
|
||||
path = source / relative
|
||||
data = path.read_bytes()
|
||||
backup = run / 'artifacts/original-build-files' / relative
|
||||
if hashlib.sha256(data).hexdigest() == manifest[relative]:
|
||||
assert data.count(before) == 1, relative
|
||||
if relative.endswith('.java'):
|
||||
assert len(re.findall(rb'\brecompile\b', data)) == 1, relative
|
||||
backup.parent.mkdir(parents=True, exist_ok=True)
|
||||
backup.write_bytes(data)
|
||||
path.write_bytes(data.replace(before, after, 1))
|
||||
elif relative == 'pom.xml' and (run / 'artifacts/pom-original.xml').exists():
|
||||
original = (run / 'artifacts/pom-original.xml').read_bytes()
|
||||
assert hashlib.sha256(original).hexdigest() == manifest[relative]
|
||||
assert data == original.replace(before, after, 1)
|
||||
backup.parent.mkdir(parents=True, exist_ok=True)
|
||||
backup.write_bytes(original)
|
||||
else:
|
||||
assert backup.exists(), 'Unexpected source change: ' + relative
|
||||
assert hashlib.sha256(backup.read_bytes()).hexdigest() == manifest[relative]
|
||||
assert data == backup.read_bytes().replace(before, after, 1)
|
||||
patches.extend(difflib.unified_diff(
|
||||
backup.read_text().splitlines(keepends=True), path.read_text().splitlines(keepends=True),
|
||||
fromfile='a/' + relative, tofile='b/' + relative,
|
||||
))
|
||||
print('Compatibility fix verified:', relative)
|
||||
(run / 'artifacts/compatibility.patch').write_text(''.join(patches))
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Register the six supplied legacy JARs in a dedicated Maven repository."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
os.umask(0o077)
|
||||
run = Path(sys.argv[1]).resolve()
|
||||
source = run / 'source'
|
||||
repo = Path.home() / '.local/share/syly2-build/m2'
|
||||
base = Path.home() / '.local/opt/syly2-build'
|
||||
env = dict(os.environ, JAVA_HOME=str(base / 'corretto-8.512.09.1'))
|
||||
env['PATH'] = env['JAVA_HOME'] + '/bin:' + env['PATH']
|
||||
mvn = base / 'apache-maven-3.9.11/bin/mvn'
|
||||
artifacts = [
|
||||
('com.oracle', 'ojdbc14', '10.2.0.4.0', 'jar/ojdbc14/ojdbc14/10.2.0.4.0/ojdbc14-10.2.0.4.0.jar'),
|
||||
*[('com.ckfinder', name, '2.4', f'jar/ckfinder/ckfinder/{name}/2.4/{name}-2.4.jar') for name in [
|
||||
'CKFinder-updateByAlexander', 'CKFinderPlugin-FileEditor',
|
||||
'CKFinderPlugin-ImageResize', 'CKFinderPlugin-Watermark',
|
||||
]],
|
||||
('com.artofsolving', 'jodconverter', '3.0-beta-4', 'src/main/webapp/WEB-INF/lib/jodconverter-3.0-beta-4.jar'),
|
||||
]
|
||||
evidence = []
|
||||
poms = run / 'artifacts/local-poms'
|
||||
poms.mkdir(exist_ok=True)
|
||||
for group, artifact, version, relative in artifacts:
|
||||
jar = source / relative
|
||||
original_pom = jar.with_suffix('.pom')
|
||||
if original_pom.is_file():
|
||||
pom = original_pom
|
||||
else:
|
||||
project = ET.Element('project', xmlns='http://maven.apache.org/POM/4.0.0')
|
||||
for tag, text in [('modelVersion', '4.0.0'), ('groupId', group), ('artifactId', artifact), ('version', version), ('packaging', 'jar')]:
|
||||
ET.SubElement(project, tag).text = text
|
||||
pom = poms / f'{artifact}-{version}.pom'
|
||||
ET.ElementTree(project).write(pom, encoding='utf-8', xml_declaration=True)
|
||||
log = run / 'logs' / f'install-{artifact}.log'
|
||||
with log.open('w') as output:
|
||||
result = subprocess.run([
|
||||
str(mvn), '-B', '-ntp', f'-Dmaven.repo.local={repo}',
|
||||
'org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file',
|
||||
f'-Dfile={jar}', f'-DpomFile={pom}',
|
||||
f'-DgroupId={group}', f'-DartifactId={artifact}', f'-Dversion={version}', '-Dpackaging=jar',
|
||||
], cwd=run, env=env, stdout=output, stderr=subprocess.STDOUT, timeout=300)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f'Installation failed: {artifact}; see {log}')
|
||||
evidence.append({'coordinates': f'{group}:{artifact}:{version}', 'source': relative, 'sha256': hashlib.sha256(jar.read_bytes()).hexdigest()})
|
||||
print('Installed', evidence[-1]['coordinates'], flush=True)
|
||||
(run / 'artifacts/private-libs.json').write_text(json.dumps(evidence, indent=2) + '\n')
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/pom.xml
|
||||
+++ b/pom.xml
|
||||
@@ -812,7 +812,7 @@
|
||||
</compilerArgs>
|
||||
<compilerArguments>
|
||||
<verbose/>
|
||||
- <bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar</bootclasspath>
|
||||
+ <bootclasspath>${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar</bootclasspath>
|
||||
</compilerArguments>
|
||||
<fork>true</fork>
|
||||
</configuration>
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install the pinned syly2 build toolchain in the current UAT user's home."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
os.umask(0o077)
|
||||
base = Path.home() / '.local/opt/syly2-build'
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
downloads = base / 'downloads'
|
||||
downloads.mkdir(exist_ok=True)
|
||||
packages = [
|
||||
{
|
||||
'name': 'corretto-8.512.09.1',
|
||||
'url': 'https://corretto.aws/downloads/resources/8.512.09.1/amazon-corretto-8.512.09.1-linux-x64.tar.gz',
|
||||
'algorithm': 'sha256',
|
||||
'checksum': '2f2df938886e61c80010ed5af0ea6504ca685280a3b807c5c59ed3ca4fb95be4',
|
||||
'checksum_source': 'https://corretto.aws/downloads/latest_sha256/amazon-corretto-8-x64-linux-jdk.tar.gz',
|
||||
'executable': 'bin/java',
|
||||
},
|
||||
{
|
||||
'name': 'apache-maven-3.9.11',
|
||||
'url': 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.tar.gz',
|
||||
'algorithm': 'sha512',
|
||||
'checksum': 'bcfe4fe305c962ace56ac7b5fc7a08b87d5abd8b7e89027ab251069faebee516b0ded8961445d6d91ec1985dfe30f8153268843c89aa392733d1a3ec956c9978',
|
||||
'checksum_source': 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.tar.gz.sha512',
|
||||
'executable': 'bin/mvn',
|
||||
},
|
||||
]
|
||||
for package in packages:
|
||||
archive = downloads / (package['name'] + '.tar.gz')
|
||||
if not archive.exists():
|
||||
print('Downloading', package['name'], flush=True)
|
||||
partial = archive.with_suffix('.part')
|
||||
with urllib.request.urlopen(package['url'], timeout=90) as response, partial.open('wb') as output:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
output.write(chunk)
|
||||
partial.rename(archive)
|
||||
digest = hashlib.new(package['algorithm'], archive.read_bytes()).hexdigest()
|
||||
if digest != package['checksum']:
|
||||
raise RuntimeError('Checksum mismatch: ' + package['name'])
|
||||
dest = base / package['name']
|
||||
if not dest.exists():
|
||||
staging = base / (package['name'] + '.extract')
|
||||
staging.mkdir(exist_ok=False)
|
||||
with tarfile.open(archive) as tar:
|
||||
tar.extractall(staging, filter='data')
|
||||
children = list(staging.iterdir())
|
||||
if len(children) != 1 or not children[0].is_dir():
|
||||
raise RuntimeError('Unexpected archive layout')
|
||||
children[0].rename(dest)
|
||||
staging.rmdir()
|
||||
if not (dest / package['executable']).is_file():
|
||||
raise RuntimeError('Incomplete installation: ' + str(dest))
|
||||
print('Verified and installed', dest, flush=True)
|
||||
|
||||
jdk = base / packages[0]['name']
|
||||
maven = base / packages[1]['name']
|
||||
env_text = f'export JAVA_HOME="{jdk}"\nexport PATH="{jdk}/bin:{maven}/bin:$PATH"\nexport MAVEN_OPTS="-Xmx1536m -Djava.awt.headless=true"\n'
|
||||
(base / 'env.sh').write_text(env_text)
|
||||
(base / 'manifest.json').write_text(json.dumps(packages, indent=2) + '\n')
|
||||
env = dict(os.environ, JAVA_HOME=str(jdk), PATH=f'{jdk}/bin:{maven}/bin:' + os.environ['PATH'])
|
||||
subprocess.run([str(jdk / 'bin/java'), '-version'], env=env, check=True)
|
||||
subprocess.run([str(maven / 'bin/mvn'), '-version'], env=env, check=True)
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
run_dir="${1:?Usage: run-build.sh RUN_DIRECTORY PHASE}"
|
||||
phase="${2:?Missing build phase}"
|
||||
case "$phase" in
|
||||
*[!a-zA-Z0-9_-]*|'') printf 'Invalid phase\n' >&2; exit 2 ;;
|
||||
esac
|
||||
source "$HOME/.local/opt/syly2-build/env.sh"
|
||||
mkdir -p "$run_dir/logs" "$HOME/.local/share/syly2-build/m2"
|
||||
cd "$run_dir/source"
|
||||
set +e
|
||||
nice -n 10 timeout 1200 mvn --batch-mode --no-transfer-progress \
|
||||
-Dstyle.color=never \
|
||||
-Dmaven.repo.local="$HOME/.local/share/syly2-build/m2" \
|
||||
-Dmaven.test.skip=true -DskipTests=true \
|
||||
clean package > "$run_dir/logs/$phase.log" 2>&1
|
||||
result=$?
|
||||
set -e
|
||||
printf '%s\n' "$result" > "$run_dir/logs/$phase.exit-code"
|
||||
printf 'Build phase %s exited %s; log: %s\n' "$phase" "$result" "$run_dir/logs/$phase.log"
|
||||
exit "$result"
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the WAR, record exact source changes, and compare shared UAT containers."""
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
os.umask(0o077)
|
||||
run = Path(sys.argv[1]).resolve()
|
||||
phase = sys.argv[2]
|
||||
assert (run / 'logs' / (phase + '.exit-code')).read_text().strip() == '0'
|
||||
log = (run / 'logs' / (phase + '.log')).read_text(errors='replace')
|
||||
assert 'BUILD SUCCESS' in log
|
||||
source = run / 'source'
|
||||
war = source / 'target/syly.war'
|
||||
required = [
|
||||
'WEB-INF/web.xml',
|
||||
'WEB-INF/classes/com/zzc/web/system/controller/core/LoginController.class',
|
||||
'WEB-INF/classes/com/zzc/core/interceptors/AuthInterceptor.class',
|
||||
'WEB-INF/classes/com/zzc/web/htoal/controller/HotelMonthlyController.class',
|
||||
]
|
||||
with zipfile.ZipFile(war) as archive:
|
||||
bad = archive.testzip()
|
||||
assert bad is None, bad
|
||||
names = archive.namelist()
|
||||
assert all(name in names for name in required)
|
||||
classes = [name for name in names if name.startswith('WEB-INF/classes/') and name.endswith('.class')]
|
||||
major_versions = collections.Counter(struct.unpack('>H', archive.read(name)[6:8])[0] for name in classes)
|
||||
assert set(major_versions) == {52}, major_versions
|
||||
libs = [name for name in names if name.startswith('WEB-INF/lib/') and name.endswith('.jar')]
|
||||
manifest = json.loads((run / 'source-manifest.json').read_text())
|
||||
changes = []
|
||||
for relative, expected in manifest.items():
|
||||
actual = hashlib.sha256((source / relative).read_bytes()).hexdigest()
|
||||
if expected != actual:
|
||||
changes.append({'path': relative, 'original_sha256': expected, 'build_sha256': actual})
|
||||
expected_changes = {
|
||||
'pom.xml',
|
||||
'src/main/java/com/zzc/web/htoal/controller/HotelQuarterStatisticController.java',
|
||||
'src/main/java/com/zzc/web/travel/controller/InlandStatisticPageController.java',
|
||||
}
|
||||
assert {change['path'] for change in changes} == expected_changes, changes
|
||||
ids = subprocess.check_output(['docker', 'ps', '-aq'], text=True).split()
|
||||
fmt = '{"id":{{json .Id}},"name":{{json .Name}},"startedAt":{{json .State.StartedAt}},"running":{{json .State.Running}},"restartCount":{{json .RestartCount}}}'
|
||||
after = [json.loads(subprocess.check_output(['docker', 'inspect', '--format', fmt, cid], text=True)) for cid in ids]
|
||||
before = json.loads((run / 'containers-before.json').read_text())
|
||||
key = lambda values: {value['id']: value for value in values}
|
||||
unchanged = key(before) == key(after)
|
||||
(run / 'containers-after.json').write_text(json.dumps(after, indent=2) + '\n')
|
||||
artifact = run / 'artifacts/syly.war'
|
||||
shutil.copyfile(war, artifact)
|
||||
artifact.chmod(0o600)
|
||||
evidence = {
|
||||
'host': '192.168.68.223', 'run': str(run), 'phase': phase, 'build_success': True,
|
||||
'artifact': str(artifact), 'artifact_bytes': artifact.stat().st_size,
|
||||
'artifact_sha256': hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
'zip_crc_check': 'passed', 'required_entries_present': required,
|
||||
'compiled_class_files': len(classes), 'class_major_versions': dict(major_versions), 'bundled_jars': len(libs),
|
||||
'main_java_sources': len(list((source / 'src/main/java').rglob('*.java'))),
|
||||
'extra_java_sources_outside_maven_root': len(list((source / 'src/main/com').rglob('*.java'))),
|
||||
'source_manifest_files': len(manifest), 'changed_source_files': changes,
|
||||
'tests_skipped': 'Tests are skipped' in log,
|
||||
'containers_before': len(before), 'containers_after': len(after), 'existing_containers_unchanged': unchanged,
|
||||
'built_at': next((line.split('Finished at:', 1)[1].strip() for line in log.splitlines() if 'Finished at:' in line), None),
|
||||
'elapsed': next((line.split('Total time:', 1)[1].strip() for line in log.splitlines() if 'Total time:' in line), None),
|
||||
'java_version': subprocess.check_output([str(Path.home() / '.local/opt/syly2-build/corretto-8.512.09.1/bin/java'), '-version'], stderr=subprocess.STDOUT, text=True).strip(),
|
||||
}
|
||||
(run / 'artifacts/verification.json').write_text(json.dumps(evidence, indent=2) + '\n')
|
||||
print(json.dumps(evidence, indent=2))
|
||||
if not unchanged:
|
||||
print('NOTICE: Container inventory changed during the build; inspect before/after records.')
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM syly2-uat-base:20261029
|
||||
COPY java/ /opt/java/
|
||||
COPY tomcat/ /opt/tomcat/
|
||||
ENV JAVA_HOME=/opt/java CATALINA_HOME=/opt/tomcat CATALINA_BASE=/opt/tomcat PATH=/opt/java/bin:$PATH
|
||||
USER 1000:1000
|
||||
CMD ["/opt/tomcat/bin/catalina.sh", "run"]
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM docker.jianmuhub.com/library/ubuntu@sha256:98f43fe45e1073f27722eb362248541a6c90599b5ac458de28cee309d92a7199
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libaio1 libnuma1 libncurses5 libtinfo5 libfontconfig1 fonts-dejavu-core tzdata ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
ENV TZ=Asia/Shanghai LANG=C.UTF-8
|
||||
RUN groupadd -g 1000 syly && useradd -u 1000 -g 1000 -M syly
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM syly2-uat-base:20261029
|
||||
COPY mysql/ /opt/mysql/
|
||||
ENV PATH=/opt/mysql/bin:$PATH
|
||||
USER 1000:1000
|
||||
ENTRYPOINT ["/opt/mysql/bin/mysqld", "--defaults-file=/etc/mysql-uat.cnf"]
|
||||
@@ -0,0 +1,16 @@
|
||||
import java.net.*;
|
||||
public class EgressProbe {
|
||||
public static void main(String[] args) throws Exception {
|
||||
String[] hosts = {"db", "10.5.34.202", "1.1.1.1"};
|
||||
int[] ports = {3306, 3306, 443};
|
||||
for (int i=0;i<hosts.length;i++) {
|
||||
boolean connected=false;
|
||||
try (Socket socket=new Socket()) {
|
||||
socket.connect(new InetSocketAddress(hosts[i],ports[i]),1500);
|
||||
connected=true;
|
||||
} catch(Exception expected) {}
|
||||
System.out.println(hosts[i]+":"+ports[i]+"="+(connected?"connected":"blocked"));
|
||||
if ((i==0)!=connected) throw new IllegalStateException("Unexpected UAT network policy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
> 历史记录:以下说明对应 2026-10-29 原始 UAT 构建及部署,当前仓库的构建入口和密钥配置请看根目录 README。
|
||||
|
||||
# syly2 独立 UAT 部署
|
||||
|
||||
目标:`ubuntu@192.168.68.223`。部署目录:`/home/ubuntu/apps/syly2-uat`。
|
||||
|
||||
## 组成
|
||||
|
||||
- 专用 Compose 项目 `syly2-uat`,数据库、Java 应用和 Nginx 各一个容器。
|
||||
- MySQL **5.7.44**,与用户提供的线上版本一致;独立数据目录 `runtime/mysql-data`,不发布数据库端口。
|
||||
- Corretto **8.512.09.1**、Tomcat **9.0.123**;使用已编译并校验的外层工程 WAR。
|
||||
- Ubuntu 基础镜像和 Nginx 来自建木镜像仓库,固定 digest;MySQL 和 Tomcat 从厂商官方 HTTPS 地址下载。
|
||||
- MySQL 官方包核验官方 MD5,再记录 SHA-256。`package-mysql.py` 从已验证的包提取必要运行文件,避免重复传输调试库、静态库和工具。运行包再核对 SHA-256。Tomcat 核验官方 SHA-512。
|
||||
|
||||
## 数据及边界
|
||||
|
||||
使用项目根目录的 `sanya_ch_full.sql`,128,389,799 字节。仅进行流式校验、传输、导入;不把备份正文写入报告。
|
||||
|
||||
原备份 SHA-256:`d6ec01e162f4b1c92350a0c97bd163b5deb633cdd187f96ff2aadb32e899c5d4`。
|
||||
|
||||
备份恢复到此项目自己的 MySQL 实例内,库名仍为 `sanya_ch`。部署脚本只允许首次恢复,拒绝覆盖已有 UAT 数据。恢复后保存所有表的精确行数,不输出用户或业务行。
|
||||
|
||||
创建专用 `uat_admin` 用户及测试组织、角色,保留恢复数据中的原账号及密码。部署过滤器仅允许专用测试用户登录。密码保存在权限 600 的 `secrets/credentials.json`,不写入 Compose 环境变量、日志或报告。
|
||||
|
||||
应用与数据库只连接 `internal` 网络。省级上报和邮件配置改为本地禁用地址,停用定时调度;浏览器 CSP 限制页面向外部地址请求。UAT 过滤器阻断免登录重置密码、修复清库、移动端绕过登录、同步、上传连接器及 Druid 入口,并对其余非公开资源检查 UAT 登录状态。
|
||||
|
||||
这些保护仅作用于部署副本,不能替代原系统安全修复。Web 应用文件以只读方式挂载,附件上传功能未在本次部署中开放。备份数据属于受限资料,UAT 不用于真实省级上报、短信或邮件发送。
|
||||
|
||||
## 首次部署顺序
|
||||
|
||||
脚本固定目标目录;不要在已有实例上重新运行初始化/恢复。
|
||||
|
||||
1. 保存原有容器 ID、启动时间、重启计数;检查端口和空间。建立权限 700 的部署目录及私有子目录。
|
||||
2. 上传脚本、Compose、Nginx 配置以及 SQL 备份;在服务端再次核验 SQL SHA-256。
|
||||
3. 下载官方发行包并校验。执行 `package-mysql.py`,上传生成的运行包和 manifest。
|
||||
4. 构建 `Dockerfile.base`,执行 `prepare-runtime.py`,构建 `Dockerfile.mysql` 和 `Dockerfile.app`。
|
||||
5. 使用 `docker compose -f compose.json run --rm --no-deps db --initialize-insecure` 初始化空数据目录;此时配置仍为 `skip_networking=ON`。
|
||||
6. 只启动 `db`。执行 `database-uat.py restore`,核对版本、表数及精确行数,再执行 `database-uat.py seed` 设置新数据库密码与测试账号。
|
||||
7. 将 `runtime/mysql.cnf` 中 `skip_networking=ON` 改为 `OFF`,只重建本项目 `db` 服务,启动 `app` 和 `web`。
|
||||
8. 检查应用启动、登录页、验证码、登录、菜单和列表;验证阻断规则、外连隔离,并比较原有服务状态。
|
||||
|
||||
## 访问与运维
|
||||
|
||||
- 业务入口:`https://192.168.68.223:8445/syly/`。
|
||||
- 本实例使用独立自签名证书,公钥证书在 `secrets/tls/server.crt`。浏览器首次访问可能提示证书不受信任。
|
||||
- 直接访问上述 UAT HTTPS 地址,不需要 SSH 隧道。验证过程中使用的临时隧道及回环 HTTP 入口已移除。
|
||||
|
||||
```sh
|
||||
cd /home/ubuntu/apps/syly2-uat
|
||||
docker compose -f compose.json ps
|
||||
docker compose -f compose.json logs --tail 100 app
|
||||
docker compose -f compose.json up -d
|
||||
```
|
||||
|
||||
只暂停本应用并保留恢复数据:
|
||||
|
||||
```sh
|
||||
docker compose -f compose.json stop web app db
|
||||
```
|
||||
|
||||
恢复运行使用上面的 `up -d`。不要全局 prune,也不要对其他 Compose 项目执行停启、清理或重建。
|
||||
|
||||
证据位于远程 `evidence/`;本地核验结果回收到项目 `.local/uat-deploy/evidence/`。密码另存私有交接文件。启动与页面验证不代表生产验收、全量报表正确性、历史附件完整性或外部接口联调通过。
|
||||
@@ -0,0 +1,46 @@
|
||||
package uat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.*;
|
||||
|
||||
/** Deployment-only boundary for the isolated restored UAT copy. */
|
||||
public class UatGuardFilter implements Filter {
|
||||
public void init(FilterConfig config) {}
|
||||
public void destroy() {}
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
HttpServletResponse res = (HttpServletResponse) response;
|
||||
String path = req.getServletPath().toLowerCase(Locale.ROOT);
|
||||
res.setHeader("X-Syly-Environment", "isolated-uat");
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
if (path.contains("appservice") || path.contains("repaircontroller") || path.contains("dataupdatecontroller")
|
||||
|| path.contains("druid") || path.equals("/rest") || path.startsWith("/rest/") || path.contains("connector.java")
|
||||
|| path.contains("filesuploadcontroller") || path.contains("/ueditor/jsp/")
|
||||
|| (path.contains("usercontroller") && req.getParameterMap().containsKey("savenewpwdforuser"))) {
|
||||
res.sendError(403, "Disabled in isolated UAT"); return;
|
||||
}
|
||||
boolean login = path.equals("/logincontroller.do") || path.equals("/logincontroller.action");
|
||||
if (login && req.getParameterMap().containsKey("checkuser")
|
||||
&& !"uat_admin".equals(req.getParameter("userName"))) {
|
||||
res.setContentType("application/json;charset=UTF-8");
|
||||
res.getWriter().write("{\"success\":false,\"msg\":\"UAT permits only the dedicated test account\"}"); return;
|
||||
}
|
||||
boolean publicPath = (login && (req.getParameterMap().containsKey("login") || req.getParameterMap().containsKey("checkuser")))
|
||||
|| path.equals("/randcodeimage") || path.equals("/") || path.equals("")
|
||||
|| path.equals("/webpage/login/login.jsp")
|
||||
|| ((path.startsWith("/plug-in/") || path.startsWith("/style/") || path.startsWith("/images/") || path.startsWith("/css/") || path.startsWith("/js/"))
|
||||
&& path.matches(".*\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|swf|map)$"));
|
||||
if (!publicPath) {
|
||||
HttpSession session = req.getSession(false);
|
||||
com.zzc.web.system.pojo.base.Client client = session == null ? null :
|
||||
com.zzc.web.system.manager.ClientManager.getInstance().getClient(session.getId());
|
||||
if (client == null || client.getUser() == null || !"uat_admin".equals(client.getUser().getUserName())) {
|
||||
res.sendError(401, "UAT login required"); return;
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "syly2-uat",
|
||||
"services": {
|
||||
"db": {
|
||||
"image": "syly2-uat-mysql:5.7.44-20261029",
|
||||
"pull_policy": "never",
|
||||
"restart": "unless-stopped",
|
||||
"user": "1000:1000",
|
||||
"read_only": true,
|
||||
"cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"mem_limit": "1g",
|
||||
"cpus": 1.5,
|
||||
"pids_limit": 200,
|
||||
"volumes": ["./runtime/mysql-data:/var/lib/mysql", "./runtime/mysql.cnf:/etc/mysql-uat.cnf:ro", "./secrets/mysql-client.cnf:/run/secrets/mysql-client.cnf:ro"],
|
||||
"tmpfs": ["/tmp:uid=1000,gid=1000,mode=1777,size=128m"],
|
||||
"networks": ["backend"],
|
||||
"healthcheck": {"test": ["CMD", "/opt/mysql/bin/mysqladmin", "--defaults-extra-file=/run/secrets/mysql-client.cnf", "ping", "--silent"], "interval": "10s", "timeout": "5s", "retries": 12},
|
||||
"logging": {"driver":"json-file", "options":{"max-size":"10m","max-file":"3"}}
|
||||
},
|
||||
"app": {
|
||||
"image": "syly2-uat-app:tomcat9.0.123-java8-20261029",
|
||||
"pull_policy": "never",
|
||||
"restart": "unless-stopped",
|
||||
"user": "1000:1000",
|
||||
"read_only": true,
|
||||
"cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"mem_limit": "2g",
|
||||
"cpus": 2,
|
||||
"pids_limit": 300,
|
||||
"environment": {"CATALINA_OPTS":"-Xms256m -Xmx1280m -XX:MaxMetaspaceSize=384m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"},
|
||||
"volumes": ["./runtime/webapp:/opt/tomcat/webapps/syly:ro", "./runtime/server.xml:/opt/tomcat/conf/server.xml:ro", "./runtime/tomcat-work:/opt/tomcat/work", "./runtime/tomcat-logs:/opt/tomcat/logs"],
|
||||
"tmpfs": ["/tmp:uid=1000,gid=1000,mode=1777,size=128m", "/opt/tomcat/temp:uid=1000,gid=1000,mode=1777,size=128m", "/opt/tomcat/conf/Catalina:uid=1000,gid=1000,mode=0755,size=8m"],
|
||||
"networks": ["backend"],
|
||||
"depends_on": {"db":{"condition":"service_healthy"}},
|
||||
"logging": {"driver":"json-file", "options":{"max-size":"10m","max-file":"3"}}
|
||||
},
|
||||
"web": {
|
||||
"image": "docker.jianmuhub.com/library/nginx@sha256:04ac43f568bd51a2e8f37fa0a185ff8f9f4d55c3aa1f1af5008f094d026f950d",
|
||||
"pull_policy": "never",
|
||||
"entrypoint": ["nginx", "-g", "daemon off;"],
|
||||
"user": "1000:1000",
|
||||
"restart": "unless-stopped",
|
||||
"read_only": true,
|
||||
"cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"mem_limit": "128m",
|
||||
"cpus": 0.5,
|
||||
"pids_limit": 50,
|
||||
"ports": ["192.168.68.223:8445:8445"],
|
||||
"volumes": ["./nginx.conf:/etc/nginx/nginx.conf:ro", "./secrets/tls:/etc/nginx/tls:ro"],
|
||||
"tmpfs": ["/var/cache/nginx:uid=1000,gid=1000", "/var/run:uid=1000,gid=1000", "/tmp:uid=1000,gid=1000"],
|
||||
"networks": ["backend", "ingress"],
|
||||
"depends_on": ["app"],
|
||||
"logging": {"driver":"json-file", "options":{"max-size":"5m","max-file":"2"}}
|
||||
}
|
||||
},
|
||||
"networks": {"backend":{"internal":true}, "ingress":{}}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Restore the user-supplied dump only to the new isolated MySQL container."""
|
||||
import json, os, subprocess, sys, time
|
||||
from pathlib import Path
|
||||
os.umask(0o077)
|
||||
ROOT=Path('/home/ubuntu/apps/syly2-uat')
|
||||
DC=['docker','compose','-f',str(ROOT/'compose.json')]
|
||||
cid=subprocess.check_output(DC+['ps','-q','db'],text=True).strip()
|
||||
assert cid
|
||||
label=subprocess.check_output(['docker','inspect','--format','{{index .Config.Labels "com.docker.compose.project"}}',cid],text=True).strip()
|
||||
assert label=='syly2-uat'
|
||||
phase=sys.argv[1]
|
||||
cmd=['docker','exec','-i',cid,'/opt/mysql/bin/mysql','--max-allowed-packet=512M','--default-character-set=utf8mb4','-uroot','-N','-B']
|
||||
if phase=='inventory': cmd.insert(-4,'--defaults-extra-file=/run/secrets/mysql-client.cnf')
|
||||
def query(sql):
|
||||
p=subprocess.run(cmd,input=sql,text=True,capture_output=True)
|
||||
if p.returncode:
|
||||
(ROOT/'evidence/db-last-error.log').write_text(p.stderr)
|
||||
raise RuntimeError('SQL failed; private error log saved')
|
||||
return p.stdout.strip()
|
||||
if phase=='restore':
|
||||
assert not (ROOT/'evidence/restore-complete.json').exists(), 'Refusing repeated restore over existing UAT'
|
||||
assert query("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name='sanya_ch'")=='0'
|
||||
started=time.time()
|
||||
with (ROOT/'incoming/sanya_ch_full.sql').open('rb') as inp,(ROOT/'evidence/restore.log').open('wb') as log:
|
||||
p=subprocess.run(cmd,stdin=inp,stdout=log,stderr=log)
|
||||
if p.returncode: raise RuntimeError('Restore failed; inspect private log')
|
||||
tables=query("SELECT table_name FROM information_schema.tables WHERE table_schema='sanya_ch' AND table_type='BASE TABLE' ORDER BY table_name").splitlines()
|
||||
counts={t:int(query('SELECT COUNT(*) FROM sanya_ch.`'+t+'`')) for t in tables}
|
||||
info={'mysql_version':query('SELECT VERSION()'),'tables':len(tables),'rows':sum(counts.values()),'row_counts':counts,'seconds':round(time.time()-started,1)}
|
||||
(ROOT/'evidence/restore-complete.json').write_text(json.dumps(info,indent=2))
|
||||
cols=query("SELECT column_name FROM information_schema.columns WHERE table_schema='sanya_ch' AND table_name='t_s_base_user'").splitlines()
|
||||
(ROOT/'evidence/base-user-columns.json').write_text(json.dumps(cols))
|
||||
print(json.dumps({k:v for k,v in info.items() if k!='row_counts'}))
|
||||
elif phase=='seed':
|
||||
assert (ROOT/'evidence/restore-complete.json').exists()
|
||||
assert query("SELECT COUNT(*) FROM sanya_ch.t_s_base_user WHERE username='uat_admin'")=='0', 'Refusing to change an existing account'
|
||||
creds=json.loads((ROOT/'secrets/credentials.json').read_text())
|
||||
jdk='/home/ubuntu/.local/opt/syly2-build/corretto-8.512.09.1/bin/'
|
||||
helper=ROOT/'scripts/HashUat.java'
|
||||
helper.write_text('import java.io.*; import org.mindrot.jbcrypt.BCrypt; public class HashUat { public static void main(String[] a) throws Exception { String p=new BufferedReader(new InputStreamReader(System.in)).readLine(); System.out.print(BCrypt.hashpw(p,BCrypt.gensalt(12))); }}')
|
||||
cp=str(ROOT/'runtime/webapp/WEB-INF/lib/*')
|
||||
subprocess.run([jdk+'javac','-cp',cp,str(helper)],check=True)
|
||||
hashed=subprocess.check_output([jdk+'java','-cp',str(ROOT/'scripts')+':'+cp,'HashUat'],input=creds['uat_password'],text=True)
|
||||
columns=json.loads((ROOT/'evidence/base-user-columns.json').read_text())
|
||||
added=[]
|
||||
if 'password_update_date' not in columns:
|
||||
query('ALTER TABLE sanya_ch.t_s_base_user ADD COLUMN password_update_date datetime NULL;')
|
||||
added.append('t_s_base_user.password_update_date')
|
||||
# All IDs belong to this synthetic UAT fixture, never to a restored account.
|
||||
uid='01a186a4e1407e328d2bfe6687e68b01'
|
||||
org='01a186a4e1407e328d2bfe6687e68b02'
|
||||
role='01a186a4e1407e328d2bfe6687e68b03'
|
||||
sql=f"""USE sanya_ch;
|
||||
START TRANSACTION;
|
||||
INSERT INTO t_s_depart(ID,departname,description,org_code,org_type) VALUES('{org}','UAT验证组织','仅用于隔离测试','UAT','1');
|
||||
INSERT INTO t_s_base_user(ID,username,password,realname,status,activitiSync,area,login_fail_count,is_locked,password_update_date) VALUES('{uid}','uat_admin','{hashed}','UAT验证管理员',1,0,'460200',0,0,NOW());
|
||||
INSERT INTO t_s_user(id,create_date,create_by,create_name) VALUES('{uid}',NOW(),'uat-deploy','UAT deployment');
|
||||
INSERT INTO t_s_user_org(ID,user_id,org_id) VALUES(REPLACE(UUID(),'-',''),'{uid}','{org}');
|
||||
INSERT INTO t_s_role(ID,rolecode,rolename) VALUES('{role}','uat_admin','UAT验证角色');
|
||||
INSERT INTO t_s_role_user(ID,roleid,userid) VALUES(REPLACE(UUID(),'-',''),'{role}','{uid}');
|
||||
INSERT INTO t_s_role_function(ID,functionid,roleid) SELECT REPLACE(UUID(),'-',''),ID,'{role}' FROM t_s_function;
|
||||
COMMIT;
|
||||
CREATE USER 'syly_uat'@'%' IDENTIFIED BY '{creds['mysql_app']}';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, SHOW VIEW ON sanya_ch.* TO 'syly_uat'@'%';
|
||||
ALTER USER 'root'@'localhost' IDENTIFIED BY '{creds['mysql_root']}';
|
||||
"""
|
||||
query(sql)
|
||||
(ROOT/'evidence/uat-seed.json').write_text(json.dumps({'username':'uat_admin','user_id':uid,'org_id':org,'role_id':role,'added_columns':added,'existing_account_passwords_changed':False,'business_rows_changed':False},indent=2))
|
||||
print('Dedicated UAT account added; original accounts preserved; DB credentials initialized.')
|
||||
else:
|
||||
raise ValueError(phase)
|
||||
@@ -0,0 +1,33 @@
|
||||
worker_processes 1;
|
||||
pid /var/run/nginx.pid;
|
||||
events { worker_connections 256; }
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
error_log /dev/stderr warn;
|
||||
client_max_body_size 10m;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 60s;
|
||||
server {
|
||||
listen 8445 ssl;
|
||||
server_name _;
|
||||
absolute_redirect off;
|
||||
ssl_certificate /etc/nginx/tls/server.crt;
|
||||
ssl_certificate_key /etc/nginx/tls/server.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src 'self'; form-action 'self'; frame-src 'self'; base-uri 'self'" always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
location = / { return 302 /syly/; }
|
||||
location = /syly/ { return 302 /syly/loginController.do?login; }
|
||||
location /syly/ {
|
||||
proxy_pass http://app:8080;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port 8445;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
location / { return 404; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the official archive and transfer only required runtime files."""
|
||||
import hashlib,json,re,tarfile
|
||||
from pathlib import Path
|
||||
root=Path(__file__).resolve().parents[1]/'.local/uat-deploy'
|
||||
archive=root/'mysql-5.7.44.tar.gz'
|
||||
def hashfile(p, alg):
|
||||
h=hashlib.new(alg)
|
||||
with p.open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
return h.hexdigest()
|
||||
expected=re.search(r'\b[0-9a-fA-F]{32}\b',(root/'mysql-5.7.44.tar.gz.md5').read_text()).group().lower()
|
||||
assert hashfile(archive,'md5')==expected
|
||||
out=root/'mysql-5.7.44-runtime.tar.gz'
|
||||
files=[]
|
||||
with tarfile.open(archive,'r|gz') as src,tarfile.open(out,'w:gz',compresslevel=6) as dst:
|
||||
for m in src:
|
||||
rel=m.name.split('/',1)[1] if '/' in m.name else ''
|
||||
take=rel in ['bin/mysqld','bin/mysql','bin/mysqladmin','bin/mysqldump'] or rel.startswith(('lib/','share/'))
|
||||
if take and '/debug/' not in rel and not rel.endswith('.a'):
|
||||
assert '..' not in Path(rel).parts
|
||||
dst.addfile(m,src.extractfile(m) if m.isfile() else None)
|
||||
files.append({'path':rel,'bytes':m.size})
|
||||
manifest={'source_url':'https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz','source_md5_verified':expected,'source_sha256':hashfile(archive,'sha256'),'runtime_sha256':hashfile(out,'sha256'),'runtime_bytes':out.stat().st_size,'files':files}
|
||||
(root/'mysql-runtime-manifest.json').write_text(json.dumps(manifest,indent=2))
|
||||
print(json.dumps({k:v for k,v in manifest.items() if k!='files'},indent=2))
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/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.')
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""No business mutations; inspect only status and non-sensitive response shape."""
|
||||
import json, ssl, urllib.request, urllib.error
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
evidence=ROOT/'.local/uat-deploy/evidence'
|
||||
ctx=ssl.create_default_context(cafile=str(evidence/'server.crt'))
|
||||
base='https://192.168.68.223:8445/syly/'
|
||||
results=[]
|
||||
cases=[
|
||||
('login-page','',200,'formLogin'),
|
||||
('login-controller','loginController.do?login',200,'formLogin'),
|
||||
('captcha','randCodeImage',200,None),
|
||||
('anonymous-business','hotelController.do?datagrid',401,None),
|
||||
('repair-disabled','repairController.do?uat_probe',403,None),
|
||||
('password-reset-disabled','userController.do?savenewpwdforuser',403,None),
|
||||
('mobile-disabled','appservice.do?uat_probe',403,None),
|
||||
('sync-disabled','hoteldataupdatecontroller.do?uat_probe',403,None),
|
||||
('druid-disabled','webpage/system/druid/index.html',403,None),
|
||||
('upload-disabled','plug-in/ckfinder/core/connector/java/connector.java',403,None),
|
||||
('rest-disabled','rest/uat_probe',403,None),
|
||||
('backup-account-login-disabled','loginController.do?checkuser&userName=admin',200,'UAT permits only'),
|
||||
]
|
||||
for name,path,expected,marker in cases:
|
||||
req=urllib.request.Request(base+path,headers={'User-Agent':'syly2-uat-deployment-verification'})
|
||||
try: res=urllib.request.urlopen(req,context=ctx,timeout=45)
|
||||
except urllib.error.HTTPError as e: res=e
|
||||
body=res.read(1024*1024)
|
||||
ok=res.status==expected and (marker is None or marker.encode() in body)
|
||||
if name=='captcha': ok=ok and body.startswith(b'\xff\xd8')
|
||||
results.append({'check':name,'status':res.status,'expected_status':expected,'passed':ok,'bytes':len(body)})
|
||||
(evidence/'http-checks.json').write_text(json.dumps(results,indent=2))
|
||||
print(json.dumps(results,indent=2))
|
||||
assert all(x['passed'] for x in results)
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
import json,subprocess
|
||||
from pathlib import Path
|
||||
root=Path('/home/ubuntu/apps/syly2-uat')
|
||||
fmt='{"id":{{json .Id}},"name":{{json .Name}},"startedAt":{{json .State.StartedAt}},"running":{{json .State.Running}},"restartCount":{{json .RestartCount}}}'
|
||||
before=[json.loads(x) for x in (root/'evidence/containers-before.jsonl').read_text().splitlines()]
|
||||
after=[]
|
||||
for old in before:
|
||||
after.append(json.loads(subprocess.check_output(['docker','inspect','--format',fmt,old['id']],text=True)))
|
||||
images={}
|
||||
runtime=[]
|
||||
for service in ['db','app','web']:
|
||||
cid=subprocess.check_output(['docker','compose','-f',str(root/'compose.json'),'ps','-q',service],text=True).strip()
|
||||
assert cid, service
|
||||
state=json.loads(subprocess.check_output(['docker','inspect','--format',fmt,cid],text=True))
|
||||
state['service']=service
|
||||
state['image']=subprocess.check_output(['docker','inspect','--format','{{.Image}}',cid],text=True).strip()
|
||||
state['network_names']=json.loads(subprocess.check_output(['docker','inspect','--format','{{json .NetworkSettings.Networks}}',cid],text=True)).keys()
|
||||
state['network_names']=list(state['network_names'])
|
||||
runtime.append(state)
|
||||
info={'original_container_count':len(before),'original_containers_unchanged':before==after,'runtime':runtime}
|
||||
(root/'evidence/containers-after.json').write_text(json.dumps(after,indent=2))
|
||||
(root/'evidence/runtime-verification.json').write_text(json.dumps(info,indent=2))
|
||||
print(json.dumps(info,indent=2))
|
||||
assert before==after
|
||||
assert all(x['running'] for x in runtime)
|
||||
assert all(x['network_names']==['syly2-uat_backend'] for x in runtime if x['service'] in ['app','db'])
|
||||
Reference in New Issue
Block a user