From c0ec554349016294f76cf5df9857ed09f499b425 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Wed, 22 Apr 2026 15:10:11 +0200 Subject: [PATCH 01/33] Add OVN provider lifecycle skeleton --- .../main/java/com/cloud/network/Network.java | 1 + .../main/java/com/cloud/network/Networks.java | 3 +- .../com/cloud/network/ovn/OvnProvider.java | 33 ++ .../com/cloud/network/ovn/OvnService.java | 28 ++ .../apache/cloudstack/api/ApiConstants.java | 8 + .../admin/network/NetworkOfferingBaseCmd.java | 5 +- .../admin/vpc/CreateVPCOfferingCmd.java | 5 +- .../api/command/utils/OfferingUtils.java | 4 + client/pom.xml | 5 + .../service/NetworkOrchestrationService.java | 3 + .../com/cloud/network/dao/OvnProviderDao.java | 24 ++ .../cloud/network/dao/OvnProviderDaoImpl.java | 47 +++ .../cloud/network/element/OvnProviderVO.java | 282 +++++++++++++++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42210to42300.sql | 23 ++ plugins/network-elements/ovn/pom.xml | 32 ++ .../api/command/AddOvnProviderCmd.java | 125 ++++++++ .../api/command/DeleteOvnProviderCmd.java | 74 +++++ .../api/command/ListOvnProvidersCmd.java | 60 ++++ .../api/response/OvnProviderResponse.java | 159 ++++++++++ .../apache/cloudstack/service/OvnElement.java | 293 ++++++++++++++++++ .../service/OvnGuestNetworkGuru.java | 80 +++++ .../cloudstack/service/OvnNbClient.java | 28 ++ .../service/OvnProviderService.java | 32 ++ .../service/OvnProviderServiceImpl.java | 175 +++++++++++ .../cloudstack/service/OvnServiceImpl.java | 55 ++++ .../core/spring-ovn-core-managers-context.xml | 31 ++ .../META-INF/cloudstack/ovn/module.properties | 21 ++ .../cloudstack/ovn/spring-ovn-context.xml | 39 +++ .../api/command/AddOvnProviderCmdTest.java | 94 ++++++ .../api/command/DeleteOvnProviderCmdTest.java | 97 ++++++ .../api/command/ListOvnProvidersCmdTest.java | 82 +++++ .../cloudstack/service/OvnElementTest.java | 47 +++ .../service/OvnProviderServiceImplTest.java | 199 ++++++++++++ .../service/OvnServiceImplTest.java | 40 +++ plugins/pom.xml | 1 + .../ConfigurationManagerImpl.java | 7 +- .../com/cloud/network/NetworkServiceImpl.java | 22 ++ 38 files changed, 2258 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/com/cloud/network/ovn/OvnProvider.java create mode 100644 api/src/main/java/com/cloud/network/ovn/OvnService.java create mode 100644 engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDao.java create mode 100644 engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/network/element/OvnProviderVO.java create mode 100644 plugins/network-elements/ovn/pom.xml create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/AddOvnProviderCmd.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmd.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/ListOvnProvidersCmd.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/response/OvnProviderResponse.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderService.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java create mode 100644 plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/core/spring-ovn-core-managers-context.xml create mode 100644 plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/module.properties create mode 100644 plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/spring-ovn-context.xml create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmdTest.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/ListOvnProvidersCmdTest.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnElementTest.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java index 0846306f70f9..1687702678f2 100644 --- a/api/src/main/java/com/cloud/network/Network.java +++ b/api/src/main/java/com/cloud/network/Network.java @@ -207,6 +207,7 @@ public static class Provider { public static final Provider Nsx = new Provider("Nsx", false); public static final Provider Netris = new Provider("Netris", false); + public static final Provider Ovn = new Provider("Ovn", false); private final String name; private final boolean isExternal; diff --git a/api/src/main/java/com/cloud/network/Networks.java b/api/src/main/java/com/cloud/network/Networks.java index 5f767686dc97..fb528b83e0d4 100644 --- a/api/src/main/java/com/cloud/network/Networks.java +++ b/api/src/main/java/com/cloud/network/Networks.java @@ -130,7 +130,8 @@ public URI toUri(T value) { OpenDaylight("opendaylight", String.class), TUNGSTEN("tf", String.class), NSX("nsx", String.class), - Netris("netris", String.class); + Netris("netris", String.class), + OVN("ovn", String.class); private final String scheme; private final Class type; diff --git a/api/src/main/java/com/cloud/network/ovn/OvnProvider.java b/api/src/main/java/com/cloud/network/ovn/OvnProvider.java new file mode 100644 index 000000000000..16bdccbeb833 --- /dev/null +++ b/api/src/main/java/com/cloud/network/ovn/OvnProvider.java @@ -0,0 +1,33 @@ +// 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.network.ovn; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface OvnProvider extends InternalIdentity, Identity { + long getZoneId(); + Long getHostId(); + String getName(); + String getNbConnection(); + String getSbConnection(); + String getCaCertPath(); + String getClientCertPath(); + String getClientPrivateKeyPath(); + String getExternalBridge(); + String getLocalnetName(); +} diff --git a/api/src/main/java/com/cloud/network/ovn/OvnService.java b/api/src/main/java/com/cloud/network/ovn/OvnService.java new file mode 100644 index 000000000000..29fad2215c66 --- /dev/null +++ b/api/src/main/java/com/cloud/network/ovn/OvnService.java @@ -0,0 +1,28 @@ +// 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.network.ovn; + +/** + * Service boundary for CloudStack's native OVN integration. + * Phase 1 only defines stable naming and connection validation helpers; OVN NB operations are added in later phases. + */ +public interface OvnService { + String getLogicalSwitchName(long networkId); + String getLogicalRouterName(long vpcId); + String getLogicalSwitchPortName(long nicId); + boolean isValidConnectionString(String connection); +} 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 7eae16a2a376..e7c61497c720 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1036,6 +1036,13 @@ public class ApiConstants { public static final String NSX_PROVIDER_PORT = "nsxproviderport"; public static final String NSX_CONTROLLER_ID = "nsxcontrollerid"; + public static final String OVN_NB_CONNECTION = "ovnnbconnection"; + public static final String OVN_SB_CONNECTION = "ovnsbconnection"; + public static final String OVN_CA_CERT_PATH = "ovncacertpath"; + public static final String OVN_CLIENT_CERT_PATH = "ovnclientcertpath"; + public static final String OVN_CLIENT_PRIVATE_KEY_PATH = "ovnclientprivatekeypath"; + public static final String OVN_EXTERNAL_BRIDGE = "ovnexternalbridge"; + public static final String OVN_LOCALNET_NAME = "ovnlocalnetname"; public static final String S3_ACCESS_KEY = "accesskey"; public static final String SECRET_KEY = "secretkey"; public static final String S3_END_POINT = "endpoint"; @@ -1307,6 +1314,7 @@ public class ApiConstants { public static final String HAS_RULES = "hasrules"; public static final String NSX_DETAIL_KEY = "forNsx"; public static final String NETRIS_DETAIL_KEY = "forNetris"; + public static final String OVN_DETAIL_KEY = "forOvn"; public static final String NETRIS_TAG = "netristag"; public static final String NETRIS_VXLAN_ID = "netrisvxlanid"; public static final String NETRIS_URL = "netrisurl"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java index 1c832b7217ef..c1321fc31823 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java @@ -55,6 +55,7 @@ import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isOvnProvider; public abstract class NetworkOfferingBaseCmd extends BaseCmd { @@ -249,7 +250,7 @@ public Long getServiceOfferingId() { } public boolean isExternalNetworkProvider() { - return Arrays.asList("NSX", "Netris").stream() + return Arrays.asList("NSX", "Netris", "OVN").stream() .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); } @@ -280,7 +281,7 @@ public List getSupportedServices() { SourceNat.getName(), PortForwarding.getName())); } - if (getNsxSupportsLbService() || (provider != null && isNetrisNatted(getProvider(), getNetworkMode()))) { + if (getNsxSupportsLbService() || (provider != null && (isNetrisNatted(getProvider(), getNetworkMode()) || isOvnProvider(getProvider())))) { services.add(Lb.getName()); } if (Boolean.TRUE.equals(forVpc)) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java index 2b934a60da7a..4eb9d5f546f9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java @@ -64,6 +64,7 @@ import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isOvnProvider; @APICommand(name = "createVPCOffering", description = "Creates VPC offering", responseObject = VpcOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -179,7 +180,7 @@ public String getDisplayText() { } public boolean isExternalNetworkProvider() { - return Arrays.asList("NSX", "Netris").stream() + return Arrays.asList("NSX", "Netris", "OVN").stream() .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); } @@ -201,7 +202,7 @@ public List getSupportedServices() { if (NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(getNetworkMode())) { supportedServices.add(Gateway.getName()); } - if (getNsxSupportsLbService() || isNetrisNatted(getProvider(), getNetworkMode())) { + if (getNsxSupportsLbService() || isNetrisNatted(getProvider(), getNetworkMode()) || isOvnProvider(getProvider())) { supportedServices.add(Lb.getName()); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java index 433a37c07cde..64e770096a3e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java @@ -35,4 +35,8 @@ public static boolean isNsxWithoutLb(String provider, boolean nsxSupportsLbServi public static boolean isNetrisRouted(String provider, String networkMode) { return "Netris".equalsIgnoreCase(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode); } + + public static boolean isOvnProvider(String provider) { + return "Ovn".equalsIgnoreCase(provider); + } } diff --git a/client/pom.xml b/client/pom.xml index 7118f455ab5f..a8e4ce5fa325 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -306,6 +306,11 @@ cloud-plugin-network-opendaylight ${project.version} + + org.apache.cloudstack + cloud-plugin-network-ovn + ${project.version} + org.apache.cloudstack cloud-plugin-network-vcs diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index b7b548fb9407..50cd3ad31ec8 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -114,6 +114,9 @@ public interface NetworkOrchestrationService { ConfigKey NETRIS_ENABLED = new ConfigKey<>(Boolean.class, "netris.plugin.enable", "Advanced", "false", "Indicates whether to enable the Netris plugin", false, ConfigKey.Scope.Zone, null); + + ConfigKey OVN_ENABLED = new ConfigKey<>(Boolean.class, "ovn.plugin.enable", "Advanced", "false", + "Indicates whether to enable the OVN plugin", false, ConfigKey.Scope.Zone, null); ConfigKey NETWORK_LB_HAPROXY_MAX_CONN = new ConfigKey<>( "Network", Integer.class, diff --git a/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDao.java b/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDao.java new file mode 100644 index 000000000000..dda7c70a0551 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDao.java @@ -0,0 +1,24 @@ +// 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.network.dao; + +import com.cloud.network.element.OvnProviderVO; +import com.cloud.utils.db.GenericDao; + +public interface OvnProviderDao extends GenericDao { + OvnProviderVO findByZoneId(long zoneId); +} diff --git a/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDaoImpl.java new file mode 100644 index 000000000000..6f97b47b6ceb --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/OvnProviderDaoImpl.java @@ -0,0 +1,47 @@ +// 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.network.dao; + +import com.cloud.network.element.OvnProviderVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +@Component +@DB() +public class OvnProviderDaoImpl extends GenericDaoBase implements OvnProviderDao { + final SearchBuilder allFieldsSearch; + + public OvnProviderDaoImpl() { + super(); + allFieldsSearch = createSearchBuilder(); + allFieldsSearch.and("id", allFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); + allFieldsSearch.and("uuid", allFieldsSearch.entity().getUuid(), SearchCriteria.Op.EQ); + allFieldsSearch.and("zone_id", allFieldsSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + allFieldsSearch.and("nb_connection", allFieldsSearch.entity().getNbConnection(), SearchCriteria.Op.EQ); + allFieldsSearch.done(); + } + + @Override + public OvnProviderVO findByZoneId(long zoneId) { + SearchCriteria sc = allFieldsSearch.create(); + sc.setParameters("zone_id", zoneId); + return findOneBy(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/network/element/OvnProviderVO.java b/engine/schema/src/main/java/com/cloud/network/element/OvnProviderVO.java new file mode 100644 index 000000000000..959e0315ec16 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/element/OvnProviderVO.java @@ -0,0 +1,282 @@ +// 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.network.element; + +import com.cloud.network.ovn.OvnProvider; + +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 java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "ovn_providers") +public class OvnProviderVO implements OvnProvider { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "host_id") + private Long hostId; + + @Column(name = "name") + private String name; + + @Column(name = "nb_connection") + private String nbConnection; + + @Column(name = "sb_connection") + private String sbConnection; + + @Column(name = "ca_cert_path") + private String caCertPath; + + @Column(name = "client_cert_path") + private String clientCertPath; + + @Column(name = "client_private_key_path") + private String clientPrivateKeyPath; + + @Column(name = "external_bridge") + private String externalBridge; + + @Column(name = "localnet_name") + private String localnetName; + + @Column(name = "created") + private Date created; + + @Column(name = "removed") + private Date removed; + + public OvnProviderVO() { + uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + @Override + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getNbConnection() { + return nbConnection; + } + + public void setNbConnection(String nbConnection) { + this.nbConnection = nbConnection; + } + + @Override + public String getSbConnection() { + return sbConnection; + } + + public void setSbConnection(String sbConnection) { + this.sbConnection = sbConnection; + } + + @Override + public String getCaCertPath() { + return caCertPath; + } + + public void setCaCertPath(String caCertPath) { + this.caCertPath = caCertPath; + } + + @Override + public String getClientCertPath() { + return clientCertPath; + } + + public void setClientCertPath(String clientCertPath) { + this.clientCertPath = clientCertPath; + } + + @Override + public String getClientPrivateKeyPath() { + return clientPrivateKeyPath; + } + + public void setClientPrivateKeyPath(String clientPrivateKeyPath) { + this.clientPrivateKeyPath = clientPrivateKeyPath; + } + + @Override + public String getExternalBridge() { + return externalBridge; + } + + public void setExternalBridge(String externalBridge) { + this.externalBridge = externalBridge; + } + + @Override + public String getLocalnetName() { + return localnetName; + } + + public void setLocalnetName(String localnetName) { + this.localnetName = localnetName; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public static final class Builder { + private long zoneId; + private Long hostId; + private String name; + private String nbConnection; + private String sbConnection; + private String caCertPath; + private String clientCertPath; + private String clientPrivateKeyPath; + private String externalBridge; + private String localnetName; + + public Builder setZoneId(long zoneId) { + this.zoneId = zoneId; + return this; + } + + public Builder setHostId(Long hostId) { + this.hostId = hostId; + return this; + } + + public Builder setName(String name) { + this.name = name; + return this; + } + + public Builder setNbConnection(String nbConnection) { + this.nbConnection = nbConnection; + return this; + } + + public Builder setSbConnection(String sbConnection) { + this.sbConnection = sbConnection; + return this; + } + + public Builder setCaCertPath(String caCertPath) { + this.caCertPath = caCertPath; + return this; + } + + public Builder setClientCertPath(String clientCertPath) { + this.clientCertPath = clientCertPath; + return this; + } + + public Builder setClientPrivateKeyPath(String clientPrivateKeyPath) { + this.clientPrivateKeyPath = clientPrivateKeyPath; + return this; + } + + public Builder setExternalBridge(String externalBridge) { + this.externalBridge = externalBridge; + return this; + } + + public Builder setLocalnetName(String localnetName) { + this.localnetName = localnetName; + return this; + } + + public OvnProviderVO build() { + OvnProviderVO provider = new OvnProviderVO(); + provider.setZoneId(zoneId); + provider.setHostId(hostId); + provider.setName(name); + provider.setNbConnection(nbConnection); + provider.setSbConnection(sbConnection); + provider.setCaCertPath(caCertPath); + provider.setClientCertPath(clientCertPath); + provider.setClientPrivateKeyPath(clientPrivateKeyPath); + provider.setExternalBridge(externalBridge); + provider.setLocalnetName(localnetName); + return provider; + } + } +} 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 edc14d9fa0cc..8467f9950fd2 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 @@ -139,6 +139,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 4cb9eb7cb2c4..9b032ba02d5e 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -117,3 +117,26 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tin --- Disable/enable NICs CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' '); + +-- OVN Plugin +CREATE TABLE IF NOT EXISTS `cloud`.`ovn_providers` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40), + `zone_id` bigint unsigned NOT NULL COMMENT 'Zone ID', + `host_id` bigint unsigned COMMENT 'Optional resource host ID if OVN command routing is enabled', + `name` varchar(255) NOT NULL, + `nb_connection` varchar(255) NOT NULL COMMENT 'OVN Northbound database connection string', + `sb_connection` varchar(255) COMMENT 'OVN Southbound database connection string', + `ca_cert_path` varchar(1024) COMMENT 'OVN TLS CA certificate path', + `client_cert_path` varchar(1024) COMMENT 'OVN TLS client certificate path', + `client_private_key_path` varchar(1024) COMMENT 'OVN TLS client private key path', + `external_bridge` varchar(255) COMMENT 'OVN external bridge used for provider network access', + `localnet_name` varchar(255) COMMENT 'OVN localnet name used for provider network mapping', + `created` datetime NOT NULL COMMENT 'created date', + `removed` datetime COMMENT 'removed date if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_ovn_providers__zone_id` FOREIGN KEY `fk_ovn_providers__zone_id` (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ovn_providers__host_id` FOREIGN KEY `fk_ovn_providers__host_id` (`host_id`) REFERENCES `host`(`id`) ON DELETE SET NULL, + UNIQUE KEY `uk_ovn_providers__zone_id` (`zone_id`), + INDEX `i_ovn_providers__zone_id`(`zone_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/plugins/network-elements/ovn/pom.xml b/plugins/network-elements/ovn/pom.xml new file mode 100644 index 000000000000..a46f7a83fa08 --- /dev/null +++ b/plugins/network-elements/ovn/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + cloud-plugin-network-ovn + Apache CloudStack Plugin - OVN + + + org.apache.cloudstack + cloudstack-plugins + 4.23.0.0-SNAPSHOT + ../../pom.xml + + diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/AddOvnProviderCmd.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/AddOvnProviderCmd.java new file mode 100644 index 000000000000..4d3e07229f14 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/AddOvnProviderCmd.java @@ -0,0 +1,125 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.network.ovn.OvnProvider; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.service.OvnProviderService; + +import javax.inject.Inject; + +@APICommand(name = AddOvnProviderCmd.APINAME, description = "Add OVN provider to CloudStack", + responseObject = OvnProviderResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class AddOvnProviderCmd extends BaseCmd { + public static final String APINAME = "addOvnProvider"; + + @Inject + OvnProviderService ovnProviderService; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, + description = "the ID of zone") + private Long zoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "OVN provider name") + private String name; + + @Parameter(name = ApiConstants.OVN_NB_CONNECTION, type = CommandType.STRING, required = true, + description = "OVN Northbound database connection string. Supported formats: tcp:host:6641, ssl:host:6641, unix:/path/to/ovnnb_db.sock") + private String nbConnection; + + @Parameter(name = ApiConstants.OVN_SB_CONNECTION, type = CommandType.STRING, + description = "OVN Southbound database connection string for diagnostics and binding checks") + private String sbConnection; + + @Parameter(name = ApiConstants.OVN_CA_CERT_PATH, type = CommandType.STRING, description = "OVN TLS CA certificate path") + private String caCertPath; + + @Parameter(name = ApiConstants.OVN_CLIENT_CERT_PATH, type = CommandType.STRING, description = "OVN TLS client certificate path") + private String clientCertPath; + + @Parameter(name = ApiConstants.OVN_CLIENT_PRIVATE_KEY_PATH, type = CommandType.STRING, description = "OVN TLS client private key path") + private String clientPrivateKeyPath; + + @Parameter(name = ApiConstants.OVN_EXTERNAL_BRIDGE, type = CommandType.STRING, description = "OVN external bridge used for provider network access") + private String externalBridge; + + @Parameter(name = ApiConstants.OVN_LOCALNET_NAME, type = CommandType.STRING, description = "OVN localnet name used for provider network mapping") + private String localnetName; + + public Long getZoneId() { + return zoneId; + } + + public String getName() { + return name; + } + + public String getNbConnection() { + return nbConnection; + } + + public String getSbConnection() { + return sbConnection; + } + + public String getCaCertPath() { + return caCertPath; + } + + public String getClientCertPath() { + return clientCertPath; + } + + public String getClientPrivateKeyPath() { + return clientPrivateKeyPath; + } + + public String getExternalBridge() { + return externalBridge; + } + + public String getLocalnetName() { + return localnetName; + } + + @Override + public void execute() throws ServerApiException, ConcurrentOperationException { + OvnProvider provider = ovnProviderService.addProvider(this); + OvnProviderResponse response = ovnProviderService.createOvnProviderResponse(provider); + if (response == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add OVN provider"); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmd.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmd.java new file mode 100644 index 000000000000..b149a62f7a4b --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmd.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.service.OvnProviderService; + +import javax.inject.Inject; + +@APICommand(name = DeleteOvnProviderCmd.APINAME, description = "Delete OVN provider from CloudStack", + responseObject = OvnProviderResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class DeleteOvnProviderCmd extends BaseCmd { + public static final String APINAME = "deleteOvnProvider"; + + @Inject + private OvnProviderService ovnProviderService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = OvnProviderResponse.class, + required = true, description = "OVN provider ID") + private Long id; + + public Long getId() { + return id; + } + + @Override + public void execute() throws ServerApiException, ConcurrentOperationException { + try { + boolean deleted = ovnProviderService.deleteOvnProvider(getId()); + if (deleted) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + return; + } + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete OVN provider from zone"); + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return 0; + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/ListOvnProvidersCmd.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/ListOvnProvidersCmd.java new file mode 100644 index 000000000000..a0f3eefff9d8 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/command/ListOvnProvidersCmd.java @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.utils.StringUtils; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.service.OvnProviderService; + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = ListOvnProvidersCmd.APINAME, description = "List all OVN providers added to CloudStack", + responseObject = OvnProviderResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") +public class ListOvnProvidersCmd extends BaseListCmd { + public static final String APINAME = "listOvnProviders"; + + @Inject + OvnProviderService ovnProviderService; + + @Parameter(name = ApiConstants.ZONE_ID, description = "ID of the zone", type = CommandType.UUID, entityType = ZoneResponse.class) + private Long zoneId; + + public Long getZoneId() { + return zoneId; + } + + @Override + public void execute() throws ServerApiException, ConcurrentOperationException { + List baseResponseList = ovnProviderService.listOvnProviders(zoneId); + List pagingList = StringUtils.applyPagination(baseResponseList, getStartIndex(), getPageSizeVal()); + ListResponse listResponse = new ListResponse<>(); + listResponse.setResponses(pagingList); + listResponse.setResponseName(getCommandName()); + setResponseObject(listResponse); + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/response/OvnProviderResponse.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/response/OvnProviderResponse.java new file mode 100644 index 000000000000..8782a4d92f66 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/api/response/OvnProviderResponse.java @@ -0,0 +1,159 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.network.ovn.OvnProvider; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = {OvnProvider.class}) +public class OvnProviderResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) + @Param(description = "OVN provider name") + private String name; + + @SerializedName(ApiConstants.UUID) + @Param(description = "OVN provider UUID") + private String uuid; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "Zone ID to which the OVN provider is associated") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) + @Param(description = "Zone name to which the OVN provider is associated") + private String zoneName; + + @SerializedName(ApiConstants.OVN_NB_CONNECTION) + @Param(description = "OVN Northbound database connection string") + private String nbConnection; + + @SerializedName(ApiConstants.OVN_SB_CONNECTION) + @Param(description = "OVN Southbound database connection string") + private String sbConnection; + + @SerializedName(ApiConstants.OVN_CA_CERT_PATH) + @Param(description = "OVN TLS CA certificate path") + private String caCertPath; + + @SerializedName(ApiConstants.OVN_CLIENT_CERT_PATH) + @Param(description = "OVN TLS client certificate path") + private String clientCertPath; + + @SerializedName(ApiConstants.OVN_CLIENT_PRIVATE_KEY_PATH) + @Param(description = "OVN TLS client private key path") + private String clientPrivateKeyPath; + + @SerializedName(ApiConstants.OVN_EXTERNAL_BRIDGE) + @Param(description = "OVN external bridge used for provider network access") + private String externalBridge; + + @SerializedName(ApiConstants.OVN_LOCALNET_NAME) + @Param(description = "OVN localnet name used for provider network mapping") + private String localnetName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public String getZoneName() { + return zoneName; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public String getNbConnection() { + return nbConnection; + } + + public void setNbConnection(String nbConnection) { + this.nbConnection = nbConnection; + } + + public String getSbConnection() { + return sbConnection; + } + + public void setSbConnection(String sbConnection) { + this.sbConnection = sbConnection; + } + + public String getCaCertPath() { + return caCertPath; + } + + public void setCaCertPath(String caCertPath) { + this.caCertPath = caCertPath; + } + + public String getClientCertPath() { + return clientCertPath; + } + + public void setClientCertPath(String clientCertPath) { + this.clientCertPath = clientCertPath; + } + + public String getClientPrivateKeyPath() { + return clientPrivateKeyPath; + } + + public void setClientPrivateKeyPath(String clientPrivateKeyPath) { + this.clientPrivateKeyPath = clientPrivateKeyPath; + } + + public String getExternalBridge() { + return externalBridge; + } + + public void setExternalBridge(String externalBridge) { + this.externalBridge = externalBridge; + } + + public String getLocalnetName() { + return localnetName; + } + + public void setLocalnetName(String localnetName) { + this.localnetName = localnetName; + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java new file mode 100644 index 000000000000..d7521b5582d9 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java @@ -0,0 +1,293 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.agent.api.to.LoadBalancerTO; +import com.cloud.deploy.DeployDestination; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.PhysicalNetworkServiceProvider; +import com.cloud.network.PublicIpAddress; +import com.cloud.network.element.DhcpServiceProvider; +import com.cloud.network.element.DnsServiceProvider; +import com.cloud.network.element.FirewallServiceProvider; +import com.cloud.network.element.IpDeployer; +import com.cloud.network.element.LoadBalancingServiceProvider; +import com.cloud.network.element.NetworkACLServiceProvider; +import com.cloud.network.element.PortForwardingServiceProvider; +import com.cloud.network.element.StaticNatServiceProvider; +import com.cloud.network.element.VpcProvider; +import com.cloud.network.lb.LoadBalancingRule; +import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.LoadBalancerContainer; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.rules.StaticNat; +import com.cloud.network.vpc.NetworkACLItem; +import com.cloud.network.vpc.PrivateGateway; +import com.cloud.network.vpc.StaticRouteProfile; +import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.NicProfile; +import com.cloud.vm.ReservationContext; +import com.cloud.vm.VirtualMachineProfile; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class OvnElement extends AdapterBase implements DhcpServiceProvider, DnsServiceProvider, VpcProvider, + StaticNatServiceProvider, IpDeployer, PortForwardingServiceProvider, FirewallServiceProvider, + NetworkACLServiceProvider, LoadBalancingServiceProvider { + + private final Map> capabilities = initCapabilities(); + + protected static Map> initCapabilities() { + Map> capabilities = new HashMap<>(); + + Map dhcpCapabilities = new HashMap<>(); + dhcpCapabilities.put(Network.Capability.DhcpAccrossMultipleSubnets, "true"); + capabilities.put(Network.Service.Dhcp, dhcpCapabilities); + + Map dnsCapabilities = new HashMap<>(); + dnsCapabilities.put(Network.Capability.AllowDnsSuffixModification, "true"); + capabilities.put(Network.Service.Dns, dnsCapabilities); + + Map sourceNatCapabilities = new HashMap<>(); + sourceNatCapabilities.put(Network.Capability.SupportedSourceNatTypes, "peraccount"); + capabilities.put(Network.Service.SourceNat, sourceNatCapabilities); + + capabilities.put(Network.Service.StaticNat, null); + capabilities.put(Network.Service.PortForwarding, null); + capabilities.put(Network.Service.NetworkACL, null); + capabilities.put(Network.Service.Gateway, null); + + Map firewallCapabilities = new HashMap<>(); + firewallCapabilities.put(Network.Capability.SupportedProtocols, "tcp,udp,icmp"); + firewallCapabilities.put(Network.Capability.SupportedEgressProtocols, "tcp,udp,icmp,all"); + firewallCapabilities.put(Network.Capability.SupportedTrafficDirection, "ingress,egress"); + capabilities.put(Network.Service.Firewall, firewallCapabilities); + + Map lbCapabilities = new HashMap<>(); + lbCapabilities.put(Network.Capability.SupportedLBAlgorithms, "roundrobin,leastconn"); + lbCapabilities.put(Network.Capability.SupportedLBIsolation, "dedicated"); + lbCapabilities.put(Network.Capability.SupportedProtocols, "tcp,udp"); + lbCapabilities.put(Network.Capability.LbSchemes, String.join(",", LoadBalancerContainer.Scheme.Internal.name(), LoadBalancerContainer.Scheme.Public.name())); + capabilities.put(Network.Service.Lb, lbCapabilities); + + capabilities.put(Network.Service.Connectivity, null); + return capabilities; + } + + @Override + public Map> getCapabilities() { + return capabilities; + } + + @Override + public Network.Provider getProvider() { + return Network.Provider.Ovn; + } + + @Override + public boolean implement(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + return true; + } + + @Override + public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + return true; + } + + @Override + public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean shutdown(Network network, ReservationContext context, boolean cleanup) throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean isReady(PhysicalNetworkServiceProvider provider) { + return true; + } + + @Override + public boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean canEnableIndividualServices() { + return true; + } + + @Override + public boolean verifyServicesCombination(Set services) { + return true; + } + + @Override + public boolean addDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean configDhcpSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean removeDhcpSupportForSubnet(Network network) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean setExtraDhcpOptions(Network network, long nicId, Map dhcpOptions) { + return true; + } + + @Override + public boolean removeDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile vmProfile) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean addDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean configDnsSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean removeDnsSupportForSubnet(Network network) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean applyIps(Network network, List ipAddress, Set services) throws ResourceUnavailableException { + return true; + } + + @Override + public IpDeployer getIpDeployer(Network network) { + return this; + } + + @Override + public boolean applyStaticNats(Network config, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean applyPFRules(Network network, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean applyFWRules(Network network, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean applyNetworkACLs(Network config, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean reorderAclRules(Vpc vpc, List networks, List networkACLItems) { + return true; + } + + @Override + public boolean applyLBRules(Network network, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean validateLBRule(Network network, LoadBalancingRule rule) { + return true; + } + + @Override + public List updateHealthChecks(Network network, List lbrules) { + return null; + } + + @Override + public boolean handlesOnlyRulesInTransitionState() { + return false; + } + + @Override + public boolean implementVpc(Vpc vpc, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + return true; + } + + @Override + public boolean shutdownVpc(Vpc vpc, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean createPrivateGateway(PrivateGateway gateway) throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean deletePrivateGateway(PrivateGateway privateGateway) throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean applyStaticRoutes(Vpc vpc, List routes) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean applyACLItemsToPrivateGw(PrivateGateway gateway, List rules) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address) { + return true; + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java new file mode 100644 index 000000000000..860a200689a8 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.dc.DataCenter; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeployDestination; +import com.cloud.network.Network; +import com.cloud.network.NetworkMigrationResponder; +import com.cloud.network.Networks; +import com.cloud.network.PhysicalNetwork; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.guru.GuestNetworkGuru; +import com.cloud.offering.NetworkOffering; +import com.cloud.user.Account; +import com.cloud.vm.NicProfile; +import com.cloud.vm.ReservationContext; +import com.cloud.vm.VirtualMachineProfile; + +public class OvnGuestNetworkGuru extends GuestNetworkGuru implements NetworkMigrationResponder { + public OvnGuestNetworkGuru() { + super(); + _isolationMethods = new PhysicalNetwork.IsolationMethod[] {new PhysicalNetwork.IsolationMethod("OVN")}; + } + + @Override + public boolean canHandle(NetworkOffering offering, DataCenter.NetworkType networkType, PhysicalNetwork physicalNetwork) { + return networkType == DataCenter.NetworkType.Advanced + && isMyTrafficType(offering.getTrafficType()) + && isMyIsolationMethod(physicalNetwork) + && networkOfferingServiceMapDao.isProviderForNetworkOffering(offering.getId(), Network.Provider.Ovn); + } + + @Override + public Network design(NetworkOffering offering, DeploymentPlan plan, Network userSpecified, String name, Long vpcId, Account owner) { + PhysicalNetworkVO physicalNetwork = _physicalNetworkDao.findById(plan.getPhysicalNetworkId()); + DataCenter dataCenter = _dcDao.findById(plan.getDataCenterId()); + if (!canHandle(offering, dataCenter.getNetworkType(), physicalNetwork)) { + logger.debug("Refusing to design this network"); + return null; + } + NetworkVO network = (NetworkVO) super.design(offering, plan, userSpecified, name, vpcId, owner); + if (network == null) { + return null; + } + network.setBroadcastDomainType(Networks.BroadcastDomainType.OVN); + network.setBroadcastUri(Networks.BroadcastDomainType.OVN.toUri(String.format("cs-net-%d", network.getId()))); + return network; + } + + @Override + public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) { + return true; + } + + @Override + public void rollbackMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { + // No OVN resources are allocated during migration preparation in Phase 1. + } + + @Override + public void commitMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { + // No OVN resources are allocated during migration preparation in Phase 1. + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java new file mode 100644 index 000000000000..390b1c2ed8d8 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import org.apache.commons.lang3.StringUtils; + +public class OvnNbClient { + public boolean isValidConnectionString(String connection) { + if (StringUtils.isBlank(connection)) { + return false; + } + return connection.matches("^(tcp|ssl):[^:]+:[0-9]+$") || connection.startsWith("unix:/"); + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderService.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderService.java new file mode 100644 index 000000000000..156a56655cb9 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderService.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 org.apache.cloudstack.service; + +import com.cloud.network.ovn.OvnProvider; +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.command.AddOvnProviderCmd; +import org.apache.cloudstack.api.response.OvnProviderResponse; + +import java.util.List; + +public interface OvnProviderService extends PluggableService { + OvnProvider addProvider(AddOvnProviderCmd cmd); + List listOvnProviders(Long zoneId); + boolean deleteOvnProvider(Long providerId); + OvnProviderResponse createOvnProviderResponse(OvnProvider provider); +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java new file mode 100644 index 000000000000..966e32136f12 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java @@ -0,0 +1,175 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Network; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.OvnProviderDao; +import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.element.OvnProviderVO; +import com.cloud.network.ovn.OvnProvider; +import com.cloud.network.ovn.OvnService; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.command.AddOvnProviderCmd; +import org.apache.cloudstack.api.command.DeleteOvnProviderCmd; +import org.apache.cloudstack.api.command.ListOvnProvidersCmd; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class OvnProviderServiceImpl implements OvnProviderService { + @Inject + DataCenterDao dataCenterDao; + @Inject + OvnProviderDao ovnProviderDao; + @Inject + PhysicalNetworkDao physicalNetworkDao; + @Inject + NetworkDao networkDao; + @Inject + OvnService ovnService; + + @Override + public OvnProvider addProvider(AddOvnProviderCmd cmd) { + validateProvider(cmd); + final long zoneId = cmd.getZoneId(); + return Transaction.execute((TransactionCallback) status -> { + OvnProviderVO provider = new OvnProviderVO.Builder() + .setZoneId(zoneId) + .setName(cmd.getName()) + .setNbConnection(cmd.getNbConnection()) + .setSbConnection(cmd.getSbConnection()) + .setCaCertPath(cmd.getCaCertPath()) + .setClientCertPath(cmd.getClientCertPath()) + .setClientPrivateKeyPath(cmd.getClientPrivateKeyPath()) + .setExternalBridge(cmd.getExternalBridge()) + .setLocalnetName(cmd.getLocalnetName()) + .build(); + return ovnProviderDao.persist(provider); + }); + } + + protected void validateProvider(AddOvnProviderCmd cmd) { + DataCenterVO zone = dataCenterDao.findById(cmd.getZoneId()); + if (zone == null) { + throw new InvalidParameterValueException(String.format("Failed to find zone with id: %s", cmd.getZoneId())); + } + if (ovnProviderDao.findByZoneId(cmd.getZoneId()) != null) { + throw new InvalidParameterValueException(String.format("OVN provider already exists for zone: %s", cmd.getZoneId())); + } + if (!ovnService.isValidConnectionString(cmd.getNbConnection())) { + throw new InvalidParameterValueException("Invalid OVN Northbound connection string"); + } + if (StringUtils.isNotBlank(cmd.getSbConnection()) && !ovnService.isValidConnectionString(cmd.getSbConnection())) { + throw new InvalidParameterValueException("Invalid OVN Southbound connection string"); + } + if (cmd.getNbConnection().startsWith("ssl:") && (StringUtils.isAnyBlank(cmd.getCaCertPath(), cmd.getClientCertPath(), cmd.getClientPrivateKeyPath()))) { + throw new InvalidParameterValueException("OVN SSL connections require CA certificate, client certificate, and client private key paths"); + } + } + + @Override + public List listOvnProviders(Long zoneId) { + List responseList = new ArrayList<>(); + if (zoneId != null) { + OvnProviderVO provider = ovnProviderDao.findByZoneId(zoneId); + if (provider != null) { + responseList.add(createOvnProviderResponse(provider)); + } + return responseList; + } + for (OvnProviderVO provider : ovnProviderDao.listAll()) { + responseList.add(createOvnProviderResponse(provider)); + } + return responseList; + } + + @Override + public boolean deleteOvnProvider(Long providerId) { + OvnProviderVO provider = ovnProviderDao.findById(providerId); + if (provider == null) { + throw new InvalidParameterValueException(String.format("Failed to find OVN provider with id: %s", providerId)); + } + validateNetworkState(provider.getZoneId()); + ovnProviderDao.remove(providerId); + return true; + } + + protected void validateNetworkState(long zoneId) { + List physicalNetworks = physicalNetworkDao.listByZone(zoneId); + for (PhysicalNetworkVO physicalNetwork : physicalNetworks) { + List networks = networkDao.listByPhysicalNetwork(physicalNetwork.getId()); + if (CollectionUtils.isNotEmpty(networks)) { + for (NetworkVO network : networks) { + if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN + && network.getState() != Network.State.Shutdown + && network.getState() != Network.State.Destroy) { + throw new CloudRuntimeException("This OVN provider cannot be deleted as there are one or more logical networks provisioned by CloudStack on it."); + } + } + } + } + } + + @Override + public OvnProviderResponse createOvnProviderResponse(OvnProvider provider) { + DataCenterVO zone = dataCenterDao.findById(provider.getZoneId()); + if (Objects.isNull(zone)) { + throw new CloudRuntimeException(String.format("Failed to find zone with id %s", provider.getZoneId())); + } + OvnProviderResponse response = new OvnProviderResponse(); + response.setName(provider.getName()); + response.setUuid(provider.getUuid()); + response.setZoneId(zone.getUuid()); + response.setZoneName(zone.getName()); + response.setNbConnection(provider.getNbConnection()); + response.setSbConnection(provider.getSbConnection()); + response.setCaCertPath(provider.getCaCertPath()); + response.setClientCertPath(provider.getClientCertPath()); + response.setClientPrivateKeyPath(provider.getClientPrivateKeyPath()); + response.setExternalBridge(provider.getExternalBridge()); + response.setLocalnetName(provider.getLocalnetName()); + response.setObjectName("ovnProvider"); + return response; + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList<>(); + if (Boolean.TRUE.equals(NetworkOrchestrationService.OVN_ENABLED.value())) { + cmdList.add(AddOvnProviderCmd.class); + cmdList.add(ListOvnProvidersCmd.class); + cmdList.add(DeleteOvnProviderCmd.class); + } + return cmdList; + } +} diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java new file mode 100644 index 000000000000..d192805c5c47 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java @@ -0,0 +1,55 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.network.ovn.OvnService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +public class OvnServiceImpl implements OvnService, Configurable { + private final OvnNbClient ovnNbClient = new OvnNbClient(); + + @Override + public String getLogicalSwitchName(long networkId) { + return String.format("cs-net-%d", networkId); + } + + @Override + public String getLogicalRouterName(long vpcId) { + return String.format("cs-vpc-%d", vpcId); + } + + @Override + public String getLogicalSwitchPortName(long nicId) { + return String.format("cs-nic-%d", nicId); + } + + @Override + public boolean isValidConnectionString(String connection) { + return ovnNbClient.isValidConnectionString(connection); + } + + @Override + public String getConfigComponentName() { + return OvnService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[0]; + } +} diff --git a/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/core/spring-ovn-core-managers-context.xml b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/core/spring-ovn-core-managers-context.xml new file mode 100644 index 000000000000..7132803bce82 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/core/spring-ovn-core-managers-context.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/module.properties b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/module.properties new file mode 100644 index 000000000000..4469dbc3a7c9 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=ovn +parent=network diff --git a/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/spring-ovn-context.xml b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/spring-ovn-context.xml new file mode 100644 index 000000000000..c1bd678db588 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/resources/META-INF/cloudstack/ovn/spring-ovn-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java new file mode 100644 index 000000000000..f7a72a37174c --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.network.ovn.OvnProvider; +import com.cloud.user.Account; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.service.OvnProviderService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class AddOvnProviderCmdTest { + @Mock + private OvnProviderService ovnProviderService; + @Mock + private CallContext callContext; + + private MockedStatic callContextMockedStatic; + + @InjectMocks + private AddOvnProviderCmd cmd; + + private AutoCloseable closeable; + + @Before + public void setup() { + closeable = MockitoAnnotations.openMocks(this); + callContextMockedStatic = Mockito.mockStatic(CallContext.class); + callContextMockedStatic.when(CallContext::current).thenReturn(callContext); + } + + @After + public void tearDown() throws Exception { + callContextMockedStatic.close(); + closeable.close(); + } + + @Test + public void testExecuteSuccess() throws ConcurrentOperationException { + OvnProvider provider = Mockito.mock(OvnProvider.class); + OvnProviderResponse response = Mockito.mock(OvnProviderResponse.class); + Mockito.when(ovnProviderService.addProvider(cmd)).thenReturn(provider); + Mockito.when(ovnProviderService.createOvnProviderResponse(provider)).thenReturn(response); + + cmd.execute(); + + Mockito.verify(ovnProviderService).addProvider(cmd); + Mockito.verify(ovnProviderService).createOvnProviderResponse(provider); + Mockito.verify(response).setResponseName(cmd.getCommandName()); + Assert.assertEquals(response, cmd.getResponseObject()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() throws ConcurrentOperationException { + OvnProvider provider = Mockito.mock(OvnProvider.class); + Mockito.when(ovnProviderService.addProvider(cmd)).thenReturn(provider); + Mockito.when(ovnProviderService.createOvnProviderResponse(provider)).thenReturn(null); + + cmd.execute(); + } + + @Test + public void testGetEntityOwnerId() { + Account account = Mockito.mock(Account.class); + Mockito.when(account.getId()).thenReturn(123L); + Mockito.when(callContext.getCallingAccount()).thenReturn(account); + + Assert.assertEquals(123L, cmd.getEntityOwnerId()); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmdTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmdTest.java new file mode 100644 index 000000000000..c497c8e1b41f --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/DeleteOvnProviderCmdTest.java @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.service.OvnProviderService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; + +public class DeleteOvnProviderCmdTest { + @Mock + private OvnProviderService ovnProviderService; + + @InjectMocks + private DeleteOvnProviderCmd cmd; + + private AutoCloseable closeable; + private static final long PROVIDER_ID = 1L; + + @Before + public void setup() throws Exception { + closeable = MockitoAnnotations.openMocks(this); + setPrivateField("id", PROVIDER_ID); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void testExecuteSuccess() throws ConcurrentOperationException { + Mockito.when(ovnProviderService.deleteOvnProvider(PROVIDER_ID)).thenReturn(true); + + cmd.execute(); + + Mockito.verify(ovnProviderService).deleteOvnProvider(PROVIDER_ID); + Assert.assertTrue(cmd.getResponseObject() instanceof SuccessResponse); + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + Assert.assertEquals(cmd.getCommandName(), response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() throws ConcurrentOperationException { + Mockito.when(ovnProviderService.deleteOvnProvider(PROVIDER_ID)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteInvalidParameterException() throws ConcurrentOperationException { + Mockito.when(ovnProviderService.deleteOvnProvider(PROVIDER_ID)).thenThrow(new InvalidParameterValueException("invalid")); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteCloudRuntimeException() throws ConcurrentOperationException { + Mockito.when(ovnProviderService.deleteOvnProvider(PROVIDER_ID)).thenThrow(new CloudRuntimeException("runtime")); + cmd.execute(); + } + + @Test + public void testGetEntityOwnerId() { + Assert.assertEquals(0L, cmd.getEntityOwnerId()); + } + + private void setPrivateField(String fieldName, Object value) throws Exception { + Field field = DeleteOvnProviderCmd.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(cmd, value); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/ListOvnProvidersCmdTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/ListOvnProvidersCmdTest.java new file mode 100644 index 000000000000..f1294c78fd33 --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/ListOvnProvidersCmdTest.java @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.exception.ConcurrentOperationException; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.apache.cloudstack.service.OvnProviderService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +public class ListOvnProvidersCmdTest { + @Mock + private OvnProviderService ovnProviderService; + + @InjectMocks + private ListOvnProvidersCmd cmd; + + private AutoCloseable closeable; + private static final long ZONE_ID = 1L; + + @Before + public void setup() throws Exception { + closeable = MockitoAnnotations.openMocks(this); + setPrivateField("zoneId", ZONE_ID); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void testExecuteSuccess() throws ConcurrentOperationException { + OvnProviderResponse providerResponse = Mockito.mock(OvnProviderResponse.class); + List providerList = Arrays.asList(providerResponse); + Mockito.when(ovnProviderService.listOvnProviders(ZONE_ID)).thenReturn(providerList); + + cmd.execute(); + + Mockito.verify(ovnProviderService).listOvnProviders(ZONE_ID); + Assert.assertTrue(cmd.getResponseObject() instanceof ListResponse); + ListResponse response = (ListResponse) cmd.getResponseObject(); + Assert.assertEquals(cmd.getCommandName(), response.getResponseName()); + } + + @Test + public void testGetEntityOwnerId() { + Assert.assertEquals(0L, cmd.getEntityOwnerId()); + } + + private void setPrivateField(String fieldName, Object value) throws Exception { + Field field = ListOvnProvidersCmd.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(cmd, value); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnElementTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnElementTest.java new file mode 100644 index 000000000000..8007c209c228 --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnElementTest.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.network.Network; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; + +public class OvnElementTest { + @Test + public void testGetProvider() { + Assert.assertEquals(Network.Provider.Ovn, new OvnElement().getProvider()); + } + + @Test + public void testCapabilitiesIncludeInitialOvnServices() { + Map> capabilities = new OvnElement().getCapabilities(); + + Assert.assertTrue(capabilities.containsKey(Network.Service.Dhcp)); + Assert.assertTrue(capabilities.containsKey(Network.Service.Dns)); + Assert.assertTrue(capabilities.containsKey(Network.Service.SourceNat)); + Assert.assertTrue(capabilities.containsKey(Network.Service.StaticNat)); + Assert.assertTrue(capabilities.containsKey(Network.Service.PortForwarding)); + Assert.assertTrue(capabilities.containsKey(Network.Service.Firewall)); + Assert.assertTrue(capabilities.containsKey(Network.Service.NetworkACL)); + Assert.assertTrue(capabilities.containsKey(Network.Service.Lb)); + Assert.assertTrue(capabilities.containsKey(Network.Service.Gateway)); + Assert.assertTrue(capabilities.containsKey(Network.Service.Connectivity)); + Assert.assertFalse(capabilities.containsKey(Network.Service.SecurityGroup)); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java new file mode 100644 index 000000000000..06c3ae16fdca --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java @@ -0,0 +1,199 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Network; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.OvnProviderDao; +import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.element.OvnProviderVO; +import com.cloud.network.ovn.OvnService; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.command.AddOvnProviderCmd; +import org.apache.cloudstack.api.response.OvnProviderResponse; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +public class OvnProviderServiceImplTest { + @Mock + private DataCenterDao dataCenterDao; + @Mock + private OvnProviderDao ovnProviderDao; + @Mock + private PhysicalNetworkDao physicalNetworkDao; + @Mock + private NetworkDao networkDao; + @Mock + private OvnService ovnService; + + @InjectMocks + private OvnProviderServiceImpl ovnProviderService; + + private AutoCloseable closeable; + private MockedStatic transactionMockedStatic; + + private static final long ZONE_ID = 1L; + private static final long PROVIDER_ID = 3L; + private static final String NAME = "test-ovn"; + private static final String NB_CONNECTION = "tcp:127.0.0.1:6641"; + private static final String SB_CONNECTION = "tcp:127.0.0.1:6642"; + + @Before + public void setup() { + closeable = MockitoAnnotations.openMocks(this); + transactionMockedStatic = Mockito.mockStatic(Transaction.class); + } + + @After + public void tearDown() throws Exception { + transactionMockedStatic.close(); + closeable.close(); + } + + @Test + public void testAddProviderPersistsProvider() throws Exception { + AddOvnProviderCmd cmd = new AddOvnProviderCmd(); + setPrivateField(cmd, "zoneId", ZONE_ID); + setPrivateField(cmd, "name", NAME); + setPrivateField(cmd, "nbConnection", NB_CONNECTION); + setPrivateField(cmd, "sbConnection", SB_CONNECTION); + + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(Mockito.mock(DataCenterVO.class)); + Mockito.when(ovnProviderDao.findByZoneId(ZONE_ID)).thenReturn(null); + Mockito.when(ovnService.isValidConnectionString(NB_CONNECTION)).thenReturn(true); + Mockito.when(ovnService.isValidConnectionString(SB_CONNECTION)).thenReturn(true); + Mockito.when(ovnProviderDao.persist(Mockito.any(OvnProviderVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + transactionMockedStatic.when(() -> Transaction.execute(Mockito.>any())).thenAnswer(invocation -> { + TransactionCallback callback = invocation.getArgument(0); + return callback.doInTransaction(null); + }); + + OvnProviderVO provider = (OvnProviderVO) ovnProviderService.addProvider(cmd); + + Assert.assertEquals(ZONE_ID, provider.getZoneId()); + Assert.assertEquals(NAME, provider.getName()); + Assert.assertEquals(NB_CONNECTION, provider.getNbConnection()); + Assert.assertEquals(SB_CONNECTION, provider.getSbConnection()); + Mockito.verify(ovnProviderDao).persist(Mockito.any(OvnProviderVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddProviderRejectsInvalidNbConnection() throws Exception { + AddOvnProviderCmd cmd = new AddOvnProviderCmd(); + setPrivateField(cmd, "zoneId", ZONE_ID); + setPrivateField(cmd, "name", NAME); + setPrivateField(cmd, "nbConnection", "invalid"); + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(Mockito.mock(DataCenterVO.class)); + Mockito.when(ovnService.isValidConnectionString("invalid")).thenReturn(false); + + ovnProviderService.addProvider(cmd); + } + + @Test + public void testListOvnProvidersWithZoneId() { + OvnProviderVO providerVO = Mockito.mock(OvnProviderVO.class); + Mockito.when(ovnProviderDao.findByZoneId(ZONE_ID)).thenReturn(providerVO); + Mockito.when(providerVO.getZoneId()).thenReturn(ZONE_ID); + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(getZone()); + + List result = ovnProviderService.listOvnProviders(ZONE_ID); + + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.get(0) instanceof OvnProviderResponse); + } + + @Test + public void testDeleteOvnProviderSuccess() { + OvnProviderVO providerVO = Mockito.mock(OvnProviderVO.class); + Mockito.when(providerVO.getZoneId()).thenReturn(ZONE_ID); + Mockito.when(ovnProviderDao.findById(PROVIDER_ID)).thenReturn(providerVO); + Mockito.when(physicalNetworkDao.listByZone(ZONE_ID)).thenReturn(Arrays.asList(Mockito.mock(PhysicalNetworkVO.class))); + + NetworkVO network = Mockito.mock(NetworkVO.class); + Mockito.when(networkDao.listByPhysicalNetwork(Mockito.anyLong())).thenReturn(Arrays.asList(network)); + Mockito.when(network.getBroadcastDomainType()).thenReturn(Networks.BroadcastDomainType.OVN); + Mockito.when(network.getState()).thenReturn(Network.State.Shutdown); + + Assert.assertTrue(ovnProviderService.deleteOvnProvider(PROVIDER_ID)); + Mockito.verify(ovnProviderDao).remove(PROVIDER_ID); + } + + @Test(expected = CloudRuntimeException.class) + public void testDeleteOvnProviderWithActiveNetworks() { + OvnProviderVO providerVO = Mockito.mock(OvnProviderVO.class); + Mockito.when(providerVO.getZoneId()).thenReturn(ZONE_ID); + Mockito.when(ovnProviderDao.findById(PROVIDER_ID)).thenReturn(providerVO); + Mockito.when(physicalNetworkDao.listByZone(ZONE_ID)).thenReturn(Arrays.asList(Mockito.mock(PhysicalNetworkVO.class))); + + NetworkVO network = Mockito.mock(NetworkVO.class); + Mockito.when(networkDao.listByPhysicalNetwork(Mockito.anyLong())).thenReturn(Arrays.asList(network)); + Mockito.when(network.getBroadcastDomainType()).thenReturn(Networks.BroadcastDomainType.OVN); + Mockito.when(network.getState()).thenReturn(Network.State.Implemented); + + ovnProviderService.deleteOvnProvider(PROVIDER_ID); + } + + @Test + public void testCreateOvnProviderResponse() { + OvnProviderVO provider = Mockito.mock(OvnProviderVO.class); + Mockito.when(provider.getZoneId()).thenReturn(ZONE_ID); + Mockito.when(provider.getName()).thenReturn(NAME); + Mockito.when(provider.getNbConnection()).thenReturn(NB_CONNECTION); + Mockito.when(provider.getSbConnection()).thenReturn(SB_CONNECTION); + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(getZone()); + + OvnProviderResponse response = ovnProviderService.createOvnProviderResponse(provider); + + Assert.assertNotNull(response); + Assert.assertEquals(NAME, response.getName()); + Assert.assertEquals(NB_CONNECTION, response.getNbConnection()); + Assert.assertEquals(SB_CONNECTION, response.getSbConnection()); + } + + private DataCenterVO getZone() { + DataCenterVO zone = Mockito.mock(DataCenterVO.class); + Mockito.when(zone.getName()).thenReturn("test-zone"); + Mockito.when(zone.getUuid()).thenReturn("zone-uuid"); + return zone; + } + + private void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java new file mode 100644 index 000000000000..f336756fa20e --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java @@ -0,0 +1,40 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import org.junit.Assert; +import org.junit.Test; + +public class OvnServiceImplTest { + private final OvnServiceImpl service = new OvnServiceImpl(); + + @Test + public void testDeterministicObjectNames() { + Assert.assertEquals("cs-net-10", service.getLogicalSwitchName(10L)); + Assert.assertEquals("cs-vpc-20", service.getLogicalRouterName(20L)); + Assert.assertEquals("cs-nic-30", service.getLogicalSwitchPortName(30L)); + } + + @Test + public void testConnectionStringValidation() { + Assert.assertTrue(service.isValidConnectionString("tcp:127.0.0.1:6641")); + Assert.assertTrue(service.isValidConnectionString("ssl:ovn.example.com:6641")); + Assert.assertTrue(service.isValidConnectionString("unix:/var/run/ovn/ovnnb_db.sock")); + Assert.assertFalse(service.isValidConnectionString("http://127.0.0.1:6641")); + Assert.assertFalse(service.isValidConnectionString(null)); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index e4904ccdf40b..324737edfbbb 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -107,6 +107,7 @@ network-elements/netscaler network-elements/nicira-nvp network-elements/opendaylight + network-elements/ovn network-elements/ovs network-elements/palo-alto network-elements/stratosphere-ssp diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 6da5dda967d0..df3ce4369771 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -8354,8 +8354,8 @@ private void validateProvider(NetworkOfferingVO sourceOffering, String detectedProvider, String networkMode) { detectedProvider = getExternalNetworkProvider(detectedProvider, sourceServiceProviderMap); - // If this is an NSX/Netris offering, prevent network mode changes - if (detectedProvider != null && (detectedProvider.equals("NSX") || detectedProvider.equals("Netris"))) { + // If this is an external provider offering, prevent network mode changes + if (detectedProvider != null && (detectedProvider.equals("NSX") || detectedProvider.equals("Netris") || detectedProvider.equals("OVN"))) { if (networkMode != null && sourceOffering.getNetworkMode() != null) { if (!networkMode.equalsIgnoreCase(sourceOffering.getNetworkMode().toString())) { throw new InvalidParameterValueException( @@ -8388,6 +8388,9 @@ public static String getExternalNetworkProvider(String detectedProvider, if (provider == Provider.Netris) { return "Netris"; } + if (provider == Provider.Ovn) { + return "OVN"; + } } } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index c9884f8c469a..daa83592401a 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -4331,6 +4331,13 @@ public PhysicalNetworkVO doInTransaction(TransactionStatus status) { logger.warn("Failed to add Netris provider to physical network due to:", ex.getMessage()); } + // Add OVN provider + try { + addOvnProviderToPhysicalNetwork(pNetwork.getId()); + } catch (Exception ex) { + logger.warn("Failed to add OVN provider to physical network due to:", ex.getMessage()); + } + CallContext.current().putContextParameter(PhysicalNetwork.class, pNetwork.getUuid()); return pNetwork; @@ -5758,6 +5765,21 @@ private PhysicalNetworkServiceProvider addNetrisProviderToPhysicalNetwork(long p return null; } + private PhysicalNetworkServiceProvider addOvnProviderToPhysicalNetwork(long physicalNetworkId) { + PhysicalNetworkVO pvo = _physicalNetworkDao.findById(physicalNetworkId); + DataCenterVO dvo = _dcDao.findById(pvo.getDataCenterId()); + if (dvo.getNetworkType() == NetworkType.Advanced) { + Provider provider = Network.Provider.getProvider(Provider.Ovn.getName()); + if (provider == null) { + return null; + } + + addProviderToPhysicalNetwork(physicalNetworkId, Provider.Ovn.getName(), null, null); + enableProvider(Provider.Ovn.getName()); + } + return null; + } + protected boolean isNetworkSystem(Network network) { NetworkOffering no = _networkOfferingDao.findByIdIncludingRemoved(network.getNetworkOfferingId()); if (no.isSystemOnly()) { From 86ba819686c7c14695e1e9c9cacdabbef5257e49 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Wed, 22 Apr 2026 17:54:24 +0200 Subject: [PATCH 02/33] OVN plugin: fix SSL cert enforcement and minor cleanups Enforce TLS certificate requirement when either the Northbound or Southbound connection uses ssl:, add a logger to the provider service impl, drop the redundant CollectionUtils.isNotEmpty check in validateNetworkState, and correct a stale migration comment in the guest network guru. --- .../service/OvnGuestNetworkGuru.java | 2 +- .../service/OvnProviderServiceImpl.java | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java index 860a200689a8..4f105595c693 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java @@ -75,6 +75,6 @@ public void rollbackMigration(NicProfile nic, Network network, VirtualMachinePro @Override public void commitMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { - // No OVN resources are allocated during migration preparation in Phase 1. + // No OVN resources are committed on migration in Phase 1. } } diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java index 966e32136f12..7b55eba8fbe8 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java @@ -38,8 +38,9 @@ import org.apache.cloudstack.api.command.ListOvnProvidersCmd; import org.apache.cloudstack.api.response.OvnProviderResponse; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import javax.inject.Inject; import java.util.ArrayList; @@ -47,6 +48,8 @@ import java.util.Objects; public class OvnProviderServiceImpl implements OvnProviderService { + protected Logger logger = LogManager.getLogger(getClass()); + @Inject DataCenterDao dataCenterDao; @Inject @@ -92,7 +95,9 @@ protected void validateProvider(AddOvnProviderCmd cmd) { if (StringUtils.isNotBlank(cmd.getSbConnection()) && !ovnService.isValidConnectionString(cmd.getSbConnection())) { throw new InvalidParameterValueException("Invalid OVN Southbound connection string"); } - if (cmd.getNbConnection().startsWith("ssl:") && (StringUtils.isAnyBlank(cmd.getCaCertPath(), cmd.getClientCertPath(), cmd.getClientPrivateKeyPath()))) { + boolean sslRequired = cmd.getNbConnection().startsWith("ssl:") + || (StringUtils.isNotBlank(cmd.getSbConnection()) && cmd.getSbConnection().startsWith("ssl:")); + if (sslRequired && StringUtils.isAnyBlank(cmd.getCaCertPath(), cmd.getClientCertPath(), cmd.getClientPrivateKeyPath())) { throw new InvalidParameterValueException("OVN SSL connections require CA certificate, client certificate, and client private key paths"); } } @@ -127,14 +132,11 @@ public boolean deleteOvnProvider(Long providerId) { protected void validateNetworkState(long zoneId) { List physicalNetworks = physicalNetworkDao.listByZone(zoneId); for (PhysicalNetworkVO physicalNetwork : physicalNetworks) { - List networks = networkDao.listByPhysicalNetwork(physicalNetwork.getId()); - if (CollectionUtils.isNotEmpty(networks)) { - for (NetworkVO network : networks) { - if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN - && network.getState() != Network.State.Shutdown - && network.getState() != Network.State.Destroy) { - throw new CloudRuntimeException("This OVN provider cannot be deleted as there are one or more logical networks provisioned by CloudStack on it."); - } + for (NetworkVO network : networkDao.listByPhysicalNetwork(physicalNetwork.getId())) { + if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN + && network.getState() != Network.State.Shutdown + && network.getState() != Network.State.Destroy) { + throw new CloudRuntimeException("This OVN provider cannot be deleted as there are one or more logical networks provisioned by CloudStack on it."); } } } From a7268151ebdc2395a6f7de25b32f772e3b8cfd9e Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Sat, 25 Apr 2026 18:51:59 +0200 Subject: [PATCH 03/33] OVN plugin: add real Northbound client backed by ODL OVSDB library Replaces the regex-only OvnNbClient with a real OVSDB JSON-RPC client that opens a transient connection to the OVN Northbound endpoint, runs an echo, and confirms the OVN_Northbound database is advertised. Both tcp: and ssl: connection strings are supported. Adds OvnSslContext, an ICertificateManager implementation that loads the operator-supplied CA, client certificate and client private key into in-memory keystores so SSL connections do not need any keystore on disk. OvnService grows verifyNbConnection so callers can fail fast when the Northbound endpoint is unreachable. The ODL OVSDB library 1.18.3 is declared as a plugin dependency with Jackson, Guava and OSGi annotations excluded, since CloudStack already pins those. --- .../com/cloud/network/ovn/OvnService.java | 8 +- plugins/network-elements/ovn/pom.xml | 40 +++++ .../cloudstack/service/OvnNbClient.java | 157 +++++++++++++++++- .../cloudstack/service/OvnServiceImpl.java | 22 ++- .../cloudstack/service/OvnSslContext.java | 130 +++++++++++++++ .../cloudstack/service/OvnNbClientTest.java | 69 ++++++++ .../service/OvnServiceImplTest.java | 19 ++- 7 files changed, 436 insertions(+), 9 deletions(-) create mode 100644 plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnSslContext.java create mode 100644 plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnNbClientTest.java diff --git a/api/src/main/java/com/cloud/network/ovn/OvnService.java b/api/src/main/java/com/cloud/network/ovn/OvnService.java index 29fad2215c66..eda51162dab5 100644 --- a/api/src/main/java/com/cloud/network/ovn/OvnService.java +++ b/api/src/main/java/com/cloud/network/ovn/OvnService.java @@ -18,11 +18,17 @@ /** * Service boundary for CloudStack's native OVN integration. - * Phase 1 only defines stable naming and connection validation helpers; OVN NB operations are added in later phases. */ public interface OvnService { String getLogicalSwitchName(long networkId); String getLogicalRouterName(long vpcId); String getLogicalSwitchPortName(long nicId); boolean isValidConnectionString(String connection); + + /** + * Opens a transient connection to the OVN Northbound endpoint described by the arguments, + * runs an OVSDB echo and confirms the OVN_Northbound database is advertised. Throws a + * {@link com.cloud.utils.exception.CloudRuntimeException} on any failure, leaving no resources behind. + */ + void verifyNbConnection(String nbConnection, String caCertPath, String clientCertPath, String clientPrivateKeyPath); } diff --git a/plugins/network-elements/ovn/pom.xml b/plugins/network-elements/ovn/pom.xml index a46f7a83fa08..36ae54b2858b 100644 --- a/plugins/network-elements/ovn/pom.xml +++ b/plugins/network-elements/ovn/pom.xml @@ -29,4 +29,44 @@ 4.23.0.0-SNAPSHOT ../../pom.xml + + + 1.18.3 + + + + + org.opendaylight.ovsdb + library + ${cs.ovsdb.library.version} + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + com.google.guava + guava + + + + org.osgi + org.osgi.service.component.annotations + + + org.osgi + org.osgi.service.metatype.annotations + + + + diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java index 390b1c2ed8d8..c6a46b8b55da 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java @@ -16,13 +16,168 @@ // under the License. package org.apache.cloudstack.service; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opendaylight.aaa.cert.api.ICertificateManager; +import org.opendaylight.ovsdb.lib.OvsdbClient; +import org.opendaylight.ovsdb.lib.impl.NettyBootstrapFactoryImpl; +import org.opendaylight.ovsdb.lib.impl.OvsdbConnectionService; + +import javax.annotation.PreDestroy; +import javax.net.ssl.SSLContext; +import java.net.InetAddress; +import java.security.KeyStore; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class OvnNbClient { + protected static final Logger logger = LogManager.getLogger(OvnNbClient.class); + private static final String NORTHBOUND_DB = "OVN_Northbound"; + private static final long DEFAULT_TIMEOUT_MS = 5_000L; + private static final Pattern CONN_PATTERN = Pattern.compile("^(tcp|ssl):([^:]+):([0-9]+)$"); + private static final ICertificateManager NOOP_CERT_MANAGER = new NoopCertificateManager(); + + private final long timeoutMs; + private NettyBootstrapFactoryImpl bootstrapFactory; + private OvsdbConnectionService tcpConnectionService; + + public OvnNbClient() { + this(DEFAULT_TIMEOUT_MS); + } + + public OvnNbClient(long timeoutMs) { + this.timeoutMs = timeoutMs; + } + public boolean isValidConnectionString(String connection) { if (StringUtils.isBlank(connection)) { return false; } - return connection.matches("^(tcp|ssl):[^:]+:[0-9]+$") || connection.startsWith("unix:/"); + return CONN_PATTERN.matcher(connection).matches() || connection.startsWith("unix:/"); + } + + /** + * Opens a transient connection to NB, runs an echo, lists the databases, and disconnects. + * Throws on failure — caller treats success as proof that the NB endpoint is reachable + * and the supplied credentials/certificates are valid. + */ + public void verifyConnection(String nbConnection, String caCertPath, String clientCertPath, String clientPrivateKeyPath) { + Endpoint ep = parse(nbConnection); + if (ep.scheme == Scheme.UNIX) { + throw new CloudRuntimeException("Unix-socket OVN connections are not supported by the management server client; use tcp: or ssl:"); + } + + OvsdbConnectionService service = null; + OvsdbClient client = null; + boolean closeServiceWhenDone = false; + try { + InetAddress addr = InetAddress.getByName(ep.host); + if (ep.scheme == Scheme.SSL) { + ICertificateManager cm = OvnSslContext.fromPaths(caCertPath, clientCertPath, clientPrivateKeyPath).asCertificateManager(); + service = new OvsdbConnectionService(bootstrapFactory(), cm); + closeServiceWhenDone = true; + client = service.connectWithSsl(addr, ep.port, cm); + } else { + service = tcpService(); + client = service.connect(addr, ep.port); + } + if (client == null) { + throw new CloudRuntimeException(String.format("OVN NB at %s did not accept the connection", nbConnection)); + } + client.echo().get(timeoutMs, TimeUnit.MILLISECONDS); + List dbs = client.getDatabases().get(timeoutMs, TimeUnit.MILLISECONDS); + if (dbs == null || !dbs.contains(NORTHBOUND_DB)) { + throw new CloudRuntimeException(String.format("OVN endpoint %s did not advertise %s; got %s", + nbConnection, NORTHBOUND_DB, dbs)); + } + logger.debug("OVN NB at {} reachable, databases={}", nbConnection, dbs); + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + throw new CloudRuntimeException("Cannot reach OVN NB at " + nbConnection + ": " + e.getMessage(), e); + } finally { + if (client != null && service != null) { + try { service.disconnect(client); } catch (Exception ignored) { } + } + if (closeServiceWhenDone && service != null) { + try { service.close(); } catch (Exception ignored) { } + } + } + } + + @PreDestroy + public synchronized void shutdown() { + if (tcpConnectionService != null) { + try { tcpConnectionService.close(); } catch (Exception ignored) { } + tcpConnectionService = null; + } + if (bootstrapFactory != null) { + try { bootstrapFactory.close(); } catch (Exception ignored) { } + bootstrapFactory = null; + } + } + + private synchronized NettyBootstrapFactoryImpl bootstrapFactory() { + if (bootstrapFactory == null) { + bootstrapFactory = new NettyBootstrapFactoryImpl(); + } + return bootstrapFactory; + } + + private synchronized OvsdbConnectionService tcpService() { + if (tcpConnectionService == null) { + tcpConnectionService = new OvsdbConnectionService(bootstrapFactory(), NOOP_CERT_MANAGER); + } + return tcpConnectionService; + } + + static Endpoint parse(String connection) { + if (StringUtils.isBlank(connection)) { + throw new CloudRuntimeException("OVN connection string is blank"); + } + if (connection.startsWith("unix:/")) { + return new Endpoint(Scheme.UNIX, connection.substring("unix:".length()), 0); + } + Matcher m = CONN_PATTERN.matcher(connection); + if (!m.matches()) { + throw new CloudRuntimeException("Invalid OVN connection string: " + connection); + } + Scheme scheme = "ssl".equals(m.group(1)) ? Scheme.SSL : Scheme.TCP; + return new Endpoint(scheme, m.group(2), Integer.parseInt(m.group(3))); + } + + enum Scheme { TCP, SSL, UNIX } + + static final class Endpoint { + final Scheme scheme; + final String host; + final int port; + + Endpoint(Scheme scheme, String host, int port) { + this.scheme = scheme; + this.host = host; + this.port = port; + } + } + + /** + * The OvsdbConnectionService constructor requires a non-null ICertificateManager even for plain + * TCP. None of its methods are invoked along the TCP code path. + */ + private static final class NoopCertificateManager implements ICertificateManager { + @Override public KeyStore getODLKeyStore() { return null; } + @Override public KeyStore getTrustKeyStore() { return null; } + @Override public String[] getCipherSuites() { return new String[0]; } + @Override public String[] getTlsProtocols() { return new String[0]; } + @Override public String getCertificateTrustStore(String s, String d, boolean p) { return null; } + @Override public String getODLKeyStoreCertificate(String s, boolean p) { return null; } + @Override public String genODLKeyStoreCertificateReq(String s, boolean p) { return null; } + @Override public SSLContext getServerContext() { return null; } + @Override public boolean importSslDataKeystores(String a, String b, String c, String d, String e, String[] f, String g) { return false; } + @Override public void exportSslDataKeystores() { } } } diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java index d192805c5c47..564d3990bf7a 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnServiceImpl.java @@ -20,8 +20,18 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import javax.annotation.PreDestroy; + public class OvnServiceImpl implements OvnService, Configurable { - private final OvnNbClient ovnNbClient = new OvnNbClient(); + private final OvnNbClient ovnNbClient; + + public OvnServiceImpl() { + this(new OvnNbClient()); + } + + OvnServiceImpl(OvnNbClient ovnNbClient) { + this.ovnNbClient = ovnNbClient; + } @Override public String getLogicalSwitchName(long networkId) { @@ -43,6 +53,11 @@ public boolean isValidConnectionString(String connection) { return ovnNbClient.isValidConnectionString(connection); } + @Override + public void verifyNbConnection(String nbConnection, String caCertPath, String clientCertPath, String clientPrivateKeyPath) { + ovnNbClient.verifyConnection(nbConnection, caCertPath, clientCertPath, clientPrivateKeyPath); + } + @Override public String getConfigComponentName() { return OvnService.class.getSimpleName(); @@ -52,4 +67,9 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[0]; } + + @PreDestroy + public void shutdown() { + ovnNbClient.shutdown(); + } } diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnSslContext.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnSslContext.java new file mode 100644 index 000000000000..02a569cdfea8 --- /dev/null +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnSslContext.java @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.StringUtils; +import org.opendaylight.aaa.cert.api.ICertificateManager; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class OvnSslContext { + private static final char[] EMPTY_PASSWORD = new char[0]; + private static final String KEYSTORE_TYPE = "JKS"; + private static final String CLIENT_KEY_ALIAS = "ovn-client"; + private static final String CA_ALIAS = "ovn-ca"; + private static final Pattern PEM_PRIVATE_KEY = Pattern.compile( + "-----BEGIN (?:RSA |EC )?PRIVATE KEY-----(.+?)-----END (?:RSA |EC )?PRIVATE KEY-----", + Pattern.DOTALL); + + private final KeyStore keyStore; + private final KeyStore trustStore; + + OvnSslContext(KeyStore keyStore, KeyStore trustStore) { + this.keyStore = keyStore; + this.trustStore = trustStore; + } + + public static OvnSslContext fromPaths(String caCertPath, String clientCertPath, String clientPrivateKeyPath) { + if (StringUtils.isAnyBlank(caCertPath, clientCertPath, clientPrivateKeyPath)) { + throw new CloudRuntimeException("OVN SSL connection requires CA, client certificate and client private key paths"); + } + try { + KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE); + trustStore.load(null, EMPTY_PASSWORD); + trustStore.setCertificateEntry(CA_ALIAS, readCertificate(caCertPath)); + + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(null, EMPTY_PASSWORD); + keyStore.setKeyEntry(CLIENT_KEY_ALIAS, readPrivateKey(clientPrivateKeyPath), EMPTY_PASSWORD, + new Certificate[]{readCertificate(clientCertPath)}); + return new OvnSslContext(keyStore, trustStore); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to build OVN SSL context: " + e.getMessage(), e); + } + } + + public ICertificateManager asCertificateManager() { + return new ICertificateManager() { + @Override public KeyStore getODLKeyStore() { return keyStore; } + @Override public KeyStore getTrustKeyStore() { return trustStore; } + @Override public String[] getCipherSuites() { return new String[0]; } + @Override public String[] getTlsProtocols() { return new String[]{"TLSv1.2", "TLSv1.3"}; } + @Override public String getCertificateTrustStore(String s, String d, boolean p) { return null; } + @Override public String getODLKeyStoreCertificate(String s, boolean p) { return null; } + @Override public String genODLKeyStoreCertificateReq(String s, boolean p) { return null; } + @Override public SSLContext getServerContext() { return buildContext(); } + @Override public boolean importSslDataKeystores(String a, String b, String c, String d, String e, String[] f, String g) { return false; } + @Override public void exportSslDataKeystores() { } + }; + } + + private SSLContext buildContext() { + try { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, EMPTY_PASSWORD); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return ctx; + } catch (Exception e) { + throw new CloudRuntimeException("Failed to initialize OVN SSL context: " + e.getMessage(), e); + } + } + + private static X509Certificate readCertificate(String path) throws IOException { + try (InputStream in = Files.newInputStream(Path.of(path))) { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in); + } catch (java.security.cert.CertificateException e) { + throw new IOException("Cannot parse certificate at " + path + ": " + e.getMessage(), e); + } + } + + private static PrivateKey readPrivateKey(String path) throws IOException { + String pem = Files.readString(Path.of(path)); + Matcher m = PEM_PRIVATE_KEY.matcher(pem); + if (!m.find()) { + throw new IOException("No PRIVATE KEY block found at " + path); + } + byte[] der = Base64.getMimeDecoder().decode(m.group(1)); + try { + return java.security.KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(der)); + } catch (Exception eRsa) { + try { + return java.security.KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(der)); + } catch (Exception eEc) { + throw new IOException("Cannot parse private key at " + path + " as RSA or EC: " + eEc.getMessage(), eEc); + } + } + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnNbClientTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnNbClientTest.java new file mode 100644 index 000000000000..23949ec9a9ea --- /dev/null +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnNbClientTest.java @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.service; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.junit.Assert; +import org.junit.Test; + +public class OvnNbClientTest { + private final OvnNbClient client = new OvnNbClient(); + + @Test + public void testIsValidConnectionString() { + Assert.assertTrue(client.isValidConnectionString("tcp:127.0.0.1:6641")); + Assert.assertTrue(client.isValidConnectionString("ssl:ovn.example.com:6641")); + Assert.assertTrue(client.isValidConnectionString("unix:/var/run/ovn/ovnnb_db.sock")); + Assert.assertFalse(client.isValidConnectionString("http://1.2.3.4:6641")); + Assert.assertFalse(client.isValidConnectionString("tcp:1.2.3.4")); + Assert.assertFalse(client.isValidConnectionString("")); + Assert.assertFalse(client.isValidConnectionString(null)); + } + + @Test + public void testParseTcpEndpoint() { + OvnNbClient.Endpoint ep = OvnNbClient.parse("tcp:10.0.34.51:6641"); + Assert.assertEquals(OvnNbClient.Scheme.TCP, ep.scheme); + Assert.assertEquals("10.0.34.51", ep.host); + Assert.assertEquals(6641, ep.port); + } + + @Test + public void testParseSslEndpoint() { + OvnNbClient.Endpoint ep = OvnNbClient.parse("ssl:nb.example.com:6641"); + Assert.assertEquals(OvnNbClient.Scheme.SSL, ep.scheme); + Assert.assertEquals("nb.example.com", ep.host); + Assert.assertEquals(6641, ep.port); + } + + @Test + public void testParseUnixEndpoint() { + OvnNbClient.Endpoint ep = OvnNbClient.parse("unix:/var/run/ovn/ovnnb_db.sock"); + Assert.assertEquals(OvnNbClient.Scheme.UNIX, ep.scheme); + Assert.assertEquals("/var/run/ovn/ovnnb_db.sock", ep.host); + } + + @Test(expected = CloudRuntimeException.class) + public void testParseInvalidThrows() { + OvnNbClient.parse("not-a-connection-string"); + } + + @Test(expected = CloudRuntimeException.class) + public void testVerifyConnectionRejectsUnix() { + client.verifyConnection("unix:/var/run/ovn/ovnnb_db.sock", null, null, null); + } +} diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java index f336756fa20e..59330d2aa859 100644 --- a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnServiceImplTest.java @@ -18,9 +18,11 @@ import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; public class OvnServiceImplTest { - private final OvnServiceImpl service = new OvnServiceImpl(); + private final OvnNbClient mockClient = Mockito.mock(OvnNbClient.class); + private final OvnServiceImpl service = new OvnServiceImpl(mockClient); @Test public void testDeterministicObjectNames() { @@ -30,11 +32,16 @@ public void testDeterministicObjectNames() { } @Test - public void testConnectionStringValidation() { + public void testConnectionStringValidationDelegatesToClient() { + Mockito.when(mockClient.isValidConnectionString("tcp:127.0.0.1:6641")).thenReturn(true); + Mockito.when(mockClient.isValidConnectionString("bogus")).thenReturn(false); Assert.assertTrue(service.isValidConnectionString("tcp:127.0.0.1:6641")); - Assert.assertTrue(service.isValidConnectionString("ssl:ovn.example.com:6641")); - Assert.assertTrue(service.isValidConnectionString("unix:/var/run/ovn/ovnnb_db.sock")); - Assert.assertFalse(service.isValidConnectionString("http://127.0.0.1:6641")); - Assert.assertFalse(service.isValidConnectionString(null)); + Assert.assertFalse(service.isValidConnectionString("bogus")); + } + + @Test + public void testVerifyNbConnectionDelegatesToClient() { + service.verifyNbConnection("tcp:1.2.3.4:6641", null, null, null); + Mockito.verify(mockClient).verifyConnection("tcp:1.2.3.4:6641", null, null, null); } } From 839af0e8b2433ab234cf01dfe2aa16dfdcd2a94e Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Sat, 25 Apr 2026 18:52:22 +0200 Subject: [PATCH 04/33] OVN plugin: health-check Northbound endpoint at addOvnProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addOvnProvider now opens a transient OVSDB connection during request validation and rejects the registration with InvalidParameterValueException if the endpoint is unreachable or does not advertise the OVN_Northbound database. This catches typos, missing TLS material and routing problems before persisting a broken provider row. Also reorders the order-of-evaluation of two pre-existing tests that constructed Mockito stubs inside thenReturn() arguments, and rewrites AddOvnProviderCmdTest to set the service field by reflection — the prior @InjectMocks-driven setup was ambiguous because BaseCmd has an Object-typed _responseObject field that matched any of the @Mock fields. --- .../service/OvnProviderServiceImpl.java | 6 +++++ .../api/command/AddOvnProviderCmdTest.java | 27 +++++++++---------- .../service/OvnProviderServiceImplTest.java | 9 ++++--- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java index 7b55eba8fbe8..b47be9967132 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnProviderServiceImpl.java @@ -100,6 +100,12 @@ protected void validateProvider(AddOvnProviderCmd cmd) { if (sslRequired && StringUtils.isAnyBlank(cmd.getCaCertPath(), cmd.getClientCertPath(), cmd.getClientPrivateKeyPath())) { throw new InvalidParameterValueException("OVN SSL connections require CA certificate, client certificate, and client private key paths"); } + try { + ovnService.verifyNbConnection(cmd.getNbConnection(), cmd.getCaCertPath(), cmd.getClientCertPath(), cmd.getClientPrivateKeyPath()); + } catch (CloudRuntimeException e) { + logger.warn("OVN NB health check failed for zone {}: {}", cmd.getZoneId(), e.getMessage()); + throw new InvalidParameterValueException("OVN NB endpoint is unreachable: " + e.getMessage()); + } } @Override diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java index f7a72a37174c..cda5a7e0677c 100644 --- a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/api/command/AddOvnProviderCmdTest.java @@ -27,36 +27,35 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; public class AddOvnProviderCmdTest { - @Mock private OvnProviderService ovnProviderService; - @Mock private CallContext callContext; - private MockedStatic callContextMockedStatic; - - @InjectMocks private AddOvnProviderCmd cmd; - private AutoCloseable closeable; - @Before - public void setup() { - closeable = MockitoAnnotations.openMocks(this); + public void setup() throws Exception { + ovnProviderService = Mockito.mock(OvnProviderService.class); + callContext = Mockito.mock(CallContext.class); callContextMockedStatic = Mockito.mockStatic(CallContext.class); callContextMockedStatic.when(CallContext::current).thenReturn(callContext); + + cmd = new AddOvnProviderCmd(); + Field svc = AddOvnProviderCmd.class.getDeclaredField("ovnProviderService"); + svc.setAccessible(true); + svc.set(cmd, ovnProviderService); } @After public void tearDown() throws Exception { - callContextMockedStatic.close(); - closeable.close(); + if (callContextMockedStatic != null) { + callContextMockedStatic.close(); + } } @Test diff --git a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java index 06c3ae16fdca..a912fd659aad 100644 --- a/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java +++ b/plugins/network-elements/ovn/src/test/java/org/apache/cloudstack/service/OvnProviderServiceImplTest.java @@ -96,6 +96,7 @@ public void testAddProviderPersistsProvider() throws Exception { Mockito.when(ovnProviderDao.findByZoneId(ZONE_ID)).thenReturn(null); Mockito.when(ovnService.isValidConnectionString(NB_CONNECTION)).thenReturn(true); Mockito.when(ovnService.isValidConnectionString(SB_CONNECTION)).thenReturn(true); + Mockito.doNothing().when(ovnService).verifyNbConnection(Mockito.eq(NB_CONNECTION), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.when(ovnProviderDao.persist(Mockito.any(OvnProviderVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); transactionMockedStatic.when(() -> Transaction.execute(Mockito.>any())).thenAnswer(invocation -> { TransactionCallback callback = invocation.getArgument(0); @@ -126,9 +127,10 @@ public void testAddProviderRejectsInvalidNbConnection() throws Exception { @Test public void testListOvnProvidersWithZoneId() { OvnProviderVO providerVO = Mockito.mock(OvnProviderVO.class); - Mockito.when(ovnProviderDao.findByZoneId(ZONE_ID)).thenReturn(providerVO); Mockito.when(providerVO.getZoneId()).thenReturn(ZONE_ID); - Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(getZone()); + Mockito.when(ovnProviderDao.findByZoneId(ZONE_ID)).thenReturn(providerVO); + DataCenterVO zone = getZone(); + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(zone); List result = ovnProviderService.listOvnProviders(ZONE_ID); @@ -174,7 +176,8 @@ public void testCreateOvnProviderResponse() { Mockito.when(provider.getName()).thenReturn(NAME); Mockito.when(provider.getNbConnection()).thenReturn(NB_CONNECTION); Mockito.when(provider.getSbConnection()).thenReturn(SB_CONNECTION); - Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(getZone()); + DataCenterVO zone = getZone(); + Mockito.when(dataCenterDao.findById(ZONE_ID)).thenReturn(zone); OvnProviderResponse response = ovnProviderService.createOvnProviderResponse(provider); From bd8ce97948dfe3dde31491c284c9efc950a1716c Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Mon, 27 Apr 2026 16:35:55 +0200 Subject: [PATCH 05/33] OVN plugin: create logical switches and persist system VM bootstrap --- .../java/com/cloud/vm/VirtualMachineGuru.java | 2 +- .../wrapper/LibvirtStartCommandWrapper.java | 12 ++ .../LibvirtComputingResourceTest.java | 6 + .../apache/cloudstack/service/OvnElement.java | 49 +++++ .../service/OvnGuestNetworkGuru.java | 22 ++- .../cloudstack/service/OvnNbClient.java | 167 +++++++++++++++--- .../network/guru/OvsGuestNetworkGuru.java | 3 +- .../consoleproxy/ConsoleProxyManagerImpl.java | 5 +- .../SecondaryStorageManagerImpl.java | 6 +- .../debian/opt/cloud/bin/setup/bootstrap.sh | 47 ++++- .../opt/cloud/bin/setup/cloud-early-config | 2 + systemvm/debian/opt/cloud/bin/setup/common.sh | 4 +- 12 files changed, 290 insertions(+), 35 deletions(-) diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java index 76f0830f369e..cb908348072c 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java @@ -88,7 +88,7 @@ public static String getEncodedString(String certificate) { return Base64.getEncoder().encodeToString(certificate.replace("\n", KeyStoreUtils.CERT_NEWLINE_ENCODER).replace(" ", KeyStoreUtils.CERT_SPACE_ENCODER).getBytes(StandardCharsets.UTF_8)); } - static void appendCertificateDetails(StringBuilder buf, Certificate certificate) { + public static void appendCertificateDetails(StringBuilder buf, Certificate certificate) { try { buf.append(" certificate=").append(getEncodedString(CertUtils.x509CertificateToPem(certificate.getClientCertificate()))); buf.append(" cacertificate=").append(getEncodedString(CertUtils.x509CertificatesToPem(certificate.getCaCertificates()))); 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 567986465906..3e1547057f2d 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 @@ -46,6 +46,8 @@ import com.cloud.network.Networks.TrafficType; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.ssh.SshHelper; import com.cloud.vm.UserVmManager; import com.cloud.vm.VirtualMachine; @@ -131,6 +133,16 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource try { File pemFile = new File(LibvirtComputingResource.SSHPRVKEYPATH); FileUtil.scpPatchFiles(controlIp, VRScripts.CONFIG_CACHE_LOCATION, Integer.parseInt(LibvirtComputingResource.DEFAULTDOMRSSHPORT), pemFile, LibvirtComputingResource.systemVmPatchFiles, LibvirtComputingResource.BASEPATH); + if (vmName.startsWith("s-") || vmName.startsWith("v-")) { + Pair setupResult = SshHelper.sshExecute(controlIp, Integer.parseInt(LibvirtComputingResource.DEFAULTDOMRSSHPORT), "root", pemFile, null, + "if [ ! -x /usr/local/cloud/systemvm/_run.sh ] || [ ! -f /usr/local/cloud/systemvm/conf/cloud.jks ]; then /opt/cloud/bin/setup/cloud-early-config; fi && systemctl restart cloud.service", + 10000, 10000, 600000); + if (!setupResult.first()) { + String errMsg = String.format("Failed to setup systemVM after copying patch files: %s", setupResult.second()); + logger.error(errMsg); + return new StartAnswer(command, errMsg); + } + } if (!virtRouterResource.isSystemVMSetup(vmName, controlIp)) { String errMsg = "Failed to patch systemVM"; logger.error(errMsg); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index b96295240076..b9cf1c3dd0a6 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -5295,6 +5295,9 @@ public void testStartCommand() throws Exception { Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), any(File.class), nullable(String.class), Mockito.anyString(), any(String[].class), Mockito.anyString())).thenAnswer(invocation -> null); + sshHelperMockedStatic.when(() -> SshHelper.sshExecute( + Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), any(File.class), nullable(String.class), + Mockito.anyString(), Mockito.anyInt(), Mockito.anyInt(), Mockito.anyInt())).thenReturn(new Pair<>(true, "")); final LibvirtRequestWrapper wrapper = LibvirtRequestWrapper.getInstance(); assertNotNull(wrapper); @@ -5375,6 +5378,9 @@ public void testStartCommandIsolationEc2() throws Exception { Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), any(File.class), nullable(String.class), Mockito.anyString(), any(String[].class), Mockito.anyString())).thenAnswer(invocation -> null); + sshHelperMockedStatic.when(() -> SshHelper.sshExecute( + Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), any(File.class), nullable(String.class), + Mockito.anyString(), Mockito.anyInt(), Mockito.anyInt(), Mockito.anyInt())).thenReturn(new Pair<>(true, "")); final LibvirtRequestWrapper wrapper = LibvirtRequestWrapper.getInstance(); assertNotNull(wrapper); diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java index d7521b5582d9..6fe6b0023074 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnElement.java @@ -17,12 +17,14 @@ package org.apache.cloudstack.service; import com.cloud.agent.api.to.LoadBalancerTO; +import com.cloud.dc.DataCenter; import com.cloud.deploy.DeployDestination; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.IpAddress; import com.cloud.network.Network; +import com.cloud.network.Networks; import com.cloud.network.PhysicalNetworkServiceProvider; import com.cloud.network.PublicIpAddress; import com.cloud.network.element.DhcpServiceProvider; @@ -31,6 +33,8 @@ import com.cloud.network.element.IpDeployer; import com.cloud.network.element.LoadBalancingServiceProvider; import com.cloud.network.element.NetworkACLServiceProvider; +import com.cloud.network.dao.OvnProviderDao; +import com.cloud.network.element.OvnProviderVO; import com.cloud.network.element.PortForwardingServiceProvider; import com.cloud.network.element.StaticNatServiceProvider; import com.cloud.network.element.VpcProvider; @@ -45,6 +49,7 @@ import com.cloud.network.vpc.Vpc; import com.cloud.offering.NetworkOffering; import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.NicProfile; import com.cloud.vm.ReservationContext; import com.cloud.vm.VirtualMachineProfile; @@ -53,12 +58,17 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.inject.Inject; public class OvnElement extends AdapterBase implements DhcpServiceProvider, DnsServiceProvider, VpcProvider, StaticNatServiceProvider, IpDeployer, PortForwardingServiceProvider, FirewallServiceProvider, NetworkACLServiceProvider, LoadBalancingServiceProvider { private final Map> capabilities = initCapabilities(); + private final OvnNbClient ovnNbClient = new OvnNbClient(); + + @Inject + OvnProviderDao ovnProviderDao; protected static Map> initCapabilities() { Map> capabilities = new HashMap<>(); @@ -110,6 +120,20 @@ public Network.Provider getProvider() { @Override public boolean implement(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN) { + OvnProviderVO provider = getProviderForNetwork(network); + String logicalSwitchName = getLogicalSwitchName(network); + Map externalIds = new HashMap<>(); + externalIds.put("cloudstack_network_id", String.valueOf(network.getId())); + externalIds.put("cloudstack_network_uuid", network.getUuid()); + externalIds.put("cloudstack_zone_id", String.valueOf(network.getDataCenterId())); + try { + ovnNbClient.createLogicalSwitch(provider.getNbConnection(), provider.getCaCertPath(), provider.getClientCertPath(), + provider.getClientPrivateKeyPath(), logicalSwitchName, externalIds); + } catch (CloudRuntimeException e) { + throw new ResourceUnavailableException(e.getMessage(), DataCenter.class, network.getDataCenterId()); + } + } return true; } @@ -127,14 +151,39 @@ public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm @Override public boolean shutdown(Network network, ReservationContext context, boolean cleanup) throws ConcurrentOperationException, ResourceUnavailableException { + if (cleanup && network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN) { + destroy(network, context); + } return true; } @Override public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { + if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.OVN) { + OvnProviderVO provider = getProviderForNetwork(network); + try { + ovnNbClient.deleteLogicalSwitch(provider.getNbConnection(), provider.getCaCertPath(), provider.getClientCertPath(), + provider.getClientPrivateKeyPath(), getLogicalSwitchName(network)); + } catch (CloudRuntimeException e) { + throw new ResourceUnavailableException(e.getMessage(), DataCenter.class, network.getDataCenterId()); + } + } return true; } + protected OvnProviderVO getProviderForNetwork(Network network) throws ResourceUnavailableException { + OvnProviderVO provider = ovnProviderDao.findByZoneId(network.getDataCenterId()); + if (provider == null) { + throw new ResourceUnavailableException(String.format("No OVN provider configured for zone %s", network.getDataCenterId()), + DataCenter.class, network.getDataCenterId()); + } + return provider; + } + + protected String getLogicalSwitchName(Network network) { + return String.format("cs-net-%d", network.getId()); + } + @Override public boolean isReady(PhysicalNetworkServiceProvider provider) { return true; diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java index 4f105595c693..a81b8639322e 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnGuestNetworkGuru.java @@ -19,6 +19,7 @@ import com.cloud.dc.DataCenter; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeployDestination; +import com.cloud.exception.InsufficientVirtualNetworkCapacityException; import com.cloud.network.Network; import com.cloud.network.NetworkMigrationResponder; import com.cloud.network.Networks; @@ -59,10 +60,25 @@ public Network design(NetworkOffering offering, DeploymentPlan plan, Network use return null; } network.setBroadcastDomainType(Networks.BroadcastDomainType.OVN); - network.setBroadcastUri(Networks.BroadcastDomainType.OVN.toUri(String.format("cs-net-%d", network.getId()))); + // Broadcast URI is deferred to implement(); the network has no persisted ID yet here. return network; } + @Override + public Network implement(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context) + throws InsufficientVirtualNetworkCapacityException { + Network implemented = super.implement(network, offering, dest, context); + if (implemented == null) { + return null; + } + if (implemented instanceof NetworkVO) { + NetworkVO impl = (NetworkVO) implemented; + impl.setBroadcastDomainType(Networks.BroadcastDomainType.OVN); + impl.setBroadcastUri(Networks.BroadcastDomainType.OVN.toUri(String.format("cs-net-%d", network.getId()))); + } + return implemented; + } + @Override public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) { return true; @@ -70,11 +86,11 @@ public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineP @Override public void rollbackMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { - // No OVN resources are allocated during migration preparation in Phase 1. + // No OVN resources are allocated during migration preparation yet. } @Override public void commitMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { - // No OVN resources are committed on migration in Phase 1. + // No OVN resources are committed on migration yet. } } diff --git a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java index c6a46b8b55da..6e2d15667b28 100644 --- a/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java +++ b/plugins/network-elements/ovn/src/main/java/org/apache/cloudstack/service/OvnNbClient.java @@ -24,12 +24,25 @@ import org.opendaylight.ovsdb.lib.OvsdbClient; import org.opendaylight.ovsdb.lib.impl.NettyBootstrapFactoryImpl; import org.opendaylight.ovsdb.lib.impl.OvsdbConnectionService; +import org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.operations.DefaultOperations; +import org.opendaylight.ovsdb.lib.operations.Insert; +import org.opendaylight.ovsdb.lib.operations.Operation; +import org.opendaylight.ovsdb.lib.operations.OperationResult; +import org.opendaylight.ovsdb.lib.operations.Operations; +import org.opendaylight.ovsdb.lib.schema.ColumnSchema; +import org.opendaylight.ovsdb.lib.schema.DatabaseSchema; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; import javax.annotation.PreDestroy; import javax.net.ssl.SSLContext; import java.net.InetAddress; import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,9 +50,11 @@ public class OvnNbClient { protected static final Logger logger = LogManager.getLogger(OvnNbClient.class); private static final String NORTHBOUND_DB = "OVN_Northbound"; + private static final String LOGICAL_SWITCH_TABLE = "Logical_Switch"; private static final long DEFAULT_TIMEOUT_MS = 5_000L; private static final Pattern CONN_PATTERN = Pattern.compile("^(tcp|ssl):([^:]+):([0-9]+)$"); private static final ICertificateManager NOOP_CERT_MANAGER = new NoopCertificateManager(); + private static final Operations OVSDB_OPS = new DefaultOperations(); private final long timeoutMs; private NettyBootstrapFactoryImpl bootstrapFactory; @@ -62,10 +77,138 @@ public boolean isValidConnectionString(String connection) { /** * Opens a transient connection to NB, runs an echo, lists the databases, and disconnects. - * Throws on failure — caller treats success as proof that the NB endpoint is reachable + * Throws on failure - caller treats success as proof that the NB endpoint is reachable * and the supplied credentials/certificates are valid. */ public void verifyConnection(String nbConnection, String caCertPath, String clientCertPath, String clientPrivateKeyPath) { + runOn(nbConnection, caCertPath, clientCertPath, clientPrivateKeyPath, client -> { + client.echo().get(timeoutMs, TimeUnit.MILLISECONDS); + List dbs = client.getDatabases().get(timeoutMs, TimeUnit.MILLISECONDS); + if (dbs == null || !dbs.contains(NORTHBOUND_DB)) { + throw new CloudRuntimeException(String.format("OVN endpoint %s did not advertise %s; got %s", + nbConnection, NORTHBOUND_DB, dbs)); + } + logger.debug("OVN NB at {} reachable, databases={}", nbConnection, dbs); + return null; + }); + } + + /** + * Creates a Logical_Switch with the given name and external_ids in the OVN_Northbound database + * exposed at {@code nbConnection}. Idempotent: if a switch with the same name already exists, + * the call succeeds without modifying it. Uses the native OVSDB JSON-RPC protocol. + */ + public void createLogicalSwitch(String nbConnection, String caCertPath, String clientCertPath, + String clientPrivateKeyPath, String logicalSwitchName, Map externalIds) { + if (StringUtils.isBlank(logicalSwitchName)) { + throw new CloudRuntimeException("Logical switch name is blank"); + } + runOn(nbConnection, caCertPath, clientCertPath, clientPrivateKeyPath, client -> { + DatabaseSchema schema = client.getSchema(NORTHBOUND_DB).get(timeoutMs, TimeUnit.MILLISECONDS); + GenericTableSchema ls = schema.table(LOGICAL_SWITCH_TABLE, GenericTableSchema.class); + ColumnSchema nameCol = ls.column("name", String.class); + + if (logicalSwitchExists(client, schema, ls, nameCol, logicalSwitchName)) { + logger.debug("Logical_Switch [{}] already exists on {} - skipping create", logicalSwitchName, nbConnection); + return null; + } + + Insert insert = OVSDB_OPS.insert(ls) + .value(nameCol, logicalSwitchName); + if (externalIds != null && !externalIds.isEmpty()) { + @SuppressWarnings({"rawtypes", "unchecked"}) + ColumnSchema extIdsCol = ls.column("external_ids", Map.class); + insert = insert.value(extIdsCol, new HashMap<>(externalIds)); + } + List results = client.transact(schema, Collections.singletonList(insert)) + .get(timeoutMs, TimeUnit.MILLISECONDS); + assertNoError(results, String.format("create Logical_Switch %s", logicalSwitchName)); + logger.info("Created OVN Logical_Switch [{}] at {}", logicalSwitchName, nbConnection); + return null; + }); + } + + /** + * Removes a Logical_Switch by name. Idempotent: missing switch is treated as a successful no-op. + */ + public void deleteLogicalSwitch(String nbConnection, String caCertPath, String clientCertPath, + String clientPrivateKeyPath, String logicalSwitchName) { + if (StringUtils.isBlank(logicalSwitchName)) { + throw new CloudRuntimeException("Logical switch name is blank"); + } + runOn(nbConnection, caCertPath, clientCertPath, clientPrivateKeyPath, client -> { + DatabaseSchema schema = client.getSchema(NORTHBOUND_DB).get(timeoutMs, TimeUnit.MILLISECONDS); + GenericTableSchema ls = schema.table(LOGICAL_SWITCH_TABLE, GenericTableSchema.class); + ColumnSchema nameCol = ls.column("name", String.class); + + if (!logicalSwitchExists(client, schema, ls, nameCol, logicalSwitchName)) { + logger.debug("Logical_Switch [{}] not present on {} - nothing to delete", logicalSwitchName, nbConnection); + return null; + } + + Operation delete = OVSDB_OPS.delete(ls) + .where(nameCol.opEqual(logicalSwitchName)).build(); + List results = client.transact(schema, Collections.singletonList(delete)) + .get(timeoutMs, TimeUnit.MILLISECONDS); + assertNoError(results, String.format("delete Logical_Switch %s", logicalSwitchName)); + logger.info("Deleted OVN Logical_Switch [{}] at {}", logicalSwitchName, nbConnection); + return null; + }); + } + + private boolean logicalSwitchExists(OvsdbClient client, DatabaseSchema schema, + GenericTableSchema ls, ColumnSchema nameCol, + String name) throws Exception { + Operation select = OVSDB_OPS.select(ls) + .column(nameCol) + .where(nameCol.opEqual(name)).build(); + List results = client.transact(schema, Collections.singletonList(select)) + .get(timeoutMs, TimeUnit.MILLISECONDS); + if (results == null || results.isEmpty()) { + return false; + } + OperationResult r = results.get(0); + if (r.getError() != null) { + throw new CloudRuntimeException("OVSDB select failed for Logical_Switch " + name + ": " + r.getError()); + } + List> rows = r.getRows(); + return rows != null && !rows.isEmpty(); + } + + private static void assertNoError(List results, String description) { + if (results == null) { + throw new CloudRuntimeException("OVSDB transact returned no result for " + description); + } + List errors = new ArrayList<>(); + for (OperationResult r : results) { + if (r != null && r.getError() != null) { + errors.add(r.getError() + ": " + r.getDetails()); + } + } + if (!errors.isEmpty()) { + throw new CloudRuntimeException(String.format("OVSDB %s failed: %s", description, String.join("; ", errors))); + } + } + + @PreDestroy + public synchronized void shutdown() { + if (tcpConnectionService != null) { + try { tcpConnectionService.close(); } catch (Exception ignored) { } + tcpConnectionService = null; + } + if (bootstrapFactory != null) { + try { bootstrapFactory.close(); } catch (Exception ignored) { } + bootstrapFactory = null; + } + } + + @FunctionalInterface + private interface NbAction { + T call(OvsdbClient client) throws Exception; + } + + private T runOn(String nbConnection, String caCertPath, String clientCertPath, String clientPrivateKeyPath, + NbAction action) { Endpoint ep = parse(nbConnection); if (ep.scheme == Scheme.UNIX) { throw new CloudRuntimeException("Unix-socket OVN connections are not supported by the management server client; use tcp: or ssl:"); @@ -88,17 +231,11 @@ public void verifyConnection(String nbConnection, String caCertPath, String clie if (client == null) { throw new CloudRuntimeException(String.format("OVN NB at %s did not accept the connection", nbConnection)); } - client.echo().get(timeoutMs, TimeUnit.MILLISECONDS); - List dbs = client.getDatabases().get(timeoutMs, TimeUnit.MILLISECONDS); - if (dbs == null || !dbs.contains(NORTHBOUND_DB)) { - throw new CloudRuntimeException(String.format("OVN endpoint %s did not advertise %s; got %s", - nbConnection, NORTHBOUND_DB, dbs)); - } - logger.debug("OVN NB at {} reachable, databases={}", nbConnection, dbs); + return action.call(client); } catch (CloudRuntimeException e) { throw e; } catch (Exception e) { - throw new CloudRuntimeException("Cannot reach OVN NB at " + nbConnection + ": " + e.getMessage(), e); + throw new CloudRuntimeException("OVN NB operation against " + nbConnection + " failed: " + e.getMessage(), e); } finally { if (client != null && service != null) { try { service.disconnect(client); } catch (Exception ignored) { } @@ -109,18 +246,6 @@ public void verifyConnection(String nbConnection, String caCertPath, String clie } } - @PreDestroy - public synchronized void shutdown() { - if (tcpConnectionService != null) { - try { tcpConnectionService.close(); } catch (Exception ignored) { } - tcpConnectionService = null; - } - if (bootstrapFactory != null) { - try { bootstrapFactory.close(); } catch (Exception ignored) { } - bootstrapFactory = null; - } - } - private synchronized NettyBootstrapFactoryImpl bootstrapFactory() { if (bootstrapFactory == null) { bootstrapFactory = new NettyBootstrapFactoryImpl(); diff --git a/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java b/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java index 327b4eb42e52..571afa8d79ed 100644 --- a/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java +++ b/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java @@ -78,7 +78,8 @@ && isMyTrafficType(offering.getTrafficType()) && offering.getGuestType() == Network.GuestType.Isolated && isMyIsolationMethod(physicalNetwork) && _ntwkOfferingSrvcDao.areServicesSupportedByNetworkOffering( - offering.getId(), Service.Connectivity)) { + offering.getId(), Service.Connectivity) + && _ntwkOfferingSrvcDao.isProviderForNetworkOffering(offering.getId(), Network.Provider.Ovs)) { return true; } else if (networkType == NetworkType.Advanced && offering.getGuestType() == GuestType.Shared diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index b3da6af21138..b12b1b57fd65 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -120,7 +120,6 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; -import com.cloud.utils.PasswordGenerator; import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; @@ -1269,7 +1268,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl if (VirtualMachine.Type.ConsoleProxy == profile.getVirtualMachine().getType()) { buf.append(" vncport=").append(getVncPort(datacenterId)); } - buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16))); + VirtualMachineGuru.appendCertificateDetails(buf, certificate); if (SystemVmEnableUserData.valueIn(dc.getId())) { String userDataUuid = ConsoleProxyVmUserData.valueIn(dc.getId()); @@ -1285,7 +1284,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl String bootArgs = buf.toString(); if (logger.isDebugEnabled()) { - logger.debug("Boot Args for " + profile + ": " + bootArgs); + logger.debug("Boot Args for " + profile + ": " + bootArgs.replaceAll("(certificate|cacertificate|privatekey|keystore_password)=[^\\s]+", "$1=******")); } return true; diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 9d4c73111595..7cdd6b2dd51f 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -137,7 +137,6 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; -import com.cloud.utils.PasswordGenerator; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.db.QueryBuilder; @@ -1233,7 +1232,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl if (StringUtils.isNotBlank(nfsVersion)) { buf.append(" nfsVersion=").append(nfsVersion); } - buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16))); + VirtualMachineGuru.appendCertificateDetails(buf, certificate); if (SystemVmEnableUserData.valueIn(dc.getId())) { String userDataUuid = SecondaryStorageVmUserData.valueIn(dc.getId()); @@ -1249,7 +1248,8 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl String bootArgs = buf.toString(); if (logger.isDebugEnabled()) { - logger.debug(String.format("Boot args for machine profile [%s]: [%s].", profile.toString(), bootArgs)); + logger.debug(String.format("Boot args for machine profile [%s]: [%s].", profile.toString(), + bootArgs.replaceAll("(certificate|cacertificate|privatekey|keystore_password)=[^\\s]+", "$1=******"))); } boolean useHttpsToUpload = VolumeApiService.UseHttpsToUpload.valueIn(dc.getId()); diff --git a/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh b/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh index f7c071c8cc0e..a6f452fe255f 100755 --- a/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh +++ b/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh @@ -24,6 +24,7 @@ rm -f /var/cache/cloud/enabled_svcs rm -f /var/cache/cloud/disabled_svcs . /lib/lsb/init-functions +. /opt/cloud/bin/setup/common.sh log_it() { echo "$(date) $@" >> /var/log/cloud.log @@ -64,16 +65,60 @@ patch_systemvm() { echo "Restored keystore file and certs using backup" >> $logfile fi rm -fr $backupfolder + + setup_agent_keystore || return 1 + # Import global cacerts into 'cloud' service's keystore keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /usr/local/cloud/systemvm/certs/realhostip.keystore -srcstorepass changeit -deststorepass vmops.com -noprompt || true return 0 } +decode_boot_arg() { + printf '%s' "$1" | base64 -d 2>/dev/null | tr '^' '\n' | tr '~' ' ' +} + +setup_agent_keystore() { + parse_cmd_line + + if [ -z "${KEYSTORE_PSSWD// }" ] || [ -z "${CERTIFICATE// }" ] || [ -z "${CACERTIFICATE// }" ]; then + log_it "Skipping agent keystore setup as certificate boot arguments are missing" + return 0 + fi + + local propsfile="/usr/local/cloud/systemvm/conf/agent.properties" + local ksfile="/usr/local/cloud/systemvm/conf/cloud.jks" + local certfile="/usr/local/cloud/systemvm/conf/cloud.crt" + local cacertfile="/usr/local/cloud/systemvm/conf/cloud.ca.crt" + local keyfile="/usr/local/cloud/systemvm/conf/cloud.key" + local import_script="/usr/local/cloud/systemvm/scripts/util/keystore-cert-import" + local ks_pass + local cert + local cacert + local privatekey + + ks_pass=$(decode_boot_arg "$KEYSTORE_PSSWD") + cert=$(decode_boot_arg "$CERTIFICATE") + cacert=$(decode_boot_arg "$CACERTIFICATE") + if [ -n "${PRIVATEKEY// }" ]; then + privatekey=$(decode_boot_arg "$PRIVATEKEY") + fi + + sed -i "/^keystore.passphrase=/d" "$propsfile" + echo "keystore.passphrase=$ks_pass" >> "$propsfile" + + if [ ! -x "$import_script" ]; then + log_it "Unable to setup agent keystore, missing $import_script" + return 1 + fi + + "$import_script" "$propsfile" "$ks_pass" "$ksfile" "agent" "$certfile" "$cert" "$cacertfile" "$cacert" "$keyfile" "$privatekey" +} + patch() { local PATCH_MOUNT=/var/cache/cloud/ local logfile="/var/log/patchsystemvm.log" - if [ "$TYPE" == "consoleproxy" ] || [ "$TYPE" == "secstorage" ] && [ -f ${PATCH_MOUNT}/agent.zip ] && [ -f /var/cache/cloud/patch.required ] + if { [ "$TYPE" == "consoleproxy" ] || [ "$TYPE" == "secstorage" ]; } && [ -f ${PATCH_MOUNT}/agent.zip ] && [ -f /var/cache/cloud/patch.required ] then echo "Patching systemvm for cloud service with mount=$PATCH_MOUNT for type=$TYPE" >> $logfile patch_systemvm ${PATCH_MOUNT}/agent.zip diff --git a/systemvm/debian/opt/cloud/bin/setup/cloud-early-config b/systemvm/debian/opt/cloud/bin/setup/cloud-early-config index ee1e872f627c..8edeb6f896fa 100755 --- a/systemvm/debian/opt/cloud/bin/setup/cloud-early-config +++ b/systemvm/debian/opt/cloud/bin/setup/cloud-early-config @@ -109,6 +109,8 @@ cleanup() { start() { log_it "Executing cloud-early-config" + exec 9>/var/lock/cloud-early-config.lock + flock 9 # Clear /tmp for file lock rm -f /tmp/*.lock diff --git a/systemvm/debian/opt/cloud/bin/setup/common.sh b/systemvm/debian/opt/cloud/bin/setup/common.sh index ef1576ab588c..7484cf8de2b0 100755 --- a/systemvm/debian/opt/cloud/bin/setup/common.sh +++ b/systemvm/debian/opt/cloud/bin/setup/common.sh @@ -730,9 +730,9 @@ parse_cmd_line() { for i in $CMDLINE do - # search for foo=bar pattern and cut out foo + # Search for foo=bar and preserve any additional '=' in encoded values. KEY=$(echo $i | cut -d= -f1) - VALUE=$(echo $i | cut -d= -f2) + VALUE=$(echo $i | cut -d= -f2-) echo -en ${COMMA} >> ${CHEF_TMP_FILE} # Two lines so values do not accidentally interpretted as escapes!! echo -n \"${KEY}\"': '\"${VALUE}\" >> ${CHEF_TMP_FILE} From 818cf6d09a02797bd2b562b0b68b187f4bf271d0 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Mon, 27 Apr 2026 16:44:20 +0200 Subject: [PATCH 06/33] OVN plugin: expose provider in UI and configuration --- .../network/CreatePhysicalNetworkCmd.java | 2 +- .../orchestration/NetworkOrchestrator.java | 2 +- tools/apidoc/gen_toc.py | 1 + ui/public/locales/en.json | 6 +++ ui/src/config/section/infra/phynetworks.js | 2 +- .../infra/network/ServiceProvidersTab.vue | 44 +++++++++++++++++++ .../views/infra/zone/PhysicalNetworksTab.vue | 2 +- .../ZoneWizardPhysicalNetworkSetupStep.vue | 1 + 8 files changed, 56 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java index 097b8a5b5458..cf913fd2fb46 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java @@ -75,7 +75,7 @@ public class CreatePhysicalNetworkCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.ISOLATION_METHODS, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "The isolation method for the physical Network[VLAN/VXLAN/GRE/STT/BCF_SEGMENT/SSP/ODL/L3VPN/VCS/NSX/NETRIS]") + description = "The isolation method for the physical Network[VLAN/VXLAN/GRE/STT/BCF_SEGMENT/SSP/ODL/L3VPN/VCS/NSX/NETRIS/OVN]") private List isolationMethods; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the physical Network") diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 7d455e7d6dc9..224357dca655 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -4939,7 +4939,7 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes, GuestDomainSuffix, NetworkThrottlingRate, MinVRVersion, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, - TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, + TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, OVN_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, NETWORK_LB_HAPROXY_IDLE_TIMEOUT}; } } diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 292f52d809bf..bf30f473b4f2 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -99,6 +99,7 @@ 'addNsxController': 'NSX', 'deleteNsxController': 'NSX', 'NetrisProvider': 'Netris', + 'OvnProvider': 'OVN', 'Vpn': 'VPN', 'Limit': 'Resource Limit', 'Netscaler': 'Netscaler', diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4a23b454252a..27acf657aa81 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1787,6 +1787,12 @@ "label.nsx.provider.transportzone": "NSX provider transport Zone", "label.nsx.supports.internal.lb": "Enable NSX internal LB service", "label.nsx.supports.lb": "Enable NSX LB service", +"label.ovn": "OVN", +"label.ovn.provider": "OVN Provider", +"label.ovnnbconnection": "OVN Northbound connection", +"label.ovnsbconnection": "OVN Southbound connection", +"label.ovnexternalbridge": "OVN external bridge", +"label.ovnlocalnetname": "OVN localnet name", "label.num.cpu.cores": "# of CPU cores", "label.numanode": "NUMA node", "label.number": "#Rule", diff --git a/ui/src/config/section/infra/phynetworks.js b/ui/src/config/section/infra/phynetworks.js index 0863eff6ec0b..b0dcb61eb30d 100644 --- a/ui/src/config/section/infra/phynetworks.js +++ b/ui/src/config/section/infra/phynetworks.js @@ -57,7 +57,7 @@ export default { args: ['name', 'zoneid', 'isolationmethods', 'vlan', 'tags', 'networkspeed', 'broadcastdomainrange'], mapping: { isolationmethods: { - options: ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS', 'NSX', 'NETRIS'] + options: ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS', 'NSX', 'NETRIS', 'OVN'] } } }, diff --git a/ui/src/views/infra/network/ServiceProvidersTab.vue b/ui/src/views/infra/network/ServiceProvidersTab.vue index f659ce1f0167..bc48793a275c 100644 --- a/ui/src/views/infra/network/ServiceProvidersTab.vue +++ b/ui/src/views/infra/network/ServiceProvidersTab.vue @@ -1113,6 +1113,50 @@ export default { columns: ['name', 'netrisurl', 'site', 'tenantname', 'netristag'] } ] + }, + { + title: 'Ovn', + details: ['name', 'state', 'id', 'physicalnetworkid', 'servicelist'], + actions: [ + { + api: 'updateNetworkServiceProvider', + icon: 'stop-outlined', + listView: true, + label: 'label.disable.provider', + confirm: 'message.confirm.disable.provider', + show: (record) => { return (record && record.id && record.state === 'Enabled') }, + mapping: { + state: { + value: (record) => { return 'Disabled' } + } + } + }, + { + api: 'updateNetworkServiceProvider', + icon: 'play-circle-outlined', + listView: true, + label: 'label.enable.provider', + confirm: 'message.confirm.enable.provider', + show: (record) => { return (record && record.id && record.state === 'Disabled') }, + mapping: { + state: { + value: (record) => { return 'Enabled' } + } + } + } + ], + lists: [ + { + title: 'label.ovn.provider', + api: 'listOvnProviders', + mapping: { + zoneid: { + value: (record) => { return record.zoneid } + } + }, + columns: ['name', 'ovnnbconnection', 'ovnsbconnection', 'ovnexternalbridge', 'ovnlocalnetname'] + } + ] } ] } diff --git a/ui/src/views/infra/zone/PhysicalNetworksTab.vue b/ui/src/views/infra/zone/PhysicalNetworksTab.vue index cccb8719805f..387a7ea8bf98 100644 --- a/ui/src/views/infra/zone/PhysicalNetworksTab.vue +++ b/ui/src/views/infra/zone/PhysicalNetworksTab.vue @@ -200,7 +200,7 @@ export default { }, computed: { isolationMethods () { - return ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS', 'NSX', 'NETRIS'] + return ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS', 'NSX', 'NETRIS', 'OVN'] } }, methods: { diff --git a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue index 88f681de3796..9e5df5e3baa6 100644 --- a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue +++ b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue @@ -68,6 +68,7 @@ TF NSX NETRIS + OVN