# Sao lưu với PBS (Advance)

Trong phần trước mình đã tìm hiểu về cách tạo backup với PBS. Phần này mình muốn nâng cấp thêm một chút.

<span style="white-space: pre-wrap;">Vì thời gian cần backup không nhiều. Việc để PBS chạy lên tục có thể gây lãng phí tài nguyên nên trong phần này mình hướng tới tự động hóa quy trình </span>**bật PBS &gt; Backup &gt; Verify &gt; tắt PBS**

Để thực hiện việc này thì sẽ sử dụng script và systemd timer.

<span style="white-space: pre-wrap;">Trước khi đến bới script chính. Thì thêm một điều cần chú ý. Script này sẽ vẫn sử dụng script backup pve host đã viết ở bài </span>[Sao lưu với PBS (Simple)](https://knowledge.thanhdv.com/books/sao-luu-voi-proxmox-backup-server/page/sao-luu-voi-pbs-simple "Sao lưu với PBS (Simple)")

Ok, Lets go!

### Quy trình backup

1. Bật PBS
2. Kiểm tra kết nối đảm bảo PBS sẵn sàng
3. Backup PVE host
4. Verify PVE host backup
5. Backup các VM được khai báo
6. Backup các CT được khai báo
7. Verify datastore chứa cả VM và CT backup
8. <span style="white-space: pre-wrap;">Prune </span>
9. GC
10. Tắt PBS

Các bước trên được thực hiện tuần tự. Xong bước trên mới thực hiện bước dưới. Dừng ngay khi có lỗi.

### Script backup

PVE Web &gt; Shell

- <span style="white-space: pre-wrap;">Tạo file thực thi backup </span>`<span class="editor-theme-code">nano /usr/local/bin/backup.sh</span>`
- <span style="white-space: pre-wrap;">Cấp quyền thực thi </span>`<span class="editor-theme-code">chmod +x /usr/local/bin/backup.sh</span>`
- <span style="white-space: pre-wrap;">Chạy thử </span>`<span class="editor-theme-code">/usr/local/bin/backup.sh</span>`
- <span style="white-space: pre-wrap;">Log kết thúc bằng dòng </span>`<span class="editor-theme-code">===== BACKUP PIPELINE DONE =====</span>`<span style="white-space: pre-wrap;"> là thành công.</span>

```bash
#!/usr/bin/env bash
set -euo pipefail

########################################
# CẤU HÌNH CHUNG
########################################
PBS_VMID=104

PBS_HOST='192.168.1.104'  # Địa chỉ IP của PBS
PBS_DATASTORE='DSM_NFS' # Datastore đã tạo trên PBS
PBS_USER_TOKEN='root@pam!PVE'   # API Token ID
PBS_TOKEN_SECRET='0218e32d-fefe-4bce-9042-c2dd89389856' # API Token Secret
PBS_FINGERPRINT='35:72:be:a5:08:5f:7a:d1:b1:50:0e:69:fa:6c:bf:78:c9:94:d1:9e:45:34:e6:a0:21:88:3d:ab:59:a6:d8:2d' # Fingerprinter

# Prune and GC job
PRUNE_JOB_ID=${PRUNE_JOB_ID:-default-prune} # Tên Prune job
PRUNE_SCHEDULE=${PRUNE_SCHEDULE:-'Sun 00:00'} # Lịch prune # Không quan trọng vì job này chạy bằng script chứ k chạy tự động
KEEP_BACKUPS=${KEEP_BACKUPS:-7} # số bản backup tối đã sẽ giữ

export PBS_REPOSITORY="${PBS_USER_TOKEN}@${PBS_HOST}:${PBS_DATASTORE}"
export PBS_PASSWORD="${PBS_TOKEN_SECRET}"
export PBS_FINGERPRINT="${PBS_FINGERPRINT}"

# Script backup host pve
# Đã viết ở phần trước
BACKUP_HOST_SCRIPT="/usr/local/bin/pve-self-backup.sh"

# Danh sách VM/CT cần backup
# Không cần dấu phẩy giữa các số
VM_LIST=(100 101 104)       # VM IDs
CT_LIST=(102 103)           # CT IDs

# Tuỳ chọn vzdump dùng cho VM/CT
VZDUMP_STORAGE="VM_PBS"
VZDUMP_OPTS=(--mode snapshot --compress zstd --ionice 7 --bwlimit 0 --storage "${VZDUMP_STORAGE}" --notes-template '{{cluster}} - {{guestname}} - {{node}} - {{vmid}}')

# Thời gian đợi PBS online tối đa (giây)
WAIT_PBS_READY_SEC=$((10*60))  # 10 phút
# Thời gian đợi PBS shutdown tối đa (giây)
WAIT_PBS_SHUT_SEC=$((5*60))    # 5 phút

########################################
# HÀM TIỆN ÍCH
########################################
# Lấy ngày giờ
# Thường dùng để in ra trong log
ts() { date '+%F %T'; }

# Kiểm tra xem lệnh có chính xác không
require_cmd() {
  command -v "$1" >/dev/null 2>&1 || { echo "[$(ts)] Missing command: $1"; exit 1; }
}

# Kiểm tra VM có chạy không
qm_is_running() {
  local vmid=$1
  qm status "$vmid" 2>/dev/null | grep -q 'status: running'
}

# Kiểm tra PBS sẵn sàng chưa bằng cách
# Thử TCP connect
# Đăng nhập PBS
# Kiểm tra dịch vụ proxmox-backup-proxy
wait_for_pbs_ready() {
  local deadline=$(( $(date +%s) + WAIT_PBS_READY_SEC ))
  local ok_streak=0
  echo "[$(ts)] Waiting for PBS services (up to ${WAIT_PBS_READY_SEC}s)..."

  while :; do
    # 1) Port 8007 mở?
    if ! timeout 2 bash -c "echo > /dev/tcp/${PBS_HOST}/8007" 2>/dev/null; then
      ok_streak=0; goto_sleep; continue
    fi
    # 2) Login repo thành công?
    if ! proxmox-backup-client login --repository "${PBS_REPOSITORY}" >/dev/null 2>&1; then
      ok_streak=0; goto_sleep; continue
    fi
    # 3) Dịch vụ proxy active bên trong PBS?
    if ! ssh -o BatchMode=yes -o ConnectTimeout=3 root@"${PBS_HOST}" \
         'systemctl is-active --quiet proxmox-backup-proxy'; then
      ok_streak=0; goto_sleep; continue
    fi

    ok_streak=$((ok_streak+1))
    if (( ok_streak >= 3 )); then
      echo "[$(ts)] PBS ready (proxy up, port 8007 OK, login OK)."
      sleep 5  # grace thêm vài giây
      return 0
    fi

    goto_sleep
  done

  echo "[$(ts)] ERROR: PBS not ready within ${WAIT_PBS_READY_SEC}s"; return 1
}

# ngủ 5s
goto_sleep() {
  if (( $(date +%s) > deadline )); then return; fi
  sleep 5
}

# Hàm không còn được sử dụng nữa
# Thử đăng nhập PBS
# Lặp lại đến khi thành công
wait_for_pbs_login() {
  local deadline=$(( $(date +%s) + WAIT_PBS_READY_SEC ))
  echo "[$(ts)] Waiting for PBS to be reachable (up to ${WAIT_PBS_READY_SEC}s)..."
  while :; do
    if proxmox-backup-client login --repository "${PBS_REPOSITORY}" >/dev/null 2>&1; then
      echo "[$(ts)] PBS login OK."
      return 0
    fi
    if (( $(date +%s) > deadline )); then
      echo "[$(ts)] ERROR: PBS not ready within ${WAIT_PBS_READY_SEC}s"
      return 1
    fi
    sleep 5
  done
}

# Verify data 
# Bỏ qua những phiên bản đã verify
verify_datastore() {
  echo "[$(ts)] Trigger verify on PBS datastore '${PBS_DATASTORE}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager verify '${PBS_DATASTORE}' --ignore-verified true" \
    && echo "[$(ts)] Verify finished." \
    || { echo "[$(ts)] ERROR: Verify failed"; return 1; }
}

# Sao lưu PVE host sử dụng pve-self-backup.sh
backup_host() {
  echo "[$(ts)] === BACKUP HOST PVE ==="
  local tries=0
  local max=5
  local rc
  while ((tries<max)); do
    if DRY_RUN=0 "${BACKUP_HOST_SCRIPT}"; then
      echo "[$(ts)] Host backup DONE."; return 0
    fi
    rc=$?
    # Nếu lỗi kết nối thì retry, còn lỗi khác thì fail ngay
    if journalctl -u run-backup-pipeline.service -n 50 --no-pager 2>/dev/null | grep -qE 'No route to host|client error \(Connect\)'; then
      tries=$((tries+1))
      echo "[$(ts)] Connect error, retry ${tries}/${max}..."
      sleep $((5*tries))
    else
      return $rc
    fi
  done
  echo "[$(ts)] ERROR: Host backup failed after retries."; return 1
}

# Backup VMs
backup_vms() {
  echo "[$(ts)] === BACKUP VMs ==="
  for id in "${VM_LIST[@]}"; do
    echo "[$(ts)] vzdump VM ${id} ..."
    vzdump "${id}" "${VZDUMP_OPTS[@]}"
    echo "[$(ts)] VM ${id} backup DONE."
  done
}

# Backup CTs
backup_cts() {
  echo "[$(ts)] === BACKUP CTs ==="
  for id in "${CT_LIST[@]}"; do
    echo "[$(ts)] vzdump CT ${id} ..."
    vzdump "${id}" "${VZDUMP_OPTS[@]}"
    echo "[$(ts)] CT ${id} backup DONE."
  done
}

# Prune
prune_datastore() {
  local job="$PRUNE_JOB_ID"
  echo "[$(ts)] Ensure PRUNE job '${job}' exists (store='${PBS_DATASTORE}', keep-last=${KEEP_BACKUPS})..."

  # Nếu job đã tồn tại -> update (giữ nguyên lịch hoặc đặt lại), disable để không tự chạy
  if ssh -o BatchMode=yes root@"${PBS_HOST}" "proxmox-backup-manager prune-job show '${job}' >/dev/null 2>&1"; then
    ssh -o BatchMode=yes root@"${PBS_HOST}" \
      "proxmox-backup-manager prune-job update '${job}' \
         --store '${PBS_DATASTORE}' \
         --keep-last ${KEEP_BACKUPS} \
         --schedule '${PRUNE_SCHEDULE}' \
         --disable true"
  else
    # Tạo job mới: cần lịch hợp lệ, nhưng disable để không tự động chạy
    ssh -o BatchMode=yes root@"${PBS_HOST}" \
      "proxmox-backup-manager prune-job create '${job}' \
         --store '${PBS_DATASTORE}' \
         --schedule '${PRUNE_SCHEDULE}' \
         --keep-last ${KEEP_BACKUPS} \
         --disable true"
  fi || { echo "[$(ts)] ERROR: create/update prune job failed"; return 1; }

  echo "[$(ts)] Running PRUNE job '${job}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager prune-job run '${job}'" \
    && echo "[$(ts)] Prune finished." \
    || { echo "[$(ts)] ERROR: Prune failed"; return 1; }
}

# GC
gc_datastore() {
  echo "[$(ts)] Running GARBAGE COLLECTION on datastore '${PBS_DATASTORE}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager garbage-collection start '${PBS_DATASTORE}'" \
    && echo "[$(ts)] GC finished." \
    || { echo "[$(ts)] ERROR: GC failed"; return 1; }
}

# Bật PBS
pbs_start() {
  echo "[$(ts)] Starting PBS VM ${PBS_VMID}..."
  if qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS already running."
  else
    qm start "${PBS_VMID}"
  fi
  wait_for_pbs_ready
}

# Tắt PBS
pbs_shutdown() {
  echo "[$(ts)] Shutting down PBS VM ${PBS_VMID}..."

  # 0) Nếu đã tắt thì thoát sớm
  if ! qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS already off."
    return 0
  fi

  # 1) Ưu tiên tắt từ bên trong guest (an toàn nhất)
  #    Yêu cầu bạn đã cấu hình SSH key PVE -> PBS
  #    Dùng systemd để poweroff không chờ (fork ra nền)
  ssh -o BatchMode=yes -o ConnectTimeout=5 root@"${PBS_HOST}" \
    'nohup systemctl poweroff >/dev/null 2>&1 &' || true
  echo "[$(ts)] Sent 'systemctl poweroff' to PBS."

  # 2) Chờ PBS tắt hẳn (polling qm status)
  SHUT_WAIT=${WAIT_PBS_SHUT_SEC:-300} # 5 phút mặc định
  deadline=$(( $(date +%s) + SHUT_WAIT ))
  while qm_is_running "${PBS_VMID}"; do
    if (( $(date +%s) > deadline )); then
      echo "[$(ts)] Guest did not poweroff within ${SHUT_WAIT}s. Trying 'qm shutdown'..."
      break
    fi
    sleep 5
  done

  # 3) Nếu vẫn còn chạy -> thử 'qm shutdown' với timeout dài hơn
  if qm_is_running "${PBS_VMID}"; then
    qm shutdown "${PBS_VMID}" --timeout "${SHUT_WAIT}" || true
  fi

  # 4) Chờ thêm chút nữa
  end2=$(( $(date +%s) + 60 ))
  while qm_is_running "${PBS_VMID}"; do
    if (( $(date +%s) > end2 )); then
      echo "[$(ts)] Still running after graceful attempts. Forcing 'qm stop'..."
      qm stop "${PBS_VMID}" || true
      break
    fi
    sleep 5
  done

  # 5) Kết luận: coi như thành công nếu thực sự đã tắt
  if ! qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS is off."
    return 0
  else
    echo "[$(ts)] WARNING: PBS looks stuck running. Please check 'qm monitor ${PBS_VMID}'."
    return 1
  fi
}


########################################
# MAIN
########################################
require_cmd qm
require_cmd proxmox-backup-client
require_cmd ssh
require_cmd vzdump

echo "[$(ts)] ===== BACKUP PIPELINE START ====="

# 1) Start PBS & wait
pbs_start

# 2) Backup host PVE
backup_host

# 3) Verify sau host backup
verify_datastore

# 4) Backup các VM/CT
backup_vms
backup_cts

# 5) Verify sau VM/CT backup
verify_datastore

# 6) PRUNE rồi GC
prune_datastore
gc_datastore

# 7) Shutdown PBS
pbs_shutdown

echo "[$(ts)] ===== BACKUP PIPELINE DONE ====="
```

### Tự động hóa

Thực hiện trên console hoặc ssh của PVE

- <span style="white-space: pre-wrap;">Tạo file </span>`<span class="editor-theme-code">.service.</span>`<span style="white-space: pre-wrap;"> Đây là hành vi sẽ được thực hiện bởi </span>`<span class="editor-theme-code">.timer</span>`
    - `<span class="editor-theme-code">nano /etc/systemd/system/backup.service</span>`
    - Sử dụng nội dung sau  
        `<span class="editor-theme-code">[Unit]</span>`  
        `<span class="editor-theme-code">Description=Backup PVE host configuration to PBS</span>`  
        `<span class="editor-theme-code">After=network-online.target</span>`  
        `<span class="editor-theme-code">[Service]</span>`  
        `<span class="editor-theme-code">Type=oneshot</span>`  
        `<span class="editor-theme-code">ExecStart=/usr/local/bin/backup.sh</span>`
- <span style="white-space: pre-wrap;">Tạo file </span>`<span class="editor-theme-code">.timer</span>`<span style="white-space: pre-wrap;">. Nó sẽ thực thi </span>`<span class="editor-theme-code">.service</span>`<span style="white-space: pre-wrap;"> theo thời gian được chỉ định.</span>
    - `<span class="editor-theme-code">nano /etc/systemd/system/backup.timer</span>`
    - Sử dụng nội dung sau  
        `<span class="editor-theme-code">[Unit]</span>`  
        `<span class="editor-theme-code">Description=Schedule PVE host config backup (Tue, Thu, Sat at 2:00)</span>`  
        `<span class="editor-theme-code">[Timer]</span>`  
        `<span class="editor-theme-code">OnCalendar=Tue,Thu,Sat 2:00</span>`  
        `<span class="editor-theme-code">Persistent=true</span>`  
        `<span class="editor-theme-code">AccuracySec=1min</span>`  
        `<span class="editor-theme-code">[Install]</span>`  
        `<span class="editor-theme-code">WantedBy=timers.target</span>`
- Kích hoạt timer
    - <span style="white-space: pre-wrap;">Reload systemd </span>`<span class="editor-theme-code">systemctl daemon-reload</span>`
    - <span style="white-space: pre-wrap;">Kích hoạt timer </span>`<span class="editor-theme-code">systemctl enable --now backup.timer</span>`
- <span style="white-space: pre-wrap;">Kiểm tra </span>
    - `<span class="editor-theme-code">systemctl list-timers | grep backup</span>`
    - `<span class="editor-theme-code">systemctl status pve-host-backup.timer</span>`
    - Kết quả tương tự như hình sau là thành công  
        [![image.png](https://knowledge.thanhdv.com/uploads/images/gallery/2025-11/scaled-1680-/rNpimage.png)](https://knowledge.thanhdv.com/uploads/images/gallery/2025-11/rNpimage.png)