Skip to content

Commit 82c59f6

Browse files
ghernadirp-
authored andcommitted
linstor: Use template's uuid if pool's downloadPath is null as resource-name (apache#11053)
Also added an integration test for templates from snapshots
1 parent 7bf536c commit 82c59f6

4 files changed

Lines changed: 233 additions & 5 deletions

File tree

plugins/storage/volume/linstor/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to Linstor CloudStack plugin will be documented in this file
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2025-07-01]
9+
10+
### Fixed
11+
12+
- Regression in 4.19.3 and 4.21.0 with templates from snapshots
13+
814
## [2025-05-07]
915

1016
### Added

plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ private static boolean isSystemTemplate(KVMPhysicalDisk disk) {
613613
try {
614614
templateProps.load(new FileInputStream(propFile.toFile()));
615615
String desc = templateProps.getProperty("description");
616-
if (desc.startsWith("SystemVM Template")) {
616+
if (desc != null && desc.startsWith("SystemVM Template")) {
617617
return true;
618618
}
619619
} catch (IOException e) {

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,14 @@
7373
import com.cloud.storage.StoragePool;
7474
import com.cloud.storage.VMTemplateStoragePoolVO;
7575
import com.cloud.storage.VMTemplateStorageResourceAssoc;
76+
import com.cloud.storage.VMTemplateVO;
7677
import com.cloud.storage.Volume;
7778
import com.cloud.storage.VolumeDetailVO;
7879
import com.cloud.storage.VolumeVO;
7980
import com.cloud.storage.dao.SnapshotDao;
8081
import com.cloud.storage.dao.SnapshotDetailsDao;
8182
import com.cloud.storage.dao.SnapshotDetailsVO;
83+
import com.cloud.storage.dao.VMTemplateDao;
8284
import com.cloud.storage.dao.VMTemplatePoolDao;
8385
import com.cloud.storage.dao.VolumeDao;
8486
import com.cloud.storage.dao.VolumeDetailsDao;
@@ -130,6 +132,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
130132
ConfigurationDao _configDao;
131133
@Inject
132134
private HostDao _hostDao;
135+
@Inject private VMTemplateDao _vmTemplateDao;
133136

134137
private long volumeStatsLastUpdate = 0L;
135138
private final Map<String, Pair<Long, Long>> volumeStats = new HashMap<>();
@@ -667,8 +670,15 @@ private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolV
667670
storagePoolVO.getId(), csCloneId, null);
668671

669672
if (tmplPoolRef != null) {
670-
final String templateRscName = LinstorUtil.RSC_PREFIX + tmplPoolRef.getLocalDownloadPath();
673+
final String templateRscName;
674+
if (tmplPoolRef.getLocalDownloadPath() == null) {
675+
VMTemplateVO vmTemplateVO = _vmTemplateDao.findById(tmplPoolRef.getTemplateId());
676+
templateRscName = LinstorUtil.RSC_PREFIX + vmTemplateVO.getUuid();
677+
} else {
678+
templateRscName = LinstorUtil.RSC_PREFIX + tmplPoolRef.getLocalDownloadPath();
679+
}
671680
final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid();
681+
672682
final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
673683

674684
try {

test/integration/plugins/linstor/test_linstor_volumes.py

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# Import Integration Libraries
2626
# base - contains all resources as entities and defines create, delete, list operations on them
2727
from marvin.lib.base import Account, DiskOffering, ServiceOffering, Snapshot, StoragePool, Template, User, \
28-
VirtualMachine, Volume
28+
VirtualMachine, VmSnapshot, Volume
2929

3030
# common - commonly used methods for all tests are listed here
3131
from marvin.lib.common import get_domain, get_template, get_zone, list_clusters, list_hosts, list_virtual_machines, \
@@ -887,8 +887,94 @@ def test_08_delete_volume_was_attached(self):
887887
"Check volume was deleted"
888888
)
889889

890+
@attr(tags=['basic'], required_hardware=False)
891+
def test_09_create_snapshot(self):
892+
"""Create snapshot of root disk"""
893+
self.virtual_machine.stop(self.apiClient)
894+
895+
volume = list_volumes(
896+
self.apiClient,
897+
virtualmachineid = self.virtual_machine.id,
898+
type = "ROOT",
899+
listall = True,
900+
)
901+
snapshot = Snapshot.create(
902+
self.apiClient,
903+
volume_id = volume[0].id,
904+
account=self.account.name,
905+
domainid=self.domain.id,
906+
)
907+
908+
self.assertIsNotNone(snapshot, "Could not create snapshot")
909+
910+
snapshot.delete(self.apiClient)
911+
912+
@attr(tags=['basic'], required_hardware=False)
913+
def test_10_create_template_from_snapshot(self):
914+
"""
915+
Create a template from a snapshot and start an instance from it
916+
"""
917+
self.virtual_machine.stop(self.apiClient)
918+
919+
volume = list_volumes(
920+
self.apiClient,
921+
virtualmachineid = self.virtual_machine.id,
922+
type = "ROOT",
923+
listall = True,
924+
)
925+
snapshot = Snapshot.create(
926+
self.apiClient,
927+
volume_id=volume[0].id,
928+
account=self.account.name,
929+
domainid=self.domain.id,
930+
)
931+
self.cleanup.append(snapshot)
932+
933+
self.assertIsNotNone(snapshot, "Could not create snapshot")
934+
935+
services = {
936+
"displaytext": "IntegrationTestTemplate",
937+
"name": "int-test-template",
938+
"ostypeid": self.template.ostypeid,
939+
"ispublic": "true"
940+
}
941+
942+
custom_template = Template.create_from_snapshot(
943+
self.apiClient,
944+
snapshot,
945+
services,
946+
)
947+
self.cleanup.append(custom_template)
948+
949+
# create VM from custom template
950+
test_virtual_machine = VirtualMachine.create(
951+
self.apiClient,
952+
self.testdata[TestData.virtualMachine2],
953+
accountid=self.account.name,
954+
zoneid=self.zone.id,
955+
serviceofferingid=self.compute_offering.id,
956+
templateid=custom_template.id,
957+
domainid=self.domain.id,
958+
startvm=False,
959+
mode='basic',
960+
)
961+
self.cleanup.append(test_virtual_machine)
962+
963+
TestLinstorVolumes._start_vm(test_virtual_machine)
964+
965+
test_virtual_machine.stop(self.apiClient)
966+
967+
test_virtual_machine.delete(self.apiClient, True)
968+
self.cleanup.remove(test_virtual_machine)
969+
970+
custom_template.delete(self.apiClient)
971+
self.cleanup.remove(custom_template)
972+
snapshot.delete(self.apiClient)
973+
self.cleanup.remove(snapshot)
974+
975+
890976
@attr(tags=['advanced', 'migration'], required_hardware=False)
891-
def test_09_migrate_volume_to_same_instance_pool(self):
977+
def test_11_migrate_volume_to_same_instance_pool(self):
892978
"""Migrate volume to the same instance pool"""
893979

894980
if not self.testdata[TestData.migrationTests]:
@@ -1020,7 +1106,7 @@ def test_09_migrate_volume_to_same_instance_pool(self):
10201106
test_virtual_machine.delete(self.apiClient, True)
10211107

10221108
@attr(tags=['advanced', 'migration'], required_hardware=False)
1023-
def test_10_migrate_volume_to_distinct_instance_pool(self):
1109+
def test_12_migrate_volume_to_distinct_instance_pool(self):
10241110
"""Migrate volume to distinct instance pool"""
10251111

10261112
if not self.testdata[TestData.migrationTests]:
@@ -1151,6 +1237,132 @@ def test_10_migrate_volume_to_distinct_instance_pool(self):
11511237

11521238
test_virtual_machine.delete(self.apiClient, True)
11531239

1240+
@attr(tags=["basic"], required_hardware=False)
1241+
def test_13_create_vm_snapshots(self):
1242+
"""Test to create VM snapshots
1243+
"""
1244+
vm = TestLinstorVolumes._start_vm(self.virtual_machine)
1245+
1246+
try:
1247+
# Login to VM and write data to file system
1248+
self.debug("virt: {}".format(vm))
1249+
ssh_client = self.virtual_machine.get_ssh_client(vm.ipaddress, retries=5)
1250+
ssh_client.execute("echo 'hello world' > testfile")
1251+
ssh_client.execute("sync")
1252+
except Exception as exc:
1253+
self.fail("SSH failed for Virtual machine {}: {}".format(self.virtual_machine.ssh_ip, exc))
1254+
1255+
time.sleep(10)
1256+
memory_snapshot = False
1257+
vm_snapshot = VmSnapshot.create(
1258+
self.apiClient,
1259+
self.virtual_machine.id,
1260+
memory_snapshot,
1261+
"VMSnapshot1",
1262+
"test snapshot"
1263+
)
1264+
self.assertEqual(
1265+
vm_snapshot.state,
1266+
"Ready",
1267+
"Check the snapshot of vm is ready!"
1268+
)
1269+
1270+
@attr(tags=["basic"], required_hardware=False)
1271+
def test_14_revert_vm_snapshots(self):
1272+
"""Test to revert VM snapshots
1273+
"""
1274+
1275+
result = None
1276+
try:
1277+
ssh_client = self.virtual_machine.get_ssh_client(reconnect=True)
1278+
result = ssh_client.execute("rm -rf testfile")
1279+
except Exception as exc:
1280+
self.fail("SSH failed for Virtual machine %s: %s".format(self.virtual_machine.ipaddress, exc))
1281+
1282+
if result is not None and "No such file or directory" in str(result):
1283+
self.fail("testfile not deleted")
1284+
1285+
time.sleep(5)
1286+
1287+
list_snapshot_response = VmSnapshot.list(
1288+
self.apiClient,
1289+
virtualmachineid=self.virtual_machine.id,
1290+
listall=True)
1291+
1292+
self.assertEqual(
1293+
isinstance(list_snapshot_response, list),
1294+
True,
1295+
"Check list response returns a valid list"
1296+
)
1297+
self.assertNotEqual(
1298+
list_snapshot_response,
1299+
None,
1300+
"Check if snapshot exists in ListSnapshot"
1301+
)
1302+
1303+
self.assertEqual(
1304+
list_snapshot_response[0].state,
1305+
"Ready",
1306+
"Check the snapshot of vm is ready!"
1307+
)
1308+
1309+
self.virtual_machine.stop(self.apiClient, forced=True)
1310+
1311+
VmSnapshot.revertToSnapshot(
1312+
self.apiClient,
1313+
list_snapshot_response[0].id
1314+
)
1315+
1316+
TestLinstorVolumes._start_vm(self.virtual_machine)
1317+
1318+
try:
1319+
ssh_client = self.virtual_machine.get_ssh_client(reconnect=True)
1320+
1321+
result = ssh_client.execute("cat testfile")
1322+
1323+
except Exception as exc:
1324+
self.fail("SSH failed for Virtual machine {}: {}".format(self.virtual_machine.ipaddress, exc))
1325+
1326+
self.assertEqual(
1327+
"hello world",
1328+
result[0],
1329+
"Check the content is the same as originally written"
1330+
)
1331+
1332+
@attr(tags=["basic"], required_hardware=False)
1333+
def test_15_delete_vm_snapshots(self):
1334+
"""Test to delete vm snapshots
1335+
"""
1336+
1337+
list_snapshot_response = VmSnapshot.list(
1338+
self.apiClient,
1339+
virtualmachineid=self.virtual_machine.id,
1340+
listall=True)
1341+
1342+
self.assertEqual(
1343+
isinstance(list_snapshot_response, list),
1344+
True,
1345+
"Check list response returns a valid list"
1346+
)
1347+
self.assertNotEqual(
1348+
list_snapshot_response,
1349+
None,
1350+
"Check if snapshot exists in ListSnapshot"
1351+
)
1352+
VmSnapshot.deleteVMSnapshot(
1353+
self.apiClient,
1354+
list_snapshot_response[0].id)
1355+
1356+
time.sleep(5)
1357+
1358+
list_snapshot_response = VmSnapshot.list(
1359+
self.apiClient,
1360+
virtualmachineid=self.virtual_machine.id,
1361+
listall=False)
1362+
self.debug('list_snapshot_response -------------------- {}'.format(list_snapshot_response))
1363+
1364+
self.assertIsNone(list_snapshot_response, "snapshot is already deleted")
1365+
11541366
def _create_vm_using_template_and_destroy_vm(self, template):
11551367
vm_name = "VM-%d" % random.randint(0, 100)
11561368

0 commit comments

Comments
 (0)