Skip to content

Commit a54de2a

Browse files
authored
Merge pull request #647 from xcp-ng/gln/add-clean-vm-tool-tsmm
Add clean tool to remove all VMs and local VDIs
2 parents b25a143 + 0f31eae commit a54de2a

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)