Files
xcp/scripts/xapi_leaked_vbds.py
Andrii Sultanov 4a277e62c1 scripts: Add helper script to list possibly leftover VBDs due to VM.revert regression
A regression was introduced to VM.revert in xapi-26.1.4-3.3, which left VBDs
attached to the VM when they are not present in the reverted-to
snapshot (instead of destroying such VBDs, as was done before).

VMs that were reverted to their snapshots after xapi-26.1.4-3.3 may have
such "leftover" VBDs. This script can be used to detect possible cases in
pools. Sadly, there is no way to automatically distinguish a VBD that
was created after a VM.revert from a VBD that survived the VM.revert,
so this script relies on the user to determine if the suspicious VBDs are
indeed affected or not.

Therefore, tbis is not a destructive script, it just uses the API to find the
affected VBDs and presents them to the user, as in this example output:

    VM e2678368-596d-1b6e-7211-9ee45f60dec1 ("Alpine minimal for CI") has
    VBDs that are not present in the snapshot it was reverted to
    (303ead66-9515-b1c8-0dc5-d33b7644b161 "Alpine minimal for CI_snapshot1"):
    xvdb (ca8610d2-76e3-6c54-4378-ee599619b4bb))

The script follows this algorithm:

1. Find all VMs that are children of snapshots
   (i.e. were reverted to a particular snapshot at some point in time)
2. Find VBDs that are currently present in such VMs but are
   not present in the snapshot

Signed-off-by: Andrii Sultanov <[email protected]>
2026-07-13 11:45:35 +00:00

77 lines
3.1 KiB
Python

#!/usr/bin/env python3
import XenAPI
def main():
session = XenAPI.xapi_local()
xapi = session.xenapi
try:
xapi.login_with_password("root", "", "0.1", "leaked_vbds_script")
# Get a list of snapshots
vms = xapi.VM.get_all_records()
snap_vms = {ref: vm for ref, vm in vms.items() if vm['is_a_snapshot']}
nonsnap_vms = {ref: vm for ref, vm in vms.items() if not vm['is_a_snapshot']}
# Dict of {vm: last_snapshot}
vm_snaps = {vm_ref: vm['parent'] for (vm_ref, vm) in nonsnap_vms.items() if
vm['parent'] != 'OpaqueRef:NULL' and
not vm['parent'].startswith('Ref:')}
# Find the snapshots the VM could have been reverted to
vm_reverted_snaps = {}
for vm_ref, snap in vm_snaps.items():
snaps = [snap]
while True:
new_snap = xapi.VM.get_parent(snap)
if new_snap.startswith('Ref:'):
new_snap = next((vm_ref for (vm_ref, vm) in snap_vms.items()
if vm['children'] and vm['children'][0] == snap),
'OpaqueRef:NULL')
if new_snap == 'OpaqueRef:NULL':
break
snaps.append(new_snap)
snap = new_snap
vm_uuid = xapi.VM.get_uuid(vm_ref)
# VM keeps the snapshot's install time
install_time = xapi.VM_metrics.get_install_time(xapi.VM.get_metrics(vm_ref))
snaps = [snap for snap in snaps if
install_time == xapi.VM_metrics.get_install_time(xapi.VM.get_metrics(snap))]
if snaps:
vm_reverted_snaps[vm_ref] = snaps
for vm, snapshots in vm_reverted_snaps.items():
# List of VBDs that are attached to VMs
vm_vbds = xapi.VM.get_VBDs(vm)
vm_vbds = {xapi.VBD.get_device(vbd_ref): vbd_ref for vbd_ref in vm_vbds}
vm_set = set(vm_vbds)
vm_uuid = xapi.VM.get_uuid(vm)
for snap in snapshots:
# List of VBDs that are attached to snapshots
snapshotted_vbds = xapi.VM.get_VBDs(snap)
snapshotted_vbds = {xapi.VBD.get_device(vbd_ref): vbd_ref for vbd_ref in snapshotted_vbds}
snap_set = set(snapshotted_vbds)
# List of VBDs that are not part of the snapshotted VM but
# are attached to the VM currently
diff = set(vm_vbds) - snap_set
if diff:
vm_uuid = xapi.VM.get_uuid(vm)
vm_name_label = xapi.VM.get_name_label(vm)
snap_uuid = xapi.VM.get_uuid(snap)
snap_name_label = xapi.VM.get_name_label(snap)
vbds = ','.join([f'{dev} ({xapi.VBD.get_uuid(vm_vbds[dev])})' for dev in diff])
print(f'VM {vm_uuid} ("{vm_name_label}") has VBDs that are'
f' not present in the snapshot it was reverted to ({snap_uuid}'
f' "{snap_name_label}"): {vbds}')
finally:
session.xenapi.session.logout()
if __name__ == "__main__":
main()