一些修改
This commit is contained in:
parent
d52e3e8db5
commit
a79437bd4c
18
README.md
18
README.md
@ -17,25 +17,33 @@ pip install -r requirements.txt
|
||||
2) 配置环境变量
|
||||
复制 `.env.example` 为 `.env`,按需修改:
|
||||
- `DATABASE_URL`:MySQL 连接串,例如 `mysql+pymysql://user:pass@localhost:3306/ip_ops`
|
||||
- `AWS_CONFIG_PATH`:AWS 账户配置文件,默认 `config/accounts.yaml`
|
||||
- `IP_RETRY_LIMIT`:新 IP 与黑名单冲突时的停机/开机重试次数
|
||||
|
||||
3) 准备数据库
|
||||
创建数据库并授权,首次运行会自动建表 `ip_operations`、`ip_account_mapping`、`server_specs`、`ip_replacement_history`:
|
||||
创建数据库并授权,首次运行会自动建表 `aws_accounts`、`ip_operations`、`ip_account_mapping`、`server_specs`、`ip_replacement_history`:
|
||||
```sql
|
||||
CREATE DATABASE ip_ops DEFAULT CHARACTER SET utf8mb4;
|
||||
GRANT ALL ON ip_ops.* TO 'user'@'%' IDENTIFIED BY 'pass';
|
||||
```
|
||||
`aws_accounts` 存储 AWS 账户凭据及默认 AMI/网络配置,可随时新增/更新记录后立即生效(无需重启)。
|
||||
`ip_account_mapping` 记录 IP 与账户名映射(运行前请先写入),例如:
|
||||
```sql
|
||||
INSERT INTO ip_account_mapping (ip_address, account_name) VALUES ('54.12.34.56', 'account_a');
|
||||
```
|
||||
|
||||
4) 配置 AWS 账户
|
||||
编辑 `config/accounts.yaml`,为每个账户填写:访问密钥、区域、AMI ID、可选子网/安全组/密钥名等(实例类型无需配置,后端按源实例类型创建;若能读取到源实例的子网与安全组,将复用它们,否则回落到配置文件;密钥名若不存在会自动忽略重试)。
|
||||
通过 SQL 或数据库管理工具向 `aws_accounts` 写入账户信息:
|
||||
```sql
|
||||
INSERT INTO aws_accounts (
|
||||
name, region, access_key_id, secret_access_key, ami_id, subnet_id, security_group_ids, key_name
|
||||
) VALUES (
|
||||
'account_a', 'us-east-1', 'AKIA...', 'SECRET...', 'ami-xxxx', 'subnet-xxxx', 'sg-1,sg-2', 'my-key'
|
||||
);
|
||||
```
|
||||
`security_group_ids` 用逗号分隔,可留空;`subnet_id`/`key_name` 可选。新增或更新记录后,接口会实时使用最新配置。
|
||||
|
||||
6) 配置初始服务器
|
||||
在url/mapping_page配置初始服务器。
|
||||
5) 配置初始服务器映射
|
||||
在 `/mapping_page` 录入 IP -> 账户名映射,或直接写入 `ip_account_mapping`。
|
||||
|
||||
6) 启动
|
||||
```bash
|
||||
|
||||
138
app.py
138
app.py
@ -7,13 +7,12 @@ from flask import Flask, jsonify, render_template, request, redirect, url_for, s
|
||||
|
||||
from aws_service import (
|
||||
AWSOperationError,
|
||||
ConfigError,
|
||||
AccountConfig,
|
||||
load_account_configs,
|
||||
replace_instance_ip,
|
||||
)
|
||||
from db import (
|
||||
add_replacement_history,
|
||||
delete_aws_account,
|
||||
get_account_by_ip,
|
||||
get_replacement_history,
|
||||
get_history_by_ip_or_group,
|
||||
@ -21,11 +20,13 @@ from db import (
|
||||
get_server_spec,
|
||||
init_db,
|
||||
load_disallowed_ips,
|
||||
list_aws_accounts,
|
||||
list_account_mappings,
|
||||
update_ip_account_mapping,
|
||||
upsert_server_spec,
|
||||
upsert_account_mapping,
|
||||
delete_account_mapping,
|
||||
upsert_aws_account,
|
||||
)
|
||||
|
||||
|
||||
@ -59,17 +60,36 @@ def zhiyun_required(fn):
|
||||
return wrapper
|
||||
|
||||
|
||||
def load_configs() -> Dict[str, AccountConfig]:
|
||||
config_path = os.getenv("AWS_CONFIG_PATH", "config/accounts.yaml")
|
||||
return load_account_configs(config_path)
|
||||
def admin_required(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not session.get("admin_authed"):
|
||||
return jsonify({"error": "未通过管理员校验"}), 403
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
try:
|
||||
account_configs = load_configs()
|
||||
init_error = ""
|
||||
except ConfigError as exc:
|
||||
account_configs = {}
|
||||
init_error = str(exc)
|
||||
def load_account_configs_from_db() -> tuple[Dict[str, AccountConfig], str]:
|
||||
try:
|
||||
accounts = list_aws_accounts()
|
||||
except Exception as exc: # noqa: BLE001 - surface DB errors to UI
|
||||
return {}, f"读取 AWS 账户配置失败: {exc}"
|
||||
if not accounts:
|
||||
return {}, "数据库中未找到任何 AWS 账户配置"
|
||||
configs: Dict[str, AccountConfig] = {}
|
||||
for item in accounts:
|
||||
configs[item["name"]] = AccountConfig(
|
||||
name=item["name"],
|
||||
region=item["region"],
|
||||
access_key_id=item["access_key_id"],
|
||||
secret_access_key=item["secret_access_key"],
|
||||
ami_id=item["ami_id"],
|
||||
subnet_id=item.get("subnet_id"),
|
||||
security_group_ids=item.get("security_group_ids", []),
|
||||
key_name=item.get("key_name"),
|
||||
)
|
||||
return configs, ""
|
||||
|
||||
retry_limit = int(os.getenv("IP_RETRY_LIMIT", "5"))
|
||||
|
||||
@ -83,9 +103,11 @@ except Exception as exc: # noqa: BLE001 - surface DB connection issues to UI
|
||||
@app.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def index():
|
||||
if init_error or db_error:
|
||||
return render_template("index.html", accounts=[], init_error=init_error or db_error)
|
||||
return render_template("index.html", accounts=account_configs.values(), init_error="")
|
||||
if db_error:
|
||||
return render_template("index.html", accounts=[], init_error=db_error)
|
||||
accounts, config_error = load_account_configs_from_db()
|
||||
error_msg = db_error or config_error
|
||||
return render_template("index.html", accounts=accounts.values(), init_error=error_msg)
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
@ -116,8 +138,10 @@ def mapping_auth():
|
||||
pwd = request.form.get("password", "")
|
||||
if pwd and pwd == ZHIYUN_PASS:
|
||||
session["zhiyun_authed"] = True
|
||||
session["admin_authed"] = True # 复用同一口令
|
||||
return jsonify({"ok": True})
|
||||
session.pop("zhiyun_authed", None)
|
||||
session.pop("admin_authed", None)
|
||||
return jsonify({"error": "密码错误"}), 403
|
||||
|
||||
|
||||
@ -170,8 +194,12 @@ def mapping_delete():
|
||||
@app.route("/replace_ip", methods=["POST"])
|
||||
@login_required
|
||||
def replace_ip():
|
||||
if init_error or db_error:
|
||||
return jsonify({"error": init_error or db_error}), 500
|
||||
if db_error:
|
||||
return jsonify({"error": db_error}), 500
|
||||
|
||||
account_configs, config_error = load_account_configs_from_db()
|
||||
if config_error:
|
||||
return jsonify({"error": config_error}), 500
|
||||
|
||||
ip_to_replace = request.form.get("ip_to_replace", "").strip()
|
||||
|
||||
@ -182,7 +210,7 @@ def replace_ip():
|
||||
if not account_name:
|
||||
return jsonify({"error": "数据库中未找到该IP对应的账户映射"}), 400
|
||||
if account_name not in account_configs:
|
||||
return jsonify({"error": f"账户 {account_name} 未在配置文件中定义"}), 400
|
||||
return jsonify({"error": f"账户 {account_name} 未在数据库中配置"}), 400
|
||||
|
||||
disallowed = load_disallowed_ips()
|
||||
fallback_spec = get_server_spec(ip_to_replace)
|
||||
@ -272,5 +300,81 @@ def history_chains():
|
||||
return jsonify({"items": records})
|
||||
|
||||
|
||||
@app.route("/admin", methods=["GET"])
|
||||
@login_required
|
||||
def admin_page():
|
||||
return render_template("admin.html")
|
||||
|
||||
|
||||
@app.route("/admin/auth", methods=["POST"])
|
||||
@login_required
|
||||
def admin_auth():
|
||||
pwd = request.form.get("password", "")
|
||||
if pwd and pwd == ZHIYUN_PASS:
|
||||
session["admin_authed"] = True
|
||||
session["zhiyun_authed"] = True # 复用映射接口的校验
|
||||
return jsonify({"ok": True})
|
||||
session.pop("admin_authed", None)
|
||||
session.pop("zhiyun_authed", None)
|
||||
return jsonify({"error": "密码错误"}), 403
|
||||
|
||||
|
||||
@app.route("/admin/aws/list", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_aws_list():
|
||||
try:
|
||||
items = list_aws_accounts()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return jsonify({"error": f"读取失败: {exc}"}), 500
|
||||
# 避免前端泄露敏感字段,可自行删减;这里仍返回全部以便编辑
|
||||
return jsonify({"items": items})
|
||||
|
||||
|
||||
@app.route("/admin/aws/upsert", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_aws_upsert():
|
||||
name = request.form.get("name", "").strip()
|
||||
region = request.form.get("region", "").strip()
|
||||
access_key_id = request.form.get("access_key_id", "").strip()
|
||||
secret_access_key = request.form.get("secret_access_key", "").strip()
|
||||
ami_id = request.form.get("ami_id", "").strip()
|
||||
subnet_id = request.form.get("subnet_id", "").strip() or None
|
||||
sg_raw = request.form.get("security_group_ids", "").strip()
|
||||
key_name = request.form.get("key_name", "").strip() or None
|
||||
if not (name and region and access_key_id and secret_access_key and ami_id):
|
||||
return jsonify({"error": "name/region/access_key_id/secret_access_key/ami_id 不能为空"}), 400
|
||||
security_group_ids = [s.strip() for s in sg_raw.split(",") if s.strip()] if sg_raw else []
|
||||
try:
|
||||
upsert_aws_account(
|
||||
name=name,
|
||||
region=region,
|
||||
access_key_id=access_key_id,
|
||||
secret_access_key=secret_access_key,
|
||||
ami_id=ami_id,
|
||||
subnet_id=subnet_id,
|
||||
security_group_ids=security_group_ids,
|
||||
key_name=key_name,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return jsonify({"error": f"保存失败: {exc}"}), 500
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/admin/aws/delete", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_aws_delete():
|
||||
name = request.form.get("name", "").strip()
|
||||
if not name:
|
||||
return jsonify({"error": "name 不能为空"}), 400
|
||||
try:
|
||||
delete_aws_account(name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return jsonify({"error": f"删除失败: {exc}"}), 500
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, List, Optional, TypedDict
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
import yaml
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
@ -41,29 +39,6 @@ class AccountConfig:
|
||||
key_name: Optional[str] = None
|
||||
|
||||
|
||||
def load_account_configs(path: str) -> Dict[str, AccountConfig]:
|
||||
if not os.path.exists(path):
|
||||
raise ConfigError(f"Config file not found at {path}")
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
if not data or "accounts" not in data:
|
||||
raise ConfigError("accounts.yaml missing 'accounts' list")
|
||||
accounts = {}
|
||||
for item in data["accounts"]:
|
||||
cfg = AccountConfig(
|
||||
name=item["name"],
|
||||
region=item["region"],
|
||||
access_key_id=item["access_key_id"],
|
||||
secret_access_key=item["secret_access_key"],
|
||||
ami_id=item["ami_id"],
|
||||
subnet_id=item.get("subnet_id"),
|
||||
security_group_ids=item.get("security_group_ids", []),
|
||||
key_name=item.get("key_name"),
|
||||
)
|
||||
accounts[cfg.name] = cfg
|
||||
return accounts
|
||||
|
||||
|
||||
def ec2_client(account: AccountConfig):
|
||||
return boto3.client(
|
||||
"ec2",
|
||||
|
||||
88
db.py
88
db.py
@ -14,6 +14,26 @@ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def _now_cn() -> datetime:
|
||||
return datetime.now(timezone(timedelta(hours=8)))
|
||||
|
||||
|
||||
class AWSAccount(Base):
|
||||
__tablename__ = "aws_accounts"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(128), unique=True, nullable=False, index=True)
|
||||
region = Column(String(64), nullable=False)
|
||||
access_key_id = Column(String(128), nullable=False)
|
||||
secret_access_key = Column(String(256), nullable=False)
|
||||
ami_id = Column(String(128), nullable=False)
|
||||
subnet_id = Column(String(128), nullable=True)
|
||||
security_group_ids = Column(String(512), nullable=True)
|
||||
key_name = Column(String(128), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=_now_cn)
|
||||
updated_at = Column(DateTime(timezone=True), nullable=False, default=_now_cn, onupdate=_now_cn)
|
||||
|
||||
|
||||
class IPOperation(Base):
|
||||
__tablename__ = "ip_operations"
|
||||
|
||||
@ -74,6 +94,12 @@ def init_db() -> None:
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def _split_csv(value: Optional[str]) -> List[str]:
|
||||
if not value:
|
||||
return []
|
||||
return [part for part in value.split(",") if part]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def db_session():
|
||||
session = SessionLocal()
|
||||
@ -93,6 +119,64 @@ def load_disallowed_ips() -> set[str]:
|
||||
return {row for row in rows}
|
||||
|
||||
|
||||
def list_aws_accounts() -> List[Dict[str, object]]:
|
||||
with db_session() as session:
|
||||
rows: Iterable[AWSAccount] = session.scalars(
|
||||
select(AWSAccount).order_by(AWSAccount.id.desc())
|
||||
)
|
||||
return [
|
||||
{
|
||||
"name": row.name,
|
||||
"region": row.region,
|
||||
"access_key_id": row.access_key_id,
|
||||
"secret_access_key": row.secret_access_key,
|
||||
"ami_id": row.ami_id,
|
||||
"subnet_id": row.subnet_id,
|
||||
"security_group_ids": _split_csv(row.security_group_ids),
|
||||
"key_name": row.key_name,
|
||||
"created_at": row.created_at,
|
||||
"updated_at": row.updated_at,
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def upsert_aws_account(
|
||||
*,
|
||||
name: str,
|
||||
region: str,
|
||||
access_key_id: str,
|
||||
secret_access_key: str,
|
||||
ami_id: str,
|
||||
subnet_id: Optional[str] = None,
|
||||
security_group_ids: Optional[List[str]] = None,
|
||||
key_name: Optional[str] = None,
|
||||
) -> None:
|
||||
with db_session() as session:
|
||||
record = session.scalar(select(AWSAccount).where(AWSAccount.name == name))
|
||||
payload = {
|
||||
"region": region,
|
||||
"access_key_id": access_key_id,
|
||||
"secret_access_key": secret_access_key,
|
||||
"ami_id": ami_id,
|
||||
"subnet_id": subnet_id,
|
||||
"security_group_ids": ",".join(security_group_ids or []),
|
||||
"key_name": key_name,
|
||||
}
|
||||
if record:
|
||||
for key, val in payload.items():
|
||||
setattr(record, key, val)
|
||||
else:
|
||||
session.add(AWSAccount(name=name, **payload))
|
||||
|
||||
|
||||
def delete_aws_account(name: str) -> None:
|
||||
with db_session() as session:
|
||||
record = session.scalar(select(AWSAccount).where(AWSAccount.name == name))
|
||||
if record:
|
||||
session.delete(record)
|
||||
|
||||
|
||||
def get_account_by_ip(ip: str) -> Optional[str]:
|
||||
with db_session() as session:
|
||||
return session.scalar(
|
||||
@ -146,10 +230,6 @@ def update_ip_account_mapping(old_ip: str, new_ip: str, account_name: str) -> No
|
||||
session.add(IPAccountMapping(ip_address=new_ip, account_name=account_name))
|
||||
|
||||
|
||||
def _now_cn() -> datetime:
|
||||
return datetime.now(timezone(timedelta(hours=8)))
|
||||
|
||||
|
||||
def upsert_server_spec(
|
||||
*,
|
||||
ip_address: str,
|
||||
|
||||
327
templates/admin.html
Normal file
327
templates/admin.html
Normal file
@ -0,0 +1,327 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>管理后台</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root {
|
||||
--bg: linear-gradient(135deg, #0f172a, #1e293b);
|
||||
--card: #0b1224;
|
||||
--accent: #22d3ee;
|
||||
--accent-2: #a855f7;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
--danger: #f87171;
|
||||
}
|
||||
body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font-family:"Segoe UI","Helvetica Neue",Arial,sans-serif; padding:24px; }
|
||||
.shell { max-width: 1100px; margin:0 auto; background:var(--card); border:1px solid rgba(255,255,255,0.08); border-radius:14px; padding:24px; box-shadow:0 25px 60px rgba(0,0,0,0.45); }
|
||||
h1,h2 { margin:0 0 10px; }
|
||||
label { display:block; font-weight:600; color:#cbd5e1; margin:8px 0 4px; }
|
||||
input, textarea { width:100%; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.04); color:var(--text); }
|
||||
button { padding:10px 14px; border:none; border-radius:10px; cursor:pointer; background:linear-gradient(135deg,var(--accent),var(--accent-2)); color:var(--text); font-weight:700; }
|
||||
table { width:100%; border-collapse:collapse; margin-top:12px; }
|
||||
th, td { padding:10px; border-bottom:1px solid rgba(255,255,255,0.08); text-align:left; }
|
||||
.muted { color:var(--muted); }
|
||||
.status { margin-top:12px; padding:10px; border-radius:10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); }
|
||||
.error { color: var(--danger); }
|
||||
.grid { display:grid; gap:10px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
||||
.section { margin-top:20px; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,0.06); }
|
||||
.row { display:flex; gap:10px; flex-wrap:wrap; }
|
||||
.row > div { flex:1; min-width:220px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<h1>管理后台</h1>
|
||||
<p class="muted" style="margin-top:-6px;">输入 .env 的 ZHIYUN_PASS 校验后可管理 AWS 账户与 IP 映射。</p>
|
||||
|
||||
<div class="status" id="auth-box">
|
||||
<div class="row">
|
||||
<div style="flex:2;">
|
||||
<label for="auth-pass">校验密码</label>
|
||||
<input id="auth-pass" type="password" placeholder="输入 ZHIYUN_PASS">
|
||||
</div>
|
||||
<div style="flex:1; align-self:flex-end;">
|
||||
<button id="auth-btn" style="width:100%;">校验</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="auth-msg" class="muted" style="margin-top:6px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="content" style="display:none;">
|
||||
<div class="section">
|
||||
<h2>AWS 账户</h2>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="acc-name">name</label>
|
||||
<input id="acc-name" placeholder="如 account_a">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-region">region</label>
|
||||
<input id="acc-region" placeholder="如 us-east-1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-aki">access_key_id</label>
|
||||
<input id="acc-aki">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-sak">secret_access_key</label>
|
||||
<input id="acc-sak">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-ami">ami_id</label>
|
||||
<input id="acc-ami">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-subnet">subnet_id (可选)</label>
|
||||
<input id="acc-subnet">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-sg">security_group_ids 逗号分隔 (可选)</label>
|
||||
<input id="acc-sg" placeholder="sg-1,sg-2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="acc-key">key_name (可选)</label>
|
||||
<input id="acc-key">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<button id="acc-save">新增/更新</button>
|
||||
<button id="acc-del" style="background:#ef4444;">删除</button>
|
||||
</div>
|
||||
<div id="acc-msg" class="status" style="display:none;"></div>
|
||||
<table id="acc-table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>name</th><th>region</th><th>ami_id</th><th>subnet_id</th><th>sg_ids</th><th>key_name</th><th>更新</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>IP-账户映射</h2>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="map-ip">IP 地址</label>
|
||||
<input id="map-ip" placeholder="如 54.12.34.56">
|
||||
</div>
|
||||
<div>
|
||||
<label for="map-acc">账户名</label>
|
||||
<input id="map-acc" placeholder="对应 aws_accounts.name">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<button id="map-save">新增/更新</button>
|
||||
<button id="map-del" style="background:#ef4444;">删除</button>
|
||||
</div>
|
||||
<div id="map-msg" class="status" style="display:none;"></div>
|
||||
<table id="map-table" style="display:none;">
|
||||
<thead>
|
||||
<tr><th>IP</th><th>账户</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const authBtn = document.getElementById('auth-btn');
|
||||
const authPass = document.getElementById('auth-pass');
|
||||
const authMsg = document.getElementById('auth-msg');
|
||||
const content = document.getElementById('content');
|
||||
|
||||
const accTable = document.getElementById('acc-table');
|
||||
const accBody = accTable.querySelector('tbody');
|
||||
const accMsg = document.getElementById('acc-msg');
|
||||
const accInputs = {
|
||||
name: document.getElementById('acc-name'),
|
||||
region: document.getElementById('acc-region'),
|
||||
aki: document.getElementById('acc-aki'),
|
||||
sak: document.getElementById('acc-sak'),
|
||||
ami: document.getElementById('acc-ami'),
|
||||
subnet: document.getElementById('acc-subnet'),
|
||||
sg: document.getElementById('acc-sg'),
|
||||
key: document.getElementById('acc-key'),
|
||||
};
|
||||
const accSave = document.getElementById('acc-save');
|
||||
const accDel = document.getElementById('acc-del');
|
||||
|
||||
const mapTable = document.getElementById('map-table');
|
||||
const mapBody = mapTable.querySelector('tbody');
|
||||
const mapMsg = document.getElementById('map-msg');
|
||||
const mapIp = document.getElementById('map-ip');
|
||||
const mapAcc = document.getElementById('map-acc');
|
||||
const mapSave = document.getElementById('map-save');
|
||||
const mapDel = document.getElementById('map-del');
|
||||
|
||||
function showMsg(el, text, isError=false) {
|
||||
el.style.display = 'block';
|
||||
el.className = 'status' + (isError ? ' error' : '');
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
async function auth() {
|
||||
authBtn.disabled = true;
|
||||
authMsg.textContent = '校验中...';
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('password', authPass.value);
|
||||
const resp = await fetch('/admin/auth', { method:'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '校验失败');
|
||||
authMsg.textContent = '校验成功';
|
||||
content.style.display = 'block';
|
||||
await Promise.all([loadAccounts(), loadMappings()]);
|
||||
} catch (err) {
|
||||
authMsg.textContent = err.message;
|
||||
content.style.display = 'none';
|
||||
} finally {
|
||||
authBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const resp = await fetch('/admin/aws/list');
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '获取失败');
|
||||
const items = data.items || [];
|
||||
accBody.innerHTML = items.map(item => `
|
||||
<tr>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.region}</td>
|
||||
<td>${item.ami_id}</td>
|
||||
<td>${item.subnet_id || ''}</td>
|
||||
<td>${(item.security_group_ids || []).join(', ')}</td>
|
||||
<td>${item.key_name || ''}</td>
|
||||
<td>${item.updated_at || ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
accTable.style.display = 'table';
|
||||
} catch (err) {
|
||||
showMsg(accMsg, err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccount() {
|
||||
const formData = new FormData();
|
||||
formData.append('name', accInputs.name.value.trim());
|
||||
formData.append('region', accInputs.region.value.trim());
|
||||
formData.append('access_key_id', accInputs.aki.value.trim());
|
||||
formData.append('secret_access_key', accInputs.sak.value.trim());
|
||||
formData.append('ami_id', accInputs.ami.value.trim());
|
||||
formData.append('subnet_id', accInputs.subnet.value.trim());
|
||||
formData.append('security_group_ids', accInputs.sg.value.trim());
|
||||
formData.append('key_name', accInputs.key.value.trim());
|
||||
accSave.disabled = true;
|
||||
try {
|
||||
const resp = await fetch('/admin/aws/upsert', { method:'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '保存失败');
|
||||
showMsg(accMsg, '保存成功');
|
||||
await loadAccounts();
|
||||
} catch (err) {
|
||||
showMsg(accMsg, err.message, true);
|
||||
} finally {
|
||||
accSave.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
const name = accInputs.name.value.trim();
|
||||
if (!name) {
|
||||
showMsg(accMsg, '删除前请填写 name', true);
|
||||
return;
|
||||
}
|
||||
accDel.disabled = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
const resp = await fetch('/admin/aws/delete', { method:'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '删除失败');
|
||||
showMsg(accMsg, '删除成功');
|
||||
await loadAccounts();
|
||||
} catch (err) {
|
||||
showMsg(accMsg, err.message, true);
|
||||
} finally {
|
||||
accDel.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMappings() {
|
||||
try {
|
||||
const resp = await fetch('/mapping/list');
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '获取失败');
|
||||
const items = data.items || [];
|
||||
mapBody.innerHTML = items.map(item => `
|
||||
<tr>
|
||||
<td>${item.ip_address}</td>
|
||||
<td>${item.account_name}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
mapTable.style.display = 'table';
|
||||
} catch (err) {
|
||||
showMsg(mapMsg, err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMapping() {
|
||||
const ip = mapIp.value.trim();
|
||||
const acc = mapAcc.value.trim();
|
||||
if (!ip || !acc) {
|
||||
showMsg(mapMsg, 'IP 和账户名不能为空', true);
|
||||
return;
|
||||
}
|
||||
mapSave.disabled = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('ip', ip);
|
||||
formData.append('account', acc);
|
||||
const resp = await fetch('/mapping/upsert', { method:'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '保存失败');
|
||||
showMsg(mapMsg, '保存成功');
|
||||
await loadMappings();
|
||||
} catch (err) {
|
||||
showMsg(mapMsg, err.message, true);
|
||||
} finally {
|
||||
mapSave.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMapping() {
|
||||
const ip = mapIp.value.trim();
|
||||
if (!ip) {
|
||||
showMsg(mapMsg, '删除前请填写 IP', true);
|
||||
return;
|
||||
}
|
||||
mapDel.disabled = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('ip', ip);
|
||||
const resp = await fetch('/mapping/delete', { method:'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || '删除失败');
|
||||
showMsg(mapMsg, '删除成功');
|
||||
await loadMappings();
|
||||
} catch (err) {
|
||||
showMsg(mapMsg, err.message, true);
|
||||
} finally {
|
||||
mapDel.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
authBtn.addEventListener('click', auth);
|
||||
accSave.addEventListener('click', saveAccount);
|
||||
accDel.addEventListener('click', deleteAccount);
|
||||
mapSave.addEventListener('click', saveMapping);
|
||||
mapDel.addEventListener('click', deleteMapping);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="account">账户名</label>
|
||||
<input id="account" placeholder="与 config/accounts.yaml 中的 name 对应">
|
||||
<input id="account" placeholder="与数据库中 AWS 账户的 name 对应">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px; display:flex; gap:10px;">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user