feat(edge): 断电保护(overlayroot+/data)与产品镜像批量生产流程

- 新增 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 批量烧录->每台初始化)
  - 移除废弃的克隆卡流程
This commit is contained in:
tian 2026-08-01 16:45:47 +08:00
parent e931e4d95b
commit 227dc04004
7 changed files with 1178 additions and 107 deletions

View File

@ -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 "读取"生成 .img16G 卡很快),再用 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 + resize2fs64G 卡 /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 # 全部 ✓ 即为生效
```
脚本自动完成:检测空闲空间建 /dataext4→ 写 fstab/data + bind`nofail`)→ 迁移存量数据
→ journald 内存日志64M 上限)→ 安装并配置 overlayroot → 重建 initramfs。
### 可选参数
| 参数 | 说明 |
|---|---|
| `--yes` | 跳过分区确认(无人值守) |
| `--hls-ram` | HLS 分片放内存 tmpfs512M减小 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 不丢);若升级脚本需要改写系统配置,母卡流程已天然覆盖。

View File

@ -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 断电保护章节"
}
# 升级

243
scripts/init-device.sh Normal file
View File

@ -0,0 +1,243 @@
#!/bin/bash
# =============================================================================
# init-device.sh — 批量烧录后的单台设备初始化
# -----------------------------------------------------------------------------
# 在每台烧录好产品镜像的盒子上运行root。完成
# 1. 设置主机名
# 2. 设置静态 IPnetplan
# 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 <IP/掩码> --gw <网关> --dns <DNS> [选项]"
echo ""
echo "必选:"
echo " --hostname <名称> 如 safesight-01"
echo " --ip <IP/掩码> 如 10.0.0.83/24"
echo " --gw <网关> 如 10.0.0.1"
echo " --dns <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. 静态 IPnetplan
# ---------------------------------------------------------------------------
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; }
# 需要 growpartcloud-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 " 在管理端发现该设备 → 设置别名 → 下发配置"

View File

@ -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"

View File

@ -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"

138
scripts/prepare-image.sh Normal file
View File

@ -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 配置能力(出厂即带断电保护)
#
# 重要:本脚本后【不要】启用 overlayrootsetup-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 在每台初始化最后一步统一启用。"

View File

@ -0,0 +1,554 @@
#!/bin/bash
# =============================================================================
# setup-overlayroot.sh — RK3588 Edge 断电保护方案一overlayroot + /data 分区)
# -----------------------------------------------------------------------------
# 解决场景SD 卡断电重启导致根文件系统损坏、无法启动。
#
# 原理:
# 1) 用 Ubuntu 官方 overlayroot 把根文件系统置于 tmpfs overlay 之上,
# 系统对根分区的一切写入进入内存,重启即丢,根分区永久只读,
# 断电不再损坏根文件系统。
# 2) 在 SD 卡剩余空间划分独立读写分区 /dataext4存放日志、
# 告警图片/视频、设备 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