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
This commit is contained in:
Pearl Dsilva 2026-01-19 03:51:47 -05:00 committed by GitHub
parent 002d9768b2
commit 8b2f1f19c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1610 additions and 316 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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<>();

View File

@ -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///////////////////
/////////////////////////////////////////////////////

View File

@ -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;

View File

@ -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");
}
/////////////////////////////////////////////////////

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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"));
}
}

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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';

View File

@ -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);
}
}

View File

@ -24,7 +24,7 @@ sonar.projectVersion=1.0
sonar.sources=src
sonar.binaries=target/classes
# Exclussions
# Exclusions
sonar.exclusions=**/*Test.java
# Language

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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 "

View File

@ -81,4 +81,6 @@
<bean id="DPDKHelper" class="com.cloud.hypervisor.kvm.dpdk.DpdkHelperImpl" />
<bean id="domainHelper" class="com.cloud.utils.DomainHelper" />
</beans>

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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));
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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,

View File

@ -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',

View File

@ -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')