From ecc953f8e9f8066ef132a0a419f4d5a8289d061d Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 20 Oct 2023 09:45:38 +0200 Subject: [PATCH 01/41] pom.xml: add veeam --- plugins/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/pom.xml b/plugins/pom.xml index 90dc95b82342..1d6fd531c6f8 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -62,6 +62,7 @@ backup/dummy backup/networker + backup/veeam ca/root-ca From 7bd856280307b12683ada38ef23af2c18b419213 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 25 Oct 2023 15:36:35 +0200 Subject: [PATCH 02/41] Veeam: import powershell module which is available since Veeam 11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The following comes from Veeam release note ``` Mods: please move to the PowerShell forum. Yes, this is known change to a PowerShell module from a snap-in, it was made during a beta stage for v11. The requirements for PowerShell 5.1 (up from 2) are covered in the Release Notes, and the details about the PowerShell module are covered in the "What's New" documentation. Details are here: PowerShell • PowerShell module — By popular demand, we switched from the PowerShell snap-in to the PowerShell module, which can be used on any machine with the backup console installed. We also no longer require PowerShell 2.0 installed on the backup server, which is something many customers had problems with. • New PowerShell cmdlet — Version 11 adds 184 new cmdlets for both newly added functionality and expanded coverage of the existing features with a particular focus on restore functionality. ``` This fixes the error message when assign VM to backup offering ``` 2023-10-25 12:35:11,032 DEBUG [o.a.c.b.v.VeeamClient] (API-Job-Executor-5:ctx-5f068dbb job-42 ctx-3fbfe1a2) (logid:5840110c) Veeam response for PowerShell commands [PowerShell Add-PSSnapin VeeamPSSnapin;$Job = Get-VBRJob -name "template_job_zone1_default";$Job.GetBackupTargetRepository() ^| select Name ^| Format-List] is: [^M ^M Name : Default Backup Repository^M ^M ^M ^M Add-PSSnapin : No snap-ins have been registered for Windows PowerShell version 5.^M At line:1 char:1^M + Add-PSSnapin VeeamPSSnapin;$Job = Get-VBRJob -name template_job_zone1 ...^M + ~~~~~~~~~~~~~~~~~~~~~~~~~~^M + CategoryInfo : InvalidArgument: (VeeamPSSnapin:String) [Add-PSSnapin], PSArgumentException^M + FullyQualifiedErrorId : AddPSSnapInRead,Microsoft.PowerShell.Commands.AddPSSnapinCommand^M ^M ]. ``` --- .../java/org/apache/cloudstack/backup/veeam/VeeamClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 40fbe97028a6..b1b630684710 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -553,7 +553,7 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); + joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell"); for (String cmd : cmds) { joiner.add(cmd); } From 1459cc4e557d4a8702cbb8e58b649c505a750a2e Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 25 Oct 2023 15:33:23 +0200 Subject: [PATCH 03/41] Veeam: silently ignore the warning messages when import Veeam module 2023-10-25 13:30:15,357 DEBUG [o.a.c.b.v.VeeamClient] (API-Job-Executor-1:ctx-8fff2101 job-46 ctx-e7b5b953) (logid:1c585ee5) Veeam response for PowerShell commands [PowerShell Import-Module Veeam.Backup.PowerShell;$Job = Get-VBRJob -name "template_job_zone1_default";$Job.GetBackupTargetRepository() ^| select Name ^| Format-List] is: [WARNING: The names of some imported commands from the module 'Veeam.Backup.PowerShell' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb. Name : Default Backup Repository ]. 2023-10-25 13:30:15,357 WARN [o.a.c.b.v.VeeamClient] (API-Job-Executor-1:ctx-8fff2101 job-46 ctx-e7b5b953) (logid:1c585ee5) Exception caught while trying to clone Veeam job: com.cloud.utils.exception.CloudRuntimeException: Can't find any repository name for Job [name: template_job_zone1_default]. --- .../java/org/apache/cloudstack/backup/veeam/VeeamClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index b1b630684710..be17ad389d28 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -553,7 +553,7 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell"); + joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); for (String cmd : cmds) { joiner.add(cmd); } From 8ea34d692154fda17bfae861bf6acf716b633c8c Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 25 Oct 2023 15:52:25 +0200 Subject: [PATCH 04/41] Veeam: fix issue when assign vm to backup offerings 2023-10-25 13:37:28,343 DEBUG [o.a.c.b.v.VeeamClient] (API-Job-Executor-1:ctx-92428196 job-49 ctx-4c5c5364) (logid:0271069c) Veeam response for PowerShell commands [PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue;$Job = Get-VBRJob -name "template_job_zone1_default";$Job.GetBackupTargetRepository() ^| select Name ^| Format-List] is: [^M ^M Name : Default Backup Repository^M ^M ^M ^M ]. 2023-10-25 13:37:28,344 WARN [o.a.c.b.v.VeeamClient] (API-Job-Executor-1:ctx-92428196 job-49 ctx-4c5c5364) (logid:0271069c) Exception caught while trying to clone Veeam job: com.cloud.utils.exception.CloudRuntimeException: Can't find any repository name for Job [name: template_job_zone1_default]. at org.apache.cloudstack.backup.veeam.VeeamClient.getRepositoryNameFromJob(VeeamClient.java:384) --- .../java/org/apache/cloudstack/backup/veeam/VeeamClient.java | 2 +- .../org/apache/cloudstack/backup/veeam/VeeamClientTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index be17ad389d28..0ebffcc37ec5 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -376,7 +376,7 @@ protected String getRepositoryNameFromJob(String backupName) { throw new CloudRuntimeException(String.format("Failed to get Repository Name from Job [name: %s].", backupName)); } - for (String block : result.second().split("\n\n")) { + for (String block : result.second().split("\r\n")) { if (block.matches("Name(\\s)+:(.)*")) { return block.split(":")[1].trim(); } diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index a155d351bc65..c4af596ea190 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -139,7 +139,7 @@ public void getRepositoryNameFromJobTestExceptionWhenResultIsInWrongFormat() { @Test public void getRepositoryNameFromJobTestSuccess() throws Exception { String backupName = "TEST-BACKUP3"; - Pair response = new Pair(Boolean.TRUE, "\n\nName : test"); + Pair response = new Pair(Boolean.TRUE, "\r\nName : test"); Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList()); String repositoryNameFromJob = mockClient.getRepositoryNameFromJob(backupName); Assert.assertEquals("test", repositoryNameFromJob); From 7a3ddf7b31f99723edf2c61b3d9c030e2dcbf81a Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 26 Oct 2023 12:01:18 +0200 Subject: [PATCH 05/41] Veeam: add marvin test --- .../smoke/test_backup_recovery_veeam.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 test/integration/smoke/test_backup_recovery_veeam.py diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py new file mode 100644 index 000000000000..e6d0d21ae991 --- /dev/null +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import (cleanup_resources) +from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup) +from marvin.lib.common import (get_domain, get_zone, get_template) +from nose.plugins.attrib import attr +from marvin.codes import FAILED + +import time + +class TestVeeamBackupAndRecovery(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + # Setup + + cls.testClient = super(TestVeeamBackupAndRecovery, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.services["mode"] = cls.zone.networktype + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.domain = get_domain(cls.api_client) + cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + if cls.template == FAILED: + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = cls.template.id + cls._cleanup = [] + + # Check backup configuration values, set them to enable the veeam provider + backup_enabled_cfg = Configurations.list(cls.api_client, name='backup.framework.enabled', zoneid=cls.zone.id) + backup_provider_cfg = Configurations.list(cls.api_client, name='backup.framework.provider.plugin', zoneid=cls.zone.id) + cls.backup_enabled = backup_enabled_cfg[0].value + cls.backup_provider = backup_provider_cfg[0].value + + if cls.backup_enabled == "false": + Configurations.update(cls.api_client, 'backup.framework.enabled', value='true', zoneid=cls.zone.id) + if cls.backup_provider != "veeam": + return + + if cls.hypervisor.lower() != 'vmware': + return + + cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id) + cls.service_offering = ServiceOffering.create(cls.api_client,cls.services["service_offerings"]["small"]) + cls._cleanup = [cls.service_offering, cls.account] + + @classmethod + def isBackupOfferingUsed(cls, existing_offerings, provider_offering): + for existing_offering in existing_offerings: + if existing_offering.externalid == provider_offering.externalid: + return True + return False + + @classmethod + def tearDownClass(cls): + try: + # Cleanup resources used + cleanup_resources(cls.api_client, cls._cleanup) + + # Restore original backup framework values values + if cls.backup_enabled == "false": + Configurations.update(cls.api_client, 'backup.framework.enabled', value=cls.backup_enabled, zoneid=cls.zone.id) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + if self.backup_provider != "veeam": + raise self.skipTest("Skipping test cases which must only run for veeam") + if self.hypervisor.lower() != 'vmware': + raise self.skipTest("Skipping test cases which must only run for VMware") + self.cleanup = [] + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_import_backup_offering(self): + """ + Import provider backup offering from Veeam Backup and Recovery Provider + """ + + # Import backup offering + offering = None + existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) + provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + for provider_offering in provider_offerings: + if not self.isBackupOfferingUsed(existing_offerings, provider_offering): + self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) + offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, + provider_offering.name, provider_offering.description) + break + if not offering: + self.fail("Failed to import backup offering") + + # Verify offering is listed + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") + self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response") + matching_offerings = [x for x in imported_offering if x.id == offering.id] + self.assertNotEqual(len(matching_offerings), 0, "Check if there is a matching offering") + + # Delete backup offering + self.debug("Deleting backup offering %s" % offering.id) + offering.delete(self.api_client) + + # Verify offering is not listed + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") + matching_offerings = [x for x in imported_offering if x.id == offering.id] + self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering") + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_vm_backup_lifecycle(self): + """ + Test VM backup lifecycle + """ + + # Import backup offering + offering = None + existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) + provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + for provider_offering in provider_offerings: + if not self.isBackupOfferingUsed(existing_offerings, provider_offering): + self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) + offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, + provider_offering.name, provider_offering.description) + self.cleanup = [offering] + break + if not offering: + self.fail("Failed to import backup offering") + + self.vm = VirtualMachine.create(self.api_client, self.services["small"], accountid=self.account.name, + domainid=self.account.domainid, serviceofferingid=self.service_offering.id, + mode=self.services["mode"]) + + # Verify there are no backups for the VM + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Assign VM to offering and create ad-hoc backup + offering.assignOffering(self.apiclient, self.vm.id) + vms = VirtualMachine.list( + self.apiclient, + id=self.vm.id, + listall=True + ) + self.assertEqual( + isinstance(vms, list), + True, + "List virtual machines should return a valid list" + ) + self.assertEqual(1, len(vms), "List of the virtual machines should have 1 vm") + self.assertEqual(offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % offering.id) + + # Create backup + Backup.create(self.apiclient, self.vm.id) + + # Verify backup is created for the VM + time.sleep(600) + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") + backup = backups[0] + + # Delete backup + Backup.delete(self.apiclient, backup.id) + + # Verify backup is deleted + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Remove VM from offering + offering.removeOffering(self.apiclient, self.vm.id) + + # Delete backup offering + self.debug("Deleting backup offering %s" % offering.id) + offering.delete(self.api_client) \ No newline at end of file From 36721048aaaa0561324841bb4f1a32060ee14fe3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 27 Oct 2023 09:39:20 +0200 Subject: [PATCH 06/41] veeam: force delete backup --- .../smoke/test_backup_recovery_veeam.py | 23 ++++++++++++------- tools/marvin/marvin/lib/base.py | 4 +++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index e6d0d21ae991..3a9fcdf20b05 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -17,7 +17,7 @@ # under the License. from marvin.cloudstackTestCase import cloudstackTestCase -from marvin.lib.utils import (cleanup_resources) +from marvin.lib.utils import (cleanup_resources, wait_until) from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup) from marvin.lib.common import (get_domain, get_zone, get_template) from nose.plugins.attrib import attr @@ -70,6 +70,17 @@ def isBackupOfferingUsed(cls, existing_offerings, provider_offering): return True return False + def waitForBackUp(self, vm): + def checkBackUp(): + backups = Backup.list(self.apiclient, vm.id) + if isinstance(backups, list) and len(backups) != 0: + return True, None + return False, None + + res, _ = wait_until(10, 60, checkBackUp) + if not res: + self.fail("Failed to wait for backup of VM %s to be Up" % vm.id) + @classmethod def tearDownClass(cls): try: @@ -180,21 +191,17 @@ def test_vm_backup_lifecycle(self): Backup.create(self.apiclient, self.vm.id) # Verify backup is created for the VM - time.sleep(600) + self.waitForBackUp(self.vm) backups = Backup.list(self.apiclient, self.vm.id) self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") backup = backups[0] # Delete backup - Backup.delete(self.apiclient, backup.id) + Backup.delete(self.apiclient, backup.id, forced=True) # Verify backup is deleted backups = Backup.list(self.apiclient, self.vm.id) self.assertEqual(backups, None, "There should not exist any backup for the VM") # Remove VM from offering - offering.removeOffering(self.apiclient, self.vm.id) - - # Delete backup offering - self.debug("Deleting backup offering %s" % offering.id) - offering.delete(self.api_client) \ No newline at end of file + offering.removeOffering(self.apiclient, self.vm.id) \ No newline at end of file diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 9d28d5a3f2a0..4ed2a11e22a3 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5994,11 +5994,13 @@ def create(self, apiclient, vmid): return (apiclient.createBackup(cmd)) @classmethod - def delete(self, apiclient, id): + def delete(self, apiclient, id, forced=None): """Delete VM backup""" cmd = deleteBackup.deleteBackupCmd() cmd.id = id + if forced: + cmd.forced = forced return (apiclient.deleteBackup(cmd)) @classmethod From 02c04b231c685789948154f5146ab12588c9aa17 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 2 Nov 2023 13:49:46 +0100 Subject: [PATCH 07/41] backup: enable group-delete action on UI --- ui/src/components/view/ListView.vue | 2 +- ui/src/config/section/storage.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 38b8b5191389..1505b10bcbbe 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -574,7 +574,7 @@ export default { }, enableGroupAction () { return ['vm', 'alert', 'vmgroup', 'ssh', 'userdata', 'affinitygroup', 'autoscalevmgroup', 'volume', 'snapshot', - 'vmsnapshot', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', + 'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering', 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment' ].includes(this.$route.name) diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 920cdb5bbb6c..316147faf3f9 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -492,7 +492,10 @@ export default { label: 'label.delete.backup', message: 'message.delete.backup', dataView: true, - show: (record) => { return record.state !== 'Destroyed' } + show: (record) => { return record.state !== 'Destroyed' }, + groupAction: true, + popup: true, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } } ] } From 3756420a1305c19b28faf3aec0c7666c2457fd5e Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 3 Nov 2023 11:22:37 +0100 Subject: [PATCH 08/41] veeam: fix authorization failure in veeam 12a marvin.cloudstackException.CloudstackAPIException: Execute cmd: listbackupproviderofferings failed, due to: errorCode: 530, errorText:Veeam B&R API call unauthorized, please ask your administrator to fix integration issues. v1_4 is not supported in veeam 12a, refer to https://helpcenter.veeam.com/docs/backup/em_rest/http_authentication.html?ver=120 --- .../java/org/apache/cloudstack/backup/veeam/VeeamClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 0ebffcc37ec5..edc8c3837283 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -135,7 +135,7 @@ protected void setVeeamSshCredentials(String hostIp, String username, String pas private void authenticate(final String username, final String password) { // https://helpcenter.veeam.com/docs/backup/rest/http_authentication.html?ver=95u4 - final HttpPost request = new HttpPost(apiURI.toString() + "/sessionMngr/?v=v1_4"); + final HttpPost request = new HttpPost(apiURI.toString() + "/sessionMngr/?v=latest"); request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())); try { final HttpResponse response = httpClient.execute(request); From 08d87251174a6cf9093ed9964cb84d082f1effaa Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 3 Nov 2023 15:33:33 +0100 Subject: [PATCH 09/41] veeam: update integration test --- .../smoke/test_backup_recovery_veeam.py | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index 3a9fcdf20b05..b794f6429bfb 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -60,11 +60,17 @@ def setUpClass(cls): return cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id) - cls.service_offering = ServiceOffering.create(cls.api_client,cls.services["service_offerings"]["small"]) + cls.user_user = cls.account.user[0] + cls.user_apiclient = cls.testClient.getUserApiClient( + cls.user_user.username, cls.domain.name + ) + cls.service_offering = ServiceOffering.create(cls.api_client, cls.services["service_offerings"]["small"]) cls._cleanup = [cls.service_offering, cls.account] @classmethod def isBackupOfferingUsed(cls, existing_offerings, provider_offering): + if not existing_offerings: + return False for existing_offering in existing_offerings: if existing_offering.externalid == provider_offering.externalid: return True @@ -72,7 +78,7 @@ def isBackupOfferingUsed(cls, existing_offerings, provider_offering): def waitForBackUp(self, vm): def checkBackUp(): - backups = Backup.list(self.apiclient, vm.id) + backups = Backup.list(self.user_apiclient, vm.id) if isinstance(backups, list) and len(backups) != 0: return True, None return False, None @@ -94,8 +100,6 @@ def tearDownClass(cls): raise Exception("Warning: Exception during cleanup : %s" % e) def setUp(self): - self.apiclient = self.testClient.getApiClient() - self.dbclient = self.testClient.getDbConnection() if self.backup_provider != "veeam": raise self.skipTest("Skipping test cases which must only run for veeam") if self.hypervisor.lower() != 'vmware': @@ -104,7 +108,7 @@ def setUp(self): def tearDown(self): try: - cleanup_resources(self.apiclient, self.cleanup) + cleanup_resources(self.api_client, self.cleanup) except Exception as e: raise Exception("Warning: Exception during cleanup : %s" % e) @@ -118,17 +122,21 @@ def test_import_backup_offering(self): offering = None existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + if not provider_offerings: + self.skipTest("Skipping test cases as the provider offering is None") for provider_offering in provider_offerings: if not self.isBackupOfferingUsed(existing_offerings, provider_offering): self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, - provider_offering.name, provider_offering.description) + provider_offering.name, provider_offering.description) + if not offering: + self.fail("Failed to import backup offering %s" % provider_offering.name) break if not offering: - self.fail("Failed to import backup offering") + self.skipTest("Skipping test cases as there is no available provider offerings to import") - # Verify offering is listed - imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + # Verify offering is listed by user + imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response") matching_offerings = [x for x in imported_offering if x.id == offering.id] @@ -138,11 +146,12 @@ def test_import_backup_offering(self): self.debug("Deleting backup offering %s" % offering.id) offering.delete(self.api_client) - # Verify offering is not listed - imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) - self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") - matching_offerings = [x for x in imported_offering if x.id == offering.id] - self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering") + # Verify offering is not listed by user + imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) + if imported_offering: + self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") + matching_offerings = [x for x in imported_offering if x.id == offering.id] + self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering") @attr(tags=["advanced", "backup"], required_hardware="false") def test_vm_backup_lifecycle(self): @@ -154,28 +163,32 @@ def test_vm_backup_lifecycle(self): offering = None existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + if not provider_offerings: + self.skipTest("Skipping test cases as the provider offering is None") for provider_offering in provider_offerings: if not self.isBackupOfferingUsed(existing_offerings, provider_offering): self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, provider_offering.name, provider_offering.description) - self.cleanup = [offering] + if not offering: + self.fail("Failed to import backup offering %s" % provider_offering.name) + self.cleanup.append(offering) break if not offering: - self.fail("Failed to import backup offering") + self.skipTest("Skipping test cases as there is no available provider offerings to import") - self.vm = VirtualMachine.create(self.api_client, self.services["small"], accountid=self.account.name, + self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name, domainid=self.account.domainid, serviceofferingid=self.service_offering.id, mode=self.services["mode"]) # Verify there are no backups for the VM - backups = Backup.list(self.apiclient, self.vm.id) + backups = Backup.list(self.user_apiclient, self.vm.id) self.assertEqual(backups, None, "There should not exist any backup for the VM") # Assign VM to offering and create ad-hoc backup - offering.assignOffering(self.apiclient, self.vm.id) + offering.assignOffering(self.user_apiclient, self.vm.id) vms = VirtualMachine.list( - self.apiclient, + self.user_apiclient, id=self.vm.id, listall=True ) @@ -188,20 +201,20 @@ def test_vm_backup_lifecycle(self): self.assertEqual(offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % offering.id) # Create backup - Backup.create(self.apiclient, self.vm.id) + Backup.create(self.user_apiclient, self.vm.id) # Verify backup is created for the VM self.waitForBackUp(self.vm) - backups = Backup.list(self.apiclient, self.vm.id) + backups = Backup.list(self.user_apiclient, self.vm.id) self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") backup = backups[0] # Delete backup - Backup.delete(self.apiclient, backup.id, forced=True) + Backup.delete(self.user_apiclient, backup.id, forced=True) # Verify backup is deleted - backups = Backup.list(self.apiclient, self.vm.id) + backups = Backup.list(self.user_apiclient, self.vm.id) self.assertEqual(backups, None, "There should not exist any backup for the VM") # Remove VM from offering - offering.removeOffering(self.apiclient, self.vm.id) \ No newline at end of file + offering.removeOffering(self.user_apiclient, self.vm.id) \ No newline at end of file From 560111680604479c0c4534defa8185e4e6a55461 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 3 Nov 2023 16:18:38 +0100 Subject: [PATCH 10/41] veeam: fix exception if backup name has space administrator@VEEAM12A C:\Users\Administrator>PowerShell Import-Module Veeam.Backup.P owerShell -WarningAction SilentlyContinue;$Job = Get-VBRJob -name "Backup Job Automat ic" Get-VBRJob : A positional parameter cannot be found that accepts argument 'Job'. At line:1 char:78 + ... gAction SilentlyContinue;$Job = Get-VBRJob -name Backup Job Automatic + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (:) [Get-VBRJob], ParameterBindingEx ception + FullyQualifiedErrorId : PositionalParameterNotFound,Veeam.Backup.PowerShell.C mdlets.GetVBRJob administrator@VEEAM12A C:\Users\Administrator>PowerShell Import-Module Veeam.Backup.P owerShell -WarningAction SilentlyContinue;$Job = Get-VBRJob -name 'Backup Job Automat ic' --- .../apache/cloudstack/backup/veeam/VeeamClient.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index edc8c3837283..d665994c9325 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -368,7 +368,7 @@ public Ref listBackupRepository(final String backupServerId, final String backup protected String getRepositoryNameFromJob(String backupName) { final List cmds = Arrays.asList( - String.format("$Job = Get-VBRJob -name \"%s\"", backupName), + String.format("$Job = Get-VBRJob -name '%s'", backupName), "$Job.GetBackupTargetRepository() ^| select Name ^| Format-List" ); Pair result = executePowerShellCommands(cmds); @@ -584,7 +584,7 @@ protected Pair executePowerShellCommands(List cmds) { public boolean setJobSchedule(final String jobName) { Pair result = executePowerShellCommands(Arrays.asList( - String.format("$job = Get-VBRJob -Name \"%s\"", jobName), + String.format("$job = Get-VBRJob -Name '%s'", jobName), "if ($job) { Set-VBRJobSchedule -Job $job -Daily -At \"11:00\" -DailyKind Weekdays }" )); return result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE); @@ -592,9 +592,9 @@ public boolean setJobSchedule(final String jobName) { public boolean deleteJobAndBackup(final String jobName) { Pair result = executePowerShellCommands(Arrays.asList( - String.format("$job = Get-VBRJob -Name \"%s\"", jobName), + String.format("$job = Get-VBRJob -Name '%s'", jobName), "if ($job) { Remove-VBRJob -Job $job -Confirm:$false }", - String.format("$backup = Get-VBRBackup -Name \"%s\"", jobName), + String.format("$backup = Get-VBRBackup -Name '%s'", jobName), "if ($backup) { Remove-VBRBackup -Backup $backup -FromDisk -Confirm:$false }", "$repo = Get-VBRBackupRepository", "Sync-VBRBackupRepository -Repository $repo" @@ -679,7 +679,7 @@ private Backup.RestorePoint getRestorePointFromBlock(String[] parts) { public List listRestorePoints(String backupName, String vmInternalName) { final List cmds = Arrays.asList( - String.format("$backup = Get-VBRBackup -Name \"%s\"", backupName), + String.format("$backup = Get-VBRBackup -Name '%s'", backupName), String.format("if ($backup) { $restore = (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true})", vmInternalName), "if ($restore) { $restore ^| Format-List } }" ); From 108d296f097b0ab35f9b77de54c22b4ce1d385b5 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Sat, 4 Nov 2023 10:27:40 +0100 Subject: [PATCH 11/41] veeam: remove offering as last step in cleanup --- test/integration/smoke/test_backup_recovery_veeam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index b794f6429bfb..98f20da85efe 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -172,7 +172,7 @@ def test_vm_backup_lifecycle(self): provider_offering.name, provider_offering.description) if not offering: self.fail("Failed to import backup offering %s" % provider_offering.name) - self.cleanup.append(offering) + self.cleanup.insert(0, offering) break if not offering: self.skipTest("Skipping test cases as there is no available provider offerings to import") From 600780821728a8e754225b4a3908a7baa7dc9cfc Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Sat, 4 Nov 2023 10:32:02 +0100 Subject: [PATCH 12/41] veeam: add zone setting backup.plugin.veeam.version --- .../apache/cloudstack/backup/VeeamBackupProvider.java | 7 ++++++- .../apache/cloudstack/backup/veeam/VeeamClient.java | 10 ++++++++-- .../cloudstack/backup/veeam/VeeamClientTest.java | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 02f08d602bb9..30b60e87e490 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -64,6 +64,10 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider, "backup.plugin.veeam.url", "https://localhost:9398/api/", "The Veeam backup and recovery URL.", true, ConfigKey.Scope.Zone); + public ConfigKey VeeamVersion = new ConfigKey<>("Advanced", Integer.class, + "backup.plugin.veeam.version", "9", + "The version of Veeam backup and recovery.", true, ConfigKey.Scope.Zone); + private ConfigKey VeeamUsername = new ConfigKey<>("Advanced", String.class, "backup.plugin.veeam.username", "administrator", "The Veeam backup and recovery username.", true, ConfigKey.Scope.Zone); @@ -92,7 +96,7 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider, protected VeeamClient getClient(final Long zoneId) { try { - return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), + return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamVersion.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId), VeeamRestoreTimeout.valueIn(zoneId)); } catch (URISyntaxException e) { throw new CloudRuntimeException("Failed to parse Veeam API URL: " + e.getMessage()); @@ -349,6 +353,7 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ VeeamUrl, + VeeamVersion, VeeamUsername, VeeamPassword, VeeamValidateSSLSecurity, diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index d665994c9325..cc281c3654be 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -92,13 +92,14 @@ public class VeeamClient { private static final String SESSION_HEADER = "X-RestSvcSessionId"; private String veeamServerIp; + private Integer veeamServerVersion; private String veeamServerUsername; private String veeamServerPassword; private String veeamSessionId = null; private int restoreTimeout; private final int veeamServerPort = 22; - public VeeamClient(final String url, final String username, final String password, final boolean validateCertificate, final int timeout, + public VeeamClient(final String url, final Integer version, final String username, final String password, final boolean validateCertificate, final int timeout, final int restoreTimeout) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException { this.apiURI = new URI(url); this.restoreTimeout = restoreTimeout; @@ -123,6 +124,7 @@ public VeeamClient(final String url, final String username, final String passwor .build(); } + this.veeamServerVersion = version; authenticate(username, password); setVeeamSshCredentials(this.apiURI.getHost(), username, password); } @@ -553,7 +555,11 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); + if (this.veeamServerVersion != null && this.veeamServerVersion >= 11) { + joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); + } else { + joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); + } for (String cmd : cmds) { joiner.add(cmd); } diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index c4af596ea190..930c669f8f96 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -62,7 +62,7 @@ public void setUp() throws Exception { .withStatus(201) .withHeader("X-RestSvcSessionId", "some-session-auth-id") .withBody(""))); - client = new VeeamClient("http://localhost:9399/api/", adminUsername, adminPassword, true, 60, 600); + client = new VeeamClient("http://localhost:9399/api/", 9, adminUsername, adminPassword, true, 60, 600); mockClient = Mockito.mock(VeeamClient.class); Mockito.when(mockClient.getRepositoryNameFromJob(Mockito.anyString())).thenCallRealMethod(); } From a2265cbe434d84bc6f42a33c91f4cbe78c184de5 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Sat, 4 Nov 2023 13:18:44 +0100 Subject: [PATCH 13/41] veeam: fix update integration test --- .../smoke/test_backup_recovery_veeam.py | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index 98f20da85efe..54b6b7db8606 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -32,13 +32,13 @@ def setUpClass(cls): # Setup cls.testClient = super(TestVeeamBackupAndRecovery, cls).getClsTestClient() - cls.api_client = cls.testClient.getApiClient() + cls.apiclient = cls.testClient.getApiClient() cls.services = cls.testClient.getParsedTestDataConfig() - cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() - cls.domain = get_domain(cls.api_client) - cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.domain = get_domain(cls.apiclient) + cls.template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id @@ -46,26 +46,21 @@ def setUpClass(cls): cls._cleanup = [] # Check backup configuration values, set them to enable the veeam provider - backup_enabled_cfg = Configurations.list(cls.api_client, name='backup.framework.enabled', zoneid=cls.zone.id) - backup_provider_cfg = Configurations.list(cls.api_client, name='backup.framework.provider.plugin', zoneid=cls.zone.id) + backup_enabled_cfg = Configurations.list(cls.apiclient, name='backup.framework.enabled', zoneid=cls.zone.id) + backup_provider_cfg = Configurations.list(cls.apiclient, name='backup.framework.provider.plugin', zoneid=cls.zone.id) cls.backup_enabled = backup_enabled_cfg[0].value cls.backup_provider = backup_provider_cfg[0].value if cls.backup_enabled == "false": - Configurations.update(cls.api_client, 'backup.framework.enabled', value='true', zoneid=cls.zone.id) + Configurations.update(cls.apiclient, 'backup.framework.enabled', value='true', zoneid=cls.zone.id) if cls.backup_provider != "veeam": return if cls.hypervisor.lower() != 'vmware': return - cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id) - cls.user_user = cls.account.user[0] - cls.user_apiclient = cls.testClient.getUserApiClient( - cls.user_user.username, cls.domain.name - ) - cls.service_offering = ServiceOffering.create(cls.api_client, cls.services["service_offerings"]["small"]) - cls._cleanup = [cls.service_offering, cls.account] + cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["small"]) + cls._cleanup = [cls.service_offering] @classmethod def isBackupOfferingUsed(cls, existing_offerings, provider_offering): @@ -89,15 +84,9 @@ def checkBackUp(): @classmethod def tearDownClass(cls): - try: - # Cleanup resources used - cleanup_resources(cls.api_client, cls._cleanup) - - # Restore original backup framework values values - if cls.backup_enabled == "false": - Configurations.update(cls.api_client, 'backup.framework.enabled', value=cls.backup_enabled, zoneid=cls.zone.id) - except Exception as e: - raise Exception("Warning: Exception during cleanup : %s" % e) + if cls.backup_enabled == "false": + Configurations.update(cls.apiclient, 'backup.framework.enabled', value=cls.backup_enabled, zoneid=cls.zone.id) + super(TestVeeamBackupAndRecovery, cls).tearDownClass() def setUp(self): if self.backup_provider != "veeam": @@ -107,10 +96,7 @@ def setUp(self): self.cleanup = [] def tearDown(self): - try: - cleanup_resources(self.api_client, self.cleanup) - except Exception as e: - raise Exception("Warning: Exception during cleanup : %s" % e) + super(TestVeeamBackupAndRecovery, self).tearDown() @attr(tags=["advanced", "backup"], required_hardware="false") def test_import_backup_offering(self): @@ -120,14 +106,14 @@ def test_import_backup_offering(self): # Import backup offering offering = None - existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) - provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + existing_offerings = BackupOffering.listByZone(self.apiclient, self.zone.id) + provider_offerings = BackupOffering.listExternal(self.apiclient, self.zone.id) if not provider_offerings: self.skipTest("Skipping test cases as the provider offering is None") for provider_offering in provider_offerings: if not self.isBackupOfferingUsed(existing_offerings, provider_offering): self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) - offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, + offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, provider_offering.name, provider_offering.description) if not offering: self.fail("Failed to import backup offering %s" % provider_offering.name) @@ -136,7 +122,7 @@ def test_import_backup_offering(self): self.skipTest("Skipping test cases as there is no available provider offerings to import") # Verify offering is listed by user - imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response") matching_offerings = [x for x in imported_offering if x.id == offering.id] @@ -144,10 +130,10 @@ def test_import_backup_offering(self): # Delete backup offering self.debug("Deleting backup offering %s" % offering.id) - offering.delete(self.api_client) + offering.delete(self.apiclient) # Verify offering is not listed by user - imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) if imported_offering: self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") matching_offerings = [x for x in imported_offering if x.id == offering.id] @@ -159,16 +145,24 @@ def test_vm_backup_lifecycle(self): Test VM backup lifecycle """ + # Create user account + self.account = Account.create(self.apiclient, self.services["account"], domainid=self.domain.id) + self.user_user = self.account.user[0] + self.user_apiclient = self.testClient.getUserApiClient( + self.user_user.username, self.domain.name + ) + self.cleanup = [self.account] + # Import backup offering offering = None - existing_offerings = BackupOffering.listByZone(self.api_client, self.zone.id) - provider_offerings = BackupOffering.listExternal(self.api_client, self.zone.id) + existing_offerings = BackupOffering.listByZone(self.apiclient, self.zone.id) + provider_offerings = BackupOffering.listExternal(self.apiclient, self.zone.id) if not provider_offerings: self.skipTest("Skipping test cases as the provider offering is None") for provider_offering in provider_offerings: if not self.isBackupOfferingUsed(existing_offerings, provider_offering): self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) - offering = BackupOffering.importExisting(self.api_client, self.zone.id, provider_offering.externalid, + offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, provider_offering.name, provider_offering.description) if not offering: self.fail("Failed to import backup offering %s" % provider_offering.name) @@ -178,8 +172,7 @@ def test_vm_backup_lifecycle(self): self.skipTest("Skipping test cases as there is no available provider offerings to import") self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name, - domainid=self.account.domainid, serviceofferingid=self.service_offering.id, - mode=self.services["mode"]) + domainid=self.account.domainid, serviceofferingid=self.service_offering.id) # Verify there are no backups for the VM backups = Backup.list(self.user_apiclient, self.vm.id) From 574672b02e236fae26ccc37432cf8947f8a6d8f3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 6 Nov 2023 21:30:57 +0100 Subject: [PATCH 14/41] veeam: fix backup metrics in veeam12 (1) get backup metrics via Veeam API on veeam11, it works ``` administrator@VEEAM11A C:\Users\Administrator>PowerShell Import-Module Veeam.Backup.P owerShell -WarningAction SilentlyContinue;$backups = Get-VBRBackup;foreach ($backup i n $backups) {$backup.JobName;$storageGroups = $backup.GetStorageGroups();foreach ($gr oup in $storageGroups) {$usedSize = 0;$dataSize = 0;$sizePerStorage = $group.GetStora ges().Stats.BackupSize;$dataPerStorage = $group.GetStorages().Stats.DataSize;foreach ($size in $sizePerStorage) {$usedSize += $size;}foreach ($size in $dataPerStorage) {$ dataSize += $size;}$usedSize;$dataSize;}echo "====="} i-13-22-VM-CSBKP-b3b3cb75-cfbf-4496-9c63-a08a93347276 ===== backup-job-based-on-sla ===== i-12-20-VM-CSBKP-9f292f11-00ec-4915-84f0-e3895828640e ===== i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6 1268682752 15624049921 ===== i-32-56-VM-CSBKP-b374b20e-9e8f-427a-a02f-1cd740bce044 569131008 2147506678 ===== ``` on veeam12, the powershell command does not return datasize and usedsize. ``` administrator@VEEAM12A C:\Users\Administrator>PowerShell Import-Module Veeam.Backup.P owerShell -WarningAction SilentlyContinue;$backups = Get-VBRBackup;foreach ($backup i n $backups) {$backup.JobName;$storageGroups = $backup.GetStorageGroups();foreach ($gr oup in $storageGroups) {$usedSize = 0;$dataSize = 0;$sizePerStorage = $group.GetStora ges().Stats.BackupSize;$dataPerStorage = $group.GetStorages().Stats.DataSize;foreach ($size in $sizePerStorage) {$usedSize += $size;}foreach ($size in $dataPerStorage) {$ dataSize += $size;}$usedSize;$dataSize;}echo "====="} Backup Job Automatic ===== Backup Job Manual ===== i-2-4-VM-CSBKP-506760dc-ed77-40d6-a91d-e0914e7a1ad8 ===== Backup Job Automatic ===== ``` (2) get VM RestorePoints via Veeam API Currently cannot insert BackupVO to database ``` Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Incorrect datetime value: '03/11/2023 16:26:12' for column 'date' at row 1 ``` fixed by getting date from CreateTimeUtc in API response instead of powershell output ``` administrator@VEEAM12A C:\Users\Administrator>PowerShell Import-Module Veeam.Backup.P owerShell -WarningAction SilentlyContinue;$restore = (Get-VBRRestorePoint -Name "i-2- 4-VM" ^| Where-Object {$_.IsConsistent -eq $true});if ($restore) { $restore ^| Format -List } ... Id : f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977 ... PointId : c030b23e-d7fa-45b6-a5a7-feb8525d2563 ... CreationTime : 03/11/2023 16:26:12 CreationTimeUtc : 03/11/2023 16:26:12 Type : Full Algorithm : Full ... IsConsistent : True ... CompletionTimeUtc : 03/11/2023 16:26:59 ... ``` (3) format Date to new format accepted by database It fixes the issue below Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Incorrect datetime value: '2023-11-03T16:26:12.209913Z' for column 'date' at row 1 The new date is '2023-11-03 16:26:12'. --- .../cloudstack/backup/veeam/VeeamClient.java | 258 +++++++++++++-- .../backup/veeam/api/BackupFile.java | 160 ++++++++++ .../backup/veeam/api/BackupFiles.java | 39 +++ .../backup/veeam/api/RestorePoint.java | 94 ++++++ .../backup/veeam/api/RestorePoints.java | 39 +++ .../backup/veeam/api/VmRestorePoint.java | 149 +++++++++ .../backup/veeam/api/VmRestorePoints.java | 39 +++ .../backup/veeam/VeeamClientTest.java | 296 ++++++++++++++++++ .../smoke/test_backup_recovery_veeam.py | 5 + tools/marvin/marvin/lib/base.py | 7 +- ui/src/config/section/storage.js | 3 +- 11 files changed, 1059 insertions(+), 30 deletions(-) create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFile.java create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFiles.java create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoint.java create mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoints.java diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index cc281c3654be..75bcb334c071 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -20,12 +20,15 @@ import static org.apache.cloudstack.backup.VeeamBackupProvider.BACKUP_IDENTIFIER; import java.io.IOException; +import java.io.InputStream; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -42,6 +45,8 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.veeam.api.BackupFile; +import org.apache.cloudstack.backup.veeam.api.BackupFiles; import org.apache.cloudstack.backup.veeam.api.BackupJobCloneInfo; import org.apache.cloudstack.backup.veeam.api.CreateObjectInJobSpec; import org.apache.cloudstack.backup.veeam.api.EntityReferences; @@ -53,9 +58,14 @@ import org.apache.cloudstack.backup.veeam.api.ObjectInJob; import org.apache.cloudstack.backup.veeam.api.ObjectsInJob; import org.apache.cloudstack.backup.veeam.api.Ref; +import org.apache.cloudstack.backup.veeam.api.RestorePoint; +import org.apache.cloudstack.backup.veeam.api.RestorePoints; import org.apache.cloudstack.backup.veeam.api.RestoreSession; import org.apache.cloudstack.backup.veeam.api.Task; +import org.apache.cloudstack.backup.veeam.api.VmRestorePoint; +import org.apache.cloudstack.backup.veeam.api.VmRestorePoints; import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.commons.collections.CollectionUtils; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; @@ -90,6 +100,14 @@ public class VeeamClient { private final HttpClient httpClient; private static final String RESTORE_VM_SUFFIX = "CS-RSTR-"; private static final String SESSION_HEADER = "X-RestSvcSessionId"; + private static final String BACKUP_REFERENCE = "BackupReference"; + private static final String HIERARCHY_ROOT_REFERENCE = "HierarchyRootReference"; + private static final String REPOSITORY_REFERENCE = "RepositoryReference"; + private static final String RESTORE_POINT_REFERENCE = "RestorePointReference"; + private static final String BACKUP_FILE_REFERENCE = "BackupFileReference"; + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + private static final SimpleDateFormat newDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private String veeamServerIp; private Integer veeamServerVersion; @@ -240,7 +258,7 @@ private String findDCHierarchy(final String vmwareDcName) { final ObjectMapper objectMapper = new XmlMapper(); final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); for (final Ref ref : references.getRefs()) { - if (ref.getName().equals(vmwareDcName) && ref.getType().equals("HierarchyRootReference")) { + if (ref.getName().equals(vmwareDcName) && ref.getType().equals(HIERARCHY_ROOT_REFERENCE)) { return ref.getUid(); } } @@ -357,7 +375,7 @@ public Ref listBackupRepository(final String backupServerId, final String backup final ObjectMapper objectMapper = new XmlMapper(); final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); for (final Ref ref : references.getRefs()) { - if (ref.getType().equals("RepositoryReference") && ref.getName().equals(repositoryName)) { + if (ref.getType().equals(REPOSITORY_REFERENCE) && ref.getName().equals(repositoryName)) { return ref; } } @@ -555,10 +573,10 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - if (this.veeamServerVersion != null && this.veeamServerVersion >= 11) { - joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); - } else { + if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); + } else { + joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); } for (String cmd : cmds) { joiner.add(cmd); @@ -616,7 +634,7 @@ public boolean deleteBackup(final String restorePointId) { "$repo = Get-VBRBackupRepository", "Sync-VBRBackupRepository -Repository $repo", "} else { ", - " Write-Output \"Failed to delete\"", + " Write-Output 'Failed to delete'", " Exit 1", "}" )); @@ -624,32 +642,112 @@ public boolean deleteBackup(final String restorePointId) { } public Map getBackupMetrics() { + if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { + return getBackupMetricsLegacy(); + } else { + return getBackupMetricsViaVeeamAPI(); + } + } + + public Map getBackupMetricsViaVeeamAPI() { + LOG.debug("Trying to get backup metrics via Veeam B&R API"); + + try { + final HttpResponse response = get(String.format("/backupFiles?format=Entity")); + checkResponseOK(response); + return processHttpResponseForBackupMetrics(response.getEntity().getContent()); + } catch (final IOException e) { + LOG.error("Failed to get backup metrics via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return new HashMap<>(); + } + + protected Map processHttpResponseForBackupMetrics(final InputStream content) { + Map metrics = new HashMap<>(); + try { + final ObjectMapper objectMapper = new XmlMapper(); + final BackupFiles backupFiles = objectMapper.readValue(content, BackupFiles.class); + if (backupFiles == null || CollectionUtils.isEmpty(backupFiles.getBackupFiles())) { + throw new CloudRuntimeException("Could not get backup metrics via Veeam B&R API"); + } + for (final BackupFile backupFile : backupFiles.getBackupFiles()) { + String vmUuid = null; + String backupName = null; + List links = backupFile.getLink(); + for (Link link : links) { + if (BACKUP_REFERENCE.equals(link.getType())) { + backupName = link.getName(); + break; + } + } + if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) { + final String[] names = backupName.split(BACKUP_IDENTIFIER); + if (names.length > 1) { + vmUuid = names[1]; + } + } + if (vmUuid == null) { + continue; + } + if (vmUuid.contains(" - ")) { + vmUuid = vmUuid.split(" - ")[0]; + } + Long usedSize = 0L; + Long dataSize = 0L; + if (metrics.containsKey(vmUuid)) { + usedSize = metrics.get(vmUuid).getBackupSize(); + dataSize = metrics.get(vmUuid).getDataSize(); + } + if (backupFile.getBackupSize() != null) { + usedSize += Long.valueOf(backupFile.getBackupSize()); + } + if (backupFile.getDataSize() != null) { + dataSize += Long.valueOf(backupFile.getDataSize()); + } + metrics.put(vmUuid, new Backup.Metric(usedSize, dataSize)); + } + } catch (final IOException e) { + LOG.error("Failed to process response to get backup metrics via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return metrics; + } + + public Map getBackupMetricsLegacy() { final String separator = "====="; final List cmds = Arrays.asList( - "$backups = Get-VBRBackup", - "foreach ($backup in $backups) {" + - "$backup.JobName;" + - "$storageGroups = $backup.GetStorageGroups();" + - "foreach ($group in $storageGroups) {" + - "$usedSize = 0;" + - "$dataSize = 0;" + - "$sizePerStorage = $group.GetStorages().Stats.BackupSize;" + - "$dataPerStorage = $group.GetStorages().Stats.DataSize;" + - "foreach ($size in $sizePerStorage) {" + + "$backups = Get-VBRBackup", + "foreach ($backup in $backups) {" + + "$backup.JobName;" + + "$storageGroups = $backup.GetStorageGroups();" + + "foreach ($group in $storageGroups) {" + + "$usedSize = 0;" + + "$dataSize = 0;" + + "$sizePerStorage = $group.GetStorages().Stats.BackupSize;" + + "$dataPerStorage = $group.GetStorages().Stats.DataSize;" + + "foreach ($size in $sizePerStorage) {" + "$usedSize += $size;" + - "}" + - "foreach ($size in $dataPerStorage) {" + + "}" + + "foreach ($size in $dataPerStorage) {" + "$dataSize += $size;" + - "}" + - "$usedSize;" + - "$dataSize;" + - "}" + - "echo \"" + separator + "\"" + - "}" + "}" + + "$usedSize;" + + "$dataSize;" + + "}" + + "echo \"" + separator + "\"" + + "}" ); Pair response = executePowerShellCommands(cmds); + return processPowerShellResultForBackupMetrics(response.second()); + } + + protected Map processPowerShellResultForBackupMetrics(final String result) { + LOG.debug("Processing powershell result: " + result); + + final String separator = "====="; final Map sizes = new HashMap<>(); - for (final String block : response.second().split(separator + "\r\n")) { + for (final String block : result.split(separator + "\r\n")) { final String[] parts = block.split("\r\n"); if (parts.length != 3) { continue; @@ -683,7 +781,7 @@ private Backup.RestorePoint getRestorePointFromBlock(String[] parts) { return new Backup.RestorePoint(id, created, type); } - public List listRestorePoints(String backupName, String vmInternalName) { + public List listRestorePointsLegacy(String backupName, String vmInternalName) { final List cmds = Arrays.asList( String.format("$backup = Get-VBRBackup -Name '%s'", backupName), String.format("if ($backup) { $restore = (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true})", vmInternalName), @@ -706,6 +804,114 @@ public List listRestorePoints(String backupName, String vmI return restorePoints; } + public List listRestorePoints(String backupName, String vmInternalName) { + if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { + return listRestorePointsLegacy(backupName, vmInternalName); + } else { + return listVmRestorePointsViaVeeamAPI(vmInternalName); + } + } + + public List listRestorePointsViaVeeamAPI(String backupName, String vmInternalName) { + LOG.debug(String.format("Trying to list restore points via Veeam B&R API for backup %s of VM %s: ", backupName, vmInternalName)); + + try { + final HttpResponse response = get("/restorePoints?format=Entity"); + checkResponseOK(response); + return processHttpResponseForRestorePoints(response.getEntity().getContent(), backupName, vmInternalName); + } catch (final IOException e) { + LOG.error("Failed to list restore points via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return new ArrayList<>(); + } + + public List processHttpResponseForRestorePoints(InputStream content, String backupName, String vmInternalName) { + List restorePointList = new ArrayList<>(); + try { + final ObjectMapper objectMapper = new XmlMapper(); + final RestorePoints restorePoints = objectMapper.readValue(content, RestorePoints.class); + if (restorePoints == null) { + throw new CloudRuntimeException("Could not get restore points via Veeam B&R API"); + } + for (final RestorePoint restorePoint : restorePoints.getRestorePoints()) { + String linkName = null; + List links = restorePoint.getLink(); + for (Link link : links) { + if (BACKUP_REFERENCE.equals(link.getType())) { + linkName = link.getName(); + break; + } + } + if (linkName != null && linkName.startsWith(backupName)) { + String restorePointId = restorePoint.getUid().substring(restorePoint.getUid().lastIndexOf(':') + 1); + restorePointList.addAll(listVmRestorePointsViaVeeamAPI(vmInternalName)); + } + } + } catch (final IOException e) { + LOG.error("Failed to process response to get restore points via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return restorePointList; + } + + public List listVmRestorePointsViaVeeamAPI(String vmInternalName) { + LOG.debug(String.format("Trying to list VM restore points via Veeam B&R API for VM %s: ", vmInternalName)); + + try { + final HttpResponse response = get(String.format("/vmRestorePoints?format=Entity")); + checkResponseOK(response); + return processHttpResponseForVmRestorePoints(response.getEntity().getContent(), vmInternalName); + } catch (final IOException e) { + LOG.error("Failed to list VM restore points via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return new ArrayList<>(); + } + + public List processHttpResponseForVmRestorePoints(InputStream content, String vmInternalName) { + List vmRestorePointList = new ArrayList<>(); + try { + final ObjectMapper objectMapper = new XmlMapper(); + final VmRestorePoints vmRestorePoints = objectMapper.readValue(content, VmRestorePoints.class); + if (vmRestorePoints == null) { + throw new CloudRuntimeException("Could not get VM restore points via Veeam B&R API"); + } + for (final VmRestorePoint vmRestorePoint : vmRestorePoints.getVmRestorePoints()) { + LOG.debug(String.format("Processing VM restore point Name=%s, VmDisplayName=%s for vm name=%s", + vmRestorePoint.getName(), vmRestorePoint.getVmDisplayName(), vmInternalName)); + if (!vmInternalName.equals(vmRestorePoint.getVmDisplayName())) { + continue; + } + boolean isReady = true; + List links = vmRestorePoint.getLink(); + for (Link link : links) { + if (Arrays.asList(BACKUP_FILE_REFERENCE, RESTORE_POINT_REFERENCE).contains(link.getType()) && !link.getRel().equals("Up")) { + LOG.info(String.format("The VM restore point is not ready. Reference: %s, state: %s", link.getType(), link.getRel())); + isReady = false; + break; + } + } + if (!isReady) { + continue; + } + String vmRestorePointId = vmRestorePoint.getUid().substring(vmRestorePoint.getUid().lastIndexOf(':') + 1); + String created = formatDate(vmRestorePoint.getCreationTimeUtc()); + String type = vmRestorePoint.getPointType(); + LOG.debug(String.format("Adding restore point %s, %s, %s", vmRestorePointId, created, type)); + vmRestorePointList.add(new Backup.RestorePoint(vmRestorePointId, created, type)); + } + } catch (final IOException | ParseException e) { + LOG.error("Failed to process response to get VM restore points via Veeam B&R API due to:", e); + checkResponseTimeOut(e); + } + return vmRestorePointList; + } + + private String formatDate(String date) throws ParseException { + return newDateFormat.format(dateFormat.parse(StringUtils.substring(date, 0, 23))); + } + public Pair restoreVMToDifferentLocation(String restorePointId, String hostIp, String dataStoreUuid) { final String restoreLocation = RESTORE_VM_SUFFIX + UUID.randomUUID().toString(); final String datastoreId = dataStoreUuid.replace("-",""); diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFile.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFile.java new file mode 100644 index 000000000000..2b28793b1fb6 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFile.java @@ -0,0 +1,160 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +import java.util.List; + +@JacksonXmlRootElement(localName = "BackupFile") +public class BackupFile { + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "FilePath") + private String filePath; + + @JacksonXmlProperty(localName = "BackupSize") + private String backupSize; + + @JacksonXmlProperty(localName = "DataSize") + private String dataSize; + + @JacksonXmlProperty(localName = "DeduplicationRatio") + private String deduplicationRatio; + + @JacksonXmlProperty(localName = "CompressRatio") + private String compressRatio; + + @JacksonXmlProperty(localName = "CreationTimeUtc") + private String creationTimeUtc; + + @JacksonXmlProperty(localName = "FileType") + private String fileType; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public String getBackupSize() { + return backupSize; + } + + public void setBackupSize(String backupSize) { + this.backupSize = backupSize; + } + + public String getDataSize() { + return dataSize; + } + + public void setDataSize(String dataSize) { + this.dataSize = dataSize; + } + + public String getDeduplicationRatio() { + return deduplicationRatio; + } + + public void setDeduplicationRatio(String deduplicationRatio) { + this.deduplicationRatio = deduplicationRatio; + } + + public String getCompressRatio() { + return compressRatio; + } + + public void setCompressRatio(String compressRatio) { + this.compressRatio = compressRatio; + } + + public String getCreationTimeUtc() { + return creationTimeUtc; + } + + public void setCreationTimeUtc(String creationTimeUtc) { + this.creationTimeUtc = creationTimeUtc; + } + + public String getFileType() { + return fileType; + } + + public void setFileType(String fileType) { + this.fileType = fileType; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFiles.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFiles.java new file mode 100644 index 000000000000..4ff7d0c088b7 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupFiles.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "BackupFiles") +public class BackupFiles { + @JacksonXmlProperty(localName = "BackupFile") + @JacksonXmlElementWrapper(localName = "BackupFile", useWrapping = false) + private List backupFiles; + + public List getBackupFiles() { + return backupFiles; + } + + public void setBackupFiles(List backupFiles) { + this.backupFiles = backupFiles; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java new file mode 100644 index 000000000000..edcf09f9e2f6 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +import java.util.List; + +@JacksonXmlRootElement(localName = "RestorePoint") +public class RestorePoint { + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "BackupDateUTC") + private String backupDateUTC; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getBackupDateUTC() { + return backupDateUTC; + } + + public void setBackupDateUTC(String backupDateUTC) { + this.backupDateUTC = backupDateUTC; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java new file mode 100644 index 000000000000..156363ec2fd8 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "RestorePoints") +public class RestorePoints { + @JacksonXmlProperty(localName = "RestorePoint") + @JacksonXmlElementWrapper(localName = "RestorePoint", useWrapping = false) + private List restorePoints; + + public List getRestorePoints() { + return restorePoints; + } + + public void setRestorePoints(List restorePoints) { + this.restorePoints = restorePoints; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoint.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoint.java new file mode 100644 index 000000000000..beaa11cd5d4d --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoint.java @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +import java.util.List; + +@JacksonXmlRootElement(localName = "VmRestorePoint") +public class VmRestorePoint { + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "VmDisplayName", isAttribute = true) + private String vmDisplayName; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "CreationTimeUTC") + private String creationTimeUtc; + + @JacksonXmlProperty(localName = "VmName") + private String vmName; + + @JacksonXmlProperty(localName = "Algorithm") + private String algorithm; + + @JacksonXmlProperty(localName = "PointType") + private String pointType; + + @JacksonXmlProperty(localName = "HierarchyObjRef") + private String hierarchyObjRef; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getVmDisplayName() { + return vmDisplayName; + } + + public void setVmDisplayName(String vmDisplayName) { + this.vmDisplayName = vmDisplayName; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getCreationTimeUtc() { + return creationTimeUtc; + } + + public void setCreationTimeUtc(String creationTimeUtc) { + this.creationTimeUtc = creationTimeUtc; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getPointType() { + return pointType; + } + + public void setPointType(String pointType) { + this.pointType = pointType; + } + + public String getHierarchyObjRef() { + return hierarchyObjRef; + } + + public void setHierarchyObjRef(String hierarchyObjRef) { + this.hierarchyObjRef = hierarchyObjRef; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoints.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoints.java new file mode 100644 index 000000000000..2b59a3ef23c0 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/VmRestorePoints.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "VmRestorePoints") +public class VmRestorePoints { + @JacksonXmlProperty(localName = "VmRestorePoint") + @JacksonXmlElementWrapper(localName = "VmRestorePoint", useWrapping = false) + private List VmRestorePoints; + + public List getVmRestorePoints() { + return VmRestorePoints; + } + + public void setVmRestorePoints(List vmRestorePoints) { + VmRestorePoints = vmRestorePoints; + } +} diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index 930c669f8f96..2b4f5498bfe1 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -27,9 +27,13 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.times; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.List; +import java.util.Map; +import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.backup.veeam.api.RestoreSession; import org.apache.http.HttpResponse; @@ -162,4 +166,296 @@ public void checkIfRestoreSessionFinishedTestTimeoutException() throws IOExcepti } Mockito.verify(mockClient, times(10)).get(Mockito.anyString()); } + + private void verifyBackupMetrics(Map metrics) { + Assert.assertEquals(2, metrics.size()); + + Assert.assertTrue(metrics.containsKey("d1bd8abd-fc73-4b77-9047-7be98a2ecb72")); + Assert.assertEquals(537776128L, (long) metrics.get("d1bd8abd-fc73-4b77-9047-7be98a2ecb72").getBackupSize()); + Assert.assertEquals(2147506644L, (long) metrics.get("d1bd8abd-fc73-4b77-9047-7be98a2ecb72").getDataSize()); + + Assert.assertTrue(metrics.containsKey("0d752ca6-d628-4d85-a739-75275e4661e6")); + Assert.assertEquals(1268682752L, (long) metrics.get("0d752ca6-d628-4d85-a739-75275e4661e6").getBackupSize()); + Assert.assertEquals(15624049921L, (long) metrics.get("0d752ca6-d628-4d85-a739-75275e4661e6").getDataSize()); + } + + @Test + public void testProcessPowerShellResultForBackupMetrics() { + String result = "i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98a2ecb72\r\n" + + "537776128\r\n" + + "2147506644\r\n" + + "=====\r\n" + + "i-13-22-VM-CSBKP-b3b3cb75-cfbf-4496-9c63-a08a93347276\r\n" + + "=====\r\n" + + "backup-job-based-on-sla\r\n" + + "=====\r\n" + + "i-12-20-VM-CSBKP-9f292f11-00ec-4915-84f0-e3895828640e\r\n" + + "=====\r\n" + + "i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\r\n" + + "1268682752\r\n" + + "15624049921\r\n" + + "=====\r\n"; + + Map metrics = client.processPowerShellResultForBackupMetrics(result); + + verifyBackupMetrics(metrics); + } + + @Test + public void testProcessHttpResponseForBackupMetricsForV11() { + String xmlResponse = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-28T000059_745D.vbk\n" + + " 579756032\n" + + " 7516219400\n" + + " 5.83\n" + + " 2.22\n" + + " 2023-10-27T23:00:13.74Z\n" + + " vbk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-05T000022_7987.vib\n" + + " 12083200\n" + + " 69232800\n" + + " 1\n" + + " 6.67\n" + + " 2023-11-05T00:00:22.827Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-01T000035_BEBF.vib\n" + + " 12398592\n" + + " 71329948\n" + + " 1\n" + + " 6.67\n" + + " 2023-11-01T00:00:35.163Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-04T000109_2AC1.vbk\n" + + " 581083136\n" + + " 7516219404\n" + + " 5.82\n" + + " 2.22\n" + + " 2023-11-04T00:00:24.973Z\n" + + " vbk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-29T000033_F468.vib\n" + + " 11870208\n" + + " 72378524\n" + + " 1\n" + + " 7.14\n" + + " 2023-10-28T23:00:33.233Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-30T000022_0CE3.vib\n" + + " 14409728\n" + + " 76572828\n" + + " 1\n" + + " 6.25\n" + + " 2023-10-30T00:00:22.7Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-06T000018_055B.vib\n" + + " 17883136\n" + + " 80767136\n" + + " 1\n" + + " 5\n" + + " 2023-11-06T00:00:18.253Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-02T000029_65BE.vib\n" + + " 12521472\n" + + " 72378525\n" + + " 1\n" + + " 6.67\n" + + " 2023-11-02T00:00:29.05Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98a2ecb72\\i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98aD2023-10-25T145951_8062.vbk\n" + + " 537776128\n" + + " 2147506644\n" + + " 1.68\n" + + " 2.38\n" + + " 2023-10-25T13:59:51.76Z\n" + + " vbk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-03T000024_7ACF.vib\n" + + " 14217216\n" + + " 76572832\n" + + " 1\n" + + " 6.25\n" + + " 2023-11-03T00:00:24.803Z\n" + + " vib\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-31T000015_4624.vib\n" + + " 12460032\n" + + " 72378524\n" + + " 1\n" + + " 6.67\n" + + " 2023-10-31T00:00:15.853Z\n" + + " vib\n" + + " \n" + + "\n"; + + InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); + Map metrics = client.processHttpResponseForBackupMetrics(inputStream); + + verifyBackupMetrics(metrics); + } + + @Test + public void testProcessHttpResponseForBackupMetricsForV12() { + String xmlResponse = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " V:\\Backup\\i-2-4-VM-CSBKP-506760dc-ed77-40d6-a91d-e0914e7a1ad8\\i-2-4-VM.vm-1036D2023-11-03T162535_89D6.vbk\n" + + " 535875584\n" + + " 2147507235\n" + + " 1.68\n" + + " 2.38\n" + + " 2023-11-03T16:25:35.920773Z\n" + + " vbk\n" + + " \n" + + ""; + + InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); + Map metrics = client.processHttpResponseForBackupMetrics(inputStream); + + Assert.assertEquals(1, metrics.size()); + + Assert.assertTrue(metrics.containsKey("506760dc-ed77-40d6-a91d-e0914e7a1ad8")); + Assert.assertEquals(535875584L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getBackupSize()); + Assert.assertEquals(2147507235L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getDataSize()); + } + + @Test + public void testProcessHttpResponseForVmRestorePoints() { + String xmlResponse = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2023-11-03T16:26:12.209913Z\n" + + " i-2-4-VM\n" + + " Full\n" + + " Full\n" + + " urn:VMware:Vm:adb5423b-b578-4c26-8ab8-cde9c1faec55.vm-1036\n" + + " \n" + + "\n"; + String vmName = "i-2-4-VM"; + + InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); + List vmRestorePointList = client.processHttpResponseForVmRestorePoints(inputStream, vmName); + + Assert.assertEquals(1, vmRestorePointList.size()); + Assert.assertEquals("f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977", vmRestorePointList.get(0).getId()); + Assert.assertEquals("2023-11-03 16:26:12", vmRestorePointList.get(0).getCreated()); + Assert.assertEquals("Full", vmRestorePointList.get(0).getType()); + } } diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index 54b6b7db8606..a7e322c69dfe 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -202,6 +202,11 @@ def test_vm_backup_lifecycle(self): self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") backup = backups[0] + # Stop VM + self.vm.stop(self.user_apiclient, forced=True) + # Restore backup + Backup.restoreVM(self.user_apiclient, backup.id) + # Delete backup Backup.delete(self.user_apiclient, backup.id, forced=True) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 4ed2a11e22a3..705f9fe40d77 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5991,7 +5991,7 @@ def create(self, apiclient, vmid): cmd = createBackup.createBackupCmd() cmd.virtualmachineid = vmid - return (apiclient.createBackup(cmd)) + return Backup(apiclient.createBackup(cmd).__dict__) @classmethod def delete(self, apiclient, id, forced=None): @@ -6012,11 +6012,12 @@ def list(self, apiclient, vmid): cmd.listall = True return (apiclient.listBackups(cmd)) - def restoreVM(self, apiclient): + @classmethod + def restoreVM(self, apiclient, backupid): """Restore VM from backup""" cmd = restoreBackup.restoreBackupCmd() - cmd.id = self.id + cmd.id = backupid return (apiclient.restoreBackup(cmd)) class ProjectRole: diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 316147faf3f9..cd011012c2a8 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -495,7 +495,8 @@ export default { show: (record) => { return record.state !== 'Destroyed' }, groupAction: true, popup: true, - groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }, + args: ['forced'] } ] } From b7f3252f954e93597d35fd9620b5314b9087aba5 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 7 Nov 2023 09:48:10 +0100 Subject: [PATCH 15/41] veeam: remove RestorePoint/RestorePoints class --- .../cloudstack/backup/veeam/VeeamClient.java | 45 --------- .../backup/veeam/api/RestorePoint.java | 94 ------------------- .../backup/veeam/api/RestorePoints.java | 39 -------- 3 files changed, 178 deletions(-) delete mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java delete mode 100644 plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 75bcb334c071..f72576e9b527 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -58,8 +58,6 @@ import org.apache.cloudstack.backup.veeam.api.ObjectInJob; import org.apache.cloudstack.backup.veeam.api.ObjectsInJob; import org.apache.cloudstack.backup.veeam.api.Ref; -import org.apache.cloudstack.backup.veeam.api.RestorePoint; -import org.apache.cloudstack.backup.veeam.api.RestorePoints; import org.apache.cloudstack.backup.veeam.api.RestoreSession; import org.apache.cloudstack.backup.veeam.api.Task; import org.apache.cloudstack.backup.veeam.api.VmRestorePoint; @@ -812,49 +810,6 @@ public List listRestorePoints(String backupName, String vmI } } - public List listRestorePointsViaVeeamAPI(String backupName, String vmInternalName) { - LOG.debug(String.format("Trying to list restore points via Veeam B&R API for backup %s of VM %s: ", backupName, vmInternalName)); - - try { - final HttpResponse response = get("/restorePoints?format=Entity"); - checkResponseOK(response); - return processHttpResponseForRestorePoints(response.getEntity().getContent(), backupName, vmInternalName); - } catch (final IOException e) { - LOG.error("Failed to list restore points via Veeam B&R API due to:", e); - checkResponseTimeOut(e); - } - return new ArrayList<>(); - } - - public List processHttpResponseForRestorePoints(InputStream content, String backupName, String vmInternalName) { - List restorePointList = new ArrayList<>(); - try { - final ObjectMapper objectMapper = new XmlMapper(); - final RestorePoints restorePoints = objectMapper.readValue(content, RestorePoints.class); - if (restorePoints == null) { - throw new CloudRuntimeException("Could not get restore points via Veeam B&R API"); - } - for (final RestorePoint restorePoint : restorePoints.getRestorePoints()) { - String linkName = null; - List links = restorePoint.getLink(); - for (Link link : links) { - if (BACKUP_REFERENCE.equals(link.getType())) { - linkName = link.getName(); - break; - } - } - if (linkName != null && linkName.startsWith(backupName)) { - String restorePointId = restorePoint.getUid().substring(restorePoint.getUid().lastIndexOf(':') + 1); - restorePointList.addAll(listVmRestorePointsViaVeeamAPI(vmInternalName)); - } - } - } catch (final IOException e) { - LOG.error("Failed to process response to get restore points via Veeam B&R API due to:", e); - checkResponseTimeOut(e); - } - return restorePointList; - } - public List listVmRestorePointsViaVeeamAPI(String vmInternalName) { LOG.debug(String.format("Trying to list VM restore points via Veeam B&R API for VM %s: ", vmInternalName)); diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java deleted file mode 100644 index edcf09f9e2f6..000000000000 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoint.java +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.backup.veeam.api; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; - -import java.util.List; - -@JacksonXmlRootElement(localName = "RestorePoint") -public class RestorePoint { - @JacksonXmlProperty(localName = "Type", isAttribute = true) - private String type; - - @JacksonXmlProperty(localName = "Href", isAttribute = true) - private String href; - - @JacksonXmlProperty(localName = "Name", isAttribute = true) - private String name; - - @JacksonXmlProperty(localName = "UID", isAttribute = true) - private String uid; - - @JacksonXmlProperty(localName = "Link") - @JacksonXmlElementWrapper(localName = "Links") - private List link; - - @JacksonXmlProperty(localName = "BackupDateUTC") - private String backupDateUTC; - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getHref() { - return href; - } - - public void setHref(String href) { - this.href = href; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getUid() { - return uid; - } - - public void setUid(String uid) { - this.uid = uid; - } - - public List getLink() { - return link; - } - - public void setLink(List link) { - this.link = link; - } - - public String getBackupDateUTC() { - return backupDateUTC; - } - - public void setBackupDateUTC(String backupDateUTC) { - this.backupDateUTC = backupDateUTC; - } -} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java deleted file mode 100644 index 156363ec2fd8..000000000000 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestorePoints.java +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.backup.veeam.api; - -import java.util.List; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; - -@JacksonXmlRootElement(localName = "RestorePoints") -public class RestorePoints { - @JacksonXmlProperty(localName = "RestorePoint") - @JacksonXmlElementWrapper(localName = "RestorePoint", useWrapping = false) - private List restorePoints; - - public List getRestorePoints() { - return restorePoints; - } - - public void setRestorePoints(List restorePoints) { - this.restorePoints = restorePoints; - } -} From a457a11dbb9a5642eedb80bc2a6232b3e978352a Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 7 Nov 2023 10:46:38 +0100 Subject: [PATCH 16/41] veeam: add integration/marvin for backup schedule --- .../smoke/test_backup_recovery_veeam.py | 22 ++++++++- tools/marvin/marvin/lib/base.py | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index a7e322c69dfe..81ac0b854b0f 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -18,7 +18,7 @@ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.utils import (cleanup_resources, wait_until) -from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup) +from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup, BackupSchedule) from marvin.lib.common import (get_domain, get_zone, get_template) from nose.plugins.attrib import attr from marvin.codes import FAILED @@ -193,6 +193,26 @@ def test_vm_backup_lifecycle(self): self.assertEqual(1, len(vms), "List of the virtual machines should have 1 vm") self.assertEqual(offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % offering.id) + # Create backup schedule on 01:00AM every Sunday + BackupSchedule.create(self.user_apiclient, self.vm.id, intervaltype="WEEKLY", timezone="CET", schedule="00:01:1") + backupSchedule = BackupSchedule.list(self.user_apiclient, self.vm.id) + self.assertIsNotNone(backupSchedule) + self.assertEqual("WEEKLY", backupSchedule.intervaltype) + self.assertEqual("00:01:1", backupSchedule.schedule) + self.assertEqual("CET", backupSchedule.timezone) + self.assertEqual(self.vm.id, backupSchedule.virtualmachineid) + self.assertEqual(self.vm.name, backupSchedule.virtualmachinename) + + # Update backup schedule on 02:00AM every 20th + BackupSchedule.update(self.user_apiclient, self.vm.id, intervaltype="MONTHLY", timezone="CET", schedule="00:02:20") + backupSchedule = BackupSchedule.list(self.user_apiclient, self.vm.id) + self.assertIsNotNone(backupSchedule) + self.assertEqual("MONTHLY", backupSchedule.intervaltype) + self.assertEqual("00:02:20", backupSchedule.schedule) + + # Delete backup schedule + BackupSchedule.delete(self.user_apiclient, self.vm.id) + # Create backup Backup.create(self.user_apiclient, self.vm.id) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 705f9fe40d77..c86aa613df82 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5914,8 +5914,10 @@ def delete(self, apiclient, resourceid, resourcetype): cmd.resourcetype = resourcetype return (apiclient.removeResourceDetail(cmd)) + # Backup and Recovery + class BackupOffering: def __init__(self, items): @@ -5980,6 +5982,7 @@ def removeOffering(self, apiclient, vmid, forced=True): cmd.forced = forced return (apiclient.removeVirtualMachineFromBackupOffering(cmd)) + class Backup: def __init__(self, items): @@ -6020,6 +6023,48 @@ def restoreVM(self, apiclient, backupid): cmd.id = backupid return (apiclient.restoreBackup(cmd)) + +class BackupSchedule: + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(self, apiclient, vmid, **kwargs): + """Create VM backup schedule""" + + cmd = createBackupSchedule.createBackupScheduleCmd() + cmd.virtualmachineid = vmid + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return BackupSchedule(apiclient.createBackupSchedule(cmd).__dict__) + + @classmethod + def delete(self, apiclient, vmid): + """Delete VM backup schedule""" + + cmd = deleteBackupSchedule.deleteBackupScheduleCmd() + cmd.virtualmachineid = vmid + return (apiclient.deleteBackupSchedule(cmd)) + + @classmethod + def list(self, apiclient, vmid): + """List VM backup schedule""" + + cmd = listBackupSchedule.listBackupScheduleCmd() + cmd.virtualmachineid = vmid + cmd.listall = True + return (apiclient.listBackupSchedule(cmd)) + + @classmethod + def update(self, apiclient, vmid, **kwargs): + """Update VM backup schedule""" + + cmd = updateBackupSchedule.updateBackupScheduleCmd() + cmd.virtualmachineid = vmid + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return (apiclient.updateBackupSchedule(cmd)) + + class ProjectRole: def __init__(self, items): From bd8d331aad30a1723354904145653f65b3b2b89f Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 14 Nov 2023 14:06:16 +0100 Subject: [PATCH 17/41] veeam: add integration/marvin for restoreVolumeFromBackupAndAttachToVM --- .../smoke/test_backup_recovery_veeam.py | 164 ++++++++++++------ tools/marvin/marvin/lib/base.py | 10 ++ 2 files changed, 123 insertions(+), 51 deletions(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index 81ac0b854b0f..d6a0b97a3e12 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -17,8 +17,9 @@ # under the License. from marvin.cloudstackTestCase import cloudstackTestCase -from marvin.lib.utils import (cleanup_resources, wait_until) -from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup, BackupSchedule) +from marvin.lib.utils import wait_until +from marvin.lib.base import (Account, ServiceOffering, DiskOffering, Volume, VirtualMachine, + BackupOffering, Configurations, Backup, BackupSchedule) from marvin.lib.common import (get_domain, get_zone, get_template) from nose.plugins.attrib import attr from marvin.codes import FAILED @@ -60,7 +61,8 @@ def setUpClass(cls): return cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["small"]) - cls._cleanup = [cls.service_offering] + cls.disk_offering = DiskOffering.create(cls.apiclient, cls.services["disk_offering"]) + cls._cleanup = [cls.service_offering, cls.disk_offering] @classmethod def isBackupOfferingUsed(cls, existing_offerings, provider_offering): @@ -95,17 +97,8 @@ def setUp(self): raise self.skipTest("Skipping test cases which must only run for VMware") self.cleanup = [] - def tearDown(self): - super(TestVeeamBackupAndRecovery, self).tearDown() - - @attr(tags=["advanced", "backup"], required_hardware="false") - def test_import_backup_offering(self): - """ - Import provider backup offering from Veeam Backup and Recovery Provider - """ - # Import backup offering - offering = None + self.offering = None existing_offerings = BackupOffering.listByZone(self.apiclient, self.zone.id) provider_offerings = BackupOffering.listExternal(self.apiclient, self.zone.id) if not provider_offerings: @@ -113,63 +106,57 @@ def test_import_backup_offering(self): for provider_offering in provider_offerings: if not self.isBackupOfferingUsed(existing_offerings, provider_offering): self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) - offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, + self.offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, provider_offering.name, provider_offering.description) - if not offering: + if not self.offering: self.fail("Failed to import backup offering %s" % provider_offering.name) break - if not offering: + if not self.offering: self.skipTest("Skipping test cases as there is no available provider offerings to import") + # Create user account + self.account = Account.create(self.apiclient, self.services["account"], domainid=self.domain.id) + self.user_user = self.account.user[0] + self.user_apiclient = self.testClient.getUserApiClient( + self.user_user.username, self.domain.name + ) + self.cleanup.append(self.account) + + def tearDown(self): + super(TestVeeamBackupAndRecovery, self).tearDown() + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_01_import_list_delete_backup_offering(self): + """ + Import provider backup offering from Veeam Backup and Recovery Provider + """ + # Verify offering is listed by user - imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response") - matching_offerings = [x for x in imported_offering if x.id == offering.id] + matching_offerings = [x for x in imported_offering if x.id == self.offering.id] self.assertNotEqual(len(matching_offerings), 0, "Check if there is a matching offering") # Delete backup offering - self.debug("Deleting backup offering %s" % offering.id) - offering.delete(self.apiclient) + self.debug("Deleting backup offering %s" % self.offering.id) + self.offering.delete(self.apiclient) # Verify offering is not listed by user - imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id) if imported_offering: self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") - matching_offerings = [x for x in imported_offering if x.id == offering.id] + matching_offerings = [x for x in imported_offering if x.id == self.offering.id] self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering") @attr(tags=["advanced", "backup"], required_hardware="false") - def test_vm_backup_lifecycle(self): + def test_02_vm_backup_lifecycle(self): """ Test VM backup lifecycle """ - # Create user account - self.account = Account.create(self.apiclient, self.services["account"], domainid=self.domain.id) - self.user_user = self.account.user[0] - self.user_apiclient = self.testClient.getUserApiClient( - self.user_user.username, self.domain.name - ) - self.cleanup = [self.account] - - # Import backup offering - offering = None - existing_offerings = BackupOffering.listByZone(self.apiclient, self.zone.id) - provider_offerings = BackupOffering.listExternal(self.apiclient, self.zone.id) - if not provider_offerings: - self.skipTest("Skipping test cases as the provider offering is None") - for provider_offering in provider_offerings: - if not self.isBackupOfferingUsed(existing_offerings, provider_offering): - self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) - offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, - provider_offering.name, provider_offering.description) - if not offering: - self.fail("Failed to import backup offering %s" % provider_offering.name) - self.cleanup.insert(0, offering) - break - if not offering: - self.skipTest("Skipping test cases as there is no available provider offerings to import") + if self.offering: + self.cleanup.insert(0, self.offering) self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name, domainid=self.account.domainid, serviceofferingid=self.service_offering.id) @@ -179,7 +166,7 @@ def test_vm_backup_lifecycle(self): self.assertEqual(backups, None, "There should not exist any backup for the VM") # Assign VM to offering and create ad-hoc backup - offering.assignOffering(self.user_apiclient, self.vm.id) + self.offering.assignOffering(self.user_apiclient, self.vm.id) vms = VirtualMachine.list( self.user_apiclient, id=self.vm.id, @@ -191,7 +178,7 @@ def test_vm_backup_lifecycle(self): "List virtual machines should return a valid list" ) self.assertEqual(1, len(vms), "List of the virtual machines should have 1 vm") - self.assertEqual(offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % offering.id) + self.assertEqual(self.offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % self.offering.id) # Create backup schedule on 01:00AM every Sunday BackupSchedule.create(self.user_apiclient, self.vm.id, intervaltype="WEEKLY", timezone="CET", schedule="00:01:1") @@ -235,4 +222,79 @@ def test_vm_backup_lifecycle(self): self.assertEqual(backups, None, "There should not exist any backup for the VM") # Remove VM from offering - offering.removeOffering(self.user_apiclient, self.vm.id) \ No newline at end of file + self.offering.removeOffering(self.user_apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_03_restore_volume_attach_vm(self): + """ + Test Volume Restore from Backup and Attach to VM + """ + + if self.offering: + self.cleanup.insert(0, self.offering) + + self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name, + domainid=self.account.domainid, serviceofferingid=self.service_offering.id) + + self.vm_with_datadisk = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name, + domainid=self.account.domainid, serviceofferingid=self.service_offering.id, + diskofferingid=self.disk_offering.id) + + # Assign VM to offering and create ad-hoc backup + self.offering.assignOffering(self.user_apiclient, self.vm_with_datadisk.id) + + # Create backup + Backup.create(self.user_apiclient, self.vm_with_datadisk.id) + + # Verify backup is created for the VM with datadisk + self.waitForBackUp(self.vm_with_datadisk) + backups = Backup.list(self.user_apiclient, self.vm_with_datadisk.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM with datadisk") + backup = backups[0] + + try: + volumes = Volume.list( + self.user_apiclient, + virtualmachineid=self.vm_with_datadisk.id, + listall=True + ) + rootDiskId = None + dataDiskId = None + for volume in volumes: + if volume.type == 'ROOT': + rootDiskId = volume.id + elif volume.type == 'DATADISK': + dataDiskId = volume.id + if rootDiskId: + # Restore ROOT volume of vm_with_datadisk and attach to vm + Backup.restoreVolumeFromBackupAndAttachToVM( + self.user_apiclient, + backupid=backup.id, + volumeid=rootDiskId, + virtualmachineid=self.vm.id + ) + vm_volumes = Volume.list( + self.user_apiclient, + virtualmachineid=self.vm.id, + listall=True + ) + self.assertTrue(isinstance(vm_volumes, list), "List volumes should return a valid list") + self.assertEqual(2, len(vm_volumes), "The number of volumes should be 2") + if dataDiskId: + # Restore DATADISK volume of vm_with_datadisk and attach to vm + Backup.restoreVolumeFromBackupAndAttachToVM( + self.user_apiclient, + backupid=backup.id, + volumeid=dataDiskId, + virtualmachineid=self.vm.id + ) + vm_volumes = Volume.list( + self.user_apiclient, + virtualmachineid=self.vm.id, + listall=True + ) + self.assertTrue(isinstance(vm_volumes, list), "List volumes should return a valid list") + self.assertEqual(3, len(vm_volumes), "The number of volumes should be 2") + finally: + # Delete backup + Backup.delete(self.user_apiclient, backup.id, forced=True) \ No newline at end of file diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index c86aa613df82..9693d20ac489 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -6023,6 +6023,16 @@ def restoreVM(self, apiclient, backupid): cmd.id = backupid return (apiclient.restoreBackup(cmd)) + @classmethod + def restoreVolumeFromBackupAndAttachToVM(self, apiclient, backupid, volumeid, virtualmachineid): + """Restore VM from backup""" + + cmd = restoreVolumeFromBackupAndAttachToVM.restoreVolumeFromBackupAndAttachToVMCmd() + cmd.backupid = backupid + cmd.volumeid = volumeid + cmd.virtualmachineid = virtualmachineid + return (apiclient.restoreVolumeFromBackupAndAttachToVM(cmd)) + class BackupSchedule: From 82337b1a5c5a555aec6c3cfebb6cecb5dfb51c8f Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 14 Nov 2023 14:24:03 +0100 Subject: [PATCH 18/41] veeam: fix EOF test/integration/smoke/test_backup_recovery_veeam.py --- test/integration/smoke/test_backup_recovery_veeam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index d6a0b97a3e12..39773d9d41fa 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -297,4 +297,4 @@ def test_03_restore_volume_attach_vm(self): self.assertEqual(3, len(vm_volumes), "The number of volumes should be 2") finally: # Delete backup - Backup.delete(self.user_apiclient, backup.id, forced=True) \ No newline at end of file + Backup.delete(self.user_apiclient, backup.id, forced=True) From 706ab9e0ba585f3dca3a76f802c6066e598811f3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 15 Nov 2023 08:41:29 +0100 Subject: [PATCH 19/41] veeam: consider that powershell commands return null --- .../org/apache/cloudstack/backup/veeam/VeeamClient.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index f72576e9b527..216a5e78d01c 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -609,7 +609,7 @@ public boolean setJobSchedule(final String jobName) { String.format("$job = Get-VBRJob -Name '%s'", jobName), "if ($job) { Set-VBRJobSchedule -Job $job -Daily -At \"11:00\" -DailyKind Weekdays }" )); - return result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE); + return result != null && result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE); } public boolean deleteJobAndBackup(final String jobName) { @@ -621,7 +621,7 @@ public boolean deleteJobAndBackup(final String jobName) { "$repo = Get-VBRBackupRepository", "Sync-VBRBackupRepository -Repository $repo" )); - return result.first() && !result.second().contains(FAILED_TO_DELETE); + return result != null && result.first() && !result.second().contains(FAILED_TO_DELETE); } public boolean deleteBackup(final String restorePointId) { @@ -636,7 +636,7 @@ public boolean deleteBackup(final String restorePointId) { " Exit 1", "}" )); - return result.first() && !result.second().contains(FAILED_TO_DELETE); + return result != null && result.first() && !result.second().contains(FAILED_TO_DELETE); } public Map getBackupMetrics() { @@ -737,6 +737,9 @@ public Map getBackupMetricsLegacy() { "}" ); Pair response = executePowerShellCommands(cmds); + if (response == null || !response.first()) { + throw new CloudRuntimeException("Failed to get backup metrics via PowerShell command"); + } return processPowerShellResultForBackupMetrics(response.second()); } From 2a1b23dedf97e0408dd10145f34ff3d3736ce156 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 15 Nov 2023 08:43:31 +0100 Subject: [PATCH 20/41] veeam: update default veeam.version to null and use VeeamAPI by default If user uses a older version (e.g 9.x), please update the backup.plugin.veeam.version (zone setting) to 9 --- .../org/apache/cloudstack/backup/VeeamBackupProvider.java | 2 +- .../java/org/apache/cloudstack/backup/veeam/VeeamClient.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 30b60e87e490..a50b04a5b07a 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -65,7 +65,7 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider, "The Veeam backup and recovery URL.", true, ConfigKey.Scope.Zone); public ConfigKey VeeamVersion = new ConfigKey<>("Advanced", Integer.class, - "backup.plugin.veeam.version", "9", + "backup.plugin.veeam.version", "", "The version of Veeam backup and recovery.", true, ConfigKey.Scope.Zone); private ConfigKey VeeamUsername = new ConfigKey<>("Advanced", String.class, diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 216a5e78d01c..db76ef38d62a 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -571,7 +571,7 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { + if (this.veeamServerVersion != null || this.veeamServerVersion < 11) { joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); } else { joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); @@ -640,7 +640,7 @@ public boolean deleteBackup(final String restorePointId) { } public Map getBackupMetrics() { - if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { + if (this.veeamServerVersion != null || this.veeamServerVersion < 11) { return getBackupMetricsLegacy(); } else { return getBackupMetricsViaVeeamAPI(); From c91cd61a75052f37f9ffe330371b72c6eba0e934 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 15 Nov 2023 08:45:27 +0100 Subject: [PATCH 21/41] veeam: update integration test as Daan comments --- test/integration/smoke/test_backup_recovery_veeam.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index 39773d9d41fa..aacc82185cd6 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -61,8 +61,9 @@ def setUpClass(cls): return cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["small"]) + cls._cleanup.append(cls.service_offering) cls.disk_offering = DiskOffering.create(cls.apiclient, cls.services["disk_offering"]) - cls._cleanup = [cls.service_offering, cls.disk_offering] + cls._cleanup.append(cls.disk_offering) @classmethod def isBackupOfferingUsed(cls, existing_offerings, provider_offering): From 1d6d9d51ef9e9f52c1765dfa3ece50dbd85e71c9 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 16 Nov 2023 09:35:46 +0100 Subject: [PATCH 22/41] veeam: reformat commands in getBackupMetricsLegacy method --- .../cloudstack/backup/veeam/VeeamClient.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index db76ef38d62a..4d3f357132e7 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -717,23 +717,23 @@ public Map getBackupMetricsLegacy() { final List cmds = Arrays.asList( "$backups = Get-VBRBackup", "foreach ($backup in $backups) {" + - "$backup.JobName;" + - "$storageGroups = $backup.GetStorageGroups();" + - "foreach ($group in $storageGroups) {" + - "$usedSize = 0;" + - "$dataSize = 0;" + - "$sizePerStorage = $group.GetStorages().Stats.BackupSize;" + - "$dataPerStorage = $group.GetStorages().Stats.DataSize;" + - "foreach ($size in $sizePerStorage) {" + - "$usedSize += $size;" + - "}" + - "foreach ($size in $dataPerStorage) {" + - "$dataSize += $size;" + - "}" + - "$usedSize;" + - "$dataSize;" + - "}" + - "echo \"" + separator + "\"" + + " $backup.JobName;" + + " $storageGroups = $backup.GetStorageGroups();" + + " foreach ($group in $storageGroups) {" + + " $usedSize = 0;" + + " $dataSize = 0;" + + " $sizePerStorage = $group.GetStorages().Stats.BackupSize;" + + " $dataPerStorage = $group.GetStorages().Stats.DataSize;" + + " foreach ($size in $sizePerStorage) {" + + " $usedSize += $size;" + + " }" + + " foreach ($size in $dataPerStorage) {" + + " $dataSize += $size;" + + " }" + + " $usedSize;" + + " $dataSize;" + + " }" + + " echo \"" + separator + "\"" + "}" ); Pair response = executePowerShellCommands(cmds); From 33fccda0aa33c46047e1a3990b3d65f08878668a Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 16 Nov 2023 11:44:27 +0100 Subject: [PATCH 23/41] Veeam: get server version via PS commands Refer to https://xkln.net/blog/getting-the-veeam-br-version-and-patch-number-with-powershell/ --- .../backup/VeeamBackupProvider.java | 2 +- .../cloudstack/backup/veeam/VeeamClient.java | 37 ++++++++++++++++--- .../backup/veeam/VeeamClientTest.java | 2 +- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index a50b04a5b07a..17dddc5d1a82 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -96,7 +96,7 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider, protected VeeamClient getClient(final Long zoneId) { try { - return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamVersion.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), + return new VeeamClient(VeeamUrl.valueIn(zoneId), null, VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId), VeeamRestoreTimeout.valueIn(zoneId)); } catch (URISyntaxException e) { throw new CloudRuntimeException("Failed to parse Veeam API URL: " + e.getMessage()); diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 4d3f357132e7..e9a2957d468d 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -79,6 +79,7 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.log4j.Logger; +import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.nio.TrustAllManager; @@ -108,11 +109,11 @@ public class VeeamClient { private String veeamServerIp; - private Integer veeamServerVersion; + private final Integer veeamServerVersion; private String veeamServerUsername; private String veeamServerPassword; private String veeamSessionId = null; - private int restoreTimeout; + private final int restoreTimeout; private final int veeamServerPort = 22; public VeeamClient(final String url, final Integer version, final String username, final String password, final boolean validateCertificate, final int timeout, @@ -140,9 +141,9 @@ public VeeamClient(final String url, final Integer version, final String usernam .build(); } - this.veeamServerVersion = version; authenticate(username, password); setVeeamSshCredentials(this.apiURI.getHost(), username, password); + this.veeamServerVersion = version != null ? version : getVeeamServerVersion(); } protected void setVeeamSshCredentials(String hostIp, String username, String password) { @@ -176,6 +177,26 @@ private void checkAuthFailure(final HttpResponse response) { } } + private Integer getVeeamServerVersion() { + final List cmds = Arrays.asList( + "$InstallPath = Get-ItemProperty -Path 'HKLM:\\Software\\Veeam\\Veeam Backup and Replication\\' ^| Select -ExpandProperty CorePath", + "Add-Type -LiteralPath \\\"$InstallPath\\Veeam.Backup.Configuration.dll\\\"", + "$ProductData = [Veeam.Backup.Configuration.BackupProduct]::Create()", + "$Version = $ProductData.ProductVersion.ToString()", + "if ($ProductData.MarketName -ne '') {$Version += \\\" $($ProductData.MarketName)\\\"}", + "$Version" + ); + Pair response = executePowerShellCommands(cmds); + if (response == null || !response.first() || response.second() == null || StringUtils.isBlank(response.second().trim())) { + LOG.error("Failed to get veeam server version, using default version"); + return 0; + } else { + Integer majorVersion = NumbersUtil.parseInt(response.second().trim().split("\\.")[0], 0); + LOG.info(String.format("Veeam server full version is %s, major version is %s", response.second().trim(), majorVersion)); + return majorVersion; + } + } + private void checkResponseOK(final HttpResponse response) { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) { LOG.debug("Requested Veeam resource does not exist"); @@ -571,7 +592,7 @@ public boolean restoreFullVM(final String vmwareInstanceName, final String resto */ protected String transformPowerShellCommandList(List cmds) { StringJoiner joiner = new StringJoiner(";"); - if (this.veeamServerVersion != null || this.veeamServerVersion < 11) { + if (isLegacyServer()) { joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); } else { joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue"); @@ -640,7 +661,7 @@ public boolean deleteBackup(final String restorePointId) { } public Map getBackupMetrics() { - if (this.veeamServerVersion != null || this.veeamServerVersion < 11) { + if (isLegacyServer()) { return getBackupMetricsLegacy(); } else { return getBackupMetricsViaVeeamAPI(); @@ -806,7 +827,7 @@ public List listRestorePointsLegacy(String backupName, Stri } public List listRestorePoints(String backupName, String vmInternalName) { - if (this.veeamServerVersion == null || this.veeamServerVersion < 11) { + if (isLegacyServer()) { return listRestorePointsLegacy(backupName, vmInternalName); } else { return listVmRestorePointsViaVeeamAPI(vmInternalName); @@ -887,4 +908,8 @@ public Pair restoreVMToDifferentLocation(String restorePointId, } return new Pair<>(result.first(), restoreLocation); } + + private boolean isLegacyServer() { + return this.veeamServerVersion != null && (this.veeamServerVersion > 0 && this.veeamServerVersion < 11); + } } diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index 2b4f5498bfe1..527fecff2d8e 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -66,7 +66,7 @@ public void setUp() throws Exception { .withStatus(201) .withHeader("X-RestSvcSessionId", "some-session-auth-id") .withBody(""))); - client = new VeeamClient("http://localhost:9399/api/", 9, adminUsername, adminPassword, true, 60, 600); + client = new VeeamClient("http://localhost:9399/api/", 12, adminUsername, adminPassword, true, 60, 600); mockClient = Mockito.mock(VeeamClient.class); Mockito.when(mockClient.getRepositoryNameFromJob(Mockito.anyString())).thenCallRealMethod(); } From 7473e647eaeee1fce7f0649b5389dd6cf2a09cc8 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 16 Nov 2023 12:57:39 +0100 Subject: [PATCH 24/41] veeam: honor global/zone setting backup.plugin.veeam.version --- .../java/org/apache/cloudstack/backup/VeeamBackupProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 17dddc5d1a82..a50b04a5b07a 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -96,7 +96,7 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider, protected VeeamClient getClient(final Long zoneId) { try { - return new VeeamClient(VeeamUrl.valueIn(zoneId), null, VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), + return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamVersion.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId), VeeamRestoreTimeout.valueIn(zoneId)); } catch (URISyntaxException e) { throw new CloudRuntimeException("Failed to parse Veeam API URL: " + e.getMessage()); From ea5934941682822756c8cdc5f58971954a5a2e31 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 16 Nov 2023 14:08:07 +0100 Subject: [PATCH 25/41] Veeam: add unit tests for getVeeamServerVersion --- .../cloudstack/backup/veeam/VeeamClient.java | 2 +- .../backup/veeam/VeeamClientTest.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index e9a2957d468d..e138bc8b40e6 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -177,7 +177,7 @@ private void checkAuthFailure(final HttpResponse response) { } } - private Integer getVeeamServerVersion() { + protected Integer getVeeamServerVersion() { final List cmds = Arrays.asList( "$InstallPath = Get-ItemProperty -Path 'HKLM:\\Software\\Veeam\\Veeam Backup and Replication\\' ^| Select -ExpandProperty CorePath", "Add-Type -LiteralPath \\\"$InstallPath\\Veeam.Backup.Configuration.dll\\\"", diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index 527fecff2d8e..a1734829c0c3 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -69,6 +69,7 @@ public void setUp() throws Exception { client = new VeeamClient("http://localhost:9399/api/", 12, adminUsername, adminPassword, true, 60, 600); mockClient = Mockito.mock(VeeamClient.class); Mockito.when(mockClient.getRepositoryNameFromJob(Mockito.anyString())).thenCallRealMethod(); + Mockito.when(mockClient.getVeeamServerVersion()).thenCallRealMethod(); } @Test @@ -458,4 +459,25 @@ public void testProcessHttpResponseForVmRestorePoints() { Assert.assertEquals("2023-11-03 16:26:12", vmRestorePointList.get(0).getCreated()); Assert.assertEquals("Full", vmRestorePointList.get(0).getType()); } + + @Test + public void testGetVeeamServerVersionAllGood() { + Pair response = new Pair(Boolean.TRUE, "12.0.0.1"); + Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList()); + Assert.assertEquals(12, (int) mockClient.getVeeamServerVersion()); + } + + @Test + public void testGetVeeamServerVersionWithError() { + Pair response = new Pair(Boolean.FALSE, ""); + Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList()); + Assert.assertEquals(0, (int) mockClient.getVeeamServerVersion()); + } + + @Test + public void testGetVeeamServerVersionWithEmptyVersion() { + Pair response = new Pair(Boolean.TRUE, ""); + Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList()); + Assert.assertEquals(0, (int) mockClient.getVeeamServerVersion()); + } } From 722b35e55c9acfa4b91af2f07575ea432009c3b3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 16 Nov 2023 16:15:38 +0100 Subject: [PATCH 26/41] plugins/pom.xml: remove backup/veeam as it depends on cloud-plugin-hypervisor-vmware --- plugins/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/pom.xml b/plugins/pom.xml index 1d6fd531c6f8..90dc95b82342 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -62,7 +62,6 @@ backup/dummy backup/networker - backup/veeam ca/root-ca From 617a4c25780ca8e5cff51b73fbd5dedbd8d65b76 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 8 Dec 2023 11:08:23 +0100 Subject: [PATCH 27/41] Veeam: update unit tests --- .../backup/veeam/VeeamClientTest.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index a1734829c0c3..bbf313543cc1 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -391,7 +391,7 @@ public void testProcessHttpResponseForBackupMetricsForV11() { } @Test - public void testProcessHttpResponseForBackupMetricsForV12() { + public void testGetBackupMetricsViaVeeamAPI() { String xmlResponse = "\n" + "\n" + ""; - InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); - Map metrics = client.processHttpResponseForBackupMetrics(inputStream); + wireMockRule.stubFor(get(urlMatching(".*/backupFiles\\?format=Entity")) + .willReturn(aResponse() + .withHeader("content-type", "application/xml") + .withStatus(200) + .withBody(xmlResponse))); + Map metrics = client.getBackupMetricsViaVeeamAPI(); Assert.assertEquals(1, metrics.size()); - Assert.assertTrue(metrics.containsKey("506760dc-ed77-40d6-a91d-e0914e7a1ad8")); Assert.assertEquals(535875584L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getBackupSize()); Assert.assertEquals(2147507235L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getDataSize()); } @Test - public void testProcessHttpResponseForVmRestorePoints() { + public void testListVmRestorePointsViaVeeamAPI() { String xmlResponse = "\n" + "\n"; String vmName = "i-2-4-VM"; - InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); - List vmRestorePointList = client.processHttpResponseForVmRestorePoints(inputStream, vmName); + wireMockRule.stubFor(get(urlMatching(".*/vmRestorePoints\\?format=Entity")) + .willReturn(aResponse() + .withHeader("content-type", "application/xml") + .withStatus(200) + .withBody(xmlResponse))); + List vmRestorePointList = client.listVmRestorePointsViaVeeamAPI(vmName); Assert.assertEquals(1, vmRestorePointList.size()); Assert.assertEquals("f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977", vmRestorePointList.get(0).getId()); From ea84af9af820d71423b63b945207e788a6eedce5 Mon Sep 17 00:00:00 2001 From: SadiJr Date: Mon, 3 Jul 2023 09:35:48 -0300 Subject: [PATCH 28/41] [Veeam] restored VMs without NICs (#6282) --- .../src/main/java/com/cloud/vm/NicVO.java | 12 ++++++++++++ .../src/main/java/com/cloud/vm/dao/NicDao.java | 2 ++ .../main/java/com/cloud/vm/dao/NicDaoImpl.java | 8 ++++++++ .../com/cloud/hypervisor/guru/VMwareGuru.java | 17 ++++++++++++++--- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index 8905ebf732b1..fba7c966c442 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -30,6 +30,9 @@ import javax.persistence.Table; import javax.persistence.Transient; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + import com.cloud.network.Networks.AddressFormat; import com.cloud.network.Networks.Mode; import com.cloud.utils.db.GenericDao; @@ -399,6 +402,15 @@ public void setNsxLogicalSwitchPortUuid(String nsxLogicalSwitchPortUuid) { } @Override + public int hashCode() { + return new HashCodeBuilder(17, 31).append(id).toHashCode(); + } + + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } + public Integer getMtu() { return mtu; } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index c52c690d8b54..22165641f7ea 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -89,6 +89,8 @@ public interface NicDao extends GenericDao { NicVO findByMacAddress(String macAddress); + NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac); + List findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName); NicVO findByIpAddressAndVmType(String ip, VirtualMachine.Type vmType); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index c8efc074a106..211e54804238 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -217,6 +217,14 @@ public NicVO findByNetworkIdAndMacAddress(long networkId, String mac) { return findOneBy(sc); } + @Override + public NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("network", networkId); + sc.setParameters("macAddress", mac); + return findOneIncludingRemovedBy(sc); + } + @Override public NicVO findDefaultNicForVM(long instanceId) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index db41ab19d566..aa3b314fb3fc 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -781,8 +781,7 @@ private void syncVMVolumes(VMInstanceVO vmInstanceVO, List virtualD volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true); operation = "created"; } - s_logger.debug(String.format("VM [id: %s, instanceName: %s] backup restore operation %s volume [id: %s].", instanceId, vmInstanceVO.getInstanceName(), - operation, volume.getUuid())); + s_logger.debug(String.format("Sync volumes to %s in backup restore operation: %s volume [id: %s].", vmInstanceVO, operation, volume.getUuid())); } } @@ -879,9 +878,13 @@ private NetworkVO getGuestNetworkFromNetworkMorName(String name, long accountId, String tag = parts[parts.length - 1]; String[] tagSplit = tag.split("-"); tag = tagSplit[tagSplit.length - 1]; + + s_logger.debug(String.format("Trying to find network with vlan: [%s].", vlan)); NetworkVO networkVO = networkDao.findByVlan(vlan); if (networkVO == null) { networkVO = createNetworkRecord(zoneId, tag, vlan, accountId, domainId); + s_logger.debug(String.format("Created new network record [id: %s] with details [zoneId: %s, tag: %s, vlan: %s, accountId: %s and domainId: %s].", + networkVO.getUuid(), zoneId, tag, vlan, accountId, domainId)); } return networkVO; } @@ -893,6 +896,7 @@ private Map getNetworksMapping(String[] vmNetworkNames, long Map mapping = new HashMap<>(); for (String networkName : vmNetworkNames) { NetworkVO networkVO = getGuestNetworkFromNetworkMorName(networkName, accountId, zoneId, domainId); + s_logger.debug(String.format("Mapping network name [%s] to networkVO [id: %s].", networkName, networkVO.getUuid())); mapping.put(networkName, networkVO); } return mapping; @@ -927,12 +931,19 @@ private void syncVMNics(VirtualDevice[] nicDevices, DatacenterMO dcMo, Map Date: Fri, 5 Jan 2024 10:23:58 +0100 Subject: [PATCH 29/41] veeam: fix inconsistent display in popup and vm details --- .../org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java | 4 ++-- ui/src/config/section/compute.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java index e609655c580a..bd3b06233125 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java @@ -95,8 +95,8 @@ public class ListVMsCmd extends BaseListTaggedResourcesCmd implements UserCmd { @Parameter(name = ApiConstants.DETAILS, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "comma separated list of host details requested, " - + "value can be a list of [all, group, nics, stats, secgrp, tmpl, servoff, diskoff, iso, volume, min, affgrp]." + description = "comma separated list of vm details requested, " + + "value can be a list of [all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]." + " If no parameter is passed in, the details will be defaulted to all") private List viewDetails; diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 007bd8c3f9d5..0ef53012ba0b 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -32,9 +32,9 @@ export default { permission: ['listVirtualMachinesMetrics'], resourceType: 'UserVm', params: () => { - var params = { details: 'servoff,tmpl,nics' } + var params = { details: 'servoff,tmpl,nics,backoff' } if (store.getters.metrics) { - params = { details: 'servoff,tmpl,nics,stats' } + params = { details: 'servoff,tmpl,nics,backoff,stats' } } return params }, From 6f021b795426db8fb10a18fe73a89381b7cef231 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 9 Jan 2024 11:43:14 +0100 Subject: [PATCH 30/41] Veeam: add interval type for backup schedules --- ui/src/views/compute/backup/BackupSchedule.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/src/views/compute/backup/BackupSchedule.vue b/ui/src/views/compute/backup/BackupSchedule.vue index 914a1121ffbe..32da2d440a73 100644 --- a/ui/src/views/compute/backup/BackupSchedule.vue +++ b/ui/src/views/compute/backup/BackupSchedule.vue @@ -40,6 +40,9 @@ +