- New: smart-home/home-assistant-dashboard-conventions (Mushroom cards, view tabs, no Bubble Cards) - Updated: rke2, ceph, galera, proxmox, brainstorming, compound-learning, 1password-cli, smart-home-automation skills - New references: ceph-cluster-administration, docker-volume-forensics, ceph-crush-weight, ceph-ec-mixed-size
348 lines
17 KiB
Markdown
348 lines
17 KiB
Markdown
# Ceph Orphaned RBD Image Audit & Cleanup — 2026-07-04
|
||
|
||
## When to Use
|
||
|
||
Ceph pool is nearfull/backfill blocked, and you need to quickly free space without adding OSDs. Orphaned RBD images (disks belonging to deleted VMs/CTs) consume real pool capacity and can be safely removed.
|
||
|
||
⚠️ **User preference: Use PVE-native tools (`pvesh`, `pvesm`) for storage inventory, NOT raw `rbd` commands.** `rbd ls` is acceptable for listing images, but VM/CT existence must be checked cluster-wide via `pvesh`, not per-node `qm config`/`pct config` (which only see local VMs).
|
||
|
||
## Identification Workflow
|
||
|
||
### Step 1: Get cluster-wide VM/CT inventory (PRIMARY METHOD)
|
||
|
||
```bash
|
||
# From PVE coordinator — returns ALL VMs/CTs cluster-wide
|
||
pvesh get /cluster/resources --type vm --output-format json | python3 -c "
|
||
import sys, json
|
||
for g in json.load(sys.stdin):
|
||
print(g['vmid'])
|
||
" | sort -n | tr '\n' ' '
|
||
```
|
||
|
||
This gives you the authoritative list of existing VMIDs across all nodes. Store this in a variable for cross-referencing.
|
||
|
||
### Step 2: List all RBD images on the pool
|
||
|
||
```bash
|
||
# List images (rbd ls is fine for listing — it's the VM existence check that must be PVE-native)
|
||
rbd ls hdd_disk
|
||
# With sizes:
|
||
rbd ls hdd_disk -l
|
||
```
|
||
|
||
### Step 3: Cross-reference — find orphans
|
||
|
||
For each RBD image, extract the VMID (`vm-NNN-disk-X` → NNN) and check against the cluster-wide VMID list from Step 1. If the VMID doesn't exist in the `pvesh` output, the image is orphaned.
|
||
|
||
```bash
|
||
# Get existing VMIDs
|
||
EXISTING=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | \
|
||
python3 -c "import sys,json; [print(g['vmid']) for g in json.load(sys.stdin)]" | \
|
||
sort -n | tr '\n' ' ')
|
||
|
||
# Find orphans
|
||
for img in $(rbd ls hdd_disk 2>/dev/null); do
|
||
vmid=$(echo "$img" | sed -n 's/^vm-\([0-9]*\)-.*/\1/p; s/^base-\([0-9]*\)-.*/\1/p')
|
||
if [ -n "$vmid" ]; then
|
||
if ! echo " $EXISTING " | grep -q " $vmid "; then
|
||
size=$(rbd info "hdd_disk/$img" 2>/dev/null | grep "size" | awk '{print $2, $3}')
|
||
echo "ORPHAN: hdd_disk:$img ($size) — VMID $vmid does not exist"
|
||
fi
|
||
else
|
||
# Non-VM image (csi-vol, test-dummy, etc.)
|
||
size=$(rbd info "hdd_disk/$img" 2>/dev/null | grep "size" | awk '{print $2, $3}')
|
||
echo "NON-VM: hdd_disk:$img ($size)"
|
||
fi
|
||
done
|
||
```
|
||
|
||
### ⚠️ Why NOT to use per-node `qm config`/`pct config`
|
||
|
||
`qm config NNN` and `pct config NNN` only check the LOCAL node. A VM running on proxmox5 won't be found by `qm config` executed on proxmox1. This leads to **false positives** — images incorrectly classified as orphaned because the VM exists on a different node.
|
||
|
||
**Always use `pvesh get /cluster/resources --type vm` for cluster-wide inventory.**
|
||
|
||
### Step 4: Verify no dependencies before deleting
|
||
|
||
For each orphaned image, check:
|
||
|
||
```bash
|
||
# Active watchers (attached clients)
|
||
rbd status -p hdd_disk <image>
|
||
# Watchers: none ← safe to delete
|
||
|
||
# Clone children (if base/parent image)
|
||
rbd children -p hdd_disk <base-image>
|
||
# (empty output) ← no clones, safe to delete
|
||
|
||
# For CSI volumes (csi-vol-*) — check watchers only
|
||
rbd status -p hdd_disk csi-vol-<uuid>
|
||
```
|
||
|
||
### Step 5: Delete orphaned images
|
||
|
||
```bash
|
||
rbd rm -p hdd_disk <image>
|
||
# Or batch:
|
||
for img in vm-104-disk-0 vm-104-disk-1 vm-108-disk-0 ...; do
|
||
rbd rm -p hdd_disk $img
|
||
done
|
||
```
|
||
|
||
### ⚠️ Protected Snapshot Deletion Order (2026-07-12)
|
||
|
||
Base images (`base-NNN-disk-X`) often have a protected `__base__` snapshot
|
||
(leftover from PVE template cloning). `rbd rm` FAILS with:
|
||
|
||
```
|
||
rbd: image has snapshots - these must be deleted with 'rbd snap purge' before the image can be removed.
|
||
```
|
||
|
||
And `rbd snap purge` FAILS if the snapshot is protected:
|
||
|
||
```
|
||
rbd: cannot remove protected snapshot __base__
|
||
```
|
||
|
||
**Correct deletion order:**
|
||
|
||
```bash
|
||
# 1. Unprotect the snapshot FIRST
|
||
rbd snap unprotect "hdd_disk/base-101-disk-0"@__base__
|
||
|
||
# 2. Purge ALL snapshots
|
||
rbd snap purge "hdd_disk/base-101-disk-0"
|
||
# → "Removing all snapshots: 100% complete...done."
|
||
|
||
# 3. Remove the image
|
||
rbd rm "hdd_disk/base-101-disk-0" --no-progress
|
||
```
|
||
|
||
⚠️ There is NO `--force` flag for `rbd snap purge` in older Ceph versions.
|
||
The unprotect step is mandatory and must succeed before purge.
|
||
|
||
⚠️ Under Ceph backfill load, `rbd rm` may hang for 60s+ — the image header
|
||
is removed immediately but object deletion happens asynchronously. Use
|
||
`terminal(background=true, notify_on_complete=true)` for batch deletions
|
||
to avoid SSH timeouts.
|
||
|
||
## Classification Rules
|
||
|
||
| Image Pattern | Check | Safe to Delete? |
|
||
|---------------|-------|-----------------|
|
||
| `vm-NNN-disk-X` | `qm config NNN` / `pct config NNN` | ✅ if VMID doesn't exist |
|
||
| `vm-NNN-cloudinit` | Same VMID check | ✅ if VMID doesn't exist |
|
||
| `base-NNN-disk-X` | `rbd children` | ✅ if no clones |
|
||
| `csi-vol-<uuid>` | `rbd status` (watchers) | ✅ if no watchers |
|
||
| `test-dummy` | Name pattern | ✅ always (test artifact) |
|
||
|
||
## Case Study: 2026-07-04 Audit
|
||
|
||
### Pool: hdd_disk (92% full, backfill blocked)
|
||
|
||
Found **40 orphaned RBD images** totaling ~2.1 TB thin-provisioned across 24 non-existent VMIDs. Largest orphans:
|
||
|
||
| Image | Size | VMID Exists? |
|
||
|-------|------|-------------|
|
||
| vm-202-disk-2 | 300 GB | ❌ |
|
||
| vm-203-disk-1 | 300 GB | ❌ |
|
||
| vm-116-disk-1 | 192 GB | ❌ |
|
||
| vm-204-disk-0 | 150 GB | ❌ |
|
||
| csi-vol-b78002cf | 100 GB | ❌ (no watchers) |
|
||
| vm-128/131/132-disk-0 | 80 GB each | ❌ |
|
||
| vm-202-disk-1, vm-203-disk-0, vm-126-disk-0 | 64 GB each | ❌ |
|
||
| ... + 27 more | | |
|
||
|
||
### Images NOT deleted (VM/CT still exists)
|
||
|
||
| Image | Size | Owner | Status |
|
||
|-------|------|-------|--------|
|
||
| vm-107-disk-0 | 82 GB | VM 107 (openclaw) | stopped |
|
||
| vm-111-disk-0/3 | 350 GB each | CT 111 (docker) | stopped, backup lock |
|
||
| vm-123-disk-0 | 40 GB | CT 123 (ollama) | stopped |
|
||
| vm-136-disk-0 | 8 GB | CT 136 (smb-media) | running |
|
||
|
||
### Key Insight
|
||
|
||
Even though images are thin-provisioned (actual usage < provisioned size), deleting orphans frees real pool capacity because Ceph tracks allocated objects per image. On a pool at 92% with `backfill_toofull` blocking recovery, removing 40 orphaned images can unblock the backfill without adding new OSDs.
|
||
|
||
## VM Destruction on Non-Ceph Nodes (stale locks)
|
||
|
||
When `qm destroy NNN` fails with `got timeout` on CFS storage locks (common when Ceph is under load), the VM config can be removed manually:
|
||
|
||
### Step 1: Delete VM config and locks on the node hosting the VM
|
||
|
||
```bash
|
||
# On the node where the VM lives (e.g., n5pro = 10.0.20.91)
|
||
rm -f /etc/pve/qemu-server/NNN.conf
|
||
rm -f /var/lock/qemu-server/lock-NNN.conf
|
||
```
|
||
|
||
⚠️ **Non-Ceph nodes (no `ceph.conf`) cannot run `rbd` commands.** n5pro has no Ceph monitor — `rbd remove` fails with "can't open ceph.conf" / "couldn't connect to the cluster". RBD image removal must be run from a Ceph monitor node (e.g., proxmox7 = 10.0.20.70).
|
||
|
||
### Step 2: Remove RBD images from a Ceph monitor node
|
||
|
||
```bash
|
||
# On proxmox7 (10.0.20.70) — the Ceph monitor node
|
||
for img in vm-NNN-disk-0 vm-NNN-disk-1 vm-NNN-cloudinit; do
|
||
rbd remove -p hdd_disk $img 2>&1
|
||
done
|
||
```
|
||
|
||
Some images may already be gone ("No such file or directory") if a partial `qm destroy` succeeded before timing out.
|
||
|
||
### Step 3: Handle "image still has watchers" errors
|
||
|
||
If `rbd remove` fails with "image still has watchers", a previous `rbd map` or crashed client holds the image open:
|
||
|
||
```bash
|
||
# Find which node has the stale mapping
|
||
rbd showmapped # on local node
|
||
# Check ALL proxmox nodes:
|
||
for node in proxmox4 proxmox5 proxmox6 proxmox7; do
|
||
ssh root@$node "rbd showmapped | grep NNN"
|
||
done
|
||
|
||
# Force unmap on the offending node
|
||
ssh root@<node> "rbd unmap -o force /dev/rbdN"
|
||
|
||
# Then retry removal from monitor node
|
||
rbd remove -p <pool> vm-NNN-disk-X
|
||
```
|
||
|
||
⚠️ `rbd unmap` may fail with "Device or resource busy" if system processes hold it. Use `-o force` flag. After force unmap, the device disappears from `/dev/rbdN` and the watcher expires within 30s.
|
||
|
||
## Identifying Orphaned VM Contents (RBD Inspection)
|
||
|
||
When a VM config is already deleted and you need to know what the VM was (for risk assessment before deleting disks):
|
||
|
||
```bash
|
||
# Map the RBD image to a block device
|
||
rbd map -p hdd_disk vm-NNN-disk-X
|
||
# → /dev/rbd10
|
||
|
||
# Check if it has a partition table
|
||
fdisk -l /dev/rbd10
|
||
blkid /dev/rbd10
|
||
|
||
# If ext4 directly (no partition table):
|
||
mount /dev/rbd10 /mnt/inspect
|
||
ls /mnt/inspect/
|
||
|
||
# If GPT partition table:
|
||
mount /dev/rbd10p1 /mnt/inspect
|
||
|
||
# Identify the OS / purpose
|
||
cat /mnt/inspect/etc/hostname
|
||
cat /mnt/inspect/etc/os-release | head -3
|
||
ls /mnt/inspect/ # look for model files, docker volumes, etc.
|
||
|
||
# ALWAYS clean up
|
||
umount /mnt/inspect
|
||
rbd unmap /dev/rbd10
|
||
```
|
||
|
||
⚠️ Mounting RBD images on a cluster under heavy backfill load can timeout (30s+). If mount hangs, kill it and try later — or infer contents from disk size and structure (e.g., 300 GB ext4 with a single `.gguf` file = AI model storage).
|
||
|
||
⚠️ **Always `rbd unmap` after inspection.** Leftover mappings create "image still has watchers" errors when you later try to delete the image.
|
||
|
||
### Case Study: VM 202/203/116 Destruction (2026-07-04)
|
||
|
||
VMs 201, 202, 203 existed on n5pro (10.0.20.91, non-Ceph node) and VM 116 on proxmox6. All were stopped, `qm destroy` failed with CFS storage lock timeouts because Ceph was under heavy backfill load.
|
||
|
||
**Resolution:**
|
||
1. Deleted VM configs manually on n5pro: `rm -f /etc/pve/qemu-server/{201,202,203}.conf` + stale locks
|
||
2. Removed RBD images from proxmox7 (Ceph monitor node): `rbd remove -p hdd_disk vm-{201,202,203}-disk-*`
|
||
3. VM 116 had a stale RBD mapping on proxmox6 (`/dev/rbd4` → `vm-116-disk-0` on `vm_disks` pool). Force unmapped: `rbd unmap -o force /dev/rbd4`, then removed both disk-0 (vm_disks) and disk-1 (hdd_disk, already gone).
|
||
|
||
**Contents identified via RBD inspection:**
|
||
- VM 202: AI inference worker (Qwen3.6-35B-A3B-UD-IQ4_NL.gguf on 300 GB disk)
|
||
- VM 203: Same structure (64 GB OS + 300 GB data), likely AI worker
|
||
- VM 116: 192 GB ext4 data disk, no partition table (likely MariaDB-01)
|
||
- Total freed: ~920 GB thin-provisioned
|
||
|
||
## Fast Orphan Verification: `/etc/pve/` Grep (O(1))
|
||
|
||
When you need to check if a **specific** image (e.g. `vm-143-disk-0`) is
|
||
referenced by ANY VM/CT config, do NOT iterate all 56 guests with `pvesh`.
|
||
Instead, grep the PVE config filesystem directly:
|
||
|
||
```bash
|
||
# From any PVE node — /etc/pve is a shared FUSE filesystem (pmxcfs)
|
||
grep -rl "vm-143" /etc/pve/qemu-server/ /etc/pve/lxc/ 2>/dev/null
|
||
# Exit 0 + filename = referenced. Exit 1 = orphan (not referenced anywhere).
|
||
```
|
||
|
||
This is **dramatically faster** than the serial `pvesh` approach:
|
||
- Serial `pvesh get /nodes/{node}/{type}/{vmid}/config` for 56 guests takes
|
||
>120s and often times out.
|
||
- `grep -rl` on `/etc/pve/` returns in <2s because pmxcfs is local memory.
|
||
|
||
Works for any image pattern: `vm-NNN`, `base-NNN`, `csi-vol-xxx`, `test-dummy`.
|
||
|
||
## Full Cluster Disk-Storage Audit Technique
|
||
|
||
To produce a complete table of all guests with their disk storage assignments:
|
||
|
||
```bash
|
||
ssh root@10.0.20.10 'bash -s' << 'SCRIPT'
|
||
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | python3 -c "
|
||
import sys, json, subprocess
|
||
|
||
guests = json.load(sys.stdin)
|
||
guests.sort(key=lambda g: g['vmid'])
|
||
|
||
for g in guests:
|
||
vmid = g['vmid']
|
||
vtype = g.get('type','?')
|
||
node = g.get('node','?')
|
||
|
||
if vtype == 'qemu':
|
||
cmd = ['pvesh', 'get', f'/nodes/{node}/qemu/{vmid}/config', '--output-format', 'json']
|
||
else:
|
||
cmd = ['pvesh', 'get', f'/nodes/{node}/lxc/{vmid}/config', '--output-format', 'json']
|
||
|
||
try:
|
||
cfg_raw = subprocess.run(cmd, capture_output=True, text=True, timeout=5).stdout
|
||
cfg = json.loads(cfg_raw) if cfg_raw.strip() else {}
|
||
except:
|
||
cfg = {}
|
||
|
||
storages = []
|
||
for key, val in cfg.items():
|
||
if key.startswith(('scsi','virtio','ide','sata','nvme','rootfs','mp')):
|
||
if isinstance(val, str) and ':' in val:
|
||
volid = val.split(',')[0]
|
||
parts = volid.split(':')
|
||
if len(parts) >= 2 and parts[0] not in ('none','cloud-init'):
|
||
storages.append(parts[0])
|
||
stores = list(dict.fromkeys(storages))
|
||
print(f'{vmid:>6} | {vtype:4} | {node:10} | {\", \".join(stores) or \"none\"}')
|
||
"
|
||
SCRIPT
|
||
```
|
||
|
||
⚠️ Needs `timeout=180` on the SSH call — 56 guests × 5s per-config timeout
|
||
can exceed 120s. Split into batches of 20 if timeout is constrained.
|
||
|
||
## Pitfalls
|
||
|
||
1. **`rbd du` timeouts** — On pools with many images, `rbd du` can hang. Use per-image `rbd info` instead, but batch to avoid SSH timeouts.
|
||
2. **CSI volumes look orphaned but may be Kubernetes/external provisions** — Always check `rbd status` for watchers before deleting `csi-vol-*` images.
|
||
3. **Base images with clones** — `rbd children` must return empty before deleting. Deleting a parent with active clones corrupts the clone.
|
||
4. **Stopped VMs still own their disks** — A stopped VM (like VM 107 openclaw) still has a valid config referencing the disk. Only delete if the VMID has NO config at all (`qm config` AND `pct config` both fail).
|
||
5. **Backup-locked CTs** — CT 111 had a `backup` lock. Don't delete disks of locked CTs even if stopped.
|
||
6. **Timeout management** — Checking 50+ VMIDs via SSH can exceed 15s. Increase timeout to 30-60s or batch in groups of 10-15.
|
||
7. **Non-Ceph nodes can't run `rbd` commands** — Nodes without `ceph.conf` (like n5pro) cannot connect to the Ceph cluster. Run all `rbd` operations from a monitor node.
|
||
8. **`qm destroy` CFS lock timeout under Ceph load** — When Ceph is slow (backfilling, nearfull), Proxmox's CFS storage lock acquisition times out. Delete configs manually with `rm -f` and remove RBD images separately from a monitor node.
|
||
9. **Stale RBD mappings block image deletion** — A previous `rbd map` that wasn't cleaned up (or a crashed QEMU) keeps watchers on the image. Check `rbd showmapped` on ALL nodes, force unmap, then retry removal.
|
||
10. **Always unmap RBD images after inspection** — Mounted RBD images create watchers that prevent later deletion. `umount` + `rbd unmap` after every inspection session.
|
||
11. **Mount can hang under heavy backfill** — When Ceph recovery I/O is high, `mount /dev/rbdN` can hang for 30s+. Abort and infer from disk structure if needed.
|
||
12. **`pvesm list` fails when pool is backfillfull** (2026-07-12) — `pvesm list hdd_disk` returns empty with `rbd error: (2) No such file or directory` when the Ceph pool is in `backfillfull` state. Similarly, `pvesh get /nodes/{node}/storage/hdd_disk/content` also fails. In this scenario, fall back to direct `rbd ls <pool>` (which still works) for image listing, and use `pvesh get /cluster/resources --type vm` for VM/CT existence checks. The PVE storage layer cannot enumerate RBD images when Ceph blocks I/O.
|
||
13. **Ghost RBD images with empty `rbd info`** (2026-07-12) — `vm-201-disk-0` and `vm-202-disk-0` appear in `rbd ls` but `rbd info` returns empty output (even with `--format json`). This is the "ghost RBD" state described in the main skill body — header is gone, data objects may remain. These need `rados rm` purge, not `rbd rm`.
|
||
14. **`execute_code` blocked for SSH-heavy scripts** (2026-07-12) — When orphan detection requires many sequential SSH calls (one per image for `rbd info`), `execute_code` may be blocked by approval policy. Use `terminal` with heredoc scripts instead: `ssh root@host 'bash -s' << 'SCRIPT' ... SCRIPT`. Batch all `rbd info` calls in a single SSH session to avoid per-call overhead.
|
||
15. **Serial `pvesh` config retrieval for 56 guests exceeds 120s** (2026-07-12) — Iterating all guests with `pvesh get /nodes/{node}/{type}/{vmid}/config` one-by-one (even with `timeout=5` per call) takes >120s for 56 guests and times out. Solutions: (a) increase SSH timeout to 180s+, (b) split into batches of 20, or (c) for specific-image checks use `grep -rl "vm-NNN" /etc/pve/qemu-server/ /etc/pve/lxc/` instead (O(1), <2s). See "Fast Orphan Verification" section above.
|
||
16. **SSH key path typo** (2026-07-12) — `id_ed25519_prozmox` vs `id_ed25519_proxmox` — transposing `x` and `m` in the key filename causes a silent fallback to password auth which then fails with "Permission denied (publickey,password)". Always double-check the key path: `~/.ssh/id_ed25519_proxmox`.
|
||
17. **CSI volume with stale watchers** (2026-07-12) — `csi-vol-e596e771-...` on hdd_disk had a watcher from IP 10.0.30.63 (K8s pod VLAN) but `kubectl get pv,pvc` showed no RBD PVCs. The watcher is a stale client from a crashed/evicted pod. `rbd rm` fails with "image still has watchers". Resolution: wait 30s for the watcher to timeout, or identify and kill the stale ceph client. Do NOT force-delete — may corrupt data if a pod reattaches.
|
||
18. **`rbd rm` succeeds but image lingers during backfill** (2026-07-12) — When Ceph is in backfill state, `rbd rm` may return exit 0 but the image still appears in `rbd ls` for several seconds. The header is removed but object cleanup is deferred. Check with `rbd info` (returns "No such file or directory" once truly gone) rather than `rbd ls` for definitive verification.
|