diff --git a/api/src/main/java/com/cloud/agent/api/to/NicTO.java b/api/src/main/java/com/cloud/agent/api/to/NicTO.java index 2ed7d9f9a201..663a039ab100 100644 --- a/api/src/main/java/com/cloud/agent/api/to/NicTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/NicTO.java @@ -37,6 +37,9 @@ public class NicTO extends NetworkTO { String networkSegmentName; + boolean trunkVlan; + List associatedNetworks; + public NicTO() { super(); } @@ -163,4 +166,21 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + // trunkVlan/associatedNetworks carry the additional networks for a multi-VLAN trunk nic; unset for ordinary nics + public boolean isTrunkVlan() { + return trunkVlan; + } + + public void setTrunkVlan(boolean trunkVlan) { + this.trunkVlan = trunkVlan; + } + + public List getAssociatedNetworks() { + return associatedNetworks; + } + + public void setAssociatedNetworks(List associatedNetworks) { + this.associatedNetworks = associatedNetworks; + } } diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index ca9e418ad7c4..e95864109de4 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -65,6 +65,8 @@ public static String[] toStrings(Host.Type... types) { String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count"; + String HOST_VLAN_FILTERING_ENABLED = "vlan.filtering.enabled"; + String HOST_VLAN_TRUNK_XML_SUPPORTED = "vlan.trunk.xml.supported"; String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; String GUEST_OS_RULE = "guest.os.rule"; diff --git a/api/src/main/java/com/cloud/vm/Nic.java b/api/src/main/java/com/cloud/vm/Nic.java index 3722e5769c92..71612cd355f1 100644 --- a/api/src/main/java/com/cloud/vm/Nic.java +++ b/api/src/main/java/com/cloud/vm/Nic.java @@ -146,6 +146,8 @@ public enum ReservationStrategy { boolean getSecondaryIp(); + boolean getMultiNetwork(); + // // IPv4 // 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..de6aeed50a01 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -318,6 +318,8 @@ public class ApiConstants { public static final String MOVE_ACL_CONSISTENCY_HASH = "aclconsistencyhash"; public static final String IMAGE_PATH = "imagepath"; public static final String INSTANCE_CONVERSION_SUPPORTED = "instanceconversionsupported"; + public static final String VLAN_FILTERING_ENABLED = "vlanfilteringenabled"; + public static final String VLAN_TRUNK_XML_SUPPORTED = "vlantrunkxmlsupported"; public static final String INTERNAL_DNS1 = "internaldns1"; public static final String INTERNAL_DNS2 = "internaldns2"; public static final String INTERNET_PROTOCOL = "internetprotocol"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 10bd62804fb2..52804cc980cd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -315,6 +315,14 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "true if the host supports instance conversion (using virt-v2v)", since = "4.19.1") private Boolean instanceConversionSupported; + @SerializedName(ApiConstants.VLAN_FILTERING_ENABLED) + @Param(description = "true if vlan_filtering is enabled on this host's guest bridge, so it can accept multi-VLAN trunk nics", since = "24.0.0") + private Boolean vlanFilteringEnabled; + + @SerializedName(ApiConstants.VLAN_TRUNK_XML_SUPPORTED) + @Param(description = "true if this host's libvirt version supports trunk vlan tap membership natively", since = "24.0.0") + private Boolean vlanTrunkXmlSupported; + @SerializedName(ApiConstants.ARCH) @Param(description = "CPU Arch of the host", since = "4.20") private String arch; @@ -904,6 +912,14 @@ public void setInstanceConversionSupported(Boolean instanceConversionSupported) this.instanceConversionSupported = instanceConversionSupported; } + public void setVlanFilteringEnabled(Boolean vlanFilteringEnabled) { + this.vlanFilteringEnabled = vlanFilteringEnabled; + } + + public void setVlanTrunkXmlSupported(Boolean vlanTrunkXmlSupported) { + this.vlanTrunkXmlSupported = vlanTrunkXmlSupported; + } + public Boolean getIsTagARule() { return isTagARule; } @@ -1000,6 +1016,14 @@ public Boolean getInstanceConversionSupported() { return instanceConversionSupported; } + public Boolean getVlanFilteringEnabled() { + return vlanFilteringEnabled; + } + + public Boolean getVlanTrunkXmlSupported() { + return vlanTrunkXmlSupported; + } + public void setExtensionId(String extensionId) { this.extensionId = extensionId; } diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index d3225f2fda5e..44ca78d08cea 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -817,8 +817,10 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT); String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR); String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION); + String vlanFilteringEnabled = detailsMap.get(Host.HOST_VLAN_FILTERING_ENABLED); + String vlanTrunkXmlSupported = detailsMap.get(Host.HOST_VLAN_TRUNK_XML_SUPPORTED); logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", uefiEnabled, host); - if (ObjectUtils.anyNotNull(uefiEnabled, diskOnlyVmSnapshotNvramSupport, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) { + if (ObjectUtils.anyNotNull(uefiEnabled, diskOnlyVmSnapshotNvramSupport, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion, vlanFilteringEnabled, vlanTrunkXmlSupported)) { boolean updateNeeded = false; if (syncBooleanHostCapability(host, Host.HOST_UEFI_ENABLE, uefiEnabled)) { updateNeeded = true; @@ -838,6 +840,12 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi host.getDetails().put(Host.HOST_VDDK_SUPPORT, vddkSupport); updateNeeded = true; } + if (syncBooleanHostCapability(host, Host.HOST_VLAN_FILTERING_ENABLED, vlanFilteringEnabled)) { + updateNeeded = true; + } + if (syncBooleanHostCapability(host, Host.HOST_VLAN_TRUNK_XML_SUPPORTED, vlanTrunkXmlSupported)) { + updateNeeded = true; + } if (!StringUtils.defaultString(vddkLibDir).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_VDDK_LIB_DIR)))) { if (StringUtils.isBlank(vddkLibDir)) { host.getDetails().remove(Host.HOST_VDDK_LIB_DIR); 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..f3ec76a27c7a 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -91,11 +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.Upgrade42300to2400; 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 Upgrade42300to2400()) .build(); } @@ -513,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/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/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index 65946b8d8210..bcda4f787755 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -128,6 +128,9 @@ protected NicVO() { @Column(name = "secondary_ip") boolean secondaryIp; + @Column(name = "multi_network") + boolean multiNetwork; + @Column(name = "mtu") Integer mtu; @@ -337,7 +340,7 @@ public String toString() { return String.format("Nic %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( this, "id", "uuid", "instanceId", - "deviceId", "broadcastUri", "reservationId", "iPv4Address")); + "deviceId", "broadcastUri", "reservationId", "iPv4Address", "multiNetwork")); } @Override @@ -381,6 +384,15 @@ public void setSecondaryIp(boolean secondaryIp) { this.secondaryIp = secondaryIp; } + @Override + public boolean getMultiNetwork() { + return multiNetwork; + } + + public void setMultiNetwork(boolean multiNetwork) { + this.multiNetwork = multiNetwork; + } + public void setVmType(VirtualMachine.Type vmType) { this.vmType = vmType; } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java new file mode 100644 index 000000000000..b5a1ed3137af --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java @@ -0,0 +1,32 @@ +// 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.vm.dao; + +import java.util.List; + +import com.cloud.utils.db.GenericDao; + +public interface NicNetworkMapDao extends GenericDao { + + List listByNicId(long nicId); + + List listByNetworkId(long networkId); + + NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId); + + List listNicIdsByNetworkId(long networkId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java new file mode 100644 index 000000000000..f905809350e5 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java @@ -0,0 +1,77 @@ +// 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.vm.dao; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Func; +import com.cloud.utils.db.SearchCriteria.Op; + +@Component +public class NicNetworkMapDaoImpl extends GenericDaoBase implements NicNetworkMapDao { + + private final SearchBuilder AllFieldsSearch; + private final GenericSearchBuilder NicIdsByNetworkSearch; + + public NicNetworkMapDaoImpl() { + super(); + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and("nicId", AllFieldsSearch.entity().getNicId(), Op.EQ); + AllFieldsSearch.and("networkId", AllFieldsSearch.entity().getNetworkId(), Op.EQ); + AllFieldsSearch.done(); + + NicIdsByNetworkSearch = createSearchBuilder(Long.class); + NicIdsByNetworkSearch.select(null, Func.DISTINCT, NicIdsByNetworkSearch.entity().getNicId()); + NicIdsByNetworkSearch.and("networkId", NicIdsByNetworkSearch.entity().getNetworkId(), Op.EQ); + NicIdsByNetworkSearch.done(); + } + + @Override + public List listByNicId(long nicId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("nicId", nicId); + return listBy(sc); + } + + @Override + public List listByNetworkId(long networkId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("networkId", networkId); + return listBy(sc); + } + + @Override + public NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("nicId", nicId); + sc.setParameters("networkId", networkId); + return findOneBy(sc); + } + + @Override + public List listNicIdsByNetworkId(long networkId) { + SearchCriteria sc = NicIdsByNetworkSearch.create(); + sc.setParameters("networkId", networkId); + return customSearch(sc, null); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java new file mode 100644 index 000000000000..793445354606 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java @@ -0,0 +1,140 @@ +// 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.vm.dao; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "nic_network_map") +public class NicNetworkMapVO implements Identity, InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "nic_id") + private long nicId; + + @Column(name = "network_id") + private long networkId; + + @Column(name = "ip4_address") + private String ip4Address; + + @Column(name = "ip6_address") + private String ip6Address; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + protected NicNetworkMapVO() { + } + + public NicNetworkMapVO(long nicId, long networkId) { + this.nicId = nicId; + this.networkId = networkId; + } + + public NicNetworkMapVO(long nicId, long networkId, String ip4Address, String ip6Address) { + this.nicId = nicId; + this.networkId = networkId; + this.ip4Address = ip4Address; + this.ip6Address = ip6Address; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public long getNicId() { + return nicId; + } + + public void setNicId(long nicId) { + this.nicId = nicId; + } + + public long getNetworkId() { + return networkId; + } + + public void setNetworkId(long networkId) { + this.networkId = networkId; + } + + public String getIp4Address() { + return ip4Address; + } + + public void setIp4Address(String ip4Address) { + this.ip4Address = ip4Address; + } + + public String getIp6Address() { + return ip6Address; + } + + public void setIp6Address(String ip6Address) { + this.ip6Address = ip6Address; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public String toString() { + return String.format("NicNetworkMap %s", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "nicId", "networkId", "ip4Address", "ip6Address")); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 932db538f30b..e89d87242b22 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -135,6 +135,7 @@ + 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..aaedd564b774 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -0,0 +1,42 @@ +-- 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 +--; + +-- Multi-VLAN trunk nics: a nic may be associated with additional networks beyond its primary nics.network_id. +CREATE TABLE IF NOT EXISTS `cloud`.`nic_network_map` ( + `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, + `uuid` varchar(40), + `nic_id` bigint unsigned NOT NULL COMMENT 'nic this association belongs to', + `network_id` bigint unsigned NOT NULL COMMENT 'additional network this nic is associated with', + `ip4_address` char(40) COMMENT 'ip4 address assigned to this nic from this network', + `ip6_address` char(40) COMMENT 'ip6 address assigned to this nic from this network', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_nic_network_map__nic_id` FOREIGN KEY (`nic_id`) REFERENCES `nics`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_nic_network_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks`(`id`), + CONSTRAINT `uc_nic_network_map__uuid` UNIQUE (`uuid`), + UNIQUE KEY `uk_nic_network_map__nic_id_network_id` (`nic_id`, `network_id`), + INDEX `i_nic_network_map__nic_id` (`nic_id`), + INDEX `i_nic_network_map__network_id` (`network_id`), + INDEX `i_nic_network_map__removed` (`removed`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics', 'multi_network', 'tinyint(1) NOT NULL DEFAULT 0 COMMENT "true if this nic has additional network associations in nic_network_map"'); 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..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,20 +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.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; @@ -55,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 { @@ -214,10 +212,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 +232,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 +266,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 +288,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 +298,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 +349,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); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java index eefe491e8b27..612d0cdb8a16 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java @@ -20,9 +20,13 @@ package com.cloud.hypervisor.kvm.resource; import java.io.File; +import java.net.URI; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,6 +37,7 @@ import org.apache.commons.lang3.StringUtils; import org.libvirt.LibvirtException; +import com.cloud.agent.api.to.NetworkTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; @@ -43,6 +48,8 @@ public class BridgeVifDriver extends VifDriverBase { + private static final String GUEST_UPLINK_TRUNK_VLAN_RANGE = "2-4094"; + private int _timeout; private final Object _vnetBridgeMonitor = new Object(); @@ -51,6 +58,7 @@ public class BridgeVifDriver extends VifDriverBase { private String _macIpScriptPath; private String _controlCidr = NetUtils.getLinkLocalCIDR(); private Long libvirtVersion; + private final Set uplinkVlanTrunkEnsuredBridges = ConcurrentHashMap.newKeySet(); private static boolean isVxlanOrNetris(String protocol) { return protocol.equals(Networks.BroadcastDomainType.Vxlan.scheme()) || protocol.equals(Networks.BroadcastDomainType.Netris.scheme()); @@ -199,6 +207,102 @@ protected boolean isValidProtocolAndVnetId(final String vNetId, final String pro return vNetId != null && protocol != null && !vNetId.equalsIgnoreCase("untagged"); } + protected void plugTrunkVlanNic(LibvirtVMDef.InterfaceDef intf, NicTO nic, String trafficLabel, String guestOsType, String nicAdapter, + Integer networkRateKBps) throws InternalErrorException { + if (nic.getBroadcastType() != Networks.BroadcastDomainType.Vlan) { + throw new InternalErrorException("Multi-VLAN trunk nics are only supported on VLAN-isolated guest networks"); + } + if (!_libvirtComputingResource.hostSupportsVlanFiltering()) { + throw new InternalErrorException("vlan_filtering is not enabled on this host's guest bridge; " + + "this host cannot accept a multi-VLAN trunk nic"); + } + + String brName = trafficLabel != null && !trafficLabel.isEmpty() ? trafficLabel : _bridges.get("guest"); + + ensureUplinkAllowsAllVlans(brName); + + List vlanTags = collectTrunkVlanTags(nic); + + logger.debug("plugging trunk nic " + nic.getMac() + " onto guest bridge " + brName + " with vlan tags " + vlanTags); + intf.defBridgeNet(brName, null, nic.getMac(), getGuestNicModel(guestOsType, nicAdapter), networkRateKBps); + + if (_libvirtComputingResource.hostSupportsVlanTrunkXml()) { + intf.setTrunkVlanTags(vlanTags); + } + // else: older libvirt can't express trunk membership; ensureVlanTrunkMembership() applies it manually once the tap exists + } + + private List collectTrunkVlanTags(NicTO nic) throws InternalErrorException { + Set vlanTags = new LinkedHashSet<>(); + vlanTags.add(parseVlanTag(nic.getBroadcastUri(), "primary network of nic " + nic.getMac())); + if (nic.getAssociatedNetworks() != null) { + for (NetworkTO associatedNetwork : nic.getAssociatedNetworks()) { + if (associatedNetwork.getBroadcastType() != Networks.BroadcastDomainType.Vlan) { + throw new InternalErrorException("Multi-VLAN trunk nics only support VLAN-isolated associated networks"); + } + vlanTags.add(parseVlanTag(associatedNetwork.getBroadcastUri(), "associated network " + associatedNetwork.getUuid())); + } + } + return new ArrayList<>(vlanTags); + } + + private Integer parseVlanTag(URI broadcastUri, String description) throws InternalErrorException { + String vlanValue = broadcastUri == null ? null : Networks.BroadcastDomainType.getValue(broadcastUri); + if (StringUtils.isBlank(vlanValue)) { + throw new InternalErrorException("Cannot determine VLAN for " + description + + ": no VLAN has been assigned yet (is the network implemented?). Refusing to plug this multi-VLAN trunk nic."); + } + try { + return Integer.valueOf(vlanValue); + } catch (NumberFormatException e) { + throw new InternalErrorException("Invalid VLAN value '" + vlanValue + "' for " + description); + } + } + + private void ensureUplinkAllowsAllVlans(String brName) throws InternalErrorException { + if (uplinkVlanTrunkEnsuredBridges.contains(brName)) { + return; + } + synchronized (_vnetBridgeMonitor) { + if (uplinkVlanTrunkEnsuredBridges.contains(brName)) { + return; + } + String uplinkPif = _pifs.get(brName); + if (StringUtils.isBlank(uplinkPif)) { + throw new InternalErrorException("Cannot determine the uplink interface for guest bridge " + brName + + "; refusing to plug a multi-VLAN trunk nic"); + } + runBridgeVlanCommand("add", uplinkPif, GUEST_UPLINK_TRUNK_VLAN_RANGE); + uplinkVlanTrunkEnsuredBridges.add(brName); + } + } + + @Override + public void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException { + if (!nic.isTrunkVlan() || _libvirtComputingResource.hostSupportsVlanTrunkXml()) { + return; + } + String tapName = iface.getDevName(); + if (StringUtils.isBlank(tapName)) { + throw new InternalErrorException("Cannot apply manual VLAN trunk membership: tap device name unknown for nic " + nic.getMac()); + } + for (Integer vlanTag : collectTrunkVlanTags(nic)) { + runBridgeVlanCommand("add", tapName, String.valueOf(vlanTag)); + } + } + + protected void runBridgeVlanCommand(String operation, String dev, String vid) throws InternalErrorException { + final Script command = new Script("bridge", _timeout, logger); + command.add("vlan"); + command.add(operation); + command.add("dev", dev); + command.add("vid", vid); + final String result = command.execute(); + if (result != null) { + throw new InternalErrorException("Failed to " + operation + " VLAN " + vid + " membership on " + dev + ": " + result); + } + } + protected String createStorageVnetBridgeIfNeeded(NicTO nic, String trafficLabel, String storageBrName) throws InternalErrorException { if (nic.getBroadcastUri() == null) { @@ -248,7 +352,9 @@ public LibvirtVMDef.InterfaceDef plug(NicTO nic, String guestOsType, String nicA } if (nic.getType() == Networks.TrafficType.Guest) { - if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) { + if (nic.isTrunkVlan()) { + plugTrunkVlanNic(intf, nic, trafficLabel, guestOsType, nicAdapter, networkRateKBps); + } else if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) { if (trafficLabel != null && !trafficLabel.isEmpty()) { logger.debug("creating a vNet dev and bridge for guest traffic per traffic label " + trafficLabel); String brName = createVnetBr(vNetId, trafficLabel, protocol); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index a5df0b3347f8..f7afe201f019 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -24,6 +24,8 @@ import static com.cloud.host.Host.HOST_VDDK_SUPPORT; import static com.cloud.host.Host.HOST_VDDK_VERSION; import static com.cloud.host.Host.HOST_VIRTV2V_VERSION; +import static com.cloud.host.Host.HOST_VLAN_FILTERING_ENABLED; +import static com.cloud.host.Host.HOST_VLAN_TRUNK_XML_SUPPORTED; import static com.cloud.host.Host.HOST_VOLUME_ENCRYPTION; import static org.apache.cloudstack.utils.linux.KVMHostInfo.isHostS390x; @@ -355,6 +357,10 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv * It is supported since Libvirt 0.9.0 */ private static final int MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE = 9000; + /** + * Libvirt supports multi-tag trunk mode (<vlan trunk='yes'>) on a standard Linux bridge since 11.0.0. + */ + private static final long MIN_LIBVIRT_VERSION_FOR_VLAN_TRUNK = 11000000; /** * Constant that defines ARM64 (aarch64) guest architectures. */ @@ -4432,6 +4438,8 @@ public StartupCommand[] initialize() { cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported)); cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk())); cmd.getHostDetails().put(HOST_CDROM_MAX_COUNT, String.valueOf(LibvirtVMDef.MAX_CDROMS_PER_VM)); + cmd.getHostDetails().put(HOST_VLAN_FILTERING_ENABLED, String.valueOf(hostSupportsVlanFiltering())); + cmd.getHostDetails().put(HOST_VLAN_TRUNK_XML_SUPPORTED, String.valueOf(hostSupportsVlanTrunkXml())); if (StringUtils.isNotBlank(vddkLibDir)) { cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir); } @@ -6253,6 +6261,39 @@ public boolean hostSupportsInstanceConversion() { return exitValue == 0; } + /** + * Static capability: whether this host's libvirt understands multi-tag trunk vlan XML + * (<vlan trunk='yes'>) on a standard Linux bridge interface. + */ + public boolean hostSupportsVlanTrunkXml() { + return hypervisorLibvirtVersion >= MIN_LIBVIRT_VERSION_FOR_VLAN_TRUNK; + } + + /** + * Live state: whether vlan_filtering is currently enabled on this host's guest bridge. + * A vlan_filtering=0 bridge floods every tagged frame to every port regardless of tag, + * so this must be true before any multi-VLAN trunk nic can be placed on this host. + */ + public boolean hostSupportsVlanFiltering() { + return isBridgeVlanFilteringEnabled(guestBridgeName); + } + + protected boolean isBridgeVlanFilteringEnabled(String bridgeName) { + if (StringUtils.isBlank(bridgeName)) { + return false; + } + File vlanFilteringFile = new File("/sys/class/net/" + bridgeName + "/bridge/vlan_filtering"); + if (!vlanFilteringFile.exists()) { + return false; + } + try { + return "1".equals(FileUtils.readFileToString(vlanFilteringFile).trim()); + } catch (IOException e) { + LOGGER.warn("Failed to read vlan_filtering state for bridge " + bridgeName, e); + return false; + } + } + public boolean hostSupportsVddk() { return hostSupportsVddk(null); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 74529d9d5fa2..112c4e06fea3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -30,6 +30,7 @@ import com.cloud.cpu.CPU; import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy; import org.apache.cloudstack.utils.qemu.QemuObject; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -1602,6 +1603,8 @@ enum HostNicType { private String _virtualPortType; private String _virtualPortInterfaceId; private int _vlanTag = -1; + private boolean _vlanTrunk = false; + private List _vlanTrunkTags; private boolean _pxeDisable = false; private boolean _linkStateUp = true; private Integer _slot; @@ -1762,6 +1765,19 @@ public int getVlanTag() { return _vlanTag; } + public void setTrunkVlanTags(List vlanTags) { + _vlanTrunk = true; + _vlanTrunkTags = vlanTags; + } + + public List getTrunkVlanTags() { + return _vlanTrunkTags; + } + + public boolean isVlanTrunk() { + return _vlanTrunk; + } + public void setSlot(Integer slot) { _slot = slot; } @@ -1865,7 +1881,13 @@ public String getContent() { } netBuilder.append("\n"); } - if (_vlanTag > 0 && _vlanTag < 4095) { + if (_vlanTrunk && CollectionUtils.isNotEmpty(_vlanTrunkTags)) { + netBuilder.append("\n"); + for (Integer tag : _vlanTrunkTags) { + netBuilder.append("\n"); + } + netBuilder.append(""); + } else if (_vlanTag > 0 && _vlanTag < 4095) { netBuilder.append("\n\n"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java index 72fb82967814..08ab8b6de5ba 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java @@ -46,4 +46,8 @@ public interface VifDriver { void deleteBr(NicTO nic); + // applies manual VLAN trunk membership to a trunk nic's live tap on hosts whose libvirt can't do it via XML; no-op otherwise + default void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException { + } + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java index b0950376a93d..99c7293c9806 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java @@ -29,6 +29,7 @@ import com.cloud.hypervisor.kvm.resource.VifDriver; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; import org.libvirt.Connect; import org.libvirt.Domain; @@ -67,6 +68,15 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour } vm.attachDevice(interfaceDef.toString()); + if (nic.isTrunkVlan()) { + try { + final InterfaceDef liveInterfaceDef = libvirtComputingResource.getInterface(conn, vmName, nic.getMac()); + vifDriver.ensureVlanTrunkMembership(liveInterfaceDef, nic); + } catch (CloudRuntimeException e) { + throw new InternalErrorException("Failed to locate live tap for trunk nic " + nic.getMac() + ": " + e.getMessage()); + } + } + // apply default network rules on new nic if (vmType == VirtualMachine.Type.User && nic.isSecurityGroupEnabled()) { final Long vmId = Long.valueOf(vmName.split("-")[2]); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java index 1ff6d7851f20..94af18b71a3b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java @@ -60,6 +60,9 @@ public Answer execute(final ReadyCommand command, final LibvirtComputingResource hostDetails.put(Host.HOST_OVFTOOL_VERSION, libvirtComputingResource.getHostOvfToolVersion()); } + hostDetails.put(Host.HOST_VLAN_FILTERING_ENABLED, Boolean.toString(libvirtComputingResource.hostSupportsVlanFiltering())); + hostDetails.put(Host.HOST_VLAN_TRUNK_XML_SUPPORTED, Boolean.toString(libvirtComputingResource.hostSupportsVlanTrunkXml())); + return new ReadyAnswer(command, hostDetails); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 486989661909..2199981d4a4c 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -49,6 +49,7 @@ import com.cloud.network.Networks.TrafficType; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmManager; import com.cloud.vm.VirtualMachine; @@ -96,6 +97,7 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource String vmFinalSpecification = performXmlTransformHook(vmInitialSpecification, libvirtComputingResource); libvirtComputingResource.startVM(conn, vmName, vmFinalSpecification); performAgentStartHook(vmName, libvirtComputingResource); + applyManualVlanTrunkMembership(conn, vmName, nics, libvirtComputingResource); libvirtComputingResource.applyDefaultNetworkRules(conn, vmSpec, false); @@ -179,6 +181,21 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource } } + private void applyManualVlanTrunkMembership(Connect conn, String vmName, NicTO[] nics, LibvirtComputingResource libvirtComputingResource) + throws InternalErrorException { + for (NicTO nic : nics) { + if (!nic.isTrunkVlan()) { + continue; + } + try { + LibvirtVMDef.InterfaceDef liveInterface = libvirtComputingResource.getInterface(conn, vmName, nic.getMac()); + libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()).ensureVlanTrunkMembership(liveInterface, nic); + } catch (CloudRuntimeException e) { + throw new InternalErrorException("Failed to locate live tap for trunk nic " + nic.getMac() + ": " + e.getMessage()); + } + } + } + private void mountSecondaryStoragesIfNeeded(StartCommand command, LibvirtComputingResource libvirtComputingResource, List secondaryStorages) { if (CollectionUtils.isNotEmpty(command.getSecondaryStorages())) { for (String secondaryStorageUrl : command.getSecondaryStorages()) { diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java index 00364948f828..f9a786cf1ac8 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java @@ -18,15 +18,22 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.agent.api.to.NetworkTO; import com.cloud.agent.api.to.NicTO; import com.cloud.exception.InternalErrorException; import com.cloud.network.Networks; @@ -36,10 +43,27 @@ public class BridgeVifDriverTest { private static final String BRIDGE_NAME = "cloudbr1"; + @Mock + private LibvirtComputingResource libvirtComputingResource; + @Spy @InjectMocks private BridgeVifDriver driver = new BridgeVifDriver(); + @Before + public void setup() throws InternalErrorException { + driver._libvirtComputingResource = libvirtComputingResource; + Map bridges = new HashMap<>(); + bridges.put("guest", BRIDGE_NAME); + driver._bridges = bridges; + Map pifs = new HashMap<>(); + pifs.put("private", "eth1"); + pifs.put(BRIDGE_NAME, "eth1"); + pifs.put("customLabel", "eth2"); + driver._pifs = pifs; + Mockito.lenient().doNothing().when(driver).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + } + @Test public void isBroadcastTypeVlanOrVxlan() { final NicTO nic = new NicTO(); @@ -100,4 +124,183 @@ public void createStorageVnetBridgeIfNeededCreatesVnetBridgeWhenUntaggedVlan() t String result = driver.createStorageVnetBridgeIfNeeded(nic, "trafficLabel", BRIDGE_NAME); Assert.assertEquals(BRIDGE_NAME, result); } + + private NicTO buildTrunkNic(int primaryVlan, List associatedVlans) { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vlan); + nic.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(primaryVlan)); + nic.setMac("00:11:22:aa:bb:dd"); + nic.setTrunkVlan(true); + if (associatedVlans != null) { + List associated = new java.util.ArrayList<>(); + for (Integer vlan : associatedVlans) { + NetworkTO associatedTo = new NetworkTO(); + associatedTo.setBroadcastType(Networks.BroadcastDomainType.Vlan); + associatedTo.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(vlan)); + associated.add(associatedTo); + } + nic.setAssociatedNetworks(associated); + } + return nic; + } + + @Test + public void plugTrunkVlanNicOnOldLibvirtSkipsXmlAndLeavesManualMembershipToPostAttachHook() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, null, null, null, 0); + + Assert.assertFalse(intf.isVlanTrunk()); + Assert.assertEquals(BRIDGE_NAME, intf.getBrName()); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsWhenVlanFilteringNotEnabled() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(false); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test + public void ensureVlanTrunkMembershipNoOpsWhenHostSupportsTrunkXml() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void ensureVlanTrunkMembershipNoOpsForNonTrunkNic() throws InternalErrorException { + NicTO nic = new NicTO(); + nic.setTrunkVlan(false); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void ensureVlanTrunkMembershipAppliesManualBridgeVlanAddOnOldLibvirt() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "100"); + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "200"); + } + + @Test(expected = InternalErrorException.class) + public void ensureVlanTrunkMembershipFailsWhenTapNameUnknown() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + NicTO nic = buildTrunkNic(100, null); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.ensureVlanTrunkMembership(intf, nic); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsForNonVlanPrimaryBroadcastType() throws InternalErrorException { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vxlan); + nic.setTrunkVlan(true); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsForNonVlanAssociatedNetwork() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + NetworkTO badAssociation = new NetworkTO(); + badAssociation.setBroadcastType(Networks.BroadcastDomainType.Vxlan); + nic.setAssociatedNetworks(Collections.singletonList(badAssociation)); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test + public void plugTrunkVlanNicBuildsInterfaceWithAllVlanTagsOnGuestBridge() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, java.util.Arrays.asList(200, 300)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, null, null, null, 0); + + Assert.assertTrue(intf.isVlanTrunk()); + Assert.assertEquals(java.util.Arrays.asList(100, 200, 300), intf.getTrunkVlanTags()); + Assert.assertEquals(BRIDGE_NAME, intf.getBrName()); + } + + @Test + public void plugTrunkVlanNicFailsClearlyWhenAssociatedNetworkHasNoVlanYet() { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + NetworkTO notYetImplemented = new NetworkTO(); + notYetImplemented.setBroadcastType(Networks.BroadcastDomainType.Vlan); + notYetImplemented.setUuid("unimplemented-network-uuid"); + // broadcastUri intentionally left null, matching a network that has never been implemented + nic.setAssociatedNetworks(Collections.singletonList(notYetImplemented)); + + try { + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + Assert.fail("expected InternalErrorException"); + } catch (InternalErrorException e) { + Assert.assertTrue(e.getMessage().contains("unimplemented-network-uuid")); + } + } + + @Test + public void plugTrunkVlanNicUsesTrafficLabelBridgeWhenPresent() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, "customLabel", null, null, 0); + + Assert.assertEquals("customLabel", intf.getBrName()); + Assert.assertEquals(Collections.singletonList(100), intf.getTrunkVlanTags()); + } + + @Test + public void plugTrunkVlanNicProgramsUplinkForWhicheverBridgeTheNicActuallyUses() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(100, null), null, null, null, 0); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(300, null), "customLabel", null, null, 0); + + Mockito.verify(driver).runBridgeVlanCommand("add", "eth1", "2-4094"); + Mockito.verify(driver).runBridgeVlanCommand("add", "eth2", "2-4094"); + } + + @Test + public void plugTrunkVlanNicOnlyProgramsUplinkOnceForRepeatedNicsOnTheSameBridge() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(100, null), null, null, null, 0); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(200, null), null, null, null, 0); + + Mockito.verify(driver, Mockito.times(1)).runBridgeVlanCommand("add", "eth1", "2-4094"); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsWhenUplinkPifUnknownForBridge() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, "unknownLabel", null, null, 0); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java index 56ad267eac7e..d4078d958132 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java @@ -140,6 +140,57 @@ public void testInterfaceBridgeSlot() { assertEquals(expected, ifDef.toString()); } + @Test + public void testInterfaceLegacySingleVlanTag() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("targetDeviceName", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setVlanTag(123); + + String expected = + "\n" + + "\n" + + "\n" + + "\n" + + "\n\n" + + "\n" + + "\n"; + + assertEquals(expected, ifDef.toString()); + assertFalse(ifDef.isVlanTrunk()); + } + + @Test + public void testInterfaceTrunkVlanTags() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("cloudbr1", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setTrunkVlanTags(Arrays.asList(100, 200, 300)); + + String expected = + "\n" + + "\n" + + "\n" + + "\n" + + "\n\n\n\n" + + "\n" + + "\n"; + + assertEquals(expected, ifDef.toString()); + assertTrue(ifDef.isVlanTrunk()); + assertEquals(Arrays.asList(100, 200, 300), ifDef.getTrunkVlanTags()); + } + + @Test + public void testInterfaceTrunkVlanTagsTakesPrecedenceOverLegacyVlanTag() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("cloudbr1", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setVlanTag(50); + ifDef.setTrunkVlanTags(Arrays.asList(50, 60)); + + String content = ifDef.toString(); + assertTrue(content.contains("")); + assertFalse(content.contains("trunk='no'")); + } + @Test public void testInterfaceWithMultiQueueAndPacked() { LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); 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/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java index 2e4025ca0013..f942a38746c6 100644 --- a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java @@ -283,6 +283,8 @@ private void setNewHostResponseBase(HostJoinVO host, EnumSet detail } else { hostResponse.setUefiCapability(new Boolean(false)); } + hostResponse.setVlanFilteringEnabled(Boolean.parseBoolean((String) hostDetails.get(Host.HOST_VLAN_FILTERING_ENABLED))); + hostResponse.setVlanTrunkXmlSupported(Boolean.parseBoolean((String) hostDetails.get(Host.HOST_VLAN_TRUNK_XML_SUPPORTED))); } if (details.contains(HostDetails.all) && Arrays.asList(Hypervisor.HypervisorType.KVM, diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 943d1e73c4b5..adc3b0e4ad36 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -67,6 +67,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DiskTO; +import com.cloud.agent.api.to.NetworkTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.configuration.ConfigurationManager; @@ -97,6 +98,8 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @@ -127,6 +130,8 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis @Inject private NicSecondaryIpDao _nicSecIpDao; @Inject + private NicNetworkMapDao nicNetworkMapDao; + @Inject private ResourceManager _resourceMgr; @Inject protected ServiceOfferingDetailsDao _serviceOfferingDetailsDao; @@ -241,6 +246,7 @@ public NicTO toNicTO(NicProfile profile) { secIps = _nicSecIpDao.getSecondaryIpAddressesForNic(nicVO.getId()); } to.setNicSecIps(secIps); + setTrunkAssociationsOnNicTO(to, nicVO); } else { logger.warn("Unable to load NicVO for NicProfile {}", profile); //Workaround for dynamically created nics @@ -255,6 +261,33 @@ public NicTO toNicTO(NicProfile profile) { return to; } + private void setTrunkAssociationsOnNicTO(final NicTO to, final NicVO nic) { + if (!nic.getMultiNetwork()) { + return; + } + final List associations = nicNetworkMapDao.listByNicId(nic.getId()); + if (associations.isEmpty()) { + return; + } + final List associatedNetworks = new ArrayList<>(); + for (final NicNetworkMapVO association : associations) { + final NetworkVO associatedNetwork = networkDao.findById(association.getNetworkId()); + if (associatedNetwork == null) { + continue; + } + final NetworkTO associatedTo = new NetworkTO(); + associatedTo.setUuid(associatedNetwork.getUuid()); + associatedTo.setBroadcastType(associatedNetwork.getBroadcastDomainType()); + associatedTo.setBroadcastUri(associatedNetwork.getBroadcastUri()); + associatedTo.setType(associatedNetwork.getTrafficType()); + associatedNetworks.add(associatedTo); + } + if (!associatedNetworks.isEmpty()) { + to.setTrunkVlan(true); + to.setAssociatedNetworks(associatedNetworks); + } + } + private String getNetworkName(long zoneId, long domainId, long accountId, VpcVO vpc, long networkId) { String prefix = String.format("D%s-A%s-Z%s", domainId, accountId, zoneId); if (Objects.isNull(vpc)) { 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"); } }