1. 创建目录(部署路径/home/docker/oxidized)

mkdir -p /home/docker/oxidized
mkdir -p /home/docker/oxidized/config
mkdir -p /home/docker/oxidized/config/model
mkdir -p /home/docker/oxidized/ssh
mkdir -p /home/docker/oxidized/config/output
mkdir -p /home/docker/oxidized/config/output/configs

2. 创建docker-compose.yml

services:
  oxidized:
    image: oxidized/oxidized:latest
    container_name: oxidized
    restart: unless-stopped
    ports:
      - "8888:8888"    # Web UI and REST API
    volumes:
      - ./config:/home/oxidized/.config/oxidized
      - ./ssh:/home/oxidized/.ssh
    environment:
      - TZ=Asia/Shanghai
      - CONFIG_RELOAD_INTERVAL=600
    networks:
      - oxidized-net

networks:
  oxidized-net:
    driver: bridge

3. 首次启动(生成默认配置后停止容器)

cd /home/docker/oxidized
docker compose up -d

# 提取内置VRP模型,后面需要修改
docker cp oxidized:/var/lib/gems/3.3.0/gems/oxidized-0.37.0/lib/oxidized/model/vrp.rb ./config/model/vrp.rb
docker compose down

4. 主配置config

创建主配置文件config

cat > /home/docker/oxidized/config/config << 'EOF'
# config - Main Oxidized configuration file
# Controls how devices are accessed and where configs are stored

# 备份间隔(秒),3600 = 每小时一轮
interval: 3600

# 日志:同时输出到控制台和文件
logger:
  appenders:
    - type: stdout
    - type: file
      file: /home/oxidized/.config/oxidized/oxidized.log

# Debug 模式(生产环境设为 false)
debug: false

# 并发连接数,同时备份 10 台设备
threads: 10

# 单次操作超时(秒),如等待提示符、执行命令
# 默认 20 秒,部分设备响应慢需调大
timeout: 600

# 整个备份任务的总时限(秒),超过即报 Job timelimit reached
# 默认 300 秒,飞塔等配置较大的设备需调大
timelimit: 600

# 失败重试次数,0 = 不重试
retries: 1

# 全局默认账号密码,router.db 未指定时使用
username: backup_user
password: backup_password

# 提示符检测:由模型自动识别
# Most devices are detected automatically by the model

# 设备清单:从 router.db 读取
source:
  default: csv
  csv:
    file: /home/oxidized/.config/oxidized/router.db
    delimiter: !ruby/regexp /:/
    map:
      name: 0        # 设备名称
      ip: 1          # 管理 IP
      model: 2       # 设备模型(vrp / fortios)
      username: 3    # 登录账号
      password: 4    # 登录密码
      group: 5       # 分组名称,用于 Web 界面分类展示
    gpg: false

# 连接方式:先 SSH 后 Telnet 自动降级
input:
  default: ssh, telnet
  debug: false
  telnet:
    secure: false
    timeout: 15      # Telnet TCP 连接超时(秒)
  ssh:
    secure: false
    timeout: 15      # SSH TCP 连接超时(秒)
  utf8_encoded: true

# 备份存储:本地文件
output:
  default: file
  file:
    directory: /home/oxidized/.config/oxidized/output/configs

# Web 管理界面,浏览器访问 http://服务器IP:8888
extensions:
  oxidized-web:
    load: true
    listen: 0.0.0.0
    port: 8888

# 模型别名(方便 router.db 用简写)
model_map:
  cisco: ios
  juniper: junos
  arista: eos
  huawei: vrp
  hw: vrp
EOF

5. 设备清单router.db

格式:设备名称:IP:模型:账号:密码:分组

创建设备清单文件router.db

cat > /home/docker/oxidized/config/router.db << 'EOF'
交换机01_SW01:10.1.1.1:vrp:admin:password:交换机
防火墙01_FW01:10.1.1.2:fortios:admin:password:防火墙
EOF
  • 模型名写vrp(华为/H3C/海康通用)或fortios(飞塔)
  • 分组只用于Web界面分类,不影响连接方式
  • 账号密码留空则使用全局默认值,中间冒号不能省

6. 自定义VRP模型

模型文件已在“步骤3”中提取到./config/model/vrp.rb。按如下方式手动修改三处:

修改1:兼容Login:Username:两种登录提示
找到:username /^Username:$/
改为:username /^([Ll]ogin|[Uu]sername):\s*$/

修改2:兼容Password:后带空格
找到:password /^Password:$/
改为:password /^Password:\s*$/

修改3:去掉all参数(部分设备不支持)
找到:cmd 'display current-configuration all' do |cfg|
改为:cmd 'display current-configuration' do |cfg|

7. 启动并验证

启动

cd /home/docker/oxidized
docker compose up -d

验证模型语法
docker exec oxidized ruby -c /home/oxidized/.config/oxidized/model/vrp.rb
正常应输出Syntax OK

Web界面
浏览器打开http://服务器IP:8888,可按分组查看设备,查看配置备份情况,手动触发单台备份

8. 问题处理

登录提示不统一
现象:无法正常登录交换机获取配置,telnet的debug日志卡在登录提示符“Login:”
原因:登录提示不统一,华为提示Username:,H3C和海康OEM提示Login:,且冒号后有空格,内置模型只匹配Username:`
解决:已在步骤“6. 自定义VRP模型”中解决

screen-length 0 temporary不被部分设备支持
现象:交换机读取配置卡住,未能完整加载配置信息,telnet的debug日志如下

received ""
received ""
received "Login: "
sent cmd "admin\r\n"
received "admin\r\n"
received "Password: "
sent cmd "password\r\n"
received "\n"
received "******************************************************************************\r\n* Copyright (c) 2004-2022 New H3C Technologies Co., Ltd. All rights reserved.*\r\n* Without the owner's prior written consent,                                 *\r\n* no decompiling or reverse-engineering shall be allowed.                    *\r\n******************************************************************************\r\n\r\n<BJSW10-2F-XinShiYanShi>"
sent cmd "screen-length 0"
received "scre"
received "en-length"
received " 0"
received "\r\n                                      ^\r\n % Unrecognized command found at '^' position.\r\n<BJSW10-2F-XinShiYanShi>"
sent cmd "display version"
received "display version\r\n"
received "H3C Comware Software, Version 7.1.070, Release 6343P05\r\nCopyright (c) 2004-2022 New H3C Technologies Co., Ltd. All rights reserved.\r\n"
received "H3C S5120V3-52P-LI uptime is 77 weeks, 2 days, 0 hours, 17 minutes\r\n"
received "Last reboot reason : Cold reboot\r\n"
received "\r\n"
received "Boot image: flash:/s5120v3_li-cmw710-boot-r6343p05.bin\r\nBoot image version: 7.1.070, Release 6343P05\r\n  Compiled Aug 26 2022 11:00:00\r\nSystem image: flash:/s5120v3_li-cmw710-system-r6343p05.bin\r\nSystem image version: 7.1.070, Release 6343P05\r\n  Compiled Aug 26 2022 11:00:00\r\n"
received "\r\n"
received "\r\nSlot 1:\r\nUptime is 77 weeks,2 days,0 hours,17 minutes\r\nS5120V3-52P-LI with 1 Processor\r\nBOARD TYPE:         S5120V3-52P-LI\r\nDRAM:               512M bytes\r\nFLASH:              256M bytes\r\nPCB 1 Version:      VER.B\r\nBootrom Version:    146\r\nCPLD 1 Version:     001\r\nRelease Version:    H3C S5120V3-52P-LI-6343P05\r\n---- More ----"

原因:H3C和海康设备不支持screen-length 0命令(正确命令是screen-length disable
解决:登录每台设备,永久关闭分页:

user-interface vty 0 4
screen-length 0

主机名含空格
现象:某交换机主机名含空格(SW-04_JiFang 5700S),VRP提示符正则[\w.-]+不匹配空格,报“PromptUndetect”
解决:修改主机名,去掉空格

9. 最终文件清单

/home/docker/oxidized/
├── docker-compose.yml
└── config/
    ├── config              # 主配置文件(手动创建)
    ├── router.db           # 设备清单(手动创建)
    ├── oxidized.log        # 运行日志(自动生成)
    ├── crash               # 崩溃日志(自动生成)
    ├── pid                 # 进程PID文件(自动生成)
    ├── model/
    │   └── vrp.rb          # 自定义VRP模型(手动创建)
    └── output/             # 备份文件存放目录(自动生成)
        └── configs/        # 备份文件存放目录(手动创建)

10. 打包Oxidized配置并备份到远程FTP

Shell脚本

#!/bin/bash
#===============================================================================
# oxidized_backup.sh
# 功能: 打包 oxidized config 目录,按日期命名,本地留存 + lftp 上传远程 FTP
#===============================================================================

set -euo pipefail

#==================== 配置区(按需修改)====================

SOURCE_DIR="/home/docker/oxidized/config"
BACKUP_DIR="/home/Backup"
RETENTION_DAYS=15

# ---------- 远程 FTP 配置 ----------
FTP_HOST="192.168.1.1"
FTP_PORT="21"
FTP_USER="username"        # ← 改成实际用户名
FTP_PASS="password"        # ← 改成实际密码
FTP_PATH="/Backup/网络设备/自动备份/oxidized"

# FTP 连接模式: "passive" 或 "active"
FTP_MODE="passive"

# SSL/TLS 模式: "none" | "explicit" (FTPES/显式TLS) | "implicit" (FTPS/隐式TLS)
FTP_SSL="none"

#==================== 脚本逻辑 ====================

DATE_STAMP=$(date +%Y%m%d)
ARCHIVE_NAME="oxidized_config_${DATE_STAMP}.tar.gz"
ARCHIVE_PATH="${BACKUP_DIR}/${ARCHIVE_NAME}"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

#------------------- 1. 准备本地目录 -------------------
mkdir -p "${BACKUP_DIR}"

if [ ! -d "${SOURCE_DIR}" ]; then
    log "ERROR: 源目录不存在: ${SOURCE_DIR}"
    exit 1
fi

#------------------- 2. 打包 -------------------
log "开始打包: ${SOURCE_DIR} -> ${ARCHIVE_PATH}"
tar -czf "${ARCHIVE_PATH}" -C "$(dirname "${SOURCE_DIR}")" "$(basename "${SOURCE_DIR}")"
log "打包完成: ${ARCHIVE_PATH} ($(du -h "${ARCHIVE_PATH}" | cut -f1))"

#------------------- 3. 上传 FTP -------------------
log "开始上传到 ${FTP_HOST}:${FTP_PORT}${FTP_PATH}"

# 根据 SSL 模式确定协议前缀
case "${FTP_SSL}" in
    implicit)  PROTO="ftps"  ;;   # 隐式 TLS,端口通常是 990
    explicit)  PROTO="ftp"   ;;   # 显式 TLS (FTPES),端口默认 21
    none|*)    PROTO="ftp"   ;;
esac

# 构建 lftp 的 set 命令
LFTP_SETS=""
if [ "${FTP_MODE}" = "passive" ]; then
    LFTP_SETS="${LFTP_SETS}set ftp:passive-mode yes;"
else
    LFTP_SETS="${LFTP_SETS}set ftp:passive-mode no;"
fi

case "${FTP_SSL}" in
    none)
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-allow no;"
        ;;
    explicit)
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-allow yes;"
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-force yes;"
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-protect-data yes;"
        LFTP_SETS="${LFTP_SETS}set ssl:verify-certificate no;"
        ;;
    implicit)
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-allow yes;"
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-force yes;"
        LFTP_SETS="${LFTP_SETS}set ftp:ssl-protect-data yes;"
        LFTP_SETS="${LFTP_SETS}set ssl:verify-certificate no;"
        ;;
esac

# 先用一条简单的 open 命令测试连接
log "测试 FTP 连接..."
CONNECTION_OK=$(lftp -c "
${LFTP_SETS}
set net:timeout 15
set net:max-retries 1
open -u '${FTP_USER}','${FTP_PASS}' -p ${FTP_PORT} ${PROTO}://${FTP_HOST}
ls
bye
" 2>&1)

if echo "${CONNECTION_OK}" | grep -qiE "login failed|not connected|connection refused|no route|timeout|cannot|denied"; then
    log "ERROR: FTP 连接失败!服务器返回:"
    echo "${CONNECTION_OK}"
    log ""
    log "============ 排查建议 ============"
    log "1. 检查 IP/端口是否正确:  telnet ${FTP_HOST} ${FTP_PORT}"
    log "2. 检查用户名密码:          lftp -u ${FTP_USER},${FTP_PASS} -p ${FTP_PORT} ${PROTO}://${FTP_HOST}"
    log "3. 尝试切换 SSL 模式:       FTP_SSL=\"explicit\" 或 \"implicit\""
    log "4. 尝试切换连接模式:        FTP_MODE=\"active\""
    log "5. 检查防火墙是否放行 FTP 数据端口"
    log "=================================="
    exit 1
fi

log "FTP 连接成功,开始上传..."

# 实际上传
lftp -c "
${LFTP_SETS}
set net:timeout 30
set net:max-retries 3
set net:reconnect-interval-base 10
open -u '${FTP_USER}','${FTP_PASS}' -p ${FTP_PORT} ${PROTO}://${FTP_HOST}
cd '${FTP_PATH}' || mkdir -p '${FTP_PATH}' && cd '${FTP_PATH}'
put -c '${ARCHIVE_PATH}' -o '${ARCHIVE_NAME}'
bye
"

if [ $? -eq 0 ]; then
    log "上传成功: ${FTP_PATH}/${ARCHIVE_NAME}"
else
    log "ERROR: 上传失败"
    exit 1
fi

#------------------- 4. 清理本地过期备份 -------------------
if [ "${RETENTION_DAYS}" -gt 0 ]; then
    log "清理 ${RETENTION_DAYS} 天前的本地备份..."
    find "${BACKUP_DIR}" -name "oxidized_config_*.tar.gz" -type f -mtime +${RETENTION_DAYS} -print -delete | while read -r f; do
        log "已删除: ${f}"
    done
fi

log "全部完成!"

标签: none