Skip to content

Commit 0f31eae

Browse files
committed
Add clean tool to remove all VMs and local VDIs
Add a new 'clean' subcommand to tools.py that removes all VMs (with all their VDIs, regardless of storage location) from the target pools and then clears the remaining orphan VDIs on the local (non-shared) storage SRs. Supports a --dry-run mode, and verifies that VMs and local SRs are actually empty before returning so downstream scripts can rely on a clean state. Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent 70caf2d commit 0f31eae

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

lib/tools/cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from lib.common import HostAddress
1212
from lib.tools import logger
1313
from lib.tools.inventory import into_inventory, load_inventory
14+
from lib.tools.tasks.clean import clean_pools
1415
from lib.tools.tasks.update import update_pools
1516

1617
def _command_update(args: argparse.Namespace) -> None:
@@ -22,6 +23,15 @@ def _command_update(args: argparse.Namespace) -> None:
2223
update_pools(inventory)
2324

2425

26+
def _command_clean(args: argparse.Namespace) -> None:
27+
if args.inventory:
28+
inventory = load_inventory(args.inventory)
29+
else:
30+
inventory = into_inventory(args.hosts, [], args.hosting_pool)
31+
32+
clean_pools(inventory, dry_run=args.dry_run)
33+
34+
2535
def cli() -> None:
2636
parser = argparse.ArgumentParser(
2737
description="Tools that help developers for running recurrent tasks on their XCP-ng sandbox."
@@ -61,6 +71,31 @@ def cli() -> None:
6171
)
6272
subparser_cmd_update.set_defaults(func=_command_update)
6373

74+
# subparser - command: clean
75+
subparser_cmd_clean = subparsers.add_parser(
76+
name="clean",
77+
description="Remove all VMs and all VDIs on local storage from target pools",
78+
help="Remove all VMs and all VDIs on local storage from target pools",
79+
)
80+
cmd_clean_excl_grp = subparser_cmd_clean.add_mutually_exclusive_group(required=True)
81+
cmd_clean_excl_grp.add_argument(
82+
"-H",
83+
"--hosts",
84+
type=HostAddress,
85+
metavar="HOST",
86+
nargs="+",
87+
help="Address (hostname|ip) of the master host in pool",
88+
)
89+
cmd_clean_excl_grp.add_argument("-i", "--inventory", type=Path, help="Use an hosts inventory file")
90+
subparser_cmd_clean.add_argument(
91+
"-n",
92+
"--dry-run",
93+
action="store_true",
94+
default=False,
95+
help="Only display what would be removed, without deleting anything",
96+
)
97+
subparser_cmd_clean.set_defaults(func=_command_clean)
98+
6499
args = parser.parse_args()
65100

66101
if args.debug:

lib/tools/tasks/clean.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Cleanup tasks.
2+
3+
This module is intended for removing all VMs and all VDIs on local storage
4+
from existing remote targets.
5+
"""
6+
from __future__ import annotations
7+
8+
from concurrent.futures import ThreadPoolExecutor
9+
10+
from lib.common import safe_split, wait_for_not
11+
from lib.pool import NotAMasterHostError, Pool
12+
from lib.sr import SR
13+
from lib.tools.inventory import Inventory
14+
from lib.vdi import VDI
15+
from lib.vm import VM
16+
17+
from .. import logger
18+
19+
def clean_pools(inventory: Inventory, dry_run: bool = False) -> None:
20+
"""Remove all VMs and all orphan VDIs on local storage from pool(s).
21+
22+
.. note::
23+
24+
Every non-master hosts in inventory will be ignored
25+
26+
*For each pool's master host declared in inventory, destroy all its VMs
27+
(with all their VDIs, regardless of their location), then destroy all the
28+
orphan VDIs left on the local (non-shared) SRs.*
29+
30+
:param Inventory inventory:
31+
Each host (key) holds its own config data (values, eg: `enablerepos`).
32+
:param bool dry_run:
33+
When True, only log what would be removed without actually deleting.
34+
"""
35+
inventory_hosts = inventory["hosts"]
36+
pools: list[Pool] = []
37+
for host in inventory_hosts:
38+
try:
39+
pools.append(Pool(host))
40+
except NotAMasterHostError:
41+
logger.warning(f"[{host}] Skipping: not a master host")
42+
43+
with ThreadPoolExecutor() as executor:
44+
futures = {executor.submit(clean_pool, p, dry_run): p for p in pools}
45+
for future in futures:
46+
pool = futures[future]
47+
try:
48+
future.result()
49+
except Exception as exc:
50+
logger.error(f"Cleaning pool has failed! The master {pool.master} cannot be cleaned.")
51+
raise exc
52+
53+
def clean_pool(pool: Pool, dry_run: bool) -> None:
54+
"""Remove all VMs and all orphan VDIs on local SRs from a single pool."""
55+
master = pool.master
56+
log_prefix = 'Would remove' if dry_run else 'Removing'
57+
58+
vm_uuids = safe_split(master.xe(
59+
'vm-list',
60+
{'is-control-domain': False, 'is-a-template': False},
61+
minimal=True,
62+
))
63+
for vm_uuid in vm_uuids:
64+
vm = VM(vm_uuid, master)
65+
logger.info(f"[{master}] {log_prefix} VM {vm.uuid} ({vm.name()})")
66+
if not dry_run:
67+
vm.destroy(verify=True)
68+
69+
sr_uuids = local_sr_uuids(pool)
70+
for sr_uuid in sr_uuids:
71+
for vdi_uuid in SR(sr_uuid, pool).vdi_uuids(managed=True):
72+
vdi = VDI(vdi_uuid, sr=SR(sr_uuid, pool))
73+
logger.info(f"[{master}] {log_prefix} orphan VDI {vdi.uuid} from local SR {sr_uuid}")
74+
if not dry_run:
75+
vdi.destroy()
76+
77+
if not dry_run:
78+
for sr_uuid in sr_uuids:
79+
wait_for_not(
80+
lambda: len(SR(sr_uuid, pool).vdi_uuids(managed=True)) > 0,
81+
f"Wait for local SR {sr_uuid} to be empty",
82+
)
83+
84+
def local_sr_uuids(pool: Pool) -> list[str]:
85+
"""Return the UUIDs of the pool's local (non-shared, user) SRs."""
86+
uuids = safe_split(pool.master.xe('sr-list', {'content-type': 'user'}, minimal=True))
87+
return [uuid for uuid in uuids if not SR(uuid, pool).is_shared()]

0 commit comments

Comments
 (0)