From 227dc040047da5c77bd5a1d3c30a5985f0a4fcd8 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 1 Aug 2026 16:45:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(edge):=20=E6=96=AD=E7=94=B5=E4=BF=9D?= =?UTF-8?q?=E6=8A=A4(overlayroot+/data)=E4=B8=8E=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E9=95=9C=E5=83=8F=E6=89=B9=E9=87=8F=E7=94=9F=E4=BA=A7=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 setup-overlayroot.sh: 一键断电保护 - overlayroot(tmpfs) 保护根分区, 断电不再损坏文件系统 - 自动建 /data 独立分区 + fstab bind 挂载运行数据 - 迁移存量数据, journald 内存日志限容 - disable/enable 一键切换维护模式(适配香橙派 U-Boot/extlinux, 无 GRUB) - 新增 prepare-image.sh: 母卡出厂化清理 - 清 machine-id/SSH 密钥/device_id/hostname/日志 - 防误: 检测 overlayroot 启用状态, 保证镜像为可写出厂状态 - 新增 init-device.sh: 每台初始化 - 主机名/静态IP(netplan)/身份验证 - /data 自动 growpart 扩容到整卡剩余空间(64G 产品卡) - 统一启用断电保护 - deploy.sh/ops.sh/package-offline.sh: 集成新脚本 - ops.sh 新增 overlay-status/disable/enable/verify - docs/deployment.md: 重写为主流程文档 - 产品镜像批量生产(16G 母卡->小镜像->Etcher 批量烧录->每台初始化) - 移除废弃的克隆卡流程 --- docs/deployment.md | 294 ++++++++++++------- scripts/deploy.sh | 19 ++ scripts/init-device.sh | 243 +++++++++++++++ scripts/ops.sh | 34 ++- scripts/package-offline.sh | 3 + scripts/prepare-image.sh | 138 +++++++++ scripts/setup-overlayroot.sh | 554 +++++++++++++++++++++++++++++++++++ 7 files changed, 1178 insertions(+), 107 deletions(-) create mode 100644 scripts/init-device.sh create mode 100644 scripts/prepare-image.sh create mode 100644 scripts/setup-overlayroot.sh diff --git a/docs/deployment.md b/docs/deployment.md index 810364c..cecaa24 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,95 +1,185 @@ # Edge 设备部署指南 -## 首次部署(从 SD 卡克隆) - -### 克隆前准备 - -在源设备上清理不需要的文件后关机,取出 SD 卡,使用 `dd` 或烧录工具克隆到新卡。 - -### 克隆后配置 - -将新 SD 卡插入新设备,开机,执行以下步骤: - -#### 1. 生成新的设备 ID - -管理端通过设备 ID 区分不同设备,克隆后必须更换: - -```bash -sudo rm -f /etc/machine-id /var/lib/safesight-edge-agent/device_id -sudo systemd-machine-id-setup -``` - -#### 2. 重新生成 SSH 主机密钥 - -```bash -sudo rm -f /etc/ssh/ssh_host_*_key* -sudo ssh-keygen -A -``` - -#### 3. 设置主机名 - -```bash -sudo hostnamectl set-hostname <新主机名> -``` - -#### 4. 配置静态 IP(生产环境) - -编辑 netplan 配置: - -```bash -sudo nano /etc/netplan/01-network-manager-all.yaml -``` - -写入: - -```yaml -network: - version: 2 - ethernets: - enP4p65s0: # 网卡名,用 ip addr 确认 - dhcp4: no - addresses: - - 10.0.0.82/24 # 目标 IP - routes: - - to: default - via: 10.0.0.1 # 网关 - nameservers: - addresses: - - 10.0.0.1 # DNS -``` - -应用: - -```bash -sudo netplan apply -``` - -#### 5. 重启 - -```bash -sudo reboot -``` - -### 验证 - -```bash -# 确认服务状态 -systemctl status safesight-edge-server safesight-edge-agent - -# 确认新 IP -ip addr show | grep inet - -# 确认新设备 ID -cat /var/lib/safesight-edge-agent/device_id - -# 管理端发现新设备后,在设备详情页设置别名 -``` +> **本文件是 Edge 设备的唯一操作手册。** 现场部署时配合 `docs/客户现场部署检查表.md` 逐项打勾。 +> +> **推荐流程(新项目)**:`产品镜像批量生产`(第 2 章)——16G 母卡制作产品镜像 → Etcher 批量烧录 → 每台脚本初始化。 +> 旧版"克隆卡"方式不再使用。 +> +> **仅部署 1~2 台**:可跳过镜像,直接在设备上执行 `deploy.sh install` 后用第 3 章启用断电保护。 --- -## 更新部署 +## 1. 涉及的工具与脚本 -### 首次安装(新设备/离线部署) +| 脚本(安装于 `/opt/safesight-edge-server/scripts/`) | 作用 | 在哪个阶段使用 | +|---|---|---| +| `install_deps.sh` | 安装运行环境(MPP / RKNN / ffmpeg-rockchip) | 母卡制作 | +| `package-offline.sh` | 编译并打包离线部署包 | 母卡制作 | +| `deploy.sh` | 安装/升级 safesight 程序与服务 | 母卡制作 | +| `setup-overlayroot.sh` | 断电保护:建 /data 分区 + overlayroot | 母卡制作 / 每台初始化 | +| `prepare-image.sh` | 出厂化清理(打镜像前必跑) | 母卡制作 | +| `init-device.sh` | 每台初始化(IP/主机名/扩 /data/启用保护) | 批量烧录后每台 | +| `ops.sh` | 运维(状态/日志/断电保护切换) | 日常 | + +--- + +## 2. 产品镜像批量生产(推荐主流程) + +> **核心思路:用一张 16G 小卡做母卡,镜像体积 = 官方镜像级别**(gzip 后约 1~2GB)。 +> 镜像结构上仍是标准整卡镜像(官方也是):分区表 + U-Boot + 分区内容。它小的原因是**母卡容量小**, +> 而不是特殊打包。烧到 64G 产品卡后,`/data` 分区由 init-device.sh 首次启动时自动 growpart 扩满整卡, +> 存储不浪费。未使用空间是零块,gzip 压缩后几乎不占体积,Etcher 直接支持 `.img.gz`。 + +### 阶段一:母卡制作(16G 开发卡,插在 Orange Pi 上) + +1. Etcher 烧录官方镜像(如 `Orangepi5_1.2.2_ubuntu_jammy_server_linux5.10.160.img`)→ **16G SD 卡**,启动。 +2. 安装运行环境:`sudo bash scripts/install_deps.sh`(MPP / RKNN / ffmpeg-rockchip)。 +3. 编译并打离线包:`bash scripts/build.sh -m -j4 && bash scripts/package-offline.sh`。 +4. 部署并验证:`sudo AGENT_TOKEN=<主钥匙> ./deploy.sh install`;导入模型/人脸库/RTSP,验证推流与算法。 +5. 创建 /data 分区与 fstab(出厂带断电保护能力,但**不启用保护**): + +```bash +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh install --yes # 建 /data 分区 + fstab + 装 overlayroot +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable # 保持可写!镜像出厂不启用保护 +``` + +> `install` 会创建 /data 分区、写入 fstab、安装 overlayroot 但**不重启**(当前会话仍可写); +> 随后 `disable` 把 overlayroot 置空,保证出厂镜像首次启动处于可写状态, +> 机器 ID 才能固化、init-device.sh 才能改配置。 + +6. **出厂化清理**(关键,否则每台设备身份相同): + +```bash +sudo /opt/safesight-edge-server/scripts/prepare-image.sh +sudo poweroff # 确保文件系统一致后摘卡 +``` + +> `prepare-image.sh` 清除 machine-id / SSH 密钥 / device_id / hostname / 全部日志与告警数据,保留程序、依赖、`/data` 分区与 fstab。 +> 若脚本提示"overlayroot 已启用",说明第 5 步的 `disable` 未生效,先补跑再执行。 + +### 阶段二:生成镜像文件(Windows / Linux 主机) + +```bash +# Linux(推荐,边读边压缩,16G 卡约 2~4 分钟): +sudo dd if=/dev/sdX bs=4M status=progress conv=fsync | gzip > safesight-product-$(date +%Y%m%d).img.gz +# Windows: 用 Win32DiskImager "读取"生成 .img(16G 卡很快),再用 7-Zip 转 .gz +``` + +> 为什么还是整卡 dd?香橙派 U-Boot 引导写在 SD 卡非文件系统区域,分区级打包会丢引导、无法启动; +> 官方镜像同样如此。只是**源卡用 16G**,镜像就是官方那种体积,不再有 64G 整卡 dump 的问题。 +> (若坚持从 64G 卡做小镜像:Linux 上用 `bmaptool` 只复制已使用块,效果相同且更慢,不推荐。) + +### 阶段三:批量烧录 + +- 用 Balena Etcher 批量烧录 `.img.gz` 到 **64G 产品卡**(写入 + 自动校验)。 +- 每台烧录后**首次启动会自动生成唯一** machine-id / SSH 主机密钥 / device_id,互不冲突。 + +### 阶段四:每台初始化 + +```bash +# 设备上电后(首次为 DHCP,用 IP 扫描找到设备后 SSH 登录) +sudo /opt/safesight-edge-server/scripts/init-device.sh \ + --hostname safesight-01 --ip 10.0.0.83/24 --gw 10.0.0.1 --dns 10.0.0.1 +sudo reboot +``` + +脚本自动完成:主机名 → 静态 IP(netplan) → 身份验证 → **/data 自动扩容到整卡剩余空间** +(growpart + resize2fs,64G 卡 /data ≈ 55G)→ 服务验证 → **启用断电保护** → 提示重启。 +可选参数:`--hls-ram`(HLS 放内存)、`--dhcp`(保持 DHCP)、`--no-overlay`、`--token <主钥匙>`(轮换 token)。 + +### 阶段五:验收 + +```bash +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh verify # 断电保护验证(全部 ✓) +df -h /data # 应为 ~55G +systemctl status safesight-edge-server safesight-edge-agent # 服务正常 +# 管理端:发现设备 → 设置别名 → 导入摄像头/模型 → 下发配置 → 验证推流与算法 +``` + +### 为什么出厂镜像不启用 overlayroot? + +- overlayroot 启用后根分区只读,`/etc/machine-id` 无法在首次启动时固化(tmpfs 重启即丢)→ 每次重启设备 ID 漂移,管理端会不断看到"新设备"。 +- 每台改 IP/主机名也需先进维护模式,批量初始化多一次重启。 +- 因此:**镜像保持可写出厂状态,overlayroot 由 init-device.sh 在每台初始化最后统一启用**,每台只重启一次即进入保护。 + +--- + +## 3. 断电保护(overlayroot + /data 分区)参考 + +> 解决 SD 卡断电重启导致根文件系统损坏、无法启动的问题。 +> **启用后根分区只读,修改系统配置需先进入维护模式。** + +### 原理 + +- 用 Ubuntu 官方 `overlayroot` 把根文件系统置于 tmpfs overlay 之上:根分区永久只读,一切写入进入内存,重启即丢,**断电不再损坏根分区**。 +- 在 SD 卡剩余空间划分独立读写分区 `/data`(ext4),保存日志、告警图片/视频、设备 ID 等必须持久化的数据。 +- `safesight` 的运行目录(`/var/lib/safesight-edge-server/*`、`/var/lib/safesight-edge-agent`)通过 fstab bind 挂载到 `/data`,**路径不变**,现有部署脚本零改动。 + +### 单台启用(未走镜像流程时) + +```bash +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh install +sudo reboot +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh verify # 全部 ✓ 即为生效 +``` + +脚本自动完成:检测空闲空间建 /data(ext4)→ 写 fstab(/data + bind,均 `nofail`)→ 迁移存量数据 +→ journald 内存日志(64M 上限)→ 安装并配置 overlayroot → 重建 initramfs。 + +### 可选参数 + +| 参数 | 说明 | +|---|---| +| `--yes` | 跳过分区确认(无人值守) | +| `--hls-ram` | HLS 分片放内存 tmpfs(512M),减小 SD 卡写入;重启后直播画面需重新拉流 | +| `--journal-data` | 系统日志持久化到 /data(默认放内存,重启即丢) | + +### 日常维护(临时进入可写模式) + +> 香橙派使用 U-Boot + extlinux 引导,**没有 GRUB**,不要在引导菜单改内核参数。用脚本一键切换: + +```bash +# 进入维护模式(装软件/改系统配置) +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable +sudo reboot + +# 维护完成,恢复保护 +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh enable +sudo reboot +``` + +或使用运维脚本:`sudo /opt/safesight-edge-server/scripts/ops.sh overlay-disable` / `overlay-enable`。 + +### 查看状态 + +```bash +sudo /opt/safesight-edge-server/scripts/ops.sh overlay-status +# 或 +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh status +``` + +### 重要注意事项 + +- **启用前**确认:静态 IP、主机名、SSH 密钥、设备 ID 均已配置完成(启用后根分区只读,改动需维护模式)。 +- **维护模式期间**断电仍有损坏风险,维护完成后务必 `enable` 恢复保护。 +- HLS 分片写入量较大(与码率、路数、保留天数相关),默认落 `/data`;若 SD 写入寿命紧张可加 `--hls-ram`。 +- 若 SD 卡无剩余空间(根分区已占满),需重新烧录含 `/data` 分区的母卡镜像后再执行本方案。 + +--- + +## 4. 更新部署(软件升级) + +### 4.1 单台升级(目标设备可访问 git) + +```bash +cd ~/apps/safesight-edge +git pull +bash scripts/build.sh -m -j4 +cd agent && go build -o safesight-edge-agent ./cmd/safesight-agent && cd .. +sudo ./scripts/deploy.sh upgrade +``` + +### 4.2 离线升级包(无 git 环境) 在开发机(已安装 safesight-edge 源码和编译环境)上: @@ -104,31 +194,23 @@ git pull && bash scripts/package-offline.sh scp safesight-upgrade-*.tar.gz orangepi@<目标IP>:/tmp/ ``` -在目标设备上: +在目标设备上(**已启用断电保护的设备需先进入维护模式**): ```bash +# 已启用断电保护的设备: +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable && sudo reboot + +# 升级: cd /tmp && tar -xzf safesight-upgrade-*.tar.gz cd safesight-upgrade-* -sudo AGENT_TOKEN=<管理端token> ./deploy.sh install +sudo AGENT_TOKEN=<管理端token> ./deploy.sh install # 或 ./deploy.sh upgrade 保留配置 + +# 恢复保护: +sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh enable && sudo reboot ``` -> 安装后配置为空模板,通过管理端部署向导下发实际配置。 +### 4.3 升级产品镜像(批量设备升级) -### 在线更新(可访问 git) +重新走第 2 章:母卡 `git pull` → 编译 → `deploy.sh upgrade` → `prepare-image.sh` → 打镜像 → 批量烧录 → 每台初始化。 -```bash -cd ~/apps/safesight-edge -git pull -bash scripts/build.sh -m -j4 -cd agent && go build -o safesight-edge-agent ./cmd/safesight-agent && cd .. -sudo ./scripts/deploy.sh upgrade -``` - -### 旧设备首次升级 - -```bash -cd ~/apps/safesight-edge && git pull -bash scripts/build.sh -m -j4 -cd agent && go build -o safesight-edge-agent ./cmd/safesight-agent -sudo ./scripts/upgrade-from-old.sh -``` +> 提示:程序升级通常不动 `/data`(设备 ID 不丢);若升级脚本需要改写系统配置,母卡流程已天然覆盖。 diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 62c26db..6d013ed 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -257,6 +257,21 @@ cmd_install_media_server() { chmod +x "$INSTALL_DIR/scripts/monitor_hw.sh" echo -e "${GREEN}✓${NC} 已复制运维脚本: monitor_hw.sh" fi + if [ -f "$PROJECT_DIR/scripts/setup-overlayroot.sh" ]; then + cp "$PROJECT_DIR/scripts/setup-overlayroot.sh" "$INSTALL_DIR/scripts/" + chmod +x "$INSTALL_DIR/scripts/setup-overlayroot.sh" + echo -e "${GREEN}✓${NC} 已复制断电保护脚本: setup-overlayroot.sh" + fi + if [ -f "$PROJECT_DIR/scripts/init-device.sh" ]; then + cp "$PROJECT_DIR/scripts/init-device.sh" "$INSTALL_DIR/scripts/" + chmod +x "$INSTALL_DIR/scripts/init-device.sh" + echo -e "${GREEN}✓${NC} 已复制设备初始化脚本: init-device.sh" + fi + if [ -f "$PROJECT_DIR/scripts/prepare-image.sh" ]; then + cp "$PROJECT_DIR/scripts/prepare-image.sh" "$INSTALL_DIR/scripts/" + chmod +x "$INSTALL_DIR/scripts/prepare-image.sh" + echo -e "${GREEN}✓${NC} 已复制镜像出厂化脚本: prepare-image.sh" + fi # 复制插件 if [ -d "$BUILD_DIR/plugins" ]; then @@ -723,6 +738,10 @@ cmd_install() { echo "" echo "HLS播放:" echo " http://$(hostname -I | awk '{print $1}'):9000/hls_player.html" + echo "" + echo "断电保护(推荐):" + echo " sudo $INSTALL_DIR/scripts/setup-overlayroot.sh install" + echo " 说明: 防止 SD 卡断电损坏,详见 docs/deployment.md 断电保护章节" } # 升级 diff --git a/scripts/init-device.sh b/scripts/init-device.sh new file mode 100644 index 0000000..efdad93 --- /dev/null +++ b/scripts/init-device.sh @@ -0,0 +1,243 @@ +#!/bin/bash +# ============================================================================= +# init-device.sh — 批量烧录后的单台设备初始化 +# ----------------------------------------------------------------------------- +# 在每台烧录好产品镜像的盒子上运行(root)。完成: +# 1. 设置主机名 +# 2. 设置静态 IP(netplan) +# 3. 验证设备身份唯一(machine-id / SSH 密钥 / device_id) +# 4. 验证 safesight 服务正常 +# 5. 启用断电保护 overlayroot(出厂镜像故意未启用,统一在此启用) +# 6. 重启 +# +# 用法: +# sudo ./init-device.sh --hostname safesight-01 \ +# --ip 10.0.0.83/24 --gw 10.0.0.1 --dns 10.0.0.1 +# 可选: +# --token <后台管理统一主钥匙> 轮换 agent token(默认沿用镜像内主钥匙) +# --hls-ram HLS 分片放内存(减小 SD 写入) +# --dhcp 保持 DHCP(默认会写静态 IP) +# --no-overlay 不启用断电保护(仅初始化) +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}⚠${NC} $*"; } +die() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } + +# --------------------------------------------------------------------------- +# 参数 +# --------------------------------------------------------------------------- +HOSTNAME="" IP_CIDR="" GW="" DNS="" TOKEN="" HLS_RAM=0 USE_DHCP=0 NO_OVERLAY=0 + +usage() { + echo "用法: sudo $0 --hostname <名> --ip --gw <网关> --dns [选项]" + echo "" + echo "必选:" + echo " --hostname <名称> 如 safesight-01" + echo " --ip 如 10.0.0.83/24" + echo " --gw <网关> 如 10.0.0.1" + echo " --dns 如 10.0.0.1" + echo "可选:" + echo " --token <主钥匙> 轮换后台管理统一主钥匙" + echo " --hls-ram HLS 分片放内存" + echo " --dhcp 保持 DHCP(不写静态 IP)" + echo " --no-overlay 不启用断电保护" + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --hostname) HOSTNAME="$2"; shift 2 ;; + --ip) IP_CIDR="$2"; shift 2 ;; + --gw) GW="$2"; shift 2 ;; + --dns) DNS="$2"; shift 2 ;; + --token) TOKEN="$2"; shift 2 ;; + --hls-ram) HLS_RAM=1; shift ;; + --dhcp) USE_DHCP=1; shift ;; + --no-overlay) NO_OVERLAY=1; shift ;; + *) warn "未知参数: $1"; shift ;; + esac +done + +[ "$(id -u)" -eq 0 ] || die "请使用 sudo 运行" + +if [ "$USE_DHCP" = "0" ]; then + [ -n "$HOSTNAME" ] || die "缺少 --hostname" + [ -n "$IP_CIDR" ] || die "缺少 --ip" + [ -n "$GW" ] || die "缺少 --gw" + [ -n "$DNS" ] || die "缺少 --dns" +fi + +OVERLAY_SCRIPT="/opt/safesight-edge-server/scripts/setup-overlayroot.sh" +[ -x "$OVERLAY_SCRIPT" ] || OVERLAY_SCRIPT="$(cd "$(dirname "$0")" && pwd)/setup-overlayroot.sh" + +# --------------------------------------------------------------------------- +# 1. 主机名 +# --------------------------------------------------------------------------- +if [ -n "$HOSTNAME" ]; then + info "设置主机名: $HOSTNAME" + hostnamectl set-hostname "$HOSTNAME" + sed -i "/^127.0.1.1/d" /etc/hosts + echo "127.0.1.1 $HOSTNAME" >> /etc/hosts + ok "主机名已设置" +fi + +# --------------------------------------------------------------------------- +# 2. 静态 IP(netplan) +# --------------------------------------------------------------------------- +NETPLAN_DIR="/etc/netplan" +if [ "$USE_DHCP" = "0" ]; then + # 检测默认网卡(优先有默认路由的网卡,其次第一个非 lo 网卡) + IFACE="$(ip route show default 2>/dev/null | awk '/default/{print $5; exit}')" + [ -n "$IFACE" ] || IFACE="$(ls /sys/class/net | grep -v '^lo$' | head -1)" + [ -n "$IFACE" ] || die "未检测到网卡" + + info "配置静态 IP: $IFACE $IP_CIDR (gw=$GW dns=$DNS)" + mkdir -p "$NETPLAN_DIR" + # 备份现有配置 + cp -a "$NETPLAN_DIR"/01-*.yaml "$NETPLAN_DIR/backup-"$(date +%s)".yaml" 2>/dev/null || true + rm -f "$NETPLAN_DIR"/01-*.yaml 2>/dev/null || true + + cat > "$NETPLAN_DIR/01-safesight-static.yaml" << EOF +network: + version: 2 + ethernets: + $IFACE: + dhcp4: no + addresses: + - $IP_CIDR + routes: + - to: default + via: $GW + nameservers: + addresses: + - $DNS +EOF + netplan apply 2>/dev/null || warn "netplan apply 失败(重启后生效)" + ok "静态 IP 已配置: $IP_CIDR" +else + info "保持 DHCP" +fi + +# --------------------------------------------------------------------------- +# 3. 设备身份验证(出厂镜像已清空,首次启动应已自动重建) +# --------------------------------------------------------------------------- +info "验证设备身份 ..." +[ -s /etc/machine-id ] || { systemd-machine-id-setup 2>/dev/null || warn "machine-id 缺失(重启后自动生成)"; } +[ -s /etc/machine-id ] && ok "machine-id: $(cat /etc/machine-id)" +if ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1; then + ok "SSH 主机密钥已生成" +else + ssh-keygen -A 2>/dev/null || true + ok "SSH 主机密钥已生成(ssh-keygen -A)" +fi + +# --------------------------------------------------------------------------- +# 4. agent token(可选轮换) +# --------------------------------------------------------------------------- +if [ -n "$TOKEN" ]; then + info "轮换 agent 主钥匙 ..." + printf '%s\n' "$TOKEN" > /opt/safesight-edge-agent/agent.token + chmod 600 /opt/safesight-edge-agent/agent.token + # 更新 agent 配置中的 token + if [ -f /opt/safesight-edge-agent/agent.config.json ]; then + sed -i "s/\"token\": \"[^\"]*\"/\"token\": \"$TOKEN\"/" /opt/safesight-edge-agent/agent.config.json + fi + ok "agent token 已更新" +fi + +# --------------------------------------------------------------------------- +# 4.5 /data 分区自动扩容(小镜像烧到 64G 产品卡后,把 /data 扩到整卡剩余) +# --------------------------------------------------------------------------- +grow_data_partition() { + info "扩容 /data 分区到 SD 卡剩余空间 ..." + DATA_PART="$(findmnt -no SOURCE /data 2>/dev/null || true)" + [ -n "$DATA_PART" ] || { warn "/data 未挂载,跳过扩容"; return; } + + DISK=""; PARTNUM="" + case "$DATA_PART" in + /dev/mmcblk*) DISK="${DATA_PART%p*}"; PARTNUM="${DATA_PART##*p}" ;; + /dev/nvme*) DISK="${DATA_PART%p*}"; PARTNUM="${DATA_PART##*p}" ;; + /dev/sd*) DISK="${DATA_PART%[0-9]*}"; PARTNUM="${DATA_PART##*[a-z]}" ;; + esac + [ -n "$DISK" ] || { warn "无法识别 /data 所在磁盘,跳过扩容"; return; } + + # 需要 growpart(cloud-guest-utils 提供) + if ! command -v growpart >/dev/null 2>&1; then + info "安装 growpart ..." + apt-get install -y cloud-guest-utils 2>/dev/null || warn "growpart 安装失败,跳过扩容" + fi + command -v growpart >/dev/null 2>&1 || return + + # 停服务并卸载 /data 后扩容(避免分区忙) + systemctl stop safesight-edge-server safesight-edge-agent 2>/dev/null || true + umount /data 2>/dev/null || true + growpart "$DISK" "$PARTNUM" 2>/dev/null || warn "growpart 无变化(可能已扩满)" + resize2fs "$DATA_PART" 2>/dev/null || warn "resize2fs 失败" + mount /data 2>/dev/null || mount -a 2>/dev/null || true + systemctl start safesight-edge-server safesight-edge-agent 2>/dev/null || true + ok "/data 扩容完成: $(df -h /data 2>/dev/null | awk 'NR==2{print "总"$2" 可用"$4}')(64G 产品卡自动占满剩余空间)" +} + +# --------------------------------------------------------------------------- +# 5. 验证 safesight 服务 +# --------------------------------------------------------------------------- +info "验证服务 ..." +systemctl start safesight-edge-server safesight-edge-agent 2>/dev/null || true +sleep 3 +for s in safesight-edge-server safesight-edge-agent; do + if systemctl is-active --quiet "$s" 2>/dev/null; then + ok "$s 运行中" + else + warn "$s 未运行(可稍后 systemctl status $s 排查)" + fi +done +if [ -f /var/lib/safesight-edge-agent/device_id ]; then + ok "device_id: $(cat /var/lib/safesight-edge-agent/device_id)" +else + info "device_id 由 agent 首次运行时生成(读取 machine-id)" +fi + +# --------------------------------------------------------------------------- +# 4.5 /data 分区自动扩容(小镜像烧到 64G 产品卡后,把 /data 扩到整卡剩余) +# --------------------------------------------------------------------------- +grow_data_partition + +# --------------------------------------------------------------------------- +# 6. 启用断电保护 +# --------------------------------------------------------------------------- +if [ "$NO_OVERLAY" = "1" ]; then + warn "跳过断电保护启用(--no-overlay)" +else + info "启用断电保护(overlayroot + /data)..." + EXTRA="" + [ "$HLS_RAM" = "1" ] && EXTRA="--hls-ram" + if [ -x "$OVERLAY_SCRIPT" ]; then + bash "$OVERLAY_SCRIPT" install --yes $EXTRA || die "断电保护启用失败" + else + die "未找到 setup-overlayroot.sh($OVERLAY_SCRIPT),请先完成部署" + fi + ok "断电保护已配置,重启后生效" +fi + +# --------------------------------------------------------------------------- +# 7. 完成 +# --------------------------------------------------------------------------- +echo "" +echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN} 初始化完成!${NC}" +echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" +echo "" +echo " IP: ${IP_CIDR:-DHCP}" +echo " 主机名: ${HOSTNAME:-未设置}" +echo " 管理界面: http://${IP_CIDR%/*}:9100" +echo "" +echo "请重启使全部配置生效:" +echo " sudo reboot" +echo "" +echo "重启后建议:" +echo " sudo $OVERLAY_SCRIPT verify # 断电保护验证" +echo " 在管理端发现该设备 → 设置别名 → 下发配置" diff --git a/scripts/ops.sh b/scripts/ops.sh index 29b019e..9392230 100755 --- a/scripts/ops.sh +++ b/scripts/ops.sh @@ -2,7 +2,7 @@ # RK3588 Edge Server 运维脚本 # 安装后位于 /opt/safesight-edge-server/script/ops.sh # -# 用法: sudo ./ops.sh [status|start|stop|restart|logs|hw|ddr-status|ddr-performance|ddr-ondemand|ddr-restore] +# 用法: sudo ./ops.sh [status|start|stop|restart|logs|hw|ddr-status|ddr-performance|ddr-ondemand|ddr-restore|overlay-status|overlay-disable|overlay-enable|overlay-verify] set -e @@ -134,6 +134,22 @@ cmd_ddr_restore() { bash /opt/safesight-edge-server/scripts/ddr_mode.sh restore } +cmd_overlay_status() { + bash /opt/safesight-edge-server/scripts/setup-overlayroot.sh status +} + +cmd_overlay_disable() { + bash /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable +} + +cmd_overlay_enable() { + bash /opt/safesight-edge-server/scripts/setup-overlayroot.sh enable +} + +cmd_overlay_verify() { + bash /opt/safesight-edge-server/scripts/setup-overlayroot.sh verify +} + # 主入口 case "${1:-status}" in status) @@ -166,6 +182,18 @@ case "${1:-status}" in ddr-restore) cmd_ddr_restore ;; + overlay-status) + cmd_overlay_status + ;; + overlay-disable) + cmd_overlay_disable + ;; + overlay-enable) + cmd_overlay_enable + ;; + overlay-verify) + cmd_overlay_verify + ;; *) echo "RK3588 Edge Server 运维脚本" echo "" @@ -182,6 +210,10 @@ case "${1:-status}" in echo " ddr-performance DDR 切到高性能模式" echo " ddr-ondemand DDR 切回 dmc_ondemand" echo " ddr-restore 恢复上次保存的 DDR 配置" + echo " overlay-status 断电保护状态 (overlayroot)" + echo " overlay-disable 断电保护-临时禁用(维护模式,可写)" + echo " overlay-enable 断电保护-重新启用(只读保护)" + echo " overlay-verify 断电保护-重启后验证" echo "" echo "示例:" echo " sudo $(basename $0) status" diff --git a/scripts/package-offline.sh b/scripts/package-offline.sh index ab09a51..68c9450 100644 --- a/scripts/package-offline.sh +++ b/scripts/package-offline.sh @@ -65,6 +65,9 @@ if [ -f scripts/ops.sh ]; then cp scripts/ops.sh "$BUILD_DIR/"; fi if [ -f scripts/monitor_hw.sh ]; then cp scripts/monitor_hw.sh "$BUILD_DIR/"; fi if [ -f scripts/ddr_mode.sh ]; then cp scripts/ddr_mode.sh "$BUILD_DIR/"; fi if [ -f scripts/install_deps.sh ]; then cp scripts/install_deps.sh "$BUILD_DIR/"; fi +if [ -f scripts/setup-overlayroot.sh ]; then cp scripts/setup-overlayroot.sh "$BUILD_DIR/"; fi +if [ -f scripts/prepare-image.sh ]; then cp scripts/prepare-image.sh "$BUILD_DIR/"; fi +if [ -f scripts/init-device.sh ]; then cp scripts/init-device.sh "$BUILD_DIR/"; fi # 系统硬件加速库(从当前设备复制,确保兼容) mkdir -p "$BUILD_DIR/lib_deps" diff --git a/scripts/prepare-image.sh b/scripts/prepare-image.sh new file mode 100644 index 0000000..4729086 --- /dev/null +++ b/scripts/prepare-image.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# ============================================================================= +# prepare-image.sh — 产品镜像出厂化清理(母卡制作镜像前运行) +# ----------------------------------------------------------------------------- +# 用法(母卡上,root): +# sudo ./prepare-image.sh 出厂化清理 +# sudo ./prepare-image.sh --zero 额外清零空闲空间(压缩镜像体积,较慢) +# +# 作用:把"部署完成、验证通过"的母卡变成干净的出厂状态: +# - 清除设备个体身份(machine-id / SSH 密钥 / device_id / hostname) +# - 清空 /data 与运行时数据(日志/告警/HLS),保留分区与 fstab +# - 保留:已部署的 safesight、已装的依赖、overlayroot 配置能力(出厂即带断电保护) +# +# 重要:本脚本后【不要】启用 overlayroot(setup-overlayroot.sh enable), +# 保持"可写出厂状态",overlayroot 留到每台设备初始化最后一步启用。 +# 否则 /etc/machine-id 无法固化,每台重启后设备 ID 漂移。 +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}⚠${NC} $*"; } +die() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } + +ZERO=0 +for arg in "$@"; do + case "$arg" in + --zero) ZERO=1 ;; + *) warn "未知参数: $arg" ;; + esac +done + +[ "$(id -u)" -eq 0 ] || die "请使用 sudo 运行" + +RUNTIME_ROOT="/var/lib/safesight-edge-server" +AGENT_LIB="/var/lib/safesight-edge-agent" +DATA_MOUNT="/data" +OVERLAY_CONF="/etc/overlayroot.conf" + +# --------------------------------------------------------------------------- +# 安全检查 +# --------------------------------------------------------------------------- +if grep -qs 'overlay' /proc/mounts; then + die "当前处于 overlayroot 保护模式!请先进入维护模式(setup-overlayroot.sh disable + 重启)再运行。" +fi +if grep -qE '^overlayroot="[^"]+"' "$OVERLAY_CONF" 2>/dev/null; then + die "检测到 $OVERLAY_CONF 已启用 overlayroot(保护模式配置)。出厂镜像必须保持可写状态,\n 请先运行 setup-overlayroot.sh disable(设置 overlayroot=\"\")后再执行出厂化。" +fi + +echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" +echo -e "${YELLOW} 即将把母卡重置为出厂状态,此操作不可恢复!${NC}" +echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" +echo "" +echo "将清除:" +echo " - /etc/machine-id、SSH 主机密钥、hostname、device_id(首次启动自动重建)" +echo " - /data 与 /var/lib/safesight-edge-server 下所有日志/告警/HLS 数据" +echo "" +echo "将保留:" +echo " - safesight 程序与配置模板、全部依赖、/data 分区与 fstab(断电保护能力)" +echo "" +read -r -p "继续? [y/N]: " yn +[ "${yn:-n}" = "y" ] || [ "${yn:-n}" = "Y" ] || die "已取消" + +# --------------------------------------------------------------------------- +# 1. 停止服务 +# --------------------------------------------------------------------------- +info "停止服务 ..." +systemctl stop safesight-edge-server safesight-edge-agent 2>/dev/null || true +ok "服务已停止" + +# --------------------------------------------------------------------------- +# 2. 清空数据(保留目录结构) +# --------------------------------------------------------------------------- +info "清空运行数据 ..." +for d in hls logs alarms clips; do + rm -rf "$RUNTIME_ROOT/$d"/* + rm -rf "$DATA_MOUNT/safesight/$d"/* 2>/dev/null || true +done +rm -rf "$AGENT_LIB"/* /data/safesight/agent/* 2>/dev/null || true +[ -f "$AGENT_LIB/device_id" ] && rm -f "$AGENT_LIB/device_id" +ok "运行数据已清空(目录保留)" + +# --------------------------------------------------------------------------- +# 3. 设备身份重置(首次启动自动重建) +# --------------------------------------------------------------------------- +info "重置设备身份 ..." +rm -f /etc/machine-id /var/lib/dbus/machine-id +rm -f /etc/ssh/ssh_host_*_key* /etc/ssh/ssh_host_*_key*.pub +rm -f "$AGENT_LIB/device_id" +hostnamectl set-hostname orangepi 2>/dev/null || true +ok "machine-id / SSH 密钥 / hostname / device_id 已重置(首次启动自动生成唯一值)" + +# --------------------------------------------------------------------------- +# 4. 日志与缓存清理 +# --------------------------------------------------------------------------- +info "清理日志与缓存 ..." +journalctl --vacuum-size=1M >/dev/null 2>&1 || true +rm -rf /var/log/journal/* 2>/dev/null || true +find /var/log -type f -name "*.log" -delete 2>/dev/null || true +apt-get clean 2>/dev/null || true +rm -f /root/.bash_history /home/orangepi/.bash_history 2>/dev/null || true +ok "日志与缓存已清理" + +# --------------------------------------------------------------------------- +# 5. 清零空闲空间(可选,压缩镜像体积) +# --------------------------------------------------------------------------- +if [ "$ZERO" = "1" ]; then + info "清零空闲空间(SD 卡写入较慢,请耐心等待)..." + for target in /data / ; do + fill="$target/zero.fill" + echo " $target ..." + dd if=/dev/zero of="$fill" bs=4M 2>/dev/null || true + rm -f "$fill" + done + ok "空闲空间已清零" +else + warn "未清零空闲空间。如需缩小镜像体积,可加 --zero 重新执行。" +fi + +# --------------------------------------------------------------------------- +# 6. 完成 +# --------------------------------------------------------------------------- +echo "" +echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN} 出厂化完成!可以关机、摘卡、制作产品镜像${NC}" +echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" +echo "" +echo "下一步:" +echo " 1) sudo poweroff # 关机(确保文件系统一致)" +echo " 2) 摘卡,在主机上制作镜像:" +echo " Windows: Win32DiskImager 读取 → img" +echo " Linux: sudo dd if=/dev/sdX bs=4M status=progress | gzip > safesight-product.img.gz" +echo " 3) 每台烧录后首次启动会自动生成唯一 machine-id / SSH 密钥 / device_id" +echo " 4) 每台初始化: sudo ./init-device.sh --hostname ... --ip ...(见脚本说明)" +echo "" +echo "注意: 此状态【尚未启用 overlayroot】——出厂镜像故意如此。" +echo " overlayroot 由 init-device.sh 在每台初始化最后一步统一启用。" diff --git a/scripts/setup-overlayroot.sh b/scripts/setup-overlayroot.sh new file mode 100644 index 0000000..5472ca7 --- /dev/null +++ b/scripts/setup-overlayroot.sh @@ -0,0 +1,554 @@ +#!/bin/bash +# ============================================================================= +# setup-overlayroot.sh — RK3588 Edge 断电保护(方案一:overlayroot + /data 分区) +# ----------------------------------------------------------------------------- +# 解决场景:SD 卡断电重启导致根文件系统损坏、无法启动。 +# +# 原理: +# 1) 用 Ubuntu 官方 overlayroot 把根文件系统置于 tmpfs overlay 之上, +# 系统对根分区的一切写入进入内存,重启即丢,根分区永久只读, +# 断电不再损坏根文件系统。 +# 2) 在 SD 卡剩余空间划分独立读写分区 /data(ext4),存放日志、 +# 告警图片/视频、设备 ID 等需要持久化的数据,不受 overlay 影响。 +# +# 用法(在设备上执行): +# sudo ./setup-overlayroot.sh install 一键启用(推荐) +# sudo ./setup-overlayroot.sh install --yes 跳过确认(无人值守) +# sudo ./setup-overlayroot.sh install --hls-ram HLS 分片放内存(tmpfs), +# 减小 SD 写入(按需) +# sudo ./setup-overlayroot.sh status 查看当前状态 +# sudo ./setup-overlayroot.sh verify 重启后验证是否生效 +# sudo ./setup-overlayroot.sh disable 临时禁用(维护模式,可写) +# sudo ./setup-overlayroot.sh enable 重新启用(退出维护模式) +# +# 可选环境变量: +# HLS_MODE=ram|data HLS 分片存放位置(默认 data,--hls-ram 等价 ram) +# JOURNAL_MODE=ram|data 系统 journal 日志(默认 ram) +# SKIP_PARTITION=1 跳过分区创建(镜像已含 /data 分区时使用) +# ============================================================================= +set -euo pipefail + +# --------------------------------------------------------------------------- +# 颜色与输出 +# --------------------------------------------------------------------------- +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}⚠${NC} $*"; } +die() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } + +# --------------------------------------------------------------------------- +# 全局配置 +# --------------------------------------------------------------------------- +DATA_MOUNT="/data" +DATA_LABEL="safesight-data" +DATA_SUBDIRS="hls logs alarms clips agent" +RUNTIME_ROOT="/var/lib/safesight-edge-server" +AGENT_LIB="/var/lib/safesight-edge-agent" +FSTAB="/etc/fstab" +OVERLAY_CONF="/etc/overlayroot.conf" +MARKER_BEGIN="# safesight-overlayroot (自动生成,勿手动修改)" +MARKER_END="# end safesight-overlayroot" + +HLS_MODE="${HLS_MODE:-data}" # data | ram +JOURNAL_MODE="${JOURNAL_MODE:-ram}" # ram | data +SKIP_PARTITION="${SKIP_PARTITION:-0}" +ASSUME_YES=0 + +# --------------------------------------------------------------------------- +# 参数解析 +# --------------------------------------------------------------------------- +CMD="${1:-install}" +shift || true +for arg in "$@"; do + case "$arg" in + --yes) ASSUME_YES=1 ;; + --hls-ram) HLS_MODE=ram ;; + --journal-data) JOURNAL_MODE=data ;; + *) warn "未知参数: $arg" ;; + esac +done + +# --------------------------------------------------------------------------- +# 基础检查 +# --------------------------------------------------------------------------- +require_root() { + [ "$(id -u)" -eq 0 ] || die "请使用 sudo 运行此脚本" +} + +detect_distro() { + if [ -r /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + case "${ID:-}" in + ubuntu|debian) return 0 ;; + esac + fi + warn "非 Ubuntu/Debian 系统,overlayroot 可能不可用,继续尝试..." +} + +overlay_active() { + grep -qs 'overlay' /proc/mounts +} + +# --------------------------------------------------------------------------- +# 磁盘/分区检测 +# --------------------------------------------------------------------------- +detect_root_disk() { + ROOT_DEV="$(findmnt -no SOURCE / 2>/dev/null || true)" + [ -n "${ROOT_DEV:-}" ] || die "无法定位根分区" + case "$ROOT_DEV" in + /dev/mmcblk*) ROOT_DISK="${ROOT_DEV%p*}"; ROOT_PART="${ROOT_DEV##*p}" ;; + /dev/nvme*) ROOT_DISK="${ROOT_DEV%p*}"; ROOT_PART="${ROOT_DEV##*p}" ;; + /dev/sd*) ROOT_DISK="${ROOT_DEV%[0-9]*}"; ROOT_PART="${ROOT_DEV##*[a-z]}" ;; + *) die "无法识别的根设备: $ROOT_DEV(本脚本仅支持 SD 卡/eMMC/NVMe 直连分区)" ;; + esac + info "根分区: $ROOT_DEV → 磁盘: $ROOT_DISK" +} + +# /data 是否已存在(已挂载,或磁盘上有数据分区) +data_partition_exists() { + # 已挂载 + if mountpoint -q "$DATA_MOUNT" 2>/dev/null; then + DATA_PART="$(findmnt -no SOURCE "$DATA_MOUNT" 2>/dev/null || true)" + return 0 + fi + # 磁盘上已有标签 safesight-data 的分区(未挂载) + for p in $(lsblk -ln -o NAME,TYPE "$ROOT_DISK" 2>/dev/null | awk '$2=="part"{print $1}'); do + d="/dev/$p" + if [ "$(blkid -s LABEL -o value "$d" 2>/dev/null)" = "$DATA_LABEL" ]; then + DATA_PART="$d" + return 0 + fi + done + return 1 +} + +find_free_space() { + # 返回 "起始MiB 大小MiB"(取最后一个 >=2048MiB 的 Free Space),否则返回空 + LANG=C parted -s "$ROOT_DISK" unit MiB print free 2>/dev/null | \ + awk '$4=="Free" && $3>=2048 {start=$1; size=$3} END {if (size>=2048) print start, size}' +} + +create_data_partition() { + info "在 $ROOT_DISK 空闲空间中创建 /data 分区($DATA_LABEL)..." + FREE_INFO="$(find_free_space || true)" + [ -n "$FREE_INFO" ] || die "未找到足够空闲空间(至少 2GiB)。请先扩容 SD 卡镜像或参考文档手动划分 /data 分区。" + FREE_START="$(echo "$FREE_INFO" | awk '{print $1}' | tr -d 'MiB')" + FREE_SIZE="$(echo "$FREE_INFO" | awk '{print $2}')" + + echo "" + echo "即将执行:" + echo " 在 $ROOT_DISK 偏移 ${FREE_START}MiB 处创建 ext4 分区(约 ${FREE_SIZE})" + echo " 分区标签: $DATA_LABEL" + echo " 该分区将挂载到 $DATA_MOUNT,保存日志/告警/设备ID等持久数据" + echo "" + if [ "$ASSUME_YES" -ne 1 ]; then + read -r -p "继续? [y/N]: " yn + [ "${yn:-n}" = "y" ] || [ "${yn:-n}" = "Y" ] || die "已取消" + fi + + # 分区表类型:gpt 用名称参数,msdos 用 primary + TABLE_TYPE="$(parted -s "$ROOT_DISK" print 2>/dev/null | awk '/Partition Table/{print $3}')" + if [ "$TABLE_TYPE" = "gpt" ]; then + parted -s "$ROOT_DISK" mkpart "$DATA_LABEL" ext4 "${FREE_START}MiB" 100% + else + parted -s "$ROOT_DISK" mkpart primary ext4 "${FREE_START}MiB" 100% + fi + + # 刷新分区表 + partprobe "$ROOT_DISK" 2>/dev/null || partx -u "$ROOT_DISK" 2>/dev/null || true + udevadm settle 2>/dev/null || sleep 3 + + # 新分区 = 磁盘上最后一个 part + DATA_PART="$(lsblk -ln -o NAME,TYPE "$ROOT_DISK" | awk '$2=="part"{p=$1} END{print "/dev/"p}')" + [ -n "${DATA_PART:-}" ] || die "分区创建失败" + + mkfs.ext4 -F -L "$DATA_LABEL" "$DATA_PART" + ok "数据分区已创建: $DATA_PART (标签 $DATA_LABEL)" +} + +# --------------------------------------------------------------------------- +# fstab 写入(先于挂载执行) +# --------------------------------------------------------------------------- +write_fstab() { + [ -n "${DATA_UUID:-}" ] || die "未获取到数据分区 UUID" + local bak="$FSTAB.bak-$(date +%Y%m%d%H%M%S)" + cp -a "$FSTAB" "$bak" + info "fstab 已备份: $bak" + + # 移除旧的管理块(幂等) + sed -i "/^${MARKER_BEGIN}/,/^${MARKER_END}/d" "$FSTAB" + + { + echo "$MARKER_BEGIN" + echo "# 独立读写数据分区(断电保护核心,rootfs 只读后唯一持久化区域)" + echo "UUID=$DATA_UUID $DATA_MOUNT ext4 defaults,noatime,nofail,x-systemd.device-timeout=10s,commit=60 0 2" + + # safesight 运行数据 bind 到 /data(路径保持 /var/lib/... 不变,兼容现有部署) + echo "# safesight 运行数据 → /data(日志/告警/视频/设备ID 断电不丢)" + if [ "$HLS_MODE" = "ram" ]; then + echo "# HLS 分片 → 内存 tmpfs(重启即丢,减小 SD 写入;容量按需调整)" + echo "tmpfs $RUNTIME_ROOT/hls tmpfs defaults,noatime,size=512m,mode=755 0 0" + else + echo "$DATA_MOUNT/safesight/hls $RUNTIME_ROOT/hls none bind,nofail 0 0" + fi + echo "$DATA_MOUNT/safesight/logs $RUNTIME_ROOT/logs none bind,nofail 0 0" + echo "$DATA_MOUNT/safesight/alarms $RUNTIME_ROOT/alarms none bind,nofail 0 0" + echo "$DATA_MOUNT/safesight/clips $RUNTIME_ROOT/clips none bind,nofail 0 0" + echo "$DATA_MOUNT/safesight/agent $AGENT_LIB none bind,nofail 0 0" + + if [ "$JOURNAL_MODE" = "data" ]; then + echo "# 系统 journal → /data(断电不丢,便于事后排查)" + echo "$DATA_MOUNT/safesight/journal /var/log/journal none bind,nofail 0 0" + fi + echo "$MARKER_END" + } >> "$FSTAB" + + ok "fstab 已更新" +} + +# --------------------------------------------------------------------------- +# /data 目录初始化 + 存量数据迁移 + 挂载 +# --------------------------------------------------------------------------- +setup_data_dirs() { + local sub + for sub in $DATA_SUBDIRS; do + mkdir -p "$DATA_MOUNT/safesight/$sub" + done + [ "$JOURNAL_MODE" = "data" ] && mkdir -p "$DATA_MOUNT/safesight/journal" + + # 迁移已有数据(bind 挂载前执行,避免覆盖) + migrate_dir() { + local src="$1" dst="$2" name="$3" + # bind 已挂载时 src 与 dst 是同一目录,跳过 + [ "$(readlink -f "$src")" != "$(readlink -f "$dst")" ] || return 0 + if [ -d "$src" ] && [ -n "$(ls -A "$src" 2>/dev/null)" ]; then + cp -a "$src"/. "$dst"/ && ok "已迁移 $name 存量数据 → $dst" + fi + } + for sub in hls logs alarms clips; do + migrate_dir "$RUNTIME_ROOT/$sub" "$DATA_MOUNT/safesight/$sub" "$sub" + done + migrate_dir "$AGENT_LIB" "$DATA_MOUNT/safesight/agent" "agent" + + # 确保 bind 目标目录存在 + for sub in hls logs alarms clips; do + mkdir -p "$RUNTIME_ROOT/$sub" + done + mkdir -p "$AGENT_LIB" + mkdir -p /var/log/journal +} + +mount_all() { + # 挂载 /data 与全部 bind(依赖上一步 fstab/目录已就绪) + mount -a 2>/dev/null || true + if ! mountpoint -q "$DATA_MOUNT" 2>/dev/null; then + die "$DATA_MOUNT 挂载失败,请检查 /etc/fstab" + fi + ok "$DATA_MOUNT 已挂载 rw (UUID=$DATA_UUID)" +} + +# --------------------------------------------------------------------------- +# journald 配置(控制系统日志内存占用 / 持久化) +# --------------------------------------------------------------------------- +setup_journald() { + mkdir -p /etc/systemd/journald.conf.d + if [ "$JOURNAL_MODE" = "data" ]; then + cat > /etc/systemd/journald.conf.d/safesight.conf << 'EOF' +[Journal] +Storage=persistent +SystemMaxUse=256M +SystemMaxFile=32M +MaxRetentionSec=14d +SyncIntervalSec=1h +EOF + ok "journald → /data(持久化,上限 256M / 保留14天)" + else + cat > /etc/systemd/journald.conf.d/safesight.conf << 'EOF' +[Journal] +Storage=volatile +RuntimeMaxUse=64M +EOF + ok "journald → 内存(volatile,上限 64M,重启即丢)" + fi + systemctl restart systemd-journald 2>/dev/null || true +} + +# --------------------------------------------------------------------------- +# overlayroot 安装与配置 +# --------------------------------------------------------------------------- +install_overlayroot() { + if ! dpkg -s overlayroot >/dev/null 2>&1; then + info "安装 overlayroot ..." + apt-get update -qq 2>/dev/null || warn "apt update 失败(设备可能离线),尝试直接安装..." + apt-get install -y overlayroot || die "overlayroot 安装失败。若设备离线,请将 overlayroot 预装进母卡镜像(sudo apt install overlayroot)后重新打包。" + else + ok "overlayroot 已安装" + fi + + # 备份旧配置 + if [ -f "$OVERLAY_CONF" ]; then + cp -a "$OVERLAY_CONF" "$OVERLAY_CONF.bak-$(date +%Y%m%d%H%M%S)" + fi + + cat > "$OVERLAY_CONF" << 'EOF' +# /etc/overlayroot.conf — 由 setup-overlayroot.sh 生成 +# 启用后根文件系统只读,一切写入进入内存(tmpfs),重启即丢,断电不损坏根分区。 +# 维护模式: 运行 sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable +overlayroot="tmpfs" +overlayroot_tmpfs_size="50%" +overlayroot_quiet="n" +# /data 是独立分区(fstab 挂载,不经过 overlay),保持可写,无需排除。 +# 若个别镜像出现其它分区被意外只读,可在此追加排除,例如: +# overlayroot_exclude_partitions="/var/lib/docker" +EOF + ok "/etc/overlayroot.conf 已配置 (overlayroot=\"tmpfs\")" + + info "重建 initramfs ..." + update-initramfs -u || die "update-initramfs 失败" + + if [ -d /boot/extlinux ]; then + warn "检测到 extlinux 引导(U-Boot/香橙派)。overlayroot 由 initramfs 加载,无需改 extlinux.conf。" + fi + ok "initramfs 已更新" +} + +# --------------------------------------------------------------------------- +# 命令实现 +# --------------------------------------------------------------------------- +cmd_install() { + require_root + detect_distro + detect_root_disk + + if overlay_active; then + warn "当前系统已处于 overlayroot 保护模式。如需调整配置,请先运行: $0 disable" + fi + + # 1) 数据分区 + if data_partition_exists; then + ok "检测到数据分区: ${DATA_PART:-(/data 已挂载)}" + else + if [ "${SKIP_PARTITION:-0}" = "1" ]; then + die "SKIP_PARTITION=1 但未检测到 $DATA_LABEL 分区,请确认镜像包含数据分区" + fi + create_data_partition + fi + DATA_UUID="$(blkid -s UUID -o value "$DATA_PART" 2>/dev/null || true)" + [ -n "$DATA_UUID" ] || die "未获取到数据分区 UUID ($DATA_PART)" + + # 2) fstab + 目录 + 挂载(fstab 先写,再按新 fstab 挂载) + write_fstab + setup_data_dirs + mount_all + + # 3) journald + setup_journald + + # 4) overlayroot + install_overlayroot + + echo "" + echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} 配置完成!重启后根文件系统将进入只读保护模式${NC}" + echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}" + echo "" + echo "下一步:" + echo " sudo reboot # 重启生效" + echo "" + echo "重启后验证:" + echo " sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh verify" + echo "" + echo "数据分区(断电不丢):" + echo " $DATA_MOUNT/safesight/{logs,alarms,clips,agent}" + echo " $DATA_MOUNT/safesight/hls # HLS 分片 (${HLS_MODE})" + echo "" + echo "日常维护(装软件/改系统配置,需临时可写):" + echo " sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh disable" + echo " sudo reboot # 进入可写维护模式" + echo " sudo /opt/safesight-edge-server/scripts/setup-overlayroot.sh enable" + echo " sudo reboot # 恢复只读保护" + echo "" + echo "注意:overlayroot 启用后,apt 安装、系统配置修改都会在重启后丢失," + echo " 必须进入维护模式操作!" +} + +cmd_status() { + require_root + echo -e "${BLUE}══════════ overlayroot 状态 ══════════${NC}" + + if overlay_active; then + echo -e " 根文件系统: ${GREEN}● 保护模式(overlay, 只读+tmpfs)${NC}" + mount | grep ' / ' | sed 's/^/ /' + echo -e " 上层(tmpfs)容量: $(df -h / | awk 'NR==2{print $2}') 已用: $(df -h / | awk 'NR==2{print $3}')" + else + echo -e " 根文件系统: ${YELLOW}○ 可写模式(overlay 未启用)${NC}" + fi + + echo "" + if mountpoint -q "$DATA_MOUNT" 2>/dev/null; then + echo -e " $DATA_MOUNT: ${GREEN}已挂载 rw${NC} $(findmnt -no SOURCE,FSTYPE "$DATA_MOUNT")" + echo " 磁盘占用: $(df -h "$DATA_MOUNT" | awk 'NR==2{print "总"$2" 已用"$3" 可用"$4}')" + else + echo -e " $DATA_MOUNT: ${RED}未挂载${NC}" + fi + + echo "" + echo " bind 挂载:" + for b in logs alarms clips hls agent; do + m="$RUNTIME_ROOT/$b" + [ "$b" = "agent" ] && m="$AGENT_LIB" + if mountpoint -q "$m" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} $m → $(findmnt -no SOURCE "$m")" + else + echo -e " ${RED}✗${NC} $m 未挂载" + fi + done + + echo "" + if [ -f "$OVERLAY_CONF" ]; then + echo " /etc/overlayroot.conf:" + grep -E '^overlayroot' "$OVERLAY_CONF" | sed 's/^/ /' + else + echo -e " /etc/overlayroot.conf: ${RED}不存在${NC}" + fi + + if [ -f /etc/systemd/journald.conf.d/safesight.conf ]; then + echo "" + echo " journald: $(grep -E '^\s*(Storage|RuntimeMaxUse|SystemMaxUse)=' /etc/systemd/journald.conf.d/safesight.conf | tr '\n' ' ')" + fi + + echo "" + if [ -f "$AGENT_LIB/device_id" ]; then + echo " 设备 ID: $(cat "$AGENT_LIB/device_id")(持久化于 /data)" + else + echo -e " 设备 ID: ${YELLOW}尚未生成${NC}" + fi +} + +cmd_verify() { + require_root + echo -e "${BLUE}══════════ 断电保护验证 ══════════${NC}" + FAIL=0 + + if overlay_active; then + ok "overlay 已启用,根分区写入受保护" + else + warn "overlay 未启用(仍处于可写模式)" + FAIL=1 + fi + + # /data 可写测试 + if mountpoint -q "$DATA_MOUNT"; then + TEST_FILE="$DATA_MOUNT/.write_test_$(date +%s)" + if echo ok > "$TEST_FILE" 2>/dev/null && rm -f "$TEST_FILE"; then + ok "$DATA_MOUNT 可读写" + else + warn "$DATA_MOUNT 不可写!" + FAIL=1 + fi + else + warn "$DATA_MOUNT 未挂载" + FAIL=1 + fi + + # bind 挂载 + for b in logs alarms clips agent; do + m="$RUNTIME_ROOT/$b" + [ "$b" = "agent" ] && m="$AGENT_LIB" + if mountpoint -q "$m" 2>/dev/null; then + ok "$m → $(findmnt -no SOURCE "$m")" + else + warn "$m 未挂载(若为首次启用,服务运行后会自动创建)" + fi + done + + # 根分区是否真的只读(写测试) + ROTEST="/.write_test_root_$$" + if touch "$ROTEST" 2>/dev/null; then + rm -f "$ROTEST" + warn "警告: 根分区仍可写!请检查 overlayroot 配置" + FAIL=1 + else + ok "根分区只读" + fi + + # 服务状态 + for s in safesight-edge-server safesight-edge-agent; do + if systemctl is-active --quiet "$s" 2>/dev/null; then + ok "$s 运行中" + else + warn "$s 未运行" + fi + done + + echo "" + if [ "$FAIL" -eq 0 ]; then + echo -e "${GREEN}✓ 断电保护配置正常,可放心断电${NC}" + else + echo -e "${YELLOW}存在未通过项,请按上方提示处理${NC}" + fi + exit "$FAIL" +} + +# 切换 overlayroot 开关(disable=可写维护 / enable=恢复保护) +toggle_overlay() { + local mode="$1" + require_root + [ -f "$OVERLAY_CONF" ] || cp /dev/null "$OVERLAY_CONF" + cp -a "$OVERLAY_CONF" "$OVERLAY_CONF.bak-$(date +%Y%m%d%H%M%S)" + + if [ "$mode" = "disable" ]; then + sed -i 's/^overlayroot=.*/overlayroot=""/' "$OVERLAY_CONF" + # 若文件里没有可识别的行则追加 + grep -q '^overlayroot=' "$OVERLAY_CONF" || echo 'overlayroot=""' >> "$OVERLAY_CONF" + echo "" + echo -e "${YELLOW}overlayroot 已设为空(可写模式)。重启后根分区恢复可写,用于安装软件/修改配置。${NC}" + else + sed -i 's/^overlayroot=.*/overlayroot="tmpfs"/' "$OVERLAY_CONF" + grep -q '^overlayroot=' "$OVERLAY_CONF" || echo 'overlayroot="tmpfs"' >> "$OVERLAY_CONF" + echo "" + echo -e "${GREEN}overlayroot 已重新启用(保护模式)。重启后根分区只读。${NC}" + fi + + info "重建 initramfs ..." + update-initramfs -u + echo "" + echo "请执行: sudo reboot" +} + +# --------------------------------------------------------------------------- +# 主入口 +# --------------------------------------------------------------------------- +case "$CMD" in + install) cmd_install ;; + status) cmd_status ;; + verify) cmd_verify ;; + disable) toggle_overlay disable ;; + enable) toggle_overlay enable ;; + *) + echo "RK3588 Edge 断电保护脚本(overlayroot + /data)" + echo "" + echo "用法: sudo $(basename "$0") [命令] [选项]" + echo "" + echo "命令:" + echo " install 一键启用断电保护(创建/挂载 /data + 启用 overlayroot)" + echo " status 查看当前保护状态" + echo " verify 重启后验证保护是否生效" + echo " disable 临时禁用(维护模式:装软件/改系统配置)" + echo " enable 重新启用(恢复保护)" + echo "" + echo "选项:" + echo " --yes 跳过确认" + echo " --hls-ram HLS 分片放内存(减小 SD 写入,重启即丢)" + echo " --journal-data 系统日志持久化到 /data(默认内存)" + echo "" + echo "示例:" + echo " sudo $(basename "$0") install" + echo " sudo $(basename "$0") install --hls-ram" + echo " sudo $(basename "$0") status" + ;; +esac