Skip to content

Secondary storage backup path GUI addition - #14255

Open
erikbocks wants to merge 6 commits into
apache:mainfrom
scclouds:secondary-storage-backup-path-kboss
Open

erikbocks wants to merge 6 commits into
apache:mainfrom
scclouds:secondary-storage-backup-path-kboss

Conversation

@erikbocks

Copy link
Copy Markdown
Collaborator

Description

Currently, the backup's path in the secondary storage is not displayed in the GUI, being necessary to query it directly from the database. Therefore, the backuppaths attribute, with the path of the backup of each VM's disk was added to the listBackups API response, and to the backup details in the GUI. Only Root Admin accounts are allowed to view the backup paths in the GUI and in the API response. Currently only supported by KBOSS provider.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Feature/Enhancement Scale

  • Major
  • Minor

Screenshots (if appropriate):

Backup paths in the GUI Screenshot from 2026-09-28 14-43-08

How Has This Been Tested?

I configured KBOSS as the backup provider, and created a new backup of an existing VM. Then, I accessed the backup's details tab and validated that the new field was added successfully. I also validated that the backuppaths attribute was present at the listBackups API response.

Copilot AI lite review requested due to automatic review settings September 28, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.18605% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.40%. Comparing base (602d9ec) to head (ae4431a).
⚠️ Report is 46 commits behind head on main.

Files with missing lines Patch % Lines
...k/api/response/helper/BackupVolumeNameAndPath.java 35.29% 11 Missing ⚠️
...apache/cloudstack/backup/InternalBackupJoinVO.java 33.33% 4 Missing ⚠️
...apache/cloudstack/api/response/BackupResponse.java 50.00% 3 Missing ⚠️
...ck/api/command/admin/backup/FinalizeBackupCmd.java 0.00% 2 Missing ⚠️
...stack/api/command/admin/backup/StartBackupCmd.java 0.00% 2 Missing ⚠️
...dstack/api/command/user/backup/ListBackupsCmd.java 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #14255      +/-   ##
============================================
+ Coverage     19.91%   20.40%   +0.48%     
- Complexity    20200    21289    +1089     
============================================
  Files          6373     6482     +109     
  Lines        577230   588079   +10849     
  Branches      70696    72022    +1326     
============================================
+ Hits         114974   119985    +5011     
- Misses       449690   455073    +5383     
- Partials      12566    13021     +455     
Flag Coverage Δ
uitests 3.70% <ø> (-0.01%) ⬇️
unittests 21.70% <44.18%> (+0.51%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erikbocks
erikbocks marked this pull request as draft September 29, 2026 18:37
@erikbocks

Copy link
Copy Markdown
Collaborator Author

I forgot to change the test, so I will leave this one as draft for a bit before I can fix it.

response.setToCheckpointId(backup.getToCheckpointId());
}

if (KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are fetching offering from dao layer and, what if it would be null? we can have null check here for offering to ignore NPE

Map<String, String> backupVolumePaths = new HashMap<>();

backupJoins.forEach(b -> {
backupVolumePaths.put(b.getVolumeName(), b.getImageStorePath());

@rajiv-jain-netapp rajiv-jain-netapp Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloudStack does not enforce uniqueness for volume names. The same volume name can be used across multiple VMs, and users can also rename volumes after creation. Given this, if multiple volumes end up sharing the same name, this put() operation could silently overwrite the path mapping for an unintended volume, potentially resulting in incorrect path associations. you can count on volumeUUID instead.

Additionally, considering that multiple secondary storage pools can exist within a zone, we could optimise the map value by storing a composite identifier that includes both the image path and the image UUID/name, rather than just the imageStorePath. This would provide a more reliable way to uniquely identify image locations while also improving maintainability and future scalability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current tests are only happy paths, can we add few negative ones also for example,

  1. what would happen for non-admin user
  2. anything other than KBOSS
  3. null offerings

@erikbocks

Copy link
Copy Markdown
Collaborator Author

@rajiv-jain-netapp, thanks for the reviews!

I made the following changes to this PR: Implemented null check for offerings, Changed how the backup path is stored in the response map and improved the test cases.

For the backup path map, thank you for pointing out about the same volume name replacement scenario. I changed the map to now use the volume UUID as key. Also, I created a new structure for the map value that stores the volume name and its image storage path as attributes.

Regarding the usage of storage data for the response, as the only value stored by internal_backup_view is the image store ID, obtained by fetching the backup details, I preferred not to include storage data because it would be necessary to include a new join on the image store table. Can you give your opinion on this?

For the tests, I extracted the BackupResponse related method to a separated class, so we could target the specific test cases for backup responses and reuse existing code for different scenarios. The following cases are addressed in this file:

  • Default response (the test that already exists);
  • KBOSS backup response for Admins;
  • KBOSS backup response for normal users;
  • KBOSS backup response with multiple equal named volumes;
  • KBOSS backup response with null offering

@erikbocks
erikbocks marked this pull request as ready for review October 19, 2026 18:59
Copilot AI lite review requested due to automatic review settings October 19, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String getBackupNameFromVM(VirtualMachine vm);

BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails);
BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin);
Comment on lines 37 to 50
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;
Comment on lines +71 to +75
<div v-else-if="$route.meta.name === 'backup' && item === 'backuppaths'">
<div v-for="(backupVolumeInfo, idx) in Object.values(dataResource[item])" :key="idx">
<span>{{ backupVolumeInfo.volumeName }} - {{ backupVolumeInfo.backupPath }}</span>
</div>
</div>
Comment on lines +20 to +27
public class BackupVolumeNameAndPath {
private String volumeName;
private String backupPath;

public BackupVolumeNameAndPath(String volumeName, String backupPath) {
this.volumeName = volumeName;
this.backupPath = backupPath;
}
Comment on lines +2684 to +2693
if (offering != null && KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) {
List<InternalBackupJoinVO> backupJoins = internalBackupJoinDao.listById(backup.getId());
Map<String, BackupVolumeNameAndPath> backupVolumePaths = new HashMap<>();

backupJoins.forEach(b -> {
backupVolumePaths.put(b.getVolumeUuid(), new BackupVolumeNameAndPath(b.getVolumeName(), b.getImageStorePath()));
});

response.setBackupPaths(backupVolumePaths);
}
Comment on lines +103 to +105
@SerializedName(ApiConstants.BACKUP_PATHS)
@Param(description = "The backups paths in the secondary storage")
private Map<String, BackupVolumeNameAndPath> backupPaths;
Copilot AI lite review requested due to automatic review settings October 19, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

12 open issues

Review effort: Lite

Comment on lines +71 to +75
<div v-else-if="$route.meta.name === 'backup' && item === 'backuppaths'">
<div v-for="(backupVolumeInfo, idx) in Object.values(dataResource[item])" :key="idx">
<span>{{ backupVolumeInfo.volumeName }} - {{ backupVolumeInfo.backupPath }}</span>
</div>
</div>
String getBackupNameFromVM(VirtualMachine vm);

BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails);
BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails, boolean isCallerRootAdmin);
Comment on lines 37 to 50
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;
Comment on lines +2684 to +2693
if (offering != null && KBOSS_BACKUP_PROVIDER.equals(offering.getProvider()) && isCallerRootAdmin) {
List<InternalBackupJoinVO> backupJoins = internalBackupJoinDao.listById(backup.getId());
Map<String, BackupVolumeNameAndPath> backupVolumePaths = new HashMap<>();

backupJoins.forEach(b -> {
backupVolumePaths.put(b.getVolumeUuid(), new BackupVolumeNameAndPath(b.getVolumeName(), b.getImageStorePath()));
});

response.setBackupPaths(backupVolumePaths);
}
Comment on lines +103 to +105
@SerializedName(ApiConstants.BACKUP_PATHS)
@Param(description = "The backups paths in the secondary storage")
private Map<String, BackupVolumeNameAndPath> backupPaths;
</div>
</div>
<div v-else-if="$route.meta.name === 'backup' && item === 'backuppaths'">
<div v-for="(backupVolumeInfo, idx) in Object.values(dataResource[item])" :key="idx">

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants