From 498b69b610da7f928c150e8a08bc4d4ea0e4fe69 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 3 Jun 2026 22:53:01 +0530 Subject: [PATCH 01/29] persist and expose effective network rate for NIC, Network and compute offering --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/response/NetworkResponse.java | 12 +++++++++++ .../cloudstack/api/response/NicResponse.java | 12 +++++++++++ .../orchestration/NetworkOrchestrator.java | 7 +++++++ .../main/java/com/cloud/api/ApiDBUtils.java | 10 +++++++++ .../java/com/cloud/api/ApiResponseHelper.java | 11 ++++++++++ .../api/query/dao/UserVmJoinDaoImpl.java | 10 +++++++++ .../com/cloud/network/NetworkServiceImpl.java | 3 +++ .../java/com/cloud/vm/UserVmManagerImpl.java | 21 ++++++++++++++++++- ui/src/config/section/network.js | 2 +- ui/src/config/section/offering.js | 4 ++-- ui/src/views/network/NicsTable.vue | 3 +++ 12 files changed, 92 insertions(+), 4 deletions(-) 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 694830ea2f36..29064df8af88 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1190,6 +1190,7 @@ public class ApiConstants { public static final String NETSCALER_CONTROLCENTER_ID = "netscalercontrolcenterid"; public static final String NETSCALER_SERVICEPACKAGE_ID = "netscalerservicepackageid"; public static final String FETCH_ROUTER_HEALTH_CHECK_RESULTS = "fetchhealthcheckresults"; + public static final String UNLIMITED = "unlimited"; public static final String ZONE_ID_LIST = "zoneids"; public static final String DESTINATION_ZONE_ID_LIST = "destzoneids"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index 3a3663af2551..bc72dda0e068 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -311,6 +311,10 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement @Param(description = "MTU configured on the network VR's private interfaces") private Integer privateMtu; + @SerializedName(ApiConstants.NETWORKRATE) + @Param(description = "Network rate (in Mb/s) configured for the Guest interface of this network", since = "4.24.0") + private String networkRate; + @SerializedName(ApiConstants.IP6_DNS1) @Param(description = "The first IPv6 DNS for the network", since = "4.18.0") private String ipv6Dns1; @@ -699,6 +703,14 @@ public void setPrivateMtu(Integer privateMtu) { this.privateMtu = privateMtu; } + public String getNetworkRate() { + return networkRate; + } + + public void setNetworkRate(String networkRate) { + this.networkRate = networkRate; + } + public void setIpv6Dns1(String ipv6Dns1) { this.ipv6Dns1 = ipv6Dns1; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index 92f25e370fb4..fcbb6ac17d11 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -138,6 +138,10 @@ public class NicResponse extends BaseResponse { @Param(description = "MTU configured on the NIC", since="4.18.0") private Integer mtu; + @SerializedName(ApiConstants.NETWORKRATE) + @Param(description = "Network rate (in Mb/s) configured for the NIC", since = "4.24.0") + private String networkRate; + @SerializedName(ApiConstants.PUBLIC_IP_ID) @Param(description = "Public IP address ID associated with this NIC via Static NAT rule") private String publicIpId; @@ -409,6 +413,14 @@ public void setMtu(Integer mtu) { this.mtu = mtu; } + public String getNetworkRate() { + return networkRate; + } + + public void setNetworkRate(String networkRate) { + this.networkRate = networkRate; + } + public String getVpcId() { return vpcId; } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 4262ee701aab..9b99f8a4a0d9 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -744,6 +744,12 @@ private void updateRouterIpInNetworkDetails(Long networkId, String routerIp, Str } } + private void saveNetworkRateInDetails(long networkId, NetworkOffering offering, long dataCenterId) { + Integer rate = _configMgr.getNetworkOfferingNetworkRate(offering.getId(), dataCenterId); + String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); + networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, networkRate, true); + } + @Override public List setupNetwork(final Account owner, final NetworkOffering offering, final DeploymentPlan plan, final String name, final String displayText, final boolean isDefault) throws ConcurrentOperationException { @@ -819,6 +825,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } updateRouterIpInNetworkDetails(networkPersisted.getId(), network.getRouterIp(), network.getRouterIpv6()); + saveNetworkRateInDetails(networkPersisted.getId(), offering, plan.getDataCenterId()); if (predefined instanceof NetworkVO && guru instanceof NetworkGuruAdditionalFunctions) { final NetworkGuruAdditionalFunctions functions = (NetworkGuruAdditionalFunctions) guru; diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 1d00e9ec16ba..4a40fcd42067 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -338,6 +338,7 @@ import com.cloud.vm.DomainRouterVO; import com.cloud.vm.InstanceGroup; import com.cloud.vm.InstanceGroupVO; +import com.cloud.vm.NicDetailVO; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.VMInstanceDetailVO; @@ -351,6 +352,7 @@ import com.cloud.vm.dao.ConsoleProxyDao; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; @@ -496,6 +498,7 @@ public class ApiDBUtils { static BackupOfferingDao s_backupOfferingDao; static BackupRepositoryDao s_backupRepositoryDao; static NicDao s_nicDao; + static NicDetailsDao s_nicDetailsDao; static ResourceManagerUtil s_resourceManagerUtil; static ApiKeyPairDao s_apiKeyPairDao; static SnapshotPolicyDetailsDao s_snapshotPolicyDetailsDao; @@ -760,6 +763,8 @@ public class ApiDBUtils { @Inject private NicDao nicDao; @Inject + private NicDetailsDao nicDetailsDao; + @Inject private ResourceIconDao resourceIconDao; @Inject private ResourceManagerUtil resourceManagerUtil; @@ -890,6 +895,7 @@ void init() { s_clusterDetailsDao = clusterDetailsDao; s_vmSnapshotDao = vmSnapshotDao; s_nicDao = nicDao; + s_nicDetailsDao = nicDetailsDao; s_nicSecondaryIpDao = nicSecondaryIpDao; s_vpcProvSvc = vpcProvSvc; s_affinityGroupDao = affinityGroupDao; @@ -2232,6 +2238,10 @@ public static NicVO findNicById(long nicId) { return s_nicDao.findById(nicId); } + public static NicDetailVO findNicDetailByName(long nicId, String detailName) { + return s_nicDetailsDao.findDetail(nicId, detailName); + } + public static TemplateResponse newTemplateUpdateResponse(TemplateJoinVO vr) { return s_templateJoinDao.newUpdateResponse(vr); } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index a8551b4c6693..4d8784d2dd26 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -454,6 +454,7 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; +import com.cloud.vm.NicDetailVO; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.snapshot.VMSnapshot; @@ -2703,6 +2704,10 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network) response.setNetworkDomain(network.getNetworkDomain()); response.setPublicMtu(network.getPublicMtu()); response.setPrivateMtu(network.getPrivateMtu()); + NetworkDetailVO networkRateDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETWORKRATE); + if (networkRateDetail != null) { + response.setNetworkRate(networkRateDetail.getValue()); + } response.setDns1(profile.getDns1()); response.setDns2(profile.getDns2()); response.setIpv6Dns1(profile.getIp6Dns1()); @@ -4876,6 +4881,12 @@ public NicResponse createNicResponse(Nic result) { } response.setEnabled(result.isEnabled()); + + NicDetailVO nicRateDetail = ApiDBUtils.findNicDetailByName(result.getId(), ApiConstants.NETWORKRATE); + if (nicRateDetail != null) { + response.setNetworkRate(nicRateDetail.getValue()); + } + return response; } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 72690091e40e..bdb49ad53e79 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -87,6 +87,7 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.net.Dhcp; +import com.cloud.vm.NicDetailVO; import com.cloud.vm.UserVmManager; import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachine; @@ -404,6 +405,10 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); + NicDetailVO nicNetworkRateDetail = ApiDBUtils.findNicDetailByName(userVm.getNicId(), ApiConstants.NETWORKRATE); + if (nicNetworkRateDetail != null) { + nicResponse.setNetworkRate(nicNetworkRateDetail.getValue()); + } userVmResponse.addNic(nicResponse); } } @@ -660,6 +665,11 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm .map(vo -> new NicExtraDhcpOptionResponse(Dhcp.DhcpOptionCode.valueOfInt(vo.getCode()).getName(), vo.getCode(), vo.getValue())) .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); + + NicDetailVO nicNetworkRateDetail = ApiDBUtils.findNicDetailByName(uvo.getNicId(), ApiConstants.NETWORKRATE); + if (nicNetworkRateDetail != null) { + nicResponse.setNetworkRate(nicNetworkRateDetail.getValue()); + } userVmData.addNic(nicResponse); } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index d18fd043f697..935bd8e83002 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -3604,6 +3604,9 @@ public void doInTransactionWithoutResult(TransactionStatus status) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), nicIdString, networkOfferingId, null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplay()); } + Integer rate = _configMgr.getNetworkOfferingNetworkRate(networkOfferingId, network.getDataCenterId()); + String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); + _networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, networkRate, true); } }); } else { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index bb55f570927b..4695a05b8056 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -403,6 +403,7 @@ import com.cloud.vm.dao.InstanceGroupDao; import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; @@ -499,6 +500,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private NicDao _nicDao; @Inject + private NicDetailsDao nicDetailsDao; + @Inject private RulesManager _rulesMgr; @Inject private LoadBalancingRulesManager _lbMgr; @@ -1532,6 +1535,7 @@ public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterV saveExtraDhcpOptions(guestNic.getId(), cmd.getDhcpOptionsMap()); _networkMgr.configureExtraDhcpOptions(network, guestNic.getId(), cmd.getDhcpOptionsMap()); cleanUp = false; + saveNetworkRateInDetails(guestNic.getId(), guestNic.getNetworkRate()); } catch (ResourceUnavailableException e) { throw new CloudRuntimeException("Unable to add NIC to " + vmInstance + ": " + e); } catch (InsufficientCapacityException e) { @@ -1552,6 +1556,18 @@ public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterV return _vmDao.findById(vmInstance.getId()); } + private void saveNetworkRateInDetails(long nicId, Integer rate) { + String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); + nicDetailsDao.addDetail(nicId, ApiConstants.NETWORKRATE, networkRate, true); + } + + private void refreshNicNetworkRates(long vmId) { + List nics = _nicDao.listByVmId(vmId); + for (NicVO nic : nics) { + saveNetworkRateInDetails(nic.getId(), _networkModel.getNetworkRate(nic.getNetworkId(), vmId)); + } + } + /** * Set NIC as default if VM has no default NIC * @param vmInstance VM instance to be checked @@ -3466,7 +3482,10 @@ public UserVm startVirtualMachine(StartVMCmd cmd) throws ExecutionException, Con additonalParams.put(VirtualMachineProfile.Param.ConsiderLastHost, cmd.getConsiderLastHost().toString()); } - return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner()).first(); + UserVm vm = startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner()).first(); + // Refresh nic_details with current network rates — the network offering may have changed since the VM was last running + refreshNicNetworkRates(vm.getId()); + return vm; } @Override diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 50c2ff4250b0..7fb0a2af76c0 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -49,7 +49,7 @@ export default { return fields }, details: () => { - const fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'asnumber', 'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account', 'domainpath', 'associatednetwork', 'associatednetworkid', 'ip4routing', 'ip6firewall', 'ip6routing', 'ip6routes', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'privatemtu'] + const fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'asnumber', 'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account', 'domainpath', 'associatednetwork', 'associatednetworkid', 'ip4routing', 'ip6firewall', 'ip6routing', 'ip6routes', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'privatemtu', 'networkrate'] if (isAdmin()) { const vlanIndex = fields.findIndex(detail => detail === 'vlan') fields.splice(vlanIndex + 1, 0, 'broadcasturi') diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 9d7b743a70aa..ee9f5d8649c4 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -39,9 +39,9 @@ export default { return params }, filters: ['active', 'inactive'], - columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order'], + columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order', 'networkrate'], details: () => { - var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails'] + var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails', 'networkrate'] if (store.getters.apis.createServiceOffering && store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) { fields.splice(6, 0, 'vspherestoragepolicy') diff --git a/ui/src/views/network/NicsTable.vue b/ui/src/views/network/NicsTable.vue index 11ba135e39a2..8a036e7b9088 100644 --- a/ui/src/views/network/NicsTable.vue +++ b/ui/src/views/network/NicsTable.vue @@ -57,6 +57,9 @@ {{ record.isolationuri }} + + {{ record.networkrate }} + From adbdec2c511e9bab10c9fd79f42f6a8fda055cd8 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 4 Sep 2026 16:25:51 +0530 Subject: [PATCH 02/29] server,engine,api: persist and expose NIC network rate from nics column - Add network_rate column to nics table (schema-42300to42400.sql) - Add DB upgrade path: Upgrade42300to42400 registered in DatabaseUpgradeChecker - Add network_rate field and getter/setter to NicVO - Set network_rate on NicVO in NetworkOrchestrator.allocateNic() where rate is already computed, eliminating secondary per-NIC update calls - Add getNetworkRate() to Nic interface so ApiResponseHelper.createNicResponse can call result.getNetworkRate() without casting or extra DB queries - Add nic_network_rate to user_vm_view and UserVmJoinVO so listVirtualMachines reads rate from the join without extra per-NIC findNicById calls - Update UserVmJoinDaoImpl to use uvo.getNicNetworkRate() directly - Expose network_rate in NicResponse as Integer (null = unlimited) - Refresh NIC rates on VM start via refreshNicNetworkRates in UserVmManagerImpl --- api/src/main/java/com/cloud/vm/Nic.java | 2 + .../cloudstack/api/response/NicResponse.java | 8 +-- .../orchestration/NetworkOrchestrator.java | 3 +- .../cloud/upgrade/DatabaseUpgradeChecker.java | 2 + .../upgrade/dao/Upgrade42300to42400.java | 54 +++++++++++++++++++ .../src/main/java/com/cloud/vm/NicVO.java | 11 ++++ .../META-INF/db/schema-42300to42400.sql | 20 +++++++ .../META-INF/db/views/cloud.user_vm_view.sql | 1 + .../java/com/cloud/api/ApiResponseHelper.java | 6 +-- .../api/query/dao/UserVmJoinDaoImpl.java | 11 +--- .../com/cloud/api/query/vo/UserVmJoinVO.java | 7 +++ .../java/com/cloud/vm/UserVmManagerImpl.java | 13 ++--- 12 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql diff --git a/api/src/main/java/com/cloud/vm/Nic.java b/api/src/main/java/com/cloud/vm/Nic.java index 3722e5769c92..08ef5ea82469 100644 --- a/api/src/main/java/com/cloud/vm/Nic.java +++ b/api/src/main/java/com/cloud/vm/Nic.java @@ -168,5 +168,7 @@ public enum ReservationStrategy { Integer getMtu(); + Integer getNetworkRate(); + boolean isEnabled(); } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index 6613c11ab87d..314d26b2ebfd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -139,8 +139,8 @@ public class NicResponse extends BaseResponse { private Integer mtu; @SerializedName(ApiConstants.NETWORKRATE) - @Param(description = "Network rate (in Mb/s) configured for the NIC", since = "4.24.0") - private String networkRate; + @Param(description = "Network rate (in Mb/s) configured for the NIC; absent if unlimited", since = "4.24.0") + private Integer networkRate; @SerializedName(ApiConstants.PUBLIC_IP_ID) @Param(description = "Public IP address ID associated with this NIC via Static NAT rule") @@ -417,11 +417,11 @@ public void setMtu(Integer mtu) { this.mtu = mtu; } - public String getNetworkRate() { + public Integer getNetworkRate() { return networkRate; } - public void setNetworkRate(String networkRate) { + public void setNetworkRate(Integer networkRate) { this.networkRate = networkRate; } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 95f4f943aae1..982c8ab2097d 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -1234,14 +1234,15 @@ public Pair allocateNic(final NicProfile requested, final N NicVO vo = checkForRaceAndAllocateNic(requested, network, isDefaultNic, deviceId, vm); final Integer networkRate = _networkModel.getNetworkRate(network.getId(), vm.getId()); + vo.setNetworkRate(networkRate != null && networkRate > 0 ? networkRate : null); final NicProfile vmNic = new NicProfile(vo, network, vo.getBroadcastUri(), vo.getIsolationUri(), networkRate, _networkModel.isSecurityGroupSupportedInNetwork(network), _networkModel.getNetworkTag(vm.getHypervisorType(), network)); if (vm.getType() == Type.DomainRouter) { Pair networks = getGuestNetworkRouterAndVpcDetails(vm.getId()); setMtuDetailsInVRNic(networks, network, vo); - _nicDao.update(vo.getId(), vo); setMtuInVRNicProfile(networks, network.getTrafficType(), vmNic); } + _nicDao.update(vo.getId(), vo); return new Pair<>(vmNic, Integer.valueOf(deviceId)); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index c3a982aa70e5..0e40d7b7401c 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -96,6 +96,7 @@ import com.cloud.upgrade.dao.Upgrade420to421; import com.cloud.upgrade.dao.Upgrade421to430; import com.cloud.upgrade.dao.Upgrade42210to42300; +import com.cloud.upgrade.dao.Upgrade42300to42400; import com.cloud.upgrade.dao.Upgrade430to440; import com.cloud.upgrade.dao.Upgrade431to440; import com.cloud.upgrade.dao.Upgrade432to440; @@ -248,6 +249,7 @@ public DatabaseUpgradeChecker() { .next("4.21.0.0", new Upgrade42100to42200()) .next("4.22.0.0", new Upgrade42200to42210()) .next("4.22.1.0", new Upgrade42210to42300()) + .next("4.23.0.0", new Upgrade42300to42400()) .build(); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java new file mode 100644 index 000000000000..ac617060eebd --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java @@ -0,0 +1,54 @@ +// 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.upgrade.dao; + +import java.io.InputStream; +import java.sql.Connection; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class Upgrade42300to42400 extends DbUpgradeAbstractImpl implements DbUpgrade { + + @Override + public String[] getUpgradableVersionRange() { + return new String[]{"4.23.0.0", "4.24.0.0"}; + } + + @Override + public String getUpgradedVersion() { + return "4.24.0.0"; + } + + @Override + public InputStream[] getPrepareScripts() { + final String scriptFile = "META-INF/db/schema-42300to42400.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + return new InputStream[]{script}; + } + + @Override + public void performDataMigration(Connection conn) { + } + + @Override + public InputStream[] getCleanupScripts() { + return null; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index 65946b8d8210..dd405381cb9b 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -131,6 +131,9 @@ protected NicVO() { @Column(name = "mtu") Integer mtu; + @Column(name = "network_rate") + Integer networkRate; + @Column(name = "enabled") boolean enabled; @@ -426,4 +429,12 @@ public Integer getMtu() { public void setMtu(Integer mtu) { this.mtu = mtu; } + + public Integer getNetworkRate() { + return networkRate; + } + + public void setNetworkRate(Integer networkRate) { + this.networkRate = networkRate; + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql new file mode 100644 index 000000000000..ea0c8e62ca0f --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql @@ -0,0 +1,20 @@ +-- 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. + +-- Schema upgrade from 4.23.0.0 to 4.24.0.0 + +ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int unsigned DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC'; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index fbf126608eae..5f6c31043596 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -144,6 +144,7 @@ SELECT `nics`.`broadcast_uri` AS `broadcast_uri`, `nics`.`isolation_uri` AS `isolation_uri`, `nics`.`enabled` AS `is_nic_enabled`, + `nics`.`network_rate` AS `nic_network_rate`, `nic_details`.`value` AS `nic_dns_name`, `vpc`.`id` AS `vpc_id`, `vpc`.`uuid` AS `vpc_uuid`, diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index f350ca262237..7977291d76d6 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -454,7 +454,6 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; -import com.cloud.vm.NicDetailVO; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.snapshot.VMSnapshot; @@ -4910,10 +4909,7 @@ public NicResponse createNicResponse(Nic result) { response.setEnabled(result.isEnabled()); - NicDetailVO nicRateDetail = ApiDBUtils.findNicDetailByName(result.getId(), ApiConstants.NETWORKRATE); - if (nicRateDetail != null) { - response.setNetworkRate(nicRateDetail.getValue()); - } + response.setNetworkRate(result.getNetworkRate()); return response; } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index d3887c1a7db8..2d5cde339616 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -97,7 +97,6 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.net.Dhcp; -import com.cloud.vm.NicDetailVO; import com.cloud.vm.UserVmManager; import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachine; @@ -448,10 +447,7 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); - NicDetailVO nicNetworkRateDetail = ApiDBUtils.findNicDetailByName(userVm.getNicId(), ApiConstants.NETWORKRATE); - if (nicNetworkRateDetail != null) { - nicResponse.setNetworkRate(nicNetworkRateDetail.getValue()); - } + nicResponse.setNetworkRate(userVm.getNicNetworkRate()); userVmResponse.addNic(nicResponse); } } @@ -750,10 +746,7 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); - NicDetailVO nicNetworkRateDetail = ApiDBUtils.findNicDetailByName(uvo.getNicId(), ApiConstants.NETWORKRATE); - if (nicNetworkRateDetail != null) { - nicResponse.setNetworkRate(nicNetworkRateDetail.getValue()); - } + nicResponse.setNetworkRate(uvo.getNicNetworkRate()); userVmData.addNic(nicResponse); } diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java index 6f84bd9b1e3e..5e63e4383e09 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java @@ -355,6 +355,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro @Column(name = "is_nic_enabled") private boolean isNicEnabled; + @Column(name = "nic_network_rate") + private Integer nicNetworkRate; + @Column(name = "ip_address") private String ipAddress; @@ -1115,6 +1118,10 @@ public boolean isNicEnabled() { return isNicEnabled; } + public Integer getNicNetworkRate() { + return nicNetworkRate; + } + public String getNicDnsName() { return nicDnsName; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 23dfb12b24c6..5f814cd192b9 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -421,7 +421,6 @@ import com.cloud.vm.dao.InstanceGroupDao; import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.NicDao; -import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; @@ -525,8 +524,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private NicDao _nicDao; @Inject - private NicDetailsDao nicDetailsDao; - @Inject private RulesManager _rulesMgr; @Inject private LoadBalancingRulesManager _lbMgr; @@ -1576,7 +1573,6 @@ public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterV saveExtraDhcpOptions(guestNic.getId(), cmd.getDhcpOptionsMap()); _networkMgr.configureExtraDhcpOptions(network, guestNic.getId(), cmd.getDhcpOptionsMap()); cleanUp = false; - saveNetworkRateInDetails(guestNic.getId(), guestNic.getNetworkRate()); } catch (ResourceUnavailableException e) { throw new CloudRuntimeException("Unable to add NIC to " + vmInstance + ": " + e); } catch (InsufficientCapacityException e) { @@ -1598,15 +1594,12 @@ public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterV return _vmDao.findById(vmInstance.getId()); } - private void saveNetworkRateInDetails(long nicId, Integer rate) { - String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); - nicDetailsDao.addDetail(nicId, ApiConstants.NETWORKRATE, networkRate, true); - } - private void refreshNicNetworkRates(long vmId) { List nics = _nicDao.listByVmId(vmId); for (NicVO nic : nics) { - saveNetworkRateInDetails(nic.getId(), _networkModel.getNetworkRate(nic.getNetworkId(), vmId)); + Integer rate = _networkModel.getNetworkRate(nic.getNetworkId(), vmId); + nic.setNetworkRate(rate != null && rate > 0 ? rate : null); + _nicDao.update(nic.getId(), nic); } } From b684b88ab249ee2d6c2179038b9196d210ebe6ab Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Mon, 7 Sep 2026 16:44:41 +0530 Subject: [PATCH 03/29] fix ui build due to merge conflict --- ui/src/config/section/offering.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index ee3de0486f14..6180d1467d2b 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -43,7 +43,6 @@ export default { } return [] }, - filters: ['active', 'inactive'], columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order', 'networkrate'], details: () => { var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails', 'networkrate'] From 0a2b534411d23a9a020df6d3530c48f5af998963 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 15 Sep 2026 10:35:37 +0530 Subject: [PATCH 04/29] Fix network rate response inconsistency between Network, Nic and NetworkOffering APIs Align networkrate field type and unlimited semantics (-1) across NetworkResponse and NicResponse with the existing NetworkOfferingResponse convention, instead of using a String unlimited value or omitting the field entirely. --- .../main/java/org/apache/cloudstack/api/ApiConstants.java | 1 - .../apache/cloudstack/api/response/NetworkResponse.java | 8 ++++---- .../org/apache/cloudstack/api/response/NicResponse.java | 2 +- .../engine/orchestration/NetworkOrchestrator.java | 3 +-- server/src/main/java/com/cloud/api/ApiResponseHelper.java | 7 +++++-- .../java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java | 6 ++++-- .../main/java/com/cloud/network/NetworkServiceImpl.java | 3 +-- 7 files changed, 16 insertions(+), 14 deletions(-) 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 507049b660f7..f74c46161180 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1216,7 +1216,6 @@ public class ApiConstants { public static final String NETSCALER_CONTROLCENTER_ID = "netscalercontrolcenterid"; public static final String NETSCALER_SERVICEPACKAGE_ID = "netscalerservicepackageid"; public static final String FETCH_ROUTER_HEALTH_CHECK_RESULTS = "fetchhealthcheckresults"; - public static final String UNLIMITED = "unlimited"; public static final String ZONE_ID_LIST = "zoneids"; public static final String DESTINATION_ZONE_ID_LIST = "destzoneids"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index eee40983cea4..f1f9f6d18dea 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -312,8 +312,8 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement private Integer privateMtu; @SerializedName(ApiConstants.NETWORKRATE) - @Param(description = "Network rate (in Mb/s) configured for the Guest interface of this network", since = "4.24.0") - private String networkRate; + @Param(description = "Network rate (in Mb/s) configured for the Guest interface of this network; -1 if unlimited", since = "4.24.0") + private Integer networkRate; @SerializedName(ApiConstants.IP6_DNS1) @Param(description = "The first IPv6 DNS for the network", since = "4.18.0") @@ -711,11 +711,11 @@ public void setPrivateMtu(Integer privateMtu) { this.privateMtu = privateMtu; } - public String getNetworkRate() { + public Integer getNetworkRate() { return networkRate; } - public void setNetworkRate(String networkRate) { + public void setNetworkRate(Integer networkRate) { this.networkRate = networkRate; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index 314d26b2ebfd..843c21f5fd34 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -139,7 +139,7 @@ public class NicResponse extends BaseResponse { private Integer mtu; @SerializedName(ApiConstants.NETWORKRATE) - @Param(description = "Network rate (in Mb/s) configured for the NIC; absent if unlimited", since = "4.24.0") + @Param(description = "Network rate (in Mb/s) configured for the NIC; -1 if unlimited", since = "4.24.0") private Integer networkRate; @SerializedName(ApiConstants.PUBLIC_IP_ID) diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 982c8ab2097d..77eebae4e2a5 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -779,8 +779,7 @@ private void updateRouterIpInNetworkDetails(Long networkId, String routerIp, Str private void saveNetworkRateInDetails(long networkId, NetworkOffering offering, long dataCenterId) { Integer rate = _configMgr.getNetworkOfferingNetworkRate(offering.getId(), dataCenterId); - String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); - networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, networkRate, true); + networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, String.valueOf(rate), true); } @Override diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 5564ab370695..c16e269ef801 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -243,6 +243,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -2717,7 +2718,8 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network) response.setPrivateMtu(network.getPrivateMtu()); NetworkDetailVO networkRateDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETWORKRATE); if (networkRateDetail != null) { - response.setNetworkRate(networkRateDetail.getValue()); + int networkRate = NumberUtils.toInt(networkRateDetail.getValue(), -1); + response.setNetworkRate(networkRate > 0 ? networkRate : -1); } response.setDns1(profile.getDns1()); response.setDns2(profile.getDns2()); @@ -4910,7 +4912,8 @@ public NicResponse createNicResponse(Nic result) { response.setEnabled(result.isEnabled()); - response.setNetworkRate(result.getNetworkRate()); + Integer nicNetworkRate = result.getNetworkRate(); + response.setNetworkRate(nicNetworkRate != null && nicNetworkRate > 0 ? nicNetworkRate : -1); return response; } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 2d5cde339616..67ab53b61132 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -447,7 +447,8 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); - nicResponse.setNetworkRate(userVm.getNicNetworkRate()); + Integer nicNetworkRate = userVm.getNicNetworkRate(); + nicResponse.setNetworkRate(nicNetworkRate != null && nicNetworkRate > 0 ? nicNetworkRate : -1); userVmResponse.addNic(nicResponse); } } @@ -746,7 +747,8 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm .collect(Collectors.toList()); nicResponse.setExtraDhcpOptions(nicExtraDhcpOptionResponses); - nicResponse.setNetworkRate(uvo.getNicNetworkRate()); + Integer nicNetworkRate = uvo.getNicNetworkRate(); + nicResponse.setNetworkRate(nicNetworkRate != null && nicNetworkRate > 0 ? nicNetworkRate : -1); userVmData.addNic(nicResponse); } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 6c8a8f4e4a0e..dd43cf16e0b3 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -3605,8 +3605,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) { null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplay()); } Integer rate = _configMgr.getNetworkOfferingNetworkRate(networkOfferingId, network.getDataCenterId()); - String networkRate = (rate == null || rate <= 0) ? ApiConstants.UNLIMITED : String.valueOf(rate); - _networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, networkRate, true); + _networkDetailsDao.addDetail(networkId, ApiConstants.NETWORKRATE, String.valueOf(rate), true); } }); } else { From 71d49b86f1ec24e55f0ec38c079a21628dc58a55 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 16 Sep 2026 09:43:36 +0530 Subject: [PATCH 05/29] Apply new precedence rules for VM and VR NIC network rate Instance NICs now derive rate solely from the VM's own offering. VR guest interfaces check the router's own system offering first, then fall back to the network offering. Persist rate once in prepareNic() for all VM types. Also surface NIC network rate on listRouters. Existing NICs won't reflect the new precedence until their VM/router is restarted; a DB backfill for pre-existing NICs is tracked as a separate follow-up. --- .../orchestration/NetworkOrchestrator.java | 2 ++ .../db/views/cloud.domain_router_view.sql | 1 + .../src/main/java/com/cloud/api/ApiDBUtils.java | 10 ---------- .../api/query/dao/DomainRouterJoinDaoImpl.java | 4 ++++ .../cloud/api/query/vo/DomainRouterJoinVO.java | 7 +++++++ .../com/cloud/network/NetworkModelImpl.java | 17 ++++++++++++++--- .../java/com/cloud/vm/UserVmManagerImpl.java | 14 +------------- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 77eebae4e2a5..ae74c8f49027 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -2307,6 +2307,8 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy nic.setState(Nic.State.Reserved); } + nic.setNetworkRate(networkRate != null && networkRate > 0 ? networkRate : null); + if (vmProfile.getType() == Type.DomainRouter) { Pair networks = getGuestNetworkRouterAndVpcDetails(vmProfile.getId()); setMtuDetailsInVRNic(networks, network, nic); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql index d5f17606cb41..524048b9198f 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql @@ -77,6 +77,7 @@ select nics.isolation_uri isolation_uri, nics.mtu mtu, nics.enabled is_nic_enabled, + nics.network_rate nic_network_rate, vpc.id vpc_id, vpc.uuid vpc_uuid, vpc.name vpc_name, diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 4067e5ab0dcb..934600eb2b61 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -343,7 +343,6 @@ import com.cloud.vm.DomainRouterVO; import com.cloud.vm.InstanceGroup; import com.cloud.vm.InstanceGroupVO; -import com.cloud.vm.NicDetailVO; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.VMInstanceDetailVO; @@ -357,7 +356,6 @@ import com.cloud.vm.dao.ConsoleProxyDao; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; -import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; @@ -504,7 +502,6 @@ public class ApiDBUtils { static BackupOfferingDao s_backupOfferingDao; static BackupRepositoryDao s_backupRepositoryDao; static NicDao s_nicDao; - static NicDetailsDao s_nicDetailsDao; static ResourceManagerUtil s_resourceManagerUtil; static ApiKeyPairDao s_apiKeyPairDao; static SnapshotPolicyDetailsDao s_snapshotPolicyDetailsDao; @@ -771,8 +768,6 @@ public class ApiDBUtils { @Inject private NicDao nicDao; @Inject - private NicDetailsDao nicDetailsDao; - @Inject private ResourceIconDao resourceIconDao; @Inject private ResourceManagerUtil resourceManagerUtil; @@ -907,7 +902,6 @@ void init() { s_clusterDetailsDao = clusterDetailsDao; s_vmSnapshotDao = vmSnapshotDao; s_nicDao = nicDao; - s_nicDetailsDao = nicDetailsDao; s_nicSecondaryIpDao = nicSecondaryIpDao; s_vpcProvSvc = vpcProvSvc; s_affinityGroupDao = affinityGroupDao; @@ -2256,10 +2250,6 @@ public static NicVO findNicById(long nicId) { return s_nicDao.findById(nicId); } - public static NicDetailVO findNicDetailByName(long nicId, String detailName) { - return s_nicDetailsDao.findDetail(nicId, detailName); - } - public static TemplateResponse newTemplateUpdateResponse(TemplateJoinVO vr) { return s_templateJoinDao.newUpdateResponse(vr); } diff --git a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java index 9bc409d455e7..d4ae4f1db240 100644 --- a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java @@ -198,6 +198,8 @@ public DomainRouterResponse newDomainRouterResponse(DomainRouterJoinVO router, A nicResponse.setIsDefault(router.isDefaultNic()); nicResponse.setEnabled(router.isNicEnabled()); nicResponse.setObjectName("nic"); + Integer routerNicNetworkRate = router.getNicNetworkRate(); + nicResponse.setNetworkRate(routerNicNetworkRate != null && routerNicNetworkRate > 0 ? routerNicNetworkRate : -1); routerResponse.addNic(nicResponse); } } @@ -292,6 +294,8 @@ public DomainRouterResponse setDomainRouterResponse(DomainRouterResponse vrData, nicResponse.setIsDefault(vr.isDefaultNic()); nicResponse.setEnabled(vr.isNicEnabled()); nicResponse.setObjectName("nic"); + Integer vrNicNetworkRate = vr.getNicNetworkRate(); + nicResponse.setNetworkRate(vrNicNetworkRate != null && vrNicNetworkRate > 0 ? vrNicNetworkRate : -1); vrData.addNic(nicResponse); } return vrData; diff --git a/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java index db7f75b6f2bb..7cdeb370a2a9 100644 --- a/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java @@ -210,6 +210,9 @@ public class DomainRouterJoinVO extends BaseViewVO implements ControlledViewEnti @Column(name = "isolation_uri") private URI isolationUri; + @Column(name = "nic_network_rate") + private Integer nicNetworkRate; + @Column(name = "network_id") private long networkId; @@ -450,6 +453,10 @@ public URI getIsolationUri() { return isolationUri; } + public Integer getNicNetworkRate() { + return nicNetworkRate; + } + public long getNetworkId() { return networkId; } diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index f47046cdc434..d0456e0e9de3 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -123,6 +123,8 @@ import com.cloud.projects.ProjectAccount; import com.cloud.projects.dao.ProjectAccountDao; import com.cloud.projects.dao.ProjectDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; @@ -173,6 +175,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi @Inject ConfigurationManager _configMgr; @Inject + ServiceOfferingDao _serviceOfferingDao; + @Inject NetworkOfferingDao _networkOfferingDao = null; @Inject NetworkDao _networksDao = null; @@ -1202,9 +1206,11 @@ public Integer getNetworkRate(long networkId, Long vmId) { final Network network = getNetwork(networkId); final NetworkOffering ntwkOff = _entityMgr.findById(NetworkOffering.class, network.getNetworkOfferingId()); - // For user VM: For default nic use network rate from the service/compute offering, + // For user VM: Use network rate from the service/compute offering for every nic (default or not), // or on NULL from vm.network.throttling.rate global setting - // For router: Get network rate for guest and public networks from the guest network offering + // For router: For guest networks, use network rate from the router's own system offering first, + // falling back to the guest network offering, or on NULL from network.throttling.rate + // For public networks, use network rate from the router's guest network offering, // or on NULL from network.throttling.rate // For others: Use network rate from their network offering, // or on NULL from network.throttling.rate setting at zone > global level @@ -1213,7 +1219,7 @@ public Integer getNetworkRate(long networkId, Long vmId) { switch (vm.getType()) { case User: final Nic nic = _nicDao.findByNtwkIdAndInstanceId(networkId, vmId); - if (nic != null && nic.isDefaultNic()) { + if (nic != null) { return _configMgr.getServiceOfferingNetworkRate(vm.getServiceOfferingId(), network.getDataCenterId()); } break; @@ -1221,6 +1227,11 @@ public Integer getNetworkRate(long networkId, Long vmId) { if (TrafficType.Guest.equals(network.getTrafficType())) { final Nic routerNic = _nicDao.findByNtwkIdAndInstanceId(networkId, vmId); if (routerNic != null) { + final ServiceOfferingVO routerOffering = _serviceOfferingDao.findById(vm.getServiceOfferingId()); + if (routerOffering != null && routerOffering.getRateMbps() != null) { + final int systemOfferingRate = routerOffering.getRateMbps(); + return systemOfferingRate > 0 ? systemOfferingRate : -1; + } return _configMgr.getNetworkOfferingNetworkRate(network.getNetworkOfferingId(), network.getDataCenterId()); } } else if (TrafficType.Public.equals(network.getTrafficType())) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 053a809ff5c0..60fdee8feaa7 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -1594,15 +1594,6 @@ public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterV return _vmDao.findById(vmInstance.getId()); } - private void refreshNicNetworkRates(long vmId) { - List nics = _nicDao.listByVmId(vmId); - for (NicVO nic : nics) { - Integer rate = _networkModel.getNetworkRate(nic.getNetworkId(), vmId); - nic.setNetworkRate(rate != null && rate > 0 ? rate : null); - _nicDao.update(nic.getId(), nic); - } - } - private void validateVmZoneTypeForAddNic(UserVmVO vmInstance, DataCenter dc) { if (!NetworkType.Basic.equals(dc.getNetworkType())) { return; @@ -3587,10 +3578,7 @@ public UserVm startVirtualMachine(StartVMCmd cmd) throws ExecutionException, Con additonalParams.put(VirtualMachineProfile.Param.ConsiderLastHost, cmd.getConsiderLastHost().toString()); } - UserVm vm = startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner(), false).first(); - // Refresh nic_details with current network rates — the network offering may have changed since the VM was last running - refreshNicNetworkRates(vm.getId()); - return vm; + return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner(), false).first(); } @Override From 4e7236cb6242dbd337a3dd13a1109cf8be360909 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 16 Sep 2026 13:39:25 +0530 Subject: [PATCH 06/29] Backfill network rate for existing NICs/networks and refresh it on migration Add an upgrade-time backfill for nics.network_rate and the network_details networkrate entry, frozen to pre-feature precedence so existing bandwidth doesn't change. Also refresh network_details rate during migrateGuestNetwork, and add unit tests for getNetworkRate precedence. --- .../cloud/upgrade/NetworkRateBackfill.java | 216 +++++++++++++++++ .../upgrade/dao/Upgrade42300to42400.java | 2 + .../network/NetworkMigrationManagerImpl.java | 3 + .../cloud/network/NetworkModelImplTest.java | 224 ++++++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java new file mode 100644 index 000000000000..7c4ff7d9348b --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -0,0 +1,216 @@ +// 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.upgrade; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.dao.ConfigurationDaoImpl; + +import com.cloud.dc.dao.DataCenterDetailsDaoImpl; +import com.cloud.network.Networks.TrafficType; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkDaoImpl; +import com.cloud.network.dao.NetworkDetailsDao; +import com.cloud.network.dao.NetworkDetailsDaoImpl; +import com.cloud.network.dao.NetworkVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.service.dao.ServiceOfferingDaoImpl; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDaoImpl; +import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDaoImpl; + +/** + * Backfills {@code nics.network_rate} and the {@code network_details} "networkrate" entry for + * pre-existing NICs/networks. Deliberately frozen to the pre-feature precedence of + * {@link com.cloud.network.NetworkModelImpl#getNetworkRate} - do not redirect this to call the + * live method, whose precedence will keep evolving. + */ +public class NetworkRateBackfill { + protected static Logger LOGGER = LogManager.getLogger(NetworkRateBackfill.class); + + private static final String CONFIG_NETWORK_THROTTLING_RATE = "network.throttling.rate"; + private static final String CONFIG_VM_NETWORK_THROTTLING_RATE = "vm.network.throttling.rate"; + private static final String NETWORKRATE_DETAIL_NAME = "networkrate"; + private static final int DEFAULT_THROTTLING_RATE = 200; + + private final NicDao nicDao = new NicDaoImpl(); + private final VMInstanceDao vmInstanceDao = new VMInstanceDaoImpl(); + private final NetworkDao networkDao = new NetworkDaoImpl(); + private final NetworkDetailsDao networkDetailsDao = new NetworkDetailsDaoImpl(); + private final ServiceOfferingDao serviceOfferingDao = new ServiceOfferingDaoImpl(); + private final DataCenterDetailsDaoImpl dataCenterDetailsDao = new DataCenterDetailsDaoImpl(); + private final ConfigurationDao configurationDao = new ConfigurationDaoImpl(); + + public void backfillNetworkRates() { + backfillNicNetworkRates(); + backfillNetworkDetailsRates(); + } + + private void backfillNicNetworkRates() { + final String sql = "SELECT id, network_id, instance_id, default_nic FROM nics " + + "WHERE removed IS NULL AND network_rate IS NULL AND instance_id IS NOT NULL"; + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement(sql); + ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + final long nicId = rs.getLong("id"); + final long networkId = rs.getLong("network_id"); + final long instanceId = rs.getLong("instance_id"); + final boolean defaultNic = rs.getBoolean("default_nic"); + try { + final Integer rate = computeLegacyNicNetworkRate(networkId, instanceId, defaultNic); + if (rate != null && rate > 0) { + updateNicNetworkRate(nicId, rate); + } + } catch (Exception e) { + LOGGER.warn("Failed to backfill network_rate for nic id=" + nicId + ": " + e.getMessage()); + } + } + } catch (SQLException e) { + LOGGER.warn("Failed to backfill nic network rates: " + e.getMessage()); + } + } + + private void updateNicNetworkRate(long nicId, int rate) throws SQLException { + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement( + "UPDATE nics SET network_rate = ? WHERE id = ?")) { + pstmt.setInt(1, rate); + pstmt.setLong(2, nicId); + pstmt.executeUpdate(); + } + } + + private Integer computeLegacyNicNetworkRate(long networkId, long instanceId, boolean defaultNic) { + final NetworkVO network = networkDao.findById(networkId); + if (network == null) { + return null; + } + final VMInstanceVO vm = vmInstanceDao.findById(instanceId); + if (vm != null) { + if (vm.getType() == VirtualMachine.Type.User) { + if (defaultNic) { + return getServiceOfferingNetworkRate(vm.getServiceOfferingId(), network.getDataCenterId()); + } + } else if (vm.getType() == VirtualMachine.Type.DomainRouter) { + if (TrafficType.Guest.equals(network.getTrafficType())) { + return getNetworkOfferingNetworkRate(network.getNetworkOfferingId(), network.getDataCenterId()); + } else if (TrafficType.Public.equals(network.getTrafficType())) { + final Integer rate = findRouterGuestNetworkRate(vm.getId(), network.getDataCenterId()); + if (rate != null) { + return rate; + } + } + } else if (vm.getType() == VirtualMachine.Type.ConsoleProxy || vm.getType() == VirtualMachine.Type.SecondaryStorageVm) { + return -1; + } + } + return getNetworkOfferingNetworkRate(network.getNetworkOfferingId(), network.getDataCenterId()); + } + + private Integer findRouterGuestNetworkRate(long routerInstanceId, long dataCenterId) { + final List routerNics = nicDao.listByVmId(routerInstanceId); + for (final NicVO routerNic : routerNics) { + final NetworkVO nw = networkDao.findById(routerNic.getNetworkId()); + if (nw != null && TrafficType.Guest.equals(nw.getTrafficType())) { + return getNetworkOfferingNetworkRate(nw.getNetworkOfferingId(), dataCenterId); + } + } + return null; + } + + private void backfillNetworkDetailsRates() { + final String sql = "SELECT n.id, n.network_offering_id, n.data_center_id FROM networks n " + + "WHERE n.removed IS NULL AND NOT EXISTS " + + "(SELECT 1 FROM network_details d WHERE d.network_id = n.id AND d.name = ?)"; + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement(sql)) { + pstmt.setString(1, NETWORKRATE_DETAIL_NAME); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + final long networkId = rs.getLong("id"); + final long networkOfferingId = rs.getLong("network_offering_id"); + final long dataCenterId = rs.getLong("data_center_id"); + try { + final int rate = getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId); + networkDetailsDao.addDetail(networkId, NETWORKRATE_DETAIL_NAME, String.valueOf(rate), true); + } catch (Exception e) { + LOGGER.warn("Failed to backfill network_details rate for network id=" + networkId + ": " + e.getMessage()); + } + } + } + } catch (SQLException e) { + LOGGER.warn("Failed to backfill network details rates: " + e.getMessage()); + } + } + + private int getServiceOfferingNetworkRate(long serviceOfferingId, long dataCenterId) { + final ServiceOfferingVO offering = serviceOfferingDao.findById(serviceOfferingId); + Integer rate = offering == null ? null : offering.getRateMbps(); + if (rate == null) { + final String vmType = offering == null ? null : offering.getVmType(); + final String configName = "DomainRouter".equalsIgnoreCase(vmType) ? CONFIG_NETWORK_THROTTLING_RATE : CONFIG_VM_NETWORK_THROTTLING_RATE; + rate = getZoneScopedConfigValue(configName, dataCenterId); + } + return normalizeRate(rate); + } + + private int getNetworkOfferingNetworkRate(long networkOfferingId, long dataCenterId) { + Integer rate = getNetworkOfferingRateMbps(networkOfferingId); + if (rate == null) { + rate = getZoneScopedConfigValue(CONFIG_NETWORK_THROTTLING_RATE, dataCenterId); + } + return normalizeRate(rate); + } + + // NetworkOfferingDaoImpl's constructor is protected, so it can't be instantiated here directly. + private Integer getNetworkOfferingRateMbps(long networkOfferingId) { + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement( + "SELECT nw_rate FROM network_offerings WHERE id = ?")) { + pstmt.setLong(1, networkOfferingId); + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + final Object nwRate = rs.getObject(1); + return nwRate == null ? null : ((Number) nwRate).intValue(); + } + } + } catch (SQLException e) { + LOGGER.warn("Failed to read nw_rate for network offering id=" + networkOfferingId + ": " + e.getMessage()); + } + return null; + } + + private int normalizeRate(int rate) { + return rate == 0 ? -1 : rate; + } + + private int getZoneScopedConfigValue(String name, long dataCenterId) { + final String zoneValue = dataCenterDetailsDao.getConfigValue(dataCenterId, name); + final String value = zoneValue != null ? zoneValue : configurationDao.getValue(name); + return value != null ? Integer.parseInt(value) : DEFAULT_THROTTLING_RATE; + } +} diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java index ac617060eebd..5b703ccd37c7 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.sql.Connection; +import com.cloud.upgrade.NetworkRateBackfill; import com.cloud.utils.exception.CloudRuntimeException; public class Upgrade42300to42400 extends DbUpgradeAbstractImpl implements DbUpgrade { @@ -45,6 +46,7 @@ public InputStream[] getPrepareScripts() { @Override public void performDataMigration(Connection conn) { + new NetworkRateBackfill().backfillNetworkRates(); } @Override diff --git a/server/src/main/java/com/cloud/network/NetworkMigrationManagerImpl.java b/server/src/main/java/com/cloud/network/NetworkMigrationManagerImpl.java index a09867b8ffc3..bc01b42b9fd7 100644 --- a/server/src/main/java/com/cloud/network/NetworkMigrationManagerImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkMigrationManagerImpl.java @@ -24,6 +24,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.cloud.entity.api.db.VMNetworkMapVO; import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; @@ -484,6 +485,8 @@ public Network upgradeNetworkToNewNetworkOffering(long networkId, long newPhysic network.setVpcId(vpcId); } _networksDao.update(network.getId(), network, _networkMgr.finalizeServicesAndProvidersForNetwork(_entityMgr.findById(NetworkOffering.class, networkOfferingId), newPhysicalNetworkId)); + Integer networkRate = _networkModel.getNetworkRate(network.getId(), null); + _networkDetailsDao.addDetail(network.getId(), ApiConstants.NETWORKRATE, String.valueOf(networkRate), true); return network; } diff --git a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java index 7810662c617b..152b92276f4d 100644 --- a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -47,10 +48,12 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.VlanVO; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Networks.TrafficType; import com.cloud.network.addr.PublicIp; import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; @@ -64,14 +67,23 @@ import com.cloud.network.element.VpcVirtualRouterElement; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.offering.NetworkOffering; 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.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.net.Ip; import com.cloud.vm.Nic; import com.cloud.vm.NicProfile; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionHelper; import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement; @@ -101,6 +113,17 @@ public class NetworkModelImplTest { @Mock private PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao; + @Mock + private VMInstanceDao vmInstanceDao; + @Mock + private NicDao nicDao; + @Mock + private ServiceOfferingDao serviceOfferingDao; + @Mock + private ConfigurationManager configMgr; + @Mock + private EntityManager entityMgr; + @Spy @InjectMocks private NetworkModelImpl networkModel = new NetworkModelImpl(); @@ -120,6 +143,11 @@ public void setUp() { networkModel.networkExtensionElement = networkExtensionElement; networkModel._physicalNetworkDao = physicalNetworkDao; networkModel._pNSPDao = physicalNetworkServiceProviderDao; + networkModel._vmDao = vmInstanceDao; + networkModel._nicDao = nicDao; + networkModel._serviceOfferingDao = serviceOfferingDao; + networkModel._configMgr = configMgr; + networkModel._entityMgr = entityMgr; Mockito.lenient().when(extensionHelper.isNetworkExtensionProvider(Mockito.anyString())).thenReturn(false); } @@ -452,4 +480,200 @@ public void listSupportedNetworkServiceProvidersExcludesExtensionBackedProviders Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll(); Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong()); } + + // ----------------------------------------------------------------------- + // Tests for getNetworkRate + // ----------------------------------------------------------------------- + + private NetworkVO mockNetwork(long networkId, long networkOfferingId, long dataCenterId, TrafficType trafficType) { + NetworkVO network = mock(NetworkVO.class); + when(network.getNetworkOfferingId()).thenReturn(networkOfferingId); + when(network.getDataCenterId()).thenReturn(dataCenterId); + when(network.getTrafficType()).thenReturn(trafficType); + when(_networksDao.findById(networkId)).thenReturn(network); + return network; + } + + private void mockNetworkOffering(long networkOfferingId) { + NetworkOffering ntwkOff = mock(NetworkOffering.class); + when(ntwkOff.getId()).thenReturn(networkOfferingId); + when(entityMgr.findById(NetworkOffering.class, networkOfferingId)).thenReturn(ntwkOff); + } + + @Test + public void getNetworkRate_userVmWithNic_returnsServiceOfferingRate() { + long networkId = 1L, vmId = 10L, serviceOfferingId = 20L, dataCenterId = 2L; + mockNetwork(networkId, 30L, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.User); + when(vm.getServiceOfferingId()).thenReturn(serviceOfferingId); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(mock(NicVO.class)); + when(configMgr.getServiceOfferingNetworkRate(serviceOfferingId, dataCenterId)).thenReturn(100); + + assertEquals(Integer.valueOf(100), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_userVmWithoutNic_fallsBackToNetworkOffering() { + long networkId = 1L, vmId = 10L, networkOfferingId = 30L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.User); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(null); + mockNetworkOffering(networkOfferingId); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(50); + + assertEquals(Integer.valueOf(50), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerGuestWithSystemOfferingRate_returnsSystemOfferingRate() { + long networkId = 1L, vmId = 11L, serviceOfferingId = 21L, dataCenterId = 2L; + mockNetwork(networkId, 31L, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vm.getServiceOfferingId()).thenReturn(serviceOfferingId); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(mock(NicVO.class)); + ServiceOfferingVO routerOffering = mock(ServiceOfferingVO.class); + when(routerOffering.getRateMbps()).thenReturn(75); + when(serviceOfferingDao.findById(serviceOfferingId)).thenReturn(routerOffering); + + assertEquals(Integer.valueOf(75), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerGuestWithZeroSystemOfferingRate_returnsUnlimited() { + long networkId = 1L, vmId = 11L, serviceOfferingId = 21L, dataCenterId = 2L; + mockNetwork(networkId, 31L, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vm.getServiceOfferingId()).thenReturn(serviceOfferingId); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(mock(NicVO.class)); + ServiceOfferingVO routerOffering = mock(ServiceOfferingVO.class); + when(routerOffering.getRateMbps()).thenReturn(0); + when(serviceOfferingDao.findById(serviceOfferingId)).thenReturn(routerOffering); + + assertEquals(Integer.valueOf(-1), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerGuestWithoutSystemOfferingRate_fallsBackToNetworkOfferingRate() { + long networkId = 1L, vmId = 11L, serviceOfferingId = 21L, networkOfferingId = 31L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vm.getServiceOfferingId()).thenReturn(serviceOfferingId); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(mock(NicVO.class)); + when(serviceOfferingDao.findById(serviceOfferingId)).thenReturn(null); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(60); + + assertEquals(Integer.valueOf(60), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerGuestWithoutNic_fallsBackToNetworkOfferingViaEntityManager() { + long networkId = 1L, vmId = 11L, networkOfferingId = 31L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.findByNtwkIdAndInstanceId(networkId, vmId)).thenReturn(null); + mockNetworkOffering(networkOfferingId); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(40); + + assertEquals(Integer.valueOf(40), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerPublicWithGuestSibling_returnsGuestNetworkOfferingRate() { + long networkId = 1L, vmId = 12L, dataCenterId = 2L, guestNetworkId = 5L, guestOfferingId = 55L; + mockNetwork(networkId, 99L, dataCenterId, TrafficType.Public); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + NicVO guestNic = mock(NicVO.class); + when(guestNic.getNetworkId()).thenReturn(guestNetworkId); + when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); + NetworkVO guestNetwork = mock(NetworkVO.class); + when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); + when(guestNetwork.getNetworkOfferingId()).thenReturn(guestOfferingId); + when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); + when(configMgr.getNetworkOfferingNetworkRate(guestOfferingId, dataCenterId)).thenReturn(80); + + assertEquals(Integer.valueOf(80), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_routerPublicWithoutGuestSibling_fallsBackToNetworkOfferingViaEntityManager() { + long networkId = 1L, vmId = 12L, networkOfferingId = 99L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Public); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(nicDao.listByVmId(vmId)).thenReturn(Collections.emptyList()); + mockNetworkOffering(networkOfferingId); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(33); + + assertEquals(Integer.valueOf(33), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_consoleProxy_returnsUnlimitedWithoutOfferingLookup() { + long networkId = 1L, vmId = 13L, dataCenterId = 2L; + mockNetwork(networkId, 40L, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.ConsoleProxy); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + + assertEquals(Integer.valueOf(-1), networkModel.getNetworkRate(networkId, vmId)); + Mockito.verifyNoInteractions(configMgr); + } + + @Test + public void getNetworkRate_secondaryStorageVm_returnsUnlimitedWithoutOfferingLookup() { + long networkId = 1L, vmId = 14L, dataCenterId = 2L; + mockNetwork(networkId, 40L, dataCenterId, TrafficType.Guest); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.SecondaryStorageVm); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + + assertEquals(Integer.valueOf(-1), networkModel.getNetworkRate(networkId, vmId)); + Mockito.verifyNoInteractions(configMgr); + } + + @Test + public void getNetworkRate_noVm_usesNetworkOfferingRate() { + long networkId = 1L, networkOfferingId = 30L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + mockNetworkOffering(networkOfferingId); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(90); + + assertEquals(Integer.valueOf(90), networkModel.getNetworkRate(networkId, null)); + } + + @Test + public void getNetworkRate_vmNotFound_fallsBackToNetworkOfferingRate() { + long networkId = 1L, vmId = 15L, networkOfferingId = 30L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + when(vmInstanceDao.findById(vmId)).thenReturn(null); + mockNetworkOffering(networkOfferingId); + when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(77); + + assertEquals(Integer.valueOf(77), networkModel.getNetworkRate(networkId, vmId)); + } + + @Test + public void getNetworkRate_noNetworkOffering_usesGlobalThrottlingRate() { + long networkId = 1L, networkOfferingId = 30L, dataCenterId = 2L; + mockNetwork(networkId, networkOfferingId, dataCenterId, TrafficType.Guest); + when(entityMgr.findById(NetworkOffering.class, networkOfferingId)).thenReturn(null); + + Integer expected = NetworkOrchestrationService.NetworkThrottlingRate.valueIn(dataCenterId); + assertEquals(expected, networkModel.getNetworkRate(networkId, null)); + } } From 817c2f9e77e1b70a55786302800b2821dfb5e2dd Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 16 Sep 2026 15:09:28 +0530 Subject: [PATCH 07/29] ui: show Unlimited instead of -1 for network rate --- ui/src/components/view/DetailsTab.vue | 3 +++ ui/src/components/view/ListView.vue | 3 +++ ui/src/views/network/NicsTable.vue | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index d2aabacb10c9..5e3ce28dad07 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -147,6 +147,9 @@
{{ dataResource[item].join(', ') }}
+
+ {{ String(dataResource[item]) === '-1' ? $t('label.unlimited') : dataResource[item] }} +
{{ dataResource[item] }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 9a7d874fef3e..ab737fc51893 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -424,6 +424,9 @@ {{ isNaN(text) ? text : (parseFloat(text) / 1024.0).toFixed(2) + ' GB' }} + diff --git a/ui/src/views/network/NicsTable.vue b/ui/src/views/network/NicsTable.vue index d6505539e8c0..ef3cf8e63994 100644 --- a/ui/src/views/network/NicsTable.vue +++ b/ui/src/views/network/NicsTable.vue @@ -58,7 +58,7 @@ {{ record.isolationuri }} - {{ record.networkrate }} + {{ String(record.networkrate) === '-1' ? $t('label.unlimited') : record.networkrate }} From 4c8b4efd0c4e591f40d9f83c21c49067c143dd56 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 16 Sep 2026 16:00:14 +0530 Subject: [PATCH 08/29] Fix backfill NPE and statement auto-close bug found via e2e testing --- .../java/com/cloud/upgrade/NetworkRateBackfill.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java index 7c4ff7d9348b..abc0a592d327 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationDaoImpl; +import com.cloud.dc.DataCenterDetailVO; import com.cloud.dc.dao.DataCenterDetailsDaoImpl; import com.cloud.network.Networks.TrafficType; import com.cloud.network.dao.NetworkDao; @@ -76,7 +77,7 @@ public void backfillNetworkRates() { private void backfillNicNetworkRates() { final String sql = "SELECT id, network_id, instance_id, default_nic FROM nics " + "WHERE removed IS NULL AND network_rate IS NULL AND instance_id IS NOT NULL"; - try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement(sql); + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { final long nicId = rs.getLong("id"); @@ -98,7 +99,7 @@ private void backfillNicNetworkRates() { } private void updateNicNetworkRate(long nicId, int rate) throws SQLException { - try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement( + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement( "UPDATE nics SET network_rate = ? WHERE id = ?")) { pstmt.setInt(1, rate); pstmt.setLong(2, nicId); @@ -148,7 +149,7 @@ private void backfillNetworkDetailsRates() { final String sql = "SELECT n.id, n.network_offering_id, n.data_center_id FROM networks n " + "WHERE n.removed IS NULL AND NOT EXISTS " + "(SELECT 1 FROM network_details d WHERE d.network_id = n.id AND d.name = ?)"; - try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement(sql)) { + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement(sql)) { pstmt.setString(1, NETWORKRATE_DETAIL_NAME); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { @@ -189,7 +190,7 @@ private int getNetworkOfferingNetworkRate(long networkOfferingId, long dataCente // NetworkOfferingDaoImpl's constructor is protected, so it can't be instantiated here directly. private Integer getNetworkOfferingRateMbps(long networkOfferingId) { - try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareAutoCloseStatement( + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement( "SELECT nw_rate FROM network_offerings WHERE id = ?")) { pstmt.setLong(1, networkOfferingId); try (ResultSet rs = pstmt.executeQuery()) { @@ -209,8 +210,8 @@ private int normalizeRate(int rate) { } private int getZoneScopedConfigValue(String name, long dataCenterId) { - final String zoneValue = dataCenterDetailsDao.getConfigValue(dataCenterId, name); - final String value = zoneValue != null ? zoneValue : configurationDao.getValue(name); + final DataCenterDetailVO detail = dataCenterDetailsDao.findDetail(dataCenterId, name); + final String value = detail != null ? detail.getValue() : configurationDao.getValue(name); return value != null ? Integer.parseInt(value) : DEFAULT_THROTTLING_RATE; } } From 7a4e287c72301b49ef31f57268878a1dfaa67acf Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 17 Sep 2026 12:04:17 +0530 Subject: [PATCH 09/29] Support throttling a VPC's public gateway network rate Adds an operator-configurable data transfer rate for a VPC's public/internet-facing network, independent of the intra-VPC tier rates that network offerings already control. Precedence: vpc_offerings.public_nw_rate (per-offering override) falls back to the new zone-scoped config vpc.public.network.throttling.rate (default 0/unlimited, preserving today's unthrottled behavior on upgrade). - createVPCOffering/updateVPCOffering/cloneVPCOffering accept publicnetworkrate - listVPCOfferings/listVPCs expose it, normalized to -1 for unlimited - vpc_details "publicnetworkrate" cache is (re)computed only where a VPC's offering can actually change: at creation and in restartVpc's make-redundant path - backfills existing VPCs to -1 on upgrade, since the new column/config can't yet hold a value - UI support for setting/viewing the rate on VPC offerings and VPCs --- .../com/cloud/network/vpc/VpcOffering.java | 2 + .../network/vpc/VpcProvisioningService.java | 2 +- .../apache/cloudstack/api/ApiConstants.java | 1 + .../admin/vpc/CreateVPCOfferingCmd.java | 9 +++ .../admin/vpc/UpdateVPCOfferingCmd.java | 9 +++ .../api/response/VpcOfferingResponse.java | 8 +++ .../cloudstack/api/response/VpcResponse.java | 8 +++ .../service/NetworkOrchestrationService.java | 3 + .../configuration/ConfigurationManager.java | 2 + .../orchestration/NetworkOrchestrator.java | 2 +- .../com/cloud/network/vpc/VpcOfferingVO.java | 12 ++++ .../cloud/upgrade/NetworkRateBackfill.java | 37 +++++++++++- .../META-INF/db/schema-42300to42400.sql | 2 + .../db/views/cloud.vpc_offering_view.sql | 1 + .../management/ContrailManagerImpl.java | 2 +- .../java/com/cloud/api/ApiResponseHelper.java | 9 +++ .../api/query/dao/VpcOfferingJoinDaoImpl.java | 1 + .../cloud/api/query/vo/VpcOfferingJoinVO.java | 8 +++ .../ConfigurationManagerImpl.java | 27 +++++++++ .../com/cloud/network/NetworkModelImpl.java | 6 ++ .../com/cloud/network/vpc/VpcManagerImpl.java | 60 ++++++++++++++----- .../cloud/network/NetworkModelImplTest.java | 46 ++++++++++++++ .../cloud/network/vpc/VpcManagerImplTest.java | 24 ++++++++ .../vpc/MockConfigurationManagerImpl.java | 6 ++ ui/public/locales/en.json | 1 + ui/src/config/section/network.js | 2 +- ui/src/config/section/offering.js | 4 +- ui/src/views/offering/AddVpcOffering.vue | 18 ++++++ 28 files changed, 289 insertions(+), 23 deletions(-) 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 f84602232159..faeba6280ab8 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java @@ -86,4 +86,6 @@ public enum State { Boolean isSpecifyAsNumber(); boolean isConserveMode(); + + Integer getPublicNetworkRate(); } 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 891cfb02d9df..8bde0fefeb34 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java @@ -42,7 +42,7 @@ VpcOffering createVpcOffering(String name, String displayText, List supp Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol, Long serviceOfferingId, String externalProvider, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, VpcOffering.State state, - NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode); + NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode, Integer publicNetworkRate); Pair,Integer> listVpcOfferings(ListVPCOfferingsCmd cmd); 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 f74c46161180..da0398ebd058 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -729,6 +729,7 @@ public class ApiConstants { public static final String IS_USER_DEFINED = "isuserdefined"; public static final String AVAILABILITY = "availability"; public static final String NETWORKRATE = "networkrate"; + public static final String PUBLIC_NETWORK_RATE = "publicnetworkrate"; public static final String HOST_TAGS = "hosttags"; public static final String SSH_KEYPAIR = "keypair"; public static final String SSH_KEYPAIRS = "keypairs"; 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 2b934a60da7a..87c99203e1be 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 @@ -165,6 +165,11 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { 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; + @Parameter(name = ApiConstants.PUBLIC_NETWORK_RATE, type = CommandType.INTEGER, + since = "4.24.0", + description = "Data transfer rate in megabits per second allowed for a VPC's public gateway (internet-facing network), created with this offering. Default is unlimited") + private Integer publicNetworkRate; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -318,6 +323,10 @@ public boolean isConserveMode() { return BooleanUtils.toBoolean(conserveMode); } + public Integer getPublicNetworkRate() { + return publicNetworkRate; + } + @Override public void create() throws ResourceAllocationException { VpcOffering vpcOff = _vpcProvSvc.createVpcOffering(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java index 97f30f6fa2ef..201fc91da910 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java @@ -68,6 +68,11 @@ public class UpdateVPCOfferingCmd extends BaseAsyncCmd implements DomainAndZoneI @Parameter(name = ApiConstants.SORT_KEY, type = CommandType.INTEGER, description = "Sort key of the VPC offering, integer") private Integer sortKey; + @Parameter(name = ApiConstants.PUBLIC_NETWORK_RATE, type = CommandType.INTEGER, + since = "4.24.0", + description = "Data transfer rate in megabits per second allowed for a VPC's public gateway (internet-facing network), created with this offering. Use 0 for unlimited") + private Integer publicNetworkRate; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -100,6 +105,10 @@ public Integer getSortKey() { return sortKey; } + public Integer getPublicNetworkRate() { + return publicNetworkRate; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// 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 2e821dae52de..4ef9dde12fd5 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 @@ -106,6 +106,10 @@ public class VpcOfferingResponse extends BaseResponse { @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; + @SerializedName(ApiConstants.PUBLIC_NETWORK_RATE) + @Param(description = "Data transfer rate in megabits per second allowed for a VPC's public gateway (internet-facing network), created with this offering; null if not set (falls back to the zone/global default)", since = "4.24.0") + private Integer publicNetworkRate; + public void setId(String id) { this.id = id; } @@ -213,4 +217,8 @@ public Boolean getConserveMode() { public void setConserveMode(Boolean conserveMode) { this.conserveMode = conserveMode; } + + public void setPublicNetworkRate(Integer publicNetworkRate) { + this.publicNetworkRate = publicNetworkRate; + } } 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 34d50d5b9f92..4e3eb5c656f0 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 @@ -77,6 +77,10 @@ public class VpcResponse extends BaseResponseWithAnnotations implements Controll @Param(description = "true if VPC offering is ip conserve mode enabled", since = "4.23") private Boolean vpcOfferingConserveMode; + @SerializedName(ApiConstants.PUBLIC_NETWORK_RATE) + @Param(description = "Data transfer rate in megabits per second allowed for this VPC's public gateway (internet-facing network); -1 if unlimited", since = "4.24.0") + private Integer publicNetworkRate; + @SerializedName(ApiConstants.CREATED) @Param(description = "The date this VPC was created") private Date created; @@ -209,6 +213,10 @@ public void setVpcOfferingConserveMode(Boolean vpcOfferingConserveMode) { this.vpcOfferingConserveMode = vpcOfferingConserveMode; } + public void setPublicNetworkRate(Integer publicNetworkRate) { + this.publicNetworkRate = publicNetworkRate; + } + public void setCreated(final Date created) { this.created = created; } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 109a44488ec4..088f6b93d19b 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -138,6 +138,9 @@ public interface NetworkOrchestrationService { ConfigKey VmNetworkThrottlingRate = new ConfigKey("Network", Integer.class, "vm.network.throttling.rate", "200", "Default data transfer rate in megabits per second allowed in User vm's default network.", true, ConfigKey.Scope.Zone); + ConfigKey VpcPublicNetworkThrottlingRate = new ConfigKey<>("Network", Integer.class, "vpc.public.network.throttling.rate", "0", + "Default data transfer rate in megabits per second allowed for a VPC's public/internet-facing network. 0 means unlimited.", true, ConfigKey.Scope.Zone); + List setupNetwork(Account owner, NetworkOffering offering, DeploymentPlan plan, String name, String displayText, boolean isDefault) throws ConcurrentOperationException; diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java index 5909d098db8b..73213c10be33 100644 --- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java +++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java @@ -82,6 +82,8 @@ public interface ConfigurationManager { Integer getServiceOfferingNetworkRate(long serviceOfferingId, Long dataCenterId); + Integer getVpcOfferingNetworkRate(long vpcOfferingId, Long dataCenterId); + /** * Updates a configuration entry with a new value */ diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index ae74c8f49027..1138ddcce8ec 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -5075,7 +5075,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes, - GuestDomainSuffix, NetworkThrottlingRate, VmNetworkThrottlingRate, MinVRVersion, DhcpLeaseTimeout, + GuestDomainSuffix, NetworkThrottlingRate, VmNetworkThrottlingRate, VpcPublicNetworkThrottlingRate, MinVRVersion, DhcpLeaseTimeout, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, NETWORK_LB_HAPROXY_IDLE_TIMEOUT}; 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 b913468384e4..0f7bbb7f11fd 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 @@ -94,6 +94,9 @@ public class VpcOfferingVO implements VpcOffering { @Column(name = "conserve_mode") private boolean conserveMode; + @Column(name = "public_nw_rate") + private Integer publicNetworkRate; + public VpcOfferingVO() { this.uuid = UUID.randomUUID().toString(); } @@ -254,4 +257,13 @@ public boolean isConserveMode() { public void setConserveMode(boolean conserveMode) { this.conserveMode = conserveMode; } + + @Override + public Integer getPublicNetworkRate() { + return publicNetworkRate; + } + + public void setPublicNetworkRate(Integer publicNetworkRate) { + this.publicNetworkRate = publicNetworkRate; + } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java index abc0a592d327..31bd3ee827f2 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -26,6 +26,8 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationDaoImpl; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDaoImpl; import com.cloud.dc.DataCenterDetailVO; import com.cloud.dc.dao.DataCenterDetailsDaoImpl; @@ -49,9 +51,12 @@ /** * Backfills {@code nics.network_rate} and the {@code network_details} "networkrate" entry for - * pre-existing NICs/networks. Deliberately frozen to the pre-feature precedence of + * pre-existing NICs/networks, deliberately frozen to the pre-feature precedence of * {@link com.cloud.network.NetworkModelImpl#getNetworkRate} - do not redirect this to call the - * live method, whose precedence will keep evolving. + * live method, whose precedence will keep evolving. Also backfills the {@code vpc_details} + * "publicnetworkrate" entry for pre-existing VPCs with a fixed "unlimited" value, since both + * {@code vpc_offerings.public_nw_rate} and the "vpc.public.network.throttling.rate" config are + * introduced by this same release and can't yet hold a pre-existing value. */ public class NetworkRateBackfill { protected static Logger LOGGER = LogManager.getLogger(NetworkRateBackfill.class); @@ -59,12 +64,15 @@ public class NetworkRateBackfill { private static final String CONFIG_NETWORK_THROTTLING_RATE = "network.throttling.rate"; private static final String CONFIG_VM_NETWORK_THROTTLING_RATE = "vm.network.throttling.rate"; private static final String NETWORKRATE_DETAIL_NAME = "networkrate"; + private static final String PUBLIC_NETWORK_RATE_DETAIL_NAME = "publicnetworkrate"; private static final int DEFAULT_THROTTLING_RATE = 200; + private static final int UNLIMITED_RATE = -1; private final NicDao nicDao = new NicDaoImpl(); private final VMInstanceDao vmInstanceDao = new VMInstanceDaoImpl(); private final NetworkDao networkDao = new NetworkDaoImpl(); private final NetworkDetailsDao networkDetailsDao = new NetworkDetailsDaoImpl(); + private final VpcDetailsDao vpcDetailsDao = new VpcDetailsDaoImpl(); private final ServiceOfferingDao serviceOfferingDao = new ServiceOfferingDaoImpl(); private final DataCenterDetailsDaoImpl dataCenterDetailsDao = new DataCenterDetailsDaoImpl(); private final ConfigurationDao configurationDao = new ConfigurationDaoImpl(); @@ -72,6 +80,7 @@ public class NetworkRateBackfill { public void backfillNetworkRates() { backfillNicNetworkRates(); backfillNetworkDetailsRates(); + backfillVpcPublicNetworkRates(); } private void backfillNicNetworkRates() { @@ -169,6 +178,30 @@ private void backfillNetworkDetailsRates() { } } + // vpc_offerings.public_nw_rate and the "vpc.public.network.throttling.rate" config are both introduced + // by this same release, so no pre-existing VPC offering or config value can be set at backfill + // time - every pre-existing VPC's effective public network rate is unconditionally unlimited. + private void backfillVpcPublicNetworkRates() { + final String sql = "SELECT v.id FROM vpc v " + + "WHERE v.removed IS NULL AND NOT EXISTS " + + "(SELECT 1 FROM vpc_details d WHERE d.vpc_id = v.id AND d.name = ?)"; + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement(sql)) { + pstmt.setString(1, PUBLIC_NETWORK_RATE_DETAIL_NAME); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + final long vpcId = rs.getLong("id"); + try { + vpcDetailsDao.addDetail(vpcId, PUBLIC_NETWORK_RATE_DETAIL_NAME, String.valueOf(UNLIMITED_RATE), true); + } catch (Exception e) { + LOGGER.warn("Failed to backfill vpc_details public network rate for vpc id=" + vpcId + ": " + e.getMessage()); + } + } + } + } catch (SQLException e) { + LOGGER.warn("Failed to backfill vpc public network rates: " + e.getMessage()); + } + } + private int getServiceOfferingNetworkRate(long serviceOfferingId, long dataCenterId) { final ServiceOfferingVO offering = serviceOfferingDao.findById(serviceOfferingId); Integer rate = offering == null ? null : offering.getRateMbps(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql index ea0c8e62ca0f..f6538848354e 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql @@ -18,3 +18,5 @@ -- Schema upgrade from 4.23.0.0 to 4.24.0.0 ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int unsigned DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC'; + +ALTER TABLE `cloud`.`vpc_offerings` ADD COLUMN `public_nw_rate` smallint unsigned DEFAULT NULL COMMENT 'public gateway (internet-facing) network rate throttle mbits/s'; 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 3669bb10122b..161410dc377d 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 @@ -39,6 +39,7 @@ select `vpc_offerings`.`routing_mode` AS `routing_mode`, `vpc_offerings`.`specify_as_number` AS `specify_as_number`, `vpc_offerings`.`conserve_mode` AS `conserve_mode`, + `vpc_offerings`.`public_nw_rate` AS `public_nw_rate`, 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/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 8badb916eeda..11917a8aa50c 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, false); + vpcOffer = _vpcProvSvc.createVpcOffering(juniperVPCOfferingName, juniperVPCOfferingDisplayText, services, serviceProviderMap, null, null, null, null, null, null, null, VpcOffering.State.Enabled, null, false, false, null); 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 c16e269ef801..accd0e353d26 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -225,6 +225,8 @@ import org.apache.cloudstack.region.PortableIp; import org.apache.cloudstack.region.PortableIpRange; import org.apache.cloudstack.region.Region; +import org.apache.cloudstack.resourcedetail.VpcDetailVO; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; import org.apache.cloudstack.secstorage.heuristics.Heuristic; import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao; import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO; @@ -501,6 +503,8 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport { @Inject NetworkDetailsDao networkDetailsDao; @Inject + VpcDetailsDao vpcDetailsDao; + @Inject private VMSnapshotDao vmSnapshotDao; @Inject private BackupOfferingDao backupOfferingDao; @@ -3597,6 +3601,11 @@ public VpcResponse createVpcResponse(ResponseView view, Vpc vpc) { response.setVpcOfferingName(voff.getName()); response.setVpcOfferingConserveMode(voff.isConserveMode()); } + VpcDetailVO publicNetworkRateDetail = vpcDetailsDao.findDetail(vpc.getId(), ApiConstants.PUBLIC_NETWORK_RATE); + if (publicNetworkRateDetail != null) { + int publicNetworkRate = NumberUtils.toInt(publicNetworkRateDetail.getValue(), -1); + response.setPublicNetworkRate(publicNetworkRate > 0 ? publicNetworkRate : -1); + } response.setCidr(vpc.getCidr()); response.setRestartRequired(vpc.isRestartRequired()); response.setNetworkDomain(vpc.getNetworkDomain()); 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 e7fe07a18c78..bd14471351e3 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 @@ -78,6 +78,7 @@ public VpcOfferingResponse newVpcOfferingResponse(VpcOffering offering) { offeringResponse.setSpecifyAsNumber(offering.isSpecifyAsNumber()); } offeringResponse.setConserveMode(offering.isConserveMode()); + offeringResponse.setPublicNetworkRate(offering.getPublicNetworkRate()); 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 9d65c19479fb..144fa6e0f490 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 @@ -115,6 +115,9 @@ public class VpcOfferingJoinVO implements VpcOffering { @Column(name = "conserve_mode") private boolean conserveMode; + @Column(name = "public_nw_rate") + private Integer publicNetworkRate; + public VpcOfferingJoinVO() { } @@ -186,6 +189,11 @@ public boolean isConserveMode() { return conserveMode; } + @Override + public Integer getPublicNetworkRate() { + return publicNetworkRate; + } + 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 c68dc390df2a..fa27ce30fb6c 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -261,6 +261,7 @@ import com.cloud.network.netris.NetrisService; import com.cloud.network.rules.LoadBalancerContainer.Scheme; import com.cloud.network.vpc.VpcManager; +import com.cloud.network.vpc.VpcOffering; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.NetworkOffering.Availability; @@ -9064,6 +9065,32 @@ public Integer getNetworkOfferingNetworkRate(final long networkOfferingId, final return networkRate; } + @Override + public Integer getVpcOfferingNetworkRate(final long vpcOfferingId, final Long dataCenterId) { + + final VpcOffering vpcOffering = _entityMgr.findById(VpcOffering.class, vpcOfferingId); + if (vpcOffering == null) { + throw new InvalidParameterValueException("Unable to find VPC offering by id=" + vpcOfferingId); + } + + Integer networkRate; + if (vpcOffering.getPublicNetworkRate() != null) { + networkRate = vpcOffering.getPublicNetworkRate(); + } else { + networkRate = NetworkOrchestrationService.VpcPublicNetworkThrottlingRate.valueIn(dataCenterId); + } + + // networkRate is unsigned int in vpc_offerings table, and can't be + // set to -1 + // so 0 means unlimited; we convert it to -1, so we are consistent with + // all our other resources where -1 means unlimited + if (networkRate == 0) { + networkRate = -1; + } + + return networkRate; + } + @Override public Account getVlanAccount(final long vlanId) { final Vlan vlan = _vlanDao.findById(vlanId); diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index d0456e0e9de3..8f4d4be61b37 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -1239,6 +1239,12 @@ public Integer getNetworkRate(long networkId, Long vmId) { for (final Nic routerNic : routerNics) { final NetworkVO nw = _networksDao.findById(routerNic.getNetworkId()); if (TrafficType.Guest.equals(nw.getTrafficType())) { + if (nw.getVpcId() != null) { + final Vpc vpc = vpcDao.findById(nw.getVpcId()); + if (vpc != null) { + return _configMgr.getVpcOfferingNetworkRate(vpc.getVpcOfferingId(), network.getDataCenterId()); + } + } return _configMgr.getNetworkOfferingNetworkRate(nw.getNetworkOfferingId(), network.getDataCenterId()); } } 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 bcf2c6176efe..9c7d5171ff6a 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -74,6 +74,7 @@ import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.ObjectUtils; @@ -279,6 +280,10 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis @Inject DataCenterDao _dcDao; @Inject + ConfigurationManager _configMgr; + @Inject + VpcDetailsDao vpcDetailsDao; + @Inject NetworkACLDao _networkAclDao; @Inject NetworkACLManager _networkAclMgr; @@ -395,7 +400,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, false, null, null, false, false, null); } // configure default vpc offering with Netscaler as LB Provider @@ -415,7 +420,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.defaultVPCNSOfferingName, VpcOffering.defaultVPCNSOfferingName, - svcProviderMap, false, State.Enabled, null, false, false, false, null, null, false, false); + svcProviderMap, false, State.Enabled, null, false, false, false, null, null, false, false, null); } @@ -436,7 +441,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } createVpcOffering(VpcOffering.redundantVPCOfferingName, VpcOffering.redundantVPCOfferingName, svcProviderMap, true, State.Enabled, - null, false, false, true, null, null, false, false); + null, false, false, true, null, null, false, false, null); } // configure default vpc offering with NSX as network service provider in NAT mode @@ -453,7 +458,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, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false, false, null); } @@ -471,7 +476,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, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false, false, null); } @@ -489,7 +494,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, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.ROUTED, null, false, false, null); } @@ -507,7 +512,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, false); + State.Enabled, null, false, false, false, NetworkOffering.NetworkMode.NATTED, null, false, false, null); } } @@ -594,6 +599,10 @@ public VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd) { boolean specifyAsNumber = cmd.getSpecifyAsNumber(); String routingModeString = cmd.getRoutingMode(); boolean conserveMode = cmd.isConserveMode(); + Integer publicNetworkRate = cmd.getPublicNetworkRate(); + if (publicNetworkRate != null && publicNetworkRate < 0) { + throw new InvalidParameterValueException("Failed to create VPC offering " + vpcOfferingName + ": specify the public network rate value as 0 or more"); + } // check if valid domain if (CollectionUtils.isNotEmpty(cmd.getDomainIds())) { @@ -632,7 +641,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, conserveMode); + domainIds, zoneIds, (enable ? State.Enabled : State.Disabled), routingMode, specifyAsNumber, conserveMode, publicNetworkRate); } @Override @@ -640,7 +649,7 @@ 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, boolean conserveMode) { + NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode, Integer publicNetworkRate) { boolean isExternalProvider = externalProvider != null && Arrays.asList("NSX", "Netris").stream().anyMatch(s -> s.equalsIgnoreCase(externalProvider)); @@ -741,7 +750,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, conserveMode); + redundantRouter, networkMode, routingMode, specifyAsNumber, conserveMode, publicNetworkRate); if (offering != null) { List detailsVO = new ArrayList<>(); @@ -769,7 +778,8 @@ 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, boolean conserveMode) { + final boolean redundantRouter, NetworkOffering.NetworkMode networkMode, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode, + Integer publicNetworkRate) { return Transaction.execute(new TransactionCallback() { @Override @@ -786,6 +796,7 @@ public VpcOfferingVO doInTransaction(final TransactionStatus status) { offering.setRoutingMode(routingMode); } offering.setConserveMode(conserveMode); + offering.setPublicNetworkRate(publicNetworkRate); logger.debug("Adding vpc offering " + offering); offering = _vpcOffDao.persist(offering); @@ -1094,6 +1105,10 @@ private void applyResolvedValuesToCommand(CloneVPCOfferingCmd cmd, VpcOfferingVO ConfigurationManagerImpl.setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString()); } + if (cmd.getPublicNetworkRate() == null && sourceOffering.getPublicNetworkRate() != null) { + ConfigurationManagerImpl.setField(cmd, "publicNetworkRate", sourceOffering.getPublicNetworkRate()); + } + if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) { if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) { ConfigurationManagerImpl.setField(cmd, "domainIds", sourceDomainIds); @@ -1433,7 +1448,7 @@ public boolean deleteVpcOffering(final long offId) { @Override @ActionEvent(eventType = EventTypes.EVENT_VPC_OFFERING_UPDATE, eventDescription = "updating vpc offering") public VpcOffering updateVpcOffering(long vpcOffId, String vpcOfferingName, String displayText, String state) { - return updateVpcOfferingInternal(vpcOffId, vpcOfferingName, displayText, state, null, null, null); + return updateVpcOfferingInternal(vpcOffId, vpcOfferingName, displayText, state, null, null, null, null); } @Override @@ -1446,6 +1461,10 @@ public VpcOffering updateVpcOffering(final UpdateVPCOfferingCmd cmd) { final List domainIds = cmd.getDomainIds(); final List zoneIds = cmd.getZoneIds(); final Integer sortKey = cmd.getSortKey(); + final Integer publicNetworkRate = cmd.getPublicNetworkRate(); + if (publicNetworkRate != null && publicNetworkRate < 0) { + throw new InvalidParameterValueException("Failed to update VPC offering " + offeringId + ": specify the public network rate value as 0 or more"); + } // check if valid domain if (CollectionUtils.isNotEmpty(domainIds)) { @@ -1464,10 +1483,11 @@ public VpcOffering updateVpcOffering(final UpdateVPCOfferingCmd cmd) { } } - return updateVpcOfferingInternal(offeringId, vpcOfferingName, displayText, state, sortKey, domainIds, zoneIds); + return updateVpcOfferingInternal(offeringId, vpcOfferingName, displayText, state, sortKey, domainIds, zoneIds, publicNetworkRate); } - private VpcOffering updateVpcOfferingInternal(long vpcOffId, String vpcOfferingName, String displayText, String state, Integer sortKey, final List domainIds, final List zoneIds) { + private VpcOffering updateVpcOfferingInternal(long vpcOffId, String vpcOfferingName, String displayText, String state, Integer sortKey, final List domainIds, final List zoneIds, + Integer publicNetworkRate) { // Verify input parameters final VpcOfferingVO offeringToUpdate = _vpcOffDao.findById(vpcOffId); if (offeringToUpdate == null) { @@ -1492,7 +1512,7 @@ private VpcOffering updateVpcOfferingInternal(long vpcOffId, String vpcOfferingN } Collections.sort(filteredZoneIds); - final boolean updateNeeded = vpcOfferingName != null || displayText != null || state != null || sortKey != null; + final boolean updateNeeded = vpcOfferingName != null || displayText != null || state != null || sortKey != null || publicNetworkRate != null; final VpcOfferingVO offering = _vpcOffDao.createForUpdate(vpcOffId); @@ -1518,6 +1538,9 @@ private VpcOffering updateVpcOfferingInternal(long vpcOffId, String vpcOfferingN if (sortKey != null) { offering.setSortKey(sortKey); } + if (publicNetworkRate != null) { + offering.setPublicNetworkRate(publicNetworkRate); + } if (!_vpcOffDao.update(vpcOffId, offering)) { return null; @@ -1841,10 +1864,16 @@ public VpcVO doInTransaction(final TransactionStatus status) { }); if (vpcVO != null) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPC_CREATE, vpcVO.getAccountId(), vpcVO.getZoneId(), vpcVO.getId(), vpcVO.getName(), Vpc.class.getName(), vpcVO.getUuid(), vpcVO.isDisplay()); + saveVpcNetworkRateInDetails(vpcVO); } return vpcVO; } + private void saveVpcNetworkRateInDetails(Vpc vpc) { + final Integer rate = _configMgr.getVpcOfferingNetworkRate(vpc.getVpcOfferingId(), vpc.getZoneId()); + vpcDetailsDao.addDetail(vpc.getId(), ApiConstants.PUBLIC_NETWORK_RATE, String.valueOf(rate), true); + } + private Map> finalizeServicesAndProvidersForVpc(final long zoneId, final long offeringId) { final Map> svcProviders = new HashMap<>(); final List servicesMap = _vpcOffSvcMapDao.listByVpcOffId(offeringId); @@ -2810,6 +2839,7 @@ public boolean restartVpc(Long vpcId, boolean cleanUp, boolean makeRedundant, bo // the restart procedure. if (vpcDao.update(vpc.getId(), entity)) { vpc = entity; + saveVpcNetworkRateInDetails(vpc); } // If the offering and redundant column are changing, force the diff --git a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java index 152b92276f4d..0d4af01fd65e 100644 --- a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java @@ -601,6 +601,7 @@ public void getNetworkRate_routerPublicWithGuestSibling_returnsGuestNetworkOffer when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); NetworkVO guestNetwork = mock(NetworkVO.class); when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); + when(guestNetwork.getVpcId()).thenReturn(null); when(guestNetwork.getNetworkOfferingId()).thenReturn(guestOfferingId); when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); when(configMgr.getNetworkOfferingNetworkRate(guestOfferingId, dataCenterId)).thenReturn(80); @@ -622,6 +623,51 @@ public void getNetworkRate_routerPublicWithoutGuestSibling_fallsBackToNetworkOff assertEquals(Integer.valueOf(33), networkModel.getNetworkRate(networkId, vmId)); } + @Test + public void getNetworkRate_routerPublicWithVpcGuestSibling_returnsVpcOfferingRate() { + long networkId = 1L, vmId = 12L, dataCenterId = 2L, guestNetworkId = 5L, vpcId = 7L, vpcOfferingId = 70L; + mockNetwork(networkId, 99L, dataCenterId, TrafficType.Public); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + NicVO guestNic = mock(NicVO.class); + when(guestNic.getNetworkId()).thenReturn(guestNetworkId); + when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); + NetworkVO guestNetwork = mock(NetworkVO.class); + when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); + when(guestNetwork.getVpcId()).thenReturn(vpcId); + when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); + VpcVO vpc = mock(VpcVO.class); + when(vpc.getVpcOfferingId()).thenReturn(vpcOfferingId); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + when(configMgr.getVpcOfferingNetworkRate(vpcOfferingId, dataCenterId)).thenReturn(120); + + assertEquals(Integer.valueOf(120), networkModel.getNetworkRate(networkId, vmId)); + Mockito.verify(configMgr, Mockito.never()).getNetworkOfferingNetworkRate(Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void getNetworkRate_routerPublicWithVpcGuestSiblingButMissingVpc_fallsBackToNetworkOfferingRate() { + long networkId = 1L, vmId = 12L, dataCenterId = 2L, guestNetworkId = 5L, vpcId = 7L, guestOfferingId = 55L; + mockNetwork(networkId, 99L, dataCenterId, TrafficType.Public); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + NicVO guestNic = mock(NicVO.class); + when(guestNic.getNetworkId()).thenReturn(guestNetworkId); + when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); + NetworkVO guestNetwork = mock(NetworkVO.class); + when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); + when(guestNetwork.getVpcId()).thenReturn(vpcId); + when(guestNetwork.getNetworkOfferingId()).thenReturn(guestOfferingId); + when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); + when(vpcDao.findById(vpcId)).thenReturn(null); + when(configMgr.getNetworkOfferingNetworkRate(guestOfferingId, dataCenterId)).thenReturn(90); + + assertEquals(Integer.valueOf(90), networkModel.getNetworkRate(networkId, vmId)); + Mockito.verify(configMgr, Mockito.never()).getVpcOfferingNetworkRate(Mockito.anyLong(), Mockito.anyLong()); + } + @Test public void getNetworkRate_consoleProxy_returnsUnlimitedWithoutOfferingLookup() { long networkId = 1L, vmId = 13L, dataCenterId = 2L; 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 2acad0c2b45d..507833bcfee2 100644 --- a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java @@ -23,6 +23,7 @@ import com.cloud.agent.api.to.IpAddressTO; import com.cloud.agent.manager.Commands; import com.cloud.alert.AlertManager; +import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.DataCenterVO; import com.cloud.dc.VlanVO; import com.cloud.dc.dao.DataCenterDao; @@ -70,6 +71,7 @@ import com.cloud.vm.dao.NicDao; import org.apache.cloudstack.acl.SecurityChecker; 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.UpdateVPCCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -78,6 +80,7 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.network.Ipv4GuestSubnetNetworkMap; import org.apache.cloudstack.network.RoutedIpv4Manager; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -171,6 +174,10 @@ public class VpcManagerImplTest { NetworkACLVO networkACLVOMock; @Mock RoutedIpv4Manager routedIpv4Manager; + @Mock + ConfigurationManager configMgr; + @Mock + VpcDetailsDao vpcDetailsDao; public static final long ACCOUNT_ID = 1; private AccountVO account; @@ -230,6 +237,8 @@ public void setup() throws NoSuchFieldException, IllegalAccessException { manager._ntwkSvc = networkServiceMock; manager._firewallDao = firewallDao; manager._networkAclDao = networkACLDaoMock; + manager._configMgr = configMgr; + manager.vpcDetailsDao = vpcDetailsDao; manager.routedIpv4Manager = routedIpv4Manager; CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class)); registerCallContext(); @@ -486,6 +495,21 @@ public void testDisabledConfigCreateIpv6VpcOffering() { manager.createVpcOffering(cmd); } + @Test(expected = InvalidParameterValueException.class) + public void testCreateVpcOfferingRejectsNegativeNetworkRate() { + CreateVPCOfferingCmd cmd = Mockito.mock(CreateVPCOfferingCmd.class); + Mockito.when(cmd.getPublicNetworkRate()).thenReturn(-5); + manager.createVpcOffering(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateVpcOfferingRejectsNegativeNetworkRate() { + UpdateVPCOfferingCmd cmd = Mockito.mock(UpdateVPCOfferingCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getPublicNetworkRate()).thenReturn(-5); + manager.updateVpcOffering(cmd); + } + private void mockVpcDnsResources(boolean supportDnsService, boolean isIpv6) { Mockito.when(accountManager.getAccount(vpcOwnerId)).thenReturn(account); vpcOfferingVO = Mockito.mock(VpcOfferingVO.class); diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index a8d3927f910e..79cb9d6da76e 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -445,6 +445,12 @@ public Integer getServiceOfferingNetworkRate(long serviceOfferingId, Long dataCe return null; } + @Override + public Integer getVpcOfferingNetworkRate(long vpcOfferingId, Long dataCenterId) { + // TODO Auto-generated method stub + return null; + } + /* (non-Javadoc) * @see com.cloud.configuration.ConfigurationService#isOfferingForVpc(com.cloud.offering.NetworkOffering) */ diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99bf2cf7aef9..fd444b1e7d21 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2104,6 +2104,7 @@ "label.publicipid": "IP address ID", "label.publickey": "Public key", "label.publicnetwork": "Public Network", +"label.publicnetworkrate": "Public network rate (Mb/s)", "label.publicport": "Public port", "label.purgeresources": "Purge Resources", "label.purge.usage.records.success": "Successfully purged usage records", diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 3bd8ef0113a5..55f55f8c99c9 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -294,7 +294,7 @@ export default { return fields }, details: () => { - const fields = ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip4routing', 'ip4routes', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'] + const fields = ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip4routing', 'ip4routes', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'publicnetworkrate'] if (isAdmin()) { fields.push({ field: 'keepmacaddressonpublicnic', diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 52610b4497ed..513765ce40a6 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -566,7 +566,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', 'conservemode', 'domain', 'zone', 'created'], + details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'conservemode', 'publicnetworkrate', 'domain', 'zone', 'created'], related: [{ name: 'vpc', title: 'label.vpc', @@ -597,7 +597,7 @@ export default { icon: 'edit-outlined', label: 'label.edit', dataView: true, - args: ['name', 'displaytext'] + args: ['name', 'displaytext', 'publicnetworkrate'] }, { api: 'updateVPCOffering', icon: 'play-circle-outlined', diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue index 780da6638f8e..11062bf93a58 100644 --- a/ui/src/views/offering/AddVpcOffering.vue +++ b/ui/src/views/offering/AddVpcOffering.vue @@ -202,6 +202,14 @@ + + + + @@ -366,6 +374,7 @@ export default { }) this.rules = reactive({ name: [{ required: true, message: this.$t('message.error.name') }], + publicnetworkrate: [{ type: 'number', validator: this.validateNetworkRate }], domainid: [{ type: 'array', required: true, message: this.$t('message.error.select') }], zoneid: [{ type: 'array', @@ -721,6 +730,9 @@ export default { } params.networkmode = values.networkmode params.conservemode = values.conservemode + if (values.publicnetworkrate) { + params.publicnetworkrate = values.publicnetworkrate + } if (!values.forVpc) { params.specifyasnumber = values.specifyasnumber } @@ -755,6 +767,12 @@ export default { }, closeAction () { this.$emit('close-action') + }, + async validateNetworkRate (rule, value) { + if (value && (isNaN(value) || value < 0)) { + return Promise.reject(this.$t('message.error.number')) + } + return Promise.resolve() } } } From cdf3135cab5bf0e1384af55e64927d736a4fe21f Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 17 Sep 2026 22:41:54 +0530 Subject: [PATCH 10/29] Fix VPC public gateway rate not applied to VR public NIC, normalize null/0 to -1 in responses The VPC-aware branch of NetworkModelImpl.getNetworkRate() resolved the router's VPC by scanning its sibling NICs for a Guest one. During initial VR deployment the public NIC is allocated before the guest NIC is persisted, so the scan found nothing and silently fell back to unlimited instead of the VPC offering's rate. Resolve the VPC from the router's own vpc_id instead, which is set at router creation and doesn't depend on NIC allocation order. Also align publicnetworkrate in VpcResponse and VpcOfferingResponse with the existing networkrate convention: always return a normalized value (-1 for unlimited) instead of a raw nullable/zero column value, and show "Unlimited" in the UI for the VPC and VPC offering views the same way NIC/network rate already does. --- .../java/com/cloud/api/ApiResponseHelper.java | 6 +-- .../api/query/dao/VpcOfferingJoinDaoImpl.java | 3 +- .../com/cloud/network/NetworkModelImpl.java | 19 +++++--- .../cloud/network/NetworkModelImplTest.java | 46 ++++++++++++++----- ui/src/components/view/DetailsTab.vue | 3 ++ ui/src/components/view/ListView.vue | 3 ++ 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index accd0e353d26..c60c5dba394c 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -3602,10 +3602,8 @@ public VpcResponse createVpcResponse(ResponseView view, Vpc vpc) { response.setVpcOfferingConserveMode(voff.isConserveMode()); } VpcDetailVO publicNetworkRateDetail = vpcDetailsDao.findDetail(vpc.getId(), ApiConstants.PUBLIC_NETWORK_RATE); - if (publicNetworkRateDetail != null) { - int publicNetworkRate = NumberUtils.toInt(publicNetworkRateDetail.getValue(), -1); - response.setPublicNetworkRate(publicNetworkRate > 0 ? publicNetworkRate : -1); - } + Integer publicNetworkRate = publicNetworkRateDetail != null ? NumberUtils.toInt(publicNetworkRateDetail.getValue(), -1) : null; + response.setPublicNetworkRate((publicNetworkRate == null || publicNetworkRate <= 0) ? -1 : publicNetworkRate); response.setCidr(vpc.getCidr()); response.setRestartRequired(vpc.isRestartRequired()); response.setNetworkDomain(vpc.getNetworkDomain()); 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 bd14471351e3..858ef8b58d11 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 @@ -78,7 +78,8 @@ public VpcOfferingResponse newVpcOfferingResponse(VpcOffering offering) { offeringResponse.setSpecifyAsNumber(offering.isSpecifyAsNumber()); } offeringResponse.setConserveMode(offering.isConserveMode()); - offeringResponse.setPublicNetworkRate(offering.getPublicNetworkRate()); + Integer pubNetworkRate = offering.getPublicNetworkRate(); + offeringResponse.setPublicNetworkRate((pubNetworkRate == null || pubNetworkRate <= 0 ) ? -1 : pubNetworkRate); if (offering instanceof VpcOfferingJoinVO) { VpcOfferingJoinVO offeringJoinVO = (VpcOfferingJoinVO) offering; offeringResponse.setDomainId(offeringJoinVO.getDomainUuid()); diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index 8f4d4be61b37..5409932cbe6a 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -143,6 +143,7 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; +import com.cloud.vm.DomainRouterVO; import com.cloud.vm.Nic; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; @@ -150,6 +151,7 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.VMInstanceDao; @@ -194,6 +196,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi VpcDao vpcDao; @Inject VpcOfferingServiceMapDao _vpcOffSvcMapDao; + @Inject + DomainRouterDao _routerDao; private List networkElements; @@ -1235,16 +1239,19 @@ public Integer getNetworkRate(long networkId, Long vmId) { return _configMgr.getNetworkOfferingNetworkRate(network.getNetworkOfferingId(), network.getDataCenterId()); } } else if (TrafficType.Public.equals(network.getTrafficType())) { + // Use the router's own vpc_id: the guest NIC isn't persisted yet when this runs for the public NIC during initial VR deployment. + final DomainRouterVO routerVO = _routerDao.findById(vmId); + final Long vpcId = routerVO != null ? routerVO.getVpcId() : null; + if (vpcId != null) { + final Vpc vpc = vpcDao.findById(vpcId); + if (vpc != null) { + return _configMgr.getVpcOfferingNetworkRate(vpc.getVpcOfferingId(), network.getDataCenterId()); + } + } List routerNics = _nicDao.listByVmId(vmId); for (final Nic routerNic : routerNics) { final NetworkVO nw = _networksDao.findById(routerNic.getNetworkId()); if (TrafficType.Guest.equals(nw.getTrafficType())) { - if (nw.getVpcId() != null) { - final Vpc vpc = vpcDao.findById(nw.getVpcId()); - if (vpc != null) { - return _configMgr.getVpcOfferingNetworkRate(vpc.getVpcOfferingId(), network.getDataCenterId()); - } - } return _configMgr.getNetworkOfferingNetworkRate(nw.getNetworkOfferingId(), network.getDataCenterId()); } } diff --git a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java index 0d4af01fd65e..79a0c1f12638 100644 --- a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java @@ -76,11 +76,13 @@ import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.net.Ip; +import com.cloud.vm.DomainRouterVO; import com.cloud.vm.Nic; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -118,6 +120,8 @@ public class NetworkModelImplTest { @Mock private NicDao nicDao; @Mock + private DomainRouterDao routerDao; + @Mock private ServiceOfferingDao serviceOfferingDao; @Mock private ConfigurationManager configMgr; @@ -596,12 +600,12 @@ public void getNetworkRate_routerPublicWithGuestSibling_returnsGuestNetworkOffer VMInstanceVO vm = mock(VMInstanceVO.class); when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(routerDao.findById(vmId)).thenReturn(null); NicVO guestNic = mock(NicVO.class); when(guestNic.getNetworkId()).thenReturn(guestNetworkId); when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); NetworkVO guestNetwork = mock(NetworkVO.class); when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); - when(guestNetwork.getVpcId()).thenReturn(null); when(guestNetwork.getNetworkOfferingId()).thenReturn(guestOfferingId); when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); when(configMgr.getNetworkOfferingNetworkRate(guestOfferingId, dataCenterId)).thenReturn(80); @@ -616,6 +620,7 @@ public void getNetworkRate_routerPublicWithoutGuestSibling_fallsBackToNetworkOff VMInstanceVO vm = mock(VMInstanceVO.class); when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); when(vmInstanceDao.findById(vmId)).thenReturn(vm); + when(routerDao.findById(vmId)).thenReturn(null); when(nicDao.listByVmId(vmId)).thenReturn(Collections.emptyList()); mockNetworkOffering(networkOfferingId); when(configMgr.getNetworkOfferingNetworkRate(networkOfferingId, dataCenterId)).thenReturn(33); @@ -623,20 +628,37 @@ public void getNetworkRate_routerPublicWithoutGuestSibling_fallsBackToNetworkOff assertEquals(Integer.valueOf(33), networkModel.getNetworkRate(networkId, vmId)); } + @Test + public void getNetworkRate_routerPublicWithVpcRouter_returnsVpcOfferingRateWithoutNicLookup() { + long networkId = 1L, vmId = 12L, dataCenterId = 2L, vpcId = 7L, vpcOfferingId = 70L; + mockNetwork(networkId, 99L, dataCenterId, TrafficType.Public); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + when(vmInstanceDao.findById(vmId)).thenReturn(vm); + DomainRouterVO router = mock(DomainRouterVO.class); + when(router.getVpcId()).thenReturn(vpcId); + when(routerDao.findById(vmId)).thenReturn(router); + VpcVO vpc = mock(VpcVO.class); + when(vpc.getVpcOfferingId()).thenReturn(vpcOfferingId); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + when(configMgr.getVpcOfferingNetworkRate(vpcOfferingId, dataCenterId)).thenReturn(10); + + // Resolved purely from the router's own vpc_id - the guest NIC does not need to exist + // in the nics table yet, matching the state during initial VR deployment. + assertEquals(Integer.valueOf(10), networkModel.getNetworkRate(networkId, vmId)); + Mockito.verify(nicDao, Mockito.never()).listByVmId(Mockito.anyLong()); + } + @Test public void getNetworkRate_routerPublicWithVpcGuestSibling_returnsVpcOfferingRate() { - long networkId = 1L, vmId = 12L, dataCenterId = 2L, guestNetworkId = 5L, vpcId = 7L, vpcOfferingId = 70L; + long networkId = 1L, vmId = 12L, dataCenterId = 2L, vpcId = 7L, vpcOfferingId = 70L; mockNetwork(networkId, 99L, dataCenterId, TrafficType.Public); VMInstanceVO vm = mock(VMInstanceVO.class); when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); when(vmInstanceDao.findById(vmId)).thenReturn(vm); - NicVO guestNic = mock(NicVO.class); - when(guestNic.getNetworkId()).thenReturn(guestNetworkId); - when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); - NetworkVO guestNetwork = mock(NetworkVO.class); - when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); - when(guestNetwork.getVpcId()).thenReturn(vpcId); - when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); + DomainRouterVO router = mock(DomainRouterVO.class); + when(router.getVpcId()).thenReturn(vpcId); + when(routerDao.findById(vmId)).thenReturn(router); VpcVO vpc = mock(VpcVO.class); when(vpc.getVpcOfferingId()).thenReturn(vpcOfferingId); when(vpcDao.findById(vpcId)).thenReturn(vpc); @@ -653,15 +675,17 @@ public void getNetworkRate_routerPublicWithVpcGuestSiblingButMissingVpc_fallsBac VMInstanceVO vm = mock(VMInstanceVO.class); when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); when(vmInstanceDao.findById(vmId)).thenReturn(vm); + DomainRouterVO router = mock(DomainRouterVO.class); + when(router.getVpcId()).thenReturn(vpcId); + when(routerDao.findById(vmId)).thenReturn(router); + when(vpcDao.findById(vpcId)).thenReturn(null); NicVO guestNic = mock(NicVO.class); when(guestNic.getNetworkId()).thenReturn(guestNetworkId); when(nicDao.listByVmId(vmId)).thenReturn(List.of(guestNic)); NetworkVO guestNetwork = mock(NetworkVO.class); when(guestNetwork.getTrafficType()).thenReturn(TrafficType.Guest); - when(guestNetwork.getVpcId()).thenReturn(vpcId); when(guestNetwork.getNetworkOfferingId()).thenReturn(guestOfferingId); when(_networksDao.findById(guestNetworkId)).thenReturn(guestNetwork); - when(vpcDao.findById(vpcId)).thenReturn(null); when(configMgr.getNetworkOfferingNetworkRate(guestOfferingId, dataCenterId)).thenReturn(90); assertEquals(Integer.valueOf(90), networkModel.getNetworkRate(networkId, vmId)); diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 5e3ce28dad07..a150f92c3bdb 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -150,6 +150,9 @@
{{ String(dataResource[item]) === '-1' ? $t('label.unlimited') : dataResource[item] }}
+
+ {{ ['-1', '0'].includes(String(dataResource[item])) ? $t('label.unlimited') : dataResource[item] }} +
{{ dataResource[item] }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index ab737fc51893..a5cdf1f55af3 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -427,6 +427,9 @@ + From b8509654d8121893c0eec659b42c63ddfc4f76da Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 7 Sep 2026 16:57:30 +0530 Subject: [PATCH 11/29] engine-schema: upgrade path for 24.0.0 Adds a 4.23.0 to 24.0.0 upgrade path. Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 8 ++--- .../cloud/upgrade/dao/Upgrade42300to2400.java | 30 +++++++++++++++++++ .../db/schema-42300to2400-cleanup.sql | 20 +++++++++++++ .../META-INF/db/schema-42300to2400.sql | 20 +++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 0e40d7b7401c..a431b63deabb 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -91,12 +91,12 @@ import com.cloud.upgrade.dao.Upgrade42020to42030; import com.cloud.upgrade.dao.Upgrade42030to42040; import com.cloud.upgrade.dao.Upgrade42040to42100; -import com.cloud.upgrade.dao.Upgrade42100to42200; -import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade420to421; +import com.cloud.upgrade.dao.Upgrade42100to42200; import com.cloud.upgrade.dao.Upgrade421to430; +import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade42210to42300; -import com.cloud.upgrade.dao.Upgrade42300to42400; +import com.cloud.upgrade.dao.Upgrade42300to2400; import com.cloud.upgrade.dao.Upgrade430to440; import com.cloud.upgrade.dao.Upgrade431to440; import com.cloud.upgrade.dao.Upgrade432to440; @@ -249,7 +249,7 @@ public DatabaseUpgradeChecker() { .next("4.21.0.0", new Upgrade42100to42200()) .next("4.22.0.0", new Upgrade42200to42210()) .next("4.22.1.0", new Upgrade42210to42300()) - .next("4.23.0.0", new Upgrade42300to42400()) + .next("4.23.0.0", new Upgrade42300to2400()) .build(); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java new file mode 100644 index 000000000000..ce217cef9e75 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java @@ -0,0 +1,30 @@ +// 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.upgrade.dao; + +public class Upgrade42300to2400 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { + + @Override + public String[] getUpgradableVersionRange() { + return new String[]{"4.23.0.0", "24.0.0"}; + } + + @Override + public String getUpgradedVersion() { + return "24.0.0"; + } +} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql new file mode 100644 index 000000000000..861a038fe7a7 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql @@ -0,0 +1,20 @@ +-- 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. + +--; +-- Schema upgrade cleanup from 4.23.0.0 to 24.0.0 +--; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql new file mode 100644 index 000000000000..7c11013a17d2 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -0,0 +1,20 @@ +-- 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. + +--; +-- Schema upgrade from 4.23.0.0 to 24.0.0 +--; From 18c74d330099eba60ba251b2dde0bbe525cdc301 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 9 Sep 2026 18:47:00 +0530 Subject: [PATCH 12/29] fix CS version Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 9 ++- .../cloudstack/veeam/api/dto/Version.java | 8 ++- .../cloudstack/utils/CloudStackVersion.java | 62 +++++++++++++++---- .../utils/CloudStackVersionTest.java | 43 +++++++++++-- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index a431b63deabb..f3ec76a27c7a 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,8 +515,13 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + if (sysVmVersion.usesNewVersioning()) { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + } else { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + } LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java index 7b7d80a0f16c..2d14443e2d58 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java @@ -87,8 +87,12 @@ public static Version fromPackageAndCSVersion(boolean complete) { } version.setMajor(String.valueOf(csVersion.getMajorRelease())); version.setMinor(String.valueOf(csVersion.getMinorRelease())); - version.setBuild(String.valueOf(csVersion.getPatchRelease())); - version.setRevision(String.valueOf(csVersion.getSecurityRelease())); + if (csVersion.usesNewVersioning()) { + version.setBuild(String.valueOf(csVersion.getSecurityRelease())); + } else { + version.setBuild(String.valueOf(csVersion.getPatchRelease())); + version.setRevision(String.valueOf(csVersion.getSecurityRelease())); + } return version; } } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index e29bd9c4e17b..8eb4c6ab9289 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -39,22 +39,23 @@ */ public final class CloudStackVersion implements Comparable { - private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("(\\d+\\.){2}(\\d+\\.)?\\d+"); - private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("(\\d+\\.){2}(\\d+\\.)?\\d+(-[a-zA-Z]+)?(-\\d+)?(-SNAPSHOT)?"); + private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?"); + private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?(?:-[a-zA-Z]+)?(?:-\\d+)?(?:-SNAPSHOT)?"); + private final static int NEW_VERSIONING_CUTOVER_MAJOR_VERSION = 24; private final int majorRelease; private final int minorRelease; - private final int patchRelease; + private final Integer patchRelease; private final Integer securityRelease; - private CloudStackVersion(final int majorRelease, final int minorRelease, final int patchRelease, final Integer securityRelease) { + private CloudStackVersion(final int majorRelease, final int minorRelease, final Integer patchRelease, final Integer securityRelease) { super(); checkArgument(majorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a majorRelease greater than 0."); checkArgument(minorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a minorRelease greater than 0."); - checkArgument(patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); - checkArgument((securityRelease != null && securityRelease >= 0) || (securityRelease == null), + checkArgument(patchRelease == null || patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); + checkArgument(securityRelease == null || securityRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a null securityRelease or a non-null value greater than 0."); this.majorRelease = majorRelease; @@ -69,11 +70,13 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final * Parses a String representation of a version that conforms one of the following * formats into a CloudStackVersion instance: *
    - *
  • <major>.<minor>.<patch>.<security>
  • - *
  • <major>.<minor>.<patch>.<security>.<security>
  • - *
  • <major>.<minor>.<patch>.<security>.<security>-<any string>
  • + *
  • <major>.<minor>.<patch> (legacy, deprecated since 24.0.0, allowed only below major version 24)
  • + *
  • <major>.<minor>.<patch>.<security> (legacy, deprecated since 24.0.0, allowed only below major version 24)
  • + *
  • <major>.<minor>.<security release> (for versions >= 24.0.0)
  • *
* + * Legacy patch-based formats remain supported for backward compatibility. + * * If the string contains a suffix that begins with a "-" character, then the "-" and all characters following it * will be dropped. * @@ -91,7 +94,7 @@ public static CloudStackVersion parse(final String value) { checkArgument(StringUtils.isNotBlank(trimmedValue), CloudStackVersion.class.getName() + ".parse(String) requires a non-blank value"); checkArgument(NUMBER_VERSION_FORMAT.matcher(trimmedValue).matches(), CloudStackVersion.class.getName() + ".parse(String) passed " + - value + ", but requires a value in the format of int.int.int(.int)(-)"); + value + ", but requires a value in the format of int.int.int(.int)(-)"); final String[] components = trimmedValue.split("\\."); @@ -100,8 +103,26 @@ public static CloudStackVersion parse(final String value) { final int majorRelease = Integer.valueOf(components[0]); final int minorRelease = Integer.valueOf(components[1]); - final int patchRelease = Integer.valueOf(components[2]); - final Integer securityRelease = components.length == 3 ? null : Integer.valueOf(components[3]); + final int thirdComponent = Integer.valueOf(components[2]); + + final int patchRelease; + final Integer securityRelease; + + if (components.length == 4) { + checkArgument(isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + + ", but major versions at or above 24 do not support legacy int.int.int.int format"); + // Deprecated legacy format: major.minor.patch.security + patchRelease = thirdComponent; + securityRelease = Integer.valueOf(components[3]); + } else if (isNewVersioning(majorRelease)) { + // New format: major.minor.securityRelease (patch dropped) + patchRelease = 0; + securityRelease = thirdComponent; + } else { + // Deprecated legacy format: major.minor.patch + patchRelease = thirdComponent; + securityRelease = null; + } return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease); @@ -207,6 +228,14 @@ private static ImmutableList normalizeVersionValues(final ImmutableList } + private static boolean isLegacyVersioning(final int majorRelease) { + return majorRelease < NEW_VERSIONING_CUTOVER_MAJOR_VERSION; + } + + private static boolean isNewVersioning(final int majorRelease) { + return majorRelease >= NEW_VERSIONING_CUTOVER_MAJOR_VERSION; + } + /** * * @return The components of this version as an {@link ImmutableList} in order of major release, minor release, @@ -244,6 +273,10 @@ public Integer getSecurityRelease() { return securityRelease; } + public boolean usesNewVersioning() { + return isNewVersioning(majorRelease); + } + @Override public boolean equals(final Object thatObject) { @@ -270,6 +303,11 @@ public int hashCode() { @Override public String toString() { + // Canonicalize cutover-and-later versions to major.minor.securityRelease. + if (securityRelease != null && patchRelease == 0 && isNewVersioning(majorRelease)) { + return Joiner.on(".").join(ImmutableList.of(majorRelease, minorRelease, securityRelease)); + } + return Joiner.on(".").join(asList()); } diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index dabaf9bc97d3..4d0b4cb0439b 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -36,7 +36,11 @@ public final class CloudStackVersionTest { "1.2.3, 1.2.3", "1.2.3.4, 1.2.3.4", "1.2.3-12, 1.2.3", - "1.2.3.4-14, 1.2.3.4" + "1.2.3.4-14, 1.2.3.4", + "23.9.5, 23.9.5", + "24.0.0, 24.0.0", + "24.0.1, 24.0.1", + "25.1.1, 25.1.1" }) public void testValidParse(final String inputValue, final String expectedVersion) { final CloudStackVersion version = CloudStackVersion.parse(inputValue); @@ -44,6 +48,28 @@ public void testValidParse(final String inputValue, final String expectedVersion assertEquals(expectedVersion, version.toString()); } + @Test + public void testParseComponentMappingForLegacyAndNewVersioning() { + final CloudStackVersion legacyVersion = CloudStackVersion.parse("23.9.5"); + assertEquals(23, legacyVersion.getMajorRelease()); + assertEquals(9, legacyVersion.getMinorRelease()); + assertEquals(5, legacyVersion.getPatchRelease()); + Assert.assertNull(legacyVersion.getSecurityRelease()); + + final CloudStackVersion newVersion = CloudStackVersion.parse("24.0.1"); + assertEquals(24, newVersion.getMajorRelease()); + assertEquals(0, newVersion.getMinorRelease()); + // Patch is retained as 0 to represent "no patch" in the new major.minor.security scheme. + assertEquals(0, newVersion.getPatchRelease()); + assertEquals(Integer.valueOf(1), newVersion.getSecurityRelease()); + + final CloudStackVersion futureNewVersion = CloudStackVersion.parse("25.1.1"); + assertEquals(25, futureNewVersion.getMajorRelease()); + assertEquals(1, futureNewVersion.getMinorRelease()); + assertEquals(0, futureNewVersion.getPatchRelease()); + assertEquals(Integer.valueOf(1), futureNewVersion.getSecurityRelease()); + } + @Test(expected = IllegalArgumentException.class) @DataProvider({ "1.2", @@ -52,7 +78,10 @@ public void testValidParse(final String inputValue, final String expectedVersion "aaaa", "", " ", - "1.2.3.4.5" + "1.2.3.4.5", + "24.0.0.1", + "25.0.0.1", + "26.2.3.4" }) public void testInvalidParse(final String invalidValue) { CloudStackVersion.parse(invalidValue); @@ -147,7 +176,9 @@ public void testEqualCompareDirect(final String value, final String thatValue) { "1.2.3.4-10, 1.0.0.0-5", "1.2.3-10, 1.0.0-5", "1.2.3.4, 1.0.0.0-5", - "1.2.3.4-10, 1.0.0" + "1.2.3.4-10, 1.0.0", + "24.0.2, 24.0.1", + "24.1.0, 24.0.9" }) public void testGreaterThanAndLessThanCompareTo(final String value, final String thatValue) { @@ -178,7 +209,9 @@ public void testGreaterThanAndLessThanCompareTo(final String value, final String "1.2.3.4-10, 1.0.0.0-5", "1.2.3-10, 1.0.0-5", "1.2.3.4, 1.0.0.0-5", - "1.2.3.4-10, 1.0.0" + "1.2.3.4-10, 1.0.0", + "24.0.2, 24.0.1", + "24.1.0, 24.0.9" }) public void testGreaterThanAndLessThanCompareDirect(final String value, final String thatValue) { @@ -213,6 +246,7 @@ private void verifyGetVMwareParentVersion(String hypervisorVersion, String expec Assert.assertEquals(CloudStackVersion.getVMwareParentVersion(hypervisorVersion), expectedParentVersion); } } + @Test public void testGetParentVersion() { verifyGetVMwareParentVersion(null, null); @@ -223,5 +257,6 @@ public void testGetParentVersion() { verifyGetVMwareParentVersion("8.0.0", "8.0"); verifyGetVMwareParentVersion("8.0.0.2", "8.0"); verifyGetVMwareParentVersion("8.0.1.0", "8.0.1"); + verifyGetVMwareParentVersion("24.1.1", "24.1"); } } From 29b9b763ee85c226b1113149405866c91dab04ce Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 10 Sep 2026 13:28:02 +0530 Subject: [PATCH 13/29] fix upgrade unit tests for cutover Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 884398cf410d..763f78fe6949 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -25,6 +25,7 @@ import javax.sql.DataSource; import org.apache.cloudstack.utils.CloudStackVersion; +import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Test; import org.junit.Before; import org.junit.After; @@ -214,10 +215,10 @@ public void testFindUpgradePath452to490() { @Test public void testCalculateUpgradePathUnknownDbVersion() { - final CloudStackVersion dbVersion = CloudStackVersion.parse("4.99.0.0"); + final CloudStackVersion dbVersion = CloudStackVersion.parse("99.0.0"); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse("4.99.1.0"); + final CloudStackVersion currentVersion = CloudStackVersion.parse("99.1.0"); assertNotNull(currentVersion); final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -234,7 +235,7 @@ public void testCalculateUpgradePathFromKnownDbVersion() { final CloudStackVersion dbVersion = CloudStackVersion.parse("4.17.0.0"); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse("4.99.1.0"); + final CloudStackVersion currentVersion = CloudStackVersion.parse("99.1.0"); assertNotNull(currentVersion); final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -268,10 +269,7 @@ public void testCalculateUpgradePathFromLatestDbVersion() { final CloudStackVersion dbVersion = checker.getLatestVersion(); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse(dbVersion.getMajorRelease() + "." - + dbVersion.getMinorRelease() + "." - + dbVersion.getPatchRelease() + "." - + (dbVersion.getSecurityRelease() + 1)); + final CloudStackVersion currentVersion = getNextSecurityRelease(dbVersion); assertNotNull(currentVersion); final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); @@ -293,10 +291,7 @@ public void testCalculateUpgradePathFrom41800toNextSecurityRelease() { final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); assertNotNull(upgrades); - final CloudStackVersion nextSecurityRelease = CloudStackVersion.parse(currentVersion.getMajorRelease() + "." - + currentVersion.getMinorRelease() + "." - + currentVersion.getPatchRelease() + "." - + (currentVersion.getSecurityRelease() + 1)); + final CloudStackVersion nextSecurityRelease = getNextSecurityRelease(currentVersion); assertNotNull(nextSecurityRelease); final DbUpgrade[] upgradesToNext = checker.calculateUpgradePath(dbVersion, nextSecurityRelease); @@ -306,16 +301,26 @@ public void testCalculateUpgradePathFrom41800toNextSecurityRelease() { assertTrue(upgradesToNext[upgradesToNext.length - 1] instanceof NoopDbUpgrade); } + private static CloudStackVersion getNextSecurityRelease(CloudStackVersion version, int increment) { + String nextSecurityReleaseVersionStr = version.getMajorRelease() + "." + + version.getMinorRelease() + "." + + (version.usesNewVersioning() ? "" : version.getPatchRelease() + ".") + + (version.getSecurityRelease() + increment); + + return CloudStackVersion.parse(nextSecurityReleaseVersionStr); + } + + private static CloudStackVersion getNextSecurityRelease(CloudStackVersion version) { + return getNextSecurityRelease(version, 1); + } + @Test public void testCalculateUpgradePathFromSecurityReleaseToLatest() { final CloudStackVersion dbVersion = CloudStackVersion.parse("4.17.2.0"); // a EOL version assertNotNull(dbVersion); - final CloudStackVersion oldSecurityRelease = CloudStackVersion.parse(dbVersion.getMajorRelease() + "." - + dbVersion.getMinorRelease() + "." - + dbVersion.getPatchRelease() + "." - + (dbVersion.getSecurityRelease() + 100)); + final CloudStackVersion oldSecurityRelease = getNextSecurityRelease(dbVersion, 100); assertNotNull(oldSecurityRelease); // fake security release 4.17.2.100 final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -347,10 +352,7 @@ public void testCalculateUpgradePathFromSecurityReleaseToNextSecurityRelease() { final CloudStackVersion currentVersion = checker.getLatestVersion(); assertNotNull(currentVersion); - final CloudStackVersion nextSecurityRelease = CloudStackVersion.parse(currentVersion.getMajorRelease() + "." - + currentVersion.getMinorRelease() + "." - + currentVersion.getPatchRelease() + "." - + (currentVersion.getSecurityRelease() + 1)); + final CloudStackVersion nextSecurityRelease = getNextSecurityRelease(currentVersion); assertNotNull(nextSecurityRelease); // fake security release final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); From ec174a1ce5eed7f5bf3466a7257d1459b0b34525 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 10 Sep 2026 16:04:15 +0530 Subject: [PATCH 14/29] fix imports Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 763f78fe6949..3810d03161d9 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -16,21 +16,25 @@ // under the License. package com.cloud.upgrade; -import java.sql.SQLException; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import javax.sql.DataSource; import org.apache.cloudstack.utils.CloudStackVersion; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.junit.Test; -import org.junit.Before; import org.junit.After; +import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Mockito; @@ -56,15 +60,8 @@ import com.cloud.upgrade.dao.Upgrade471to480; import com.cloud.upgrade.dao.Upgrade480to481; import com.cloud.upgrade.dao.Upgrade490to4910; - import com.cloud.utils.db.TransactionLegacy; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertArrayEquals; - @RunWith(MockitoJUnitRunner.class) public class DatabaseUpgradeCheckerTest { From 673fa86043a542d2e6032cf350380a08ff8dedb3 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 10:44:46 +0530 Subject: [PATCH 15/29] fix template version --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index f3ec76a27c7a..8340d0e84c42 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -517,7 +517,7 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); if (sysVmVersion.usesNewVersioning()) { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMinorRelease()); } else { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); From 20f686cbe063541fabb17ff2b13b917d71523e1b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:56:54 +0530 Subject: [PATCH 16/29] continue using 3 part version for sys template --- .../main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 8340d0e84c42..2076c50507bd 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -521,7 +521,8 @@ protected void doUpgrades(GlobalLock lock) { } else { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); - } + SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); + SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From 8720eb35a1f6c74e981dc6c7c9ad349527f9b180 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:57:53 +0530 Subject: [PATCH 17/29] fix --- .../java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 2076c50507bd..c7f819aee407 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,12 +515,8 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - if (sysVmVersion.usesNewVersioning()) { - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMinorRelease()); - } else { - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); From d4c35495a9c1c0c1ce8e0c516db6c6f3cbf728c4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:58:39 +0530 Subject: [PATCH 18/29] fix more --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index c7f819aee407..74ee71274cd3 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -516,8 +516,6 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From 0f4c5ef87dfa6889a1206ac67d82a8cb144115c9 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 17:05:53 +0530 Subject: [PATCH 19/29] use security version after cutover Signed-off-by: Abhishek Kumar --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 74ee71274cd3..657a3fccdf01 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -516,7 +516,7 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From e1b7a2f1b181a3a09c322b9be5dff26b0af5298d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 18:20:23 +0530 Subject: [PATCH 20/29] improvements and add tests Signed-off-by: Abhishek Kumar --- engine/schema/pom.xml | 14 +++ .../cloud/upgrade/DatabaseUpgradeChecker.java | 15 ++- .../upgrade/DatabaseUpgradeCheckerTest.java | 24 +++++ .../SystemVmTemplateRegistrationTest.java | 37 +++++++ engine/schema/templateConfig.sh | 61 ++++++----- engine/schema/test_templateConfig.sh | 100 ++++++++++++++++++ .../cloudstack/utils/CloudStackVersion.java | 19 +++- .../utils/CloudStackVersionTest.java | 4 +- 8 files changed, 245 insertions(+), 29 deletions(-) create mode 100755 engine/schema/test_templateConfig.sh diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml index a7d53d01b5f7..6e078d7ec9f8 100644 --- a/engine/schema/pom.xml +++ b/engine/schema/pom.xml @@ -153,6 +153,20 @@ exec-maven-plugin ${cs.exec-maven-plugin.version} + + test-templateConfig + test + + exec + + + ${basedir}/ + bash + + test_templateConfig.sh + + + systemvm-template-metadata package diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 657a3fccdf01..334cdd3c7e5f 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,8 +515,7 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + updateSystemVmTemplateVersion(sysVmVersion); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); @@ -542,6 +541,18 @@ protected void doUpgrades(GlobalLock lock) { } } + /** + * Sets the system VM template major/tiny version used to look up the matching system VM template, + * from the version parsed out of the system VM template metadata file. Below the versioning cutover + * (major < 24) the tiny version is the legacy patch release; from the cutover onwards it is the + * security release, since the patch position is dropped in that scheme. + */ + @VisibleForTesting + protected static void updateSystemVmTemplateVersion(CloudStackVersion sysVmVersion) { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + } + /** * Hook that is called when an upgrade is required but the management server is clustered. * Default behavior is to exit the JVM, tests can override to throw instead. diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 3810d03161d9..553e537c853f 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -421,4 +421,28 @@ public void testCalculateUpgradePath42010to42100() { assertTrue(upgrades[2] instanceof Upgrade42040to42100); assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); } + + @Test + public void testUpdateSystemVmTemplateVersionBelowCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("4.22.1.0")); + + assertEquals("4.22", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("1", SystemVmTemplateRegistration.CS_TINY_VERSION); + } + + @Test + public void testUpdateSystemVmTemplateVersionAtCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("24.0.1")); + + assertEquals("24.0", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("1", SystemVmTemplateRegistration.CS_TINY_VERSION); + } + + @Test + public void testUpdateSystemVmTemplateVersionAfterCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("25.3.2")); + + assertEquals("25.3", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("2", SystemVmTemplateRegistration.CS_TINY_VERSION); + } } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java index 51db952eb613..dfc67b1dece3 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java @@ -1638,4 +1638,41 @@ public void updateTemplateUrlChecksumAndGuestOsId_ThrowsExceptionWhenUpdateFails assertTrue(exception.getMessage().contains("Exception while updating 'url' and 'checksum' for hypervisor type")); verify(vmTemplateDao).update(templateVO.getId(), templateVO); } + + @Test + public void getSystemVmTemplateVersion_UsesExplicitVersionWhenSet() { + SystemVmTemplateRegistration registration = new SystemVmTemplateRegistration("4.22.0.0"); + + assertEquals("4.22.0.0", registration.getSystemVmTemplateVersion()); + } + + @Test + public void getSystemVmTemplateVersion_BuildsFromMajorAndTinyVersionBelowCutover() { + String previousMajor = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + String previousTiny = SystemVmTemplateRegistration.CS_TINY_VERSION; + try { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = "4.22"; + SystemVmTemplateRegistration.CS_TINY_VERSION = "1"; + + assertEquals("4.22.1", systemVmTemplateRegistration.getSystemVmTemplateVersion()); + } finally { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousMajor; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousTiny; + } + } + + @Test + public void getSystemVmTemplateVersion_BuildsFromMajorAndTinyVersionAfterCutover() { + String previousMajor = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + String previousTiny = SystemVmTemplateRegistration.CS_TINY_VERSION; + try { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = "24.0"; + SystemVmTemplateRegistration.CS_TINY_VERSION = "1"; + + assertEquals("24.0.1", systemVmTemplateRegistration.getSystemVmTemplateVersion()); + } finally { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousMajor; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousTiny; + } + } } diff --git a/engine/schema/templateConfig.sh b/engine/schema/templateConfig.sh index 21a4ee795745..944e7992d4b5 100755 --- a/engine/schema/templateConfig.sh +++ b/engine/schema/templateConfig.sh @@ -17,16 +17,26 @@ # specific language governing permissions and limitations # under the License. +NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 + function getTemplateVersion() { projVersion=$1 version="$(cut -d'-' -f1 <<<"$projVersion")" subversion1="$(cut -d'.' -f1 <<<"$version")" subversion2="$(cut -d'.' -f2 <<<"$version")" minorversion="$(cut -d'.' -f3 <<<"$version")" - securityversion="$(cut -d'.' -f4 <<<"$version")" export CS_VERSION="${subversion1}"."${subversion2}" export CS_MINOR_VERSION="${minorversion}" export VERSION="${CS_VERSION}.${CS_MINOR_VERSION}" + if [[ "$subversion1" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then + # New versioning (major.minor.security): the third component is the + # security release itself, there is no separate patch component. + export FULL_VERSION="${VERSION}" + else + # Legacy versioning (major.minor.patch.security) + securityversion="$(cut -d'.' -f4 <<<"$version")" + export FULL_VERSION="${VERSION}.${securityversion}" + fi export CS_SYSTEMTEMPLATE_REPO="https://download.cloudstack.org/systemvm/" } @@ -64,7 +74,7 @@ function getChecksum() { function createMetadataFile() { local fileData=$(cat "$SOURCEFILE") - echo -e "["default"]\nversion = $VERSION.${securityversion}\ndownloadrepository = $CS_SYSTEMTEMPLATE_REPO\n" >> "$METADATAFILE" + echo -e "["default"]\nversion = $FULL_VERSION\ndownloadrepository = $CS_SYSTEMTEMPLATE_REPO\n" >> "$METADATAFILE" for template in "${templates[@]}" do section="${template%%:*}" @@ -81,27 +91,30 @@ function createMetadataFile() { done } -declare -a templates -getTemplateVersion $1 -declare -A template_specs=( - [kvm-x86_64]="x86_64-kvm.qcow2.bz2" - [kvm-aarch64]="aarch64-kvm.qcow2.bz2" - [vmware]="x86_64-vmware.ova" - [xenserver]="x86_64-xen.vhd.bz2" - [hyperv]="x86_64-hyperv.vhd.zip" - [lxc]="x86_64-kvm.qcow2.bz2" - [ovm3]="x86_64-ovm.raw.bz2" -) +# Guard so the file can be sourced (e.g. by tests) without running the steps below. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + declare -a templates + getTemplateVersion $1 + declare -A template_specs=( + [kvm-x86_64]="x86_64-kvm.qcow2.bz2" + [kvm-aarch64]="aarch64-kvm.qcow2.bz2" + [vmware]="x86_64-vmware.ova" + [xenserver]="x86_64-xen.vhd.bz2" + [hyperv]="x86_64-hyperv.vhd.zip" + [lxc]="x86_64-kvm.qcow2.bz2" + [ovm3]="x86_64-ovm.raw.bz2" + ) -templates=() -for key in "${!template_specs[@]}"; do - url="${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-$VERSION-${template_specs[$key]}" - templates+=("$key:$url") -done + templates=() + for key in "${!template_specs[@]}"; do + url="${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-$VERSION-${template_specs[$key]}" + templates+=("$key:$url") + done -PARENTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/dist/systemvm-templates/" -mkdir -p "$PARENTPATH" -METADATAFILE="${PARENTPATH}metadata.ini" -echo > "$METADATAFILE" -SOURCEFILE="${PARENTPATH}sha512sum.txt" -createMetadataFile + PARENTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/dist/systemvm-templates/" + mkdir -p "$PARENTPATH" + METADATAFILE="${PARENTPATH}metadata.ini" + echo > "$METADATAFILE" + SOURCEFILE="${PARENTPATH}sha512sum.txt" + createMetadataFile +fi diff --git a/engine/schema/test_templateConfig.sh b/engine/schema/test_templateConfig.sh new file mode 100755 index 000000000000..2a29268bacd5 --- /dev/null +++ b/engine/schema/test_templateConfig.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# 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. +# +# Standalone smoke test for templateConfig.sh's version handling across the +# 4.x (legacy) to 24.x (cutover) versioning schemes. Run directly: +# bash engine/schema/test_templateConfig.sh + +set -u + +SCRIPT_DIR="$( cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 ; pwd -P )" +# shellcheck source=templateConfig.sh +source "${SCRIPT_DIR}/templateConfig.sh" + +pass=0 +fail=0 + +assert_eq() { + local description="$1" + local expected="$2" + local actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass=$((pass + 1)) + echo "PASS: $description" + else + fail=$((fail + 1)) + echo "FAIL: $description (expected '$expected', got '$actual')" + fi +} + +# --- getTemplateVersion: legacy versioning (major < 24) --- + +getTemplateVersion "4.22.0.0" +assert_eq "legacy: CS_VERSION" "4.22" "$CS_VERSION" +assert_eq "legacy: VERSION" "4.22.0" "$VERSION" +assert_eq "legacy: FULL_VERSION keeps the 4th (security) component" "4.22.0.0" "$FULL_VERSION" + +getTemplateVersion "4.23.0.1-SNAPSHOT" +assert_eq "legacy with -SNAPSHOT suffix: VERSION" "4.23.0" "$VERSION" +assert_eq "legacy with -SNAPSHOT suffix: FULL_VERSION" "4.23.0.1" "$FULL_VERSION" + +# --- getTemplateVersion: new versioning (major >= 24, post-cutover) --- + +getTemplateVersion "24.0.0" +assert_eq "cutover: CS_VERSION" "24.0" "$CS_VERSION" +assert_eq "cutover: VERSION" "24.0.0" "$VERSION" +assert_eq "cutover: FULL_VERSION has no trailing dot (patch dropped)" "24.0.0" "$FULL_VERSION" + +getTemplateVersion "24.1.2-SNAPSHOT" +assert_eq "cutover with -SNAPSHOT suffix: VERSION" "24.1.2" "$VERSION" +assert_eq "cutover with -SNAPSHOT suffix: FULL_VERSION" "24.1.2" "$FULL_VERSION" + +getTemplateVersion "99.9.9" +assert_eq "future major: VERSION" "99.9.9" "$VERSION" +assert_eq "future major: FULL_VERSION" "99.9.9" "$FULL_VERSION" + +# --- createMetadataFile: end-to-end metadata.ini "version" line --- + +run_create_metadata_file() { + local projVersion="$1" + local workdir + workdir="$(mktemp -d)" + + getTemplateVersion "$projVersion" + METADATAFILE="${workdir}/metadata.ini" + SOURCEFILE="${workdir}/sha512sum.txt" + printf "abc123 systemvmtemplate-%s-x86_64-kvm.qcow2.bz2\n" "$VERSION" > "$SOURCEFILE" + templates=("kvm-x86_64:${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-${VERSION}-x86_64-kvm.qcow2.bz2") + : > "$METADATAFILE" + + createMetadataFile + grep '^version = ' "$METADATAFILE" | head -1 | cut -d' ' -f3 + + rm -rf "$workdir" +} + +actual="$(run_create_metadata_file "4.22.0.0")" +assert_eq "metadata.ini legacy 'version' line" "4.22.0.0" "$actual" + +actual="$(run_create_metadata_file "24.0.0")" +assert_eq "metadata.ini cutover 'version' line has no trailing dot" "24.0.0" "$actual" + +echo "" +echo "${pass} passed, ${fail} failed" +[[ $fail -eq 0 ]] diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index 8eb4c6ab9289..7e8bc9126d96 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -75,7 +75,9 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final *
  • <major>.<minor>.<security release> (for versions >= 24.0.0)
  • * * - * Legacy patch-based formats remain supported for backward compatibility. + * Legacy patch-based formats remain supported for backward compatibility, e.g. 4.23.0.1-SNAPSHOT, + * but only below major version 24: a 4-position value whose major release is at or above 24, e.g. + * 24.0.0.1, is rejected. * * If the string contains a suffix that begins with a "-" character, then the "-" and all characters following it * will be dropped. @@ -208,13 +210,26 @@ public int compareTo(final CloudStackVersion thatVersion) { public static String trimRouterVersion(String version) { final String[] tokens = version.split(" "); - if (tokens.length >= 3 && FULL_VERSION_FORMAT.matcher(tokens[2]).matches()) { + if (tokens.length >= 3 && FULL_VERSION_FORMAT.matcher(tokens[2]).matches() && isParseableVersion(tokens[2])) { return tokens[2]; } return "0"; } + /** + * Whether {@link #parse(String)} would accept the given value, following the same cutover rule that + * rejects a 4-position major.minor.patch.security value once the major release reaches 24. + */ + private static boolean isParseableVersion(final String value) { + try { + parse(value); + return true; + } catch (RuntimeException e) { + return false; + } + } + private static ImmutableList normalizeVersionValues(final ImmutableList values) { checkArgument(values != null); diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index 4d0b4cb0439b..4892644e8688 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -231,7 +231,9 @@ public void testGreaterThanAndLessThanCompareDirect(final String value, final St "Cloudstack Release 1.2.3.4-brnading-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-1519453362", "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0", "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0" + "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0", + "Cloudstack Release 24.0.0 Mon Jan 1 10:10:10 UTC 2018, 24.0.0", + "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0" }) public void testTrimRouterVersion(final String value, final String expected) { From d6198c62d91e13f57e084ee5e3c67e46259fcd9a Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 06:58:00 +0530 Subject: [PATCH 21/29] fix more scripts Signed-off-by: Abhishek Kumar --- scripts/installer/export-templates.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index dbd560e44c76..75da49a7653e 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -16,11 +16,21 @@ # specific language governing permissions and limitations # under the License. +NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 + METADATA_FILE="metadata.ini" IMAGE_PATH=${3:-"/usr/share/cloudstack-management/templates/systemvm/"} TEMPLATE_VERSION=$(awk -F "=" '/version/ {print $2}' ${IMAGE_PATH}${METADATA_FILE} | xargs) TEMPLATE_PATH="/usr/share/cloudstack-management/templates/systemvm/" -VERSION="${TEMPLATE_VERSION%.*}" +TEMPLATE_MAJOR_VERSION="$(cut -d'.' -f1 <<<"$TEMPLATE_VERSION")" +if [[ "$TEMPLATE_MAJOR_VERSION" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then + # New versioning (major.minor.security): the generated template files use the full version as-is. + VERSION="${TEMPLATE_VERSION}" +else + # Legacy versioning (major.minor.patch.security): the generated template files drop the trailing + # security component. + VERSION="${TEMPLATE_VERSION%.*}" +fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/" From 5f8b0dc5c1226b97baef34399020722a02677ed4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 07:20:26 +0530 Subject: [PATCH 22/29] address remaining comments Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 29 ++++++++++++++++ .../cloudstack/veeam/api/dto/VersionTest.java | 34 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 553e537c853f..aa39567af842 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -52,6 +52,7 @@ import com.cloud.upgrade.dao.Upgrade42020to42030; import com.cloud.upgrade.dao.Upgrade42030to42040; import com.cloud.upgrade.dao.Upgrade42040to42100; +import com.cloud.upgrade.dao.Upgrade42300to2400; import com.cloud.upgrade.dao.Upgrade452to453; import com.cloud.upgrade.dao.Upgrade453to460; import com.cloud.upgrade.dao.Upgrade460to461; @@ -79,6 +80,8 @@ public class DatabaseUpgradeCheckerTest { ResultSet resultSet; private DataSource backupDataSource; + private String previousCsMajorVersion; + private String previousCsTinyVersion; @Before public void setup() throws Exception { @@ -90,6 +93,9 @@ public void setup() throws Exception { Mockito.when(dataSource.getConnection()).thenReturn(connection); Mockito.when(connection.prepareStatement(ArgumentMatchers.anyString())).thenReturn(preparedStatement); Mockito.when(preparedStatement.executeQuery()).thenReturn(resultSet); + + previousCsMajorVersion = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + previousCsTinyVersion = SystemVmTemplateRegistration.CS_TINY_VERSION; } @After @@ -97,6 +103,9 @@ public void cleanup() throws Exception { Field dsField = TransactionLegacy.class.getDeclaredField("s_ds"); dsField.setAccessible(true); dsField.set(null, backupDataSource); + + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousCsMajorVersion; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousCsTinyVersion; } @Test @@ -422,6 +431,26 @@ public void testCalculateUpgradePath42010to42100() { assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); } + @Test + public void testCalculateUpgradePath42300to2400() { + + final CloudStackVersion dbVersion = CloudStackVersion.parse("4.23.0.0"); + assertNotNull(dbVersion); + + final CloudStackVersion currentVersion = CloudStackVersion.parse("24.0.0"); + assertNotNull(currentVersion); + + final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); + final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); + + assertNotNull(upgrades); + assertEquals(1, upgrades.length); + assertTrue(upgrades[0] instanceof Upgrade42300to2400); + + assertArrayEquals(new String[]{"4.23.0.0", "24.0.0"}, upgrades[0].getUpgradableVersionRange()); + assertEquals(currentVersion.toString(), upgrades[0].getUpgradedVersion()); + } + @Test public void testUpdateSystemVmTemplateVersionBelowCutover() { DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("4.22.1.0")); diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java index a91f193ba42a..72f1d92755e9 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java @@ -62,6 +62,40 @@ public void fromPackageAndCSVersion_IncompleteVersion_DoesNotSetFullVersion() { } } + @Test + public void fromPackageAndCSVersion_NewVersioningZeroSecurity_MapsSecurityToBuildAndOmitsRevision() { + CloudStackVersion csVersion = CloudStackVersion.parse("24.0.0"); + try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { + mocked.when(VeeamControlService::getPackageVersion).thenReturn("24.0.0"); + mocked.when(VeeamControlService::getCSVersion).thenReturn(csVersion); + + Version version = Version.fromPackageAndCSVersion(true); + + assertEquals("24.0.0", version.getFullVersion()); + assertEquals("24", version.getMajor()); + assertEquals("0", version.getMinor()); + assertEquals("0", version.getBuild()); + assertNull(version.getRevision()); + } + } + + @Test + public void fromPackageAndCSVersion_NewVersioningNonZeroSecurity_MapsSecurityToBuildAndOmitsRevision() { + CloudStackVersion csVersion = CloudStackVersion.parse("24.1.3"); + try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { + mocked.when(VeeamControlService::getPackageVersion).thenReturn("24.1.3"); + mocked.when(VeeamControlService::getCSVersion).thenReturn(csVersion); + + Version version = Version.fromPackageAndCSVersion(true); + + assertEquals("24.1.3", version.getFullVersion()); + assertEquals("24", version.getMajor()); + assertEquals("1", version.getMinor()); + assertEquals("3", version.getBuild()); + assertNull(version.getRevision()); + } + } + @Test public void fromPackageAndCSVersion_NullCloudStackVersion_ReturnsWithoutNumericParts() { try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { From dec9da98e655c4f24f4f778a88024a304620b498 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 09:56:22 +0530 Subject: [PATCH 23/29] more changes Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- engine/schema/templateConfig.sh | 22 ++++- engine/schema/test_templateConfig.sh | 12 +++ .../cloudstack/veeam/api/dto/Version.java | 6 +- scripts/installer/export-templates.sh | 2 + .../nvp/plugin/NiciraNvpApiVersion.java | 2 +- .../cloudstack/utils/CloudStackVersion.java | 87 ++++++++++++++++--- .../utils/CloudStackVersionTest.java | 75 ++++++++++++++-- 8 files changed, 181 insertions(+), 27 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 334cdd3c7e5f..57ae2c3d6491 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -550,7 +550,7 @@ protected void doUpgrades(GlobalLock lock) { @VisibleForTesting protected static void updateSystemVmTemplateVersion(CloudStackVersion sysVmVersion) { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getTinyRelease()); } /** diff --git a/engine/schema/templateConfig.sh b/engine/schema/templateConfig.sh index 944e7992d4b5..7a8283e77c1c 100755 --- a/engine/schema/templateConfig.sh +++ b/engine/schema/templateConfig.sh @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +# Kept in sync with CloudStackVersion.NEW_VERSIONING_CUTOVER_MAJOR_VERSION (utils module) and the +# same constant in scripts/installer/export-templates.sh. NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 function getTemplateVersion() { @@ -25,17 +27,29 @@ function getTemplateVersion() { subversion1="$(cut -d'.' -f1 <<<"$version")" subversion2="$(cut -d'.' -f2 <<<"$version")" minorversion="$(cut -d'.' -f3 <<<"$version")" + fourthversion="$(cut -d'.' -f4 <<<"$version")" export CS_VERSION="${subversion1}"."${subversion2}" export CS_MINOR_VERSION="${minorversion}" export VERSION="${CS_VERSION}.${CS_MINOR_VERSION}" if [[ "$subversion1" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then # New versioning (major.minor.security): the third component is the - # security release itself, there is no separate patch component. + # security release itself, there is no separate patch component. A + # 4th component is invalid in this scheme (matches CloudStackVersion.parse()) + # and must not be silently dropped. + if [[ -n "$fourthversion" ]]; then + echo "Invalid version '${projVersion}': major versions at or above ${NEW_VERSIONING_CUTOVER_MAJOR_VERSION} do not support a 4-component major.minor.patch.security format" >&2 + exit 1 + fi export FULL_VERSION="${VERSION}" else - # Legacy versioning (major.minor.patch.security) - securityversion="$(cut -d'.' -f4 <<<"$version")" - export FULL_VERSION="${VERSION}.${securityversion}" + # Legacy versioning: major.minor.patch, or major.minor.patch.security when a 4th + # component is present. Without a 4th component, VERSION is already correct as-is; + # appending an empty security component would leave a dangling trailing dot. + if [[ -n "$fourthversion" ]]; then + export FULL_VERSION="${VERSION}.${fourthversion}" + else + export FULL_VERSION="${VERSION}" + fi fi export CS_SYSTEMTEMPLATE_REPO="https://download.cloudstack.org/systemvm/" } diff --git a/engine/schema/test_templateConfig.sh b/engine/schema/test_templateConfig.sh index 2a29268bacd5..2acb4281edb7 100755 --- a/engine/schema/test_templateConfig.sh +++ b/engine/schema/test_templateConfig.sh @@ -54,6 +54,11 @@ getTemplateVersion "4.23.0.1-SNAPSHOT" assert_eq "legacy with -SNAPSHOT suffix: VERSION" "4.23.0" "$VERSION" assert_eq "legacy with -SNAPSHOT suffix: FULL_VERSION" "4.23.0.1" "$FULL_VERSION" +# A legacy 3-component major.minor.patch value (no security component) is a documented, valid +# CloudStackVersion.parse() format below the cutover and must not get a dangling trailing dot. +getTemplateVersion "4.23.1" +assert_eq "legacy 3-component (no security): FULL_VERSION has no trailing dot" "4.23.1" "$FULL_VERSION" + # --- getTemplateVersion: new versioning (major >= 24, post-cutover) --- getTemplateVersion "24.0.0" @@ -69,6 +74,13 @@ getTemplateVersion "99.9.9" assert_eq "future major: VERSION" "99.9.9" "$VERSION" assert_eq "future major: FULL_VERSION" "99.9.9" "$FULL_VERSION" +# A 4-component major.minor.patch.security value is invalid at/after the cutover (matches +# CloudStackVersion.parse()) and must fail the build rather than silently drop the 4th component. +# Run in a subshell since getTemplateVersion calls `exit` on this input. +( getTemplateVersion "24.0.0.1" ) >/dev/null 2>&1 +cutover_4component_exit_code="$?" +assert_eq "cutover: 4-component version is rejected instead of silently normalized" "1" "$cutover_4component_exit_code" + # --- createMetadataFile: end-to-end metadata.ini "version" line --- run_create_metadata_file() { diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java index 2d14443e2d58..5e1aa52533c1 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java @@ -87,10 +87,8 @@ public static Version fromPackageAndCSVersion(boolean complete) { } version.setMajor(String.valueOf(csVersion.getMajorRelease())); version.setMinor(String.valueOf(csVersion.getMinorRelease())); - if (csVersion.usesNewVersioning()) { - version.setBuild(String.valueOf(csVersion.getSecurityRelease())); - } else { - version.setBuild(String.valueOf(csVersion.getPatchRelease())); + version.setBuild(String.valueOf(csVersion.getTinyRelease())); + if (!csVersion.usesNewVersioning()) { version.setRevision(String.valueOf(csVersion.getSecurityRelease())); } return version; diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index 75da49a7653e..cd44ed674e5e 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -16,6 +16,8 @@ # specific language governing permissions and limitations # under the License. +# Kept in sync with CloudStackVersion.NEW_VERSIONING_CUTOVER_MAJOR_VERSION (utils module) and the +# same constant in engine/schema/templateConfig.sh. NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 METADATA_FILE="metadata.ini" diff --git a/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java b/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java index 7e4dd5b19b4e..5c79d78a52a9 100755 --- a/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java +++ b/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java @@ -37,7 +37,7 @@ public static synchronized boolean isApiVersionLowerThan(String apiVersion){ if (niciraApiVersion == null) { return false; } - int compare = CloudStackVersion.compare(niciraApiVersion, apiVersion); + int compare = CloudStackVersion.compare(niciraApiVersion, apiVersion, true); return (compare < 0); } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index 7e8bc9126d96..fd15df7d5ea8 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -41,20 +41,30 @@ public final class CloudStackVersion implements Comparable { private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?"); private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?(?:-[a-zA-Z]+)?(?:-\\d+)?(?:-SNAPSHOT)?"); + // Kept in sync with the same constant in engine/schema/templateConfig.sh and + // scripts/installer/export-templates.sh, which independently generate/consume + // system VM template metadata using this same versioning cutover rule. private final static int NEW_VERSIONING_CUTOVER_MAJOR_VERSION = 24; private final int majorRelease; private final int minorRelease; - private final Integer patchRelease; + private final int patchRelease; private final Integer securityRelease; + // Whether this instance was parsed via parse(value, true) — a version scheme other than + // CloudStack's own. Deliberately excluded from equals()/hashCode(): two instances with the same + // component values represent the same version regardless of which parsing mode produced them. + // It only gates whether the new-versioning cutover rule's *interpretation* (toString()'s + // canonicalization, usesNewVersioning()) applies, since that rule is specific to CloudStack's own + // release numbering. + private final boolean external; - private CloudStackVersion(final int majorRelease, final int minorRelease, final Integer patchRelease, final Integer securityRelease) { + private CloudStackVersion(final int majorRelease, final int minorRelease, final int patchRelease, final Integer securityRelease, final boolean external) { super(); checkArgument(majorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a majorRelease greater than 0."); checkArgument(minorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a minorRelease greater than 0."); - checkArgument(patchRelease == null || patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); + checkArgument(patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); checkArgument(securityRelease == null || securityRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a null securityRelease or a non-null value greater than 0."); @@ -62,6 +72,7 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final this.minorRelease = minorRelease; this.patchRelease = patchRelease; this.securityRelease = securityRelease; + this.external = external; } @@ -90,6 +101,26 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final * */ public static CloudStackVersion parse(final String value) { + return parse(value, false); + } + + /** + * Parses a version string the same way as {@link #parse(String)}, but with {@code external} set to + * true, always applies the legacy major.minor.patch(.security) component mapping and + * never the major-24-and-above new-versioning cutover rule. + *

    + * CloudStack's own versioning cutover (see {@link #NEW_VERSIONING_CUTOVER_MAJOR_VERSION}) is a fact + * about CloudStack's own release numbering. It has no bearing on unrelated version schemes, such as a + * VMware/ESXi hypervisor version, that may coincidentally reach the same major version number. Callers + * parsing such external version strings must pass external = true so a value like + * 24.0.1 is not misread as a CloudStack security release. + * + * @param value The value to parse which must be non-blank and conform the formats listed above + * @param external whether {@code value} comes from a version scheme other than CloudStack's own + * + * @return value parsed into a CloudStackVersion instance + */ + public static CloudStackVersion parse(final String value, final boolean external) { // Strip out any legacy patch information from the version string ... final String trimmedValue = StringUtils.substringBefore(value, "-"); @@ -111,12 +142,12 @@ public static CloudStackVersion parse(final String value) { final Integer securityRelease; if (components.length == 4) { - checkArgument(isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + + checkArgument(external || isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + ", but major versions at or above 24 do not support legacy int.int.int.int format"); // Deprecated legacy format: major.minor.patch.security patchRelease = thirdComponent; securityRelease = Integer.valueOf(components[3]); - } else if (isNewVersioning(majorRelease)) { + } else if (!external && isNewVersioning(majorRelease)) { // New format: major.minor.securityRelease (patch dropped) patchRelease = 0; securityRelease = thirdComponent; @@ -126,7 +157,7 @@ public static CloudStackVersion parse(final String value) { securityRelease = null; } - return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease); + return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease, external); } @@ -143,7 +174,26 @@ public static CloudStackVersion parse(final String value) { * @since 4.12.0.0 */ public static int compare(String version1, String version2) { - return parse(version1).compareTo(parse(version2)); + return compare(version1, version2, false); + } + + /** + * Shortcut method to {@link #parse(String, boolean)} and {@link #compareTo(CloudStackVersion)} two versions. + * Pass external = true when comparing version strings from a scheme other than CloudStack's + * own (e.g. a VMware/ESXi or NSX/Nicira NVP version), so CloudStack's own new-versioning cutover rule is + * not applied to them. + * + * @param version1 the first value to be parsed and compared + * @param version2 the second value to be parsed and compared + * @param external whether version1/version2 come from a version scheme other + * than CloudStack's own + * + * @return A value less than zero (0) indicates version1 is less than version2. A value + * equal to zero (0) indicates version1 equals version2. A value greater than zero (0) + * indicates version1 is greater than version2. + */ + public static int compare(String version1, String version2, boolean external) { + return parse(version1, external).compareTo(parse(version2, external)); } /** @@ -214,7 +264,11 @@ public static String trimRouterVersion(String version) { return tokens[2]; } - return "0"; + // A sentinel that sorts lower than any real version, so callers that feed this straight into + // parse()/compare() (as the router-version-check call sites do, with no try/catch) get a + // "definitely needs upgrading" result instead of an uncaught parse failure. Must itself be a + // value parse() accepts. + return "0.0.0"; } /** @@ -289,7 +343,18 @@ public Integer getSecurityRelease() { } public boolean usesNewVersioning() { - return isNewVersioning(majorRelease); + return !external && isNewVersioning(majorRelease); + } + + /** + * The release component that identifies this version's "tiny"/build release: the security + * release for a new-versioning (post-cutover) CloudStack version, or the patch release otherwise. + * Consolidates a mapping that callers (e.g. system VM template version resolution, the Veeam + * integration's version reporting) would otherwise have to re-derive from + * {@link #usesNewVersioning()} themselves. + */ + public int getTinyRelease() { + return usesNewVersioning() ? securityRelease : patchRelease; } @Override @@ -319,7 +384,7 @@ public int hashCode() { @Override public String toString() { // Canonicalize cutover-and-later versions to major.minor.securityRelease. - if (securityRelease != null && patchRelease == 0 && isNewVersioning(majorRelease)) { + if (securityRelease != null && patchRelease == 0 && usesNewVersioning()) { return Joiner.on(".").join(ImmutableList.of(majorRelease, minorRelease, securityRelease)); } @@ -332,7 +397,7 @@ public String toString() { */ public static String getVMwareParentVersion(String hypervisorVersion) { try { - CloudStackVersion version = CloudStackVersion.parse(hypervisorVersion); + CloudStackVersion version = CloudStackVersion.parse(hypervisorVersion, true); String parentVersion = String.format("%s.%s", version.getMajorRelease(), version.getMinorRelease()); if (version.getPatchRelease() != 0) { parentVersion = String.format("%s.%s", parentVersion, version.getPatchRelease()); diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index 4892644e8688..e9bbb35af3dc 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -87,12 +87,58 @@ public void testInvalidParse(final String invalidValue) { CloudStackVersion.parse(invalidValue); } + @Test + public void testExternalParseIgnoresCutoverRule() { + // A 3-component value with a major at/above the cutover is treated as major.minor.patch, + // not major.minor.security, when parsed as an external (non-CloudStack) version. + CloudStackVersion version = CloudStackVersion.parse("24.1.1", true); + assertEquals(24, version.getMajorRelease()); + assertEquals(1, version.getMinorRelease()); + assertEquals(1, version.getPatchRelease()); + Assert.assertNull(version.getSecurityRelease()); + + // A 4-component value with a major at/above the cutover, rejected by parse(value), is accepted + // when parsed as external. + CloudStackVersion legacyShapedVersion = CloudStackVersion.parse("24.1.1.2", true); + assertEquals(24, legacyShapedVersion.getMajorRelease()); + assertEquals(1, legacyShapedVersion.getMinorRelease()); + assertEquals(1, legacyShapedVersion.getPatchRelease()); + assertEquals(Integer.valueOf(2), legacyShapedVersion.getSecurityRelease()); + } + + @Test + public void testExternalParseNotCanonicalizedAsCloudStackVersion() { + // An external version with major >= 24 and a literal patch digit of 0 (e.g. a 4-component + // value like "24.0.0.5") must not have its toString()/usesNewVersioning() apply CloudStack's + // own cutover canonicalization: doing so would misreport it as CloudStack version "24.0.5", + // silently discarding the real ("0") patch component. + CloudStackVersion version = CloudStackVersion.parse("24.0.0.5", true); + Assert.assertFalse(version.usesNewVersioning()); + assertEquals("24.0.0.5", version.toString()); + } + + @Test + public void testExternalCompareIgnoresCutoverRule() { + // Without external=true, "24.1.0" would be new-versioning (patch dropped to 0, third + // component treated as security), making it equal to "24.1.1"; external=true keeps the + // patch component significant so these two compare as different, non-CloudStack versions. + assertEquals(0, CloudStackVersion.compare("24.1.0", "24.1.0", true)); + Assert.assertTrue(CloudStackVersion.compare("24.1.1", "24.1.0", true) > 0); + Assert.assertTrue(CloudStackVersion.compare("24.1.0", "24.1.1", true) < 0); + + // A 4-component value with a major at/above the cutover, rejected by compare(v1, v2), is + // accepted when compared as external. + assertEquals(0, CloudStackVersion.compare("24.1.1.2", "24.1.1.2", true)); + } + @Test @DataProvider({ "1.0.0", "1.0.0.0", "1.2.3", - "1.2.3.4" + "1.2.3.4", + "1.2.200", + "1.2.200.5" }) public void testEquals(final String value) { @@ -229,11 +275,11 @@ public void testGreaterThanAndLessThanCompareDirect(final String value, final St "Cloudstack Release 1.2.3.4-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-1519453362", "Cloudstack Release 1.2.3.4-brnading-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-SNAPSHOT", "Cloudstack Release 1.2.3.4-brnading-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-1519453362", - "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0", + "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0.0.0", + "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0.0.0", + "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0.0.0", "Cloudstack Release 24.0.0 Mon Jan 1 10:10:10 UTC 2018, 24.0.0", - "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0" + "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0.0.0" }) public void testTrimRouterVersion(final String value, final String expected) { @@ -241,6 +287,18 @@ public void testTrimRouterVersion(final String value, final String expected) { } + @Test + public void testTrimRouterVersionInvalidFallbackIsItselfParseable() { + // The invalid-version fallback must be a value CloudStackVersion.parse()/compare() accepts: + // callers (e.g. NetworkHelperImpl.checkRouterVersion) pass it straight into compare() with no + // try/catch, so an unparseable fallback would turn a "router needs upgrading" case into an + // uncaught exception. + String fallback = CloudStackVersion.trimRouterVersion("not a router version string"); + CloudStackVersion parsed = CloudStackVersion.parse(fallback); + assertNotNull(parsed); + Assert.assertTrue(CloudStackVersion.compare(fallback, "1.0.0") < 0); + } + private void verifyGetVMwareParentVersion(String hypervisorVersion, String expectedParentVersion) { if (expectedParentVersion == null) { Assert.assertNull(CloudStackVersion.getVMwareParentVersion(hypervisorVersion)); @@ -259,6 +317,11 @@ public void testGetParentVersion() { verifyGetVMwareParentVersion("8.0.0", "8.0"); verifyGetVMwareParentVersion("8.0.0.2", "8.0"); verifyGetVMwareParentVersion("8.0.1.0", "8.0.1"); - verifyGetVMwareParentVersion("24.1.1", "24.1"); + // A hypervisor version whose major happens to match CloudStack's own new-versioning cutover (24) + // must still be treated as an ordinary major.minor.patch VMware version, not as a CloudStack + // major.minor.security value: the patch component ("1") must not be dropped. + verifyGetVMwareParentVersion("24.1.0", "24.1"); + verifyGetVMwareParentVersion("24.1.1", "24.1.1"); + verifyGetVMwareParentVersion("24.1.1.2", "24.1.1"); } } From 1fbbe0fed4c494dde7d81e011315e4127947b183 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 14:44:18 +0530 Subject: [PATCH 24/29] Update versioning logic in export-templates.sh Refactor versioning logic to handle four-component metadata separately. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/installer/export-templates.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index cd44ed674e5e..c5639a865e15 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -29,10 +29,13 @@ if [[ "$TEMPLATE_MAJOR_VERSION" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; # New versioning (major.minor.security): the generated template files use the full version as-is. VERSION="${TEMPLATE_VERSION}" else - # Legacy versioning (major.minor.patch.security): the generated template files drop the trailing - # security component. - VERSION="${TEMPLATE_VERSION%.*}" -fi + # Legacy versioning drops the trailing security component only for four-component metadata; + # a valid three-component major.minor.patch value is already the filename version. + if [[ "$TEMPLATE_VERSION" == *.*.*.* ]]; then + VERSION="${TEMPLATE_VERSION%.*}" + else + VERSION="${TEMPLATE_VERSION}" + fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/" From 29415b3ebd86d5357cfce0c2d26534df9142348b Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 17 Sep 2026 11:07:21 -0300 Subject: [PATCH 25/29] Fix export-templates.sh syntax --- scripts/installer/export-templates.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index c5639a865e15..820596837131 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -36,6 +36,7 @@ else else VERSION="${TEMPLATE_VERSION}" fi +fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/" From 57d48817838afb9e77716ddc9ec76a1c4c4bdaa1 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 18 Sep 2026 13:13:20 +0530 Subject: [PATCH 26/29] Align VPC public gateway rate upgrade path with the 4.23.0.0 to 24.0.0 versioning cutover Cherry-picked the upstream 4.23.0.0 -> 24.0.0 version-cutover upgrade path (management server versions dropping the leading "4." from 24.0.0 onwards) so our own migration targets the real next version. Our previous Upgrade42300to42400 registered itself against "4.24.0.0", which no longer exists once the cutover lands - the project's actual next version is 24.0.0, matching pom.xml's 24.0.0-SNAPSHOT. Folded the public_nw_rate/network_rate column additions and the NetworkRateBackfill data migration into the new Upgrade42300to2400 class and its schema-42300to2400.sql, and removed the now-redundant Upgrade42300to42400/schema-42300to42400.sql. --- .../cloud/upgrade/dao/Upgrade42300to2400.java | 21 +++++++ .../upgrade/dao/Upgrade42300to42400.java | 56 ------------------- .../META-INF/db/schema-42300to2400.sql | 4 ++ .../META-INF/db/schema-42300to42400.sql | 22 -------- 4 files changed, 25 insertions(+), 78 deletions(-) delete mode 100644 engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java delete mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java index ce217cef9e75..472642a50a81 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java @@ -16,6 +16,12 @@ // under the License. package com.cloud.upgrade.dao; +import java.io.InputStream; +import java.sql.Connection; + +import com.cloud.upgrade.NetworkRateBackfill; +import com.cloud.utils.exception.CloudRuntimeException; + public class Upgrade42300to2400 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { @Override @@ -27,4 +33,19 @@ public String[] getUpgradableVersionRange() { public String getUpgradedVersion() { return "24.0.0"; } + + @Override + public InputStream[] getPrepareScripts() { + final String scriptFile = "META-INF/db/schema-42300to2400.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + return new InputStream[]{script}; + } + + @Override + public void performDataMigration(Connection conn) { + new NetworkRateBackfill().backfillNetworkRates(); + } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java deleted file mode 100644 index 5b703ccd37c7..000000000000 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to42400.java +++ /dev/null @@ -1,56 +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.upgrade.dao; - -import java.io.InputStream; -import java.sql.Connection; - -import com.cloud.upgrade.NetworkRateBackfill; -import com.cloud.utils.exception.CloudRuntimeException; - -public class Upgrade42300to42400 extends DbUpgradeAbstractImpl implements DbUpgrade { - - @Override - public String[] getUpgradableVersionRange() { - return new String[]{"4.23.0.0", "4.24.0.0"}; - } - - @Override - public String getUpgradedVersion() { - return "4.24.0.0"; - } - - @Override - public InputStream[] getPrepareScripts() { - final String scriptFile = "META-INF/db/schema-42300to42400.sql"; - final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); - if (script == null) { - throw new CloudRuntimeException("Unable to find " + scriptFile); - } - return new InputStream[]{script}; - } - - @Override - public void performDataMigration(Connection conn) { - new NetworkRateBackfill().backfillNetworkRates(); - } - - @Override - public InputStream[] getCleanupScripts() { - return null; - } -} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 7c11013a17d2..1a084b76d854 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -18,3 +18,7 @@ --; -- Schema upgrade from 4.23.0.0 to 24.0.0 --; + +ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int unsigned DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC'; + +ALTER TABLE `cloud`.`vpc_offerings` ADD COLUMN `public_nw_rate` smallint unsigned DEFAULT NULL COMMENT 'public gateway (internet-facing) network rate throttle mbits/s'; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql deleted file mode 100644 index f6538848354e..000000000000 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to42400.sql +++ /dev/null @@ -1,22 +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. - --- Schema upgrade from 4.23.0.0 to 4.24.0.0 - -ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int unsigned DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC'; - -ALTER TABLE `cloud`.`vpc_offerings` ADD COLUMN `public_nw_rate` smallint unsigned DEFAULT NULL COMMENT 'public gateway (internet-facing) network rate throttle mbits/s'; From 8800f0cf2895c43946dc2c1f7f5dc3b606a7db2a Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 18 Sep 2026 17:02:35 +0530 Subject: [PATCH 27/29] Fix two NetworkRateBackfill migration bugs found via a real upgrade test Found by actually running the 4.23.0.0 -> 24.0.0 upgrade path via RPM against a real DB, not just unit tests: - findRouterGuestNetworkRate() called NicDao.listByVmId(), but this class's DAOs are constructed outside Spring during upgrade bootstrap, and that query relies on a SearchBuilder that isn't initialized in that context, throwing an NPE that was silently caught. Every VPC/ isolated-network router's public NIC failed to backfill as a result. Replaced with a raw SQL query, matching this class's existing pattern for DAOs that aren't safe to use directly here. - backfillNicNetworkRates() only persisted a computed rate when it was greater than zero, silently skipping the legitimate "-1 (unlimited)" case - affecting ConsoleProxy/SecondaryStorageVm NICs and any NIC whose legacy rate resolved to unlimited. Left network_rate NULL instead of -1, which the API renders as blank instead of "Unlimited" until the NIC is reallocated. --- .../cloud/upgrade/NetworkRateBackfill.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java index 31bd3ee827f2..2f9c99136d1b 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -19,7 +19,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -41,11 +40,8 @@ import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDaoImpl; import com.cloud.utils.db.TransactionLegacy; -import com.cloud.vm.NicVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; -import com.cloud.vm.dao.NicDao; -import com.cloud.vm.dao.NicDaoImpl; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDaoImpl; @@ -68,7 +64,6 @@ public class NetworkRateBackfill { private static final int DEFAULT_THROTTLING_RATE = 200; private static final int UNLIMITED_RATE = -1; - private final NicDao nicDao = new NicDaoImpl(); private final VMInstanceDao vmInstanceDao = new VMInstanceDaoImpl(); private final NetworkDao networkDao = new NetworkDaoImpl(); private final NetworkDetailsDao networkDetailsDao = new NetworkDetailsDaoImpl(); @@ -95,7 +90,7 @@ private void backfillNicNetworkRates() { final boolean defaultNic = rs.getBoolean("default_nic"); try { final Integer rate = computeLegacyNicNetworkRate(networkId, instanceId, defaultNic); - if (rate != null && rate > 0) { + if (rate != null && rate != 0) { updateNicNetworkRate(nicId, rate); } } catch (Exception e) { @@ -143,13 +138,19 @@ private Integer computeLegacyNicNetworkRate(long networkId, long instanceId, boo return getNetworkOfferingNetworkRate(network.getNetworkOfferingId(), network.getDataCenterId()); } + // Raw SQL instead of NicDao.listByVmId(): its SearchBuilder isn't safe to use on a DAO built outside Spring here. private Integer findRouterGuestNetworkRate(long routerInstanceId, long dataCenterId) { - final List routerNics = nicDao.listByVmId(routerInstanceId); - for (final NicVO routerNic : routerNics) { - final NetworkVO nw = networkDao.findById(routerNic.getNetworkId()); - if (nw != null && TrafficType.Guest.equals(nw.getTrafficType())) { - return getNetworkOfferingNetworkRate(nw.getNetworkOfferingId(), dataCenterId); + final String sql = "SELECT n.network_offering_id FROM nics ni JOIN networks n ON ni.network_id = n.id " + + "WHERE ni.instance_id = ? AND ni.removed IS NULL AND n.traffic_type = 'Guest' LIMIT 1"; + try (PreparedStatement pstmt = TransactionLegacy.currentTxn().prepareStatement(sql)) { + pstmt.setLong(1, routerInstanceId); + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + return getNetworkOfferingNetworkRate(rs.getLong(1), dataCenterId); + } } + } catch (SQLException e) { + LOGGER.warn("Failed to find router's guest network for instance id=" + routerInstanceId + ": " + e.getMessage()); } return null; } From f7d3e723754640ca4a9745fcbafc7540a5ffc18d Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 18 Sep 2026 17:21:16 +0530 Subject: [PATCH 28/29] Revert incorrect guard change from the previous NetworkRateBackfill fix nics.network_rate is an unsigned int column, so it cannot store -1; NULL is the established convention for "unlimited" there, exactly matching how the live precedence code (NetworkOrchestrator.allocateNic()) already stores it. Changing the backfill's guard to write -1 caused a real "Data truncation: Out of range value" failure, confirmed via a second real upgrade run. The original rate > 0 check (skip writing on unlimited, leaving NULL) was correct; only the router public-NIC NPE fix from the previous commit was a genuine bug. --- .../src/main/java/com/cloud/upgrade/NetworkRateBackfill.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java index 2f9c99136d1b..d4d4db47c254 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -90,7 +90,7 @@ private void backfillNicNetworkRates() { final boolean defaultNic = rs.getBoolean("default_nic"); try { final Integer rate = computeLegacyNicNetworkRate(networkId, instanceId, defaultNic); - if (rate != null && rate != 0) { + if (rate != null && rate > 0) { updateNicNetworkRate(nicId, rate); } } catch (Exception e) { From c9b2ce2f2ca78f67cfbcb06b04667adfe26f8791 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 18 Sep 2026 17:27:06 +0530 Subject: [PATCH 29/29] Store -1 directly for unlimited NIC network rate instead of NULL nics.network_rate was unsigned, so the only way to represent "unlimited" was NULL, even though every response-building call site already normalizes null/non-positive to -1 for API output - meaning NULL and -1 were already behaviorally equivalent, just inconsistent at the storage layer. NetworkModelImpl.getNetworkRate() never actually returns null (always a positive rate or -1), so the null-coalescing in NetworkOrchestrator was dead code once the column can hold -1 directly. Make the column signed so it can store -1 like every other "resolved" rate value in the codebase, simplify both NetworkOrchestrator call sites to store the value as-is, and let the backfill persist -1 for NICs whose legacy rate is unlimited instead of leaving them NULL. --- .../cloudstack/engine/orchestration/NetworkOrchestrator.java | 4 ++-- .../src/main/java/com/cloud/upgrade/NetworkRateBackfill.java | 2 +- .../src/main/resources/META-INF/db/schema-42300to2400.sql | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 1138ddcce8ec..7059c531b07d 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -1233,7 +1233,7 @@ public Pair allocateNic(final NicProfile requested, final N NicVO vo = checkForRaceAndAllocateNic(requested, network, isDefaultNic, deviceId, vm); final Integer networkRate = _networkModel.getNetworkRate(network.getId(), vm.getId()); - vo.setNetworkRate(networkRate != null && networkRate > 0 ? networkRate : null); + vo.setNetworkRate(networkRate); final NicProfile vmNic = new NicProfile(vo, network, vo.getBroadcastUri(), vo.getIsolationUri(), networkRate, _networkModel.isSecurityGroupSupportedInNetwork(network), _networkModel.getNetworkTag(vm.getHypervisorType(), network)); if (vm.getType() == Type.DomainRouter) { @@ -2307,7 +2307,7 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy nic.setState(Nic.State.Reserved); } - nic.setNetworkRate(networkRate != null && networkRate > 0 ? networkRate : null); + nic.setNetworkRate(networkRate); if (vmProfile.getType() == Type.DomainRouter) { Pair networks = getGuestNetworkRouterAndVpcDetails(vmProfile.getId()); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java index d4d4db47c254..2f9c99136d1b 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/NetworkRateBackfill.java @@ -90,7 +90,7 @@ private void backfillNicNetworkRates() { final boolean defaultNic = rs.getBoolean("default_nic"); try { final Integer rate = computeLegacyNicNetworkRate(networkId, instanceId, defaultNic); - if (rate != null && rate > 0) { + if (rate != null && rate != 0) { updateNicNetworkRate(nicId, rate); } } catch (Exception e) { diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 1a084b76d854..800386275f21 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -19,6 +19,6 @@ -- Schema upgrade from 4.23.0.0 to 24.0.0 --; -ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int unsigned DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC'; +ALTER TABLE `cloud`.`nics` ADD COLUMN `network_rate` int DEFAULT NULL COMMENT 'effective network rate in Mb/s for this NIC, -1 means unlimited'; ALTER TABLE `cloud`.`vpc_offerings` ADD COLUMN `public_nw_rate` smallint unsigned DEFAULT NULL COMMENT 'public gateway (internet-facing) network rate throttle mbits/s';