Files
ykxmz/_build/lssh.py
T
xmz7891 0ad3f648c6 修上一轮清理留下的副作用:占位符把运维脚本弄连不上了
上一个提交把仓库里的服务器 IP 换成了占位符,但**那些脚本本身要用这个 IP
去连服务器** —— 换掉之后它们就跑不起来了。这个提交把取值方式理清楚。

== 取值统一走 _build/lssh.py ==

    YKXMZ_LS_HOST 环境变量
    或 _build/.lightsail_host(一行 IP,已 gitignore)
    都没有就报错,并告诉你怎么设

十个运维脚本改成 `from lssh import ... HOST as LS_HOST`,
公开仓库里一个具体地址都不留。

== 顺带修的两个自己造的 bug ==

  * 用正则往 `from lssh import ...` 后面追加东西时,
    追加到了行尾的 `# noqa: E402` **注释里面** ——
    十处导入全是废的,脚本一跑就 NameError。
    改成先剥掉注释、拼好导入、再把 noqa 加回去。

  * 同一个正则还把 `from lssh import HOST as LS_HOST` 加进了
    **lssh.py 自己**,造成自我导入。

教训:**正则批量改导入语句很危险**(注释、行尾、顺序都能坑到),
这种地方宁可逐个 edit。

== 为什么要重建产物 ==

docs/AFTER-INSTALL.txt 会被打进服务端 zip,而它上一轮改过
(去掉了写死的旧管理员密码)—— 不重打的话发出去的还是旧的。

重打后确认:三个产物里都没有旧密码、没有服务器 IP。

== 验证 ==

    全量回归退出码 0,41 个阶段全 PASS
    603 个 Python 文件语法通过
    ykxmz-agent / server / watch 三个产物扫描干净
    提交前泄漏闸门通过
2026-10-09 00:02:32 +08:00

153 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''连 AWS Lightsail 那台新服务器的 SSH 助手。
== 为什么单独一个模块,不改 devssh.py ==
devssh.py 是连**随身 WiFi 那台 4G 小机器**的(192.168.2.156,密码登录)。
那台还在跑(网站暂时还挂在上面),不能动它的连接逻辑。
新服务器用**密钥**登录、IP 是公网 <源站IP>,认证方式完全不同,
所以单独一个模块更清楚。
== 用法 ==
from lssh import run, upload, download, probe
run("uname -a") # 跑命令
upload("relay/relay.py", "/opt/ykxmz-relay/relay.py")
'''
from __future__ import annotations
import os
import socket
from pathlib import Path
import paramiko
def _host() -> str:
"""服务器公网 IP —— **不写死在代码里**。
这个仓库是公开的。IP 写进来等于把源站地址告诉所有人,
而源站躲在 Cloudflare 后面本来就不该被知道(直接打 IP 就绕过 CDN
和防护了)。2026-10-08 就因为 IP 泄漏返工过一次。
取值顺序:
1. 环境变量 YKXMZ_LS_HOST
2. _build/.lightsail_host(一行 IP,已 gitignore)
"""
env = os.environ.get("YKXMZ_LS_HOST", "").strip()
if env:
return env
f = Path(__file__).resolve().parent / ".lightsail_host"
if f.is_file():
return f.read_text(encoding="utf-8").strip()
raise SystemExit(
"没拿到服务器地址。二选一:\n"
" set YKXMZ_LS_HOST=<你的服务器IP>\n"
" 或把 IP 写进 _build/.lightsail_host(该文件已在 .gitignore 里)")
HOST = _host()
USER = os.environ.get("YKXMZ_LS_USER", "admin")
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
KEY = Path(os.environ.get(
"YKXMZ_LS_KEY", str(Path.home() / ".ssh" / "lightsail-ykxmz.pem")))
_client = None
def _connect(timeout: int = 25):
global _client
if _client is not None:
try:
# 探一下还活着没
_client.exec_command("true", timeout=5)
return _client
except Exception: # noqa: BLE001
_client = None
if not KEY.is_file():
raise RuntimeError(
"找不到 SSH 密钥:%s\n"
"(把 Lightsail 的 .pem 放到那里,或者设 YKXMZ_LS_KEY)" % KEY)
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(HOST, port=PORT, username=USER,
key_filename=str(KEY), timeout=timeout,
allow_agent=False, look_for_keys=False,
banner_timeout=30, auth_timeout=30)
_client = cli
return cli
def run(command: str, timeout: int = 300, quiet: bool = False) -> tuple:
"""跑一条命令,返回 (退出码, 输出)。"""
cli = _connect()
if not quiet:
print(" [ls] %s" % command.splitlines()[0][:90])
stdin, stdout, stderr = cli.exec_command(command, timeout=timeout,
get_pty=False)
out = stdout.read().decode("utf-8", "replace")
err = stderr.read().decode("utf-8", "replace")
code = stdout.channel.recv_exit_status()
return code, (out + err)
def upload(local: str, remote: str, mode: int = 0o644) -> None:
"""传一个文件上去。"""
cli = _connect()
sftp = cli.open_sftp()
try:
# 先把远端目录建出来
d = os.path.dirname(remote)
if d:
try:
sftp.stat(d)
except IOError:
# 逐级建
parts = d.strip("/").split("/")
cur = ""
for p in parts:
cur += "/" + p
try:
sftp.stat(cur)
except IOError:
try:
sftp.mkdir(cur)
except IOError:
pass
sftp.put(local, remote)
sftp.chmod(remote, mode)
finally:
sftp.close()
print(" 已上传 %s -> %s" % (os.path.basename(local), remote))
def download(remote: str, local: str) -> None:
cli = _connect()
sftp = cli.open_sftp()
try:
Path(local).parent.mkdir(parents=True, exist_ok=True)
sftp.get(remote, local)
finally:
sftp.close()
print(" 已下载 %s -> %s" % (remote, local))
def probe() -> str | None:
"""通不通。返回一句话描述,或 None。"""
try:
code, out = run("uname -sm; nproc; free -m | sed -n 2p",
timeout=30, quiet=True)
if code != 0:
return None
return out.strip().replace("\n", " | ")
except Exception as exc: # noqa: BLE001
return "连接失败:%s" % exc
if __name__ == "__main__":
print(" 目标: %s@%s:%d 密钥 %s" % (USER, HOST, PORT, KEY))
print(" " + (probe() or "不通"))