This is an automated email from the ASF dual-hosted git repository.
harikrishna pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 8b2f1f19c27 Support dedicating backup offerings to domains (#12194)
8b2f1f19c27 is described below
commit 8b2f1f19c27bebf908f9cda53ec1fadd005e2520
Author: Pearl Dsilva <[email protected]>
AuthorDate: Mon Jan 19 03:51:47 2026 -0500
Support dedicating backup offerings to domains (#12194)
* Add support for dedicating backup offerings to domains
* Add tests and UI support and update response params
* add license header
* exclude backupofferingdetailsvo from sonar
* fix pre-commit checks - missing / extra EOF line
* add test
* EOF
* filter backup offerings by domain id
* add unit tests
* add more unit tests and remove response file from code coverage check
* update checks
* address review comments: extract common code, fix tests
* added bean definition
* address comments
* add unit tests to increase coverage
* pre-commit check failure fix
* address merge issue
* allow updating backup offering when only domain id is modified
---
.../main/java/com/cloud/user/AccountService.java | 3 +
.../org/apache/cloudstack/acl/SecurityChecker.java | 4 +
.../apache/cloudstack/api/BaseBackupListCmd.java | 2 +-
.../admin/backup/ImportBackupOfferingCmd.java | 22 ++
.../admin/backup/UpdateBackupOfferingCmd.java | 28 +-
.../admin/network/UpdateNetworkOfferingCmd.java | 65 +---
.../admin/offering/UpdateDiskOfferingCmd.java | 62 +---
.../admin/offering/UpdateServiceOfferingCmd.java | 62 +---
.../command/admin/vpc/UpdateVPCOfferingCmd.java | 64 +---
.../command/offering/DomainAndZoneIdResolver.java | 114 +++++++
.../api/response/BackupOfferingResponse.java | 19 ++
.../apache/cloudstack/backup/BackupManager.java | 2 +
.../offering/DomainAndZoneIdResolverTest.java | 149 +++++++++
.../cloudstack/backup/BackupOfferingDetailsVO.java | 86 +++++
.../apache/cloudstack/backup/BackupOfferingVO.java | 7 +
.../backup/dao/BackupOfferingDaoImpl.java | 23 +-
.../backup/dao/BackupOfferingDetailsDao.java | 32 ++
.../backup/dao/BackupOfferingDetailsDaoImpl.java | 101 ++++++
...n-daos-between-management-and-usage-context.xml | 3 +-
.../resources/META-INF/db/schema-42210to42300.sql | 10 +
.../dao/BackupOfferingDetailsDaoImplTest.java | 251 ++++++++++++++
plugins/hypervisors/ovm3/sonar-project.properties | 2 +-
.../contrail/management/MockAccountManager.java | 6 +
pom.xml | 2 +
.../src/main/java/com/cloud/acl/DomainChecker.java | 33 ++
.../configuration/ConfigurationManagerImpl.java | 47 +--
.../java/com/cloud/network/vpc/VpcManagerImpl.java | 31 +-
.../java/com/cloud/user/AccountManagerImpl.java | 16 +
.../main/java/com/cloud/utils/DomainHelper.java | 63 ++++
.../cloudstack/backup/BackupManagerImpl.java | 117 ++++++-
.../core/spring-server-core-misc-context.xml | 2 +
.../test/java/com/cloud/acl/DomainCheckerTest.java | 45 +++
.../ConfigurationManagerImplTest.java | 3 +
.../java/com/cloud/vm/UserVmManagerImplTest.java | 3 +-
.../cloudstack/backup/BackupManagerTest.java | 366 ++++++++++++++++++++-
.../networkoffering/CreateNetworkOfferingTest.java | 4 +
tools/marvin/setup.py | 2 +-
ui/src/config/section/offering.js | 6 +-
ui/src/views/offering/ImportBackupOffering.vue | 69 +++-
39 files changed, 1610 insertions(+), 316 deletions(-)
diff --git a/api/src/main/java/com/cloud/user/AccountService.java
b/api/src/main/java/com/cloud/user/AccountService.java
index 09fe5ffc059..8f29a2fbc42 100644
--- a/api/src/main/java/com/cloud/user/AccountService.java
+++ b/api/src/main/java/com/cloud/user/AccountService.java
@@ -36,6 +36,7 @@ import com.cloud.offering.DiskOffering;
import com.cloud.offering.NetworkOffering;
import com.cloud.offering.ServiceOffering;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
+import org.apache.cloudstack.backup.BackupOffering;
public interface AccountService {
@@ -115,6 +116,8 @@ public interface AccountService {
void checkAccess(Account account, VpcOffering vof, DataCenter zone) throws
PermissionDeniedException;
+ void checkAccess(Account account, BackupOffering bof) throws
PermissionDeniedException;
+
void checkAccess(User user, ControlledEntity entity);
void checkAccess(Account account, AccessType accessType, boolean
sameOwner, String apiName, ControlledEntity... entities) throws
PermissionDeniedException;
diff --git a/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java
b/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java
index 82a8ec5fe93..fa17df7c6ed 100644
--- a/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java
+++ b/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java
@@ -27,6 +27,8 @@ import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.component.Adapter;
+import org.apache.cloudstack.backup.BackupOffering;
+
/**
* SecurityChecker checks the ownership and access control to objects within
*/
@@ -145,4 +147,6 @@ public interface SecurityChecker extends Adapter {
boolean checkAccess(Account account, NetworkOffering nof, DataCenter zone)
throws PermissionDeniedException;
boolean checkAccess(Account account, VpcOffering vof, DataCenter zone)
throws PermissionDeniedException;
+
+ boolean checkAccess(Account account, BackupOffering bof) throws
PermissionDeniedException;
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java
b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java
index 0aa8366bcd5..2a64a1fb6fd 100644
--- a/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java
@@ -25,7 +25,7 @@ import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.context.CallContext;
-public abstract class BaseBackupListCmd extends BaseListCmd {
+public abstract class BaseBackupListCmd extends BaseListAccountResourcesCmd {
protected void setupResponseBackupOfferingsList(final List<BackupOffering>
offerings, final Integer count) {
final ListResponse<BackupOfferingResponse> response = new
ListResponse<>();
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java
index 2e73698e7aa..5e702585a2c 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java
@@ -27,6 +27,7 @@ import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
+import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
@@ -40,6 +41,11 @@ import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.commons.collections.CollectionUtils;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
@APICommand(name = "importBackupOffering",
description = "Imports a backup offering using a backup provider",
@@ -76,6 +82,13 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
description = "Whether users are allowed to create adhoc backups
and backup schedules", required = true)
private Boolean userDrivenBackups;
+ @Parameter(name = ApiConstants.DOMAIN_ID,
+ type = CommandType.LIST,
+ collectionType = CommandType.UUID,
+ entityType = DomainResponse.class,
+ description = "the ID of the containing domain(s), null for public
offerings")
+ private List<Long> domainIds;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -100,6 +113,15 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
return userDrivenBackups == null ? false : userDrivenBackups;
}
+ public List<Long> getDomainIds() {
+ if (CollectionUtils.isNotEmpty(domainIds)) {
+ Set<Long> set = new LinkedHashSet<>(domainIds);
+ domainIds.clear();
+ domainIds.addAll(set);
+ }
+ return domainIds;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java
index a645b1e0c8d..2f0dd6acd0e 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java
@@ -25,19 +25,24 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
import
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.Account;
import com.cloud.utils.exception.CloudRuntimeException;
+import java.util.List;
+import java.util.function.LongFunction;
+
@APICommand(name = "updateBackupOffering", description = "Updates a backup
offering.", responseObject = BackupOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since =
"4.16.0")
-public class UpdateBackupOfferingCmd extends BaseCmd {
+public class UpdateBackupOfferingCmd extends BaseCmd implements
DomainAndZoneIdResolver {
@Inject
private BackupManager backupManager;
@@ -57,6 +62,13 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type =
CommandType.BOOLEAN, description = "Whether to allow user driven backups or
not")
private Boolean allowUserDrivenBackups;
+ @Parameter(name = ApiConstants.DOMAIN_ID,
+ type = CommandType.STRING,
+ description = "the ID of the containing domain(s) as comma
separated string, public for public offerings",
+ since = "4.23.0",
+ length = 4096)
+ private String domainIds;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -82,7 +94,7 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
@Override
public void execute() {
try {
- if (StringUtils.isAllEmpty(getName(), getDescription()) &&
getAllowUserDrivenBackups() == null) {
+ if (StringUtils.isAllEmpty(getName(), getDescription()) &&
getAllowUserDrivenBackups() == null && CollectionUtils.isEmpty(getDomainIds()))
{
throw new InvalidParameterValueException(String.format("Can't
update Backup Offering [id: %s] because there are no parameters to be updated,
at least one of the",
"following should be informed: name, description or
allowUserDrivenBackups.", id));
}
@@ -103,6 +115,18 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
}
}
+ public List<Long> getDomainIds() {
+ // backupManager may be null in unit tests where the command is spied
without injection.
+ // Avoid creating a method reference to a null receiver which causes
NPE. When backupManager
+ // is null, pass null as the defaultDomainsProvider so
resolveDomainIds will simply return
+ // an empty list or parse the explicit domainIds string.
+ LongFunction<List<Long>> defaultDomainsProvider = null;
+ if (backupManager != null) {
+ defaultDomainsProvider = backupManager::getBackupOfferingDomains;
+ }
+ return resolveDomainIds(domainIds, id, defaultDomainsProvider, "backup
offering");
+ }
+
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
index 9af10262b2d..e3fac81a793 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
@@ -16,7 +16,6 @@
// under the License.
package org.apache.cloudstack.api.command.admin.network;
-import java.util.ArrayList;
import java.util.List;
import org.apache.cloudstack.api.APICommand;
@@ -26,18 +25,16 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.NetworkOfferingResponse;
-import org.apache.commons.lang3.StringUtils;
-import com.cloud.dc.DataCenter;
-import com.cloud.domain.Domain;
-import com.cloud.exception.InvalidParameterValueException;
+
import com.cloud.offering.NetworkOffering;
import com.cloud.user.Account;
@APICommand(name = "updateNetworkOffering", description = "Updates a network
offering.", responseObject = NetworkOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
-public class UpdateNetworkOfferingCmd extends BaseCmd {
+public class UpdateNetworkOfferingCmd extends BaseCmd implements
DomainAndZoneIdResolver {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@@ -129,63 +126,11 @@ public class UpdateNetworkOfferingCmd extends BaseCmd {
}
public List<Long> getDomainIds() {
- List<Long> validDomainIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(domainIds)) {
- if (domainIds.contains(",")) {
- String[] domains = domainIds.split(",");
- for (String domain : domains) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domain.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create network offering because invalid domain has been specified.");
- }
- }
- } else {
- domainIds = domainIds.trim();
- if (!domainIds.matches("public")) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domainIds.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create network offering because invalid domain has been specified.");
- }
- }
- }
- } else {
-
validDomainIds.addAll(_configService.getNetworkOfferingDomains(id));
- }
- return validDomainIds;
+ return resolveDomainIds(domainIds, id,
_configService::getNetworkOfferingDomains, "network offering");
}
public List<Long> getZoneIds() {
- List<Long> validZoneIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(zoneIds)) {
- if (zoneIds.contains(",")) {
- String[] zones = zoneIds.split(",");
- for (String zone : zones) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zone.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create network offering because invalid zone has been specified.");
- }
- }
- } else {
- zoneIds = zoneIds.trim();
- if (!zoneIds.matches("all")) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zoneIds.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create network offering because invalid zone has been specified.");
- }
- }
- }
- } else {
- validZoneIds.addAll(_configService.getNetworkOfferingZones(id));
- }
- return validZoneIds;
+ return resolveZoneIds(zoneIds, id,
_configService::getNetworkOfferingZones, "network offering");
}
/////////////////////////////////////////////////////
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java
index 2f07f85f983..917d7ff42d8 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java
@@ -16,7 +16,6 @@
// under the License.
package org.apache.cloudstack.api.command.admin.offering;
-import java.util.ArrayList;
import java.util.List;
import com.cloud.offering.DiskOffering.State;
@@ -27,19 +26,18 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
-import com.cloud.dc.DataCenter;
-import com.cloud.domain.Domain;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.offering.DiskOffering;
import com.cloud.user.Account;
@APICommand(name = "updateDiskOffering", description = "Updates a disk
offering.", responseObject = DiskOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
-public class UpdateDiskOfferingCmd extends BaseCmd {
+public class UpdateDiskOfferingCmd extends BaseCmd implements
DomainAndZoneIdResolver {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@@ -151,63 +149,11 @@ public class UpdateDiskOfferingCmd extends BaseCmd {
}
public List<Long> getDomainIds() {
- List<Long> validDomainIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(domainIds)) {
- if (domainIds.contains(",")) {
- String[] domains = domainIds.split(",");
- for (String domain : domains) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domain.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create disk offering because invalid domain has been specified.");
- }
- }
- } else {
- domainIds = domainIds.trim();
- if (!domainIds.matches("public")) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domainIds.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create disk offering because invalid domain has been specified.");
- }
- }
- }
- } else {
- validDomainIds.addAll(_configService.getDiskOfferingDomains(id));
- }
- return validDomainIds;
+ return resolveDomainIds(domainIds, id,
_configService::getDiskOfferingDomains, "disk offering");
}
public List<Long> getZoneIds() {
- List<Long> validZoneIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(zoneIds)) {
- if (zoneIds.contains(",")) {
- String[] zones = zoneIds.split(",");
- for (String zone : zones) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zone.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create disk offering because invalid zone has been specified.");
- }
- }
- } else {
- zoneIds = zoneIds.trim();
- if (!zoneIds.matches("all")) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zoneIds.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create disk offering because invalid zone has been specified.");
- }
- }
- }
- } else {
- validZoneIds.addAll(_configService.getDiskOfferingZones(id));
- }
- return validZoneIds;
+ return resolveZoneIds(zoneIds, id,
_configService::getDiskOfferingZones, "disk offering");
}
public String getTags() {
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java
index 4027662574a..3a6d6639a5b 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java
@@ -16,7 +16,6 @@
// under the License.
package org.apache.cloudstack.api.command.admin.offering;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -28,19 +27,18 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
-import com.cloud.dc.DataCenter;
-import com.cloud.domain.Domain;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.offering.ServiceOffering;
import com.cloud.user.Account;
@APICommand(name = "updateServiceOffering", description = "Updates a service
offering.", responseObject = ServiceOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
-public class UpdateServiceOfferingCmd extends BaseCmd {
+public class UpdateServiceOfferingCmd extends BaseCmd implements
DomainAndZoneIdResolver {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@@ -130,63 +128,11 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
}
public List<Long> getDomainIds() {
- List<Long> validDomainIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(domainIds)) {
- if (domainIds.contains(",")) {
- String[] domains = domainIds.split(",");
- for (String domain : domains) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domain.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create service offering because invalid domain has been specified.");
- }
- }
- } else {
- domainIds = domainIds.trim();
- if (!domainIds.matches("public")) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domainIds.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create service offering because invalid domain has been specified.");
- }
- }
- }
- } else {
-
validDomainIds.addAll(_configService.getServiceOfferingDomains(id));
- }
- return validDomainIds;
+ return resolveDomainIds(domainIds, id,
_configService::getServiceOfferingDomains, "service offering");
}
public List<Long> getZoneIds() {
- List<Long> validZoneIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(zoneIds)) {
- if (zoneIds.contains(",")) {
- String[] zones = zoneIds.split(",");
- for (String zone : zones) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zone.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create service offering because invalid zone has been specified.");
- }
- }
- } else {
- zoneIds = zoneIds.trim();
- if (!zoneIds.matches("all")) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zoneIds.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create service offering because invalid zone has been specified.");
- }
- }
- }
- } else {
- validZoneIds.addAll(_configService.getServiceOfferingZones(id));
- }
- return validZoneIds;
+ return resolveZoneIds(zoneIds, id,
_configService::getServiceOfferingZones, "service offering");
}
public String getStorageTags() {
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java
index b8a8077b30b..300584428ea 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java
@@ -16,7 +16,6 @@
// under the License.
package org.apache.cloudstack.api.command.admin.vpc;
-import java.util.ArrayList;
import java.util.List;
import org.apache.cloudstack.api.APICommand;
@@ -26,19 +25,16 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.VpcOfferingResponse;
-import org.apache.commons.lang3.StringUtils;
-import com.cloud.dc.DataCenter;
-import com.cloud.domain.Domain;
import com.cloud.event.EventTypes;
-import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.vpc.VpcOffering;
import com.cloud.user.Account;
@APICommand(name = "updateVPCOffering", description = "Updates VPC offering",
responseObject = VpcOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
-public class UpdateVPCOfferingCmd extends BaseAsyncCmd {
+public class UpdateVPCOfferingCmd extends BaseAsyncCmd implements
DomainAndZoneIdResolver {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@@ -92,63 +88,11 @@ public class UpdateVPCOfferingCmd extends BaseAsyncCmd {
}
public List<Long> getDomainIds() {
- List<Long> validDomainIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(domainIds)) {
- if (domainIds.contains(",")) {
- String[] domains = domainIds.split(",");
- for (String domain : domains) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domain.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create VPC offering because invalid domain has been specified.");
- }
- }
- } else {
- domainIds = domainIds.trim();
- if (!domainIds.matches("public")) {
- Domain validDomain = _entityMgr.findByUuid(Domain.class,
domainIds.trim());
- if (validDomain != null) {
- validDomainIds.add(validDomain.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create VPC offering because invalid domain has been specified.");
- }
- }
- }
- } else {
- validDomainIds.addAll(_vpcProvSvc.getVpcOfferingDomains(id));
- }
- return validDomainIds;
+ return resolveDomainIds(domainIds, id,
_vpcProvSvc::getVpcOfferingDomains, "VPC offering");
}
public List<Long> getZoneIds() {
- List<Long> validZoneIds = new ArrayList<>();
- if (StringUtils.isNotEmpty(zoneIds)) {
- if (zoneIds.contains(",")) {
- String[] zones = zoneIds.split(",");
- for (String zone : zones) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zone.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create VPC offering because invalid zone has been specified.");
- }
- }
- } else {
- zoneIds = zoneIds.trim();
- if (!zoneIds.matches("all")) {
- DataCenter validZone =
_entityMgr.findByUuid(DataCenter.class, zoneIds.trim());
- if (validZone != null) {
- validZoneIds.add(validZone.getId());
- } else {
- throw new InvalidParameterValueException("Failed to
create VPC offering because invalid zone has been specified.");
- }
- }
- }
- } else {
- validZoneIds.addAll(_vpcProvSvc.getVpcOfferingZones(id));
- }
- return validZoneIds;
+ return resolveZoneIds(zoneIds, id, _vpcProvSvc::getVpcOfferingZones,
"VPC offering");
}
public Integer getSortKey() {
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java
b/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java
new file mode 100644
index 00000000000..b302c4a9bee
--- /dev/null
+++
b/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java
@@ -0,0 +1,114 @@
+// 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.command.offering;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.LongFunction;
+
+import com.cloud.dc.DataCenter;
+import com.cloud.domain.Domain;
+import com.cloud.exception.InvalidParameterValueException;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Helper for commands that accept a domainIds or zoneIds string and need to
+ * resolve them to lists of IDs, falling back to an offering-specific
+ * default provider.
+ */
+public interface DomainAndZoneIdResolver {
+ /**
+ * Parse the provided domainIds string and return a list of domain IDs.
+ * If domainIds is empty, the defaultDomainsProvider will be invoked with
the
+ * provided resource id to obtain the current domains.
+ */
+ default List<Long> resolveDomainIds(final String domainIds, final Long id,
final LongFunction<List<Long>> defaultDomainsProvider, final String
resourceTypeName) {
+ final List<Long> validDomainIds = new ArrayList<>();
+ final BaseCmd base = (BaseCmd) this;
+ final Logger logger = LogManager.getLogger(base.getClass());
+
+ if (StringUtils.isEmpty(domainIds)) {
+ if (defaultDomainsProvider != null) {
+ final List<Long> defaults = defaultDomainsProvider.apply(id);
+ if (defaults != null) {
+ validDomainIds.addAll(defaults);
+ }
+ }
+ return validDomainIds;
+ }
+
+ final String[] domains = domainIds.split(",");
+ final String type = (resourceTypeName == null ||
resourceTypeName.isEmpty()) ? "offering" : resourceTypeName;
+ for (String domain : domains) {
+ final String trimmed = domain == null ? "" : domain.trim();
+ if (trimmed.isEmpty() || "public".equalsIgnoreCase(trimmed)) {
+ continue;
+ }
+
+ final Domain validDomain =
base._entityMgr.findByUuid(Domain.class, trimmed);
+ if (validDomain == null) {
+ logger.warn("Invalid domain specified for {}", type);
+ throw new InvalidParameterValueException("Failed to create " +
type + " because invalid domain has been specified.");
+ }
+ validDomainIds.add(validDomain.getId());
+ }
+
+ return validDomainIds;
+ }
+
+ /**
+ * Parse the provided zoneIds string and return a list of zone IDs.
+ * If zoneIds is empty, the defaultZonesProvider will be invoked with the
+ * provided resource id to obtain the current zones.
+ */
+ default List<Long> resolveZoneIds(final String zoneIds, final Long id,
final LongFunction<List<Long>> defaultZonesProvider, final String
resourceTypeName) {
+ final List<Long> validZoneIds = new ArrayList<>();
+ final BaseCmd base = (BaseCmd) this;
+ final Logger logger = LogManager.getLogger(base.getClass());
+
+ if (StringUtils.isEmpty(zoneIds)) {
+ if (defaultZonesProvider != null) {
+ final List<Long> defaults = defaultZonesProvider.apply(id);
+ if (defaults != null) {
+ validZoneIds.addAll(defaults);
+ }
+ }
+ return validZoneIds;
+ }
+
+ final String[] zones = zoneIds.split(",");
+ final String type = (resourceTypeName == null ||
resourceTypeName.isEmpty()) ? "offering" : resourceTypeName;
+ for (String zone : zones) {
+ final String trimmed = zone == null ? "" : zone.trim();
+ if (trimmed.isEmpty() || "all".equalsIgnoreCase(trimmed)) {
+ continue;
+ }
+
+ final DataCenter validZone =
base._entityMgr.findByUuid(DataCenter.class, trimmed);
+ if (validZone == null) {
+ logger.warn("Invalid zone specified for {}: {}", type,
trimmed);
+ throw new InvalidParameterValueException("Failed to create " +
type + " because invalid zone has been specified.");
+ }
+ validZoneIds.add(validZone.getId());
+ }
+
+ return validZoneIds;
+ }
+}
diff --git
a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java
b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java
index b3a7d036219..c4f3ee31dad 100644
---
a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java
+++
b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java
@@ -61,6 +61,16 @@ public class BackupOfferingResponse extends BaseResponse {
@Param(description = "Zone name")
private String zoneName;
+ @SerializedName(ApiConstants.DOMAIN_ID)
+ @Param(description = "the domain ID(s) this backup offering belongs to.",
+ since = "4.23.0")
+ private String domainId;
+
+ @SerializedName(ApiConstants.DOMAIN)
+ @Param(description = "the domain name(s) this backup offering belongs to.",
+ since = "4.23.0")
+ private String domain;
+
@SerializedName(ApiConstants.CROSS_ZONE_INSTANCE_CREATION)
@Param(description = "the backups with this offering can be used to create
Instances on all Zones", since = "4.22.0")
private Boolean crossZoneInstanceCreation;
@@ -108,4 +118,13 @@ public class BackupOfferingResponse extends BaseResponse {
public void setCreated(Date created) {
this.created = created;
}
+
+ public void setDomainId(String domainId) {
+ this.domainId = domainId;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
}
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 db051313d96..cbaf6140597 100644
--- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java
+++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java
@@ -136,6 +136,8 @@ public interface BackupManager extends BackupService,
Configurable, PluggableSer
*/
BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd);
+ List<Long> getBackupOfferingDomains(final Long offeringId);
+
/**
* List backup offerings
* @param ListBackupOfferingsCmd API cmd
diff --git
a/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java
b/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java
new file mode 100644
index 00000000000..e679bbf2d1f
--- /dev/null
+++
b/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java
@@ -0,0 +1,149 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.api.command.offering;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.LongFunction;
+
+import com.cloud.dc.DataCenter;
+import com.cloud.domain.Domain;
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.utils.db.EntityManager;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.ServerApiException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DomainAndZoneIdResolverTest {
+ static class TestCmd extends BaseCmd implements DomainAndZoneIdResolver {
+ @Override
+ public void execute() throws ResourceUnavailableException,
InsufficientCapacityException, ServerApiException,
ConcurrentOperationException, ResourceAllocationException,
NetworkRuleConflictException {
+ // No implementation needed for tests
+ }
+
+ @Override
+ public String getCommandName() {
+ return "test";
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return 1L;
+ }
+ }
+
+ private void setEntityMgr(final BaseCmd cmd, final EntityManager
entityMgr) throws Exception {
+ Field f = BaseCmd.class.getDeclaredField("_entityMgr");
+ f.setAccessible(true);
+ f.set(cmd, entityMgr);
+ }
+
+ @Test
+ public void resolveDomainIds_usesDefaultProviderWhenEmpty() {
+ TestCmd cmd = new TestCmd();
+
+ final LongFunction<List<Long>> defaultsProvider = id ->
Arrays.asList(100L, 200L);
+
+ List<Long> result = cmd.resolveDomainIds("", 42L, defaultsProvider,
"offering");
+ Assert.assertEquals(Arrays.asList(100L, 200L), result);
+ }
+
+ @Test
+ public void resolveDomainIds_resolvesValidUuids() throws Exception {
+ TestCmd cmd = new TestCmd();
+
+ EntityManager em = mock(EntityManager.class);
+ setEntityMgr(cmd, em);
+
+ Domain d1 = mock(Domain.class);
+ when(d1.getId()).thenReturn(10L);
+ Domain d2 = mock(Domain.class);
+ when(d2.getId()).thenReturn(20L);
+
+ when(em.findByUuid(Domain.class, "uuid1")).thenReturn(d1);
+ when(em.findByUuid(Domain.class, "uuid2")).thenReturn(d2);
+
+ List<Long> ids = cmd.resolveDomainIds("uuid1, public, uuid2", null,
null, "template");
+ Assert.assertEquals(Arrays.asList(10L, 20L), ids);
+ }
+
+ @Test
+ public void resolveDomainIds_invalidUuid_throws() throws Exception {
+ TestCmd cmd = new TestCmd();
+
+ EntityManager em = mock(EntityManager.class);
+ setEntityMgr(cmd, em);
+
+ when(em.findByUuid(Domain.class, "bad-uuid")).thenReturn(null);
+
+ Assert.assertThrows(InvalidParameterValueException.class,
+ () -> cmd.resolveDomainIds("bad-uuid", null, null, "offering"));
+ }
+
+ @Test
+ public void resolveZoneIds_usesDefaultProviderWhenEmpty() {
+ TestCmd cmd = new TestCmd();
+
+ final LongFunction<List<Long>> defaultsProvider = id ->
Collections.singletonList(300L);
+
+ List<Long> result = cmd.resolveZoneIds("", 99L, defaultsProvider,
"offering");
+ Assert.assertEquals(Collections.singletonList(300L), result);
+ }
+
+ @Test
+ public void resolveZoneIds_resolvesValidUuids() throws Exception {
+ TestCmd cmd = new TestCmd();
+
+ EntityManager em = mock(EntityManager.class);
+ setEntityMgr(cmd, em);
+
+ DataCenter z1 = mock(DataCenter.class);
+ when(z1.getId()).thenReturn(30L);
+ DataCenter z2 = mock(DataCenter.class);
+ when(z2.getId()).thenReturn(40L);
+
+ when(em.findByUuid(DataCenter.class, "zone-1")).thenReturn(z1);
+ when(em.findByUuid(DataCenter.class, "zone-2")).thenReturn(z2);
+
+ List<Long> ids = cmd.resolveZoneIds("zone-1, all, zone-2", null, null,
"service");
+ Assert.assertEquals(Arrays.asList(30L, 40L), ids);
+ }
+
+ @Test
+ public void resolveZoneIds_invalidUuid_throws() throws Exception {
+ TestCmd cmd = new TestCmd();
+
+ EntityManager em = mock(EntityManager.class);
+ setEntityMgr(cmd, em);
+
+ when(em.findByUuid(DataCenter.class, "bad-zone")).thenReturn(null);
+
+ Assert.assertThrows(InvalidParameterValueException.class,
+ () -> cmd.resolveZoneIds("bad-zone", null, null, "offering"));
+ }
+}
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java
new file mode 100644
index 00000000000..6bdf7602a9d
--- /dev/null
+++
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java
@@ -0,0 +1,86 @@
+// 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 javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.apache.cloudstack.api.ResourceDetail;
+
+@Entity
+@Table(name = "backup_offering_details")
+public class BackupOfferingDetailsVO implements ResourceDetail {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "backup_offering_id")
+ private long resourceId;
+
+ @Column(name = "name")
+ private String name;
+
+ @Column(name = "value")
+ private String value;
+
+ @Column(name = "display")
+ private boolean display = true;
+
+ protected BackupOfferingDetailsVO() {
+ }
+
+ public BackupOfferingDetailsVO(long backupOfferingId, String name, String
value, boolean display) {
+ this.resourceId = backupOfferingId;
+ this.name = name;
+ this.value = value;
+ this.display = display;
+ }
+
+ @Override
+ public long getResourceId() {
+ return resourceId;
+ }
+
+ public void setResourceId(long backupOfferingId) {
+ this.resourceId = backupOfferingId;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public boolean isDisplay() {
+ return display;
+ }
+}
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java
index d30385af575..ebeb7d4a2d5 100644
---
a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java
+++
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java
@@ -17,6 +17,8 @@
package org.apache.cloudstack.backup;
+import
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+
import java.util.Date;
import java.util.UUID;
@@ -131,4 +133,9 @@ public class BackupOfferingVO implements BackupOffering {
public Date getCreated() {
return created;
}
+
+ @Override
+ public String toString() {
+ return String.format("Backup offering %s.",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "name",
"uuid"));
+ }
}
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java
index a41e4e70d33..708faeef464 100644
---
a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java
+++
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java
@@ -20,6 +20,8 @@ package org.apache.cloudstack.backup.dao;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
+import com.cloud.domain.DomainVO;
+import com.cloud.domain.dao.DomainDao;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.backup.BackupOfferingVO;
@@ -30,10 +32,16 @@ import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
+import java.util.List;
+
public class BackupOfferingDaoImpl extends GenericDaoBase<BackupOfferingVO,
Long> implements BackupOfferingDao {
@Inject
DataCenterDao dataCenterDao;
+ @Inject
+ BackupOfferingDetailsDao backupOfferingDetailsDao;
+ @Inject
+ DomainDao domainDao;
private SearchBuilder<BackupOfferingVO> backupPoliciesSearch;
@@ -51,8 +59,9 @@ public class BackupOfferingDaoImpl extends
GenericDaoBase<BackupOfferingVO, Long
@Override
public BackupOfferingResponse newBackupOfferingResponse(BackupOffering
offering, Boolean crossZoneInstanceCreation) {
- DataCenterVO zone = dataCenterDao.findById(offering.getZoneId());
+ DataCenterVO zone = dataCenterDao.findById(offering.getZoneId());
+ List<Long> domainIds =
backupOfferingDetailsDao.findDomainIds(offering.getId());
BackupOfferingResponse response = new BackupOfferingResponse();
response.setId(offering.getUuid());
response.setName(offering.getName());
@@ -64,6 +73,18 @@ public class BackupOfferingDaoImpl extends
GenericDaoBase<BackupOfferingVO, Long
response.setZoneId(zone.getUuid());
response.setZoneName(zone.getName());
}
+ if (domainIds != null && !domainIds.isEmpty()) {
+ String domainUUIDs =
domainIds.stream().map(Long::valueOf).map(domainId -> {
+ DomainVO domain = domainDao.findById(domainId);
+ return domain != null ? domain.getUuid() : "";
+ }).filter(name -> !name.isEmpty()).reduce((a, b) -> a + "," +
b).orElse("");
+ String domainNames =
domainIds.stream().map(Long::valueOf).map(domainId -> {
+ DomainVO domain = domainDao.findById(domainId);
+ return domain != null ? domain.getName() : "";
+ }).filter(name -> !name.isEmpty()).reduce((a, b) -> a + "," +
b).orElse("");
+ response.setDomain(domainNames);
+ response.setDomainId(domainUUIDs);
+ }
if (crossZoneInstanceCreation) {
response.setCrossZoneInstanceCreation(true);
}
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
new file mode 100644
index 00000000000..390fcba1e0e
--- /dev/null
+++
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
@@ -0,0 +1,32 @@
+// 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.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.backup.BackupOfferingDetailsVO;
+import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface BackupOfferingDetailsDao extends
GenericDao<BackupOfferingDetailsVO, Long>,
ResourceDetailsDao<BackupOfferingDetailsVO> {
+ List<Long> findDomainIds(final long resourceId);
+ List<Long> findZoneIds(final long resourceId);
+ String getDetail(Long backupOfferingId, String key);
+ List<Long> findOfferingIdsByDomainIds(List<Long> domainIds);
+ void updateBackupOfferingDomainIdsDetail(long backupOfferingId, List<Long>
filteredDomainIds);
+}
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java
new file mode 100644
index 00000000000..f052c93f981
--- /dev/null
+++
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java
@@ -0,0 +1,101 @@
+// 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.dao;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.cloud.utils.db.DB;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.backup.BackupOfferingDetailsVO;
+import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BackupOfferingDetailsDaoImpl extends
ResourceDetailsDaoBase<BackupOfferingDetailsVO> implements
BackupOfferingDetailsDao {
+
+ @Override
+ public void addDetail(long resourceId, String key, String value, boolean
display) {
+ super.addDetail(new BackupOfferingDetailsVO(resourceId, key, value,
display));
+ }
+
+ @Override
+ public List<Long> findDomainIds(long resourceId) {
+ final List<Long> domainIds = new ArrayList<>();
+ for (final BackupOfferingDetailsVO detail: findDetails(resourceId,
ApiConstants.DOMAIN_ID)) {
+ final Long domainId = Long.valueOf(detail.getValue());
+ if (domainId > 0) {
+ domainIds.add(domainId);
+ }
+ }
+ return domainIds;
+ }
+
+ @Override
+ public List<Long> findZoneIds(long resourceId) {
+ final List<Long> zoneIds = new ArrayList<>();
+ for (final BackupOfferingDetailsVO detail: findDetails(resourceId,
ApiConstants.ZONE_ID)) {
+ final Long zoneId = Long.valueOf(detail.getValue());
+ if (zoneId > 0) {
+ zoneIds.add(zoneId);
+ }
+ }
+ return zoneIds;
+ }
+
+ @Override
+ public String getDetail(Long backupOfferingId, String key) {
+ String detailValue = null;
+ BackupOfferingDetailsVO backupOfferingDetail =
findDetail(backupOfferingId, key);
+ if (backupOfferingDetail != null) {
+ detailValue = backupOfferingDetail.getValue();
+ }
+ return detailValue;
+ }
+
+ @Override
+ public List<Long> findOfferingIdsByDomainIds(List<Long> domainIds) {
+ Object[] dIds = domainIds.stream().map(s ->
String.valueOf(s)).collect(Collectors.toList()).toArray();
+ return findResourceIdsByNameAndValueIn("domainid", dIds);
+ }
+
+ @DB
+ @Override
+ public void updateBackupOfferingDomainIdsDetail(long backupOfferingId,
List<Long> filteredDomainIds) {
+ SearchBuilder<BackupOfferingDetailsVO> sb = createSearchBuilder();
+ List<BackupOfferingDetailsVO> detailsVO = new ArrayList<>();
+ sb.and("offeringId", sb.entity().getResourceId(),
SearchCriteria.Op.EQ);
+ sb.and("detailName", sb.entity().getName(), SearchCriteria.Op.EQ);
+ sb.done();
+ SearchCriteria<BackupOfferingDetailsVO> sc = sb.create();
+ sc.setParameters("offeringId", String.valueOf(backupOfferingId));
+ sc.setParameters("detailName", ApiConstants.DOMAIN_ID);
+ remove(sc);
+ for (Long domainId : filteredDomainIds) {
+ detailsVO.add(new BackupOfferingDetailsVO(backupOfferingId,
ApiConstants.DOMAIN_ID, String.valueOf(domainId), false));
+ }
+ if (!detailsVO.isEmpty()) {
+ for (BackupOfferingDetailsVO detailVO : detailsVO) {
+ persist(detailVO);
+ }
+ }
+ }
+}
diff --git
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml
index d308a9e5aaf..1846c3c62a0 100644
---
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml
+++
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml
@@ -71,6 +71,7 @@
<bean id="NetworkDaoImpl"
class="org.apache.cloudstack.quota.dao.NetworkDaoImpl" />
<bean id="VpcDaoImpl"
class="org.apache.cloudstack.quota.dao.VpcDaoImpl" />
<bean id="volumeDaoImpl" class="com.cloud.storage.dao.VolumeDaoImpl" />
- <bean id="reservationDao"
class="org.apache.cloudstack.reservation.dao.ReservationDaoImpl" />
+ <bean id="reservationDao"
class="org.apache.cloudstack.reservation.dao.ReservationDaoImpl" />
<bean id="backupOfferingDaoImpl"
class="org.apache.cloudstack.backup.dao.BackupOfferingDaoImpl" />
+ <bean id="backupOfferingDetailsDaoImpl"
class="org.apache.cloudstack.backup.dao.BackupOfferingDetailsDaoImpl" />
</beans>
diff --git
a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index 07f394b19c9..d330ecd0c0d 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
@@ -19,6 +19,16 @@
-- Schema upgrade from 4.22.1.0 to 4.23.0.0
--;
+CREATE TABLE `cloud`.`backup_offering_details` (
+ `id` bigint unsigned NOT NULL auto_increment,
+ `backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id',
+ `name` varchar(255) NOT NULL,
+ `value` varchar(1024) NOT NULL,
+ `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Should detail be
displayed to the end user',
+ PRIMARY KEY (`id`),
+ CONSTRAINT `fk_offering_details__backup_offering_id` FOREIGN KEY
`fk_offering_details__backup_offering_id`(`backup_offering_id`) REFERENCES
`backup_offering`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
-- Update value to random for the config 'vm.allocation.algorithm' or
'volume.allocation.algorithm' if configured as userconcentratedpod_random
-- Update value to firstfit for the config 'vm.allocation.algorithm' or
'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit
UPDATE `cloud`.`configuration` SET value='random' WHERE name IN
('vm.allocation.algorithm', 'volume.allocation.algorithm') AND
value='userconcentratedpod_random';
diff --git
a/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
b/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
new file mode 100644
index 00000000000..fc8f2d0fcf7
--- /dev/null
+++
b/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
@@ -0,0 +1,251 @@
+// 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.dao;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.backup.BackupOfferingDetailsVO;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.utils.db.SearchCriteria;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BackupOfferingDetailsDaoImplTest {
+
+ @Spy
+ @InjectMocks
+ private BackupOfferingDetailsDaoImpl backupOfferingDetailsDao;
+
+ private static final long RESOURCE_ID = 1L;
+ private static final long OFFERING_ID = 100L;
+ private static final String TEST_KEY = "testKey";
+ private static final String TEST_VALUE = "testValue";
+
+ @Test
+ public void testAddDetail() {
+ BackupOfferingDetailsVO detailVO = new
BackupOfferingDetailsVO(RESOURCE_ID, TEST_KEY, TEST_VALUE, true);
+
+ Assert.assertEquals("Resource ID should match", RESOURCE_ID,
detailVO.getResourceId());
+ Assert.assertEquals("Detail name/key should match", TEST_KEY,
detailVO.getName());
+ Assert.assertEquals("Detail value should match", TEST_VALUE,
detailVO.getValue());
+ Assert.assertTrue("Display flag should be true", detailVO.isDisplay());
+
+ BackupOfferingDetailsVO detailVOHidden = new
BackupOfferingDetailsVO(RESOURCE_ID, "hiddenKey", "hiddenValue", false);
+ Assert.assertFalse("Display flag should be false",
detailVOHidden.isDisplay());
+ }
+
+ @Test
+ public void testFindDomainIdsWithMultipleDomains() {
+ List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "1",
false),
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "2",
false),
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "3", false)
+ );
+
+ Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+ List<Long> domainIds =
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+ Assert.assertNotNull(domainIds);
+ Assert.assertEquals(3, domainIds.size());
+ Assert.assertEquals(Arrays.asList(1L, 2L, 3L), domainIds);
+ }
+
+ @Test
+ public void testFindDomainIdsWithEmptyList() {
+
Mockito.doReturn(Collections.emptyList()).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+ List<Long> domainIds =
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+ Assert.assertNotNull(domainIds);
+ Assert.assertTrue(domainIds.isEmpty());
+ }
+
+ @Test
+ public void testFindDomainIdsExcludesZeroOrNegativeValues() {
+ List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "1",
false),
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "0",
false),
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "-1",
false),
+ createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "2", false)
+ );
+
+ Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+ List<Long> domainIds =
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+ Assert.assertNotNull(domainIds);
+ Assert.assertEquals(2, domainIds.size());
+ Assert.assertEquals(Arrays.asList(1L, 2L), domainIds);
+ }
+
+ @Test
+ public void testFindZoneIdsWithMultipleZones() {
+ List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "10", false),
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "20", false),
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "30", false)
+ );
+
+ Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+ List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+ Assert.assertNotNull(zoneIds);
+ Assert.assertEquals(3, zoneIds.size());
+ Assert.assertEquals(Arrays.asList(10L, 20L, 30L), zoneIds);
+ }
+
+ @Test
+ public void testFindZoneIdsWithEmptyList() {
+
Mockito.doReturn(Collections.emptyList()).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+ List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+ Assert.assertNotNull(zoneIds);
+ Assert.assertTrue(zoneIds.isEmpty());
+ }
+
+ @Test
+ public void testFindZoneIdsExcludesZeroOrNegativeValues() {
+ List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "10", false),
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "0", false),
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "-5", false),
+ createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "20", false)
+ );
+
+ Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+ .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+ List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+ Assert.assertNotNull(zoneIds);
+ Assert.assertEquals(2, zoneIds.size());
+ Assert.assertEquals(Arrays.asList(10L, 20L), zoneIds);
+ }
+
+ @Test
+ public void testGetDetailWhenDetailExists() {
+ BackupOfferingDetailsVO mockDetail = createDetailVO(OFFERING_ID,
TEST_KEY, TEST_VALUE, true);
+
+ Mockito.doReturn(mockDetail).when(backupOfferingDetailsDao)
+ .findDetail(OFFERING_ID, TEST_KEY);
+
+ String detailValue = backupOfferingDetailsDao.getDetail(OFFERING_ID,
TEST_KEY);
+
+ Assert.assertNotNull(detailValue);
+ Assert.assertEquals(TEST_VALUE, detailValue);
+ }
+
+ @Test
+ public void testGetDetailWhenDetailDoesNotExist() {
+ Mockito.doReturn(null).when(backupOfferingDetailsDao)
+ .findDetail(OFFERING_ID, TEST_KEY);
+
+ String detailValue = backupOfferingDetailsDao.getDetail(OFFERING_ID,
TEST_KEY);
+
+ Assert.assertNull(detailValue);
+ }
+
+ @Test
+ public void testFindOfferingIdsByDomainIds() {
+ List<Long> domainIds = Arrays.asList(1L, 2L, 3L);
+ List<Long> expectedOfferingIds = Arrays.asList(100L, 101L, 102L);
+
+ Mockito.doReturn(expectedOfferingIds).when(backupOfferingDetailsDao)
+ .findResourceIdsByNameAndValueIn(Mockito.eq("domainid"),
Mockito.any(Object[].class));
+
+ List<Long> offeringIds =
backupOfferingDetailsDao.findOfferingIdsByDomainIds(domainIds);
+
+ Assert.assertNotNull(offeringIds);
+ Assert.assertEquals(expectedOfferingIds, offeringIds);
+
Mockito.verify(backupOfferingDetailsDao).findResourceIdsByNameAndValueIn(
+ Mockito.eq("domainid"), Mockito.any(Object[].class));
+ }
+
+ @Test
+ public void testFindOfferingIdsByDomainIdsWithEmptyList() {
+ List<Long> domainIds = Collections.emptyList();
+ List<Long> expectedOfferingIds = Collections.emptyList();
+
+ Mockito.doReturn(expectedOfferingIds).when(backupOfferingDetailsDao)
+ .findResourceIdsByNameAndValueIn(Mockito.eq("domainid"),
Mockito.any(Object[].class));
+
+ List<Long> offeringIds =
backupOfferingDetailsDao.findOfferingIdsByDomainIds(domainIds);
+
+ Assert.assertNotNull(offeringIds);
+ Assert.assertTrue(offeringIds.isEmpty());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testUpdateBackupOfferingDomainIdsDetail() {
+ List<Long> newDomainIds = Arrays.asList(1L, 2L, 3L);
+
+
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+
Mockito.doReturn(null).when(backupOfferingDetailsDao).persist(Mockito.any(BackupOfferingDetailsVO.class));
+
+
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID,
newDomainIds);
+
+ Mockito.verify(backupOfferingDetailsDao,
Mockito.times(3)).persist(Mockito.any(BackupOfferingDetailsVO.class));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testUpdateBackupOfferingDomainIdsDetailWithEmptyList() {
+ List<Long> emptyDomainIds = Collections.emptyList();
+
+
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+
+
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID,
emptyDomainIds);
+
+ Mockito.verify(backupOfferingDetailsDao,
Mockito.never()).persist(Mockito.any(BackupOfferingDetailsVO.class));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testUpdateBackupOfferingDomainIdsDetailWithSingleDomain() {
+ List<Long> singleDomainId = Collections.singletonList(5L);
+
+
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+
Mockito.doReturn(null).when(backupOfferingDetailsDao).persist(Mockito.any(BackupOfferingDetailsVO.class));
+
+
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID,
singleDomainId);
+
+ Mockito.verify(backupOfferingDetailsDao,
Mockito.times(1)).persist(Mockito.any(BackupOfferingDetailsVO.class));
+ }
+
+ private BackupOfferingDetailsVO createDetailVO(long resourceId, String
name, String value, boolean display) {
+ return new BackupOfferingDetailsVO(resourceId, name, value, display);
+ }
+}
diff --git a/plugins/hypervisors/ovm3/sonar-project.properties
b/plugins/hypervisors/ovm3/sonar-project.properties
index 7355f1df4f7..7cc3a41f2a5 100644
--- a/plugins/hypervisors/ovm3/sonar-project.properties
+++ b/plugins/hypervisors/ovm3/sonar-project.properties
@@ -24,7 +24,7 @@ sonar.projectVersion=1.0
sonar.sources=src
sonar.binaries=target/classes
-# Exclussions
+# Exclusions
sonar.exclusions=**/*Test.java
# Language
diff --git
a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
index 2ff68b4836f..54e76463bca 100644
---
a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
+++
b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
@@ -30,6 +30,7 @@ import
org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
import
org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
+import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.acl.ControlledEntity;
@@ -491,6 +492,11 @@ public class MockAccountManager extends ManagerBase
implements AccountManager {
// TODO Auto-generated method stub
}
+ @Override
+ public void checkAccess(Account account, BackupOffering bof) throws
PermissionDeniedException {
+ // TODO Auto-generated method stub
+ }
+
@Override
public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd){
return null;
diff --git a/pom.xml b/pom.xml
index 33b232045ca..fa57b98ce7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,8 @@
<project.systemvm.template.version>4.22.0.0</project.systemvm.template.version>
<sonar.organization>apache</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
+
<sonar.exclusions>engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java</sonar.exclusions>
+
<sonar.exclusions>api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java</sonar.exclusions>
<!-- Build properties -->
<cs.jdk.version>11</cs.jdk.version>
diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java
b/server/src/main/java/com/cloud/acl/DomainChecker.java
index 24b6346d0af..0500960abb1 100644
--- a/server/src/main/java/com/cloud/acl/DomainChecker.java
+++ b/server/src/main/java/com/cloud/acl/DomainChecker.java
@@ -32,6 +32,8 @@ import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.springframework.stereotype.Component;
+import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao;
+import org.apache.cloudstack.backup.BackupOffering;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
@@ -70,6 +72,8 @@ public class DomainChecker extends AdapterBase implements
SecurityChecker {
@Inject
DomainDao _domainDao;
@Inject
+ BackupOfferingDetailsDao backupOfferingDetailsDao;
+ @Inject
AccountDao _accountDao;
@Inject
LaunchPermissionDao _launchPermissionDao;
@@ -474,6 +478,35 @@ public class DomainChecker extends AdapterBase implements
SecurityChecker {
return hasAccess;
}
+ @Override
+ public boolean checkAccess(Account account, BackupOffering backupOffering)
throws PermissionDeniedException {
+ boolean hasAccess = false;
+ if (account == null || backupOffering == null) {
+ hasAccess = true;
+ } else {
+ if (_accountService.isRootAdmin(account.getId())) {
+ hasAccess = true;
+ }
+ else if (_accountService.isNormalUser(account.getId())
+ || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN
+ || _accountService.isDomainAdmin(account.getId())
+ || account.getType() == Account.Type.PROJECT) {
+ final List<Long> boDomainIds =
backupOfferingDetailsDao.findDomainIds(backupOffering.getId());
+ if (boDomainIds.isEmpty()) {
+ hasAccess = true;
+ } else {
+ for (Long domainId : boDomainIds) {
+ if (_domainDao.isChildDomain(domainId,
account.getDomainId())) {
+ hasAccess = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return hasAccess;
+ }
+
@Override
public boolean checkAccess(Account account, DataCenter zone) throws
PermissionDeniedException {
if (account == null || zone.getDomainId() == null) {//public zone
diff --git
a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
index e8dd45ce719..5331cb10ed6 100644
--- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
+++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
@@ -49,6 +49,11 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
+import com.cloud.consoleproxy.ConsoleProxyManager;
+import com.cloud.network.router.VirtualNetworkApplianceManager;
+import com.cloud.storage.secondary.SecondaryStorageVmManager;
+import com.cloud.utils.DomainHelper;
+import com.cloud.vm.VirtualMachineManager;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.affinity.AffinityGroup;
@@ -150,7 +155,6 @@ import com.cloud.api.query.vo.NetworkOfferingJoinVO;
import com.cloud.capacity.CapacityManager;
import com.cloud.capacity.dao.CapacityDao;
import com.cloud.configuration.Resource.ResourceType;
-import com.cloud.consoleproxy.ConsoleProxyManager;
import com.cloud.dc.AccountVlanMapVO;
import com.cloud.dc.ClusterDetailsDao;
import com.cloud.dc.ClusterDetailsVO;
@@ -245,7 +249,6 @@ import com.cloud.network.dao.UserIpv6AddressDao;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.element.NsxProviderVO;
import com.cloud.network.netris.NetrisService;
-import com.cloud.network.router.VirtualNetworkApplianceManager;
import com.cloud.network.rules.LoadBalancerContainer.Scheme;
import com.cloud.network.vpc.VpcManager;
import com.cloud.offering.DiskOffering;
@@ -280,7 +283,6 @@ import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.StoragePoolTagsDao;
import com.cloud.storage.dao.VMTemplateZoneDao;
import com.cloud.storage.dao.VolumeDao;
-import com.cloud.storage.secondary.SecondaryStorageVmManager;
import com.cloud.test.IPRangeConfig;
import com.cloud.user.Account;
import com.cloud.user.AccountDetailVO;
@@ -314,7 +316,6 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.NicIpAlias;
import com.cloud.vm.VirtualMachine;
-import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.NicIpAliasDao;
import com.cloud.vm.dao.NicIpAliasVO;
@@ -399,6 +400,8 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
ClusterDao _clusterDao;
@Inject
AlertManager _alertMgr;
+ @Inject
+ DomainHelper domainHelper;
List<SecurityChecker> _secChecker;
List<ExternalProvisioner> externalProvisioners;
@@ -3519,7 +3522,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
final boolean
isCustomized, final boolean encryptRoot, Long vgpuProfileId, Integer gpuCount,
Boolean gpuDisplay, final boolean purgeResources, Integer leaseDuration,
VMLeaseManager.ExpiryAction leaseExpiryAction) {
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
// Check if user exists in the system
final User user = _userDao.findById(userId);
@@ -3908,7 +3911,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
final Account account = _accountDao.findById(user.getAccountId());
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
Collections.sort(filteredDomainIds);
// avoid domain update of service offering if any instance is
associated to it
@@ -4118,7 +4121,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
}
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
// Check if user exists in the system
final User user = _userDao.findById(userId);
@@ -4394,7 +4397,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
final Account account = _accountDao.findById(user.getAccountId());
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
Collections.sort(filteredDomainIds);
List<Long> filteredZoneIds = new ArrayList<>();
@@ -7401,7 +7404,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
}
if (offering != null) {
// Filter child domains when both parent and child
domains are present
- List<Long> filteredDomainIds =
filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
List<NetworkOfferingDetailsVO> detailsVO = new
ArrayList<>();
for (Long domainId : filteredDomainIds) {
detailsVO.add(new
NetworkOfferingDetailsVO(offering.getId(), Detail.domainid,
String.valueOf(domainId), false));
@@ -7867,7 +7870,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
}
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
Collections.sort(filteredDomainIds);
List<Long> filteredZoneIds = new ArrayList<>();
@@ -8434,30 +8437,6 @@ public class ConfigurationManagerImpl extends
ManagerBase implements Configurati
return false;
}
- private List<Long> filterChildSubDomains(final List<Long> domainIds) {
- List<Long> filteredDomainIds = new ArrayList<>();
- if (domainIds != null) {
- filteredDomainIds.addAll(domainIds);
- }
- if (filteredDomainIds.size() > 1) {
- for (int i = filteredDomainIds.size() - 1; i >= 1; i--) {
- long first = filteredDomainIds.get(i);
- for (int j = i - 1; j >= 0; j--) {
- long second = filteredDomainIds.get(j);
- if (_domainDao.isChildDomain(filteredDomainIds.get(i),
filteredDomainIds.get(j))) {
- filteredDomainIds.remove(j);
- i--;
- }
- if (_domainDao.isChildDomain(filteredDomainIds.get(j),
filteredDomainIds.get(i))) {
- filteredDomainIds.remove(i);
- break;
- }
- }
- }
- }
- return filteredDomainIds;
- }
-
protected void validateCacheMode(String cacheMode){
if(cacheMode != null &&
!Enums.getIfPresent(DiskOffering.DiskCacheMode.class,
diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
index e4219c858da..60b93d409aa 100644
--- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
@@ -63,6 +63,7 @@ import com.cloud.network.element.NetworkACLServiceProvider;
import com.cloud.network.element.NsxProviderVO;
import com.cloud.network.rules.RulesManager;
import com.cloud.network.vpn.RemoteAccessVpnService;
+import com.cloud.utils.DomainHelper;
import com.cloud.vm.dao.VMInstanceDao;
import com.google.common.collect.Sets;
import org.apache.cloudstack.acl.ControlledEntity.ACLType;
@@ -285,6 +286,8 @@ public class VpcManagerImpl extends ManagerBase implements
VpcManager, VpcProvis
@Inject
DomainDao domainDao;
@Inject
+ DomainHelper domainHelper;
+ @Inject
private AnnotationDao annotationDao;
@Inject
NetworkOfferingDao _networkOfferingDao;
@@ -636,7 +639,7 @@ public class VpcManagerImpl extends ManagerBase implements
VpcManager, VpcProvis
}
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
final Map<Network.Service, Set<Network.Provider>> svcProviderMap = new
HashMap<Network.Service, Set<Network.Provider>>();
final Set<Network.Provider> defaultProviders = new
HashSet<Network.Provider>();
@@ -1118,7 +1121,7 @@ public class VpcManagerImpl extends ManagerBase
implements VpcManager, VpcProvis
// Filter child domains when both parent and child domains are present
- List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
Collections.sort(filteredDomainIds);
List<Long> filteredZoneIds = new ArrayList<>();
@@ -3658,30 +3661,6 @@ public class VpcManagerImpl extends ManagerBase
implements VpcManager, VpcProvis
return _ntwkMgr.areRoutersRunning(routerDao.listByVpcId(vpc.getId()));
}
- private List<Long> filterChildSubDomains(final List<Long> domainIds) {
- List<Long> filteredDomainIds = new ArrayList<>();
- if (domainIds != null) {
- filteredDomainIds.addAll(domainIds);
- }
- if (filteredDomainIds.size() > 1) {
- for (int i = filteredDomainIds.size() - 1; i >= 1; i--) {
- long first = filteredDomainIds.get(i);
- for (int j = i - 1; j >= 0; j--) {
- long second = filteredDomainIds.get(j);
- if (domainDao.isChildDomain(filteredDomainIds.get(i),
filteredDomainIds.get(j))) {
- filteredDomainIds.remove(j);
- i--;
- }
- if (domainDao.isChildDomain(filteredDomainIds.get(j),
filteredDomainIds.get(i))) {
- filteredDomainIds.remove(i);
- break;
- }
- }
- }
- }
- return filteredDomainIds;
- }
-
protected boolean isGlobalAcl(Long aclVpcId) {
return aclVpcId != null && aclVpcId == 0;
}
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index dd60fbfb9cc..6f68a104867 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -67,6 +67,7 @@ import
org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupRespon
import org.apache.cloudstack.auth.UserAuthenticator;
import
org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
+import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@@ -3574,6 +3575,21 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
throw new PermissionDeniedException("There's no way to confirm " +
account + " has access to " + vof);
}
+ @Override
+ public void checkAccess(Account account, BackupOffering bof) throws
PermissionDeniedException {
+ for (SecurityChecker checker : _securityCheckers) {
+ if (checker.checkAccess(account, bof)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Access granted to " + account + " to " + bof
+ " by " + checker.getName());
+ }
+ return;
+ }
+ }
+
+ assert false : "How can all of the security checkers pass on checking
this caller?";
+ throw new PermissionDeniedException("There's no way to confirm " +
account + " has access to " + bof);
+ }
+
@Override
public void checkAccess(User user, ControlledEntity entity) throws
PermissionDeniedException {
for (SecurityChecker checker : _securityCheckers) {
diff --git a/server/src/main/java/com/cloud/utils/DomainHelper.java
b/server/src/main/java/com/cloud/utils/DomainHelper.java
new file mode 100644
index 00000000000..480726d256b
--- /dev/null
+++ b/server/src/main/java/com/cloud/utils/DomainHelper.java
@@ -0,0 +1,63 @@
+// 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 com.cloud.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import javax.inject.Inject;
+
+import org.springframework.stereotype.Component;
+
+import com.cloud.domain.dao.DomainDao;
+
+@Component
+public class DomainHelper {
+
+ @Inject
+ private DomainDao domainDao;
+
+ /**
+ *
+ * @param domainIds List of domain IDs to filter
+ * @return Filtered list containing only domains that are not descendants
of other domains in the list
+ */
+ public List<Long> filterChildSubDomains(final List<Long> domainIds) {
+ if (domainIds == null || domainIds.size() <= 1) {
+ return domainIds == null ? new ArrayList<>() : new
ArrayList<>(domainIds);
+ }
+
+ final List<Long> result = new ArrayList<>();
+ for (final Long candidate : domainIds) {
+ boolean isDescendant = false;
+ for (final Long other : domainIds) {
+ if (Objects.equals(candidate, other)) {
+ continue;
+ }
+ if (domainDao.isChildDomain(other, candidate)) {
+ isDescendant = true;
+ break;
+ }
+ }
+ if (!isDescendant) {
+ result.add(candidate);
+ }
+ }
+ return result;
+ }
+}
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 ef3ba917de7..5743c729923 100644
--- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
@@ -38,6 +38,7 @@ import java.util.stream.Stream;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
+import com.cloud.utils.DomainHelper;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.InternalIdentity;
@@ -68,6 +69,7 @@ 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.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
@@ -81,12 +83,12 @@ import org.apache.cloudstack.poll.BackgroundPollTask;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
-import com.amazonaws.util.CollectionUtils;
import com.cloud.alert.AlertManager;
import com.cloud.api.ApiDispatcher;
import com.cloud.api.ApiGsonHelper;
@@ -184,6 +186,8 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
@Inject
private BackupOfferingDao backupOfferingDao;
@Inject
+ private BackupOfferingDetailsDao backupOfferingDetailsDao;
+ @Inject
private VMInstanceDao vmInstanceDao;
@Inject
private AccountService accountService;
@@ -237,6 +241,8 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
private AlertManager alertManager;
@Inject
private GuestOSDao _guestOSDao;
+ @Inject
+ private DomainHelper domainHelper;
private AsyncJobDispatcher asyncJobDispatcher;
private Timer backupTimer;
@@ -280,6 +286,20 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
throw new CloudRuntimeException("A backup offering with the same
name already exists in this zone");
}
+ if (CollectionUtils.isNotEmpty(cmd.getDomainIds())) {
+ for (final Long domainId: cmd.getDomainIds()) {
+ if (domainDao.findById(domainId) == null) {
+ throw new InvalidParameterValueException("Please specify a
valid domain id");
+ }
+ }
+ }
+
+ final Account caller = CallContext.current().getCallingAccount();
+ List<Long> filteredDomainIds = cmd.getDomainIds() == null ? new
ArrayList<>() : new ArrayList<>(cmd.getDomainIds());
+ if (filteredDomainIds.size() > 1) {
+ filteredDomainIds =
domainHelper.filterChildSubDomains(filteredDomainIds);
+ }
+
final BackupProvider provider = getBackupProvider(cmd.getZoneId());
if (!provider.isValidProviderOffering(cmd.getZoneId(),
cmd.getExternalId())) {
throw new CloudRuntimeException("Backup offering '" +
cmd.getExternalId() + "' does not exist on provider " + provider.getName() + "
on zone " + cmd.getZoneId());
@@ -292,15 +312,34 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
if (savedOffering == null) {
throw new CloudRuntimeException("Unable to create backup offering:
" + cmd.getExternalId() + ", name: " + cmd.getName());
}
+ if (CollectionUtils.isNotEmpty(filteredDomainIds)) {
+ List<BackupOfferingDetailsVO> detailsVOList = new ArrayList<>();
+ for (Long domainId : filteredDomainIds) {
+ detailsVOList.add(new
BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID,
String.valueOf(domainId), false));
+ }
+ if (!detailsVOList.isEmpty()) {
+ backupOfferingDetailsDao.saveDetails(detailsVOList);
+ }
+ }
logger.debug("Successfully created backup offering " + cmd.getName() +
" mapped to backup provider offering " + cmd.getExternalId());
return savedOffering;
}
+ @Override
+ public List<Long> getBackupOfferingDomains(Long offeringId) {
+ final BackupOffering backupOffering =
backupOfferingDao.findById(offeringId);
+ if (backupOffering == null) {
+ throw new InvalidParameterValueException("Unable to find backup
offering for id: " + offeringId);
+ }
+ return backupOfferingDetailsDao.findDomainIds(offeringId);
+ }
+
@Override
public Pair<List<BackupOffering>, Integer> listBackupOfferings(final
ListBackupOfferingsCmd cmd) {
final Long offeringId = cmd.getOfferingId();
final Long zoneId = cmd.getZoneId();
final String keyword = cmd.getKeyword();
+ Long domainId = cmd.getDomainId();
if (offeringId != null) {
BackupOfferingVO offering = backupOfferingDao.findById(offeringId);
@@ -314,8 +353,13 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
SearchBuilder<BackupOfferingVO> sb =
backupOfferingDao.createSearchBuilder();
sb.and("zone_id", sb.entity().getZoneId(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
+
CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
+ if (Account.Type.ADMIN != caller.getType() && domainId == null) {
+ domainId = caller.getDomainId();
+ }
+
if (Account.Type.NORMAL == caller.getType()) {
sb.and("user_backups_allowed",
sb.entity().isUserDrivenBackupAllowed(), SearchCriteria.Op.EQ);
}
@@ -328,13 +372,36 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
if (keyword != null) {
sc.setParameters("name", "%" + keyword + "%");
}
+
if (Account.Type.NORMAL == caller.getType()) {
sc.setParameters("user_backups_allowed", true);
}
+
Pair<List<BackupOfferingVO>, Integer> result =
backupOfferingDao.searchAndCount(sc, searchFilter);
+
+ if (domainId != null) {
+ List<BackupOfferingVO> filteredOfferings = new ArrayList<>();
+ for (BackupOfferingVO offering : result.first()) {
+ List<Long> offeringDomains =
backupOfferingDetailsDao.findDomainIds(offering.getId());
+ if (offeringDomains.isEmpty() ||
offeringDomains.contains(domainId) || containsParentDomain(offeringDomains,
domainId)) {
+ filteredOfferings.add(offering);
+ }
+ }
+ return new Pair<>(new ArrayList<>(filteredOfferings),
filteredOfferings.size());
+ }
+
return new Pair<>(new ArrayList<>(result.first()), result.second());
}
+ private boolean containsParentDomain(List<Long> offeringDomains, Long
domainId) {
+ for (Long offeringDomainId : offeringDomains) {
+ if (domainDao.isChildDomain(offeringDomainId, domainId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public boolean deleteBackupOffering(final Long offeringId) {
final BackupOfferingVO offering =
backupOfferingDao.findById(offeringId);
@@ -342,6 +409,8 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
throw new CloudRuntimeException("Could not find a backup offering
with id: " + offeringId);
}
+ accountManager.checkAccess(CallContext.current().getCallingAccount(),
offering);
+
if (backupDao.listByOfferingId(offering.getId()).size() > 0) {
throw new CloudRuntimeException("Backup Offering cannot be removed
as it has backups associated with it.");
}
@@ -452,6 +521,12 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
throw new CloudRuntimeException("Provided backup offering does not
exist");
}
+ Account owner = accountManager.getAccount(vm.getAccountId());
+ if (owner == null) {
+ throw new CloudRuntimeException("Unable to find the owner of the
VM");
+ }
+ accountManager.checkAccess(owner, offering);
+
final BackupProvider backupProvider =
getBackupProvider(offering.getProvider());
if (backupProvider == null) {
throw new CloudRuntimeException("Failed to get the backup provider
for the zone, please contact the administrator");
@@ -762,10 +837,11 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
@ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CREATE,
eventDescription = "creating VM backup", async = true)
public boolean createBackup(CreateBackupCmd cmd, Object job) throws
ResourceAllocationException {
Long vmId = cmd.getVmId();
+ Account caller = CallContext.current().getCallingAccount();
final VMInstanceVO vm = findVmById(vmId);
validateBackupForZone(vm.getDataCenterId());
- accountManager.checkAccess(CallContext.current().getCallingAccount(),
null, true, vm);
+ accountManager.checkAccess(caller, null, true, vm);
if (vm.getBackupOfferingId() == null) {
throw new CloudRuntimeException("VM has not backup offering
configured, cannot create backup before assigning it to a backup offering");
@@ -1065,7 +1141,7 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
}
// This is done to handle historic backups if any with Veeam /
Networker plugins
- List<Backup.VolumeInfo> backupVolumes =
CollectionUtils.isNullOrEmpty(backup.getBackedUpVolumes()) ?
+ List<Backup.VolumeInfo> backupVolumes =
CollectionUtils.isEmpty(backup.getBackedUpVolumes()) ?
vm.getBackupVolumeList() : backup.getBackedUpVolumes();
List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());
if (vmVolumes.size() != backupVolumes.size()) {
@@ -2112,11 +2188,15 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
String name = updateBackupOfferingCmd.getName();
String description = updateBackupOfferingCmd.getDescription();
Boolean allowUserDrivenBackups =
updateBackupOfferingCmd.getAllowUserDrivenBackups();
+ List<Long> domainIds = updateBackupOfferingCmd.getDomainIds();
BackupOfferingVO backupOfferingVO = backupOfferingDao.findById(id);
if (backupOfferingVO == null) {
throw new InvalidParameterValueException(String.format("Unable to
find Backup Offering with id: [%s].", id));
}
+
+ accountManager.checkAccess(CallContext.current().getCallingAccount(),
backupOfferingVO);
+
logger.debug("Trying to update Backup Offering {} to {}.",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backupOfferingVO,
"uuid", "name", "description", "userDrivenBackupAllowed"),
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(updateBackupOfferingCmd,
"name", "description", "allowUserDrivenBackups"));
@@ -2139,16 +2219,43 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
fields.add("allowUserDrivenBackups: " + allowUserDrivenBackups);
}
- if (!backupOfferingDao.update(id, offering)) {
+ if (CollectionUtils.isNotEmpty(domainIds)) {
+ for (final Long domainId: domainIds) {
+ if (domainDao.findById(domainId) == null) {
+ throw new InvalidParameterValueException("Please specify a
valid domain id");
+ }
+ }
+ }
+ List<Long> filteredDomainIds =
domainHelper.filterChildSubDomains(domainIds);
+ Collections.sort(filteredDomainIds);
+
+ boolean success = backupOfferingDao.update(id, offering);
+ if (!success) {
logger.warn(String.format("Couldn't update Backup offering (%s)
with [%s].", backupOfferingVO, String.join(", ", fields)));
}
+ if (success || fields.isEmpty()) {
+ List<Long> existingDomainIds =
backupOfferingDetailsDao.findDomainIds(id);
+ Collections.sort(existingDomainIds);
+ updateBackupOfferingDomainDetails(id, filteredDomainIds,
existingDomainIds);
+ }
+
BackupOfferingVO response = backupOfferingDao.findById(id);
CallContext.current().setEventDetails(String.format("Backup Offering
updated [%s].",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(response, "id",
"name", "description", "userDrivenBackupAllowed", "externalId")));
return response;
}
+ private void updateBackupOfferingDomainDetails(Long id, List<Long>
filteredDomainIds, List<Long> existingDomainIds) {
+ if (existingDomainIds == null) {
+ existingDomainIds = new ArrayList<>();
+ }
+
+ if(!filteredDomainIds.equals(existingDomainIds)) {
+ backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(id,
filteredDomainIds);
+ }
+ }
+
Map<String, String> getDetailsFromBackupDetails(Long backupId) {
Map<String, String> details =
backupDetailsDao.listDetailsKeyPairs(backupId, true);
if (details == null) {
@@ -2270,7 +2377,7 @@ public class BackupManagerImpl extends ManagerBase
implements BackupManager {
return;
}
List<Backup> backupsForVm =
backupDao.listByVmIdAndOffering(vm.getDataCenterId(), vm.getId(),
vm.getBackupOfferingId());
- if
(org.apache.commons.collections.CollectionUtils.isEmpty(backupsForVm)) {
+ if (CollectionUtils.isEmpty(backupsForVm)) {
removeVMFromBackupOffering(vm.getId(), true);
} else {
throw new CloudRuntimeException(String.format("This Instance
[uuid: %s, name: %s] has a "
diff --git
a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
index c633a3b0abd..f4fd57d59fc 100644
---
a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
+++
b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
@@ -81,4 +81,6 @@
<bean id="DPDKHelper" class="com.cloud.hypervisor.kvm.dpdk.DpdkHelperImpl"
/>
+ <bean id="domainHelper" class="com.cloud.utils.DomainHelper" />
+
</beans>
diff --git a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
index a5ec41306d8..8c7817c2b84 100644
--- a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
+++ b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
@@ -18,6 +18,9 @@ package com.cloud.acl;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
+import org.apache.cloudstack.backup.BackupOfferingVO;
+import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao;
+import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -35,6 +38,8 @@ import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.Ternary;
+import java.util.Collections;
+
@RunWith(MockitoJUnitRunner.class)
public class DomainCheckerTest {
@@ -46,6 +51,8 @@ public class DomainCheckerTest {
DomainDao _domainDao;
@Mock
ProjectManager _projectMgr;
+ @Mock
+ BackupOfferingDetailsDao backupOfferingDetailsDao;
@Spy
@InjectMocks
@@ -163,4 +170,42 @@ public class DomainCheckerTest {
domainChecker.validateCallerHasAccessToEntityOwner(caller, entity,
SecurityChecker.AccessType.ListEntry);
}
+ @Test
+ public void testBackupOfferingAccessRootAdmin() {
+ Account rootAdmin = Mockito.mock(Account.class);
+ Mockito.when(rootAdmin.getId()).thenReturn(1L);
+ BackupOfferingVO backupOfferingVO =
Mockito.mock(BackupOfferingVO.class);
+
Mockito.when(_accountService.isRootAdmin(rootAdmin.getId())).thenReturn(true);
+
+ boolean hasAccess = domainChecker.checkAccess(rootAdmin,
backupOfferingVO);
+ Assert.assertTrue(hasAccess);
+ }
+
+ @Test
+ public void testBackupOfferingAccessDomainAdmin() {
+ Account domainAdmin = Mockito.mock(Account.class);
+ Mockito.when(domainAdmin.getId()).thenReturn(2L);
+ BackupOfferingVO backupOfferingVO =
Mockito.mock(BackupOfferingVO.class);
+ AccountVO owner = Mockito.mock(AccountVO.class);
+
Mockito.when(_accountService.isDomainAdmin(domainAdmin.getId())).thenReturn(true);
+ Mockito.when(domainAdmin.getDomainId()).thenReturn(10L);
+ Mockito.when(_domainDao.isChildDomain(100L, 10L)).thenReturn(true);
+
Mockito.when(backupOfferingDetailsDao.findDomainIds(backupOfferingVO.getId())).thenReturn(Collections.singletonList(100L));
+
+ boolean hasAccess = domainChecker.checkAccess(domainAdmin,
backupOfferingVO);
+ Assert.assertTrue(hasAccess);
+ }
+
+ @Test
+ public void testBackupOfferingAccessNoAccess() {
+ Account normalUser = Mockito.mock(Account.class);
+ Mockito.when(normalUser.getId()).thenReturn(3L);
+ BackupOfferingVO backupOfferingVO =
Mockito.mock(BackupOfferingVO.class);
+
Mockito.when(_accountService.isRootAdmin(normalUser.getId())).thenReturn(false);
+
Mockito.when(_accountService.isDomainAdmin(normalUser.getId())).thenReturn(false);
+
+ boolean hasAccess = domainChecker.checkAccess(normalUser,
backupOfferingVO);
+ Assert.assertFalse(hasAccess);
+ }
+
}
diff --git
a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java
b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java
index 60263cea9b8..8295202fcc5 100644
---
a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java
+++
b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java
@@ -49,6 +49,7 @@ import com.cloud.storage.dao.VolumeDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManagerImpl;
import com.cloud.user.User;
+import com.cloud.utils.DomainHelper;
import com.cloud.utils.Pair;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchCriteria;
@@ -178,6 +179,8 @@ public class ConfigurationManagerImplTest {
PrimaryDataStoreDao storagePoolDao;
@Mock
StoragePoolDetailsDao storagePoolDetailsDao;
+ @Mock
+ DomainHelper domainHelper;
DeleteZoneCmd deleteZoneCmd;
CreateNetworkOfferingCmd createNetworkOfferingCmd;
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index fe4ea0838f1..1ec141a8be1 100644
--- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
@@ -59,6 +59,7 @@ import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
+import com.cloud.domain.Domain;
import com.cloud.storage.dao.SnapshotPolicyDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
@@ -3107,7 +3108,7 @@ public class UserVmManagerImplTest {
configureDoNothingForMethodsThatWeDoNotWantToTest();
-
doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class),
Mockito.any());
+
doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class),
Mockito.any(Domain.class));
Assert.assertThrows(PermissionDeniedException.class, () ->
userVmManagerImpl.moveVmToUser(assignVmCmdMock));
}
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 8b13fd47494..a9c083228e2 100644
--- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java
+++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java
@@ -60,6 +60,7 @@ import com.cloud.user.ResourceLimitService;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.DateUtil;
+import com.cloud.utils.DomainHelper;
import com.cloud.utils.Pair;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@@ -80,11 +81,13 @@ import
org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd;
import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd;
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.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
@@ -241,6 +244,12 @@ public class BackupManagerTest {
@Mock
private GuestOSDao _guestOSDao;
+ @Mock
+ private BackupOfferingDetailsDao backupOfferingDetailsDao;
+
+ @Mock
+ DomainHelper domainHelper;
+
private Gson gson;
private String[] hostPossibleValues = {"127.0.0.1", "hostname"};
@@ -352,6 +361,7 @@ public class BackupManagerTest {
when(cmd.getName()).thenReturn("New name");
when(cmd.getDescription()).thenReturn("New description");
when(cmd.getAllowUserDrivenBackups()).thenReturn(true);
+
when(backupOfferingDetailsDao.findDomainIds(id)).thenReturn(Collections.emptyList());
BackupOffering updated = backupManager.updateBackupOffering(cmd);
assertEquals("New name", updated.getName());
@@ -1081,7 +1091,7 @@ public class BackupManagerTest {
assertEquals("root-disk-offering-uuid",
VmDiskInfo.getDiskOffering().getUuid());
assertEquals(Long.valueOf(5), VmDiskInfo.getSize());
- assertEquals(null, VmDiskInfo.getDeviceId());
+ assertNull(VmDiskInfo.getDeviceId());
}
@Test
@@ -1106,7 +1116,7 @@ public class BackupManagerTest {
assertEquals("Test Offering", result.getName());
assertEquals("Test Description", result.getDescription());
- assertEquals(true, result.isUserDrivenBackupAllowed());
+ assertTrue(result.isUserDrivenBackupAllowed());
assertEquals("external-id", result.getExternalId());
assertEquals("testbackupprovider", result.getProvider());
}
@@ -1149,6 +1159,8 @@ public class BackupManagerTest {
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vm.getId()).thenReturn(vmId);
BackupOfferingVO offering = mock(BackupOfferingVO.class);
+ Account owner = mock(Account.class);
+
overrideBackupFrameworkConfigValue();
@@ -1159,6 +1171,8 @@ public class BackupManagerTest {
when(vm.getBackupOfferingId()).thenReturn(null);
when(offering.getProvider()).thenReturn("testbackupprovider");
when(backupProvider.assignVMToBackupOffering(vm,
offering)).thenReturn(true);
+ when(vm.getAccountId()).thenReturn(3L);
+ when(accountManager.getAccount(vm.getAccountId())).thenReturn(owner);
when(vmInstanceDao.update(1L, vm)).thenReturn(true);
try (MockedStatic<UsageEventUtils> ignored2 =
Mockito.mockStatic(UsageEventUtils.class)) {
@@ -2156,4 +2170,352 @@ public class BackupManagerTest {
verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId);
verify(volumeDao, times(1)).findByInstance(vmId);
}
+
+ @Test
+ public void getBackupOfferingDomainsTestOfferingNotFound() {
+ Long offeringId = 1L;
+ when(backupOfferingDao.findById(offeringId)).thenReturn(null);
+
+ InvalidParameterValueException exception =
Assert.assertThrows(InvalidParameterValueException.class,
+ () -> backupManager.getBackupOfferingDomains(offeringId));
+ assertEquals("Unable to find backup offering for id: " + offeringId,
exception.getMessage());
+ }
+
+ @Test
+ public void getBackupOfferingDomainsTestReturnsDomains() {
+ Long offeringId = 1L;
+ BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.findById(offeringId)).thenReturn(offering);
+
when(backupOfferingDetailsDao.findDomainIds(offeringId)).thenReturn(List.of(10L,
20L));
+
+ List<Long> result = backupManager.getBackupOfferingDomains(offeringId);
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains(10L));
+ assertTrue(result.contains(20L));
+ }
+
+ @Test
+ public void testUpdateBackupOfferingThrowsWhenDomainIdInvalid() {
+ Long id = 1234L;
+ UpdateBackupOfferingCmd cmd =
Mockito.spy(UpdateBackupOfferingCmd.class);
+ when(cmd.getId()).thenReturn(id);
+ when(cmd.getDomainIds()).thenReturn(List.of(99L));
+
+ when(domainDao.findById(99L)).thenReturn(null);
+
+ InvalidParameterValueException exception =
Assert.assertThrows(InvalidParameterValueException.class,
+ () -> backupManager.updateBackupOffering(cmd));
+ assertEquals("Please specify a valid domain id",
exception.getMessage());
+ }
+
+ @Test
+ public void testUpdateBackupOfferingPersistsDomainDetailsWhenProvided() {
+ Long id = 1234L;
+ Long domainId = 11L;
+ UpdateBackupOfferingCmd cmd =
Mockito.spy(UpdateBackupOfferingCmd.class);
+ when(cmd.getId()).thenReturn(id);
+ when(cmd.getDomainIds()).thenReturn(List.of(domainId));
+
+ DomainVO domain = Mockito.mock(DomainVO.class);
+ when(domainDao.findById(domainId)).thenReturn(domain);
+
+
when(domainHelper.filterChildSubDomains(List.of(domainId))).thenReturn(new
ArrayList<>(List.of(domainId)));
+ when(backupOfferingDetailsDao.findDomainIds(id)).thenReturn(new
ArrayList<>());
+
+ BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class);
+ BackupOfferingVO offeringUpdate = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.findById(id)).thenReturn(offering);
+ when(backupOfferingDao.createForUpdate(id)).thenReturn(offeringUpdate);
+ when(backupOfferingDao.update(id, offeringUpdate)).thenReturn(true);
+
+ BackupOffering updated = backupManager.updateBackupOffering(cmd);
+
+ verify(backupOfferingDetailsDao,
times(1)).updateBackupOfferingDomainIdsDetail(id, List.of(domainId));
+ }
+
+ @Test
+ public void
testListBackupOfferingsWithDomainFilteringIncludesGlobalOfferings() {
+ Long requestedDomainId = 3L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(requestedDomainId);
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO globalOffering = createMockOffering(1L, "Global
Offering");
+ BackupOfferingVO domainOffering = createMockOffering(2L, "Domain
Offering");
+
+ List<BackupOfferingVO> allOfferings = List.of(globalOffering,
domainOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+
when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(Collections.emptyList());
+
when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(2L));
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(1, result.first().size());
+ assertEquals("Global Offering", result.first().get(0).getName());
+ }
+ }
+
+ @Test
+ public void
testListBackupOfferingsWithDomainFilteringIncludesDirectDomainMapping() {
+ Long requestedDomainId = 3L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(requestedDomainId);
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO directDomainOffering = createMockOffering(1L, "Direct
Domain Offering");
+ BackupOfferingVO otherDomainOffering = createMockOffering(2L, "Other
Domain Offering");
+
+ List<BackupOfferingVO> allOfferings = List.of(directDomainOffering,
otherDomainOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+
when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(requestedDomainId));
+
when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(5L));
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(1, result.first().size());
+ assertEquals("Direct Domain Offering",
result.first().get(0).getName());
+ }
+ }
+
+ @Test
+ public void
testListBackupOfferingsWithDomainFilteringIncludesParentDomainOfferings() {
+ Long parentDomainId = 1L;
+ Long childDomainId = 3L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(childDomainId);
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO parentDomainOffering = createMockOffering(1L, "Parent
Domain Offering");
+ BackupOfferingVO siblingDomainOffering = createMockOffering(2L,
"Sibling Domain Offering");
+
+ List<BackupOfferingVO> allOfferings = List.of(parentDomainOffering,
siblingDomainOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+
when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(parentDomainId));
+
when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(4L));
+
+ when(domainDao.isChildDomain(parentDomainId,
childDomainId)).thenReturn(true);
+ when(domainDao.isChildDomain(4L, childDomainId)).thenReturn(false);
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(1, result.first().size());
+ assertEquals("Parent Domain Offering",
result.first().get(0).getName());
+ }
+ }
+
+ @Test
+ public void
testListBackupOfferingsWithDomainFilteringExcludesSiblingDomainOfferings() {
+ Long requestedDomainId = 3L;
+ Long siblingDomainId = 4L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(requestedDomainId);
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO siblingOffering = createMockOffering(1L, "Sibling
Domain Offering");
+ List<BackupOfferingVO> allOfferings = List.of(siblingOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+
when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(siblingDomainId));
+ when(domainDao.isChildDomain(siblingDomainId,
requestedDomainId)).thenReturn(false);
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(0, result.first().size());
+ }
+ }
+
+ @Test
+ public void
testListBackupOfferingsWithDomainFilteringMultipleDomainMappings() {
+ Long requestedDomainId = 5L;
+ Long parentDomainId1 = 1L;
+ Long parentDomainId2 = 2L;
+ Long unrelatedDomainId = 8L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(requestedDomainId);
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO multiDomainOffering = createMockOffering(1L,
"Multi-Domain Offering");
+ List<BackupOfferingVO> allOfferings = List.of(multiDomainOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+ when(backupOfferingDetailsDao.findDomainIds(1L))
+ .thenReturn(List.of(parentDomainId1, unrelatedDomainId,
parentDomainId2));
+
+ when(domainDao.isChildDomain(parentDomainId1,
requestedDomainId)).thenReturn(false);
+ when(domainDao.isChildDomain(unrelatedDomainId,
requestedDomainId)).thenReturn(false);
+ when(domainDao.isChildDomain(parentDomainId2,
requestedDomainId)).thenReturn(true);
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(1, result.first().size());
+ assertEquals("Multi-Domain Offering",
result.first().get(0).getName());
+ }
+ }
+
+ @Test
+ public void testListBackupOfferingsNormalUserDefaultsToDomainFiltering() {
+ Long userDomainId = 7L;
+
+ ListBackupOfferingsCmd cmd =
+ Mockito.mock(ListBackupOfferingsCmd.class);
+ when(cmd.getOfferingId()).thenReturn(null);
+ when(cmd.getDomainId()).thenReturn(null); // User didn't pass domain
filter
+ when(cmd.getStartIndex()).thenReturn(0L);
+ when(cmd.getPageSizeVal()).thenReturn(20L);
+
+ BackupOfferingVO globalOffering = createMockOffering(1L, "Global
Offering");
+ BackupOfferingVO userDomainOffering = createMockOffering(2L, "User
Domain Offering");
+ BackupOfferingVO otherDomainOffering = createMockOffering(3L, "Other
Domain Offering");
+
+ List<BackupOfferingVO> allOfferings = List.of(globalOffering,
userDomainOffering, otherDomainOffering);
+
+ SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<BackupOfferingVO> sc =
Mockito.mock(SearchCriteria.class);
+ BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class);
+ when(backupOfferingDao.createSearchBuilder()).thenReturn(sb);
+ when(sb.entity()).thenReturn(entityMock);
+ when(sb.and(Mockito.anyString(), Mockito.any(),
Mockito.any(SearchCriteria.Op.class))).thenReturn(sb);
+ when(sb.create()).thenReturn(sc);
+ when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any()))
+ .thenReturn(new Pair<>(allOfferings, allOfferings.size()));
+
+
when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(Collections.emptyList());
// Global
+
when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(userDomainId));
// User's domain
+
when(backupOfferingDetailsDao.findDomainIds(3L)).thenReturn(List.of(99L)); //
Other domain
+
+ when(domainDao.isChildDomain(99L, userDomainId)).thenReturn(false);
+
+ Account account = Mockito.mock(Account.class);
+ when(account.getType()).thenReturn(Account.Type.NORMAL);
+ when(account.getDomainId()).thenReturn(userDomainId);
+
+ try (MockedStatic<CallContext> mockedCallContext =
Mockito.mockStatic(CallContext.class)) {
+ CallContext contextMock = Mockito.mock(CallContext.class);
+
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+ when(contextMock.getCallingAccount()).thenReturn(account);
+
+ Pair<List<BackupOffering>, Integer> result =
backupManager.listBackupOfferings(cmd);
+
+ assertEquals(2, result.first().size());
+ assertTrue(result.first().stream().anyMatch(o ->
o.getName().equals("Global Offering")));
+ assertTrue(result.first().stream().anyMatch(o ->
o.getName().equals("User Domain Offering")));
+ }
+ }
+
+ private BackupOfferingVO createMockOffering(Long id, String name) {
+ BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class);
+ when(offering.getId()).thenReturn(id);
+ when(offering.getName()).thenReturn(name);
+ return offering;
+ }
+
}
diff --git
a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java
b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java
index 2fa9a2d7a44..f175c567422 100644
---
a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java
+++
b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java
@@ -27,6 +27,7 @@ import java.util.Set;
import javax.inject.Inject;
+import com.cloud.utils.DomainHelper;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@@ -98,6 +99,9 @@ public class CreateNetworkOfferingTest extends TestCase {
@Mock
LoadBalancerVMMapDao _loadBalancerVMMapDao;
+ @Mock
+ DomainHelper domainHelper;
+
@Mock
AnnotationDao annotationDao;
@Inject
diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py
index 05ce9d41023..6c9aa087bc7 100644
--- a/tools/marvin/setup.py
+++ b/tools/marvin/setup.py
@@ -27,7 +27,7 @@ except ImportError:
raise RuntimeError("python setuptools is required to build Marvin")
-VERSION = "4.23.0.0-SNAPSHOT"
+VERSION = "4.23.0.0"
setup(name="Marvin",
version=VERSION,
diff --git a/ui/src/config/section/offering.js
b/ui/src/config/section/offering.js
index 4a32619b8c2..bc95772d6f7 100644
--- a/ui/src/config/section/offering.js
+++ b/ui/src/config/section/offering.js
@@ -340,9 +340,9 @@ export default {
icon: 'cloud-upload-outlined',
docHelp: 'adminguide/virtual_machines.html#backup-offerings',
permission: ['listBackupOfferings'],
- searchFilters: ['zoneid'],
- columns: ['name', 'description', 'zonename'],
- details: ['name', 'id', 'description', 'externalid', 'zone',
'allowuserdrivenbackups', 'created'],
+ searchFilters: ['zoneid', 'domainid'],
+ columns: ['name', 'description', 'domain', 'zonename'],
+ details: ['name', 'id', 'description', 'externalid', 'domain', 'zone',
'allowuserdrivenbackups', 'created'],
related: [{
name: 'vm',
title: 'label.instances',
diff --git a/ui/src/views/offering/ImportBackupOffering.vue
b/ui/src/views/offering/ImportBackupOffering.vue
index b8ac7d8e8e6..f680eacd4a7 100644
--- a/ui/src/views/offering/ImportBackupOffering.vue
+++ b/ui/src/views/offering/ImportBackupOffering.vue
@@ -85,6 +85,33 @@
</template>
<a-switch v-model:checked="form.allowuserdrivenbackups"/>
</a-form-item>
+ <a-form-item name="ispublic" ref="ispublic"
:label="$t('label.ispublic')" v-if="isAdmin()">
+ <a-switch v-model:checked="form.ispublic" />
+ </a-form-item>
+ <a-form-item name="domainid" ref="domainid" v-if="!form.ispublic">
+ <template #label>
+ <tooltip-label :title="$t('label.domainid')"
:tooltip="apiParams.domainid.description"/>
+ </template>
+ <a-select
+ mode="multiple"
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.domainid"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => {
+ return option.label.toLowerCase().indexOf(input.toLowerCase())
>= 0
+ }"
+ :loading="domains.loading"
+ :placeholder="apiParams.domainid.description">
+ <a-select-option v-for="(opt, optIndex) in domains.opts"
:key="optIndex" :label="opt.path || opt.name || opt.description">
+ <span>
+ <resource-icon v-if="opt && opt.icon"
:image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
+ <block-outlined v-else style="margin-right: 5px" />
+ {{ opt.path || opt.name || opt.description }}
+ </span>
+ </a-select-option>
+ </a-select>
+ </a-form-item>
<div :span="24" class="action-button">
<a-button :loading="loading" @click="closeAction">{{
this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary"
@click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
@@ -96,6 +123,7 @@
<script>
import { ref, reactive, toRaw } from 'vue'
import { getAPI, postAPI } from '@/api'
+import { isAdmin } from '@/role'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
@@ -108,6 +136,10 @@ export default {
data () {
return {
loading: false,
+ domains: {
+ loading: false,
+ opts: []
+ },
zones: {
loading: false,
opts: []
@@ -129,17 +161,23 @@ export default {
initForm () {
this.formRef = ref()
this.form = reactive({
- allowuserdrivenbackups: true
+ allowuserdrivenbackups: true,
+ ispublic: true
})
this.rules = reactive({
name: [{ required: true, message:
this.$t('message.error.required.input') }],
description: [{ required: true, message:
this.$t('message.error.required.input') }],
zoneid: [{ required: true, message: this.$t('message.error.select') }],
- externalid: [{ required: true, message:
this.$t('message.error.select') }]
+ externalid: [{ required: true, message:
this.$t('message.error.select') }],
+ domainid: [{ type: 'array', message: this.$t('message.error.select') }]
})
},
+ isAdmin () {
+ return isAdmin()
+ },
fetchData () {
this.fetchZone()
+ this.fetchDomainData()
},
fetchZone () {
this.zones.loading = true
@@ -151,6 +189,19 @@ export default {
this.zones.loading = false
})
},
+ fetchDomainData () {
+ const params = {}
+ params.listAll = true
+ params.details = 'min'
+ this.domains.loading = true
+ getAPI('listDomains', params).then(json => {
+ this.domains.opts = json.listdomainsresponse.domain || []
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.domains.loading = false
+ })
+ },
fetchExternal (zoneId) {
if (!zoneId) {
this.externals.opts = []
@@ -179,6 +230,20 @@ export default {
params[key] = input
}
}
+ if (values.ispublic !== true) {
+ var domainIndexes = values.domainid
+ var domainId = null
+ if (domainIndexes && domainIndexes.length > 0) {
+ var domainIds = []
+ for (var i = 0; i < domainIndexes.length; i++) {
+ domainIds =
domainIds.concat(this.domains.opts[domainIndexes[i]].id)
+ }
+ domainId = domainIds.join(',')
+ }
+ if (domainId) {
+ params.domainid = domainId
+ }
+ }
params.allowuserdrivenbackups = values.allowuserdrivenbackups
this.loading = true
const title = this.$t('label.import.offering')