129 lines
5.5 KiB
Python
129 lines
5.5 KiB
Python
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()
|