Skip to content

Commit ab03d6a

Browse files
committed
KVM: allow cluster-wide storage pools for VM deployment from volumes
Allow both ZONE and CLUSTER scoped storage pools for VM deployment from existing volumes and snapshots. Previously only ZONE-scoped pools were accepted, restricting deployment flexibility for cluster-wide storage configurations like NFS and RBD pools. Update validation in UserVmManagerImpl to accept CLUSTER scope alongside ZONE scope, and update error messages accordingly. Update test_vm_lifecycle_with_snapshot_or_volume.py smoke tests to: - Accept both ZONE and CLUSTER scoped storage pools - Add RBD-specific handling for snapshot lifecycle (snapshots cannot outlive source volumes on RBD with snapshot.backup.to.secondary=false) - Skip unsupported test scenarios on RBD backends - Fix VM creation to use service configs instead of hardcoded values - Add reconnect logic to SSH client initialization - Remove unused uuid import
1 parent 17294ca commit ab03d6a

3 files changed

Lines changed: 57 additions & 28 deletions

File tree

‎server/src/main/java/com/cloud/vm/UserVmManagerImpl.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7011,8 +7011,9 @@ private VolumeInfo getVolume(long id, Long templateId, boolean isSnapshot) {
70117011
if (volume != null) {
70127012
if (!isSnapshot
70137013
&& (volume.getDataStore() == null
7014-
|| !ScopeType.ZONE.equals(volume.getDataStore().getScope().getScopeType()))) {
7015-
throw new InvalidParameterValueException("Deployment of virtual machine is supported only for Zone-wide storage pools");
7014+
|| (!ScopeType.ZONE.equals(volume.getDataStore().getScope().getScopeType())
7015+
&& !ScopeType.CLUSTER.equals(volume.getDataStore().getScope().getScopeType())))) {
7016+
throw new InvalidParameterValueException("Deployment of virtual machine is supported only for Zone-wide or Cluster-wide storage pools");
70167017
}
70177018
checkIfVolumeTemplateIsTheSameAsTheProvided(volume, templateId);
70187019
if (volume.getInstanceId() != null && !isSnapshot) {

‎server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4213,7 +4213,7 @@ public void createVirtualMachineWithVolumeFromNonZoneScopedStorageFails() {
42134213

42144214
InvalidParameterValueException ex = assertThrows(InvalidParameterValueException.class,
42154215
() -> userVmManagerImpl.createVirtualMachine(deployVMCmd));
4216-
assertEquals("Deployment of virtual machine is supported only for Zone-wide storage pools", ex.getMessage());
4216+
assertEquals("Deployment of virtual machine is supported only for Zone-wide or Cluster-wide storage pools", ex.getMessage());
42174217
}
42184218

42194219
@Test
@@ -4229,7 +4229,7 @@ public void createVirtualMachineWithVolumeWithNullDataStoreFails() {
42294229

42304230
InvalidParameterValueException ex = assertThrows(InvalidParameterValueException.class,
42314231
() -> userVmManagerImpl.createVirtualMachine(deployVMCmd));
4232-
assertEquals("Deployment of virtual machine is supported only for Zone-wide storage pools", ex.getMessage());
4232+
assertEquals("Deployment of virtual machine is supported only for Zone-wide or Cluster-wide storage pools", ex.getMessage());
42334233
}
42344234

42354235
@Test

‎test/integration/smoke/test_vm_lifecycle_with_snapshot_or_volume.py‎

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,19 @@
4040
from marvin.codes import FAILED, PASS
4141
from nose.plugins.attrib import attr
4242

43-
import uuid
4443
import unittest
4544

4645
class TestDeployVMFromSnapshotOrVolume(cloudstackTestCase):
4746

47+
def rbd_snapshot_cannot_outlive_source_volume(self):
48+
'''
49+
On RBD, a snapshot lives on primary storage with its parent
50+
volume, so with snapshot.backup.to.secondary=false, deleting the
51+
parent volume destroys the snapshot too - it can no longer be
52+
used to deploy a new volume/VM afterward.
53+
'''
54+
return self.storage_pool.type == 'RBD'
55+
4856
@classmethod
4957
def setUpClass(cls):
5058
testClient = super(TestDeployVMFromSnapshotOrVolume, cls).getClsTestClient()
@@ -80,23 +88,33 @@ def setUpClass(cls):
8088
cls._cleanup.append(cls.account)
8189
cls.debug(cls.account.id)
8290

91+
# Prefer a zone-wide pool, but also accept a cluster-scope NFS/RBD pool.
8392
storage_pools_response = list_storage_pools(cls.apiclient,
8493
zoneid=cls.zone.id,
8594
scope="ZONE")
8695

96+
if not storage_pools_response:
97+
cluster_storage_pools = list_storage_pools(cls.apiclient,
98+
zoneid=cls.zone.id,
99+
scope="CLUSTER") or []
100+
storage_pools_response = [
101+
pool for pool in cluster_storage_pools
102+
if getattr(pool, "type", None) in ("NetworkFilesystem", "RBD")
103+
]
104+
87105
if storage_pools_response:
88-
cls.zone_wide_storage = storage_pools_response[0]
106+
cls.storage_pool = storage_pools_response[0]
89107

90108
cls.debug(
91-
"zone wide storage id is %s" %
92-
cls.zone_wide_storage.id)
109+
"storage pool id is %s, scope is %s" %
110+
(cls.storage_pool.id, cls.storage_pool.scope))
93111
update1 = StoragePool.update(cls.apiclient,
94-
id=cls.zone_wide_storage.id,
112+
id=cls.storage_pool.id,
95113
tags="test-vm"
96114
)
97115
cls.debug(
98116
"Storage %s pool tag%s" %
99-
(cls.zone_wide_storage.id, update1.tags))
117+
(cls.storage_pool.id, update1.tags))
100118
cls.service_offering = ServiceOffering.create(
101119
cls.apiclient,
102120
cls.services["service_offerings"]["small"],
@@ -116,7 +134,7 @@ def setUpClass(cls):
116134
)
117135
cls._cleanup.append(cls.disk_offering)
118136
else:
119-
raise unittest.SkipTest("No zone wide storage found. Skipping tests")
137+
raise unittest.SkipTest("No zone-wide or cluster-scope NFS/RBD storage pool found. Skipping tests")
120138

121139

122140
cls.virtual_machine = VirtualMachine.create(
@@ -177,16 +195,16 @@ def test_03_deploy_vm_with_existing_volume_deleted_template(self):
177195
template = Template.create_from_snapshot(self.apiclient, self.snapshot, services)
178196
self._cleanup.append(template)
179197
virtual_machine = VirtualMachine.create(self.apiclient,
180-
{"name": "Test-%s" % uuid.uuid4()},
198+
self.services['small'],
181199
accountid=self.account.name,
182200
domainid=self.account.domainid,
183201
zoneid=self.zone.id,
184202
serviceofferingid=self.service_offering.id,
185203
templateid=template.id,
186-
mode="basic",
204+
mode=self.services['mode'],
187205
)
188206
try:
189-
ssh_client = virtual_machine.get_ssh_client()
207+
ssh_client = virtual_machine.get_ssh_client(reconnect=True)
190208
except Exception as e:
191209
self.fail("SSH failed for virtual machine: %s - %s" %
192210
(virtual_machine.ipaddress, e))
@@ -202,29 +220,35 @@ def test_03_deploy_vm_with_existing_volume_deleted_template(self):
202220
root_volume.id,
203221
)
204222
VirtualMachine.delete(virtual_machine, self.apiclient, expunge=True)
205-
self.create_volume_from_snapshot_deploy_vm(snapshot.id)
223+
if not self.rbd_snapshot_cannot_outlive_source_volume():
224+
self.create_volume_from_snapshot_deploy_vm(snapshot.id)
206225

207226
@attr(tags=["advanced"], required_hardware="false")
208227
def test_04_deploy_vm_with_existing_snapshot_deleted_template(self):
209228
'''
210229
Deploy a Virtual machine with existing snapshot of a ROOT volume created from a templated which was deleted
211230
'''
231+
if self.rbd_snapshot_cannot_outlive_source_volume():
232+
raise unittest.SkipTest(
233+
"Covered by test_03 on RBD: deploying from a snapshot after "
234+
"its source ROOT volume was deleted is not supported here."
235+
)
212236
services = {"displaytext": "Template-1", "name": "Template-1-name", "ostypeid": self.template.ostypeid,
213237
"ispublic": "true"}
214238

215239
template = Template.create_from_snapshot(self.apiclient, self.snapshot, services)
216240
self._cleanup.append(template)
217241
virtual_machine = VirtualMachine.create(self.apiclient,
218-
{"name": "Test-%s" % uuid.uuid4()},
242+
self.services['small'],
219243
accountid=self.account.name,
220244
domainid=self.account.domainid,
221245
zoneid=self.zone.id,
222246
serviceofferingid=self.service_offering.id,
223247
templateid=template.id,
224-
mode="basic",
248+
mode=self.services['mode'],
225249
)
226250
try:
227-
ssh_client = virtual_machine.get_ssh_client()
251+
ssh_client = virtual_machine.get_ssh_client(reconnect=True)
228252
except Exception as e:
229253
self.fail("SSH failed for virtual machine: %s - %s" %
230254
(virtual_machine.ipaddress, e))
@@ -247,18 +271,22 @@ def test_05_deploy_vm_with_existing_snapshot_deleted_volume(self):
247271
'''
248272
Deploy a Virtual machine with existing snapshot of a ROOT volume which was deleted
249273
'''
250-
274+
if self.rbd_snapshot_cannot_outlive_source_volume():
275+
raise unittest.SkipTest(
276+
"Covered by test_03 on RBD: deploying from a snapshot after "
277+
"its source ROOT volume was deleted is not supported here."
278+
)
251279
virtual_machine = VirtualMachine.create(self.apiclient,
252-
{"name": "Test-%s" % uuid.uuid4()},
280+
self.services['small'],
253281
accountid=self.account.name,
254282
domainid=self.account.domainid,
255283
zoneid=self.zone.id,
256284
serviceofferingid=self.service_offering.id,
257285
templateid=self.template.id,
258-
mode="basic",
286+
mode=self.services['mode'],
259287
)
260288
try:
261-
ssh_client = virtual_machine.get_ssh_client()
289+
ssh_client = virtual_machine.get_ssh_client(reconnect=True)
262290
except Exception as e:
263291
self.fail("SSH failed for virtual machine: %s - %s" %
264292
(virtual_machine.ipaddress, e))
@@ -278,16 +306,16 @@ def test_05_deploy_vm_with_existing_snapshot_deleted_volume(self):
278306

279307
def deploy_vm_from_snapshot(self, snapshot):
280308
virtual_machine = VirtualMachine.create(self.apiclient,
281-
{"name": "Test-%s" % uuid.uuid4()},
309+
self.services['small'],
282310
accountid=self.account.name,
283311
domainid=self.account.domainid,
284312
zoneid=self.zone.id,
285313
serviceofferingid=self.service_offering.id,
286314
snapshotid=snapshot.id,
287-
mode="basic",
315+
mode=self.services['mode'],
288316
)
289317
try:
290-
ssh_client = virtual_machine.get_ssh_client()
318+
ssh_client = virtual_machine.get_ssh_client(reconnect=True)
291319
except Exception as e:
292320
self.fail("SSH failed for virtual machine: %s - %s" %
293321
(virtual_machine.ipaddress, e))
@@ -303,16 +331,16 @@ def create_volume_from_snapshot_deploy_vm(self, snapshotid):
303331
zoneid=self.zone.id,
304332
)
305333
virtual_machine = VirtualMachine.create(self.apiclient,
306-
{"name": "Test-%s" % uuid.uuid4()},
334+
self.services['small'],
307335
accountid=self.account.name,
308336
domainid=self.account.domainid,
309337
zoneid=self.zone.id,
310338
serviceofferingid=self.service_offering.id,
311339
volumeid=volume.id,
312-
mode="basic",
340+
mode=self.services['mode'],
313341
)
314342
try:
315-
ssh_client = virtual_machine.get_ssh_client()
343+
ssh_client = virtual_machine.get_ssh_client(reconnect=True)
316344
except Exception as e:
317345
self.fail("SSH failed for virtual machine: %s - %s" %
318346
(virtual_machine.ipaddress, e))

0 commit comments

Comments
 (0)