Skip to content

Commit 8306d7b

Browse files
committed
smoke tests
1 parent f4cf4f9 commit 8306d7b

3 files changed

Lines changed: 366 additions & 1 deletion

File tree

plugins/storage/image/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackImageStoreDriverImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ private String createObjectNameForExtractUrl(String installPath, ImageFormat for
7474
}
7575

7676
if (format != null) {
77-
if (dataObject.getTO() != null
77+
if (dataObject != null && dataObject.getTO() != null
7878
&& DataObjectType.VOLUME.equals(dataObject.getTO().getObjectType())
7979
&& HypervisorType.KVM.equals(dataObject.getTO().getHypervisorType())) {
8080
// Fix: The format of KVM volumes on image store is qcow2
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#!/usr/bin/env python
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
from marvin.cloudstackTestCase import cloudstackTestCase
20+
from marvin.lib import utils
21+
from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering,
22+
Backup, Configurations, Volume, StoragePool)
23+
from marvin.lib.common import (get_domain, get_zone, get_template)
24+
from nose.plugins.attrib import attr
25+
from marvin.codes import FAILED
26+
import time
27+
import tempfile
28+
import urllib.parse
29+
import urllib.request
30+
import os
31+
32+
class TestKBOSSBackupAndRecovery(cloudstackTestCase):
33+
34+
@classmethod
35+
def setUpClass(cls):
36+
# Setup
37+
38+
cls.testClient = super(TestKBOSSBackupAndRecovery, cls).getClsTestClient()
39+
cls.api_client = cls.testClient.getApiClient()
40+
print(cls.api_client)
41+
cls.services = cls.testClient.getParsedTestDataConfig()
42+
cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
43+
cls.services["mode"] = cls.zone.networktype
44+
cls.hypervisor = cls.testClient.getHypervisorInfo()
45+
cls.domain = get_domain(cls.api_client)
46+
cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"])
47+
if cls.template == FAILED:
48+
assert False, "get_template() failed to return template with description %s" % cls.services["ostype"]
49+
cls.services["small"]["zoneid"] = cls.zone.id
50+
cls.services["small"]["template"] = cls.template.id
51+
cls._cleanup = []
52+
53+
if cls.hypervisor.lower() != 'kvm':
54+
cls.skipTest(cls, reason="Test can be run only on KVM hypervisor")
55+
56+
cls.storage_pool = StoragePool.list(cls.api_client)[0]
57+
if cls.storage_pool.type.lower() != 'networkfilesystem':
58+
cls.skipTest(cls, reason="Test can be run only if the primary storage is of type NFS")
59+
60+
# Check backup configuration values, set them to enable the kboss provider
61+
backup_enabled_cfg = Configurations.list(cls.api_client, name='backup.framework.enabled')
62+
backup_provider_cfg = Configurations.list(cls.api_client, name='backup.framework.provider.plugin')
63+
cls.backup_enabled = backup_enabled_cfg[0].value
64+
cls.backup_provider = backup_provider_cfg[0].value
65+
66+
if cls.backup_enabled == "false":
67+
cls.skipTest(cls, reason="Test can be run only if the config backup.framework.enabled is true")
68+
if cls.backup_provider != "kboss":
69+
Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value='kboss')
70+
71+
cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id)
72+
73+
cls._cleanup = [cls.account]
74+
75+
cls.basic_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id)
76+
cls._cleanup.append(cls.basic_backup_offering)
77+
cls.compress_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id, compress=True)
78+
cls._cleanup.append(cls.compress_backup_offering)
79+
cls.validate_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id, validate=True,
80+
validationsteps="screenshot")
81+
cls._cleanup.append(cls.validate_backup_offering)
82+
83+
cls.offering = ServiceOffering.create(cls.api_client,cls.services["service_offerings"]["small"])
84+
cls.diskoffering = DiskOffering.create(cls.api_client, cls.services["disk_offering"])
85+
cls._cleanup.extend([cls.offering, cls.diskoffering])
86+
cls.vm = VirtualMachine.create(cls.api_client, cls.services["small"], accountid=cls.account.name,
87+
domainid=cls.account.domainid, serviceofferingid=cls.offering.id,
88+
diskofferingid=cls.diskoffering.id, mode=cls.services["mode"])
89+
90+
91+
@classmethod
92+
def tearDownClass(cls):
93+
try:
94+
# Cleanup resources used
95+
utils.cleanup_resources(cls.api_client, cls._cleanup)
96+
97+
if cls.backup_provider != "kboss":
98+
Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value=cls.backup_provider)
99+
except Exception as e:
100+
raise Exception("Warning: Exception during cleanup : %s" % e)
101+
102+
def setUp(self):
103+
if self.hypervisor.lower() != 'kvm':
104+
raise self.skipTest("Skipping test cases which must only run for Simulator")
105+
self.cleanup = []
106+
107+
def tearDown(self):
108+
try:
109+
utils.cleanup_resources(self.api_client, self.cleanup)
110+
except Exception as e:
111+
raise Exception("Warning: Exception during cleanup : %s" % e)
112+
113+
def waitForCompression(self, vm):
114+
def checkBackupCompression():
115+
backups = Backup.list(self.api_client, vm.id)
116+
if isinstance(backups, list) and len(backups) != 0 and backups[0].compressionstatus == "Compressed":
117+
return True, None
118+
return False, None
119+
120+
res, _ = utils.wait_until(10, 60, checkBackupCompression)
121+
if not res:
122+
self.fail("Failed to wait for backup compression of VM %s" % vm.id)
123+
124+
def waitForValidation(self, vm):
125+
def checkBackupValidation():
126+
backups = Backup.list(self.api_client, vm.id)
127+
if isinstance(backups, list) and len(backups) != 0 and backups[0].validationstatus == "Valid":
128+
return True, None
129+
return False, None
130+
131+
res, _ = utils.wait_until(10, 60, checkBackupValidation)
132+
if not res:
133+
self.fail("Failed to wait for backup compression of VM %s" % vm.id)
134+
135+
def download_screenshot(self, backupid):
136+
extract_ss = Backup.downloadValidationScreenshot(self.api_client, backupid)
137+
138+
try:
139+
formatted_url = urllib.parse.unquote_plus(extract_ss.url)
140+
self.debug("Attempting to download screenshot at url %s" % formatted_url)
141+
response = urllib.request.urlopen(formatted_url)
142+
self.debug("response from screenshot url %s" % response.getcode())
143+
fd, path = tempfile.mkstemp()
144+
self.debug("Saving screenshot %s to path %s" % (backupid, path))
145+
os.close(fd)
146+
with open(path, 'wb') as fd:
147+
fd.write(response.read())
148+
self.debug("Saved screenshot successfully")
149+
except Exception:
150+
self.fail(
151+
"Extract screenshot of backup Failed with invalid URL %s (backup id: %s)" \
152+
% (extract_ss.url, backupid)
153+
)
154+
155+
@attr(tags=["advanced", "backup"], required_hardware="true")
156+
def test_vm_backup_lifecycle(self):
157+
"""
158+
Test VM backup lifecycle
159+
"""
160+
161+
# Verify there are no backups for the VM
162+
backups = Backup.list(self.api_client, self.vm.id)
163+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
164+
165+
# Assign VM to offering and create ad-hoc backup
166+
self.basic_backup_offering.assignOffering(self.api_client, self.vm.id)
167+
Backup.create(self.api_client, self.vm.id)
168+
169+
# Verify backup is created for the VM
170+
backups = Backup.list(self.api_client, self.vm.id)
171+
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
172+
backup = backups[0]
173+
174+
# Delete backup
175+
Backup.delete(self.api_client, backup.id)
176+
177+
# Verify backup is deleted
178+
backups = Backup.list(self.api_client, self.vm.id)
179+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
180+
181+
# Remove VM from offering
182+
self.basic_backup_offering.removeOffering(self.api_client, self.vm.id)
183+
184+
@attr(tags=["advanced", "backup"], required_hardware="true")
185+
def test_vm_backup_lifecycle_with_compression(self):
186+
"""
187+
Test VM backup lifecycle with compression
188+
"""
189+
190+
compression_enabled = Configurations.list(self.api_client, name='backup.compression.task.enabled')
191+
if compression_enabled[0].value == "false":
192+
self.skipTest("Skipping test due to backup compression task is disabled.")
193+
194+
# Verify there are no backups for the VM
195+
backups = Backup.list(self.api_client, self.vm.id)
196+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
197+
198+
# Assign VM to offering and create ad-hoc backup
199+
self.compress_backup_offering.assignOffering(self.api_client, self.vm.id)
200+
Backup.create(self.api_client, self.vm.id)
201+
202+
# Verify backup is created for the VM
203+
backups = Backup.list(self.api_client, self.vm.id)
204+
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
205+
backup = backups[0]
206+
207+
self.waitForCompression(self.vm)
208+
209+
# Delete backup
210+
Backup.delete(self.api_client, backup.id)
211+
212+
# Verify backup is deleted
213+
backups = Backup.list(self.api_client, self.vm.id)
214+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
215+
216+
# Remove VM from offering
217+
self.compress_backup_offering.removeOffering(self.api_client, self.vm.id)
218+
219+
@attr(tags=["advanced", "backup"], required_hardware="true")
220+
def test_vm_backup_lifecycle_with_validation(self):
221+
"""
222+
Test VM backup lifecycle with validation
223+
"""
224+
225+
validation_enabled = Configurations.list(self.api_client, name='backup.validation.task.enabled')
226+
if validation_enabled[0].value == "false":
227+
self.skipTest("Skipping test due to backup compression task is disabled.")
228+
229+
# Verify there are no backups for the VM
230+
backups = Backup.list(self.api_client, self.vm.id)
231+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
232+
233+
# Assign VM to offering and create ad-hoc backup
234+
self.validate_backup_offering.assignOffering(self.api_client, self.vm.id)
235+
Backup.create(self.api_client, self.vm.id)
236+
237+
# Verify backup is created for the VM
238+
backups = Backup.list(self.api_client, self.vm.id)
239+
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
240+
backup = backups[0]
241+
242+
# Verify validation is performed
243+
self.waitForValidation(self.vm)
244+
self.download_screenshot(backup.id)
245+
246+
# Delete backup
247+
Backup.delete(self.api_client, backup.id)
248+
249+
# Verify backup is deleted
250+
backups = Backup.list(self.api_client, self.vm.id)
251+
self.assertEqual(backups, None, "There should not exist any backup for the VM")
252+
253+
# Remove VM from offering
254+
self.validate_backup_offering.removeOffering(self.api_client, self.vm.id)
255+
256+
@attr(tags=["advanced", "backup"], required_hardware="true")
257+
def test_vm_backup_create_vm_from_backup(self):
258+
"""
259+
Test creating a new VM from a backup
260+
"""
261+
self.basic_backup_offering.assignOffering(self.api_client, self.vm.id)
262+
263+
# Create a file and take backup
264+
try:
265+
ssh_client_vm = self.vm.get_ssh_client(reconnect=True)
266+
ssh_client_vm.execute("touch test_backup_and_recovery.txt")
267+
except Exception as err:
268+
self.fail("SSH failed for Virtual machine: %s due to %s" % (self.vm.ipaddress, err))
269+
270+
time.sleep(5)
271+
272+
Backup.create(self.api_client, self.vm.id, "backup1")
273+
Backup.create(self.api_client, self.vm.id, "backup2")
274+
275+
# Verify backup is created for the VM
276+
backups = Backup.list(self.api_client, self.vm.id)
277+
self.assertEqual(len(backups), 2, "There should exist two backups for the VM")
278+
279+
# Remove VM from offering
280+
self.basic_backup_offering.removeOffering(self.api_client, self.vm.id)
281+
282+
# Verify no. of backups after removing the backup offering
283+
backups = Backup.list(self.api_client, self.vm.id)
284+
self.assertEqual(len(backups), 2, "There should exist two backups for the VM")
285+
286+
# Create a new VM from first backup
287+
new_vm_name = "vm-from-backup1-" + str(int(time.time()))
288+
new_vm = Backup.createVMFromBackup(
289+
self.api_client,
290+
self.services["small"],
291+
mode=self.services["mode"],
292+
backupid=backups[0].id,
293+
vmname=new_vm_name,
294+
accountname=self.account.name,
295+
domainid=self.account.domainid,
296+
zoneid=self.zone.id,
297+
networkids=None,
298+
templateid=None
299+
)
300+
self.cleanup.append(new_vm)
301+
302+
# Verify the new VM was created successfully
303+
self.assertIsNotNone(new_vm, "Failed to create VM from backup")
304+
self.assertEqual(new_vm.name, new_vm_name, "VM name does not match the requested name")
305+
306+
# Verify the new VM is running
307+
self.assertEqual(new_vm.state, "Running", "New VM should be in Running state")
308+
309+
# Verify the new VM has the correct service offering
310+
self.assertEqual(new_vm.serviceofferingid, self.offering.id,
311+
"New VM should have the correct service offering")
312+
313+
# Verify the new VM has the correct zone
314+
self.assertEqual(new_vm.zoneid, self.zone.id, "New VM should be in the correct zone")
315+
316+
# Verify the new VM has the correct number of volumes (ROOT + DATADISK)
317+
volumes = Volume.list(
318+
self.api_client,
319+
virtualmachineid=new_vm.id,
320+
listall=True
321+
)
322+
self.assertTrue(isinstance(volumes, list), "List volumes should return a valid list")
323+
self.assertEqual(2, len(volumes), "The new VM should have 2 volumes (ROOT + DATADISK)")
324+
325+
# Verify that the file is present in the Instance created from backup
326+
try:
327+
ssh_client_new_vm = new_vm.get_ssh_client(reconnect=True)
328+
result = ssh_client_new_vm.execute("ls test_backup_and_recovery.txt")
329+
self.assertEqual(result[0], "test_backup_and_recovery.txt",
330+
"Instance created from Backup should have the same file as the backup.")
331+
except Exception as err:
332+
self.fail("SSH failed for Virtual machine: %s due to %s" % (self.vm.ipaddress, err))
333+
334+
# Delete backups
335+
Backup.delete(self.api_client, backups[0].id)
336+
Backup.delete(self.api_client, backups[1].id)

tools/marvin/marvin/lib/base.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6215,6 +6215,27 @@ def removeOffering(self, apiclient, vmid, forced=True):
62156215
cmd.forced = forced
62166216
return (apiclient.removeVirtualMachineFromBackupOffering(cmd))
62176217

6218+
@classmethod
6219+
def createOffering(cls, api_client, description, name, zoneid, allowquickrestore=False, compress=False, validate=False, validationsteps=None, allowuserdrivenbackups=True):
6220+
"""Create a backup offering"""
6221+
6222+
cmd = createBackupOffering.createBackupOfferingCmd()
6223+
cmd.description = description
6224+
cmd.name = name
6225+
cmd.zoneid = zoneid
6226+
cmd.allowuserdrivenbackups = allowuserdrivenbackups
6227+
6228+
if allowquickrestore:
6229+
cmd.allowquickrestore = allowquickrestore
6230+
if compress:
6231+
cmd.compress = compress
6232+
if validate:
6233+
cmd.validate = validate
6234+
if validationsteps:
6235+
cmd.validationsteps = validationsteps
6236+
6237+
return BackupOffering(api_client.createBackupOffering(cmd).__dict__)
6238+
62186239

62196240
class Backup:
62206241

@@ -6289,6 +6310,14 @@ def createVMFromBackup(cls, apiclient, services, mode, backupid, accountname, do
62896310
VirtualMachine.program_ssh_access(apiclient, services, mode, cmd.networkids, virtual_machine)
62906311
return virtual_machine
62916312

6313+
@classmethod
6314+
def downloadValidationScreenshot(self, apiclient, backupid):
6315+
"""Delete VM backup"""
6316+
6317+
cmd = downloadValidationScreenshot.downloadValidationScreenshotCmd()
6318+
cmd.backupid = backupid
6319+
return (apiclient.downloadValidationScreenshot(cmd))
6320+
62926321
class BackupSchedule:
62936322

62946323
def __init__(self, items):

0 commit comments

Comments
 (0)