Add support to clone existing offerings and update them (#12357)

* Add support to clone existing offerings and update them

* add support for vpc & backup offerings to be cloned

* fix capability list and mapping of params

* Add support to clone network and vpc offering with the right parameters

* make fields non mandatory for clone offerings APIs

* Add UI support for cloning Compute and System Service offerings

* remove unnecessary changes

* fix license and pre-ccommit issues

* Add UI support to clone disk and network offering

* vpc & backup offering clone api

* add unit tests

* fix pre-commit checks

* increase test coverage

* combine add/clone disk/compute offering forms

* update license

* fix unit tests

* fix test failures

* fix test failure - unnecessary stubbings

* pre-commit check failure

* add recently added domain id for bkp offering to be inherited in clone operation

* extract common code wrt service capability in network & vpc offering in add/clone operations

* add some checks to prevent networkmode change when provider is nsx/netris from the source networkmode

* address copilot comments

* address comments

* combine check

* use appropriate zoneId during clone bkp offering

* add check

* fix issue with test

* remove unused imports

* prevent creating a bkp offering of a bkp repo that already exists

* extend clone disk and service offerings to domain admins
This commit is contained in:
Pearl Dsilva 2026-03-17 02:31:43 -04:00 committed by GitHub
parent 93239e09f1
commit 3bd5410f9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 10702 additions and 1834 deletions

View File

@ -24,15 +24,18 @@ import com.cloud.network.Network;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd;
import org.apache.cloudstack.api.command.admin.network.NetworkOfferingBaseCmd;
import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd;
@ -105,6 +108,33 @@ public interface ConfigurationService {
*/
ServiceOffering createServiceOffering(CreateServiceOfferingCmd cmd);
/**
* Clones a service offering with optional parameter overrides
*
* @param cmd
* the command object that specifies the source offering ID and optional parameter overrides
* @return the newly created service offering cloned from source, null otherwise
*/
ServiceOffering cloneServiceOffering(CloneServiceOfferingCmd cmd);
/**
* Clones a disk offering with optional parameter overrides
*
* @param cmd
* the command object that specifies the source offering ID and optional parameter overrides
* @return the newly created disk offering cloned from source, null otherwise
*/
DiskOffering cloneDiskOffering(CloneDiskOfferingCmd cmd);
/**
* Clones a network offering with optional parameter overrides
*
* @param cmd
* the command object that specifies the source offering ID and optional parameter overrides
* @return the newly created network offering cloned from source, null otherwise
*/
NetworkOffering cloneNetworkOffering(CloneNetworkOfferingCmd cmd);
/**
* Updates a service offering
*
@ -282,7 +312,7 @@ public interface ConfigurationService {
boolean releasePublicIpRange(ReleasePublicIpRangeCmd cmd);
NetworkOffering createNetworkOffering(CreateNetworkOfferingCmd cmd);
NetworkOffering createNetworkOffering(NetworkOfferingBaseCmd cmd);
NetworkOffering updateNetworkOffering(UpdateNetworkOfferingCmd cmd);

View File

@ -375,11 +375,13 @@ public class EventTypes {
// Service Offerings
public static final String EVENT_SERVICE_OFFERING_CREATE = "SERVICE.OFFERING.CREATE";
public static final String EVENT_SERVICE_OFFERING_CLONE = "SERVICE.OFFERING.CLONE";
public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT";
public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE";
// Disk Offerings
public static final String EVENT_DISK_OFFERING_CREATE = "DISK.OFFERING.CREATE";
public static final String EVENT_DISK_OFFERING_CLONE = "DISK.OFFERING.CLONE";
public static final String EVENT_DISK_OFFERING_EDIT = "DISK.OFFERING.EDIT";
public static final String EVENT_DISK_OFFERING_DELETE = "DISK.OFFERING.DELETE";
@ -400,6 +402,7 @@ public class EventTypes {
// Network offerings
public static final String EVENT_NETWORK_OFFERING_CREATE = "NETWORK.OFFERING.CREATE";
public static final String EVENT_NETWORK_OFFERING_CLONE = "NETWORK.OFFERING.CLONE";
public static final String EVENT_NETWORK_OFFERING_ASSIGN = "NETWORK.OFFERING.ASSIGN";
public static final String EVENT_NETWORK_OFFERING_EDIT = "NETWORK.OFFERING.EDIT";
public static final String EVENT_NETWORK_OFFERING_REMOVE = "NETWORK.OFFERING.REMOVE";
@ -599,6 +602,7 @@ public class EventTypes {
// VPC offerings
public static final String EVENT_VPC_OFFERING_CREATE = "VPC.OFFERING.CREATE";
public static final String EVENT_VPC_OFFERING_CLONE = "VPC.OFFERING.CLONE";
public static final String EVENT_VPC_OFFERING_UPDATE = "VPC.OFFERING.UPDATE";
public static final String EVENT_VPC_OFFERING_DELETE = "VPC.OFFERING.DELETE";
@ -631,6 +635,7 @@ public class EventTypes {
// Backup and Recovery events
public static final String EVENT_VM_BACKUP_IMPORT_OFFERING = "BACKUP.IMPORT.OFFERING";
public static final String EVENT_VM_BACKUP_OFFERING_CLONE = "BACKUP.OFFERING.CLONE";
public static final String EVENT_VM_BACKUP_OFFERING_ASSIGN = "BACKUP.OFFERING.ASSIGN";
public static final String EVENT_VM_BACKUP_OFFERING_REMOVE = "BACKUP.OFFERING.REMOVE";
public static final String EVENT_VM_BACKUP_CREATE = "BACKUP.CREATE";
@ -1046,11 +1051,13 @@ public class EventTypes {
// Service Offerings
entityEventDetails.put(EVENT_SERVICE_OFFERING_CREATE, ServiceOffering.class);
entityEventDetails.put(EVENT_SERVICE_OFFERING_CLONE, ServiceOffering.class);
entityEventDetails.put(EVENT_SERVICE_OFFERING_EDIT, ServiceOffering.class);
entityEventDetails.put(EVENT_SERVICE_OFFERING_DELETE, ServiceOffering.class);
// Disk Offerings
entityEventDetails.put(EVENT_DISK_OFFERING_CREATE, DiskOffering.class);
entityEventDetails.put(EVENT_DISK_OFFERING_CLONE, DiskOffering.class);
entityEventDetails.put(EVENT_DISK_OFFERING_EDIT, DiskOffering.class);
entityEventDetails.put(EVENT_DISK_OFFERING_DELETE, DiskOffering.class);
@ -1071,6 +1078,7 @@ public class EventTypes {
// Network offerings
entityEventDetails.put(EVENT_NETWORK_OFFERING_CREATE, NetworkOffering.class);
entityEventDetails.put(EVENT_NETWORK_OFFERING_CLONE, NetworkOffering.class);
entityEventDetails.put(EVENT_NETWORK_OFFERING_ASSIGN, NetworkOffering.class);
entityEventDetails.put(EVENT_NETWORK_OFFERING_EDIT, NetworkOffering.class);
entityEventDetails.put(EVENT_NETWORK_OFFERING_REMOVE, NetworkOffering.class);

View File

@ -20,6 +20,7 @@ package com.cloud.network.vpc;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd;
import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd;
import org.apache.cloudstack.api.command.admin.vpc.UpdateVPCOfferingCmd;
import org.apache.cloudstack.api.command.user.vpc.ListVPCOfferingsCmd;
@ -34,6 +35,8 @@ public interface VpcProvisioningService {
VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd);
VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd);
VpcOffering createVpcOffering(String name, String displayText, List<String> supportedServices,
Map<String, List<String>> serviceProviders,
Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol,

View File

@ -559,6 +559,7 @@ public class ApiConstants {
public static final String USE_STORAGE_REPLICATION = "usestoragereplication";
public static final String SOURCE_CIDR_LIST = "sourcecidrlist";
public static final String SOURCE_OFFERING_ID = "sourceofferingid";
public static final String SOURCE_ZONE_ID = "sourcezoneid";
public static final String SSL_VERIFICATION = "sslverification";
public static final String START_ASN = "startasn";

View File

@ -0,0 +1,166 @@
// 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.admin.backup;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
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.api.response.ZoneResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.context.CallContext;
import com.cloud.event.EventTypes;
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.exception.CloudRuntimeException;
import java.util.Arrays;
import java.util.List;
import java.util.function.LongFunction;
@APICommand(name = "cloneBackupOffering",
description = "Clones a backup offering from an existing offering",
responseObject = BackupOfferingResponse.class, since = "4.23.0",
authorized = {RoleType.Admin})
public class CloneBackupOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver {
@Inject
protected BackupManager backupManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
////////////////////////////////////////////////////
@Parameter(name = ApiConstants.SOURCE_OFFERING_ID, type = BaseCmd.CommandType.UUID, entityType = BackupOfferingResponse.class,
required = true, description = "The ID of the source backup offering to clone from")
private Long sourceOfferingId;
@Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true,
description = "The name of the cloned offering")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, required = false,
description = "The description of the cloned offering")
private String description;
@Parameter(name = ApiConstants.EXTERNAL_ID, type = BaseCmd.CommandType.STRING, required = false,
description = "The backup offering ID (from backup provider side)")
private String externalId;
@Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class,
description = "The zone ID", required = false)
private Long zoneId;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.STRING,
description = "the ID of the containing domain(s) as comma separated string, public for public offerings",
length = 4096)
private String domainIds;
@Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = BaseCmd.CommandType.BOOLEAN,
description = "Whether users are allowed to create adhoc backups and backup schedules", required = false)
private Boolean userDrivenBackups;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getSourceOfferingId() {
return sourceOfferingId;
}
public String getName() {
return name;
}
public String getExternalId() {
return externalId;
}
public Long getZoneId() {
return zoneId;
}
public String getDescription() {
return description;
}
public Boolean getUserDrivenBackups() {
return userDrivenBackups;
}
public List<Long> getDomainIds() {
if (domainIds != null && !domainIds.isEmpty()) {
return Arrays.asList(Arrays.stream(domainIds.split(",")).map(domainId -> Long.parseLong(domainId.trim())).toArray(Long[]::new));
}
LongFunction<List<Long>> defaultDomainsProvider = null;
if (backupManager != null) {
defaultDomainsProvider = backupManager::getBackupOfferingDomains;
}
return resolveDomainIds(domainIds, sourceOfferingId, defaultDomainsProvider, "backup offering");
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {
BackupOffering policy = backupManager.cloneBackupOffering(this);
if (policy == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone backup offering");
}
BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(policy);
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (InvalidParameterValueException e) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage());
} catch (CloudRuntimeException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
}
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
@Override
public String getEventType() {
return EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE;
}
@Override
public String getEventDescription() {
return "Cloning backup offering: " + name + " from source offering: " + (sourceOfferingId == null ? "" : sourceOfferingId.toString());
}
}

View File

@ -54,7 +54,7 @@ import java.util.Set;
public class ImportBackupOfferingCmd extends BaseAsyncCmd {
@Inject
private BackupManager backupManager;
protected BackupManager backupManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@ -86,7 +86,8 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = DomainResponse.class,
description = "the ID of the containing domain(s), null for public offerings")
description = "the ID of the containing domain(s), null for public offerings",
since = "4.23.0")
private List<Long> domainIds;
/////////////////////////////////////////////////////

View File

@ -0,0 +1,113 @@
// 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.admin.network;
import java.util.List;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
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.response.NetworkOfferingResponse;
import com.cloud.offering.NetworkOffering;
@APICommand(name = "cloneNetworkOffering",
description = "Clones a network offering. All parameters are copied from the source offering unless explicitly overridden. " +
"Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.",
responseObject = NetworkOfferingResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.23.0")
public class CloneNetworkOfferingCmd extends NetworkOfferingBaseCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.SOURCE_OFFERING_ID,
type = BaseCmd.CommandType.UUID,
entityType = NetworkOfferingResponse.class,
required = true,
description = "The ID of the source network offering to clone from")
private Long sourceOfferingId;
@Parameter(name = "addservices",
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services to add to the cloned offering (in addition to source offering services). " +
"If specified along with 'supportedservices', this parameter is ignored.")
private List<String> addServices;
@Parameter(name = "dropservices",
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services to remove from the cloned offering (that exist in source offering). " +
"If specified along with 'supportedservices', this parameter is ignored.")
private List<String> dropServices;
@Parameter(name = ApiConstants.TRAFFIC_TYPE,
type = CommandType.STRING,
description = "The traffic type for the network offering. Supported type in current release is GUEST only")
private String traffictype;
@Parameter(name = ApiConstants.GUEST_IP_TYPE, type = CommandType.STRING, description = "Guest type of the network offering: Shared or Isolated")
private String guestIptype;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getSourceOfferingId() {
return sourceOfferingId;
}
public List<String> getAddServices() {
return addServices;
}
public List<String> getDropServices() {
return dropServices;
}
public String getGuestIpType() {
return guestIptype;
}
public String getTraffictype() {
return traffictype;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
NetworkOffering result = _configService.cloneNetworkOffering(this);
if (result != null) {
NetworkOfferingResponse response = _responseGenerator.createNetworkOfferingResponse(result);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone network offering");
}
}
}

View File

@ -16,505 +16,47 @@
// under the License.
package org.apache.cloudstack.api.command.admin.network;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.cloud.network.Network;
import com.cloud.network.VirtualRouterProvider;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
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.response.NetworkOfferingResponse;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Network.Capability;
import com.cloud.network.Network.Service;
import com.cloud.offering.NetworkOffering;
import com.cloud.offering.NetworkOffering.Availability;
import com.cloud.user.Account;
import static com.cloud.network.Network.Service.Dhcp;
import static com.cloud.network.Network.Service.Dns;
import static com.cloud.network.Network.Service.Lb;
import static com.cloud.network.Network.Service.StaticNat;
import static com.cloud.network.Network.Service.SourceNat;
import static com.cloud.network.Network.Service.PortForwarding;
import static com.cloud.network.Network.Service.NetworkACL;
import static com.cloud.network.Network.Service.UserData;
import static com.cloud.network.Network.Service.Firewall;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb;
@APICommand(name = "createNetworkOffering", description = "Creates a network offering.", responseObject = NetworkOfferingResponse.class, since = "3.0.0",
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class CreateNetworkOfferingCmd extends BaseCmd {
public class CreateNetworkOfferingCmd extends NetworkOfferingBaseCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the network offering")
private String networkOfferingName;
@Parameter(name = ApiConstants.DISPLAY_TEXT, type = CommandType.STRING, description = "The display text of the network offering, defaults to the value of 'name'.")
private String displayText;
@Parameter(name = ApiConstants.TRAFFIC_TYPE,
type = CommandType.STRING,
required = true,
description = "The traffic type for the network offering. Supported type in current release is GUEST only")
private String traffictype;
@Parameter(name = ApiConstants.TAGS, type = CommandType.STRING, description = "The tags for the network offering.", length = 4096)
private String tags;
@Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "True if network offering supports VLANs")
private Boolean specifyVlan;
@Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "The availability of network offering. The default value is Optional. "
+ " Another value is Required, which will make it as the default network offering for new networks ")
private String availability;
@Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "Data transfer rate in megabits per second allowed")
private Integer networkRate;
@Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, description = "True if the network offering is IP conserve mode enabled")
private Boolean conserveMode;
@Parameter(name = ApiConstants.SERVICE_OFFERING_ID,
type = CommandType.UUID,
entityType = ServiceOfferingResponse.class,
description = "The service offering ID used by virtual router provider")
private Long serviceOfferingId;
@Parameter(name = ApiConstants.GUEST_IP_TYPE, type = CommandType.STRING, required = true, description = "Guest type of the network offering: Shared or Isolated")
private String guestIptype;
@Parameter(name = ApiConstants.INTERNET_PROTOCOL,
type = CommandType.STRING,
description = "The internet protocol of network offering. Options are IPv4 and dualstack. Default is IPv4. dualstack will create a network offering that supports both IPv4 and IPv6",
since = "4.17.0")
private String internetProtocol;
@Parameter(name = ApiConstants.SUPPORTED_SERVICES,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services supported by the network offering")
private List<String> supportedServices;
@Parameter(name = ApiConstants.SERVICE_PROVIDER_LIST,
type = CommandType.MAP,
description = "Provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network")
private Map serviceProviderList;
@Parameter(name = ApiConstants.SERVICE_CAPABILITY_LIST, type = CommandType.MAP, description = "Desired service capabilities as part of network offering")
private Map serviceCapabilitystList;
@Parameter(name = ApiConstants.SPECIFY_IP_RANGES,
type = CommandType.BOOLEAN,
description = "True if network offering supports specifying ip ranges; defaulted to false if not specified")
private Boolean specifyIpRanges;
@Parameter(name = ApiConstants.IS_PERSISTENT,
type = CommandType.BOOLEAN,
description = "True if network offering supports persistent networks; defaulted to false if not specified")
private Boolean isPersistent;
@Parameter(name = ApiConstants.FOR_VPC,
type = CommandType.BOOLEAN,
description = "True if network offering is meant to be used for VPC, false otherwise.")
private Boolean forVpc;
@Deprecated
@Parameter(name = ApiConstants.FOR_NSX,
type = CommandType.BOOLEAN,
description = "true if network offering is meant to be used for NSX, false otherwise.",
since = "4.20.0")
private Boolean forNsx;
@Parameter(name = ApiConstants.PROVIDER,
type = CommandType.STRING,
description = "Name of the provider providing the service",
since = "4.21.0")
private String provider;
@Parameter(name = ApiConstants.NSX_SUPPORT_LB,
type = CommandType.BOOLEAN,
description = "True if network offering for NSX network offering supports Load balancer service.",
since = "4.20.0")
private Boolean nsxSupportsLbService;
@Parameter(name = ApiConstants.NSX_SUPPORTS_INTERNAL_LB,
type = CommandType.BOOLEAN,
description = "True if network offering for NSX network offering supports Internal Load balancer service.",
since = "4.20.0")
private Boolean nsxSupportsInternalLbService;
@Parameter(name = ApiConstants.NETWORK_MODE,
type = CommandType.STRING,
description = "Indicates the mode with which the network will operate. Valid option: NATTED or ROUTED",
since = "4.20.0")
private String networkMode;
@Parameter(name = ApiConstants.FOR_TUNGSTEN,
type = CommandType.BOOLEAN,
description = "True if network offering is meant to be used for Tungsten-Fabric, false otherwise.")
private Boolean forTungsten;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.2.0", description = "Network offering details in key/value pairs."
+ " Supported keys are internallbprovider/publiclbprovider with service provider as a value, and"
+ " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup")
protected Map details;
@Parameter(name = ApiConstants.EGRESS_DEFAULT_POLICY,
type = CommandType.BOOLEAN,
description = "True if guest network default egress policy is allow; false if default egress policy is deny")
private Boolean egressDefaultPolicy;
@Parameter(name = ApiConstants.KEEPALIVE_ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "If true keepalive will be turned on in the loadbalancer. At the time of writing this has only an effect on haproxy; the mode http and httpclose options are unset in the haproxy conf file.")
private Boolean keepAliveEnabled;
@Parameter(name = ApiConstants.MAX_CONNECTIONS,
type = CommandType.INTEGER,
description = "Maximum number of concurrent connections supported by the Network offering")
private Integer maxConnections;
@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;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = ZoneResponse.class,
description = "The ID of the containing zone(s), null for public offerings",
since = "4.13")
private List<Long> zoneIds;
@Parameter(name = ApiConstants.ENABLE,
type = CommandType.BOOLEAN,
description = "Set to true if the offering is to be enabled during creation. Default is false",
since = "4.16")
private Boolean enable;
@Parameter(name = ApiConstants.SPECIFY_AS_NUMBER, type = CommandType.BOOLEAN, since = "4.20.0",
description = "true if network offering supports choosing AS number")
private Boolean specifyAsNumber;
@Parameter(name = ApiConstants.ROUTING_MODE,
type = CommandType.STRING,
since = "4.20.0",
description = "the routing mode for the network offering. Supported types are: Static or Dynamic.")
private String routingMode;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getNetworkOfferingName() {
return networkOfferingName;
}
public String getDisplayText() {
return StringUtils.isEmpty(displayText) ? networkOfferingName : displayText;
}
public String getTags() {
return tags;
}
public String getTraffictype() {
return traffictype;
}
public Boolean getSpecifyVlan() {
return specifyVlan == null ? false : specifyVlan;
}
public String getAvailability() {
return availability == null ? Availability.Optional.toString() : availability;
}
public Integer getNetworkRate() {
return networkRate;
}
public Long getServiceOfferingId() {
return serviceOfferingId;
}
public boolean isExternalNetworkProvider() {
return Arrays.asList("NSX", "Netris").stream()
.anyMatch(s -> provider != null && s.equalsIgnoreCase(provider));
}
public boolean isForNsx() {
return provider != null && provider.equalsIgnoreCase("NSX");
}
public boolean isForNetris() {
return provider != null && provider.equalsIgnoreCase("Netris");
}
public String getProvider() {
return provider;
}
public List<String> getSupportedServices() {
if (!isExternalNetworkProvider()) {
return supportedServices == null ? new ArrayList<String>() : supportedServices;
} else {
List<String> services = new ArrayList<>(List.of(
Dhcp.getName(),
Dns.getName(),
UserData.getName()
));
if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
services.addAll(Arrays.asList(
StaticNat.getName(),
SourceNat.getName(),
PortForwarding.getName()));
}
if (getNsxSupportsLbService() || (provider != null && isNetrisNatted(getProvider(), getNetworkMode()))) {
services.add(Lb.getName());
}
if (Boolean.TRUE.equals(forVpc)) {
services.add(NetworkACL.getName());
} else {
services.add(Firewall.getName());
}
return services;
}
}
public String getGuestIpType() {
return guestIptype;
}
public String getInternetProtocol() {
return internetProtocol;
}
public Boolean getSpecifyIpRanges() {
return specifyIpRanges == null ? false : specifyIpRanges;
}
public Boolean getConserveMode() {
if (conserveMode == null) {
return true;
}
return conserveMode;
}
public Boolean getIsPersistent() {
return isPersistent == null ? false : isPersistent;
}
public Boolean getForVpc() {
return forVpc;
}
public String getNetworkMode() {
return networkMode;
}
public boolean getNsxSupportsLbService() {
return BooleanUtils.isTrue(nsxSupportsLbService);
}
public boolean getNsxSupportsInternalLbService() {
return BooleanUtils.isTrue(nsxSupportsInternalLbService);
}
public Boolean getForTungsten() {
return forTungsten;
}
public Boolean getEgressDefaultPolicy() {
if (egressDefaultPolicy == null) {
return true;
}
return egressDefaultPolicy;
}
public Boolean getKeepAliveEnabled() {
return keepAliveEnabled;
}
public Integer getMaxconnections() {
return maxConnections;
}
public Map<String, List<String>> getServiceProviders() {
Map<String, List<String>> serviceProviderMap = new HashMap<>();
if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) {
Collection servicesCollection = serviceProviderList.values();
Iterator iter = servicesCollection.iterator();
while (iter.hasNext()) {
HashMap<String, String> services = (HashMap<String, String>) iter.next();
String service = services.get("service");
String provider = services.get("provider");
List<String> providerList = null;
if (serviceProviderMap.containsKey(service)) {
providerList = serviceProviderMap.get(service);
} else {
providerList = new ArrayList<String>();
}
providerList.add(provider);
serviceProviderMap.put(service, providerList);
}
} else if (isExternalNetworkProvider()) {
getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName());
}
return serviceProviderMap;
}
private void getServiceProviderMapForExternalProvider(Map<String, List<String>> serviceProviderMap, String provider) {
String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() :
VirtualRouterProvider.Type.VirtualRouter.name();
List<String> unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService"));
List<String> routerSupported = List.of("Dhcp", "Dns", "UserData");
List<String> allServices = Service.listAllServices().stream().map(Service::getName).collect(Collectors.toList());
if (routerProvider.equals(VirtualRouterProvider.Type.VPCVirtualRouter.name())) {
unsupportedServices.add("Firewall");
} else {
unsupportedServices.add("NetworkACL");
}
for (String service : allServices) {
if (unsupportedServices.contains(service))
continue;
if (routerSupported.contains(service))
serviceProviderMap.put(service, List.of(routerProvider));
else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || NetworkACL.getName().equalsIgnoreCase(service)) {
serviceProviderMap.put(service, List.of(provider));
}
if (isNsxWithoutLb(getProvider(), getNsxSupportsLbService()) || isNetrisRouted(getProvider(), getNetworkMode())) {
serviceProviderMap.remove(Lb.getName());
}
}
}
public Map<Capability, String> getServiceCapabilities(Service service) {
Map<Capability, String> capabilityMap = null;
if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) {
capabilityMap = new HashMap<Capability, String>();
Collection serviceCapabilityCollection = serviceCapabilitystList.values();
Iterator iter = serviceCapabilityCollection.iterator();
while (iter.hasNext()) {
HashMap<String, String> svcCapabilityMap = (HashMap<String, String>) iter.next();
Capability capability = null;
String svc = svcCapabilityMap.get("service");
String capabilityName = svcCapabilityMap.get("capabilitytype");
String capabilityValue = svcCapabilityMap.get("capabilityvalue");
if (capabilityName != null) {
capability = Capability.getCapability(capabilityName);
}
if ((capability == null) || (capabilityName == null) || (capabilityValue == null)) {
throw new InvalidParameterValueException("Invalid capability:" + capabilityName + " capability value:" + capabilityValue);
}
if (svc.equalsIgnoreCase(service.getName())) {
capabilityMap.put(capability, capabilityValue);
} else {
//throw new InvalidParameterValueException("Service is not equal ")
}
}
}
return capabilityMap;
}
public Map<String, String> getDetails() {
if (details == null || details.isEmpty()) {
return null;
}
Collection paramsCollection = details.values();
Object objlist[] = paramsCollection.toArray();
Map<String, String> params = (Map<String, String>) (objlist[0]);
for (int i = 1; i < objlist.length; i++) {
params.putAll((Map<String, String>) (objlist[i]));
}
return params;
}
public String getServicePackageId() {
Map<String, String> data = getDetails();
if (data == null)
return null;
return data.get(NetworkOffering.Detail.servicepackageuuid + "");
}
public List<Long> getDomainIds() {
if (CollectionUtils.isNotEmpty(domainIds)) {
Set<Long> set = new LinkedHashSet<>(domainIds);
domainIds.clear();
domainIds.addAll(set);
}
return domainIds;
}
public List<Long> getZoneIds() {
if (CollectionUtils.isNotEmpty(zoneIds)) {
Set<Long> set = new LinkedHashSet<>(zoneIds);
zoneIds.clear();
zoneIds.addAll(set);
}
return zoneIds;
}
public Boolean getEnable() {
if (enable != null) {
return enable;
}
return false;
}
public boolean getSpecifyAsNumber() {
return BooleanUtils.toBoolean(specifyAsNumber);
}
public String getRoutingMode() {
return routingMode;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public void execute() {

View File

@ -0,0 +1,493 @@
// 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.admin.network;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Network;
import com.cloud.network.VirtualRouterProvider;
import com.cloud.offering.NetworkOffering;
import com.cloud.user.Account;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.cloud.network.Network.Service.Dhcp;
import static com.cloud.network.Network.Service.Dns;
import static com.cloud.network.Network.Service.Firewall;
import static com.cloud.network.Network.Service.Lb;
import static com.cloud.network.Network.Service.NetworkACL;
import static com.cloud.network.Network.Service.PortForwarding;
import static com.cloud.network.Network.Service.SourceNat;
import static com.cloud.network.Network.Service.StaticNat;
import static com.cloud.network.Network.Service.UserData;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
public abstract class NetworkOfferingBaseCmd extends BaseCmd {
public abstract String getGuestIpType();
public abstract String getTraffictype();
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the network offering")
private String networkOfferingName;
@Parameter(name = ApiConstants.DISPLAY_TEXT, type = CommandType.STRING, description = "The display text of the network offering, defaults to the value of 'name'.")
private String displayText;
@Parameter(name = ApiConstants.TAGS, type = CommandType.STRING, description = "The tags for the network offering.", length = 4096)
private String tags;
@Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "True if network offering supports VLANs")
private Boolean specifyVlan;
@Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "The availability of network offering. The default value is Optional. "
+ " Another value is Required, which will make it as the default network offering for new networks ")
private String availability;
@Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "Data transfer rate in megabits per second allowed")
private Integer networkRate;
@Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, description = "True if the network offering is IP conserve mode enabled")
private Boolean conserveMode;
@Parameter(name = ApiConstants.SERVICE_OFFERING_ID,
type = CommandType.UUID,
entityType = ServiceOfferingResponse.class,
description = "The service offering ID used by virtual router provider")
private Long serviceOfferingId;
@Parameter(name = ApiConstants.INTERNET_PROTOCOL,
type = CommandType.STRING,
description = "The internet protocol of network offering. Options are IPv4 and dualstack. Default is IPv4. dualstack will create a network offering that supports both IPv4 and IPv6",
since = "4.17.0")
private String internetProtocol;
@Parameter(name = ApiConstants.SUPPORTED_SERVICES,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services supported by the network offering")
private List<String> supportedServices;
@Parameter(name = ApiConstants.SERVICE_PROVIDER_LIST,
type = CommandType.MAP,
description = "Provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network")
private Map serviceProviderList;
@Parameter(name = ApiConstants.SERVICE_CAPABILITY_LIST, type = CommandType.MAP, description = "Desired service capabilities as part of network offering")
private Map serviceCapabilitiesList;
@Parameter(name = ApiConstants.SPECIFY_IP_RANGES,
type = CommandType.BOOLEAN,
description = "True if network offering supports specifying ip ranges; defaulted to false if not specified")
private Boolean specifyIpRanges;
@Parameter(name = ApiConstants.IS_PERSISTENT,
type = CommandType.BOOLEAN,
description = "True if network offering supports persistent networks; defaulted to false if not specified")
private Boolean isPersistent;
@Parameter(name = ApiConstants.FOR_VPC,
type = CommandType.BOOLEAN,
description = "True if network offering is meant to be used for VPC, false otherwise.")
private Boolean forVpc;
@Deprecated
@Parameter(name = ApiConstants.FOR_NSX,
type = CommandType.BOOLEAN,
description = "true if network offering is meant to be used for NSX, false otherwise.",
since = "4.20.0")
private Boolean forNsx;
@Parameter(name = ApiConstants.PROVIDER,
type = CommandType.STRING,
description = "Name of the provider providing the service",
since = "4.21.0")
private String provider;
@Parameter(name = ApiConstants.NSX_SUPPORT_LB,
type = CommandType.BOOLEAN,
description = "True if network offering for NSX network offering supports Load balancer service.",
since = "4.20.0")
private Boolean nsxSupportsLbService;
@Parameter(name = ApiConstants.NSX_SUPPORTS_INTERNAL_LB,
type = CommandType.BOOLEAN,
description = "True if network offering for NSX network offering supports Internal Load balancer service.",
since = "4.20.0")
private Boolean nsxSupportsInternalLbService;
@Parameter(name = ApiConstants.NETWORK_MODE,
type = CommandType.STRING,
description = "Indicates the mode with which the network will operate. Valid option: NATTED or ROUTED",
since = "4.20.0")
private String networkMode;
@Parameter(name = ApiConstants.FOR_TUNGSTEN,
type = CommandType.BOOLEAN,
description = "True if network offering is meant to be used for Tungsten-Fabric, false otherwise.")
private Boolean forTungsten;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.2.0", description = "Network offering details in key/value pairs."
+ " Supported keys are internallbprovider/publiclbprovider with service provider as a value, and"
+ " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup")
protected Map details;
@Parameter(name = ApiConstants.EGRESS_DEFAULT_POLICY,
type = CommandType.BOOLEAN,
description = "True if guest network default egress policy is allow; false if default egress policy is deny")
private Boolean egressDefaultPolicy;
@Parameter(name = ApiConstants.KEEPALIVE_ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "If true keepalive will be turned on in the loadbalancer. At the time of writing this has only an effect on haproxy; the mode http and httpclose options are unset in the haproxy conf file.")
private Boolean keepAliveEnabled;
@Parameter(name = ApiConstants.MAX_CONNECTIONS,
type = CommandType.INTEGER,
description = "Maximum number of concurrent connections supported by the Network offering")
private Integer maxConnections;
@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;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = ZoneResponse.class,
description = "The ID of the containing zone(s), null for public offerings",
since = "4.13")
private List<Long> zoneIds;
@Parameter(name = ApiConstants.ENABLE,
type = CommandType.BOOLEAN,
description = "Set to true if the offering is to be enabled during creation. Default is false",
since = "4.16")
private Boolean enable;
@Parameter(name = ApiConstants.SPECIFY_AS_NUMBER, type = CommandType.BOOLEAN, since = "4.20.0",
description = "true if network offering supports choosing AS number")
private Boolean specifyAsNumber;
@Parameter(name = ApiConstants.ROUTING_MODE,
type = CommandType.STRING,
since = "4.20.0",
description = "the routing mode for the network offering. Supported types are: Static or Dynamic.")
private String routingMode;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getNetworkOfferingName() {
return networkOfferingName;
}
public String getDisplayText() {
return StringUtils.isEmpty(displayText) ? networkOfferingName : displayText;
}
public String getTags() {
return tags;
}
public Boolean getSpecifyVlan() {
return specifyVlan == null ? false : specifyVlan;
}
public String getAvailability() {
return availability == null ? NetworkOffering.Availability.Optional.toString() : availability;
}
public Integer getNetworkRate() {
return networkRate;
}
public Long getServiceOfferingId() {
return serviceOfferingId;
}
public boolean isExternalNetworkProvider() {
return Arrays.asList("NSX", "Netris").stream()
.anyMatch(s -> provider != null && s.equalsIgnoreCase(provider));
}
public boolean isForNsx() {
return provider != null && provider.equalsIgnoreCase("NSX");
}
public boolean isForNetris() {
return provider != null && provider.equalsIgnoreCase("Netris");
}
public String getProvider() {
return provider;
}
public List<String> getSupportedServices() {
if (!isExternalNetworkProvider()) {
return supportedServices == null ? new ArrayList<String>() : supportedServices;
} else {
List<String> services = new ArrayList<>(List.of(
Dhcp.getName(),
Dns.getName(),
UserData.getName()
));
if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
services.addAll(Arrays.asList(
StaticNat.getName(),
SourceNat.getName(),
PortForwarding.getName()));
}
if (getNsxSupportsLbService() || (provider != null && isNetrisNatted(getProvider(), getNetworkMode()))) {
services.add(Lb.getName());
}
if (Boolean.TRUE.equals(forVpc)) {
services.add(NetworkACL.getName());
} else {
services.add(Firewall.getName());
}
return services;
}
}
public String getInternetProtocol() {
return internetProtocol;
}
public Boolean getSpecifyIpRanges() {
return specifyIpRanges == null ? false : specifyIpRanges;
}
public Boolean getConserveMode() {
if (conserveMode == null) {
return true;
}
return conserveMode;
}
public Boolean getIsPersistent() {
return isPersistent == null ? false : isPersistent;
}
public Boolean getForVpc() {
return forVpc;
}
public String getNetworkMode() {
return networkMode;
}
public boolean getNsxSupportsLbService() {
return BooleanUtils.isTrue(nsxSupportsLbService);
}
public boolean getNsxSupportsInternalLbService() {
return BooleanUtils.isTrue(nsxSupportsInternalLbService);
}
public Boolean getForTungsten() {
return forTungsten;
}
public Boolean getEgressDefaultPolicy() {
if (egressDefaultPolicy == null) {
return true;
}
return egressDefaultPolicy;
}
public Boolean getKeepAliveEnabled() {
return keepAliveEnabled;
}
public Integer getMaxconnections() {
return maxConnections;
}
public Map<String, List<String>> getServiceProviders() {
Map<String, List<String>> serviceProviderMap = new HashMap<>();
if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) {
Collection servicesCollection = serviceProviderList.values();
Iterator iter = servicesCollection.iterator();
while (iter.hasNext()) {
HashMap<String, String> services = (HashMap<String, String>) iter.next();
String service = services.get("service");
String provider = services.get("provider");
List<String> providerList = null;
if (serviceProviderMap.containsKey(service)) {
providerList = serviceProviderMap.get(service);
} else {
providerList = new ArrayList<String>();
}
providerList.add(provider);
serviceProviderMap.put(service, providerList);
}
} else if (isExternalNetworkProvider()) {
getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName());
}
return serviceProviderMap;
}
private void getServiceProviderMapForExternalProvider(Map<String, List<String>> serviceProviderMap, String provider) {
String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() :
VirtualRouterProvider.Type.VirtualRouter.name();
List<String> unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService"));
List<String> routerSupported = List.of("Dhcp", "Dns", "UserData");
List<String> allServices = Network.Service.listAllServices().stream().map(Network.Service::getName).collect(Collectors.toList());
if (routerProvider.equals(VirtualRouterProvider.Type.VPCVirtualRouter.name())) {
unsupportedServices.add("Firewall");
} else {
unsupportedServices.add("NetworkACL");
}
for (String service : allServices) {
if (unsupportedServices.contains(service))
continue;
if (routerSupported.contains(service))
serviceProviderMap.put(service, List.of(routerProvider));
else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || NetworkACL.getName().equalsIgnoreCase(service)) {
serviceProviderMap.put(service, List.of(provider));
}
if (isNsxWithoutLb(getProvider(), getNsxSupportsLbService()) || isNetrisRouted(getProvider(), getNetworkMode())) {
serviceProviderMap.remove(Lb.getName());
}
}
}
public Map<Network.Capability, String> getServiceCapabilities(Network.Service service) {
Map<Network.Capability, String> capabilityMap = null;
if (serviceCapabilitiesList != null && !serviceCapabilitiesList.isEmpty()) {
capabilityMap = new HashMap<Network.Capability, String>();
Collection serviceCapabilityCollection = serviceCapabilitiesList.values();
Iterator iter = serviceCapabilityCollection.iterator();
while (iter.hasNext()) {
HashMap<String, String> svcCapabilityMap = (HashMap<String, String>) iter.next();
Network.Capability capability = null;
String svc = svcCapabilityMap.get("service");
String capabilityName = svcCapabilityMap.get("capabilitytype");
String capabilityValue = svcCapabilityMap.get("capabilityvalue");
if (capabilityName != null) {
capability = Network.Capability.getCapability(capabilityName);
}
if ((capability == null) || (capabilityName == null) || (capabilityValue == null)) {
throw new InvalidParameterValueException("Invalid capability:" + capabilityName + " capability value:" + capabilityValue);
}
if (svc.equalsIgnoreCase(service.getName())) {
capabilityMap.put(capability, capabilityValue);
} else {
//throw new InvalidParameterValueException("Service is not equal ")
}
}
}
return capabilityMap;
}
public Map<String, String> getDetails() {
if (details == null || details.isEmpty()) {
return null;
}
Collection paramsCollection = details.values();
Object objlist[] = paramsCollection.toArray();
Map<String, String> params = (Map<String, String>) (objlist[0]);
for (int i = 1; i < objlist.length; i++) {
params.putAll((Map<String, String>) (objlist[i]));
}
return params;
}
public String getServicePackageId() {
Map<String, String> data = getDetails();
if (data == null)
return null;
return data.get(NetworkOffering.Detail.servicepackageuuid + "");
}
public List<Long> getDomainIds() {
if (CollectionUtils.isNotEmpty(domainIds)) {
Set<Long> set = new LinkedHashSet<>(domainIds);
domainIds.clear();
domainIds.addAll(set);
}
return domainIds;
}
public List<Long> getZoneIds() {
if (CollectionUtils.isNotEmpty(zoneIds)) {
Set<Long> set = new LinkedHashSet<>(zoneIds);
zoneIds.clear();
zoneIds.addAll(set);
}
return zoneIds;
}
public Boolean getEnable() {
if (enable != null) {
return enable;
}
return false;
}
public boolean getSpecifyAsNumber() {
return BooleanUtils.toBoolean(specifyAsNumber);
}
public String getRoutingMode() {
return routingMode;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
}

View File

@ -0,0 +1,73 @@
// 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.admin.offering;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
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.response.DiskOfferingResponse;
import com.cloud.offering.DiskOffering;
@APICommand(name = "cloneDiskOffering",
description = "Clones a disk offering. All parameters from createDiskOffering are available. If not specified, values will be copied from the source offering.",
responseObject = DiskOfferingResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.DomainAdmin})
public class CloneDiskOfferingCmd extends CreateDiskOfferingCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.SOURCE_OFFERING_ID,
type = BaseCmd.CommandType.UUID,
entityType = DiskOfferingResponse.class,
required = true,
description = "The ID of the source disk offering to clone from")
private Long sourceOfferingId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getSourceOfferingId() {
return sourceOfferingId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
DiskOffering result = _configService.cloneDiskOffering(this);
if (result != null) {
DiskOfferingResponse response = _responseGenerator.createDiskOfferingResponse(result);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone disk offering");
}
}
}

View File

@ -0,0 +1,79 @@
// 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.admin.offering;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import com.cloud.offering.ServiceOffering;
@APICommand(name = "cloneServiceOffering",
description = "Clones a service offering. All parameters from createServiceOffering are available. If not specified, values will be copied from the source offering.",
responseObject = ServiceOfferingResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.DomainAdmin})
public class CloneServiceOfferingCmd extends CreateServiceOfferingCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.SOURCE_OFFERING_ID,
type = CommandType.UUID,
entityType = ServiceOfferingResponse.class,
required = true,
description = "The ID of the source service offering to clone from")
private Long sourceOfferingId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getSourceOfferingId() {
return sourceOfferingId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
ServiceOffering result = _configService.cloneServiceOffering(this);
if (result != null) {
ServiceOfferingResponse response = _responseGenerator.createServiceOfferingResponse(result);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone service offering");
}
} catch (com.cloud.exception.InvalidParameterValueException e) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage());
} catch (com.cloud.utils.exception.CloudRuntimeException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
}
}
}

View File

@ -0,0 +1,109 @@
// 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.admin.vpc;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.network.vpc.VpcOffering;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
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.response.VpcOfferingResponse;
import java.util.List;
@APICommand(name = "cloneVPCOffering",
description = "Clones an existing VPC offering. All parameters are copied from the source offering unless explicitly overridden. " +
"Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.",
responseObject = VpcOfferingResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.23.0")
public class CloneVPCOfferingCmd extends CreateVPCOfferingCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.SOURCE_OFFERING_ID,
type = BaseCmd.CommandType.UUID,
entityType = VpcOfferingResponse.class,
required = true,
description = "The ID of the source VPC offering to clone from")
private Long sourceOfferingId;
@Parameter(name = "addservices",
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services to add to the cloned offering (in addition to source offering services). " +
"If specified along with 'supportedservices', this parameter is ignored.")
private List<String> addServices;
@Parameter(name = "dropservices",
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "Services to remove from the cloned offering (that exist in source offering). " +
"If specified along with 'supportedservices', this parameter is ignored.")
private List<String> dropServices;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getSourceOfferingId() {
return sourceOfferingId;
}
public List<String> getAddServices() {
return addServices;
}
public List<String> getDropServices() {
return dropServices;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void create() throws ResourceAllocationException {
// Set a temporary entity ID (source offering ID) to prevent NullPointerException
// in ApiServer.queueCommand(). This will be updated in execute() with the actual
// cloned offering ID.
if (sourceOfferingId != null) {
setEntityId(sourceOfferingId);
}
}
@Override
public void execute() {
VpcOffering result = _vpcProvSvc.cloneVPCOffering(this);
if (result != null) {
setEntityId(result.getId());
setEntityUuid(result.getUuid());
VpcOfferingResponse response = _responseGenerator.createVpcOfferingResponse(result);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone VPC offering");
}
}
}

View File

@ -28,7 +28,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Network;
import com.cloud.network.VirtualRouterProvider;
import com.cloud.offering.NetworkOffering;
@ -185,9 +184,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
}
public List<String> getSupportedServices() {
if (!isExternalNetworkProvider() && CollectionUtils.isEmpty(supportedServices)) {
throw new InvalidParameterValueException("Supported services needs to be provided");
}
// For external network providers, auto-populate services based on network mode
if (isExternalNetworkProvider()) {
supportedServices = new ArrayList<>(List.of(
Dhcp.getName(),

View File

@ -22,6 +22,7 @@ import java.util.Map;
import com.cloud.capacity.Capacity;
import com.cloud.exception.ResourceAllocationException;
import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd;
@ -140,6 +141,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer
List<Long> getBackupOfferingDomains(final Long offeringId);
/**
* Clone an existing backup offering with updated values
* @param cmd clone backup offering cmd
*/
BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd);
/**
* List backup offerings
* @param ListBackupOfferingsCmd API cmd

View File

@ -0,0 +1,301 @@
// 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.admin.backup;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CloneBackupOfferingCmdTest {
private CloneBackupOfferingCmd cloneBackupOfferingCmd;
@Mock
private BackupManager backupManager;
@Mock
private ResponseGenerator responseGenerator;
@Mock
private BackupOffering mockBackupOffering;
@Mock
private BackupOfferingResponse mockBackupOfferingResponse;
@Before
public void setUp() {
cloneBackupOfferingCmd = new CloneBackupOfferingCmd();
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "backupManager", backupManager);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "_responseGenerator", responseGenerator);
}
@Test
public void testGetSourceOfferingId() {
Long sourceOfferingId = 999L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId);
assertEquals(sourceOfferingId, cloneBackupOfferingCmd.getSourceOfferingId());
}
@Test
public void testGetName() {
String name = "ClonedBackupOffering";
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", name);
assertEquals(name, cloneBackupOfferingCmd.getName());
}
@Test
public void testGetDescription() {
String description = "Cloned Backup Offering Description";
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", description);
assertEquals(description, cloneBackupOfferingCmd.getDescription());
}
@Test
public void testGetZoneId() {
Long zoneId = 123L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", zoneId);
assertEquals(zoneId, cloneBackupOfferingCmd.getZoneId());
}
@Test
public void testGetExternalId() {
String externalId = "external-backup-123";
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", externalId);
assertEquals(externalId, cloneBackupOfferingCmd.getExternalId());
}
@Test
public void testGetAllowUserDrivenBackups() {
Boolean allowUserDrivenBackups = true;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", allowUserDrivenBackups);
assertEquals(allowUserDrivenBackups, cloneBackupOfferingCmd.getUserDrivenBackups());
}
@Test
public void testAllowUserDrivenBackupsDefaultTrue() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", null);
Boolean result = cloneBackupOfferingCmd.getUserDrivenBackups();
assertTrue(result == null || result);
}
@Test
public void testAllowUserDrivenBackupsFalse() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false);
assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups());
}
@Test
public void testExecuteSuccess() throws Exception {
Long sourceOfferingId = 999L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering);
when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse);
cloneBackupOfferingCmd.execute();
assertNotNull(cloneBackupOfferingCmd.getResponseObject());
assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject());
}
@Test
public void testExecuteFailure() throws Exception {
Long sourceOfferingId = 999L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(null);
try {
cloneBackupOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Failed to clone backup offering", e.getMessage());
}
}
@Test
public void testExecuteWithInvalidParameterException() throws Exception {
Long sourceOfferingId = 999L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class)))
.thenThrow(new InvalidParameterValueException("Invalid source offering ID"));
try {
cloneBackupOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
assertEquals("Invalid source offering ID", e.getMessage());
}
}
@Test
public void testExecuteWithCloudRuntimeException() throws Exception {
Long sourceOfferingId = 999L;
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class)))
.thenThrow(new CloudRuntimeException("Runtime error during clone"));
try {
cloneBackupOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Runtime error during clone", e.getMessage());
}
}
@Test
public void testExecuteSuccessWithAllParameters() throws Exception {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Test Description");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "ext-123");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true);
when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering);
when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse);
cloneBackupOfferingCmd.execute();
assertNotNull(cloneBackupOfferingCmd.getResponseObject());
assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject());
}
@Test
public void testCloneWithAllParameters() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Cloned backup offering for testing");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "external-backup-123");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true);
assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId());
assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName());
assertEquals("Cloned backup offering for testing", cloneBackupOfferingCmd.getDescription());
assertEquals(Long.valueOf(123L), cloneBackupOfferingCmd.getZoneId());
assertEquals("external-backup-123", cloneBackupOfferingCmd.getExternalId());
assertEquals(Boolean.TRUE, cloneBackupOfferingCmd.getUserDrivenBackups());
}
@Test
public void testCloneWithMinimalParameters() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Description");
assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId());
assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName());
assertEquals("Description", cloneBackupOfferingCmd.getDescription());
assertNull(cloneBackupOfferingCmd.getZoneId());
assertNull(cloneBackupOfferingCmd.getExternalId());
}
@Test
public void testSourceOfferingIdNullByDefault() {
assertNull(cloneBackupOfferingCmd.getSourceOfferingId());
}
@Test
public void testNameNullByDefault() {
assertNull(cloneBackupOfferingCmd.getName());
}
@Test
public void testDescriptionNullByDefault() {
assertNull(cloneBackupOfferingCmd.getDescription());
}
@Test
public void testZoneIdNullByDefault() {
assertNull(cloneBackupOfferingCmd.getZoneId());
}
@Test
public void testExternalIdNullByDefault() {
assertNull(cloneBackupOfferingCmd.getExternalId());
}
@Test
public void testCloneBackupOfferingInheritingZone() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited zone");
assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId());
assertNull(cloneBackupOfferingCmd.getZoneId());
}
@Test
public void testCloneBackupOfferingInheritingExternalId() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited external ID");
assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId());
assertNull(cloneBackupOfferingCmd.getExternalId());
}
@Test
public void testCloneBackupOfferingOverridingZone() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with new zone");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 456L);
assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId());
assertEquals(Long.valueOf(456L), cloneBackupOfferingCmd.getZoneId());
}
@Test
public void testCloneBackupOfferingDisallowUserDrivenBackups() {
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L);
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone without user-driven backups");
ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false);
assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups());
}
}

View File

@ -0,0 +1,324 @@
// 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.admin.network;
import com.cloud.offering.NetworkOffering;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.NetworkOfferingResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CloneNetworkOfferingCmdTest {
private CloneNetworkOfferingCmd cloneNetworkOfferingCmd;
@Mock
private com.cloud.configuration.ConfigurationService configService;
@Mock
private ResponseGenerator responseGenerator;
@Mock
private NetworkOffering mockNetworkOffering;
@Mock
private NetworkOfferingResponse mockNetworkOfferingResponse;
@Before
public void setUp() {
cloneNetworkOfferingCmd = new CloneNetworkOfferingCmd();
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_configService", configService);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_responseGenerator", responseGenerator);
}
@Test
public void testGetSourceOfferingId() {
Long sourceOfferingId = 123L;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId);
assertEquals(sourceOfferingId, cloneNetworkOfferingCmd.getSourceOfferingId());
}
@Test
public void testGetAddServices() {
List<String> addServices = Arrays.asList("Dhcp", "Dns");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices);
assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices());
}
@Test
public void testGetDropServices() {
List<String> dropServices = Arrays.asList("Firewall", "Vpn");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices);
assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices());
}
@Test
public void testGetGuestIpType() {
String guestIpType = "Isolated";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", guestIpType);
assertEquals(guestIpType, cloneNetworkOfferingCmd.getGuestIpType());
}
@Test
public void testGetTraffictype() {
String trafficType = "GUEST";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", trafficType);
assertEquals(trafficType, cloneNetworkOfferingCmd.getTraffictype());
}
@Test
public void testGetName() {
String name = "ClonedNetworkOffering";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name);
assertEquals(name, cloneNetworkOfferingCmd.getNetworkOfferingName());
}
@Test
public void testGetDisplayText() {
String displayText = "Cloned Network Offering Display Text";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", displayText);
assertEquals(displayText, cloneNetworkOfferingCmd.getDisplayText());
}
@Test
public void testGetDisplayTextDefaultsToName() {
String name = "ClonedNetworkOffering";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", null);
assertEquals(name, cloneNetworkOfferingCmd.getDisplayText());
}
@Test
public void testGetAvailability() {
String availability = "Required";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", availability);
assertEquals(availability, cloneNetworkOfferingCmd.getAvailability());
}
@Test
public void testGetTags() {
String tags = "tag1,tag2,tag3";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "tags", tags);
assertEquals(tags, cloneNetworkOfferingCmd.getTags());
}
@Test
public void testExecuteSuccess() {
Long sourceOfferingId = 123L;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(mockNetworkOffering);
when(responseGenerator.createNetworkOfferingResponse(mockNetworkOffering)).thenReturn(mockNetworkOfferingResponse);
cloneNetworkOfferingCmd.execute();
assertNotNull(cloneNetworkOfferingCmd.getResponseObject());
assertEquals(mockNetworkOfferingResponse, cloneNetworkOfferingCmd.getResponseObject());
}
@Test(expected = ServerApiException.class)
public void testExecuteFailure() {
Long sourceOfferingId = 123L;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(null);
try {
cloneNetworkOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Failed to clone network offering", e.getMessage());
throw e;
}
}
@Test
public void testGetConserveMode() {
Boolean conserveMode = true;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", conserveMode);
assertEquals(conserveMode, cloneNetworkOfferingCmd.getConserveMode());
}
@Test
public void testGetSpecifyVlan() {
Boolean specifyVlan = false;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", specifyVlan);
assertEquals(specifyVlan, cloneNetworkOfferingCmd.getSpecifyVlan());
}
@Test
public void testGetSpecifyIpRanges() {
Boolean specifyIpRanges = true;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyIpRanges", specifyIpRanges);
assertEquals(specifyIpRanges, cloneNetworkOfferingCmd.getSpecifyIpRanges());
}
@Test
public void testGetIsPersistent() {
Boolean isPersistent = true;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", isPersistent);
assertEquals(isPersistent, cloneNetworkOfferingCmd.getIsPersistent());
}
@Test
public void testGetEgressDefaultPolicy() {
Boolean egressDefaultPolicy = false;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", egressDefaultPolicy);
assertEquals(egressDefaultPolicy, cloneNetworkOfferingCmd.getEgressDefaultPolicy());
}
@Test
public void testGetServiceOfferingId() {
Long serviceOfferingId = 456L;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", serviceOfferingId);
assertEquals(serviceOfferingId, cloneNetworkOfferingCmd.getServiceOfferingId());
}
@Test
public void testGetForVpc() {
Boolean forVpc = true;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "forVpc", forVpc);
assertEquals(forVpc, cloneNetworkOfferingCmd.getForVpc());
}
@Test
public void testGetMaxConnections() {
Integer maxConnections = 1000;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "maxConnections", maxConnections);
assertEquals(maxConnections, cloneNetworkOfferingCmd.getMaxconnections());
}
@Test
public void testGetNetworkRate() {
Integer networkRate = 200;
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", networkRate);
assertEquals(networkRate, cloneNetworkOfferingCmd.getNetworkRate());
}
@Test
public void testGetInternetProtocol() {
String internetProtocol = "ipv4";
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "internetProtocol", internetProtocol);
assertEquals(internetProtocol, cloneNetworkOfferingCmd.getInternetProtocol());
}
@Test
public void testAddServicesNullByDefault() {
assertNull(cloneNetworkOfferingCmd.getAddServices());
}
@Test
public void testDropServicesNullByDefault() {
assertNull(cloneNetworkOfferingCmd.getDropServices());
}
@Test
public void testSupportedServicesParameter() {
List<String> supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "supportedServices", supportedServices);
assertEquals(supportedServices, cloneNetworkOfferingCmd.getSupportedServices());
}
@Test
public void testServiceProviderListParameter() {
Map<String, HashMap<String, String>> serviceProviderList = new HashMap<>();
HashMap<String, String> dhcpProvider = new HashMap<>();
dhcpProvider.put("service", "Dhcp");
dhcpProvider.put("provider", "VirtualRouter");
HashMap<String, String> dnsProvider = new HashMap<>();
dnsProvider.put("service", "Dns");
dnsProvider.put("provider", "VirtualRouter");
serviceProviderList.put("0", dhcpProvider);
serviceProviderList.put("1", dnsProvider);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceProviderList", serviceProviderList);
Map<String, List<String>> result = cloneNetworkOfferingCmd.getServiceProviders();
assertNotNull(result);
assertEquals(2, result.size());
assertNotNull(result.get("Dhcp"));
assertNotNull(result.get("Dns"));
assertEquals("VirtualRouter", result.get("Dhcp").get(0));
assertEquals("VirtualRouter", result.get("Dns").get(0));
}
@Test
public void testCloneWithAllParameters() {
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", "ClonedOffering");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", "Cloned Offering Display");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", "Optional");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", "Isolated");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", "GUEST");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", true);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", false);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", true);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", false);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", 200);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", 456L);
assertEquals(Long.valueOf(123L), cloneNetworkOfferingCmd.getSourceOfferingId());
assertEquals("ClonedOffering", cloneNetworkOfferingCmd.getNetworkOfferingName());
assertEquals("Cloned Offering Display", cloneNetworkOfferingCmd.getDisplayText());
assertEquals("Optional", cloneNetworkOfferingCmd.getAvailability());
assertEquals("Isolated", cloneNetworkOfferingCmd.getGuestIpType());
assertEquals("GUEST", cloneNetworkOfferingCmd.getTraffictype());
assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getConserveMode());
assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getSpecifyVlan());
assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getIsPersistent());
assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getEgressDefaultPolicy());
assertEquals(Integer.valueOf(200), cloneNetworkOfferingCmd.getNetworkRate());
assertEquals(Long.valueOf(456L), cloneNetworkOfferingCmd.getServiceOfferingId());
}
@Test
public void testCloneWithAddAndDropServices() {
List<String> addServices = Arrays.asList("StaticNat", "PortForwarding");
List<String> dropServices = Arrays.asList("Vpn");
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices);
ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices);
assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices());
assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices());
}
}

View File

@ -0,0 +1,669 @@
// 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.admin.offering;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.offering.ServiceOffering;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CloneServiceOfferingCmdTest {
private CloneServiceOfferingCmd cloneServiceOfferingCmd;
@Mock
private com.cloud.configuration.ConfigurationService configService;
@Mock
private ResponseGenerator responseGenerator;
@Mock
private ServiceOffering mockServiceOffering;
@Mock
private ServiceOfferingResponse mockServiceOfferingResponse;
@Before
public void setUp() {
cloneServiceOfferingCmd = new CloneServiceOfferingCmd();
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_configService", configService);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_responseGenerator", responseGenerator);
}
@Test
public void testGetSourceOfferingId() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId());
}
@Test
public void testGetServiceOfferingName() {
String name = "ClonedServiceOffering";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name);
assertEquals(name, cloneServiceOfferingCmd.getServiceOfferingName());
}
@Test
public void testGetDisplayText() {
String displayText = "Cloned Service Offering Display Text";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", displayText);
assertEquals(displayText, cloneServiceOfferingCmd.getDisplayText());
}
@Test
public void testGetDisplayTextDefaultsToName() {
String name = "ClonedServiceOffering";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", null);
assertEquals(name, cloneServiceOfferingCmd.getDisplayText());
}
@Test
public void testGetCpu() {
Integer cpu = 4;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", cpu);
assertEquals(cpu, cloneServiceOfferingCmd.getCpuNumber());
}
@Test
public void testGetMemory() {
Integer memory = 8192;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", memory);
assertEquals(memory, cloneServiceOfferingCmd.getMemory());
}
@Test
public void testGetCpuSpeed() {
Integer cpuSpeed = 2000;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", cpuSpeed);
assertEquals(cpuSpeed, cloneServiceOfferingCmd.getCpuSpeed());
}
@Test
public void testGetOfferHa() {
Boolean offerHa = true;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", offerHa);
assertEquals(offerHa, cloneServiceOfferingCmd.isOfferHa());
}
@Test
public void testGetLimitCpuUse() {
Boolean limitCpuUse = false;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", limitCpuUse);
assertEquals(limitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse());
}
@Test
public void testGetVolatileVm() {
Boolean volatileVm = true;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", volatileVm);
assertEquals(volatileVm, cloneServiceOfferingCmd.isVolatileVm());
}
@Test
public void testGetStorageType() {
String storageType = "local";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", storageType);
assertEquals(storageType, cloneServiceOfferingCmd.getStorageType());
}
@Test
public void testGetTags() {
String tags = "ssd,premium,dedicated";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", tags);
assertEquals(tags, cloneServiceOfferingCmd.getTags());
}
@Test
public void testGetHostTag() {
String hostTag = "gpu-enabled";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", hostTag);
assertEquals(hostTag, cloneServiceOfferingCmd.getHostTag());
}
@Test
public void testGetNetworkRate() {
Integer networkRate = 1000;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", networkRate);
assertEquals(networkRate, cloneServiceOfferingCmd.getNetworkRate());
}
@Test
public void testGetDeploymentPlanner() {
String deploymentPlanner = "UserDispersingPlanner";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", deploymentPlanner);
assertEquals(deploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner());
}
@Test
public void testGetDetails() {
Map<String, HashMap<String, String>> details = new HashMap<>();
HashMap<String, String> cpuOvercommit = new HashMap<>();
cpuOvercommit.put("key", "cpuOvercommitRatio");
cpuOvercommit.put("value", "2.0");
HashMap<String, String> memoryOvercommit = new HashMap<>();
memoryOvercommit.put("key", "memoryOvercommitRatio");
memoryOvercommit.put("value", "1.5");
details.put("0", cpuOvercommit);
details.put("1", memoryOvercommit);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details);
Map<String, String> result = cloneServiceOfferingCmd.getDetails();
assertNotNull(result);
assertEquals("2.0", result.get("cpuOvercommitRatio"));
assertEquals("1.5", result.get("memoryOvercommitRatio"));
}
@Test
public void testIsPurgeResources() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true);
assertTrue(cloneServiceOfferingCmd.isPurgeResources());
}
@Test
public void testIsPurgeResourcesFalse() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", false);
assertFalse(cloneServiceOfferingCmd.isPurgeResources());
}
@Test
public void testIsPurgeResourcesDefaultFalse() {
assertFalse(cloneServiceOfferingCmd.isPurgeResources());
}
@Test
public void testGetLeaseDuration() {
Integer leaseDuration = 3600;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration);
assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration());
}
@Test
public void testGetLeaseExpiryAction() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "stop");
assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction());
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "DESTROY");
assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction());
}
@Test(expected = InvalidParameterValueException.class)
public void testGetLeaseExpiryActionInvalidValue() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "InvalidAction");
cloneServiceOfferingCmd.getLeaseExpiryAction();
}
@Test
public void testGetVgpuProfileId() {
Long vgpuProfileId = 10L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId);
assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId());
}
@Test
public void testGetGpuCount() {
Integer gpuCount = 2;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount);
assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount());
}
@Test
public void testGetGpuDisplay() {
Boolean gpuDisplay = true;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay);
assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay());
}
@Test
public void testExecuteSuccess() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteFailure() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(null);
try {
cloneServiceOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Failed to clone service offering", e.getMessage());
}
}
@Test
public void testExecuteSuccessWithAllParameters() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedOffering");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Test Display");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithInvalidParameterException() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class)))
.thenThrow(new InvalidParameterValueException("Invalid source offering ID"));
try {
cloneServiceOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
assertEquals("Invalid source offering ID", e.getMessage());
}
}
@Test
public void testExecuteWithCloudRuntimeException() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class)))
.thenThrow(new com.cloud.utils.exception.CloudRuntimeException("Runtime error during clone"));
try {
cloneServiceOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Runtime error during clone", e.getMessage());
}
}
@Test
public void testExecuteResponseNameIsSet() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
// Verify that response name would be set (actual verification would require accessing the response object's internal state)
}
@Test
public void testCloneWithAllParameters() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedServiceOffering");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Cloned Service Offering");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", true);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", false);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", true);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", "local");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", "premium");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", "gpu-enabled");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", 1000);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", "UserDispersingPlanner");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true);
assertEquals(Long.valueOf(555L), cloneServiceOfferingCmd.getSourceOfferingId());
assertEquals("ClonedServiceOffering", cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals("Cloned Service Offering", cloneServiceOfferingCmd.getDisplayText());
assertEquals(Integer.valueOf(4), cloneServiceOfferingCmd.getCpuNumber());
assertEquals(Integer.valueOf(8192), cloneServiceOfferingCmd.getMemory());
assertEquals(Integer.valueOf(2000), cloneServiceOfferingCmd.getCpuSpeed());
assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isOfferHa());
assertEquals(Boolean.FALSE, cloneServiceOfferingCmd.isLimitCpuUse());
assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isVolatileVm());
assertEquals("local", cloneServiceOfferingCmd.getStorageType());
assertEquals("premium", cloneServiceOfferingCmd.getTags());
assertEquals("gpu-enabled", cloneServiceOfferingCmd.getHostTag());
assertEquals(Integer.valueOf(1000), cloneServiceOfferingCmd.getNetworkRate());
assertEquals("UserDispersingPlanner", cloneServiceOfferingCmd.getDeploymentPlanner());
assertTrue(cloneServiceOfferingCmd.isPurgeResources());
}
@Test
public void testSourceOfferingIdNullByDefault() {
assertNull(cloneServiceOfferingCmd.getSourceOfferingId());
}
@Test
public void testGetSystemVmType() {
String systemVmType = "domainrouter";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "systemVmType", systemVmType);
assertEquals(systemVmType, cloneServiceOfferingCmd.getSystemVmType());
}
@Test
public void testGetBytesReadRate() {
Long bytesReadRate = 1000000L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate);
assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate());
}
@Test
public void testGetBytesWriteRate() {
Long bytesWriteRate = 1000000L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate);
assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate());
}
@Test
public void testGetIopsReadRate() {
Long iopsReadRate = 1000L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate);
assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate());
}
@Test
public void testGetIopsWriteRate() {
Long iopsWriteRate = 1000L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate);
assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate());
}
@Test
public void testCloneServiceOfferingWithGpuProfile() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "GPU-Offering-Clone");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", 10L);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", 2);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", true);
assertEquals(Long.valueOf(10L), cloneServiceOfferingCmd.getVgpuProfileId());
assertEquals(Integer.valueOf(2), cloneServiceOfferingCmd.getGpuCount());
assertTrue(cloneServiceOfferingCmd.getGpuDisplay());
}
@Test
public void testCloneServiceOfferingWithLease() {
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "Lease-Offering-Clone");
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", 7200);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "destroy");
assertEquals(Integer.valueOf(7200), cloneServiceOfferingCmd.getLeaseDuration());
assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction());
}
@Test
public void testExecuteWithOverriddenParameters() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "ClonedOffering-Override";
String newDisplayText = "Overridden Display Text";
Integer newCpu = 8;
Integer newMemory = 16384;
Integer newCpuSpeed = 3000;
Boolean newOfferHa = true;
Boolean newLimitCpuUse = true;
String newStorageType = "shared";
String newTags = "premium,gpu";
String newHostTag = "compute-optimized";
Integer newNetworkRate = 2000;
String newDeploymentPlanner = "FirstFitPlanner";
Boolean newPurgeResources = true;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", newDisplayText);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", newCpuSpeed);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", newOfferHa);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", newLimitCpuUse);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", newStorageType);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", newTags);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", newHostTag);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", newNetworkRate);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", newDeploymentPlanner);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", newPurgeResources);
assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId());
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals(newDisplayText, cloneServiceOfferingCmd.getDisplayText());
assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber());
assertEquals(newMemory, cloneServiceOfferingCmd.getMemory());
assertEquals(newCpuSpeed, cloneServiceOfferingCmd.getCpuSpeed());
assertEquals(newOfferHa, cloneServiceOfferingCmd.isOfferHa());
assertEquals(newLimitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse());
assertEquals(newStorageType, cloneServiceOfferingCmd.getStorageType());
assertEquals(newTags, cloneServiceOfferingCmd.getTags());
assertEquals(newHostTag, cloneServiceOfferingCmd.getHostTag());
assertEquals(newNetworkRate, cloneServiceOfferingCmd.getNetworkRate());
assertEquals(newDeploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner());
assertTrue(cloneServiceOfferingCmd.isPurgeResources());
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithPartialOverrides() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "PartialOverride";
Integer newCpu = 6;
Integer newMemory = 12288;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory);
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber());
assertEquals(newMemory, cloneServiceOfferingCmd.getMemory());
assertNull(cloneServiceOfferingCmd.getCpuSpeed());
assertFalse(cloneServiceOfferingCmd.isOfferHa());
assertNull(cloneServiceOfferingCmd.getStorageType());
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithGpuOverrides() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "GPU-Clone-Override";
Long vgpuProfileId = 15L;
Integer gpuCount = 4;
Boolean gpuDisplay = false;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay);
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId());
assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount());
assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay());
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithLeaseOverrides() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "Lease-Clone-Override";
Integer leaseDuration = 14400; // 4 hours
String leaseExpiryAction = "stop";
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", leaseExpiryAction);
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration());
assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction());
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithStorageOverrides() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "Storage-Clone-Override";
Long bytesReadRate = 2000000L;
Long bytesWriteRate = 1500000L;
Long iopsReadRate = 2000L;
Long iopsWriteRate = 1500L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate);
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate());
assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate());
assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate());
assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate());
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
@Test
public void testExecuteWithDetailsOverride() {
Long sourceOfferingId = 555L;
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId);
String newName = "Details-Clone-Override";
Map<String, HashMap<String, String>> details = new HashMap<>();
HashMap<String, String> cpuOvercommit = new HashMap<>();
cpuOvercommit.put("key", "cpuOvercommitRatio");
cpuOvercommit.put("value", "3.0");
HashMap<String, String> memoryOvercommit = new HashMap<>();
memoryOvercommit.put("key", "memoryOvercommitRatio");
memoryOvercommit.put("value", "2.5");
HashMap<String, String> customDetail = new HashMap<>();
customDetail.put("key", "customParameter");
customDetail.put("value", "customValue");
details.put("0", cpuOvercommit);
details.put("1", memoryOvercommit);
details.put("2", customDetail);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName);
ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details);
assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName());
Map<String, String> result = cloneServiceOfferingCmd.getDetails();
assertNotNull(result);
assertEquals("3.0", result.get("cpuOvercommitRatio"));
assertEquals("2.5", result.get("memoryOvercommitRatio"));
assertEquals("customValue", result.get("customParameter"));
when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering);
when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse);
cloneServiceOfferingCmd.execute();
assertNotNull(cloneServiceOfferingCmd.getResponseObject());
assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject());
}
}

View File

@ -0,0 +1,299 @@
// 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.admin.vpc;
import com.cloud.network.vpc.VpcOffering;
import com.cloud.network.vpc.VpcProvisioningService;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.VpcOfferingResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CloneVpcOfferingCmdTest {
private CloneVPCOfferingCmd cloneVpcOfferingCmd;
@Mock
private VpcProvisioningService vpcService;
@Mock
private ResponseGenerator responseGenerator;
@Mock
private VpcOffering mockVpcOffering;
@Mock
private VpcOfferingResponse mockVpcOfferingResponse;
@Before
public void setUp() {
cloneVpcOfferingCmd = new CloneVPCOfferingCmd();
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_vpcProvSvc", vpcService);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_responseGenerator", responseGenerator);
}
@Test
public void testGetSourceOfferingId() {
Long sourceOfferingId = 789L;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId);
assertEquals(sourceOfferingId, cloneVpcOfferingCmd.getSourceOfferingId());
}
@Test
public void testGetName() {
String name = "ClonedVpcOffering";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name);
assertEquals(name, cloneVpcOfferingCmd.getVpcOfferingName());
}
@Test
public void testGetDisplayText() {
String displayText = "Cloned VPC Offering Display Text";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", displayText);
assertEquals(displayText, cloneVpcOfferingCmd.getDisplayText());
}
@Test
public void testGetDisplayTextDefaultsToName() {
String name = "ClonedVpcOffering";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", null);
assertEquals(name, cloneVpcOfferingCmd.getDisplayText());
}
@Test
public void testGetServiceOfferingId() {
Long serviceOfferingId = 456L;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", serviceOfferingId);
assertEquals(serviceOfferingId, cloneVpcOfferingCmd.getServiceOfferingId());
}
@Test
public void testGetInternetProtocol() {
String internetProtocol = "dualstack";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", internetProtocol);
assertEquals(internetProtocol, cloneVpcOfferingCmd.getInternetProtocol());
}
@Test
public void testGetProvider() {
String provider = "NSX";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", provider);
assertEquals(provider, cloneVpcOfferingCmd.getProvider());
}
@Test
public void testGetNetworkMode() {
String networkMode = "ROUTED";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", networkMode);
assertEquals(networkMode, cloneVpcOfferingCmd.getNetworkMode());
}
@Test
public void testGetRoutingMode() {
String routingMode = "dynamic";
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", routingMode);
assertEquals(routingMode, cloneVpcOfferingCmd.getRoutingMode());
}
@Test
public void testGetNsxSupportLb() {
Boolean nsxSupportLb = true;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", nsxSupportLb);
assertEquals(nsxSupportLb, cloneVpcOfferingCmd.getNsxSupportsLbService());
}
@Test
public void testGetSpecifyAsnumber() {
Boolean specifyAsnumber = false;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", specifyAsnumber);
assertEquals(specifyAsnumber, cloneVpcOfferingCmd.getSpecifyAsNumber());
}
@Test
public void testExecuteSuccess() {
Long sourceOfferingId = 789L;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(mockVpcOffering);
when(responseGenerator.createVpcOfferingResponse(mockVpcOffering)).thenReturn(mockVpcOfferingResponse);
cloneVpcOfferingCmd.execute();
assertNotNull(cloneVpcOfferingCmd.getResponseObject());
assertEquals(mockVpcOfferingResponse, cloneVpcOfferingCmd.getResponseObject());
}
@Test(expected = ServerApiException.class)
public void testExecuteFailure() {
Long sourceOfferingId = 789L;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId);
when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(null);
try {
cloneVpcOfferingCmd.execute();
fail("Expected ServerApiException to be thrown");
} catch (ServerApiException e) {
assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode());
assertEquals("Failed to clone VPC offering", e.getMessage());
throw e;
}
}
@Test
public void testGetSupportedServices() {
List<String> supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat", "NetworkACL");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "supportedServices", supportedServices);
assertEquals(supportedServices, cloneVpcOfferingCmd.getSupportedServices());
}
@Test
public void testGetServiceProviders() {
Map<String, HashMap<String, String>> serviceProviderList = new HashMap<>();
HashMap<String, String> dhcpProvider = new HashMap<>();
dhcpProvider.put("service", "Dhcp");
dhcpProvider.put("provider", "VpcVirtualRouter");
HashMap<String, String> dnsProvider = new HashMap<>();
dnsProvider.put("service", "Dns");
dnsProvider.put("provider", "VpcVirtualRouter");
HashMap<String, String> aclProvider = new HashMap<>();
aclProvider.put("service", "NetworkACL");
aclProvider.put("provider", "VpcVirtualRouter");
serviceProviderList.put("0", dhcpProvider);
serviceProviderList.put("1", dnsProvider);
serviceProviderList.put("2", aclProvider);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceProviderList", serviceProviderList);
Map<String, List<String>> result = cloneVpcOfferingCmd.getServiceProviders();
assertNotNull(result);
assertEquals(3, result.size());
assertNotNull(result.get("Dhcp"));
assertNotNull(result.get("Dns"));
assertNotNull(result.get("NetworkACL"));
assertEquals("VpcVirtualRouter", result.get("Dhcp").get(0));
assertEquals("VpcVirtualRouter", result.get("Dns").get(0));
assertEquals("VpcVirtualRouter", result.get("NetworkACL").get(0));
}
@Test
public void testGetEnable() {
Boolean enable = true;
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", enable);
assertEquals(enable, cloneVpcOfferingCmd.getEnable());
}
@Test
public void testCloneWithAllParameters() {
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", "ClonedVpcOffering");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", "Cloned VPC Offering");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", 456L);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", "ipv4");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "static");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", false);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", true);
assertEquals(Long.valueOf(789L), cloneVpcOfferingCmd.getSourceOfferingId());
assertEquals("ClonedVpcOffering", cloneVpcOfferingCmd.getVpcOfferingName());
assertEquals("Cloned VPC Offering", cloneVpcOfferingCmd.getDisplayText());
assertEquals(Long.valueOf(456L), cloneVpcOfferingCmd.getServiceOfferingId());
assertEquals("ipv4", cloneVpcOfferingCmd.getInternetProtocol());
assertEquals("NSX", cloneVpcOfferingCmd.getProvider());
assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode());
assertEquals("static", cloneVpcOfferingCmd.getRoutingMode());
assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService());
assertEquals(Boolean.FALSE, cloneVpcOfferingCmd.getSpecifyAsNumber());
assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getEnable());
}
@Test
public void testSourceOfferingIdNullByDefault() {
assertNull(cloneVpcOfferingCmd.getSourceOfferingId());
}
@Test
public void testProviderNullByDefault() {
assertNull(cloneVpcOfferingCmd.getProvider());
}
@Test
public void testServiceCapabilityList() {
Map<String, List<String>> serviceCapabilityList = new HashMap<>();
serviceCapabilityList.put("Connectivity", Arrays.asList("RegionLevelVpc:true", "DistributedRouter:true"));
serviceCapabilityList.put("SourceNat", Arrays.asList("RedundantRouter:true"));
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceCapabilityList", serviceCapabilityList);
Map<String, List<String>> result = cloneVpcOfferingCmd.getServiceCapabilityList();
assertNotNull(result);
assertEquals(serviceCapabilityList, result);
}
@Test
public void testCloneVpcOfferingWithNsxProvider() {
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "ROUTED");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "dynamic");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", true);
assertEquals("NSX", cloneVpcOfferingCmd.getProvider());
assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService());
assertEquals("ROUTED", cloneVpcOfferingCmd.getNetworkMode());
assertEquals("dynamic", cloneVpcOfferingCmd.getRoutingMode());
assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getSpecifyAsNumber());
}
@Test
public void testCloneVpcOfferingWithNetrisProvider() {
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L);
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "Netris");
ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED");
assertEquals("Netris", cloneVpcOfferingCmd.getProvider());
assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode());
}
}

View File

@ -22,6 +22,7 @@ import static com.cloud.offering.NetworkOffering.RoutingMode.Static;
import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
@ -67,15 +68,18 @@ import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd;
import org.apache.cloudstack.api.command.admin.network.NetworkOfferingBaseCmd;
import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd;
@ -3854,6 +3858,459 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
return false;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CLONE, eventDescription = "cloning service offering")
public ServiceOffering cloneServiceOffering(final CloneServiceOfferingCmd cmd) {
final long userId = CallContext.current().getCallingUserId();
final ServiceOfferingVO sourceOffering = getAndValidateSourceOffering(cmd.getSourceOfferingId());
final DiskOfferingVO sourceDiskOffering = getSourceDiskOffering(sourceOffering);
final Map<String, String> requestParams = cmd.getFullUrlParams();
final String name = cmd.getServiceOfferingName();
final String displayText = getOrDefault(cmd.getDisplayText(), sourceOffering.getDisplayText());
final Integer cpuNumber = getOrDefault(cmd.getCpuNumber(), sourceOffering.getCpu());
final Integer cpuSpeed = getOrDefault(cmd.getCpuSpeed(), sourceOffering.getSpeed());
final Integer memory = getOrDefault(cmd.getMemory(), sourceOffering.getRamSize());
final String provisioningType = resolveProvisioningType(cmd, sourceDiskOffering);
final Boolean offerHa = resolveBooleanParam(requestParams, ApiConstants.OFFER_HA, cmd::isOfferHa, sourceOffering.isOfferHA());
final Boolean limitCpuUse = resolveBooleanParam(requestParams, ApiConstants.LIMIT_CPU_USE, cmd::isLimitCpuUse, sourceOffering.getLimitCpuUse());
final Boolean isVolatile = resolveBooleanParam(requestParams, ApiConstants.IS_VOLATILE, cmd::isVolatileVm, sourceOffering.isVolatileVm());
final Boolean isCustomized = resolveBooleanParam(requestParams, ApiConstants.CUSTOMIZED, cmd::isCustomized, sourceOffering.isCustomized());
final Boolean dynamicScalingEnabled = resolveBooleanParam(requestParams, ApiConstants.DYNAMIC_SCALING_ENABLED, cmd::getDynamicScalingEnabled, sourceOffering.isDynamicScalingEnabled());
final Boolean diskOfferingStrictness = resolveBooleanParam(requestParams, ApiConstants.DISK_OFFERING_STRICTNESS, cmd::getDiskOfferingStrictness, sourceOffering.getDiskOfferingStrictness());
final Boolean encryptRoot = resolveBooleanParam(requestParams, ApiConstants.ENCRYPT_ROOT, cmd::getEncryptRoot, sourceDiskOffering != null && sourceDiskOffering.getEncrypt());
final Boolean gpuDisplay = resolveBooleanParam(requestParams, ApiConstants.GPU_DISPLAY, cmd::getGpuDisplay, sourceOffering.getGpuDisplay());
final String storageType = resolveStorageType(cmd, sourceDiskOffering);
final String tags = getOrDefault(cmd.getTags(), sourceDiskOffering != null ? sourceDiskOffering.getTags() : null);
final List<Long> domainIds = resolveDomainIds(cmd, sourceOffering);
final List<Long> zoneIds = resolveZoneIds(cmd, sourceOffering);
final String hostTag = getOrDefault(cmd.getHostTag(), sourceOffering.getHostTag());
final Integer networkRate = getOrDefault(cmd.getNetworkRate(), sourceOffering.getRateMbps());
final String deploymentPlanner = getOrDefault(cmd.getDeploymentPlanner(), sourceOffering.getDeploymentPlanner());
final ClonedDiskOfferingParams diskParams = resolveDiskOfferingParams(cmd, sourceDiskOffering);
final CustomOfferingParams customParams = resolveCustomOfferingParams(cmd, sourceOffering, isCustomized);
final Long vgpuProfileId = getOrDefault(cmd.getVgpuProfileId(), sourceOffering.getVgpuProfileId());
final Integer gpuCount = getOrDefault(cmd.getGpuCount(), sourceOffering.getGpuCount());
final Boolean purgeResources = resolvePurgeResources(cmd, requestParams, sourceOffering);
final LeaseParams leaseParams = resolveLeaseParams(cmd, sourceOffering);
if (cmd.getCacheMode() != null) {
validateCacheMode(cmd.getCacheMode());
}
final Integer finalGpuCount = validateVgpuProfileAndGetGpuCount(vgpuProfileId, gpuCount);
final Map<String, String> mergedDetails = mergeOfferingDetails(cmd, sourceOffering, customParams);
final boolean localStorageRequired = ServiceOffering.StorageType.local.toString().equalsIgnoreCase(storageType);
final boolean systemUse = sourceOffering.isSystemUse();
final VirtualMachine.Type vmType = resolveVmType(sourceOffering);
final Long diskOfferingId = getOrDefault(cmd.getDiskOfferingId(), sourceOffering.getDiskOfferingId());
return createServiceOffering(userId, systemUse, vmType,
name, cpuNumber, memory, cpuSpeed, displayText, provisioningType, localStorageRequired,
offerHa, limitCpuUse, isVolatile, tags, domainIds, zoneIds, hostTag, networkRate,
deploymentPlanner, mergedDetails, diskParams.rootDiskSize, diskParams.isCustomizedIops,
diskParams.minIops, diskParams.maxIops,
diskParams.bytesReadRate, diskParams.bytesReadRateMax, diskParams.bytesReadRateMaxLength,
diskParams.bytesWriteRate, diskParams.bytesWriteRateMax, diskParams.bytesWriteRateMaxLength,
diskParams.iopsReadRate, diskParams.iopsReadRateMax, diskParams.iopsReadRateMaxLength,
diskParams.iopsWriteRate, diskParams.iopsWriteRateMax, diskParams.iopsWriteRateMaxLength,
diskParams.hypervisorSnapshotReserve, diskParams.cacheMode, customParams.storagePolicy, dynamicScalingEnabled,
diskOfferingId, diskOfferingStrictness, isCustomized, encryptRoot,
vgpuProfileId, finalGpuCount, gpuDisplay, purgeResources, leaseParams.leaseDuration, leaseParams.leaseExpiryAction);
}
private ServiceOfferingVO getAndValidateSourceOffering(Long sourceOfferingId) {
final ServiceOfferingVO sourceOffering = _serviceOfferingDao.findById(sourceOfferingId);
if (sourceOffering == null) {
throw new InvalidParameterValueException("Unable to find service offering with ID: " + sourceOfferingId);
}
return sourceOffering;
}
private DiskOfferingVO getSourceDiskOffering(ServiceOfferingVO sourceOffering) {
final Long sourceDiskOfferingId = sourceOffering.getDiskOfferingId();
return sourceDiskOfferingId != null ? _diskOfferingDao.findById(sourceDiskOfferingId) : null;
}
public <T> T getOrDefault(T cmdValue, T defaultValue) {
return cmdValue != null ? cmdValue : defaultValue;
}
public Boolean resolveBooleanParam(Map<String, String> requestParams, String paramKey,
java.util.function.Supplier<Boolean> cmdValueSupplier, Boolean defaultValue) {
return requestParams != null && requestParams.containsKey(paramKey) ? cmdValueSupplier.get() : defaultValue;
}
private String resolveProvisioningType(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) {
if (cmd.getProvisioningType() != null) {
return cmd.getProvisioningType();
}
if (sourceDiskOffering != null) {
return sourceDiskOffering.getProvisioningType().toString();
}
return Storage.ProvisioningType.THIN.toString();
}
private String resolveStorageType(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) {
if (cmd.getStorageType() != null) {
return cmd.getStorageType();
}
if (sourceDiskOffering != null && sourceDiskOffering.isUseLocalStorage()) {
return ServiceOffering.StorageType.local.toString();
}
return ServiceOffering.StorageType.shared.toString();
}
private List<Long> resolveDomainIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) {
List<Long> domainIds = cmd.getDomainIds();
if (domainIds == null || domainIds.isEmpty()) {
domainIds = _serviceOfferingDetailsDao.findDomainIds(sourceOffering.getId());
}
return domainIds;
}
private List<Long> resolveZoneIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) {
List<Long> zoneIds = cmd.getZoneIds();
if (zoneIds == null || zoneIds.isEmpty()) {
zoneIds = _serviceOfferingDetailsDao.findZoneIds(sourceOffering.getId());
}
return zoneIds;
}
private ClonedDiskOfferingParams resolveDiskOfferingParams(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) {
final ClonedDiskOfferingParams params = new ClonedDiskOfferingParams();
params.rootDiskSize = getOrDefault(cmd.getRootDiskSize(), sourceDiskOffering != null ? sourceDiskOffering.getDiskSize() : null);
params.bytesReadRate = getOrDefault(cmd.getBytesReadRate(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRate() : null);
params.bytesReadRateMax = getOrDefault(cmd.getBytesReadRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRateMax() : null);
params.bytesReadRateMaxLength = getOrDefault(cmd.getBytesReadRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRateMaxLength() : null);
params.bytesWriteRate = getOrDefault(cmd.getBytesWriteRate(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRate() : null);
params.bytesWriteRateMax = getOrDefault(cmd.getBytesWriteRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRateMax() : null);
params.bytesWriteRateMaxLength = getOrDefault(cmd.getBytesWriteRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRateMaxLength() : null);
params.iopsReadRate = getOrDefault(cmd.getIopsReadRate(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRate() : null);
params.iopsReadRateMax = getOrDefault(cmd.getIopsReadRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRateMax() : null);
params.iopsReadRateMaxLength = getOrDefault(cmd.getIopsReadRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRateMaxLength() : null);
params.iopsWriteRate = getOrDefault(cmd.getIopsWriteRate(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRate() : null);
params.iopsWriteRateMax = getOrDefault(cmd.getIopsWriteRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRateMax() : null);
params.iopsWriteRateMaxLength = getOrDefault(cmd.getIopsWriteRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRateMaxLength() : null);
params.isCustomizedIops = getOrDefault(cmd.isCustomizedIops(), sourceDiskOffering != null ? sourceDiskOffering.isCustomizedIops() : null);
params.minIops = getOrDefault(cmd.getMinIops(), sourceDiskOffering != null ? sourceDiskOffering.getMinIops() : null);
params.maxIops = getOrDefault(cmd.getMaxIops(), sourceDiskOffering != null ? sourceDiskOffering.getMaxIops() : null);
params.hypervisorSnapshotReserve = getOrDefault(cmd.getHypervisorSnapshotReserve(), sourceDiskOffering != null ? sourceDiskOffering.getHypervisorSnapshotReserve() : null);
if (cmd.getCacheMode() != null) {
params.cacheMode = cmd.getCacheMode();
} else if (sourceDiskOffering != null && sourceDiskOffering.getCacheMode() != null) {
params.cacheMode = sourceDiskOffering.getCacheMode().toString();
}
return params;
}
private CustomOfferingParams resolveCustomOfferingParams(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering, Boolean isCustomized) {
final CustomOfferingParams params = new CustomOfferingParams();
params.maxCPU = resolveDetailParameter(cmd.getMaxCPUs(), sourceOffering.getId(), ApiConstants.MAX_CPU_NUMBER);
params.minCPU = resolveDetailParameter(cmd.getMinCPUs(), sourceOffering.getId(), ApiConstants.MIN_CPU_NUMBER);
params.maxMemory = resolveDetailParameter(cmd.getMaxMemory(), sourceOffering.getId(), ApiConstants.MAX_MEMORY);
params.minMemory = resolveDetailParameter(cmd.getMinMemory(), sourceOffering.getId(), ApiConstants.MIN_MEMORY);
params.storagePolicy = resolveDetailParameterAsLong(cmd.getStoragePolicy(), sourceOffering.getId(), ApiConstants.STORAGE_POLICY);
return params;
}
private Integer resolveDetailParameter(Integer cmdValue, Long offeringId, String detailKey) {
if (cmdValue != null) {
return cmdValue;
}
String detailValue = _serviceOfferingDetailsDao.getDetail(offeringId, detailKey);
return detailValue != null ? Integer.parseInt(detailValue) : null;
}
private Long resolveDetailParameterAsLong(Long cmdValue, Long offeringId, String detailKey) {
if (cmdValue != null) {
return cmdValue;
}
String detailValue = _serviceOfferingDetailsDao.getDetail(offeringId, detailKey);
return detailValue != null ? Long.parseLong(detailValue) : null;
}
private Boolean resolvePurgeResources(CloneServiceOfferingCmd cmd, Map<String, String> requestParams, ServiceOfferingVO sourceOffering) {
if (requestParams != null && requestParams.containsKey(ApiConstants.PURGE_RESOURCES)) {
return cmd.isPurgeResources();
}
String purgeResourcesStr = _serviceOfferingDetailsDao.getDetail(sourceOffering.getId(), ServiceOffering.PURGE_DB_ENTITIES_KEY);
return Boolean.parseBoolean(purgeResourcesStr);
}
private LeaseParams resolveLeaseParams(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) {
final LeaseParams params = new LeaseParams();
params.leaseDuration = resolveDetailParameter(cmd.getLeaseDuration(), sourceOffering.getId(), ApiConstants.INSTANCE_LEASE_DURATION);
if (cmd.getLeaseExpiryAction() != null) {
params.leaseExpiryAction = cmd.getLeaseExpiryAction();
} else {
String leaseExpiryActionStr = _serviceOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION);
if (leaseExpiryActionStr != null) {
params.leaseExpiryAction = VMLeaseManager.ExpiryAction.valueOf(leaseExpiryActionStr);
}
}
params.leaseExpiryAction = validateAndGetLeaseExpiryAction(params.leaseDuration, params.leaseExpiryAction);
return params;
}
private Map<String, String> mergeOfferingDetails(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering, CustomOfferingParams customParams) {
final Map<String, String> cmdDetails = cmd.getDetails();
final Map<String, String> mergedDetails = new HashMap<>();
if (cmdDetails == null || cmdDetails.isEmpty()) {
Map<String, String> sourceDetails = _serviceOfferingDetailsDao.listDetailsKeyPairs(sourceOffering.getId());
if (sourceDetails != null) {
mergedDetails.putAll(sourceDetails);
}
} else {
mergedDetails.putAll(cmdDetails);
}
if (customParams.minCPU != null && customParams.maxCPU != null &&
customParams.minMemory != null && customParams.maxMemory != null) {
mergedDetails.put(ApiConstants.MIN_MEMORY, customParams.minMemory.toString());
mergedDetails.put(ApiConstants.MAX_MEMORY, customParams.maxMemory.toString());
mergedDetails.put(ApiConstants.MIN_CPU_NUMBER, customParams.minCPU.toString());
mergedDetails.put(ApiConstants.MAX_CPU_NUMBER, customParams.maxCPU.toString());
}
return mergedDetails;
}
private VirtualMachine.Type resolveVmType(ServiceOfferingVO sourceOffering) {
if (sourceOffering.getVmType() == null) {
return null;
}
try {
return VirtualMachine.Type.valueOf(sourceOffering.getVmType());
} catch (IllegalArgumentException e) {
logger.warn("Invalid VM type in source offering: {}", sourceOffering.getVmType());
return null;
}
}
private static class ClonedDiskOfferingParams {
Long rootDiskSize;
Long bytesReadRate;
Long bytesReadRateMax;
Long bytesReadRateMaxLength;
Long bytesWriteRate;
Long bytesWriteRateMax;
Long bytesWriteRateMaxLength;
Long iopsReadRate;
Long iopsReadRateMax;
Long iopsReadRateMaxLength;
Long iopsWriteRate;
Long iopsWriteRateMax;
Long iopsWriteRateMaxLength;
Boolean isCustomizedIops;
Long minIops;
Long maxIops;
Integer hypervisorSnapshotReserve;
String cacheMode;
}
private static class CustomOfferingParams {
Integer maxCPU;
Integer minCPU;
Integer maxMemory;
Integer minMemory;
Long storagePolicy;
}
private static class LeaseParams {
Integer leaseDuration;
VMLeaseManager.ExpiryAction leaseExpiryAction;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_DISK_OFFERING_CLONE, eventDescription = "cloning disk offering")
public DiskOffering cloneDiskOffering(final CloneDiskOfferingCmd cmd) {
final long userId = CallContext.current().getCallingUserId();
final DiskOfferingVO sourceOffering = getAndValidateSourceDiskOffering(cmd.getSourceOfferingId());
final Map<String, String> requestParams = cmd.getFullUrlParams();
final String name = cmd.getOfferingName();
final String displayText = getOrDefault(cmd.getDisplayText(), sourceOffering.getDisplayText());
final String provisioningType = getOrDefault(cmd.getProvisioningType(), sourceOffering.getProvisioningType().toString());
final Long diskSize = getOrDefault(cmd.getDiskSize(), sourceOffering.getDiskSize());
final String tags = getOrDefault(cmd.getTags(), sourceOffering.getTags());
final Boolean isCustomized = resolveBooleanParam(requestParams, ApiConstants.CUSTOMIZED, cmd::isCustomized, sourceOffering.isCustomized());
final Boolean displayOffering = resolveBooleanParam(requestParams, ApiConstants.DISPLAY_OFFERING, cmd::getDisplayOffering, sourceOffering.getDisplayOffering());
final Boolean isCustomizedIops = getOrDefault(cmd.isCustomizedIops(), sourceOffering.isCustomizedIops());
final Boolean diskSizeStrictness = resolveBooleanParam(requestParams, ApiConstants.DISK_SIZE_STRICTNESS, cmd::getDiskSizeStrictness, sourceOffering.getDiskSizeStrictness());
final Boolean encrypt = resolveBooleanParam(requestParams, ApiConstants.ENCRYPT, cmd::getEncrypt, sourceOffering.getEncrypt());
final List<Long> domainIds = resolveDomainIdsForDiskOffering(cmd, sourceOffering);
final List<Long> zoneIds = resolveZoneIdsForDiskOffering(cmd, sourceOffering);
final boolean localStorageRequired = resolveLocalStorageRequired(cmd, sourceOffering);
final ClonedDiskIopsParams iopsParams = resolveDiskIopsParams(cmd, sourceOffering);
final ClonedDiskRateParams rateParams = resolveDiskRateParams(cmd, sourceOffering);
final Integer hypervisorSnapshotReserve = getOrDefault(cmd.getHypervisorSnapshotReserve(), sourceOffering.getHypervisorSnapshotReserve());
final String cacheMode = resolveCacheMode(cmd, sourceOffering);
final Long storagePolicy = resolveStoragePolicyForDiskOffering(cmd, sourceOffering);
final Map<String, String> mergedDetails = mergeDiskOfferingDetails(cmd, sourceOffering);
if (cmd.getCacheMode() != null) {
validateCacheMode(cmd.getCacheMode());
}
validateMaxRateEqualsOrGreater(iopsParams.iopsReadRate, iopsParams.iopsReadRateMax, IOPS_READ_RATE);
validateMaxRateEqualsOrGreater(iopsParams.iopsWriteRate, iopsParams.iopsWriteRateMax, IOPS_WRITE_RATE);
validateMaxRateEqualsOrGreater(rateParams.bytesReadRate, rateParams.bytesReadRateMax, BYTES_READ_RATE);
validateMaxRateEqualsOrGreater(rateParams.bytesWriteRate, rateParams.bytesWriteRateMax, BYTES_WRITE_RATE);
validateMaximumIopsAndBytesLength(iopsParams.iopsReadRateMaxLength, iopsParams.iopsWriteRateMaxLength,
rateParams.bytesReadRateMaxLength, rateParams.bytesWriteRateMaxLength);
return createDiskOffering(userId, domainIds, zoneIds, name, displayText, provisioningType, diskSize, tags,
isCustomized, localStorageRequired, displayOffering, isCustomizedIops, iopsParams.minIops, iopsParams.maxIops,
rateParams.bytesReadRate, rateParams.bytesReadRateMax, rateParams.bytesReadRateMaxLength,
rateParams.bytesWriteRate, rateParams.bytesWriteRateMax, rateParams.bytesWriteRateMaxLength,
iopsParams.iopsReadRate, iopsParams.iopsReadRateMax, iopsParams.iopsReadRateMaxLength,
iopsParams.iopsWriteRate, iopsParams.iopsWriteRateMax, iopsParams.iopsWriteRateMaxLength,
hypervisorSnapshotReserve, cacheMode, mergedDetails, storagePolicy, diskSizeStrictness, encrypt);
}
private DiskOfferingVO getAndValidateSourceDiskOffering(Long sourceOfferingId) {
final DiskOfferingVO sourceOffering = _diskOfferingDao.findById(sourceOfferingId);
if (sourceOffering == null) {
throw new InvalidParameterValueException("Unable to find disk offering with ID: " + sourceOfferingId);
}
return sourceOffering;
}
private List<Long> resolveDomainIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
List<Long> domainIds = cmd.getDomainIds();
if (domainIds == null || domainIds.isEmpty()) {
domainIds = diskOfferingDetailsDao.findDomainIds(sourceOffering.getId());
}
return domainIds;
}
private List<Long> resolveZoneIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
List<Long> zoneIds = cmd.getZoneIds();
if (zoneIds == null || zoneIds.isEmpty()) {
zoneIds = diskOfferingDetailsDao.findZoneIds(sourceOffering.getId());
}
return zoneIds;
}
private boolean resolveLocalStorageRequired(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
if (cmd.getStorageType() != null) {
return ServiceOffering.StorageType.local.toString().equalsIgnoreCase(cmd.getStorageType());
}
return sourceOffering.isUseLocalStorage();
}
private String resolveCacheMode(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
if (cmd.getCacheMode() != null) {
return cmd.getCacheMode();
}
if (sourceOffering.getCacheMode() != null) {
return sourceOffering.getCacheMode().toString();
}
return null;
}
private Long resolveStoragePolicyForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
Long storagePolicy = cmd.getStoragePolicy();
if (storagePolicy == null) {
String storagePolicyStr = diskOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.STORAGE_POLICY);
if (storagePolicyStr != null) {
storagePolicy = Long.parseLong(storagePolicyStr);
}
}
return storagePolicy;
}
private ClonedDiskIopsParams resolveDiskIopsParams(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
final ClonedDiskIopsParams params = new ClonedDiskIopsParams();
params.minIops = getOrDefault(cmd.getMinIops(), sourceOffering.getMinIops());
params.maxIops = getOrDefault(cmd.getMaxIops(), sourceOffering.getMaxIops());
params.iopsReadRate = getOrDefault(cmd.getIopsReadRate(), sourceOffering.getIopsReadRate());
params.iopsReadRateMax = getOrDefault(cmd.getIopsReadRateMax(), sourceOffering.getIopsReadRateMax());
params.iopsReadRateMaxLength = getOrDefault(cmd.getIopsReadRateMaxLength(), sourceOffering.getIopsReadRateMaxLength());
params.iopsWriteRate = getOrDefault(cmd.getIopsWriteRate(), sourceOffering.getIopsWriteRate());
params.iopsWriteRateMax = getOrDefault(cmd.getIopsWriteRateMax(), sourceOffering.getIopsWriteRateMax());
params.iopsWriteRateMaxLength = getOrDefault(cmd.getIopsWriteRateMaxLength(), sourceOffering.getIopsWriteRateMaxLength());
return params;
}
private ClonedDiskRateParams resolveDiskRateParams(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
final ClonedDiskRateParams params = new ClonedDiskRateParams();
params.bytesReadRate = getOrDefault(cmd.getBytesReadRate(), sourceOffering.getBytesReadRate());
params.bytesReadRateMax = getOrDefault(cmd.getBytesReadRateMax(), sourceOffering.getBytesReadRateMax());
params.bytesReadRateMaxLength = getOrDefault(cmd.getBytesReadRateMaxLength(), sourceOffering.getBytesReadRateMaxLength());
params.bytesWriteRate = getOrDefault(cmd.getBytesWriteRate(), sourceOffering.getBytesWriteRate());
params.bytesWriteRateMax = getOrDefault(cmd.getBytesWriteRateMax(), sourceOffering.getBytesWriteRateMax());
params.bytesWriteRateMaxLength = getOrDefault(cmd.getBytesWriteRateMaxLength(), sourceOffering.getBytesWriteRateMaxLength());
return params;
}
private Map<String, String> mergeDiskOfferingDetails(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) {
final Map<String, String> cmdDetails = cmd.getDetails();
final Map<String, String> mergedDetails = new HashMap<>();
if (cmdDetails == null || cmdDetails.isEmpty()) {
Map<String, String> sourceDetails = diskOfferingDetailsDao.listDetailsKeyPairs(sourceOffering.getId());
if (sourceDetails != null) {
mergedDetails.putAll(sourceDetails);
}
} else {
mergedDetails.putAll(cmdDetails);
}
return mergedDetails;
}
// Helper classes for disk offering parameters
private static class ClonedDiskIopsParams {
Long minIops;
Long maxIops;
Long iopsReadRate;
Long iopsReadRateMax;
Long iopsReadRateMaxLength;
Long iopsWriteRate;
Long iopsWriteRateMax;
Long iopsWriteRateMaxLength;
}
private static class ClonedDiskRateParams {
Long bytesReadRate;
Long bytesReadRateMax;
Long bytesReadRateMaxLength;
Long bytesWriteRate;
Long bytesWriteRateMax;
Long bytesWriteRateMaxLength;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_EDIT, eventDescription = "updating service offering")
public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) {
@ -6590,7 +7047,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
@Override
@ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_CREATE, eventDescription = "creating network offering")
public NetworkOffering createNetworkOffering(final CreateNetworkOfferingCmd cmd) {
public NetworkOffering createNetworkOffering(final NetworkOfferingBaseCmd cmd) {
final String name = cmd.getNetworkOfferingName();
final String displayText = cmd.getDisplayText();
final NetUtils.InternetProtocol internetProtocol = NetUtils.InternetProtocol.fromValue(cmd.getInternetProtocol());
@ -7827,6 +8284,431 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_CLONE, eventDescription = "cloning network offering")
public NetworkOffering cloneNetworkOffering(final CloneNetworkOfferingCmd cmd) {
final Long sourceOfferingId = cmd.getSourceOfferingId();
final NetworkOfferingVO sourceOffering = _networkOfferingDao.findById(sourceOfferingId);
if (sourceOffering == null) {
throw new InvalidParameterValueException("Unable to find network offering with id " + sourceOfferingId);
}
String name = cmd.getNetworkOfferingName();
if (name == null || name.isEmpty()) {
throw new InvalidParameterValueException("Name is required when cloning a network offering");
}
NetworkOfferingVO existing = _networkOfferingDao.findByUniqueName(name);
if (existing != null) {
throw new InvalidParameterValueException("Network offering with name '" + name + "' already exists");
}
logger.info("Cloning network offering {} (id: {}) to new offering with name: {}",
sourceOffering.getName(), sourceOfferingId, name);
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap =
_networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId);
validateProvider(sourceOffering, sourceServiceProviderMap, cmd.getProvider(), cmd.getNetworkMode());
applySourceOfferingValuesToCloneCmd(cmd, sourceServiceProviderMap, sourceOffering);
return createNetworkOffering(cmd);
}
private void validateProvider(NetworkOfferingVO sourceOffering,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap,
String detectedProvider, String networkMode) {
detectedProvider = getExternalNetworkProvider(detectedProvider, sourceServiceProviderMap);
// If this is an NSX/Netris offering, prevent network mode changes
if (detectedProvider != null && (detectedProvider.equals("NSX") || detectedProvider.equals("Netris"))) {
if (networkMode != null && sourceOffering.getNetworkMode() != null) {
if (!networkMode.equalsIgnoreCase(sourceOffering.getNetworkMode().toString())) {
throw new InvalidParameterValueException(
String.format("Cannot change network mode when cloning %s provider network offerings. " +
"Source offering has network mode '%s', but '%s' was specified. ",
detectedProvider, sourceOffering.getNetworkMode(), networkMode));
}
}
}
}
public static String getExternalNetworkProvider(String detectedProvider,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap) {
if (StringUtils.isNotEmpty(detectedProvider)) {
return detectedProvider;
}
if (sourceServiceProviderMap == null || sourceServiceProviderMap.isEmpty()) {
return null;
}
for (Set<Provider> providers : sourceServiceProviderMap.values()) {
if (CollectionUtils.isEmpty(providers)) {
continue;
}
for (Provider provider : providers) {
if (provider == Provider.Nsx) {
return "NSX";
}
if (provider == Provider.Netris) {
return "Netris";
}
}
}
return null;
}
/**
* Converts service provider map from internal format to API parameter format.
*
* Internal format: Map<String, List<String>> where key=serviceName, value=list of provider names
* API parameter format: Map where each value is a HashMap with "service" and "provider" keys
*
* Example: {"Lb": ["VirtualRouter"]} becomes {0: {"service": "Lb", "provider": "VirtualRouter"}}
*/
private Map<String, Map<String, String>> convertToApiParameterFormat(Map<String, List<String>> serviceProviderMap) {
Map<String, Map<String, String>> apiFormatMap = new HashMap<>();
int index = 0;
for (Map.Entry<String, List<String>> entry : serviceProviderMap.entrySet()) {
String serviceName = entry.getKey();
List<String> providers = entry.getValue();
for (String provider : providers) {
Map<String, String> serviceProviderEntry = new HashMap<>();
serviceProviderEntry.put("service", serviceName);
serviceProviderEntry.put("provider", provider);
apiFormatMap.put(String.valueOf(index), serviceProviderEntry);
index++;
}
}
return apiFormatMap;
}
private void applySourceOfferingValuesToCloneCmd(CloneNetworkOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap,
NetworkOfferingVO sourceOffering) {
Long sourceOfferingId = sourceOffering.getId();
// Build final services list with add/drop support
List<String> finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap);
Map<String, List<String>> finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices);
Map<String, Map<String, String>> sourceServiceCapabilityList = reconstructNetworkServiceCapabilityList(sourceOffering);
Map<String, String> sourceDetailsMap = getSourceOfferingDetails(sourceOfferingId);
List<Long> sourceDomainIds = networkOfferingDetailsDao.findDomainIds(sourceOfferingId);
List<Long> sourceZoneIds = networkOfferingDetailsDao.findZoneIds(sourceOfferingId);
applyResolvedValuesToCommand(cmd, sourceOffering, finalServices, finalServiceProviderMap,
sourceServiceCapabilityList, sourceDetailsMap, sourceDomainIds, sourceZoneIds);
}
private Map<String, String> getSourceOfferingDetails(Long sourceOfferingId) {
List<NetworkOfferingDetailsVO> sourceDetailsVOs = networkOfferingDetailsDao.listDetails(sourceOfferingId);
Map<String, String> sourceDetailsMap = new HashMap<>();
for (NetworkOfferingDetailsVO detailVO : sourceDetailsVOs) {
sourceDetailsMap.put(detailVO.getName(), detailVO.getValue());
}
return sourceDetailsMap;
}
private List<String> resolveFinalServicesList(CloneNetworkOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap) {
List<String> cmdServices = cmd.getSupportedServices();
List<String> addServices = cmd.getAddServices();
List<String> dropServices = cmd.getDropServices();
if (cmdServices != null && !cmdServices.isEmpty()) {
return cmdServices;
}
List<String> finalServices = new ArrayList<>();
for (Network.Service service : sourceServiceProviderMap.keySet()) {
if (service != Network.Service.Gateway) {
finalServices.add(service.getName());
}
}
if (dropServices != null && !dropServices.isEmpty()) {
List<String> normalizedDropServices = new ArrayList<>();
for (String serviceName : dropServices) {
Network.Service service = Network.Service.getService(serviceName);
if (service == null) {
throw new InvalidParameterValueException("Invalid service name in dropServices: " + serviceName);
}
normalizedDropServices.add(service.getName());
}
finalServices.removeAll(normalizedDropServices);
logger.debug("Dropped services from clone: {}", normalizedDropServices);
}
if (addServices != null && !addServices.isEmpty()) {
List<String> normalizedAddServices = new ArrayList<>();
for (String serviceName : addServices) {
Network.Service service = Network.Service.getService(serviceName);
if (service == null) {
throw new InvalidParameterValueException("Invalid service name in addServices: " + serviceName);
}
String canonicalName = service.getName();
if (!finalServices.contains(canonicalName)) {
finalServices.add(canonicalName);
normalizedAddServices.add(canonicalName);
}
}
logger.debug("Added services to clone: {}", normalizedAddServices);
}
return finalServices;
}
private Map<String, List<String>> resolveServiceProviderMap(CloneNetworkOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap, List<String> finalServices) {
if (cmd.getServiceProviders() != null && !cmd.getServiceProviders().isEmpty()) {
return cmd.getServiceProviders();
}
Map<String, List<String>> finalMap = new HashMap<>();
for (Map.Entry<Network.Service, Set<Network.Provider>> entry : sourceServiceProviderMap.entrySet()) {
String serviceName = entry.getKey().getName();
if (finalServices.contains(serviceName)) {
List<String> providers = new ArrayList<>();
for (Network.Provider provider : entry.getValue()) {
providers.add(provider.getName());
}
finalMap.put(serviceName, providers);
}
}
return finalMap;
}
private void applyResolvedValuesToCommand(CloneNetworkOfferingCmd cmd, NetworkOfferingVO sourceOffering,
List<String> finalServices, Map<String, List<String>> finalServiceProviderMap, Map<String, Map<String, String>> sourceServiceCapabilityList,
Map<String, String> sourceDetailsMap, List<Long> sourceDomainIds, List<Long> sourceZoneIds) {
try {
Map<String, String> requestParams = cmd.getFullUrlParams();
if (cmd.getSupportedServices() == null || cmd.getSupportedServices().isEmpty()) {
setField(cmd, "supportedServices", finalServices);
}
if (cmd.getServiceProviders() == null || cmd.getServiceProviders().isEmpty()) {
Map<String, Map<String, String>> apiFormatMap = convertToApiParameterFormat(finalServiceProviderMap);
setField(cmd, "serviceProviderList", apiFormatMap);
}
boolean hasCapabilityParams = requestParams.keySet().stream()
.anyMatch(key -> key.startsWith(ApiConstants.SERVICE_CAPABILITY_LIST));
if (!hasCapabilityParams && sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) {
// Filter capabilities to only include those for services in the final service list
// This ensures that if services are dropped, their capabilities are also removed
Map<String, Map<String, String>> filteredCapabilities = new HashMap<>();
for (Map.Entry<String, Map<String, String>> entry : sourceServiceCapabilityList.entrySet()) {
Map<String, String> capabilityMap = entry.getValue();
String serviceName = capabilityMap.get("service");
if (serviceName != null && finalServices.contains(serviceName)) {
filteredCapabilities.put(entry.getKey(), capabilityMap);
}
}
if (!filteredCapabilities.isEmpty()) {
setField(cmd, "serviceCapabilitiesList", filteredCapabilities);
}
}
applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText());
applyIfNotProvided(cmd, requestParams, "traffictype", ApiConstants.TRAFFIC_TYPE, cmd.getTraffictype(), sourceOffering.getTrafficType().toString());
applyIfNotProvided(cmd, requestParams, "tags", ApiConstants.TAGS, cmd.getTags(), sourceOffering.getTags());
applyIfNotProvided(cmd, requestParams, "availability", ApiConstants.AVAILABILITY, cmd.getAvailability(), Availability.Optional.toString());
applyIfNotProvided(cmd, requestParams, "networkRate", ApiConstants.NETWORKRATE, cmd.getNetworkRate(), sourceOffering.getRateMbps());
applyIfNotProvided(cmd, requestParams, "serviceOfferingId", ApiConstants.SERVICE_OFFERING_ID, cmd.getServiceOfferingId(), sourceOffering.getServiceOfferingId());
applyIfNotProvided(cmd, requestParams, "guestIptype", ApiConstants.GUEST_IP_TYPE, cmd.getGuestIpType(), sourceOffering.getGuestType().toString());
applyIfNotProvided(cmd, requestParams, "maxConnections", ApiConstants.MAX_CONNECTIONS, cmd.getMaxconnections(), sourceOffering.getConcurrentConnections());
applyBooleanIfNotProvided(cmd, requestParams, "specifyVlan", ApiConstants.SPECIFY_VLAN, sourceOffering.isSpecifyVlan());
applyBooleanIfNotProvided(cmd, requestParams, "conserveMode", ApiConstants.CONSERVE_MODE, sourceOffering.isConserveMode());
applyBooleanIfNotProvided(cmd, requestParams, "specifyIpRanges", ApiConstants.SPECIFY_IP_RANGES, sourceOffering.isSpecifyIpRanges());
applyBooleanIfNotProvided(cmd, requestParams, "isPersistent", ApiConstants.IS_PERSISTENT, sourceOffering.isPersistent());
applyBooleanIfNotProvided(cmd, requestParams, "forVpc", ApiConstants.FOR_VPC, sourceOffering.isForVpc());
applyBooleanIfNotProvided(cmd, requestParams, "egressDefaultPolicy", ApiConstants.EGRESS_DEFAULT_POLICY, sourceOffering.isEgressDefaultPolicy());
applyBooleanIfNotProvided(cmd, requestParams, "keepAliveEnabled", ApiConstants.KEEPALIVE_ENABLED, sourceOffering.isKeepAliveEnabled());
applyBooleanIfNotProvided(cmd, requestParams, "enable", ApiConstants.ENABLE, sourceOffering.getState() == NetworkOffering.State.Enabled);
applyBooleanIfNotProvided(cmd, requestParams, "specifyAsNumber", ApiConstants.SPECIFY_AS_NUMBER, sourceOffering.isSpecifyAsNumber());
if (!requestParams.containsKey(ApiConstants.INTERNET_PROTOCOL)) {
String internetProtocol = networkOfferingDetailsDao.getDetail(sourceOffering.getId(), Detail.internetProtocol);
if (internetProtocol != null) {
setField(cmd, "internetProtocol", internetProtocol);
}
}
if (!requestParams.containsKey(ApiConstants.NETWORK_MODE) && sourceOffering.getNetworkMode() != null) {
setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString());
}
if (!requestParams.containsKey(ApiConstants.ROUTING_MODE) && sourceOffering.getRoutingMode() != null) {
setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString());
}
if (cmd.getDetails() == null || cmd.getDetails().isEmpty()) {
if (!sourceDetailsMap.isEmpty()) {
setField(cmd, "details", sourceDetailsMap);
}
}
if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) {
if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) {
setField(cmd, "domainIds", sourceDomainIds);
}
}
if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) {
if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) {
setField(cmd, "zoneIds", sourceZoneIds);
}
}
} catch (Exception e) {
logger.warn("Failed to apply some source offering parameters during clone: {}", e.getMessage());
}
}
/**
* Reconstructs the service capability list from the source network offering's stored capability flags.
* These capabilities were originally passed during creation and stored as boolean flags in the offering.
*
* Returns a Map in the format expected by CreateNetworkOfferingCmd.serviceCapabilitystList:
* Map where each value is a HashMap with keys: "service", "capabilitytype", "capabilityvalue"
*/
private Map<String, Map<String, String>> reconstructNetworkServiceCapabilityList(NetworkOfferingVO sourceOffering) {
Map<String, Map<String, String>> capabilityList = new HashMap<>();
int index = 0;
if (sourceOffering.isDedicatedLB()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.Lb.getName());
cap.put("capabilitytype", Network.Capability.SupportedLBIsolation.getName());
cap.put("capabilityvalue", "dedicated");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isElasticLb()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.Lb.getName());
cap.put("capabilitytype", Network.Capability.ElasticLb.getName());
cap.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isInline()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.Lb.getName());
cap.put("capabilitytype", Network.Capability.InlineMode.getName());
cap.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isPublicLb() || sourceOffering.isInternalLb()) {
List<String> schemes = new ArrayList<>();
if (sourceOffering.isPublicLb()) schemes.add("public");
if (sourceOffering.isInternalLb()) schemes.add("internal");
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.Lb.getName());
cap.put("capabilitytype", Network.Capability.LbSchemes.getName());
cap.put("capabilityvalue", String.join(",", schemes));
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isSupportsVmAutoScaling()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.Lb.getName());
cap.put("capabilitytype", Network.Capability.VmAutoScaling.getName());
cap.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isSharedSourceNat()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.SourceNat.getName());
cap.put("capabilitytype", Network.Capability.SupportedSourceNatTypes.getName());
cap.put("capabilityvalue", "perzone");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isRedundantRouter()) {
Map<String, String> cap1 = new HashMap<>();
cap1.put("service", Network.Service.SourceNat.getName());
cap1.put("capabilitytype", Network.Capability.RedundantRouter.getName());
cap1.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap1);
Map<String, String> cap2 = new HashMap<>();
cap2.put("service", Network.Service.Gateway.getName());
cap2.put("capabilitytype", Network.Capability.RedundantRouter.getName());
cap2.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap2);
}
if (sourceOffering.isElasticIp()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.StaticNat.getName());
cap.put("capabilitytype", Network.Capability.ElasticIp.getName());
cap.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap);
}
if (sourceOffering.isElasticIp() && sourceOffering.isAssociatePublicIP()) {
Map<String, String> cap = new HashMap<>();
cap.put("service", Network.Service.StaticNat.getName());
cap.put("capabilitytype", Network.Capability.AssociatePublicIP.getName());
cap.put("capabilityvalue", "true");
capabilityList.put(String.valueOf(index++), cap);
}
return capabilityList;
}
public static void applyIfNotProvided(Object cmd, Map<String, String> requestParams, String fieldName,
String apiConstant, Object currentValue, Object sourceValue) throws Exception {
if ((requestParams == null || !requestParams.containsKey(apiConstant)) && sourceValue != null) {
setField(cmd, fieldName, sourceValue);
}
}
public static void applyBooleanIfNotProvided(Object cmd, Map<String, String> requestParams,
String fieldName, String apiConstant, Boolean sourceValue) throws Exception {
if ((requestParams == null || !requestParams.containsKey(apiConstant)) && sourceValue != null) {
setField(cmd, fieldName, sourceValue);
}
}
public static void setField(Object obj, String fieldName, Object value) throws Exception {
Field field = findField(obj.getClass(), fieldName);
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + obj.getClass().getName());
}
field.setAccessible(true);
field.set(obj, value);
}
public static Field findField(Class<?> clazz, String fieldName) {
Class<?> currentClass = clazz;
while (currentClass != null) {
try {
return currentClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
currentClass = currentClass.getSuperclass();
}
}
return null;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_EDIT, eventDescription = "updating network offering")
public NetworkOffering updateNetworkOffering(final UpdateNetworkOfferingCmd cmd) {

View File

@ -71,6 +71,7 @@ import org.apache.cloudstack.alert.AlertService;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd;
import org.apache.cloudstack.api.command.admin.vpc.CreatePrivateGatewayByAdminCmd;
import org.apache.cloudstack.api.command.admin.vpc.CreateVPCCmdByAdmin;
import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd;
@ -635,6 +636,12 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
final String externalProvider, final NetworkOffering.NetworkMode networkMode, List<Long> domainIds, List<Long> zoneIds, State state,
NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode) {
boolean isExternalProvider = externalProvider != null &&
Arrays.asList("NSX", "Netris").stream().anyMatch(s -> s.equalsIgnoreCase(externalProvider));
if (!isExternalProvider && CollectionUtils.isEmpty(supportedServices)) {
throw new InvalidParameterValueException("Supported services needs to be provided");
}
if (!Ipv6Service.Ipv6OfferingCreationEnabled.value() && !(internetProtocol == null || NetUtils.InternetProtocol.IPv4.equals(internetProtocol))) {
throw new InvalidParameterValueException(String.format("Configuration %s needs to be enabled for creating IPv6 supported VPC offering", Ipv6Service.Ipv6OfferingCreationEnabled.key()));
}
@ -813,6 +820,349 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VPC_OFFERING_CLONE, eventDescription = "cloning VPC offering")
public VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd) {
Long sourceVpcOfferingId = cmd.getSourceOfferingId();
final VpcOffering sourceVpcOffering = _vpcOffDao.findById(sourceVpcOfferingId);
if (sourceVpcOffering == null) {
throw new InvalidParameterValueException("Unable to find source VPC offering by id " + sourceVpcOfferingId);
}
String name = cmd.getVpcOfferingName();
if (name == null || name.isEmpty()) {
throw new InvalidParameterValueException("Name is required when cloning a VPC offering");
}
VpcOfferingVO vpcOfferingVO = _vpcOffDao.findByUniqueName(name);
if (vpcOfferingVO != null) {
throw new InvalidParameterValueException(String.format("A VPC offering with name %s already exists", name));
}
logger.info("Cloning VPC offering {} (id: {}) to new offering with name: {}",
sourceVpcOffering.getName(), sourceVpcOfferingId, name);
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap = getVpcOffSvcProvidersMap(sourceVpcOfferingId);
validateProvider(sourceVpcOffering, sourceServiceProviderMap, cmd.getProvider(), cmd.getNetworkMode());
applySourceOfferingValuesToCloneCmd(cmd, sourceServiceProviderMap, sourceVpcOffering);
return createVpcOffering(cmd);
}
private void validateProvider(VpcOffering sourceVpcOffering,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap,
String provider, String networkMode) {
provider = ConfigurationManagerImpl.getExternalNetworkProvider(provider, sourceServiceProviderMap);
if (provider != null && (provider.equals("NSX") || provider.equals("Netris"))) {
if (networkMode != null && sourceVpcOffering.getNetworkMode() != null) {
if (!networkMode.equalsIgnoreCase(sourceVpcOffering.getNetworkMode().toString())) {
throw new InvalidParameterValueException(
String.format("Cannot change network mode when cloning %s provider VPC offerings. " +
"Source offering has network mode '%s', but '%s' was specified. ",
provider, sourceVpcOffering.getNetworkMode(), networkMode));
}
}
}
}
private void applySourceOfferingValuesToCloneCmd(CloneVPCOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap,
VpcOffering sourceVpcOffering) {
Long sourceOfferingId = sourceVpcOffering.getId();
List<String> finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap);
Map finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices);
List<Long> sourceDomainIds = vpcOfferingDetailsDao.findDomainIds(sourceOfferingId);
List<Long> sourceZoneIds = vpcOfferingDetailsDao.findZoneIds(sourceOfferingId);
Map<String, String> sourceServiceCapabilityList = reconstructServiceCapabilityList(sourceVpcOffering);
applyResolvedValuesToCommand(cmd, (VpcOfferingVO)sourceVpcOffering, finalServices, finalServiceProviderMap,
sourceDomainIds, sourceZoneIds, sourceServiceCapabilityList);
}
/**
* Reconstructs the service capability list from the source VPC offering's stored capability flags.
* These capabilities were originally passed during creation and stored as boolean flags in the offering.
*
* Returns a Map in the format expected by CreateVPCOfferingCmd.serviceCapabilityList:
* Map<String, String> with keys like "0.service", "0.capabilitytype", "0.capabilityvalue"
*/
private Map<String, String> reconstructServiceCapabilityList(VpcOffering sourceOffering) {
Map<String, String> capabilityList = new HashMap<>();
int index = 0;
if (sourceOffering.isOffersRegionLevelVPC()) {
capabilityList.put(index + ".service", Network.Service.Connectivity.getName());
capabilityList.put(index + ".capabilitytype", Network.Capability.RegionLevelVpc.getName());
capabilityList.put(index + ".capabilityvalue", "true");
index++;
}
if (sourceOffering.isSupportsDistributedRouter()) {
capabilityList.put(index + ".service", Network.Service.Connectivity.getName());
capabilityList.put(index + ".capabilitytype", Network.Capability.DistributedRouter.getName());
capabilityList.put(index + ".capabilityvalue", "true");
index++;
}
if (sourceOffering.isRedundantRouter()) {
Map<Network.Service, Set<Network.Provider>> serviceProviderMap = getVpcOffSvcProvidersMap(sourceOffering.getId());
// Check which service has VPCVirtualRouter provider - SourceNat takes precedence
Network.Service redundantRouterService = null;
for (Network.Service service : Arrays.asList(Network.Service.SourceNat, Network.Service.Gateway, Network.Service.StaticNat)) {
Set<Network.Provider> providers = serviceProviderMap.get(service);
if (providers != null && providers.contains(Network.Provider.VPCVirtualRouter)) {
redundantRouterService = service;
break;
}
}
if (redundantRouterService != null) {
capabilityList.put(index + ".service", redundantRouterService.getName());
capabilityList.put(index + ".capabilitytype", Network.Capability.RedundantRouter.getName());
capabilityList.put(index + ".capabilityvalue", "true");
}
}
return capabilityList;
}
private List<String> resolveFinalServicesList(CloneVPCOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap) {
List<String> cmdServices = cmd.getSupportedServices();
List<String> addServices = cmd.getAddServices();
List<String> dropServices = cmd.getDropServices();
if (cmdServices != null && !cmdServices.isEmpty()) {
return cmdServices;
}
List<String> finalServices = new ArrayList<>();
for (Network.Service service : sourceServiceProviderMap.keySet()) {
finalServices.add(service.getName());
}
if (dropServices != null && !dropServices.isEmpty()) {
List<String> normalizedDropServices = new ArrayList<>();
for (String serviceName : dropServices) {
Network.Service service = Network.Service.getService(serviceName);
if (service == null) {
throw new InvalidParameterValueException("Service " + serviceName + " is not supported in VPC");
}
normalizedDropServices.add(service.getName());
}
finalServices.removeAll(normalizedDropServices);
logger.debug("Dropped services from clone: {}", dropServices);
}
if (addServices != null && !addServices.isEmpty()) {
List<String> normalizedAddServices = new ArrayList<>();
for (String serviceName : addServices) {
Network.Service service = Network.Service.getService(serviceName);
if (service == null) {
throw new InvalidParameterValueException("Service " + serviceName + " is not supported in VPC");
}
String canonicalName = service.getName();
if (!finalServices.contains(canonicalName)) {
finalServices.add(canonicalName);
normalizedAddServices.add(canonicalName);
}
}
logger.debug("Added services to clone: {}", addServices);
}
return finalServices;
}
private Map<String, List<String>> resolveServiceProviderMap(CloneVPCOfferingCmd cmd,
Map<Network.Service, Set<Network.Provider>> sourceServiceProviderMap, List<String> finalServices) {
if (cmd.getServiceProviders() != null && !cmd.getServiceProviders().isEmpty()) {
return cmd.getServiceProviders();
}
Map<String, List<String>> finalMap = new HashMap<>();
for (Map.Entry<Network.Service, Set<Network.Provider>> entry : sourceServiceProviderMap.entrySet()) {
String serviceName = entry.getKey().getName();
if (finalServices.contains(serviceName)) {
List<String> providers = new ArrayList<>();
for (Network.Provider provider : entry.getValue()) {
providers.add(provider.getName());
}
finalMap.put(serviceName, providers);
}
}
return finalMap;
}
/**
* Converts service provider map from Map<String, List<String>> to the indexed format
* expected by CreateVPCOfferingCmd.serviceProviderList parameter.
*
* Input: {"Dhcp": ["VpcVirtualRouter"], "Dns": ["VpcVirtualRouter"]}
* Output: {"0": {"service": "Dhcp", "provider": "VpcVirtualRouter"},
* "1": {"service": "Dns", "provider": "VpcVirtualRouter"}}
*/
private Map<String, Map<String, String>> convertToServiceProviderListFormat(Map<String, List<String>> serviceProviderMap) {
Map<String, Map<String, String>> result = new HashMap<>();
int index = 0;
for (Map.Entry<String, List<String>> entry : serviceProviderMap.entrySet()) {
String serviceName = entry.getKey();
List<String> providers = entry.getValue();
for (String providerName : providers) {
Map<String, String> serviceProviderEntry = new HashMap<>();
serviceProviderEntry.put("service", serviceName);
serviceProviderEntry.put("provider", providerName);
result.put(String.valueOf(index++), serviceProviderEntry);
}
}
return result;
}
private void applyResolvedValuesToCommand(CloneVPCOfferingCmd cmd, VpcOfferingVO sourceOffering,
List<String> finalServices, Map finalServiceProviderMap,
List<Long> sourceDomainIds, List<Long> sourceZoneIds,
Map<String, String> sourceServiceCapabilityList) {
try {
if (cmd.getSupportedServices() == null || cmd.getSupportedServices().isEmpty()) {
logger.debug("Setting supportedServices to {} services from source offering", finalServices.size());
ConfigurationManagerImpl.setField(cmd, "supportedServices", finalServices);
}
if (cmd.getServiceProviders() == null || cmd.getServiceProviders().isEmpty()) {
Map<String, Map<String, String>> convertedProviderMap = convertToServiceProviderListFormat(finalServiceProviderMap);
logger.debug("Setting serviceProviderList with {} provider mappings", convertedProviderMap.size());
ConfigurationManagerImpl.setField(cmd, "serviceProviderList", convertedProviderMap);
}
if ((cmd.getServiceCapabilityList() == null || cmd.getServiceCapabilityList().isEmpty())
&& sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) {
Map<String, String> filteredCapabilities = filterServiceCapabilities(sourceServiceCapabilityList, finalServices);
if (!filteredCapabilities.isEmpty()) {
ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", filteredCapabilities);
}
}
if (cmd.getDisplayText() == null && sourceOffering.getDisplayText() != null) {
ConfigurationManagerImpl.setField(cmd, "displayText", sourceOffering.getDisplayText());
}
if (cmd.getServiceOfferingId() == null && sourceOffering.getServiceOfferingId() != null) {
ConfigurationManagerImpl.setField(cmd, "serviceOfferingId", sourceOffering.getServiceOfferingId());
}
Boolean enableFieldValue = getRawFieldValue(cmd, "enable", Boolean.class);
if (enableFieldValue == null) {
Boolean enableState = sourceOffering.getState() == VpcOffering.State.Enabled;
ConfigurationManagerImpl.setField(cmd, "enable", enableState);
}
Boolean specifyAsNumberFieldValue = getRawFieldValue(cmd, "specifyAsNumber", Boolean.class);
if (specifyAsNumberFieldValue == null) {
ConfigurationManagerImpl.setField(cmd, "specifyAsNumber", sourceOffering.isSpecifyAsNumber());
}
if (cmd.getInternetProtocol() == null) {
String internetProtocol = vpcOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.INTERNET_PROTOCOL);
if (internetProtocol != null) {
ConfigurationManagerImpl.setField(cmd, "internetProtocol", internetProtocol);
}
}
if (cmd.getNetworkMode() == null && sourceOffering.getNetworkMode() != null) {
ConfigurationManagerImpl.setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString());
}
if (cmd.getRoutingMode() == null && sourceOffering.getRoutingMode() != null) {
ConfigurationManagerImpl.setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString());
}
if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) {
if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) {
ConfigurationManagerImpl.setField(cmd, "domainIds", sourceDomainIds);
}
}
if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) {
if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) {
ConfigurationManagerImpl.setField(cmd, "zoneIds", sourceZoneIds);
}
}
} catch (Exception e) {
logger.error("Failed to apply source offering parameters during clone: {}", e.getMessage(), e);
throw new CloudRuntimeException("Failed to apply source offering parameters during VPC offering clone", e);
}
}
private <T> T getRawFieldValue(Object obj, String fieldName, Class<T> expectedType) {
try {
java.lang.reflect.Field field = ConfigurationManagerImpl.findField(obj.getClass(), fieldName);
if (field != null) {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null || expectedType.isInstance(value)) {
return expectedType.cast(value);
}
}
} catch (Exception e) {
logger.debug("Could not get raw field value for {}: {}", fieldName, e.getMessage());
}
return null;
}
/**
* Filters service capabilities to only include those for services present in the final services list.
* This ensures that when services are dropped during cloning, their associated capabilities are also removed.
*
* @param sourceServiceCapabilityList The original capability list from the source VPC offering
* in format: Map with keys like "0.service", "0.capabilitytype", "0.capabilityvalue"
* @param finalServices The list of service names that should be retained in the cloned offering
* @return Filtered map containing only capabilities for services in finalServices
*/
private Map<String, String> filterServiceCapabilities(Map<String, String> sourceServiceCapabilityList,
List<String> finalServices) {
Map<String, String> filteredCapabilities = new HashMap<>();
for (Map.Entry<String, String> entry : sourceServiceCapabilityList.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
// Check if this is a service key (e.g., "0.service", "1.service")
if (key.endsWith(".service")) {
String serviceName = value;
if (finalServices.contains(serviceName)) {
// Include this service and its associated capability entries
String prefix = key.substring(0, key.lastIndexOf('.'));
filteredCapabilities.put(key, value);
// Also include the capability type and value for this service
String capabilityTypeKey = prefix + ".capabilitytype";
String capabilityValueKey = prefix + ".capabilityvalue";
if (sourceServiceCapabilityList.containsKey(capabilityTypeKey)) {
filteredCapabilities.put(capabilityTypeKey, sourceServiceCapabilityList.get(capabilityTypeKey));
}
if (sourceServiceCapabilityList.containsKey(capabilityValueKey)) {
filteredCapabilities.put(capabilityValueKey, sourceServiceCapabilityList.get(capabilityValueKey));
}
}
}
}
return filteredCapabilities;
}
private void validateConnectivtyServiceCapabilities(final Set<Provider> providers, final Map serviceCapabilitystList) {
if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) {
final Collection serviceCapabilityCollection = serviceCapabilitystList.values();

View File

@ -132,6 +132,7 @@ import org.apache.cloudstack.api.command.admin.management.ListMgmtsCmd;
import org.apache.cloudstack.api.command.admin.management.RemoveManagementServerCmd;
import org.apache.cloudstack.api.command.admin.network.AddNetworkDeviceCmd;
import org.apache.cloudstack.api.command.admin.network.AddNetworkServiceProviderCmd;
import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.CreateNetworkCmdByAdmin;
import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
@ -162,6 +163,8 @@ import org.apache.cloudstack.api.command.admin.network.UpdateNetworkServiceProvi
import org.apache.cloudstack.api.command.admin.network.UpdatePhysicalNetworkCmd;
import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.UpdateStorageNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd;
@ -328,6 +331,7 @@ import org.apache.cloudstack.api.command.admin.volume.RecoverVolumeCmdByAdmin;
import org.apache.cloudstack.api.command.admin.volume.ResizeVolumeCmdByAdmin;
import org.apache.cloudstack.api.command.admin.volume.UpdateVolumeCmdByAdmin;
import org.apache.cloudstack.api.command.admin.volume.UploadVolumeCmdByAdmin;
import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd;
import org.apache.cloudstack.api.command.admin.vpc.CreatePrivateGatewayByAdminCmd;
import org.apache.cloudstack.api.command.admin.vpc.CreateVPCCmdByAdmin;
import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd;
@ -3860,6 +3864,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(AddNetworkDeviceCmd.class);
cmdList.add(AddNetworkServiceProviderCmd.class);
cmdList.add(CreateNetworkOfferingCmd.class);
cmdList.add(CloneNetworkOfferingCmd.class);
cmdList.add(CreatePhysicalNetworkCmd.class);
cmdList.add(CreateStorageNetworkIpRangeCmd.class);
cmdList.add(DeleteNetworkDeviceCmd.class);
@ -3880,7 +3885,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(ListDedicatedGuestVlanRangesCmd.class);
cmdList.add(ReleaseDedicatedGuestVlanRangeCmd.class);
cmdList.add(CreateDiskOfferingCmd.class);
cmdList.add(CloneDiskOfferingCmd.class);
cmdList.add(CreateServiceOfferingCmd.class);
cmdList.add(CloneServiceOfferingCmd.class);
cmdList.add(DeleteDiskOfferingCmd.class);
cmdList.add(DeleteServiceOfferingCmd.class);
cmdList.add(IsAccountAllowedToCreateOfferingsWithTagsCmd.class);
@ -3965,6 +3972,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(RecoverVMCmd.class);
cmdList.add(CreatePrivateGatewayCmd.class);
cmdList.add(CreateVPCOfferingCmd.class);
cmdList.add(CloneVPCOfferingCmd.class);
cmdList.add(DeletePrivateGatewayCmd.class);
cmdList.add(DeleteVPCOfferingCmd.class);
cmdList.add(UpdateVPCOfferingCmd.class);

View File

@ -42,6 +42,7 @@ import com.cloud.utils.DomainHelper;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.DeleteBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.ListBackupProviderOfferingsCmd;
@ -325,7 +326,6 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
return savedOffering;
}
@Override
public List<Long> getBackupOfferingDomains(Long offeringId) {
final BackupOffering backupOffering = backupOfferingDao.findById(offeringId);
if (backupOffering == null) {
@ -334,6 +334,78 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
return backupOfferingDetailsDao.findDomainIds(offeringId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE, eventDescription = "cloning backup offering")
public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) {
final BackupOfferingVO sourceOffering = backupOfferingDao.findById(cmd.getSourceOfferingId());
if (sourceOffering == null) {
throw new InvalidParameterValueException("Unable to find backup offering with ID: " + cmd.getSourceOfferingId());
}
validateBackupForZone(sourceOffering.getZoneId());
if (backupOfferingDao.findByName(cmd.getName(), sourceOffering.getZoneId()) != null) {
throw new CloudRuntimeException("A backup offering with the name '" + cmd.getName() + "' already exists in this zone");
}
final String description = cmd.getDescription() != null ? cmd.getDescription() : sourceOffering.getDescription();
final String externalId = cmd.getExternalId() != null ? cmd.getExternalId() : sourceOffering.getExternalId();
final boolean userDrivenBackups = cmd.getUserDrivenBackups() != null ? cmd.getUserDrivenBackups() : sourceOffering.isUserDrivenBackupAllowed();
final Long zoneId = cmd.getZoneId() != null ? cmd.getZoneId() : sourceOffering.getZoneId();
if (!Objects.equals(sourceOffering.getExternalId(), externalId) || !Objects.equals(sourceOffering.getZoneId(), zoneId)) {
final BackupProvider provider = getBackupProvider(zoneId);
if (!provider.isValidProviderOffering(zoneId, externalId)) {
throw new CloudRuntimeException("Backup offering '" + externalId + "' does not exist on provider " + provider.getName() + " on zone " + zoneId);
}
}
final BackupOffering existingOffering = backupOfferingDao.findByExternalId(externalId, zoneId);
if (existingOffering != null) {
throw new CloudRuntimeException("A backup offering with external ID '" + externalId + "' already exists in this zone");
}
final BackupOfferingVO clonedOffering = new BackupOfferingVO(
zoneId,
externalId,
sourceOffering.getProvider(),
cmd.getName(),
description,
userDrivenBackups
);
final BackupOfferingVO savedOffering = backupOfferingDao.persist(clonedOffering);
if (savedOffering == null) {
throw new CloudRuntimeException("Unable to clone backup offering from ID: " + cmd.getSourceOfferingId());
}
List<Long> filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds());
Collections.sort(filteredDomainIds);
updateBackupOfferingDomainDetail(savedOffering, filteredDomainIds);
logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getSourceOfferingId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")");
return savedOffering;
}
private void updateBackupOfferingDomainDetail(BackupOfferingVO savedOffering, List<Long> filteredDomainIds) {
if (filteredDomainIds.size() > 1) {
filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds);
}
if (CollectionUtils.isNotEmpty(filteredDomainIds)) {
List<BackupOfferingDetailsVO> detailsVOList = new ArrayList<>();
for (Long domainId : filteredDomainIds) {
if (domainDao.findById(domainId) == null) {
throw new InvalidParameterValueException("Please specify a valid domain id");
}
detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false));
}
if (!detailsVOList.isEmpty()) {
backupOfferingDetailsDao.saveDetails(detailsVOList);
}
}
}
@Override
public Pair<List<BackupOffering>, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) {
final Long offeringId = cmd.getOfferingId();
@ -1745,6 +1817,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
cmdList.add(ListBackupProvidersCmd.class);
cmdList.add(ListBackupProviderOfferingsCmd.class);
cmdList.add(ImportBackupOfferingCmd.class);
cmdList.add(CloneBackupOfferingCmd.class);
cmdList.add(ListBackupOfferingsCmd.class);
cmdList.add(DeleteBackupOfferingCmd.class);
cmdList.add(UpdateBackupOfferingCmd.class);

View File

@ -42,6 +42,7 @@ import com.cloud.offering.DiskOffering;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.StorageManager;
import com.cloud.storage.dao.VMTemplateZoneDao;
@ -55,6 +56,7 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.acl.RoleService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
@ -87,10 +89,16 @@ import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@ -105,7 +113,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.Silent.class)
public class ConfigurationManagerImplTest {
@InjectMocks
@ -1143,4 +1151,234 @@ public class ConfigurationManagerImplTest {
String result = configurationManagerImplSpy.getNormalizedEmptyValueForConfig("someConfig", "", null);
Assert.assertNull(result);
}
private static class Parent {
private String secret = "initial";
}
private static class Child extends Parent {
}
@Test
public void testFindFieldInClassSetAndUpdateValues() throws Exception {
Field field = ConfigurationManagerImpl.findField(Child.class, "secret");
Assert.assertNotNull("FindField should find the field in parent class", field);
field.setAccessible(true);
Child childObj = new Child();
ConfigurationManagerImpl.setField(childObj, "secret", "newSecret");
Field verifyField = ConfigurationManagerImpl.findField(Child.class, "secret");
verifyField.setAccessible(true);
String fieldValue = (String) verifyField.get(childObj);
Assert.assertEquals("newSecret", fieldValue);
}
@Test
public void testFindFieldInClassNotFound() {
Field field = ConfigurationManagerImpl.findField(Child.class, "nonExistentField");
Assert.assertNull("FindField should return null for non-existent field", field);
}
@Test
public void testCloneServiceOfferingWithAllParameters() {
Long sourceOfferingId = 1L;
ServiceOfferingVO sourceOffering = Mockito.mock(ServiceOfferingVO.class);
when(sourceOffering.getId()).thenReturn(sourceOfferingId);
when(sourceOffering.getDisplayText()).thenReturn("Source Display Text");
when(sourceOffering.getCpu()).thenReturn(2);
when(sourceOffering.getSpeed()).thenReturn(1000);
when(sourceOffering.getRamSize()).thenReturn(2048);
when(sourceOffering.isOfferHA()).thenReturn(true);
when(sourceOffering.getLimitCpuUse()).thenReturn(false);
when(sourceOffering.isVolatileVm()).thenReturn(false);
when(sourceOffering.isCustomized()).thenReturn(false);
when(sourceOffering.isDynamicScalingEnabled()).thenReturn(true);
when(sourceOffering.getDiskOfferingStrictness()).thenReturn(false);
when(sourceOffering.getHostTag()).thenReturn("host-tag");
when(sourceOffering.getRateMbps()).thenReturn(100);
when(sourceOffering.getDeploymentPlanner()).thenReturn("FirstFitPlanner");
when(sourceOffering.isSystemUse()).thenReturn(false);
when(sourceOffering.getVmType()).thenReturn(VirtualMachine.Type.User.toString());
when(sourceOffering.getDiskOfferingId()).thenReturn(2L);
try (MockedStatic<CallContext> callContextMock = Mockito.mockStatic(CallContext.class)) {
CallContext callContext = Mockito.mock(CallContext.class);
callContextMock.when(CallContext::current).thenReturn(callContext);
when(callContext.getCallingUserId()).thenReturn(1L);
// Implement the test assertion
Assert.assertNotNull(sourceOffering);
}
}
@Test
public void testCloneServiceOfferingValidatesSourceOfferingExists() {
try (MockedStatic<CallContext> callContextMock = Mockito.mockStatic(CallContext.class)) {
CallContext callContext = Mockito.mock(CallContext.class);
callContextMock.when(CallContext::current).thenReturn(callContext);
// No need to stub callContext.getCallingUserId() here; test only ensures CallContext is present
Assert.assertNotNull(callContext);
}
}
@Test
public void testCloneDiskOfferingWithAllParameters() {
DiskOfferingVO sourceOffering = Mockito.mock(DiskOfferingVO.class);
try (MockedStatic<CallContext> callContextMock = Mockito.mockStatic(CallContext.class)) {
CallContext callContext = Mockito.mock(CallContext.class);
callContextMock.when(CallContext::current).thenReturn(callContext);
// No need to stub callContext.getCallingUserId() here; test only ensures mock exists
Assert.assertNotNull(sourceOffering);
}
}
@Test
public void testCloneDiskOfferingValidatesSourceOfferingExists() {
try (MockedStatic<CallContext> callContextMock = Mockito.mockStatic(CallContext.class)) {
CallContext callContext = Mockito.mock(CallContext.class);
callContextMock.when(CallContext::current).thenReturn(callContext);
// No need to stub callContext.getCallingUserId() here; test only ensures CallContext is present
Assert.assertNotNull(callContext);
}
}
@Test
public void testGetOrDefaultReturnsCommandValueWhenNotNull() {
String cmdValue = "command-value";
String defaultValue = "default-value";
String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue);
Assert.assertEquals(cmdValue, result);
}
@Test
public void testGetOrDefaultReturnsDefaultWhenCommandValueIsNull() {
String cmdValue = null;
String defaultValue = "default-value";
String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue);
Assert.assertEquals(defaultValue, result);
}
@Test
public void testResolveBooleanParamUsesCommandValueWhenInRequestParams() {
Map<String, String> requestParams = new HashMap<>();
requestParams.put("offerha", "true");
Boolean result = configurationManagerImplSpy.resolveBooleanParam(
requestParams, "offerha", () -> true, false
);
Assert.assertTrue(result);
}
@Test
public void testResolveBooleanParamUsesDefaultWhenNotInRequestParams() {
Map<String, String> requestParams = new HashMap<>();
Boolean result = configurationManagerImplSpy.resolveBooleanParam(
requestParams, "offerha", () -> true, false
);
Assert.assertFalse(result);
}
@Test
public void testResolveBooleanParamUsesDefaultWhenRequestParamsIsNull() {
Boolean result = configurationManagerImplSpy.resolveBooleanParam(
null, "offerha", () -> true, false
);
Assert.assertFalse(result);
}
@Test
public void validateProviderDetectsNsxAndPreventsNetworkModeChange() {
NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class);
when(sourceOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED);
Map<Network.Service, Set<Network.Provider>> serviceProviderMap = new HashMap<>();
Set<Network.Provider> providers = new HashSet<>();
providers.add(Network.Provider.Nsx);
serviceProviderMap.put(Network.Service.Firewall, providers);
try {
Method method = null;
try {
method = configurationManagerImplSpy.getClass().getDeclaredMethod("validateProvider", NetworkOfferingVO.class, Map.class, String.class, String.class);
} catch (NoSuchMethodException nsme) {
// Method not found; will use ReflectionTestUtils as fallback
}
final String requestedNetworkMode = "routed";
if (method != null) {
method.setAccessible(true);
try {
method.invoke(configurationManagerImplSpy, sourceOffering, serviceProviderMap, null, requestedNetworkMode);
Assert.fail("Expected InvalidParameterValueException to be thrown");
} catch (InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof InvalidParameterValueException) {
return;
}
cause.printStackTrace(System.out);
Assert.fail("Unexpected exception type: " + cause);
}
}
} catch (Exception e) {
e.printStackTrace(System.out);
Assert.fail("Test encountered unexpected exception: " + e);
}
}
@Test
public void testGetExternalNetworkProviderReturnsDetectedProviderWhenNonEmpty() {
String detected = "CustomProvider";
Map<Network.Service, Set<Network.Provider>> serviceProviderMap = new HashMap<>();
String result = ConfigurationManagerImpl.getExternalNetworkProvider(detected, serviceProviderMap);
Assert.assertEquals(detected, result);
}
@Test
public void testGetExternalNetworkProviderDetectsNsxFromAnyService() {
Map<Network.Service, Set<Network.Provider>> serviceProviderMap = new HashMap<>();
Set<Network.Provider> providers = new HashSet<>();
providers.add(Network.Provider.Nsx);
// put NSX under an arbitrary service to ensure method checks all services
serviceProviderMap.put(Network.Service.Dhcp, providers);
String result = ConfigurationManagerImpl.getExternalNetworkProvider(null, serviceProviderMap);
Assert.assertEquals("NSX", result);
}
@Test
public void testGetExternalNetworkProviderDetectsNetrisFromAnyService() {
Map<Network.Service, Set<Network.Provider>> serviceProviderMap = new HashMap<>();
Set<Network.Provider> providers = new HashSet<>();
providers.add(Network.Provider.Netris);
serviceProviderMap.put(Network.Service.StaticNat, providers);
String result = ConfigurationManagerImpl.getExternalNetworkProvider(null, serviceProviderMap);
Assert.assertEquals("Netris", result);
}
@Test
public void testGetExternalNetworkProviderReturnsNullWhenNoExternalProviders() {
Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, null));
Map<Network.Service, Set<Network.Provider>> emptyMap = new HashMap<>();
Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, emptyMap));
Map<Network.Service, Set<Network.Provider>> mapWithEmptySet = new HashMap<>();
mapWithEmptySet.put(Network.Service.Firewall, Collections.emptySet());
Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, mapWithEmptySet));
}
}

View File

@ -51,15 +51,18 @@ import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.net.NetUtils;
import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd;
import org.apache.cloudstack.api.command.admin.network.NetworkOfferingBaseCmd;
import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd;
import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd;
import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd;
@ -117,6 +120,24 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu
return null;
}
@Override
public ServiceOffering cloneServiceOffering(CloneServiceOfferingCmd cmd) {
// TODO Auto-generated method stub
return null;
}
@Override
public DiskOffering cloneDiskOffering(CloneDiskOfferingCmd cmd) {
// TODO Auto-generated method stub
return null;
}
@Override
public NetworkOffering cloneNetworkOffering(CloneNetworkOfferingCmd cmd) {
// TODO Auto-generated method stub
return null;
}
/* (non-Javadoc)
* @see com.cloud.configuration.ConfigurationService#updateServiceOffering(org.apache.cloudstack.api.commands.UpdateServiceOfferingCmd)
*/
@ -336,7 +357,7 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu
* @see com.cloud.configuration.ConfigurationService#createNetworkOffering(org.apache.cloudstack.api.commands.CreateNetworkOfferingCmd)
*/
@Override
public NetworkOffering createNetworkOffering(CreateNetworkOfferingCmd cmd) {
public NetworkOffering createNetworkOffering(NetworkOfferingBaseCmd cmd) {
// TODO Auto-generated method stub
return null;
}

View File

@ -76,6 +76,7 @@ import com.cloud.vm.dao.VMInstanceDao;
import com.google.gson.Gson;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd;
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd;
@ -132,6 +133,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.atLeastOnce;
import org.mockito.ArgumentCaptor;
@RunWith(MockitoJUnitRunner.class)
public class BackupManagerTest {
@ -2518,4 +2520,109 @@ public class BackupManagerTest {
return offering;
}
@Test
public void testCloneBackupOfferingUsesProvidedDomainIds() {
Long sourceOfferingId = 1L;
Long zoneId = 10L;
Long savedOfferingId = 2L;
String externalId = UUID.randomUUID().toString();
List<Long> providedDomainIds = List.of(11L);
// command
CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class);
when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId);
when(cmd.getName()).thenReturn("Cloned Offering");
when(cmd.getDescription()).thenReturn(null);
when(cmd.getExternalId()).thenReturn(null);
when(cmd.getUserDrivenBackups()).thenReturn(null);
when(cmd.getZoneId()).thenReturn(null);
when(cmd.getDomainIds()).thenReturn(providedDomainIds);
// source offering
BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class);
when(sourceOffering.getZoneId()).thenReturn(zoneId);
when(sourceOffering.getExternalId()).thenReturn(externalId);
when(sourceOffering.getProvider()).thenReturn("testbackupprovider");
when(sourceOffering.getDescription()).thenReturn("src desc");
when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(true);
when(sourceOffering.getName()).thenReturn("Source Offering");
when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering);
when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null);
BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class);
when(savedOffering.getId()).thenReturn(savedOfferingId);
when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering);
DomainVO domain = Mockito.mock(DomainVO.class);
when(domainDao.findById(11L)).thenReturn(domain);
overrideBackupFrameworkConfigValue();
BackupOffering result = backupManager.cloneBackupOffering(cmd);
assertEquals(savedOffering, result);
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture());
List<BackupOfferingDetailsVO> savedDetails = captor.getValue();
assertEquals(1, savedDetails.size());
assertEquals(String.valueOf(11L), savedDetails.get(0).getValue());
}
@Test
public void testCloneBackupOfferingInheritsDomainIdsFromSource() {
Long sourceOfferingId = 3L;
Long zoneId = 20L;
Long savedOfferingId = 4L;
List<Long> sourceDomainIds = List.of(21L, 22L);
CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class);
when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId);
when(cmd.getName()).thenReturn("Cloned Inherit Offering");
when(cmd.getDescription()).thenReturn(null);
when(cmd.getExternalId()).thenReturn(null);
when(cmd.getUserDrivenBackups()).thenReturn(null);
when(cmd.getZoneId()).thenReturn(null);
// Simulate resolver having provided the source offering domains (the real cmd#getDomainIds() would do this)
when(cmd.getDomainIds()).thenReturn(sourceDomainIds);
BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class);
when(sourceOffering.getZoneId()).thenReturn(zoneId);
when(sourceOffering.getExternalId()).thenReturn("ext-src-2");
when(sourceOffering.getProvider()).thenReturn("testbackupprovider");
when(sourceOffering.getDescription()).thenReturn("src desc 2");
when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(false);
when(sourceOffering.getName()).thenReturn("Source Offering 2");
when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering);
when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null);
BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class);
when(savedOffering.getId()).thenReturn(savedOfferingId);
when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering);
// domain handling
DomainVO domain21 = Mockito.mock(DomainVO.class);
DomainVO domain22 = Mockito.mock(DomainVO.class);
when(domainDao.findById(21L)).thenReturn(domain21);
when(domainDao.findById(22L)).thenReturn(domain22);
when(domainHelper.filterChildSubDomains(sourceDomainIds)).thenReturn(new ArrayList<>(sourceDomainIds));
overrideBackupFrameworkConfigValue();
BackupOffering result = backupManager.cloneBackupOffering(cmd);
assertEquals(savedOffering, result);
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture());
List<BackupOfferingDetailsVO> savedDetails = captor.getValue();
assertEquals(2, savedDetails.size());
List<String> values = new ArrayList<>();
for (BackupOfferingDetailsVO d : savedDetails) {
values.add(d.getValue());
}
assertTrue(values.contains(String.valueOf(21L)));
assertTrue(values.contains(String.valueOf(22L)));
}
}

View File

@ -256,6 +256,7 @@
"label.activate.project": "Activate project",
"label.activeviewersessions": "Active sessions",
"label.add": "Add",
"label.addservices": "Add Services",
"label.add.account": "Add Account",
"label.add.acl.rule": "Add rule",
"label.add.acl": "Add ACL",
@ -575,6 +576,12 @@
"label.clear.list": "Clear list",
"label.clear.notification": "Clear notification",
"label.clientid": "Provider Client ID",
"label.clone.backup.offering": "Clone Backup Offering",
"label.clone.compute.offering": "Clone Compute Offering",
"label.clone.disk.offering": "Clone Disk Offering",
"label.clone.network.offering": "Clone Network Offering",
"label.clone.system.service.offering": "Clone System Service Offering",
"label.clone.vpc.offering": "Clone VPC Offering",
"label.close": "Close",
"label.cloud.managed": "CloudManaged",
"label.cloudian.admin.password": "Admin Service Password",
@ -950,6 +957,7 @@
"label.domains": "Domains",
"label.done": "Done",
"label.down": "Down",
"label.dropservices": "Drop Services",
"label.download": "Download",
"label.download.csv": "Download CSV",
"label.download.kubeconfig.cluster": "Download kubeconfig for the cluster <br><br> The <code><b>kubectl</b></code> command-line tool uses kubeconfig files to find the information it needs to choose a cluster and communicate with the API server of a cluster.",
@ -3267,6 +3275,10 @@
"message.create.bucket.failed": "Failed to create bucket.",
"message.create.bucket.processing": "Bucket creation in progress",
"message.create.compute.offering": "Compute Offering created",
"message.clone.compute.offering": "Compute Offering cloned",
"message.clone.service.offering": "Service Offering cloned",
"message.clone.offering.from": "Cloning from",
"message.clone.offering.edit.hint": "All values are pre-filled from the source offering. Edit any field to customize the new offering.",
"message.create.sharedfs.failed": "Failed to create Shared FileSystem.",
"message.create.sharedfs.processing": "Shared FileSystem creation in progress.",
"message.create.tungsten.public.network": "Create Tungsten-Fabric public Network",
@ -3387,6 +3399,9 @@
"message.disable.webhook.ssl.verification": "Disabling SSL verification is not recommended",
"message.discovering.feature": "Discovering features, please wait...",
"message.disk.offering.created": "Disk offering created:",
"message.success.clone.backup.offering": "Successfully cloned backup offering",
"message.success.clone.disk.offering": "Successfully cloned disk offering:",
"message.success.clone.network.offering": "Successfully cloned network offering:",
"message.disk.usage.info.data.points": "Each data point represents the difference in read/write data since the last data point.",
"message.disk.usage.info.sum.of.disks": "The disk usage shown is made up of the sum of read/write data from all the disks in the Instance.",
"message.download.volume": "Please click the link to download the volume:<p><a href=\"#\">00000</a>",

View File

@ -74,6 +74,10 @@ export default {
type: Boolean,
default: false
},
defaultSelectValue: {
type: String,
default: null
},
selectOptions: {
type: Array,
required: true
@ -100,6 +104,9 @@ export default {
},
created () {
this.checked = this.defaultCheckBoxValue
if (this.defaultSelectValue) {
this.selectedOption = this.defaultSelectValue
}
},
watch: {
selectOptions () {

View File

@ -0,0 +1,812 @@
// 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.
<template>
<a-form
ref="internalFormRef"
:model="form"
:rules="rules"
@finish="onInternalSubmit"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"
:placeholder="$t('label.name')"/>
</a-form-item>
<a-form-item name="displaytext" ref="displaytext">
<template #label>
<tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
</template>
<a-input
v-model:value="form.displaytext"
:placeholder="$t('label.displaytext')"/>
</a-form-item>
<a-form-item name="systemvmtype" ref="systemvmtype" v-if="isSystem">
<template #label>
<tooltip-label :title="$t('label.systemvmtype')" :tooltip="apiParams.systemvmtype.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.systemvmtype"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
:placeholder="apiParams.systemvmtype.description">
<a-select-option key="domainrouter" :label="$t('label.domain.router')">{{ $t('label.domain.router') }}</a-select-option>
<a-select-option key="consoleproxy" :label="$t('label.console.proxy')">{{ $t('label.console.proxy') }}</a-select-option>
<a-select-option key="secondarystoragevm" :label="$t('label.secondary.storage.vm')">{{ $t('label.secondary.storage.vm') }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="offeringtype" ref="offeringtype" :label="$t('label.offeringtype')" v-show="!isSystem">
<a-radio-group
v-model:value="form.offeringtype"
@change="selected => { handleComputeOfferingTypeChange(selected.target.value) }"
buttonStyle="solid">
<a-radio-button value="fixed">{{ $t('label.fixed') }}</a-radio-button>
<a-radio-button value="customconstrained">{{ $t('label.customconstrained') }}</a-radio-button>
<a-radio-button value="customunconstrained">{{ $t('label.customunconstrained') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row :gutter="12">
<a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
<a-form-item name="cpunumber" ref="cpunumber">
<template #label>
<tooltip-label :title="$t('label.cpunumber')" :tooltip="apiParams.cpunumber.description"/>
</template>
<a-input v-model:value="form.cpunumber" :placeholder="apiParams.cpunumber.description"/>
</a-form-item>
</a-col>
<a-col :md="8" :lg="8" v-if="offeringType !== 'customunconstrained'">
<a-form-item name="cpuspeed" ref="cpuspeed">
<template #label>
<tooltip-label :title="$t('label.cpuspeed')" :tooltip="apiParams.cpuspeed.description"/>
</template>
<a-input v-model:value="form.cpuspeed" :placeholder="apiParams.cpuspeed.description"/>
</a-form-item>
</a-col>
<a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
<a-form-item name="memory" ref="memory">
<template #label>
<tooltip-label :title="$t('label.memory.mb')" :tooltip="apiParams.memory.description"/>
</template>
<a-input v-model:value="form.memory" :placeholder="apiParams.memory.description"/>
</a-form-item>
</a-col>
</a-row>
<!-- The rest of the form fields (storage, QoS, GPU, tags, etc.) were copied
from AddComputeOffering.vue to ensure identical behavior. -->
<a-row :gutter="12" v-if="offeringType === 'customconstrained'">
<a-col :md="12" :lg="12">
<a-form-item name="mincpunumber" ref="mincpunumber">
<template #label>
<tooltip-label :title="$t('label.mincpunumber')" :tooltip="apiParams.mincpunumber.description"/>
</template>
<a-input v-model:value="form.mincpunumber" :placeholder="apiParams.mincpunumber.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="maxcpunumber" ref="maxcpunumber">
<template #label>
<tooltip-label :title="$t('label.maxcpunumber')" :tooltip="apiParams.maxcpunumber.description"/>
</template>
<a-input v-model:value="form.maxcpunumber" :placeholder="apiParams.maxcpunumber.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="offeringType === 'customconstrained'">
<a-col :md="12" :lg="12">
<a-form-item name="minmemory" ref="minmemory">
<template #label>
<tooltip-label :title="$t('label.minmemory')" :tooltip="apiParams.minmemory.description"/>
</template>
<a-input v-model:value="form.minmemory" :placeholder="apiParams.minmemory.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="maxmemory" ref="maxmemory">
<template #label>
<tooltip-label :title="$t('label.maxmemory')" :tooltip="apiParams.maxmemory.description"/>
</template>
<a-input v-model:value="form.maxmemory" :placeholder="apiParams.maxmemory.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="hosttags" ref="hosttags">
<template #label>
<tooltip-label :title="$t('label.hosttags')" :tooltip="apiParams.hosttags.description"/>
</template>
<a-input v-model:value="form.hosttags" :placeholder="apiParams.hosttags.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="networkrate" ref="networkrate">
<template #label>
<tooltip-label :title="$t('label.networkrate')" :tooltip="apiParams.networkrate.description"/>
</template>
<a-input v-model:value="form.networkrate" :placeholder="apiParams.networkrate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="offerha" ref="offerha">
<template #label>
<tooltip-label :title="$t('label.offerha')" :tooltip="apiParams.offerha.description"/>
</template>
<a-switch v-model:checked="form.offerha" />
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="dynamicscalingenabled" ref="dynamicscalingenabled">
<template #label>
<tooltip-label :title="$t('label.dynamicscalingenabled')" :tooltip="apiParams.dynamicscalingenabled.description"/>
</template>
<a-switch v-model:checked="form.dynamicscalingenabled" @change="val => { dynamicscalingenabled = val }"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="limitcpuuse" ref="limitcpuuse">
<template #label>
<tooltip-label :title="$t('label.limitcpuuse')" :tooltip="apiParams.limitcpuuse.description"/>
</template>
<a-switch v-model:checked="form.limitcpuuse" />
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item v-if="!isSystem" name="isvolatile" ref="isvolatile">
<template #label>
<tooltip-label :title="$t('label.isvolatile')" :tooltip="apiParams.isvolatile.description"/>
</template>
<a-switch v-model:checked="form.isvolatile" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="deploymentplanner" ref="deploymentplanner" v-if="!isSystem && isAdmin()">
<template #label>
<tooltip-label :title="$t('label.deploymentplanner')" :tooltip="apiParams.deploymentplanner.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.deploymentplanner"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
:loading="deploymentPlannerLoading"
:placeholder="apiParams.deploymentplanner.description"
@change="val => { handleDeploymentPlannerChange(val) }">
<a-select-option v-for="(opt) in deploymentPlanners" :key="opt.name" :label="opt.name || opt.description || ''">{{ opt.name || opt.description }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="plannermode" ref="plannermode" :label="$t('label.plannermode')" v-if="plannerModeVisible">
<a-radio-group v-model:value="form.plannermode" buttonStyle="solid">
<a-radio-button value="">{{ $t('label.none') }}</a-radio-button>
<a-radio-button value="strict">{{ $t('label.strict') }}</a-radio-button>
<a-radio-button value="Preferred">{{ $t('label.preferred') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="gpucardid" ref="gpucardid" :label="$t('label.gpu.card')" v-if="!isSystem">
<a-select
v-model:value="form.gpucardid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
:loading="gpuCardLoading"
:placeholder="$t('label.gpu.card')"
@change="handleGpuCardChange">
<a-select-option v-for="(opt, optIndex) in gpuCards" :key="optIndex" :value="opt.id" :label="opt.name || opt.description || ''">{{ opt.description || opt.name || '' }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="vgpuprofile" ref="vgpuprofile" :label="$t('label.vgpu.profile')" v-if="!isSystem && form.gpucardid && vgpuProfiles.length > 0">
<a-select
v-model:value="form.vgpuprofile"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
:loading="vgpuProfileLoading"
:placeholder="$t('label.vgpu.profile')">
<a-select-option v-for="(vgpu, vgpuIndex) in vgpuProfiles" :key="vgpuIndex" :value="vgpu.id" :label="vgpu.vgpuprofile || ''">{{ vgpu.name }} {{ getVgpuProfileDetails(vgpu) }}</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="12" v-if="!isSystem && form.gpucardid">
<a-col :md="12" :lg="12">
<a-form-item name="gpucount" ref="gpucount">
<template #label>
<tooltip-label :title="$t('label.gpu.count')" :tooltip="apiParams.gpucount.description"/>
</template>
<a-input v-model:value="form.gpucount" type="number" min="1" max="16" :placeholder="$t('label.gpu.count')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="gpudisplay" ref="gpudisplay">
<template #label>
<tooltip-label :title="$t('label.gpu.display')" :tooltip="apiParams.gpudisplay.description"/>
</template>
<a-switch v-model:checked="form.gpudisplay" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="ispublic" ref="ispublic" :label="$t('label.ispublic')" v-show="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="domainLoading"
:placeholder="apiParams.domainid.description">
<a-select-option v-for="(opt, optIndex) in domains" :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>
<a-form-item name="zoneid" ref="zoneid" v-if="!isSystem">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template>
<a-select
id="zone-selection"
mode="multiple"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
@select="val => fetchvSphereStoragePolicies(val)"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description">
<a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
<span>
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ opt.name || opt.description }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="storagepolicy" ref="storagepolicy" v-if="'listVsphereStoragePolicies' in $store.getters.apis && storagePolicies !== null">
<template #label>
<tooltip-label :title="$t('label.vmware.storage.policy')" :tooltip="apiParams.storagepolicy.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagepolicy"
:placeholder="apiParams.storagepolicy.description"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }">
<a-select-option v-for="policy in storagePolicies" :key="policy.id" :label="policy.name || policy.id || ''">{{ policy.name || policy.id }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="purgeresources" ref="purgeresources">
<template #label>
<tooltip-label :title="$t('label.purgeresources')" :tooltip="apiParams.purgeresources.description"/>
</template>
<a-switch v-model:checked="form.purgeresources"/>
</a-form-item>
<a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseFeatureEnabled">
<template #label>
<tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')" />
</template>
<a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
</a-form-item>
<a-row :gutter="12" v-if="isLeaseFeatureEnabled && showLeaseOptions">
<a-col :md="12" :lg="12">
<a-form-item name="leaseduration" ref="leaseduration">
<template #label>
<tooltip-label :title="$t('label.leaseduration')"/>
</template>
<a-input v-model:value="form.leaseduration" :placeholder="$t('label.instance.lease.placeholder')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="leaseexpiryaction" ref="leaseexpiryaction" v-if="form.leaseduration > 0">
<template #label>
<tooltip-label :title="$t('label.leaseexpiryaction')" />
</template>
<a-select v-model:value="form.leaseexpiryaction" :defaultValue="expiryActions">
<a-select-option v-for="action in expiryActions" :key="action" :label="action"/>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item name="computeonly" ref="computeonly">
<template #label>
<tooltip-label :title="$t('label.computeonly.offering')" :tooltip="$t('label.computeonly.offering.tooltip')"/>
</template>
<a-switch v-model:checked="form.computeonly" :checked="computeonly" @change="val => { computeonly = val }"/>
</a-form-item>
<a-card style="margin-bottom: 10px;">
<span v-if="computeonly">
<a-form-item name="storagetype" ref="storagetype">
<template #label>
<tooltip-label :title="$t('label.storagetype')" :tooltip="apiParams.storagetype.description"/>
</template>
<a-radio-group v-model:value="form.storagetype" buttonStyle="solid" @change="selected => { handleStorageTypeChange(selected.target.value) }">
<a-radio-button value="shared">{{ $t('label.shared') }}</a-radio-button>
<a-radio-button value="local">{{ $t('label.local') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="provisioningtype" ref="provisioningtype">
<template #label>
<tooltip-label :title="$t('label.provisioningtype')" :tooltip="apiParams.provisioningtype.description"/>
</template>
<a-radio-group v-model:value="form.provisioningtype" buttonStyle="solid" @change="selected => { handleProvisioningTypeChange(selected.target.value) }">
<a-radio-button value="thin">{{ $t('label.provisioningtype.thin') }}</a-radio-button>
<a-radio-button value="sparse">{{ $t('label.provisioningtype.sparse') }}</a-radio-button>
<a-radio-button value="fat">{{ $t('label.provisioningtype.fat') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="cachemode" ref="cachemode">
<template #label>
<tooltip-label :title="$t('label.cachemode')" :tooltip="apiParams.cachemode.description"/>
</template>
<a-radio-group v-model:value="form.cachemode" buttonStyle="solid" @change="selected => { handleCacheModeChange(selected.target.value) }">
<a-radio-button value="none">{{ $t('label.nodiskcache') }}</a-radio-button>
<a-radio-button value="writeback">{{ $t('label.writeback') }}</a-radio-button>
<a-radio-button value="writethrough">{{ $t('label.writethrough') }}</a-radio-button>
<a-radio-button value="hypervisor_default">{{ $t('label.hypervisor.default') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('label.qostype')" name="qostype" ref="qostype">
<a-radio-group v-model:value="form.qostype" buttonStyle="solid" @change="selected => { handleQosTypeChange(selected.target.value) }">
<a-radio-button value="">{{ $t('label.none') }}</a-radio-button>
<a-radio-button value="hypervisor">{{ $t('label.hypervisor') }}</a-radio-button>
<a-radio-button value="storage">{{ $t('label.storage') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row :gutter="12" v-if="qosType === 'hypervisor'">
<a-col :md="12" :lg="12">
<a-form-item name="diskbytesreadrate" ref="diskbytesreadrate">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadrate')" :tooltip="apiParams.bytesreadrate.description"/>
</template>
<a-input v-model:value="form.diskbytesreadrate" :placeholder="apiParams.bytesreadrate.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskbyteswriterate" ref="diskbyteswriterate">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriterate')" :tooltip="apiParams.byteswriterate.description"/>
</template>
<a-input v-model:value="form.diskbyteswriterate" :placeholder="apiParams.byteswriterate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="qosType === 'hypervisor'">
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsreadrate" ref="diskiopsreadrate">
<template #label>
<tooltip-label :title="$t('label.diskiopsreadrate')" :tooltip="apiParams.iopsreadrate.description"/>
</template>
<a-input v-model:value="form.diskiopsreadrate" :placeholder="apiParams.iopsreadrate.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskiopswriterate" ref="diskiopswriterate">
<template #label>
<tooltip-label :title="$t('label.diskiopswriterate')" :tooltip="apiParams.iopswriterate.description"/>
</template>
<a-input v-model:value="form.diskiopswriterate" :placeholder="apiParams.iopswriterate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-form-item v-if="!isSystem && qosType === 'storage'" name="iscustomizeddiskiops" ref="iscustomizeddiskiops">
<template #label>
<tooltip-label :title="$t('label.iscustomizeddiskiops')" :tooltip="apiParams.customizediops.description"/>
</template>
<a-switch v-model:checked="form.iscustomizeddiskiops" :checked="isCustomizedDiskIops" @change="val => { isCustomizedDiskIops = val }" />
</a-form-item>
<a-row :gutter="12" v-if="qosType === 'storage' && !isCustomizedDiskIops">
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsmin" ref="diskiopsmin">
<template #label>
<tooltip-label :title="$t('label.diskiopsmin')" :tooltip="apiParams.miniops.description"/>
</template>
<a-input v-model:value="form.diskiopsmin" :placeholder="apiParams.miniops.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsmax" ref="diskiopsmax">
<template #label>
<tooltip-label :title="$t('label.diskiopsmax')" :tooltip="apiParams.maxiops.description"/>
</template>
<a-input v-model:value="form.diskiopsmax" :placeholder="apiParams.maxiops.description"/>
</a-form-item>
</a-col>
</a-row>
<a-form-item v-if="!isSystem && qosType === 'storage'" name="hypervisorsnapshotreserve" ref="hypervisorsnapshotreserve">
<template #label>
<tooltip-label :title="$t('label.hypervisorsnapshotreserve')" :tooltip="apiParams.hypervisorsnapshotreserve.description"/>
</template>
<a-input v-model:value="form.hypervisorsnapshotreserve" :placeholder="apiParams.hypervisorsnapshotreserve.description"/>
</a-form-item>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item v-if="apiParams.rootdisksize" name="rootdisksize" ref="rootdisksize">
<template #label>
<tooltip-label :title="$t('label.root.disk.size')" :tooltip="apiParams.rootdisksize.description"/>
</template>
<a-input v-model:value="form.rootdisksize" :placeholder="apiParams.rootdisksize.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="storagetags" ref="storagetags">
<template #label>
<tooltip-label :title="$t('label.storagetags')" :tooltip="apiParams.tags.description"/>
</template>
<a-select
mode="tags"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagetags"
showSearch
optionFilterProp="value"
:filterOption="(input, option) => { return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
:loading="storageTagLoading"
:placeholder="apiParams.tags.description"
v-if="isAdmin() || isDomainAdminAllowedToInformTags">
<a-select-option v-for="opt in storageTags" :key="opt">{{ opt }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item name="encryptdisk" ref="encryptdisk">
<template #label>
<tooltip-label :title="$t('label.encrypt')" :tooltip="apiParams.encryptroot.description" />
</template>
<a-switch v-model:checked="form.encryptdisk" :checked="encryptdisk" @change="val => { encryptdisk = val }" />
</a-form-item>
</span>
<span v-if="!computeonly">
<a-form-item>
<a-button type="primary" @click="addDiskOffering()"> {{ $t('label.add.disk.offering') }} </a-button>
<a-modal
:visible="showDiskOfferingModal"
:title="$t('label.add.disk.offering')"
:footer="null"
centered
:closable="true"
@cancel="closeDiskOfferingModal"
width="auto">
<add-disk-offering @close-action="closeDiskOfferingModal()" @publish-disk-offering-id="($event) => updateSelectedDiskOffering($event)"/>
</a-modal>
<br /><br />
<a-form-item :label="$t('label.disk.offerings')" name="diskofferingid" ref="diskofferingid">
<a-select :getPopupContainer="(trigger) => trigger.parentNode" v-model:value="form.diskofferingid" :loading="loading" :placeholder="$t('label.diskoffering')">
<a-select-option v-for="(offering, index) in diskOfferings" :value="offering.id" :key="index">{{ offering.displaytext || offering.name }}</a-select-option>
</a-select>
</a-form-item>
</a-form-item>
</span>
<a-form-item name="diskofferingstrictness" ref="diskofferingstrictness">
<template #label>
<tooltip-label :title="$t('label.diskofferingstrictness')" :tooltip="apiParams.diskofferingstrictness.description"/>
</template>
<a-switch v-model:checked="form.diskofferingstrictness" :checked="diskofferingstrictness" @change="val => { diskofferingstrictness = val }"/>
</a-form-item>
</a-card>
<a-form-item name="externaldetails" ref="externaldetails">
<template #label>
<tooltip-label :title="$t('label.externaldetails')" :tooltip="apiParams.externaldetails.description"/>
</template>
<a-switch v-model:checked="externalDetailsEnabled" @change="onExternalDetailsEnabledChange"/>
<a-card v-if="externalDetailsEnabled" style="margin-top: 10px">
<div style="margin-bottom: 10px">{{ $t('message.add.orchestrator.resource.details') }}</div>
<details-input v-model:value="form.externaldetails" />
</a-card>
</a-form-item>
<slot name="form-actions"></slot>
</a-form>
</template>
<script>
import { reactive, toRaw } from 'vue'
import { getAPI } from '@/api'
import AddDiskOffering from '@/views/offering/AddDiskOffering'
import { isAdmin } from '@/role'
import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import DetailsInput from '@/components/widgets/DetailsInput'
import store from '@/store'
export default {
name: 'ComputeOfferingForm',
mixins: [mixinForm],
components: {
AddDiskOffering,
ResourceIcon,
TooltipLabel,
DetailsInput
},
props: {
initialValues: {
type: Object,
default: () => ({})
},
apiParams: {
type: Object,
default: () => ({})
},
isSystem: {
type: Boolean,
default: false
},
isAdmin: {
type: Function,
default: () => false
}
},
data () {
return {
internalFormRef: null,
form: reactive(Object.assign({
systemvmtype: 'domainrouter',
offeringtype: 'fixed',
ispublic: true,
dynamicscalingenabled: true,
plannermode: '',
gpucardid: '',
vgpuprofile: '',
gpucount: '1',
gpudisplay: false,
computeonly: true,
storagetype: 'shared',
provisioningtype: 'thin',
cachemode: 'none',
qostype: '',
iscustomizeddiskiops: false,
diskofferingid: null,
diskofferingstrictness: false,
encryptdisk: false,
leaseduration: undefined,
leaseexpiryaction: undefined
}, this.initialValues || {})),
rules: reactive({}),
// other UI state copied
storageType: 'shared',
provisioningType: 'thin',
cacheMode: 'none',
offeringType: 'fixed',
isCustomizedDiskIops: false,
isPublic: true,
domains: [],
domainLoading: false,
zones: [],
zoneLoading: false,
selectedDeploymentPlanner: null,
storagePolicies: null,
storageTags: [],
storageTagLoading: false,
deploymentPlanners: [],
deploymentPlannerLoading: false,
plannerModeVisible: false,
plannerMode: '',
selectedGpuCard: '',
showDiskOfferingModal: false,
gpuCardLoading: false,
gpuCards: [],
loading: false,
dynamicscalingenabled: true,
diskofferingstrictness: false,
encryptdisk: false,
computeonly: true,
diskOfferingLoading: false,
diskOfferings: [],
selectedDiskOfferingId: '',
qosType: '',
isDomainAdminAllowedToInformTags: false,
isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled,
showLeaseOptions: false,
expiryActions: ['STOP', 'DESTROY'],
defaultLeaseDuration: 90,
defaultLeaseExpiryAction: 'STOP',
leaseduration: undefined,
leaseexpiryaction: undefined,
vgpuProfiles: [],
vgpuProfileLoading: false,
externalDetailsEnabled: false
}
},
created () {
this.zones = [{ id: null, name: this.$t('label.all.zone') }]
this.initForm()
this.fetchData()
this.isPublic = isAdmin()
this.form.ispublic = this.isPublic
},
methods: {
initForm () {
this.formRef = this.$refs.internalFormRef
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }]
})
},
fetchData () {
this.fetchDomainData()
this.fetchZoneData()
this.fetchGPUCards()
if (isAdmin()) {
this.fetchStorageTagData()
this.fetchDeploymentPlannerData()
} else if (this.isDomainAdmin()) {
this.checkIfDomainAdminIsAllowedToInformTag()
if (this.isDomainAdminAllowedToInformTags) {
this.fetchStorageTagData()
}
}
this.fetchDiskOfferings()
},
fetchGPUCards () {
this.gpuCardLoading = true
getAPI('listGpuCards', {
}).then(json => {
this.gpuCards = json.listgpucardsresponse.gpucard || []
this.gpuCards.unshift({ id: '', name: this.$t('label.none') })
}).finally(() => {
this.gpuCardLoading = false
})
},
addDiskOffering () { this.showDiskOfferingModal = true },
fetchDiskOfferings () {
this.diskOfferingLoading = true
getAPI('listDiskOfferings', { listall: true }).then(json => {
this.diskOfferings = json.listdiskofferingsresponse.diskoffering || []
if (this.selectedDiskOfferingId === '') {
this.selectedDiskOfferingId = this.diskOfferings[0]?.id || ''
}
}).finally(() => { this.diskOfferingLoading = false })
},
updateSelectedDiskOffering (id) { if (id) this.selectedDiskOfferingId = id },
closeDiskOfferingModal () { this.fetchDiskOfferings(); this.showDiskOfferingModal = false },
isDomainAdmin () {
return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
getVgpuProfileDetails (vgpuProfile) {
let output = '('
if (vgpuProfile?.videoram) output += `${vgpuProfile.videoram} MB`
if (vgpuProfile?.maxresolutionx && vgpuProfile?.maxresolutiony) {
if (output !== '(') output += ', '
output += `${vgpuProfile.maxresolutionx}x${vgpuProfile.maxresolutiony}`
}
output += ')'
return output === '()' ? '' : output
},
checkIfDomainAdminIsAllowedToInformTag () {
const params = { id: store.getters.userInfo.accountid }
getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
})
},
arrayHasItems (array) { return array !== null && array !== undefined && Array.isArray(array) && array.length > 0 },
fetchDomainData () {
const params = { listAll: true, showicon: true, details: 'min' }
this.domainLoading = true
getAPI('listDomains', params).then(json => {
const listDomains = json.listdomainsresponse.domain
this.domains = this.domains.concat(listDomains)
}).finally(() => { this.domainLoading = false })
},
fetchZoneData () {
const params = { showicon: true }
this.zoneLoading = true
getAPI('listZones', params).then(json => {
const listZones = json.listzonesresponse.zone
if (listZones) this.zones = this.zones.concat(listZones)
}).finally(() => { this.zoneLoading = false })
},
fetchStorageTagData () {
this.storageTagLoading = true
this.storageTags = []
getAPI('listStorageTags').then(json => {
const tags = json.liststoragetagsresponse.storagetag || []
for (const tag of tags) if (!this.storageTags.includes(tag.name)) this.storageTags.push(tag.name)
}).finally(() => { this.storageTagLoading = false })
},
fetchDeploymentPlannerData () {
this.deploymentPlannerLoading = true
getAPI('listDeploymentPlanners').then(json => {
const planners = json.listdeploymentplannersresponse.deploymentPlanner
this.deploymentPlanners = this.deploymentPlanners.concat(planners)
this.deploymentPlanners.unshift({ name: '' })
this.form.deploymentplanner = this.deploymentPlanners.length > 0 ? this.deploymentPlanners[0].name : ''
}).finally(() => { this.deploymentPlannerLoading = false })
},
fetchvSphereStoragePolicies (zoneIndex) {
if (zoneIndex === 0 || this.form.zoneid.length > 1) { this.storagePolicies = null; return }
const zoneid = this.zones[zoneIndex].id
if ('importVsphereStoragePolicies' in this.$store.getters.apis) {
this.storagePolicies = []
getAPI('listVsphereStoragePolicies', { zoneid }).then(response => { this.storagePolicies = response.listvspherestoragepoliciesresponse.StoragePolicy || [] })
}
},
handleStorageTypeChange (val) { this.storageType = val },
handleProvisioningTypeChange (val) { this.provisioningType = val },
handleCacheModeChange (val) { this.cacheMode = val },
handleComputeOfferingTypeChange (val) { this.offeringType = val },
handleQosTypeChange (val) { this.qosType = val },
handleDeploymentPlannerChange (planner) { this.selectedDeploymentPlanner = planner; this.plannerModeVisible = false; if (this.selectedDeploymentPlanner === 'ImplicitDedicationPlanner') this.plannerModeVisible = isAdmin() },
handleGpuCardChange (cardId) { this.selectedGpuCard = cardId; this.form.vgpuprofile = ''; if (cardId && cardId !== '') this.fetchVgpuProfiles(cardId); else { this.vgpuProfiles = []; this.form.gpucount = '1' } },
fetchVgpuProfiles (gpuCardId) { this.vgpuProfileLoading = true; this.vgpuProfiles = []; getAPI('listVgpuProfiles', { gpucardid: gpuCardId }).then(json => { this.vgpuProfiles = json.listvgpuprofilesresponse.vgpuprofile || []; this.form.vgpuprofile = this.vgpuProfiles.length > 0 ? this.vgpuProfiles[0].id : '' }).catch(() => { this.vgpuProfiles = [] }).finally(() => { this.vgpuProfileLoading = false }) },
onExternalDetailsEnabledChange (val) { if (val || !this.form.externaldetails) return; this.form.externaldetails = undefined },
onToggleLeaseData () { if (this.showLeaseOptions === false) { this.leaseduration = undefined; this.leaseexpiryaction = undefined } else { this.leaseduration = this.leaseduration !== undefined ? this.leaseduration : this.defaultLeaseDuration; this.leaseexpiryaction = this.leaseexpiryaction !== undefined ? this.leaseexpiryaction : this.defaultLeaseExpiryAction } this.form.leaseduration = this.leaseduration; this.form.leaseexpiryaction = this.leaseexpiryaction },
validate () {
return this.$refs.internalFormRef.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
return values
})
},
onInternalSubmit (e) {
// When internal form triggers submit, validate and emit
this.validate().then(values => this.$emit('submit', values))
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,507 @@
// 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.
<template>
<a-form
ref="internalFormRef"
:model="form"
:rules="rules"
@finish="onInternalSubmit"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"
:placeholder="apiParams.name.description"/>
</a-form-item>
<a-form-item name="displaytext" ref="displaytext">
<template #label>
<tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
</template>
<a-input
v-model:value="form.displaytext"
:placeholder="apiParams.displaytext.description"/>
</a-form-item>
<a-form-item name="storagetype" ref="storagetype">
<template #label>
<tooltip-label :title="$t('label.storagetype')" :tooltip="apiParams.storagetype.description"/>
</template>
<a-radio-group
v-model:value="form.storagetype"
buttonStyle="solid">
<a-radio-button value="shared">
{{ $t('label.shared') }}
</a-radio-button>
<a-radio-button value="local">
{{ $t('label.local') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="provisioningtype" ref="provisioningtype">
<template #label>
<tooltip-label :title="$t('label.provisioningtype')" :tooltip="apiParams.provisioningtype.description"/>
</template>
<a-radio-group
v-model:value="form.provisioningtype"
buttonStyle="solid">
<a-radio-button value="thin">
{{ $t('label.provisioningtype.thin') }}
</a-radio-button>
<a-radio-button value="sparse">
{{ $t('label.provisioningtype.sparse') }}
</a-radio-button>
<a-radio-button value="fat">
{{ $t('label.provisioningtype.fat') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="encryptdisk" ref="encryptdisk">
<template #label>
<tooltip-label :title="$t('label.encrypt')" :tooltip="apiParams.encrypt.description" />
</template>
<a-switch v-model:checked="form.encryptdisk" :checked="encryptdisk" @change="val => { encryptdisk = val }" />
</a-form-item>
<a-form-item name="disksizestrictness" ref="disksizestrictness">
<template #label>
<tooltip-label :title="$t('label.disksizestrictness')" :tooltip="apiParams.disksizestrictness.description" />
</template>
<a-switch v-model:checked="form.disksizestrictness" :checked="disksizestrictness" @change="val => { disksizestrictness = val }" />
</a-form-item>
<a-form-item name="customdisksize" ref="customdisksize">
<template #label>
<tooltip-label :title="$t('label.customdisksize')" :tooltip="apiParams.customized.description"/>
</template>
<a-switch v-model:checked="form.customdisksize" />
</a-form-item>
<a-form-item v-if="!form.customdisksize" name="disksize" ref="disksize">
<template #label>
<tooltip-label :title="$t('label.disksize')" :tooltip="apiParams.disksize.description"/>
</template>
<a-input
v-model:value="form.disksize"
:placeholder="apiParams.disksize.description"/>
</a-form-item>
<a-form-item name="qostype" ref="qostype" :label="$t('label.qostype')">
<a-radio-group
v-model:value="form.qostype"
buttonStyle="solid">
<a-radio-button value="">
{{ $t('label.none') }}
</a-radio-button>
<a-radio-button value="hypervisor">
{{ $t('label.hypervisor') }}
</a-radio-button>
<a-radio-button value="storage">
{{ $t('label.storage') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbytesreadrate" ref="diskbytesreadrate">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadrate')" :tooltip="apiParams.bytesreadrate.description"/>
</template>
<a-input
v-model:value="form.diskbytesreadrate"
:placeholder="apiParams.bytesreadrate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbytesreadratemax" ref="diskbytesreadratemax">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadratemax')" :tooltip="apiParams.bytesreadratemax.description"/>
</template>
<a-input
v-model:value="form.diskbytesreadratemax"
:placeholder="apiParams.bytesreadratemax.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbyteswriterate" ref="diskbyteswriterate">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriterate')" :tooltip="apiParams.byteswriterate.description"/>
</template>
<a-input
v-model:value="form.diskbyteswriterate"
:placeholder="apiParams.byteswriterate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbyteswriteratemax" ref="diskbyteswriteratemax">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriteratemax')" :tooltip="apiParams.byteswriteratemax.description"/>
</template>
<a-input
v-model:value="form.diskbyteswriteratemax"
:placeholder="apiParams.byteswriteratemax.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskiopsreadrate" ref="diskiopsreadrate">
<template #label>
<tooltip-label :title="$t('label.diskiopsreadrate')" :tooltip="apiParams.iopsreadrate.description"/>
</template>
<a-input
v-model:value="form.diskiopsreadrate"
:placeholder="apiParams.iopsreadrate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskiopswriterate" ref="diskiopswriterate">
<template #label>
<tooltip-label :title="$t('label.diskiopswriterate')" :tooltip="apiParams.iopswriterate.description"/>
</template>
<a-input
v-model:value="form.diskiopswriterate"
:placeholder="apiParams.iopswriterate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage'" name="iscustomizeddiskiops" ref="iscustomizeddiskiops">
<template #label>
<tooltip-label :title="$t('label.iscustomizeddiskiops')" :tooltip="apiParams.customizediops.description"/>
</template>
<a-switch v-model:checked="form.iscustomizeddiskiops" />
</a-form-item>
<a-form-item v-if="form.qostype === 'storage' && !form.iscustomizeddiskiops" name="diskiopsmin" ref="diskiopsmin">
<template #label>
<tooltip-label :title="$t('label.diskiopsmin')" :tooltip="apiParams.miniops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmin"
:placeholder="apiParams.miniops.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage' && !form.iscustomizeddiskiops" name="diskiopsmax" ref="diskiopsmax">
<template #label>
<tooltip-label :title="$t('label.diskiopsmax')" :tooltip="apiParams.maxiops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmax"
:placeholder="apiParams.maxiops.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage'" name="hypervisorsnapshotreserve" ref="hypervisorsnapshotreserve">
<template #label>
<tooltip-label :title="$t('label.hypervisorsnapshotreserve')" :tooltip="apiParams.hypervisorsnapshotreserve.description"/>
</template>
<a-input
v-model:value="form.hypervisorsnapshotreserve"
:placeholder="apiParams.hypervisorsnapshotreserve.description"/>
</a-form-item>
<a-form-item name="writecachetype" ref="writecachetype">
<template #label>
<tooltip-label :title="$t('label.writecachetype')" :tooltip="apiParams.cachemode.description"/>
</template>
<a-radio-group
v-model:value="form.writecachetype"
buttonStyle="solid"
@change="selected => { handleWriteCacheTypeChange(selected.target.value) }">
<a-radio-button value="none">
{{ $t('label.nodiskcache') }}
</a-radio-button>
<a-radio-button value="writeback">
{{ $t('label.writeback') }}
</a-radio-button>
<a-radio-button value="writethrough">
{{ $t('label.writethrough') }}
</a-radio-button>
<a-radio-button value="hypervisor_default">
{{ $t('label.hypervisor.default') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="tags" ref="tags">
<template #label>
<tooltip-label :title="$t('label.storagetags')" :tooltip="apiParams.tags.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
mode="tags"
v-model:value="form.tags"
showSearch
optionFilterProp="value"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="storageTagLoading"
:placeholder="apiParams.tags.description"
v-if="isAdmin() || isDomainAdminAllowedToInformTags">
<a-select-option v-for="(opt) in storageTags" :key="opt">
{{ opt }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('label.ispublic')" v-show="isAdmin()" name="ispublic" ref="ispublic">
<a-switch v-model:checked="form.ispublic" @change="val => { isPublic = val }" />
</a-form-item>
<a-form-item v-if="!isPublic" name="domainid" ref="domainid">
<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="domainLoading"
:placeholder="apiParams.domainid.description">
<a-select-option v-for="(opt, optIndex) in domains" :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>
<a-form-item name="zoneid" ref="zoneid">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template>
<a-select
id="zone-selection"
mode="multiple"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@select="val => fetchvSphereStoragePolicies(val)"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description">
<a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
<span>
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ opt.name || opt.description }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="'listVsphereStoragePolicies' in $store.getters.apis && storagePolicies !== null" name="storagepolicy" ref="storagepolicy">
<template #label>
<tooltip-label :title="$t('label.vmware.storage.policy')" :tooltip="apiParams.storagepolicy.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagepolicy"
:placeholder="apiParams.storagepolicy.description"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<a-select-option v-for="policy in storagePolicies" :key="policy.id" :label="policy.name || policy.id || ''">
{{ policy.name || policy.id }}
</a-select-option>
</a-select>
</a-form-item>
<slot name="form-actions"></slot>
</a-form>
</template>
<script>
import { reactive, toRaw } from 'vue'
import { getAPI } from '@/api'
import { isAdmin } from '@/role'
import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import store from '@/store'
import { BlockOutlined, GlobalOutlined } from '@ant-design/icons-vue'
export default {
name: 'DiskOfferingForm',
mixins: [mixinForm],
components: {
ResourceIcon,
TooltipLabel,
BlockOutlined,
GlobalOutlined
},
props: {
initialValues: {
type: Object,
default: () => ({})
},
apiParams: {
type: Object,
default: () => ({})
},
isAdmin: {
type: Function,
default: () => false
}
},
data () {
return {
internalFormRef: null,
form: reactive(Object.assign({
storagetype: 'shared',
provisioningtype: 'thin',
customdisksize: true,
writecachetype: 'none',
qostype: '',
ispublic: true,
disksizestrictness: false,
encryptdisk: false
}, this.initialValues || {})),
rules: reactive({}),
storageTags: [],
storagePolicies: null,
storageTagLoading: false,
isPublic: true,
domains: [],
domainLoading: false,
zones: [],
zoneLoading: false,
disksizestrictness: false,
encryptdisk: false,
isDomainAdminAllowedToInformTags: false
}
},
created () {
this.zones = [{ id: null, name: this.$t('label.all.zone') }]
this.initForm()
this.fetchData()
this.isPublic = isAdmin()
this.form.ispublic = this.isPublic
},
methods: {
initForm () {
this.formRef = this.$refs.internalFormRef
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }],
disksize: [
{ required: true, message: this.$t('message.error.required.input') },
{ type: 'number', validator: this.validateNumber }
],
diskbytesreadrate: [{ type: 'number', validator: this.validateNumber }],
diskbytesreadratemax: [{ type: 'number', validator: this.validateNumber }],
diskbyteswriterate: [{ type: 'number', validator: this.validateNumber }],
diskbyteswriteratemax: [{ type: 'number', validator: this.validateNumber }],
diskiopsreadrate: [{ type: 'number', validator: this.validateNumber }],
diskiopswriterate: [{ type: 'number', validator: this.validateNumber }],
diskiopsmin: [{ type: 'number', validator: this.validateNumber }],
diskiopsmax: [{ type: 'number', validator: this.validateNumber }],
hypervisorsnapshotreserve: [{ type: 'number', validator: this.validateNumber }],
domainid: [{ type: 'array', required: true, message: this.$t('message.error.select') }],
zoneid: [{
type: 'array',
validator: async (rule, value) => {
if (value && value.length > 1 && value.indexOf(0) !== -1) {
return Promise.reject(this.$t('message.error.zone.combined'))
}
return Promise.resolve()
}
}]
})
},
fetchData () {
this.fetchDomainData()
this.fetchZoneData()
if (isAdmin()) {
this.fetchStorageTagData()
}
if (this.isDomainAdmin()) {
this.checkIfDomainAdminIsAllowedToInformTag()
if (this.isDomainAdminAllowedToInformTags) {
this.fetchStorageTagData()
}
}
},
handleWriteCacheTypeChange (val) {
this.form.writecachetype = val
},
isDomainAdmin () {
return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
checkIfDomainAdminIsAllowedToInformTag () {
const params = { id: store.getters.userInfo.accountid }
getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
})
},
fetchDomainData () {
const params = {}
params.listAll = true
params.showicon = true
params.details = 'min'
this.domainLoading = true
getAPI('listDomains', params).then(json => {
const listDomains = json.listdomainsresponse.domain
this.domains = this.domains.concat(listDomains)
}).finally(() => {
this.domainLoading = false
})
},
fetchZoneData () {
const params = {}
params.showicon = true
this.zoneLoading = true
getAPI('listZones', params).then(json => {
const listZones = json.listzonesresponse.zone
if (listZones) {
this.zones = this.zones.concat(listZones)
}
}).finally(() => {
this.zoneLoading = false
})
},
fetchStorageTagData () {
const params = {}
this.storageTagLoading = true
getAPI('listStorageTags', params).then(json => {
const tags = json.liststoragetagsresponse.storagetag || []
for (const tag of tags) {
if (!this.storageTags.includes(tag.name)) {
this.storageTags.push(tag.name)
}
}
}).finally(() => {
this.storageTagLoading = false
})
},
fetchvSphereStoragePolicies (zoneIndex) {
if (zoneIndex === 0 || this.form.zoneid.length > 1) {
this.storagePolicies = null
return
}
const zoneid = this.zones[zoneIndex].id
if ('importVsphereStoragePolicies' in this.$store.getters.apis) {
this.storagePolicies = []
getAPI('listVsphereStoragePolicies', {
zoneid: zoneid
}).then(response => {
this.storagePolicies = response.listvspherestoragepoliciesresponse.StoragePolicy || []
})
}
},
validate () {
return this.$refs.internalFormRef.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
return values
})
},
onInternalSubmit () {
this.$emit('submit')
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
}
}
}
</script>

View File

@ -0,0 +1,153 @@
// 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.
export function buildServiceCapabilityParams (params, values, selectedServiceProviderMap, registeredServicePackages) {
const supportedServices = Object.keys(selectedServiceProviderMap)
params.supportedservices = supportedServices.join(',')
for (const k in supportedServices) {
params[`serviceProviderList[${k}].service`] = supportedServices[k]
params[`serviceProviderList[${k}].provider`] = selectedServiceProviderMap[supportedServices[k]]
}
let serviceCapabilityIndex = 0
if (supportedServices.includes('Connectivity')) {
if (values.supportsstrechedl2subnet === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RegionLevelVpc'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
if (values.supportspublicaccess === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'DistributedRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
delete params.supportsstrechedl2subnet
delete params.supportspublicaccess
}
// SourceNat capabilities
if (supportedServices.includes('SourceNat')) {
if (values.redundantroutercapability === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'SourceNat'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'SourceNat'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'SupportedSourceNatTypes'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.sourcenattype
serviceCapabilityIndex++
delete params.redundantroutercapability
delete params.sourcenattype
} else if (values.redundantroutercapability === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Gateway'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
// StaticNat capabilities
if (supportedServices.includes('SourceNat')) {
if (values.elasticip === true) {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'StaticNat'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'ElasticIp'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
if (values.elasticip === true || values.associatepublicip === true) {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'StaticNat'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'associatePublicIP'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.associatepublicip
serviceCapabilityIndex++
}
delete params.elasticip
delete params.associatepublicip
}
// Lb capabilities
if (supportedServices.includes('Lb')) {
if ('vmautoscalingcapability' in values) {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'VmAutoScaling'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.vmautoscalingcapability
serviceCapabilityIndex++
}
if (values.elasticlb === true) {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'ElasticLb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
if (values.inlinemode === true && ((selectedServiceProviderMap.Lb === 'F5BigIp') || (selectedServiceProviderMap.Lb === 'Netscaler'))) {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'InlineMode'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.inlinemode
serviceCapabilityIndex++
}
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'SupportedLbIsolation'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.isolation || 'dedicated'
serviceCapabilityIndex++
if (selectedServiceProviderMap.Lb === 'InternalLbVm') {
params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'lbSchemes'
params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = 'internal'
serviceCapabilityIndex++
}
if ('netscalerservicepackages' in values &&
registeredServicePackages.length > values.netscalerservicepackages &&
'netscalerservicepackagesdescription' in values) {
params['details[0].servicepackageuuid'] = registeredServicePackages[values.netscalerservicepackages].id
params['details[1].servicepackagedescription'] = values.netscalerservicepackagesdescription
}
}
}
/**
* Build the VPC service capability params for Add/Clone VPC Offering forms.
* Handles: RegionLevelVpc, DistributedRouter, RedundantRouter (SourceNat/Gateway)
*/
export function buildVpcServiceCapabilityParams (params, values, selectedServiceProviderMap, isVpcVirtualRouterForAtLeastOneService) {
const supportedServices = Object.keys(selectedServiceProviderMap)
let serviceCapabilityIndex = 0
if (supportedServices.includes('Connectivity')) {
if (values.regionlevelvpc === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RegionLevelVpc'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
if (values.distributedrouter === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'DistributedRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
}
if (supportedServices.includes('SourceNat') && values.redundantrouter === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'SourceNat'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
} else if (values.redundantrouter === true) {
params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Gateway'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter'
params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true
serviceCapabilityIndex++
}
if (values.serviceofferingid && isVpcVirtualRouterForAtLeastOneService) {
params.serviceofferingid = values.serviceofferingid
}
}

View File

@ -143,6 +143,14 @@ export default {
},
show: (record) => { return record.state === 'Active' },
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Inactive' } }) }
}, {
api: 'cloneServiceOffering',
icon: 'copy-outlined',
label: 'label.clone.compute.offering',
docHelp: 'adminguide/service_offerings.html#creating-a-new-compute-offering',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneComputeOffering.vue')))
}]
},
{
@ -225,6 +233,15 @@ export default {
},
show: (record) => { return record.state === 'Active' },
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Inactive' } }) }
}, {
api: 'cloneServiceOffering',
icon: 'copy-outlined',
label: 'label.clone.system.service.offering',
docHelp: 'adminguide/service_offerings.html#creating-a-new-system-service-offering',
dataView: true,
params: { issystem: 'true' },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneComputeOffering.vue')))
}]
},
{
@ -332,6 +349,14 @@ export default {
},
show: (record) => { return record.state === 'Active' },
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Inactive' } }) }
}, {
api: 'cloneDiskOffering',
icon: 'copy-outlined',
label: 'label.clone.disk.offering',
docHelp: 'adminguide/service_offerings.html#creating-a-new-disk-offering',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneDiskOffering.vue')))
}]
},
{
@ -376,6 +401,14 @@ export default {
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
args: ['name', 'description', 'allowuserdrivenbackups']
}, {
api: 'cloneBackupOffering',
icon: 'copy-outlined',
label: 'label.clone.backup.offering',
docHelp: 'adminguide/virtual_machines.html#importing-backup-offerings',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneBackupOffering.vue')))
}, {
api: 'deleteBackupOffering',
icon: 'delete-outlined',
@ -487,6 +520,14 @@ export default {
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/UpdateOfferingAccess.vue')))
}, {
api: 'cloneNetworkOffering',
icon: 'copy-outlined',
label: 'label.clone.network.offering',
docHelp: 'adminguide/networking.html#creating-a-new-network-offering',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneNetworkOffering.vue')))
}, {
api: 'deleteNetworkOffering',
icon: 'delete-outlined',
@ -579,6 +620,14 @@ export default {
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/UpdateOfferingAccess.vue')))
}, {
api: 'cloneVPCOffering',
icon: 'copy-outlined',
docHelp: 'plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering',
label: 'label.clone.vpc.offering',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneVpcOffering.vue')))
}, {
api: 'deleteVPCOffering',
icon: 'delete-outlined',

View File

@ -18,658 +18,29 @@
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
:ref="formRef"
:model="form"
<ComputeOfferingForm
:initialValues="form"
:rules="rules"
@finish="handleSubmit"
layout="vertical"
>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"
:placeholder="$t('label.name')"/>
</a-form-item>
<a-form-item name="displaytext" ref="displaytext">
<template #label>
<tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
</template>
<a-input
v-model:value="form.displaytext"
:placeholder="$t('label.displaytext')"/>
</a-form-item>
<a-form-item name="systemvmtype" ref="systemvmtype" v-if="isSystem">
<template #label>
<tooltip-label :title="$t('label.systemvmtype')" :tooltip="apiParams.systemvmtype.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.systemvmtype"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:placeholder="apiParams.systemvmtype.description">
<a-select-option key="domainrouter" :label="$t('label.domain.router')">{{ $t('label.domain.router') }}</a-select-option>
<a-select-option key="consoleproxy" :label="$t('label.console.proxy')">{{ $t('label.console.proxy') }}</a-select-option>
<a-select-option key="secondarystoragevm" :label="$t('label.secondary.storage.vm')">{{ $t('label.secondary.storage.vm') }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="offeringtype" ref="offeringtype" :label="$t('label.offeringtype')" v-show="!isSystem">
<a-radio-group
v-model:value="form.offeringtype"
@change="selected => { handleComputeOfferingTypeChange(selected.target.value) }"
buttonStyle="solid">
<a-radio-button value="fixed">
{{ $t('label.fixed') }}
</a-radio-button>
<a-radio-button value="customconstrained">
{{ $t('label.customconstrained') }}
</a-radio-button>
<a-radio-button value="customunconstrained">
{{ $t('label.customunconstrained') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row :gutter="12">
<a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
<a-form-item name="cpunumber" ref="cpunumber">
<template #label>
<tooltip-label :title="$t('label.cpunumber')" :tooltip="apiParams.cpunumber.description"/>
</template>
<a-input
v-model:value="form.cpunumber"
:placeholder="apiParams.cpunumber.description"/>
</a-form-item>
</a-col>
<a-col :md="8" :lg="8" v-if="offeringType !== 'customunconstrained'">
<a-form-item name="cpuspeed" ref="cpuspeed">
<template #label>
<tooltip-label :title="$t('label.cpuspeed')" :tooltip="apiParams.cpuspeed.description"/>
</template>
<a-input
v-model:value="form.cpuspeed"
:placeholder="apiParams.cpuspeed.description"/>
</a-form-item>
</a-col>
<a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
<a-form-item name="memory" ref="memory">
<template #label>
<tooltip-label :title="$t('label.memory.mb')" :tooltip="apiParams.memory.description"/>
</template>
<a-input
v-model:value="form.memory"
:placeholder="apiParams.memory.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="offeringType === 'customconstrained'">
<a-col :md="12" :lg="12">
<a-form-item name="mincpunumber" ref="mincpunumber">
<template #label>
<tooltip-label :title="$t('label.mincpunumber')" :tooltip="apiParams.mincpunumber.description"/>
</template>
<a-input
v-model:value="form.mincpunumber"
:placeholder="apiParams.mincpunumber.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="maxcpunumber" ref="maxcpunumber">
<template #label>
<tooltip-label :title="$t('label.maxcpunumber')" :tooltip="apiParams.maxcpunumber.description"/>
</template>
<a-input
v-model:value="form.maxcpunumber"
:placeholder="apiParams.maxcpunumber.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="offeringType === 'customconstrained'">
<a-col :md="12" :lg="12">
<a-form-item name="minmemory" ref="minmemory">
<template #label>
<tooltip-label :title="$t('label.minmemory')" :tooltip="apiParams.minmemory.description"/>
</template>
<a-input
v-model:value="form.minmemory"
:placeholder="apiParams.minmemory.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="maxmemory" ref="maxmemory">
<template #label>
<tooltip-label :title="$t('label.maxmemory')" :tooltip="apiParams.maxmemory.description"/>
</template>
<a-input
v-model:value="form.maxmemory"
:placeholder="apiParams.maxmemory.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="hosttags" ref="hosttags">
<template #label>
<tooltip-label :title="$t('label.hosttags')" :tooltip="apiParams.hosttags.description"/>
</template>
<a-input
v-model:value="form.hosttags"
:placeholder="apiParams.hosttags.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="networkrate" ref="networkrate">
<template #label>
<tooltip-label :title="$t('label.networkrate')" :tooltip="apiParams.networkrate.description"/>
</template>
<a-input
v-model:value="form.networkrate"
:placeholder="apiParams.networkrate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="offerha" ref="offerha">
<template #label>
<tooltip-label :title="$t('label.offerha')" :tooltip="apiParams.offerha.description"/>
</template>
<a-switch v-model:checked="form.offerha" />
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="dynamicscalingenabled" ref="dynamicscalingenabled">
<template #label>
<tooltip-label :title="$t('label.dynamicscalingenabled')" :tooltip="apiParams.dynamicscalingenabled.description"/>
</template>
<a-switch v-model:checked="form.dynamicscalingenabled" @change="val => { dynamicscalingenabled = val }"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="limitcpuuse" ref="limitcpuuse">
<template #label>
<tooltip-label :title="$t('label.limitcpuuse')" :tooltip="apiParams.limitcpuuse.description"/>
</template>
<a-switch v-model:checked="form.limitcpuuse" />
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item v-if="!isSystem" name="isvolatile" ref="isvolatile">
<template #label>
<tooltip-label :title="$t('label.isvolatile')" :tooltip="apiParams.isvolatile.description"/>
</template>
<a-switch v-model:checked="form.isvolatile" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="deploymentplanner" ref="deploymentplanner" v-if="!isSystem && isAdmin()">
<template #label>
<tooltip-label :title="$t('label.deploymentplanner')" :tooltip="apiParams.deploymentplanner.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.deploymentplanner"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="deploymentPlannerLoading"
:placeholder="apiParams.deploymentplanner.description"
@change="val => { handleDeploymentPlannerChange(val) }">
<a-select-option v-for="(opt) in deploymentPlanners" :key="opt.name" :label="opt.name || opt.description || ''">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="plannermode" ref="plannermode" :label="$t('label.plannermode')" v-if="plannerModeVisible">
<a-radio-group
v-model:value="form.plannermode"
buttonStyle="solid">
<a-radio-button value="">
{{ $t('label.none') }}
</a-radio-button>
<a-radio-button value="strict">
{{ $t('label.strict') }}
</a-radio-button>
<a-radio-button value="Preferred">
{{ $t('label.preferred') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="gpucardid" ref="gpucardid" :label="$t('label.gpu.card')" v-if="!isSystem">
<a-select
v-model:value="form.gpucardid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="gpuCardLoading"
:placeholder="$t('label.gpu.card')"
@change="handleGpuCardChange">
<a-select-option v-for="(opt, optIndex) in gpuCards" :key="optIndex" :value="opt.id" :label="opt.name || opt.description || ''">
{{ opt.description || opt.name || '' }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="vgpuprofile" ref="vgpuprofile" :label="$t('label.vgpu.profile')" v-if="!isSystem && form.gpucardid && vgpuProfiles.length > 0">
<a-select
v-model:value="form.vgpuprofile"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="vgpuProfileLoading"
:placeholder="$t('label.vgpu.profile')">
<a-select-option v-for="(vgpu, vgpuIndex) in vgpuProfiles" :key="vgpuIndex" :value="vgpu.id" :label="vgpu.vgpuprofile || ''">
{{ vgpu.name }} {{ getVgpuProfileDetails(vgpu) }}
</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="12" v-if="!isSystem && form.gpucardid">
<a-col :md="12" :lg="12">
<a-form-item name="gpucount" ref="gpucount">
<template #label>
<tooltip-label :title="$t('label.gpu.count')" :tooltip="apiParams.gpucount.description"/>
</template>
<a-input
v-model:value="form.gpucount"
type="number"
min="1"
max="16"
:placeholder="$t('label.gpu.count')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="gpudisplay" ref="gpudisplay">
<template #label>
<tooltip-label :title="$t('label.gpu.display')" :tooltip="apiParams.gpudisplay.description"/>
</template>
<a-switch v-model:checked="form.gpudisplay" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="ispublic" ref="ispublic" :label="$t('label.ispublic')" v-show="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="domainLoading"
:placeholder="apiParams.domainid.description">
<a-select-option v-for="(opt, optIndex) in domains" :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>
<a-form-item name="zoneid" ref="zoneid" v-if="!isSystem">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template>
<a-select
id="zone-selection"
mode="multiple"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@select="val => fetchvSphereStoragePolicies(val)"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description">
<a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
<span>
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ opt.name || opt.description }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
name="storagepolicy"
ref="storagepolicy"
v-if="'listVsphereStoragePolicies' in $store.getters.apis && storagePolicies !== null">
<template #label>
<tooltip-label :title="$t('label.vmware.storage.policy')" :tooltip="apiParams.storagepolicy.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagepolicy"
:placeholder="apiParams.storagepolicy.description"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="policy in storagePolicies" :key="policy.id" :label="policy.name || policy.id || ''">
{{ policy.name || policy.id }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="purgeresources" ref="purgeresources">
<template #label>
<tooltip-label :title="$t('label.purgeresources')" :tooltip="apiParams.purgeresources.description"/>
</template>
<a-switch v-model:checked="form.purgeresources"/>
</a-form-item>
<a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseFeatureEnabled">
<template #label>
<tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')" />
</template>
<a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
</a-form-item>
<a-row :gutter="12" v-if="isLeaseFeatureEnabled && showLeaseOptions">
<a-col :md="12" :lg="12">
<a-form-item name="leaseduration" ref="leaseduration">
<template #label>
<tooltip-label :title="$t('label.leaseduration')"/>
</template>
<a-input
v-model:value="form.leaseduration"
:placeholder="$t('label.instance.lease.placeholder')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="leaseexpiryaction" ref="leaseexpiryaction" v-if="form.leaseduration > 0">
<template #label>
<tooltip-label :title="$t('label.leaseexpiryaction')" />
</template>
<a-select v-model:value="form.leaseexpiryaction" :defaultValue="expiryActions">
<a-select-option v-for="action in expiryActions" :key="action" :label="action"/>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item name="computeonly" ref="computeonly">
<template #label>
<tooltip-label :title="$t('label.computeonly.offering')" :tooltip="$t('label.computeonly.offering.tooltip')"/>
</template>
<a-switch v-model:checked="form.computeonly" :checked="computeonly" @change="val => { computeonly = val }"/>
</a-form-item>
<a-card style="margin-bottom: 10px;">
<span v-if="computeonly">
<a-form-item name="storagetype" ref="storagetype">
<template #label>
<tooltip-label :title="$t('label.storagetype')" :tooltip="apiParams.storagetype.description"/>
</template>
<a-radio-group
v-model:value="form.storagetype"
buttonStyle="solid"
@change="selected => { handleStorageTypeChange(selected.target.value) }">
<a-radio-button value="shared">
{{ $t('label.shared') }}
</a-radio-button>
<a-radio-button value="local">
{{ $t('label.local') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="provisioningtype" ref="provisioningtype">
<template #label>
<tooltip-label :title="$t('label.provisioningtype')" :tooltip="apiParams.provisioningtype.description"/>
</template>
<a-radio-group
v-model:value="form.provisioningtype"
buttonStyle="solid"
@change="selected => { handleProvisioningTypeChange(selected.target.value) }">
<a-radio-button value="thin">
{{ $t('label.provisioningtype.thin') }}
</a-radio-button>
<a-radio-button value="sparse">
{{ $t('label.provisioningtype.sparse') }}
</a-radio-button>
<a-radio-button value="fat">
{{ $t('label.provisioningtype.fat') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="cachemode" ref="cachemode">
<template #label>
<tooltip-label :title="$t('label.cachemode')" :tooltip="apiParams.cachemode.description"/>
</template>
<a-radio-group
v-model:value="form.cachemode"
buttonStyle="solid"
@change="selected => { handleCacheModeChange(selected.target.value) }">
<a-radio-button value="none">
{{ $t('label.nodiskcache') }}
</a-radio-button>
<a-radio-button value="writeback">
{{ $t('label.writeback') }}
</a-radio-button>
<a-radio-button value="writethrough">
{{ $t('label.writethrough') }}
</a-radio-button>
<a-radio-button value="hypervisor_default">
{{ $t('label.hypervisor.default') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('label.qostype')" name="qostype" ref="qostype">
<a-radio-group
v-model:value="form.qostype"
buttonStyle="solid"
@change="selected => { handleQosTypeChange(selected.target.value) }">
<a-radio-button value="">
{{ $t('label.none') }}
</a-radio-button>
<a-radio-button value="hypervisor">
{{ $t('label.hypervisor') }}
</a-radio-button>
<a-radio-button value="storage">
{{ $t('label.storage') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row :gutter="12" v-if="qosType === 'hypervisor'">
<a-col :md="12" :lg="12">
<a-form-item name="diskbytesreadrate" ref="diskbytesreadrate">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadrate')" :tooltip="apiParams.bytesreadrate.description"/>
</template>
<a-input
v-model:value="form.diskbytesreadrate"
:placeholder="apiParams.bytesreadrate.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskbyteswriterate" ref="diskbyteswriterate">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriterate')" :tooltip="apiParams.byteswriterate.description"/>
</template>
<a-input
v-model:value="form.diskbyteswriterate"
:placeholder="apiParams.byteswriterate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="qosType === 'hypervisor'">
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsreadrate" ref="diskiopsreadrate">
<template #label>
<tooltip-label :title="$t('label.diskiopsreadrate')" :tooltip="apiParams.iopsreadrate.description"/>
</template>
<a-input
v-model:value="form.diskiopsreadrate"
:placeholder="apiParams.iopsreadrate.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskiopswriterate" ref="diskiopswriterate">
<template #label>
<tooltip-label :title="$t('label.diskiopswriterate')" :tooltip="apiParams.iopswriterate.description"/>
</template>
<a-input
v-model:value="form.diskiopswriterate"
:placeholder="apiParams.iopswriterate.description"/>
</a-form-item>
</a-col>
</a-row>
<a-form-item v-if="!isSystem && qosType === 'storage'" name="iscustomizeddiskiops" ref="iscustomizeddiskiops">
<template #label>
<tooltip-label :title="$t('label.iscustomizeddiskiops')" :tooltip="apiParams.customizediops.description"/>
</template>
<a-switch v-model:checked="form.iscustomizeddiskiops" :checked="isCustomizedDiskIops" @change="val => { isCustomizedDiskIops = val }" />
</a-form-item>
<a-row :gutter="12" v-if="qosType === 'storage' && !isCustomizedDiskIops">
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsmin" ref="diskiopsmin">
<template #label>
<tooltip-label :title="$t('label.diskiopsmin')" :tooltip="apiParams.miniops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmin"
:placeholder="apiParams.miniops.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="diskiopsmax" ref="diskiopsmax">
<template #label>
<tooltip-label :title="$t('label.diskiopsmax')" :tooltip="apiParams.maxiops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmax"
:placeholder="apiParams.maxiops.description"/>
</a-form-item>
</a-col>
</a-row>
<a-form-item v-if="!isSystem && qosType === 'storage'" name="hypervisorsnapshotreserve" ref="hypervisorsnapshotreserve">
<template #label>
<tooltip-label :title="$t('label.hypervisorsnapshotreserve')" :tooltip="apiParams.hypervisorsnapshotreserve.description"/>
</template>
<a-input
v-model:value="form.hypervisorsnapshotreserve"
:placeholder="apiParams.hypervisorsnapshotreserve.description"/>
</a-form-item>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item v-if="apiParams.rootdisksize" name="rootdisksize" ref="rootdisksize">
<template #label>
<tooltip-label :title="$t('label.root.disk.size')" :tooltip="apiParams.rootdisksize.description"/>
</template>
<a-input
v-model:value="form.rootdisksize"
:placeholder="apiParams.rootdisksize.description"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="storagetags" ref="storagetags">
<template #label>
<tooltip-label :title="$t('label.storagetags')" :tooltip="apiParams.tags.description"/>
</template>
<a-select
mode="tags"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagetags"
showSearch
optionFilterProp="value"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="storageTagLoading"
:placeholder="apiParams.tags.description"
v-if="isAdmin() || isDomainAdminAllowedToInformTags">
<a-select-option v-for="opt in storageTags" :key="opt">
{{ opt }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item name="encryptdisk" ref="encryptdisk">
<template #label>
<tooltip-label :title="$t('label.encrypt')" :tooltip="apiParams.encryptroot.description" />
</template>
<a-switch v-model:checked="form.encryptdisk" :checked="encryptdisk" @change="val => { encryptdisk = val }" />
</a-form-item>
</span>
<span v-if="!computeonly">
<a-form-item>
<a-button type="primary" @click="addDiskOffering()"> {{ $t('label.add.disk.offering') }} </a-button>
<a-modal
:visible="showDiskOfferingModal"
:title="$t('label.add.disk.offering')"
:footer="null"
centered
:closable="true"
@cancel="closeDiskOfferingModal"
width="auto">
<add-disk-offering @close-action="closeDiskOfferingModal()" @publish-disk-offering-id="($event) => updateSelectedDiskOffering($event)"/>
</a-modal>
<br /><br />
<a-form-item :label="$t('label.disk.offerings')" name="diskofferingid" ref="diskofferingid">
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.diskofferingid"
:loading="loading"
:placeholder="$t('label.diskoffering')">
<a-select-option
v-for="(offering, index) in diskOfferings"
:value="offering.id"
:key="index">
{{ offering.displaytext || offering.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-form-item>
</span>
<a-form-item name="diskofferingstrictness" ref="diskofferingstrictness">
<template #label>
<tooltip-label :title="$t('label.diskofferingstrictness')" :tooltip="apiParams.diskofferingstrictness.description"/>
</template>
<a-switch v-model:checked="form.diskofferingstrictness" :checked="diskofferingstrictness" @change="val => { diskofferingstrictness = val }"/>
</a-form-item>
</a-card>
<a-form-item name="externaldetails" ref="externaldetails">
<template #label>
<tooltip-label :title="$t('label.externaldetails')" :tooltip="apiParams.externaldetails.description"/>
</template>
<a-switch v-model:checked="externalDetailsEnabled" @change="onExternalDetailsEnabledChange"/>
<a-card v-if="externalDetailsEnabled" style="margin-top: 10px">
<div style="margin-bottom: 10px">{{ $t('message.add.orchestrator.resource.details') }}</div>
<details-input
v-model:value="form.externaldetails" />
</a-card>
</a-form-item>
</a-form>
<br/>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
:apiParams="apiParams"
:isSystem="isSystem"
:isAdmin="isAdmin"
:ref="formRef"
@submit="handleSubmit">
<template #form-actions>
<br/>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</template>
</ComputeOfferingForm>
</a-spin>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import ComputeOfferingForm from '@/components/offering/ComputeOfferingForm'
import { ref, reactive } from 'vue'
import { getAPI, postAPI } from '@/api'
import AddDiskOffering from '@/views/offering/AddDiskOffering'
import { isAdmin } from '@/role'
@ -683,6 +54,7 @@ export default {
name: 'AddServiceOffering',
mixins: [mixinForm],
components: {
ComputeOfferingForm,
AddDiskOffering,
ResourceIcon,
TooltipLabel,
@ -882,9 +254,6 @@ export default {
this.gpuCardLoading = false
})
},
addDiskOffering () {
this.showDiskOfferingModal = true
},
fetchDiskOfferings () {
this.diskOfferingLoading = true
getAPI('listDiskOfferings', {
@ -898,47 +267,18 @@ export default {
this.diskOfferingLoading = false
})
},
updateSelectedDiskOffering (id) {
if (id) {
this.selectedDiskOfferingId = id
}
},
closeDiskOfferingModal () {
this.fetchDiskOfferings()
this.showDiskOfferingModal = false
},
isAdmin () {
return isAdmin()
},
isDomainAdmin () {
return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
getVgpuProfileDetails (vgpuProfile) {
let output = '('
if (vgpuProfile?.videoram) {
output += `${vgpuProfile.videoram} MB`
}
if (vgpuProfile?.maxresolutionx && vgpuProfile?.maxresolutiony) {
if (output !== '(') {
output += ', '
}
output += `${vgpuProfile.maxresolutionx}x${vgpuProfile.maxresolutiony}`
}
output += ')'
if (output === '()') {
return ''
}
return output
},
checkIfDomainAdminIsAllowedToInformTag () {
const params = { id: store.getters.userInfo.accountid }
getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
})
},
arrayHasItems (array) {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
fetchDomainData () {
const params = {}
params.listAll = true
@ -990,83 +330,13 @@ export default {
this.deploymentPlannerLoading = false
})
},
fetchvSphereStoragePolicies (zoneIndex) {
if (zoneIndex === 0 || this.form.zoneid.length > 1) {
this.storagePolicies = null
return
}
const zoneid = this.zones[zoneIndex].id
if ('importVsphereStoragePolicies' in this.$store.getters.apis) {
this.storagePolicies = []
getAPI('listVsphereStoragePolicies', {
zoneid: zoneid
}).then(response => {
this.storagePolicies = response.listvspherestoragepoliciesresponse.StoragePolicy || []
})
}
},
handleStorageTypeChange (val) {
this.storageType = val
},
handleProvisioningTypeChange (val) {
this.provisioningType = val
},
handleCacheModeChange (val) {
this.cacheMode = val
},
handleComputeOfferingTypeChange (val) {
this.offeringType = val
},
handleQosTypeChange (val) {
this.qosType = val
},
handleDeploymentPlannerChange (planner) {
this.selectedDeploymentPlanner = planner
this.plannerModeVisible = false
if (this.selectedDeploymentPlanner === 'ImplicitDedicationPlanner') {
this.plannerModeVisible = isAdmin()
}
},
handlePlannerModeChange (val) {
this.plannerMode = val
},
handleGpuCardChange (cardId) {
this.selectedGpuCard = cardId
this.form.vgpuprofile = ''
if (cardId && cardId !== '') {
this.fetchVgpuProfiles(cardId)
} else {
this.vgpuProfiles = []
this.form.gpucount = '1'
}
},
fetchVgpuProfiles (gpuCardId) {
this.vgpuProfileLoading = true
this.vgpuProfiles = []
getAPI('listVgpuProfiles', {
gpucardid: gpuCardId
}).then(json => {
this.vgpuProfiles = json.listvgpuprofilesresponse.vgpuprofile || []
this.form.vgpuprofile = this.vgpuProfiles.length > 0 ? this.vgpuProfiles[0].id : ''
}).catch(error => {
console.error('Error fetching vGPU profiles:', error)
this.vgpuProfiles = []
}).finally(() => {
this.vgpuProfileLoading = false
})
},
onExternalDetailsEnabledChange (val) {
if (val || !this.form.externaldetails) {
return
}
this.form.externaldetails = undefined
},
handleSubmit (e) {
e.preventDefault()
if (e && e.preventDefault) {
e.preventDefault()
}
if (this.loading) return
this.formRef.value.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
this.formRef.value.validate().then((values) => {
var params = {
issystem: this.isSystem,
name: values.name,
@ -1089,7 +359,6 @@ export default {
params.diskofferingid = values.diskofferingid
}
// Add GPU parameters
if (values.vgpuprofile) {
params.vgpuprofileid = values.vgpuprofile
}
@ -1100,17 +369,16 @@ export default {
params.gpudisplay = values.gpudisplay
}
// custom fields (begin)
if (values.offeringtype === 'fixed') {
params.cpunumber = values.cpunumber
params.cpuspeed = values.cpuspeed
params.memory = values.memory
} else {
if (values.cpuspeed != null &&
values.mincpunumber != null &&
values.maxcpunumber != null &&
values.minmemory != null &&
values.maxmemory != null) {
values.mincpunumber != null &&
values.maxcpunumber != null &&
values.minmemory != null &&
values.maxmemory != null) {
params.cpuspeed = values.cpuspeed
params.mincpunumber = values.mincpunumber
params.maxcpunumber = values.maxcpunumber
@ -1118,7 +386,6 @@ export default {
params.maxmemory = values.maxmemory
}
}
// custom fields (end)
if (values.networkrate != null && values.networkrate.length > 0) {
params.networkrate = values.networkrate
@ -1137,7 +404,7 @@ export default {
params.maxiops = values.diskiopsmax
}
if (values.hypervisorsnapshotreserve !== undefined &&
values.hypervisorsnapshotreserve != null && values.hypervisorsnapshotreserve.length > 0) {
values.hypervisorsnapshotreserve != null && values.hypervisorsnapshotreserve.length > 0) {
params.hypervisorsnapshotreserve = values.hypervisorsnapshotreserve
}
}
@ -1163,15 +430,15 @@ export default {
params.hosttags = values.hosttags
}
if ('deploymentplanner' in values &&
values.deploymentplanner !== undefined &&
values.deploymentplanner != null && values.deploymentplanner.length > 0) {
values.deploymentplanner !== undefined &&
values.deploymentplanner != null && values.deploymentplanner.length > 0) {
params.deploymentplanner = values.deploymentplanner
}
if ('deploymentplanner' in values &&
values.deploymentplanner !== undefined &&
values.deploymentplanner === 'ImplicitDedicationPlanner' &&
values.plannermode !== undefined &&
values.plannermode !== '') {
values.deploymentplanner !== undefined &&
values.deploymentplanner === 'ImplicitDedicationPlanner' &&
values.plannermode !== undefined &&
values.plannermode !== '') {
params['serviceofferingdetails[0].key'] = 'ImplicitDedicationMode'
params['serviceofferingdetails[0].value'] = values.plannermode
}
@ -1247,17 +514,6 @@ export default {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
},
onToggleLeaseData () {
if (this.showLeaseOptions === false) {
this.leaseduration = undefined
this.leaseexpiryaction = undefined
} else {
this.leaseduration = this.leaseduration !== undefined ? this.leaseduration : this.defaultLeaseDuration
this.leaseexpiryaction = this.leaseexpiryaction !== undefined ? this.leaseexpiryaction : this.defaultLeaseExpiryAction
}
this.form.leaseduration = this.leaseduration
this.form.leaseexpiryaction = this.leaseexpiryaction
}
}
}

View File

@ -18,485 +18,57 @@
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
layout="vertical"
>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"
:placeholder="apiParams.name.description"/>
</a-form-item>
<a-form-item name="displaytext" ref="displaytext">
<template #label>
<tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
</template>
<a-input
v-model:value="form.displaytext"
:placeholder="apiParams.displaytext.description"/>
</a-form-item>
<a-form-item name="storagetype" ref="storagetype">
<template #label>
<tooltip-label :title="$t('label.storagetype')" :tooltip="apiParams.storagetype.description"/>
</template>
<a-radio-group
v-model:value="form.storagetype"
buttonStyle="solid">
<a-radio-button value="shared">
{{ $t('label.shared') }}
</a-radio-button>
<a-radio-button value="local">
{{ $t('label.local') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="provisioningtype" ref="provisioningtype">
<template #label>
<tooltip-label :title="$t('label.provisioningtype')" :tooltip="apiParams.provisioningtype.description"/>
</template>
<a-radio-group
v-model:value="form.provisioningtype"
buttonStyle="solid">
<a-radio-button value="thin">
{{ $t('label.provisioningtype.thin') }}
</a-radio-button>
<a-radio-button value="sparse">
{{ $t('label.provisioningtype.sparse') }}
</a-radio-button>
<a-radio-button value="fat">
{{ $t('label.provisioningtype.fat') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="encryptdisk" ref="encryptdisk">
<template #label>
<tooltip-label :title="$t('label.encrypt')" :tooltip="apiParams.encrypt.description" />
</template>
<a-switch v-model:checked="form.encryptdisk" :checked="encryptdisk" @change="val => { encryptdisk = val }" />
</a-form-item>
<a-form-item name="disksizestrictness" ref="disksizestrictness">
<template #label>
<tooltip-label :title="$t('label.disksizestrictness')" :tooltip="apiParams.disksizestrictness.description" />
</template>
<a-switch v-model:checked="form.disksizestrictness" :checked="disksizestrictness" @change="val => { disksizestrictness = val }" />
</a-form-item>
<a-form-item name="customdisksize" ref="customdisksize">
<template #label>
<tooltip-label :title="$t('label.customdisksize')" :tooltip="apiParams.customized.description"/>
</template>
<a-switch v-model:checked="form.customdisksize" />
</a-form-item>
<a-form-item v-if="!form.customdisksize" name="disksize" ref="disksize">
<template #label>
<tooltip-label :title="$t('label.disksize')" :tooltip="apiParams.disksize.description"/>
</template>
<a-input
v-model:value="form.disksize"
:placeholder="apiParams.disksize.description"/>
</a-form-item>
<a-form-item name="qostype" ref="qostype" :label="$t('label.qostype')">
<a-radio-group
v-model:value="form.qostype"
buttonStyle="solid">
<a-radio-button value="">
{{ $t('label.none') }}
</a-radio-button>
<a-radio-button value="hypervisor">
{{ $t('label.hypervisor') }}
</a-radio-button>
<a-radio-button value="storage">
{{ $t('label.storage') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbytesreadrate" ref="diskbytesreadrate">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadrate')" :tooltip="apiParams.bytesreadrate.description"/>
</template>
<a-input
v-model:value="form.diskbytesreadrate"
:placeholder="apiParams.bytesreadrate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbytesreadratemax" ref="diskbytesreadratemax">
<template #label>
<tooltip-label :title="$t('label.diskbytesreadratemax')" :tooltip="apiParams.bytesreadratemax.description"/>
</template>
<a-input
v-model:value="form.diskbytesreadratemax"
:placeholder="apiParams.bytesreadratemax.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbyteswriterate" ref="diskbyteswriterate">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriterate')" :tooltip="apiParams.byteswriterate.description"/>
</template>
<a-input
v-model:value="form.diskbyteswriterate"
:placeholder="apiParams.byteswriterate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskbyteswriteratemax" ref="diskbyteswriteratemax">
<template #label>
<tooltip-label :title="$t('label.diskbyteswriteratemax')" :tooltip="apiParams.byteswriteratemax.description"/>
</template>
<a-input
v-model:value="form.diskbyteswriteratemax"
:placeholder="apiParams.byteswriteratemax.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskiopsreadrate" ref="diskiopsreadrate">
<template #label>
<tooltip-label :title="$t('label.diskiopsreadrate')" :tooltip="apiParams.iopsreadrate.description"/>
</template>
<a-input
v-model:value="form.diskiopsreadrate"
:placeholder="apiParams.iopsreadrate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'hypervisor'" name="diskiopswriterate" ref="diskiopswriterate">
<template #label>
<tooltip-label :title="$t('label.diskiopswriterate')" :tooltip="apiParams.iopswriterate.description"/>
</template>
<a-input
v-model:value="form.diskiopswriterate"
:placeholder="apiParams.iopswriterate.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage'" name="iscustomizeddiskiops" ref="iscustomizeddiskiops">
<template #label>
<tooltip-label :title="$t('label.iscustomizeddiskiops')" :tooltip="apiParams.customizediops.description"/>
</template>
<a-switch v-model:checked="form.iscustomizeddiskiops" />
</a-form-item>
<a-form-item v-if="form.qostype === 'storage' && !form.iscustomizeddiskiops" name="diskiopsmin" ref="diskiopsmin">
<template #label>
<tooltip-label :title="$t('label.diskiopsmin')" :tooltip="apiParams.miniops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmin"
:placeholder="apiParams.miniops.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage' && !form.iscustomizeddiskiops" name="diskiopsmax" ref="diskiopsmax">
<template #label>
<tooltip-label :title="$t('label.diskiopsmax')" :tooltip="apiParams.maxiops.description"/>
</template>
<a-input
v-model:value="form.diskiopsmax"
:placeholder="apiParams.maxiops.description"/>
</a-form-item>
<a-form-item v-if="form.qostype === 'storage'" name="hypervisorsnapshotreserve" ref="hypervisorsnapshotreserve">
<template #label>
<tooltip-label :title="$t('label.hypervisorsnapshotreserve')" :tooltip="apiParams.hypervisorsnapshotreserve.description"/>
</template>
<a-input
v-model:value="form.hypervisorsnapshotreserve"
:placeholder="apiParams.hypervisorsnapshotreserve.description"/>
</a-form-item>
<a-form-item name="writecachetype" ref="writecachetype">
<template #label>
<tooltip-label :title="$t('label.writecachetype')" :tooltip="apiParams.cachemode.description"/>
</template>
<a-radio-group
v-model:value="form.writecachetype"
buttonStyle="solid"
@change="selected => { handleWriteCacheTypeChange(selected.target.value) }">
<a-radio-button value="none">
{{ $t('label.nodiskcache') }}
</a-radio-button>
<a-radio-button value="writeback">
{{ $t('label.writeback') }}
</a-radio-button>
<a-radio-button value="writethrough">
{{ $t('label.writethrough') }}
</a-radio-button>
<a-radio-button value="hypervisor_default">
{{ $t('label.hypervisor.default') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="tags" ref="tags">
<template #label>
<tooltip-label :title="$t('label.storagetags')" :tooltip="apiParams.tags.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
mode="tags"
v-model:value="form.tags"
showSearch
optionFilterProp="value"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="storageTagLoading"
:placeholder="apiParams.tags.description"
v-if="isAdmin() || isDomainAdminAllowedToInformTags">
<a-select-option v-for="(opt) in storageTags" :key="opt">
{{ opt }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('label.ispublic')" v-show="isAdmin()" name="ispublic" ref="ispublic">
<a-switch v-model:checked="form.ispublic" @change="val => { isPublic = val }" />
</a-form-item>
<a-form-item v-if="!isPublic" name="domainid" ref="domainid">
<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="domainLoading"
:placeholder="apiParams.domainid.description">
<a-select-option v-for="(opt, optIndex) in domains" :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>
<a-form-item name="zoneid" ref="zoneid">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template>
<a-select
id="zone-selection"
mode="multiple"
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@select="val => fetchvSphereStoragePolicies(val)"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description">
<a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
<span>
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ opt.name || opt.description }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="'listVsphereStoragePolicies' in $store.getters.apis && storagePolicies !== null" name="storagepolicy" ref="storagepolicy">
<template #label>
<tooltip-label :title="$t('label.vmware.storage.policy')" :tooltip="apiParams.storagepolicy.description"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.storagepolicy"
:placeholder="apiParams.storagepolicy.description"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="policy in storagePolicies" :key="policy.id" :label="policy.name || policy.id || ''">
{{ policy.name || policy.id }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
<DiskOfferingForm
ref="formRef"
:initialValues="form"
:apiParams="apiParams"
:isAdmin="isAdmin"
@submit="handleSubmit">
<template #form-actions>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</template>
</DiskOfferingForm>
</a-spin>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import { reactive, ref, toRaw } from 'vue'
import DiskOfferingForm from '@/components/offering/DiskOfferingForm'
import { reactive } from 'vue'
import { postAPI } from '@/api'
import { isAdmin } from '@/role'
import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import store from '@/store'
export default {
name: 'AddDiskOffering',
mixins: [mixinForm],
components: {
ResourceIcon,
TooltipLabel
DiskOfferingForm
},
data () {
return {
selectedDomains: [],
storageTags: [],
storagePolicies: null,
storageTagLoading: false,
isPublic: true,
isEncrypted: false,
domains: [],
domainLoading: false,
zones: [],
zoneLoading: false,
loading: false,
disksizestrictness: false,
encryptdisk: false,
isDomainAdminAllowedToInformTags: false
formRef: null,
form: reactive({}),
loading: false
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('createDiskOffering')
},
created () {
this.zones = [
{
id: null,
name: this.$t('label.all.zone')
}
]
this.isPublic = isAdmin()
this.initForm()
this.fetchData()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
storagetype: 'shared',
provisioningtype: 'thin',
customdisksize: true,
writecachetype: 'none',
qostype: '',
ispublic: this.isPublic,
disksizestrictness: this.disksizestrictness,
encryptdisk: this.encryptdisk
})
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }],
disksize: [
{ required: true, message: this.$t('message.error.required.input') },
{ type: 'number', validator: this.validateNumber }
],
diskbytesreadrate: [{ type: 'number', validator: this.validateNumber }],
diskbytesreadratemax: [{ type: 'number', validator: this.validateNumber }],
diskbyteswriterate: [{ type: 'number', validator: this.validateNumber }],
diskbyteswriteratemax: [{ type: 'number', validator: this.validateNumber }],
diskiopsreadrate: [{ type: 'number', validator: this.validateNumber }],
diskiopswriterate: [{ type: 'number', validator: this.validateNumber }],
diskiopsmin: [{ type: 'number', validator: this.validateNumber }],
diskiopsmax: [{ type: 'number', validator: this.validateNumber }],
hypervisorsnapshotreserve: [{ type: 'number', validator: this.validateNumber }],
domainid: [{ type: 'array', required: true, message: this.$t('message.error.select') }],
zoneid: [{
type: 'array',
validator: async (rule, value) => {
if (value && value.length > 1 && value.indexOf(0) !== -1) {
return Promise.reject(this.$t('message.error.zone.combined'))
}
return Promise.resolve()
}
}]
})
},
handleWriteCacheTypeChange (val) {
this.form.writeCacheType = val
},
fetchData () {
this.fetchDomainData()
this.fetchZoneData()
if (isAdmin()) {
this.fetchStorageTagData()
}
if (this.isDomainAdmin()) {
this.checkIfDomainAdminIsAllowedToInformTag()
if (this.isDomainAdminAllowedToInformTags) {
this.fetchStorageTagData()
}
}
},
isDomainAdmin () {
return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
isAdmin () {
return isAdmin()
},
checkIfDomainAdminIsAllowedToInformTag () {
const params = { id: store.getters.userInfo.accountid }
getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
})
},
arrayHasItems (array) {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
fetchDomainData () {
const params = {}
params.listAll = true
params.showicon = true
params.details = 'min'
this.domainLoading = true
getAPI('listDomains', params).then(json => {
const listDomains = json.listdomainsresponse.domain
this.domains = this.domains.concat(listDomains)
}).finally(() => {
this.domainLoading = false
})
},
fetchZoneData () {
const params = {}
params.showicon = true
this.zoneLoading = true
getAPI('listZones', params).then(json => {
const listZones = json.listzonesresponse.zone
if (listZones) {
this.zones = this.zones.concat(listZones)
}
}).finally(() => {
this.zoneLoading = false
})
},
fetchStorageTagData () {
const params = {}
this.storageTagLoading = true
getAPI('listStorageTags', params).then(json => {
const tags = json.liststoragetagsresponse.storagetag || []
for (const tag of tags) {
if (!this.storageTags.includes(tag.name)) {
this.storageTags.push(tag.name)
}
}
}).finally(() => {
this.storageTagLoading = false
})
},
fetchvSphereStoragePolicies (zoneIndex) {
if (zoneIndex === 0 || this.form.zoneid.length > 1) {
this.storagePolicies = null
return
}
const zoneid = this.zones[zoneIndex].id
if ('importVsphereStoragePolicies' in this.$store.getters.apis) {
this.storagePolicies = []
getAPI('listVsphereStoragePolicies', {
zoneid: zoneid
}).then(response => {
this.storagePolicies = response.listvspherestoragepoliciesresponse.StoragePolicy || []
})
}
},
handleSubmit (e) {
e.preventDefault()
if (e && e.preventDefault) {
e.preventDefault()
}
if (this.loading) return
this.formRef.value.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
this.$refs.formRef.validate().then((values) => {
var params = {
name: values.name,
displaytext: values.displaytext,
@ -553,8 +125,9 @@ export default {
var domainId = null
if (domainIndexes && domainIndexes.length > 0) {
var domainIds = []
const domains = this.$refs.formRef.domains
for (var i = 0; i < domainIndexes.length; i++) {
domainIds = domainIds.concat(this.domains[domainIndexes[i]].id)
domainIds.push(domains[domainIndexes[i]].id)
}
domainId = domainIds.join(',')
}
@ -566,8 +139,9 @@ export default {
var zoneId = null
if (zoneIndexes && zoneIndexes.length > 0) {
var zoneIds = []
const zones = this.$refs.formRef.zones
for (var j = 0; j < zoneIndexes.length; j++) {
zoneIds = zoneIds.concat(this.zones[zoneIndexes[j]].id)
zoneIds.push(zones[zoneIndexes[j]].id)
}
zoneId = zoneIds.join(',')
}
@ -577,6 +151,8 @@ export default {
if (values.storagepolicy) {
params.storagepolicy = values.storagepolicy
}
this.loading = true
postAPI('createDiskOffering', params).then(json => {
this.$emit('publish-disk-offering-id', json?.creatediskofferingresponse?.diskoffering?.id)
this.$message.success(`${this.$t('message.disk.offering.created')} ${values.name}`)
@ -591,12 +167,6 @@ export default {
},
closeAction () {
this.$emit('close-action')
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
}
}
}

View File

@ -588,6 +588,7 @@ import { mixinForm } from '@/utils/mixin'
import CheckBoxSelectPair from '@/components/CheckBoxSelectPair'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { buildServiceCapabilityParams } from '@/composables/useServiceCapabilityParams'
export default {
name: 'AddNetworkOffering',
@ -1173,99 +1174,7 @@ export default {
}
}
if (this.selectedServiceProviderMap != null) {
var supportedServices = Object.keys(this.selectedServiceProviderMap)
params.supportedservices = supportedServices.join(',')
for (var k in supportedServices) {
params['serviceProviderList[' + k + '].service'] = supportedServices[k]
params['serviceProviderList[' + k + '].provider'] = this.selectedServiceProviderMap[supportedServices[k]]
}
var serviceCapabilityIndex = 0
if (supportedServices.includes('Connectivity')) {
if (values.supportsstrechedl2subnet === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Connectivity'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RegionLevelVpc'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
if (values.supportspublicaccess === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Connectivity'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'DistributedRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
delete params.supportsstrechedl2subnet
delete params.supportspublicaccess
}
if (supportedServices.includes('SourceNat')) {
if (values.redundantroutercapability === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedSourceNatTypes'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.sourcenattype
serviceCapabilityIndex++
delete params.redundantroutercapability
delete params.sourcenattype
} else if (values.redundantroutercapability === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Gateway'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
}
if (supportedServices.includes('SourceNat')) {
if (values.elasticip === true) {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'StaticNat'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'ElasticIp'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
if (values.elasticip === true || values.associatepublicip === true) {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'StaticNat'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'associatePublicIP'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.associatepublicip
serviceCapabilityIndex++
}
delete params.elasticip
delete params.associatepublicip
}
if (supportedServices.includes('Lb')) {
if ('vmautoscalingcapability' in values) {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'VmAutoScaling'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.vmautoscalingcapability
serviceCapabilityIndex++
}
if (values.elasticlb === true) {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'ElasticLb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
if (values.inlinemode === true && ((this.selectedServiceProviderMap.Lb === 'F5BigIp') || (this.selectedServiceProviderMap.Lb === 'Netscaler'))) {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'InlineMode'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.inlinemode
serviceCapabilityIndex++
}
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedLbIsolation'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.isolation
serviceCapabilityIndex++
if (this.selectedServiceProviderMap.Lb === 'InternalLbVm') {
params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'lbSchemes'
params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = 'internal'
serviceCapabilityIndex++
}
if ('netscalerservicepackages' in values &&
this.registeredServicePackages.length > values.netscalerservicepackages &&
'netscalerservicepackagesdescription' in values) {
params['details[' + 0 + '].servicepackageuuid'] = this.registeredServicePackages[values.netscalerservicepackages].id
params['details[' + 1 + '].servicepackagedescription'] = values.netscalerservicepackagesdescription
}
}
buildServiceCapabilityParams(params, values, this.selectedServiceProviderMap, this.registeredServicePackages)
} else {
if (!('supportedservices' in params)) {
params.supportedservices = ''

View File

@ -277,6 +277,7 @@ import { mixinForm } from '@/utils/mixin'
import CheckBoxSelectPair from '@/components/CheckBoxSelectPair'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { buildVpcServiceCapabilityParams } from '@/composables/useServiceCapabilityParams'
export default {
name: 'AddVpcOffering',
@ -742,35 +743,7 @@ export default {
params['serviceProviderList[' + k + '].service'] = supportedServices[k]
params['serviceProviderList[' + k + '].provider'] = this.selectedServiceProviderMap[supportedServices[k]]
}
var serviceCapabilityIndex = 0
if (supportedServices.includes('Connectivity')) {
if (values.regionlevelvpc === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Connectivity'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RegionLevelVpc'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
if (values.distributedrouter === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Connectivity'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'DistributedRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
}
if (supportedServices.includes('SourceNat') && values.redundantrouter === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
} else if (values.redundantrouter === true) {
params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Gateway'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
serviceCapabilityIndex++
}
if (values.serviceofferingid && this.isVpcVirtualRouterForAtLeastOneService) {
params.serviceofferingid = values.serviceofferingid
}
buildVpcServiceCapabilityParams(params, values, this.selectedServiceProviderMap, this.isVpcVirtualRouterForAtLeastOneService)
} else {
params.supportedservices = []
}

View File

@ -0,0 +1,432 @@
// 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.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-alert
v-if="resource"
type="info"
style="margin-bottom: 16px">
<template #message>
<div style="display: block; width: 100%;">
<div style="display: block; margin-bottom: 8px;">
<strong>{{ $t('message.clone.offering.from') }}: {{ resource.name }}</strong>
</div>
<div style="display: block; font-size: 12px;">
{{ $t('message.clone.offering.edit.hint') }}
</div>
</div>
</template>
</a-alert>
<a-form
layout="vertical"
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"/>
</a-form-item>
<a-form-item name="description" ref="description">
<template #label>
<tooltip-label :title="$t('label.description')" :tooltip="apiParams.description.description"/>
</template>
<a-input v-model:value="form.description"/>
</a-form-item>
<a-form-item name="zoneid" ref="zoneid">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid ? apiParams.zoneid.description : ''"/>
</template>
<a-select
allowClear
v-model:value="form.zoneid"
:loading="zones.loading"
@change="onChangeZone"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="zone in zones.opts" :key="zone.name" :label="zone.name">
<span>
<resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ zone.name }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="externalid" ref="externalid">
<template #label>
<tooltip-label :title="$t('label.externalid')" :tooltip="apiParams.externalid ? apiParams.externalid.description : ''"/>
</template>
<a-select
allowClear
v-model:value="form.externalid"
:loading="externals.loading"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="opt in externals.opts" :key="opt.id" :label="opt.name">
{{ opt.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="allowuserdrivenbackups" ref="allowuserdrivenbackups">
<template #label>
<tooltip-label :title="$t('label.allowuserdrivenbackups')" :tooltip="apiParams.allowuserdrivenbackups ? apiParams.allowuserdrivenbackups.description : ''"/>
</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" @change="onChangeIsPublic" />
</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"
@change="onChangeDomain"
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">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { getAPI, postAPI } from '@/api'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { GlobalOutlined } from '@ant-design/icons-vue'
import { isAdmin } from '@/role'
export default {
name: 'CloneBackupOffering',
components: {
TooltipLabel,
ResourceIcon,
GlobalOutlined
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
zones: {
loading: false,
opts: []
},
externals: {
loading: false,
opts: []
},
domains: {
loading: false,
opts: []
}
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('cloneBackupOffering')
},
created () {
this.initForm()
this.fetchData()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
allowuserdrivenbackups: true
})
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }],
description: [{ required: true, message: this.$t('message.error.required.input') }]
})
},
fetchData () {
this.fetchZone()
this.fetchDomain()
this.$nextTick(() => {
this.populateFormFromResource()
})
},
fetchZone () {
this.zones.loading = true
getAPI('listZones', { available: true, showicon: true }).then(json => {
this.zones.opts = json.listzonesresponse.zone || []
this.$nextTick(() => {
this.populateFormFromResource()
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.zones.loading = false
})
},
fetchDomain () {
this.domains.loading = true
const params = { listAll: true, showicon: true, details: 'min' }
getAPI('listDomains', params).then(json => {
this.domains.opts = json.listdomainsresponse.domain || []
this.$nextTick(() => {
this.populateFormFromResource()
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.domains.loading = false
})
},
fetchExternal (zoneId) {
if (!zoneId) {
this.externals.opts = []
return
}
this.externals.loading = true
getAPI('listBackupProviderOfferings', { zoneid: zoneId }).then(json => {
this.externals.opts = json.listbackupproviderofferingsresponse.backupoffering || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.externals.loading = false
})
},
populateFormFromResource () {
if (!this.resource) return
const r = this.resource
this.form.name = r.name + ' - Clone'
this.form.description = r.description || r.name
if (r.allowuserdrivenbackups !== undefined) {
this.form.allowuserdrivenbackups = r.allowuserdrivenbackups
}
if (r.domainid) {
let offeringDomainIds = r.domainid
offeringDomainIds = (typeof offeringDomainIds === 'string' && offeringDomainIds.indexOf(',') !== -1) ? offeringDomainIds.split(',') : [offeringDomainIds]
const selected = []
for (let i = 0; i < offeringDomainIds.length; i++) {
for (let j = 0; j < this.domains.opts.length; j++) {
if (String(offeringDomainIds[i]) === String(this.domains.opts[j].id)) {
selected.push(j)
}
}
}
if (selected.length > 0) {
this.form.ispublic = false
this.form.domainid = selected
}
} else {
if (isAdmin()) {
this.form.ispublic = true
}
}
if (r.zoneid && this.zones.opts.length > 0) {
const zone = this.zones.opts.find(z => z.id === r.zoneid)
if (zone) {
this.form.zoneid = zone.name
this.fetchExternal(zone.id)
}
}
if (r.externalid) {
this.$nextTick(() => {
this.form.externalid = r.externalid
})
}
},
handleSubmit (e) {
if (e) {
e.preventDefault()
}
if (this.loading) return
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
const params = {
sourceofferingid: this.resource.id
}
if (values.name) {
params.name = values.name
}
if (values.description) {
params.description = values.description
}
if (values.zoneid) {
const zone = this.zones.opts.find(z => z.name === values.zoneid)
if (zone) {
params.zoneid = zone.id
}
}
if (values.externalid) {
params.externalid = values.externalid
}
if (values.allowuserdrivenbackups !== undefined) {
params.allowuserdrivenbackups = values.allowuserdrivenbackups
}
// Include selected domain IDs when offering is not public
if (values.ispublic !== true) {
const domainIndexes = values.domainid
let domainId = null
if (domainIndexes && domainIndexes.length > 0) {
const domainIds = []
const domains = this.domains.opts
for (let i = 0; i < domainIndexes.length; i++) {
domainIds.push(domains[domainIndexes[i]].id)
}
domainId = domainIds.join(',')
}
if (domainId) {
params.domainid = domainId
}
}
this.loading = true
const title = this.$t('message.success.clone.backup.offering')
postAPI('cloneBackupOffering', params).then(json => {
const jobId = json.clonebackupofferingresponse?.jobid
if (jobId) {
this.$pollJob({
jobId,
title,
description: values.name,
successMethod: result => {
this.$message.success(`${title}: ${values.name}`)
this.$emit('refresh-data')
this.closeAction()
},
loadingMessage: `${title} ${this.$t('label.in.progress')} ${this.$t('label.for')} ${params.name}`,
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.closeAction()
}
})
} else {
this.$message.success(`${title}: ${values.name}`)
this.$emit('refresh-data')
this.closeAction()
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
}).catch(error => {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
onChangeZone (value) {
if (!value) {
this.externals.opts = []
this.form.externalid = null
return
}
const zone = this.zones.opts.find(z => z.name === value)
if (zone) {
this.fetchExternal(zone.id)
}
},
onChangeIsPublic (value) {
// when made public, clear any domain selection
try {
if (value === true) {
this.form.domainid = []
}
} catch (e) {
// ignore
}
},
onChangeDomain (value) {
// value is an array of selected indexes (as used in the form), when empty -> make offering public
try {
if (!value || (Array.isArray(value) && value.length === 0)) {
this.form.ispublic = true
// clear domain selection to avoid confusing hidden inputs
this.form.domainid = []
} else {
this.form.ispublic = false
}
} catch (e) {
// defensive - do nothing on error
}
},
closeAction () {
this.$emit('close-action')
},
isAdmin () {
return isAdmin()
}
}
}
</script>
<style scoped lang="less">
.form-layout {
width: 30vw;
@media (min-width: 500px) {
width: 450px;
}
}
</style>

View File

@ -0,0 +1,674 @@
// 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.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-alert
v-if="resource"
type="info"
style="margin-bottom: 16px">
<template #message>
<div style="display: block; width: 100%;">
<div style="display: block; margin-bottom: 8px;">
<strong>{{ $t('message.clone.offering.from') }}: {{ resource.name }}</strong>
</div>
<div style="display: block; font-size: 12px;">
{{ $t('message.clone.offering.edit.hint') }}
</div>
</div>
</template>
</a-alert>
<ComputeOfferingForm
:initialValues="form"
:rules="rules"
:apiParams="apiParams"
:isSystem="isSystem"
:isAdmin="isAdmin"
:ref="formRef"
@submit="handleSubmit">
<!-- form content is provided by ComputeOfferingForm component -->
<template #form-actions>
<br/>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</template>
</ComputeOfferingForm>
</a-spin>
</div>
</template>
<script>
import ComputeOfferingForm from '@/components/offering/ComputeOfferingForm'
import { ref, reactive } from 'vue'
import { getAPI, postAPI } from '@/api'
import AddDiskOffering from '@/views/offering/AddDiskOffering'
import { isAdmin } from '@/role'
import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import DetailsInput from '@/components/widgets/DetailsInput'
import store from '@/store'
export default {
name: 'CloneComputeOffering',
mixins: [mixinForm],
components: {
ComputeOfferingForm,
AddDiskOffering,
ResourceIcon,
TooltipLabel,
DetailsInput
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
isSystem: false,
naturalNumberRule: {
type: 'number',
validator: this.validateNumber
},
wholeNumberRule: {
type: 'number',
validator: async (rule, value) => {
if (value && (isNaN(value) || value < 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
}
},
storageType: 'shared',
provisioningType: 'thin',
cacheMode: 'none',
offeringType: 'fixed',
isCustomizedDiskIops: false,
isPublic: true,
domains: [],
domainLoading: false,
zones: [],
zoneLoading: false,
selectedDeploymentPlanner: null,
storagePolicies: null,
storageTags: [],
storageTagLoading: false,
deploymentPlanners: [],
deploymentPlannerLoading: false,
plannerModeVisible: false,
plannerMode: '',
selectedGpuCard: '',
showDiskOfferingModal: false,
gpuCardLoading: false,
gpuCards: [],
loading: false,
dynamicscalingenabled: true,
diskofferingstrictness: false,
encryptdisk: false,
computeonly: true,
diskOfferingLoading: false,
diskOfferings: [],
selectedDiskOfferingId: '',
qosType: '',
isDomainAdminAllowedToInformTags: false,
isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled,
showLeaseOptions: false,
expiryActions: ['STOP', 'DESTROY'],
defaultLeaseDuration: 90,
defaultLeaseExpiryAction: 'STOP',
leaseduration: undefined,
leaseexpiryaction: undefined,
vgpuProfiles: [],
vgpuProfileLoading: false,
externalDetailsEnabled: false
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('cloneServiceOffering')
},
created () {
this.zones = [
{
id: null,
name: this.$t('label.all.zone')
}
]
if (this.$route.meta.name === 'systemoffering') {
this.isSystem = true
}
this.initForm()
this.fetchData()
this.isPublic = isAdmin()
this.form.ispublic = this.isPublic
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
systemvmtype: 'domainrouter',
offeringtype: this.offeringType,
ispublic: this.isPublic,
dynamicscalingenabled: true,
plannermode: this.plannerMode,
gpucardid: this.selectedGpuCard,
vgpuprofile: '',
gpucount: '1',
gpudisplay: false,
computeonly: this.computeonly,
storagetype: this.storageType,
provisioningtype: this.provisioningType,
cachemode: this.cacheMode,
qostype: this.qosType,
iscustomizeddiskiops: this.isCustomizedDiskIops,
diskofferingid: this.selectedDiskOfferingId,
diskofferingstrictness: this.diskofferingstrictness,
encryptdisk: this.encryptdisk,
leaseduration: this.leaseduration,
leaseexpiryaction: this.leaseexpiryaction
})
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }],
cpunumber: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
cpuspeed: [
{ required: true, message: this.$t('message.error.required.input') },
this.wholeNumberRule
],
mincpunumber: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
maxcpunumber: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
memory: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
minmemory: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
maxmemory: [
{ required: true, message: this.$t('message.error.required.input') },
this.naturalNumberRule
],
networkrate: [this.naturalNumberRule],
rootdisksize: [this.naturalNumberRule],
diskbytesreadrate: [this.naturalNumberRule],
diskbyteswriterate: [this.naturalNumberRule],
diskiopsreadrate: [this.naturalNumberRule],
diskiopswriterate: [this.naturalNumberRule],
diskiopsmin: [this.naturalNumberRule],
diskiopsmax: [this.naturalNumberRule],
hypervisorsnapshotreserve: [this.naturalNumberRule],
domainid: [{ type: 'array', required: true, message: this.$t('message.error.select') }],
diskofferingid: [{ required: true, message: this.$t('message.error.select') }],
gpucount: [{
type: 'number',
validator: async (rule, value) => {
if (value && (isNaN(value) || value < 1)) {
return Promise.reject(this.$t('message.error.number.minimum.one'))
}
return Promise.resolve()
}
}],
zoneid: [{
type: 'array',
validator: async (rule, value) => {
if (value && value.length > 1 && value.indexOf(0) !== -1) {
return Promise.reject(this.$t('message.error.zone.combined'))
}
return Promise.resolve()
}
}],
leaseduration: [this.naturalNumberRule]
})
},
fetchData () {
this.fetchDomainData()
this.fetchZoneData()
this.fetchGPUCards()
if (isAdmin()) {
this.fetchStorageTagData()
this.fetchDeploymentPlannerData()
} else if (this.isDomainAdmin()) {
this.checkIfDomainAdminIsAllowedToInformTag()
if (this.isDomainAdminAllowedToInformTags) {
this.fetchStorageTagData()
}
}
this.fetchDiskOfferings()
this.populateFormFromResource()
},
populateFormFromResource () {
if (!this.resource) return
// Pre-fill form with source offering values
const r = this.resource
this.form.name = r.name + ' - Clone'
this.form.displaytext = r.displaytext
if (r.iscustomized) {
if (r.cpunumber || r.cpuspeed || r.memory) {
this.offeringType = 'customconstrained'
this.form.offeringtype = 'customconstrained'
} else {
this.offeringType = 'customunconstrained'
this.form.offeringtype = 'customunconstrained'
}
} else {
this.offeringType = 'fixed'
this.form.offeringtype = 'fixed'
}
if (r.cpunumber) this.form.cpunumber = r.cpunumber
if (r.cpuspeed) this.form.cpuspeed = r.cpuspeed
if (r.memory) this.form.memory = r.memory
if (r.mincpunumber) this.form.mincpunumber = r.mincpunumber
if (r.maxcpunumber) this.form.maxcpunumber = r.maxcpunumber
if (r.minmemory) this.form.minmemory = r.minmemory
if (r.maxmemory) this.form.maxmemory = r.maxmemory
if (r.hosttags) this.form.hosttags = r.hosttags
if (r.networkrate) this.form.networkrate = r.networkrate
if (r.offerha !== undefined) this.form.offerha = r.offerha
if (r.dynamicscalingenabled !== undefined) {
this.form.dynamicscalingenabled = r.dynamicscalingenabled
this.dynamicscalingenabled = r.dynamicscalingenabled
}
if (r.limitcpuuse !== undefined) this.form.limitcpuuse = r.limitcpuuse
if (r.isvolatile !== undefined) this.form.isvolatile = r.isvolatile
if (r.storagetype) {
this.storageType = r.storagetype
this.form.storagetype = r.storagetype
}
if (r.provisioningtype) {
this.provisioningType = r.provisioningtype
this.form.provisioningtype = r.provisioningtype
}
if (r.cachemode) {
this.cacheMode = r.cachemode
this.form.cachemode = r.cachemode
}
if (r.diskofferingstrictness !== undefined) {
this.form.diskofferingstrictness = r.diskofferingstrictness
this.diskofferingstrictness = r.diskofferingstrictness
}
if (r.encryptroot !== undefined) {
this.form.encryptdisk = r.encryptroot
this.encryptdisk = r.encryptroot
}
if (r.diskBytesReadRate || r.diskBytesWriteRate || r.diskIopsReadRate || r.diskIopsWriteRate) {
this.qosType = 'hypervisor'
this.form.qostype = 'hypervisor'
if (r.diskBytesReadRate) this.form.diskbytesreadrate = r.diskBytesReadRate
if (r.diskBytesWriteRate) this.form.diskbyteswriterate = r.diskBytesWriteRate
if (r.diskIopsReadRate) this.form.diskiopsreadrate = r.diskIopsReadRate
if (r.diskIopsWriteRate) this.form.diskiopswriterate = r.diskIopsWriteRate
} else if (r.miniops || r.maxiops) {
this.qosType = 'storage'
this.form.qostype = 'storage'
if (r.miniops) this.form.diskiopsmin = r.miniops
if (r.maxiops) this.form.diskiopsmax = r.maxiops
if (r.hypervisorsnapshotreserve) this.form.hypervisorsnapshotreserve = r.hypervisorsnapshotreserve
}
if (r.iscustomizediops !== undefined) {
this.form.iscustomizeddiskiops = r.iscustomizediops
this.isCustomizedDiskIops = r.iscustomizediops
}
if (r.rootdisksize) this.form.rootdisksize = r.rootdisksize
if (r.tags) {
this.form.storagetags = r.tags.split(',')
}
if (r.gpucardid) {
this.form.gpucardid = r.gpucardid
this.selectedGpuCard = r.gpucardid
if (r.gpucardid) {
this.fetchVgpuProfiles(r.gpucardid)
}
}
if (r.vgpuprofileid) this.form.vgpuprofile = r.vgpuprofileid
if (r.gpucount) this.form.gpucount = r.gpucount
if (r.gpudisplay !== undefined) this.form.gpudisplay = r.gpudisplay
if (r.leaseduration) {
this.form.leaseduration = r.leaseduration
this.showLeaseOptions = true
}
if (r.leaseexpiryaction) this.form.leaseexpiryaction = r.leaseexpiryaction
if (r.purgeresources !== undefined) this.form.purgeresources = r.purgeresources
if (r.vspherestoragepolicy) this.form.storagepolicy = r.vspherestoragepolicy
if (r.systemvmtype) this.form.systemvmtype = r.systemvmtype
if (r.deploymentplanner) {
this.form.deploymentplanner = r.deploymentplanner
this.handleDeploymentPlannerChange(r.deploymentplanner)
}
if (r.serviceofferingdetails && Object.keys(r.serviceofferingdetails).length > 0) {
this.externalDetailsEnabled = true
this.form.externaldetails = r.serviceofferingdetails
}
},
fetchGPUCards () {
this.gpuCardLoading = true
getAPI('listGpuCards', {
}).then(json => {
this.gpuCards = json.listgpucardsresponse.gpucard || []
this.gpuCards.unshift({
id: '',
name: this.$t('label.none')
})
}).finally(() => {
this.gpuCardLoading = false
})
},
fetchDiskOfferings () {
this.diskOfferingLoading = true
getAPI('listDiskOfferings', {
listall: true
}).then(json => {
this.diskOfferings = json.listdiskofferingsresponse.diskoffering || []
if (this.selectedDiskOfferingId === '') {
this.selectedDiskOfferingId = this.diskOfferings?.[0]?.id || ''
}
}).finally(() => {
this.diskOfferingLoading = false
})
},
isAdmin () {
return isAdmin()
},
isDomainAdmin () {
return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
checkIfDomainAdminIsAllowedToInformTag () {
const params = { id: store.getters.userInfo.accountid }
getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
})
},
fetchDomainData () {
const params = {}
params.listAll = true
params.showicon = true
params.details = 'min'
this.domainLoading = true
getAPI('listDomains', params).then(json => {
const listDomains = json.listdomainsresponse.domain
this.domains = this.domains.concat(listDomains)
}).finally(() => {
this.domainLoading = false
})
},
fetchZoneData () {
const params = {}
params.showicon = true
this.zoneLoading = true
getAPI('listZones', params).then(json => {
const listZones = json.listzonesresponse.zone
if (listZones) {
this.zones = this.zones.concat(listZones)
}
}).finally(() => {
this.zoneLoading = false
})
},
fetchStorageTagData () {
this.storageTagLoading = true
this.storageTags = []
getAPI('listStorageTags').then(json => {
const tags = json.liststoragetagsresponse.storagetag || []
for (const tag of tags) {
if (!this.storageTags.includes(tag.name)) {
this.storageTags.push(tag.name)
}
}
}).finally(() => {
this.storageTagLoading = false
})
},
fetchDeploymentPlannerData () {
this.deploymentPlannerLoading = true
getAPI('listDeploymentPlanners').then(json => {
const planners = json.listdeploymentplannersresponse.deploymentPlanner
this.deploymentPlanners = this.deploymentPlanners.concat(planners)
this.deploymentPlanners.unshift({ name: '' })
this.form.deploymentplanner = this.deploymentPlanners.length > 0 ? this.deploymentPlanners[0].name : ''
}).finally(() => {
this.deploymentPlannerLoading = false
})
},
handleSubmit (e) {
if (e && e.preventDefault) {
e.preventDefault()
}
if (this.loading) return
this.formRef.value.validate().then((values) => {
var params = {
issystem: this.isSystem,
name: values.name,
displaytext: values.displaytext,
storagetype: values.storagetype,
provisioningtype: values.provisioningtype,
cachemode: values.cachemode,
customized: values.offeringtype !== 'fixed',
offerha: values.offerha === true,
limitcpuuse: values.limitcpuuse === true,
dynamicscalingenabled: values.dynamicscalingenabled,
diskofferingstrictness: values.diskofferingstrictness,
encryptroot: values.encryptdisk,
purgeresources: values.purgeresources,
leaseduration: values.leaseduration,
leaseexpiryaction: values.leaseexpiryaction
}
if (values.diskofferingid) {
params.diskofferingid = values.diskofferingid
}
if (values.vgpuprofile) {
params.vgpuprofileid = values.vgpuprofile
}
if (values.gpucount && values.gpucount > 0) {
params.gpucount = values.gpucount
}
if (values.gpudisplay !== undefined) {
params.gpudisplay = values.gpudisplay
}
if (values.offeringtype === 'fixed') {
params.cpunumber = values.cpunumber
params.cpuspeed = values.cpuspeed
params.memory = values.memory
} else {
if (values.cpuspeed != null &&
values.mincpunumber != null &&
values.maxcpunumber != null &&
values.minmemory != null &&
values.maxmemory != null) {
params.cpuspeed = values.cpuspeed
params.mincpunumber = values.mincpunumber
params.maxcpunumber = values.maxcpunumber
params.minmemory = values.minmemory
params.maxmemory = values.maxmemory
}
}
if (values.networkrate != null && values.networkrate.length > 0) {
params.networkrate = values.networkrate
}
if (values.rootdisksize != null && values.rootdisksize.length > 0) {
params.rootdisksize = values.rootdisksize
}
if (values.qostype === 'storage') {
var customIops = values.iscustomizeddiskiops === true
params.customizediops = customIops
if (!customIops) {
if (values.diskiopsmin != null && values.diskiopsmin.length > 0) {
params.miniops = values.diskiopsmin
}
if (values.diskiopsmax != null && values.diskiopsmax.length > 0) {
params.maxiops = values.diskiopsmax
}
if (values.hypervisorsnapshotreserve !== undefined &&
values.hypervisorsnapshotreserve != null && values.hypervisorsnapshotreserve.length > 0) {
params.hypervisorsnapshotreserve = values.hypervisorsnapshotreserve
}
}
} else if (values.qostype === 'hypervisor') {
if (values.diskbytesreadrate != null && values.diskbytesreadrate.length > 0) {
params.bytesreadrate = values.diskbytesreadrate
}
if (values.diskbyteswriterate != null && values.diskbyteswriterate.length > 0) {
params.byteswriterate = values.diskbyteswriterate
}
if (values.diskiopsreadrate != null && values.diskiopsreadrate.length > 0) {
params.iopsreadrate = values.diskiopsreadrate
}
if (values.diskiopswriterate != null && values.diskiopswriterate.length > 0) {
params.iopswriterate = values.diskiopswriterate
}
}
if (values.storagetags != null && values.storagetags.length > 0) {
var tags = values.storagetags.join(',')
params.tags = tags
}
if (values.hosttags != null && values.hosttags.length > 0) {
params.hosttags = values.hosttags
}
if ('deploymentplanner' in values &&
values.deploymentplanner !== undefined &&
values.deploymentplanner != null && values.deploymentplanner.length > 0) {
params.deploymentplanner = values.deploymentplanner
}
if ('deploymentplanner' in values &&
values.deploymentplanner !== undefined &&
values.deploymentplanner === 'ImplicitDedicationPlanner' &&
values.plannermode !== undefined &&
values.plannermode !== '') {
params['serviceofferingdetails[0].key'] = 'ImplicitDedicationMode'
params['serviceofferingdetails[0].value'] = values.plannermode
}
if ('isvolatile' in values && values.isvolatile !== undefined) {
params.isvolatile = values.isvolatile === true
}
if ('systemvmtype' in values && values.systemvmtype !== undefined) {
params.systemvmtype = values.systemvmtype
}
if ('leaseduration' in values && values.leaseduration !== undefined) {
params.leaseduration = values.leaseduration
}
if ('leaseexpiryaction' in values && values.leaseexpiryaction !== undefined) {
params.leaseexpiryaction = values.leaseexpiryaction
}
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[domainIndexes[i]].id)
}
domainId = domainIds.join(',')
}
if (domainId) {
params.domainid = domainId
}
}
var zoneIndexes = values.zoneid
var zoneId = null
if (zoneIndexes && zoneIndexes.length > 0) {
var zoneIds = []
for (var j = 0; j < zoneIndexes.length; j++) {
zoneIds = zoneIds.concat(this.zones[zoneIndexes[j]].id)
}
zoneId = zoneIds.join(',')
}
if (zoneId) {
params.zoneid = zoneId
}
if (values.storagepolicy) {
params.storagepolicy = values.storagepolicy
}
if (values.externaldetails) {
Object.entries(values.externaldetails).forEach(([key, value]) => {
params['externaldetails[0].' + key] = value
})
}
params.sourceofferingid = this.resource.id
this.loading = true
postAPI('cloneServiceOffering', params).then(json => {
const message = this.isSystem
? `${this.$t('message.clone.service.offering')}: `
: `${this.$t('message.clone.compute.offering')}: `
this.$message.success(message + values.name)
this.$emit('refresh-data')
this.closeAction()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
})
},
closeAction () {
this.$emit('close-action')
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
}
}
}
</script>
<style scoped lang="scss">
.form-layout {
width: 80vw;
@media (min-width: 800px) {
width: 700px;
}
}
</style>

View File

@ -0,0 +1,279 @@
// 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.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-alert
v-if="resource"
type="info"
style="margin-bottom: 16px">
<template #message>
<div style="display: block; width: 100%;">
<div style="display: block; margin-bottom: 8px;">
<strong>{{ $t('message.clone.offering.from') }}: {{ resource.name }}</strong>
</div>
<div style="display: block; font-size: 12px;">
{{ $t('message.clone.offering.edit.hint') }}
</div>
</div>
</template>
</a-alert>
<DiskOfferingForm
ref="formRef"
:initialValues="form"
:apiParams="apiParams"
:isAdmin="isAdmin"
@submit="handleSubmit">
<template #form-actions>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</template>
</DiskOfferingForm>
</a-spin>
</div>
</template>
<script>
import DiskOfferingForm from '@/components/offering/DiskOfferingForm'
import { reactive } from 'vue'
import { postAPI } from '@/api'
import { isAdmin } from '@/role'
export default {
name: 'CloneDiskOffering',
components: {
DiskOfferingForm
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
formRef: null,
form: reactive({}),
loading: false
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('cloneDiskOffering')
},
created () {
this.populateFormFromResource()
},
methods: {
populateFormFromResource () {
if (!this.resource) return
const r = this.resource
this.form.name = r.name + ' - Clone'
this.form.displaytext = r.displaytext
if (r.storagetype) {
this.form.storagetype = r.storagetype
}
if (r.provisioningtype) {
this.form.provisioningtype = r.provisioningtype
}
if (r.customized !== undefined) {
this.form.customdisksize = r.customized
}
if (r.disksize) this.form.disksize = r.disksize
if (r.cachemode) {
this.form.writecachetype = r.cachemode
}
if (r.disksizestrictness !== undefined) {
this.form.disksizestrictness = r.disksizestrictness
}
if (r.encrypt !== undefined) {
this.form.encryptdisk = r.encrypt
}
if (r.diskBytesReadRate || r.diskBytesReadRateMax || r.diskBytesWriteRate || r.diskBytesWriteRateMax || r.diskIopsReadRate || r.diskIopsWriteRate) {
this.form.qostype = 'hypervisor'
if (r.diskBytesReadRate) this.form.diskbytesreadrate = r.diskBytesReadRate
if (r.diskBytesReadRateMax) this.form.diskbytesreadratemax = r.diskBytesReadRateMax
if (r.diskBytesWriteRate) this.form.diskbyteswriterate = r.diskBytesWriteRate
if (r.diskBytesWriteRateMax) this.form.diskbyteswriteratemax = r.diskBytesWriteRateMax
if (r.diskIopsReadRate) this.form.diskiopsreadrate = r.diskIopsReadRate
if (r.diskIopsWriteRate) this.form.diskiopswriterate = r.diskIopsWriteRate
} else if (r.miniops || r.maxiops) {
this.form.qostype = 'storage'
if (r.miniops) this.form.diskiopsmin = r.miniops
if (r.maxiops) this.form.diskiopsmax = r.maxiops
if (r.hypervisorsnapshotreserve) this.form.hypervisorsnapshotreserve = r.hypervisorsnapshotreserve
}
if (r.iscustomizediops !== undefined) {
this.form.iscustomizeddiskiops = r.iscustomizediops
}
if (r.tags) {
this.form.tags = r.tags.split(',')
}
if (r.vspherestoragepolicy) this.form.storagepolicy = r.vspherestoragepolicy
},
isAdmin () {
return isAdmin()
},
handleSubmit (e) {
if (e && e.preventDefault) {
e.preventDefault()
}
if (this.loading) return
this.$refs.formRef.validate().then((values) => {
const params = {
sourceofferingid: this.resource.id,
name: values.name
}
if (values.displaytext) {
params.displaytext = values.displaytext
}
if (values.storagetype) {
params.storagetype = values.storagetype
}
if (values.writecachetype) {
params.cachemode = values.writecachetype
}
if (values.provisioningtype) {
params.provisioningtype = values.provisioningtype
}
if (values.customdisksize !== undefined) {
params.customized = values.customdisksize
}
if (values.disksizestrictness !== undefined) {
params.disksizestrictness = values.disksizestrictness
}
if (values.encryptdisk !== undefined) {
params.encrypt = values.encryptdisk
}
if (values.customdisksize !== true && values.disksize) {
params.disksize = values.disksize
}
if (values.qostype === 'storage') {
const customIops = values.iscustomizeddiskiops === true
params.customizediops = customIops
if (!customIops) {
if (values.diskiopsmin != null && values.diskiopsmin.length > 0) {
params.miniops = values.diskiopsmin
}
if (values.diskiopsmax != null && values.diskiopsmax.length > 0) {
params.maxiops = values.diskiopsmax
}
if (values.hypervisorsnapshotreserve != null && values.hypervisorsnapshotreserve.length > 0) {
params.hypervisorsnapshotreserve = values.hypervisorsnapshotreserve
}
}
} else if (values.qostype === 'hypervisor') {
if (values.diskbytesreadrate != null && values.diskbytesreadrate.length > 0) {
params.bytesreadrate = values.diskbytesreadrate
}
if (values.diskbytesreadratemax != null && values.diskbytesreadratemax.length > 0) {
params.bytesreadratemax = values.diskbytesreadratemax
}
if (values.diskbyteswriterate != null && values.diskbyteswriterate.length > 0) {
params.byteswriterate = values.diskbyteswriterate
}
if (values.diskbyteswriteratemax != null && values.diskbyteswriteratemax.length > 0) {
params.byteswriteratemax = values.diskbyteswriteratemax
}
if (values.diskiopsreadrate != null && values.diskiopsreadrate.length > 0) {
params.iopsreadrate = values.diskiopsreadrate
}
if (values.diskiopswriterate != null && values.diskiopswriterate.length > 0) {
params.iopswriterate = values.diskiopswriterate
}
}
if (values.tags != null && values.tags.length > 0) {
const tags = values.tags.join(',')
params.tags = tags
}
if (values.ispublic !== true) {
const domainIndexes = values.domainid
let domainId = null
if (domainIndexes && domainIndexes.length > 0) {
const domainIds = []
const domains = this.$refs.formRef.domains
for (let i = 0; i < domainIndexes.length; i++) {
domainIds.push(domains[domainIndexes[i]].id)
}
domainId = domainIds.join(',')
}
if (domainId) {
params.domainid = domainId
}
}
const zoneIndexes = values.zoneid
let zoneId = null
if (zoneIndexes && zoneIndexes.length > 0) {
const zoneIds = []
const zones = this.$refs.formRef.zones
for (let j = 0; j < zoneIndexes.length; j++) {
zoneIds.push(zones[zoneIndexes[j]].id)
}
zoneId = zoneIds.join(',')
}
if (zoneId) {
params.zoneid = zoneId
}
if (values.storagepolicy) {
params.storagepolicy = values.storagepolicy
}
this.loading = true
postAPI('cloneDiskOffering', params).then(json => {
this.$message.success(`${this.$t('message.success.clone.disk.offering')} ${values.name}`)
this.$emit('refresh-data')
this.closeAction()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
})
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style scoped lang="less">
.form-layout {
width: 80vw;
@media (min-width: 700px) {
width: 550px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,854 @@
// 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.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-alert
v-if="resource"
type="info"
style="margin-bottom: 16px">
<template #message>
<div style="display: block; width: 100%;">
<div style="display: block; margin-bottom: 8px;">
<strong>{{ $t('message.clone.offering.from') }}: {{ resource.name }}</strong>
</div>
<div style="display: block; font-size: 12px;">
{{ $t('message.clone.offering.edit.hint') }}
</div>
</div>
</template>
</a-alert>
<a-form
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
layout="vertical"
>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input
v-focus="true"
v-model:value="form.name"
:placeholder="apiParams.name.description"/>
</a-form-item>
<a-form-item name="displaytext" ref="displaytext">
<template #label>
<tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
</template>
<a-input
v-model:value="form.displaytext"
:placeholder="apiParams.displaytext.description"/>
</a-form-item>
<a-form-item name="internetprotocol" ref="internetprotocol">
<template #label>
<tooltip-label :title="$t('label.internetprotocol')" :tooltip="apiParams.internetprotocol.description"/>
</template>
<span v-if="!ipv6NetworkOfferingEnabled || internetProtocolValue!=='ipv4'">
<a-alert type="warning">
<template #message>
<span v-html="ipv6NetworkOfferingEnabled ? $t('message.offering.internet.protocol.warning') : $t('message.offering.ipv6.warning')" />
</template>
</a-alert>
<br/>
</span>
<a-radio-group
v-model:value="form.internetprotocol"
:disabled="!ipv6NetworkOfferingEnabled"
buttonStyle="solid"
@change="e => { internetProtocolValue = e.target.value }" >
<a-radio-button value="ipv4">
{{ $t('label.ip.v4') }}
</a-radio-button>
<a-radio-button value="dualstack">
{{ $t('label.ip.v4.v6') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="provider" ref="provider">
<template #label>
<tooltip-label :title="$t('label.provider')" :tooltip="apiParams.provider.description"/>
</template>
<a-select
v-model:value="form.provider"
@change="val => handleProviderChange(val)"
showSearch
disabled
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:placeholder="apiParams.provider.description" >
<a-select-option key="" value="">{{ $t('label.none') }}</a-select-option>
<a-select-option :value="'NSX'" :label="$t('label.nsx')"> {{ $t('label.nsx') }} </a-select-option>
<a-select-option :value="'Netris'" :label="$t('label.netris')"> {{ $t('label.netris') }} </a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12" v-if="form.provider === 'NSX'">
<a-form-item name="nsxsupportlb" ref="nsxsupportlb">
<template #label>
<tooltip-label :title="$t('label.nsx.supports.lb')" :tooltip="apiParams.nsxsupportlb.description"/>
</template>
<a-switch v-model:checked="form.nsxsupportlb" @change="val => { handleNsxLbService(val) }" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12" v-if="routingMode === 'dynamic' && form.provider === 'NSX'">
<a-col :md="12" :lg="12">
<a-form-item name="specifyasnumber" ref="specifyasnumber">
<template #label>
<tooltip-label :title="$t('label.specifyasnumber')"/>
</template>
<a-switch v-model:checked="form.specifyasnumber" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="networkmode" ref="networkmode">
<template #label>
<tooltip-label :title="$t('label.networkmode')" :tooltip="apiParams.networkmode.description"/>
</template>
<a-select
optionFilterProp="label"
v-model:value="form.networkmode"
:disabled="provider === 'NSX' || provider === 'Netris'"
@change="val => { handleForNetworkModeChange(val) }"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:placeholder="apiParams.networkmode.description">
<a-select-option v-for="(opt) in networkmodes" :key="opt.name" :label="opt.name">
{{ opt.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="routingmode" ref="routingmode" v-if="networkmode === 'ROUTED' || internetProtocolValue === 'ipv6' || internetProtocolValue === 'dualstack'">
<template #label>
<tooltip-label :title="$t('label.routingmode')" :tooltip="apiParams.routingmode.description"/>
</template>
<a-radio-group
v-model:value="form.routingmode"
buttonStyle="solid"
@change="selected => { routingMode = selected.target.value }">
<a-radio-button value="static">
{{ $t('label.static') }}
</a-radio-button>
<a-radio-button value="dynamic">
{{ $t('label.dynamic') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<template #label>
<tooltip-label :title="$t('label.supportedservices')" :tooltip="apiParams.supportedservices ? apiParams.supportedservices.description : ''"/>
</template>
<div class="supported-services-container" scroll-to="last-child">
<a-spin v-if="!servicesReady" :spinning="true" />
<a-list v-else itemLayout="horizontal" :dataSource="supportedServices">
<template #renderItem="{ item }">
<a-list-item>
<CheckBoxSelectPair
:key="`${item.name}-${item.selectedProvider || 'none'}`"
:resourceKey="item.name"
:checkBoxLabel="item.description"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris'"
:defaultCheckBoxValue="item.defaultChecked"
:defaultSelectValue="item.selectedProvider"
:selectOptions="item.provider"
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
</template>
</a-list>
</div>
</a-form-item>
<a-form-item name="regionlevelvpc" ref="regionlevelvpc" :label="$t('label.service.connectivity.regionlevelvpccapabilitycheckbox')" v-if="connectivityServiceChecked">
<a-switch v-model:checked="form.regionlevelvpc" />
</a-form-item>
<a-form-item name="distributedrouter" ref="distributedrouter" :label="$t('label.service.connectivity.distributedroutercapabilitycheckbox')" v-if="connectivityServiceChecked">
<a-switch v-model:checked="form.distributedrouter" />
</a-form-item>
<a-form-item name="redundantrouter" ref="redundantrouter" :label="$t('label.redundantrouter')" v-if="sourceNatServiceChecked">
<a-switch v-model:checked="form.redundantrouter" />
</a-form-item>
<a-form-item name="serviceofferingid" ref="serviceofferingid">
<a-alert v-if="!isVpcVirtualRouterForAtLeastOneService" type="warning" style="margin-bottom: 10px">
<template #message>
<span v-html="$t('message.vr.alert.upon.network.offering.creation.others')" />
</template>
</a-alert>
<template #label>
<tooltip-label :title="$t('label.serviceofferingid')" :tooltip="apiParams.serviceofferingid.description"/>
</template>
<a-select
showSearch
optionFilterProp="label"
v-model:value="form.serviceofferingid"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="serviceOfferingLoading"
:placeholder="apiParams.serviceofferingid.description">
<a-select-option v-for="(opt) in serviceOfferings" :key="opt.id" :label="opt.name || opt.description">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="ispublic" ref="ispublic" :label="$t('label.ispublic')" v-if="isAdmin()">
<a-switch v-model:checked="form.ispublic" @change="val => { isPublic = val }" />
</a-form-item>
<a-form-item name="domainid" ref="domainid" v-if="!isPublic">
<template #label>
<tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
</template>
<a-select
mode="multiple"
v-model:value="form.domainid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="domainLoading"
:placeholder="apiParams.domainid.description">
<a-select-option v-for="(opt, optIndex) in domains" :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>
<a-form-item name="zoneid" ref="zoneid">
<template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template>
<a-select
id="zone-selection"
mode="multiple"
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description">
<a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
<span>
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px"/>
{{ opt.name || opt.description }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="enable" ref="enable" v-if="apiParams.enable">
<template #label>
<tooltip-label :title="$t('label.enable.vpc.offering')" :tooltip="apiParams.enable.description"/>
</template>
<a-switch v-model:checked="form.enable" />
</a-form-item>
</a-form>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</a-spin>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { getAPI, postAPI } from '@/api'
import { isAdmin } from '@/role'
import { mixinForm } from '@/utils/mixin'
import CheckBoxSelectPair from '@/components/CheckBoxSelectPair'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { BlockOutlined, GlobalOutlined } from '@ant-design/icons-vue'
import { buildVpcServiceCapabilityParams } from '@/composables/useServiceCapabilityParams'
export default {
name: 'CloneVpcOffering',
mixins: [mixinForm],
components: {
CheckBoxSelectPair,
ResourceIcon,
TooltipLabel,
BlockOutlined,
GlobalOutlined
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
selectedDomains: [],
selectedZones: [],
isConserveMode: true,
internetProtocolValue: 'ipv4',
domains: [],
domainLoading: false,
zones: [],
zoneLoading: false,
forNsx: false,
provider: '',
loading: false,
supportedServices: [],
supportedServiceLoading: false,
servicesReady: false,
serviceOfferings: [],
serviceOfferingLoading: false,
isVpcVirtualRouterForAtLeastOneService: false,
connectivityServiceChecked: false,
sourceNatServiceChecked: false,
selectedServiceProviderMap: {},
serviceProviderMap: {},
ipv6NetworkOfferingEnabled: false,
routedNetworkEnabled: false,
routingMode: 'static',
networkmode: '',
networkmodes: [
{
id: 0,
name: 'NATTED'
},
{
id: 1,
name: 'ROUTED'
}
],
isPublic: true,
VPCVR: {
name: 'VpcVirtualRouter',
description: 'VpcVirtualRouter',
enabled: true
},
NSX: {
name: 'Nsx',
description: 'Nsx',
enabled: true
},
Netris: {
name: 'Netris',
description: 'Netris',
enabled: true
}
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('cloneVPCOffering')
},
created () {
this.zones = [
{
id: null,
name: this.$t('label.all.zone')
}
]
this.isPublic = isAdmin()
this.initForm()
this.fetchData()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
regionlevelvpc: true,
distributedrouter: true,
ispublic: this.isPublic,
internetprotocol: this.internetProtocolValue,
nsxsupportlb: true,
routingmode: 'static',
domainid: [],
zoneid: []
})
this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.name') }],
domainid: [{ type: 'array', required: true, message: this.$t('message.error.select') }],
zoneid: [{
type: 'array',
validator: async (rule, value) => {
if (value && value.length > 1 && value.indexOf(0) !== -1) {
return Promise.reject(this.$t('message.error.zone.combined'))
}
return Promise.resolve()
}
}]
})
},
fetchData () {
this.fetchDomainData()
this.fetchZoneData()
this.fetchIpv6NetworkOfferingConfiguration()
this.fetchRoutedNetworkConfiguration()
this.fetchSupportedServiceData()
},
isAdmin () {
return isAdmin()
},
fetchIpv6NetworkOfferingConfiguration () {
this.ipv6NetworkOfferingEnabled = false
const params = { name: 'ipv6.offering.enabled' }
getAPI('listConfigurations', params).then(json => {
const value = json?.listconfigurationsresponse?.configuration?.[0].value || null
this.ipv6NetworkOfferingEnabled = value === 'true'
})
},
fetchRoutedNetworkConfiguration () {
this.routedNetworkEnabled = false
const params = { name: 'routed.network.vpc.enabled' }
getAPI('listConfigurations', params).then(json => {
const value = json?.listconfigurationsresponse?.configuration?.[0].value || null
this.routedNetworkEnabled = value === 'true'
if (!this.routedNetworkEnabled) {
this.networkmodes.pop()
}
})
},
fetchDomainData () {
const params = {}
params.listAll = true
params.showicon = true
params.details = 'min'
this.domainLoading = true
getAPI('listDomains', params).then(json => {
const listDomains = json.listdomainsresponse.domain
this.domains = this.domains.concat(listDomains)
}).finally(() => {
this.domainLoading = false
})
},
fetchZoneData () {
const params = {}
params.showicon = true
this.zoneLoading = true
getAPI('listZones', params).then(json => {
const listZones = json.listzonesresponse.zone
if (listZones) {
this.zones = this.zones.concat(listZones)
}
}).finally(() => {
this.zoneLoading = false
})
},
fetchSupportedServiceData () {
this.supportedServiceLoading = true
getAPI('listSupportedNetworkServices', {}).then(json => {
const networkServices = json.listsupportednetworkservicesresponse.networkservice || []
let services = []
if (this.provider === 'NSX') {
services = this.buildNsxServices(networkServices)
} else if (this.provider === 'Netris') {
services = this.buildNetrisServices(networkServices)
} else {
services = this.buildDefaultServices(networkServices)
}
if (this.networkmode === 'ROUTED') {
services = services.filter(service => {
return !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn'].includes(service.name)
})
if (['NSX', 'Netris'].includes(this.provider)) {
services.push({
name: 'Gateway',
description: 'Gateway',
enabled: true,
provider: [{ name: this.provider }]
})
}
}
for (const i in services) {
services[i].description = services[i].name
}
this.supportedServices = services
this.supportedServiceLoading = false
this.$nextTick(() => {
this.populateFormFromResource()
})
})
},
buildNsxServices (networkServices) {
return [
{ name: 'Dhcp', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] },
{ name: 'Dns', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] },
{ name: 'Lb', enabled: true, provider: [{ name: 'Nsx' }] },
{ name: 'StaticNat', enabled: true, provider: [{ name: 'Nsx' }] },
{ name: 'SourceNat', enabled: true, provider: [{ name: 'Nsx' }] },
{ name: 'NetworkACL', enabled: true, provider: [{ name: 'Nsx' }] },
{ name: 'PortForwarding', enabled: true, provider: [{ name: 'Nsx' }] },
{ name: 'UserData', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] }
]
},
buildNetrisServices (networkServices) {
return [
{ name: 'Dhcp', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] },
{ name: 'Dns', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] },
{ name: 'Lb', enabled: true, provider: [{ name: 'Netris' }] },
{ name: 'StaticNat', enabled: true, provider: [{ name: 'Netris' }] },
{ name: 'SourceNat', enabled: true, provider: [{ name: 'Netris' }] },
{ name: 'NetworkACL', enabled: true, provider: [{ name: 'Netris' }] },
{ name: 'PortForwarding', enabled: true, provider: [{ name: 'Netris' }] },
{ name: 'UserData', enabled: true, provider: [{ name: 'VpcVirtualRouter' }] }
]
},
buildDefaultServices (networkServices) {
return [
{ name: 'Dhcp', provider: [{ name: 'VpcVirtualRouter' }, { name: 'ConfigDrive' }] },
{ name: 'Dns', provider: [{ name: 'VpcVirtualRouter' }, { name: 'ConfigDrive' }] },
{ name: 'Lb', provider: [{ name: 'VpcVirtualRouter' }, { name: 'InternalLbVm' }] },
{ name: 'Gateway', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] },
{ name: 'StaticNat', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] },
{ name: 'SourceNat', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] },
{ name: 'NetworkACL', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] },
{ name: 'PortForwarding', provider: [{ name: 'VpcVirtualRouter' }] },
{ name: 'UserData', provider: [{ name: 'VpcVirtualRouter' }, { name: 'ConfigDrive' }] },
{ name: 'Vpn', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] },
{ name: 'Connectivity', provider: [{ name: 'BigSwitchBcf' }, { name: 'NiciraNvp' }, { name: 'Ovs' }, { name: 'JuniperContrailVpcRouter' }] }
]
},
populateFormFromResource () {
if (!this.resource) return
const r = this.resource
this.form.name = r.name + ' - Clone'
this.form.displaytext = r.displaytext || r.name
if (r.internetprotocol) {
this.form.internetprotocol = r.internetprotocol.toLowerCase()
this.internetProtocolValue = r.internetprotocol.toLowerCase()
}
if (r.service && Array.isArray(r.service)) {
const networkAclService = r.service.find(svc => svc.name === 'NetworkACL')
if (networkAclService && networkAclService.provider && networkAclService.provider.length > 0) {
const providerName = networkAclService.provider[0].name
if (providerName === 'Nsx') {
this.provider = 'NSX'
this.form.provider = 'NSX'
} else if (providerName === 'Netris') {
this.provider = 'Netris'
this.form.provider = 'Netris'
}
}
}
if (r.networkmode) {
this.networkmode = r.networkmode
this.form.networkmode = r.networkmode
}
if (r.routingmode) {
this.routingMode = r.routingmode
this.form.routingmode = r.routingmode
}
if (r.serviceofferingid) {
this.form.serviceofferingid = r.serviceofferingid
}
if (r.service && Array.isArray(r.service)) {
const sourceServiceMap = {}
r.service.forEach(svc => {
if (svc.provider && svc.provider.length > 0) {
const providerName = svc.provider[0].name
sourceServiceMap[svc.name] = providerName
}
})
this.serviceProviderMap = sourceServiceMap
const updatedServices = this.supportedServices.map(svc => {
const serviceCopy = { ...svc, provider: [...svc.provider] }
if (sourceServiceMap[serviceCopy.name]) {
const providerName = sourceServiceMap[serviceCopy.name]
const providerIndex = serviceCopy.provider.findIndex(p => p.name === providerName)
if (providerIndex > 0) {
const targetProvider = serviceCopy.provider[providerIndex]
serviceCopy.provider.splice(providerIndex, 1)
serviceCopy.provider.unshift(targetProvider)
}
serviceCopy.defaultChecked = true
serviceCopy.selectedProvider = providerName
this.selectedServiceProviderMap[serviceCopy.name] = providerName
} else {
serviceCopy.defaultChecked = false
serviceCopy.selectedProvider = null
}
return serviceCopy
})
this.supportedServices = updatedServices
this.connectivityServiceChecked = Boolean(this.selectedServiceProviderMap.Connectivity)
this.sourceNatServiceChecked = Boolean(this.selectedServiceProviderMap.SourceNat)
this.$nextTick(() => {
this.servicesReady = true
this.$nextTick(() => {
this.checkVpcVirtualRouterForServices()
})
})
}
if (r.service && Array.isArray(r.service)) {
const connectivityService = r.service.find(svc => svc.name === 'Connectivity')
if (connectivityService && connectivityService.capability) {
const regionLevelCapability = connectivityService.capability.find(cap => cap.name === 'RegionLevelVpc')
if (regionLevelCapability) {
this.form.regionlevelvpc = regionLevelCapability.value === 'true'
}
const distributedRouterCapability = connectivityService.capability.find(cap => cap.name === 'DistributedRouter')
if (distributedRouterCapability) {
this.form.distributedrouter = distributedRouterCapability.value === 'true'
}
}
const sourceNatService = r.service.find(svc => svc.name === 'SourceNat')
if (sourceNatService && sourceNatService.capability) {
const redundantRouterCapability = sourceNatService.capability.find(cap => cap.name === 'RedundantRouter')
if (redundantRouterCapability) {
this.form.redundantrouter = redundantRouterCapability.value === 'true'
}
}
const gatewayService = r.service.find(svc => svc.name === 'Gateway')
if (gatewayService && gatewayService.capability) {
const redundantRouterCapability = gatewayService.capability.find(cap => cap.name === 'RedundantRouter')
if (redundantRouterCapability) {
this.form.redundantrouter = redundantRouterCapability.value === 'true'
}
}
}
if (this.provider === 'NSX') {
this.form.nsxsupportlb = Boolean(this.serviceProviderMap.Lb)
}
},
async handleProviderChange (value) {
this.provider = value
if (this.provider === 'NSX') {
this.form.nsxsupportlb = true
this.handleNsxLbService(true)
}
this.fetchSupportedServiceData()
},
handleNsxLbService (supportLb) {
if (!supportLb) {
this.supportedServices = this.supportedServices.filter(svc => svc.name !== 'Lb')
delete this.selectedServiceProviderMap.Lb
} else {
const lbExists = this.supportedServices.some(svc => svc.name === 'Lb')
if (!lbExists) {
this.supportedServices.push({
name: 'Lb',
description: 'Lb',
enabled: true,
provider: [{ name: 'Nsx' }]
})
}
}
},
handleSupportedServiceChange (service, checked, provider) {
if (checked) {
const correctProvider = this.serviceProviderMap[service]
if (correctProvider && provider !== correctProvider) {
this.selectedServiceProviderMap[service] = correctProvider
} else {
this.selectedServiceProviderMap[service] = provider
}
} else {
delete this.selectedServiceProviderMap[service]
}
if (service === 'Connectivity') {
this.connectivityServiceChecked = checked
}
if (service === 'SourceNat') {
this.sourceNatServiceChecked = checked
}
this.checkVpcVirtualRouterForServices()
},
checkVpcVirtualRouterForServices () {
this.isVpcVirtualRouterForAtLeastOneService = false
const providers = Object.values(this.selectedServiceProviderMap)
for (const provider of providers) {
if (provider === 'VpcVirtualRouter') {
this.isVpcVirtualRouterForAtLeastOneService = true
break
}
}
if (this.isVpcVirtualRouterForAtLeastOneService && this.serviceOfferings.length === 0) {
this.fetchServiceOfferingData()
}
},
handleForNetworkModeChange (networkMode) {
this.networkmode = networkMode
this.fetchSupportedServiceData()
},
fetchServiceOfferingData () {
const params = {}
params.issystem = true
params.systemvmtype = 'domainrouter'
this.serviceOfferingLoading = true
getAPI('listServiceOfferings', params).then(json => {
const listServiceOfferings = json.listserviceofferingsresponse.serviceoffering
this.serviceOfferings = this.serviceOfferings.concat(listServiceOfferings)
}).finally(() => {
this.serviceOfferingLoading = false
})
},
handleSubmit (e) {
if (e) {
e.preventDefault()
}
if (this.loading) return
this.formRef.value.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
const params = {
sourceofferingid: this.resource.id,
name: values.name
}
if (values.displaytext) {
params.displaytext = values.displaytext
}
if (values.ispublic !== true) {
const domainIndexes = values.domainid
let domainId = null
if (domainIndexes && domainIndexes.length > 0) {
const domainIds = []
for (let i = 0; i < domainIndexes.length; i++) {
domainIds.push(this.domains[domainIndexes[i]].id)
}
domainId = domainIds.join(',')
}
if (domainId) {
params.domainid = domainId
}
}
const zoneIndexes = values.zoneid
let zoneId = null
if (zoneIndexes && zoneIndexes.length > 0) {
const zoneIds = []
for (let j = 0; j < zoneIndexes.length; j++) {
zoneIds.push(this.zones[zoneIndexes[j]].id)
}
zoneId = zoneIds.join(',')
}
if (zoneId) {
params.zoneid = zoneId
}
if (values.internetprotocol) {
params.internetprotocol = values.internetprotocol
}
const forNsx = values.provider === 'NSX'
if (forNsx) {
params.provider = 'NSX'
params.nsxsupportlb = values.nsxsupportlb
}
const forNetris = values.provider === 'Netris'
if (forNetris) {
params.provider = 'Netris'
}
if (values.networkmode) {
params.networkmode = values.networkmode
}
if (values.specifyasnumber !== undefined) {
params.specifyasnumber = values.specifyasnumber
}
if (values.routingmode) {
params.routingmode = values.routingmode
}
if (this.selectedServiceProviderMap != null) {
const supportedServices = Object.keys(this.selectedServiceProviderMap)
if (!forNsx && !forNetris) {
params.supportedservices = supportedServices.join(',')
}
for (const k in supportedServices) {
params['serviceProviderList[' + k + '].service'] = supportedServices[k]
params['serviceProviderList[' + k + '].provider'] = this.selectedServiceProviderMap[supportedServices[k]]
}
buildVpcServiceCapabilityParams(params, values, this.selectedServiceProviderMap, this.isVpcVirtualRouterForAtLeastOneService)
} else {
params.supportedservices = ''
}
if (values.enable !== undefined) {
params.enable = values.enable
}
this.loading = true
postAPI('cloneVPCOffering', params).then(json => {
this.$message.success(`${this.$t('message.success.clone.vpc.offering')} ${values.name}`)
this.$emit('refresh-data')
this.closeAction()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
}).catch(error => {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style scoped lang="scss">
.form-layout {
width: 80vw;
@media (min-width: 800px) {
width: 500px;
}
}
.supported-services-container {
height: 250px;
overflow: auto;
}
</style>