From 9e46c65d48157ff00ee5753ad83cc05a0d9ff50f Mon Sep 17 00:00:00 2001 From: Dogface2k <100990646+Dogface2k@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:30:04 +0100 Subject: [PATCH 1/2] NSX: support segment discovery and security profiles --- .../com/cloud/offering/NetworkOffering.java | 4 +- .../agent/api/CreateNsxSegmentCommand.java | 26 ++++++ .../cloudstack/resource/NsxResource.java | 3 +- .../cloudstack/service/NsxApiClient.java | 82 ++++++++++++++++- .../service/NsxGuestNetworkGuru.java | 12 ++- .../apache/cloudstack/utils/NsxHelper.java | 8 +- .../cloudstack/resource/NsxResourceTest.java | 19 ++++ .../cloudstack/service/NsxApiClientTest.java | 91 +++++++++++++++++++ .../service/NsxGuestNetworkGuruTest.java | 25 +++++ .../ConfigurationManagerImpl.java | 20 ++++ .../ConfigurationManagerImplTest.java | 44 +++++++++ ui/public/locales/en.json | 6 ++ ui/src/views/offering/AddNetworkOffering.vue | 38 +++++++- .../views/offering/CloneNetworkOffering.vue | 54 ++++++++++- 14 files changed, 425 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/com/cloud/offering/NetworkOffering.java b/api/src/main/java/com/cloud/offering/NetworkOffering.java index 5000a4f8c626..89463916b93e 100644 --- a/api/src/main/java/com/cloud/offering/NetworkOffering.java +++ b/api/src/main/java/com/cloud/offering/NetworkOffering.java @@ -40,7 +40,9 @@ public enum State { } public enum Detail { - InternalLbProvider, PublicLbProvider, servicepackageuuid, servicepackagedescription, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RelatedNetworkOffering, domainid, zoneid, pvlanType, internetProtocol + InternalLbProvider, PublicLbProvider, servicepackageuuid, servicepackagedescription, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, + NsxIpDiscoveryProfileId, NsxMacDiscoveryProfileId, NsxSegmentSecurityProfileId, + RelatedNetworkOffering, domainid, zoneid, pvlanType, internetProtocol } public enum NetworkMode { diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/CreateNsxSegmentCommand.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/CreateNsxSegmentCommand.java index b4b86bd640a6..e03fb1751190 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/CreateNsxSegmentCommand.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/CreateNsxSegmentCommand.java @@ -26,10 +26,21 @@ public class CreateNsxSegmentCommand extends NsxCommand { private String networkName; private String networkGateway; private String networkCidr; + private String ipDiscoveryProfileId; + private String macDiscoveryProfileId; + private String segmentSecurityProfileId; public CreateNsxSegmentCommand(long domainId, long accountId, long zoneId, Long vpcId, String vpcName, long networkId, String networkName, String networkGateway, String networkCidr) { + this(domainId, accountId, zoneId, vpcId, vpcName, networkId, networkName, networkGateway, + networkCidr, null, null, null); + } + + public CreateNsxSegmentCommand(long domainId, long accountId, long zoneId, + Long vpcId, String vpcName, long networkId, String networkName, + String networkGateway, String networkCidr, String ipDiscoveryProfileId, + String macDiscoveryProfileId, String segmentSecurityProfileId) { super(domainId, accountId, zoneId); this.vpcId = vpcId; this.vpcName = vpcName; @@ -37,6 +48,9 @@ public CreateNsxSegmentCommand(long domainId, long accountId, long zoneId, this.networkName = networkName; this.networkGateway = networkGateway; this.networkCidr = networkCidr; + this.ipDiscoveryProfileId = ipDiscoveryProfileId; + this.macDiscoveryProfileId = macDiscoveryProfileId; + this.segmentSecurityProfileId = segmentSecurityProfileId; } public Long getVpcId() { @@ -63,6 +77,18 @@ public String getNetworkCidr() { return networkCidr; } + public String getIpDiscoveryProfileId() { + return ipDiscoveryProfileId; + } + + public String getMacDiscoveryProfileId() { + return macDiscoveryProfileId; + } + + public String getSegmentSecurityProfileId() { + return segmentSecurityProfileId; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java index 78a9363a5e49..c14e9483c801 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java @@ -351,7 +351,8 @@ private Answer executeRequest(CreateNsxSegmentCommand cmd) { boolean isResourceVpc = !Objects.isNull(cmd.getVpcId()); String tier1GatewayName = NsxControllerUtils.getTier1GatewayName(cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), networkResourceId, isResourceVpc); - nsxApiClient.createSegment(segmentName, tier1GatewayName, gatewayAddress, enforcementPointPath, transportZones); + nsxApiClient.createSegment(segmentName, tier1GatewayName, gatewayAddress, enforcementPointPath, transportZones, + cmd.getIpDiscoveryProfileId(), cmd.getMacDiscoveryProfileId(), cmd.getSegmentSecurityProfileId()); nsxApiClient.createGroupForSegment(segmentName); } catch (Exception e) { logger.error(String.format("Failed to create network: %s", cmd.getNetworkName())); diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxApiClient.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxApiClient.java index 4d78f2a0ab26..d11d74de1633 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxApiClient.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxApiClient.java @@ -26,11 +26,14 @@ import com.vmware.nsx.model.TransportZone; import com.vmware.nsx.model.TransportZoneListResult; import com.vmware.nsx_policy.infra.DhcpRelayConfigs; +import com.vmware.nsx_policy.infra.IpDiscoveryProfiles; import com.vmware.nsx_policy.infra.LbAppProfiles; import com.vmware.nsx_policy.infra.LbMonitorProfiles; import com.vmware.nsx_policy.infra.LbPools; import com.vmware.nsx_policy.infra.LbServices; import com.vmware.nsx_policy.infra.LbVirtualServers; +import com.vmware.nsx_policy.infra.MacDiscoveryProfiles; +import com.vmware.nsx_policy.infra.SegmentSecurityProfiles; import com.vmware.nsx_policy.infra.Segments; import com.vmware.nsx_policy.infra.Services; import com.vmware.nsx_policy.infra.Sites; @@ -39,6 +42,8 @@ import com.vmware.nsx_policy.infra.domains.SecurityPolicies; import com.vmware.nsx_policy.infra.domains.groups.members.SegmentPorts; import com.vmware.nsx_policy.infra.domains.security_policies.Rules; +import com.vmware.nsx_policy.infra.segments.SegmentDiscoveryProfileBindingMaps; +import com.vmware.nsx_policy.infra.segments.SegmentSecurityProfileBindingMaps; import com.vmware.nsx_policy.infra.sites.EnforcementPoints; import com.vmware.nsx_policy.infra.tier_0s.LocaleServices; import com.vmware.nsx_policy.infra.tier_1s.nat.NatRules; @@ -49,6 +54,7 @@ import com.vmware.nsx_policy.model.Group; import com.vmware.nsx_policy.model.GroupListResult; import com.vmware.nsx_policy.model.ICMPTypeServiceEntry; +import com.vmware.nsx_policy.model.IPDiscoveryProfile; import com.vmware.nsx_policy.model.L4PortSetServiceEntry; import com.vmware.nsx_policy.model.LBAppProfileListResult; import com.vmware.nsx_policy.model.LBIcmpMonitorProfile; @@ -61,6 +67,7 @@ import com.vmware.nsx_policy.model.LBVirtualServer; import com.vmware.nsx_policy.model.LBVirtualServerListResult; import com.vmware.nsx_policy.model.LocaleServicesListResult; +import com.vmware.nsx_policy.model.MacDiscoveryProfile; import com.vmware.nsx_policy.model.PathExpression; import com.vmware.nsx_policy.model.PolicyGroupMembersListResult; import com.vmware.nsx_policy.model.PolicyNatRule; @@ -69,6 +76,9 @@ import com.vmware.nsx_policy.model.Rule; import com.vmware.nsx_policy.model.SecurityPolicy; import com.vmware.nsx_policy.model.Segment; +import com.vmware.nsx_policy.model.SegmentDiscoveryProfileBindingMap; +import com.vmware.nsx_policy.model.SegmentSecurityProfile; +import com.vmware.nsx_policy.model.SegmentSecurityProfileBindingMap; import com.vmware.nsx_policy.model.SegmentSubnet; import com.vmware.nsx_policy.model.ServiceListResult; import com.vmware.nsx_policy.model.Site; @@ -90,9 +100,10 @@ import org.apache.cloudstack.resource.NsxNetworkRule; import org.apache.cloudstack.utils.NsxControllerUtils; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.commons.lang3.BooleanUtils; import java.util.ArrayList; import java.util.List; @@ -128,6 +139,8 @@ public class NsxApiClient { private static final String TIER_1_RESOURCE_TYPE = "Tier1"; private static final String TIER_1_LOCALE_SERVICE_ID = "default"; private static final String SEGMENT_RESOURCE_TYPE = "Segment"; + private static final String SEGMENT_DISCOVERY_PROFILE_BINDING_ID = "cloudstack-discovery-profile-binding"; + private static final String SEGMENT_SECURITY_PROFILE_BINDING_ID = "cloudstack-security-profile-binding"; private static final String TIER_0_GATEWAY_PATH_PREFIX = "/infra/tier-0s/"; private static final String TIER_1_GATEWAY_PATH_PREFIX = "/infra/tier-1s/"; protected static final String SEGMENTS_PATH = "/infra/segments"; @@ -466,7 +479,16 @@ public TransportZoneListResult getTransportZones() { public void createSegment(String segmentName, String tier1GatewayName, String gatewayAddress, String enforcementPointPath, List transportZones) { + createSegment(segmentName, tier1GatewayName, gatewayAddress, enforcementPointPath, transportZones, null, null, null); + } + + public void createSegment(String segmentName, String tier1GatewayName, String gatewayAddress, String enforcementPointPath, + List transportZones, String ipDiscoveryProfileId, String macDiscoveryProfileId, + String segmentSecurityProfileId) { try { + String ipDiscoveryProfilePath = getIpDiscoveryProfilePath(ipDiscoveryProfileId); + String macDiscoveryProfilePath = getMacDiscoveryProfilePath(macDiscoveryProfileId); + String segmentSecurityProfilePath = getSegmentSecurityProfilePath(segmentSecurityProfileId); Segments segmentService = (Segments) nsxService.apply(Segments.class); SegmentSubnet subnet = new SegmentSubnet.Builder() .setGatewayAddress(gatewayAddress) @@ -481,6 +503,7 @@ public void createSegment(String segmentName, String tier1GatewayName, String ga .setTransportZonePath(enforcementPointPath + "/transport-zones/" + transportZones.get(0).getId()) .build(); segmentService.patch(segmentName, segment); + bindSegmentProfiles(segmentName, ipDiscoveryProfilePath, macDiscoveryProfilePath, segmentSecurityProfilePath); } catch (Error error) { ApiError ae = error.getData()._convertTo(ApiError.class); String msg = String.format("Error creating segment %s: %s", segmentName, ae.getErrorMessage()); @@ -489,6 +512,63 @@ public void createSegment(String segmentName, String tier1GatewayName, String ga } } + protected String getIpDiscoveryProfilePath(String profileId) { + if (StringUtils.isBlank(profileId)) { + return null; + } + IpDiscoveryProfiles profiles = (IpDiscoveryProfiles) nsxService.apply(IpDiscoveryProfiles.class); + IPDiscoveryProfile profile = profiles.get(profileId); + return validateProfilePath(profileId, profile.getPath()); + } + + protected String getMacDiscoveryProfilePath(String profileId) { + if (StringUtils.isBlank(profileId)) { + return null; + } + MacDiscoveryProfiles profiles = (MacDiscoveryProfiles) nsxService.apply(MacDiscoveryProfiles.class); + MacDiscoveryProfile profile = profiles.get(profileId); + return validateProfilePath(profileId, profile.getPath()); + } + + protected String getSegmentSecurityProfilePath(String profileId) { + if (StringUtils.isBlank(profileId)) { + return null; + } + SegmentSecurityProfiles profiles = (SegmentSecurityProfiles) nsxService.apply(SegmentSecurityProfiles.class); + SegmentSecurityProfile profile = profiles.get(profileId); + return validateProfilePath(profileId, profile.getPath()); + } + + protected String validateProfilePath(String profileId, String profilePath) { + if (StringUtils.isBlank(profilePath)) { + throw new CloudRuntimeException(String.format("NSX profile %s did not return a canonical resource path", profileId)); + } + return profilePath; + } + + protected void bindSegmentProfiles(String segmentName, String ipDiscoveryProfilePath, String macDiscoveryProfilePath, + String segmentSecurityProfilePath) { + if (StringUtils.isNotBlank(ipDiscoveryProfilePath) || StringUtils.isNotBlank(macDiscoveryProfilePath)) { + SegmentDiscoveryProfileBindingMaps discoveryBindings = + (SegmentDiscoveryProfileBindingMaps) nsxService.apply(SegmentDiscoveryProfileBindingMaps.class); + SegmentDiscoveryProfileBindingMap binding = new SegmentDiscoveryProfileBindingMap.Builder() + .setId(SEGMENT_DISCOVERY_PROFILE_BINDING_ID) + .setIpDiscoveryProfilePath(ipDiscoveryProfilePath) + .setMacDiscoveryProfilePath(macDiscoveryProfilePath) + .build(); + discoveryBindings.patch(segmentName, SEGMENT_DISCOVERY_PROFILE_BINDING_ID, binding); + } + if (StringUtils.isNotBlank(segmentSecurityProfilePath)) { + SegmentSecurityProfileBindingMaps securityBindings = + (SegmentSecurityProfileBindingMaps) nsxService.apply(SegmentSecurityProfileBindingMaps.class); + SegmentSecurityProfileBindingMap binding = new SegmentSecurityProfileBindingMap.Builder() + .setId(SEGMENT_SECURITY_PROFILE_BINDING_ID) + .setSegmentSecurityProfilePath(segmentSecurityProfilePath) + .build(); + securityBindings.patch(segmentName, SEGMENT_SECURITY_PROFILE_BINDING_ID, binding); + } + } + public void deleteSegment(long zoneId, long domainId, long accountId, Long vpcId, long networkId, String segmentName) { try { removeSegmentDistributedFirewallRules(segmentName); diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxGuestNetworkGuru.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxGuestNetworkGuru.java index 0f7865d7d73f..6d1c717e90c4 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxGuestNetworkGuru.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxGuestNetworkGuru.java @@ -59,6 +59,7 @@ import javax.inject.Inject; import java.util.List; +import java.util.Map; import java.util.Objects; public class NsxGuestNetworkGuru extends GuestNetworkGuru implements NetworkMigrationResponder { @@ -324,10 +325,19 @@ public void createNsxSegment(NetworkVO networkVO, DataCenter zone) { throw new CloudRuntimeException(msg); } } - CreateNsxSegmentCommand command = NsxHelper.createNsxSegmentCommand(domain, account, zone, vpcName, networkVO); + Map offeringDetails = _networkModel.getNtwkOffDetails(networkVO.getNetworkOfferingId()); + String ipDiscoveryProfile = getOfferingDetail(offeringDetails, NetworkOffering.Detail.NsxIpDiscoveryProfileId); + String macDiscoveryProfile = getOfferingDetail(offeringDetails, NetworkOffering.Detail.NsxMacDiscoveryProfileId); + String segmentSecurityProfile = getOfferingDetail(offeringDetails, NetworkOffering.Detail.NsxSegmentSecurityProfileId); + CreateNsxSegmentCommand command = NsxHelper.createNsxSegmentCommand(domain, account, zone, vpcName, networkVO, + ipDiscoveryProfile, macDiscoveryProfile, segmentSecurityProfile); NsxAnswer answer = nsxControllerUtils.sendNsxCommand(command, zone.getId()); if (!answer.getResult()) { throw new CloudRuntimeException("can not create NSX network"); } } + + protected String getOfferingDetail(Map offeringDetails, NetworkOffering.Detail detail) { + return offeringDetails == null ? null : offeringDetails.get(detail); + } } diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/utils/NsxHelper.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/utils/NsxHelper.java index b0668a0704f9..a4986ab44c69 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/utils/NsxHelper.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/utils/NsxHelper.java @@ -41,8 +41,14 @@ public static CreateNsxDhcpRelayConfigCommand createNsxDhcpRelayConfigCommand(Do } public static CreateNsxSegmentCommand createNsxSegmentCommand(DomainVO domain, Account account, DataCenter zone, String vpcName, NetworkVO networkVO) { + return createNsxSegmentCommand(domain, account, zone, vpcName, networkVO, null, null, null); + } + + public static CreateNsxSegmentCommand createNsxSegmentCommand(DomainVO domain, Account account, DataCenter zone, String vpcName, NetworkVO networkVO, + String ipDiscoveryProfileId, String macDiscoveryProfileId, String segmentSecurityProfileId) { return new CreateNsxSegmentCommand(domain.getId(), account.getId(), zone.getId(), - networkVO.getVpcId(), vpcName, networkVO.getId(), networkVO.getName(), networkVO.getGateway(), networkVO.getCidr()); + networkVO.getVpcId(), vpcName, networkVO.getId(), networkVO.getName(), networkVO.getGateway(), networkVO.getCidr(), + ipDiscoveryProfileId, macDiscoveryProfileId, segmentSecurityProfileId); } public static CreateOrUpdateNsxTier1NatRuleCommand createOrUpdateNsxNatRuleCommand(long domainId, long accountId, long zoneId, diff --git a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java index 0d74bb8a3b3d..21cb60fe9c29 100644 --- a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java +++ b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java @@ -60,6 +60,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -184,6 +185,24 @@ public void testCreateNsxSegment() { assertTrue(answer.getResult()); } + @Test + public void testCreateNsxSegmentPassesProfileIdsToApiClient() { + List transportZoneList = List.of(new TransportZone.Builder().setDisplayName(transportZone).build()); + CreateNsxSegmentCommand command = new CreateNsxSegmentCommand(domainId, accountId, zoneId, + 2L, "VPC01", 3L, "Web", "10.10.10.1", "10.10.10.0/24", + "ip-profile", "mac-profile", "security-profile"); + when(nsxApi.getDefaultSiteId()).thenReturn("site1"); + when(nsxApi.getDefaultEnforcementPointPath("site1")).thenReturn("enforcementPointPath"); + when(nsxApi.getTransportZones()).thenReturn(transportZoneListResult); + when(transportZoneListResult.getResults()).thenReturn(transportZoneList); + + NsxAnswer answer = (NsxAnswer) nsxResource.executeRequest(command); + + assertTrue(answer.getResult()); + verify(nsxApi).createSegment(anyString(), anyString(), anyString(), eq("enforcementPointPath"), + eq(transportZoneList), eq("ip-profile"), eq("mac-profile"), eq("security-profile")); + } + @Test public void testCreateNsxSegmentEmptySites() { when(nsxApi.getDefaultSiteId()).thenReturn(null); diff --git a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxApiClientTest.java b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxApiClientTest.java index 5f8d771f75df..f6f82f76dea5 100644 --- a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxApiClientTest.java +++ b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxApiClientTest.java @@ -22,14 +22,21 @@ import com.vmware.nsx.cluster.Status; import com.vmware.nsx.model.ClusterStatus; import com.vmware.nsx.model.ControllerClusterStatus; +import com.vmware.nsx_policy.infra.IpDiscoveryProfiles; import com.vmware.nsx_policy.infra.LbAppProfiles; import com.vmware.nsx_policy.infra.LbMonitorProfiles; import com.vmware.nsx_policy.infra.LbPools; import com.vmware.nsx_policy.infra.LbServices; import com.vmware.nsx_policy.infra.LbVirtualServers; +import com.vmware.nsx_policy.infra.MacDiscoveryProfiles; +import com.vmware.nsx_policy.infra.SegmentSecurityProfiles; +import com.vmware.nsx_policy.infra.Segments; import com.vmware.nsx_policy.infra.domains.Groups; +import com.vmware.nsx_policy.infra.segments.SegmentDiscoveryProfileBindingMaps; +import com.vmware.nsx_policy.infra.segments.SegmentSecurityProfileBindingMaps; import com.vmware.nsx_policy.model.ApiError; import com.vmware.nsx_policy.model.Group; +import com.vmware.nsx_policy.model.IPDiscoveryProfile; import com.vmware.nsx_policy.model.LBAppProfileListResult; import com.vmware.nsx_policy.model.LBIcmpMonitorProfile; import com.vmware.nsx_policy.model.LBService; @@ -37,7 +44,12 @@ import com.vmware.nsx_policy.model.LBPool; import com.vmware.nsx_policy.model.LBPoolMember; import com.vmware.nsx_policy.model.LBVirtualServer; +import com.vmware.nsx_policy.model.MacDiscoveryProfile; import com.vmware.nsx_policy.model.PathExpression; +import com.vmware.nsx_policy.model.Segment; +import com.vmware.nsx_policy.model.SegmentDiscoveryProfileBindingMap; +import com.vmware.nsx_policy.model.SegmentSecurityProfile; +import com.vmware.nsx_policy.model.SegmentSecurityProfileBindingMap; import com.vmware.vapi.bindings.Service; import com.vmware.vapi.bindings.Structure; import com.vmware.vapi.std.errors.Error; @@ -47,6 +59,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -102,6 +115,84 @@ public void testCreateGroupForSegment() { } } + @Test + public void testCreateSegmentBindsConfiguredProfiles() { + Segments segmentService = Mockito.mock(Segments.class); + IpDiscoveryProfiles ipProfiles = Mockito.mock(IpDiscoveryProfiles.class); + MacDiscoveryProfiles macProfiles = Mockito.mock(MacDiscoveryProfiles.class); + SegmentSecurityProfiles securityProfiles = Mockito.mock(SegmentSecurityProfiles.class); + SegmentDiscoveryProfileBindingMaps discoveryBindings = Mockito.mock(SegmentDiscoveryProfileBindingMaps.class); + SegmentSecurityProfileBindingMaps securityBindings = Mockito.mock(SegmentSecurityProfileBindingMaps.class); + IPDiscoveryProfile ipProfile = Mockito.mock(IPDiscoveryProfile.class); + MacDiscoveryProfile macProfile = Mockito.mock(MacDiscoveryProfile.class); + SegmentSecurityProfile securityProfile = Mockito.mock(SegmentSecurityProfile.class); + when(nsxService.apply(Segments.class)).thenReturn(segmentService); + when(nsxService.apply(IpDiscoveryProfiles.class)).thenReturn(ipProfiles); + when(nsxService.apply(MacDiscoveryProfiles.class)).thenReturn(macProfiles); + when(nsxService.apply(SegmentSecurityProfiles.class)).thenReturn(securityProfiles); + when(nsxService.apply(SegmentDiscoveryProfileBindingMaps.class)).thenReturn(discoveryBindings); + when(nsxService.apply(SegmentSecurityProfileBindingMaps.class)).thenReturn(securityBindings); + when(ipProfiles.get("ip-profile")).thenReturn(ipProfile); + when(macProfiles.get("mac-profile")).thenReturn(macProfile); + when(securityProfiles.get("security-profile")).thenReturn(securityProfile); + when(ipProfile.getPath()).thenReturn("/infra/ip-discovery-profiles/ip-profile"); + when(macProfile.getPath()).thenReturn("/infra/mac-discovery-profiles/mac-profile"); + when(securityProfile.getPath()).thenReturn("/infra/segment-security-profiles/security-profile"); + ArgumentCaptor discoveryCaptor = + ArgumentCaptor.forClass(SegmentDiscoveryProfileBindingMap.class); + ArgumentCaptor securityCaptor = + ArgumentCaptor.forClass(SegmentSecurityProfileBindingMap.class); + + client.createSegment("segment", "tier1", "10.10.10.1/24", "/infra/sites/default/enforcement-points/default", + List.of(new com.vmware.nsx.model.TransportZone.Builder().setId("tz").build()), + "ip-profile", "mac-profile", "security-profile"); + + verify(segmentService).patch(eq("segment"), any(Segment.class)); + verify(discoveryBindings).patch(eq("segment"), anyString(), discoveryCaptor.capture()); + verify(securityBindings).patch(eq("segment"), anyString(), securityCaptor.capture()); + Assert.assertEquals("/infra/ip-discovery-profiles/ip-profile", discoveryCaptor.getValue().getIpDiscoveryProfilePath()); + Assert.assertEquals("/infra/mac-discovery-profiles/mac-profile", discoveryCaptor.getValue().getMacDiscoveryProfilePath()); + Assert.assertEquals("/infra/segment-security-profiles/security-profile", + securityCaptor.getValue().getSegmentSecurityProfilePath()); + } + + @Test + public void testCreateSegmentWithoutProfilesPreservesExistingBehavior() { + Segments segmentService = Mockito.mock(Segments.class); + when(nsxService.apply(Segments.class)).thenReturn(segmentService); + + client.createSegment("segment", "tier1", "10.10.10.1/24", "/infra/sites/default/enforcement-points/default", + List.of(new com.vmware.nsx.model.TransportZone.Builder().setId("tz").build())); + + verify(segmentService).patch(eq("segment"), any(Segment.class)); + verify(nsxService, never()).apply(IpDiscoveryProfiles.class); + verify(nsxService, never()).apply(MacDiscoveryProfiles.class); + verify(nsxService, never()).apply(SegmentSecurityProfiles.class); + verify(nsxService, never()).apply(SegmentDiscoveryProfileBindingMaps.class); + verify(nsxService, never()).apply(SegmentSecurityProfileBindingMaps.class); + } + + @Test + public void testCreateSegmentRejectsProfileWithoutCanonicalPathBeforeCreatingSegment() { + Segments segmentService = Mockito.mock(Segments.class); + IpDiscoveryProfiles ipProfiles = Mockito.mock(IpDiscoveryProfiles.class); + IPDiscoveryProfile ipProfile = Mockito.mock(IPDiscoveryProfile.class); + when(nsxService.apply(Segments.class)).thenReturn(segmentService); + when(nsxService.apply(IpDiscoveryProfiles.class)).thenReturn(ipProfiles); + when(ipProfiles.get("ip-profile")).thenReturn(ipProfile); + when(ipProfile.getPath()).thenReturn(" "); + + Assert.assertThrows(CloudRuntimeException.class, + () -> client.createSegment("segment", "tier1", "10.10.10.1/24", + "/infra/sites/default/enforcement-points/default", + List.of(new com.vmware.nsx.model.TransportZone.Builder().setId("tz").build()), + "ip-profile", null, null)); + + verify(segmentService, never()).patch(anyString(), any(Segment.class)); + verify(nsxService, never()).apply(SegmentDiscoveryProfileBindingMaps.class); + verify(nsxService, never()).apply(SegmentSecurityProfileBindingMaps.class); + } + @Test public void testGetGroupsForTrafficIngress() { SDNProviderNetworkRule rule = Mockito.mock(SDNProviderNetworkRule.class); diff --git a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxGuestNetworkGuruTest.java b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxGuestNetworkGuruTest.java index cb79873f364d..251e5214e43f 100644 --- a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxGuestNetworkGuruTest.java +++ b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/service/NsxGuestNetworkGuruTest.java @@ -58,6 +58,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Mockito; @@ -66,6 +67,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; @@ -304,6 +306,29 @@ public void testCreateNsxSegmentForVpc() { anyLong()); } + @Test + public void testCreateNsxSegmentPassesNetworkOfferingProfiles() { + NetworkVO networkVO = Mockito.mock(NetworkVO.class); + DataCenter dataCenter = Mockito.mock(DataCenter.class); + when(networkVO.getAccountId()).thenReturn(1L); + when(networkVO.getNetworkOfferingId()).thenReturn(42L); + when(networkModel.getNtwkOffDetails(42L)).thenReturn(Map.of( + NetworkOffering.Detail.NsxIpDiscoveryProfileId, "ip-profile", + NetworkOffering.Detail.NsxMacDiscoveryProfileId, "mac-profile", + NetworkOffering.Detail.NsxSegmentSecurityProfileId, "security-profile")); + when(nsxControllerUtils.sendNsxCommand(any(CreateNsxSegmentCommand.class), anyLong())) + .thenReturn(new NsxAnswer(new NsxCommand(), true, "")); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(CreateNsxSegmentCommand.class); + + guru.createNsxSegment(networkVO, dataCenter); + + verify(nsxControllerUtils).sendNsxCommand(commandCaptor.capture(), anyLong()); + CreateNsxSegmentCommand command = commandCaptor.getValue(); + assertEquals("ip-profile", command.getIpDiscoveryProfileId()); + assertEquals("mac-profile", command.getMacDiscoveryProfileId()); + assertEquals("security-profile", command.getSegmentSecurityProfileId()); + } + @Test public void testCreateNsxSegmentForIsolatedNetwork() { diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 837254ed8b36..ae03615cc78d 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -572,6 +572,9 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati private static final String DefaultVlanForPodIpRange = Vlan.UNTAGGED; private static final Set VPC_ONLY_PROVIDERS = Sets.newHashSet(Provider.VPCVirtualRouter, Provider.JuniperContrailVpcRouter, Provider.InternalLbVm); + private static final Set NSX_SEGMENT_PROFILE_DETAILS = Set.of(Detail.NsxIpDiscoveryProfileId, + Detail.NsxMacDiscoveryProfileId, Detail.NsxSegmentSecurityProfileId); + private static final int MAX_NSX_PROFILE_ID_LENGTH = 255; private static final List SUPPORTED_ROUTING_MODE_STRS = Arrays.asList(Static.toString().toLowerCase(), Dynamic.toString().toLowerCase()); private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; @@ -7987,8 +7990,25 @@ boolean isSharedSourceNat(Map> serviceProviderMap, Map details, final Map> serviceProviderMap) { + boolean nsxProviderConfigured = serviceProviderMap != null && serviceProviderMap.values().stream() + .filter(Objects::nonNull) + .anyMatch(providers -> providers.contains(Provider.Nsx)); for (final Detail detail : details.keySet()) { + if (NSX_SEGMENT_PROFILE_DETAILS.contains(detail)) { + String profileId = details.get(detail); + if (!nsxProviderConfigured) { + throw new InvalidParameterValueException(String.format("Detail %s is supported only by NSX network offerings", detail)); + } + if (StringUtils.isBlank(profileId)) { + throw new InvalidParameterValueException(String.format("A non-empty NSX profile ID is required for detail %s", detail)); + } + if (profileId.length() > MAX_NSX_PROFILE_ID_LENGTH) { + throw new InvalidParameterValueException(String.format("NSX profile ID for detail %s cannot exceed %d characters", + detail, MAX_NSX_PROFILE_ID_LENGTH)); + } + } + Provider lbProvider = null; if (detail == NetworkOffering.Detail.InternalLbProvider || detail == NetworkOffering.Detail.PublicLbProvider) { // 1) Vaidate the detail values - have to match the lb provider diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 9a0b150780e4..649f96021e51 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -1410,4 +1410,48 @@ public void testGetExternalNetworkProviderReturnsNullWhenNoExternalProviders() { mapWithEmptySet.put(Network.Service.Firewall, Collections.emptySet()); Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, mapWithEmptySet)); } + + @Test + public void testValidateNetworkOfferingDetailsAcceptsNsxSegmentProfilesForNsxOffering() { + Map details = Map.of( + NetworkOffering.Detail.NsxIpDiscoveryProfileId, "cloudstack-ip-discovery", + NetworkOffering.Detail.NsxMacDiscoveryProfileId, "cloudstack-mac-discovery", + NetworkOffering.Detail.NsxSegmentSecurityProfileId, "cloudstack-segment-security"); + Map> serviceProviderMap = Map.of( + Network.Service.Connectivity, Set.of(Network.Provider.Nsx)); + + configurationManagerImplSpy.validateNtwkOffDetails(details, serviceProviderMap); + } + + @Test + public void testValidateNetworkOfferingDetailsRejectsNsxSegmentProfileForNonNsxOffering() { + Map details = Map.of( + NetworkOffering.Detail.NsxIpDiscoveryProfileId, "cloudstack-ip-discovery"); + Map> serviceProviderMap = Map.of( + Network.Service.Connectivity, Set.of(Network.Provider.VPCVirtualRouter)); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> configurationManagerImplSpy.validateNtwkOffDetails(details, serviceProviderMap)); + } + + @Test + public void testValidateNetworkOfferingDetailsRejectsBlankNsxSegmentProfile() { + Map details = Map.of(NetworkOffering.Detail.NsxMacDiscoveryProfileId, " "); + Map> serviceProviderMap = Map.of( + Network.Service.Connectivity, Set.of(Network.Provider.Nsx)); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> configurationManagerImplSpy.validateNtwkOffDetails(details, serviceProviderMap)); + } + + @Test + public void testValidateNetworkOfferingDetailsRejectsOversizedNsxSegmentProfile() { + Map details = Map.of( + NetworkOffering.Detail.NsxSegmentSecurityProfileId, "x".repeat(256)); + Map> serviceProviderMap = Map.of( + Network.Service.Connectivity, Set.of(Network.Provider.Nsx)); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> configurationManagerImplSpy.validateNtwkOffDetails(details, serviceProviderMap)); + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 775de26103a0..095f0a513fa7 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1880,6 +1880,9 @@ "label.nsx.provider.edgecluster": "NSX provider edge Cluster", "label.nsx.provider.tier0gateway": "NSX provider tier-0 gateway", "label.nsx.provider.transportzone": "NSX provider transport Zone", +"label.nsx.ip.discovery.profile.id": "NSX IP discovery profile ID", +"label.nsx.mac.discovery.profile.id": "NSX MAC discovery profile ID", +"label.nsx.segment.security.profile.id": "NSX segment security profile ID", "label.nsx.supports.internal.lb": "Enable NSX internal LB service", "label.nsx.supports.lb": "Enable NSX LB service", "label.num.cpu.cores": "# of CPU cores", @@ -3891,6 +3894,9 @@ "message.network.offering.mac.address.changes": "Applicable for guest Networks on VMware hypervisor only.\nReject - If the guest OS changes the effective MAC address of the Instance to a value that is different from the MAC address of the Instance Network adapter (set in the .vmx configuration file), the switch drops all inbound frames to the adapter.\nIf the guest OS changes the effective MAC address of the Instance back to the MAC address of the Instance Network adapter, the virtual machine receives frames again.\nAccept - If the guest OS changes the effective MAC address of the virtual machine to a value that is different from the MAC address of the Instance Network adapter, the switch allows frames to the new address to pass.\nNone - Default to value from global setting.", "message.network.offering.mac.learning": "Applicable for guest Networks on VMware hypervisor only with VMware Distributed Virtual Switches version 6.6.0 & above and vSphere version 6.7 & above.\nMAC learning enables Network connectivity for multiple MAC addresses behind a single vNIC.\nNone - Default to value from global setting.", "message.network.offering.mac.learning.warning": "WARNING: In order to use MAC Learning you must ensure your hypervisor hosts are running ESXi 6.7+ and the Network uses distributed vSwitch 6.6.0+.", +"message.network.offering.nsx.ip.discovery.profile.id": "ID of an existing NSX IP discovery profile to bind to segments created from this offering.", +"message.network.offering.nsx.mac.discovery.profile.id": "ID of an existing NSX MAC discovery profile to bind to segments created from this offering.", +"message.network.offering.nsx.segment.security.profile.id": "ID of an existing NSX segment security profile to bind to segments created from this offering.", "message.network.offering.promiscuous.mode": "Applicable for guest Networks on VMware hypervisor only.\nReject - The switch drops any outbound frame from a virtual machine adapter with a source MAC address that is different from the one in the .vmx configuration file.\nAccept - The switch does not perform filtering, and permits all outbound frames.\nNone - Default to value from global setting.", "message.network.removenic": "Please confirm that want to remove this NIC, which will also remove the associated Network from the Instance.", "message.network.restart.required": "Restart is required for network(s). Click here to view network(s) which require restart.", diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index 995b81ce68c6..b1b47f87478b 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -160,6 +160,38 @@ + + + + + + + + + + + + + + + + + + + +