From 626cf253f161c651b410a100fb24662b64353c14 Mon Sep 17 00:00:00 2001 From: "dejing.liu" Date: Wed, 24 Jun 2026 12:02:12 +0800 Subject: [PATCH 1/4] [lb]: support tcp ipvs listener Persist listener dataPlane and forwardMode for TCP IPVS full_nat. Expose the fields through inventory, SDK, CLI binding, and lb.xml. Keep generated SDK forwardMode valid values aligned with P0. Validate TCP IPVS creation and cover schema upgrade behavior. Test: ./runMavenProfile sdk. Resolves: ZSTAC-86152 Change-Id: I85298aceccc2b3bba6260b093864bf5158935244 --- conf/db/upgrade/V5.5.28__schema.sql | 12 + .../lb/APICreateLoadBalancerListenerMsg.java | 20 ++ ...ateLoadBalancerListenerMsgDoc_zh_cn.groovy | 20 ++ .../lb/LoadBalancerApiInterceptor.java | 32 +++ .../network/service/lb/LoadBalancerBase.java | 2 + .../service/lb/LoadBalancerConstants.java | 7 + .../lb/LoadBalancerListenerInventory.java | 23 ++ .../service/lb/LoadBalancerListenerVO.java | 22 ++ .../service/lb/LoadBalancerListenerVO_.java | 2 + .../service/lb/LoadBalancerManagerImpl.java | 5 +- .../lb/VirtualRouterLoadBalancerBackend.java | 20 ++ .../sdk/CreateLoadBalancerListenerAction.java | 6 + .../sdk/LoadBalancerListenerInventory.java | 16 ++ .../db/schema/CheckSchemaUpgradeCase.groovy | 14 + .../TcpIpvsLoadBalancerListenerApiCase.groovy | 262 ++++++++++++++++++ 15 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy diff --git a/conf/db/upgrade/V5.5.28__schema.sql b/conf/db/upgrade/V5.5.28__schema.sql index 7c1ab9fe15f..8252f8d2233 100644 --- a/conf/db/upgrade/V5.5.28__schema.sql +++ b/conf/db/upgrade/V5.5.28__schema.sql @@ -98,3 +98,15 @@ CREATE TABLE IF NOT EXISTS `zstack`.`ScimEventVO` ( UNIQUE KEY `ukScimEventVOClientEvent` (`clientId`, `eventId`), KEY `idxScimEventVOResourceVersion` (`clientId`, `resourceType`, `resourceId`, `resourceVersion`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- SUG-2795: listener-level TCP IPVS data plane and forward mode. +CALL ADD_COLUMN('LoadBalancerListenerVO', 'data_plane', 'VARCHAR(32)', 1, NULL); +CALL ADD_COLUMN('LoadBalancerListenerVO', 'forward_mode', 'VARCHAR(32)', 1, NULL); + +UPDATE `zstack`.`LoadBalancerListenerVO` +SET data_plane = 'ipvs' +WHERE protocol = 'udp' AND data_plane IS NULL; + +UPDATE `zstack`.`LoadBalancerListenerVO` +SET data_plane = 'haproxy' +WHERE data_plane IS NULL; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java index 5b495571cc9..9f5b3898b86 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java @@ -53,6 +53,10 @@ public class APICreateLoadBalancerListenerMsg extends APICreateMessage implement private List aclUuids; @APIParam(validValues = {"white","black"}, required = false) private String aclType = LoadBalancerAclType.black.toString(); + @APIParam(validValues = {LoadBalancerConstants.DATA_PLANE_HAPROXY, LoadBalancerConstants.DATA_PLANE_IPVS}, required = false) + private String dataPlane; + @APIParam(validValues = {LoadBalancerConstants.FORWARD_MODE_FULL_NAT}, required = false) + private String forwardMode; @APIParam(validValues = {LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_DEFAULT, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_0, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_1, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_2, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_2_STRICT, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_2_STRICT_WITH_1_3}, required = false) @@ -180,6 +184,22 @@ public void setAclType(String aclType) { this.aclType = aclType; } + public String getDataPlane() { + return dataPlane; + } + + public void setDataPlane(String dataPlane) { + this.dataPlane = dataPlane; + } + + public String getForwardMode() { + return forwardMode; + } + + public void setForwardMode(String forwardMode) { + this.forwardMode = forwardMode; + } + public String getSecurityPolicyType() { return securityPolicyType; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy index 9496c068530..312db7b08c2 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy @@ -225,6 +225,26 @@ doc { optional true since "5.0.0" } + column { + name "dataPlane" + enclosedIn "params" + desc "" + location "body" + type "String" + optional true + since "5.5.28" + values ("haproxy","ipvs") + } + column { + name "forwardMode" + enclosedIn "params" + desc "" + location "body" + type "String" + optional true + since "5.5.28" + values ("full_nat") + } } } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java index b846b54185e..d53b3ea96f2 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java @@ -787,6 +787,38 @@ private void validate(APICreateLoadBalancerListenerMsg msg) { } } + if (msg.getDataPlane() == null) { + msg.setDataPlane(LoadBalancerConstants.DATA_PLANE_HAPROXY); + } + + if (LoadBalancerConstants.DATA_PLANE_IPVS.equals(msg.getDataPlane())) { + if (!LB_PROTOCOL_TCP.equals(msg.getProtocol())) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "data plane [%s] only supports tcp listener", msg.getDataPlane())); + } + + if (msg.getForwardMode() == null) { + msg.setForwardMode(LoadBalancerConstants.FORWARD_MODE_FULL_NAT); + } + + if (!LoadBalancerConstants.FORWARD_MODE_FULL_NAT.equals(msg.getForwardMode())) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "TCP IPVS only supports forwardMode[%s] in current version, but got [%s]", + LoadBalancerConstants.FORWARD_MODE_FULL_NAT, msg.getForwardMode())); + } + + } else { + if (!LoadBalancerConstants.DATA_PLANE_HAPROXY.equals(msg.getDataPlane())) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "invalid dataPlane[%s], valid dataPlanes are [haproxy, ipvs]", msg.getDataPlane())); + } + + if (msg.getForwardMode() != null) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "forwardMode is only supported when dataPlane is ipvs")); + } + } + List healthCheckTargets = getSystemTagTokens(msg, LoadBalancerSystemTags.HEALTH_TARGET, LoadBalancerSystemTags.HEALTH_TARGET_TOKEN); if (healthCheckTargets.size() > 1) { diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java index e219f24eb8b..97536dbb100 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java @@ -1836,6 +1836,8 @@ private void createListener(final APICreateLoadBalancerListenerMsg msg, final No vo.setInstancePort(msg.getInstancePort()); vo.setLoadBalancerPort(msg.getLoadBalancerPort()); vo.setProtocol(msg.getProtocol()); + vo.setDataPlane(msg.getDataPlane()); + vo.setForwardMode(LoadBalancerConstants.DATA_PLANE_IPVS.equals(msg.getDataPlane()) ? msg.getForwardMode() : null); vo.setAccountUuid(msg.getSession().getAccountUuid()); vo.setSecurityPolicyType(msg.getSecurityPolicyType()); vo = dbf.persistAndRefresh(vo); diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java index f48b53c930b..7b2d6c86b28 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java @@ -111,6 +111,13 @@ public String toString() { public static final int PROTOCOL_HTTP_DEFAULT_PORT = 80; public static final int PROTOCOL_HTTPS_DEFAULT_PORT = 443; + public static final String DATA_PLANE_HAPROXY = "haproxy"; + public static final String DATA_PLANE_IPVS = "ipvs"; + + public static final String FORWARD_MODE_FULL_NAT = "full_nat"; + public static final String FORWARD_MODE_NAT = "nat"; + public static final String FORWARD_MODE_DR = "dr"; + public static final int DNS_PORT = 53; public static final int SSH_PORT = 22; public static final int ZVR_PORT = 7272; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerInventory.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerInventory.java index 3650a9fd9b2..2111af073ea 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerInventory.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerInventory.java @@ -38,6 +38,8 @@ public class LoadBalancerListenerInventory implements Serializable { private Integer loadBalancerPort; private String securityPolicyType; private String protocol; + private String dataPlane; + private String forwardMode; private String serverGroupUuid; private Timestamp createDate; private Timestamp lastOpDate; @@ -55,6 +57,11 @@ public static LoadBalancerListenerInventory valueOf(LoadBalancerListenerVO vo) { inv.setInstancePort(vo.getInstancePort()); inv.setLoadBalancerPort(vo.getLoadBalancerPort()); inv.setProtocol(vo.getProtocol()); + if (LoadBalancerConstants.LB_PROTOCOL_TCP.equals(vo.getProtocol()) && + LoadBalancerConstants.DATA_PLANE_IPVS.equals(vo.getDataPlane())) { + inv.setDataPlane(vo.getDataPlane()); + inv.setForwardMode(vo.getForwardMode()); + } inv.setSecurityPolicyType(vo.getSecurityPolicyType()); inv.setName(vo.getName()); inv.setDescription(vo.getDescription()); @@ -164,6 +171,22 @@ public void setProtocol(String protocol) { this.protocol = protocol; } + public String getDataPlane() { + return dataPlane; + } + + public void setDataPlane(String dataPlane) { + this.dataPlane = dataPlane; + } + + public String getForwardMode() { + return forwardMode; + } + + public void setForwardMode(String forwardMode) { + this.forwardMode = forwardMode; + } + public String getSecurityPolicyType() { return securityPolicyType; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO.java index eb153fd6577..d77c0a8c61c 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO.java @@ -48,6 +48,12 @@ public class LoadBalancerListenerVO extends ResourceVO implements OwnedByAccount @Column private String protocol; + @Column(name = "data_plane") + private String dataPlane; + + @Column(name = "forward_mode") + private String forwardMode; + @Column private String securityPolicyType; @@ -156,6 +162,22 @@ public void setProtocol(String protocol) { this.protocol = protocol; } + public String getDataPlane() { + return dataPlane; + } + + public void setDataPlane(String dataPlane) { + this.dataPlane = dataPlane; + } + + public String getForwardMode() { + return forwardMode; + } + + public void setForwardMode(String forwardMode) { + this.forwardMode = forwardMode; + } + public String getSecurityPolicyType() { return securityPolicyType; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO_.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO_.java index 4051b03137b..d3922257359 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO_.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerListenerVO_.java @@ -18,6 +18,8 @@ public class LoadBalancerListenerVO_ extends ResourceVO_ { public static volatile SingularAttribute instancePort; public static volatile SingularAttribute loadBalancerPort; public static volatile SingularAttribute protocol; + public static volatile SingularAttribute dataPlane; + public static volatile SingularAttribute forwardMode; public static volatile SingularAttribute serverGroupUuid; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java index 6b4da585104..3c1d0d0d3e0 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java @@ -740,8 +740,11 @@ public void validateSystemTag(String resourceUuid, Class resourceType, String sy .eq(LoadBalancerListenerVO_.uuid, resourceUuid) .find(); if (listener != null) { + boolean isTcpIpvsListener = LoadBalancerConstants.LB_PROTOCOL_TCP.equals(listener.getProtocol()) && + LoadBalancerConstants.DATA_PLANE_IPVS.equals(listener.getDataPlane()); if (LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_NONE.equals(protocol) && - !LoadBalancerConstants.LB_PROTOCOL_UDP.equals(listener.getProtocol())) { + !LoadBalancerConstants.LB_PROTOCOL_UDP.equals(listener.getProtocol()) && + !isTcpIpvsListener) { throw new OperationFailureException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10014, "invalid health target[%s], health check protocol none is only supported by udp listener", systemTag)); } } diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java index 1b4266d6e05..c577213a043 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java @@ -267,6 +267,8 @@ public static class LbTO { int instancePort; int loadBalancerPort; String mode; + String dataPlane; + String forwardMode; List parameters; String certificateUuid; String securityPolicyType; @@ -462,6 +464,22 @@ public void setMode(String mode) { this.mode = mode; } + public String getDataPlane() { + return dataPlane; + } + + public void setDataPlane(String dataPlane) { + this.dataPlane = dataPlane; + } + + public String getForwardMode() { + return forwardMode; + } + + public void setForwardMode(String forwardMode) { + this.forwardMode = forwardMode; + } + public String getCertificateUuid() { return certificateUuid; } @@ -898,6 +916,8 @@ public LbTO call(LoadBalancerListenerInventory l) { to.setLbUuid(l.getLoadBalancerUuid()); to.setListenerUuid(l.getUuid()); to.setMode(l.getProtocol()); + to.setDataPlane(l.getDataPlane()); + to.setForwardMode(l.getForwardMode()); if (vip != null) { to.setVip(vip.getIp()); } diff --git a/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java b/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java index dd4399e0b0f..57573934838 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java @@ -67,6 +67,12 @@ public Result throwExceptionIfError() { @Param(required = false, validValues = {"white","black"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String aclType = "black"; + @Param(required = false, validValues = {"haproxy","ipvs"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String dataPlane; + + @Param(required = false, validValues = {"full_nat"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String forwardMode; + @Param(required = false, validValues = {"tls_cipher_policy_default","tls_cipher_policy_1_0","tls_cipher_policy_1_1","tls_cipher_policy_1_2","tls_cipher_policy_1_2_strict","tls_cipher_policy_1_2_strict_with_1_3"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String securityPolicyType; diff --git a/sdk/src/main/java/org/zstack/sdk/LoadBalancerListenerInventory.java b/sdk/src/main/java/org/zstack/sdk/LoadBalancerListenerInventory.java index fe361c584e5..5dc0eeecaec 100644 --- a/sdk/src/main/java/org/zstack/sdk/LoadBalancerListenerInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/LoadBalancerListenerInventory.java @@ -68,6 +68,22 @@ public java.lang.String getProtocol() { return this.protocol; } + public java.lang.String dataPlane; + public void setDataPlane(java.lang.String dataPlane) { + this.dataPlane = dataPlane; + } + public java.lang.String getDataPlane() { + return this.dataPlane; + } + + public java.lang.String forwardMode; + public void setForwardMode(java.lang.String forwardMode) { + this.forwardMode = forwardMode; + } + public java.lang.String getForwardMode() { + return this.forwardMode; + } + public java.lang.String serverGroupUuid; public void setServerGroupUuid(java.lang.String serverGroupUuid) { this.serverGroupUuid = serverGroupUuid; diff --git a/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckSchemaUpgradeCase.groovy b/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckSchemaUpgradeCase.groovy index 91d985ce50e..773206c7303 100644 --- a/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckSchemaUpgradeCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckSchemaUpgradeCase.groovy @@ -32,5 +32,19 @@ class CheckSchemaUpgradeCase extends SubCase { @Override void test() { + testLoadBalancerListenerDataPlaneUpgradeSchema() + } + + void testLoadBalancerListenerDataPlaneUpgradeSchema() { + String upgradeSchemaDir = Paths.get("../conf/db/upgrade").toAbsolutePath().normalize().toString() + File schema = new File(upgradeSchemaDir + "/V5.5.28__schema.sql") + assert schema.exists() + + String sql = schema.text + assert sql.contains("CALL ADD_COLUMN('LoadBalancerListenerVO', 'data_plane', 'VARCHAR(32)', 1, NULL)") + assert sql.contains("CALL ADD_COLUMN('LoadBalancerListenerVO', 'forward_mode', 'VARCHAR(32)', 1, NULL)") + assert sql.contains("WHERE protocol = 'udp' AND data_plane IS NULL") + assert sql.contains("SET data_plane = 'haproxy'") + assert sql.contains("WHERE data_plane IS NULL") } } diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy new file mode 100644 index 00000000000..b859cd9e458 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy @@ -0,0 +1,262 @@ +package org.zstack.test.integration.networkservice.provider.virtualrouter.loadbalancer + +import org.zstack.core.db.Q +import org.zstack.header.network.service.NetworkServiceType +import org.zstack.network.service.eip.EipConstant +import org.zstack.network.service.lb.LoadBalancerConstants +import org.zstack.network.service.lb.LoadBalancerListenerVO +import org.zstack.network.service.lb.LoadBalancerListenerVO_ +import org.zstack.network.service.portforwarding.PortForwardingConstant +import org.zstack.network.service.virtualrouter.vyos.VyosConstants +import org.zstack.sdk.ApiResult +import org.zstack.sdk.CreateLoadBalancerListenerAction +import org.zstack.sdk.CreateLoadBalancerListenerResult +import org.zstack.sdk.L3NetworkInventory +import org.zstack.sdk.LoadBalancerInventory +import org.zstack.sdk.LoadBalancerListenerInventory +import org.zstack.sdk.VipInventory +import org.zstack.sdk.ZSClient +import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit + +import static java.util.Arrays.asList + +class TcpIpvsLoadBalancerListenerApiCase extends SubCase { + EnvSpec env + LoadBalancerInventory lb + + @Override + void setup() { + useSpring(NetworkServiceProviderTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image" + url = "http://zstack.org/download/test.qcow2" + } + + image { + name = "vr" + url = "http://zstack.org/download/vr.qcow2" + } + } + zone { + name = "zone" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "localhost" + username = "root" + password = "password" + totalCpu = 8 + totalMem = SizeUnit.GIGABYTE.toByte(12) + } + + attachPrimaryStorage("local") + attachL2Network("l2") + } + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "publicL3" + ip { + startIp = "172.20.58.160" + endIp = "172.20.58.200" + gateway = "172.20.0.1" + netmask = "255.255.0.0" + } + service { + provider = VyosConstants.VYOS_ROUTER_PROVIDER_TYPE + types = [NetworkServiceType.DHCP.toString(), + NetworkServiceType.DNS.toString(), + PortForwardingConstant.PORTFORWARDING_NETWORK_SERVICE_TYPE, + LoadBalancerConstants.LB_NETWORK_SERVICE_TYPE_STRING, + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + } + + l3Network { + name = "managementL3" + ip { + startIp = "172.21.58.160" + endIp = "172.21.58.200" + gateway = "172.21.0.1" + netmask = "255.255.0.0" + } + service { + provider = VyosConstants.VYOS_ROUTER_PROVIDER_TYPE + types = [NetworkServiceType.DHCP.toString(), + NetworkServiceType.DNS.toString()] + } + } + } + attachBackupStorage("sftp") + + virtualRouterOffering { + name = "vro" + memory = SizeUnit.MEGABYTE.toByte(512) + cpu = 2 + useManagementL3Network("managementL3") + usePublicL3Network("publicL3") + useImage("vr") + isDefault = true + } + } + } + } + + @Override + void test() { + env.create { + prepareDedicatedLoadBalancer() + testTcpHaproxyDefaultDataPlane() + testTcpIpvsFullNatListener() + testTcpIpvsDefaultForwardMode() + testTcpIpvsCreateValidation() + } + } + + void prepareDedicatedLoadBalancer() { + L3NetworkInventory publicL3 = env.inventoryByName("publicL3") as L3NetworkInventory + VipInventory vip = createVip { + delegate.name = "tcp-ipvs-api-vip" + delegate.l3NetworkUuid = publicL3.uuid + } + + lb = createLoadBalancer { + delegate.name = "tcp-ipvs-api-lb" + delegate.vipUuid = vip.uuid + delegate.systemTags = asList("separateVirtualRouterVm") + } + } + + void testTcpHaproxyDefaultDataPlane() { + CreateLoadBalancerListenerAction action = new CreateLoadBalancerListenerAction() + action.name = "tcp-haproxy-default" + action.loadBalancerUuid = lb.uuid + action.protocol = LoadBalancerConstants.LB_PROTOCOL_TCP + action.loadBalancerPort = 11080 + action.instancePort = 8080 + action.sessionId = adminSession() + + ApiResult result = ZSClient.call(action) + assert result.error == null + + String rawResult = getApiResultString(result) + assert !rawResult.contains("\"dataPlane\"") + assert !rawResult.contains("\"forwardMode\"") + + LoadBalancerListenerInventory listener = result.getResult(CreateLoadBalancerListenerResult.class).inventory + + assert listener.dataPlane == null + assert listener.forwardMode == null + + LoadBalancerListenerVO vo = Q.New(LoadBalancerListenerVO.class) + .eq(LoadBalancerListenerVO_.uuid, listener.uuid) + .find() + assert vo.dataPlane == LoadBalancerConstants.DATA_PLANE_HAPROXY + assert vo.forwardMode == null + } + + void testTcpIpvsFullNatListener() { + LoadBalancerListenerInventory listener = createLoadBalancerListener { + delegate.name = "tcp-ipvs-full-nat" + delegate.loadBalancerUuid = lb.uuid + delegate.protocol = LoadBalancerConstants.LB_PROTOCOL_TCP + delegate.loadBalancerPort = 11081 + delegate.instancePort = 8080 + delegate.dataPlane = LoadBalancerConstants.DATA_PLANE_IPVS + delegate.forwardMode = LoadBalancerConstants.FORWARD_MODE_FULL_NAT + } + + assert listener.dataPlane == LoadBalancerConstants.DATA_PLANE_IPVS + assert listener.forwardMode == LoadBalancerConstants.FORWARD_MODE_FULL_NAT + + LoadBalancerListenerVO vo = Q.New(LoadBalancerListenerVO.class) + .eq(LoadBalancerListenerVO_.uuid, listener.uuid) + .find() + assert vo.dataPlane == LoadBalancerConstants.DATA_PLANE_IPVS + assert vo.forwardMode == LoadBalancerConstants.FORWARD_MODE_FULL_NAT + assert vo.instancePort == 8080 + } + + void testTcpIpvsDefaultForwardMode() { + LoadBalancerListenerInventory listener = createLoadBalancerListener { + delegate.name = "tcp-ipvs-default-forward-mode" + delegate.loadBalancerUuid = lb.uuid + delegate.protocol = LoadBalancerConstants.LB_PROTOCOL_TCP + delegate.loadBalancerPort = 11082 + delegate.instancePort = 8080 + delegate.dataPlane = LoadBalancerConstants.DATA_PLANE_IPVS + } + + assert listener.dataPlane == LoadBalancerConstants.DATA_PLANE_IPVS + assert listener.forwardMode == LoadBalancerConstants.FORWARD_MODE_FULL_NAT + } + + void testTcpIpvsCreateValidation() { + assertCreateListenerError(11083, LoadBalancerConstants.LB_PROTOCOL_HTTP, + LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_FULL_NAT) + assertCreateListenerError(11084, LoadBalancerConstants.LB_PROTOCOL_UDP, + LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_FULL_NAT) + assertCreateListenerError(11085, LoadBalancerConstants.LB_PROTOCOL_TCP, + LoadBalancerConstants.DATA_PLANE_HAPROXY, LoadBalancerConstants.FORWARD_MODE_FULL_NAT) + assertCreateListenerError(11086, LoadBalancerConstants.LB_PROTOCOL_TCP, + LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_NAT) + assertCreateListenerError(11087, LoadBalancerConstants.LB_PROTOCOL_TCP, + LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_DR) + } + + void assertCreateListenerError(int port, String protocol, String dataPlane, String forwardMode) { + CreateLoadBalancerListenerAction action = new CreateLoadBalancerListenerAction() + action.name = "tcp-ipvs-invalid-${port}" + action.loadBalancerUuid = lb.uuid + action.protocol = protocol + action.loadBalancerPort = port + action.instancePort = 8080 + action.dataPlane = dataPlane + action.forwardMode = forwardMode + action.sessionId = adminSession() + + CreateLoadBalancerListenerAction.Result result = action.call() + assert result.error != null + } + + String getApiResultString(ApiResult result) { + def field = ApiResult.class.getDeclaredField("resultString") + field.setAccessible(true) + return field.get(result) as String + } + + @Override + void clean() { + env.delete() + } +} From 814a79cfb770185889496556014bc2836670038e Mon Sep 17 00:00:00 2001 From: "dejing.liu" Date: Thu, 25 Jun 2026 13:26:50 +0800 Subject: [PATCH 2/4] [lb]: allow ipvs mode validation Allow the SDK to submit all declared TCP IPVS forward modes. This lets the API interceptor return the version validation error. Unsupported modes are still rejected by the backend interceptor. Keep the generated SDK and API Groovy template aligned. Related: ZSTAC-86152 Change-Id: I1d72e07c4343d1f6420033ab130543981d8be43b --- .../service/lb/APICreateLoadBalancerListenerMsg.java | 2 +- .../APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy | 2 +- .../zstack/sdk/CreateLoadBalancerListenerAction.java | 2 +- .../TcpIpvsLoadBalancerListenerApiCase.groovy | 10 +++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java index 9f5b3898b86..e8e4b4694dc 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java @@ -55,7 +55,7 @@ public class APICreateLoadBalancerListenerMsg extends APICreateMessage implement private String aclType = LoadBalancerAclType.black.toString(); @APIParam(validValues = {LoadBalancerConstants.DATA_PLANE_HAPROXY, LoadBalancerConstants.DATA_PLANE_IPVS}, required = false) private String dataPlane; - @APIParam(validValues = {LoadBalancerConstants.FORWARD_MODE_FULL_NAT}, required = false) + @APIParam(validValues = {LoadBalancerConstants.FORWARD_MODE_FULL_NAT, LoadBalancerConstants.FORWARD_MODE_NAT, LoadBalancerConstants.FORWARD_MODE_DR}, required = false) private String forwardMode; @APIParam(validValues = {LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_DEFAULT, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_0, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_1, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_2, LoadBalanceSecurityPolicyConstant.TLS_CIPHER_POLICY_1_2_STRICT, diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy index 312db7b08c2..a959973ee74 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy @@ -243,7 +243,7 @@ doc { type "String" optional true since "5.5.28" - values ("full_nat") + values ("full_nat","nat","dr") } } } diff --git a/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java b/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java index 57573934838..f0693d3c872 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateLoadBalancerListenerAction.java @@ -70,7 +70,7 @@ public Result throwExceptionIfError() { @Param(required = false, validValues = {"haproxy","ipvs"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String dataPlane; - @Param(required = false, validValues = {"full_nat"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = false, validValues = {"full_nat","nat","dr"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String forwardMode; @Param(required = false, validValues = {"tls_cipher_policy_default","tls_cipher_policy_1_0","tls_cipher_policy_1_1","tls_cipher_policy_1_2","tls_cipher_policy_1_2_strict","tls_cipher_policy_1_2_strict_with_1_3"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy index b859cd9e458..2dec78d5cc1 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy @@ -228,13 +228,16 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_FULL_NAT) assertCreateListenerError(11085, LoadBalancerConstants.LB_PROTOCOL_TCP, LoadBalancerConstants.DATA_PLANE_HAPROXY, LoadBalancerConstants.FORWARD_MODE_FULL_NAT) - assertCreateListenerError(11086, LoadBalancerConstants.LB_PROTOCOL_TCP, + CreateLoadBalancerListenerAction.Result natResult = assertCreateListenerError(11086, LoadBalancerConstants.LB_PROTOCOL_TCP, LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_NAT) - assertCreateListenerError(11087, LoadBalancerConstants.LB_PROTOCOL_TCP, + assert natResult.error.details.contains("TCP IPVS only supports forwardMode[full_nat]") + + CreateLoadBalancerListenerAction.Result drResult = assertCreateListenerError(11087, LoadBalancerConstants.LB_PROTOCOL_TCP, LoadBalancerConstants.DATA_PLANE_IPVS, LoadBalancerConstants.FORWARD_MODE_DR) + assert drResult.error.details.contains("TCP IPVS only supports forwardMode[full_nat]") } - void assertCreateListenerError(int port, String protocol, String dataPlane, String forwardMode) { + CreateLoadBalancerListenerAction.Result assertCreateListenerError(int port, String protocol, String dataPlane, String forwardMode) { CreateLoadBalancerListenerAction action = new CreateLoadBalancerListenerAction() action.name = "tcp-ipvs-invalid-${port}" action.loadBalancerUuid = lb.uuid @@ -247,6 +250,7 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { CreateLoadBalancerListenerAction.Result result = action.call() assert result.error != null + return result } String getApiResultString(ApiResult result) { From cad44bada53fafded5d3153a2b0f6620e0bcb2f8 Mon Sep 17 00:00:00 2001 From: "dejing.liu" Date: Thu, 25 Jun 2026 23:04:49 +0800 Subject: [PATCH 3/4] [lb]: validate tcp ipvs health check Add TCP IPVS listener health check API validation. Update listener tags for timeout changes and SDK actions. Test: lb and vr provider package builds; sdk; docpremium. Resolves: ZSTAC-86152 Change-Id: I83f933cc780329ba8fb63ad6f3f3958693812de1 --- .../lb/APIChangeLoadBalancerListenerMsg.java | 11 +++ ...ngeLoadBalancerListenerMsgDoc_zh_cn.groovy | 9 +++ .../lb/LoadBalancerApiInterceptor.java | 80 ++++++++++++++++++- .../network/service/lb/LoadBalancerBase.java | 18 +++++ .../service/lb/LoadBalancerConstants.java | 3 + .../service/lb/LoadBalancerManagerImpl.java | 2 +- .../sdk/ChangeLoadBalancerListenerAction.java | 3 + .../zstack/test/lb/TestVirtualRouterLb13.java | 33 ++++++++ 8 files changed, 155 insertions(+), 4 deletions(-) diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java index 3d77b9d1335..6e47b12bebe 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java @@ -44,6 +44,9 @@ public class APIChangeLoadBalancerListenerMsg extends APIMessage implements Load @APIParam(numberRange = {LoadBalancerConstants.HEALTH_CHECK_INTERVAL_MIN, LoadBalancerConstants.HEALTH_CHECK_INTERVAL_MAX}, required = false) private Integer healthCheckInterval; + @APIParam(numberRange = {LoadBalancerConstants.HEALTH_CHECK_TIMEOUT_MIN, LoadBalancerConstants.HEALTH_CHECK_TIMEOUT_MAX}, required = false) + private Integer healthCheckTimeout; + @APIParam(validValues = {LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_TCP, LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_UDP, LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_HTTP, LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_NONE}, required = false) private String healthCheckProtocol; @APIParam(validValues = {"GET", "HEAD"}, required = false) @@ -167,6 +170,14 @@ public void setHealthCheckInterval(Integer healthCheckInterval) { this.healthCheckInterval = healthCheckInterval; } + public Integer getHealthCheckTimeout() { + return healthCheckTimeout; + } + + public void setHealthCheckTimeout(Integer healthCheckTimeout) { + this.healthCheckTimeout = healthCheckTimeout; + } + public String getLoadBalancerUuid() { return loadBalancerUuid; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsgDoc_zh_cn.groovy b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsgDoc_zh_cn.groovy index c92f14186a4..bf2c1f80054 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsgDoc_zh_cn.groovy +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsgDoc_zh_cn.groovy @@ -94,6 +94,15 @@ doc { optional true since "3.4" } + column { + name "healthCheckTimeout" + enclosedIn "changeLoadBalancerListener" + desc "健康检查超时时间" + location "body" + type "Integer" + optional true + since "5.5.28" + } column { name "healthCheckProtocol" enclosedIn "changeLoadBalancerListener" diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java index d53b3ea96f2..707538f2d5a 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java @@ -728,18 +728,48 @@ private Boolean verifyHttpCode(String httpCode) { }); } - private boolean isHealthCheckProtocolNotSupportedByListenerProtocol(String listenerProtocol, String healthCheckProtocol) { + private boolean isHealthCheckProtocolNotSupportedByListenerProtocol(String listenerProtocol, String dataPlane, String healthCheckProtocol) { boolean isUdpListener = LoadBalancerConstants.LB_PROTOCOL_UDP.equals(listenerProtocol); + boolean isTcpIpvsListener = isTcpIpvsListener(listenerProtocol, dataPlane); boolean isUdpHealthCheck = LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_UDP.equals(healthCheckProtocol); + boolean isTcpHealthCheck = LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_TCP.equals(healthCheckProtocol); boolean isNoneHealthCheck = LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_NONE.equals(healthCheckProtocol); if (isUdpListener) { return !(isUdpHealthCheck || isNoneHealthCheck); } + if (isTcpIpvsListener) { + return !(isTcpHealthCheck || isNoneHealthCheck); + } + return isUdpHealthCheck || isNoneHealthCheck; } + private String getListenerDataPlane(String listenerUuid) { + LoadBalancerListenerVO listener = Q.New(LoadBalancerListenerVO.class) + .eq(LoadBalancerListenerVO_.uuid, listenerUuid) + .find(); + return listener == null || listener.getDataPlane() == null ? DATA_PLANE_HAPROXY : listener.getDataPlane(); + } + + private boolean isTcpIpvsListener(String listenerProtocol, String dataPlane) { + return LoadBalancerConstants.LB_PROTOCOL_TCP.equals(listenerProtocol) && + DATA_PLANE_IPVS.equals(dataPlane); + } + + private boolean hasHttpHealthCheckParameters(APICreateLoadBalancerListenerMsg msg) { + return msg.getHealthCheckMethod() != null || + msg.getHealthCheckURI() != null || + msg.getHealthCheckHttpCode() != null; + } + + private boolean hasHttpHealthCheckParameters(APIChangeLoadBalancerListenerMsg msg) { + return msg.getHealthCheckMethod() != null || + msg.getHealthCheckURI() != null || + msg.getHealthCheckHttpCode() != null; + } + private String getHealthCheckProtocolFromTarget(String healthCheckTarget) { if (healthCheckTarget == null) { return null; @@ -769,6 +799,36 @@ private boolean isNoneHealthCheckTargetWithSpecificPort(String healthCheckTarget !"default".equals(ts[1]); } + private boolean isValidHealthCheckProtocolInTarget(String protocol) { + return LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_TCP.equals(protocol) || + LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_UDP.equals(protocol) || + LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_HTTP.equals(protocol) || + LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_NONE.equals(protocol); + } + + private void normalizeChangeHealthCheckTarget(APIChangeLoadBalancerListenerMsg msg) { + String target = msg.getHealthCheckTarget(); + if (target == null || !target.contains(":")) { + return; + } + + String[] ts = target.split(":"); + if (ts.length != 2 || !isValidHealthCheckProtocolInTarget(ts[0])) { + throw new ApiMessageInterceptionException( + argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10100, "healthCheck target [%s] error, it must be 'default' or number between[1~65535] ", + target)); + } + + if (msg.getHealthCheckProtocol() != null && !msg.getHealthCheckProtocol().equals(ts[0])) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "the health check protocol [%s] conflicts with health check target [%s]", + msg.getHealthCheckProtocol(), target)); + } + + msg.setHealthCheckProtocol(ts[0]); + msg.setHealthCheckTarget(ts[1]); + } + private void validate(APICreateLoadBalancerListenerMsg msg) { LoadBalancerVO lbVO = dbf.findByUuid(msg.getLoadBalancerUuid(), LoadBalancerVO.class); @@ -818,6 +878,7 @@ private void validate(APICreateLoadBalancerListenerMsg msg) { operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "forwardMode is only supported when dataPlane is ipvs")); } } + String dataPlane = msg.getDataPlane(); List healthCheckTargets = getSystemTagTokens(msg, LoadBalancerSystemTags.HEALTH_TARGET, LoadBalancerSystemTags.HEALTH_TARGET_TOKEN); @@ -850,7 +911,13 @@ private void validate(APICreateLoadBalancerListenerMsg msg) { } } - if (isHealthCheckProtocolNotSupportedByListenerProtocol(msg.getProtocol(), msg.getHealthCheckProtocol())) { + if (isTcpIpvsListener(msg.getProtocol(), dataPlane) && + (hasHttpHealthCheckParameters(msg) || hasTag(msg, LoadBalancerSystemTags.HEALTH_PARAMETER))) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "tcp ipvs listener doesn't support http health check parameters")); + } + + if (isHealthCheckProtocolNotSupportedByListenerProtocol(msg.getProtocol(), dataPlane, msg.getHealthCheckProtocol())) { throw new ApiMessageInterceptionException( operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10062, "the listener with protocol [%s] doesn't support this health check:[%s]", msg.getProtocol(), msg.getHealthCheckProtocol())); @@ -1338,6 +1405,7 @@ private void validate(APIRemoveCertificateFromLoadBalancerListenerMsg msg) { } private void validate(APIChangeLoadBalancerListenerMsg msg) { + normalizeChangeHealthCheckTarget(msg); String target = msg.getHealthCheckTarget(); if (target != null) { if (!LoadBalancerConstants.HEALTH_CHECK_TARGET_DEFAULT.equals(target)) { @@ -1489,6 +1557,7 @@ private void validate(APIChangeLoadBalancerListenerMsg msg) { LoadBalancerListenerVO listenerVO = Q.New(LoadBalancerListenerVO.class). eq(LoadBalancerListenerVO_.uuid,msg.getLoadBalancerListenerUuid()).find(); + String dataPlane = getListenerDataPlane(msg.getLoadBalancerListenerUuid()); if (msg.getSecurityPolicyType() != null) { if (!listenerVO.getProtocol().equals(LB_PROTOCOL_HTTPS)) { @@ -1515,8 +1584,13 @@ private void validate(APIChangeLoadBalancerListenerMsg msg) { effectiveHealthCheckTarget)); } + if (isTcpIpvsListener(listenerVO.getProtocol(), dataPlane) && hasHttpHealthCheckParameters(msg)) { + throw new ApiMessageInterceptionException( + operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10179, "tcp ipvs listener doesn't support http health check parameters")); + } + if (msg.getHealthCheckProtocol() != null) { - if (isHealthCheckProtocolNotSupportedByListenerProtocol(listenerVO.getProtocol(), msg.getHealthCheckProtocol())) { + if (isHealthCheckProtocolNotSupportedByListenerProtocol(listenerVO.getProtocol(), dataPlane, msg.getHealthCheckProtocol())) { throw new ApiMessageInterceptionException( operr(ORG_ZSTACK_NETWORK_SERVICE_LB_10120, "the listener with protocol [%s] doesn't support this health check:[%s]", listenerVO.getProtocol(), msg.getHealthCheckProtocol())); diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java index 97536dbb100..75a1e7c5ffc 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java @@ -2336,6 +2336,9 @@ public void run(SyncTaskChain chain) { updateLoadBalancerListenerSystemTag(LoadBalancerSystemTags.CONNECTION_IDLE_TIMEOUT, msg.getUuid(), LoadBalancerSystemTags.CONNECTION_IDLE_TIMEOUT_TOKEN, msg.getConnectionIdleTimeout()); } + final String oldHealthCheckTimeout = LoadBalancerSystemTags.HEALTH_TIMEOUT.getTokenByResourceUuid( + msg.getUuid(), LoadBalancerSystemTags.HEALTH_TIMEOUT_TOKEN); + if (msg.getHealthCheckInterval() != null) { updateLoadBalancerListenerSystemTag(LoadBalancerSystemTags.HEALTH_INTERVAL, msg.getUuid(), LoadBalancerSystemTags.HEALTH_INTERVAL_TOKEN, msg.getHealthCheckInterval()); } @@ -2361,6 +2364,10 @@ public void run(SyncTaskChain chain) { updateLoadBalancerListenerSystemTag(LoadBalancerSystemTags.UNHEALTHY_THRESHOLD, msg.getUuid(), LoadBalancerSystemTags.UNHEALTHY_THRESHOLD_TOKEN, msg.getUnhealthyThreshold()); } + if (msg.getHealthCheckTimeout() != null) { + updateLoadBalancerListenerSystemTag(LoadBalancerSystemTags.HEALTH_TIMEOUT, msg.getUuid(), LoadBalancerSystemTags.HEALTH_TIMEOUT_TOKEN, msg.getHealthCheckTimeout()); + } + if (msg.getMaxConnection() != null) { updateLoadBalancerListenerSystemTag(LoadBalancerSystemTags.MAX_CONNECTION, msg.getUuid(), LoadBalancerSystemTags.MAX_CONNECTION_TOKEN, msg.getMaxConnection()); } @@ -2490,6 +2497,17 @@ public void run(MessageReply reply) { LoadBalancerSystemTags.BALANCER_ACL.delete(msg.getUuid()); } } + if (msg.getHealthCheckTimeout() != null) { + logger.warn(String.format( "rollback health check timeout for listener [uuid:%s]", msg.getUuid())); + if (oldHealthCheckTimeout != null) { + LoadBalancerSystemTags.HEALTH_TIMEOUT.update(msg.getUuid(), + LoadBalancerSystemTags.HEALTH_TIMEOUT.instantiateTag(map( + e(LoadBalancerSystemTags.HEALTH_TIMEOUT_TOKEN, oldHealthCheckTimeout) + ))); + } else { + LoadBalancerSystemTags.HEALTH_TIMEOUT.delete(msg.getUuid()); + } + } } else { evt.setInventory(LoadBalancerListenerInventory.valueOf(lblVo)); } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java index 7b2d6c86b28..d0edf9139e2 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerConstants.java @@ -166,6 +166,9 @@ public static enum Param { public static final int HEALTH_CHECK_INTERVAL_MIN = 1; public static final int HEALTH_CHECK_INTERVAL_MAX = Integer.MAX_VALUE; + public static final int HEALTH_CHECK_TIMEOUT_MIN = 1; + public static final int HEALTH_CHECK_TIMEOUT_MAX = Integer.MAX_VALUE; + public static final int NUMBER_OF_PROCESS_MIN = 1; public static final int NUMBER_OF_PROCESS_MAX = 64; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java index 3c1d0d0d3e0..fe6c99a0826 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerManagerImpl.java @@ -745,7 +745,7 @@ public void validateSystemTag(String resourceUuid, Class resourceType, String sy if (LoadBalancerConstants.HEALTH_CHECK_TARGET_PROTOCL_NONE.equals(protocol) && !LoadBalancerConstants.LB_PROTOCOL_UDP.equals(listener.getProtocol()) && !isTcpIpvsListener) { - throw new OperationFailureException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10014, "invalid health target[%s], health check protocol none is only supported by udp listener", systemTag)); + throw new OperationFailureException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10014, "invalid health target[%s], health check protocol none is only supported by udp listener and tcp ipvs listener", systemTag)); } } diff --git a/sdk/src/main/java/org/zstack/sdk/ChangeLoadBalancerListenerAction.java b/sdk/src/main/java/org/zstack/sdk/ChangeLoadBalancerListenerAction.java index b5314b49922..6ab8413099c 100644 --- a/sdk/src/main/java/org/zstack/sdk/ChangeLoadBalancerListenerAction.java +++ b/sdk/src/main/java/org/zstack/sdk/ChangeLoadBalancerListenerAction.java @@ -49,6 +49,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,2147483647L}, noTrim = false) public java.lang.Integer healthCheckInterval; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,2147483647L}, noTrim = false) + public java.lang.Integer healthCheckTimeout; + @Param(required = false, validValues = {"tcp","udp","http","none"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String healthCheckProtocol; diff --git a/test/src/test/java/org/zstack/test/lb/TestVirtualRouterLb13.java b/test/src/test/java/org/zstack/test/lb/TestVirtualRouterLb13.java index 477c43f3d6a..d110a447e11 100755 --- a/test/src/test/java/org/zstack/test/lb/TestVirtualRouterLb13.java +++ b/test/src/test/java/org/zstack/test/lb/TestVirtualRouterLb13.java @@ -14,6 +14,7 @@ import org.zstack.simulator.appliancevm.ApplianceVmSimulatorConfig; import org.zstack.simulator.virtualrouter.VirtualRouterSimulatorConfig; import org.zstack.test.Api; +import org.zstack.test.ApiSender; import org.zstack.test.ApiSenderException; import org.zstack.test.DBUtil; import org.zstack.test.WebBeanConstructor; @@ -322,5 +323,37 @@ public String call(String arg) { } }); Assert.assertNotNull(tval); + + final Integer changedHealthTimeout = 12; + final String changedHealthTimeoutTag = LoadBalancerSystemTags.HEALTH_TIMEOUT.instantiateTag( + map(e(LoadBalancerSystemTags.HEALTH_TIMEOUT_TOKEN, changedHealthTimeout)) + ); + APIChangeLoadBalancerListenerMsg cmsg = new APIChangeLoadBalancerListenerMsg(); + cmsg.setUuid(l2.getUuid()); + cmsg.setHealthCheckTimeout(changedHealthTimeout); + cmsg.setSession(session); + ApiSender sender = new ApiSender(); + vconfig.refreshLbCmds.clear(); + sender.send(cmsg, APIChangeLoadBalancerListenerEvent.class); + + String changedTimeout = LoadBalancerSystemTags.HEALTH_TIMEOUT.getTokenByResourceUuid(l2.getUuid(), + LoadBalancerSystemTags.HEALTH_TIMEOUT_TOKEN); + Assert.assertEquals(changedHealthTimeout, Integer.valueOf(changedTimeout)); + + cmd = vconfig.refreshLbCmds.get(vconfig.refreshLbCmds.size() - 1); + to = CollectionUtils.find(cmd.getLbs(), new Function() { + @Override + public LbTO call(LbTO arg) { + return arg.getListenerUuid().equals(l2.getUuid()) ? arg : null; + } + }); + Assert.assertNotNull(to); + tval = CollectionUtils.find(to.getParameters(), new Function() { + @Override + public String call(String arg) { + return arg.equals(changedHealthTimeoutTag) ? arg : null; + } + }); + Assert.assertNotNull(JSONObjectUtil.toJsonString(to), tval); } } From a937ee2a5af08d0090f3ed483b84ff44ee68c859 Mon Sep 17 00:00:00 2001 From: "dejing.liu" Date: Fri, 26 Jun 2026 17:37:39 +0800 Subject: [PATCH 4/4] [lb]: keep shared server ip guard Keep shared load balancer server IP backend validation unchanged for the current TCP IPVS T2 patch. Do not use separateVirtualRouterVm as a broad capability switch, and remove the no-VM-NIC providerType fallback that depended on that tag. Test: git diff --check Resolves: ZSTAC-86152 Change-Id: Ib1b6ea92fa8b4579b6c8a22be7cfe256 [lb]: keep slb multi nic compatibility Restore the legacy VM nic backend fallback when selecting the backend L3 used to create a virtual router. Keep the stricter multi-L3 validation for server IP backend discovery, where there is no VM nic ordering to preserve. Test: git diff --check Test: mvn -pl plugin/loadBalancer,plugin/virtualRouterProvider -am -DskipTests compile Resolves: ZSTAC-86152 Change-Id: I0a5774b8e4fdef15fb9c5d0a96459dd23b6b669c [lb]: align listener doc template Match the SUG-2795 listener doc template output generated by the UTBuild docpremium check so the working tree stays clean after template generation. Test: git diff --check Resolves: ZSTAC-86152 Change-Id: I337645c6290c9fc730057439ec4d38633c2c7336 [lb]: resolve T2 review blockers Fix the SUG-2795 T2 Cloud review findings on top of the feature LB IPVS branch. Add API docs for IPVS listener fields, return the dedicated VR found through peer-L3 fallback, and collect all matching backend L3 networks for server IP backends. Test: git diff --check Resolves: ZSTAC-86152 Change-Id: Id0b8e8c1b972c2683e82a5ddb0d77888774c84ba [lb]: complete tcp ipvs t2 path Complete the SUG-2795 dedicated TCP IPVS T2 path. Allow separate virtual router listeners to use server IP backends, preserve server group metadata in refresh payloads, and fix listener removal cleanup semantics. Test: TcpIpvsLoadBalancerListenerApiCase and live 172.24.241.166 Resolves: ZSTAC-2795 Change-Id: I328135e3a1fb4dcfd7eb647a02151743c7157098 --- ...ateLoadBalancerListenerMsgDoc_zh_cn.groovy | 4 +- .../lb/VirtualRouterLoadBalancerBackend.java | 1 + .../TcpIpvsLoadBalancerListenerApiCase.groovy | 239 +++++++++++++++++- 3 files changed, 239 insertions(+), 5 deletions(-) diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy index a959973ee74..099aa923130 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsgDoc_zh_cn.groovy @@ -228,7 +228,7 @@ doc { column { name "dataPlane" enclosedIn "params" - desc "" + desc "监听器使用的数据平面。haproxy 表示使用 HAProxy,ipvs 表示使用 IPVS。" location "body" type "String" optional true @@ -238,7 +238,7 @@ doc { column { name "forwardMode" enclosedIn "params" - desc "" + desc "监听器使用 IPVS 数据平面时的转发模式。当前 TCP IPVS 监听器支持 full_nat。" location "body" type "String" optional true diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java index c577213a043..72faff543a0 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lb/VirtualRouterLoadBalancerBackend.java @@ -250,6 +250,7 @@ private VirtualRouterVmInventory findVirtualRouterVm(String lbUuid, List vmNicL3NetworkUuids, vr.get().getUuid(), lbUuid)); throw new CloudRuntimeException("not support separate vr with multiple networks vpc!"); } + return vrInventory; } DebugUtils.Assert(vrs.size() <= 1, String.format("multiple virtual routers[uuids:%s] found", diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy index 2dec78d5cc1..3fe612e0641 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/virtualrouter/loadbalancer/TcpIpvsLoadBalancerListenerApiCase.groovy @@ -6,26 +6,35 @@ import org.zstack.network.service.eip.EipConstant import org.zstack.network.service.lb.LoadBalancerConstants import org.zstack.network.service.lb.LoadBalancerListenerVO import org.zstack.network.service.lb.LoadBalancerListenerVO_ +import org.zstack.network.service.lb.LoadBalancerVO +import org.zstack.network.service.lb.LoadBalancerVO_ import org.zstack.network.service.portforwarding.PortForwardingConstant +import org.zstack.network.service.virtualrouter.lb.VirtualRouterLoadBalancerBackend import org.zstack.network.service.virtualrouter.vyos.VyosConstants +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.KVMAgentCommands +import org.zstack.sdk.ChangeLoadBalancerListenerAction import org.zstack.sdk.ApiResult import org.zstack.sdk.CreateLoadBalancerListenerAction import org.zstack.sdk.CreateLoadBalancerListenerResult import org.zstack.sdk.L3NetworkInventory import org.zstack.sdk.LoadBalancerInventory import org.zstack.sdk.LoadBalancerListenerInventory +import org.zstack.sdk.LoadBalancerServerGroupInventory import org.zstack.sdk.VipInventory +import org.zstack.sdk.VmInstanceInventory import org.zstack.sdk.ZSClient import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase import org.zstack.utils.data.SizeUnit - -import static java.util.Arrays.asList +import org.zstack.utils.gson.JSONObjectUtil +import org.springframework.http.HttpEntity class TcpIpvsLoadBalancerListenerApiCase extends SubCase { EnvSpec env LoadBalancerInventory lb + List refreshCmds = [] @Override void setup() { @@ -102,6 +111,22 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { } } + l3Network { + name = "backendL3" + ip { + startIp = "10.58.0.160" + endIp = "10.58.0.200" + gateway = "10.58.0.1" + netmask = "255.255.255.0" + } + service { + provider = VyosConstants.VYOS_ROUTER_PROVIDER_TYPE + types = [NetworkServiceType.DHCP.toString(), + NetworkServiceType.DNS.toString(), + LoadBalancerConstants.LB_NETWORK_SERVICE_TYPE_STRING] + } + } + l3Network { name = "managementL3" ip { @@ -129,6 +154,34 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { isDefault = true } } + + vm { + name = "backend-vm-1" + useImage("image") + useL3Networks("backendL3") + useInstanceOffering("instanceOffering") + } + + vm { + name = "backend-vm-2" + useImage("image") + useL3Networks("backendL3") + useInstanceOffering("instanceOffering") + } + + vm { + name = "backend-vm-3" + useImage("image") + useL3Networks("backendL3") + useInstanceOffering("instanceOffering") + } + + vm { + name = "backend-vm-4" + useImage("image") + useL3Networks("backendL3") + useInstanceOffering("instanceOffering") + } } } @@ -140,6 +193,7 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { testTcpIpvsFullNatListener() testTcpIpvsDefaultForwardMode() testTcpIpvsCreateValidation() + testTcpIpvsDedicatedListenerBackendRefreshPayload() } } @@ -153,7 +207,7 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { lb = createLoadBalancer { delegate.name = "tcp-ipvs-api-lb" delegate.vipUuid = vip.uuid - delegate.systemTags = asList("separateVirtualRouterVm") + delegate.systemTags = ["separateVirtualRouterVm"] } } @@ -237,6 +291,185 @@ class TcpIpvsLoadBalancerListenerApiCase extends SubCase { assert drResult.error.details.contains("TCP IPVS only supports forwardMode[full_nat]") } + void testTcpIpvsDedicatedListenerBackendRefreshPayload() { + env.afterSimulator(VirtualRouterLoadBalancerBackend.REFRESH_LB_PATH) { rsp, HttpEntity e -> + refreshCmds.add(JSONObjectUtil.toObject(e.body, VirtualRouterLoadBalancerBackend.RefreshLbCmd.class)) + return rsp + } + List destroyVmCmds = [] + env.afterSimulator(KVMConstant.KVM_DESTROY_VM_PATH) { rsp, HttpEntity e -> + destroyVmCmds.add(JSONObjectUtil.toObject(e.body, KVMAgentCommands.DestroyVmCmd.class)) + return rsp + } + + LoadBalancerListenerInventory listener = createTcpIpvsListener("tcp-ipvs-t2", 11090, LoadBalancerConstants.BALANCE_ALGORITHM_LEAST_CONN) + + String backendIp1 = backendIp("backend-vm-1") + String backendIp2 = backendIp("backend-vm-2") + String backendIp3 = backendIp("backend-vm-3") + String backendIp4 = backendIp("backend-vm-4") + + LoadBalancerServerGroupInventory group1 = createServerGroupWithVmNics( + "tcp-ipvs-t2-group-1", + [backendVmNic("backend-vm-1", "100")]) + addServerGroupToLoadBalancerListener { + listenerUuid = listener.uuid + serverGroupUuid = group1.uuid + } + + VirtualRouterLoadBalancerBackend.LbTO to = lastLbTO(listener.uuid) + assertTcpIpvsTO(to, listener.uuid, 11090, LoadBalancerConstants.BALANCE_ALGORITHM_LEAST_CONN) + assertServerGroups(to, [ + (group1.uuid): [(backendIp1): 100L] + ]) + + addBackendServerToServerGroup { + serverGroupUuid = group1.uuid + vmNics = [backendVmNic("backend-vm-2", "50"), backendVmNic("backend-vm-4", "80")] + } + to = lastLbTO(listener.uuid) + assertServerGroups(to, [ + (group1.uuid): [(backendIp1): 100L, (backendIp2): 50L, (backendIp4): 80L] + ]) + + changeLoadBalancerBackendServer { + serverGroupUuid = group1.uuid + vmNics = [backendVmNic("backend-vm-2", "0")] + } + to = lastLbTO(listener.uuid) + assertServerGroups(to, [ + (group1.uuid): [(backendIp1): 100L, (backendIp2): 0L, (backendIp4): 80L] + ]) + + LoadBalancerServerGroupInventory group2 = createServerGroupWithVmNics( + "tcp-ipvs-t2-group-2", + [backendVmNic("backend-vm-3", "30")]) + addServerGroupToLoadBalancerListener { + listenerUuid = listener.uuid + serverGroupUuid = group2.uuid + } + to = lastLbTO(listener.uuid) + assertServerGroups(to, [ + (group1.uuid): [(backendIp1): 100L, (backendIp2): 0L, (backendIp4): 80L], + (group2.uuid): [(backendIp3): 30L] + ]) + + [ + LoadBalancerConstants.BALANCE_ALGORITHM_ROUND_ROBIN, + LoadBalancerConstants.BALANCE_ALGORITHM_WEIGHT_ROUND_ROBIN, + LoadBalancerConstants.BALANCE_ALGORITHM_LEAST_CONN, + LoadBalancerConstants.BALANCE_ALGORITHM_LEAST_SOURCE + ].each { String algorithm -> + int offset = refreshCmds.size() + changeBalancerAlgorithm(listener.uuid, algorithm) + to = lastLbTO(listener.uuid, offset) + assertTcpIpvsTO(to, listener.uuid, 11090, algorithm) + } + + assert Q.New(LoadBalancerVO.class) + .select(LoadBalancerVO_.providerType) + .eq(LoadBalancerVO_.uuid, lb.uuid) + .findValue() == VyosConstants.VYOS_ROUTER_PROVIDER_TYPE + + int destroyOffset = destroyVmCmds.size() + deleteLoadBalancer { + uuid = lb.uuid + } + assert destroyVmCmds.size() > destroyOffset + assert destroyVmCmds.drop(destroyOffset).any { it.uuid != null } + } + + LoadBalancerListenerInventory createTcpIpvsListener(String name, int port, String algorithm) { + return createLoadBalancerListener { + delegate.name = name + delegate.loadBalancerUuid = lb.uuid + delegate.protocol = LoadBalancerConstants.LB_PROTOCOL_TCP + delegate.loadBalancerPort = port + delegate.instancePort = 8080 + delegate.dataPlane = LoadBalancerConstants.DATA_PLANE_IPVS + delegate.forwardMode = LoadBalancerConstants.FORWARD_MODE_FULL_NAT + delegate.systemTags = ["balancerAlgorithm::${algorithm}".toString()] + } + } + + Map backendVmNic(String vmName, String weight) { + return ["uuid": backendVmNicUuid(vmName), "weight": weight] + } + + String backendVmNicUuid(String vmName) { + return (env.inventoryByName(vmName) as VmInstanceInventory).vmNics[0].uuid + } + + String backendIp(String vmName) { + return (env.inventoryByName(vmName) as VmInstanceInventory).vmNics[0].ip + } + + LoadBalancerServerGroupInventory createServerGroupWithVmNics(String groupName, List> backendNics) { + LoadBalancerServerGroupInventory group = createLoadBalancerServerGroup { + loadBalancerUuid = lb.uuid + name = groupName + } + addBackendServerToServerGroup { + serverGroupUuid = group.uuid + vmNics = backendNics + } + return group + } + + VirtualRouterLoadBalancerBackend.LbTO lastLbTO(String listenerUuid, int offset = 0) { + long deadline = System.currentTimeMillis() + 5000 + VirtualRouterLoadBalancerBackend.LbTO to = null + while (System.currentTimeMillis() < deadline) { + if (refreshCmds.size() > offset) { + to = refreshCmds.drop(offset).reverse() + .collectMany { it.lbs } + .find { it.listenerUuid == listenerUuid } + if (to != null) { + break + } + } + Thread.sleep(100) + } + assert refreshCmds.size() > offset + assert to != null + return to + } + + void assertTcpIpvsTO(VirtualRouterLoadBalancerBackend.LbTO to, String listenerUuid, int port, String algorithm) { + assert to.listenerUuid == listenerUuid + assert to.mode == LoadBalancerConstants.LB_PROTOCOL_TCP + assert to.dataPlane == LoadBalancerConstants.DATA_PLANE_IPVS + assert to.forwardMode == LoadBalancerConstants.FORWARD_MODE_FULL_NAT + assert to.loadBalancerPort == port + assert to.instancePort == 8080 + assert to.parameters.contains("balancerAlgorithm::${algorithm}".toString()) + } + + void assertServerGroups(VirtualRouterLoadBalancerBackend.LbTO to, Map> expected) { + assert to.serverGroups.size() == expected.size() + expected.each { String groupUuid, Map servers -> + def group = to.serverGroups.find { it.serverGroupUuid == groupUuid } + assert group != null + assert group.backendServers.size() == servers.size() + servers.each { String ip, Long weight -> + def server = group.backendServers.find { it.ip == ip } + assert server != null + assert server.weight == weight + assert to.nicIps.contains(ip) + assert to.parameters.contains("balancerWeight::${ip}::${weight}".toString()) + } + } + } + + void changeBalancerAlgorithm(String listenerUuid, String algorithm) { + ChangeLoadBalancerListenerAction action = new ChangeLoadBalancerListenerAction() + action.uuid = listenerUuid + action.balancerAlgorithm = algorithm + action.sessionId = adminSession() + ChangeLoadBalancerListenerAction.Result result = action.call() + assert result.error == null + } + CreateLoadBalancerListenerAction.Result assertCreateListenerError(int port, String protocol, String dataPlane, String forwardMode) { CreateLoadBalancerListenerAction action = new CreateLoadBalancerListenerAction() action.name = "tcp-ipvs-invalid-${port}"