Skip to content

Commit 6c52216

Browse files
Add QoS cases to the three ONTAP migration suites.
Disks created with min and max IOPS should keep those policies after live, stopped, and offline moves.
1 parent a2cc545 commit 6c52216

5 files changed

Lines changed: 275 additions & 5 deletions

File tree

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

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,28 @@ def _root_disk_offering(cls, storage_tag):
12451245
cls._cleanup.insert(0, offering)
12461246
return offering
12471247

1248+
def _qos_disk_offering(self, disksize, miniops, maxiops, storage_tag=None):
1249+
"""Create a disk offering that carries fixed min and max IOPS."""
1250+
data = {
1251+
"name": "ontap-migration-qos-%d-%d-%d" % (
1252+
miniops, maxiops, random.randint(0, 999999)
1253+
),
1254+
"displaytext": "ONTAP migration QoS disk",
1255+
"disksize": disksize,
1256+
"miniops": miniops,
1257+
"maxiops": maxiops,
1258+
}
1259+
if storage_tag:
1260+
data["tags"] = storage_tag
1261+
offering = DiskOffering.create(self.apiClient, data)
1262+
self.__class__._cleanup.insert(0, offering)
1263+
return offering
1264+
1265+
def _qos_policy_name(self, miniops, maxiops):
1266+
"""Return the ONTAP policy name the driver builds for these IOPS."""
1267+
svm = str(self.__class__.svm_name).replace(".", "_")
1268+
return "cs_%d_to_%d_iops_%s" % (miniops, maxiops, svm)
1269+
12481270
@classmethod
12491271
def _create_ontap_pool(cls, protocol, scope, cluster_id=None):
12501272
config = cls.config_data
@@ -1324,9 +1346,11 @@ def _create_ontap_pool(cls, protocol, scope, cluster_id=None):
13241346
)
13251347
return pool
13261348

1327-
def _deploy_vm(self, storage_tag=None, host_id=None):
1349+
def _deploy_vm(self, storage_tag=None, host_id=None, root_offering=None):
13281350
offering = self._service_offering()
1329-
root_disk_offering = self._root_disk_offering(storage_tag)
1351+
root_disk_offering = root_offering or self._root_disk_offering(
1352+
storage_tag
1353+
)
13301354
cmd = deployVirtualMachineAPI.deployVirtualMachineCmd()
13311355
cmd.zoneid = self.__class__.zone.id
13321356
cmd.templateid = self.__class__.template_id
@@ -1453,9 +1477,12 @@ def _require_secondary_cluster(cls):
14531477
)
14541478
return cls.secondary_cluster_id
14551479

1456-
def _create_data_volume(self, pool):
1480+
def _create_data_volume(self, pool, offering=None):
14571481
cls = self.__class__
1458-
offering_id = getattr(cls, "small_disk_offering_id", None)
1482+
if offering is not None:
1483+
offering_id = offering.id
1484+
else:
1485+
offering_id = getattr(cls, "small_disk_offering_id", None)
14591486
if offering_id is None:
14601487
offering = DiskOffering.create(cls.apiClient, {
14611488
"name": "ontap-migration-data-%d" % random.randint(0, 999999),
@@ -1475,6 +1502,67 @@ def _create_data_volume(self, pool):
14751502
cls.created_volumes.append(volume)
14761503
return volume
14771504

1505+
def _assert_volume_qos(self, volume, protocol, pool, miniops, maxiops):
1506+
"""Check CloudStack IOPS and the destination ONTAP QoS policy."""
1507+
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)
1511+
if protocol.upper() == "NFS3":
1512+
record = self.__class__.ontap.get_nfs_file(
1513+
pool.name, current.path
1514+
)
1515+
else:
1516+
record = self.__class__.ontap.get_lun(
1517+
self.__class__.svm_name,
1518+
self._backend_name("ISCSI", pool, current),
1519+
)
1520+
policy = (record or {}).get("qos_policy") or {}
1521+
self.assertEqual(
1522+
policy.get("name"),
1523+
expected,
1524+
"Volume %s on %s has QoS %s, expected %s"
1525+
% (current.id, pool.name, policy.get("name"), expected),
1526+
)
1527+
return current
1528+
1529+
def _assert_vm_qos(self, vm, protocol, pool, root_iops, data_iops):
1530+
"""Check root and data disks after a VM storage migration."""
1531+
volumes = self._vm_volumes(vm.id)
1532+
roots = [
1533+
volume for volume in volumes
1534+
if str(getattr(volume, "type", "")).upper() == "ROOT"
1535+
]
1536+
data = [
1537+
volume for volume in volumes
1538+
if str(getattr(volume, "type", "")).upper() != "ROOT"
1539+
]
1540+
self.assertEqual(len(roots), 1)
1541+
self.assertTrue(data)
1542+
self._assert_volume_qos(
1543+
roots[0], protocol, pool, root_iops[0], root_iops[1]
1544+
)
1545+
for volume in data:
1546+
self._assert_volume_qos(
1547+
volume, protocol, pool, data_iops[0], data_iops[1]
1548+
)
1549+
1550+
def _prepare_qos_vm(self, root_iops, data_iops, stop=False):
1551+
"""Deploy a DefaultPrimary VM whose root and data disks have IOPS."""
1552+
host = self._host_for_cluster(self.__class__.cluster.id)
1553+
root = self._qos_disk_offering(
1554+
8, root_iops[0], root_iops[1], self._default_storage_tag()
1555+
)
1556+
vm = self._deploy_vm(host_id=host.id, root_offering=root)
1557+
data = self._create_data_volume(
1558+
self.__class__.default_pool,
1559+
self._qos_disk_offering(1, data_iops[0], data_iops[1]),
1560+
)
1561+
self._attach_volume(vm, data)
1562+
if stop:
1563+
self._stop_vm(vm)
1564+
return self._get_vm(vm.id)
1565+
14781566
def _attach_volume(self, vm, volume):
14791567
cmd = attachVolumeAPI.attachVolumeCmd()
14801568
cmd.id = volume.id

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
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
2729
"""
2830

2931
import unittest
@@ -592,3 +594,38 @@ def test_22_reject_pool_not_reachable_from_destination_host(self):
592594
destination_pool=dest,
593595
)
594596
self._assert_host_snapshot_unchanged(host_snapshot, vm)
597+
598+
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)
601+
vm, pool = self._live_migrate(vm, pool, protocol)
602+
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)
607+
608+
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
609+
def test_23_nfs3_qos_default_primary_to_cluster(self):
610+
"""Live-migrate QoS disks from DefaultPrimary onto NFS3."""
611+
self._live_qos("NFS3", (100, 500), (100, 500))
612+
613+
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
614+
def test_24_nfs3_qos_cluster_to_cluster(self):
615+
"""Live-migrate QoS disks between two NFS3 cluster pools."""
616+
self._live_qos("NFS3", (100, 500), (100, 500), second_pool=True)
617+
618+
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
619+
def test_25_iscsi_qos_default_primary_to_cluster(self):
620+
"""Live-migrate QoS disks from DefaultPrimary onto iSCSI."""
621+
self._live_qos("ISCSI", (100, 500), (100, 500))
622+
623+
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
624+
def test_26_iscsi_qos_cluster_to_cluster(self):
625+
"""Live-migrate QoS disks between two iSCSI cluster pools."""
626+
self._live_qos("ISCSI", (100, 500), (100, 500), second_pool=True)
627+
628+
@attr(tags=["ontap_migration", "live_storage"], required_hardware=True)
629+
def test_27_nfs3_qos_root_and_data_different_policies(self):
630+
"""Live NFS3 move keeps a different QoS policy on each disk."""
631+
self._live_qos("NFS3", (100, 500), (200, 800))

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
13 reject NFS3-to-iSCSI storage migration
4141
14 reject iSCSI-to-NFS3 storage migration
4242
15 reject the stopped-VM API form once the VM is running
43+
16-19 stopped QoS migration for NFS3 and iSCSI
44+
20 stopped iSCSI move where root and data keep different QoS policies
4345
4446
Migration out of ONTAP into non-ONTAP storage is never requested; the
4547
DefaultPrimary pool is only ever a source.
@@ -314,3 +316,52 @@ def test_15_reject_running_vm(self):
314316
"VM is not Stopped",
315317
)
316318
self.assertEqual(self._get_vm(vm.id).state, "Running")
319+
320+
def _stopped_qos(self, protocol, root_iops, data_iops, second_pool=False):
321+
vm = self._prepare_qos_vm(root_iops, data_iops, stop=True)
322+
volumes = self._vm_volumes(vm.id)
323+
source = self._pool_by_id(volumes[0].storageid)
324+
pool = self._migration_pool(protocol)
325+
self._migrate_stopped_vm_storage(vm, pool)
326+
self._assert_migration_success(
327+
volumes, pool, protocol, vm=vm,
328+
expected_vm_state="Stopped", expected_vm_id=vm.id,
329+
source_pool=source,
330+
)
331+
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)
343+
344+
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
345+
def test_16_nfs3_qos_default_primary_to_cluster(self):
346+
"""Move stopped QoS disks from DefaultPrimary onto NFS3."""
347+
self._stopped_qos("NFS3", (100, 500), (100, 500))
348+
349+
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
350+
def test_17_nfs3_qos_cluster_to_cluster(self):
351+
"""Move stopped QoS disks between two NFS3 cluster pools."""
352+
self._stopped_qos("NFS3", (100, 500), (100, 500), second_pool=True)
353+
354+
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
355+
def test_18_iscsi_qos_default_primary_to_cluster(self):
356+
"""Move stopped QoS disks from DefaultPrimary onto iSCSI."""
357+
self._stopped_qos("ISCSI", (100, 500), (100, 500))
358+
359+
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
360+
def test_19_iscsi_qos_cluster_to_cluster(self):
361+
"""Move stopped QoS disks between two iSCSI cluster pools."""
362+
self._stopped_qos("ISCSI", (100, 500), (100, 500), second_pool=True)
363+
364+
@attr(tags=["ontap_migration", "stopped_vm"], required_hardware=True)
365+
def test_20_iscsi_qos_root_and_data_different_policies(self):
366+
"""Stopped iSCSI move keeps a different QoS policy on each disk."""
367+
self._stopped_qos("ISCSI", (100, 500), (200, 800))

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
21 reject running-attached migration without livemigrate
2929
22 reject running-attached migration with livemigrate
3030
23 reject migration to the volume's current pool
31+
24-25 migrate a detached QoS volume between cluster pools
32+
26-27 migrate one attached QoS disk and leave the root disk
33+
28 migrate a detached QoS volume from DefaultPrimary onto NFS3
3134
3235
Every successful migration verifies CloudStack identity and attachment state,
3336
the destination ONTAP object and access state, and source cleanup when the
@@ -488,3 +491,76 @@ def test_23_reject_same_pool(self):
488491
False,
489492
"already on the destination storage pool",
490493
)
494+
495+
def _detached_qos(self, protocol, miniops, maxiops):
496+
source = self._migration_pool(protocol)
497+
destination = self._migration_pool(protocol, 1)
498+
offering = self._qos_disk_offering(1, miniops, maxiops)
499+
volume = self._create_data_volume(source, offering)
500+
self._migrate_volume_offline(volume, destination)
501+
self._assert_migration_success(
502+
[volume], destination, protocol, source_pool=source
503+
)
504+
self._assert_volume_qos(
505+
volume, protocol, destination, miniops, maxiops
506+
)
507+
508+
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)
512+
root = self._root_volume(vm)
513+
data = [
514+
volume for volume in self._vm_volumes(vm.id)
515+
if volume.id != root.id
516+
][0]
517+
destination = self._migration_pool(protocol, scope="ZONE")
518+
self._migrate_volume_offline(data, destination)
519+
self._assert_migration_success(
520+
[data], destination, protocol, vm=vm,
521+
expected_vm_state="Stopped", expected_vm_id=vm.id,
522+
source_pool=source,
523+
)
524+
self._assert_volume_qos(data, protocol, destination, 200, 800)
525+
self.assertEqual(self._get_volume(root.id).storageid, source.id)
526+
self._assert_volume_qos(root, protocol, source, 100, 500)
527+
528+
@attr(tags=["ontap_migration", "volume_migration"],
529+
required_hardware=True)
530+
def test_24_nfs3_qos_detached_cluster_to_cluster(self):
531+
"""Migrate a detached NFS3 volume that was created with QoS."""
532+
self._detached_qos("NFS3", 100, 500)
533+
534+
@attr(tags=["ontap_migration", "volume_migration"],
535+
required_hardware=True)
536+
def test_25_iscsi_qos_detached_cluster_to_cluster(self):
537+
"""Migrate a detached iSCSI volume that was created with QoS."""
538+
self._detached_qos("ISCSI", 100, 500)
539+
540+
@attr(tags=["ontap_migration", "volume_migration"],
541+
required_hardware=True)
542+
def test_26_nfs3_qos_attached_stopped_vm(self):
543+
"""Migrate one stopped NFS3 data disk and leave the root disk."""
544+
self._attached_qos_leaves_root("NFS3")
545+
546+
@attr(tags=["ontap_migration", "volume_migration"],
547+
required_hardware=True)
548+
def test_27_iscsi_qos_attached_stopped_vm(self):
549+
"""Migrate one stopped iSCSI data disk and leave the root disk."""
550+
self._attached_qos_leaves_root("ISCSI")
551+
552+
@attr(tags=["ontap_migration", "volume_migration"],
553+
required_hardware=True)
554+
def test_28_nfs3_qos_detached_default_primary_to_ontap(self):
555+
"""Migrate a QoS volume from DefaultPrimary onto NFS3."""
556+
offering = self._qos_disk_offering(1, 100, 500)
557+
volume = self._create_data_volume(
558+
self.__class__.default_pool, offering
559+
)
560+
destination = self._migration_pool("NFS3")
561+
self._migrate_volume_offline(volume, destination)
562+
self._assert_migration_success(
563+
[volume], destination, "NFS3",
564+
source_pool=self.__class__.default_pool,
565+
)
566+
self._assert_volume_qos(volume, "NFS3", destination, 100, 500)

‎test/integration/plugins/ontap/ontap_test_base.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ def get_lun(self, svm_name, lun_path):
624624
"""Return the ONTAP LUN record for the given full path, or None."""
625625
data = self._get("/storage/luns",
626626
params={"svm.name": svm_name, "name": lun_path,
627-
"fields": "name,uuid,serial_number,enabled,status,space.size"})
627+
"fields": "name,uuid,serial_number,qos_policy,enabled,status,space.size"})
628628
records = data.get("records", [])
629629
return records[0] if records else None
630630

@@ -679,6 +679,24 @@ def list_files_in_volume(self, vol_name, path="/"):
679679
return [r.get("name", "") for r in resp.get("records", [])
680680
if r.get("name") not in (".", "..")]
681681

682+
def get_nfs_file(self, vol_name, file_name):
683+
"""Return one file record, including its QoS policy, or None."""
684+
vol = self.get_volume(vol_name)
685+
if not vol or not vol.get("uuid"):
686+
return None
687+
from urllib.parse import quote
688+
encoded_path = quote("/" + str(file_name), safe="")
689+
try:
690+
resp = self._get(
691+
"/storage/volumes/%s/files/%s" % (vol["uuid"], encoded_path),
692+
params={"fields": "name,qos_policy"},
693+
)
694+
except Exception:
695+
return None
696+
if isinstance(resp, dict) and resp.get("records"):
697+
return resp["records"][0]
698+
return resp or None
699+
682700
def _unmap_lun(self, svm_name, lun_uuid):
683701
"""Best-effort removal of all lun-maps for a LUN UUID."""
684702
try:

0 commit comments

Comments
 (0)