fdm/deploy/deploy.py

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()