diff --git a/.asf.yaml b/.asf.yaml index 857324b27e2c..a19e3a60a2ba 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -54,7 +54,6 @@ github: - erikbocks - Imvedansh - Damans227 - - DaanHoogland protected_branches: ~ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2af9f54edc44..a0602871ef1e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,3 +20,5 @@ .pre-commit-config.yaml @jbampton /.github/linters/ @jbampton + +/plugins/network-elements/nsx/ @Pearl1594 @nvazquez diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4edd448067ae..6957d3f54464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,6 +146,7 @@ jobs: smoke/test_vm_snapshot_kvm smoke/test_vm_snapshots smoke/test_volumes + smoke/test_vpc_conserve_mode smoke/test_vpc_ipv6 smoke/test_vpc_redundant smoke/test_vpc_router_nics 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 6524d9de15e0..42395bf89992 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -375,11 +375,13 @@ public class EventTypes { // Service Offerings public static final String EVENT_SERVICE_OFFERING_CREATE = "SERVICE.OFFERING.CREATE"; + public static final String EVENT_SERVICE_OFFERING_CLONE = "SERVICE.OFFERING.CLONE"; public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT"; public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE"; // Disk Offerings public static final String EVENT_DISK_OFFERING_CREATE = "DISK.OFFERING.CREATE"; + public static final String EVENT_DISK_OFFERING_CLONE = "DISK.OFFERING.CLONE"; public static final String EVENT_DISK_OFFERING_EDIT = "DISK.OFFERING.EDIT"; public static final String EVENT_DISK_OFFERING_DELETE = "DISK.OFFERING.DELETE"; @@ -400,6 +402,7 @@ public class EventTypes { // Network offerings public static final String EVENT_NETWORK_OFFERING_CREATE = "NETWORK.OFFERING.CREATE"; + public static final String EVENT_NETWORK_OFFERING_CLONE = "NETWORK.OFFERING.CLONE"; public static final String EVENT_NETWORK_OFFERING_ASSIGN = "NETWORK.OFFERING.ASSIGN"; public static final String EVENT_NETWORK_OFFERING_EDIT = "NETWORK.OFFERING.EDIT"; public static final String EVENT_NETWORK_OFFERING_REMOVE = "NETWORK.OFFERING.REMOVE"; @@ -599,6 +602,7 @@ public class EventTypes { // VPC offerings public static final String EVENT_VPC_OFFERING_CREATE = "VPC.OFFERING.CREATE"; + public static final String EVENT_VPC_OFFERING_CLONE = "VPC.OFFERING.CLONE"; public static final String EVENT_VPC_OFFERING_UPDATE = "VPC.OFFERING.UPDATE"; public static final String EVENT_VPC_OFFERING_DELETE = "VPC.OFFERING.DELETE"; @@ -631,6 +635,7 @@ public class EventTypes { // Backup and Recovery events public static final String EVENT_VM_BACKUP_IMPORT_OFFERING = "BACKUP.IMPORT.OFFERING"; + public static final String EVENT_VM_BACKUP_OFFERING_CLONE = "BACKUP.OFFERING.CLONE"; public static final String EVENT_VM_BACKUP_OFFERING_ASSIGN = "BACKUP.OFFERING.ASSIGN"; public static final String EVENT_VM_BACKUP_OFFERING_REMOVE = "BACKUP.OFFERING.REMOVE"; public static final String EVENT_VM_BACKUP_CREATE = "BACKUP.CREATE"; @@ -1046,11 +1051,13 @@ public class EventTypes { // Service Offerings entityEventDetails.put(EVENT_SERVICE_OFFERING_CREATE, ServiceOffering.class); + entityEventDetails.put(EVENT_SERVICE_OFFERING_CLONE, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_EDIT, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_DELETE, ServiceOffering.class); // Disk Offerings entityEventDetails.put(EVENT_DISK_OFFERING_CREATE, DiskOffering.class); + entityEventDetails.put(EVENT_DISK_OFFERING_CLONE, DiskOffering.class); entityEventDetails.put(EVENT_DISK_OFFERING_EDIT, DiskOffering.class); entityEventDetails.put(EVENT_DISK_OFFERING_DELETE, DiskOffering.class); @@ -1071,6 +1078,7 @@ public class EventTypes { // Network offerings entityEventDetails.put(EVENT_NETWORK_OFFERING_CREATE, NetworkOffering.class); + entityEventDetails.put(EVENT_NETWORK_OFFERING_CLONE, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_ASSIGN, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_EDIT, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_REMOVE, NetworkOffering.class); diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java index ce905b293ff3..80f6a6045c72 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java @@ -98,7 +98,7 @@ enum State { s_fsm.addTransition(State.Running, Event.ScaleDownRequested, State.Scaling); s_fsm.addTransition(State.Stopped, Event.ScaleUpRequested, State.ScalingStoppedCluster); s_fsm.addTransition(State.Scaling, Event.OperationSucceeded, State.Running); - s_fsm.addTransition(State.Scaling, Event.OperationFailed, State.Alert); + s_fsm.addTransition(State.Scaling, Event.OperationFailed, State.Running); s_fsm.addTransition(State.ScalingStoppedCluster, Event.OperationSucceeded, State.Stopped); s_fsm.addTransition(State.ScalingStoppedCluster, Event.OperationFailed, State.Alert); diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java index 37b8907b454a..5a6eaa3f7b9a 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java @@ -18,6 +18,7 @@ import org.apache.cloudstack.acl.ControlledEntity; +import java.util.List; import java.util.Map; import com.cloud.user.Account; @@ -33,8 +34,10 @@ enum KubernetesClusterNodeType { ControlledEntity findByUuid(String uuid); ControlledEntity findByVmId(long vmId); void checkVmCanBeDestroyed(UserVm userVm); + void checkVmAffinityGroupsCanBeUpdated(UserVm userVm); boolean isValidNodeType(String nodeType); Map getServiceOfferingNodeTypeMap(Map> serviceOfferingNodeTypeMap); Map getTemplateNodeTypeMap(Map> templateNodeTypeMap); + Map> getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); void cleanupForAccount(Account account); } diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index 742206c7e3bf..53692f932a4e 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -279,4 +279,6 @@ Network createPrivateNetwork(String networkName, String displayText, long physic IpAddresses getIpAddressesFromIps(String ipAddress, String ip6Address, String macAddress); String getNicVlanValueForExternalVm(NicTO nic); + + Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId); } diff --git a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java index 0bf06be15d87..b7fe3b26761c 100644 --- a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java +++ b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java @@ -108,7 +108,7 @@ LoadBalancer createPublicLoadBalancerRule(String xId, String name, String descri /** * Assign a virtual machine or list of virtual machines, or Map of to a load balancer. */ - boolean assignToLoadBalancer(long lbRuleId, List vmIds, Map> vmIdIpMap, boolean isAutoScaleVM); + boolean assignToLoadBalancer(long lbRuleId, List vmIds, Map> vmIdIpMap, Map vmIdNetworkMap, boolean isAutoScaleVM); boolean assignSSLCertToLoadBalancerRule(Long lbRuleId, String certName, String publicCert, String privateKey); diff --git a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java index 17f49bb36521..f84602232159 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java @@ -84,4 +84,6 @@ public enum State { NetworkOffering.RoutingMode getRoutingMode(); Boolean isSpecifyAsNumber(); + + boolean isConserveMode(); } 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..891cfb02d9df 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,12 +35,14 @@ 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, Long serviceOfferingId, String externalProvider, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, VpcOffering.State state, - NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber); + NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode); Pair,Integer> listVpcOfferings(ListVPCOfferingsCmd cmd); diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index db7665724973..9e56bf4f17b2 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -96,6 +96,7 @@ public interface VmDetailConstants { String CKS_NODE_TYPE = "node"; String OFFERING = "offering"; String TEMPLATE = "template"; + String AFFINITY_GROUP = "affinitygroup"; // VMware to KVM VM migrations specific String VMWARE_TO_KVM_PREFIX = "vmware-to-kvm"; diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java index 018e5f5bab5a..03992c0c1c7c 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java @@ -66,5 +66,4 @@ public interface AffinityGroupService { boolean isAffinityGroupAvailableInDomain(long affinityGroupId, long domainId); - } diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java index 9995d8039e1f..96ca35f264ca 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java @@ -29,6 +29,9 @@ public class AffinityProcessorBase extends AdapterBase implements AffinityGroupProcessor { + public static final String AFFINITY_TYPE_HOST = "host affinity"; + public static final String AFFINITY_TYPE_HOST_ANTI = "host anti-affinity"; + protected String _type; @Override 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 25ba233cf80e..3d827641358b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -559,6 +559,7 @@ public class ApiConstants { public static final String USE_STORAGE_REPLICATION = "usestoragereplication"; public static final String SOURCE_CIDR_LIST = "sourcecidrlist"; + public static final String SOURCE_OFFERING_ID = "sourceofferingid"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; public static final String SSL_VERIFICATION = "sslverification"; public static final String START_ASN = "startasn"; @@ -986,6 +987,7 @@ public class ApiConstants { public static final String REGION_ID = "regionid"; public static final String VPC_OFF_ID = "vpcofferingid"; public static final String VPC_OFF_NAME = "vpcofferingname"; + public static final String VPC_OFFERING_CONSERVE_MODE = "vpcofferingconservemode"; public static final String NETWORK = "network"; public static final String VPC_ID = "vpcid"; public static final String VPC_NAME = "vpcname"; @@ -1244,6 +1246,13 @@ public class ApiConstants { public static final String MAX_SIZE = "maxsize"; public static final String NODE_TYPE_OFFERING_MAP = "nodeofferings"; public static final String NODE_TYPE_TEMPLATE_MAP = "nodetemplates"; + public static final String NODE_TYPE_AFFINITY_GROUP_MAP = "nodeaffinitygroups"; + public static final String CONTROL_AFFINITY_GROUP_IDS = "controlaffinitygroupids"; + public static final String CONTROL_AFFINITY_GROUP_NAMES = "controlaffinitygroupnames"; + public static final String WORKER_AFFINITY_GROUP_IDS = "workeraffinitygroupids"; + public static final String WORKER_AFFINITY_GROUP_NAMES = "workeraffinitygroupnames"; + public static final String ETCD_AFFINITY_GROUP_IDS = "etcdaffinitygroupids"; + public static final String ETCD_AFFINITY_GROUP_NAMES = "etcdaffinitygroupnames"; public static final String BOOT_TYPE = "boottype"; public static final String BOOT_MODE = "bootmode"; 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..500a77f3d4fc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +import java.util.Arrays; +import java.util.List; +import java.util.function.LongFunction; + +@APICommand(name = "cloneBackupOffering", + description = "Clones a backup offering from an existing offering", + responseObject = BackupOfferingResponse.class, since = "4.23.0", + authorized = {RoleType.Admin}) +public class CloneBackupOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver { + + @Inject + protected BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + //////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, type = BaseCmd.CommandType.UUID, entityType = BackupOfferingResponse.class, + required = true, description = "The ID of the source backup offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true, + description = "The name of the cloned offering") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, required = false, + description = "The description of the cloned offering") + private String description; + + @Parameter(name = ApiConstants.EXTERNAL_ID, type = BaseCmd.CommandType.STRING, required = false, + description = "The backup offering ID (from backup provider side)") + private String externalId; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "The zone ID", required = false) + private Long zoneId; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.STRING, + description = "the ID of the containing domain(s) as comma separated string, public for public offerings", + length = 4096) + private String domainIds; + + @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = BaseCmd.CommandType.BOOLEAN, + description = "Whether users are allowed to create adhoc backups and backup schedules", required = false) + private Boolean userDrivenBackups; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public String getName() { + return name; + } + + public String getExternalId() { + return externalId; + } + + public Long getZoneId() { + return zoneId; + } + + public String getDescription() { + return description; + } + + public Boolean getUserDrivenBackups() { + return userDrivenBackups; + } + + public List getDomainIds() { + if (domainIds != null && !domainIds.isEmpty()) { + return Arrays.asList(Arrays.stream(domainIds.split(",")).map(domainId -> Long.parseLong(domainId.trim())).toArray(Long[]::new)); + } + LongFunction> defaultDomainsProvider = null; + if (backupManager != null) { + defaultDomainsProvider = backupManager::getBackupOfferingDomains; + } + return resolveDomainIds(domainIds, sourceOfferingId, defaultDomainsProvider, "backup offering"); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + BackupOffering policy = backupManager.cloneBackupOffering(this); + if (policy == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone backup offering"); + } + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(policy); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE; + } + + @Override + public String getEventDescription() { + return "Cloning backup offering: " + name + " from source offering: " + (sourceOfferingId == null ? "" : sourceOfferingId.toString()); + } +} 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 f852f7e25776..4cf27c561508 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 @@ -54,7 +54,7 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { @Inject - private BackupManager backupManager; + protected BackupManager backupManager; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -86,7 +86,8 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { type = CommandType.LIST, collectionType = CommandType.UUID, entityType = DomainResponse.class, - description = "the ID of the containing domain(s), null for public offerings") + description = "the ID of the containing domain(s), null for public offerings", + since = "4.23.0") private List domainIds; ///////////////////////////////////////////////////// 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..19760ffaaa10 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.network; + +import java.util.List; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkOfferingResponse; + +import com.cloud.offering.NetworkOffering; + +@APICommand(name = "cloneNetworkOffering", + description = "Clones a network offering. All parameters are copied from the source offering unless explicitly overridden. " + + "Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.", + responseObject = NetworkOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneNetworkOfferingCmd extends NetworkOfferingBaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = NetworkOfferingResponse.class, + required = true, + description = "The ID of the source network offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = "addservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to add to the cloned offering (in addition to source offering services). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List 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/CreatePhysicalNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java index 75444bebd3dd..097b8a5b5458 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java @@ -75,7 +75,7 @@ public class CreatePhysicalNetworkCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.ISOLATION_METHODS, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "The isolation method for the physical Network[VLAN/L3/GRE]") + description = "The isolation method for the physical Network[VLAN/VXLAN/GRE/STT/BCF_SEGMENT/SSP/ODL/L3VPN/VCS/NSX/NETRIS]") private List isolationMethods; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the physical Network") 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..1c832b7217ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java @@ -0,0 +1,493 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.network; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Network; +import com.cloud.network.VirtualRouterProvider; +import com.cloud.offering.NetworkOffering; +import com.cloud.user.Account; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.cloud.network.Network.Service.Dhcp; +import static com.cloud.network.Network.Service.Dns; +import static com.cloud.network.Network.Service.Firewall; +import static com.cloud.network.Network.Service.Lb; +import static com.cloud.network.Network.Service.NetworkACL; +import static com.cloud.network.Network.Service.PortForwarding; +import static com.cloud.network.Network.Service.SourceNat; +import static com.cloud.network.Network.Service.StaticNat; +import static com.cloud.network.Network.Service.UserData; + +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; + +public abstract class NetworkOfferingBaseCmd extends BaseCmd { + + public abstract String getGuestIpType(); + public abstract String getTraffictype(); + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the network offering") + private String networkOfferingName; + + @Parameter(name = ApiConstants.DISPLAY_TEXT, type = CommandType.STRING, description = "The display text of the network offering, defaults to the value of 'name'.") + private String displayText; + + @Parameter(name = ApiConstants.TAGS, type = CommandType.STRING, description = "The tags for the network offering.", length = 4096) + private String tags; + + @Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "True if network offering supports VLANs") + private Boolean specifyVlan; + + @Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "The availability of network offering. The default value is Optional. " + + " Another value is Required, which will make it as the default network offering for new networks ") + private String availability; + + @Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "Data transfer rate in megabits per second allowed") + private Integer networkRate; + + @Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, description = "True if the network offering is IP conserve mode enabled") + private Boolean conserveMode; + + @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, + type = CommandType.UUID, + entityType = ServiceOfferingResponse.class, + description = "The service offering ID used by virtual router provider") + private Long serviceOfferingId; + + @Parameter(name = ApiConstants.INTERNET_PROTOCOL, + type = CommandType.STRING, + description = "The internet protocol of network offering. Options are IPv4 and dualstack. Default is IPv4. dualstack will create a network offering that supports both IPv4 and IPv6", + since = "4.17.0") + private String internetProtocol; + + @Parameter(name = ApiConstants.SUPPORTED_SERVICES, + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services supported by the network offering") + private List 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; + } + + ///////////////////////////////////////////////////// + /////////////// 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..8d822be203ad --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DiskOfferingResponse; + +import com.cloud.offering.DiskOffering; + +@APICommand(name = "cloneDiskOffering", + description = "Clones a disk offering. All parameters from createDiskOffering are available. If not specified, values will be copied from the source offering.", + responseObject = DiskOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class CloneDiskOfferingCmd extends CreateDiskOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = DiskOfferingResponse.class, + required = true, + description = "The ID of the source disk offering to clone from") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + DiskOffering result = _configService.cloneDiskOffering(this); + if (result != null) { + DiskOfferingResponse response = _responseGenerator.createDiskOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone disk offering"); + } + } +} 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..d01ca4c195da --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; + +import com.cloud.offering.ServiceOffering; + +@APICommand(name = "cloneServiceOffering", + description = "Clones a service offering. All parameters from createServiceOffering are available. If not specified, values will be copied from the source offering.", + responseObject = ServiceOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class CloneServiceOfferingCmd extends CreateServiceOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = CommandType.UUID, + entityType = ServiceOfferingResponse.class, + required = true, + description = "The ID of the source service offering to clone from") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + + @Override + public void execute() { + try { + ServiceOffering result = _configService.cloneServiceOffering(this); + if (result != null) { + ServiceOfferingResponse response = _responseGenerator.createServiceOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone service offering"); + } + } catch (com.cloud.exception.InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (com.cloud.utils.exception.CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } +} 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..2148ff5c2d4f --- /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 source VPC offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = "addservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to add to the cloned offering (in addition to source offering services). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List 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..2b934a60da7a 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; @@ -161,6 +160,12 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { description = "the routing mode for the VPC offering. Supported types are: Static or Dynamic.") private String routingMode; + @Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, + since = "4.23.0", + description = "True if the VPC offering is IP conserve mode enabled, allowing public IPs to be used across multiple VPC tiers. Default value is false") + private Boolean conserveMode; + + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -179,9 +184,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(), @@ -311,6 +314,10 @@ public String getRoutingMode() { return routingMode; } + public boolean isConserveMode() { + return BooleanUtils.toBoolean(conserveMode); + } + @Override public void create() throws ResourceAllocationException { VpcOffering vpcOff = _vpcProvSvc.createVpcOffering(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java index 056807b9b535..2bc5fc2ee68b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java @@ -54,7 +54,6 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class CreatePortForwardingRuleCmd extends BaseAsyncCreateCmd implements PortForwardingRule { - // /////////////////////////////////////////////////// // ////////////// API parameters ///////////////////// // /////////////////////////////////////////////////// @@ -278,13 +277,7 @@ public State getState() { @Override public long getNetworkId() { IpAddress ip = _entityMgr.findById(IpAddress.class, getIpAddressId()); - Long ntwkId = null; - - if (ip.getAssociatedWithNetworkId() != null) { - ntwkId = ip.getAssociatedWithNetworkId(); - } else { - ntwkId = networkId; - } + Long ntwkId = _networkService.getPreferredNetworkIdForPublicIpRuleAssignment(ip, networkId); if (ntwkId == null) { throw new InvalidParameterValueException("Unable to create port forwarding rule for the ipAddress id=" + ipAddressId + " as ip is not associated with any network and no networkId is passed in"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java index 6d8d356cea4d..cc7cd2382b75 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Map; +import com.cloud.network.Network; +import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.APICommand; @@ -72,7 +74,7 @@ public class AssignToLoadBalancerRuleCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID_IP, type = CommandType.MAP, - description = "VM ID and IP map, vmidipmap[0].vmid=1 vmidipmap[0].vmip=10.1.1.75", + description = "VM ID and IP map, vmidipmap[0].vmid=1 vmidipmap[0].vmip=10.1.1.75. (Optional, for VPC Conserve Mode) Pass vmnetworkid. Example: vmidipmap[0].vmnetworkid=NETWORK_TIER_UUID", since = "4.4") private Map vmIdIpMap; @@ -116,8 +118,9 @@ public String getEventDescription() { } - public Map> getVmIdIpListMap() { - Map> vmIdIpsMap = new HashMap>(); + public Pair>, Map> getVmIdIpListMapAndVmIdNetworkMap() { + Map> vmIdIpsMap = new HashMap<>(); + Map vmIdNetworkMap = new HashMap<>(); if (vmIdIpMap != null && !vmIdIpMap.isEmpty()) { Collection idIpsCollection = vmIdIpMap.values(); Iterator iter = idIpsCollection.iterator(); @@ -125,6 +128,7 @@ public Map> getVmIdIpListMap() { HashMap idIpsMap = (HashMap)iter.next(); String vmId = idIpsMap.get("vmid"); String vmIp = idIpsMap.get("vmip"); + String vmNetworkUuid = idIpsMap.get("vmnetworkid"); VirtualMachine lbvm = _entityMgr.findByUuid(VirtualMachine.class, vmId); if (lbvm == null) { @@ -145,25 +149,35 @@ public Map> getVmIdIpListMap() { ipsList = new ArrayList(); } ipsList.add(vmIp); + + if (vmNetworkUuid != null) { + Network vmNetwork = _entityMgr.findByUuid(Network.class, vmNetworkUuid); + if (vmNetwork == null) { + throw new InvalidParameterValueException("Unable to find Network ID: " + vmNetworkUuid); + } + vmIdNetworkMap.put(longVmId, vmNetwork.getId()); + } vmIdIpsMap.put(longVmId, ipsList); } } - return vmIdIpsMap; + return new Pair<>(vmIdIpsMap, vmIdNetworkMap); } @Override public void execute() { CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID) + " Instances IDs: " + StringUtils.join(getVirtualMachineIds(), ",")); - Map> vmIdIpsMap = getVmIdIpListMap(); + Pair>, Map> mapsPair = getVmIdIpListMapAndVmIdNetworkMap(); + Map> vmIdIpsMap = mapsPair.first(); + Map vmIdNetworkMap = mapsPair.second(); boolean result = false; try { - result = _lbService.assignToLoadBalancer(getLoadBalancerId(), virtualMachineIds, vmIdIpsMap, false); + result = _lbService.assignToLoadBalancer(getLoadBalancerId(), virtualMachineIds, vmIdIpsMap, vmIdNetworkMap, false); }catch (CloudRuntimeException ex) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to assign load balancer rule"); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to assign load balancer rule due to: " + ex.getMessage()); } if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java index 48097e51d992..aed56a369089 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java @@ -94,6 +94,10 @@ public class FirewallRuleResponse extends BaseResponse { @Param(description = "The ID of the guest Network the port forwarding rule belongs to") private String networkId; + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "The Name of the guest Network the port forwarding rule belongs to") + private String networkName; + @SerializedName(ApiConstants.FOR_DISPLAY) @Param(description = "Is firewall for display to the regular user", since = "4.4", authorized = {RoleType.Admin}) private Boolean forDisplay; @@ -223,6 +227,10 @@ public void setNetworkId(String networkId) { this.networkId = networkId; } + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + public void setForDisplay(Boolean forDisplay) { this.forDisplay = forDisplay; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java index a0516e660e48..2e821dae52de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java @@ -102,6 +102,10 @@ public class VpcOfferingResponse extends BaseResponse { @Param(description = "The routing mode for the network offering, supported types are Static or Dynamic.") private String routingMode; + @SerializedName(ApiConstants.CONSERVE_MODE) + @Param(description = "True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers.", since = "4.23.0") + private Boolean conserveMode; + public void setId(String id) { this.id = id; } @@ -201,4 +205,12 @@ public String getRoutingMode() { public void setRoutingMode(String routingMode) { this.routingMode = routingMode; } + + public Boolean getConserveMode() { + return conserveMode; + } + + public void setConserveMode(Boolean conserveMode) { + this.conserveMode = conserveMode; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java index 2648ba836785..acfabb113502 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java @@ -73,6 +73,10 @@ public class VpcResponse extends BaseResponseWithAnnotations implements Controll @Param(description = "VPC offering name the VPC is created from", since = "4.13.2") private String vpcOfferingName; + @SerializedName(ApiConstants.VPC_OFFERING_CONSERVE_MODE) + @Param(description = "true if VPC offering is ip conserve mode enabled", since = "4.23") + private Boolean vpcOfferingConserveMode; + @SerializedName(ApiConstants.CREATED) @Param(description = "The date this VPC was created") private Date created; @@ -197,6 +201,10 @@ public void setDisplayText(final String displayText) { this.displayText = displayText; } + public void setVpcOfferingConserveMode(Boolean vpcOfferingConserveMode) { + this.vpcOfferingConserveMode = vpcOfferingConserveMode; + } + public void setCreated(final Date created) { this.created = created; } 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 e83db3a25895..6c0121a3e4d8 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; @@ -140,6 +141,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer List getBackupOfferingDomains(final Long offeringId); + /** + * Clone an existing backup offering with updated values + * @param cmd clone backup offering cmd + */ + BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd); + /** * List backup offerings * @param ListBackupOfferingsCmd API cmd diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java new file mode 100644 index 000000000000..a1412d5a76a7 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java @@ -0,0 +1,301 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneBackupOfferingCmdTest { + + private CloneBackupOfferingCmd cloneBackupOfferingCmd; + + @Mock + private BackupManager backupManager; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private BackupOffering mockBackupOffering; + + @Mock + private BackupOfferingResponse mockBackupOfferingResponse; + + @Before + public void setUp() { + cloneBackupOfferingCmd = new CloneBackupOfferingCmd(); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "backupManager", backupManager); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneBackupOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetName() { + String name = "ClonedBackupOffering"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", name); + assertEquals(name, cloneBackupOfferingCmd.getName()); + } + + @Test + public void testGetDescription() { + String description = "Cloned Backup Offering Description"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", description); + assertEquals(description, cloneBackupOfferingCmd.getDescription()); + } + + @Test + public void testGetZoneId() { + Long zoneId = 123L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", zoneId); + assertEquals(zoneId, cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testGetExternalId() { + String externalId = "external-backup-123"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", externalId); + assertEquals(externalId, cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testGetAllowUserDrivenBackups() { + Boolean allowUserDrivenBackups = true; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", allowUserDrivenBackups); + assertEquals(allowUserDrivenBackups, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testAllowUserDrivenBackupsDefaultTrue() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", null); + Boolean result = cloneBackupOfferingCmd.getUserDrivenBackups(); + assertTrue(result == null || result); + } + + @Test + public void testAllowUserDrivenBackupsFalse() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false); + assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testExecuteSuccess() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering); + when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse); + + cloneBackupOfferingCmd.execute(); + + assertNotNull(cloneBackupOfferingCmd.getResponseObject()); + assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteFailure() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(null); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone backup offering", e.getMessage()); + } + } + + @Test + public void testExecuteWithInvalidParameterException() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))) + .thenThrow(new InvalidParameterValueException("Invalid source offering ID")); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode()); + assertEquals("Invalid source offering ID", e.getMessage()); + } + } + + @Test + public void testExecuteWithCloudRuntimeException() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))) + .thenThrow(new CloudRuntimeException("Runtime error during clone")); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Runtime error during clone", e.getMessage()); + } + } + + @Test + public void testExecuteSuccessWithAllParameters() throws Exception { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Test Description"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "ext-123"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering); + when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse); + + cloneBackupOfferingCmd.execute(); + + assertNotNull(cloneBackupOfferingCmd.getResponseObject()); + assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject()); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Cloned backup offering for testing"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "external-backup-123"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName()); + assertEquals("Cloned backup offering for testing", cloneBackupOfferingCmd.getDescription()); + assertEquals(Long.valueOf(123L), cloneBackupOfferingCmd.getZoneId()); + assertEquals("external-backup-123", cloneBackupOfferingCmd.getExternalId()); + assertEquals(Boolean.TRUE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testCloneWithMinimalParameters() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Description"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName()); + assertEquals("Description", cloneBackupOfferingCmd.getDescription()); + + assertNull(cloneBackupOfferingCmd.getZoneId()); + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testNameNullByDefault() { + assertNull(cloneBackupOfferingCmd.getName()); + } + + @Test + public void testDescriptionNullByDefault() { + assertNull(cloneBackupOfferingCmd.getDescription()); + } + + @Test + public void testZoneIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testExternalIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testCloneBackupOfferingInheritingZone() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited zone"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertNull(cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testCloneBackupOfferingInheritingExternalId() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited external ID"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testCloneBackupOfferingOverridingZone() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with new zone"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 456L); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals(Long.valueOf(456L), cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testCloneBackupOfferingDisallowUserDrivenBackups() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone without user-driven backups"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false); + + assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java new file mode 100644 index 000000000000..096395b1359e --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java @@ -0,0 +1,324 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.network; + +import com.cloud.offering.NetworkOffering; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkOfferingResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneNetworkOfferingCmdTest { + + private CloneNetworkOfferingCmd cloneNetworkOfferingCmd; + + @Mock + private com.cloud.configuration.ConfigurationService configService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private NetworkOffering mockNetworkOffering; + + @Mock + private NetworkOfferingResponse mockNetworkOfferingResponse; + + @Before + public void setUp() { + cloneNetworkOfferingCmd = new CloneNetworkOfferingCmd(); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_configService", configService); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneNetworkOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetAddServices() { + List addServices = Arrays.asList("Dhcp", "Dns"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices); + assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices()); + } + + @Test + public void testGetDropServices() { + List dropServices = Arrays.asList("Firewall", "Vpn"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices); + assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices()); + } + + @Test + public void testGetGuestIpType() { + String guestIpType = "Isolated"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", guestIpType); + assertEquals(guestIpType, cloneNetworkOfferingCmd.getGuestIpType()); + } + + @Test + public void testGetTraffictype() { + String trafficType = "GUEST"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", trafficType); + assertEquals(trafficType, cloneNetworkOfferingCmd.getTraffictype()); + } + + @Test + public void testGetName() { + String name = "ClonedNetworkOffering"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name); + assertEquals(name, cloneNetworkOfferingCmd.getNetworkOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned Network Offering Display Text"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneNetworkOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedNetworkOffering"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", null); + assertEquals(name, cloneNetworkOfferingCmd.getDisplayText()); + } + + @Test + public void testGetAvailability() { + String availability = "Required"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", availability); + assertEquals(availability, cloneNetworkOfferingCmd.getAvailability()); + } + + @Test + public void testGetTags() { + String tags = "tag1,tag2,tag3"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "tags", tags); + assertEquals(tags, cloneNetworkOfferingCmd.getTags()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(mockNetworkOffering); + when(responseGenerator.createNetworkOfferingResponse(mockNetworkOffering)).thenReturn(mockNetworkOfferingResponse); + + cloneNetworkOfferingCmd.execute(); + + assertNotNull(cloneNetworkOfferingCmd.getResponseObject()); + assertEquals(mockNetworkOfferingResponse, cloneNetworkOfferingCmd.getResponseObject()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(null); + + try { + cloneNetworkOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone network offering", e.getMessage()); + throw e; + } + } + + @Test + public void testGetConserveMode() { + Boolean conserveMode = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", conserveMode); + assertEquals(conserveMode, cloneNetworkOfferingCmd.getConserveMode()); + } + + @Test + public void testGetSpecifyVlan() { + Boolean specifyVlan = false; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", specifyVlan); + assertEquals(specifyVlan, cloneNetworkOfferingCmd.getSpecifyVlan()); + } + + @Test + public void testGetSpecifyIpRanges() { + Boolean specifyIpRanges = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyIpRanges", specifyIpRanges); + assertEquals(specifyIpRanges, cloneNetworkOfferingCmd.getSpecifyIpRanges()); + } + + @Test + public void testGetIsPersistent() { + Boolean isPersistent = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", isPersistent); + assertEquals(isPersistent, cloneNetworkOfferingCmd.getIsPersistent()); + } + + @Test + public void testGetEgressDefaultPolicy() { + Boolean egressDefaultPolicy = false; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", egressDefaultPolicy); + assertEquals(egressDefaultPolicy, cloneNetworkOfferingCmd.getEgressDefaultPolicy()); + } + + @Test + public void testGetServiceOfferingId() { + Long serviceOfferingId = 456L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", serviceOfferingId); + assertEquals(serviceOfferingId, cloneNetworkOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testGetForVpc() { + Boolean forVpc = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "forVpc", forVpc); + assertEquals(forVpc, cloneNetworkOfferingCmd.getForVpc()); + } + + @Test + public void testGetMaxConnections() { + Integer maxConnections = 1000; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "maxConnections", maxConnections); + assertEquals(maxConnections, cloneNetworkOfferingCmd.getMaxconnections()); + } + + @Test + public void testGetNetworkRate() { + Integer networkRate = 200; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", networkRate); + assertEquals(networkRate, cloneNetworkOfferingCmd.getNetworkRate()); + } + + @Test + public void testGetInternetProtocol() { + String internetProtocol = "ipv4"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "internetProtocol", internetProtocol); + assertEquals(internetProtocol, cloneNetworkOfferingCmd.getInternetProtocol()); + } + + @Test + public void testAddServicesNullByDefault() { + assertNull(cloneNetworkOfferingCmd.getAddServices()); + } + + @Test + public void testDropServicesNullByDefault() { + assertNull(cloneNetworkOfferingCmd.getDropServices()); + } + + @Test + public void testSupportedServicesParameter() { + List supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "supportedServices", supportedServices); + assertEquals(supportedServices, cloneNetworkOfferingCmd.getSupportedServices()); + } + + @Test + public void testServiceProviderListParameter() { + Map> serviceProviderList = new HashMap<>(); + + HashMap dhcpProvider = new HashMap<>(); + dhcpProvider.put("service", "Dhcp"); + dhcpProvider.put("provider", "VirtualRouter"); + + HashMap dnsProvider = new HashMap<>(); + dnsProvider.put("service", "Dns"); + dnsProvider.put("provider", "VirtualRouter"); + + serviceProviderList.put("0", dhcpProvider); + serviceProviderList.put("1", dnsProvider); + + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceProviderList", serviceProviderList); + + Map> result = cloneNetworkOfferingCmd.getServiceProviders(); + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get("Dhcp")); + assertNotNull(result.get("Dns")); + assertEquals("VirtualRouter", result.get("Dhcp").get(0)); + assertEquals("VirtualRouter", result.get("Dns").get(0)); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", "ClonedOffering"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", "Cloned Offering Display"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", "Optional"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", "Isolated"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", "GUEST"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", true); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", false); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", true); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", false); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", 200); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", 456L); + + assertEquals(Long.valueOf(123L), cloneNetworkOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedOffering", cloneNetworkOfferingCmd.getNetworkOfferingName()); + assertEquals("Cloned Offering Display", cloneNetworkOfferingCmd.getDisplayText()); + assertEquals("Optional", cloneNetworkOfferingCmd.getAvailability()); + assertEquals("Isolated", cloneNetworkOfferingCmd.getGuestIpType()); + assertEquals("GUEST", cloneNetworkOfferingCmd.getTraffictype()); + assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getConserveMode()); + assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getSpecifyVlan()); + assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getIsPersistent()); + assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getEgressDefaultPolicy()); + assertEquals(Integer.valueOf(200), cloneNetworkOfferingCmd.getNetworkRate()); + assertEquals(Long.valueOf(456L), cloneNetworkOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testCloneWithAddAndDropServices() { + List addServices = Arrays.asList("StaticNat", "PortForwarding"); + List dropServices = Arrays.asList("Vpn"); + + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices); + + assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices()); + assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java new file mode 100644 index 000000000000..b4f7c55bd1fa --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java @@ -0,0 +1,669 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.offering; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.vm.lease.VMLeaseManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneServiceOfferingCmdTest { + + private CloneServiceOfferingCmd cloneServiceOfferingCmd; + + @Mock + private com.cloud.configuration.ConfigurationService configService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private ServiceOffering mockServiceOffering; + + @Mock + private ServiceOfferingResponse mockServiceOfferingResponse; + + @Before + public void setUp() { + cloneServiceOfferingCmd = new CloneServiceOfferingCmd(); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_configService", configService); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetServiceOfferingName() { + String name = "ClonedServiceOffering"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name); + assertEquals(name, cloneServiceOfferingCmd.getServiceOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned Service Offering Display Text"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneServiceOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedServiceOffering"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", null); + assertEquals(name, cloneServiceOfferingCmd.getDisplayText()); + } + + @Test + public void testGetCpu() { + Integer cpu = 4; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", cpu); + assertEquals(cpu, cloneServiceOfferingCmd.getCpuNumber()); + } + + @Test + public void testGetMemory() { + Integer memory = 8192; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", memory); + assertEquals(memory, cloneServiceOfferingCmd.getMemory()); + } + + @Test + public void testGetCpuSpeed() { + Integer cpuSpeed = 2000; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", cpuSpeed); + assertEquals(cpuSpeed, cloneServiceOfferingCmd.getCpuSpeed()); + } + + @Test + public void testGetOfferHa() { + Boolean offerHa = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", offerHa); + assertEquals(offerHa, cloneServiceOfferingCmd.isOfferHa()); + } + + @Test + public void testGetLimitCpuUse() { + Boolean limitCpuUse = false; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", limitCpuUse); + assertEquals(limitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse()); + } + + @Test + public void testGetVolatileVm() { + Boolean volatileVm = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", volatileVm); + assertEquals(volatileVm, cloneServiceOfferingCmd.isVolatileVm()); + } + + @Test + public void testGetStorageType() { + String storageType = "local"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", storageType); + assertEquals(storageType, cloneServiceOfferingCmd.getStorageType()); + } + + @Test + public void testGetTags() { + String tags = "ssd,premium,dedicated"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", tags); + assertEquals(tags, cloneServiceOfferingCmd.getTags()); + } + + @Test + public void testGetHostTag() { + String hostTag = "gpu-enabled"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", hostTag); + assertEquals(hostTag, cloneServiceOfferingCmd.getHostTag()); + } + + @Test + public void testGetNetworkRate() { + Integer networkRate = 1000; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", networkRate); + assertEquals(networkRate, cloneServiceOfferingCmd.getNetworkRate()); + } + + @Test + public void testGetDeploymentPlanner() { + String deploymentPlanner = "UserDispersingPlanner"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", deploymentPlanner); + assertEquals(deploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner()); + } + + @Test + public void testGetDetails() { + Map> details = new HashMap<>(); + + HashMap cpuOvercommit = new HashMap<>(); + cpuOvercommit.put("key", "cpuOvercommitRatio"); + cpuOvercommit.put("value", "2.0"); + + HashMap memoryOvercommit = new HashMap<>(); + memoryOvercommit.put("key", "memoryOvercommitRatio"); + memoryOvercommit.put("value", "1.5"); + + details.put("0", cpuOvercommit); + details.put("1", memoryOvercommit); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details); + + Map result = cloneServiceOfferingCmd.getDetails(); + assertNotNull(result); + assertEquals("2.0", result.get("cpuOvercommitRatio")); + assertEquals("1.5", result.get("memoryOvercommitRatio")); + } + + @Test + public void testIsPurgeResources() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesFalse() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", false); + assertFalse(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesDefaultFalse() { + assertFalse(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testGetLeaseDuration() { + Integer leaseDuration = 3600; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration); + assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration()); + } + + @Test + public void testGetLeaseExpiryAction() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "stop"); + assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction()); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "DESTROY"); + assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetLeaseExpiryActionInvalidValue() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "InvalidAction"); + cloneServiceOfferingCmd.getLeaseExpiryAction(); + } + + @Test + public void testGetVgpuProfileId() { + Long vgpuProfileId = 10L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId); + assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId()); + } + + @Test + public void testGetGpuCount() { + Integer gpuCount = 2; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount); + assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount()); + } + + @Test + public void testGetGpuDisplay() { + Boolean gpuDisplay = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay); + assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteFailure() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(null); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone service offering", e.getMessage()); + } + } + + @Test + public void testExecuteSuccessWithAllParameters() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedOffering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Test Display"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithInvalidParameterException() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))) + .thenThrow(new InvalidParameterValueException("Invalid source offering ID")); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode()); + assertEquals("Invalid source offering ID", e.getMessage()); + } + } + + @Test + public void testExecuteWithCloudRuntimeException() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))) + .thenThrow(new com.cloud.utils.exception.CloudRuntimeException("Runtime error during clone")); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Runtime error during clone", e.getMessage()); + } + } + + @Test + public void testExecuteResponseNameIsSet() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + // Verify that response name would be set (actual verification would require accessing the response object's internal state) + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedServiceOffering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Cloned Service Offering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", true); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", false); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", true); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", "local"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", "premium"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", "gpu-enabled"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", 1000); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", "UserDispersingPlanner"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true); + + assertEquals(Long.valueOf(555L), cloneServiceOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedServiceOffering", cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals("Cloned Service Offering", cloneServiceOfferingCmd.getDisplayText()); + assertEquals(Integer.valueOf(4), cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(Integer.valueOf(8192), cloneServiceOfferingCmd.getMemory()); + assertEquals(Integer.valueOf(2000), cloneServiceOfferingCmd.getCpuSpeed()); + assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isOfferHa()); + assertEquals(Boolean.FALSE, cloneServiceOfferingCmd.isLimitCpuUse()); + assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isVolatileVm()); + assertEquals("local", cloneServiceOfferingCmd.getStorageType()); + assertEquals("premium", cloneServiceOfferingCmd.getTags()); + assertEquals("gpu-enabled", cloneServiceOfferingCmd.getHostTag()); + assertEquals(Integer.valueOf(1000), cloneServiceOfferingCmd.getNetworkRate()); + assertEquals("UserDispersingPlanner", cloneServiceOfferingCmd.getDeploymentPlanner()); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneServiceOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetSystemVmType() { + String systemVmType = "domainrouter"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "systemVmType", systemVmType); + assertEquals(systemVmType, cloneServiceOfferingCmd.getSystemVmType()); + } + + @Test + public void testGetBytesReadRate() { + Long bytesReadRate = 1000000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate); + assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate()); + } + + @Test + public void testGetBytesWriteRate() { + Long bytesWriteRate = 1000000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate); + assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate()); + } + + @Test + public void testGetIopsReadRate() { + Long iopsReadRate = 1000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate); + assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate()); + } + + @Test + public void testGetIopsWriteRate() { + Long iopsWriteRate = 1000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate); + assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate()); + } + + @Test + public void testCloneServiceOfferingWithGpuProfile() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "GPU-Offering-Clone"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", 10L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", 2); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", true); + + assertEquals(Long.valueOf(10L), cloneServiceOfferingCmd.getVgpuProfileId()); + assertEquals(Integer.valueOf(2), cloneServiceOfferingCmd.getGpuCount()); + assertTrue(cloneServiceOfferingCmd.getGpuDisplay()); + } + + @Test + public void testCloneServiceOfferingWithLease() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "Lease-Offering-Clone"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", 7200); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "destroy"); + + assertEquals(Integer.valueOf(7200), cloneServiceOfferingCmd.getLeaseDuration()); + assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction()); + } + + @Test + public void testExecuteWithOverriddenParameters() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "ClonedOffering-Override"; + String newDisplayText = "Overridden Display Text"; + Integer newCpu = 8; + Integer newMemory = 16384; + Integer newCpuSpeed = 3000; + Boolean newOfferHa = true; + Boolean newLimitCpuUse = true; + String newStorageType = "shared"; + String newTags = "premium,gpu"; + String newHostTag = "compute-optimized"; + Integer newNetworkRate = 2000; + String newDeploymentPlanner = "FirstFitPlanner"; + Boolean newPurgeResources = true; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", newDisplayText); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", newCpuSpeed); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", newOfferHa); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", newLimitCpuUse); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", newStorageType); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", newTags); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", newHostTag); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", newNetworkRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", newDeploymentPlanner); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", newPurgeResources); + + assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId()); + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(newDisplayText, cloneServiceOfferingCmd.getDisplayText()); + assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(newMemory, cloneServiceOfferingCmd.getMemory()); + assertEquals(newCpuSpeed, cloneServiceOfferingCmd.getCpuSpeed()); + assertEquals(newOfferHa, cloneServiceOfferingCmd.isOfferHa()); + assertEquals(newLimitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse()); + assertEquals(newStorageType, cloneServiceOfferingCmd.getStorageType()); + assertEquals(newTags, cloneServiceOfferingCmd.getTags()); + assertEquals(newHostTag, cloneServiceOfferingCmd.getHostTag()); + assertEquals(newNetworkRate, cloneServiceOfferingCmd.getNetworkRate()); + assertEquals(newDeploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner()); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithPartialOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "PartialOverride"; + Integer newCpu = 6; + Integer newMemory = 12288; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(newMemory, cloneServiceOfferingCmd.getMemory()); + + assertNull(cloneServiceOfferingCmd.getCpuSpeed()); + assertFalse(cloneServiceOfferingCmd.isOfferHa()); + assertNull(cloneServiceOfferingCmd.getStorageType()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithGpuOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "GPU-Clone-Override"; + Long vgpuProfileId = 15L; + Integer gpuCount = 4; + Boolean gpuDisplay = false; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId()); + assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount()); + assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithLeaseOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "Lease-Clone-Override"; + Integer leaseDuration = 14400; // 4 hours + String leaseExpiryAction = "stop"; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", leaseExpiryAction); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration()); + assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithStorageOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + String newName = "Storage-Clone-Override"; + Long bytesReadRate = 2000000L; + Long bytesWriteRate = 1500000L; + Long iopsReadRate = 2000L; + Long iopsWriteRate = 1500L; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate()); + assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate()); + assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate()); + assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithDetailsOverride() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "Details-Clone-Override"; + Map> details = new HashMap<>(); + + HashMap cpuOvercommit = new HashMap<>(); + cpuOvercommit.put("key", "cpuOvercommitRatio"); + cpuOvercommit.put("value", "3.0"); + + HashMap memoryOvercommit = new HashMap<>(); + memoryOvercommit.put("key", "memoryOvercommitRatio"); + memoryOvercommit.put("value", "2.5"); + + HashMap customDetail = new HashMap<>(); + customDetail.put("key", "customParameter"); + customDetail.put("value", "customValue"); + + details.put("0", cpuOvercommit); + details.put("1", memoryOvercommit); + details.put("2", customDetail); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + Map result = cloneServiceOfferingCmd.getDetails(); + assertNotNull(result); + assertEquals("3.0", result.get("cpuOvercommitRatio")); + assertEquals("2.5", result.get("memoryOvercommitRatio")); + assertEquals("customValue", result.get("customParameter")); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java new file mode 100644 index 000000000000..1e6d6c9e0969 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java @@ -0,0 +1,299 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.vpc; + +import com.cloud.network.vpc.VpcOffering; +import com.cloud.network.vpc.VpcProvisioningService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.VpcOfferingResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneVpcOfferingCmdTest { + + private CloneVPCOfferingCmd cloneVpcOfferingCmd; + + @Mock + private VpcProvisioningService vpcService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private VpcOffering mockVpcOffering; + + @Mock + private VpcOfferingResponse mockVpcOfferingResponse; + + @Before + public void setUp() { + cloneVpcOfferingCmd = new CloneVPCOfferingCmd(); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_vpcProvSvc", vpcService); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneVpcOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetName() { + String name = "ClonedVpcOffering"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name); + assertEquals(name, cloneVpcOfferingCmd.getVpcOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned VPC Offering Display Text"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneVpcOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedVpcOffering"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", null); + assertEquals(name, cloneVpcOfferingCmd.getDisplayText()); + } + + @Test + public void testGetServiceOfferingId() { + Long serviceOfferingId = 456L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", serviceOfferingId); + assertEquals(serviceOfferingId, cloneVpcOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testGetInternetProtocol() { + String internetProtocol = "dualstack"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", internetProtocol); + assertEquals(internetProtocol, cloneVpcOfferingCmd.getInternetProtocol()); + } + + @Test + public void testGetProvider() { + String provider = "NSX"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", provider); + assertEquals(provider, cloneVpcOfferingCmd.getProvider()); + } + + @Test + public void testGetNetworkMode() { + String networkMode = "ROUTED"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", networkMode); + assertEquals(networkMode, cloneVpcOfferingCmd.getNetworkMode()); + } + + @Test + public void testGetRoutingMode() { + String routingMode = "dynamic"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", routingMode); + assertEquals(routingMode, cloneVpcOfferingCmd.getRoutingMode()); + } + + @Test + public void testGetNsxSupportLb() { + Boolean nsxSupportLb = true; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", nsxSupportLb); + assertEquals(nsxSupportLb, cloneVpcOfferingCmd.getNsxSupportsLbService()); + } + + @Test + public void testGetSpecifyAsnumber() { + Boolean specifyAsnumber = false; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", specifyAsnumber); + assertEquals(specifyAsnumber, cloneVpcOfferingCmd.getSpecifyAsNumber()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(mockVpcOffering); + when(responseGenerator.createVpcOfferingResponse(mockVpcOffering)).thenReturn(mockVpcOfferingResponse); + + cloneVpcOfferingCmd.execute(); + + assertNotNull(cloneVpcOfferingCmd.getResponseObject()); + assertEquals(mockVpcOfferingResponse, cloneVpcOfferingCmd.getResponseObject()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(null); + + try { + cloneVpcOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone VPC offering", e.getMessage()); + throw e; + } + } + + @Test + public void testGetSupportedServices() { + List supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat", "NetworkACL"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "supportedServices", supportedServices); + assertEquals(supportedServices, cloneVpcOfferingCmd.getSupportedServices()); + } + + @Test + public void testGetServiceProviders() { + Map> serviceProviderList = new HashMap<>(); + + HashMap dhcpProvider = new HashMap<>(); + dhcpProvider.put("service", "Dhcp"); + dhcpProvider.put("provider", "VpcVirtualRouter"); + + HashMap dnsProvider = new HashMap<>(); + dnsProvider.put("service", "Dns"); + dnsProvider.put("provider", "VpcVirtualRouter"); + + HashMap aclProvider = new HashMap<>(); + aclProvider.put("service", "NetworkACL"); + aclProvider.put("provider", "VpcVirtualRouter"); + + serviceProviderList.put("0", dhcpProvider); + serviceProviderList.put("1", dnsProvider); + serviceProviderList.put("2", aclProvider); + + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceProviderList", serviceProviderList); + + Map> result = cloneVpcOfferingCmd.getServiceProviders(); + assertNotNull(result); + assertEquals(3, result.size()); + assertNotNull(result.get("Dhcp")); + assertNotNull(result.get("Dns")); + assertNotNull(result.get("NetworkACL")); + assertEquals("VpcVirtualRouter", result.get("Dhcp").get(0)); + assertEquals("VpcVirtualRouter", result.get("Dns").get(0)); + assertEquals("VpcVirtualRouter", result.get("NetworkACL").get(0)); + } + + @Test + public void testGetEnable() { + Boolean enable = true; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", enable); + assertEquals(enable, cloneVpcOfferingCmd.getEnable()); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", "ClonedVpcOffering"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", "Cloned VPC Offering"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", 456L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", "ipv4"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "static"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", false); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", true); + + assertEquals(Long.valueOf(789L), cloneVpcOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedVpcOffering", cloneVpcOfferingCmd.getVpcOfferingName()); + assertEquals("Cloned VPC Offering", cloneVpcOfferingCmd.getDisplayText()); + assertEquals(Long.valueOf(456L), cloneVpcOfferingCmd.getServiceOfferingId()); + assertEquals("ipv4", cloneVpcOfferingCmd.getInternetProtocol()); + assertEquals("NSX", cloneVpcOfferingCmd.getProvider()); + assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode()); + assertEquals("static", cloneVpcOfferingCmd.getRoutingMode()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService()); + assertEquals(Boolean.FALSE, cloneVpcOfferingCmd.getSpecifyAsNumber()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getEnable()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneVpcOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testProviderNullByDefault() { + assertNull(cloneVpcOfferingCmd.getProvider()); + } + + @Test + public void testServiceCapabilityList() { + Map> serviceCapabilityList = new HashMap<>(); + serviceCapabilityList.put("Connectivity", Arrays.asList("RegionLevelVpc:true", "DistributedRouter:true")); + serviceCapabilityList.put("SourceNat", Arrays.asList("RedundantRouter:true")); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceCapabilityList", serviceCapabilityList); + + Map> result = cloneVpcOfferingCmd.getServiceCapabilityList(); + assertNotNull(result); + assertEquals(serviceCapabilityList, result); + } + + @Test + public void testCloneVpcOfferingWithNsxProvider() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "ROUTED"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "dynamic"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", true); + + assertEquals("NSX", cloneVpcOfferingCmd.getProvider()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService()); + assertEquals("ROUTED", cloneVpcOfferingCmd.getNetworkMode()); + assertEquals("dynamic", cloneVpcOfferingCmd.getRoutingMode()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getSpecifyAsNumber()); + } + + @Test + public void testCloneVpcOfferingWithNetrisProvider() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "Netris"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED"); + + assertEquals("Netris", cloneVpcOfferingCmd.getProvider()); + assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode()); + } +} diff --git a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java index b1cad20b19ec..454cb10a2f2b 100644 --- a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java @@ -288,4 +288,6 @@ List listAvailablePublicIps(final long dcId, PublicIpQuarantine updatePublicIpAddressInQuarantine(Long quarantineProcessId, Date endDate); void updateSourceNatIpAddress(IPAddressVO requestedIp, List userIps) throws Exception; + + Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId); } diff --git a/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java b/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java index 669456cbdcc2..d8011e9ade12 100644 --- a/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java @@ -39,7 +39,7 @@ public interface LoadBalancingRulesManager { LoadBalancer createPublicLoadBalancer(String xId, String name, String description, int srcPort, int destPort, long sourceIpId, String protocol, String algorithm, - boolean openFirewall, CallContext caller, String lbProtocol, Boolean forDisplay, String cidrList) throws NetworkRuleConflictException; + boolean openFirewall, CallContext caller, String lbProtocol, Boolean forDisplay, String cidrList, Long networkId) throws NetworkRuleConflictException; boolean removeAllLoadBalanacersForIp(long ipId, Account caller, long callerUserId); diff --git a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java index e7f41d079a74..792a3a6b397f 100644 --- a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java @@ -211,4 +211,9 @@ public interface VpcManager { void reconfigStaticNatForVpcVr(Long vpcId); boolean applyStaticRouteForVpcVpnIfNeeded(Long vpcId, boolean updateAllVpn) throws ResourceUnavailableException; + + /** + * Returns true if the network is part of a VPC, and the VPC is created from conserve mode enabled VPC offering + */ + boolean isNetworkOnVpcEnabledConserveMode(Network network); } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java index 9320a37bc96e..b913468384e4 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java @@ -91,6 +91,9 @@ public class VpcOfferingVO implements VpcOffering { @Column(name = "specify_as_number") private Boolean specifyAsNumber = false; + @Column(name = "conserve_mode") + private boolean conserveMode; + public VpcOfferingVO() { this.uuid = UUID.randomUUID().toString(); } @@ -242,4 +245,13 @@ public Boolean isSpecifyAsNumber() { public void setSpecifyAsNumber(Boolean specifyAsNumber) { this.specifyAsNumber = specifyAsNumber; } + + @Override + public boolean isConserveMode() { + return conserveMode; + } + + public void setConserveMode(boolean conserveMode) { + this.conserveMode = conserveMode; + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 7923ef89ffde..d69b524b85d9 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -34,6 +34,19 @@ CREATE TABLE `cloud`.`backup_offering_details` ( UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; UPDATE `cloud`.`configuration` SET value='firstfit' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_firstfit'; +-- Create kubernetes_cluster_affinity_group_map table for CKS per-node-type affinity groups +CREATE TABLE IF NOT EXISTS `cloud`.`kubernetes_cluster_affinity_group_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `cluster_id` bigint unsigned NOT NULL COMMENT 'kubernetes cluster id', + `node_type` varchar(32) NOT NULL COMMENT 'CONTROL, WORKER, or ETCD', + `affinity_group_id` bigint unsigned NOT NULL COMMENT 'affinity group id', + PRIMARY KEY (`id`), + CONSTRAINT `fk_kubernetes_cluster_ag_map__cluster_id` FOREIGN KEY (`cluster_id`) REFERENCES `kubernetes_cluster`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kubernetes_cluster_ag_map__ag_id` FOREIGN KEY (`affinity_group_id`) REFERENCES `affinity_group`(`id`) ON DELETE CASCADE, + INDEX `i_kubernetes_cluster_ag_map__cluster_id`(`cluster_id`), + INDEX `i_kubernetes_cluster_ag_map__ag_id`(`affinity_group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- Create webhook_filter table DROP TABLE IF EXISTS `cloud`.`webhook_filter`; CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( @@ -98,3 +111,6 @@ ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key; CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'deleteUserKeys', 'ALLOW'); CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'deleteUserKeys', 'ALLOW'); CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKeys', 'ALLOW'); + +-- Add conserve mode for VPC offerings +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' '); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql index 751d8f91a259..3669bb10122b 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql @@ -38,6 +38,7 @@ select `vpc_offerings`.`sort_key` AS `sort_key`, `vpc_offerings`.`routing_mode` AS `routing_mode`, `vpc_offerings`.`specify_as_number` AS `specify_as_number`, + `vpc_offerings`.`conserve_mode` AS `conserve_mode`, group_concat(distinct `domain`.`id` separator ',') AS `domain_id`, group_concat(distinct `domain`.`uuid` separator ',') AS `domain_uuid`, group_concat(distinct `domain`.`name` separator ',') AS `domain_name`, diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java new file mode 100644 index 000000000000..19babc86690d --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.kubernetes.cluster; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "kubernetes_cluster_affinity_group_map") +public class KubernetesClusterAffinityGroupMapVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "cluster_id") + private long clusterId; + + @Column(name = "node_type") + private String nodeType; + + @Column(name = "affinity_group_id") + private long affinityGroupId; + + public KubernetesClusterAffinityGroupMapVO() { + } + + public KubernetesClusterAffinityGroupMapVO(long clusterId, String nodeType, long affinityGroupId) { + this.clusterId = clusterId; + this.nodeType = nodeType; + this.affinityGroupId = affinityGroupId; + } + + @Override + public long getId() { + return id; + } + + public long getClusterId() { + return clusterId; + } + + public void setClusterId(long clusterId) { + this.clusterId = clusterId; + } + + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public long getAffinityGroupId() { + return affinityGroupId; + } + + public void setAffinityGroupId(long affinityGroupId) { + this.affinityGroupId = affinityGroupId; + } +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterEventTypes.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterEventTypes.java index 486a093e4ad4..d11a0fc3dea5 100755 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterEventTypes.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterEventTypes.java @@ -25,4 +25,5 @@ public class KubernetesClusterEventTypes { public static final String EVENT_KUBERNETES_CLUSTER_UPGRADE = "KUBERNETES.CLUSTER.UPGRADE"; public static final String EVENT_KUBERNETES_CLUSTER_NODES_ADD = "KUBERNETES.CLUSTER.NODES.ADD"; public static final String EVENT_KUBERNETES_CLUSTER_NODES_REMOVE = "KUBERNETES.CLUSTER.NODES.REMOVE"; + public static final String EVENT_KUBERNETES_CLUSTER_AFFINITY_UPDATE = "KUBERNETES.CLUSTER.AFFINITY.UPDATE"; } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index f3ee6bd56b22..fea20eb124fc 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -58,7 +58,9 @@ import org.apache.cloudstack.acl.Rule; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.AffinityProcessorBase; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -87,6 +89,7 @@ import org.apache.cloudstack.api.command.user.kubernetes.cluster.ScaleKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.StartKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.StopKubernetesClusterCmd; +import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpdateKubernetesClusterAffinityGroupCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpgradeKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.loadbalancer.AssignToLoadBalancerRuleCmd; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; @@ -170,6 +173,7 @@ import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterStartWorker; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterStopWorker; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterUpgradeWorker; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -316,6 +320,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne @Inject public KubernetesClusterDetailsDao kubernetesClusterDetailsDao; @Inject + public KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + @Inject public KubernetesSupportedVersionDao kubernetesSupportedVersionDao; @Inject protected SSHKeyPairDao sshKeyPairDao; @@ -330,6 +336,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne @Inject protected AffinityGroupDao affinityGroupDao; @Inject + protected AffinityGroupVMMapDao affinityGroupVMMapDao; + @Inject protected ServiceOfferingDao serviceOfferingDao; @Inject protected UserDataDao userDataDao; @@ -699,8 +707,8 @@ private DeployDestination plan(final long nodesCount, final DataCenter zone, fin } else if (Objects.nonNull(domainId)) { dedicatedHosts = dedicatedResourceDao.listByDomainId(domainId); } - for (DedicatedResourceVO dedicatedHost : dedicatedHosts) { - hosts.add(hostDao.findById(dedicatedHost.getHostId())); + for (DedicatedResourceVO dedicatedResource : dedicatedHosts) { + hosts.addAll(getHostsForDedicatedResource(dedicatedResource, zone)); useDedicatedHosts = true; } } @@ -768,6 +776,23 @@ private DeployDestination plan(final long nodesCount, final DataCenter zone, fin throw new InsufficientServerCapacityException(msg, DataCenter.class, zone.getId()); } + public List getHostsForDedicatedResource(DedicatedResourceVO dedicatedResource, DataCenter zone) { + if (dedicatedResource.getHostId() != null) { + HostVO host = hostDao.findById(dedicatedResource.getHostId()); + return host != null ? List.of(host) : Collections.emptyList(); + } + if (dedicatedResource.getClusterId() != null) { + return hostDao.findByClusterId(dedicatedResource.getClusterId()); + } + if (dedicatedResource.getPodId() != null) { + return hostDao.findByPodId(dedicatedResource.getPodId(), Host.Type.Routing); + } + if (dedicatedResource.getDataCenterId() != null) { + return resourceManager.listAllHostsInOneZoneByType(Host.Type.Routing, dedicatedResource.getDataCenterId()); + } + return Collections.emptyList(); + } + protected void setNodeTypeServiceOfferingResponse(KubernetesClusterResponse response, KubernetesClusterNodeType nodeType, Long offeringId) { @@ -859,24 +884,38 @@ public KubernetesClusterResponse createKubernetesClusterResponse(long kubernetes List vmResponses = new ArrayList<>(); List vmList = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); - ResponseView respView = ResponseView.Restricted; + ResponseView userVmResponseView = ResponseView.Restricted; Account caller = CallContext.current().getCallingAccount(); if (accountService.isRootAdmin(caller.getId())) { - respView = ResponseView.Full; + userVmResponseView = ResponseView.Full; } final String responseName = "virtualmachine"; if (vmList != null && !vmList.isEmpty()) { - for (KubernetesClusterVmMapVO vmMapVO : vmList) { - UserVmJoinVO userVM = userVmJoinDao.findById(vmMapVO.getVmId()); - if (userVM != null) { - UserVmResponse vmResponse = ApiDBUtils.newUserVmResponse(respView, responseName, userVM, - EnumSet.of(VMDetails.nics), caller); + Map vmMapById = vmList.stream() + .collect(Collectors.toMap(KubernetesClusterVmMapVO::getVmId, vm -> vm)); + Long[] vmIds = vmMapById.keySet().toArray(new Long[0]); + List userVmJoinVOs = userVmJoinDao.searchByIds(vmIds); + if (userVmJoinVOs != null && !userVmJoinVOs.isEmpty()) { + Map vmResponseMap = new HashMap<>(); + for (UserVmJoinVO userVM : userVmJoinVOs) { + Long vmId = userVM.getId(); + UserVmResponse vmResponse = vmResponseMap.get(vmId); + if (vmResponse == null) { + vmResponse = ApiDBUtils.newUserVmResponse(userVmResponseView, responseName, userVM, + EnumSet.of(VMDetails.nics, VMDetails.affgrp), caller); + vmResponseMap.put(vmId, vmResponse); + } else { + ApiDBUtils.fillVmDetails(userVmResponseView, vmResponse, userVM); + } + } + for (Map.Entry vmIdResponseEntry : vmResponseMap.entrySet()) { KubernetesUserVmResponse kubernetesUserVmResponse = new KubernetesUserVmResponse(); try { - BeanUtils.copyProperties(kubernetesUserVmResponse, vmResponse); + BeanUtils.copyProperties(kubernetesUserVmResponse, vmIdResponseEntry.getValue()); } catch (IllegalAccessException | InvocationTargetException e) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to generate zone metrics response"); } + KubernetesClusterVmMapVO vmMapVO = vmMapById.get(vmIdResponseEntry.getKey()); kubernetesUserVmResponse.setExternalNode(vmMapVO.isExternalNode()); kubernetesUserVmResponse.setEtcdNode(vmMapVO.isEtcdNode()); kubernetesUserVmResponse.setNodeVersion(vmMapVO.getNodeVersion()); @@ -906,10 +945,45 @@ public KubernetesClusterResponse createKubernetesClusterResponse(long kubernetes response.setClusterType(kubernetesCluster.getClusterType()); response.setCsiEnabled(kubernetesCluster.isCsiEnabled()); response.setCreated(kubernetesCluster.getCreated()); + setNodeTypeAffinityGroupResponse(response, kubernetesCluster.getId()); return response; } + protected void setNodeTypeAffinityGroupResponse(KubernetesClusterResponse response, long clusterId) { + setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + setAffinityGroupResponseForNodeType(response, clusterId, WORKER.name()); + setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + } + + protected void setAffinityGroupResponseForNodeType(KubernetesClusterResponse response, long clusterId, String nodeType) { + List affinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, nodeType); + if (CollectionUtils.isEmpty(affinityGroupIds)) { + return; + } + List affinityGroupUuids = new ArrayList<>(); + List affinityGroupNames = new ArrayList<>(); + for (Long affinityGroupId : affinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (affinityGroup != null) { + affinityGroupUuids.add(affinityGroup.getUuid()); + affinityGroupNames.add(affinityGroup.getName()); + } + } + String affinityGroupUuidsCsv = String.join(",", affinityGroupUuids); + String affinityGroupNamesCsv = String.join(",", affinityGroupNames); + if (CONTROL.name().equals(nodeType)) { + response.setControlAffinityGroupIds(affinityGroupUuidsCsv); + response.setControlAffinityGroupNames(affinityGroupNamesCsv); + } else if (WORKER.name().equals(nodeType)) { + response.setWorkerAffinityGroupIds(affinityGroupUuidsCsv); + response.setWorkerAffinityGroupNames(affinityGroupNamesCsv); + } else if (ETCD.name().equals(nodeType)) { + response.setEtcdAffinityGroupIds(affinityGroupUuidsCsv); + response.setEtcdAffinityGroupNames(affinityGroupNamesCsv); + } + } + private DataCenter validateAndGetZoneForKubernetesCreateParameters(Long zoneId, Long networkId) { DataCenter zone = dataCenterDao.findById(zoneId); if (zone == null) { @@ -1191,6 +1265,20 @@ private Network getKubernetesClusterNetworkIfMissing(final String clusterName, f return network; } + private void persistAffinityGroupMappings(long clusterId, Map> affinityGroupNodeTypeMap) { + if (MapUtils.isEmpty(affinityGroupNodeTypeMap)) { + return; + } + for (Map.Entry> nodeTypeAffinityGroupEntry : affinityGroupNodeTypeMap.entrySet()) { + String nodeType = nodeTypeAffinityGroupEntry.getKey(); + List affinityGroupIds = nodeTypeAffinityGroupEntry.getValue(); + for (Long affinityGroupId : affinityGroupIds) { + kubernetesClusterAffinityGroupMapDao.persist( + new KubernetesClusterAffinityGroupMapVO(clusterId, nodeType, affinityGroupId)); + } + } + } + private void addKubernetesClusterDetails(final KubernetesCluster kubernetesCluster, final Network network, final CreateKubernetesClusterCmd cmd) { final String externalLoadBalancerIpAddress = cmd.getExternalLoadBalancerIpAddress(); final String dockerRegistryUserName = cmd.getDockerRegistryUserName(); @@ -1630,6 +1718,7 @@ public KubernetesCluster createManagedKubernetesCluster(CreateKubernetesClusterC } Map templateNodeTypeMap = cmd.getTemplateNodeTypeMap(); + Map> affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); final VMTemplateVO finalTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, DEFAULT, clusterKubernetesVersion); final VMTemplateVO controlNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, CONTROL, clusterKubernetesVersion); final VMTemplateVO workerNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, WORKER, clusterKubernetesVersion); @@ -1675,6 +1764,7 @@ public KubernetesClusterVO doInTransaction(TransactionStatus status) { } newCluster.setCsiEnabled(cmd.getEnableCsi()); kubernetesClusterDao.persist(newCluster); + persistAffinityGroupMappings(newCluster.getId(), affinityGroupNodeTypeMap); addKubernetesClusterDetails(newCluster, defaultNetwork, cmd); return newCluster; } @@ -2231,6 +2321,94 @@ public boolean upgradeKubernetesCluster(UpgradeKubernetesClusterCmd cmd) throws return upgradeWorker.upgradeCluster(); } + @Override + @ActionEvent(eventType = KubernetesClusterEventTypes.EVENT_KUBERNETES_CLUSTER_AFFINITY_UPDATE, + eventDescription = "updating Kubernetes cluster affinity groups") + public boolean updateKubernetesClusterAffinityGroups(UpdateKubernetesClusterAffinityGroupCmd cmd) throws CloudRuntimeException { + if (!KubernetesServiceEnabled.value()) { + logAndThrow(Level.ERROR, "Kubernetes Service plugin is disabled"); + } + KubernetesClusterVO kubernetesCluster = validateClusterForAffinityGroupUpdate(cmd.getId()); + Map> affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); + validateNodeAffinityGroups(affinityGroupNodeTypeMap, kubernetesCluster.getAccountId()); + + final Long clusterId = kubernetesCluster.getId(); + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + kubernetesClusterAffinityGroupMapDao.removeByClusterId(clusterId); + persistAffinityGroupMappings(clusterId, affinityGroupNodeTypeMap); + syncVmAffinityGroups(clusterId, affinityGroupNodeTypeMap); + } + }); + logger.info("Updated affinity groups for Kubernetes cluster {}", kubernetesCluster.getName()); + return true; + } + + private KubernetesClusterVO validateClusterForAffinityGroupUpdate(Long clusterId) { + KubernetesClusterVO kubernetesCluster = kubernetesClusterDao.findById(clusterId); + if (Objects.isNull(kubernetesCluster) || Objects.nonNull(kubernetesCluster.getRemoved())) { + throw new InvalidParameterValueException("Invalid Kubernetes cluster ID"); + } + if (!KubernetesCluster.ClusterType.CloudManaged.equals(kubernetesCluster.getClusterType())) { + throw new InvalidParameterValueException("Affinity groups can only be updated for CloudManaged Kubernetes clusters"); + } + if (!KubernetesCluster.State.Stopped.equals(kubernetesCluster.getState())) { + throw new InvalidParameterValueException(String.format( + "Kubernetes cluster %s must be stopped before updating affinity groups (current state: %s)", + kubernetesCluster.getName(), kubernetesCluster.getState())); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), + SecurityChecker.AccessType.OperateEntry, false, kubernetesCluster); + return kubernetesCluster; + } + + private void validateNodeAffinityGroups(Map> affinityGroupNodeTypeMap, long ownerAccountId) { + if (MapUtils.isEmpty(affinityGroupNodeTypeMap)) { + return; + } + Account owner = accountDao.findById(ownerAccountId); + for (List affinityGroupIds : affinityGroupNodeTypeMap.values()) { + for (Long affinityGroupId : affinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (Objects.isNull(affinityGroup)) { + throw new InvalidParameterValueException("Unable to find affinity group with ID: " + affinityGroupId); + } + if (affinityGroup.getAccountId() != owner.getAccountId()) { + throw new InvalidParameterValueException(String.format( + "Affinity group %s does not belong to the cluster owner account %s", + affinityGroup.getName(), owner.getAccountName())); + } + } + } + } + + private void syncVmAffinityGroups(Long clusterId, Map> affinityGroupNodeTypeMap) { + List clusterVmMappings = kubernetesClusterVmMapDao.listByClusterId(clusterId); + if (CollectionUtils.isEmpty(clusterVmMappings)) { + return; + } + Map> nodeTypeAffinityMap = MapUtils.isEmpty(affinityGroupNodeTypeMap) + ? Collections.emptyMap() : affinityGroupNodeTypeMap; + for (KubernetesClusterVmMapVO clusterVmMapping : clusterVmMappings) { + if (clusterVmMapping.isExternalNode()) { + continue; + } + String nodeType = getNodeType(clusterVmMapping); + affinityGroupVMMapDao.updateMap(clusterVmMapping.getVmId(), + nodeTypeAffinityMap.getOrDefault(nodeType, Collections.emptyList())); + } + } + + private String getNodeType(KubernetesClusterVmMapVO clusterVmMapping) { + if (clusterVmMapping.isControlNode()) { + return CONTROL.name(); + } else if (clusterVmMapping.isEtcdNode()) { + return ETCD.name(); + } + return WORKER.name(); + } + private void updateNodeCount(KubernetesClusterVO kubernetesCluster) { List nodeList = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); kubernetesCluster.setControlNodeCount(nodeList.stream().filter(KubernetesClusterVmMapVO::isControlNode).count()); @@ -2292,6 +2470,7 @@ public boolean addNodesToKubernetesCluster(AddNodesToKubernetesClusterCmd cmd) { if (validNodeIds.isEmpty()) { throw new CloudRuntimeException("No valid nodes found to be added to the Kubernetes cluster"); } + validateNodeAffinityGroups(validNodeIds, kubernetesCluster); KubernetesClusterAddWorker addWorker = new KubernetesClusterAddWorker(kubernetesCluster, KubernetesClusterManagerImpl.this); addWorker = ComponentContext.inject(addWorker); return addWorker.addNodesToCluster(validNodeIds, cmd.isMountCksIsoOnVr(), cmd.isManualUpgrade()); @@ -2351,6 +2530,98 @@ private List validateNodes(List nodeIds, Long networkId, String netw return validNodeIds; } + protected void validateNodeAffinityGroups(List nodeIds, KubernetesCluster cluster) { + List workerAffinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType( + cluster.getId(), WORKER.name()); + if (CollectionUtils.isEmpty(workerAffinityGroupIds)) { + return; + } + + Set existingWorkerHostIds = getExistingWorkerHostIds(cluster); + + for (Long affinityGroupId : workerAffinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (affinityGroup == null) { + continue; + } + + validateNodesAgainstExistingWorkers(nodeIds, existingWorkerHostIds, affinityGroup, cluster); + validateNewNodesAntiAffinity(nodeIds, affinityGroup, cluster); + } + } + + protected Set getExistingWorkerHostIds(KubernetesCluster cluster) { + List existingWorkerVms = kubernetesClusterVmMapDao.listByClusterIdAndVmType(cluster.getId(), WORKER); + Set existingWorkerHostIds = new HashSet<>(); + for (KubernetesClusterVmMapVO workerVmMap : existingWorkerVms) { + VMInstanceVO workerVm = vmInstanceDao.findById(workerVmMap.getVmId()); + if (workerVm != null && workerVm.getHostId() != null) { + existingWorkerHostIds.add(workerVm.getHostId()); + } + } + return existingWorkerHostIds; + } + + protected void validateNodesAgainstExistingWorkers(List nodeIds, Set existingWorkerHostIds, + AffinityGroupVO affinityGroup, KubernetesCluster cluster) { + for (Long nodeId : nodeIds) { + VMInstanceVO node = vmInstanceDao.findById(nodeId); + if (node == null || node.getHostId() == null) { + continue; + } + Long nodeHostId = node.getHostId(); + String nodeHostName = getHostName(nodeHostId); + + if (AffinityProcessorBase.AFFINITY_TYPE_HOST_ANTI.equals(affinityGroup.getType())) { + if (existingWorkerHostIds.contains(nodeHostId)) { + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. VM is running on host %s which violates the cluster's " + + "host anti-affinity rule (affinity group: %s). Existing worker VMs are already running on this host.", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName())); + } + } else if (AffinityProcessorBase.AFFINITY_TYPE_HOST.equals(affinityGroup.getType())) { + if (!existingWorkerHostIds.isEmpty() && !existingWorkerHostIds.contains(nodeHostId)) { + List existingHostNames = existingWorkerHostIds.stream() + .map(this::getHostName) + .collect(Collectors.toList()); + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. VM is running on host %s which violates the cluster's " + + "host affinity rule (affinity group: %s). All worker VMs must run on the same host. " + + "Existing workers are on host(s): %s.", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName(), + String.join(", ", existingHostNames))); + } + } + } + } + + protected void validateNewNodesAntiAffinity(List nodeIds, AffinityGroupVO affinityGroup, KubernetesCluster cluster) { + if (!AffinityProcessorBase.AFFINITY_TYPE_HOST_ANTI.equals(affinityGroup.getType())) { + return; + } + + Set newNodeHostIds = new HashSet<>(); + for (Long nodeId : nodeIds) { + VMInstanceVO node = vmInstanceDao.findById(nodeId); + if (node != null && node.getHostId() != null) { + Long nodeHostId = node.getHostId(); + if (newNodeHostIds.contains(nodeHostId)) { + String nodeHostName = getHostName(nodeHostId); + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. Multiple VMs being added are running on the same host %s, " + + "which violates the cluster's host anti-affinity rule (affinity group: %s).", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName())); + } + newNodeHostIds.add(nodeHostId); + } + } + } + + protected String getHostName(Long hostId) { + HostVO host = hostDao.findById(hostId); + return host != null ? host.getName() : String.valueOf(hostId); + } + @Override public List removeVmsFromCluster(RemoveVirtualMachinesFromKubernetesClusterCmd cmd) { if (!KubernetesServiceEnabled.value()) { @@ -2487,6 +2758,7 @@ public List> getCommands() { cmdList.add(RemoveVirtualMachinesFromKubernetesClusterCmd.class); cmdList.add(AddNodesToKubernetesClusterCmd.class); cmdList.add(RemoveNodesFromKubernetesClusterCmd.class); + cmdList.add(UpdateKubernetesClusterAffinityGroupCmd.class); return cmdList; } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterService.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterService.java index 6bdb4265e019..1d19127dc54a 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterService.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterService.java @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.command.user.kubernetes.cluster.ScaleKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.StartKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.StopKubernetesClusterCmd; +import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpdateKubernetesClusterAffinityGroupCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpgradeKubernetesClusterCmd; import org.apache.cloudstack.api.response.KubernetesClusterConfigResponse; import org.apache.cloudstack.api.response.KubernetesClusterResponse; @@ -171,6 +172,8 @@ public interface KubernetesClusterService extends PluggableService, Configurable boolean upgradeKubernetesCluster(UpgradeKubernetesClusterCmd cmd) throws CloudRuntimeException; + boolean updateKubernetesClusterAffinityGroups(UpdateKubernetesClusterAffinityGroupCmd cmd) throws CloudRuntimeException; + boolean addVmsToCluster(AddVirtualMachinesToKubernetesClusterCmd cmd); boolean addNodesToKubernetesCluster(AddNodesToKubernetesClusterCmd cmd); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java index 30465c99780d..9faeb56aea4e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java @@ -18,12 +18,16 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import javax.inject.Inject; +import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.offering.ServiceOffering; import com.cloud.service.dao.ServiceOfferingDao; @@ -66,6 +70,8 @@ public class KubernetesServiceHelperImpl extends AdapterBase implements Kubernet @Inject protected VMTemplateDao vmTemplateDao; @Inject + protected AffinityGroupDao affinityGroupDao; + @Inject KubernetesClusterService kubernetesClusterService; protected void setEventTypeEntityDetails(Class eventTypeDefinedClass, Class entityClass) { @@ -123,6 +129,27 @@ public void checkVmCanBeDestroyed(UserVm userVm) { throw new CloudRuntimeException(msg); } + @Override + public void checkVmAffinityGroupsCanBeUpdated(UserVm userVm) { + if (!UserVmManager.CKS_NODE.equals(userVm.getUserVmType())) { + return; + } + KubernetesClusterVmMapVO clusterVmMapping = kubernetesClusterVmMapDao.findByVmId(userVm.getId()); + if (Objects.isNull(clusterVmMapping)) { + return; + } + KubernetesCluster kubernetesCluster = kubernetesClusterDao.findById(clusterVmMapping.getClusterId()); + String errorMessage = "Affinity groups cannot be updated for a VM part of Kubernetes cluster"; + if (Objects.nonNull(kubernetesCluster)) { + if (KubernetesCluster.ClusterType.ExternalManaged.equals(kubernetesCluster.getClusterType())) { + return; + } + errorMessage += String.format(": %s", kubernetesCluster.getName()); + } + errorMessage += ". Please use the cluster's Change Affinity option instead."; + throw new CloudRuntimeException(errorMessage); + } + @Override public boolean isValidNodeType(String nodeType) { if (StringUtils.isBlank(nodeType)) { @@ -244,6 +271,81 @@ public Map getTemplateNodeTypeMap(Map> return mapping; } + protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeType, String affinityGroupUuids) { + if (StringUtils.isAnyBlank(nodeType, affinityGroupUuids)) { + String error = String.format("Any Node Type to Affinity Group entry should have valid '%s' and '%s' values", + VmDetailConstants.CKS_NODE_TYPE, VmDetailConstants.AFFINITY_GROUP); + logger.error(error); + throw new InvalidParameterValueException(error); + } + } + + protected void checkNodeTypeAffinityGroupEntryNodeType(String nodeType) { + if (!isValidNodeType(nodeType)) { + String error = String.format("The provided value '%s' for Node Type is invalid", nodeType); + logger.error(error); + throw new InvalidParameterValueException(error); + } + } + + protected Long validateAffinityGroupUuidAndGetId(String affinityGroupUuid) { + if (StringUtils.isBlank(affinityGroupUuid)) { + String error = "Empty affinity group UUID provided"; + logger.error(error); + throw new InvalidParameterValueException(error); + } + AffinityGroup affinityGroup = affinityGroupDao.findByUuid(affinityGroupUuid); + if (affinityGroup == null) { + String error = String.format("Cannot find an affinity group with ID %s", affinityGroupUuid); + logger.error(error); + throw new InvalidParameterValueException(error); + } + return affinityGroup.getId(); + } + + protected List validateAndGetAffinityGroupIds(String affinityGroupUuids) { + String[] uuids = affinityGroupUuids.split(","); + List affinityGroupIds = new ArrayList<>(); + for (String uuid : uuids) { + String trimmedUuid = uuid.trim(); + Long affinityGroupId = validateAffinityGroupUuidAndGetId(trimmedUuid); + affinityGroupIds.add(affinityGroupId); + } + return affinityGroupIds; + } + + protected void addNodeTypeAffinityGroupEntry(String nodeType, List affinityGroupIds, Map> nodeTypeToAffinityGroupIds) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Node Type: '%s' should use affinity group IDs: '%s'", nodeType, affinityGroupIds)); + } + KubernetesClusterNodeType clusterNodeType = KubernetesClusterNodeType.valueOf(nodeType.toUpperCase()); + nodeTypeToAffinityGroupIds.put(clusterNodeType.name(), affinityGroupIds); + } + + protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map nodeTypeAffinityConfig, Map> nodeTypeToAffinityGroupIds) { + if (MapUtils.isEmpty(nodeTypeAffinityConfig)) { + return; + } + String nodeType = nodeTypeAffinityConfig.get(VmDetailConstants.CKS_NODE_TYPE); + String affinityGroupUuids = nodeTypeAffinityConfig.get(VmDetailConstants.AFFINITY_GROUP); + checkNodeTypeAffinityGroupEntryCompleteness(nodeType, affinityGroupUuids); + checkNodeTypeAffinityGroupEntryNodeType(nodeType); + + List affinityGroupIds = validateAndGetAffinityGroupIds(affinityGroupUuids); + addNodeTypeAffinityGroupEntry(nodeType, affinityGroupIds, nodeTypeToAffinityGroupIds); + } + + @Override + public Map> getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { + Map> nodeTypeToAffinityGroupIds = new HashMap<>(); + if (MapUtils.isNotEmpty(affinityGroupNodeTypeMap)) { + for (Map nodeTypeAffinityConfig : affinityGroupNodeTypeMap.values()) { + processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(nodeTypeAffinityConfig, nodeTypeToAffinityGroupIds); + } + } + return nodeTypeToAffinityGroupIds; + } + public void cleanupForAccount(Account account) { kubernetesClusterService.cleanupForAccount(account); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java index 1d4b6e8d0a84..4fcf6fa7686d 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java @@ -28,9 +28,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -63,7 +65,9 @@ import com.cloud.vm.dao.NicDao; import com.cloud.vm.UserVmManager; import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.AffinityProcessorBase; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.firewall.CreateFirewallRuleCmd; @@ -90,6 +94,7 @@ import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; import com.cloud.kubernetes.cluster.KubernetesClusterVO; import com.cloud.kubernetes.cluster.KubernetesClusterVmMapVO; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -124,10 +129,12 @@ import com.cloud.utils.fsm.StateMachine2; import com.cloud.utils.ssh.SshHelper; import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.UserVmService; import com.cloud.vm.UserVmVO; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.CONTROL; @@ -213,10 +220,15 @@ public class KubernetesClusterActionWorker { private NicDao nicDao; @Inject protected AffinityGroupDao affinityGroupDao; + @Inject + protected AffinityGroupVMMapDao affinityGroupVMMapDao; + @Inject + protected VMInstanceDao vmInstanceDao; protected KubernetesClusterDao kubernetesClusterDao; protected KubernetesClusterVmMapDao kubernetesClusterVmMapDao; protected KubernetesClusterDetailsDao kubernetesClusterDetailsDao; + protected KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; protected KubernetesSupportedVersionDao kubernetesSupportedVersionDao; protected KubernetesCluster kubernetesCluster; @@ -251,6 +263,7 @@ protected KubernetesClusterActionWorker(final KubernetesCluster kubernetesCluste this.kubernetesClusterDao = clusterManager.kubernetesClusterDao; this.kubernetesClusterDetailsDao = clusterManager.kubernetesClusterDetailsDao; this.kubernetesClusterVmMapDao = clusterManager.kubernetesClusterVmMapDao; + this.kubernetesClusterAffinityGroupMapDao = clusterManager.kubernetesClusterAffinityGroupMapDao; this.kubernetesSupportedVersionDao = clusterManager.kubernetesSupportedVersionDao; this.manager = clusterManager; } @@ -1112,4 +1125,76 @@ public Long getExplicitAffinityGroup(Long domainId, Long accountId) { } return null; } + + protected List getAffinityGroupIdsForNodeType(KubernetesClusterNodeType nodeType) { + List affinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType( + kubernetesCluster.getId(), nodeType.name()); + if (CollectionUtils.isEmpty(affinityGroupIds)) { + return new ArrayList<>(); + } + return new ArrayList<>(affinityGroupIds); + } + + protected List getMergedAffinityGroupIds(KubernetesClusterNodeType nodeType, Long domainId, Long accountId) { + List affinityGroupIds = getAffinityGroupIdsForNodeType(nodeType); + Long explicitAffinityGroupId = getExplicitAffinityGroup(domainId, accountId); + if (explicitAffinityGroupId != null && !affinityGroupIds.contains(explicitAffinityGroupId)) { + affinityGroupIds.add(explicitAffinityGroupId); + } + return affinityGroupIds.isEmpty() ? null : affinityGroupIds; + } + + private Set getRunningVmHostIds(Long affinityGroupId) { + return affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId).stream() + .map(vmInstanceDao::findById) + .filter(vm -> Objects.nonNull(vm) && Objects.nonNull(vm.getHostId()) && VirtualMachine.State.Running.equals(vm.getState())) + .map(VMInstanceVO::getHostId) + .collect(Collectors.toSet()); + } + + protected AffinityConstraints resolveAffinityConstraints(KubernetesClusterNodeType nodeType, Long domainId, Long accountId) { + Set antiAffinityOccupiedHosts = new HashSet<>(); + Long requiredHostId = null; + boolean hasHostAntiAffinity = false; + boolean hasHostAffinity = false; + + if (Objects.nonNull(nodeType)) { + List affinityGroupIds = getMergedAffinityGroupIds(nodeType, domainId, accountId); + if (CollectionUtils.isNotEmpty(affinityGroupIds)) { + for (Long affinityGroupId : affinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (Objects.isNull(affinityGroup)) { + continue; + } + if (AffinityProcessorBase.AFFINITY_TYPE_HOST_ANTI.equals(affinityGroup.getType())) { + hasHostAntiAffinity = true; + antiAffinityOccupiedHosts.addAll(getRunningVmHostIds(affinityGroupId)); + } else if (AffinityProcessorBase.AFFINITY_TYPE_HOST.equals(affinityGroup.getType())) { + hasHostAffinity = true; + Set hostIds = getRunningVmHostIds(affinityGroupId); + if (CollectionUtils.isNotEmpty(hostIds)) { + requiredHostId = hostIds.iterator().next(); + } + } + } + } + } + + return new AffinityConstraints(hasHostAntiAffinity, hasHostAffinity, antiAffinityOccupiedHosts, requiredHostId); + } + + protected static class AffinityConstraints { + final boolean hasHostAntiAffinity; + final boolean hasHostAffinity; + final Set antiAffinityOccupiedHosts; + final Long requiredHostId; + + AffinityConstraints(boolean hasHostAntiAffinity, boolean hasHostAffinity, + Set antiAffinityOccupiedHosts, Long requiredHostId) { + this.hasHostAntiAffinity = hasHostAntiAffinity; + this.hasHostAffinity = hasHostAffinity; + this.antiAffinityOccupiedHosts = antiAffinityOccupiedHosts; + this.requiredHostId = requiredHostId; + } + } } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java index 62bd8b4576a4..dc886117b22e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java @@ -348,6 +348,7 @@ public boolean destroy() throws CloudRuntimeException { stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded); annotationDao.removeByEntityType(AnnotationService.EntityType.KUBERNETES_CLUSTER.name(), kubernetesCluster.getUuid()); kubernetesClusterDetailsDao.removeDetails(kubernetesCluster.getId()); + kubernetesClusterAffinityGroupMapDao.removeByClusterId(kubernetesCluster.getId()); boolean deleted = kubernetesClusterDao.remove(kubernetesCluster.getId()); if (!deleted) { logMessage(Level.WARN, String.format("Failed to delete Kubernetes cluster: %s", kubernetesCluster), null); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java index cf69234d19e0..1ed75f14dfb1 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java @@ -26,7 +26,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -116,7 +115,6 @@ import com.cloud.vm.UserVmManager; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VmDetailConstants; -import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.context.CallContext; import org.apache.logging.log4j.Level; @@ -152,8 +150,6 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu @Inject protected LoadBalancerDao loadBalancerDao; @Inject - protected VMInstanceDao vmInstanceDao; - @Inject protected UserVmManager userVmManager; @Inject protected LaunchPermissionDao launchPermissionDao; @@ -177,8 +173,33 @@ protected void init() { kubernetesClusterNodeNamePrefix = getKubernetesClusterNodeNamePrefix(); } + protected List filterHostsByAffinityConstraints(List hosts, AffinityConstraints constraints, DataCenter zone) + throws InsufficientServerCapacityException { + if (constraints.hasHostAffinity && Objects.nonNull(constraints.requiredHostId)) { + hosts = hosts.stream().filter(host -> host.getId() == constraints.requiredHostId.longValue()).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(hosts)) { + String msg = String.format("Cannot find capacity for Kubernetes cluster: host affinity requires all VMs on host %d but it is not available in zone %s", + constraints.requiredHostId, zone.getName()); + throw new InsufficientServerCapacityException(msg, DataCenter.class, zone.getId()); + } + } + + if (constraints.hasHostAntiAffinity) { + hosts = hosts.stream().filter(host -> !constraints.antiAffinityOccupiedHosts.contains(host.getId())).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(hosts)) { + String msg = String.format("Cannot find capacity for Kubernetes cluster: host anti-affinity requires each VM on a separate host, " + + "but all %d available hosts in zone %s are already occupied by existing cluster VMs", + constraints.antiAffinityOccupiedHosts.size(), zone.getName()); + throw new InsufficientServerCapacityException(msg, DataCenter.class, zone.getId()); + } + } + + return hosts; + } + protected DeployDestination plan(final long nodesCount, final DataCenter zone, final ServiceOffering offering, - final Long domainId, final Long accountId, final Hypervisor.HypervisorType hypervisorType, CPU.CPUArch arch) throws InsufficientServerCapacityException { + final Long domainId, final Long accountId, final Hypervisor.HypervisorType hypervisorType, + CPU.CPUArch arch, KubernetesClusterNodeType nodeType) throws InsufficientServerCapacityException { final int cpu_requested = offering.getCpu() * offering.getSpeed(); final long ram_requested = offering.getRamSize() * 1024L * 1024L; boolean useDedicatedHosts = false; @@ -191,26 +212,30 @@ protected DeployDestination plan(final long nodesCount, final DataCenter zone, f } else if (Objects.nonNull(domainId)) { dedicatedHosts = dedicatedResourceDao.listByDomainId(domainId); } - for (DedicatedResourceVO dedicatedHost : dedicatedHosts) { - hosts.add(hostDao.findById(dedicatedHost.getHostId())); + for (DedicatedResourceVO dedicatedResource : dedicatedHosts) { + hosts.addAll(manager.getHostsForDedicatedResource(dedicatedResource, zone)); useDedicatedHosts = true; } } if (hosts.isEmpty()) { hosts = resourceManager.listAllHostsInOneZoneByType(Host.Type.Routing, zone.getId()); } - if (hypervisorType != null) { + if (Objects.nonNull(hypervisorType)) { hosts = hosts.stream().filter(x -> x.getHypervisorType() == hypervisorType).collect(Collectors.toList()); } - if (arch != null) { + if (Objects.nonNull(arch)) { hosts = hosts.stream().filter(x -> x.getArch().equals(arch)).collect(Collectors.toList()); } if (CollectionUtils.isEmpty(hosts)) { String msg = String.format("Cannot find enough capacity for Kubernetes cluster(requested cpu=%d memory=%s) with offering: %s hypervisor: %s and arch: %s", - cpu_requested * nodesCount, toHumanReadableSize(ram_requested * nodesCount), offering.getName(), clusterTemplate.getHypervisorType().toString(), arch.getType()); + cpu_requested * nodesCount, toHumanReadableSize(ram_requested * nodesCount), offering.getName(), clusterTemplate.getHypervisorType().toString(), + Objects.nonNull(arch) ? arch.getType() : "null"); logAndThrow(Level.WARN, msg, new InsufficientServerCapacityException(msg, DataCenter.class, zone.getId())); } + AffinityConstraints affinityConstraints = resolveAffinityConstraints(nodeType, domainId, accountId); + hosts = filterHostsByAffinityConstraints(hosts, affinityConstraints, zone); + final Map> hosts_with_resevered_capacity = new ConcurrentHashMap>(); for (HostVO h : hosts) { hosts_with_resevered_capacity.put(h.getUuid(), new Pair(h, 0)); @@ -230,6 +255,9 @@ protected DeployDestination plan(final long nodesCount, final DataCenter zone, f continue; } int reserved = hp.second(); + if (affinityConstraints.hasHostAntiAffinity && reserved > 0) { + continue; + } reserved++; ClusterVO cluster = clusterDao.findById(h.getClusterId()); ClusterDetailsVO cluster_detail_cpu = clusterDetailsDao.findDetail(cluster.getId(), "cpuOvercommitRatio"); @@ -264,10 +292,17 @@ protected DeployDestination plan(final long nodesCount, final DataCenter zone, f } return new DeployDestination(zone, null, null, null); } - String msg = String.format("Cannot find enough capacity for Kubernetes cluster(requested cpu=%d memory=%s) with offering: %s hypervisor: %s and arch: %s", - cpu_requested * nodesCount, toHumanReadableSize(ram_requested * nodesCount), offering.getName(), clusterTemplate.getHypervisorType().toString(), arch.getType()); - - logger.warn(msg); + String msg; + if (affinityConstraints.hasHostAntiAffinity) { + msg = String.format("Cannot find enough capacity for Kubernetes cluster (requested cpu=%d memory=%s) with offering: %s. " + + "Host anti-affinity requires %d separate hosts but not enough suitable hosts are available in zone %s", + cpu_requested * nodesCount, toHumanReadableSize(ram_requested * nodesCount), offering.getName(), + nodesCount, zone.getName()); + } else { + msg = String.format("Cannot find enough capacity for Kubernetes cluster(requested cpu=%d memory=%s) with offering: %s hypervisor: %s and arch: %s", + cpu_requested * nodesCount, toHumanReadableSize(ram_requested * nodesCount), offering.getName(), clusterTemplate.getHypervisorType().toString(), + Objects.nonNull(arch) ? arch.getType() : "null"); + } throw new InsufficientServerCapacityException(msg, DataCenter.class, zone.getId()); } @@ -296,7 +331,7 @@ protected Map planKubernetesCluster(Long domainId, Lo if (logger.isDebugEnabled()) { logger.debug("Checking deployment destination for {} nodes on Kubernetes cluster : {} in zone : {}", nodeType.name(), kubernetesCluster.getName(), zone.getName()); } - DeployDestination planForNodeType = plan(nodes, zone, nodeOffering, domainId, accountId, hypervisorType, arch); + DeployDestination planForNodeType = plan(nodes, zone, nodeOffering, domainId, accountId, hypervisorType, arch, nodeType); destinationMap.put(nodeType.name(), planForNodeType); } return destinationMap; @@ -426,21 +461,19 @@ protected UserVm createKubernetesNode(String joinIp, Long domainId, Long account if (StringUtils.isNotBlank(kubernetesCluster.getKeyPair())) { keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(WORKER, domainId, accountId); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, List.of(kubernetesCluster.getSecurityGroupId()))) { List securityGroupIds = new ArrayList<>(); securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); nodeVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, workerNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { nodeVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, workerNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { logger.info("Created node VM : {}, {} in the Kubernetes cluster : {}", hostName, nodeVm, kubernetesCluster.getName()); @@ -660,7 +693,7 @@ protected void provisionLoadBalancerRule(final IpAddress publicIp, final Network ips.add(controlVmNic.getIPv4Address()); vmIdIpMap.put(clusterVMIds.get(i), ips); } - lbService.assignToLoadBalancer(lb.getId(), null, vmIdIpMap, false); + lbService.assignToLoadBalancer(lb.getId(), null, vmIdIpMap, null, false); } protected Map createFirewallRules(IpAddress publicIp, List clusterVMIds, boolean apiRule) throws ManagementServerException { diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java index 6d22a1a3a03d..c623426b137a 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java @@ -24,10 +24,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; -import javax.inject.Inject; - import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.VMTemplateVO; @@ -63,7 +63,6 @@ import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; -import com.cloud.vm.dao.VMInstanceDao; import org.apache.logging.log4j.Level; import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.CONTROL; @@ -73,9 +72,6 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModifierActionWorker { - @Inject - protected VMInstanceDao vmInstanceDao; - private Map serviceOfferingNodeTypeMap; private Long clusterSize; private List nodeIds; @@ -325,7 +321,7 @@ private void validateKubernetesClusterScaleOfferingParameters() throws CloudRunt } } - private void validateKubernetesClusterScaleSizeParameters() throws CloudRuntimeException { + private void validateKubernetesClusterScaleSizeParameters(KubernetesClusterNodeType nodeType) throws CloudRuntimeException { final long originalClusterSize = kubernetesCluster.getNodeCount(); if (network == null) { logTransitStateToFailedIfNeededAndThrow(Level.WARN, String.format("Scaling failed for Kubernetes cluster : %s, cluster network not found", kubernetesCluster.getName())); @@ -341,12 +337,12 @@ private void validateKubernetesClusterScaleSizeParameters() throws CloudRuntimeE VMTemplateVO clusterTemplate = templateDao.findById(kubernetesCluster.getTemplateId()); try { if (originalState.equals(KubernetesCluster.State.Running)) { - plan(newVmRequiredCount, zone, clusterServiceOffering, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId(), clusterTemplate.getHypervisorType(), clusterTemplate.getArch()); + plan(newVmRequiredCount, zone, clusterServiceOffering, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId(), clusterTemplate.getHypervisorType(), clusterTemplate.getArch(), nodeType); } else { - plan(kubernetesCluster.getTotalNodeCount() + newVmRequiredCount, zone, clusterServiceOffering, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId(), clusterTemplate.getHypervisorType(), clusterTemplate.getArch()); + plan(kubernetesCluster.getTotalNodeCount() + newVmRequiredCount, zone, clusterServiceOffering, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId(), clusterTemplate.getHypervisorType(), clusterTemplate.getArch(), nodeType); } } catch (InsufficientCapacityException e) { - logTransitStateToFailedIfNeededAndThrow(Level.WARN, String.format("Scaling failed for Kubernetes cluster : %s in zone : %s, insufficient capacity", kubernetesCluster.getName(), zone.getName())); + logTransitStateToFailedIfNeededAndThrow(Level.WARN, String.format("Scaling failed for Kubernetes cluster : %s in zone : %s, insufficient capacity: %s", kubernetesCluster.getName(), zone.getName(), e.getMessage())); } } List vmList = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); @@ -465,10 +461,38 @@ public List getWorkerNodesToRemove() { return new ArrayList<>(workerVMsMap.subList(startIndex, totalWorkerNodes)); } + private void cleanupNewlyCreatedVms(Set originalVmIds) { + List currentVmMaps = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); + for (KubernetesClusterVmMapVO clusterVmMap : currentVmMaps) { + if (originalVmIds.contains(clusterVmMap.getVmId())) { + continue; + } + UserVmVO userVM = userVmDao.findById(clusterVmMap.getVmId()); + if (Objects.isNull(userVM)) { + kubernetesClusterVmMapDao.expunge(clusterVmMap.getId()); + continue; + } + logger.warn("Cleaning up VM {} created during failed scale-up of Kubernetes cluster {}", userVM, kubernetesCluster); + CallContext vmContext = CallContext.register(CallContext.current(), ApiCommandResourceType.VirtualMachine); + vmContext.setEventResourceId(userVM.getId()); + try { + userVmService.destroyVm(userVM.getId(), true); + userVmManager.expunge(userVM); + } catch (Exception e) { + logger.warn("Failed to cleanup VM {} during scale-up rollback for Kubernetes cluster {}", userVM, kubernetesCluster, e); + } finally { + CallContext.unregister(); + } + kubernetesClusterVmMapDao.expunge(clusterVmMap.getId()); + } + } + private void scaleUpKubernetesClusterSize(final long newVmCount) throws CloudRuntimeException { if (!kubernetesCluster.getState().equals(KubernetesCluster.State.Scaling)) { stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.ScaleUpRequested); } + Set originalVmIds = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()) + .stream().map(KubernetesClusterVmMapVO::getVmId).collect(Collectors.toSet()); List clusterVMs = new ArrayList<>(); if (isDefaultTemplateUsed()) { LaunchPermissionVO launchPermission = new LaunchPermissionVO(clusterTemplate.getId(), owner.getId()); @@ -478,6 +502,7 @@ private void scaleUpKubernetesClusterSize(final long newVmCount) throws CloudRun clusterVMs = provisionKubernetesClusterNodeVms((int)(newVmCount + kubernetesCluster.getNodeCount()), (int)kubernetesCluster.getNodeCount(), publicIpAddress, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId()); updateLoginUserDetails(clusterVMs.stream().map(InternalIdentity::getId).collect(Collectors.toList())); } catch (CloudRuntimeException | ManagementServerException | ResourceUnavailableException | InsufficientCapacityException e) { + cleanupNewlyCreatedVms(originalVmIds); logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to provision node VM in the cluster", kubernetesCluster.getName()), e); } try { @@ -486,6 +511,7 @@ private void scaleUpKubernetesClusterSize(final long newVmCount) throws CloudRun clusterVMIds.addAll(externalNodeIds); scaleKubernetesClusterNetworkRules(clusterVMIds); } catch (ManagementServerException e) { + cleanupNewlyCreatedVms(originalVmIds); logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to update network rules", kubernetesCluster.getName()), e); } attachIsoKubernetesVMs(clusterVMs); @@ -496,12 +522,13 @@ private void scaleUpKubernetesClusterSize(final long newVmCount) throws CloudRun detachIsoKubernetesVMs(clusterVMs); deleteTemplateLaunchPermission(); if (!readyNodesCountValid) { // Scaling failed + cleanupNewlyCreatedVms(originalVmIds); logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling unsuccessful for Kubernetes cluster : %s as it does not have desired number of nodes in ready state", kubernetesCluster.getName())); } } private void scaleKubernetesClusterSize(KubernetesClusterNodeType nodeType) throws CloudRuntimeException { - validateKubernetesClusterScaleSizeParameters(); + validateKubernetesClusterScaleSizeParameters(nodeType); final long originalClusterSize = kubernetesCluster.getNodeCount(); final long newVmRequiredCount = clusterSize - originalClusterSize; if (KubernetesCluster.State.Created.equals(originalState)) { diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java index aa9317e619b0..4ed5ff0167c2 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java @@ -270,7 +270,7 @@ private Pair createKubernetesControlNode(final Network network, S keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(CONTROL, domainId, accountId); String userDataDetails = kubernetesCluster.getCniConfigDetails(); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, @@ -279,15 +279,13 @@ private Pair createKubernetesControlNode(final Network network, S securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); controlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, userDataId, userDataDetails, keypairs, - requestedIps, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + requestedIps, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { controlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, userDataId, userDataDetails, keypairs, - requestedIps, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + requestedIps, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { logger.info("Created control VM: {}, {} in the Kubernetes cluster: {}", controlVm, hostName, kubernetesCluster); @@ -439,7 +437,7 @@ private UserVm createKubernetesAdditionalControlNode(final String joinIp, final keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(CONTROL, domainId, accountId); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, List.of(kubernetesCluster.getSecurityGroupId()))) { @@ -447,15 +445,13 @@ private UserVm createKubernetesAdditionalControlNode(final String joinIp, final securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); additionalControlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { additionalControlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { @@ -483,7 +479,7 @@ private UserVm createEtcdNode(List requestedIps, List affinityGroupIds = getMergedAffinityGroupIds(ETCD, domainId, accountId); String hostName = etcdNodeHostnames.get(etcdNodeIndex); Map customParameterMap = new HashMap(); if (zone.isSecurityGroupEnabled()) { @@ -491,15 +487,13 @@ private UserVm createEtcdNode(List requestedIps, List { + + List listByClusterIdAndNodeType(long clusterId, String nodeType); + + List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, String nodeType); + + int removeByClusterId(long clusterId); +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java new file mode 100644 index 000000000000..fcd4d6f7c97f --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.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 com.cloud.kubernetes.cluster.dao; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import com.cloud.kubernetes.cluster.KubernetesClusterAffinityGroupMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class KubernetesClusterAffinityGroupMapDaoImpl extends GenericDaoBase + implements KubernetesClusterAffinityGroupMapDao { + + private final SearchBuilder clusterIdAndNodeTypeSearch; + private final SearchBuilder clusterIdSearch; + + public KubernetesClusterAffinityGroupMapDaoImpl() { + clusterIdAndNodeTypeSearch = createSearchBuilder(); + clusterIdAndNodeTypeSearch.and("clusterId", clusterIdAndNodeTypeSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + clusterIdAndNodeTypeSearch.and("nodeType", clusterIdAndNodeTypeSearch.entity().getNodeType(), SearchCriteria.Op.EQ); + clusterIdAndNodeTypeSearch.done(); + + clusterIdSearch = createSearchBuilder(); + clusterIdSearch.and("clusterId", clusterIdSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + clusterIdSearch.done(); + } + + @Override + public List listByClusterIdAndNodeType(long clusterId, String nodeType) { + SearchCriteria sc = clusterIdAndNodeTypeSearch.create(); + sc.setParameters("clusterId", clusterId); + sc.setParameters("nodeType", nodeType); + return listBy(sc); + } + + @Override + public List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, String nodeType) { + List maps = listByClusterIdAndNodeType(clusterId, nodeType); + if (CollectionUtils.isEmpty(maps)) { + return new ArrayList<>(); + } + return maps.stream().map(KubernetesClusterAffinityGroupMapVO::getAffinityGroupId).collect(Collectors.toList()); + } + + @Override + public int removeByClusterId(long clusterId) { + SearchCriteria sc = clusterIdSearch.create(); + sc.setParameters("clusterId", clusterId); + return remove(sc); + } +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java index d30922269da9..8d64fee26206 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.kubernetes.cluster; import java.security.InvalidParameterException; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -79,7 +80,7 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { @Inject public KubernetesClusterService kubernetesClusterService; @Inject - protected KubernetesServiceHelper kubernetesClusterHelper; + protected KubernetesServiceHelper kubernetesServiceHelper; @Inject private ConfigurationDao configurationDao; @Inject @@ -125,6 +126,12 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { since = "4.21.0") private Map> templateNodeTypeMap; + @ACL(accessType = AccessType.UseEntry) + @Parameter(name = ApiConstants.NODE_TYPE_AFFINITY_GROUP_MAP, type = CommandType.MAP, + description = "(Optional) Node Type to Affinity Group ID mapping. If provided, VMs of each node type will be added to the specified affinity group", + since = "4.23.0") + private Map> affinityGroupNodeTypeMap; + @ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.ETCD_NODES, type = CommandType.LONG, description = "(Optional) Number of Kubernetes cluster etcd nodes, default is 0." + @@ -314,11 +321,15 @@ public String getClusterType() { } public Map getServiceOfferingNodeTypeMap() { - return kubernetesClusterHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + return kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); } public Map getTemplateNodeTypeMap() { - return kubernetesClusterHelper.getTemplateNodeTypeMap(templateNodeTypeMap); + return kubernetesServiceHelper.getTemplateNodeTypeMap(templateNodeTypeMap); + } + + public Map> getAffinityGroupNodeTypeMap() { + return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); } public Hypervisor.HypervisorType getHypervisorType() { diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java index c7ee0b7da92a..1cff2649428d 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java @@ -57,7 +57,7 @@ public class ScaleKubernetesClusterCmd extends BaseAsyncCmd { @Inject public KubernetesClusterService kubernetesClusterService; @Inject - protected KubernetesServiceHelper kubernetesClusterHelper; + protected KubernetesServiceHelper kubernetesServiceHelper; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -114,7 +114,7 @@ public Long getServiceOfferingId() { } public Map getServiceOfferingNodeTypeMap() { - return kubernetesClusterHelper.getServiceOfferingNodeTypeMap(this.serviceOfferingNodeTypeMap); + return kubernetesServiceHelper.getServiceOfferingNodeTypeMap(this.serviceOfferingNodeTypeMap); } public Long getClusterSize() { diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/UpdateKubernetesClusterAffinityGroupCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/UpdateKubernetesClusterAffinityGroupCmd.java new file mode 100644 index 000000000000..16a19e410acd --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/UpdateKubernetesClusterAffinityGroupCmd.java @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.kubernetes.cluster; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +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.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.KubernetesClusterResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.kubernetes.cluster.KubernetesCluster; +import com.cloud.kubernetes.cluster.KubernetesClusterService; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "updateKubernetesClusterAffinityGroups", + description = "Updates the affinity group mappings for a stopped Kubernetes cluster", + responseObject = KubernetesClusterResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {KubernetesCluster.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateKubernetesClusterAffinityGroupCmd extends BaseCmd { + + @Inject + public KubernetesClusterService kubernetesClusterService; + @Inject + protected KubernetesServiceHelper kubernetesServiceHelper; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, required = true, + entityType = KubernetesClusterResponse.class, + description = "The ID of the Kubernetes cluster") + private Long id; + + @ACL(accessType = SecurityChecker.AccessType.UseEntry) + @Parameter(name = ApiConstants.NODE_TYPE_AFFINITY_GROUP_MAP, type = CommandType.MAP, + description = "Node Type to Affinity Group ID mapping. VMs of each node type will be added to the specified affinity group", + since = "4.23.0") + private Map> affinityGroupNodeTypeMap; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Map> getAffinityGroupNodeTypeMap() { + return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KubernetesCluster; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException { + try { + if (!kubernetesClusterService.updateKubernetesClusterAffinityGroups(this)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + String.format("Failed to update affinity groups for Kubernetes cluster ID: %d", getId())); + } + final KubernetesClusterResponse response = kubernetesClusterService.createKubernetesClusterResponse(getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException exception) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, exception.getMessage()); + } + } +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java index 0a7e7a97939d..932d722de354 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java @@ -220,6 +220,30 @@ public class KubernetesClusterResponse extends BaseResponseWithAnnotations imple @Param(description = "The date when this Kubernetes cluster was created") private Date created; + @SerializedName(ApiConstants.CONTROL_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with control nodes", since = "4.23.0") + private String controlAffinityGroupIds; + + @SerializedName(ApiConstants.CONTROL_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with control nodes", since = "4.23.0") + private String controlAffinityGroupNames; + + @SerializedName(ApiConstants.WORKER_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with worker nodes", since = "4.23.0") + private String workerAffinityGroupIds; + + @SerializedName(ApiConstants.WORKER_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with worker nodes", since = "4.23.0") + private String workerAffinityGroupNames; + + @SerializedName(ApiConstants.ETCD_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with etcd nodes", since = "4.23.0") + private String etcdAffinityGroupIds; + + @SerializedName(ApiConstants.ETCD_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with etcd nodes", since = "4.23.0") + private String etcdAffinityGroupNames; + public KubernetesClusterResponse() { } @@ -535,4 +559,28 @@ public void setCniConfigName(String cniConfigName) { public void setCsiEnabled(Boolean csiEnabled) { isCsiEnabled = csiEnabled; } + + public void setControlAffinityGroupIds(String controlAffinityGroupIds) { + this.controlAffinityGroupIds = controlAffinityGroupIds; + } + + public void setControlAffinityGroupNames(String controlAffinityGroupNames) { + this.controlAffinityGroupNames = controlAffinityGroupNames; + } + + public void setWorkerAffinityGroupIds(String workerAffinityGroupIds) { + this.workerAffinityGroupIds = workerAffinityGroupIds; + } + + public void setWorkerAffinityGroupNames(String workerAffinityGroupNames) { + this.workerAffinityGroupNames = workerAffinityGroupNames; + } + + public void setEtcdAffinityGroupIds(String etcdAffinityGroupIds) { + this.etcdAffinityGroupIds = etcdAffinityGroupIds; + } + + public void setEtcdAffinityGroupNames(String etcdAffinityGroupNames) { + this.etcdAffinityGroupNames = etcdAffinityGroupNames; + } } diff --git a/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml b/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml index 9d236eed26cd..053366786292 100644 --- a/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml +++ b/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml @@ -32,6 +32,7 @@ + diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java new file mode 100644 index 000000000000..d0aafc7d1e5e --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.kubernetes.cluster; + +import org.junit.Assert; +import org.junit.Test; + +public class KubernetesClusterAffinityGroupMapVOTest { + + @Test + public void testConstructorAndGetters() { + KubernetesClusterAffinityGroupMapVO vo = + new KubernetesClusterAffinityGroupMapVO(1L, "CONTROL", 100L); + + Assert.assertEquals(1L, vo.getClusterId()); + Assert.assertEquals("CONTROL", vo.getNodeType()); + Assert.assertEquals(100L, vo.getAffinityGroupId()); + } + + @Test + public void testDefaultConstructor() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + Assert.assertNotNull(vo); + } + + @Test + public void testSetClusterId() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setClusterId(2L); + Assert.assertEquals(2L, vo.getClusterId()); + } + + @Test + public void testSetNodeType() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setNodeType("WORKER"); + Assert.assertEquals("WORKER", vo.getNodeType()); + } + + @Test + public void testSetAffinityGroupId() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setAffinityGroupId(200L); + Assert.assertEquals(200L, vo.getAffinityGroupId()); + } + + @Test + public void testAllNodeTypes() { + KubernetesClusterAffinityGroupMapVO controlVo = + new KubernetesClusterAffinityGroupMapVO(1L, "CONTROL", 10L); + KubernetesClusterAffinityGroupMapVO workerVo = + new KubernetesClusterAffinityGroupMapVO(1L, "WORKER", 20L); + KubernetesClusterAffinityGroupMapVO etcdVo = + new KubernetesClusterAffinityGroupMapVO(1L, "ETCD", 30L); + + Assert.assertEquals("CONTROL", controlVo.getNodeType()); + Assert.assertEquals("WORKER", workerVo.getNodeType()); + Assert.assertEquals("ETCD", etcdVo.getNodeType()); + } + + @Test + public void testSettersChain() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + + vo.setClusterId(5L); + vo.setNodeType("ETCD"); + vo.setAffinityGroupId(500L); + + Assert.assertEquals(5L, vo.getClusterId()); + Assert.assertEquals("ETCD", vo.getNodeType()); + Assert.assertEquals(500L, vo.getAffinityGroupId()); + } +} diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java deleted file mode 100644 index 298f1dfbcd61..000000000000 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java +++ /dev/null @@ -1,145 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -package com.cloud.kubernetes.cluster; - -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.service.ServiceOfferingVO; -import com.cloud.service.dao.ServiceOfferingDao; -import com.cloud.vm.VmDetailConstants; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.CONTROL; -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.ETCD; -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.WORKER; - -@RunWith(MockitoJUnitRunner.class) -public class KubernetesClusterHelperImplTest { - - @Mock - private ServiceOfferingDao serviceOfferingDao; - @Mock - private ServiceOfferingVO workerServiceOffering; - @Mock - private ServiceOfferingVO controlServiceOffering; - @Mock - private ServiceOfferingVO etcdServiceOffering; - - private static final String workerNodesOfferingId = UUID.randomUUID().toString(); - private static final String controlNodesOfferingId = UUID.randomUUID().toString(); - private static final String etcdNodesOfferingId = UUID.randomUUID().toString(); - private static final Long workerOfferingId = 1L; - private static final Long controlOfferingId = 2L; - private static final Long etcdOfferingId = 3L; - - private final KubernetesServiceHelperImpl helper = new KubernetesServiceHelperImpl(); - - @Before - public void setUp() { - helper.serviceOfferingDao = serviceOfferingDao; - Mockito.when(serviceOfferingDao.findByUuid(workerNodesOfferingId)).thenReturn(workerServiceOffering); - Mockito.when(serviceOfferingDao.findByUuid(controlNodesOfferingId)).thenReturn(controlServiceOffering); - Mockito.when(serviceOfferingDao.findByUuid(etcdNodesOfferingId)).thenReturn(etcdServiceOffering); - Mockito.when(workerServiceOffering.getId()).thenReturn(workerOfferingId); - Mockito.when(controlServiceOffering.getId()).thenReturn(controlOfferingId); - Mockito.when(etcdServiceOffering.getId()).thenReturn(etcdOfferingId); - } - - @Test - public void testIsValidNodeTypeEmptyNodeType() { - Assert.assertFalse(helper.isValidNodeType(null)); - } - - @Test - public void testIsValidNodeTypeInvalidNodeType() { - String nodeType = "invalidNodeType"; - Assert.assertFalse(helper.isValidNodeType(nodeType)); - } - - @Test - public void testIsValidNodeTypeValidNodeTypeLowercase() { - String nodeType = KubernetesServiceHelper.KubernetesClusterNodeType.WORKER.name().toLowerCase(); - Assert.assertTrue(helper.isValidNodeType(nodeType)); - } - - private Map createMapEntry(KubernetesServiceHelper.KubernetesClusterNodeType nodeType, - String nodeTypeOfferingUuid) { - Map map = new HashMap<>(); - map.put(VmDetailConstants.CKS_NODE_TYPE, nodeType.name().toLowerCase()); - map.put(VmDetailConstants.OFFERING, nodeTypeOfferingUuid); - return map; - } - - @Test - public void testNodeOfferingMap() { - Map> serviceOfferingNodeTypeMap = new HashMap<>(); - Map firstMap = createMapEntry(WORKER, workerNodesOfferingId); - Map secondMap = createMapEntry(CONTROL, controlNodesOfferingId); - serviceOfferingNodeTypeMap.put("map1", firstMap); - serviceOfferingNodeTypeMap.put("map2", secondMap); - Map map = helper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); - Assert.assertNotNull(map); - Assert.assertEquals(2, map.size()); - Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(CONTROL.name())); - Assert.assertEquals(workerOfferingId, map.get(WORKER.name())); - Assert.assertEquals(controlOfferingId, map.get(CONTROL.name())); - } - - @Test - public void testNodeOfferingMapNullMap() { - Map map = helper.getServiceOfferingNodeTypeMap(null); - Assert.assertTrue(map.isEmpty()); - } - - @Test - public void testNodeOfferingMapEtcdNodes() { - Map> serviceOfferingNodeTypeMap = new HashMap<>(); - Map firstMap = createMapEntry(ETCD, etcdNodesOfferingId); - serviceOfferingNodeTypeMap.put("map1", firstMap); - Map map = helper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); - Assert.assertNotNull(map); - Assert.assertEquals(1, map.size()); - Assert.assertTrue(map.containsKey(ETCD.name())); - Assert.assertEquals(etcdOfferingId, map.get(ETCD.name())); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryCompletenessInvalidParameters() { - helper.checkNodeTypeOfferingEntryCompleteness(WORKER.name(), null); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryValuesInvalidNodeType() { - String invalidNodeType = "invalidNodeTypeName"; - helper.checkNodeTypeOfferingEntryValues(invalidNodeType, workerServiceOffering, workerNodesOfferingId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryValuesEmptyOffering() { - String nodeType = WORKER.name(); - helper.checkNodeTypeOfferingEntryValues(nodeType, null, workerNodesOfferingId); - } -} diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java index 2a381f282de2..71949459c865 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java @@ -26,6 +26,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterActionWorker; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; import com.cloud.kubernetes.version.KubernetesSupportedVersion; @@ -46,9 +47,14 @@ import com.cloud.utils.net.NetUtils; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.AddVirtualMachinesToKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.RemoveVirtualMachinesFromKubernetesClusterCmd; +import org.apache.cloudstack.api.response.KubernetesClusterResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.collections.MapUtils; @@ -103,6 +109,15 @@ public class KubernetesClusterManagerImplTest { @Mock private ServiceOfferingDao serviceOfferingDao; + @Mock + private KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + + @Mock + private AffinityGroupDao affinityGroupDao; + + @Mock + private HostDao hostDao; + @Spy @InjectMocks KubernetesClusterManagerImpl kubernetesClusterManager; @@ -441,4 +456,462 @@ public void testGetCksClusterPreferredArchSameArch() { String cksClusterPreferredArch = kubernetesClusterManager.getCksClusterPreferredArch(systemVMArch, cksIso); Assert.assertEquals(CPU.CPUArch.amd64.getType(), cksClusterPreferredArch); } + + @Test + public void testSetAffinityGroupResponseForNodeTypeControl() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getUuid()).thenReturn("uuid-1"); + Mockito.when(ag1.getName()).thenReturn("affinity-group-1"); + Mockito.when(ag2.getUuid()).thenReturn("uuid-2"); + Mockito.when(ag2.getName()).thenReturn("affinity-group-2"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Arrays.asList(1L, 2L)); + Mockito.when(affinityGroupDao.findById(1L)).thenReturn(ag1); + Mockito.when(affinityGroupDao.findById(2L)).thenReturn(ag2); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name()); + Mockito.verify(affinityGroupDao).findById(1L); + Mockito.verify(affinityGroupDao).findById(2L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeWorker() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag.getUuid()).thenReturn("worker-uuid"); + Mockito.when(ag.getName()).thenReturn("worker-affinity"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(ag); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, WORKER.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name()); + Mockito.verify(affinityGroupDao).findById(10L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeEtcd() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag.getUuid()).thenReturn("etcd-uuid"); + Mockito.when(ag.getName()).thenReturn("etcd-affinity"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name())) + .thenReturn(Arrays.asList(20L)); + Mockito.when(affinityGroupDao.findById(20L)).thenReturn(ag); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name()); + Mockito.verify(affinityGroupDao).findById(20L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeEmptyList() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(affinityGroupDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeNullList() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name())) + .thenReturn(null); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + + Mockito.verify(affinityGroupDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeNullAffinityGroup() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getUuid()).thenReturn("uuid-1"); + Mockito.when(ag1.getName()).thenReturn("affinity-group-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Arrays.asList(1L, 2L)); + Mockito.when(affinityGroupDao.findById(1L)).thenReturn(ag1); + Mockito.when(affinityGroupDao.findById(2L)).thenReturn(null); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(affinityGroupDao).findById(1L); + Mockito.verify(affinityGroupDao).findById(2L); + } + + @Test + public void testSetNodeTypeAffinityGroupResponse() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(Mockito.eq(clusterId), Mockito.anyString())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.setNodeTypeAffinityGroupResponse(response, clusterId); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name()); + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name()); + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name()); + } + + @Test + public void testValidateNodeAffinityGroupsNoAffinityGroups() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + List nodeIds = Arrays.asList(100L, 101L); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.validateNodeAffinityGroups(nodeIds, cluster); + + Mockito.verify(kubernetesClusterVmMapDao, Mockito.never()).listByClusterIdAndVmType(Mockito.anyLong(), Mockito.any()); + } + + @Test + public void testValidateNodeAffinityGroupsNullAffinityGroups() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + List nodeIds = Arrays.asList(100L, 101L); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(null); + + kubernetesClusterManager.validateNodeAffinityGroups(nodeIds, cluster); + + Mockito.verify(kubernetesClusterVmMapDao, Mockito.never()).listByClusterIdAndVmType(Mockito.anyLong(), Mockito.any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnExistingHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(sharedHostId); + Mockito.when(newNode.getInstanceName()).thenReturn("new-node-vm"); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(sharedHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + Mockito.when(hostDao.findById(sharedHostId)).thenReturn(host); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + } + + @Test + public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnDifferentHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long existingHostId = 1000L; + Long newNodeHostId = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(existingHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test + public void testValidateNodeAffinityGroupsAffinityNewNodeOnSameHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(sharedHostId); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(sharedHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAffinityNewNodeOnDifferentHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long existingHostId = 1000L; + Long newNodeHostId = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + Mockito.when(newNode.getInstanceName()).thenReturn("new-node-vm"); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(existingHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + HostVO newHost = Mockito.mock(HostVO.class); + Mockito.when(newHost.getName()).thenReturn("host-2"); + + HostVO existingHost = Mockito.mock(HostVO.class); + Mockito.when(existingHost.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + Mockito.when(hostDao.findById(newNodeHostId)).thenReturn(newHost); + Mockito.when(hostDao.findById(existingHostId)).thenReturn(existingHost); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnSameHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId1 = 100L; + Long newNodeId2 = 101L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode1.getHostId()).thenReturn(sharedHostId); + Mockito.lenient().when(newNode1.getInstanceName()).thenReturn("new-node-vm-1"); + + VMInstanceVO newNode2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode2.getHostId()).thenReturn(sharedHostId); + Mockito.when(newNode2.getInstanceName()).thenReturn("new-node-vm-2"); + + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId1)).thenReturn(newNode1); + Mockito.when(vmInstanceDao.findById(newNodeId2)).thenReturn(newNode2); + Mockito.when(hostDao.findById(sharedHostId)).thenReturn(host); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId1, newNodeId2), cluster); + } + + @Test + public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnDifferentHosts() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId1 = 100L; + Long newNodeId2 = 101L; + Long hostId1 = 1000L; + Long hostId2 = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode1.getHostId()).thenReturn(hostId1); + + VMInstanceVO newNode2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode2.getHostId()).thenReturn(hostId2); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId1)).thenReturn(newNode1); + Mockito.when(vmInstanceDao.findById(newNodeId2)).thenReturn(newNode2); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId1, newNodeId2), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test + public void testValidateNodeAffinityGroupsNodeWithNullHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(null); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(vmInstanceDao, Mockito.atLeastOnce()).findById(newNodeId); + } + + @Test + public void testValidateNodeAffinityGroupsNullNode() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(null); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(vmInstanceDao, Mockito.atLeastOnce()).findById(newNodeId); + } + + @Test + public void testValidateNodeAffinityGroupsAffinityNoExistingWorkers() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long newNodeHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + } diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java index 3e6688e87577..3994cadc307f 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java @@ -17,6 +17,15 @@ package com.cloud.kubernetes.cluster; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -24,11 +33,16 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.uservm.UserVm; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmManager; +import com.cloud.vm.VmDetailConstants; @RunWith(MockitoJUnitRunner.class) public class KubernetesServiceHelperImplTest { @@ -36,6 +50,10 @@ public class KubernetesServiceHelperImplTest { KubernetesClusterVmMapDao kubernetesClusterVmMapDao; @Mock KubernetesClusterDao kubernetesClusterDao; + @Mock + AffinityGroupDao affinityGroupDao; + @Mock + ServiceOfferingDao serviceOfferingDao; @InjectMocks KubernetesServiceHelperImpl kubernetesServiceHelper = new KubernetesServiceHelperImpl(); @@ -84,4 +102,302 @@ public void testCheckVmCanBeDestroyedInExternalManagedCluster() { Mockito.when(kubernetesCluster.getClusterType()).thenReturn(KubernetesCluster.ClusterType.ExternalManaged); kubernetesServiceHelper.checkVmCanBeDestroyed(vm); } + + @Test + public void testIsValidNodeTypeEmptyNodeType() { + Assert.assertFalse(kubernetesServiceHelper.isValidNodeType(null)); + } + + @Test + public void testIsValidNodeTypeInvalidNodeType() { + Assert.assertFalse(kubernetesServiceHelper.isValidNodeType("invalidNodeType")); + } + + @Test + public void testIsValidNodeTypeValidNodeTypeLowercase() { + String nodeType = KubernetesClusterNodeType.WORKER.name().toLowerCase(); + Assert.assertTrue(kubernetesServiceHelper.isValidNodeType(nodeType)); + } + + private Map createServiceOfferingMapEntry(KubernetesClusterNodeType nodeType, String offeringUuid) { + Map map = new HashMap<>(); + map.put(VmDetailConstants.CKS_NODE_TYPE, nodeType.name().toLowerCase()); + map.put(VmDetailConstants.OFFERING, offeringUuid); + return map; + } + + @Test + public void testGetServiceOfferingNodeTypeMap() { + String workerOfferingUuid = UUID.randomUUID().toString(); + String controlOfferingUuid = UUID.randomUUID().toString(); + + ServiceOfferingVO workerOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(workerOffering.getId()).thenReturn(1L); + Mockito.when(serviceOfferingDao.findByUuid(workerOfferingUuid)).thenReturn(workerOffering); + + ServiceOfferingVO controlOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(controlOffering.getId()).thenReturn(2L); + Mockito.when(serviceOfferingDao.findByUuid(controlOfferingUuid)).thenReturn(controlOffering); + + Map> serviceOfferingNodeTypeMap = new HashMap<>(); + serviceOfferingNodeTypeMap.put("map1", createServiceOfferingMapEntry(KubernetesClusterNodeType.WORKER, workerOfferingUuid)); + serviceOfferingNodeTypeMap.put("map2", createServiceOfferingMapEntry(KubernetesClusterNodeType.CONTROL, controlOfferingUuid)); + + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.WORKER.name())); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.CONTROL.name())); + Assert.assertEquals(Long.valueOf(1L), result.get(KubernetesClusterNodeType.WORKER.name())); + Assert.assertEquals(Long.valueOf(2L), result.get(KubernetesClusterNodeType.CONTROL.name())); + } + + @Test + public void testGetServiceOfferingNodeTypeMapNullMap() { + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(null); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetServiceOfferingNodeTypeMapEtcdNodes() { + String etcdOfferingUuid = UUID.randomUUID().toString(); + + ServiceOfferingVO etcdOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(etcdOffering.getId()).thenReturn(3L); + Mockito.when(serviceOfferingDao.findByUuid(etcdOfferingUuid)).thenReturn(etcdOffering); + + Map> serviceOfferingNodeTypeMap = new HashMap<>(); + serviceOfferingNodeTypeMap.put("map1", createServiceOfferingMapEntry(KubernetesClusterNodeType.ETCD, etcdOfferingUuid)); + + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.ETCD.name())); + Assert.assertEquals(Long.valueOf(3L), result.get(KubernetesClusterNodeType.ETCD.name())); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryCompletenessInvalidParameters() { + kubernetesServiceHelper.checkNodeTypeOfferingEntryCompleteness(KubernetesClusterNodeType.WORKER.name(), null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryValuesInvalidNodeType() { + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + kubernetesServiceHelper.checkNodeTypeOfferingEntryValues("invalidNodeTypeName", offering, "some-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryValuesEmptyOffering() { + kubernetesServiceHelper.checkNodeTypeOfferingEntryValues(KubernetesClusterNodeType.WORKER.name(), null, "some-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryCompletenessBlankNodeType() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("", "affinity-group-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryCompletenessBlankAffinityGroupUuid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("control", ""); + } + + @Test + public void testCheckNodeTypeAffinityGroupEntryCompletenessValid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("control", "affinity-group-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryNodeTypeInvalid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryNodeType("invalid-node-type"); + } + + @Test + public void testCheckNodeTypeAffinityGroupEntryNodeTypeValid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryNodeType("control"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateAffinityGroupUuidAndGetIdBlank() { + kubernetesServiceHelper.validateAffinityGroupUuidAndGetId(""); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateAffinityGroupUuidAndGetIdNotFound() { + Mockito.when(affinityGroupDao.findByUuid("non-existent-uuid")).thenReturn(null); + kubernetesServiceHelper.validateAffinityGroupUuidAndGetId("non-existent-uuid"); + } + + @Test + public void testValidateAffinityGroupUuidAndGetIdValid() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); + Mockito.when(affinityGroupDao.findByUuid("valid-uuid")).thenReturn(affinityGroup); + Long result = kubernetesServiceHelper.validateAffinityGroupUuidAndGetId("valid-uuid"); + Assert.assertEquals(Long.valueOf(100L), result); + } + + @Test + public void testValidateAndGetAffinityGroupIdsSingleUuid() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(1L); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup); + + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1"); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(Long.valueOf(1L), result.get(0)); + } + + @Test + public void testValidateAndGetAffinityGroupIdsMultipleUuids() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); + Mockito.when(affinityGroup3.getId()).thenReturn(3L); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + Mockito.when(affinityGroupDao.findByUuid("uuid3")).thenReturn(affinityGroup3); + + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1,uuid2,uuid3"); + Assert.assertEquals(3, result.size()); + Assert.assertEquals(Arrays.asList(1L, 2L, 3L), result); + } + + @Test + public void testValidateAndGetAffinityGroupIdsWithSpaces() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds(" uuid1 , uuid2 "); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Arrays.asList(1L, 2L), result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateAndGetAffinityGroupIdsOneInvalid() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("invalid-uuid")).thenReturn(null); + + kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1,invalid-uuid"); + } + + @Test + public void testAddNodeTypeAffinityGroupEntry() { + Map> mapping = new HashMap<>(); + kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", Arrays.asList(1L, 2L), mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals(Arrays.asList(1L, 2L), mapping.get("CONTROL")); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntry() { + Map> mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(new HashMap<>(), mapping); + Assert.assertTrue(mapping.isEmpty()); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidValidEntry() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); + Mockito.when(affinityGroupDao.findByUuid("affinity-group-uuid")).thenReturn(affinityGroup); + + Map entry = new HashMap<>(); + entry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + entry.put(VmDetailConstants.AFFINITY_GROUP, "affinity-group-uuid"); + + Map> mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals(Arrays.asList(100L), mapping.get("CONTROL")); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidMultipleUuids() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + + Map entry = new HashMap<>(); + entry.put(VmDetailConstants.CKS_NODE_TYPE, "worker"); + entry.put(VmDetailConstants.AFFINITY_GROUP, "uuid1,uuid2"); + + Map> mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals(Arrays.asList(1L, 2L), mapping.get("WORKER")); + } + + @Test + public void testGetAffinityGroupNodeTypeMapEmptyMap() { + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); + Assert.assertTrue(result.isEmpty()); + + result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(new HashMap<>()); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetAffinityGroupNodeTypeMapValidEntries() { + AffinityGroupVO controlAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(controlAffinityGroup.getId()).thenReturn(100L); + Mockito.when(affinityGroupDao.findByUuid("control-affinity-uuid")).thenReturn(controlAffinityGroup); + + AffinityGroupVO workerAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(workerAffinityGroup.getId()).thenReturn(200L); + Mockito.when(affinityGroupDao.findByUuid("worker-affinity-uuid")).thenReturn(workerAffinityGroup); + + Map> affinityGroupNodeTypeMap = new HashMap<>(); + + Map controlEntry = new HashMap<>(); + controlEntry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + controlEntry.put(VmDetailConstants.AFFINITY_GROUP, "control-affinity-uuid"); + affinityGroupNodeTypeMap.put("0", controlEntry); + + Map workerEntry = new HashMap<>(); + workerEntry.put(VmDetailConstants.CKS_NODE_TYPE, "worker"); + workerEntry.put(VmDetailConstants.AFFINITY_GROUP, "worker-affinity-uuid"); + affinityGroupNodeTypeMap.put("1", workerEntry); + + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Arrays.asList(100L), result.get("CONTROL")); + Assert.assertEquals(Arrays.asList(200L), result.get("WORKER")); + } + + @Test + public void testGetAffinityGroupNodeTypeMapMultipleIdsPerNodeType() { + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag2 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getId()).thenReturn(1L); + Mockito.when(ag2.getId()).thenReturn(2L); + Mockito.when(ag3.getId()).thenReturn(3L); + Mockito.when(affinityGroupDao.findByUuid("ag1")).thenReturn(ag1); + Mockito.when(affinityGroupDao.findByUuid("ag2")).thenReturn(ag2); + Mockito.when(affinityGroupDao.findByUuid("ag3")).thenReturn(ag3); + + Map> affinityGroupNodeTypeMap = new HashMap<>(); + + Map controlEntry = new HashMap<>(); + controlEntry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + controlEntry.put(VmDetailConstants.AFFINITY_GROUP, "ag1,ag2,ag3"); + affinityGroupNodeTypeMap.put("0", controlEntry); + + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(Arrays.asList(1L, 2L, 3L), result.get("CONTROL")); + } } diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java index 1eb55808e09d..a25ec55cc04a 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java @@ -16,8 +16,14 @@ // under the License. package com.cloud.kubernetes.cluster.actionworkers; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.ApiConstants; import org.junit.Assert; import org.junit.Before; @@ -30,6 +36,8 @@ import com.cloud.kubernetes.cluster.KubernetesCluster; import com.cloud.kubernetes.cluster.KubernetesClusterDetailsVO; import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -60,6 +68,12 @@ public class KubernetesClusterActionWorkerTest { @Mock IPAddressDao ipAddressDao; + @Mock + KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + + @Mock + AffinityGroupDao affinityGroupDao; + KubernetesClusterActionWorker actionWorker = null; final static Long DEFAULT_ID = 1L; @@ -70,10 +84,12 @@ public void setUp() throws Exception { kubernetesClusterManager.kubernetesSupportedVersionDao = kubernetesSupportedVersionDao; kubernetesClusterManager.kubernetesClusterDetailsDao = kubernetesClusterDetailsDao; kubernetesClusterManager.kubernetesClusterVmMapDao = kubernetesClusterVmMapDao; + kubernetesClusterManager.kubernetesClusterAffinityGroupMapDao = kubernetesClusterAffinityGroupMapDao; KubernetesCluster kubernetesCluster = Mockito.mock(KubernetesCluster.class); Mockito.when(kubernetesCluster.getId()).thenReturn(DEFAULT_ID); actionWorker = new KubernetesClusterActionWorker(kubernetesCluster, kubernetesClusterManager); actionWorker.ipAddressDao = ipAddressDao; + actionWorker.affinityGroupDao = affinityGroupDao; } @Test @@ -130,4 +146,87 @@ public void testGetVpcTierKubernetesPublicIpValid() { IpAddress result = actionWorker.getVpcTierKubernetesPublicIp(mockNetworkForGetVpcTierKubernetesPublicIpTest()); Assert.assertNotNull(result); } + + @Test + public void testGetAffinityGroupIdsForNodeTypeReturnsIds() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(Arrays.asList(1L, 2L)); + + List result = actionWorker.getAffinityGroupIdsForNodeType(KubernetesClusterNodeType.CONTROL); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsAll(Arrays.asList(1L, 2L))); + } + + @Test + public void testGetAffinityGroupIdsForNodeTypeReturnsEmptyList() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "WORKER")) + .thenReturn(Collections.emptyList()); + + List result = actionWorker.getAffinityGroupIdsForNodeType(KubernetesClusterNodeType.WORKER); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetMergedAffinityGroupIdsWithExplicitDedication() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(new ArrayList<>(Arrays.asList(1L))); + + AffinityGroupVO explicitGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(explicitGroup.getId()).thenReturn(99L); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(explicitGroup); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.CONTROL, 1L, 1L); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(1L)); + Assert.assertTrue(result.contains(99L)); + } + + @Test + public void testGetMergedAffinityGroupIdsNoExplicitDedication() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "WORKER")) + .thenReturn(new ArrayList<>(Arrays.asList(1L, 2L))); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(null); + Mockito.when(affinityGroupDao.findDomainLevelGroupByType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(null); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.WORKER, 1L, 1L); + + Assert.assertEquals(2, result.size()); + } + + @Test + public void testGetMergedAffinityGroupIdsReturnsNullWhenEmpty() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "ETCD")) + .thenReturn(new ArrayList<>()); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.anyString())) + .thenReturn(null); + Mockito.when(affinityGroupDao.findDomainLevelGroupByType(Mockito.anyLong(), Mockito.anyString())) + .thenReturn(null); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.ETCD, 1L, 1L); + + Assert.assertNull(result); + } + + @Test + public void testGetMergedAffinityGroupIdsExplicitDedicationAlreadyInList() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(new ArrayList<>(Arrays.asList(99L, 2L))); + + AffinityGroupVO explicitGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(explicitGroup.getId()).thenReturn(99L); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(explicitGroup); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.CONTROL, 1L, 1L); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(99L)); + Assert.assertTrue(result.contains(2L)); + } } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/LoadBalanceRuleHandler.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/LoadBalanceRuleHandler.java index 3df58470fc68..fc167b71c23d 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/LoadBalanceRuleHandler.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/LoadBalanceRuleHandler.java @@ -363,7 +363,7 @@ private LoadBalancer handleCreateLoadBalancerRuleWithLock(final CreateLoadBalanc lb.setSourceIpAddressId(ipId); result = _lbMgr.createPublicLoadBalancer(lb.getXid(), lb.getName(), lb.getDescription(), lb.getSourcePortStart(), lb.getDefaultPortStart(), ipId.longValue(), - lb.getProtocol(), lb.getAlgorithm(), false, CallContext.current(), lb.getLbProtocol(), true, null); + lb.getProtocol(), lb.getAlgorithm(), false, CallContext.current(), lb.getLbProtocol(), true, null, networkId); } catch (final NetworkRuleConflictException e) { logger.warn("Failed to create LB rule, not continuing with ELB deployment"); if (newIp) { diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ContrailManagerImpl.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ContrailManagerImpl.java index f360fab01124..8badb916eeda 100644 --- a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ContrailManagerImpl.java +++ b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ContrailManagerImpl.java @@ -293,7 +293,7 @@ private VpcOffering locateVpcOffering() { } serviceProviderMap.put(svc, providerSet); } - vpcOffer = _vpcProvSvc.createVpcOffering(juniperVPCOfferingName, juniperVPCOfferingDisplayText, services, serviceProviderMap, null, null, null, null, null, null, null, VpcOffering.State.Enabled, null, false); + vpcOffer = _vpcProvSvc.createVpcOffering(juniperVPCOfferingName, juniperVPCOfferingDisplayText, services, serviceProviderMap, null, null, null, null, null, null, null, VpcOffering.State.Enabled, null, false, false); long id = vpcOffer.getId(); _vpcOffDao.update(id, (VpcOfferingVO)vpcOffer); return _vpcOffDao.findById(id); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 51cf82b13b06..cf98df0da243 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -1675,7 +1675,7 @@ public FirewallRuleResponse createPortForwardingRuleResponse(PortForwardingRule Network guestNtwk = ApiDBUtils.findNetworkById(fwRule.getNetworkId()); response.setNetworkId(guestNtwk.getUuid()); - + response.setNetworkName(guestNtwk.getName()); IpAddress ip = ApiDBUtils.findIpAddressById(fwRule.getSourceIpAddressId()); @@ -3535,6 +3535,7 @@ public VpcResponse createVpcResponse(ResponseView view, Vpc vpc) { if (voff != null) { response.setVpcOfferingId(voff.getUuid()); response.setVpcOfferingName(voff.getName()); + response.setVpcOfferingConserveMode(voff.isConserveMode()); } response.setCidr(vpc.getCidr()); response.setRestartRequired(vpc.isRestartRequired()); diff --git a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java index 7ea4b7d5834f..e7fe07a18c78 100644 --- a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java @@ -77,6 +77,7 @@ public VpcOfferingResponse newVpcOfferingResponse(VpcOffering offering) { if (offering.isSpecifyAsNumber() != null) { offeringResponse.setSpecifyAsNumber(offering.isSpecifyAsNumber()); } + offeringResponse.setConserveMode(offering.isConserveMode()); if (offering instanceof VpcOfferingJoinVO) { VpcOfferingJoinVO offeringJoinVO = (VpcOfferingJoinVO) offering; offeringResponse.setDomainId(offeringJoinVO.getDomainUuid()); diff --git a/server/src/main/java/com/cloud/api/query/vo/VpcOfferingJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/VpcOfferingJoinVO.java index 4e0707edf880..9d65c19479fb 100644 --- a/server/src/main/java/com/cloud/api/query/vo/VpcOfferingJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/VpcOfferingJoinVO.java @@ -112,6 +112,9 @@ public class VpcOfferingJoinVO implements VpcOffering { @Column(name = "specify_as_number") private Boolean specifyAsNumber = false; + @Column(name = "conserve_mode") + private boolean conserveMode; + public VpcOfferingJoinVO() { } @@ -178,6 +181,11 @@ public Boolean isSpecifyAsNumber() { return specifyAsNumber; } + @Override + public boolean isConserveMode() { + return conserveMode; + } + public void setSpecifyAsNumber(Boolean specifyAsNumber) { this.specifyAsNumber = specifyAsNumber; } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 0ce4b913a029..6cc816a81dec 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -22,6 +22,7 @@ import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; @@ -67,15 +68,18 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; -import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; +import org.apache.cloudstack.api.command.admin.network.NetworkOfferingBaseCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -3854,6 +3858,459 @@ 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; + } + + public T getOrDefault(T cmdValue, T defaultValue) { + return cmdValue != null ? cmdValue : defaultValue; + } + + public 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 + @ActionEvent(eventType = EventTypes.EVENT_DISK_OFFERING_CLONE, eventDescription = "cloning disk offering") + public DiskOffering cloneDiskOffering(final CloneDiskOfferingCmd cmd) { + final long userId = CallContext.current().getCallingUserId(); + final DiskOfferingVO sourceOffering = getAndValidateSourceDiskOffering(cmd.getSourceOfferingId()); + final Map 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) { @@ -6590,7 +7047,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()); @@ -7827,6 +8284,431 @@ public boolean deleteNetworkOffering(final DeleteNetworkOfferingCmd cmd) { } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_CLONE, eventDescription = "cloning network offering") + public NetworkOffering cloneNetworkOffering(final CloneNetworkOfferingCmd cmd) { + final Long sourceOfferingId = cmd.getSourceOfferingId(); + + final NetworkOfferingVO sourceOffering = _networkOfferingDao.findById(sourceOfferingId); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find network offering with id " + sourceOfferingId); + } + + String name = cmd.getNetworkOfferingName(); + if (name == null || name.isEmpty()) { + throw new InvalidParameterValueException("Name is required when cloning a network offering"); + } + + NetworkOfferingVO existing = _networkOfferingDao.findByUniqueName(name); + if (existing != null) { + throw new InvalidParameterValueException("Network offering with name '" + name + "' already exists"); + } + + logger.info("Cloning network offering {} (id: {}) to new offering with name: {}", + sourceOffering.getName(), sourceOfferingId, name); + + Map> sourceServiceProviderMap = + _networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId); + + validateProvider(sourceOffering, sourceServiceProviderMap, cmd.getProvider(), cmd.getNetworkMode()); + + applySourceOfferingValuesToCloneCmd(cmd, sourceServiceProviderMap, sourceOffering); + + return createNetworkOffering(cmd); + } + + private void validateProvider(NetworkOfferingVO sourceOffering, + Map> sourceServiceProviderMap, + String detectedProvider, String networkMode) { + + detectedProvider = getExternalNetworkProvider(detectedProvider, sourceServiceProviderMap); + // If this is an NSX/Netris offering, prevent network mode changes + if (detectedProvider != null && (detectedProvider.equals("NSX") || detectedProvider.equals("Netris"))) { + if (networkMode != null && sourceOffering.getNetworkMode() != null) { + if (!networkMode.equalsIgnoreCase(sourceOffering.getNetworkMode().toString())) { + throw new InvalidParameterValueException( + String.format("Cannot change network mode when cloning %s provider network offerings. " + + "Source offering has network mode '%s', but '%s' was specified. ", + detectedProvider, sourceOffering.getNetworkMode(), networkMode)); + } + } + } + } + + public static String getExternalNetworkProvider(String detectedProvider, + Map> sourceServiceProviderMap) { + if (StringUtils.isNotEmpty(detectedProvider)) { + return detectedProvider; + } + + if (sourceServiceProviderMap == null || sourceServiceProviderMap.isEmpty()) { + return null; + } + + for (Set providers : sourceServiceProviderMap.values()) { + if (CollectionUtils.isEmpty(providers)) { + continue; + } + for (Provider provider : providers) { + if (provider == Provider.Nsx) { + return "NSX"; + } + if (provider == Provider.Netris) { + return "Netris"; + } + } + } + + return null; + } + + /** + * Converts service provider map from internal format to API parameter format. + * + * Internal format: Map> 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, + Map> sourceServiceProviderMap, + NetworkOfferingVO sourceOffering) { + Long sourceOfferingId = sourceOffering.getId(); + + // 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()) { + // Filter capabilities to only include those for services in the final service list + // This ensures that if services are dropped, their capabilities are also removed + Map> filteredCapabilities = new HashMap<>(); + for (Map.Entry> entry : sourceServiceCapabilityList.entrySet()) { + Map capabilityMap = entry.getValue(); + String serviceName = capabilityMap.get("service"); + if (serviceName != null && finalServices.contains(serviceName)) { + filteredCapabilities.put(entry.getKey(), capabilityMap); + } + } + + if (!filteredCapabilities.isEmpty()) { + setField(cmd, "serviceCapabilitiesList", filteredCapabilities); + } + } + + applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText()); + applyIfNotProvided(cmd, requestParams, "traffictype", ApiConstants.TRAFFIC_TYPE, cmd.getTraffictype(), sourceOffering.getTrafficType().toString()); + applyIfNotProvided(cmd, requestParams, "tags", ApiConstants.TAGS, cmd.getTags(), sourceOffering.getTags()); + applyIfNotProvided(cmd, requestParams, "availability", ApiConstants.AVAILABILITY, cmd.getAvailability(), Availability.Optional.toString()); + applyIfNotProvided(cmd, requestParams, "networkRate", ApiConstants.NETWORKRATE, cmd.getNetworkRate(), sourceOffering.getRateMbps()); + applyIfNotProvided(cmd, requestParams, "serviceOfferingId", ApiConstants.SERVICE_OFFERING_ID, cmd.getServiceOfferingId(), sourceOffering.getServiceOfferingId()); + applyIfNotProvided(cmd, requestParams, "guestIptype", ApiConstants.GUEST_IP_TYPE, cmd.getGuestIpType(), sourceOffering.getGuestType().toString()); + applyIfNotProvided(cmd, requestParams, "maxConnections", ApiConstants.MAX_CONNECTIONS, cmd.getMaxconnections(), sourceOffering.getConcurrentConnections()); + + applyBooleanIfNotProvided(cmd, requestParams, "specifyVlan", ApiConstants.SPECIFY_VLAN, sourceOffering.isSpecifyVlan()); + applyBooleanIfNotProvided(cmd, requestParams, "conserveMode", ApiConstants.CONSERVE_MODE, sourceOffering.isConserveMode()); + applyBooleanIfNotProvided(cmd, requestParams, "specifyIpRanges", ApiConstants.SPECIFY_IP_RANGES, sourceOffering.isSpecifyIpRanges()); + applyBooleanIfNotProvided(cmd, requestParams, "isPersistent", ApiConstants.IS_PERSISTENT, sourceOffering.isPersistent()); + applyBooleanIfNotProvided(cmd, requestParams, "forVpc", ApiConstants.FOR_VPC, sourceOffering.isForVpc()); + applyBooleanIfNotProvided(cmd, requestParams, "egressDefaultPolicy", ApiConstants.EGRESS_DEFAULT_POLICY, sourceOffering.isEgressDefaultPolicy()); + applyBooleanIfNotProvided(cmd, requestParams, "keepAliveEnabled", ApiConstants.KEEPALIVE_ENABLED, sourceOffering.isKeepAliveEnabled()); + applyBooleanIfNotProvided(cmd, requestParams, "enable", ApiConstants.ENABLE, sourceOffering.getState() == NetworkOffering.State.Enabled); + applyBooleanIfNotProvided(cmd, requestParams, "specifyAsNumber", ApiConstants.SPECIFY_AS_NUMBER, sourceOffering.isSpecifyAsNumber()); + + if (!requestParams.containsKey(ApiConstants.INTERNET_PROTOCOL)) { + String internetProtocol = networkOfferingDetailsDao.getDetail(sourceOffering.getId(), Detail.internetProtocol); + if (internetProtocol != null) { + setField(cmd, "internetProtocol", internetProtocol); + } + } + + if (!requestParams.containsKey(ApiConstants.NETWORK_MODE) && sourceOffering.getNetworkMode() != null) { + setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString()); + } + + if (!requestParams.containsKey(ApiConstants.ROUTING_MODE) && sourceOffering.getRoutingMode() != null) { + setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString()); + } + + if (cmd.getDetails() == null || cmd.getDetails().isEmpty()) { + if (!sourceDetailsMap.isEmpty()) { + setField(cmd, "details", sourceDetailsMap); + } + } + + if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) { + if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) { + setField(cmd, "domainIds", sourceDomainIds); + } + } + if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) { + if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) { + setField(cmd, "zoneIds", sourceZoneIds); + } + } + + } catch (Exception e) { + logger.warn("Failed to apply some source offering parameters during clone: {}", e.getMessage()); + } + } + + /** + * Reconstructs the service capability list from the source network offering's stored capability flags. + * These capabilities were originally passed during creation and stored as boolean flags in the offering. + * + * Returns a Map in the format expected by CreateNetworkOfferingCmd.serviceCapabilitystList: + * Map where each value is a HashMap with keys: "service", "capabilitytype", "capabilityvalue" + */ + private Map> 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 { + Field field = findField(obj.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + obj.getClass().getName()); + } + field.setAccessible(true); + field.set(obj, value); + } + + public static Field findField(Class clazz, String fieldName) { + Class currentClass = clazz; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + return null; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_EDIT, eventDescription = "updating network offering") public NetworkOffering updateNetworkOffering(final UpdateNetworkOfferingCmd cmd) { diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index 20ca189994ef..7f41a1a106cb 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -1543,6 +1543,14 @@ public IPAddressVO doInTransaction(TransactionStatus status) throws Insufficient return ipaddr; } + protected IPAddressVO getExistingSourceNatInVPC(Long vpcId) { + List ips = _ipAddressDao.listByAssociatedVpc(vpcId, true); + if (CollectionUtils.isEmpty(ips)) { + return null; + } + return ips.get(0); + } + protected IPAddressVO getExistingSourceNatInNetwork(long ownerId, Long networkId) { List addrs; Network guestNetwork = _networksDao.findById(networkId); @@ -1723,7 +1731,11 @@ protected boolean isSourceNatAvailableForNetwork(Account owner, IPAddressVO ipTo NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId()); boolean sharedSourceNat = offering.isSharedSourceNat(); boolean isSourceNat = false; - if (!sharedSourceNat) { + if (network.getVpcId() != null) { + // For VPCs: Check if the VPC Source NAT IP address is the same we are associating + IPAddressVO vpcSourceNatIpAddress = getExistingSourceNatInVPC(network.getVpcId()); + isSourceNat = vpcSourceNatIpAddress != null && vpcSourceNatIpAddress.getId() == ipToAssoc.getId(); + } else if (!sharedSourceNat) { if (getExistingSourceNatInNetwork(owner.getId(), network.getId()) == null) { if (network.getGuestType() == GuestType.Isolated && network.getVpcId() == null && !ipToAssoc.isPortable()) { isSourceNat = true; @@ -2647,4 +2659,31 @@ public void updateSourceNatIpAddress(IPAddressVO requestedIp, List }); } + @Override + public Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId) { + boolean vpcConserveMode = isPublicIpOnVpcConserveMode(ip); + return getPreferredNetworkIdForRule(ip, vpcConserveMode, networkId); + } + + protected Long getPreferredNetworkIdForRule(IpAddress ip, boolean vpcConserveModeEnabled, Long networkId) { + if (vpcConserveModeEnabled) { + // Since VPC Conserve mode allows rules from multiple VPC tiers, always check the networkId parameter first + return networkId != null ? networkId : ip.getAssociatedWithNetworkId(); + } else { + // In case of Guest Networks or VPC Tier Networks VPC Conserve mode disabled prefer the associated networkId + return ip.getAssociatedWithNetworkId() != null ? ip.getAssociatedWithNetworkId() : networkId; + } + } + + protected boolean isPublicIpOnVpcConserveMode(IpAddress ip) { + if (ip.getVpcId() == null) { + return false; + } + Vpc vpc = _vpcMgr.getActiveVpc(ip.getVpcId()); + if (vpc == null) { + return false; + } + VpcOffering vpcOffering = vpcOfferingDao.findById(vpc.getVpcOfferingId()); + return vpcOffering != null && vpcOffering.isConserveMode(); + } } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index b959cc478d6f..0a2e679b723e 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -6434,6 +6434,11 @@ public String getNicVlanValueForExternalVm(NicTO nic) { return Networks.BroadcastDomainType.getValue(nic.getBroadcastUri()); } + @Override + public Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId) { + return _ipAddrMgr.getPreferredNetworkIdForPublicIpRuleAssignment(ip, networkId); + } + @Override public Network.IpAddresses getIpAddressesFromIps(String ipAddress, String ip6Address, String macAddress) { if (ip6Address != null) { diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index 805a897e0806..805ac4aed86c 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -2051,7 +2051,7 @@ private boolean assignLBruleToNewVm(long vmId, AutoScaleVmGroupVO asGroup) { } lstVmId.add(new Long(vmId)); try { - return loadBalancingRulesService.assignToLoadBalancer(lbId, lstVmId, new HashMap<>(), true); + return loadBalancingRulesService.assignToLoadBalancer(lbId, lstVmId, new HashMap<>(), null, true); } catch (CloudRuntimeException ex) { logger.warn("Caught exception: ", ex); return false; diff --git a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java index 00863c28dd22..779d26d51f1c 100644 --- a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java +++ b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java @@ -30,6 +30,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.dao.VpcOfferingDao; import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; @@ -159,6 +161,8 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService, IpAddressManager _ipAddrMgr; @Inject RoutedIpv4Manager routedIpv4Manager; + @Inject + VpcOfferingDao vpcOfferingDao; private boolean _elbEnabled = false; static Boolean rulesContinueOnErrFlag = true; @@ -395,6 +399,10 @@ public void detectRulesConflict(FirewallRule newRule) throws NetworkRuleConflict assert (rules.size() >= 1); } + NetworkVO newRuleNetwork = getNewRuleNetwork(newRule); + boolean newRuleIsOnVpcNetwork = newRuleNetwork.getVpcId() != null; + boolean vpcConserveModeEnabled = _vpcMgr.isNetworkOnVpcEnabledConserveMode(newRuleNetwork); + for (FirewallRuleVO rule : rules) { if (rule.getId() == newRule.getId()) { continue; // Skips my own rule. @@ -443,8 +451,15 @@ public void detectRulesConflict(FirewallRule newRule) throws NetworkRuleConflict } // Checking if the rule applied is to the same network that is passed in the rule. - if (rule.getNetworkId() != newRule.getNetworkId() && rule.getState() != State.Revoke) { - throw new NetworkRuleConflictException("New rule is for a different network than what's specified in rule " + rule.getXid()); + // (except for VPCs with conserve mode = true) + if ((!newRuleIsOnVpcNetwork || !vpcConserveModeEnabled) + && rule.getNetworkId() != newRule.getNetworkId() && rule.getState() != State.Revoke) { + String errMsg = String.format("New rule is for a different network than what's specified in rule %s", rule.getXid()); + if (newRuleIsOnVpcNetwork) { + Vpc vpc = _vpcMgr.getActiveVpc(newRuleNetwork.getVpcId()); + errMsg += String.format(" - VPC id=%s is not using conserve mode", vpc.getUuid()); + } + throw new NetworkRuleConflictException(errMsg); } //Check for the ICMP protocol. This has to be done separately from other protocols as we need to check the ICMP codes and ICMP type also. @@ -493,6 +508,14 @@ public void detectRulesConflict(FirewallRule newRule) throws NetworkRuleConflict } } + protected NetworkVO getNewRuleNetwork(FirewallRule newRule) { + NetworkVO newRuleNetwork = _networkDao.findById(newRule.getNetworkId()); + if (newRuleNetwork == null) { + throw new InvalidParameterValueException("Unable to create firewall rule as cannot find network by id=" + newRule.getNetworkId()); + } + return newRuleNetwork; + } + protected boolean checkIfRulesHaveConflictingPortRanges(FirewallRule newRule, FirewallRule rule, boolean oneOfRulesIsFirewall, boolean bothRulesFirewall, boolean bothRulesPortForwarding, boolean duplicatedCidrs) { String rulesAsString = String.format("[%s] and [%s]", rule, newRule); diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java index ced1d781ab57..5f00261f3290 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -53,6 +53,8 @@ import org.apache.cloudstack.lb.dao.ApplicationLoadBalancerRuleDao; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.to.LoadBalancerTO; @@ -1018,7 +1020,7 @@ private boolean isRollBackAllowedForProvider(LoadBalancerVO loadBalancer) { @Override @DB @ActionEvent(eventType = EventTypes.EVENT_ASSIGN_TO_LOAD_BALANCER_RULE, eventDescription = "assigning to load balancer", async = true) - public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, Map> vmIdIpMap, boolean isAutoScaleVM) { + public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, Map> vmIdIpMap, Map vmIdNetworkMap, boolean isAutoScaleVM) { CallContext ctx = CallContext.current(); Account caller = ctx.getCallingAccount(); @@ -1091,28 +1093,12 @@ public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, _rulesMgr.checkRuleAndUserVm(loadBalancer, vm, caller); Account vmOwner = _accountDao.findById(vm.getAccountId()); - Network network = _networkDao.findById(loadBalancer.getNetworkId()); - _accountMgr.checkAccess(vmOwner, SecurityChecker.AccessType.UseEntry, false, network); - - // Let's check to make sure the vm has a nic in the same network as - // the load balancing rule. - List nics = _networkModel.getNics(vm.getId()); - Nic nicInSameNetwork = null; - for (Nic nic : nics) { - if (nic.getNetworkId() == loadBalancer.getNetworkId()) { - nicInSameNetwork = nic; - break; - } - } + Network loadBalancerNetwork = _networkDao.findById(loadBalancer.getNetworkId()); + _accountMgr.checkAccess(vmOwner, SecurityChecker.AccessType.UseEntry, false, loadBalancerNetwork); - if (nicInSameNetwork == null) { - InvalidParameterValueException ex = - new InvalidParameterValueException("VM with id specified cannot be added because it doesn't belong in the same network."); - ex.addProxyObject(vm.getUuid(), "instanceId"); - throw ex; - } + Nic vmNicInLb = getVmNicInLoadBalancer(vm, loadBalancer, loadBalancerNetwork, vmIdNetworkMap, vmOwner); - String priIp = nicInSameNetwork.getIPv4Address(); + String priIp = vmNicInLb.getIPv4Address(); if (existingVmIdIps.containsKey(instanceId)) { // now check for ip address @@ -1142,9 +1128,9 @@ public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, if (ip.equals(priIp)) { continue; } - if(_nicSecondaryIpDao.findByIp4AddressAndNicId(ip,nicInSameNetwork.getId()) == null) { + if(_nicSecondaryIpDao.findByIp4AddressAndNicId(ip,vmNicInLb.getId()) == null) { throw new InvalidParameterValueException("Instance IP "+ ip + " specified does not belong to " + - "NIC in Network " + nicInSameNetwork.getNetworkId()); + "NIC in Network " + vmNicInLb.getNetworkId()); } } } else { @@ -1234,6 +1220,63 @@ public void doInTransactionWithoutResult(TransactionStatus status) { return success; } + protected Nic getVmNicInLoadBalancer(UserVm vm, LoadBalancerVO loadBalancer, Network loadBalancerNetwork, Map vmIdNetworkMap, Account vmOwner) { + boolean isVpcConserveModeEnabled = _vpcMgr.isNetworkOnVpcEnabledConserveMode(loadBalancerNetwork); + + boolean isNetworkPassedVpcConserveMode = isVpcConserveModeEnabled && MapUtils.isNotEmpty(vmIdNetworkMap) && vmIdNetworkMap.containsKey(vm.getId()); + Nic vmNicInLb = isNetworkPassedVpcConserveMode ? + getNicForVmInVpcConserveModeTierNetwork(vm, vmIdNetworkMap, vmOwner, loadBalancerNetwork) : + getNicForVmLbNetwork(vm, loadBalancer); + + if (vmNicInLb == null) { + String msg = !isVpcConserveModeEnabled ? + "VM with id specified cannot be added because it doesn't belong in the same network." : + "VM with id specified cannot be added to the load balancing rule for VPC Conserve Mode."; + InvalidParameterValueException ex = new InvalidParameterValueException(msg); + ex.addProxyObject(vm.getUuid(), "instanceId"); + throw ex; + } + return vmNicInLb; + } + + /** + * For Isolated Networks or Network tiers of VPCs not using Conserve mode, use the same network as the load balancer + * @return the nic of the VM in the load balancer network + */ + protected Nic getNicForVmLbNetwork(UserVm vm, LoadBalancerVO loadBalancer) { + List nics = _networkModel.getNics(vm.getId()); + for (Nic nic : nics) { + if (nic.getNetworkId() == loadBalancer.getNetworkId()) { + return nic; + } + } + return null; + } + + /** + * On VPC Conserve Mode, VMs from multiple VPC networks tiers can be assigned to the same load balancer. + * @return the nic of the VM in the specified tier network in `vmIdNetworkMap` + */ + protected Nic getNicForVmInVpcConserveModeTierNetwork(UserVm vm, Map vmIdNetworkMap, Account vmOwner, Network loadBalancerNetwork) { + Long vmNetworkId = vmIdNetworkMap.get(vm.getId()); + Network vmNetwork = _networkDao.findById(vmNetworkId); + _accountMgr.checkAccess(vmOwner, SecurityChecker.AccessType.UseEntry, false, vmNetwork); + checkNetworkBelongsToLoadBalancerVpc(vmNetwork, loadBalancerNetwork); + return _networkModel.getNicInNetwork(vm.getId(), vmNetworkId); + } + + protected void checkNetworkBelongsToLoadBalancerVpc(Network vmNetwork, Network loadBalancerNetwork) { + if (ObjectUtils.anyNull(vmNetwork, loadBalancerNetwork)) { + throw new InvalidParameterValueException("Cannot add VM to load balancer because the VM network or load balancer network is null"); + } + if (ObjectUtils.anyNull(vmNetwork.getVpcId(), loadBalancerNetwork.getVpcId())) { + throw new InvalidParameterValueException("Cannot add VM to load balancer because the VM network or load balancer network are not part of a VPC"); + } + if (!vmNetwork.getVpcId().equals(loadBalancerNetwork.getVpcId())) { + throw new InvalidParameterValueException("Cannot add VM to load balancer because the VM network and load balancer network are not part of the same VPC"); + } + } + @Override public boolean assignSSLCertToLoadBalancerRule(Long lbId, String certName, String publicCert, String privateKey) { logger.error("Calling the manager for LB"); @@ -1740,6 +1783,8 @@ public LoadBalancer createPublicLoadBalancerRule(String xId, String name, String throw new NetworkRuleConflictException("Can't do load balance on IP address: " + ipVO.getAddress()); } + verifyLoadBalancerRuleNetwork(name, network, ipVO); + String cidrString = generateCidrString(cidrList); boolean performedIpAssoc = false; @@ -1763,7 +1808,7 @@ public LoadBalancer createPublicLoadBalancerRule(String xId, String name, String } result = createPublicLoadBalancer(xId, name, description, srcPortStart, defPortStart, ipVO.getId(), protocol, algorithm, openFirewall, CallContext.current(), - lbProtocol, forDisplay, cidrString); + lbProtocol, forDisplay, cidrString, networkId); } catch (Exception ex) { logger.warn("Failed to create load balancer due to ", ex); if (ex instanceof NetworkRuleConflictException) { @@ -1792,7 +1837,18 @@ public LoadBalancer createPublicLoadBalancerRule(String xId, String name, String return result; } - /** + + protected void verifyLoadBalancerRuleNetwork(String lbName, Network network, IPAddressVO ipVO) { + boolean isVpcConserveModeEnabled = _vpcMgr.isNetworkOnVpcEnabledConserveMode(network); + if (!isVpcConserveModeEnabled && ipVO.getAssociatedWithNetworkId() != null && network.getId() != ipVO.getAssociatedWithNetworkId()) { + String msg = String.format("Cannot create Load Balancer rule %s as the IP address %s is not associated " + + "with the network %s (ID=%s)", lbName, ipVO.getAddress(), network.getName(), network.getUuid()); + logger.error(msg); + throw new InvalidParameterValueException(msg); + } + } + + /** * Transforms the cidrList from a List of Strings to a String which contains all the CIDRs from cidrList separated by whitespaces. This is used to facilitate both the persistence * in the DB and also later when building the configuration String in the getRulesForPool method of the HAProxyConfigurator class. */ @@ -1826,7 +1882,7 @@ private String validateCidr(String cidr) { @Override public LoadBalancer createPublicLoadBalancer(final String xId, final String name, final String description, final int srcPort, final int destPort, final long sourceIpId, final String protocol, final String algorithm, final boolean openFirewall, final CallContext caller, final String lbProtocol, - final Boolean forDisplay, String cidrList) throws NetworkRuleConflictException { + final Boolean forDisplay, String cidrList, Long networkIdParam) throws NetworkRuleConflictException { if (!NetUtils.isValidPort(destPort)) { throw new InvalidParameterValueException("privatePort is an invalid value: " + destPort); } @@ -1855,7 +1911,7 @@ public LoadBalancer createPublicLoadBalancer(final String xId, final String name _accountMgr.checkAccess(caller.getCallingAccount(), null, true, ipAddr); - final Long networkId = ipAddr.getAssociatedWithNetworkId(); + final Long networkId = _ipAddrMgr.getPreferredNetworkIdForPublicIpRuleAssignment(ipAddr, networkIdParam); if (networkId == null) { InvalidParameterValueException ex = new InvalidParameterValueException("Unable to create load balancer rule ; specified sourceip id is not associated with any network"); 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 86d1fba038b7..156e71e72b8f 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -71,6 +71,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; @@ -388,7 +389,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } createVpcOffering(VpcOffering.defaultVPCOfferingName, VpcOffering.defaultVPCOfferingName, svcProviderMap, true, State.Enabled, null, false, - false, false, null, null, false); + false, false, null, null, false, false); } // configure default vpc offering with Netscaler as LB Provider @@ -408,7 +409,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.defaultVPCNSOfferingName, VpcOffering.defaultVPCNSOfferingName, - svcProviderMap, false, State.Enabled, null, false, false, false, null, null, false); + svcProviderMap, false, State.Enabled, null, false, false, false, null, null, false, false); } @@ -429,7 +430,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.redundantVPCOfferingName, VpcOffering.redundantVPCOfferingName, svcProviderMap, true, State.Enabled, - null, false, false, true, null, null, false); + null, false, false, true, null, null, false, false); } // configure default vpc offering with NSX as network service provider in NAT mode @@ -446,7 +447,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.DEFAULT_VPC_NAT_NSX_OFFERING_NAME, VpcOffering.DEFAULT_VPC_NAT_NSX_OFFERING_NAME, svcProviderMap, false, - State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false, false); } @@ -464,7 +465,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.DEFAULT_VPC_ROUTE_NSX_OFFERING_NAME, VpcOffering.DEFAULT_VPC_ROUTE_NSX_OFFERING_NAME, svcProviderMap, false, - State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false, false); } @@ -482,7 +483,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.DEFAULT_VPC_ROUTE_NETRIS_OFFERING_NAME, VpcOffering.DEFAULT_VPC_ROUTE_NETRIS_OFFERING_NAME, svcProviderMap, false, - State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false, false); } @@ -500,7 +501,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.DEFAULT_VPC_NAT_NETRIS_OFFERING_NAME, VpcOffering.DEFAULT_VPC_NAT_NETRIS_OFFERING_NAME, svcProviderMap, false, - State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false, false); } } @@ -586,6 +587,7 @@ public VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd) { } boolean specifyAsNumber = cmd.getSpecifyAsNumber(); String routingModeString = cmd.getRoutingMode(); + boolean conserveMode = cmd.isConserveMode(); // check if valid domain if (CollectionUtils.isNotEmpty(cmd.getDomainIds())) { @@ -624,7 +626,7 @@ public VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd) { return createVpcOffering(vpcOfferingName, displayText, supportedServices, serviceProviderList, serviceCapabilityList, internetProtocol, serviceOfferingId, provider, networkMode, - domainIds, zoneIds, (enable ? State.Enabled : State.Disabled), routingMode, specifyAsNumber); + domainIds, zoneIds, (enable ? State.Enabled : State.Disabled), routingMode, specifyAsNumber, conserveMode); } @Override @@ -632,7 +634,13 @@ public VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd) { public VpcOffering createVpcOffering(final String name, final String displayText, final List supportedServices, final Map> serviceProviders, final Map serviceCapabilityList, final NetUtils.InternetProtocol internetProtocol, final Long serviceOfferingId, final String externalProvider, final NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, State state, - NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber) { + NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode) { + + boolean isExternalProvider = externalProvider != null && + Arrays.asList("NSX", "Netris").stream().anyMatch(s -> s.equalsIgnoreCase(externalProvider)); + if (!isExternalProvider && CollectionUtils.isEmpty(supportedServices)) { + throw new InvalidParameterValueException("Supported services needs to be provided"); + } if (!Ipv6Service.Ipv6OfferingCreationEnabled.value() && !(internetProtocol == null || NetUtils.InternetProtocol.IPv4.equals(internetProtocol))) { throw new InvalidParameterValueException(String.format("Configuration %s needs to be enabled for creating IPv6 supported VPC offering", Ipv6Service.Ipv6OfferingCreationEnabled.key())); @@ -727,7 +735,7 @@ public VpcOffering createVpcOffering(final String name, final String displayText final boolean offersRegionLevelVPC = isVpcOfferingForRegionLevelVpc(serviceCapabilityList); final boolean redundantRouter = isVpcOfferingRedundantRouter(serviceCapabilityList, redundantRouterService); final VpcOfferingVO offering = createVpcOffering(name, displayText, svcProviderMap, false, state, serviceOfferingId, supportsDistributedRouter, offersRegionLevelVPC, - redundantRouter, networkMode, routingMode, specifyAsNumber); + redundantRouter, networkMode, routingMode, specifyAsNumber, conserveMode); if (offering != null) { List detailsVO = new ArrayList<>(); @@ -755,7 +763,7 @@ public VpcOffering createVpcOffering(final String name, final String displayText @DB protected VpcOfferingVO createVpcOffering(final String name, final String displayText, final Map> svcProviderMap, final boolean isDefault, final State state, final Long serviceOfferingId, final boolean supportsDistributedRouter, final boolean offersRegionLevelVPC, - final boolean redundantRouter, NetworkOffering.NetworkMode networkMode, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber) { + final boolean redundantRouter, NetworkOffering.NetworkMode networkMode, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode) { return Transaction.execute(new TransactionCallback() { @Override @@ -771,6 +779,7 @@ public VpcOfferingVO doInTransaction(final TransactionStatus status) { if (Objects.nonNull(routingMode)) { offering.setRoutingMode(routingMode); } + offering.setConserveMode(conserveMode); logger.debug("Adding vpc offering " + offering); offering = _vpcOffDao.persist(offering); @@ -811,6 +820,349 @@ protected void checkCapabilityPerServiceProvider(final Set providers, } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VPC_OFFERING_CLONE, eventDescription = "cloning VPC offering") + public VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd) { + Long sourceVpcOfferingId = cmd.getSourceOfferingId(); + + final VpcOffering sourceVpcOffering = _vpcOffDao.findById(sourceVpcOfferingId); + if (sourceVpcOffering == null) { + throw new InvalidParameterValueException("Unable to find source VPC offering by id " + sourceVpcOfferingId); + } + + String name = cmd.getVpcOfferingName(); + if (name == null || name.isEmpty()) { + throw new InvalidParameterValueException("Name is required when cloning a VPC offering"); + } + + VpcOfferingVO vpcOfferingVO = _vpcOffDao.findByUniqueName(name); + if (vpcOfferingVO != null) { + throw new InvalidParameterValueException(String.format("A VPC offering with name %s already exists", name)); + } + + logger.info("Cloning VPC offering {} (id: {}) to new offering with name: {}", + sourceVpcOffering.getName(), sourceVpcOfferingId, name); + + Map> sourceServiceProviderMap = getVpcOffSvcProvidersMap(sourceVpcOfferingId); + validateProvider(sourceVpcOffering, sourceServiceProviderMap, cmd.getProvider(), cmd.getNetworkMode()); + + applySourceOfferingValuesToCloneCmd(cmd, sourceServiceProviderMap, sourceVpcOffering); + + return createVpcOffering(cmd); + } + + private void validateProvider(VpcOffering sourceVpcOffering, + Map> sourceServiceProviderMap, + String provider, String networkMode) { + provider = ConfigurationManagerImpl.getExternalNetworkProvider(provider, sourceServiceProviderMap); + if (provider != null && (provider.equals("NSX") || provider.equals("Netris"))) { + if (networkMode != null && sourceVpcOffering.getNetworkMode() != null) { + if (!networkMode.equalsIgnoreCase(sourceVpcOffering.getNetworkMode().toString())) { + throw new InvalidParameterValueException( + String.format("Cannot change network mode when cloning %s provider VPC offerings. " + + "Source offering has network mode '%s', but '%s' was specified. ", + provider, sourceVpcOffering.getNetworkMode(), networkMode)); + } + } + } + } + + private void applySourceOfferingValuesToCloneCmd(CloneVPCOfferingCmd cmd, + Map> sourceServiceProviderMap, + VpcOffering sourceVpcOffering) { + Long sourceOfferingId = sourceVpcOffering.getId(); + + 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(normalizedDropServices); + 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()) { + Map filteredCapabilities = filterServiceCapabilities(sourceServiceCapabilityList, finalServices); + if (!filteredCapabilities.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", filteredCapabilities); + } + } + + if (cmd.getDisplayText() == null && sourceOffering.getDisplayText() != null) { + ConfigurationManagerImpl.setField(cmd, "displayText", sourceOffering.getDisplayText()); + } + + if (cmd.getServiceOfferingId() == null && sourceOffering.getServiceOfferingId() != null) { + ConfigurationManagerImpl.setField(cmd, "serviceOfferingId", sourceOffering.getServiceOfferingId()); + } + + Boolean enableFieldValue = getRawFieldValue(cmd, "enable", Boolean.class); + if (enableFieldValue == null) { + Boolean enableState = sourceOffering.getState() == VpcOffering.State.Enabled; + ConfigurationManagerImpl.setField(cmd, "enable", enableState); + } + + Boolean specifyAsNumberFieldValue = getRawFieldValue(cmd, "specifyAsNumber", Boolean.class); + if (specifyAsNumberFieldValue == null) { + ConfigurationManagerImpl.setField(cmd, "specifyAsNumber", sourceOffering.isSpecifyAsNumber()); + } + + if (cmd.getInternetProtocol() == null) { + String internetProtocol = vpcOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.INTERNET_PROTOCOL); + if (internetProtocol != null) { + ConfigurationManagerImpl.setField(cmd, "internetProtocol", internetProtocol); + } + } + + if (cmd.getNetworkMode() == null && sourceOffering.getNetworkMode() != null) { + ConfigurationManagerImpl.setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString()); + } + + if (cmd.getRoutingMode() == null && sourceOffering.getRoutingMode() != null) { + ConfigurationManagerImpl.setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString()); + } + + if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) { + if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "domainIds", sourceDomainIds); + } + } + + if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) { + if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "zoneIds", sourceZoneIds); + } + } + + } catch (Exception e) { + logger.error("Failed to apply source offering parameters during clone: {}", e.getMessage(), e); + throw new CloudRuntimeException("Failed to apply source offering parameters during VPC offering clone", e); + } + } + + private T 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; + } + + /** + * Filters service capabilities to only include those for services present in the final services list. + * This ensures that when services are dropped during cloning, their associated capabilities are also removed. + * + * @param sourceServiceCapabilityList The original capability list from the source VPC offering + * in format: Map with keys like "0.service", "0.capabilitytype", "0.capabilityvalue" + * @param finalServices The list of service names that should be retained in the cloned offering + * @return Filtered map containing only capabilities for services in finalServices + */ + private Map filterServiceCapabilities(Map sourceServiceCapabilityList, + List finalServices) { + Map filteredCapabilities = new HashMap<>(); + + for (Map.Entry entry : sourceServiceCapabilityList.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + // Check if this is a service key (e.g., "0.service", "1.service") + if (key.endsWith(".service")) { + String serviceName = value; + if (finalServices.contains(serviceName)) { + // Include this service and its associated capability entries + String prefix = key.substring(0, key.lastIndexOf('.')); + filteredCapabilities.put(key, value); + + // Also include the capability type and value for this service + String capabilityTypeKey = prefix + ".capabilitytype"; + String capabilityValueKey = prefix + ".capabilityvalue"; + if (sourceServiceCapabilityList.containsKey(capabilityTypeKey)) { + filteredCapabilities.put(capabilityTypeKey, sourceServiceCapabilityList.get(capabilityTypeKey)); + } + if (sourceServiceCapabilityList.containsKey(capabilityValueKey)) { + filteredCapabilities.put(capabilityValueKey, sourceServiceCapabilityList.get(capabilityValueKey)); + } + } + } + } + + return filteredCapabilities; + } + private void validateConnectivtyServiceCapabilities(final Set providers, final Map serviceCapabilitystList) { if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { final Collection serviceCapabilityCollection = serviceCapabilitystList.values(); @@ -2954,6 +3306,20 @@ public boolean applyStaticRouteForVpcVpnIfNeeded(final Long vpcId, boolean updat return true; } + protected boolean isNetworkOnVpc(Network network) { + return network.getVpcId() != null; + } + + @Override + public boolean isNetworkOnVpcEnabledConserveMode(Network newRuleNetwork) { + if (isNetworkOnVpc(newRuleNetwork)) { + Vpc vpc = getActiveVpc(newRuleNetwork.getVpcId()); + VpcOfferingVO vpcOffering = vpc != null ? _vpcOffDao.findById(vpc.getVpcOfferingId()) : null; + return vpcOffering != null && vpcOffering.isConserveMode(); + } + return false; + } + protected boolean applyStaticRoutes(final List routes, final Account caller, final boolean updateRoutesInDB) throws ResourceUnavailableException { final boolean success = true; final List staticRouteProfiles = getVpcStaticRoutes(routes); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index c68d9ea47984..f8c4d1d44d4c 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -132,6 +132,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; @@ -162,6 +163,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; @@ -328,6 +331,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; @@ -3860,6 +3864,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); @@ -3880,7 +3885,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); @@ -3965,6 +3972,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/affinity/AffinityGroupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java index d212e7435b29..a2ef86e3c0c1 100644 --- a/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java @@ -25,6 +25,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -41,6 +43,8 @@ import com.cloud.domain.dao.DomainDao; import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper; +import com.cloud.utils.component.ComponentContext; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.hypervisor.Hypervisor; @@ -435,6 +439,13 @@ public UserVm updateVMAffinityGroups(Long vmId, List affinityGroupIds) { if (UserVmManager.SHAREDFSVM.equals(vmInstance.getUserVmType())) { throw new InvalidParameterValueException("Operation not supported on Shared FileSystem Instance"); } + try { + KubernetesServiceHelper kubernetesServiceHelper = + ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class); + kubernetesServiceHelper.checkVmAffinityGroupsCanBeUpdated(vmInstance); + } catch (NoSuchBeanDefinitionException ignored) { + logger.debug("No KubernetesServiceHelper bean found"); + } if (Hypervisor.HypervisorType.External.equals(vmInstance.getHypervisorType())) { logger.error("Update VM Affinity Group not supported for {} as it is {} hypervisor instance", vmInstance, Hypervisor.HypervisorType.External.name()); 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 ed8391fe0c65..db636c7f0f42 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -42,6 +42,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; @@ -325,7 +326,6 @@ public BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd) { return savedOffering; } - @Override public List getBackupOfferingDomains(Long offeringId) { final BackupOffering backupOffering = backupOfferingDao.findById(offeringId); if (backupOffering == null) { @@ -334,6 +334,78 @@ public List getBackupOfferingDomains(Long offeringId) { return backupOfferingDetailsDao.findDomainIds(offeringId); } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE, eventDescription = "cloning backup offering") + public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) { + final BackupOfferingVO sourceOffering = backupOfferingDao.findById(cmd.getSourceOfferingId()); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find backup offering with ID: " + cmd.getSourceOfferingId()); + } + + validateBackupForZone(sourceOffering.getZoneId()); + + if (backupOfferingDao.findByName(cmd.getName(), sourceOffering.getZoneId()) != null) { + throw new CloudRuntimeException("A backup offering with the name '" + cmd.getName() + "' already exists in this zone"); + } + + final String description = cmd.getDescription() != null ? cmd.getDescription() : sourceOffering.getDescription(); + final String externalId = cmd.getExternalId() != null ? cmd.getExternalId() : sourceOffering.getExternalId(); + final boolean userDrivenBackups = cmd.getUserDrivenBackups() != null ? cmd.getUserDrivenBackups() : sourceOffering.isUserDrivenBackupAllowed(); + final Long zoneId = cmd.getZoneId() != null ? cmd.getZoneId() : sourceOffering.getZoneId(); + + if (!Objects.equals(sourceOffering.getExternalId(), externalId) || !Objects.equals(sourceOffering.getZoneId(), zoneId)) { + final BackupProvider provider = getBackupProvider(zoneId); + if (!provider.isValidProviderOffering(zoneId, externalId)) { + throw new CloudRuntimeException("Backup offering '" + externalId + "' does not exist on provider " + provider.getName() + " on zone " + zoneId); + } + } + + final BackupOffering existingOffering = backupOfferingDao.findByExternalId(externalId, zoneId); + if (existingOffering != null) { + throw new CloudRuntimeException("A backup offering with external ID '" + externalId + "' already exists in this zone"); + } + + final BackupOfferingVO clonedOffering = new BackupOfferingVO( + zoneId, + externalId, + sourceOffering.getProvider(), + cmd.getName(), + description, + userDrivenBackups + ); + + final BackupOfferingVO savedOffering = backupOfferingDao.persist(clonedOffering); + if (savedOffering == null) { + throw new CloudRuntimeException("Unable to clone backup offering from ID: " + cmd.getSourceOfferingId()); + } + + List filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds()); + Collections.sort(filteredDomainIds); + updateBackupOfferingDomainDetail(savedOffering, filteredDomainIds); + + logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getSourceOfferingId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")"); + return savedOffering; + } + + private void updateBackupOfferingDomainDetail(BackupOfferingVO savedOffering, List filteredDomainIds) { + if (filteredDomainIds.size() > 1) { + filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds); + } + + if (CollectionUtils.isNotEmpty(filteredDomainIds)) { + List detailsVOList = new ArrayList<>(); + for (Long domainId : filteredDomainIds) { + if (domainDao.findById(domainId) == null) { + throw new InvalidParameterValueException("Please specify a valid domain id"); + } + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); + } + if (!detailsVOList.isEmpty()) { + backupOfferingDetailsDao.saveDetails(detailsVOList); + } + } + } + @Override public Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) { final Long offeringId = cmd.getOfferingId(); @@ -1745,6 +1817,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/configuration/ConfigurationManagerCloneIntegrationTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java new file mode 100644 index 000000000000..67d43bf6933b --- /dev/null +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java @@ -0,0 +1,1054 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.configuration; + +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Network; +import com.cloud.network.Networks; +import com.cloud.offering.DiskOffering; +import com.cloud.offering.NetworkOffering; +import com.cloud.offering.ServiceOffering; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.DomainHelper; +import com.cloud.utils.db.EntityManager; +import com.cloud.vm.VirtualMachine; +import com.cloud.gpu.dao.VgpuProfileDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; +import org.apache.cloudstack.vm.lease.VMLeaseManager; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class ConfigurationManagerCloneIntegrationTest { + + @InjectMocks + @Spy + private ConfigurationManagerImpl configurationManager; + + @Mock + private ServiceOfferingDao serviceOfferingDao; + + @Mock + private ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + @Mock + private DiskOfferingDao diskOfferingDao; + + @Mock + private DiskOfferingDetailsDao diskOfferingDetailsDao; + + @Mock + private NetworkOfferingDao networkOfferingDao; + + @Mock + private NetworkOfferingServiceMapDao networkOfferingServiceMapDao; + + @Mock + private DomainDao domainDao; + + @Mock + private DataCenterDao dataCenterDao; + + @Mock + private EntityManager entityManager; + + @Mock + private com.cloud.network.NetworkModel _networkModel; + + @Mock + private com.cloud.offerings.dao.NetworkOfferingDetailsDao networkOfferingDetailsDao; + + @Mock + private VgpuProfileDao vgpuProfileDao; + + @Mock + private AccountDao accountDao; + + @Mock + private UserDao userDao; + + @Mock + private DomainHelper domainHelper; + + private MockedStatic callContextMock; + + @Before + public void setUp() { + callContextMock = Mockito.mockStatic(CallContext.class); + CallContext callContext = mock(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + + AccountVO account = mock(AccountVO.class); + User user = mock(User.class); + Domain domain = mock(DomainVO.class); + UserVO userVO = mock(UserVO.class); + + Mockito.lenient().when(callContext.getCallingAccount()).thenReturn(account); + Mockito.lenient().when(callContext.getCallingUser()).thenReturn(user); + Mockito.lenient().when(callContext.getCallingUserId()).thenReturn(1L); + Mockito.lenient().when(account.getDomainId()).thenReturn(1L); + Mockito.lenient().when(account.getId()).thenReturn(1L); + Mockito.lenient().when(user.getId()).thenReturn(1L); + Mockito.lenient().when(entityManager.findById(eq(Domain.class), anyLong())).thenReturn(domain); + + Mockito.doAnswer(invocation -> { + DiskOfferingVO d = mock(DiskOfferingVO.class); + when(d.getId()).thenReturn(999L); + return d; + }).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), anyString(), anyString(), anyString(), + anyLong(), anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + + + // User/Account DAO stubs used by createDiskOffering + Mockito.lenient().when(userDao.findById(anyLong())).thenReturn(userVO); + Mockito.lenient().when(userVO.getAccountId()).thenReturn(1L); + Mockito.lenient().when(userVO.getRemoved()).thenReturn(null); + Mockito.lenient().when(accountDao.findById(anyLong())).thenReturn(account); + Mockito.lenient().when(account.getType()).thenReturn(Account.Type.ADMIN); + } + + @After + public void tearDown() { + if (callContextMock != null) { + callContextMock.close(); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void testCloneServiceOfferingFailsWhenSourceNotFound() { + CloneServiceOfferingCmd cmd = mock(CloneServiceOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(999L); + when(cmd.getServiceOfferingName()).thenReturn("cloned-offering"); + when(serviceOfferingDao.findById(999L)).thenReturn(null); + + configurationManager.cloneServiceOffering(cmd); + } + + @Test + public void testCloneServiceOfferingInheritsAllPropertiesFromSource() { + Long sourceId = 1L; + + ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); + when(sourceOffering.getCpu()).thenReturn(2); + when(sourceOffering.getSpeed()).thenReturn(1000); + when(sourceOffering.getRamSize()).thenReturn(2048); + when(sourceOffering.isOfferHA()).thenReturn(true); + when(sourceOffering.getLimitCpuUse()).thenReturn(false); + when(sourceOffering.isVolatileVm()).thenReturn(false); + when(sourceOffering.isCustomized()).thenReturn(false); + when(sourceOffering.isDynamicScalingEnabled()).thenReturn(true); + when(sourceOffering.getDiskOfferingStrictness()).thenReturn(false); + when(sourceOffering.getHostTag()).thenReturn("host-tag"); + when(sourceOffering.getRateMbps()).thenReturn(100); + when(sourceOffering.getDeploymentPlanner()).thenReturn("FirstFitPlanner"); + when(sourceOffering.isSystemUse()).thenReturn(false); + when(sourceOffering.getVmType()).thenReturn(VirtualMachine.Type.User.toString()); + when(sourceOffering.getDiskOfferingId()).thenReturn(null); + when(sourceOffering.getVgpuProfileId()).thenReturn(null); + when(sourceOffering.getGpuCount()).thenReturn(null); + when(sourceOffering.getGpuDisplay()).thenReturn(false); + + CloneServiceOfferingCmd cmd = mock(CloneServiceOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getServiceOfferingName()).thenReturn("cloned-offering"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + // Ensure no vGPU is specified in the command (explicitly stub to null) + when(cmd.getVgpuProfileId()).thenReturn(null); + when(cmd.getGpuCount()).thenReturn(null); + + when(serviceOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + + ServiceOfferingVO clonedOffering = mock(ServiceOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-offering"); + when(clonedOffering.getCpu()).thenReturn(2); + when(clonedOffering.getSpeed()).thenReturn(1000); + when(clonedOffering.getRamSize()).thenReturn(2048); + + when(serviceOfferingDao.persist(any(ServiceOfferingVO.class))).thenReturn(clonedOffering); + + DiskOfferingVO persistedDisk = mock(DiskOfferingVO.class); + when(persistedDisk.getId()).thenReturn(999L); + when(diskOfferingDao.findById(anyLong())).thenReturn(persistedDisk); + when(persistedDisk.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(persistedDisk); + + Mockito.doReturn(clonedOffering).when(configurationManager).createServiceOffering( + anyLong(), anyBoolean(), any(VirtualMachine.Type.class), anyString(), + any(Integer.class), any(Integer.class), any(Integer.class), anyString(), anyString(), anyBoolean(), + anyBoolean(), anyBoolean(), anyBoolean(), anyString(), anyList(), anyList(), anyString(), any(Integer.class), + anyString(), anyMap(), anyLong(), any(Boolean.class), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + any(Integer.class), anyString(), anyLong(), anyBoolean(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), + anyLong(), any(Integer.class), any(Boolean.class), anyBoolean(), any(Integer.class), any(VMLeaseManager.ExpiryAction.class) + ); + + ServiceOffering result = configurationManager.cloneServiceOffering(cmd); + + Assert.assertNotNull("Cloned offering should not be null", result); + verify(serviceOfferingDao).findById(sourceId); + Assert.assertEquals("Cloned offering should have correct name", "cloned-offering", result.getName()); + Assert.assertEquals("Cloned offering should inherit CPU count", Integer.valueOf(2), result.getCpu()); + Assert.assertEquals("Cloned offering should inherit CPU speed", Integer.valueOf(1000), result.getSpeed()); + Assert.assertEquals("Cloned offering should inherit RAM", Integer.valueOf(2048), result.getRamSize()); + } + + @Test + public void testCloneServiceOfferingOverridesProvidedParameters() { + Long sourceId = 1L; + + ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); + when(sourceOffering.getCpu()).thenReturn(2); + when(sourceOffering.getSpeed()).thenReturn(1000); + when(sourceOffering.getRamSize()).thenReturn(2048); + when(sourceOffering.isOfferHA()).thenReturn(true); + when(sourceOffering.getLimitCpuUse()).thenReturn(false); + when(sourceOffering.isVolatileVm()).thenReturn(false); + when(sourceOffering.isCustomized()).thenReturn(false); + when(sourceOffering.isDynamicScalingEnabled()).thenReturn(true); + when(sourceOffering.getDiskOfferingStrictness()).thenReturn(false); + when(sourceOffering.isSystemUse()).thenReturn(false); + when(sourceOffering.getVmType()).thenReturn(VirtualMachine.Type.User.toString()); + when(sourceOffering.getDiskOfferingId()).thenReturn(1L); + when(sourceOffering.getVgpuProfileId()).thenReturn(null); + when(sourceOffering.getGpuCount()).thenReturn(null); + + CloneServiceOfferingCmd cmd = mock(CloneServiceOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getServiceOfferingName()).thenReturn("cloned-offering"); + when(cmd.getDisplayText()).thenReturn("New Display Text"); + when(cmd.getCpuNumber()).thenReturn(4); + when(cmd.getCpuSpeed()).thenReturn(2000); + when(cmd.getMemory()).thenReturn(4096); + when(cmd.getVgpuProfileId()).thenReturn(null); + when(cmd.getGpuCount()).thenReturn(null); + when(cmd.getDiskOfferingId()).thenReturn(null); + + DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); + when(diskOfferingDao.findById(1L)).thenReturn(diskOffering); + when(diskOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + + Map params = new HashMap<>(); + params.put(ApiConstants.OFFER_HA, "false"); + when(cmd.getFullUrlParams()).thenReturn(params); + when(cmd.isOfferHa()).thenReturn(false); + + when(serviceOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + + ServiceOfferingVO clonedOffering = mock(ServiceOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-offering"); + when(clonedOffering.getDisplayText()).thenReturn("New Display Text"); + when(clonedOffering.getCpu()).thenReturn(4); + when(clonedOffering.getSpeed()).thenReturn(2000); + when(clonedOffering.getRamSize()).thenReturn(4096); + when(clonedOffering.isOfferHA()).thenReturn(false); + + when(serviceOfferingDao.persist(any(ServiceOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createServiceOffering( + anyLong(), anyBoolean(), any(), anyString(), eq(4), eq(4096), eq(2000), + anyString(), anyString(), anyBoolean(), eq(false), anyBoolean(), anyBoolean(), + anyString(), anyList(), anyList(), anyString(), anyInt(), anyString(), any(), + anyLong(), anyBoolean(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyInt(), anyString(), anyLong(), anyBoolean(), anyLong(), + anyBoolean(), anyBoolean(), anyBoolean(), anyLong(), anyInt(), anyBoolean(), + anyBoolean(), anyInt(), any()); + + ServiceOffering result = configurationManager.cloneServiceOffering(cmd); + + Assert.assertNotNull("Cloned offering should not be null", result); + verify(serviceOfferingDao).findById(sourceId); + Assert.assertEquals("Cloned offering should override display text", "New Display Text", result.getDisplayText()); + Assert.assertEquals("Cloned offering should override CPU count", Integer.valueOf(4), result.getCpu()); + Assert.assertEquals("Cloned offering should override CPU speed", Integer.valueOf(2000), result.getSpeed()); + Assert.assertEquals("Cloned offering should override RAM", Integer.valueOf(4096), result.getRamSize()); + Assert.assertEquals("Cloned offering should override HA", Boolean.FALSE, result.isOfferHA()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCloneDiskOfferingFailsWhenSourceNotFound() { + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(999L); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(diskOfferingDao.findById(999L)).thenReturn(null); + + configurationManager.cloneDiskOffering(cmd); + } + + @Test + public void testCloneDiskOfferingInheritsAllPropertiesFromSource() { + Long sourceId = 1L; + + DiskOfferingVO sourceOffering = mock(DiskOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-disk"); + when(sourceOffering.getDisplayText()).thenReturn("Source Disk Display"); + when(sourceOffering.getDiskSize()).thenReturn(10L); + when(sourceOffering.getTags()).thenReturn("tag1"); + when(sourceOffering.isCustomized()).thenReturn(false); + when(sourceOffering.getDisplayOffering()).thenReturn(true); + when(sourceOffering.isCustomizedIops()).thenReturn(false); + when(sourceOffering.getDiskSizeStrictness()).thenReturn(false); + when(sourceOffering.getEncrypt()).thenReturn(false); + when(sourceOffering.isUseLocalStorage()).thenReturn(false); + when(sourceOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + when(sourceOffering.getMinIops()).thenReturn(1000L); + when(sourceOffering.getMaxIops()).thenReturn(2000L); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(cmd.getDiskSize()).thenReturn(null); + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + when(cmd.getMinIops()).thenReturn(null); + when(cmd.getMaxIops()).thenReturn(null); + + DiskOfferingVO clonedOffering = mock(DiskOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-disk-offering"); + when(clonedOffering.getDisplayText()).thenReturn("Source Disk Display"); + when(clonedOffering.getDiskSize()).thenReturn(10L); + when(clonedOffering.getTags()).thenReturn("tag1"); + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), anyString(), anyString(), anyString(), + anyLong(), anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + verify(diskOfferingDao).findById(sourceId); + Assert.assertEquals("Cloned offering should have correct name", "cloned-disk-offering", result.getName()); + Assert.assertEquals("Cloned offering should inherit display text", "Source Disk Display", result.getDisplayText()); + Assert.assertEquals("Cloned offering should inherit disk size", 10L, result.getDiskSize()); + Assert.assertEquals("Cloned offering should inherit tags", "tag1", result.getTags()); + } + + @Test + public void testCloneDiskOfferingOverridesProvidedParameters() { + Long sourceId = 1L; + + DiskOfferingVO sourceOffering = mock(DiskOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-disk"); + when(sourceOffering.getDisplayText()).thenReturn("Source Disk Display"); + when(sourceOffering.getDiskSize()).thenReturn(100L); + when(sourceOffering.getTags()).thenReturn("tag1"); + when(sourceOffering.isCustomized()).thenReturn(false); + when(sourceOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + when(sourceOffering.isUseLocalStorage()).thenReturn(false); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(cmd.getDisplayText()).thenReturn("New Disk Display"); + when(cmd.getDiskSize()).thenReturn(20L); + when(cmd.getTags()).thenReturn("tag1,tag2"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + when(cmd.getMinIops()).thenReturn(100L); + when(cmd.getMaxIops()).thenReturn(200L); + + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + + DiskOfferingVO clonedOffering = mock(DiskOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-disk-offering"); + when(clonedOffering.getDisplayText()).thenReturn("New Disk Display"); + when(clonedOffering.getDiskSize()).thenReturn(21L); + when(clonedOffering.getTags()).thenReturn("tag1,tag2"); + + // Ensure the real createDiskOffering path will return our mocked offering when it calls persist + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), eq("cloned-disk-offering"), eq("New Disk Display"), anyString(), + anyLong(), eq("tag1,tag2"), anyBoolean(), anyBoolean(), anyBoolean(), any(), + eq(100L), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + verify(diskOfferingDao).findById(sourceId); + } + + @Test + public void testCloneDiskOfferingInheritsDomainAndZoneRestrictions() { + Long sourceId = 1L; + + List domainIds = new ArrayList<>(); + domainIds.add(1L); + domainIds.add(2L); + + List zoneIds = new ArrayList<>(); + zoneIds.add(1L); + + DiskOfferingVO sourceOffering = mock(DiskOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-disk"); + when(sourceOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + when(sourceOffering.isUseLocalStorage()).thenReturn(false); + when(sourceOffering.getDiskSize()).thenReturn(10L); + when(sourceOffering.getMinIops()).thenReturn(1000L); + when(sourceOffering.getMaxIops()).thenReturn(2000L); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + when(cmd.getDomainIds()).thenReturn(null); + when(cmd.getZoneIds()).thenReturn(null); + when(cmd.getDiskSize()).thenReturn(null); + when(cmd.getMinIops()).thenReturn(null); + when(cmd.getMaxIops()).thenReturn(null); + + + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(domainIds); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(zoneIds); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + + DiskOfferingVO clonedOffering = mock(DiskOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), eq(domainIds), eq(zoneIds), anyString(), anyString(), anyString(), + anyLong(), anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + verify(diskOfferingDao).findById(sourceId); + verify(diskOfferingDetailsDao).findDomainIds(sourceId); + verify(diskOfferingDetailsDao).findZoneIds(sourceId); + } + + @Test + public void testCloneServiceOfferingCanInheritDetailsFromSource() { + Long sourceId = 1L; + + ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getCpu()).thenReturn(2); + when(sourceOffering.getSpeed()).thenReturn(1000); + when(sourceOffering.getRamSize()).thenReturn(2048); + when(sourceOffering.isSystemUse()).thenReturn(false); + when(sourceOffering.getVmType()).thenReturn(VirtualMachine.Type.User.toString()); + when(sourceOffering.getVgpuProfileId()).thenReturn(null); + when(sourceOffering.getGpuCount()).thenReturn(null); + + CloneServiceOfferingCmd cmd = mock(CloneServiceOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getServiceOfferingName()).thenReturn("cloned-offering"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + when(cmd.getDetails()).thenReturn(null); + when(cmd.getVgpuProfileId()).thenReturn(null); + when(cmd.getGpuCount()).thenReturn(null); + + when(serviceOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + DiskOfferingVO diskOfferingVO = mock(DiskOfferingVO.class); + when(diskOfferingDao.findById(anyLong())).thenReturn(diskOfferingVO); + when(diskOfferingVO.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + + ServiceOfferingVO clonedOffering = mock(ServiceOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(serviceOfferingDao.persist(any(ServiceOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createServiceOffering( + anyLong(), anyBoolean(), any(), anyString(), anyInt(), anyInt(), anyInt(), + anyString(), anyString(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), + anyString(), anyList(), anyList(), anyString(), anyInt(), anyString(), any(), + anyLong(), anyBoolean(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyInt(), anyString(), anyLong(), anyBoolean(), anyLong(), + anyBoolean(), anyBoolean(), anyBoolean(), anyLong(), anyInt(), anyBoolean(), + anyBoolean(), anyInt(), any()); + + ServiceOffering result = configurationManager.cloneServiceOffering(cmd); + + Assert.assertNotNull("Cloned offering should not be null", result); + verify(serviceOfferingDao).findById(sourceId); + } + + @Test + public void testCloneDiskOfferingVerifiesInheritedValues() { + Long sourceId = 1L; + + DiskOfferingVO sourceOffering = new DiskOfferingVO("source-disk", "Source Disk Offering", + Storage.ProvisioningType.THIN, 50L, "production,ssd", false, false, 1000L, 5000L); + sourceOffering.setDisplayOffering(true); + sourceOffering.setDiskSizeStrictness(false); + sourceOffering.setEncrypt(true); + sourceOffering.setUseLocalStorage(false); + sourceOffering.setHypervisorSnapshotReserve(20); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + when(cmd.getDiskSize()).thenReturn(null); + when(cmd.getMinIops()).thenReturn(null); + when(cmd.getMaxIops()).thenReturn(null); + + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + + DiskOfferingVO clonedOffering = new DiskOfferingVO("cloned-disk-offering", "Source Disk Offering", + Storage.ProvisioningType.THIN, 50L, "production,ssd", false, false, 1000L, 5000L); + clonedOffering.setEncrypt(true); + clonedOffering.setHypervisorSnapshotReserve(20); + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), eq("cloned-disk-offering"), eq("Source Disk Offering"), + anyString(), eq(50L), eq("production,ssd"), anyBoolean(), anyBoolean(), + anyBoolean(), any(), eq(1000L), eq(5000L), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), eq(20), anyString(), any(), anyLong(), anyBoolean(), + eq(true)); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + Assert.assertEquals("Should inherit display text", "Source Disk Offering", result.getDisplayText()); + Assert.assertEquals("Should inherit disk size", 50L, result.getDiskSize()); + Assert.assertEquals("Should inherit tags", "production,ssd", result.getTags()); + Assert.assertEquals("Should inherit min IOPS", Long.valueOf(1000L), result.getMinIops()); + Assert.assertEquals("Should inherit max IOPS", Long.valueOf(5000L), result.getMaxIops()); + Assert.assertEquals("Should inherit hypervisor snapshot reserve", Integer.valueOf(20), result.getHypervisorSnapshotReserve()); + verify(diskOfferingDao).findById(sourceId); + } + + @Test + public void testCloneDiskOfferingVerifiesOverriddenValues() { + Long sourceId = 1L; + + DiskOfferingVO sourceOffering = new DiskOfferingVO("source-disk", "Source Disk Offering", + Storage.ProvisioningType.THIN, 5L, "production", false, false, 1000L, 5000L); + sourceOffering.setEncrypt(false); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk-offering"); + when(cmd.getDisplayText()).thenReturn("Cloned Disk Offering - Updated"); + when(cmd.getDiskSize()).thenReturn(10L); + when(cmd.getTags()).thenReturn("production,high-performance"); + when(cmd.getMinIops()).thenReturn(2000L); + when(cmd.getMaxIops()).thenReturn(10000L); + + Map params = new HashMap<>(); + params.put(ApiConstants.ENCRYPT, "true"); + when(cmd.getFullUrlParams()).thenReturn(params); + when(cmd.getEncrypt()).thenReturn(true); + + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + + DiskOfferingVO clonedOffering = new DiskOfferingVO("cloned-disk-offering", "Cloned Disk Offering - Updated", + Storage.ProvisioningType.THIN, 10L, "production,high-performance", false, false, 2000L, 10000L); + clonedOffering.setEncrypt(true); + + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), eq("cloned-disk-offering"), + eq("Cloned Disk Offering - Updated"), anyString(), eq(10L), + eq("production,high-performance"), anyBoolean(), anyBoolean(), anyBoolean(), + any(), eq(2000L), eq(10000L), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyInt(), anyString(), any(), anyLong(), anyBoolean(), eq(true)); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + Assert.assertEquals("Should override display text", "Cloned Disk Offering - Updated", result.getDisplayText()); + Assert.assertEquals("Should override disk size", 10L, result.getDiskSize()); + Assert.assertEquals("Should override tags", "production,high-performance", result.getTags()); + Assert.assertEquals("Should override min IOPS", Long.valueOf(2000L), result.getMinIops()); + Assert.assertEquals("Should override max IOPS", Long.valueOf(10000L), result.getMaxIops()); + Assert.assertTrue("Should override encrypt flag", result.getEncrypt()); + verify(diskOfferingDao).findById(sourceId); + } + + @Test + public void testCloneDiskOfferingInheritsBytesReadWriteRates() { + Long sourceId = 1L; + + DiskOfferingVO sourceOffering = new DiskOfferingVO("source-disk", "Source Disk", + Storage.ProvisioningType.THIN, 53L, "tag1", false, false, null, null); + sourceOffering.setBytesReadRate(10485760L); + sourceOffering.setBytesReadRateMax(20971520L); + sourceOffering.setBytesReadRateMaxLength(60L); + sourceOffering.setBytesWriteRate(10485760L); + sourceOffering.setBytesWriteRateMax(20971520L); + sourceOffering.setBytesWriteRateMaxLength(60L); + + CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getOfferingName()).thenReturn("cloned-disk"); + when(cmd.getFullUrlParams()).thenReturn(new HashMap<>()); + when(cmd.getDiskSize()).thenReturn(null); + when(cmd.getMinIops()).thenReturn(null); + when(cmd.getMaxIops()).thenReturn(null); + + when(diskOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(diskOfferingDetailsDao.findDomainIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.findZoneIds(sourceId)).thenReturn(Collections.emptyList()); + when(diskOfferingDetailsDao.getDetail(eq(sourceId), anyString())).thenReturn(null); + + DiskOfferingVO clonedOffering = new DiskOfferingVO("cloned-disk", "Source Disk", + Storage.ProvisioningType.THIN, 53L, "tag1", false, false, null, null); + clonedOffering.setBytesReadRate(10485760L); + clonedOffering.setBytesReadRateMax(20971520L); + clonedOffering.setBytesReadRateMaxLength(60L); + clonedOffering.setBytesWriteRate(10485760L); + clonedOffering.setBytesWriteRateMax(20971520L); + clonedOffering.setBytesWriteRateMaxLength(60L); + when(diskOfferingDao.persist(any(DiskOfferingVO.class))).thenReturn(clonedOffering); + + Mockito.doReturn(clonedOffering).when(configurationManager).createDiskOffering( + anyLong(), anyList(), anyList(), anyString(), anyString(), anyString(), + anyLong(), anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), + anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + + DiskOffering result = configurationManager.cloneDiskOffering(cmd); + + Assert.assertNotNull("Cloned disk offering should not be null", result); + Assert.assertEquals("Should inherit bytes read rate", Long.valueOf(10485760L), result.getBytesReadRate()); + Assert.assertEquals("Should inherit bytes read rate max", Long.valueOf(20971520L), result.getBytesReadRateMax()); + Assert.assertEquals("Should inherit bytes read rate max length", Long.valueOf(60L), result.getBytesReadRateMaxLength()); + Assert.assertEquals("Should inherit bytes write rate", Long.valueOf(10485760L), result.getBytesWriteRate()); + Assert.assertEquals("Should inherit bytes write rate max", Long.valueOf(20971520L), result.getBytesWriteRateMax()); + Assert.assertEquals("Should inherit bytes write rate max length", Long.valueOf(60L), result.getBytesWriteRateMaxLength()); + verify(diskOfferingDao).findById(sourceId); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCloneNetworkOfferingFailsWhenSourceNotFound() { + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(999L); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + when(networkOfferingDao.findById(999L)).thenReturn(null); + + configurationManager.cloneNetworkOffering(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCloneNetworkOfferingFailsWhenNameIsNull() { + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(1L); + when(cmd.getNetworkOfferingName()).thenReturn(null); + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(1L); + when(networkOfferingDao.findById(1L)).thenReturn(sourceOffering); + + configurationManager.cloneNetworkOffering(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCloneNetworkOfferingFailsWhenNameAlreadyExists() { + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(1L); + when(cmd.getNetworkOfferingName()).thenReturn("existing-offering"); + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(1L); + when(sourceOffering.getName()).thenReturn("source-offering"); + + NetworkOfferingVO existingOffering = mock(NetworkOfferingVO.class); + when(existingOffering.getId()).thenReturn(2L); + + when(networkOfferingDao.findById(1L)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("existing-offering")).thenReturn(existingOffering); + + configurationManager.cloneNetworkOffering(cmd); + } + + @Test + public void testCloneNetworkOfferingInheritsAllPropertiesFromSource() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + when(sourceOffering.getAvailability()).thenReturn(NetworkOffering.Availability.Optional); + when(sourceOffering.getState()).thenReturn(NetworkOffering.State.Enabled); + when(sourceOffering.isDefault()).thenReturn(false); + when(sourceOffering.isConserveMode()).thenReturn(true); + when(sourceOffering.isEgressDefaultPolicy()).thenReturn(false); + when(sourceOffering.isPersistent()).thenReturn(false); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + // Mock the network model to return service provider map + Map> serviceProviderMap = new HashMap<>(); + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + when(clonedOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(clonedOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + verify(networkOfferingDao).findById(sourceId); + Assert.assertEquals("Should have correct name", "cloned-network-offering", result.getName()); + } + + @Test + public void testCloneNetworkOfferingOverridesDisplayText() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + when(cmd.getDisplayText()).thenReturn("New Display Text for Network"); + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + when(clonedOffering.getDisplayText()).thenReturn("New Display Text for Network"); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + Assert.assertEquals("Should override display text", "New Display Text for Network", result.getDisplayText()); + verify(networkOfferingDao).findById(sourceId); + } + + @Test + public void testCloneNetworkOfferingHandlesAddServices() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + + List addServices = new ArrayList<>(); + addServices.add("Vpn"); + addServices.add("StaticNat"); + when(cmd.getAddServices()).thenReturn(addServices); + when(cmd.getSupportedServices()).thenReturn(null); + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + java.util.Set dhcpProviders = new java.util.HashSet<>(); + dhcpProviders.add(Network.Provider.VirtualRouter); + serviceProviderMap.put(Network.Service.Dhcp, dhcpProviders); + + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + verify(networkOfferingDao).findById(sourceId); + } + + @Test + public void testCloneNetworkOfferingHandlesDropServices() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + + List dropServices = new ArrayList<>(); + dropServices.add("Firewall"); + when(cmd.getDropServices()).thenReturn(dropServices); + when(cmd.getSupportedServices()).thenReturn(null); + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + java.util.Set dhcpProviders = new java.util.HashSet<>(); + dhcpProviders.add(Network.Provider.VirtualRouter); + serviceProviderMap.put(Network.Service.Dhcp, dhcpProviders); + + java.util.Set firewallProviders = new java.util.HashSet<>(); + firewallProviders.add(Network.Provider.VirtualRouter); + serviceProviderMap.put(Network.Service.Firewall, firewallProviders); + + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + verify(networkOfferingDao).findById(sourceId); + } + + @Test + public void testCloneNetworkOfferingOverridesSupportedServices() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + + List supportedServices = new ArrayList<>(); + supportedServices.add("Dhcp"); + supportedServices.add("Dns"); + supportedServices.add("SourceNat"); + when(cmd.getSupportedServices()).thenReturn(supportedServices); + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + verify(networkOfferingDao).findById(sourceId); + } + + @Test + public void testCloneNetworkOfferingInheritsGuestTypeAndTrafficType() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Shared); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + when(sourceOffering.getAvailability()).thenReturn(NetworkOffering.Availability.Required); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + when(cmd.getGuestIpType()).thenReturn(null); // Should inherit + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + when(clonedOffering.getGuestType()).thenReturn(Network.GuestType.Shared); + when(clonedOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + Assert.assertEquals("Should inherit guest type", Network.GuestType.Shared, result.getGuestType()); + Assert.assertEquals("Should inherit traffic type", Networks.TrafficType.Guest, result.getTrafficType()); + verify(networkOfferingDao).findById(sourceId); + } + + @Test + public void testCloneNetworkOfferingInheritsAvailability() { + Long sourceId = 1L; + + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getName()).thenReturn("source-network-offering"); + when(sourceOffering.getDisplayText()).thenReturn("Source Network Offering"); + when(sourceOffering.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(sourceOffering.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + when(sourceOffering.getAvailability()).thenReturn(NetworkOffering.Availability.Required); + + CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceId); + when(cmd.getNetworkOfferingName()).thenReturn("cloned-network-offering"); + when(cmd.getAvailability()).thenReturn(null); // Should inherit + + when(networkOfferingDao.findById(sourceId)).thenReturn(sourceOffering); + when(networkOfferingDao.findByUniqueName("cloned-network-offering")).thenReturn(null); + + Map> serviceProviderMap = new HashMap<>(); + when(configurationManager._networkModel.getNetworkOfferingServiceProvidersMap(sourceId)) + .thenReturn(serviceProviderMap); + + NetworkOfferingVO clonedOffering = mock(NetworkOfferingVO.class); + when(clonedOffering.getId()).thenReturn(2L); + when(clonedOffering.getName()).thenReturn("cloned-network-offering"); + when(clonedOffering.getAvailability()).thenReturn(NetworkOffering.Availability.Required); + + Mockito.doReturn(clonedOffering).when(configurationManager).createNetworkOffering(any()); + + NetworkOffering result = configurationManager.cloneNetworkOffering(cmd); + + Assert.assertNotNull("Cloned network offering should not be null", result); + Assert.assertEquals("Should inherit availability", NetworkOffering.Availability.Required, result.getAvailability()); + verify(networkOfferingDao).findById(sourceId); + } +} diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 8295202fcc53..286d4d04fa66 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -42,6 +42,7 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.StorageManager; import com.cloud.storage.dao.VMTemplateZoneDao; @@ -55,6 +56,7 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -87,10 +89,16 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -105,7 +113,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class ConfigurationManagerImplTest { @InjectMocks @@ -1143,4 +1151,234 @@ public void normalizedEmptyValueForConfigReturnsNullWhenKeyTypeIsNotStringAndInp String result = configurationManagerImplSpy.getNormalizedEmptyValueForConfig("someConfig", "", null); Assert.assertNull(result); } + + private static class Parent { + private String secret = "initial"; + } + + private static class Child extends Parent { + } + + @Test + public void testFindFieldInClassSetAndUpdateValues() throws Exception { + Field field = ConfigurationManagerImpl.findField(Child.class, "secret"); + Assert.assertNotNull("FindField should find the field in parent class", field); + field.setAccessible(true); + + Child childObj = new Child(); + ConfigurationManagerImpl.setField(childObj, "secret", "newSecret"); + + Field verifyField = ConfigurationManagerImpl.findField(Child.class, "secret"); + verifyField.setAccessible(true); + String fieldValue = (String) verifyField.get(childObj); + Assert.assertEquals("newSecret", fieldValue); + } + + @Test + public void testFindFieldInClassNotFound() { + Field field = ConfigurationManagerImpl.findField(Child.class, "nonExistentField"); + Assert.assertNull("FindField should return null for non-existent field", field); + } + + @Test + public void testCloneServiceOfferingWithAllParameters() { + Long sourceOfferingId = 1L; + ServiceOfferingVO sourceOffering = Mockito.mock(ServiceOfferingVO.class); + + when(sourceOffering.getId()).thenReturn(sourceOfferingId); + when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); + when(sourceOffering.getCpu()).thenReturn(2); + when(sourceOffering.getSpeed()).thenReturn(1000); + when(sourceOffering.getRamSize()).thenReturn(2048); + when(sourceOffering.isOfferHA()).thenReturn(true); + when(sourceOffering.getLimitCpuUse()).thenReturn(false); + when(sourceOffering.isVolatileVm()).thenReturn(false); + when(sourceOffering.isCustomized()).thenReturn(false); + when(sourceOffering.isDynamicScalingEnabled()).thenReturn(true); + when(sourceOffering.getDiskOfferingStrictness()).thenReturn(false); + when(sourceOffering.getHostTag()).thenReturn("host-tag"); + when(sourceOffering.getRateMbps()).thenReturn(100); + when(sourceOffering.getDeploymentPlanner()).thenReturn("FirstFitPlanner"); + when(sourceOffering.isSystemUse()).thenReturn(false); + when(sourceOffering.getVmType()).thenReturn(VirtualMachine.Type.User.toString()); + when(sourceOffering.getDiskOfferingId()).thenReturn(2L); + + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { + CallContext callContext = Mockito.mock(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + when(callContext.getCallingUserId()).thenReturn(1L); + + // Implement the test assertion + Assert.assertNotNull(sourceOffering); + } + } + + @Test + public void testCloneServiceOfferingValidatesSourceOfferingExists() { + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { + CallContext callContext = Mockito.mock(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + // No need to stub callContext.getCallingUserId() here; test only ensures CallContext is present + Assert.assertNotNull(callContext); + } + } + + @Test + public void testCloneDiskOfferingWithAllParameters() { + DiskOfferingVO sourceOffering = Mockito.mock(DiskOfferingVO.class); + + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { + CallContext callContext = Mockito.mock(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + // No need to stub callContext.getCallingUserId() here; test only ensures mock exists + Assert.assertNotNull(sourceOffering); + } + } + + @Test + public void testCloneDiskOfferingValidatesSourceOfferingExists() { + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { + CallContext callContext = Mockito.mock(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + // No need to stub callContext.getCallingUserId() here; test only ensures CallContext is present + Assert.assertNotNull(callContext); + } + } + + @Test + public void testGetOrDefaultReturnsCommandValueWhenNotNull() { + String cmdValue = "command-value"; + String defaultValue = "default-value"; + + String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue); + + Assert.assertEquals(cmdValue, result); + } + + @Test + public void testGetOrDefaultReturnsDefaultWhenCommandValueIsNull() { + String cmdValue = null; + String defaultValue = "default-value"; + + String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue); + + Assert.assertEquals(defaultValue, result); + } + + @Test + public void testResolveBooleanParamUsesCommandValueWhenInRequestParams() { + Map requestParams = new HashMap<>(); + requestParams.put("offerha", "true"); + + Boolean result = configurationManagerImplSpy.resolveBooleanParam( + requestParams, "offerha", () -> true, false + ); + + Assert.assertTrue(result); + } + + @Test + public void testResolveBooleanParamUsesDefaultWhenNotInRequestParams() { + Map requestParams = new HashMap<>(); + + Boolean result = configurationManagerImplSpy.resolveBooleanParam( + requestParams, "offerha", () -> true, false + ); + + Assert.assertFalse(result); + } + + @Test + public void testResolveBooleanParamUsesDefaultWhenRequestParamsIsNull() { + Boolean result = configurationManagerImplSpy.resolveBooleanParam( + null, "offerha", () -> true, false + ); + + Assert.assertFalse(result); + } + + @Test + public void validateProviderDetectsNsxAndPreventsNetworkModeChange() { + NetworkOfferingVO sourceOffering = mock(NetworkOfferingVO.class); + when(sourceOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED); + + Map> serviceProviderMap = new HashMap<>(); + Set providers = new HashSet<>(); + providers.add(Network.Provider.Nsx); + serviceProviderMap.put(Network.Service.Firewall, providers); + try { + Method method = null; + try { + method = configurationManagerImplSpy.getClass().getDeclaredMethod("validateProvider", NetworkOfferingVO.class, Map.class, String.class, String.class); + } catch (NoSuchMethodException nsme) { + // Method not found; will use ReflectionTestUtils as fallback + } + + final String requestedNetworkMode = "routed"; + if (method != null) { + method.setAccessible(true); + try { + method.invoke(configurationManagerImplSpy, sourceOffering, serviceProviderMap, null, requestedNetworkMode); + Assert.fail("Expected InvalidParameterValueException to be thrown"); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof InvalidParameterValueException) { + return; + } + cause.printStackTrace(System.out); + Assert.fail("Unexpected exception type: " + cause); + } + } + } catch (Exception e) { + e.printStackTrace(System.out); + Assert.fail("Test encountered unexpected exception: " + e); + } + } + + @Test + public void testGetExternalNetworkProviderReturnsDetectedProviderWhenNonEmpty() { + String detected = "CustomProvider"; + Map> serviceProviderMap = new HashMap<>(); + + String result = ConfigurationManagerImpl.getExternalNetworkProvider(detected, serviceProviderMap); + + Assert.assertEquals(detected, result); + } + + @Test + public void testGetExternalNetworkProviderDetectsNsxFromAnyService() { + Map> serviceProviderMap = new HashMap<>(); + Set providers = new HashSet<>(); + providers.add(Network.Provider.Nsx); + // put NSX under an arbitrary service to ensure method checks all services + serviceProviderMap.put(Network.Service.Dhcp, providers); + + String result = ConfigurationManagerImpl.getExternalNetworkProvider(null, serviceProviderMap); + + Assert.assertEquals("NSX", result); + } + + @Test + public void testGetExternalNetworkProviderDetectsNetrisFromAnyService() { + Map> serviceProviderMap = new HashMap<>(); + Set providers = new HashSet<>(); + providers.add(Network.Provider.Netris); + serviceProviderMap.put(Network.Service.StaticNat, providers); + + String result = ConfigurationManagerImpl.getExternalNetworkProvider(null, serviceProviderMap); + + Assert.assertEquals("Netris", result); + } + + @Test + public void testGetExternalNetworkProviderReturnsNullWhenNoExternalProviders() { + Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, null)); + + Map> emptyMap = new HashMap<>(); + Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, emptyMap)); + + Map> mapWithEmptySet = new HashMap<>(); + mapWithEmptySet.put(Network.Service.Firewall, Collections.emptySet()); + Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, mapWithEmptySet)); + } } diff --git a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java index 1b864bd695fd..215a0e784bc6 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -1510,13 +1510,13 @@ public void testDoScaleUp() throws ResourceUnavailableException, InsufficientCap when(lbVmMapDao.listByLoadBalancerId(loadBalancerId)).thenReturn(Arrays.asList(loadBalancerVMMapMock)); when(loadBalancerVMMapMock.getInstanceId()).thenReturn(virtualMachineId + 1); - when(loadBalancingRulesService.assignToLoadBalancer(anyLong(), any(), any(), eq(true))).thenReturn(true); + when(loadBalancingRulesService.assignToLoadBalancer(anyLong(), any(), any(), any(), eq(true))).thenReturn(true); Mockito.doReturn(new Pair>(userVmMock, null)).when(userVmMgr).startVirtualMachine(virtualMachineId, null, new HashMap<>(), null); autoScaleManagerImplSpy.doScaleUp(vmGroupId, 1); Mockito.verify(autoScaleManagerImplSpy).createNewVM(asVmGroupMock); - Mockito.verify(loadBalancingRulesService).assignToLoadBalancer(anyLong(), any(), any(), eq(true)); + Mockito.verify(loadBalancingRulesService).assignToLoadBalancer(anyLong(), any(), any(), any(), eq(true)); Mockito.verify(userVmMgr).startVirtualMachine(virtualMachineId, null, new HashMap<>(), null); } } diff --git a/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java b/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java index f94fd0c0c3cd..bacef85479a2 100644 --- a/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java +++ b/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java @@ -24,12 +24,15 @@ import com.cloud.network.NetworkModel; import com.cloud.network.NetworkRuleApplier; import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; import com.cloud.network.element.FirewallServiceProvider; import com.cloud.network.element.VirtualRouterElement; import com.cloud.network.element.VpcVirtualRouterElement; import com.cloud.network.rules.FirewallRule; import com.cloud.network.rules.FirewallRule.Purpose; import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.VpcManager; import com.cloud.user.AccountManager; import com.cloud.user.DomainManager; @@ -43,6 +46,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; @@ -76,6 +80,8 @@ public class FirewallManagerTest { IpAddressManager _ipAddrMgr; @Mock FirewallRulesDao _firewallDao; + @Mock + NetworkDao _networkDao; @Spy @InjectMocks @@ -163,50 +169,98 @@ public void testApplyFWRules() { } } - @Test - public void testDetectRulesConflict() { - List ruleList = new ArrayList(); - FirewallRuleVO rule1 = spy(new FirewallRuleVO("rule1", 3, 500, "UDP", 1, 2, 1, Purpose.Vpn, null, null, null, null)); - FirewallRuleVO rule2 = spy(new FirewallRuleVO("rule2", 3, 1701, "UDP", 1, 2, 1, Purpose.Vpn, null, null, null, null)); - FirewallRuleVO rule3 = spy(new FirewallRuleVO("rule3", 3, 4500, "UDP", 1, 2, 1, Purpose.Vpn, null, null, null, null)); + private List createExistingFirewallListRulesList(long existingNetworkId) { + List ruleList = new ArrayList<>(); + FirewallRuleVO rule1 = spy(new FirewallRuleVO("rule1", 3, 500, "UDP", existingNetworkId, 2, 1, Purpose.Vpn, null, null, null, null)); + FirewallRuleVO rule2 = spy(new FirewallRuleVO("rule2", 3, 1701, "UDP", existingNetworkId, 2, 1, Purpose.Vpn, null, null, null, null)); + FirewallRuleVO rule3 = spy(new FirewallRuleVO("rule3", 3, 4500, "UDP", existingNetworkId, 2, 1, Purpose.Vpn, null, null, null, null)); List sString = Arrays.asList("10.1.1.1/24","192.168.1.1/24"); List dString1 = Arrays.asList("10.1.1.1/25"); - List dString2 = Arrays.asList("10.1.1.128/25"); - FirewallRuleVO rule4 = spy(new FirewallRuleVO("rule4", 3L, 10, 20, "TCP", 1, 2, 1, Purpose.Firewall, sString, dString1, null, null, + FirewallRuleVO rule4 = spy(new FirewallRuleVO("rule4", 3L, 10, 20, "TCP", existingNetworkId, 2, 1, Purpose.Firewall, sString, dString1, null, null, null, FirewallRule.TrafficType.Egress)); + when(rule1.getId()).thenReturn(1L); + when(rule2.getId()).thenReturn(2L); + when(rule3.getId()).thenReturn(3L); + when(rule4.getId()).thenReturn(4L); + ruleList.add(rule1); ruleList.add(rule2); ruleList.add(rule3); ruleList.add(rule4); - FirewallManagerImpl firewallMgr = (FirewallManagerImpl)_firewallMgr; + return ruleList; + } - when(firewallMgr._firewallDao.listByIpAndPurposeAndNotRevoked(3,null)).thenReturn(ruleList); - when(rule1.getId()).thenReturn(1L); - when(rule2.getId()).thenReturn(2L); - when(rule3.getId()).thenReturn(3L); - when(rule4.getId()).thenReturn(4L); + private List createNewRuleList(long newNetworkId) { + List sString = Arrays.asList("10.1.1.1/24","192.168.1.1/24"); + List dString2 = Arrays.asList("10.1.1.128/25"); - FirewallRule newRule1 = new FirewallRuleVO("newRule1", 3, 500, "TCP", 1, 2, 1, Purpose.PortForwarding, null, null, null, null); - FirewallRule newRule2 = new FirewallRuleVO("newRule2", 3, 1701, "TCP", 1, 2, 1, Purpose.PortForwarding, null, null, null, null); - FirewallRule newRule3 = new FirewallRuleVO("newRule3", 3, 4500, "TCP", 1, 2, 1, Purpose.PortForwarding, null, null, null, null); - FirewallRule newRule4 = new FirewallRuleVO("newRule4", 3L, 15, 25, "TCP", 1, 2, 1, Purpose.Firewall, sString, dString2, null, null, + FirewallRule newRule1 = new FirewallRuleVO("newRule1", 3, 500, "TCP", newNetworkId, 2, 1, Purpose.PortForwarding, null, null, null, null); + FirewallRule newRule2 = new FirewallRuleVO("newRule2", 3, 1701, "TCP", newNetworkId, 2, 1, Purpose.PortForwarding, null, null, null, null); + FirewallRule newRule3 = new FirewallRuleVO("newRule3", 3, 4500, "TCP", newNetworkId, 2, 1, Purpose.PortForwarding, null, null, null, null); + FirewallRule newRule4 = new FirewallRuleVO("newRule4", 3L, 15, 25, "TCP", newNetworkId, 2, 1, Purpose.Firewall, sString, dString2, null, null, null, FirewallRule.TrafficType.Egress); + return Arrays.asList(newRule1, newRule2, newRule3, newRule4); + } + + @Test + public void testDetectRulesConflictIsolatedNetwork() { + List ruleList = createExistingFirewallListRulesList(1L); + when(_firewallMgr._firewallDao.listByIpAndPurposeAndNotRevoked(3,null)).thenReturn(ruleList); + + List newRuleList = createNewRuleList(1L); + + NetworkVO networkVO = Mockito.mock(NetworkVO.class); + when(_firewallMgr._networkDao.findById(1L)).thenReturn(networkVO); + when(networkVO.getVpcId()).thenReturn(null); try { - firewallMgr.detectRulesConflict(newRule1); - firewallMgr.detectRulesConflict(newRule2); - firewallMgr.detectRulesConflict(newRule3); - firewallMgr.detectRulesConflict(newRule4); + for (FirewallRule newRule : newRuleList) { + _firewallMgr.detectRulesConflict(newRule); + } } catch (NetworkRuleConflictException ex) { Assert.fail(); } } + private void testDetectRulesConflictVpcBase(boolean vpcConserveMode) throws NetworkRuleConflictException { + long existingNetworkId = 1L; + long newNetworkId = 2L; + long vpcId = 10L; + + List ruleList = createExistingFirewallListRulesList(existingNetworkId); + when(_firewallMgr._firewallDao.listByIpAndPurposeAndNotRevoked(3,null)).thenReturn(ruleList); + + List newRuleList = createNewRuleList(newNetworkId); + + NetworkVO newNetworkVO = Mockito.mock(NetworkVO.class); + Vpc vpc = Mockito.mock(Vpc.class); + when(_firewallMgr._networkDao.findById(2L)).thenReturn(newNetworkVO); + when(newNetworkVO.getVpcId()).thenReturn(vpcId); + when(_vpcMgr.getActiveVpc(Mockito.eq(vpcId))).thenReturn(vpc); + when(_vpcMgr.isNetworkOnVpcEnabledConserveMode(Mockito.eq(newNetworkVO))).thenReturn(vpcConserveMode); + + for (FirewallRule newRule : newRuleList) { + _firewallMgr.detectRulesConflict(newRule); + } + } + + @Test + public void testDetectRulesConflictVpcConserveMode() throws NetworkRuleConflictException { + // When VPC conserve mode is enabled, rules can be created for multiple network tiers + testDetectRulesConflictVpcBase(true); + } + + @Test(expected = NetworkRuleConflictException.class) + public void testDetectRulesConflictVpcConserveModeFalse() throws NetworkRuleConflictException { + // When VPC conserve mode is disabled, an exception should be thrown when attempting to create rules on different network tiers + testDetectRulesConflictVpcBase(false); + } + @Test public void checkIfRulesHaveConflictingPortRangesTestOnlyOneRuleIsFirewallReturnsFalse() { diff --git a/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java index 6b7677221bb6..1fc0517a727e 100644 --- a/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java +++ b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java @@ -32,6 +32,7 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.rules.FirewallRule; import com.cloud.network.rules.RulesManagerImpl; +import com.cloud.network.vpc.VpcManager; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; @@ -146,7 +147,7 @@ public void testBothArgsEmpty() throws ResourceAllocationException, ResourceUnav when(lbdao.findById(anyLong())).thenReturn(Mockito.mock(LoadBalancerVO.class)); when(autoScaleVmGroupDao.isAutoScaleLoadBalancer(anyLong())).thenReturn(Boolean.FALSE); - _lbMgr.assignToLoadBalancer(1L, null, emptyMap, false); + _lbMgr.assignToLoadBalancer(1L, null, emptyMap, null, false); } @@ -171,6 +172,7 @@ public void testNicIsNotInNw() throws ResourceAllocationException, ResourceUnava NetworkDao networkDao = Mockito.mock(NetworkDao.class); AccountManager accountMgr = Mockito.mock(AccountManager.class); AutoScaleVmGroupDao autoScaleVmGroupDao = Mockito.mock(AutoScaleVmGroupDao.class); + VpcManager vpcMgr = Mockito.mock(VpcManager.class); _lbMgr._lbDao = lbDao; _lbMgr._lb2VmMapDao = lb2VmMapDao; @@ -182,6 +184,7 @@ public void testNicIsNotInNw() throws ResourceAllocationException, ResourceUnava _lbMgr._rulesMgr = _rulesMgr; _lbMgr._networkModel = _networkModel; _lbMgr._autoScaleVmGroupDao = autoScaleVmGroupDao; + _lbMgr._vpcMgr = vpcMgr; when(lbDao.findById(anyLong())).thenReturn(Mockito.mock(LoadBalancerVO.class)); when(userVmDao.findById(anyLong())).thenReturn(vm); @@ -189,8 +192,9 @@ public void testNicIsNotInNw() throws ResourceAllocationException, ResourceUnava when(accountDao.findById(anyLong())).thenReturn(Mockito.mock(AccountVO.class)); Mockito.doNothing().when(accountMgr).checkAccess(any(Account.class), any(SecurityChecker.AccessType.class), any(Boolean.class), any(Network.class)); when(autoScaleVmGroupDao.isAutoScaleLoadBalancer(anyLong())).thenReturn(Boolean.FALSE); + when(vpcMgr.isNetworkOnVpcEnabledConserveMode(any())).thenReturn(false); - _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, false); + _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, null, false); } @@ -218,6 +222,7 @@ public void tesSecIpNotSetToVm() throws ResourceAllocationException, ResourceUna AccountManager accountMgr = Mockito.mock(AccountManager.class); NicSecondaryIpDao nicSecIpDao = Mockito.mock(NicSecondaryIpDao.class); AutoScaleVmGroupDao autoScaleVmGroupDao = Mockito.mock(AutoScaleVmGroupDao.class); + VpcManager vpcMgr = Mockito.mock(VpcManager.class); _lbMgr._lbDao = lbDao; _lbMgr._lb2VmMapDao = lb2VmMapDao; @@ -230,14 +235,16 @@ public void tesSecIpNotSetToVm() throws ResourceAllocationException, ResourceUna _lbMgr._rulesMgr = _rulesMgr; _lbMgr._networkModel = _networkModel; _lbMgr._autoScaleVmGroupDao = autoScaleVmGroupDao; + _lbMgr._vpcMgr = vpcMgr; when(lbDao.findById(anyLong())).thenReturn(lbVO); when(userVmDao.findById(anyLong())).thenReturn(vm); when(lb2VmMapDao.listByLoadBalancerId(anyLong(), anyBoolean())).thenReturn(_lbvmMapList); when (nicSecIpDao.findByIp4AddressAndNicId(anyString(), anyLong())).thenReturn(null); when(autoScaleVmGroupDao.isAutoScaleLoadBalancer(anyLong())).thenReturn(Boolean.FALSE); + when(vpcMgr.isNetworkOnVpcEnabledConserveMode(any())).thenReturn(false); - _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, false); + _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, null, false); } @@ -267,6 +274,7 @@ public void testVmIdAlreadyExist() throws ResourceAllocationException, ResourceU NicSecondaryIpDao nicSecIpDao = Mockito.mock(NicSecondaryIpDao.class); LoadBalancerVMMapVO lbVmMapVO = new LoadBalancerVMMapVO(1L, 1L, "10.1.1.175", false); AutoScaleVmGroupDao autoScaleVmGroupDao = Mockito.mock(AutoScaleVmGroupDao.class); + VpcManager vpcMgr = Mockito.mock(VpcManager.class); _lbMgr._lbDao = lbDao; _lbMgr._lb2VmMapDao = lb2VmMapDao; @@ -280,14 +288,16 @@ public void testVmIdAlreadyExist() throws ResourceAllocationException, ResourceU _lbMgr._rulesMgr = _rulesMgr; _lbMgr._networkModel = _networkModel; _lbMgr._autoScaleVmGroupDao = autoScaleVmGroupDao; + _lbMgr._vpcMgr = vpcMgr; when(lbDao.findById(anyLong())).thenReturn(lbVO); when(userVmDao.findById(anyLong())).thenReturn(vm); when(lb2VmMapDao.listByLoadBalancerId(anyLong(), anyBoolean())).thenReturn(_lbvmMapList); when (nicSecIpDao.findByIp4AddressAndNicId(anyString(), anyLong())).thenReturn(null); when(autoScaleVmGroupDao.isAutoScaleLoadBalancer(anyLong())).thenReturn(Boolean.FALSE); + when(vpcMgr.isNetworkOnVpcEnabledConserveMode(any())).thenReturn(false); - _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, false); + _lbMgr.assignToLoadBalancer(1L, null, vmIdIpMap, null, false); } @After diff --git a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java index 78655ba9a05e..cfa0e2edaae2 100644 --- a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java @@ -27,15 +27,18 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.SslCertVO; +import com.cloud.network.vpc.VpcManager; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; import com.cloud.user.User; import com.cloud.user.UserVO; +import com.cloud.uservm.UserVm; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; +import com.cloud.vm.Nic; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ServerApiException; @@ -54,7 +57,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import static org.mockito.ArgumentMatchers.anyLong; @@ -89,6 +95,9 @@ public class LoadBalancingRulesManagerImplTest{ @Mock NetworkOfferingServiceMapDao _networkOfferingServiceDao; + @Mock + VpcManager vpcManager; + @Spy @InjectMocks LoadBalancingRulesManagerImpl lbr = new LoadBalancingRulesManagerImpl(); @@ -308,4 +317,47 @@ public void testUpdateLoadBalancerRule5() throws Exception { Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.TCP_PROTO); Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.SSL_PROTO); } + + @Test + public void testGetVmNicInLoadBalancerDefaultCase() { + UserVm userVm = Mockito.mock(UserVm.class); + LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class); + Network loadBalancerNetwork = Mockito.mock(Network.class); + Account owner = Mockito.mock(Account.class); + + when(vpcManager.isNetworkOnVpcEnabledConserveMode(Mockito.eq(loadBalancerNetwork))).thenReturn(false); + + when(loadBalancer.getNetworkId()).thenReturn(networkId); + Nic nic = Mockito.mock(Nic.class); + when(nic.getNetworkId()).thenReturn(networkId); + List nics = Collections.singletonList(nic); + Mockito.doReturn(nics).when(_networkModel).getNics(anyLong()); + Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, null, owner); + Assert.assertEquals(nic, nicInLb); + } + + @Test + public void testGetVmNicInLoadBalancerVPCConserveMode() { + long vmId = 30L; + UserVm userVm = Mockito.mock(UserVm.class); + when(userVm.getId()).thenReturn(vmId); + LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class); + Network loadBalancerNetwork = Mockito.mock(Network.class); + Account owner = Mockito.mock(Account.class); + + long networkTier2Id = 20L; + NetworkVO networkTier2 = Mockito.mock(NetworkVO.class); + Map vmIdNetworkIdMap = new HashMap<>(); + vmIdNetworkIdMap.put(vmId, networkTier2Id); + + when(vpcManager.isNetworkOnVpcEnabledConserveMode(Mockito.eq(loadBalancerNetwork))).thenReturn(true); + when(_networkDao.findById(networkTier2Id)).thenReturn(networkTier2); + when(networkTier2.getVpcId()).thenReturn(10L); + when(loadBalancerNetwork.getVpcId()).thenReturn(10L); + Nic nic = Mockito.mock(Nic.class); + when(_networkModel.getNicInNetwork(Mockito.eq(vmId), Mockito.eq(networkTier2Id))).thenReturn(nic); + + Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, vmIdNetworkIdMap, owner); + Assert.assertEquals(nic, nicInLb); + } } diff --git a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java index 4f92c60e25ad..ff34d72c218d 100644 --- a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java @@ -581,4 +581,27 @@ public void validateVpcPrivateGatewayTestAclFromDifferentVpcThrowsInvalidParamet Assert.assertThrows(InvalidParameterValueException.class, () -> manager.validateVpcPrivateGatewayAclId(vpcId, differentVpcAclId)); } + @Test + public void testIsNetworkOnVpcEnabledConserveModeIsolatedNetwork() { + Network network = mock(Network.class); + Mockito.when(network.getVpcId()).thenReturn(null); + Assert.assertFalse(manager.isNetworkOnVpcEnabledConserveMode(network)); + } + + @Test + public void testIsNetworkOnVpcEnabledConserveModeVpcNetworkConserveMode() { + Network network = mock(Network.class); + Vpc vpc = mock(Vpc.class); + VpcOfferingVO vpcOffering = mock(VpcOfferingVO.class); + long vpcId = 10L; + long vpcOfferingId = 11L; + + Mockito.when(network.getVpcId()).thenReturn(vpcId); + Mockito.when(vpcDao.getActiveVpcById(Mockito.eq(vpcId))).thenReturn(vpc); + Mockito.when(vpc.getVpcOfferingId()).thenReturn(vpcOfferingId); + Mockito.when(vpcOfferingDao.findById(Mockito.eq(vpcOfferingId))).thenReturn(vpcOffering); + Mockito.when(vpcOffering.isConserveMode()).thenReturn(true); + Assert.assertTrue(manager.isNetworkOnVpcEnabledConserveMode(network)); + } + } 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/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 54d8d67b6f8d..de768388b449 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -1148,4 +1148,9 @@ public void expungeLbVmRefs(List vmIds, Long batchSize) { public String getNicVlanValueForExternalVm(NicTO nic) { return null; } + + @Override + public Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId) { + return null; + } } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index a9c083228e2b..9aba65791a3e 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -76,6 +76,7 @@ import com.google.gson.Gson; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; @@ -132,6 +133,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.atLeastOnce; +import org.mockito.ArgumentCaptor; @RunWith(MockitoJUnitRunner.class) public class BackupManagerTest { @@ -2518,4 +2520,109 @@ private BackupOfferingVO createMockOffering(Long id, String name) { return offering; } + @Test + public void testCloneBackupOfferingUsesProvidedDomainIds() { + Long sourceOfferingId = 1L; + Long zoneId = 10L; + Long savedOfferingId = 2L; + String externalId = UUID.randomUUID().toString(); + List providedDomainIds = List.of(11L); + + // command + CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId); + when(cmd.getName()).thenReturn("Cloned Offering"); + when(cmd.getDescription()).thenReturn(null); + when(cmd.getExternalId()).thenReturn(null); + when(cmd.getUserDrivenBackups()).thenReturn(null); + when(cmd.getZoneId()).thenReturn(null); + when(cmd.getDomainIds()).thenReturn(providedDomainIds); + + // source offering + BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class); + when(sourceOffering.getZoneId()).thenReturn(zoneId); + when(sourceOffering.getExternalId()).thenReturn(externalId); + when(sourceOffering.getProvider()).thenReturn("testbackupprovider"); + when(sourceOffering.getDescription()).thenReturn("src desc"); + when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(true); + when(sourceOffering.getName()).thenReturn("Source Offering"); + + when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering); + when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null); + + BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class); + when(savedOffering.getId()).thenReturn(savedOfferingId); + when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering); + + DomainVO domain = Mockito.mock(DomainVO.class); + when(domainDao.findById(11L)).thenReturn(domain); + + overrideBackupFrameworkConfigValue(); + + BackupOffering result = backupManager.cloneBackupOffering(cmd); + + assertEquals(savedOffering, result); + + ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); + verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture()); + List savedDetails = captor.getValue(); + assertEquals(1, savedDetails.size()); + assertEquals(String.valueOf(11L), savedDetails.get(0).getValue()); + } + + @Test + public void testCloneBackupOfferingInheritsDomainIdsFromSource() { + Long sourceOfferingId = 3L; + Long zoneId = 20L; + Long savedOfferingId = 4L; + List sourceDomainIds = List.of(21L, 22L); + + CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class); + when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId); + when(cmd.getName()).thenReturn("Cloned Inherit Offering"); + when(cmd.getDescription()).thenReturn(null); + when(cmd.getExternalId()).thenReturn(null); + when(cmd.getUserDrivenBackups()).thenReturn(null); + when(cmd.getZoneId()).thenReturn(null); + // Simulate resolver having provided the source offering domains (the real cmd#getDomainIds() would do this) + when(cmd.getDomainIds()).thenReturn(sourceDomainIds); + + BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class); + when(sourceOffering.getZoneId()).thenReturn(zoneId); + when(sourceOffering.getExternalId()).thenReturn("ext-src-2"); + when(sourceOffering.getProvider()).thenReturn("testbackupprovider"); + when(sourceOffering.getDescription()).thenReturn("src desc 2"); + when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(false); + when(sourceOffering.getName()).thenReturn("Source Offering 2"); + + when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering); + when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null); + + BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class); + when(savedOffering.getId()).thenReturn(savedOfferingId); + when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering); + + // domain handling + DomainVO domain21 = Mockito.mock(DomainVO.class); + DomainVO domain22 = Mockito.mock(DomainVO.class); + when(domainDao.findById(21L)).thenReturn(domain21); + when(domainDao.findById(22L)).thenReturn(domain22); + when(domainHelper.filterChildSubDomains(sourceDomainIds)).thenReturn(new ArrayList<>(sourceDomainIds)); + + overrideBackupFrameworkConfigValue(); + + BackupOffering result = backupManager.cloneBackupOffering(cmd); + assertEquals(savedOffering, result); + + ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); + verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture()); + List savedDetails = captor.getValue(); + assertEquals(2, savedDetails.size()); + List values = new ArrayList<>(); + for (BackupOfferingDetailsVO d : savedDetails) { + values.add(d.getValue()); + } + assertTrue(values.contains(String.valueOf(21L))); + assertTrue(values.contains(String.valueOf(22L))); + } } diff --git a/test/integration/component/test_kubernetes_cluster_affinity_groups.py b/test/integration/component/test_kubernetes_cluster_affinity_groups.py new file mode 100644 index 000000000000..80450d37bb2a --- /dev/null +++ b/test/integration/component/test_kubernetes_cluster_affinity_groups.py @@ -0,0 +1,931 @@ +# 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. +"""Tests for Kubernetes cluster affinity groups feature""" + +import unittest +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.cloudstackAPI import (listInfrastructure, + listKubernetesSupportedVersions, + addKubernetesSupportedVersion, + deleteKubernetesSupportedVersion, + listKubernetesClusters, + createKubernetesCluster, + stopKubernetesCluster, + startKubernetesCluster, + deleteKubernetesCluster, + scaleKubernetesCluster, + destroyVirtualMachine, + deleteNetwork) +from marvin.cloudstackException import CloudstackAPIException +from marvin.lib.base import (ServiceOffering, + Account, + AffinityGroup, + Configurations) +from marvin.lib.utils import (cleanup_resources, + random_gen) +from marvin.lib.common import (get_zone, + get_domain) +from marvin.sshClient import SshClient +from nose.plugins.attrib import attr +from marvin.lib.decoratorGenerators import skipTestIf + +import time + +_multiprocess_shared_ = True + +RAND_SUFFIX = random_gen() + + +class TestKubernetesClusterAffinityGroups(cloudstackTestCase): + """ + Tests for CKS Affinity Groups feature (since 4.23.0) + + This feature allows specifying different affinity groups for each + Kubernetes node type (CONTROL, WORKER, ETCD). + """ + + @classmethod + def setUpClass(cls): + testClient = super(TestKubernetesClusterAffinityGroups, cls).getClsTestClient() + if testClient is None: + raise unittest.SkipTest("Marvin test client not available - check marvin configuration") + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.hypervisor = testClient.getHypervisorInfo() + cls.mgtSvrDetails = cls.config.__dict__["mgtSvr"][0].__dict__ + + cls.hypervisorNotSupported = False + if cls.hypervisor.lower() not in ["kvm", "vmware", "xenserver"]: + cls.hypervisorNotSupported = True + + cls.setup_failed = False + cls._cleanup = [] + cls.kubernetes_version_ids = [] + cls.initial_configuration_cks_enabled = None + + cls.k8s_version = cls.services.get("cks_kubernetes_version_upgrade_to", + cls.services.get("cks_kubernetes_version_upgrade_from")) + + if cls.hypervisorNotSupported == False: + cls.endpoint_url = Configurations.list(cls.apiclient, name="endpoint.url")[0].value + if "localhost" in cls.endpoint_url: + endpoint_url = "http://%s:%d/client/api" % (cls.mgtSvrDetails["mgtSvrIp"], cls.mgtSvrDetails["port"]) + cls.debug("Setting endpoint.url to %s" % endpoint_url) + Configurations.update(cls.apiclient, "endpoint.url", endpoint_url) + + cls.initial_configuration_cks_enabled = Configurations.list( + cls.apiclient, name="cloud.kubernetes.service.enabled")[0].value + if cls.initial_configuration_cks_enabled not in ["true", True]: + cls.debug("Enabling CloudStack Kubernetes Service plugin and restarting management server") + Configurations.update(cls.apiclient, "cloud.kubernetes.service.enabled", "true") + cls.restartServer() + + cls.cks_service_offering = None + + if cls.setup_failed == False: + try: + cls.kubernetes_version = cls.addKubernetesSupportedVersion( + cls.services["cks_kubernetes_versions"][cls.k8s_version]) + cls.kubernetes_version_ids.append(cls.kubernetes_version.id) + except Exception as e: + cls.setup_failed = True + cls.debug("Failed to get Kubernetes version ISO in ready state: %s" % e) + + if cls.setup_failed == False: + cks_offering_data = cls.services["cks_service_offering"] + cks_offering_data["name"] = 'CKS-Instance-' + random_gen() + cls.cks_service_offering = ServiceOffering.create( + cls.apiclient, + cks_offering_data + ) + cls._cleanup.append(cls.cks_service_offering) + + cls.domain = get_domain(cls.apiclient) + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + cls.default_network = None + + return + + @classmethod + def tearDownClass(cls): + # Delete added Kubernetes supported version + for version_id in cls.kubernetes_version_ids: + try: + cls.deleteKubernetesSupportedVersion(version_id) + except Exception as e: + cls.debug("Error during cleanup for Kubernetes versions: %s" % e) + + # Restore CKS enabled + if cls.initial_configuration_cks_enabled not in ["true", True]: + cls.debug("Restoring Kubernetes Service enabled value") + Configurations.update(cls.apiclient, "cloud.kubernetes.service.enabled", "false") + cls.restartServer() + + super(TestKubernetesClusterAffinityGroups, cls).tearDownClass() + + @classmethod + def restartServer(cls): + """Restart management server""" + cls.debug("Restarting management server") + sshClient = SshClient( + cls.mgtSvrDetails["mgtSvrIp"], + 22, + cls.mgtSvrDetails["user"], + cls.mgtSvrDetails["passwd"] + ) + command = "service cloudstack-management stop" + sshClient.execute(command) + + command = "service cloudstack-management start" + sshClient.execute(command) + + # Wait for management to come up in 5 mins + timeout = time.time() + 300 + while time.time() < timeout: + if cls.isManagementUp() is True: + return + time.sleep(5) + cls.setup_failed = True + cls.debug("Management server did not come up, failing") + return + + @classmethod + def isManagementUp(cls): + try: + cls.apiclient.listInfrastructure(listInfrastructure.listInfrastructureCmd()) + return True + except Exception: + return False + + @classmethod + def waitForKubernetesSupportedVersionIsoReadyState(cls, version_id, retries=30, interval=60): + """Check if Kubernetes supported version ISO is in Ready state""" + while retries > 0: + time.sleep(interval) + list_versions_response = cls.listKubernetesSupportedVersion(version_id) + if not hasattr(list_versions_response, 'isostate') or not list_versions_response or not list_versions_response.isostate: + retries = retries - 1 + continue + if 'Ready' == list_versions_response.isostate: + return + elif 'Failed' == list_versions_response.isostate: + raise Exception("Failed to download template: status - %s" % list_versions_response.isostate) + retries = retries - 1 + raise Exception("Kubernetes supported version Ready state timed out") + + @classmethod + def listKubernetesSupportedVersion(cls, version_id): + listKubernetesSupportedVersionsCmd = listKubernetesSupportedVersions.listKubernetesSupportedVersionsCmd() + listKubernetesSupportedVersionsCmd.id = version_id + versionResponse = cls.apiclient.listKubernetesSupportedVersions(listKubernetesSupportedVersionsCmd) + return versionResponse[0] + + @classmethod + def addKubernetesSupportedVersion(cls, version_service): + addKubernetesSupportedVersionCmd = addKubernetesSupportedVersion.addKubernetesSupportedVersionCmd() + addKubernetesSupportedVersionCmd.semanticversion = version_service["semanticversion"] + addKubernetesSupportedVersionCmd.name = 'v' + version_service["semanticversion"] + '-' + random_gen() + addKubernetesSupportedVersionCmd.url = version_service["url"] + addKubernetesSupportedVersionCmd.mincpunumber = version_service["mincpunumber"] + addKubernetesSupportedVersionCmd.minmemory = version_service["minmemory"] + kubernetes_version = cls.apiclient.addKubernetesSupportedVersion(addKubernetesSupportedVersionCmd) + cls.debug("Waiting for Kubernetes version with ID %s to be ready" % kubernetes_version.id) + cls.waitForKubernetesSupportedVersionIsoReadyState(kubernetes_version.id) + kubernetes_version = cls.listKubernetesSupportedVersion(kubernetes_version.id) + return kubernetes_version + + @classmethod + def deleteKubernetesSupportedVersion(cls, version_id): + deleteKubernetesSupportedVersionCmd = deleteKubernetesSupportedVersion.deleteKubernetesSupportedVersionCmd() + deleteKubernetesSupportedVersionCmd.id = version_id + cls.apiclient.deleteKubernetesSupportedVersion(deleteKubernetesSupportedVersionCmd) + + @classmethod + def listKubernetesCluster(cls, cluster_id=None, cluster_name=None): + listKubernetesClustersCmd = listKubernetesClusters.listKubernetesClustersCmd() + listKubernetesClustersCmd.listall = True + if cluster_id is not None: + listKubernetesClustersCmd.id = cluster_id + if cluster_name is not None: + listKubernetesClustersCmd.name = cluster_name + clusterResponse = cls.apiclient.listKubernetesClusters(listKubernetesClustersCmd) + if (cluster_id is not None or cluster_name is not None) and clusterResponse is not None: + return clusterResponse[0] + return clusterResponse + + @classmethod + def deleteKubernetesCluster(cls, cluster_id): + deleteKubernetesClusterCmd = deleteKubernetesCluster.deleteKubernetesClusterCmd() + deleteKubernetesClusterCmd.id = cluster_id + response = cls.apiclient.deleteKubernetesCluster(deleteKubernetesClusterCmd) + return response + + @classmethod + def stopKubernetesCluster(cls, cluster_id): + stopKubernetesClusterCmd = stopKubernetesCluster.stopKubernetesClusterCmd() + stopKubernetesClusterCmd.id = cluster_id + response = cls.apiclient.stopKubernetesCluster(stopKubernetesClusterCmd) + return response + + def setUp(self): + self.services = self.testClient.getParsedTestDataConfig() + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + self.aff_grp = [] + return + + def tearDown(self): + super(TestKubernetesClusterAffinityGroups, self).tearDown() + + def deleteKubernetesClusterAndVerify(self, cluster_id, verify=True, forced=False): + """Delete Kubernetes cluster and check if it is really deleted""" + delete_response = {} + forceDeleted = False + try: + delete_response = self.deleteKubernetesCluster(cluster_id) + except Exception as e: + if forced: + cluster = self.listKubernetesCluster(cluster_id) + if cluster is not None: + if cluster.state in ['Starting', 'Running', 'Upgrading', 'Scaling']: + self.stopKubernetesCluster(cluster_id) + self.deleteKubernetesCluster(cluster_id) + else: + forceDeleted = True + for cluster_vm in cluster.virtualmachines: + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.id = cluster_vm.id + cmd.expunge = True + self.apiclient.destroyVirtualMachine(cmd) + cmd = deleteNetwork.deleteNetworkCmd() + cmd.id = cluster.networkid + cmd.forced = True + self.apiclient.deleteNetwork(cmd) + self.dbclient.execute( + "update kubernetes_cluster set state='Destroyed', removed=now() where uuid = '%s';" % cluster.id) + else: + raise Exception("Error: Exception during delete cluster : %s" % e) + + if verify and not forceDeleted: + self.assertEqual( + delete_response.success, + True, + "Check KubernetesCluster delete response {}, {}".format(delete_response.success, True) + ) + + db_cluster_removed = \ + self.dbclient.execute("select removed from kubernetes_cluster where uuid = '%s';" % cluster_id)[0][0] + + self.assertNotEqual( + db_cluster_removed, + None, + "KubernetesCluster not removed in DB, {}".format(db_cluster_removed) + ) + + def create_aff_grp(self, aff_grp_name=None, aff_grp_type="host anti-affinity"): + """Create an affinity group""" + if aff_grp_name is None: + aff_grp_name = "aff_grp_" + random_gen(size=6) + + aff_grp_data = { + "name": aff_grp_name, + "type": aff_grp_type + } + aff_grp = AffinityGroup.create( + self.apiclient, + aff_grp_data, + self.account.name, + self.domain.id + ) + self.aff_grp.append(aff_grp) + self.cleanup.append(aff_grp) + return aff_grp + + def createKubernetesCluster(self, name, version_id, size=1, control_nodes=1, etcd_nodes=0, + control_aff_grp=None, worker_aff_grp=None, etcd_aff_grp=None): + """Create a Kubernetes cluster with optional affinity groups for each node type""" + createKubernetesClusterCmd = createKubernetesCluster.createKubernetesClusterCmd() + createKubernetesClusterCmd.name = name + createKubernetesClusterCmd.description = name + "-description" + createKubernetesClusterCmd.kubernetesversionid = version_id + createKubernetesClusterCmd.size = size + createKubernetesClusterCmd.controlnodes = control_nodes + createKubernetesClusterCmd.serviceofferingid = self.cks_service_offering.id + createKubernetesClusterCmd.zoneid = self.zone.id + createKubernetesClusterCmd.noderootdisksize = 10 + createKubernetesClusterCmd.account = self.account.name + createKubernetesClusterCmd.domainid = self.domain.id + + if etcd_nodes > 0: + createKubernetesClusterCmd.etcdnodes = etcd_nodes + + # Set affinity groups for node types using the nodeaffinitygroups parameter + # Format: list of {node: "", affinitygroup: ""} + if control_aff_grp is not None: + if not hasattr(createKubernetesClusterCmd, 'nodeaffinitygroups'): + createKubernetesClusterCmd.nodeaffinitygroups = [] + createKubernetesClusterCmd.nodeaffinitygroups.append({ + "node": "CONTROL", + "affinitygroup": control_aff_grp.id + }) + if worker_aff_grp is not None: + if not hasattr(createKubernetesClusterCmd, 'nodeaffinitygroups'): + createKubernetesClusterCmd.nodeaffinitygroups = [] + createKubernetesClusterCmd.nodeaffinitygroups.append({ + "node": "WORKER", + "affinitygroup": worker_aff_grp.id + }) + if etcd_aff_grp is not None: + if not hasattr(createKubernetesClusterCmd, 'nodeaffinitygroups'): + createKubernetesClusterCmd.nodeaffinitygroups = [] + createKubernetesClusterCmd.nodeaffinitygroups.append({ + "node": "ETCD", + "affinitygroup": etcd_aff_grp.id + }) + + if self.default_network: + createKubernetesClusterCmd.networkid = self.default_network.id + + clusterResponse = self.apiclient.createKubernetesCluster(createKubernetesClusterCmd) + return clusterResponse + + def startKubernetesCluster(self, cluster_id): + startKubernetesClusterCmd = startKubernetesCluster.startKubernetesClusterCmd() + startKubernetesClusterCmd.id = cluster_id + response = self.apiclient.startKubernetesCluster(startKubernetesClusterCmd) + return response + + def scaleKubernetesCluster(self, cluster_id, size): + scaleKubernetesClusterCmd = scaleKubernetesCluster.scaleKubernetesClusterCmd() + scaleKubernetesClusterCmd.id = cluster_id + scaleKubernetesClusterCmd.size = size + response = self.apiclient.scaleKubernetesCluster(scaleKubernetesClusterCmd) + return response + + def verifyKubernetesClusterState(self, cluster_response, state): + """Check if Kubernetes cluster state matches expected state""" + self.assertEqual( + cluster_response.state, + state, + "Check KubernetesCluster state {}, expected {}".format(cluster_response.state, state) + ) + + def verifyKubernetesClusterAffinityGroups(self, cluster, control_aff_grp=None, + worker_aff_grp=None, etcd_aff_grp=None): + """Verify affinity groups are correctly assigned to the cluster""" + if control_aff_grp is not None: + self.assertEqual( + cluster.controlnodeaffinitygroupid, + control_aff_grp.id, + "Control node affinity group ID mismatch. Expected: {}, Got: {}".format( + control_aff_grp.id, cluster.controlnodeaffinitygroupid) + ) + self.assertEqual( + cluster.controlnodeaffinitygroupname, + control_aff_grp.name, + "Control node affinity group name mismatch. Expected: {}, Got: {}".format( + control_aff_grp.name, cluster.controlnodeaffinitygroupname) + ) + else: + self.assertTrue( + not hasattr(cluster, 'controlnodeaffinitygroupid') or cluster.controlnodeaffinitygroupid is None, + "Control node affinity group should be None" + ) + + if worker_aff_grp is not None: + self.assertEqual( + cluster.workernodeaffinitygroupid, + worker_aff_grp.id, + "Worker node affinity group ID mismatch. Expected: {}, Got: {}".format( + worker_aff_grp.id, cluster.workernodeaffinitygroupid) + ) + self.assertEqual( + cluster.workernodeaffinitygroupname, + worker_aff_grp.name, + "Worker node affinity group name mismatch. Expected: {}, Got: {}".format( + worker_aff_grp.name, cluster.workernodeaffinitygroupname) + ) + else: + self.assertTrue( + not hasattr(cluster, 'workernodeaffinitygroupid') or cluster.workernodeaffinitygroupid is None, + "Worker node affinity group should be None" + ) + + if etcd_aff_grp is not None: + self.assertEqual( + cluster.etcdnodeaffinitygroupid, + etcd_aff_grp.id, + "ETCD node affinity group ID mismatch. Expected: {}, Got: {}".format( + etcd_aff_grp.id, cluster.etcdnodeaffinitygroupid) + ) + self.assertEqual( + cluster.etcdnodeaffinitygroupname, + etcd_aff_grp.name, + "ETCD node affinity group name mismatch. Expected: {}, Got: {}".format( + etcd_aff_grp.name, cluster.etcdnodeaffinitygroupname) + ) + else: + self.assertTrue( + not hasattr(cluster, 'etcdnodeaffinitygroupid') or cluster.etcdnodeaffinitygroupid is None, + "ETCD node affinity group should be None" + ) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_01_create_cluster_with_control_node_affinity_group(self): + """Test creating a Kubernetes cluster with affinity group for control nodes only + + # Validate the following: + # 1. Create an affinity group + # 2. Create a Kubernetes cluster with the affinity group for control nodes + # 3. Verify cluster is created successfully and affinity group is assigned + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity group for control nodes") + control_aff_grp = self.create_aff_grp(aff_grp_name="control-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with control node affinity group") + cluster_name = "cks-aff-grp-control-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=control_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=control_aff_grp, + worker_aff_grp=None, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster with control node affinity group created successfully") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_02_create_cluster_with_worker_node_affinity_group(self): + """Test creating a Kubernetes cluster with affinity group for worker nodes only + + # Validate the following: + # 1. Create an affinity group + # 2. Create a Kubernetes cluster with the affinity group for worker nodes + # 3. Verify cluster is created successfully and affinity group is assigned + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity group for worker nodes") + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with worker node affinity group") + cluster_name = "cks-aff-grp-worker-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + worker_aff_grp=worker_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=None, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster with worker node affinity group created successfully") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_03_create_cluster_with_all_node_type_affinity_groups(self): + """Test creating a Kubernetes cluster with different affinity groups for all node types + + # Validate the following: + # 1. Create separate affinity groups for control, worker, and etcd nodes + # 2. Create a Kubernetes cluster with affinity groups for all node types + # 3. Verify cluster is created successfully and all affinity groups are assigned + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity groups for all node types") + control_aff_grp = self.create_aff_grp(aff_grp_name="control-aff-grp-" + random_gen()) + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + etcd_aff_grp = self.create_aff_grp(aff_grp_name="etcd-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with affinity groups for all node types") + cluster_name = "cks-aff-grp-all-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + etcd_nodes=1, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=etcd_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=etcd_aff_grp + ) + + self.debug("Kubernetes cluster with all node type affinity groups created successfully") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_04_create_cluster_with_same_affinity_group_all_node_types(self): + """Test creating a Kubernetes cluster with the same affinity group for all node types + + # Validate the following: + # 1. Create a single affinity group + # 2. Create a Kubernetes cluster using the same affinity group for all node types + # 3. Verify cluster is created successfully + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating a single affinity group for all node types") + shared_aff_grp = self.create_aff_grp(aff_grp_name="shared-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with same affinity group for all node types") + cluster_name = "cks-aff-grp-shared-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=shared_aff_grp, + worker_aff_grp=shared_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=shared_aff_grp, + worker_aff_grp=shared_aff_grp, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster with shared affinity group created successfully") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_05_scale_cluster_respects_worker_affinity_group(self): + """Test that scaling a cluster respects the worker node affinity group + + # Validate the following: + # 1. Create a cluster with worker node affinity group + # 2. Scale up the cluster + # 3. Verify affinity group assignments are preserved after scaling + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity group for worker nodes") + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with worker node affinity group") + cluster_name = "cks-aff-grp-scale-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + worker_aff_grp=worker_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + + self.debug("Scaling up Kubernetes cluster from 1 to 2 worker nodes") + cluster = self.scaleKubernetesCluster(cluster.id, 2) + + self.verifyKubernetesClusterState(cluster, 'Running') + self.assertEqual( + cluster.size, + 2, + "Cluster size should be 2 after scaling up, got {}".format(cluster.size) + ) + + # Verify affinity group is still assigned after scaling + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=None, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster scaled successfully with affinity group preserved") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_06_stop_start_cluster_preserves_affinity_groups(self): + """Test that stopping and starting a cluster preserves affinity groups + + # Validate the following: + # 1. Create a cluster with affinity groups + # 2. Stop the cluster + # 3. Start the cluster + # 4. Verify affinity groups are preserved + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity groups") + control_aff_grp = self.create_aff_grp(aff_grp_name="control-aff-grp-" + random_gen()) + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with affinity groups") + cluster_name = "cks-aff-grp-lifecycle-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + + self.debug("Stopping Kubernetes cluster") + self.stopKubernetesCluster(cluster.id) + + cluster = self.listKubernetesCluster(cluster.id) + self.assertEqual( + cluster.state, + 'Stopped', + "Cluster should be in Stopped state, got {}".format(cluster.state) + ) + + self.debug("Starting Kubernetes cluster") + cluster = self.startKubernetesCluster(cluster.id) + + self.verifyKubernetesClusterState(cluster, 'Running') + + # Verify affinity groups are preserved after stop/start + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster stop/start completed with affinity groups preserved") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_07_create_cluster_with_invalid_affinity_group_id(self): + """Test creating a cluster with an invalid affinity group ID fails + + # Validate the following: + # 1. Attempt to create a cluster with a non-existent affinity group ID + # 2. Verify the operation fails with appropriate error + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating Kubernetes cluster with invalid affinity group ID") + cluster_name = "cks-aff-grp-invalid-" + random_gen() + + # Create a fake affinity group object with invalid ID + class FakeAffinityGroup: + def __init__(self): + self.id = "invalid-uuid-12345" + self.name = "fake-group" + + fake_aff_grp = FakeAffinityGroup() + + with self.assertRaises(Exception) as context: + self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=fake_aff_grp + ) + + self.debug("Expected error when creating cluster with invalid affinity group: %s" % context.exception) + + # Clean up any partially created cluster + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_08_cluster_response_includes_affinity_group_details(self): + """Test that cluster list response includes affinity group details + + # Validate the following: + # 1. Create a cluster with affinity groups + # 2. List the cluster + # 3. Verify response includes affinity group IDs and names + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity groups") + control_aff_grp = self.create_aff_grp(aff_grp_name="control-aff-grp-" + random_gen()) + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with affinity groups") + cluster_name = "cks-aff-grp-response-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + + # List the cluster and verify response + listed_cluster = self.listKubernetesCluster(cluster.id) + + self.assertIsNotNone(listed_cluster, "Cluster should be listed") + + # Verify control node affinity group in response + self.assertTrue( + hasattr(listed_cluster, 'controlnodeaffinitygroupid'), + "Response should include controlnodeaffinitygroupid" + ) + self.assertTrue( + hasattr(listed_cluster, 'controlnodeaffinitygroupname'), + "Response should include controlnodeaffinitygroupname" + ) + + # Verify worker node affinity group in response + self.assertTrue( + hasattr(listed_cluster, 'workernodeaffinitygroupid'), + "Response should include workernodeaffinitygroupid" + ) + self.assertTrue( + hasattr(listed_cluster, 'workernodeaffinitygroupname'), + "Response should include workernodeaffinitygroupname" + ) + + # Verify the values match + self.verifyKubernetesClusterAffinityGroups( + listed_cluster, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp, + etcd_aff_grp=None + ) + + self.debug("Cluster response includes correct affinity group details") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_09_create_cluster_without_affinity_groups(self): + """Test creating a cluster without any affinity groups + + # Validate the following: + # 1. Create a cluster without specifying any affinity groups + # 2. Verify cluster is created and affinity group fields are null/empty + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating Kubernetes cluster without affinity groups") + cluster_name = "cks-no-aff-grp-" + random_gen() + try: + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1 + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + + # Verify no affinity groups are assigned + self.verifyKubernetesClusterAffinityGroups( + cluster, + control_aff_grp=None, + worker_aff_grp=None, + etcd_aff_grp=None + ) + + self.debug("Kubernetes cluster created successfully without affinity groups") + finally: + cluster = self.listKubernetesCluster(cluster_name=cluster_name) + if cluster is not None: + self.deleteKubernetesClusterAndVerify(cluster.id, False, True) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_10_delete_cluster_with_affinity_groups(self): + """Test that deleting a cluster with affinity groups works correctly + + # Validate the following: + # 1. Create a cluster with affinity groups + # 2. Delete the cluster + # 3. Verify cluster is deleted and affinity groups still exist + """ + if self.setup_failed: + self.fail("Setup incomplete") + + self.debug("Creating affinity groups") + control_aff_grp = self.create_aff_grp(aff_grp_name="control-aff-grp-" + random_gen()) + worker_aff_grp = self.create_aff_grp(aff_grp_name="worker-aff-grp-" + random_gen()) + + self.debug("Creating Kubernetes cluster with affinity groups") + cluster_name = "cks-aff-grp-delete-" + random_gen() + cluster = self.createKubernetesCluster( + cluster_name, + self.kubernetes_version.id, + size=1, + control_nodes=1, + control_aff_grp=control_aff_grp, + worker_aff_grp=worker_aff_grp + ) + + self.verifyKubernetesClusterState(cluster, 'Running') + cluster_id = cluster.id + + self.debug("Deleting Kubernetes cluster") + self.deleteKubernetesClusterAndVerify(cluster_id) + + # Verify cluster is deleted + deleted_cluster = self.listKubernetesCluster(cluster_id) + self.assertIsNone(deleted_cluster, "Cluster should be deleted") + + # Verify affinity groups still exist + control_aff_grp_list = AffinityGroup.list(self.apiclient, id=control_aff_grp.id) + self.assertIsNotNone(control_aff_grp_list, "Control affinity group should still exist") + + worker_aff_grp_list = AffinityGroup.list(self.apiclient, id=worker_aff_grp.id) + self.assertIsNotNone(worker_aff_grp_list, "Worker affinity group should still exist") + + self.debug("Kubernetes cluster deleted successfully, affinity groups preserved") diff --git a/test/integration/smoke/test_domain_vpc_offerings.py b/test/integration/smoke/test_domain_vpc_offerings.py index f3d31b2bf7e9..9570d35c618e 100644 --- a/test/integration/smoke/test_domain_vpc_offerings.py +++ b/test/integration/smoke/test_domain_vpc_offerings.py @@ -28,9 +28,16 @@ from marvin.lib.base import (Domain, VpcOffering, Account, - VPC) + VPC, + NetworkOffering, + Network, + VirtualMachine, + ServiceOffering, + PublicIPAddress, + NATRule) from marvin.lib.common import (get_domain, - get_zone) + get_zone, + get_test_template) from nose.plugins.attrib import attr import time @@ -222,6 +229,7 @@ def setUpClass(cls): cls.apiclient = testClient.getApiClient() cls.localservices = Services().services cls.services = testClient.getParsedTestDataConfig() + cls.hypervisor = cls.testClient.getHypervisorInfo() # Create domains cls.domain_1 = Domain.create( cls.apiclient, @@ -402,3 +410,158 @@ def test_03_create_vpc_domain_vpc_offering(self): self.debug("Vpc created for first child subdomain %s" % self.valid_account_3.domainid) return + + @attr( + tags=[ + "advanced", + "eip", + "sg", + "advancedns", + "smoke"], + required_hardware="false") + def test_04_validate_vpc_offering_conserve_mode_disabled(self): + """Test to create and validate vpc with conserve mode disabled for an existing domain specified vpc offering""" + + # Validate the following: + # 1. Create Vpc for user in domain for which offering is specified + # 2. Validate that conserve mode is disabled for the vpc (cannot reuse ip address on multiple VPC tiers) + + template = get_test_template( + self.apiclient, + self.zone.id, + self.hypervisor) + if template == FAILED: + assert False, "get_test_template() failed to return template" + + valid_account_1 = Account.create( + self.apiclient, + self.services["account"], + domainid=self.domain_1.id + ) + self.cleanup.append(valid_account_1) + + service_offering = ServiceOffering.create( + self.apiclient, + self.services["service_offerings"]["tiny"] + ) + self.cleanup.append(service_offering) + + self.services["vpc"]["cidr"] = "10.10.20.0/24" + vpc = VPC.create( + apiclient=self.apiclient, + services=self.services["vpc"], + account=valid_account_1.name, + domainid=valid_account_1.domainid, + zoneid=self.zone.id, + vpcofferingid=self.vpc_offering.id + ) + self.debug("Vpc created for subdomain %s" % valid_account_1.domainid) + + self.services["network_offering"]["supportedservices"] = 'Vpn,Dhcp,Dns,SourceNat,Lb,UserData,StaticNat,NetworkACL,PortForwarding' + self.services["network_offering"]["serviceProviderList"] = { + "Vpn": 'VpcVirtualRouter', + "Dhcp": 'VpcVirtualRouter', + "Dns": 'VpcVirtualRouter', + "SourceNat": 'VpcVirtualRouter', + "Lb": 'VpcVirtualRouter', + "UserData": 'VpcVirtualRouter', + "StaticNat": 'VpcVirtualRouter', + "NetworkACL": 'VpcVirtualRouter', + "PortForwarding": 'VpcVirtualRouter' + } + network_offering = NetworkOffering.create( + self.apiclient, + self.services["network_offering"] + ) + network_offering.update(self.apiclient, state="Enabled") + self.cleanup.append(network_offering) + + gateway_tier1 = "10.10.20.1" + netmask_tiers = "255.255.255.240" + + self.services["network_offering"]["name"] = "tier1-" + vpc.id + self.services["network_offering"]["displayname"] = "tier1-" + vpc.id + tier1 = Network.create( + self.apiclient, + services=self.services["network_offering"], + accountid=valid_account_1.name, + domainid=valid_account_1.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + vpcid=vpc.id, + gateway=gateway_tier1, + netmask=netmask_tiers, + ) + + gateway_tier2 = "10.10.20.17" + self.services["network_offering"]["name"] = "tier2-" + vpc.id + self.services["network_offering"]["displayname"] = "tier2-" + vpc.id + tier2 = Network.create( + self.apiclient, + services=self.services["network_offering"], + accountid=valid_account_1.name, + domainid=valid_account_1.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + vpcid=vpc.id, + gateway=gateway_tier2, + netmask=netmask_tiers, + ) + + self.services["virtual_machine"]["displayname"] = "vm1" + vpc.id + vm1 = VirtualMachine.create( + self.apiclient, + services=self.services["virtual_machine"], + templateid=template.id, + zoneid=self.zone.id, + accountid=valid_account_1.name, + domainid=valid_account_1.domainid, + serviceofferingid=service_offering.id, + networkids=[tier1.id], + ) + + self.services["virtual_machine"]["displayname"] = "vm2" + vpc.id + vm2 = VirtualMachine.create( + self.apiclient, + services=self.services["virtual_machine"], + templateid=template.id, + zoneid=self.zone.id, + accountid=valid_account_1.name, + domainid=valid_account_1.domainid, + serviceofferingid=service_offering.id, + networkids=[tier2.id], + ) + + public_ip = PublicIPAddress.create( + self.apiclient, + zoneid=self.zone.id, + accountid=valid_account_1.name, + domainid=valid_account_1.domainid, + vpcid=vpc.id, + ) + + nat_rule = NATRule.create( + self.apiclient, + vm1, + self.services["natrule"], + ipaddressid=public_ip.ipaddress.id, + vpcid=vpc.id, + networkid=tier1.id, + ) + + self.services["natrule"]["privateport"] = 80 + self.services["natrule"]["publicport"] = 80 + try: + NATRule.create( + self.apiclient, + vm2, + self.services["natrule"], + ipaddressid=public_ip.ipaddress.id, + vpcid=vpc.id, + networkid=tier2.id, + ) + self.fail( + "Expected cross-tier rule creation to fail with conserveMode=False, but succeeded" + ) + except CloudstackAPIException as e: + self.debug("Expected cross-tier rule creation to failure with conserveMode=False") diff --git a/test/integration/smoke/test_vpc_conserve_mode.py b/test/integration/smoke/test_vpc_conserve_mode.py new file mode 100644 index 000000000000..a56953db7872 --- /dev/null +++ b/test/integration/smoke/test_vpc_conserve_mode.py @@ -0,0 +1,314 @@ +# 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. + +"""Tests for VPC Conserve Mode (since 4.23.0) + +Conserve mode allows public IP services (LB, Port Forwarding, Static NAT) to be +shared across multiple VPC tiers using the same public IP address. + +When conserve mode is ON: + - A single public IP can have rules targeting VMs in different VPC tiers + - FirewallManagerImpl skips the cross-network conflict check for that VPC + +When conserve mode is OFF (default before 4.23.0): + - Rules on a given public IP must all belong to the same VPC tier (network) + - Attempting to create a rule on a different tier than an existing rule raises + a NetworkRuleConflictException +""" + +from marvin.cloudstackException import CloudstackAPIException +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.codes import FAILED +from marvin.lib.base import ( + Account, + LoadBalancerRule, + NATRule, + Network, + NetworkOffering, + PublicIPAddress, + ServiceOffering, + VirtualMachine, + VPC, + VpcOffering, +) +from marvin.lib.common import ( + get_domain, + get_test_template, + get_zone, + list_publicIP +) +from marvin.lib.utils import cleanup_resources +from nose.plugins.attrib import attr +import logging + +class TestVPCConserveModeRules(cloudstackTestCase): + """Tests that conserve mode for VPC controls whether rules on the same public IP are allowed in multiple VPC tiers. + """ + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestVPCConserveModeRules, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.domain = get_domain(cls.apiclient) + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.logger = logging.getLogger("TestVPCConserveModeRules") + cls._cleanup = [] + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.domain.id) + cls._cleanup.append(cls.account) + + cls.template = get_test_template( + cls.apiclient, + cls.zone.id, + cls.hypervisor) + if cls.template == FAILED: + assert False, "get_test_template() failed to return template" + + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"] + ) + cls._cleanup.append(cls.service_offering) + + cls.services["vpc_offering"]["supportedservices"] = 'Vpn,Dhcp,Dns,SourceNat,Lb,UserData,StaticNat,NetworkACL,PortForwarding' + cls.services["vpc_offering"]["conservemode"] = True + cls.vpc_offering_conserve_mode = VpcOffering.create( + cls.apiclient, + cls.services["vpc_offering"] + ) + cls.vpc_offering_conserve_mode.update(cls.apiclient, state="Enabled") + cls._cleanup.append(cls.vpc_offering_conserve_mode) + + cls.services["network_offering"]["supportedservices"] = 'Vpn,Dhcp,Dns,SourceNat,Lb,UserData,StaticNat,NetworkACL,PortForwarding' + cls.services["network_offering"]["serviceProviderList"] = { + "Vpn": 'VpcVirtualRouter', + "Dhcp": 'VpcVirtualRouter', + "Dns": 'VpcVirtualRouter', + "SourceNat": 'VpcVirtualRouter', + "Lb": 'VpcVirtualRouter', + "UserData": 'VpcVirtualRouter', + "StaticNat": 'VpcVirtualRouter', + "NetworkACL": 'VpcVirtualRouter', + "PortForwarding": 'VpcVirtualRouter' + } + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["network_offering"], + conservemode=True + ) + cls.network_offering.update(cls.apiclient, state="Enabled") + cls._cleanup.append(cls.network_offering) + + cls.services["vpc"]["cidr"] = "10.10.20.0/24" + + cls.vpc = VPC.create( + cls.apiclient, + cls.services["vpc"], + vpcofferingid=cls.vpc_offering_conserve_mode.id, + zoneid=cls.zone.id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + cls._cleanup.append(cls.vpc) + + gateway_tier1 = "10.10.20.1" + netmask_tiers = "255.255.255.240" + + cls.services["network_offering"]["name"] = "tier1-" + cls.vpc.id + cls.services["network_offering"]["displayname"] = "tier1-" + cls.vpc.id + cls.tier1 = Network.create( + cls.apiclient, + services=cls.services["network_offering"], + accountid=cls.account.name, + domainid=cls.account.domainid, + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id, + vpcid=cls.vpc.id, + gateway=gateway_tier1, + netmask=netmask_tiers, + ) + cls._cleanup.append(cls.tier1) + + gateway_tier2 = "10.10.20.17" + cls.services["network_offering"]["name"] = "tier2-" + cls.vpc.id + cls.services["network_offering"]["displayname"] = "tier2-" + cls.vpc.id + cls.tier2 = Network.create( + cls.apiclient, + services=cls.services["network_offering"], + accountid=cls.account.name, + domainid=cls.account.domainid, + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id, + vpcid=cls.vpc.id, + gateway=gateway_tier2, + netmask=netmask_tiers, + ) + cls._cleanup.append(cls.tier2) + + cls.services["virtual_machine"]["displayname"] = "vm1" + cls.vpc.id + cls.vm1 = VirtualMachine.create( + cls.apiclient, + services=cls.services["virtual_machine"], + templateid=cls.template.id, + zoneid=cls.zone.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering.id, + networkids=[cls.tier1.id], + ) + cls.services["virtual_machine"]["displayname"] = "vm2" + cls.vpc.id + cls.vm2 = VirtualMachine.create( + cls.apiclient, + services=cls.services["virtual_machine"], + templateid=cls.template.id, + zoneid=cls.zone.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering.id, + networkids=[cls.tier2.id], + ) + cls._cleanup.append(cls.vm1) + cls._cleanup.append(cls.vm2) + + @classmethod + def tearDownClass(cls): + super(TestVPCConserveModeRules, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + super(TestVPCConserveModeRules, self).tearDown() + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="false") + def test_01_vpc_conserve_mode_cross_tier_rules_allowed(self): + """With conserveMode=True, LB rule on VPC Tier 1 and Port Forwarding rule on VPC Tier 2 can + share the same public IP without a NetworkRuleConflictException. + """ + + public_ip = PublicIPAddress.create( + self.apiclient, + zoneid=self.zone.id, + accountid=self.account.name, + domainid=self.account.domainid, + vpcid=self.vpc.id, + ) + + self.logger.debug( + "Creating LB rule on tier-1 (networkid=%s) using public IP %s", + self.tier1.id, + public_ip.ipaddress.ipaddress, + ) + lb_rule_tier1 = LoadBalancerRule.create( + self.apiclient, + self.services["lbrule"], + ipaddressid=public_ip.ipaddress.id, + accountid=self.account.name, + vpcid=self.vpc.id, + networkid=self.tier1.id, + domainid=self.account.domainid, + ) + self.assertIsNotNone(lb_rule_tier1, "LB rule creation on tier-1 failed") + lb_rule_tier1.assign(self.apiclient, [self.vm1]) + + self.logger.debug( + "Creating Port Forwarding rule on tier-2 (networkid=%s) " + "using the same public IP %s – should succeed with conserve mode", + self.tier2.id, + public_ip.ipaddress.ipaddress, + ) + try: + nat_rule = NATRule.create( + self.apiclient, + self.vm2, + self.services["natrule"], + ipaddressid=public_ip.ipaddress.id, + vpcid=self.vpc.id, + networkid=self.tier2.id, + ) + self.assertIsNotNone( + nat_rule, + "Port Forwarding rule creation on tier-2 failed unexpectedly", + ) + except CloudstackAPIException as e: + self.fail( + "Expected cross-tier Port Forwarding rule to succeed with " + "conserveMode=True, but got exception: %s" % e + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="false") + def test_02_vpc_conserve_mode_reuse_source_nat_ip_address(self): + """With VPC conserve mode enabled, a NAT rule can be created on a VPC tier (conserve mode enabled) + with a source NAT IP address + """ + source_nat_ip_resp = list_publicIP( + self.apiclient, + vpcid=self.vpc.id, + listall=True, + issourcenat=True + ) + + source_nat_ip = source_nat_ip_resp[0] + + self.logger.debug( + "Creating Port Forwarding rule on tier-2 (networkid=%s) " + "using the source NAT public IP %s – should succeed with conserve mode", + self.tier1.id, + source_nat_ip.ipaddress, + ) + try: + nat_rule = NATRule.create( + self.apiclient, + self.vm2, + self.services["natrule"], + ipaddressid=source_nat_ip.id, + vpcid=self.vpc.id, + networkid=self.tier2.id, + ) + self.assertIsNotNone( + nat_rule, + "Port Forwarding rule creation on tier-2 failed unexpectedly", + ) + self.logger.debug( + "Creating LB rule on tier-1 (networkid=%s) " + "using the source NAT public IP %s – should succeed with conserve mode", + self.tier1.id, + source_nat_ip.ipaddress, + ) + lb_rule_tier1 = LoadBalancerRule.create( + self.apiclient, + self.services["lbrule"], + ipaddressid=source_nat_ip.id, + accountid=self.account.name, + vpcid=self.vpc.id, + networkid=self.tier2.id, + domainid=self.account.domainid, + ) + self.assertIsNotNone(lb_rule_tier1, "LB rule creation on tier-2 failed") + lb_rule_tier1.assign(self.apiclient, [self.vm2]) + except CloudstackAPIException as e: + self.fail( + "Expected multiple rules on VPC Source NAT IP to succeed with " + "conserveMode=True, but got exception: %s" % e + ) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 825159a2e53a..636c73209a3f 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5227,6 +5227,8 @@ def create(cls, apiclient, services): cmd.networkmode = services["networkmode"] if "routingmode" in services: cmd.routingmode = services["routingmode"] + if "conservemode" in services: + cmd.conservemode = services["conservemode"] return VpcOffering(apiclient.createVPCOffering(cmd).__dict__) def update(self, apiclient, name=None, displaytext=None, state=None): diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 8bcc5d0a94bf..513dfcdaa368 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -256,6 +256,7 @@ "label.activate.project": "Activate project", "label.activeviewersessions": "Active sessions", "label.add": "Add", +"label.addservices": "Add Services", "label.add.account": "Add Account", "label.add.acl.rule": "Add rule", "label.add.acl": "Add ACL", @@ -566,12 +567,21 @@ "label.cks.cluster.size": "Cluster size (Worker nodes)", "label.cks.cluster.worker.nodes.offeringid": "Service Offering for Worker Nodes", "label.cks.cluster.worker.nodes.templateid": "Template for Worker Nodes", +"label.cks.cluster.control.nodes.affinitygroupid": "Affinity Groups for Control Nodes", +"label.cks.cluster.worker.nodes.affinitygroupid": "Affinity Groups for Worker Nodes", +"label.cks.cluster.etcd.nodes.affinitygroupid": "Affinity Groups for ETCD Nodes", "label.cleanup": "Clean up", "label.clear": "Clear", "label.clear.all": "Clear all", "label.clear.list": "Clear list", "label.clear.notification": "Clear notification", "label.clientid": "Provider Client ID", +"label.clone.backup.offering": "Clone Backup Offering", +"label.clone.compute.offering": "Clone Compute Offering", +"label.clone.disk.offering": "Clone Disk Offering", +"label.clone.network.offering": "Clone Network Offering", +"label.clone.system.service.offering": "Clone System Service Offering", +"label.clone.vpc.offering": "Clone VPC Offering", "label.close": "Close", "label.cloud.managed": "CloudManaged", "label.cloudian.admin.password": "Admin Service Password", @@ -947,6 +957,7 @@ "label.domains": "Domains", "label.done": "Done", "label.down": "Down", +"label.dropservices": "Drop Services", "label.download": "Download", "label.download.csv": "Download CSV", "label.download.kubeconfig.cluster": "Download kubeconfig for the cluster

The kubectl command-line tool uses kubeconfig files to find the information it needs to choose a cluster and communicate with the API server of a cluster.", @@ -2905,6 +2916,9 @@ "label.edgecluster": "Edge Cluster", "label.encryption": "Encryption", "label.etcdnodes": "Number of etcd nodes", +"label.controlaffinitygroupnames": "Control Affinity Groups", +"label.workeraffinitygroupnames": "Worker Affinity Groups", +"label.etcdaffinitygroupnames": "ETCD Affinity Groups", "label.versioning": "Versioning", "label.objectlocking": "Object Lock", "label.bucket.policy": "Bucket Policy", @@ -3261,6 +3275,10 @@ "message.create.bucket.failed": "Failed to create bucket.", "message.create.bucket.processing": "Bucket creation in progress", "message.create.compute.offering": "Compute Offering created", +"message.clone.compute.offering": "Compute Offering cloned", +"message.clone.service.offering": "Service Offering cloned", +"message.clone.offering.from": "Cloning from", +"message.clone.offering.edit.hint": "All values are pre-filled from the source offering. Edit any field to customize the new offering.", "message.create.sharedfs.failed": "Failed to create Shared FileSystem.", "message.create.sharedfs.processing": "Shared FileSystem creation in progress.", "message.create.tungsten.public.network": "Create Tungsten-Fabric public Network", @@ -3381,6 +3399,9 @@ "message.disable.webhook.ssl.verification": "Disabling SSL verification is not recommended", "message.discovering.feature": "Discovering features, please wait...", "message.disk.offering.created": "Disk offering created:", +"message.success.clone.backup.offering": "Successfully cloned backup offering", +"message.success.clone.disk.offering": "Successfully cloned disk offering:", +"message.success.clone.network.offering": "Successfully cloned network offering:", "message.disk.usage.info.data.points": "Each data point represents the difference in read/write data since the last data point.", "message.disk.usage.info.sum.of.disks": "The disk usage shown is made up of the sum of read/write data from all the disks in the Instance.", "message.download.volume": "Please click the link to download the volume:

00000", diff --git a/ui/src/components/CheckBoxSelectPair.vue b/ui/src/components/CheckBoxSelectPair.vue index 480e515be29f..a874144458ae 100644 --- a/ui/src/components/CheckBoxSelectPair.vue +++ b/ui/src/components/CheckBoxSelectPair.vue @@ -74,6 +74,10 @@ export default { type: Boolean, default: false }, + defaultSelectValue: { + type: String, + default: null + }, selectOptions: { type: Array, required: true @@ -100,6 +104,9 @@ export default { }, created () { this.checked = this.defaultCheckBoxValue + if (this.defaultSelectValue) { + this.selectedOption = this.defaultSelectValue + } }, watch: { selectOptions () { diff --git a/ui/src/components/offering/ComputeOfferingForm.vue b/ui/src/components/offering/ComputeOfferingForm.vue new file mode 100644 index 000000000000..c621324f108e --- /dev/null +++ b/ui/src/components/offering/ComputeOfferingForm.vue @@ -0,0 +1,812 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + + + + diff --git a/ui/src/components/offering/DiskOfferingForm.vue b/ui/src/components/offering/DiskOfferingForm.vue new file mode 100644 index 000000000000..f3f39647fefa --- /dev/null +++ b/ui/src/components/offering/DiskOfferingForm.vue @@ -0,0 +1,507 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + + diff --git a/ui/src/composables/useServiceCapabilityParams.js b/ui/src/composables/useServiceCapabilityParams.js new file mode 100644 index 000000000000..734f30ec3f4a --- /dev/null +++ b/ui/src/composables/useServiceCapabilityParams.js @@ -0,0 +1,153 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export function buildServiceCapabilityParams (params, values, selectedServiceProviderMap, registeredServicePackages) { + const supportedServices = Object.keys(selectedServiceProviderMap) + params.supportedservices = supportedServices.join(',') + for (const k in supportedServices) { + params[`serviceProviderList[${k}].service`] = supportedServices[k] + params[`serviceProviderList[${k}].provider`] = selectedServiceProviderMap[supportedServices[k]] + } + let serviceCapabilityIndex = 0 + if (supportedServices.includes('Connectivity')) { + if (values.supportsstrechedl2subnet === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RegionLevelVpc' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + if (values.supportspublicaccess === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'DistributedRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + delete params.supportsstrechedl2subnet + delete params.supportspublicaccess + } + // SourceNat capabilities + if (supportedServices.includes('SourceNat')) { + if (values.redundantroutercapability === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'SourceNat' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'SourceNat' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'SupportedSourceNatTypes' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.sourcenattype + serviceCapabilityIndex++ + delete params.redundantroutercapability + delete params.sourcenattype + } else if (values.redundantroutercapability === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Gateway' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + // StaticNat capabilities + if (supportedServices.includes('SourceNat')) { + if (values.elasticip === true) { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'StaticNat' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'ElasticIp' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + if (values.elasticip === true || values.associatepublicip === true) { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'StaticNat' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'associatePublicIP' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.associatepublicip + serviceCapabilityIndex++ + } + delete params.elasticip + delete params.associatepublicip + } + // Lb capabilities + if (supportedServices.includes('Lb')) { + if ('vmautoscalingcapability' in values) { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'VmAutoScaling' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.vmautoscalingcapability + serviceCapabilityIndex++ + } + if (values.elasticlb === true) { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'ElasticLb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + if (values.inlinemode === true && ((selectedServiceProviderMap.Lb === 'F5BigIp') || (selectedServiceProviderMap.Lb === 'Netscaler'))) { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'InlineMode' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.inlinemode + serviceCapabilityIndex++ + } + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'SupportedLbIsolation' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = values.isolation || 'dedicated' + serviceCapabilityIndex++ + if (selectedServiceProviderMap.Lb === 'InternalLbVm') { + params[`servicecapabilitylist[${serviceCapabilityIndex}].service`] = 'lb' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilitytype`] = 'lbSchemes' + params[`servicecapabilitylist[${serviceCapabilityIndex}].capabilityvalue`] = 'internal' + serviceCapabilityIndex++ + } + if ('netscalerservicepackages' in values && + registeredServicePackages.length > values.netscalerservicepackages && + 'netscalerservicepackagesdescription' in values) { + params['details[0].servicepackageuuid'] = registeredServicePackages[values.netscalerservicepackages].id + params['details[1].servicepackagedescription'] = values.netscalerservicepackagesdescription + } + } +} + +/** + * Build the VPC service capability params for Add/Clone VPC Offering forms. + * Handles: RegionLevelVpc, DistributedRouter, RedundantRouter (SourceNat/Gateway) + */ +export function buildVpcServiceCapabilityParams (params, values, selectedServiceProviderMap, isVpcVirtualRouterForAtLeastOneService) { + const supportedServices = Object.keys(selectedServiceProviderMap) + let serviceCapabilityIndex = 0 + if (supportedServices.includes('Connectivity')) { + if (values.regionlevelvpc === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RegionLevelVpc' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + if (values.distributedrouter === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Connectivity' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'DistributedRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + } + if (supportedServices.includes('SourceNat') && values.redundantrouter === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'SourceNat' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } else if (values.redundantrouter === true) { + params[`serviceCapabilityList[${serviceCapabilityIndex}].service`] = 'Gateway' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilitytype`] = 'RedundantRouter' + params[`serviceCapabilityList[${serviceCapabilityIndex}].capabilityvalue`] = true + serviceCapabilityIndex++ + } + if (values.serviceofferingid && isVpcVirtualRouterForAtLeastOneService) { + params.serviceofferingid = values.serviceofferingid + } +} diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 32e888bb53dc..dd17116df02d 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -331,7 +331,7 @@ export default { docHelp: 'adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm', dataView: true, args: ['affinitygroupids'], - show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' && record.vmtype !== 'cksnode' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeAffinity'))), popup: true }, @@ -576,7 +576,7 @@ export default { const filters = ['cloud.managed', 'external.managed'] return filters }, - details: ['name', 'description', 'zonename', 'kubernetesversionname', 'autoscalingenabled', 'csienabled', 'minsize', 'maxsize', 'size', 'controlnodes', 'etcdnodes', 'cpunumber', 'memory', 'keypair', 'cniconfigname', 'associatednetworkname', 'account', 'domain', 'zonename', 'clustertype', 'created'], + details: ['name', 'description', 'zonename', 'kubernetesversionname', 'autoscalingenabled', 'csienabled', 'minsize', 'maxsize', 'size', 'controlnodes', 'controlaffinitygroupnames', 'etcdnodes', 'etcdaffinitygroupnames', 'workeraffinitygroupnames', 'cpunumber', 'memory', 'keypair', 'cniconfigname', 'associatednetworkname', 'account', 'domain', 'zonename', 'clustertype', 'created'], tabs: [ { name: 'k8s', @@ -797,7 +797,7 @@ export default { }, { api: 'scaleKubernetesCluster', - icon: 'swap-outlined', + icon: 'arrows-alt-outlined', label: 'label.kubernetes.cluster.scale', message: 'message.kubernetes.cluster.scale', docHelp: 'plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster', @@ -806,6 +806,15 @@ export default { popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleKubernetesCluster.vue'))) }, + { + api: 'updateKubernetesClusterAffinityGroups', + icon: 'swap-outlined', + label: 'label.change.affinity', + dataView: true, + show: (record) => { return ['Stopped'].includes(record.state) && record.clustertype === 'CloudManaged' }, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeKubernetesClusterAffinity.vue'))) + }, { api: 'upgradeKubernetesCluster', icon: 'plus-circle-outlined', @@ -1325,12 +1334,8 @@ export default { label: 'label.add.affinity.group', docHelp: 'adminguide/virtual_machines.html#creating-a-new-affinity-group', listView: true, - args: ['name', 'description', 'type'], - mapping: { - type: { - options: ['host anti-affinity (Strict)', 'host affinity (Strict)', 'host anti-affinity (Non-Strict)', 'host affinity (Non-Strict)'] - } - } + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/compute/CreateAffinityGroup.vue'))) }, { api: 'deleteAffinityGroup', diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index bc95772d6f7a..9d7b743a70aa 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'))) }] }, { @@ -332,6 +349,14 @@ export default { }, show: (record) => { return record.state === 'Active' }, groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Inactive' } }) } + }, { + api: 'cloneDiskOffering', + icon: 'copy-outlined', + label: 'label.clone.disk.offering', + docHelp: 'adminguide/service_offerings.html#creating-a-new-disk-offering', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneDiskOffering.vue'))) }] }, { @@ -376,6 +401,14 @@ export default { popup: true, groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, args: ['name', 'description', 'allowuserdrivenbackups'] + }, { + api: 'cloneBackupOffering', + icon: 'copy-outlined', + label: 'label.clone.backup.offering', + docHelp: 'adminguide/virtual_machines.html#importing-backup-offerings', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneBackupOffering.vue'))) }, { api: 'deleteBackupOffering', icon: 'delete-outlined', @@ -487,6 +520,14 @@ export default { dataView: true, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/offering/UpdateOfferingAccess.vue'))) + }, { + api: 'cloneNetworkOffering', + icon: 'copy-outlined', + label: 'label.clone.network.offering', + docHelp: 'adminguide/networking.html#creating-a-new-network-offering', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneNetworkOffering.vue'))) }, { api: 'deleteNetworkOffering', icon: 'delete-outlined', @@ -508,7 +549,7 @@ export default { searchFilters: ['name', 'zoneid', 'domainid'], resourceType: 'VpcOffering', columns: ['name', 'state', 'displaytext', 'domain', 'zone', 'order'], - details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'], + details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'conservemode', 'domain', 'zone', 'created'], related: [{ name: 'vpc', title: 'label.vpc', @@ -579,6 +620,14 @@ export default { dataView: true, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/offering/UpdateOfferingAccess.vue'))) + }, { + api: 'cloneVPCOffering', + icon: 'copy-outlined', + docHelp: 'plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering', + label: 'label.clone.vpc.offering', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneVpcOffering.vue'))) }, { api: 'deleteVPCOffering', icon: 'delete-outlined', diff --git a/ui/src/views/compute/ChangeKubernetesClusterAffinity.vue b/ui/src/views/compute/ChangeKubernetesClusterAffinity.vue new file mode 100644 index 000000000000..f06fa65d1703 --- /dev/null +++ b/ui/src/views/compute/ChangeKubernetesClusterAffinity.vue @@ -0,0 +1,200 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/CreateAffinityGroup.vue b/ui/src/views/compute/CreateAffinityGroup.vue new file mode 100644 index 000000000000..27d9828138d4 --- /dev/null +++ b/ui/src/views/compute/CreateAffinityGroup.vue @@ -0,0 +1,173 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/CreateKubernetesCluster.vue b/ui/src/views/compute/CreateKubernetesCluster.vue index 1799933bf2ef..618e46cb72d9 100644 --- a/ui/src/views/compute/CreateKubernetesCluster.vue +++ b/ui/src/views/compute/CreateKubernetesCluster.vue @@ -65,6 +65,7 @@ +