Skip to content

Commit a7da1e7

Browse files
Changes to support migration of volumes with QoS
1 parent 6c52216 commit a7da1e7

5 files changed

Lines changed: 200 additions & 65 deletions

File tree

‎test/integration/plugins/ontap/migration/migration_test_base.py‎

Lines changed: 122 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def setUpClass(cls):
157157
cls.created_vms = []
158158
cls.service_offerings = {}
159159
cls.disk_offerings = {}
160+
cls.qos_disk_offerings = {}
160161
try:
161162
cls.default_pool = cls._find_default_primary_pool()
162163
cls.ontap_pools = cls._ensure_migration_pools()
@@ -174,6 +175,7 @@ def setUpClass(cls):
174175
)
175176
cls.template_id = cls._find_template()
176177
cls.network_id = cls._find_or_create_network()
178+
cls._ensure_disk_offerings()
177179
except Exception:
178180
cls.tearDownClass()
179181
raise
@@ -1209,6 +1211,40 @@ def _default_storage_tag(cls):
12091211
return pool.get("tags")
12101212
return getattr(cls.default_pool, "tags", None)
12111213

1214+
@classmethod
1215+
def _ensure_disk_offerings(cls):
1216+
"""Create the plain and QoS disk offerings used by this suite."""
1217+
cls._service_offering()
1218+
cls._small_data_offering()
1219+
default_tag = cls._default_storage_tag()
1220+
cls._root_disk_offering(default_tag)
1221+
cls._qos_disk_offering(8, 100, 500, default_tag)
1222+
cls._qos_disk_offering(1, 100, 500)
1223+
cls._qos_disk_offering(1, 100, 800)
1224+
protocols = cls.config_data.get("storagePool", {}).get(
1225+
"protocols", {}
1226+
)
1227+
for key in ("nfs3", "iscsi"):
1228+
config = protocols.get(key, {})
1229+
if not config.get("enabled", False):
1230+
continue
1231+
tag = config.get("storagePoolTags")
1232+
cls._root_disk_offering(tag)
1233+
cls._qos_disk_offering(8, 100, 500, tag)
1234+
1235+
@classmethod
1236+
def _small_data_offering(cls):
1237+
if getattr(cls, "small_disk_offering_id", None):
1238+
return cls.small_disk_offering_id
1239+
offering = DiskOffering.create(cls.apiClient, {
1240+
"name": "ontap-migration-data-%d" % random.randint(0, 999999),
1241+
"displaytext": "ONTAP migration 1GB data disk",
1242+
"disksize": 1,
1243+
})
1244+
cls.small_disk_offering_id = offering.id
1245+
cls._cleanup.insert(0, offering)
1246+
return offering.id
1247+
12121248
@classmethod
12131249
def _service_offering(cls):
12141250
if "shared" in cls.service_offerings:
@@ -1245,21 +1281,37 @@ def _root_disk_offering(cls, storage_tag):
12451281
cls._cleanup.insert(0, offering)
12461282
return offering
12471283

1248-
def _qos_disk_offering(self, disksize, miniops, maxiops, storage_tag=None):
1249-
"""Create a disk offering that carries fixed min and max IOPS."""
1284+
@classmethod
1285+
def _qos_disk_offering(cls, disksize, miniops, maxiops, storage_tag=None):
1286+
"""Return the cached disk offering for these fixed IOPS."""
1287+
key = (int(disksize), int(miniops), int(maxiops), storage_tag or "")
1288+
offering = cls.qos_disk_offerings.get(key)
1289+
if offering is not None:
1290+
return offering
1291+
# createDiskOffering rejects a minimum of 0. A non-AFF pool rejects
1292+
# a minimum above 0 and accepts a maximum alone (cs_0_to_{max}).
1293+
api_min = int(miniops)
1294+
api_max = int(maxiops)
1295+
if api_min <= 0 or api_min > api_max:
1296+
api_min = api_max
12501297
data = {
12511298
"name": "ontap-migration-qos-%d-%d-%d" % (
1252-
miniops, maxiops, random.randint(0, 999999)
1299+
api_min, api_max, random.randint(0, 999999)
12531300
),
12541301
"displaytext": "ONTAP migration QoS disk",
12551302
"disksize": disksize,
1256-
"miniops": miniops,
1257-
"maxiops": maxiops,
1303+
"miniops": api_min,
1304+
"maxiops": api_max,
12581305
}
12591306
if storage_tag:
12601307
data["tags"] = storage_tag
1261-
offering = DiskOffering.create(self.apiClient, data)
1262-
self.__class__._cleanup.insert(0, offering)
1308+
offering = DiskOffering.create(cls.apiClient, data)
1309+
cls.dbConnection.execute(
1310+
"UPDATE disk_offering SET min_iops = 0 WHERE uuid = %s",
1311+
(offering.id,),
1312+
)
1313+
cls.qos_disk_offerings[key] = offering
1314+
cls._cleanup.insert(0, offering)
12631315
return offering
12641316

12651317
def _qos_policy_name(self, miniops, maxiops):
@@ -1482,15 +1534,7 @@ def _create_data_volume(self, pool, offering=None):
14821534
if offering is not None:
14831535
offering_id = offering.id
14841536
else:
1485-
offering_id = getattr(cls, "small_disk_offering_id", None)
1486-
if offering_id is None:
1487-
offering = DiskOffering.create(cls.apiClient, {
1488-
"name": "ontap-migration-data-%d" % random.randint(0, 999999),
1489-
"displaytext": "ONTAP migration 1GB data disk",
1490-
"disksize": 1,
1491-
})
1492-
cls.small_disk_offering_id = offering.id
1493-
offering_id = offering.id
1537+
offering_id = cls._small_data_offering()
14941538
cmd = createVolumeAPI.createVolumeCmd()
14951539
cmd.name = "%s_%d" % (self._vol_name_prefix, random.randint(0, 99999))
14961540
cmd.diskofferingid = offering_id
@@ -1503,11 +1547,16 @@ def _create_data_volume(self, pool, offering=None):
15031547
return volume
15041548

15051549
def _assert_volume_qos(self, volume, protocol, pool, miniops, maxiops):
1506-
"""Check CloudStack IOPS and the destination ONTAP QoS policy."""
1550+
"""Check CloudStack IOPS and the destination ONTAP QoS policy.
1551+
1552+
miniops is the value requested on the offering. Non-AFF storage
1553+
persists 0, so the volume and the policy are checked against 0.
1554+
"""
15071555
current = self._get_volume(volume.id)
1508-
self.assertEqual(int(current.miniops), miniops)
1509-
self.assertEqual(int(current.maxiops), maxiops)
1510-
expected = self._qos_policy_name(miniops, maxiops)
1556+
# The offering row stores min_iops 0. The volume and the policy use that.
1557+
self.assertEqual(int(current.miniops or 0), 0)
1558+
self.assertEqual(int(current.maxiops or 0), maxiops)
1559+
expected = self._qos_policy_name(0, maxiops)
15111560
if protocol.upper() == "NFS3":
15121561
record = self.__class__.ontap.get_nfs_file(
15131562
pool.name, current.path
@@ -1547,22 +1596,50 @@ def _assert_vm_qos(self, vm, protocol, pool, root_iops, data_iops):
15471596
volume, protocol, pool, data_iops[0], data_iops[1]
15481597
)
15491598

1550-
def _prepare_qos_vm(self, root_iops, data_iops, stop=False):
1551-
"""Deploy a DefaultPrimary VM whose root and data disks have IOPS."""
1599+
def _prepare_qos_vm(self, root_iops, data_iops, stop=False, protocol=None):
1600+
"""Deploy a QoS VM. A protocol puts both disks on that ONTAP pool."""
15521601
host = self._host_for_cluster(self.__class__.cluster.id)
1602+
tag = (
1603+
self._storage_tag(protocol) if protocol
1604+
else self._default_storage_tag()
1605+
)
15531606
root = self._qos_disk_offering(
1554-
8, root_iops[0], root_iops[1], self._default_storage_tag()
1607+
8, root_iops[0], root_iops[1], tag
15551608
)
15561609
vm = self._deploy_vm(host_id=host.id, root_offering=root)
1610+
if protocol:
1611+
roots = [
1612+
volume for volume in self._vm_volumes(vm.id)
1613+
if str(getattr(volume, "type", "")).upper() == "ROOT"
1614+
]
1615+
data_pool = self._pool_by_id(roots[0].storageid)
1616+
else:
1617+
data_pool = self.__class__.default_pool
15571618
data = self._create_data_volume(
1558-
self.__class__.default_pool,
1619+
data_pool,
15591620
self._qos_disk_offering(1, data_iops[0], data_iops[1]),
15601621
)
15611622
self._attach_volume(vm, data)
1623+
# The offering row is min 0. Copy that onto the volume rows before any
1624+
# ONTAP create, so a non-AFF pool is not asked for a minimum above 0.
1625+
for volume in self._vm_volumes(vm.id):
1626+
self.__class__.dbConnection.execute(
1627+
"UPDATE volumes SET min_iops = 0 WHERE uuid = %s",
1628+
(volume.id,),
1629+
)
15621630
if stop:
15631631
self._stop_vm(vm)
15641632
return self._get_vm(vm.id)
15651633

1634+
def _place_stopped_qos_vm(self, pool, root_iops, data_iops):
1635+
"""Put a new stopped QoS VM on one pool, then return it."""
1636+
vm = self._prepare_qos_vm(root_iops, data_iops, stop=True)
1637+
volumes = self._vm_volumes(vm.id)
1638+
self._migrate_stopped_vm_storage(vm, pool)
1639+
for volume in volumes:
1640+
self._poll_volume(volume.id, "storageid", pool.id, timeout=600)
1641+
return self._get_vm(vm.id)
1642+
15661643
def _attach_volume(self, vm, volume):
15671644
cmd = attachVolumeAPI.attachVolumeCmd()
15681645
cmd.id = volume.id
@@ -2469,7 +2546,9 @@ def tearDownClass(cls):
24692546
None,
24702547
)
24712548
if old_timeout is not None:
2472-
cls.apiClient.connection.asyncTimeout = 60
2549+
# Stop of an ONTAP-backed VM can take longer than a minute.
2550+
# A short timeout must not be what skips the expunge below.
2551+
cls.apiClient.connection.asyncTimeout = 300
24732552
try:
24742553
cls._expunge_account_vms()
24752554
except Exception as exc:
@@ -2567,17 +2646,27 @@ def _expunge_account_vms(cls):
25672646
)
25682647
except Exception as exc:
25692648
logger.warning("Could not list VMs for cleanup: %s", exc)
2570-
cleanup_states = ("stopped", "destroyed", "expunging", "error")
2571-
skip_stop_states = cleanup_states + ("starting", "migrating")
2649+
already_gone = ("destroyed", "expunging")
2650+
needs_stop = ("running", "starting", "stopping", "migrating")
25722651
for vm in vms:
25732652
try:
25742653
current = cls._list_vm_for_cleanup(vm.id)
2575-
if (current
2576-
and str(current.state).lower() not in skip_stop_states):
2577-
stop_cmd = stopVirtualMachineAPI.stopVirtualMachineCmd()
2578-
stop_cmd.id = vm.id
2579-
stop_cmd.forced = True
2580-
cls.apiClient.stopVirtualMachine(stop_cmd)
2654+
if current is None:
2655+
continue
2656+
state = str(current.state).lower()
2657+
if state in already_gone:
2658+
continue
2659+
if state in needs_stop:
2660+
try:
2661+
stop_cmd = stopVirtualMachineAPI.stopVirtualMachineCmd()
2662+
stop_cmd.id = vm.id
2663+
stop_cmd.forced = True
2664+
cls.apiClient.stopVirtualMachine(stop_cmd)
2665+
except Exception as exc:
2666+
logger.warning(
2667+
"Could not stop VM %s before expunge: %s",
2668+
vm.id, exc,
2669+
)
25812670
destroy_cmd = (
25822671
destroyVirtualMachineAPI.destroyVirtualMachineCmd()
25832672
)

‎test/integration/plugins/ontap/migration/test_01_live_vm_with_storage_migration.py‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
14 relocate one host once, then NFS3 cross-cluster C1/C2/Z cases
2525
18-21 iSCSI cross-cluster C1/C2/Z cases
2626
22 reject a cluster-1 pool mapped to a cluster-2 destination host
27-
23-26 live QoS migration for NFS3 and iSCSI, including two policies
28-
27 live NFS3 move where root and data keep different QoS policies
27+
23-27 put both hosts back in the primary cluster, then live QoS
2928
"""
3029

3130
import unittest
@@ -596,14 +595,25 @@ def test_22_reject_pool_not_reachable_from_destination_host(self):
596595
self._assert_host_snapshot_unchanged(host_snapshot, vm)
597596

598597
def _live_qos(self, protocol, root_iops, data_iops, second_pool=False):
599-
vm = self._prepare_qos_vm(root_iops, data_iops)
600-
pool = self._c1a(protocol)
598+
# test_14 leaves one host in each cluster. Live migration needs
599+
# two hosts in the primary cluster, same as test_01 through test_13.
600+
self.__class__._ensure_hosts_in_primary_cluster(2)
601+
self.__class__._refresh_host_inventory()
602+
vm = self._prepare_qos_vm(
603+
root_iops, data_iops,
604+
protocol=protocol if second_pool else None,
605+
)
606+
if second_pool:
607+
source = self._pool_by_id(self._vm_volumes(vm.id)[0].storageid)
608+
pools = (
609+
self._migration_pool(protocol, 0),
610+
self._migration_pool(protocol, 1),
611+
)
612+
pool = pools[1] if source.id == pools[0].id else pools[0]
613+
else:
614+
pool = self._c1a(protocol)
601615
vm, pool = self._live_migrate(vm, pool, protocol)
602616
self._assert_vm_qos(vm, protocol, pool, root_iops, data_iops)
603-
if second_pool:
604-
pool = self._c1b(protocol)
605-
vm, pool = self._live_migrate(vm, pool, protocol)
606-
self._assert_vm_qos(vm, protocol, pool, root_iops, data_iops)
607617

608618
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
609619
def test_23_nfs3_qos_default_primary_to_cluster(self):

‎test/integration/plugins/ontap/migration/test_02_stopped_vm_storage_migration.py‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -318,28 +318,27 @@ def test_15_reject_running_vm(self):
318318
self.assertEqual(self._get_vm(vm.id).state, "Running")
319319

320320
def _stopped_qos(self, protocol, root_iops, data_iops, second_pool=False):
321-
vm = self._prepare_qos_vm(root_iops, data_iops, stop=True)
321+
vm = self._prepare_qos_vm(
322+
root_iops, data_iops, stop=True,
323+
protocol=protocol if second_pool else None,
324+
)
322325
volumes = self._vm_volumes(vm.id)
323326
source = self._pool_by_id(volumes[0].storageid)
324-
pool = self._migration_pool(protocol)
327+
if second_pool:
328+
pools = (
329+
self._migration_pool(protocol, 0),
330+
self._migration_pool(protocol, 1),
331+
)
332+
pool = pools[1] if source.id == pools[0].id else pools[0]
333+
else:
334+
pool = self._migration_pool(protocol)
325335
self._migrate_stopped_vm_storage(vm, pool)
326336
self._assert_migration_success(
327337
volumes, pool, protocol, vm=vm,
328338
expected_vm_state="Stopped", expected_vm_id=vm.id,
329339
source_pool=source,
330340
)
331341
self._assert_vm_qos(vm, protocol, pool, root_iops, data_iops)
332-
if not second_pool:
333-
return
334-
moved = self._vm_volumes(vm.id)
335-
pool = self._migration_pool(protocol, 1)
336-
self._migrate_stopped_vm_storage(vm, pool)
337-
self._assert_migration_success(
338-
moved, pool, protocol, vm=vm,
339-
expected_vm_state="Stopped", expected_vm_id=vm.id,
340-
source_pool=self._migration_pool(protocol),
341-
)
342-
self._assert_vm_qos(vm, protocol, pool, root_iops, data_iops)
343342

344343
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
345344
def test_16_nfs3_qos_default_primary_to_cluster(self):

‎test/integration/plugins/ontap/migration/test_03_volume_migration.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,11 @@ def _detached_qos(self, protocol, miniops, maxiops):
506506
)
507507

508508
def _attached_qos_leaves_root(self, protocol):
509-
vm = self._prepare_qos_vm((100, 500), (200, 800), stop=True)
510-
source = self._migration_pool(protocol)
511-
self._migrate_stopped_vm_storage(vm, source)
509+
vm = self._prepare_qos_vm(
510+
(100, 500), (200, 800), stop=True, protocol=protocol
511+
)
512512
root = self._root_volume(vm)
513+
source = self._pool_by_id(root.storageid)
513514
data = [
514515
volume for volume in self._vm_volumes(vm.id)
515516
if volume.id != root.id

0 commit comments

Comments
 (0)