From d52e3e8db5427a5248b935209ca746c1bb92a47d Mon Sep 17 00:00:00 2001 From: wangqifan Date: Mon, 5 Jan 2026 11:23:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + README.md | 9 +- app.py | 71 ++++++++++++++++ db.py | 28 +++++++ templates/mapping.html | 185 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 templates/mapping.html diff --git a/.env b/.env index 23384dc..5307415 100644 --- a/.env +++ b/.env @@ -5,3 +5,4 @@ IP_RETRY_LIMIT=5 APP_USER=admin APP_PASSWORD=Pc9mVTm3kKo0pO SECRET_KEY=51aiapi +ZHIYUN_PASS=PbUeI1MZwep9vp \ No newline at end of file diff --git a/README.md b/README.md index 0d12a3c..56c1700 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flask + boto3 + MySQL 的小工具,用于: - 根据输入 IP 查找对应 EC2 实例并终止,使用预设 AMI 创建新实例 - 通过数据库中的 IP-账户映射自动选择 AWS 账户,前端不暴露账户列表 - 如果新公网 IP 落入运维黑名单,自动停机/开机循环更换 IP(受 `IP_RETRY_LIMIT` 控制) -- MySQL 存储黑名单 (`ip_operations`)、IP-账户映射 (`ip_account_mapping`)、服务器规格 (`server_specs`,含实例类型/Name/磁盘/安全组/区域/子网/AZ)、IP 替换历史 (`ip_replacement_history`,含 group_id 链路标识,默认取旧 IP) +- MySQL 存储黑名单 (`ip_operations`)、IP-账户映射 (`ip_account_mapping`)、服务器规格 (`server_specs`,含实例类型/Name/磁盘/安全组/区域/子网/AZ)、IP 替换历史 (`ip_replacement_history`) ## 快速开始 1) 安装依赖 @@ -34,7 +34,10 @@ INSERT INTO ip_account_mapping (ip_address, account_name) VALUES ('54.12.34.56', 4) 配置 AWS 账户 编辑 `config/accounts.yaml`,为每个账户填写:访问密钥、区域、AMI ID、可选子网/安全组/密钥名等(实例类型无需配置,后端按源实例类型创建;若能读取到源实例的子网与安全组,将复用它们,否则回落到配置文件;密钥名若不存在会自动忽略重试)。 -5) 启动 +6) 配置初始服务器 +在url/mapping_page配置初始服务器。 + +6) 启动 ```bash flask --app app run --host 0.0.0.0 --port 5000 # 或 python app.py @@ -44,7 +47,7 @@ flask --app app run --host 0.0.0.0 --port 5000 1) 页面输入需要替换的 IP,后端用 `ip_account_mapping` 定位账户并读取对应 AWS 配置 2) 在该账户中查找公/私网 IP 匹配的实例,读取实例类型、Name、根盘大小/类型、安全组(ID/名称)、区域/子网/AZ,并记录到 `server_specs`;若实例未找到则回退使用数据库中已存的规格 3) 按记录的规格创建新实例(实例类型、磁盘类型/大小、安全组、子网/AZ),如新公网 IP 在 `ip_operations` 黑名单中,则停机/开机循环直至获得可用 IP(或达到重试上限);旧实例的终止异步触发,不会阻塞新实例创建 -4) 记录 IP 替换历史到 `ip_replacement_history`,group_id 默认用旧 IP;前端主页可跳转到历史页按 IP/group 查询链路;同时更新 `server_specs` 中的 IP 规格为最新 IP +4) 记录 IP 替换历史到 `ip_replacement_history`,前端可查看最近 100 条替换记录;同时更新 `server_specs` 中的 IP 规格为最新 IP ## 注意事项 - 真实环境会产生终止/创建实例等成本操作,先在测试账户验证流程 diff --git a/app.py b/app.py index a503535..468fc3d 100644 --- a/app.py +++ b/app.py @@ -21,8 +21,11 @@ from db import ( get_server_spec, init_db, load_disallowed_ips, + list_account_mappings, update_ip_account_mapping, upsert_server_spec, + upsert_account_mapping, + delete_account_mapping, ) @@ -33,6 +36,7 @@ app.secret_key = os.getenv("SECRET_KEY", "please-change-me") APP_USER = os.getenv("APP_USER", "") APP_PASSWORD = os.getenv("APP_PASSWORD", "") +ZHIYUN_PASS = os.getenv("ZHIYUN_PASS", "") def login_required(fn): @@ -45,6 +49,16 @@ def login_required(fn): return wrapper +def zhiyun_required(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if not session.get("zhiyun_authed"): + return jsonify({"error": "未通过校验密码"}), 403 + return fn(*args, **kwargs) + + return wrapper + + def load_configs() -> Dict[str, AccountConfig]: config_path = os.getenv("AWS_CONFIG_PATH", "config/accounts.yaml") return load_account_configs(config_path) @@ -96,6 +110,63 @@ def logout(): return redirect(url_for("login")) +@app.route("/mapping/auth", methods=["POST"]) +@login_required +def mapping_auth(): + pwd = request.form.get("password", "") + if pwd and pwd == ZHIYUN_PASS: + session["zhiyun_authed"] = True + return jsonify({"ok": True}) + session.pop("zhiyun_authed", None) + return jsonify({"error": "密码错误"}), 403 + + +@app.route("/mapping_page", methods=["GET"]) +@login_required +def mapping_page(): + return render_template("mapping.html") + + +@app.route("/mapping/list", methods=["GET"]) +@login_required +@zhiyun_required +def mapping_list(): + try: + data = list_account_mappings() + except Exception as exc: # noqa: BLE001 + return jsonify({"error": f"读取失败: {exc}"}), 500 + return jsonify({"items": data}) + + +@app.route("/mapping/upsert", methods=["POST"]) +@login_required +@zhiyun_required +def mapping_upsert(): + ip = request.form.get("ip", "").strip() + account = request.form.get("account", "").strip() + if not ip or not account: + return jsonify({"error": "IP 和账户名不能为空"}), 400 + try: + upsert_account_mapping(ip, account) + except Exception as exc: # noqa: BLE001 + return jsonify({"error": f"保存失败: {exc}"}), 500 + return jsonify({"ok": True}) + + +@app.route("/mapping/delete", methods=["POST"]) +@login_required +@zhiyun_required +def mapping_delete(): + ip = request.form.get("ip", "").strip() + if not ip: + return jsonify({"error": "IP 不能为空"}), 400 + try: + delete_account_mapping(ip) + except Exception as exc: # noqa: BLE001 + return jsonify({"error": f"删除失败: {exc}"}), 500 + return jsonify({"ok": True}) + + @app.route("/replace_ip", methods=["POST"]) @login_required def replace_ip(): diff --git a/db.py b/db.py index 337f173..e246572 100644 --- a/db.py +++ b/db.py @@ -100,6 +100,34 @@ def get_account_by_ip(ip: str) -> Optional[str]: ) +def list_account_mappings() -> List[Dict[str, str]]: + with db_session() as session: + rows: Iterable[IPAccountMapping] = session.scalars( + select(IPAccountMapping).order_by(IPAccountMapping.id.desc()) + ) + return [{"ip_address": row.ip_address, "account_name": row.account_name} for row in rows] + + +def upsert_account_mapping(ip: str, account_name: str) -> None: + with db_session() as session: + record = session.scalar( + select(IPAccountMapping).where(IPAccountMapping.ip_address == ip) + ) + if record: + record.account_name = account_name + else: + session.add(IPAccountMapping(ip_address=ip, account_name=account_name)) + + +def delete_account_mapping(ip: str) -> None: + with db_session() as session: + record = session.scalar( + select(IPAccountMapping).where(IPAccountMapping.ip_address == ip) + ) + if record: + session.delete(record) + + def update_ip_account_mapping(old_ip: str, new_ip: str, account_name: str) -> None: with db_session() as session: existing_mapping = session.scalar( diff --git a/templates/mapping.html b/templates/mapping.html new file mode 100644 index 0000000..c412002 --- /dev/null +++ b/templates/mapping.html @@ -0,0 +1,185 @@ + + + + + IP-账户映射管理 + + + + +
+

IP-账户映射管理

+

操作需输入额外密码(ZHIYUN_PASS)。通过校验后可增删改。

+ +
+
+
+ + +
+
+ +
+
+
+
+ + + + + + + + + + +
+ + + +