diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 438283136d2c..729f72b23ca2 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -24,15 +24,18 @@ 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 @@ Vlan updateVlanAndPublicIpRange(UpdateVlanIpRangeCmd cmd) throws ConcurrentOpera boolean releasePublicIpRange(ReleasePublicIpRangeCmd cmd); - NetworkOffering createNetworkOffering(CreateNetworkOfferingCmd cmd); + NetworkOffering createNetworkOffering(NetworkOfferingBaseCmd cmd); NetworkOffering updateNetworkOffering(UpdateNetworkOfferingCmd cmd); diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 38e601c790a7..992bb4ac37ba 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -374,6 +374,7 @@ 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"; @@ -628,6 +629,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_CLONE_OFFERING = "BACKUP.CLONE.OFFERING"; 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"; diff --git a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java index 97b95339ecf3..2988a94f5fe4 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java @@ -20,6 +20,7 @@ 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 supportedServices, Map> serviceProviders, Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol, diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8fca652518f2..67d2d57eb3bc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -552,6 +552,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"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java new file mode 100644 index 000000000000..0a5308494749 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java @@ -0,0 +1,144 @@ +// 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.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; + +@APICommand(name = "cloneBackupOffering", + description = "Clones a backup offering from an existing offering", + responseObject = BackupOfferingResponse.class, since = "4.14.0", + authorized = {RoleType.Admin}) +public class CloneBackupOfferingCmd extends BaseAsyncCmd { + + @Inject + protected BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + //////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, type = BaseCmd.CommandType.UUID, + 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 = false, + 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.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; + } + + ///////////////////////////////////////////////////// + /////////////// 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 a 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_CLONE_OFFERING; + } + + @Override + public String getEventDescription() { + return "Cloning backup offering: " + name + " from source offering: " + (sourceOfferingId == null ? "" : sourceOfferingId.toString()); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java index 2e73698e7aa1..84fa23e4a35f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java @@ -48,7 +48,7 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { @Inject - private BackupManager backupManager; + protected BackupManager backupManager; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java new file mode 100644 index 000000000000..d8b54856119b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java @@ -0,0 +1,114 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.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 network offering to clone") + 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 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 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 getAddServices() { + return addServices; + } + + public List 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"); + } + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java index a0559f57dab0..5c39060f9fa3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java @@ -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 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 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 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 getSupportedServices() { - if (!isExternalNetworkProvider()) { - return supportedServices == null ? new ArrayList() : supportedServices; - } else { - List 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> getServiceProviders() { - Map> serviceProviderMap = new HashMap<>(); - if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { - Collection servicesCollection = serviceProviderList.values(); - Iterator iter = servicesCollection.iterator(); - while (iter.hasNext()) { - HashMap services = (HashMap) iter.next(); - String service = services.get("service"); - String provider = services.get("provider"); - List providerList = null; - if (serviceProviderMap.containsKey(service)) { - providerList = serviceProviderMap.get(service); - } else { - providerList = new ArrayList(); - } - providerList.add(provider); - serviceProviderMap.put(service, providerList); - } - } else if (isExternalNetworkProvider()) { - getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); - } - return serviceProviderMap; - } - - private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { - String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() : - VirtualRouterProvider.Type.VirtualRouter.name(); - List unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService")); - List routerSupported = List.of("Dhcp", "Dns", "UserData"); - List 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 getServiceCapabilities(Service service) { - Map capabilityMap = null; - - if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { - capabilityMap = new HashMap(); - Collection serviceCapabilityCollection = serviceCapabilitystList.values(); - Iterator iter = serviceCapabilityCollection.iterator(); - while (iter.hasNext()) { - HashMap svcCapabilityMap = (HashMap) 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 getDetails() { - if (details == null || details.isEmpty()) { - return null; - } - - Collection paramsCollection = details.values(); - Object objlist[] = paramsCollection.toArray(); - Map params = (Map) (objlist[0]); - for (int i = 1; i < objlist.length; i++) { - params.putAll((Map) (objlist[i])); - } - - return params; - } - - public String getServicePackageId() { - Map data = getDetails(); - if (data == null) - return null; - return data.get(NetworkOffering.Detail.servicepackageuuid + ""); - } - - public List getDomainIds() { - if (CollectionUtils.isNotEmpty(domainIds)) { - Set set = new LinkedHashSet<>(domainIds); - domainIds.clear(); - domainIds.addAll(set); - } - return domainIds; - } - - public List getZoneIds() { - if (CollectionUtils.isNotEmpty(zoneIds)) { - Set 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() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java new file mode 100644 index 000000000000..63d4bb4683c3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java @@ -0,0 +1,485 @@ +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 { + + // Abstract methods that subclasses must implement + 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 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 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 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 getSupportedServices() { + if (!isExternalNetworkProvider()) { + return supportedServices == null ? new ArrayList() : supportedServices; + } else { + List 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> getServiceProviders() { + Map> serviceProviderMap = new HashMap<>(); + if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { + Collection servicesCollection = serviceProviderList.values(); + Iterator iter = servicesCollection.iterator(); + while (iter.hasNext()) { + HashMap services = (HashMap) iter.next(); + String service = services.get("service"); + String provider = services.get("provider"); + List providerList = null; + if (serviceProviderMap.containsKey(service)) { + providerList = serviceProviderMap.get(service); + } else { + providerList = new ArrayList(); + } + providerList.add(provider); + serviceProviderMap.put(service, providerList); + } + } else if (isExternalNetworkProvider()) { + getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); + } + return serviceProviderMap; + } + + private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { + String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() : + VirtualRouterProvider.Type.VirtualRouter.name(); + List unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService")); + List routerSupported = List.of("Dhcp", "Dns", "UserData"); + List 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 getServiceCapabilities(Network.Service service) { + Map capabilityMap = null; + + if (serviceCapabilitiesList != null && !serviceCapabilitiesList.isEmpty()) { + capabilityMap = new HashMap(); + Collection serviceCapabilityCollection = serviceCapabilitiesList.values(); + Iterator iter = serviceCapabilityCollection.iterator(); + while (iter.hasNext()) { + HashMap svcCapabilityMap = (HashMap) 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 getDetails() { + if (details == null || details.isEmpty()) { + return null; + } + + Collection paramsCollection = details.values(); + Object objlist[] = paramsCollection.toArray(); + Map params = (Map) (objlist[0]); + for (int i = 1; i < objlist.length; i++) { + params.putAll((Map) (objlist[i])); + } + + return params; + } + + public String getServicePackageId() { + Map data = getDetails(); + if (data == null) + return null; + return data.get(NetworkOffering.Detail.servicepackageuuid + ""); + } + + public List getDomainIds() { + if (CollectionUtils.isNotEmpty(domainIds)) { + Set set = new LinkedHashSet<>(domainIds); + domainIds.clear(); + domainIds.addAll(set); + } + return domainIds; + } + + public List getZoneIds() { + if (CollectionUtils.isNotEmpty(zoneIds)) { + Set 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; + } + + /** + * Compatibility method for camelCase variant - delegates to getTraffictype() + */ + public String getTrafficType() { + return getTraffictype(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java new file mode 100644 index 000000000000..7e576d1b3b20 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java @@ -0,0 +1,72 @@ +// 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.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") +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 disk offering to clone") + 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"); + } + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java new file mode 100644 index 000000000000..2515b873e3e6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java @@ -0,0 +1,72 @@ +// 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.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") +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 service offering to clone") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + + @Override + public void execute() { + 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"); + } + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java new file mode 100644 index 000000000000..b4f9f4ef52f5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java @@ -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 VPC offering to clone") + 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 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 dropServices; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public List getAddServices() { + return addServices; + } + + public List 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"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java index 6b425bc10d21..bf0f6aab5bee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java @@ -28,7 +28,6 @@ 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; @@ -179,9 +178,7 @@ public boolean isExternalNetworkProvider() { } public List 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(), diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index db051313d962..5bbe1ee347a2 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -22,6 +22,7 @@ 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; @@ -136,6 +137,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer */ BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd); + /** + * 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 diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 21995d5ae650..91b8bbe18340 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -66,15 +66,18 @@ 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; @@ -3843,6 +3846,458 @@ protected boolean serviceOfferingExternalDetailsNeedUpdate(final Map 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 domainIds = resolveDomainIds(cmd, sourceOffering); + final List 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 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; + } + + private T getOrDefault(T cmdValue, T defaultValue) { + return cmdValue != null ? cmdValue : defaultValue; + } + + private Boolean resolveBooleanParam(Map requestParams, String paramKey, + java.util.function.Supplier 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 resolveDomainIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) { + List domainIds = cmd.getDomainIds(); + if (domainIds == null || domainIds.isEmpty()) { + domainIds = _serviceOfferingDetailsDao.findDomainIds(sourceOffering.getId()); + } + return domainIds; + } + + private List resolveZoneIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) { + List 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 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 mergeOfferingDetails(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering, CustomOfferingParams customParams) { + final Map cmdDetails = cmd.getDetails(); + final Map mergedDetails = new HashMap<>(); + + if (cmdDetails == null || cmdDetails.isEmpty()) { + Map 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 + public DiskOffering cloneDiskOffering(final CloneDiskOfferingCmd cmd) { + final long userId = CallContext.current().getCallingUserId(); + final DiskOfferingVO sourceOffering = getAndValidateSourceDiskOffering(cmd.getSourceOfferingId()); + final Map 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 domainIds = resolveDomainIdsForDiskOffering(cmd, sourceOffering); + final List 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 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 resolveDomainIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + List domainIds = cmd.getDomainIds(); + if (domainIds == null || domainIds.isEmpty()) { + domainIds = diskOfferingDetailsDao.findDomainIds(sourceOffering.getId()); + } + return domainIds; + } + + private List resolveZoneIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + List 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 mergeDiskOfferingDetails(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + final Map cmdDetails = cmd.getDetails(); + final Map mergedDetails = new HashMap<>(); + + if (cmdDetails == null || cmdDetails.isEmpty()) { + Map 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) { @@ -6573,7 +7028,7 @@ public void checkZoneAccess(final Account caller, final DataCenter zone) { @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()); @@ -7809,6 +8264,370 @@ public boolean deleteNetworkOffering(final DeleteNetworkOfferingCmd cmd) { } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_CREATE, 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); + + // Resolve parameters from source offering and apply add/drop logic + applySourceOfferingValuesToCloneCmd(cmd, sourceOffering); + + return createNetworkOffering(cmd); + } + + /** + * Converts service provider map from internal format to API parameter format. + * + * Internal format: Map> 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> convertToApiParameterFormat(Map> serviceProviderMap) { + Map> apiFormatMap = new HashMap<>(); + int index = 0; + + for (Map.Entry> entry : serviceProviderMap.entrySet()) { + String serviceName = entry.getKey(); + List providers = entry.getValue(); + + for (String provider : providers) { + Map 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, NetworkOfferingVO sourceOffering) { + Long sourceOfferingId = sourceOffering.getId(); + + Map> sourceServiceProviderMap = + _networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId); + + // Build final services list with add/drop support + List finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap); + + Map> finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices); + + Map> sourceServiceCapabilityList = reconstructNetworkServiceCapabilityList(sourceOffering); + + Map sourceDetailsMap = getSourceOfferingDetails(sourceOfferingId); + + List sourceDomainIds = networkOfferingDetailsDao.findDomainIds(sourceOfferingId); + List sourceZoneIds = networkOfferingDetailsDao.findZoneIds(sourceOfferingId); + + applyResolvedValuesToCommand(cmd, sourceOffering, finalServices, finalServiceProviderMap, + sourceServiceCapabilityList, sourceDetailsMap, sourceDomainIds, sourceZoneIds); + } + + private Map getSourceOfferingDetails(Long sourceOfferingId) { + List sourceDetailsVOs = networkOfferingDetailsDao.listDetails(sourceOfferingId); + Map sourceDetailsMap = new HashMap<>(); + for (NetworkOfferingDetailsVO detailVO : sourceDetailsVOs) { + sourceDetailsMap.put(detailVO.getName(), detailVO.getValue()); + } + return sourceDetailsMap; + } + + private List resolveFinalServicesList(CloneNetworkOfferingCmd cmd, + Map> sourceServiceProviderMap) { + + List cmdServices = cmd.getSupportedServices(); + List addServices = cmd.getAddServices(); + List dropServices = cmd.getDropServices(); + + if (cmdServices != null && !cmdServices.isEmpty()) { + return cmdServices; + } + + List finalServices = new ArrayList<>(); + for (Network.Service service : sourceServiceProviderMap.keySet()) { + if (service != Network.Service.Gateway) { + finalServices.add(service.getName()); + } + } + + if (dropServices != null && !dropServices.isEmpty()) { + List 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 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> resolveServiceProviderMap(CloneNetworkOfferingCmd cmd, + Map> sourceServiceProviderMap, List finalServices) { + + if (cmd.getServiceProviders() != null && !cmd.getServiceProviders().isEmpty()) { + return cmd.getServiceProviders(); + } + + Map> finalMap = new HashMap<>(); + for (Map.Entry> entry : sourceServiceProviderMap.entrySet()) { + String serviceName = entry.getKey().getName(); + if (finalServices.contains(serviceName)) { + List 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 finalServices, Map> finalServiceProviderMap, Map> sourceServiceCapabilityList, + Map sourceDetailsMap, List sourceDomainIds, List sourceZoneIds) { + + try { + Map requestParams = cmd.getFullUrlParams(); + + if (cmd.getSupportedServices() == null || cmd.getSupportedServices().isEmpty()) { + setField(cmd, "supportedServices", finalServices); + } + if (cmd.getServiceProviders() == null || cmd.getServiceProviders().isEmpty()) { + Map> 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()) { + setField(cmd, "serviceCapabilitiesList", sourceServiceCapabilityList); + } + + 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> reconstructNetworkServiceCapabilityList(NetworkOfferingVO sourceOffering) { + Map> capabilityList = new HashMap<>(); + int index = 0; + + if (sourceOffering.isDedicatedLB()) { + Map 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 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 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 schemes = new ArrayList<>(); + if (sourceOffering.isPublicLb()) schemes.add("public"); + if (sourceOffering.isInternalLb()) schemes.add("internal"); + Map 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 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 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 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 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 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 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 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 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 { + java.lang.reflect.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 java.lang.reflect.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) { diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index e4219c858da6..4595ebd4c6bc 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -70,6 +70,7 @@ 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; @@ -631,6 +632,12 @@ public VpcOffering createVpcOffering(final String name, final String displayText final String externalProvider, final NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, State state, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber) { + 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())); } @@ -808,6 +815,286 @@ protected void checkCapabilityPerServiceProvider(final Set providers, } } + @Override + 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); + + applySourceOfferingValuesToCloneCmd(cmd, sourceVpcOffering); + + return createVpcOffering(cmd); + } + + private void applySourceOfferingValuesToCloneCmd(CloneVPCOfferingCmd cmd, VpcOffering sourceVpcOffering) { + Long sourceOfferingId = sourceVpcOffering.getId(); + + Map> sourceServiceProviderMap = getVpcOffSvcProvidersMap(sourceOfferingId); + + List finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap); + + Map finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices); + + List sourceDomainIds = vpcOfferingDetailsDao.findDomainIds(sourceOfferingId); + List sourceZoneIds = vpcOfferingDetailsDao.findZoneIds(sourceOfferingId); + + Map 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 with keys like "0.service", "0.capabilitytype", "0.capabilityvalue" + */ + private Map reconstructServiceCapabilityList(VpcOffering sourceOffering) { + Map 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> 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 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 resolveFinalServicesList(CloneVPCOfferingCmd cmd, + Map> sourceServiceProviderMap) { + + List cmdServices = cmd.getSupportedServices(); + List addServices = cmd.getAddServices(); + List dropServices = cmd.getDropServices(); + + if (cmdServices != null && !cmdServices.isEmpty()) { + return cmdServices; + } + + List finalServices = new ArrayList<>(); + for (Network.Service service : sourceServiceProviderMap.keySet()) { + finalServices.add(service.getName()); + } + + if (dropServices != null && !dropServices.isEmpty()) { + List 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(dropServices); + logger.debug("Dropped services from clone: {}", dropServices); + } + + if (addServices != null && !addServices.isEmpty()) { + List 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> resolveServiceProviderMap(CloneVPCOfferingCmd cmd, + Map> sourceServiceProviderMap, List finalServices) { + + if (cmd.getServiceProviders() != null && !cmd.getServiceProviders().isEmpty()) { + return cmd.getServiceProviders(); + } + + Map> finalMap = new HashMap<>(); + for (Map.Entry> entry : sourceServiceProviderMap.entrySet()) { + String serviceName = entry.getKey().getName(); + if (finalServices.contains(serviceName)) { + List 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> 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> convertToServiceProviderListFormat(Map> serviceProviderMap) { + Map> result = new HashMap<>(); + int index = 0; + + for (Map.Entry> entry : serviceProviderMap.entrySet()) { + String serviceName = entry.getKey(); + List providers = entry.getValue(); + + for (String providerName : providers) { + Map 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 finalServices, Map finalServiceProviderMap, + List sourceDomainIds, List sourceZoneIds, + Map 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> 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()) { + ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", sourceServiceCapabilityList); + } + + 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 getRawFieldValue(Object obj, String fieldName, Class 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; + } + private void validateConnectivtyServiceCapabilities(final Set providers, final Map serviceCapabilitystList) { if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { final Collection serviceCapabilityCollection = serviceCapabilitystList.values(); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 47dcf60eb32d..8e868832a664 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -130,6 +130,7 @@ 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; @@ -160,6 +161,8 @@ 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; @@ -323,6 +326,7 @@ 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; @@ -3840,6 +3844,7 @@ public List> getCommands() { 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); @@ -3860,7 +3865,9 @@ public List> getCommands() { 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); @@ -3945,6 +3952,7 @@ public List> getCommands() { 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); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index ef3ba917de74..8ee4a64a5a54 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -41,6 +41,7 @@ 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; @@ -296,6 +297,56 @@ public BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd) { return savedOffering; } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CLONE_OFFERING, eventDescription = "cloning backup offering", create = true) + 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(); + + if (!externalId.equals(sourceOffering.getExternalId())) { + final BackupProvider provider = getBackupProvider(sourceOffering.getZoneId()); + if (!provider.isValidProviderOffering(sourceOffering.getZoneId(), externalId)) { + throw new CloudRuntimeException("Backup offering '" + externalId + "' does not exist on provider " + provider.getName() + " on zone " + sourceOffering.getZoneId()); + } + } + + if (!externalId.equals(sourceOffering.getExternalId())) { + final BackupOffering existingOffering = backupOfferingDao.findByExternalId(externalId, sourceOffering.getZoneId()); + if (existingOffering != null) { + throw new CloudRuntimeException("A backup offering with external ID '" + externalId + "' already exists in this zone"); + } + } + + final BackupOfferingVO clonedOffering = new BackupOfferingVO( + sourceOffering.getZoneId(), + 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()); + } + + logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getSourceOfferingId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")"); + return savedOffering; + } + @Override public Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) { final Long offeringId = cmd.getOfferingId(); @@ -1669,6 +1720,7 @@ public List> getCommands() { 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); diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index 2982c19ccdd4..a8d3927f910e 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -51,15 +51,18 @@ 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 ServiceOffering createServiceOffering(CreateServiceOfferingCmd cmd) { 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 boolean deleteVlanIpRange(DeleteVlanIpRangeCmd cmd) { * @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; } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index d29bab3521a1..89d33a97d555 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -558,6 +558,8 @@ "label.clear.list": "Clear list", "label.clear.notification": "Clear notification", "label.clientid": "Provider Client ID", +"label.clone.compute.offering": "Clone Compute Offering", +"label.clone.system.service.offering": "Clone System Service Offering", "label.close": "Close", "label.cloud.managed": "CloudManaged", "label.cloudian.admin.password": "Admin Service Password", @@ -3215,6 +3217,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", diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 4a32619b8c2f..ed31e94992ef 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -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'))) }] }, { diff --git a/ui/src/views/offering/CloneComputeOffering.vue b/ui/src/views/offering/CloneComputeOffering.vue new file mode 100644 index 000000000000..11ce1d316fbc --- /dev/null +++ b/ui/src/views/offering/CloneComputeOffering.vue @@ -0,0 +1,1413 @@ +// 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. + + + + + +