🔄 卡若AI 同步 2026-02-20 07:15 | 更新:金仓、运营中枢工作台 | 排除 >20MB: 5 个

This commit is contained in:
2026-02-20 07:15:08 +08:00
parent 112d602733
commit 63d2b15ad3
8 changed files with 445 additions and 0 deletions

View File

@@ -308,6 +308,7 @@ ss -tlnp | grep :端口号
| `快速检查服务器.py` | 一键检查所有服务器状态 | `./scripts/` |
| `一键部署.py` | 根据配置文件部署项目 | `./scripts/` |
| `ssl证书检查.py` | 检查/修复SSL证书 | `./scripts/` |
| `腾讯云镜像快照备份到CKB_NAS/tencent_image_snapshot_backup_to_nas.py` | 腾讯云镜像/快照元数据备份到 CKB NAS1000G 限制与超限邮件告警 | `./scripts/腾讯云镜像快照备份到CKB_NAS/` |
---

View File

@@ -0,0 +1,91 @@
# 腾讯云镜像/快照备份到 CKB NAS
将腾讯云 CVM 自定义镜像、云硬盘快照的元数据与(可选)镜像文件备份到 **CKB NAS**192.168.1.201),并限制备份目录总容量为 **1000GB**,超过时发邮件告警。
## 功能
- **镜像**:拉取账号下全部地域的**自定义镜像**列表,写入 NAS 的 `meta/images_*.json``meta/images_latest.json`
- **快照**:拉取全部地域的**云硬盘快照**列表,写入 NAS 的 `meta/snapshots_*.json``meta/snapshots_latest.json`
- **容量限制**:备份根目录总大小超过 **1000GB**(可配置)时:
- 发送告警邮件到配置的邮箱
- 不再拉取新的镜像文件(仅更新元数据)
- **镜像文件落盘**:腾讯云不提供镜像/快照“直接下载”。需在控制台将**自定义镜像导出到 COS**,再通过 COS 工具(如 coscmd或本脚本扩展逻辑从 COS 下载到 NAS。
## 前置条件
1. **CKB NAS 已挂载到本机**
- CKB NAS 内网 IP`192.168.1.201`(见群晖 NAS 管理 SKILL
- 在 Mac访达 → 前往 → 连接服务器 → `smb://192.168.1.201/共享名`,挂载后得到如 `/Volumes/ckb_backup`
- 本脚本使用的目录为挂载点下的子目录,如 `/Volumes/ckb_backup/tencent_cloud_backup`
2. **腾讯云凭证**
- 环境变量 `TENCENTCLOUD_SECRET_ID``TENCENTCLOUD_SECRET_KEY`,或
-`运营中枢/工作台/00_账号与API索引.md` 的「腾讯云」段落填写 SecretId/SecretKey
3. **告警邮件(可选)**
- 使用 QQ 邮箱时需在 QQ 邮箱 → 设置 → 账户 → 开启 SMTP 并生成**授权码**,在 `config.env` 中填 `SMTP_PASSWORD=授权码`
## 配置
1. 复制配置示例并编辑:
```bash
cd "01_卡资/金仓_存储备份/服务器管理/scripts/腾讯云镜像快照备份到CKB_NAS"
cp config.example.env config.env
```
2. 编辑 `config.env`
- **NAS_BACKUP_ROOT**NAS 上备份根目录(必须已挂载),如 `/Volumes/ckb_backup/tencent_cloud_backup`
- **SIZE_LIMIT_GB**容量上限GB默认 `1000`
- **ALERT_EMAIL_TO**:超限告警收件人
- **SMTP_*****发信用QQ 邮箱用授权码)
## 运行
```bash
# 建议使用项目根下已有 venv含 tencentcloud-sdk
cd /Users/karuo/Documents/个人/卡若AI
. .venv_tencent/bin/activate # 或 服务器管理/scripts 下的 venv
pip install tencentcloud-sdk-python-common tencentcloud-sdk-python-cvm tencentcloud-sdk-python-cbs
python3 "01_卡资/金仓_存储备份/服务器管理/scripts/腾讯云镜像快照备份到CKB_NAS/tencent_image_snapshot_backup_to_nas.py"
```
## 定时拉取(有新镜像/快照即同步元数据)
- 建议用 cron 或 launchd 定期执行(如每天 2:00这样**新产生的镜像/快照**会在下次执行时被拉取并写入 NAS 的 `meta/`。
- 示例 crontab每天 2:00
```bash
0 2 * * * /Users/karuo/Documents/个人/卡若AI/.venv_tencent/bin/python3 /Users/karuo/Documents/个人/卡若AI/01_卡资/金仓_存储备份/服务器管理/scripts/腾讯云镜像快照备份到CKB_NAS/tencent_image_snapshot_backup_to_nas.py >> /tmp/tencent_backup_nas.log 2>&1
```
- 若备份目录超过 1000GB脚本会发邮件提醒且不会从 COS 拉取新镜像文件(仅更新元数据)。
## 镜像文件如何落到 NAS需先导出到 COS
1. 腾讯云控制台 → 云服务器 → 镜像 → 自定义镜像 → 选择镜像 → **导出镜像** → 选择同地域 COS 桶与前缀。
2. 导出完成后,在 COS 桶中会得到镜像文件(如 `.qcow2` / `.vhd`)。
3. 将 COS 中的文件下载到 NAS
- 方式 A安装 [coscmd](https://cloud.tencent.com/document/product/436/10976),配置后执行 `coscmd download -r bucket://prefix /path/on/nas`
- 方式 B在本脚本中扩展「从 COS 列举并下载」逻辑(需配置 `COS_BUCKET`、`COS_REGION`、`COS_PREFIX`)。
## 快照说明
- 腾讯云**云硬盘快照**不提供“下载到本地”的接口,仅支持云内恢复、从快照创建云盘/镜像。
- 本脚本将快照**元数据**ID、名称、大小、创建时间等同步到 NAS便于审计与清单管理。
- 若需将某快照“备份成文件”,需:用该快照创建自定义镜像 → 再按上文将镜像导出到 COS → 从 COS 下载到 NAS。
## 文件结构NAS 备份根目录下)
```
tencent_cloud_backup/
├── meta/
│ ├── images_20260219_020000.json
│ ├── images_latest.json
│ ├── snapshots_20260219_020000.json
│ └── snapshots_latest.json
└── 可选images/ # 若从 COS 下载镜像文件,可放于此
```
## 相关
- 服务器管理 SKILL`01_卡资/金仓_存储备份/服务器管理/SKILL.md`
- 群晖/CKB NAS`01_卡资/金仓_存储备份/群晖NAS管理/SKILL.md`
- 账号与 API 索引:`运营中枢/工作台/00_账号与API索引.md`

View File

@@ -0,0 +1,26 @@
# 腾讯云镜像/快照备份到 CKB NAS - 配置示例
# 复制为 config.env 并填写真实值config.env 勿提交 Git
# ---------- NAS 备份目录(必须先挂载 CKB NAS 的 SMB 共享)
# 示例Mac 挂载后 /Volumes/ckb_backup Linux /mnt/ckb/tencent_cloud_backup
NAS_BACKUP_ROOT=/Volumes/ckb_backup/tencent_cloud_backup
# 容量上限GB超过则发邮件告警且本次不再拉取新镜像文件
SIZE_LIMIT_GB=1000
# ---------- 告警邮件(超过 SIZE_LIMIT_GB 时发送)
ALERT_EMAIL_TO=zhiqun@qq.com
# QQ 邮箱发信需用授权码非登录密码。见QQ邮箱 -> 设置 -> 账户 -> POP3/IMAP -> 生成授权码
SMTP_HOST=smtp.qq.com
SMTP_PORT=465
SMTP_USER=zhiqun@qq.com
SMTP_PASSWORD=
# ---------- 腾讯云(可选,不填则从 00_账号与API索引.md 读取)
# TENCENTCLOUD_SECRET_ID=
# TENCENTCLOUD_SECRET_KEY=
# ---------- COS 同步(可选。若在控制台已将镜像导出到 COS填此项可从 COS 拉取到 NAS
# COS_BUCKET=your-bucket-1251077262
# COS_REGION=ap-guangzhou
# COS_PREFIX=export_images

View File

@@ -0,0 +1,14 @@
# 腾讯云镜像/快照备份到 CKB NAS - 定时执行示例
# 每天 2:00 执行,有新镜像/快照会自动拉取元数据到 NAS超过 1000G 会发邮件告警
# 1) 先挂载 CKB NASMac 可开机自动挂载或手动)
# 访达 → 连接服务器 → smb://192.168.1.201/共享名
# 挂载后例如:/Volumes/ckb_backup
# 2) 在脚本目录配置 config.envNAS_BACKUP_ROOT、告警邮箱、SMTP
# 3) crontab -e 添加(路径按本机实际修改):
0 2 * * * NAS_BACKUP_ROOT=/Volumes/ckb_backup/tencent_cloud_backup /Users/karuo/Documents/个人/卡若AI/.venv_tencent/bin/python3 /Users/karuo/Documents/个人/卡若AI/01_卡资/金仓_存储备份/服务器管理/scripts/腾讯云镜像快照备份到CKB_NAS/tencent_image_snapshot_backup_to_nas.py >> /tmp/tencent_backup_nas.log 2>&1
# 或使用 config.env 中的 NAS_BACKUP_ROOT不设环境变量
# 0 2 * * * cd /Users/karuo/Documents/个人/卡若AI/01_卡资/金仓_存储备份/服务器管理/scripts/腾讯云镜像快照备份到CKB_NAS && /Users/karuo/Documents/个人/卡若AI/.venv_tencent/bin/python3 tencent_image_snapshot_backup_to_nas.py >> /tmp/tencent_backup_nas.log 2>&1

View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
腾讯云 CVM 镜像与云硬盘快照 → 备份到 CKB NAS
- 拉取账号下全部自定义镜像、快照的元数据并写入 NAS
- 可选:从 COS 同步已导出的镜像文件到 NAS需先在控制台将镜像导出到 COS
- 备份目录容量上限(默认 1000GB超过则发邮件告警且不再拉取新文件
凭证:环境变量 或 00_账号与API索引.md § 腾讯云
配置:同目录 config.env参考 config.example.env
依赖tencentcloud-sdk-python-common, tencentcloud-sdk-python-cvm, tencentcloud-sdk-python-cbs
"""
from __future__ import annotations
import json
import os
import re
import smtplib
import sys
from datetime import datetime
from email.mime.text import MIMEText
from pathlib import Path
# 多地域
CVM_REGIONS = [
"ap-guangzhou", "ap-shanghai", "ap-beijing", "ap-chengdu",
"ap-nanjing", "ap-shenzhen-fsi", "ap-hongkong",
]
CBS_REGIONS = CVM_REGIONS # 云硬盘与 CVM 地域一致
def _find_karuo_ai_root():
d = Path(__file__).resolve().parent
for _ in range(6):
if d.name == "卡若AI" or (
(d / "运营中枢").is_dir() and (d / "01_卡资").is_dir()
):
return d
d = d.parent
return None
def _read_tencent_creds():
sid = os.environ.get("TENCENTCLOUD_SECRET_ID")
skey = os.environ.get("TENCENTCLOUD_SECRET_KEY")
if sid and skey:
return sid, skey
root = _find_karuo_ai_root()
if not root:
return None, None
idx = root / "运营中枢" / "工作台" / "00_账号与API索引.md"
if not idx.is_file():
return None, None
text = idx.read_text(encoding="utf-8")
in_tencent = False
sid = skey = None
for line in text.splitlines():
if "### 腾讯云" in line:
in_tencent = True
continue
if in_tencent and line.strip().startswith("###"):
break
if not in_tencent:
continue
m = re.search(r"\|\s*[^|]*(?:SecretId|密钥)[^|]*\|\s*`([^`]+)`", line, re.I)
if m and m.group(1).strip().startswith("AKID"):
sid = m.group(1).strip()
m = re.search(r"\|\s*SecretKey\s*\|\s*`([^`]+)`", line, re.I)
if m:
skey = m.group(1).strip()
return sid, skey
def _load_config():
script_dir = Path(__file__).resolve().parent
env_file = script_dir / "config.env"
if not env_file.is_file():
env_file = script_dir / "config.example.env"
config = {
"NAS_BACKUP_ROOT": os.environ.get("NAS_BACKUP_ROOT", ""),
"SIZE_LIMIT_GB": int(os.environ.get("SIZE_LIMIT_GB", "1000")),
"ALERT_EMAIL_TO": os.environ.get("ALERT_EMAIL_TO", ""),
"SMTP_HOST": os.environ.get("SMTP_HOST", "smtp.qq.com"),
"SMTP_PORT": int(os.environ.get("SMTP_PORT", "465")),
"SMTP_USER": os.environ.get("SMTP_USER", ""),
"SMTP_PASSWORD": os.environ.get("SMTP_PASSWORD", ""),
}
if env_file.is_file():
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
k, v = k.strip(), v.strip().strip('"').strip("'")
if k in config:
if k == "SIZE_LIMIT_GB":
try:
config[k] = int(v)
except ValueError:
pass
elif k == "SMTP_PORT":
try:
config[k] = int(v)
except ValueError:
pass
else:
config[k] = v
for k, v in os.environ.items():
if k in config and v:
if k == "SIZE_LIMIT_GB":
try:
config[k] = int(v)
except ValueError:
pass
elif k == "SMTP_PORT":
try:
config[k] = int(v)
except ValueError:
pass
else:
config[k] = v
return config
def _dir_size_gb(path: Path) -> float:
"""目录占用大小GB"""
if not path.exists() or not path.is_dir():
return 0.0
total = 0
try:
for entry in path.rglob("*"):
if entry.is_file():
try:
total += entry.stat().st_size
except OSError:
pass
except OSError:
pass
return total / (1024 ** 3)
def _send_alert_email(config: dict, used_gb: float, limit_gb: int):
to_addr = config.get("ALERT_EMAIL_TO") or ""
if not to_addr:
print("[告警] 未配置 ALERT_EMAIL_TO跳过发邮件。")
return
user = config.get("SMTP_USER") or ""
password = config.get("SMTP_PASSWORD") or ""
if not user or not password:
print("[告警] 未配置 SMTP_USER/SMTP_PASSWORD跳过发邮件。")
return
subject = "[腾讯云备份] CKB NAS 备份目录已超过 %d GB 限制" % limit_gb
body = (
"腾讯云镜像/快照备份目录容量超限告警\n\n"
"备份根目录:%s\n"
"当前占用:%.2f GB\n"
"设定上限:%d GB\n\n"
"请清理旧备份或扩大限额,否则将不再拉取新镜像/快照文件。\n"
"时间:%s\n"
) % (
config.get("NAS_BACKUP_ROOT", ""),
used_gb,
limit_gb,
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = user
msg["To"] = to_addr
try:
with smtplib.SMTP_SSL(config.get("SMTP_HOST", "smtp.qq.com"), config.get("SMTP_PORT", 465)) as s:
s.login(user, password)
s.sendmail(user, [to_addr], msg.as_string())
print("[告警] 已发送邮件至 %s" % to_addr)
except Exception as e:
print("[告警] 发邮件失败: %s" % e)
def main():
config = _load_config()
nas_root = config.get("NAS_BACKUP_ROOT") or ""
if not nas_root:
print("请配置 NAS_BACKUP_ROOTCKB NAS 挂载后的备份根目录)")
print("参考 config.example.env复制为 config.env 并填写。")
return 1
nas_root = Path(nas_root)
limit_gb = config.get("SIZE_LIMIT_GB", 1000)
# 检查当前占用
used_gb = _dir_size_gb(nas_root)
if used_gb >= limit_gb:
print("备份目录已超过 %d GB当前 %.2f GB发送告警邮件并跳过拉取新文件。" % (limit_gb, used_gb))
_send_alert_email(config, used_gb, limit_gb)
# 仍可更新元数据(不占大空间)
else:
print("备份目录当前 %.2f GB / %d GB" % (used_gb, limit_gb))
sid, skey = _read_tencent_creds()
if not sid or not skey:
print("未配置腾讯云 SecretId/SecretKey环境变量或 00_账号与API索引.md")
return 1
try:
from tencentcloud.common import credential
from tencentcloud.cvm.v20170312 import cvm_client, models as cvm_models
from tencentcloud.cbs.v20170312 import cbs_client, models as cbs_models
except ImportError:
print("请安装: pip install tencentcloud-sdk-python-common tencentcloud-sdk-python-cvm tencentcloud-sdk-python-cbs")
return 1
cred = credential.Credential(sid, skey)
meta_dir = nas_root / "meta"
meta_dir.mkdir(parents=True, exist_ok=True)
# 1) 拉取自定义镜像(多地域)
all_images = []
for region in CVM_REGIONS:
try:
client = cvm_client.CvmClient(cred, region)
req = cvm_models.DescribeImagesRequest()
req.Limit = 100
req.Offset = 0
# 只拉自定义镜像
while True:
resp = client.DescribeImages(req)
items = getattr(resp, "ImageSet", None) or []
for img in items:
if getattr(img, "ImageType", "") == "PRIVATE_IMAGE":
all_images.append({
"Region": region,
"ImageId": getattr(img, "ImageId", ""),
"ImageName": getattr(img, "ImageName", ""),
"ImageSize": getattr(img, "ImageSize", 0),
"CreatedTime": getattr(img, "CreatedTime", ""),
"ImageState": getattr(img, "ImageState", ""),
})
if len(items) < req.Limit:
break
req.Offset += req.Limit
except Exception as e:
print("[%s] 镜像列表: %s" % (region, e))
images_file = meta_dir / ("images_%s.json" % datetime.now().strftime("%Y%m%d_%H%M%S"))
images_file.write_text(json.dumps(all_images, ensure_ascii=False, indent=2), encoding="utf-8")
# 保留最新一份为 images_latest.json
(meta_dir / "images_latest.json").write_text(
json.dumps(all_images, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("镜像元数据已写入 %s(共 %d 个自定义镜像)" % (images_file, len(all_images)))
# 2) 拉取快照(多地域)
all_snapshots = []
for region in CBS_REGIONS:
try:
client = cbs_client.CbsClient(cred, region)
req = cbs_models.DescribeSnapshotsRequest()
req.Limit = 100
req.Offset = 0
while True:
resp = client.DescribeSnapshots(req)
items = getattr(resp, "SnapshotSet", None) or []
for sn in items:
all_snapshots.append({
"Region": region,
"SnapshotId": getattr(sn, "SnapshotId", ""),
"SnapshotName": getattr(sn, "SnapshotName", ""),
"DiskSize": getattr(sn, "DiskSize", 0),
"CreatedTime": getattr(sn, "CreatedTime", ""),
"SnapshotState": getattr(sn, "SnapshotState", ""),
})
if len(items) < req.Limit:
break
req.Offset += req.Limit
except Exception as e:
print("[%s] 快照列表: %s" % (region, e))
snapshots_file = meta_dir / ("snapshots_%s.json" % datetime.now().strftime("%Y%m%d_%H%M%S"))
snapshots_file.write_text(json.dumps(all_snapshots, ensure_ascii=False, indent=2), encoding="utf-8")
(meta_dir / "snapshots_latest.json").write_text(
json.dumps(all_snapshots, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("快照元数据已写入 %s(共 %d 个快照)" % (snapshots_file, len(all_snapshots)))
# 3) 若配置了 COS可从 COS 同步镜像文件到 NAS需 tencentcloud-sdk-python-cos
cos_bucket = os.environ.get("COS_BUCKET") or ""
if not cos_bucket and os.path.isfile(Path(__file__).parent / "config.env"):
for line in (Path(__file__).parent / "config.env").read_text(encoding="utf-8").splitlines():
if line.strip().startswith("COS_BUCKET=") and "=" in line:
cos_bucket = line.split("=", 1)[1].strip().strip('"').strip("'")
break
if cos_bucket and used_gb < limit_gb:
try:
from tencentcloud.cos.cos_client import CosS3Client
from tencentcloud.cos.cos_config import CosConfig
region = os.environ.get("COS_REGION", "ap-guangzhou")
cos_prefix = os.environ.get("COS_PREFIX", "export_images")
config_cos = CosConfig(Region=region, SecretId=sid, SecretKey=skey)
client_cos = CosS3Client(config_cos)
# 列举并下载(示例:只列举,实际下载需根据 COS 返回的 Key 逐条 download
# 此处简化:仅提示用户可在此扩展 COS 下载逻辑
print("[COS] 已配置 COS_BUCKET如需自动从 COS 拉取镜像文件,请在脚本中扩展 COS 下载逻辑。")
except ImportError:
print("[COS] 未安装 cos-python-sdk-v5跳过从 COS 同步。")
else:
print("镜像文件需在腾讯云控制台「导出镜像」到 COS 后,再通过 COS 同步或 coscmd 下载到 NAS。")
# 若开始时未超限但结束时超限(例如本轮有下载),再发一次告警
used_gb_after = _dir_size_gb(nas_root)
if used_gb_after >= limit_gb and used_gb < limit_gb:
_send_alert_email(config, used_gb_after, limit_gb)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -37,3 +37,4 @@
| 2026-02-19 18:07:18 | 🔄 卡若AI 同步 2026-02-19 18:07 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 |
| 2026-02-19 19:40:16 | 🔄 卡若AI 同步 2026-02-19 19:40 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 5 个 |
| 2026-02-19 20:11:22 | 🔄 卡若AI 同步 2026-02-19 20:11 | 更新:金仓、运营中枢工作台 | 排除 >20MB: 5 个 |
| 2026-02-20 07:09:16 | 🔄 卡若AI 同步 2026-02-20 07:09 | 更新:金仓、水桥平台对接、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 5 个 |

View File

@@ -40,3 +40,4 @@
| 2026-02-19 18:07:18 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-19 18:07 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-19 19:40:16 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-19 19:40 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-19 20:11:22 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-19 20:11 | 更新:金仓、运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-20 07:09:16 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-20 07:09 | 更新:金仓、水桥平台对接、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |