From 2b3b323769444f0c5e503f3ce138c79a4986d606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 28 Sep 2026 15:21:47 -0300 Subject: [PATCH 1/9] Secondary storage backup path GUI addition --- .../org/apache/cloudstack/api/ApiConstants.java | 1 + .../command/admin/backup/FinalizeBackupCmd.java | 4 +++- .../api/command/admin/backup/StartBackupCmd.java | 3 ++- .../api/command/user/backup/ListBackupsCmd.java | 3 ++- .../cloudstack/api/response/BackupResponse.java | 13 +++++++++++++ .../apache/cloudstack/backup/BackupManager.java | 2 +- .../cloudstack/backup/InternalBackupJoinVO.java | 7 +++++++ .../db/views/cloud.internal_backup_view.sql | 2 ++ .../cloudstack/backup/BackupManagerImpl.java | 16 +++++++++++++++- .../cloudstack/backup/BackupManagerTest.java | 14 +++++++++++++- ui/public/locales/en.json | 1 + ui/public/locales/pt_BR.json | 1 + ui/src/components/view/DetailsTab.vue | 5 +++++ ui/src/config/section/storage.js | 2 +- 14 files changed, 67 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index f74c46161180..11ca219041fc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -61,6 +61,7 @@ public class ApiConstants { public static final String BACKUP_AVAILABLE = "backupavailable"; public static final String BACKUP_ID = "backupid"; public static final String BACKUP_LIMIT = "backuplimit"; + public static final String BACKUP_PATHS = "backuppaths"; public static final String BACKUP_OFFERING_NAME = "backupofferingname"; public static final String BACKUP_OFFERING_ID = "backupofferingid"; public static final String BACKUP_OFFERING_DETAILS = "backupofferingdetails"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java index 45173f8668ee..d0ddd0ec75f6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java @@ -79,7 +79,9 @@ public void execute() { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Backup"); } - BackupResponse response = backupManager.createBackupResponse(backup, null); + boolean isCallerRootAdmin = _accountService.isRootAdmin(CallContext.current().getCallingAccountId()); + + BackupResponse response = backupManager.createBackupResponse(backup, null, isCallerRootAdmin); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java index 1bf6d45db049..3fd41180b900 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java @@ -82,7 +82,8 @@ public String getDescription() { public void execute() { try { Backup backup = kvmBackupExportService.startBackup(this); - BackupResponse response = backupManager.createBackupResponse(backup, null); + boolean isCallerRootAdmin = _accountService.isRootAdmin(CallContext.current().getCallingAccountId()); + BackupResponse response = backupManager.createBackupResponse(backup, null, isCallerRootAdmin); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java index 703fd9cd4358..a94151b9344a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java @@ -148,12 +148,13 @@ public Boolean getListVmDetails() { ///////////////////////////////////////////////////// protected void setupResponseBackupList(final List backups, final Integer count) { + boolean isCallerRootAdmin = _accountService.isRootAdmin(CallContext.current().getCallingAccountId()); final List responses = new ArrayList<>(); for (Backup backup : backups) { if (backup == null) { continue; } - BackupResponse backupResponse = backupManager.createBackupResponse(backup, this.getListVmDetails()); + BackupResponse backupResponse = backupManager.createBackupResponse(backup, this.getListVmDetails(), isCallerRootAdmin); responses.add(backupResponse); } final ListResponse response = new ListResponse<>(); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index 70db01445edd..0d6fd08c82e8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -99,6 +99,10 @@ public class BackupResponse extends BaseResponse { @Param(description = "Backup offering name") private String backupOfferingName; + @SerializedName(ApiConstants.BACKUP_PATHS) + @Param(description = "The backups paths in the secondary storage") + private Map backupPaths; + @SerializedName(ApiConstants.ACCOUNT_ID) @Param(description = "Account id") private String accountId; @@ -386,4 +390,13 @@ public void setHostId(String hostId) { public String getHostId() { return this.hostId; } + + public Map getBackupPaths() { + return backupPaths; + } + + public void setBackupPaths(Map backupPaths) { + this.backupPaths = backupPaths; + } + } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index 30fc2bbec40d..b5dfe0d8e818 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -259,7 +259,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer String getBackupNameFromVM(VirtualMachine vm); - BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails); + BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin); Capacity getBackupStorageUsedStats(Long zoneId); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java index acc54ccd41fa..d39d436db990 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -98,6 +98,9 @@ public class InternalBackupJoinVO { @Column(name = "volume_id") private long volumeId; + @Column(name = "volume_name") + private String volumeName; + @Column(name = "isolated") private Boolean isolated; @@ -175,6 +178,10 @@ public long getVolumeId() { return volumeId; } + public String getVolumeName() { + return volumeName; + } + public Boolean getIsolated() { return BooleanUtils.isTrue(isolated); } diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql index a1fbd102630b..02a1622f501a 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql @@ -37,11 +37,13 @@ SELECT b.id, MAX(CASE WHEN bd.name = 'current' THEN bd.value END) current, COALESCE(MAX(CASE WHEN bd.name = 'isolated' THEN bd.value END), 'false') isolated, nbpr.volume_id, + v.name as volume_name, nbsr.path image_store_path FROM backups b LEFT JOIN backup_details bd ON b.id = bd.backup_id LEFT JOIN backup_offering bo ON b.backup_offering_id = bo.id LEFT JOIN internal_backup_store_ref nbsr ON b.id = nbsr.backup_id LEFT JOIN internal_backup_pool_ref nbpr ON nbpr.volume_id = nbsr.volume_id +LEFT JOIN volumes v ON v.id = nbsr.volume_id WHERE bo.provider='kboss' GROUP BY b.id, nbsr.volume_id; diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 782862df6d83..879c4dc36596 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -85,6 +85,7 @@ import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; @@ -255,6 +256,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { private DomainHelper domainHelper; @Inject ReservationDao reservationDao; + @Inject + private InternalBackupJoinDao internalBackupJoinDao; private AsyncJobDispatcher asyncJobDispatcher; private Timer backupTimer; @@ -2595,7 +2598,7 @@ Map getDetailsFromBackupDetails(Long backupId) { } @Override - public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails) { + public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin) { VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); AccountVO account = accountDao.findByIdIncludingRemoved(backup.getAccountId()); DomainVO domain = domainDao.findByIdIncludingRemoved(backup.getDomainId()); @@ -2677,6 +2680,17 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails) response.setToCheckpointId(backup.getToCheckpointId()); } + if (KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) { + List backupJoins = internalBackupJoinDao.listById(backup.getId()); + Map backupVolumePaths = new HashMap<>(); + + backupJoins.forEach(b -> { + backupVolumePaths.put(b.getVolumeName(), b.getImageStorePath()); + }); + + response.setBackupPaths(backupVolumePaths); + } + response.setObjectName("backup"); return response; } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 04bd03670079..94113811034a 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -59,6 +59,7 @@ import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; @@ -196,6 +197,9 @@ public class BackupManagerTest { @Mock private AlertManager alertManagerMock; + @Mock + private InternalBackupJoinDao internalBackupJoinDaoMock; + @Mock private Domain domainMock; @@ -1713,7 +1717,14 @@ public void testNewBackupResponse() { Mockito.when(backupDetailsDao.listDetailsKeyPairs(backup.getId(), true)).thenReturn(details); - BackupResponse response = backupManager.createBackupResponse(backup, true); + String backupPath = "/backup/path"; + String volumeName = "ROOT"; + InternalBackupJoinVO backupJoin = new InternalBackupJoinVO(); + ReflectionTestUtils.setField(backupJoin, "imageStorePath", backupPath); + ReflectionTestUtils.setField(backupJoin, "volumeName", volumeName); + Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(Collections.singletonList(backupJoin)); + + BackupResponse response = backupManager.createBackupResponse(backup, true, true); Assert.assertEquals("backup-uuid", response.getId()); Assert.assertEquals("test-vm", response.getVmName()); @@ -1730,6 +1741,7 @@ public void testNewBackupResponse() { "nics=[{\"networkid\":\"network-uuid1\",\"networkname\":\"network1\"}], serviceofferingname=service-offering1, " + "templatename=template1, templateid=template-uuid1}", response.getVmDetails().toString()); Assert.assertEquals(true, response.getVmOfferingRemoved()); + Assert.assertEquals(backupPath, response.getBackupPaths().get(volumeName)); } @Test diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99bf2cf7aef9..47ee6fe569c8 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -489,6 +489,7 @@ "label.backup.restore": "Restore Instance backup", "label.backup.schedule.create.failed": "Failed to create Backup Schedule", "label.backuplimit": "Backup Limits", +"label.backuppaths": "Backup secondary storage paths", "label.backup.schedules": "Backup Schedules", "label.backup.storage": "Backup Storage", "label.backupstoragelimit": "Backup Storage Limits (GiB)", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index 8fba779b1d12..f180518074fa 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -442,6 +442,7 @@ "label.backingup": "Salvando", "label.backup.attach.restore": "Restaurar e anexar volume de backup", "label.backuplimit": "Limite de backups", +"label.backuppaths": "Caminho dos backups no armazenamento secund\u00e1rio", "label.backup.storage": "Armazenamento de backup", "label.backupstoragelimit": "Limite de armazenamento de backup (GiB)", "label.backup.configure.schedule": "Configurar Agendamento de Backup", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index d2aabacb10c9..b6b742434645 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -68,6 +68,11 @@ {{ volume.type }} - {{ volume.path }} ({{ parseFloat(volume.size / (1024.0 * 1024.0 * 1024.0)).toFixed(1) }} GiB) +
+
+ {{ volumeName }} - {{ backupPath }} +
+
{{ dataResource.rootdisksize }} GB diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 75bdfd4d5fa6..5b8625a2c6ce 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -494,7 +494,7 @@ export default { permission: ['listBackups'], params: { listvmdetails: 'true' }, columns: ['name', 'status', 'compressionstatus', 'validationstatus', 'size', 'virtualsize', 'virtualmachinename', 'backupofferingname', 'intervaltype', 'type', 'created', 'account', 'domain', 'zone'], - details: ['name', 'description', 'virtualmachinename', 'id', 'intervaltype', 'type', 'externalid', 'size', 'virtualsize', 'volumes', 'backupofferingname', 'zone', 'account', 'domain', 'created'], + details: ['name', 'description', 'virtualmachinename', 'id', 'intervaltype', 'type', 'externalid', 'size', 'virtualsize', 'volumes', 'backupofferingname', 'backuppaths', 'zone', 'account', 'domain', 'created'], searchFilters: ['name', 'zoneid', 'domainid', 'account', 'backupofferingid', 'status', 'type', 'virtualmachineid'], tabs: [ { From bb9ccb76553dc6beccaadb9d74b6f523da1231f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 19 Oct 2026 10:52:38 -0300 Subject: [PATCH 2/9] Add null check to offering --- .../java/org/apache/cloudstack/backup/BackupManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 879c4dc36596..4a5afffcc018 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -2680,7 +2680,7 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, response.setToCheckpointId(backup.getToCheckpointId()); } - if (KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) { + if (offering != null && KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) { List backupJoins = internalBackupJoinDao.listById(backup.getId()); Map backupVolumePaths = new HashMap<>(); From a8ec02752e786484005f1380a5c35efefba202c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 19 Oct 2026 13:53:07 -0300 Subject: [PATCH 3/9] Store volume UUID as map key and use dedicated structure with volume name and path as value --- .../api/response/BackupResponse.java | 8 ++-- .../helper/BackupVolumeNameAndPath.java | 45 +++++++++++++++++++ .../backup/InternalBackupJoinVO.java | 8 ++++ .../db/views/cloud.internal_backup_view.sql | 1 + .../cloudstack/backup/BackupManagerImpl.java | 5 ++- ui/src/components/view/DetailsTab.vue | 4 +- 6 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index 0d6fd08c82e8..4fccdb176e14 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -20,6 +20,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.Backup; import com.cloud.serializer.Param; @@ -101,7 +102,7 @@ public class BackupResponse extends BaseResponse { @SerializedName(ApiConstants.BACKUP_PATHS) @Param(description = "The backups paths in the secondary storage") - private Map backupPaths; + private Map backupPaths; @SerializedName(ApiConstants.ACCOUNT_ID) @Param(description = "Account id") @@ -391,12 +392,11 @@ public String getHostId() { return this.hostId; } - public Map getBackupPaths() { + public Map getBackupPaths() { return backupPaths; } - public void setBackupPaths(Map backupPaths) { + public void setBackupPaths(Map backupPaths) { this.backupPaths = backupPaths; } - } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java b/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java new file mode 100644 index 000000000000..00a7b97564b8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java @@ -0,0 +1,45 @@ +// 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.api.response.helper; + +public class BackupVolumeNameAndPath { + private String volumeName; + private String backupPath; + + public BackupVolumeNameAndPath(String volumeName, String backupPath) { + this.volumeName = volumeName; + this.backupPath = backupPath; + } + + public String getVolumeName() { + return volumeName; + } + + public void setVolumeName(String volumeName) { + this.volumeName = volumeName; + } + + public String getBackupPath() { + return backupPath; + } + + public void setBackupPath(String backupPath) { + this.backupPath = backupPath; + } +} + diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java index d39d436db990..b79ac477cee8 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -98,6 +98,9 @@ public class InternalBackupJoinVO { @Column(name = "volume_id") private long volumeId; + @Column(name = "volume_uuid") + private String volumeUuid; + @Column(name = "volume_name") private String volumeName; @@ -190,6 +193,11 @@ public Backup.CompressionStatus getCompressionStatus() { return compressionStatus; } + + public String getVolumeUuid() { + return volumeUuid; + } + @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql index 02a1622f501a..fb8bed4478cb 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql @@ -38,6 +38,7 @@ SELECT b.id, COALESCE(MAX(CASE WHEN bd.name = 'isolated' THEN bd.value END), 'false') isolated, nbpr.volume_id, v.name as volume_name, + v.uuid as volume_uuid, nbsr.path image_store_path FROM backups b LEFT JOIN backup_details bd ON b.id = bd.backup_id diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 4a5afffcc018..da359b3ef11e 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -80,6 +80,7 @@ import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd; import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; @@ -2682,10 +2683,10 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, if (offering != null && KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) { List backupJoins = internalBackupJoinDao.listById(backup.getId()); - Map backupVolumePaths = new HashMap<>(); + Map backupVolumePaths = new HashMap<>(); backupJoins.forEach(b -> { - backupVolumePaths.put(b.getVolumeName(), b.getImageStorePath()); + backupVolumePaths.put(b.getVolumeUuid(), new BackupVolumeNameAndPath(b.getVolumeName(), b.getImageStorePath())); }); response.setBackupPaths(backupVolumePaths); diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index b6b742434645..ec21a94949d2 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -69,8 +69,8 @@
-
- {{ volumeName }} - {{ backupPath }} +
+ {{ backupVolumeInfo.volumeName }} - {{ backupVolumeInfo.backupPath }}
From 24a2c2abca1898a9586a8d2f3c022094beacee11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 19 Oct 2026 15:39:08 -0300 Subject: [PATCH 4/9] Improve backup response tests --- .../backup/BackupManagerResponsesTest.java | 262 ++++++++++++++++++ .../cloudstack/backup/BackupManagerTest.java | 104 ------- 2 files changed, 262 insertions(+), 104 deletions(-) create mode 100644 server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java new file mode 100644 index 000000000000..2600c2e8ef26 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java @@ -0,0 +1,262 @@ +package org.apache.cloudstack.backup; + +import com.cloud.dc.DataCenter; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BackupManagerResponsesTest { + @Spy + @InjectMocks + private BackupManagerImpl backupManager = new BackupManagerImpl(); + + @Mock + private VMInstanceDao vmInstanceDao; + @Mock + private AccountDao accountDao; + @Mock + private DomainDao domainDao; + @Mock + private DataCenterDao dataCenterDao; + @Mock + private BackupOfferingDao backupOfferingDao; + @Mock + private VMTemplateDao vmTemplateDao; + @Mock + private ServiceOfferingDao serviceOfferingDao; + @Mock + private NetworkDao networkDao; + @Mock + private BackupDetailsDao backupDetailsDao; + @Mock + private InternalBackupJoinDao internalBackupJoinDaoMock; + + // Entities + private BackupVO backup; + private VMInstanceVO virtualMachine; + private AccountVO account; + private DomainVO domain; + private DataCenterVO zone; + private BackupOfferingVO backupOffering; + private VMTemplateVO template; + private ServiceOfferingVO serviceOffering; + private NetworkVO network; + + @Before + public void setup() { + Long vmId = 1L; + Long accountId = 2L; + Long domainId = 3L; + Long zoneId = 4L; + Long vmOfferingId = 5L; + Long backupOfferingId = 6L; + Long backupId = 7L; + Long templateId = 8L; + String templateUuid = "template-uuid1"; + String serviceOfferingUuid = "service-offering-uuid1"; + + backup = new BackupVO(); + ReflectionTestUtils.setField(backup, "id", backupId); + ReflectionTestUtils.setField(backup, "uuid", "backup-uuid"); + backup.setVmId(vmId); + backup.setAccountId(accountId); + backup.setDomainId(domainId); + backup.setZoneId(zoneId); + backup.setBackupOfferingId(backupOfferingId); + backup.setType("Full"); + backup.setBackupScheduleId(null); + + virtualMachine = new VMInstanceVO(vmId, 0L, "test-vm", "test-vm", VirtualMachine.Type.User, + 0L, Hypervisor.HypervisorType.Simulator, 0L, domainId, accountId, 0L, false); + virtualMachine.setDataCenterId(zoneId); + virtualMachine.setBackupOfferingId(vmOfferingId); + virtualMachine.setTemplateId(templateId); + + account = new AccountVO(); + account.setUuid("account-uuid"); + account.setAccountName("test-account"); + + domain = new DomainVO(); + domain.setUuid("domain-uuid"); + domain.setName("test-domain"); + + zone = new DataCenterVO(1L, "test-zone", null, null, null, null, null, null, null, null, DataCenter.NetworkType.Advanced, null, null); + zone.setUuid("zone-uuid"); + + backupOffering = new BackupOfferingVO(); + ReflectionTestUtils.setField(backupOffering, "id", backupOfferingId); + ReflectionTestUtils.setField(backupOffering, "uuid", "offering-uuid"); + backupOffering.setName("test-offering"); + + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(virtualMachine); + Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account); + Mockito.when(domainDao.findByIdIncludingRemoved(domainId)).thenReturn(domain); + Mockito.when(dataCenterDao.findByIdIncludingRemoved(zoneId)).thenReturn(zone); + Mockito.when(backupOfferingDao.findByIdIncludingRemoved(backupOfferingId)).thenReturn(backupOffering); + + template = new VMTemplateVO(); + template.setFormat(Storage.ImageFormat.QCOW2); + template.setName("template1"); + when(vmTemplateDao.findByUuid(templateUuid)).thenReturn(template); + + + serviceOffering = mock(ServiceOfferingVO.class); + when(serviceOffering.getName()).thenReturn("service-offering1"); + when(serviceOfferingDao.findByUuid(serviceOfferingUuid)).thenReturn(serviceOffering); + + network = new NetworkVO(); + network.setName("network1"); + when(networkDao.findByUuid("network-uuid1")).thenReturn(network); + + + Map details = new HashMap<>(); + details.put(ApiConstants.TEMPLATE_ID, templateUuid); + details.put(ApiConstants.SERVICE_OFFERING_ID, serviceOfferingUuid); + details.put(ApiConstants.NICS, "[{\"networkid\":\"network-uuid1\"}]"); + Mockito.when(backupDetailsDao.listDetailsKeyPairs(backup.getId(), true)).thenReturn(details); + } + + @Test + public void createBackupResponseTestBasicResponse() { + BackupResponse response = backupManager.createBackupResponse(backup, true, true); + + assertBasicBackupResponseFields(response); + Assert.assertNull(response.getBackupPaths()); + } + + @Test + public void createBackupResponseTestKBOSSResponseAdmin() { + ReflectionTestUtils.setField(backupOffering, "provider", BackupManagerImpl.KBOSS_BACKUP_PROVIDER); + + InternalBackupJoinVO backupJoin = createBackupJoin(); + Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(Collections.singletonList(backupJoin)); + + BackupResponse response = backupManager.createBackupResponse(backup, true, true); + + assertBasicBackupResponseFields(response); + Assert.assertFalse(response.getBackupPaths().isEmpty()); + Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getVolumeUuid())); + Assert.assertEquals(backupJoin.getImageStorePath(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getBackupPath()); + Assert.assertEquals(backupJoin.getVolumeName(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getVolumeName()); + } + + @Test + public void createBackupResponseTestKBOSSResponseMultipleEqualNamedVolumes() { + ReflectionTestUtils.setField(backupOffering, "provider", BackupManagerImpl.KBOSS_BACKUP_PROVIDER); + + int joinsAmount = 3; + List backupJoins = getListOfBackupJoins(joinsAmount); + Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(backupJoins); + + BackupResponse response = backupManager.createBackupResponse(backup, true, true); + + assertBasicBackupResponseFields(response); + Assert.assertFalse(response.getBackupPaths().isEmpty()); + Assert.assertEquals(joinsAmount, response.getBackupPaths().size()); + + for (InternalBackupJoinVO backupJoin : backupJoins) { + Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getVolumeUuid())); + Assert.assertEquals(backupJoin.getImageStorePath(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getBackupPath()); + Assert.assertEquals(backupJoin.getVolumeName(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getVolumeName()); + } + } + + @Test + public void createBackupResponseTestKBOSSResponseNormalUser() { + ReflectionTestUtils.setField(backupOffering, "provider", BackupManagerImpl.KBOSS_BACKUP_PROVIDER); + + BackupResponse response = backupManager.createBackupResponse(backup, true, false); + + assertBasicBackupResponseFields(response); + Assert.assertNull(response.getBackupPaths()); + } + + @Test + public void createBackupResponseTestKBOSSResponseNullOffering() { + ReflectionTestUtils.setField(backupOffering, "provider", null); + + BackupResponse response = backupManager.createBackupResponse(backup, true, false); + + assertBasicBackupResponseFields(response); + Assert.assertNull(response.getBackupPaths()); + } + + private void assertBasicBackupResponseFields(BackupResponse response) { + Assert.assertEquals("backup-uuid", response.getId()); + Assert.assertEquals("test-vm", response.getVmName()); + Assert.assertEquals("account-uuid", response.getAccountId()); + Assert.assertEquals("test-account", response.getAccount()); + Assert.assertEquals("domain-uuid", response.getDomainId()); + Assert.assertEquals("test-domain", response.getDomain()); + Assert.assertEquals("zone-uuid", response.getZoneId()); + Assert.assertEquals("test-zone", response.getZone()); + Assert.assertEquals("offering-uuid", response.getBackupOfferingId()); + Assert.assertEquals("test-offering", response.getBackupOffering()); + Assert.assertEquals("MANUAL", response.getIntervalType()); + Assert.assertEquals("{serviceofferingid=service-offering-uuid1, isiso=false, hypervisor=Simulator, " + + "nics=[{\"networkid\":\"network-uuid1\",\"networkname\":\"network1\"}], serviceofferingname=service-offering1, " + + "templatename=template1, templateid=template-uuid1}", response.getVmDetails().toString()); + Assert.assertEquals(true, response.getVmOfferingRemoved()); + } + + private List getListOfBackupJoins(int listSize) { + List backupJoins = new java.util.ArrayList<>(); + + for (int i = 0; i < listSize; i++) { + backupJoins.add(createBackupJoin()); + } + + return backupJoins; + } + + private InternalBackupJoinVO createBackupJoin() { + String backupPath = "/backup/path"; + String volumeName = "ROOT"; + String volumeUuid = UUID.randomUUID().toString(); + InternalBackupJoinVO backupJoin = new InternalBackupJoinVO(); + ReflectionTestUtils.setField(backupJoin, "imageStorePath", backupPath); + ReflectionTestUtils.setField(backupJoin, "volumeName", volumeName); + ReflectionTestUtils.setField(backupJoin, "volumeUuid", volumeUuid); + + return backupJoin; + } +} diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 94113811034a..b770bdd452ae 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -53,13 +53,11 @@ import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; -import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; -import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; @@ -92,7 +90,6 @@ import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.capacity.CapacityVO; import com.cloud.configuration.Resource; -import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.domain.Domain; @@ -114,7 +111,6 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; -import com.cloud.storage.Storage; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; @@ -197,9 +193,6 @@ public class BackupManagerTest { @Mock private AlertManager alertManagerMock; - @Mock - private InternalBackupJoinDao internalBackupJoinDaoMock; - @Mock private Domain domainMock; @@ -1647,103 +1640,6 @@ public void testDeleteBackupBlockedByPendingJobs() throws ResourceAllocationExce backupManager.deleteBackup(backupId, false); } - @Test - public void testNewBackupResponse() { - Long vmId = 1L; - Long accountId = 2L; - Long domainId = 3L; - Long zoneId = 4L; - Long vmOfferingId = 5L; - Long backupOfferingId = 6L; - Long backupId = 7L; - Long templateId = 8L; - String templateUuid = "template-uuid1"; - String serviceOfferingUuid = "service-offering-uuid1"; - - BackupVO backup = new BackupVO(); - ReflectionTestUtils.setField(backup, "id", backupId); - ReflectionTestUtils.setField(backup, "uuid", "backup-uuid"); - backup.setVmId(vmId); - backup.setAccountId(accountId); - backup.setDomainId(domainId); - backup.setZoneId(zoneId); - backup.setBackupOfferingId(backupOfferingId); - backup.setType("Full"); - backup.setBackupScheduleId(null); - - VMInstanceVO vm = new VMInstanceVO(vmId, 0L, "test-vm", "test-vm", VirtualMachine.Type.User, - 0L, Hypervisor.HypervisorType.Simulator, 0L, domainId, accountId, 0L, false); - vm.setDataCenterId(zoneId); - vm.setBackupOfferingId(vmOfferingId); - vm.setTemplateId(templateId); - - AccountVO account = new AccountVO(); - account.setUuid("account-uuid"); - account.setAccountName("test-account"); - - DomainVO domain = new DomainVO(); - domain.setUuid("domain-uuid"); - domain.setName("test-domain"); - - DataCenterVO zone = new DataCenterVO(1L, "test-zone", null, null, null, null, null, null, null, null, DataCenter.NetworkType.Advanced, null, null); - zone.setUuid("zone-uuid"); - - BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class); - Mockito.when(offering.getUuid()).thenReturn("offering-uuid"); - Mockito.when(offering.getName()).thenReturn("test-offering"); - - Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); - Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account); - Mockito.when(domainDao.findByIdIncludingRemoved(domainId)).thenReturn(domain); - Mockito.when(dataCenterDao.findByIdIncludingRemoved(zoneId)).thenReturn(zone); - Mockito.when(backupOfferingDao.findByIdIncludingRemoved(backupOfferingId)).thenReturn(offering); - - VMTemplateVO template = mock(VMTemplateVO.class); - when(template.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); - when(template.getName()).thenReturn("template1"); - when(vmTemplateDao.findByUuid(templateUuid)).thenReturn(template); - Map details = new HashMap<>(); - details.put(ApiConstants.TEMPLATE_ID, templateUuid); - - ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class); - when(serviceOffering.getName()).thenReturn("service-offering1"); - when(serviceOfferingDao.findByUuid(serviceOfferingUuid)).thenReturn(serviceOffering); - details.put(ApiConstants.SERVICE_OFFERING_ID, serviceOfferingUuid); - - NetworkVO network = mock(NetworkVO.class); - when(network.getName()).thenReturn("network1"); - when(networkDao.findByUuid("network-uuid1")).thenReturn(network); - details.put(ApiConstants.NICS, "[{\"networkid\":\"network-uuid1\"}]"); - - Mockito.when(backupDetailsDao.listDetailsKeyPairs(backup.getId(), true)).thenReturn(details); - - String backupPath = "/backup/path"; - String volumeName = "ROOT"; - InternalBackupJoinVO backupJoin = new InternalBackupJoinVO(); - ReflectionTestUtils.setField(backupJoin, "imageStorePath", backupPath); - ReflectionTestUtils.setField(backupJoin, "volumeName", volumeName); - Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(Collections.singletonList(backupJoin)); - - BackupResponse response = backupManager.createBackupResponse(backup, true, true); - - Assert.assertEquals("backup-uuid", response.getId()); - Assert.assertEquals("test-vm", response.getVmName()); - Assert.assertEquals("account-uuid", response.getAccountId()); - Assert.assertEquals("test-account", response.getAccount()); - Assert.assertEquals("domain-uuid", response.getDomainId()); - Assert.assertEquals("test-domain", response.getDomain()); - Assert.assertEquals("zone-uuid", response.getZoneId()); - Assert.assertEquals("test-zone", response.getZone()); - Assert.assertEquals("offering-uuid", response.getBackupOfferingId()); - Assert.assertEquals("test-offering", response.getBackupOffering()); - Assert.assertEquals("MANUAL", response.getIntervalType()); - Assert.assertEquals("{serviceofferingid=service-offering-uuid1, isiso=false, hypervisor=Simulator, " + - "nics=[{\"networkid\":\"network-uuid1\",\"networkname\":\"network1\"}], serviceofferingname=service-offering1, " + - "templatename=template1, templateid=template-uuid1}", response.getVmDetails().toString()); - Assert.assertEquals(true, response.getVmOfferingRemoved()); - Assert.assertEquals(backupPath, response.getBackupPaths().get(volumeName)); - } - @Test public void validateAndGetDefaultBackupRetentionIfRequiredTestReturnZeroAsDefaultValue() { int retention = backupManager.validateAndGetDefaultBackupRetentionIfRequired(null, backupOfferingVOMock, null); From e43d5a58eb2e207d6b32297a11233778c445b019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 19 Oct 2026 15:59:15 -0300 Subject: [PATCH 5/9] Separate offering related assertions and fix null offering test --- .../backup/BackupManagerResponsesTest.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java index 2600c2e8ef26..38b671919bbd 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java @@ -159,7 +159,7 @@ public void setup() { public void createBackupResponseTestBasicResponse() { BackupResponse response = backupManager.createBackupResponse(backup, true, true); - assertBasicBackupResponseFields(response); + assertFullBackupResponseFields(response); Assert.assertNull(response.getBackupPaths()); } @@ -172,7 +172,7 @@ public void createBackupResponseTestKBOSSResponseAdmin() { BackupResponse response = backupManager.createBackupResponse(backup, true, true); - assertBasicBackupResponseFields(response); + assertFullBackupResponseFields(response); Assert.assertFalse(response.getBackupPaths().isEmpty()); Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getVolumeUuid())); Assert.assertEquals(backupJoin.getImageStorePath(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getBackupPath()); @@ -189,7 +189,7 @@ public void createBackupResponseTestKBOSSResponseMultipleEqualNamedVolumes() { BackupResponse response = backupManager.createBackupResponse(backup, true, true); - assertBasicBackupResponseFields(response); + assertFullBackupResponseFields(response); Assert.assertFalse(response.getBackupPaths().isEmpty()); Assert.assertEquals(joinsAmount, response.getBackupPaths().size()); @@ -206,20 +206,25 @@ public void createBackupResponseTestKBOSSResponseNormalUser() { BackupResponse response = backupManager.createBackupResponse(backup, true, false); - assertBasicBackupResponseFields(response); + assertFullBackupResponseFields(response); Assert.assertNull(response.getBackupPaths()); } @Test public void createBackupResponseTestKBOSSResponseNullOffering() { - ReflectionTestUtils.setField(backupOffering, "provider", null); + Mockito.when(backupOfferingDao.findByIdIncludingRemoved(Mockito.anyLong())).thenReturn(null); - BackupResponse response = backupManager.createBackupResponse(backup, true, false); + BackupResponse response = backupManager.createBackupResponse(backup, true, true); assertBasicBackupResponseFields(response); Assert.assertNull(response.getBackupPaths()); } + private void assertFullBackupResponseFields(BackupResponse response) { + assertBasicBackupResponseFields(response); + assertBackupResponseOfferingFields(response); + } + private void assertBasicBackupResponseFields(BackupResponse response) { Assert.assertEquals("backup-uuid", response.getId()); Assert.assertEquals("test-vm", response.getVmName()); @@ -229,8 +234,6 @@ private void assertBasicBackupResponseFields(BackupResponse response) { Assert.assertEquals("test-domain", response.getDomain()); Assert.assertEquals("zone-uuid", response.getZoneId()); Assert.assertEquals("test-zone", response.getZone()); - Assert.assertEquals("offering-uuid", response.getBackupOfferingId()); - Assert.assertEquals("test-offering", response.getBackupOffering()); Assert.assertEquals("MANUAL", response.getIntervalType()); Assert.assertEquals("{serviceofferingid=service-offering-uuid1, isiso=false, hypervisor=Simulator, " + "nics=[{\"networkid\":\"network-uuid1\",\"networkname\":\"network1\"}], serviceofferingname=service-offering1, " + @@ -238,6 +241,11 @@ private void assertBasicBackupResponseFields(BackupResponse response) { Assert.assertEquals(true, response.getVmOfferingRemoved()); } + private void assertBackupResponseOfferingFields(BackupResponse response) { + Assert.assertEquals("offering-uuid", response.getBackupOfferingId()); + Assert.assertEquals("test-offering", response.getBackupOffering()); + } + private List getListOfBackupJoins(int listSize) { List backupJoins = new java.util.ArrayList<>(); From ae4431a25e9d6e196ba9e4867d087a92cf07faeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Mon, 19 Oct 2026 16:12:54 -0300 Subject: [PATCH 6/9] Add license to file --- .../backup/BackupManagerResponsesTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java index 38b671919bbd..daac8469eab8 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java @@ -1,3 +1,20 @@ +// 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; import com.cloud.dc.DataCenter; From ae8f87ec53e68afa8024b001a4ca4b6d4935b208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Thu, 12 Nov 2026 10:39:56 -0300 Subject: [PATCH 7/9] Address copilot's reviews --- .../main/java/org/apache/cloudstack/api/ApiConstants.java | 1 + .../org/apache/cloudstack/api/response/BackupResponse.java | 2 +- .../api/response/helper/BackupVolumeNameAndPath.java | 6 ++++++ .../java/org/apache/cloudstack/backup/BackupManager.java | 2 ++ .../org/apache/cloudstack/backup/BackupManagerImpl.java | 5 +++++ ui/src/components/view/DetailsTab.vue | 2 +- 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 11ca219041fc..e48ffd39f71c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -61,6 +61,7 @@ public class ApiConstants { public static final String BACKUP_AVAILABLE = "backupavailable"; public static final String BACKUP_ID = "backupid"; public static final String BACKUP_LIMIT = "backuplimit"; + public static final String BACKUP_PATH = "backuppath"; public static final String BACKUP_PATHS = "backuppaths"; public static final String BACKUP_OFFERING_NAME = "backupofferingname"; public static final String BACKUP_OFFERING_ID = "backupofferingid"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index 4fccdb176e14..6f3d35235155 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -101,7 +101,7 @@ public class BackupResponse extends BaseResponse { private String backupOfferingName; @SerializedName(ApiConstants.BACKUP_PATHS) - @Param(description = "The backups paths in the secondary storage") + @Param(description = "The backup paths in the secondary storage") private Map backupPaths; @SerializedName(ApiConstants.ACCOUNT_ID) diff --git a/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java b/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java index 00a7b97564b8..9f6dc9f18307 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/helper/BackupVolumeNameAndPath.java @@ -17,8 +17,14 @@ package org.apache.cloudstack.api.response.helper; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; + public class BackupVolumeNameAndPath { + @SerializedName(ApiConstants.VOLUME_NAME) private String volumeName; + + @SerializedName(ApiConstants.BACKUP_PATH) private String backupPath; public BackupVolumeNameAndPath(String volumeName, String backupPath) { diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index b5dfe0d8e818..aa4f43066f19 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -259,6 +259,8 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer String getBackupNameFromVM(VirtualMachine vm); + BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails); + BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin); Capacity getBackupStorageUsedStats(Long zoneId); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index da359b3ef11e..ed0a437603a5 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -2598,6 +2598,11 @@ Map getDetailsFromBackupDetails(Long backupId) { return details; } + @Override + public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails) { + return createBackupResponse(backup, listVmDetails, false); + } + @Override public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin) { VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index ec21a94949d2..b2d6ec42d5d7 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -68,7 +68,7 @@ {{ volume.type }} - {{ volume.path }} ({{ parseFloat(volume.size / (1024.0 * 1024.0 * 1024.0)).toFixed(1) }} GiB)
-
+
{{ backupVolumeInfo.volumeName }} - {{ backupVolumeInfo.backupPath }}
From 253331eaf723533a787863030acb17f6c77d86d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Thu, 12 Nov 2026 15:28:33 -0300 Subject: [PATCH 8/9] Refactor internal_backup_view to enhance backup listing performance --- .../backup/InternalBackupJoinVO.java | 15 -- .../dao/InternalBackupDataStoreDao.java | 4 + .../dao/InternalBackupDataStoreDaoImpl.java | 35 +++- .../db/views/cloud.internal_backup_view.sql | 3 - .../cloudstack/backup/BackupManagerImpl.java | 16 +- .../backup/BackupManagerResponsesTest.java | 61 ++++--- ui/public/config.json | 150 +----------------- ui/src/components/view/DetailsTab.vue | 2 +- 8 files changed, 73 insertions(+), 213 deletions(-) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java index b79ac477cee8..acc54ccd41fa 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -98,12 +98,6 @@ public class InternalBackupJoinVO { @Column(name = "volume_id") private long volumeId; - @Column(name = "volume_uuid") - private String volumeUuid; - - @Column(name = "volume_name") - private String volumeName; - @Column(name = "isolated") private Boolean isolated; @@ -181,10 +175,6 @@ public long getVolumeId() { return volumeId; } - public String getVolumeName() { - return volumeName; - } - public Boolean getIsolated() { return BooleanUtils.isTrue(isolated); } @@ -193,11 +183,6 @@ public Backup.CompressionStatus getCompressionStatus() { return compressionStatus; } - - public String getVolumeUuid() { - return volumeUuid; - } - @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java index e71ffe5ebde9..16a470834617 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java @@ -17,9 +17,11 @@ package org.apache.cloudstack.backup.dao; import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.InternalBackupDataStoreVO; import java.util.List; +import java.util.Map; public interface InternalBackupDataStoreDao extends GenericDao { @@ -30,4 +32,6 @@ public interface InternalBackupDataStoreDao extends GenericDao listVolumeNamesAndBackupPaths(long backupId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java index 34a064ff62a2..0239d8c971df 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java @@ -19,19 +19,28 @@ import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.UpdateBuilder; +import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.InternalBackupDataStoreVO; import javax.annotation.PostConstruct; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class InternalBackupDataStoreDaoImpl extends GenericDaoBase implements InternalBackupDataStoreDao { - private SearchBuilder backupSearch; private static final String BACKUP_ID = "backup_id"; private static final String VOLUME_ID = "volume_id"; + private static final String SELECT_SECONDARY_PATH_AND_VOLUME_NAME = "select v.name, v.uuid, ibsr.`path` from internal_backup_store_ref ibsr join volumes v on ibsr.volume_id = v.id " + + "where ibsr.backup_id = ?"; + @PostConstruct protected void init() { backupSearch = createSearchBuilder(); @@ -71,4 +80,28 @@ public void updateVolumeId(long oldVolumeId, long newVolumeId) { UpdateBuilder ub = getUpdateBuilder(delta); update(ub, sc, null); } + + @Override + public Map listVolumeNamesAndBackupPaths(long backupId) { + Map volumeIdentifierToObjectMap = new HashMap<>(); + try (TransactionLegacy transactionLegacy = TransactionLegacy.currentTxn()) { + try (PreparedStatement preparedStatement = transactionLegacy.prepareStatement(SELECT_SECONDARY_PATH_AND_VOLUME_NAME)) { + preparedStatement.setLong(1, backupId); + + try (ResultSet resultSet = preparedStatement.executeQuery()) { + while (resultSet.next()) { + String volumeName = resultSet.getString(1); + String volumeUuid = resultSet.getString(2); + String backupPath = resultSet.getString(3); + + volumeIdentifierToObjectMap.put(volumeUuid, new BackupVolumeNameAndPath(volumeName, backupPath)); + } + } + } + } catch (SQLException e) { + logger.warn("Failed to find volume names and backup paths on secondary for backup [{}].", backupId, e); + } + return volumeIdentifierToObjectMap; + } + } diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql index fb8bed4478cb..a1fbd102630b 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql @@ -37,14 +37,11 @@ SELECT b.id, MAX(CASE WHEN bd.name = 'current' THEN bd.value END) current, COALESCE(MAX(CASE WHEN bd.name = 'isolated' THEN bd.value END), 'false') isolated, nbpr.volume_id, - v.name as volume_name, - v.uuid as volume_uuid, nbsr.path image_store_path FROM backups b LEFT JOIN backup_details bd ON b.id = bd.backup_id LEFT JOIN backup_offering bo ON b.backup_offering_id = bo.id LEFT JOIN internal_backup_store_ref nbsr ON b.id = nbsr.backup_id LEFT JOIN internal_backup_pool_ref nbpr ON nbpr.volume_id = nbsr.volume_id -LEFT JOIN volumes v ON v.id = nbsr.volume_id WHERE bo.provider='kboss' GROUP BY b.id, nbsr.volume_id; diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index bd77fe9a46c2..c540fe9537a2 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -80,14 +80,13 @@ import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd; import org.apache.cloudstack.api.response.BackupResponse; -import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.backup.dao.BackupUsageMetricDao; -import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; @@ -261,7 +260,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { @Inject ReservationDao reservationDao; @Inject - private InternalBackupJoinDao internalBackupJoinDao; + private InternalBackupDataStoreDao internalBackupDataStoreDao; private AsyncJobDispatcher asyncJobDispatcher; private Timer backupTimer; @@ -2669,6 +2668,7 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, DataCenterVO zone = dataCenterDao.findByIdIncludingRemoved(backup.getZoneId()); Long offeringId = backup.getBackupOfferingId(); BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(offeringId); + logger.trace("Got offering [{}] for backup [{}].", offering, backup); BackupResponse response = new BackupResponse(); response.setId(backup.getUuid()); @@ -2702,6 +2702,7 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, } if (backup.getBackupScheduleId() != null) { BackupScheduleVO scheduleVO = backupScheduleDao.findById(backup.getBackupScheduleId()); + logger.trace("Got backup schedule [{}] for backup [{}].", scheduleVO, backup); if (scheduleVO != null) { response.setIntervalType(scheduleVO.getScheduleType().toString()); } @@ -2745,14 +2746,7 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, } if (offering != null && KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) { - List backupJoins = internalBackupJoinDao.listById(backup.getId()); - Map backupVolumePaths = new HashMap<>(); - - backupJoins.forEach(b -> { - backupVolumePaths.put(b.getVolumeUuid(), new BackupVolumeNameAndPath(b.getVolumeName(), b.getImageStorePath())); - }); - - response.setBackupPaths(backupVolumePaths); + response.setBackupPaths(internalBackupDataStoreDao.listVolumeNamesAndBackupPaths(backup.getId())); } response.setObjectName("backup"); diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java index daac8469eab8..6eedf9912738 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerResponsesTest.java @@ -37,9 +37,10 @@ import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.helper.BackupVolumeNameAndPath; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; -import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -51,9 +52,7 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -85,7 +84,7 @@ public class BackupManagerResponsesTest { @Mock private BackupDetailsDao backupDetailsDao; @Mock - private InternalBackupJoinDao internalBackupJoinDaoMock; + private InternalBackupDataStoreDao internalBackupDataStoreDao; // Entities private BackupVO backup; @@ -184,36 +183,39 @@ public void createBackupResponseTestBasicResponse() { public void createBackupResponseTestKBOSSResponseAdmin() { ReflectionTestUtils.setField(backupOffering, "provider", BackupManagerImpl.KBOSS_BACKUP_PROVIDER); - InternalBackupJoinVO backupJoin = createBackupJoin(); - Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(Collections.singletonList(backupJoin)); + Map backupJoins = getMapOfBackupJoins(1); + Mockito.when(internalBackupDataStoreDao.listVolumeNamesAndBackupPaths(Mockito.anyLong())).thenReturn(backupJoins); BackupResponse response = backupManager.createBackupResponse(backup, true, true); assertFullBackupResponseFields(response); Assert.assertFalse(response.getBackupPaths().isEmpty()); - Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getVolumeUuid())); - Assert.assertEquals(backupJoin.getImageStorePath(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getBackupPath()); - Assert.assertEquals(backupJoin.getVolumeName(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getVolumeName()); + + for (Map.Entry backupJoin : backupJoins.entrySet()) { + Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getKey())); + Assert.assertEquals(backupJoin.getValue().getBackupPath(), response.getBackupPaths().get(backupJoin.getKey()).getBackupPath()); + Assert.assertEquals(backupJoin.getValue().getVolumeName(), response.getBackupPaths().get(backupJoin.getKey()).getVolumeName()); + } } @Test public void createBackupResponseTestKBOSSResponseMultipleEqualNamedVolumes() { ReflectionTestUtils.setField(backupOffering, "provider", BackupManagerImpl.KBOSS_BACKUP_PROVIDER); - int joinsAmount = 3; - List backupJoins = getListOfBackupJoins(joinsAmount); - Mockito.when(internalBackupJoinDaoMock.listById(Mockito.anyLong())).thenReturn(backupJoins); + int backupAmount = 3; + Map backupJoins = getMapOfBackupJoins(backupAmount); + Mockito.when(internalBackupDataStoreDao.listVolumeNamesAndBackupPaths(Mockito.anyLong())).thenReturn(backupJoins); BackupResponse response = backupManager.createBackupResponse(backup, true, true); assertFullBackupResponseFields(response); Assert.assertFalse(response.getBackupPaths().isEmpty()); - Assert.assertEquals(joinsAmount, response.getBackupPaths().size()); + Assert.assertEquals(backupAmount, response.getBackupPaths().size()); - for (InternalBackupJoinVO backupJoin : backupJoins) { - Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getVolumeUuid())); - Assert.assertEquals(backupJoin.getImageStorePath(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getBackupPath()); - Assert.assertEquals(backupJoin.getVolumeName(), response.getBackupPaths().get(backupJoin.getVolumeUuid()).getVolumeName()); + for (Map.Entry backupJoin : backupJoins.entrySet()) { + Assert.assertTrue(response.getBackupPaths().containsKey(backupJoin.getKey())); + Assert.assertEquals(backupJoin.getValue().getBackupPath(), response.getBackupPaths().get(backupJoin.getKey()).getBackupPath()); + Assert.assertEquals(backupJoin.getValue().getVolumeName(), response.getBackupPaths().get(backupJoin.getKey()).getVolumeName()); } } @@ -263,25 +265,18 @@ private void assertBackupResponseOfferingFields(BackupResponse response) { Assert.assertEquals("test-offering", response.getBackupOffering()); } - private List getListOfBackupJoins(int listSize) { - List backupJoins = new java.util.ArrayList<>(); + private Map getMapOfBackupJoins(int backups) { + String basePath = "/backup/path/"; + String baseName = "VOL-"; + Map backupJoins = new HashMap<>(); + + for (int i = 0; i < backups; i++) { + String path = basePath + i; + String name = baseName + i; - for (int i = 0; i < listSize; i++) { - backupJoins.add(createBackupJoin()); + backupJoins.put(UUID.randomUUID().toString(), new BackupVolumeNameAndPath(path, name)); } return backupJoins; } - - private InternalBackupJoinVO createBackupJoin() { - String backupPath = "/backup/path"; - String volumeName = "ROOT"; - String volumeUuid = UUID.randomUUID().toString(); - InternalBackupJoinVO backupJoin = new InternalBackupJoinVO(); - ReflectionTestUtils.setField(backupJoin, "imageStorePath", backupPath); - ReflectionTestUtils.setField(backupJoin, "volumeName", volumeName); - ReflectionTestUtils.setField(backupJoin, "volumeUuid", volumeUuid); - - return backupJoin; - } } diff --git a/ui/public/config.json b/ui/public/config.json index 1ca42f04b67c..81d3938a5dee 100644 --- a/ui/public/config.json +++ b/ui/public/config.json @@ -105,155 +105,7 @@ "imageSelectionInterface": "modern", "showUserCategoryForModernImageSelection": true, "showAllCategoryForModernImageSelection": false, - "docHelpMappings": { - "adminguide/accounts.html": "adminguide/accounts.html", - "adminguide/accounts.html#domains": "adminguide/accounts.html#domains", - "adminguide/accounts.html#keypairs": "adminguide/accounts.html#keypairs", - "adminguide/accounts.html#roles": "adminguide/accounts.html#roles", - "adminguide/accounts.html#users": "adminguide/accounts.html#users", - "adminguide/accounts.html#using-an-ldap-server-for-user-authentication": "adminguide/accounts.html#using-an-ldap-server-for-user-authentication", - "adminguide/autoscale_with_virtual_router.html": "adminguide/autoscale_with_virtual_router.html", - "adminguide/events.html": "adminguide/events.html", - "adminguide/events.html#creating-webhooks": "adminguide/events.html#creating-webhooks", - "adminguide/events.html#deleting-and-archiving-events-and-alerts": "adminguide/events.html#deleting-and-archiving-events-and-alerts", - "adminguide/extensions.html": "adminguide/extensions.html", - "adminguide/extensions.html#custom-actions": "adminguide/extensions.html#custom-actions", - "adminguide/guest_os.html#guest-os": "adminguide/guest_os.html#guest-os", - "adminguide/guest_os.html#guest-os-categories": "adminguide/guest_os.html#guest-os-categories", - "adminguide/guest_os.html#guest-os-hypervisor-mapping": "adminguide/guest_os.html#guest-os-hypervisor-mapping", - "adminguide/hosts.html#disabling-and-enabling-zones-pods-and-clusters": "adminguide/hosts.html#disabling-and-enabling-zones-pods-and-clusters", - "adminguide/hosts.html?highlight=Hypervisor%20capabilities#hypervisor-capabilities": "adminguide/hosts.html?highlight=Hypervisor%20capabilities#hypervisor-capabilities", - "adminguide/hosts.html#kvm-rolling-maintenance": "adminguide/hosts.html#kvm-rolling-maintenance", - "adminguide/hosts.html#maintaining-hypervisors-on-hosts": "adminguide/hosts.html#maintaining-hypervisors-on-hosts", - "adminguide/hosts.html#out-of-band-management": "adminguide/hosts.html#out-of-band-management", - "adminguide/hosts.html#removing-hosts": "adminguide/hosts.html#removing-hosts", - "adminguide/index.html#tuning": "adminguide/index.html#tuning", - "adminguide/kms.html#adding-an-hsm-profile": "adminguide/kms.html#adding-an-hsm-profile", - "adminguide/kms.html#creating-a-kms-key": "adminguide/kms.html#creating-a-kms-key", - "adminguide/kms.html#migrating-existing-volumes-to-kms": "adminguide/kms.html#migrating-existing-volumes-to-kms", - "adminguide/kms.html#rotating-a-kms-key": "adminguide/kms.html#rotating-a-kms-key", - "adminguide/management.html#administrator-alerts": "adminguide/management.html#administrator-alerts", - "adminguide/management.html#metrics": "adminguide/management.html#metrics", - "adminguide/management.html#reporting-cpu-sockets": "adminguide/management.html#reporting-cpu-sockets", - "adminguide/nas_plugin.html": "adminguide/nas_plugin.html", - "adminguide/networking_and_traffic.html#acl-on-private-gateway": "adminguide/networking_and_traffic.html#acl-on-private-gateway", - "adminguide/networking_and_traffic.html#adding-an-additional-guest-network": "adminguide/networking_and_traffic.html#adding-an-additional-guest-network", - "adminguide/networking_and_traffic.html#adding-a-private-gateway-to-a-vpc": "adminguide/networking_and_traffic.html#adding-a-private-gateway-to-a-vpc", - "adminguide/networking_and_traffic.html#adding-a-security-group": "adminguide/networking_and_traffic.html#adding-a-security-group", - "adminguide/networking_and_traffic.html#adding-a-virtual-private-cloud": "adminguide/networking_and_traffic.html#adding-a-virtual-private-cloud", - "adminguide/networking_and_traffic.html#advanced-zone-physical-network-configuration": "adminguide/networking_and_traffic.html#advanced-zone-physical-network-configuration", - "adminguide/networking_and_traffic.html#basic-zone-physical-network-configuration": "adminguide/networking_and_traffic.html#basic-zone-physical-network-configuration", - "adminguide/networking_and_traffic.html#configure-guest-traffic-in-an-advanced-zone": "adminguide/networking_and_traffic.html#configure-guest-traffic-in-an-advanced-zone", - "adminguide/networking_and_traffic.html#configuring-a-virtual-private-cloud": "adminguide/networking_and_traffic.html#configuring-a-virtual-private-cloud", - "adminguide/networking_and_traffic.html#configuring-network-access-control-list": "adminguide/networking_and_traffic.html#configuring-network-access-control-list", - "adminguide/networking_and_traffic.html#creating-acl-lists": "adminguide/networking_and_traffic.html#creating-acl-lists", - "adminguide/networking_and_traffic.html#creating-and-updating-a-vpn-customer-gateway": "adminguide/networking_and_traffic.html#creating-and-updating-a-vpn-customer-gateway", - "adminguide/networking_and_traffic.html#creating-an-internal-lb-rule": "adminguide/networking_and_traffic.html#creating-an-internal-lb-rule", - "adminguide/networking_and_traffic.html#creating-a-vpn-connection": "adminguide/networking_and_traffic.html#creating-a-vpn-connection", - "adminguide/networking_and_traffic.html#creating-a-vpn-gateway-for-the-vpc": "adminguide/networking_and_traffic.html#creating-a-vpn-gateway-for-the-vpc", - "adminguide/networking_and_traffic.html#enabling-or-disabling-static-nat": "adminguide/networking_and_traffic.html#enabling-or-disabling-static-nat", - "adminguide/networking_and_traffic.html#load-balancing-across-tiers": "adminguide/networking_and_traffic.html#load-balancing-across-tiers", - "adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc": "adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc", - "adminguide/networking_and_traffic.html#reserving-public-ip-addresses-and-vlans-for-accounts": "adminguide/networking_and_traffic.html#reserving-public-ip-addresses-and-vlans-for-accounts", - "adminguide/networking_and_traffic.html#restarting-and-removing-a-vpn-connection": "adminguide/networking_and_traffic.html#restarting-and-removing-a-vpn-connection", - "adminguide/networking_and_traffic.html#security-groups": "adminguide/networking_and_traffic.html#security-groups", - "adminguide/networking_and_traffic.html#setting-up-a-site-to-site-vpn-connection": "adminguide/networking_and_traffic.html#setting-up-a-site-to-site-vpn-connection", - "adminguide/networking_and_traffic.html#updating-and-removing-a-vpn-customer-gateway": "adminguide/networking_and_traffic.html#updating-and-removing-a-vpn-customer-gateway", - "adminguide/networking.html#creating-a-new-network-offering": "adminguide/networking.html#creating-a-new-network-offering", - "adminguide/networking.html#network-offerings": "adminguide/networking.html#network-offerings", - "adminguide/networking.html#network-service-providers": "adminguide/networking.html#network-service-providers", - "adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances": "adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances", - "adminguide/object_storage.html#update-bucket": "adminguide/object_storage.html#update-bucket", - "adminguide/projects.html": "adminguide/projects.html", - "adminguide/projects.html#accepting-a-membership-invitation": "adminguide/projects.html#accepting-a-membership-invitation", - "adminguide/projects.html#adding-project-members-from-the-ui": "adminguide/projects.html#adding-project-members-from-the-ui", - "adminguide/projects.html#creating-a-new-project": "adminguide/projects.html#creating-a-new-project", - "adminguide/projects.html#sending-project-membership-invitations": "adminguide/projects.html#sending-project-membership-invitations", - "adminguide/projects.html#suspending-or-deleting-a-project": "adminguide/projects.html#suspending-or-deleting-a-project", - "adminguide/reliability.html#ha-for-hosts": "adminguide/reliability.html#ha-for-hosts", - "adminguide/service_offerings.html#compute-and-disk-service-offerings": "adminguide/service_offerings.html#compute-and-disk-service-offerings", - "adminguide/service_offerings.html#creating-a-new-compute-offering": "adminguide/service_offerings.html#creating-a-new-compute-offering", - "adminguide/service_offerings.html#creating-a-new-disk-offering": "adminguide/service_offerings.html#creating-a-new-disk-offering", - "adminguide/service_offerings.html#creating-a-new-system-service-offering": "adminguide/service_offerings.html#creating-a-new-system-service-offering", - "adminguide/service_offerings.html#modifying-or-deleting-a-service-offering": "adminguide/service_offerings.html#modifying-or-deleting-a-service-offering", - "adminguide/service_offerings.html#system-service-offerings": "adminguide/service_offerings.html#system-service-offerings", - "adminguide/storage.html#creating-a-new-file-share": "adminguide/storage.html#creating-a-new-file-share", - "adminguide/storage.html#creating-a-new-volume": "adminguide/storage.html#creating-a-new-volume", - "adminguide/storage.html#id2": "adminguide/storage.html#id2", - "adminguide/storage.html#lifecycle-operations": "adminguide/storage.html#lifecycle-operations", - "adminguide/storage.html#object-storage": "adminguide/storage.html#object-storage", - "adminguide/storage.html#primary-storage": "adminguide/storage.html#primary-storage", - "adminguide/storage.html#resizing-volumes": "adminguide/storage.html#resizing-volumes", - "adminguide/storage.html#secondary-storage": "adminguide/storage.html#secondary-storage", - "adminguide/storage.html#uploading-an-existing-volume-to-a-virtual-machine": "adminguide/storage.html#uploading-an-existing-volume-to-a-virtual-machine", - "adminguide/storage.html#working-with-volumes": "adminguide/storage.html#working-with-volumes", - "adminguide/storage.html#working-with-volume-snapshots": "adminguide/storage.html#working-with-volume-snapshots", - "adminguide/systemvm.html": "adminguide/systemvm.html", - "adminguide/systemvm.html#upgrading-virtual-routers": "adminguide/systemvm.html#upgrading-virtual-routers", - "adminguide/systemvm.html#virtual-router": "adminguide/systemvm.html#virtual-router", - "adminguide/templates.html": "adminguide/templates.html", - "adminguide/templates.html#attaching-an-iso-to-a-vm": "adminguide/templates.html#attaching-an-iso-to-a-vm", - "adminguide/templates.html#exporting-templates": "adminguide/templates.html#exporting-templates", - "adminguide/templates.html#id10": "adminguide/templates.html#id10", - "adminguide/templates.html#sharing-templates-with-other-accounts-projects": "adminguide/templates.html#sharing-templates-with-other-accounts-projects", - "adminguide/templates.html#uploading-templates-and-isos-from-a-local-computer": "adminguide/templates.html#uploading-templates-and-isos-from-a-local-computer", - "adminguide/templates.html#uploading-templates-from-a-remote-http-server": "adminguide/templates.html#uploading-templates-from-a-remote-http-server", - "adminguide/templates.html#working-with-isos": "adminguide/templates.html#working-with-isos", - "adminguide/virtual_machines.html": "adminguide/virtual_machines.html", - "adminguide/virtual_machines.html#affinity-groups": "adminguide/virtual_machines.html#affinity-groups", - "adminguide/virtual_machines.html#backup-offerings": "adminguide/virtual_machines.html#backup-offerings", - "adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm": "adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm", - "adminguide/virtual_machines.html#changing-the-vm-name-os-or-group": "adminguide/virtual_machines.html#changing-the-vm-name-os-or-group", - "adminguide/virtual_machines.html#creating-a-new-affinity-group": "adminguide/virtual_machines.html#creating-a-new-affinity-group", - "adminguide/virtual_machines.html#creating-a-new-instance-from-backup": "adminguide/virtual_machines.html#creating-a-new-instance-from-backup", - "adminguide/virtual_machines.html#creating-the-ssh-keypair": "adminguide/virtual_machines.html#creating-the-ssh-keypair", - "adminguide/virtual_machines.html#creating-vm-backups": "adminguide/virtual_machines.html#creating-vm-backups", - "adminguide/virtual_machines.html#creating-vms": "adminguide/virtual_machines.html#creating-vms", - "adminguide/virtual_machines.html#delete-an-affinity-group": "adminguide/virtual_machines.html#delete-an-affinity-group", - "adminguide/virtual_machines.html#deleting-vms": "adminguide/virtual_machines.html#deleting-vms", - "adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram": "adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram", - "adminguide/virtual_machines.html#importing-and-unmanaging-virtual-machine": "adminguide/virtual_machines.html#importing-and-unmanaging-virtual-machine", - "adminguide/virtual_machines.html#importing-and-unmanaging-volume": "adminguide/virtual_machines.html#importing-and-unmanaging-volume", - "adminguide/virtual_machines.html#importing-backup-offerings": "adminguide/virtual_machines.html#importing-backup-offerings", - "adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration": "adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration", - "adminguide/virtual_machines.html#resetting-ssh-keys": "adminguide/virtual_machines.html#resetting-ssh-keys", - "adminguide/virtual_machines.html#resetting-userdata": "adminguide/virtual_machines.html#resetting-userdata", - "adminguide/virtual_machines.html#restoring-instance-backups": "adminguide/virtual_machines.html#restoring-instance-backups", - "adminguide/virtual_machines.html#restoring-vm-backups": "adminguide/virtual_machines.html#restoring-vm-backups", - "adminguide/virtual_machines.html#stopping-and-starting-vms": "adminguide/virtual_machines.html#stopping-and-starting-vms", - "adminguide/virtual_machines.html#user-data-and-meta-data": "adminguide/virtual_machines.html#user-data-and-meta-data", - "adminguide/virtual_machines.html#using-ssh-keys-for-authentication": "adminguide/virtual_machines.html#using-ssh-keys-for-authentication", - "adminguide/virtual_machines.html#virtual-machine-snapshots": "adminguide/virtual_machines.html#virtual-machine-snapshots", - "adminguide/webhooks.html": "adminguide/webhooks.html", - "conceptsandterminology/concepts.html#about-clusters": "conceptsandterminology/concepts.html#about-clusters", - "conceptsandterminology/concepts.html#about-hosts": "conceptsandterminology/concepts.html#about-hosts", - "conceptsandterminology/concepts.html#about-pods": "conceptsandterminology/concepts.html#about-pods", - "conceptsandterminology/concepts.html#about-zones": "conceptsandterminology/concepts.html#about-zones", - "conceptsandterminology/concepts.html#management-server-overview": "conceptsandterminology/concepts.html#management-server-overview", - "conceptsandterminology/network_setup.html#vlan-allocation-example": "conceptsandterminology/network_setup.html#vlan-allocation-example", - "installguide/configuration.html#adding-a-cluster": "installguide/configuration.html#adding-a-cluster", - "installguide/configuration.html#adding-a-host": "installguide/configuration.html#adding-a-host", - "installguide/configuration.html#adding-a-pod": "installguide/configuration.html#adding-a-pod", - "installguide/configuration.html#adding-a-zone": "installguide/configuration.html#adding-a-zone", - "installguide/configuration.html#add-object-storage": "installguide/configuration.html#add-object-storage", - "installguide/configuration.html#add-primary-storage": "installguide/configuration.html#add-primary-storage", - "installguide/configuration.html#add-secondary-storage": "installguide/configuration.html#add-secondary-storage", - "installguide/configuration.html#create-bucket": "installguide/configuration.html#create-bucket", - "plugins/cloudian-connector.html": "plugins/cloudian-connector.html", - "plugins/cloudstack-kubernetes-service.html": "plugins/cloudstack-kubernetes-service.html", - "plugins/cloudstack-kubernetes-service.html#creating-a-new-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#creating-a-new-kubernetes-cluster", - "plugins/cloudstack-kubernetes-service.html#deleting-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#deleting-kubernetes-cluster", - "plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions": "plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions", - "plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster", - "plugins/cloudstack-kubernetes-service.html#starting-a-stopped-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#starting-a-stopped-kubernetes-cluster", - "plugins/cloudstack-kubernetes-service.html#stopping-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#stopping-kubernetes-cluster", - "plugins/cloudstack-kubernetes-service.html#upgrading-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#upgrading-kubernetes-cluster", - "plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering": "plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering", - "plugins/nuage-plugin.html?#vpc-offerings": "plugins/nuage-plugin.html?#vpc-offerings", - "plugins/quota.html": "plugins/quota.html", - "plugins/quota.html#quota-credits": "plugins/quota.html#quota-credits", - "plugins/quota.html#quota-tariff": "plugins/quota.html#quota-tariff" - }, + "docHelpMappings": {}, "notifyLatestCSVersion": true, "showSearchFilters": true, "announcementBanner": { diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 6d7bd1f2f97d..6f82535b6fdf 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -70,7 +70,7 @@
- {{ backupVolumeInfo.volumeName }} - {{ backupVolumeInfo.backupPath }} + {{ backupVolumeInfo.volumename }} - {{ backupVolumeInfo.backuppath }}
From acf05cad8db9cd3f45c7e66cb7d070a07e6c4694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= Date: Thu, 12 Nov 2026 15:33:45 -0300 Subject: [PATCH 9/9] Use volume UUID as list element key --- ui/src/components/view/DetailsTab.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 6f82535b6fdf..b6ca1442ec98 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -69,7 +69,7 @@
-
+
{{ backupVolumeInfo.volumename }} - {{ backupVolumeInfo.backuppath }}