2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2017 - 2019 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END=========================================================
22 * Copyright (C) 2018 Bell Canada. All rights reserved.
24 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
25 * the License. You may obtain a copy of the License at
27 * http://www.apache.org/licenses/LICENSE-2.0
29 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
30 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
31 * specific language governing permissions and limitations under the License.
33 package org.onap.so.heatbridge;
35 import java.util.HashMap;
36 import java.util.List;
38 import java.util.Objects;
39 import java.util.Optional;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.function.Function;
42 import java.util.function.Predicate;
43 import java.util.stream.Collectors;
44 import javax.annotation.Nonnull;
45 import javax.ws.rs.WebApplicationException;
46 import org.apache.commons.collections.CollectionUtils;
47 import org.apache.commons.validator.routines.InetAddressValidator;
48 import org.onap.aai.domain.yang.Flavor;
49 import org.onap.aai.domain.yang.Image;
50 import org.onap.aai.domain.yang.L3InterfaceIpv4AddressList;
51 import org.onap.aai.domain.yang.LInterface;
52 import org.onap.aai.domain.yang.PInterface;
53 import org.onap.aai.domain.yang.Pserver;
54 import org.onap.aai.domain.yang.SriovPf;
55 import org.onap.aai.domain.yang.SriovPfs;
56 import org.onap.aai.domain.yang.SriovVf;
57 import org.onap.aai.domain.yang.SriovVfs;
58 import org.onap.aai.domain.yang.Vlan;
59 import org.onap.aai.domain.yang.Vlans;
60 import org.onap.aai.domain.yang.Vserver;
61 import org.onap.aai.domain.yang.VfModule;
62 import org.onap.so.client.aai.AAIObjectType;
63 import org.onap.so.client.aai.AAIResourcesClient;
64 import org.onap.so.client.aai.AAISingleTransactionClient;
65 import org.onap.so.client.aai.entities.AAIResultWrapper;
66 import org.onap.so.client.aai.entities.Relationships;
67 import org.onap.so.client.aai.entities.uri.AAIResourceUri;
68 import org.onap.so.client.aai.entities.uri.AAIUriFactory;
69 import org.onap.so.client.graphinventory.entities.uri.Depth;
70 import org.onap.so.client.graphinventory.exceptions.BulkProcessFailed;
71 import org.onap.so.client.PreconditionFailedException;
72 import org.onap.so.db.catalog.beans.CloudIdentity;
73 import org.onap.so.heatbridge.constants.HeatBridgeConstants;
74 import org.onap.so.heatbridge.factory.MsoCloudClientFactoryImpl;
75 import org.onap.so.heatbridge.helpers.AaiHelper;
76 import org.onap.so.heatbridge.openstack.api.OpenstackClient;
77 import org.onap.so.heatbridge.openstack.factory.OpenstackClientFactoryImpl;
78 import org.onap.so.heatbridge.utils.HeatBridgeUtils;
79 import org.onap.so.logger.ErrorCode;
80 import org.onap.so.logger.LoggingAnchor;
81 import org.onap.so.logger.MessageEnum;
82 import org.openstack4j.model.compute.Server;
83 import org.openstack4j.model.heat.Resource;
84 import org.openstack4j.model.network.IP;
85 import org.openstack4j.model.network.Network;
86 import org.openstack4j.model.network.NetworkType;
87 import org.openstack4j.model.network.Port;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
90 import com.google.common.base.Preconditions;
91 import com.google.common.base.Strings;
92 import com.google.common.collect.ImmutableMap;
95 * This class provides an implementation of {@link HeatBridgeApi}
97 public class HeatBridgeImpl implements HeatBridgeApi {
99 private static final Logger logger = LoggerFactory.getLogger(HeatBridgeImpl.class);
100 private static final String ERR_MSG_NULL_OS_CLIENT =
101 "Initialization error: Null openstack client. Authenticate with Keystone first.";
102 private static final String OOB_MGT_NETWORK_IDENTIFIER = "Management";
103 private OpenstackClient osClient;
104 private AAIResourcesClient resourcesClient;
105 private AAISingleTransactionClient transaction;
106 private String cloudOwner;
107 private String cloudRegionId;
108 private String regionId;
109 private String tenantId;
110 private AaiHelper aaiHelper = new AaiHelper();
111 private CloudIdentity cloudIdentity;
113 public HeatBridgeImpl(AAIResourcesClient resourcesClient, final CloudIdentity cloudIdentity,
114 @Nonnull final String cloudOwner, @Nonnull final String cloudRegionId, @Nonnull final String regionId,
115 @Nonnull final String tenantId) {
116 Objects.requireNonNull(cloudOwner, "Null cloud-owner value!");
117 Objects.requireNonNull(cloudRegionId, "Null cloud-region identifier!");
118 Objects.requireNonNull(tenantId, "Null tenant identifier!");
119 Objects.requireNonNull(regionId, "Null regionId identifier!");
121 this.cloudIdentity = cloudIdentity;
122 this.cloudOwner = cloudOwner;
123 this.cloudRegionId = cloudRegionId;
124 this.regionId = regionId;
125 this.tenantId = tenantId;
126 this.resourcesClient = resourcesClient;
127 this.transaction = resourcesClient.beginSingleTransaction();
130 public HeatBridgeImpl() {
131 this.resourcesClient = new AAIResourcesClient();
132 this.transaction = resourcesClient.beginSingleTransaction();
136 public OpenstackClient authenticate() throws HeatBridgeException {
137 this.osClient = new MsoCloudClientFactoryImpl(new OpenstackClientFactoryImpl()).getOpenstackClient(
138 cloudIdentity.getIdentityUrl(), cloudIdentity.getMsoId(), cloudIdentity.getMsoPass(), regionId,
140 logger.debug("Successfully authenticated with keystone for tenant: " + tenantId + " and region: " + regionId);
145 public List<Resource> queryNestedHeatStackResources(final String heatStackId) {
146 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
147 Preconditions.checkState(!Strings.isNullOrEmpty(heatStackId), "Invalid heatStackId!");
148 List<Resource> stackBasedResources =
149 osClient.getStackBasedResources(heatStackId, HeatBridgeConstants.OS_DEFAULT_HEAT_NESTING);
150 logger.debug(stackBasedResources.size() + " heat stack resources are extracted for stack: " + heatStackId);
151 return stackBasedResources;
155 public List<String> extractStackResourceIdsByResourceType(final List<Resource> stackResources,
156 final String resourceType) {
157 return stackResources.stream().filter(stackResource -> stackResource.getType().equals(resourceType))
158 .map(Resource::getPhysicalResourceId).collect(Collectors.toList());
162 public List<String> extractNetworkIds(final List<String> networkNameList) {
163 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
164 return networkNameList.stream()
165 .map(netName -> osClient
166 .listNetworksByFilter(ImmutableMap.of(HeatBridgeConstants.OS_NAME_KEY, netName)))
167 .filter(nets -> nets != null && nets.size() == 1) // extract network-id only if network-name is unique
168 .map(nets -> nets.get(0).getId()).collect(Collectors.toList());
172 public List<Server> getAllOpenstackServers(final List<Resource> stackResources) {
173 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
175 // Filter Openstack Compute resources
176 List<String> serverIds =
177 extractStackResourceIdsByResourceType(stackResources, HeatBridgeConstants.OS_SERVER_RESOURCE_TYPE);
178 return serverIds.stream().map(serverId -> osClient.getServerById(serverId)).collect(Collectors.toList());
182 public List<org.openstack4j.model.compute.Image> extractOpenstackImagesFromServers(final List<Server> servers) {
183 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
184 return servers.stream().map(Server::getImage)
185 .filter(distinctByProperty(org.openstack4j.model.compute.Image::getId)).collect(Collectors.toList());
189 public List<org.openstack4j.model.compute.Flavor> extractOpenstackFlavorsFromServers(final List<Server> servers) {
190 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
191 return servers.stream().map(Server::getFlavor)
192 .filter(distinctByProperty(org.openstack4j.model.compute.Flavor::getId)).collect(Collectors.toList());
196 public void buildAddImagesToAaiAction(final List<org.openstack4j.model.compute.Image> images)
197 throws HeatBridgeException {
198 for (org.openstack4j.model.compute.Image image : images) {
199 Image aaiImage = aaiHelper.buildImage(image);
201 AAIResourceUri uri = AAIUriFactory.createResourceUri(AAIObjectType.IMAGE, cloudOwner, cloudRegionId,
202 aaiImage.getImageId());
203 if (!resourcesClient.exists(uri)) {
204 transaction.create(uri, aaiImage);
205 logger.debug("Queuing AAI command to add image: " + aaiImage.getImageId());
207 logger.debug("Nothing to add since image: " + aaiImage.getImageId() + "already exists in AAI.");
209 } catch (WebApplicationException e) {
210 throw new HeatBridgeException(
211 "Failed to update image to AAI: " + aaiImage.getImageId() + ". Error" + " cause: " + e, e);
217 public void buildAddFlavorsToAaiAction(final List<org.openstack4j.model.compute.Flavor> flavors)
218 throws HeatBridgeException {
219 for (org.openstack4j.model.compute.Flavor flavor : flavors) {
220 Flavor aaiFlavor = aaiHelper.buildFlavor(flavor);
222 AAIResourceUri uri = AAIUriFactory.createResourceUri(AAIObjectType.FLAVOR, cloudOwner, cloudRegionId,
223 aaiFlavor.getFlavorId());
224 if (!resourcesClient.exists(uri)) {
225 transaction.create(uri, aaiFlavor);
226 logger.debug("Queuing AAI command to add flavor: " + aaiFlavor.getFlavorId());
228 logger.debug("Nothing to add since flavor: " + aaiFlavor.getFlavorId() + "already exists in AAI.");
230 } catch (WebApplicationException e) {
231 throw new HeatBridgeException(
232 "Failed to update flavor to AAI: " + aaiFlavor.getFlavorId() + ". Error" + " cause: " + e, e);
238 public void buildAddVserversToAaiAction(final String genericVnfId, final String vfModuleId,
239 final List<Server> servers) {
240 servers.forEach(server -> {
241 Vserver vserver = aaiHelper.buildVserver(server.getId(), server);
243 // Build vserver relationships to: image, flavor, pserver, vf-module
244 vserver.setRelationshipList(
245 aaiHelper.getVserverRelationshipList(cloudOwner, cloudRegionId, genericVnfId, vfModuleId, server));
246 transaction.create(AAIUriFactory.createResourceUri(AAIObjectType.VSERVER, cloudOwner, cloudRegionId,
247 tenantId, vserver.getVserverId()), vserver);
252 public void buildAddVserverLInterfacesToAaiAction(final List<Resource> stackResources,
253 final List<String> oobMgtNetIds) {
254 Objects.requireNonNull(osClient, ERR_MSG_NULL_OS_CLIENT);
255 List<String> portIds =
256 extractStackResourceIdsByResourceType(stackResources, HeatBridgeConstants.OS_PORT_RESOURCE_TYPE);
257 for (String portId : portIds) {
258 Port port = osClient.getPortById(portId);
259 LInterface lIf = new LInterface();
260 lIf.setInterfaceId(port.getId());
261 lIf.setInterfaceName(port.getName());
262 lIf.setMacaddr(port.getMacAddress());
263 if (oobMgtNetIds != null && oobMgtNetIds.contains(port.getNetworkId())) {
264 lIf.setInterfaceRole(OOB_MGT_NETWORK_IDENTIFIER);
266 lIf.setInterfaceRole(port.getvNicType());
269 updateLInterfaceIps(port, lIf);
270 updateLInterfaceVlan(port, lIf);
272 // Update l-interface to the vserver
273 transaction.create(AAIUriFactory.createResourceUri(AAIObjectType.L_INTERFACE, cloudOwner, cloudRegionId,
274 tenantId, port.getDeviceId(), lIf.getInterfaceName()), lIf);
279 public void createPserversAndPinterfacesIfNotPresentInAai(final List<Resource> stackResources)
280 throws HeatBridgeException {
281 Map<String, Pserver> serverHostnames = getPserverMapping(stackResources);
282 createPServerIfNotExists(serverHostnames);
283 List<String> portIds =
284 extractStackResourceIdsByResourceType(stackResources, HeatBridgeConstants.OS_PORT_RESOURCE_TYPE);
285 for (String portId : portIds) {
286 Port port = osClient.getPortById(portId);
287 if (port.getvNicType().equalsIgnoreCase(HeatBridgeConstants.OS_SRIOV_PORT_TYPE)) {
288 createPServerPInterfaceIfNotExists(serverHostnames.get(port.getHostId()).getHostname(),
289 aaiHelper.buildPInterface(port));
294 private Map<String, Pserver> getPserverMapping(final List<Resource> stackResources) {
295 List<Server> osServers = getAllOpenstackServers(stackResources);
296 Map<String, Pserver> pserverMap = new HashMap<>();
297 for (Server server : osServers) {
298 pserverMap.put(server.getHost(), aaiHelper.buildPserver(server));
303 private void createPServerIfNotExists(Map<String, Pserver> serverHostnames) {
304 for (Pserver pserver : serverHostnames.values()) {
305 AAIResourceUri uri = AAIUriFactory.createResourceUri(AAIObjectType.PSERVER, pserver.getHostname());
306 resourcesClient.createIfNotExists(uri, Optional.of(pserver));
310 private void createPServerPInterfaceIfNotExists(String pserverHostname, PInterface pInterface) {
311 AAIResourceUri uri = AAIUriFactory.createResourceUri(AAIObjectType.P_INTERFACE, pserverHostname,
312 pInterface.getInterfaceName());
313 resourcesClient.createIfNotExists(uri, Optional.of(pInterface));
316 private void updateLInterfaceVlan(final Port port, final LInterface lIf) {
317 Vlan vlan = new Vlan();
318 Network network = osClient.getNetworkById(port.getNetworkId());
319 lIf.setNetworkName(network.getName());
320 if (network.getNetworkType().equals(NetworkType.VLAN)) {
321 vlan.setVlanInterface(network.getProviderSegID());
322 Vlans vlans = new Vlans();
323 List<Vlan> vlanList = vlans.getVlan();
327 // Build sriov-vf to the l-interface
328 if (port.getvNicType().equalsIgnoreCase(HeatBridgeConstants.OS_SRIOV_PORT_TYPE)) {
329 SriovVfs sriovVfs = new SriovVfs();
330 // JAXB does not generate setters for list, however getter ensures its creation.
331 // Thus, all list manipulations must be made on live list.
332 List<SriovVf> sriovVfList = sriovVfs.getSriovVf();
333 SriovVf sriovVf = new SriovVf();
334 sriovVf.setPciId(port.getProfile().get(HeatBridgeConstants.OS_PCI_SLOT_KEY).toString());
335 sriovVf.setNeutronNetworkId(port.getNetworkId());
336 if (port.getVifDetails() != null) {
337 sriovVf.setVfVlanFilter((String) port.getVifDetails().get(HeatBridgeConstants.OS_VLAN_NETWORK_KEY));
339 sriovVfList.add(sriovVf);
341 lIf.setSriovVfs(sriovVfs);
343 // For the given port create sriov-pf for host pserver/p-interface if absent
344 updateSriovPfToPserver(port, lIf);
349 * Needs to be corrected according to the specification that is in draft If pserver/p-interface does not have a
350 * SRIOV-PF object matching the PCI-ID of the Openstack port object, then create it in AAI. Openstack SRIOV Port
351 * object has pci-id (to match sriov-pf on pserver/p-interface), physical-network ID (that matches the p-interface
354 * @param port Openstack port object
355 * @param lIf AAI l-interface object
357 private void updateSriovPfToPserver(final Port port, final LInterface lIf) {
358 if (port.getProfile() == null || Strings
359 .isNullOrEmpty(port.getProfile().get(HeatBridgeConstants.OS_PHYSICAL_NETWORK_KEY).toString())) {
360 logger.debug("The SRIOV port:" + port.getName() + " is missing physical-network-id, cannot update "
361 + "sriov-pf object for host pserver: " + port.getHostId());
364 Optional<String> matchingPifName = HeatBridgeUtils.getMatchingPserverPifName(
365 port.getProfile().get(HeatBridgeConstants.OS_PHYSICAL_NETWORK_KEY).toString());
366 if (matchingPifName.isPresent()) {
367 // Update l-interface description
368 String pserverHostName = port.getHostId();
369 lIf.setInterfaceDescription("Attached to SR-IOV port: " + pserverHostName + "::" + matchingPifName.get());
371 Optional<PInterface> matchingPIf = resourcesClient.get(PInterface.class,
373 .createResourceUri(AAIObjectType.P_INTERFACE, pserverHostName, matchingPifName.get())
375 if (matchingPIf.isPresent()) {
376 SriovPfs pIfSriovPfs = matchingPIf.get().getSriovPfs();
377 if (pIfSriovPfs == null) {
378 pIfSriovPfs = new SriovPfs();
380 // Extract PCI-ID from OS port object
381 String pfPciId = port.getProfile().get(HeatBridgeConstants.OS_PCI_SLOT_KEY).toString();
383 List<SriovPf> existingSriovPfs = pIfSriovPfs.getSriovPf();
384 if (CollectionUtils.isEmpty(existingSriovPfs) || existingSriovPfs.stream()
385 .noneMatch(existingSriovPf -> existingSriovPf.getPfPciId().equals(pfPciId))) {
386 // Add sriov-pf object with PCI-ID to AAI
387 SriovPf sriovPf = new SriovPf();
388 sriovPf.setPfPciId(pfPciId);
389 logger.debug("Queuing AAI command to update sriov-pf object to pserver: " + pserverHostName
390 + "/" + matchingPifName.get());
391 transaction.create(AAIUriFactory.createResourceUri(AAIObjectType.SRIOV_PF, pserverHostName,
392 matchingPifName.get(), sriovPf.getPfPciId()), sriovPf);
395 } catch (WebApplicationException e) {
396 // Silently log that we failed to update the Pserver p-interface with PCI-ID
397 logger.error(LoggingAnchor.NINE, MessageEnum.GENERAL_EXCEPTION, pserverHostName, matchingPifName.get(),
398 cloudOwner, tenantId, "OpenStack", "Heatbridge", ErrorCode.DataError.getValue(),
399 "Exception - Failed to add sriov-pf object to pserver", e);
404 private void updateLInterfaceIps(final Port port, final LInterface lIf) {
405 List<L3InterfaceIpv4AddressList> lInterfaceIps = lIf.getL3InterfaceIpv4AddressList();
406 for (IP ip : port.getFixedIps()) {
407 String ipAddress = ip.getIpAddress();
408 if (InetAddressValidator.getInstance().isValidInet4Address(ipAddress)) {
409 L3InterfaceIpv4AddressList lInterfaceIp = new L3InterfaceIpv4AddressList();
410 lInterfaceIp.setL3InterfaceIpv4Address(ipAddress);
411 lInterfaceIp.setNeutronNetworkId(port.getNetworkId());
412 lInterfaceIp.setNeutronSubnetId(ip.getSubnetId());
413 lInterfaceIp.setL3InterfaceIpv4PrefixLength(32L);
414 lInterfaceIps.add(lInterfaceIp);
420 public void submitToAai() throws HeatBridgeException {
422 transaction.execute();
423 } catch (BulkProcessFailed e) {
424 String msg = "Failed to commit transaction";
425 logger.debug(msg + " with error: " + e);
426 throw new HeatBridgeException(msg, e);
431 public void deleteVfModuleData(@Nonnull final String vnfId, @Nonnull final String vfModuleId)
432 throws HeatBridgeException {
433 Objects.requireNonNull(vnfId, "Null vnf-id!");
434 Objects.requireNonNull(vfModuleId, "Null vf-module-id!");
436 Optional<VfModule> vfModule = resourcesClient.get(VfModule.class,
437 AAIUriFactory.createResourceUri(AAIObjectType.VF_MODULE, vnfId, vfModuleId).depth(Depth.ONE));
438 if (vfModule.isPresent()) {
440 AAIResultWrapper resultWrapper = new AAIResultWrapper(vfModule);
441 Optional<Relationships> relationships = resultWrapper.getRelationships();
442 if (relationships.isPresent()) {
443 List<AAIResourceUri> vserverUris = relationships.get().getRelatedUris(AAIObjectType.VSERVER);
444 createTransactionToDeleteSriovPfFromPserver(vserverUris);
445 if (!vserverUris.isEmpty()) {
446 for (AAIResourceUri vserverUri : vserverUris) {
447 resourcesClient.delete(vserverUri);
452 } catch (Exception e) {
453 String msg = "Failed to commit delete heatbridge data transaction";
454 logger.debug(msg + " with error: " + e);
455 throw new HeatBridgeException(msg, e);
459 private void createTransactionToDeleteSriovPfFromPserver(List<AAIResourceUri> vserverUris) {
460 Map<String, List<String>> pserverToPciIdMap = getPserverToPciIdMap(vserverUris);
461 for (Map.Entry<String, List<String>> entry : pserverToPciIdMap.entrySet()) {
462 String pserverName = entry.getKey();
463 List<String> pciIds = entry.getValue();
464 Optional<Pserver> pserver = resourcesClient.get(Pserver.class,
465 AAIUriFactory.createResourceUri(AAIObjectType.PSERVER, pserverName).depth(Depth.TWO));
466 if (pserver.isPresent()) {
467 // For each pserver/p-interface match sriov-vfs by pic-id and delete them.
468 pserver.get().getPInterfaces().getPInterface().stream().filter(
469 pIf -> pIf.getSriovPfs() != null && CollectionUtils.isNotEmpty(pIf.getSriovPfs().getSriovPf()))
470 .forEach(pIf -> pIf.getSriovPfs().getSriovPf().forEach(sriovPf -> {
471 if (pciIds.contains(sriovPf.getPfPciId())) {
472 logger.debug("creating transaction to delete SR-IOV PF: " + pIf.getInterfaceName()
473 + " from PServer: " + pserverName);
474 resourcesClient.delete(AAIUriFactory.createResourceUri(AAIObjectType.SRIOV_PF,
475 pserverName, pIf.getInterfaceName(), sriovPf.getPfPciId()));
482 private Map<String, List<String>> getPserverToPciIdMap(List<AAIResourceUri> vserverUris) {
483 Map<String, List<String>> pserverToPciIdMap = new HashMap<>();
484 for (AAIResourceUri vserverUri : vserverUris) {
485 AAIResultWrapper vserverWrapper = resourcesClient.get(vserverUri.depth(Depth.TWO));
486 Optional<Relationships> vserverRelationships = vserverWrapper.getRelationships();
487 if (vserverRelationships.isPresent()
488 && CollectionUtils.isNotEmpty(vserverRelationships.get().getRelatedLinks(AAIObjectType.PSERVER))) {
489 Vserver vserver = vserverWrapper.asBean(Vserver.class).get();
490 List<String> pciIds = HeatBridgeUtils.extractPciIdsFromVServer(vserver);
491 if (CollectionUtils.isNotEmpty(pciIds)) {
492 List<String> matchingPservers = vserverRelationships.get().getRelatedLinks(AAIObjectType.PSERVER);
493 if (matchingPservers != null && matchingPservers.size() == 1) {
494 pserverToPciIdMap.put(matchingPservers.get(0), pciIds);
499 return pserverToPciIdMap;
502 private <T> Predicate<T> distinctByProperty(Function<? super T, Object> keyExtractor) {
503 Map<Object, Boolean> map = new ConcurrentHashMap<>();
504 return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;