import base64 import getpass import hashlib import hmac import json import posixpath import shlex import time import urllib.parse import urllib.error import urllib.request import uuid from datetime import datetime, timezone from pathlib import Path import paramiko HOST = "47.108.78.12" DOMAIN = "odacm.cn" ROOT = Path(__file__).resolve().parent.parent def dns_request(action, key, secret, **fields): params = { "Format": "JSON", "Version": "2015-01-09", "AccessKeyId": key, "SignatureMethod": "HMAC-SHA1", "SignatureVersion": "1.0", "SignatureNonce": str(uuid.uuid4()), "Timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "Action": action, **fields, } quote = lambda value: urllib.parse.quote(str(value), safe="~") canonical = "&".join(f"{quote(k)}={quote(v)}" for k, v in sorted(params.items())) message = "POST&%2F&" + quote(canonical) params["Signature"] = base64.b64encode( hmac.new((secret + "&").encode(), message.encode(), hashlib.sha1).digest() ).decode() request = urllib.request.Request( "https://alidns.aliyuncs.com/", data=urllib.parse.urlencode(params).encode(), ) try: with urllib.request.urlopen(request, timeout=30) as response: result = json.load(response) except urllib.error.HTTPError as error: result = json.load(error) raise RuntimeError("DNS API: " + result.get("Code", str(error.code))) from None if "Code" in result: raise RuntimeError(result["Code"]) return result def main(): password = getpass.getpass("SSH password: ") key = getpass.getpass("Aliyun AccessKey ID: ") secret = getpass.getpass("Aliyun AccessKey secret: ") client = paramiko.SSHClient() client.load_host_keys(str(Path.home() / ".ssh" / "known_hosts")) client.connect(HOST, username="root", password=password, timeout=20) password = None def run(command, timeout=300): print("REMOTE:", command, flush=True) _, stdout, stderr = client.exec_command(command, timeout=timeout) output = stdout.read().decode("utf-8", "replace") error = stderr.read().decode("utf-8", "replace") print(output + error, flush=True) if stdout.channel.recv_exit_status() != 0: raise RuntimeError("Remote command failed") return output try: result = dns_request( "DescribeDomainRecords", key, secret, DomainName=DOMAIN, PageSize="500" ) records = result.get("DomainRecords", {}).get("Record", []) relevant = [r for r in records if r["RR"] == "@" and r["Type"] in ("A", "AAAA", "CNAME")] print("DNS:", json.dumps([ {k: r.get(k) for k in ("RR", "Type", "Value", "Status")} for r in relevant ]), flush=True) if any(r["Type"] != "A" or r["Value"] != HOST or r["Status"] != "ENABLE" for r in relevant): raise RuntimeError("Existing DNS conflicts; no records changed") if not relevant: dns_request("AddDomainRecord", key, secret, DomainName=DOMAIN, RR="@", Type="A", Value=HOST, TTL="600") print("DNS A record created", flush=True) key = secret = None stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") release = "/var/www/fdm/releases/" + stamp run("test ! -e /etc/nginx/conf.d/fdm-odacm.conf && test ! -e /etc/nginx/conf.d/fdm-odacm-ssl.conf && test ! -e /var/www/fdm/current") run(f"install -d -m 755 {release} /var/lib/letsencrypt/.well-known/acme-challenge /var/backups/fdm/{stamp}") run(f"cp -a /etc/nginx/conf.d /var/backups/fdm/{stamp}/") sftp = client.open_sftp() files = sorted((ROOT / "dist").rglob("*")) directories = sorted([p for p in files if p.is_dir()], key=lambda p: len(p.parts)) for path in directories: sftp.mkdir(posixpath.join(release, path.relative_to(ROOT / "dist").as_posix()), mode=0o755) for path in files: if path.is_file(): target = posixpath.join(release, path.relative_to(ROOT / "dist").as_posix()) sftp.put(str(path), target) sftp.chmod(target, 0o644) print("Release uploaded:", release, flush=True) sftp.put(str(ROOT / "deploy/nginx-http.conf"), "/etc/nginx/conf.d/fdm-odacm.conf") run(f"ln -s {release} /var/www/fdm/current && nginx -t && systemctl reload nginx") run("curl -fsS -o /dev/null -w 'upstream=%{http_code}\\n' http://127.0.0.1:9999/") for attempt in range(30): answer = run("dig +short @223.5.5.5 odacm.cn A").strip() if HOST in answer.splitlines(): break time.sleep(20) else: raise RuntimeError("DNS propagation pending; certificate not requested") run("certbot certonly --webroot -w /var/lib/letsencrypt -d odacm.cn --cert-name odacm.cn --non-interactive --agree-tos --register-unsafely-without-email --server https://acme-v02.api.letsencrypt.org/directory", timeout=420) sftp.put(str(ROOT / "deploy/nginx-https.conf"), "/etc/nginx/conf.d/fdm-odacm-ssl.conf") sftp.close() run("nginx -t && systemctl reload nginx && systemctl enable nginx") run("curl -fsS --resolve odacm.cn:443:127.0.0.1 -o /dev/null -w 'https=%{http_code}\\n' https://odacm.cn/ && openssl x509 -in /etc/letsencrypt/live/odacm.cn/fullchain.pem -noout -issuer -subject -dates") print("DEPLOYMENT COMPLETE", flush=True) finally: client.close() if __name__ == "__main__": main()