Files
kefu/im/deploy/baota_bind_go_domain.py
T
2026-09-03 08:38:17 +08:00

218 lines
10 KiB
Python

#!/usr/bin/env python3
"""Move this site's domain and HTTPS gateway into its actual BaoTa Go project.
This migrates only panel metadata/configuration. It never deletes site files,
restarts the Go process, or touches the application's MySQL database.
"""
import json
import os
import shutil
import sqlite3
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
PANEL = Path('/www/server/panel')
DOMAIN = 'im.bchongw.com'
PROJECT = 'xingyu_im'
OLD_CONF = PANEL / 'vhost/nginx/html_im.bchongw.com.conf'
GO_CONF = PANEL / 'vhost/nginx/go_xingyu_im.conf'
ROUTES = PANEL / 'vhost/nginx/extension/xingyu_im/xingyu_routes.conf'
CERT = PANEL / 'vhost/cert/xingyu_im'
SITE_DB = PANEL / 'data/db/site.db'
def run(*args):
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
if result.returncode:
raise RuntimeError('{} failed: {}'.format(args[0], result.stderr.strip()))
def write_private(path, text, mode=0o600):
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
with os.fdopen(fd, 'w', encoding='utf-8') as target:
target.write(text)
def check_public():
for path, content in (('/healthz', '"status":"ok"'), ('/admin/', '星遇社交运营中心'),
('/app/', '星遇社交'), ('/app/static/favicon.svg', '<svg')):
for attempt in range(20):
try:
with urllib.request.urlopen('https://' + DOMAIN + path, timeout=5) as response:
assert response.status == 200
assert content in response.read().decode('utf-8')
break
except Exception:
if attempt == 19:
raise RuntimeError('Public verification failed: ' + path)
time.sleep(0.25)
def verify():
model = GoProject()
get = public.dict_obj()
get.project_name = PROJECT
project = model.get_project_find(PROJECT)
domains = model.project_get_domain(get)
assert any(item['name'] == DOMAIN and int(item['port']) == 80 for item in domains)
assert int(project['project_config']['bind_extranet']) == 1
assert model.get_project_run_state(project_name=PROJECT)
ssl_info = model.get_ssl_end_date(PROJECT)
assert isinstance(ssl_info, dict) and DOMAIN in ssl_info.get('dns', [])
ssl_get = public.dict_obj()
ssl_get.siteName = PROJECT
ssl_state = panelSite.panelSite().GetSSL(ssl_get)
assert ssl_state['status'] and ssl_state['httpTohttps']
assert ROUTES.is_file() and str(ROUTES.parent) in GO_CONF.read_text()
check_public()
print(json.dumps({
'project': PROJECT, 'domains': [{'name': d['name'], 'port': d['port']} for d in domains],
'external_mapping': True, 'running': True, 'ssl_enabled': ssl_state['status'],
'force_https': ssl_state['httpTohttps'], 'certificate_expires': ssl_info['notAfter'],
'nginx_config': str(GO_CONF), 'routes_config': str(ROUTES),
}, ensure_ascii=False), flush=True)
def migrate():
model = GoProject()
project = model.get_project_find(PROJECT)
assert project and not project['project_config']['domains']
assert OLD_CONF.is_file() and not GO_CONF.exists() and not ROUTES.exists() and not CERT.exists()
html_site = public.M('sites').where('name=? AND project_type=?', (DOMAIN, 'html')).find()
assert html_site and html_site['path'] == '/www/wwwroot/im.bchongw.com'
domain_rows = public.M('domain').where('name=?', (DOMAIN,)).select()
assert len(domain_rows) == 1 and domain_rows[0]['pid'] == html_site['id'] and int(domain_rows[0]['port']) == 80
assert public.M('domain').where('pid=?', (html_site['id'],)).count() == 1
check_public()
backup = Path('/www/backup/xingyu-go-domain-' + time.strftime('%Y%m%d-%H%M%S'))
backup.mkdir(mode=0o700, parents=True, exist_ok=False)
os.chmod(str(backup), 0o700)
shutil.copy2(str(OLD_CONF), str(backup / OLD_CONF.name))
with sqlite3.connect(str(SITE_DB)) as db:
with sqlite3.connect(str(backup / 'site.db')) as out:
db.backup(out)
original_go = public.M('sites').where('id=?', (project['id'],)).find()
write_private(backup / 'original-records.json', json.dumps({
'html_site': html_site, 'go_site': original_go, 'domains': domain_rows,
}, ensure_ascii=False))
# Custom routes live in BaoTa's supported per-project extension directory;
# saving/rebuilding the Go project's main Nginx file retains this include.
old_text = OLD_CONF.read_text()
start = old_text.index(' # XINGYU_H5_START')
end = old_text.index('\n location / {', start)
routes = '''# Xingyu frontend routes and WebSocket gateway. Keep in this extension.
server_tokens off;
client_max_body_size 16m;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header X-Frame-Options DENY always;
location ^~ /.well-known/acme-challenge/ {
root /www/wwwroot/im.bchongw.com;
default_type text/plain;
try_files $uri =404;
}
''' + old_text[start:end] + '\n'
ROUTES.parent.mkdir(parents=True, exist_ok=True)
write_private(ROUTES, routes, 0o644)
CERT.mkdir(mode=0o700)
for filename in ('fullchain.pem', 'privkey.pem'):
source = Path('/etc/letsencrypt/live') / DOMAIN / filename
assert source.is_file()
os.symlink(str(source), str(CERT / filename))
rewrite = PANEL / 'vhost/rewrite/go_xingyu_im.conf'
if not rewrite.exists():
write_private(rewrite, '# Project-specific rewrite rules; frontend routes are in the extension directory.\n', 0o644)
well_known = PANEL / 'vhost/nginx/well-known/xingyu_im.conf'
if not well_known.exists():
write_private(well_known, '# ACME routes are in extension/xingyu_im/xingyu_routes.conf.\n', 0o644)
ssl = '''ssl_certificate /www/server/panel/vhost/cert/xingyu_im/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/xingyu_im/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:IMSSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
error_page 497 https://$host$request_uri;
#HTTP_TO_HTTPS_START
if ($server_port !~ 443) {
return 301 https://$host$request_uri;
}
#HTTP_TO_HTTPS_END'''
template = (PANEL / 'vhost/template/nginx/go_http.conf').read_text()
body = template.format(
listen_ports='listen 80;\n listen 443 ssl;\n http2 on;', domains=DOMAIN,
site_path=project['path'], project_name=PROJECT, panel_path=str(PANEL),
log_path='/www/wwwlogs', url='http://127.0.0.1:18888', host='$host', ssl_config=ssl,
)
body = body.replace('proxy_set_header X-Scheme $scheme;',
'proxy_set_header X-Scheme $scheme;\n proxy_set_header X-Forwarded-Proto $scheme;')
from mod.base.web_conf import ng_ext
body = ng_ext.set_extension_by_config(PROJECT, body)
stage = GO_CONF.with_name(GO_CONF.name + '.prepared')
write_private(stage, body, 0o644)
metadata_changed = False
files_switched = False
try:
os.rename(str(OLD_CONF), str(backup / 'original-html-active.conf'))
files_switched = True
os.rename(str(stage), str(GO_CONF))
run('/www/server/nginx/sbin/nginx', '-t')
new_config = dict(project['project_config'])
new_config['domains'] = [DOMAIN + ':80']
new_config['bind_extranet'] = 1
with sqlite3.connect(str(SITE_DB), timeout=10) as db:
db.execute('BEGIN IMMEDIATE')
assert db.execute('SELECT pid FROM domain WHERE id=?', (domain_rows[0]['id'],)).fetchone()[0] == html_site['id']
db.execute('UPDATE domain SET pid=? WHERE id=? AND pid=?',
(project['id'], domain_rows[0]['id'], html_site['id']))
db.execute('UPDATE sites SET project_config=?, ps=? WHERE id=? AND project_type=?',
(json.dumps(new_config), '星遇 IM · im.bchongw.com · 管理端 /admin/ · H5 /app/', project['id'], 'Go'))
# Merge the now-redundant HTML project record only. All its files,
# release symlinks, certificate links, and backups remain intact.
db.execute('DELETE FROM sites WHERE id=? AND name=? AND project_type=?',
(html_site['id'], DOMAIN, 'html'))
metadata_changed = True
run('/etc/init.d/nginx', 'reload')
verify()
public.WriteLog('项目管理', '域名 im.bchongw.com、HTTPS、管理端和 H5 网关已合并至 Go 项目 xingyu_im;备份:' + str(backup))
print('DOMAIN_MIGRATION_BACKUP=' + str(backup), flush=True)
except Exception:
if metadata_changed:
with sqlite3.connect(str(SITE_DB), timeout=10) as db:
db.execute('BEGIN IMMEDIATE')
db.execute('UPDATE domain SET pid=? WHERE id=?', (html_site['id'], domain_rows[0]['id']))
db.execute('UPDATE sites SET project_config=?, ps=? WHERE id=?',
(original_go['project_config'], original_go['ps'], project['id']))
keys = list(html_site)
db.execute('INSERT INTO sites ({}) VALUES ({})'.format(','.join(keys), ','.join('?' for _ in keys)),
[html_site[k] for k in keys])
if files_switched:
if GO_CONF.exists():
os.rename(str(GO_CONF), str(backup / 'failed-go.conf'))
os.rename(str(backup / 'original-html-active.conf'), str(OLD_CONF))
run('/www/server/nginx/sbin/nginx', '-t')
run('/etc/init.d/nginx', 'reload')
print('ROLLED_BACK; backup=' + str(backup), flush=True)
raise
if __name__ == '__main__':
assert os.geteuid() == 0 and PANEL.is_dir()
os.chdir(str(PANEL))
sys.path.insert(0, str(PANEL))
sys.path.insert(0, str(PANEL / 'class'))
import public
import panelSite
from projectModel.goModel import main as GoProject
action = sys.argv[1] if len(sys.argv) > 1 else 'verify'
if action not in ('migrate', 'verify'):
raise SystemExit('Usage: baota_bind_go_domain.py [migrate|verify]')
{'migrate': migrate, 'verify': verify}[action]()