diff --git a/PendingReleaseNotes b/PendingReleaseNotes index 9670b6e7c13a..efb33625a3f8 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -39,3 +39,9 @@ example.ver.1 > example.ver.2: which can now be attached to Instances. This is to prevent the Secondary Storage to grow to enormous sizes as Linux Distributions keep growing in size while a stripped down Linux should fit on a 2.88MB floppy. + +4.22.1.0 > 4.23.0.0: + * NSX network offerings can reference existing IP discovery, MAC discovery, + and segment security profiles. New NSX segments bind selected profiles in + the same hierarchical request as segment creation. Existing networks are + not silently reconfigured when profile bindings change. 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/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 9b42be137314..d1b758122644 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 @@ -166,7 +166,9 @@ public abstract class NetworkOfferingBaseCmd extends BaseCmd { @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.2.0", description = "Network offering details in key/value pairs." + " Supported keys are internallbprovider/publiclbprovider with service provider as a value, and" - + " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup") + + " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup." + + " NSX offerings also support nsxipdiscoveryprofileid, nsxmacdiscoveryprofileid, and nsxsegmentsecurityprofileid." + + " Values are IDs of existing operator-managed NSX profiles to bind to segments created from the offering.") protected Map details; @Parameter(name = ApiConstants.EGRESS_DEFAULT_POLICY, 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..f66d82ab627d 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 @@ -25,12 +25,16 @@ import com.vmware.nsx.model.ControllerClusterStatus; import com.vmware.nsx.model.TransportZone; import com.vmware.nsx.model.TransportZoneListResult; +import com.vmware.nsx_policy.Infra; 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; @@ -43,12 +47,16 @@ import com.vmware.nsx_policy.infra.tier_0s.LocaleServices; import com.vmware.nsx_policy.infra.tier_1s.nat.NatRules; import com.vmware.nsx_policy.model.ApiError; +import com.vmware.nsx_policy.model.ChildSegment; +import com.vmware.nsx_policy.model.ChildSegmentDiscoveryProfileBindingMap; +import com.vmware.nsx_policy.model.ChildSegmentSecurityProfileBindingMap; import com.vmware.nsx_policy.model.DhcpRelayConfig; import com.vmware.nsx_policy.model.EnforcementPoint; import com.vmware.nsx_policy.model.EnforcementPointListResult; 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 +69,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 +78,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 +102,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 +141,11 @@ 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 INFRA_RESOURCE_TYPE = "Infra"; + private static final String SEGMENT_DISCOVERY_PROFILE_BINDING_RESOURCE_TYPE = "SegmentDiscoveryProfileBindingMap"; + private static final String SEGMENT_SECURITY_PROFILE_BINDING_RESOURCE_TYPE = "SegmentSecurityProfileBindingMap"; + 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,8 +484,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 { - Segments segmentService = (Segments) nsxService.apply(Segments.class); + String ipDiscoveryProfilePath = getIpDiscoveryProfilePath(ipDiscoveryProfileId); + String macDiscoveryProfilePath = getMacDiscoveryProfilePath(macDiscoveryProfileId); + String segmentSecurityProfilePath = getSegmentSecurityProfilePath(segmentSecurityProfileId); SegmentSubnet subnet = new SegmentSubnet.Builder() .setGatewayAddress(gatewayAddress) .build(); @@ -480,7 +506,24 @@ public void createSegment(String segmentName, String tier1GatewayName, String ga .setSubnets(List.of(subnet)) .setTransportZonePath(enforcementPointPath + "/transport-zones/" + transportZones.get(0).getId()) .build(); - segmentService.patch(segmentName, segment); + List profileBindings = getSegmentProfileBindings(ipDiscoveryProfilePath, macDiscoveryProfilePath, + segmentSecurityProfilePath); + if (profileBindings.isEmpty()) { + Segments segmentService = (Segments) nsxService.apply(Segments.class); + segmentService.patch(segmentName, segment); + } else { + segment.setChildren(profileBindings); + ChildSegment childSegment = new ChildSegment.Builder() + .setId(segmentName) + .setSegment(segment) + .build(); + com.vmware.nsx_policy.model.Infra infra = new com.vmware.nsx_policy.model.Infra.Builder() + .setResourceType(INFRA_RESOURCE_TYPE) + .setChildren(List.of(childSegment)) + .build(); + Infra infraService = (Infra) nsxService.apply(Infra.class); + infraService.patch(infra, false); + } } catch (Error error) { ApiError ae = error.getData()._convertTo(ApiError.class); String msg = String.format("Error creating segment %s: %s", segmentName, ae.getErrorMessage()); @@ -489,6 +532,76 @@ 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 validateProfile(profileId, profile.getId(), profile.getPath(), "/infra/ip-discovery-profiles/", profile.getMarkedForDelete()); + } + + protected String getMacDiscoveryProfilePath(String profileId) { + if (StringUtils.isBlank(profileId)) { + return null; + } + MacDiscoveryProfiles profiles = (MacDiscoveryProfiles) nsxService.apply(MacDiscoveryProfiles.class); + MacDiscoveryProfile profile = profiles.get(profileId); + return validateProfile(profileId, profile.getId(), profile.getPath(), "/infra/mac-discovery-profiles/", profile.getMarkedForDelete()); + } + + protected String getSegmentSecurityProfilePath(String profileId) { + if (StringUtils.isBlank(profileId)) { + return null; + } + SegmentSecurityProfiles profiles = (SegmentSecurityProfiles) nsxService.apply(SegmentSecurityProfiles.class); + SegmentSecurityProfile profile = profiles.get(profileId); + return validateProfile(profileId, profile.getId(), profile.getPath(), "/infra/segment-security-profiles/", profile.getMarkedForDelete()); + } + + protected String validateProfile(String requestedId, String resolvedId, String profilePath, + String expectedPathPrefix, Boolean markedForDelete) { + if (!Objects.equals(requestedId, resolvedId)) { + throw new CloudRuntimeException(String.format("NSX returned profile %s while resolving requested profile %s", resolvedId, requestedId)); + } + if (!Objects.equals(expectedPathPrefix + requestedId, profilePath)) { + throw new CloudRuntimeException(String.format("NSX profile %s did not return a canonical resource path of the expected type", requestedId)); + } + if (Boolean.TRUE.equals(markedForDelete)) { + throw new CloudRuntimeException(String.format("NSX profile %s is marked for deletion", requestedId)); + } + return profilePath; + } + + protected List getSegmentProfileBindings(String ipDiscoveryProfilePath, String macDiscoveryProfilePath, + String segmentSecurityProfilePath) { + List bindings = new ArrayList<>(); + if (StringUtils.isNotBlank(ipDiscoveryProfilePath) || StringUtils.isNotBlank(macDiscoveryProfilePath)) { + SegmentDiscoveryProfileBindingMap binding = new SegmentDiscoveryProfileBindingMap.Builder() + .setResourceType(SEGMENT_DISCOVERY_PROFILE_BINDING_RESOURCE_TYPE) + .setId(SEGMENT_DISCOVERY_PROFILE_BINDING_ID) + .setIpDiscoveryProfilePath(ipDiscoveryProfilePath) + .setMacDiscoveryProfilePath(macDiscoveryProfilePath) + .build(); + bindings.add(new ChildSegmentDiscoveryProfileBindingMap.Builder() + .setId(SEGMENT_DISCOVERY_PROFILE_BINDING_ID) + .setSegmentDiscoveryProfileBindingMap(binding) + .build()); + } + if (StringUtils.isNotBlank(segmentSecurityProfilePath)) { + SegmentSecurityProfileBindingMap binding = new SegmentSecurityProfileBindingMap.Builder() + .setResourceType(SEGMENT_SECURITY_PROFILE_BINDING_RESOURCE_TYPE) + .setId(SEGMENT_SECURITY_PROFILE_BINDING_ID) + .setSegmentSecurityProfilePath(segmentSecurityProfilePath) + .build(); + bindings.add(new ChildSegmentSecurityProfileBindingMap.Builder() + .setId(SEGMENT_SECURITY_PROFILE_BINDING_ID) + .setSegmentSecurityProfileBindingMap(binding) + .build()); + } + return bindings; + } + 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..d418ca81dd75 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,26 @@ import com.vmware.nsx.cluster.Status; import com.vmware.nsx.model.ClusterStatus; import com.vmware.nsx.model.ControllerClusterStatus; +import com.vmware.nsx_policy.Infra; +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.tier_1s.ipsec_vpn_services.Sessions; +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.ChildSegment; +import com.vmware.nsx_policy.model.ChildSegmentDiscoveryProfileBindingMap; +import com.vmware.nsx_policy.model.ChildSegmentSecurityProfileBindingMap; 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 +49,16 @@ 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.StaticRoutesListResult; +import com.vmware.nsx_policy.model.Tag; +import com.vmware.nsx_policy.model.Tier1; +import com.vmware.nsx_policy.model.TunnelInterfaceIPSubnet; +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 +68,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 +124,187 @@ public void testCreateGroupForSegment() { } } + @Test + public void testCreateSegmentBindsConfiguredProfiles() { + Infra infraService = Mockito.mock(Infra.class); + IpDiscoveryProfiles ipProfiles = Mockito.mock(IpDiscoveryProfiles.class); + MacDiscoveryProfiles macProfiles = Mockito.mock(MacDiscoveryProfiles.class); + SegmentSecurityProfiles securityProfiles = Mockito.mock(SegmentSecurityProfiles.class); + IPDiscoveryProfile ipProfile = Mockito.mock(IPDiscoveryProfile.class); + MacDiscoveryProfile macProfile = Mockito.mock(MacDiscoveryProfile.class); + SegmentSecurityProfile securityProfile = Mockito.mock(SegmentSecurityProfile.class); + when(nsxService.apply(Infra.class)).thenReturn(infraService); + when(nsxService.apply(IpDiscoveryProfiles.class)).thenReturn(ipProfiles); + when(nsxService.apply(MacDiscoveryProfiles.class)).thenReturn(macProfiles); + when(nsxService.apply(SegmentSecurityProfiles.class)).thenReturn(securityProfiles); + when(ipProfiles.get("ip-profile")).thenReturn(ipProfile); + when(macProfiles.get("mac-profile")).thenReturn(macProfile); + when(securityProfiles.get("security-profile")).thenReturn(securityProfile); + when(ipProfile.getId()).thenReturn("ip-profile"); + when(macProfile.getId()).thenReturn("mac-profile"); + when(securityProfile.getId()).thenReturn("security-profile"); + 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 infraCaptor = + ArgumentCaptor.forClass(com.vmware.nsx_policy.model.Infra.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(infraService).patch(infraCaptor.capture(), eq(false)); + verify(nsxService, never()).apply(Segments.class); + Assert.assertEquals("Infra", infraCaptor.getValue().getResourceType()); + Assert.assertEquals(1, infraCaptor.getValue().getChildren().size()); + ChildSegment childSegment = (ChildSegment) infraCaptor.getValue().getChildren().get(0); + Assert.assertEquals("ChildSegment", childSegment.getResourceType()); + Assert.assertEquals("segment", childSegment.getId()); + Assert.assertEquals("Segment", childSegment.getSegment().getResourceType()); + Assert.assertEquals("segment", childSegment.getSegment().getId()); + Assert.assertEquals(2, childSegment.getSegment().getChildren().size()); + ChildSegmentDiscoveryProfileBindingMap discoveryChild = (ChildSegmentDiscoveryProfileBindingMap) + childSegment.getSegment().getChildren().get(0); + Assert.assertEquals("ChildSegmentDiscoveryProfileBindingMap", discoveryChild.getResourceType()); + SegmentDiscoveryProfileBindingMap discoveryBinding = discoveryChild.getSegmentDiscoveryProfileBindingMap(); + Assert.assertEquals("cloudstack-discovery-profile-binding", discoveryChild.getId()); + Assert.assertEquals("cloudstack-discovery-profile-binding", discoveryBinding.getId()); + Assert.assertEquals("SegmentDiscoveryProfileBindingMap", discoveryBinding.getResourceType()); + Assert.assertEquals("/infra/ip-discovery-profiles/ip-profile", discoveryBinding.getIpDiscoveryProfilePath()); + Assert.assertEquals("/infra/mac-discovery-profiles/mac-profile", discoveryBinding.getMacDiscoveryProfilePath()); + ChildSegmentSecurityProfileBindingMap securityChild = (ChildSegmentSecurityProfileBindingMap) + childSegment.getSegment().getChildren().get(1); + Assert.assertEquals("cloudstack-security-profile-binding", securityChild.getId()); + Assert.assertEquals("cloudstack-security-profile-binding", + securityChild.getSegmentSecurityProfileBindingMap().getId()); + Assert.assertEquals("ChildSegmentSecurityProfileBindingMap", securityChild.getResourceType()); + Assert.assertEquals("SegmentSecurityProfileBindingMap", + securityChild.getSegmentSecurityProfileBindingMap().getResourceType()); + Assert.assertEquals("/infra/segment-security-profiles/security-profile", + securityChild.getSegmentSecurityProfileBindingMap().getSegmentSecurityProfilePath()); + } + + @Test + public void testSegmentProfileBindingsWithOnlyIpDiscoveryProfile() { + List bindings = client.getSegmentProfileBindings("/infra/ip-discovery-profiles/ip-profile", null, null); + + Assert.assertEquals(1, bindings.size()); + ChildSegmentDiscoveryProfileBindingMap child = (ChildSegmentDiscoveryProfileBindingMap) bindings.get(0); + Assert.assertEquals("ChildSegmentDiscoveryProfileBindingMap", child.getResourceType()); + Assert.assertEquals("SegmentDiscoveryProfileBindingMap", + child.getSegmentDiscoveryProfileBindingMap().getResourceType()); + Assert.assertEquals("/infra/ip-discovery-profiles/ip-profile", + child.getSegmentDiscoveryProfileBindingMap().getIpDiscoveryProfilePath()); + Assert.assertNull(child.getSegmentDiscoveryProfileBindingMap().getMacDiscoveryProfilePath()); + } + + @Test + public void testSegmentProfileBindingsWithOnlyMacDiscoveryProfile() { + List bindings = client.getSegmentProfileBindings(null, "/infra/mac-discovery-profiles/mac-profile", null); + + Assert.assertEquals(1, bindings.size()); + ChildSegmentDiscoveryProfileBindingMap child = (ChildSegmentDiscoveryProfileBindingMap) bindings.get(0); + Assert.assertEquals("ChildSegmentDiscoveryProfileBindingMap", child.getResourceType()); + Assert.assertEquals("SegmentDiscoveryProfileBindingMap", + child.getSegmentDiscoveryProfileBindingMap().getResourceType()); + Assert.assertNull(child.getSegmentDiscoveryProfileBindingMap().getIpDiscoveryProfilePath()); + Assert.assertEquals("/infra/mac-discovery-profiles/mac-profile", + child.getSegmentDiscoveryProfileBindingMap().getMacDiscoveryProfilePath()); + } + + @Test + public void testSegmentProfileBindingsWithOnlySecurityProfile() { + List bindings = client.getSegmentProfileBindings(null, null, + "/infra/segment-security-profiles/security-profile"); + + Assert.assertEquals(1, bindings.size()); + ChildSegmentSecurityProfileBindingMap child = (ChildSegmentSecurityProfileBindingMap) bindings.get(0); + Assert.assertEquals("ChildSegmentSecurityProfileBindingMap", child.getResourceType()); + Assert.assertEquals("SegmentSecurityProfileBindingMap", + child.getSegmentSecurityProfileBindingMap().getResourceType()); + Assert.assertEquals("/infra/segment-security-profiles/security-profile", + child.getSegmentSecurityProfileBindingMap().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(Infra.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.getId()).thenReturn("ip-profile"); + 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(Infra.class); + } + + @Test + public void testCreateSegmentRejectsProfileMarkedForDeletionBeforeCreatingSegment() { + 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.getId()).thenReturn("ip-profile"); + when(ipProfile.getPath()).thenReturn("/infra/ip-discovery-profiles/ip-profile"); + when(ipProfile.getMarkedForDelete()).thenReturn(true); + + 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(Infra.class); + } + + @Test + public void testCreateSegmentRejectsProfileResolvedWithDifferentIdBeforeCreatingSegment() { + 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.getId()).thenReturn("different-profile"); + when(ipProfile.getPath()).thenReturn("/infra/ip-discovery-profiles/different-profile"); + + 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(Infra.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..76de06350e2e 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,28 @@ 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.equals(profileId.trim())) { + throw new InvalidParameterValueException(String.format("NSX profile ID for detail %s must not contain surrounding whitespace", 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/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 2853fa96330d..cca44705694e 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -4194,6 +4194,12 @@ protected boolean canUpgrade(Network network, long oldNetworkOfferingId, long ne NetworkOffering oldNetworkOffering = _networkOfferingDao.findByIdIncludingRemoved(oldNetworkOfferingId); NetworkOffering newNetworkOffering = _networkOfferingDao.findById(newNetworkOfferingId); + if (!haveMatchingNsxSegmentProfiles(oldNetworkOfferingId, newNetworkOfferingId)) { + logger.debug("Network offerings {} and {} have different NSX segment profile bindings, can't upgrade", + oldNetworkOffering, newNetworkOffering); + return false; + } + // security group service should be the same if (areServicesSupportedByNetworkOffering(oldNetworkOfferingId, Service.SecurityGroup) != areServicesSupportedByNetworkOffering(newNetworkOfferingId, Service.SecurityGroup)) { logger.debug("Offerings {} and {} have different securityGroupProperty, can't upgrade", newNetworkOffering, oldNetworkOffering); @@ -4230,6 +4236,20 @@ protected boolean canUpgrade(Network network, long oldNetworkOfferingId, long ne return canMoveToPhysicalNetwork(network, oldNetworkOfferingId, newNetworkOfferingId); } + protected boolean haveMatchingNsxSegmentProfiles(long oldNetworkOfferingId, long newNetworkOfferingId) { + Map oldDetails = _networkModel.getNtwkOffDetails(oldNetworkOfferingId); + Map newDetails = _networkModel.getNtwkOffDetails(newNetworkOfferingId); + for (NetworkOffering.Detail detail : List.of(NetworkOffering.Detail.NsxIpDiscoveryProfileId, + NetworkOffering.Detail.NsxMacDiscoveryProfileId, NetworkOffering.Detail.NsxSegmentSecurityProfileId)) { + String oldProfileId = oldDetails == null ? null : oldDetails.get(detail); + String newProfileId = newDetails == null ? null : newDetails.get(detail); + if (!Objects.equals(oldProfileId, newProfileId)) { + return false; + } + } + return true; + } + @Override @DB @ActionEvent(eventType = EventTypes.EVENT_PHYSICAL_NETWORK_CREATE, eventDescription = "Creating Physical Network", create = true) diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 9a0b150780e4..74a41b29b58e 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -1410,4 +1410,59 @@ 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 testValidateNetworkOfferingDetailsRejectsWhitespaceInNsxProfileId() { + Map details = Map.of( + NetworkOffering.Detail.NsxMacDiscoveryProfileId, " mac-profile "); + Map> serviceProviderMap = Map.of( + Network.Service.Connectivity, Set.of(Network.Provider.Nsx)); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> 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/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java index cd7d40d68951..c5c287565f48 100644 --- a/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java @@ -670,6 +670,42 @@ public void testCheckAndUpdateNetworkNoUpdate() { Assert.assertFalse(service.checkAndUpdateNetworkDns(network1, Mockito.mock(NetworkOffering.class), ip4Dns[0], null, ip6Dns[0], null)); } + @Test + public void testMatchingNsxSegmentProfilesAllowOfferingUpgrade() { + long oldOfferingId = 1L; + long newOfferingId = 2L; + Map profiles = Map.of( + NetworkOffering.Detail.NsxIpDiscoveryProfileId, "ip-profile", + NetworkOffering.Detail.NsxMacDiscoveryProfileId, "mac-profile", + NetworkOffering.Detail.NsxSegmentSecurityProfileId, "security-profile"); + Mockito.when(networkModel.getNtwkOffDetails(oldOfferingId)).thenReturn(profiles); + Mockito.when(networkModel.getNtwkOffDetails(newOfferingId)).thenReturn(profiles); + + Assert.assertTrue(service.haveMatchingNsxSegmentProfiles(oldOfferingId, newOfferingId)); + } + + @Test + public void testDifferentNsxSegmentProfilesRejectOfferingUpgrade() { + long oldOfferingId = 1L; + long newOfferingId = 2L; + Mockito.when(networkModel.getNtwkOffDetails(oldOfferingId)).thenReturn(Map.of( + NetworkOffering.Detail.NsxMacDiscoveryProfileId, "old-mac-profile")); + Mockito.when(networkModel.getNtwkOffDetails(newOfferingId)).thenReturn(Map.of( + NetworkOffering.Detail.NsxMacDiscoveryProfileId, "new-mac-profile")); + + Assert.assertFalse(service.haveMatchingNsxSegmentProfiles(oldOfferingId, newOfferingId)); + } + + @Test + public void testAbsentNsxSegmentProfilesAllowOfferingUpgrade() { + long oldOfferingId = 1L; + long newOfferingId = 2L; + Mockito.when(networkModel.getNtwkOffDetails(oldOfferingId)).thenReturn(null); + Mockito.when(networkModel.getNtwkOffDetails(newOfferingId)).thenReturn(Map.of()); + + Assert.assertTrue(service.haveMatchingNsxSegmentProfiles(oldOfferingId, newOfferingId)); + } + @Test public void testCheckAndUpdateNetworkOfferingChangeReset() { NetworkVO networkVO = new NetworkVO(); 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 @@ + + + + + + + + + + + + + + + + + + + +