[pve] Weekly backup 2026-07-05 — 20 files changed, 228 insertions(+), 71 deletions(-)

This commit is contained in:
Proxmox Backup
2026-07-05 03:00:03 -05:00
parent fd7b7bc615
commit 0bf6bda39e
20 changed files with 228 additions and 71 deletions
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# Collects MSP360 backup plan status from all known hosts and writes a JSON
# status file, then pushes it to web.orbishosting.com (VM110).
set -u
CBB="/opt/local/MSP360 Backup/bin/cbb"
SSHPW="Joker1974!!!"
OUT=/tmp/backup-status.json
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
BACKUP_ROOT=/mnt/nas-backups/MSPBackups
get_plan_status() {
# $1=host $2=user $3=planname $4=mode(local|hop)
local host="$1" user="$2" plan="$3" mode="$4" out
if [ "$mode" = "local" ]; then
out="$("$CBB" plan -l 2>&1)"
elif [ "$mode" = "hop" ]; then
out="$(sshpass -p "$SSHPW" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 "$user@$host" "'$CBB' plan -l 2>&1")"
fi
local state result id
state=$(echo "$out" | grep 'State:' | head -1 | sed 's/.*State: *//')
result=$(echo "$out" | grep 'Last result:' | head -1 | sed 's/.*Last result: *//')
id=$(echo "$out" | grep 'Id:' | head -1 | sed 's/.*Id: *//')
echo "${state:-unreachable}|${result:-unreachable}|${id:-}"
}
# Reads the destination generation folders on the NAS (mounted locally on
# PVE1) to find the last backup that actually completed and wrote data.
# More trustworthy than the plan's self-reported "Last result", since a
# plan can report Warning/Fail on a run after previously succeeding, and
# we want to know how stale the actual backed-up data is.
get_last_success() {
local nas_folder="$1" plan_id="$2" latest
[ -z "$plan_id" ] && { echo "null"; return; }
latest=$(ls "$BACKUP_ROOT/$nas_folder/CBB_Configuration/${plan_id}.cbb\$/" 2>/dev/null | sort | tail -1)
if [ -z "$latest" ]; then
echo "null"
else
date -u -d "${latest:0:4}-${latest:4:2}-${latest:6:2} ${latest:8:2}:${latest:10:2}:${latest:12:2}" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "null"
fi
}
echo "[" > "$OUT"
first=1
emit() {
local name="$1" ip="$2" state="$3" result="$4" last_success="$5"
[ "$first" -eq 1 ] && first=0 || echo "," >> "$OUT"
if [ "$last_success" = "null" ]; then
cat >> "$OUT" <<EOF
{"name": "$name", "ip": "$ip", "state": "$state", "result": "$result", "last_success": null}
EOF
else
cat >> "$OUT" <<EOF
{"name": "$name", "ip": "$ip", "state": "$state", "result": "$result", "last_success": "$last_success"}
EOF
fi
}
collect() {
local name="$1" ip="$2" user="$3" mode="$4" nas_folder="$5"
local r state result id last_success
r=$(get_plan_status "$ip" "$user" "" "$mode")
state="${r%%|*}"
rest="${r#*|}"
result="${rest%%|*}"
id="${rest#*|}"
last_success=$(get_last_success "$nas_folder" "$id")
emit "$name" "$ip" "$state" "$result" "$last_success"
}
# Homebridge dropped its in-guest MSP360 agent (repeated, unresolved storage
# errors) in favor of the cluster-wide Proxmox vzdump job, which snapshots
# the whole VM disk nightly at 21:00 to the same NAS. Reads the dump
# directory directly instead of querying an in-guest agent.
collect_proxmox_vm() {
local name="$1" ip="$2" vmid="$3" dump_dir="/mnt/pve/SynologyProx/dump"
local latest_dump latest_log ts iso result state
latest_dump=$(ls "$dump_dir"/vzdump-qemu-"$vmid"-*.vma.zst 2>/dev/null | sort | tail -1)
if [ -z "$latest_dump" ]; then
emit "$name" "$ip" "Unknown" "unreachable" "null"
return
fi
ts=$(basename "$latest_dump" | sed -E 's/vzdump-qemu-[0-9]+-([0-9_]+-[0-9_]+)\.vma\.zst/\1/')
iso=$(date -u -d "$(echo "$ts" | sed -E 's/(....)_(..)_(..)-(..)_(..)_(..)/\1-\2-\3 \4:\5:\6/')" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
latest_log="$dump_dir/vzdump-qemu-$vmid-$ts.log"
if [ -f "$latest_log" ] && grep -q "Finished Backup" "$latest_log"; then
result="Success"
else
result="Warning"
fi
state="Proxmox nightly"
if [ -n "$iso" ]; then
emit "$name" "$ip" "$state" "$result" "$iso"
else
emit "$name" "$ip" "$state" "$result" "null"
fi
}
collect "PVE1" "10.48.200.90" "" "local" "CBB_pve"
collect "JARVIS" "10.48.200.211" "root" "hop" "CBB_JARVIS-211"
collect "NovaCPX" "10.48.200.110" "root" "hop" "CBB_NovaCPX-110"
collect "Jellyfin" "10.48.200.33" "root" "hop" "CBB_Jellyfin-33"
collect "MediaStack" "10.48.200.35" "root" "hop" "CBB_MediaStack-35"
collect_proxmox_vm "Homebridge" "10.48.200.18" "118"
echo "]" >> "$OUT"
python3 -c "
import json
data = json.load(open('$OUT'))
data = {'updated': '$NOW', 'hosts': data}
json.dump(data, open('$OUT', 'w'), indent=2)
"
sshpass -p "$SSHPW" scp -o StrictHostKeyChecking=no "$OUT" root@10.48.200.110:/home/webacct/public_html/downloads/backup-status.json
+6 -2
View File
@@ -2,9 +2,13 @@
# JARVIS Network Scanner — runs on PVE1, pushes nmap results to JARVIS
# Cron: */3 * * * * /usr/local/bin/jarvis-netscan.sh >/dev/null 2>&1
JARVIS_URL="https://165.22.1.228"
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
SUBNET="10.48.200.0/24"
TMPFILE=$(mktemp)
+29 -11
View File
@@ -3,9 +3,13 @@
# Pings all Yealink phones + checks FusionPBX SIP registration (read-only)
# 200.3 is on an external FusionPBX — ping only, no SIP check
JARVIS_URL="https://165.22.1.228"
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
FUSION_HOST="134.209.72.226"
# IP|alias|extension(none=skip SIP check)|mac
@@ -21,19 +25,17 @@ PHONES=(
REG_OUTPUT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
root@$FUSION_HOST "fs_cli -x 'show registrations'" 2>/dev/null || echo "")
DEVICES="["
FIRST=1
# Collect results as TSV, delegate JSON building to python3 to avoid injection
RESULTS=""
for PHONE in "${PHONES[@]}"; do
IFS='|' read -r IP ALIAS EXT MAC <<< "$PHONE"
# Ping probe
if ping -c 1 -W 2 "$IP" > /dev/null 2>&1; then
STATUS="online"
else
STATUS="offline"
fi
# SIP check — skip for external phones (ext=none)
if [ "$EXT" = "none" ]; then
SIP="external"
elif [ -n "$REG_OUTPUT" ] && echo "$REG_OUTPUT" | grep -q "^${EXT},"; then
@@ -42,15 +44,31 @@ for PHONE in "${PHONES[@]}"; do
SIP="unregistered"
fi
[ $FIRST -eq 0 ] && DEVICES+=","
DEVICES+="{\"ip\":\"$IP\",\"alias\":\"$ALIAS\",\"mac\":\"$MAC\",\"vendor\":\"Yealink\",\"status\":\"$STATUS\",\"sip_status\":\"$SIP\",\"extension\":\"$EXT\"}"
FIRST=0
RESULTS="${RESULTS}${IP}\t${ALIAS}\t${MAC}\t${STATUS}\t${SIP}\t${EXT}\n"
done
DEVICES+="]"
JSON=$(printf "%b" "$RESULTS" | python3 -c "
import sys, json
devices = []
for line in sys.stdin:
line = line.rstrip('\n')
if not line:
continue
parts = line.split('\t')
if len(parts) < 6:
continue
ip, alias, mac, status, sip, ext = parts[:6]
devices.append({
'ip': ip, 'alias': alias, 'mac': mac,
'vendor': 'Yealink', 'status': status,
'sip_status': sip, 'extension': ext,
})
print(json.dumps({'devices': devices}))
")
curl -sk --max-time 10 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "{\"devices\":$DEVICES}" > /dev/null 2>&1
-d "$JSON" > /dev/null 2>&1
+2 -3
View File
@@ -11,7 +11,7 @@ import urllib.request
import urllib.error
import ssl
JARVIS_URL = "https://165.22.1.228"
JARVIS_URL = "http://10.48.200.211"
HOST_HEADER = "jarvis.orbishosting.com"
# Devices to probe: agent_id → api_key
@@ -93,8 +93,7 @@ def main():
status = "online" if alive else "offline"
print(f"{agent_id} ({ip}): {status}", flush=True)
heartbeat(agent_id, api_key, alive)
if alive:
update_status(agent_id, api_key, status)
update_status(agent_id, api_key, status)
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Ensures the NAS CIFS mount and the MSP360 bind-mount are both live.
# MSP360's pre-flight check does `mountpoint <Path>` on the exact configured
# storage Path (/mnt/nas-backups/MSPBackups). Since that's a subdirectory of
# the real CIFS mount (/mnt/nas-backups), it never registers as a mountpoint
# on its own -- so we bind-mount it onto itself to make it one.
set -u
MOUNT=/mnt/nas-backups
SUBDIR="$MOUNT/MSPBackups"
LOG=/var/log/msp360-mount-ensure.log
log() { echo "$(date '+%F %T') $1" >> "$LOG"; }
if ! mountpoint -q "$MOUNT"; then
log "CIFS mount down, remounting $MOUNT..."
if mount "$MOUNT" 2>>"$LOG"; then
log "CIFS remount OK"
umount "$SUBDIR" 2>/dev/null || true
else
log "CIFS remount FAILED"
exit 1
fi
fi
if ! mountpoint -q "$SUBDIR"; then
log "Bind mount down, rebinding $SUBDIR..."
mkdir -p "$SUBDIR"
if mount --bind "$SUBDIR" "$SUBDIR" 2>>"$LOG"; then
log "Bind mount OK"
else
log "Bind mount FAILED"
exit 1
fi
fi
exit 0