use of org.opendaylight.yang.gen.v1.urn.opendaylight.neutron.bgpvpns.rev150903.bgpvpns.attributes.bgpvpns.Bgpvpn in project netvirt by opendaylight.
the class VpnInterfaceManager method processVpnInterfaceUp.
// "Unconditional wait" and "Wait not in loop" wrt the VpnNotifyTask below - suppressing the FB violation -
// see comments below.
@SuppressFBWarnings({ "UW_UNCOND_WAIT", "WA_NOT_IN_LOOP" })
protected void processVpnInterfaceUp(final BigInteger dpId, VpnInterface vpnInterface, final String primaryRd, final int lportTag, boolean isInterfaceUp, WriteTransaction writeConfigTxn, WriteTransaction writeOperTxn, WriteTransaction writeInvTxn, Interface interfaceState, final String vpnName) {
final String interfaceName = vpnInterface.getName();
Optional<VpnInterfaceOpDataEntry> optOpVpnInterface = VpnUtil.getVpnInterfaceOpDataEntry(dataBroker, interfaceName, vpnName);
VpnInterfaceOpDataEntry opVpnInterface = optOpVpnInterface.isPresent() ? optOpVpnInterface.get() : null;
boolean isBgpVpnInternetVpn = VpnUtil.isBgpVpnInternet(dataBroker, vpnName);
if (!isInterfaceUp) {
LOG.info("processVpnInterfaceUp: Binding vpn service to interface {} onto dpn {} for vpn {}", interfaceName, dpId, vpnName);
long vpnId = VpnUtil.getVpnId(dataBroker, vpnName);
if (vpnId == VpnConstants.INVALID_ID) {
LOG.warn("processVpnInterfaceUp: VpnInstance to VPNId mapping not available for VpnName {}" + " processing vpninterface {} on dpn {}, bailing out now.", vpnName, interfaceName, dpId);
return;
}
boolean waitForVpnInterfaceOpRemoval = false;
if (opVpnInterface != null && !opVpnInterface.isScheduledForRemove()) {
String opVpnName = opVpnInterface.getVpnInstanceName();
String primaryInterfaceIp = null;
if (opVpnName.equals(vpnName)) {
// Please check if the primary VRF Entry does not exist for VPNInterface
// If so, we have to process ADD, as this might be a DPN Restart with Remove and Add triggered
// back to back
// However, if the primary VRF Entry for this VPNInterface exists, please continue bailing out !
List<Adjacency> adjs = VpnUtil.getAdjacenciesForVpnInterfaceFromConfig(dataBroker, interfaceName);
if (adjs == null) {
LOG.error("processVpnInterfaceUp: VPN Interface {} on dpn {} for vpn {} failed as adjacencies" + " for this vpn interface could not be obtained", interfaceName, dpId, vpnName);
return;
}
for (Adjacency adj : adjs) {
if (adj.getAdjacencyType() == AdjacencyType.PrimaryAdjacency) {
primaryInterfaceIp = adj.getIpAddress();
break;
}
}
if (primaryInterfaceIp == null) {
LOG.error("processVpnInterfaceUp: VPN Interface {} addition on dpn {} for vpn {} failed" + " as primary adjacency for this vpn interface could not be obtained", interfaceName, dpId, vpnName);
return;
}
// Get the rd of the vpn instance
VrfEntry vrf = VpnUtil.getVrfEntry(dataBroker, primaryRd, primaryInterfaceIp);
if (vrf != null) {
LOG.error("processVpnInterfaceUp: VPN Interface {} on dpn {} for vpn {} already provisioned ," + " bailing out from here.", interfaceName, dpId, vpnName);
return;
}
waitForVpnInterfaceOpRemoval = true;
} else {
LOG.error("processVpnInterfaceUp: vpn interface {} to go to configured vpn {} on dpn {}," + " but in operational vpn {}", interfaceName, vpnName, dpId, opVpnName);
}
}
if (!waitForVpnInterfaceOpRemoval) {
// Add the VPNInterface and quit
vpnFootprintService.updateVpnToDpnMapping(dpId, vpnName, primaryRd, interfaceName, null, /*ipAddressSourceValuePair*/
true);
processVpnInterfaceAdjacencies(dpId, lportTag, vpnName, primaryRd, interfaceName, vpnId, writeConfigTxn, writeOperTxn, writeInvTxn, interfaceState);
if (!isBgpVpnInternetVpn) {
VpnUtil.bindService(vpnName, interfaceName, dataBroker, false, /*isTunnelInterface*/
jobCoordinator);
}
LOG.info("processVpnInterfaceUp: Plumbed vpn interface {} onto dpn {} for vpn {}", interfaceName, dpId, vpnName);
if (interfaceManager.isExternalInterface(interfaceName)) {
processExternalVpnInterface(interfaceName, vpnName, vpnId, dpId, lportTag, writeInvTxn, NwConstants.ADD_FLOW);
}
return;
}
// FIB didn't get a chance yet to clean up this VPNInterface
// Let us give it a chance here !
LOG.info("processVpnInterfaceUp: Trying to add VPN Interface {} on dpn {} for vpn {}," + " but waiting for FIB to clean up! ", interfaceName, dpId, vpnName);
try {
Runnable notifyTask = new VpnNotifyTask();
synchronized (notifyTask) {
// Per FB's "Unconditional wait" violation, the code should really verify that the condition it
// intends to wait for is not already satisfied before calling wait. However the VpnNotifyTask is
// published here while holding the lock on it so this path will hit the wait before notify can be
// invoked.
vpnIntfMap.put(interfaceName, notifyTask);
try {
notifyTask.wait(VpnConstants.MAX_WAIT_TIME_IN_MILLISECONDS);
} catch (InterruptedException e) {
// Ignored
}
}
} finally {
vpnIntfMap.remove(interfaceName);
}
if (opVpnInterface != null) {
LOG.warn("processVpnInterfaceUp: VPN Interface {} removal on dpn {} for vpn {}" + " by FIB did not complete on time," + " bailing addition ...", interfaceName, dpId, vpnName);
VpnUtil.unsetScheduledToRemoveForVpnInterface(txRunner, interfaceName);
return;
}
// VPNInterface got removed, proceed with Add
LOG.info("processVpnInterfaceUp: Continuing to plumb vpn interface {} onto dpn {} for vpn {}", interfaceName, dpId, vpnName);
vpnFootprintService.updateVpnToDpnMapping(dpId, vpnName, primaryRd, interfaceName, null, /*ipAddressSourceValuePair*/
true);
processVpnInterfaceAdjacencies(dpId, lportTag, vpnName, primaryRd, interfaceName, vpnId, writeConfigTxn, writeOperTxn, writeInvTxn, interfaceState);
if (!isBgpVpnInternetVpn) {
VpnUtil.bindService(vpnName, interfaceName, dataBroker, false, /*isTunnelInterface*/
jobCoordinator);
}
LOG.info("processVpnInterfaceUp: Plumbed vpn interface {} onto dpn {} for vpn {} after waiting for" + " FIB to clean up", interfaceName, dpId, vpnName);
if (interfaceManager.isExternalInterface(interfaceName)) {
processExternalVpnInterface(interfaceName, vpnName, vpnId, dpId, lportTag, writeInvTxn, NwConstants.ADD_FLOW);
}
} else {
// Interface is retained in the DPN, but its Link Up.
// Advertise prefixes again for this interface to BGP
InstanceIdentifier<VpnInterface> identifier = VpnUtil.getVpnInterfaceIdentifier(vpnInterface.getName());
InstanceIdentifier<VpnInterfaceOpDataEntry> vpnInterfaceOpIdentifier = VpnUtil.getVpnInterfaceOpDataEntryIdentifier(interfaceName, vpnName);
advertiseAdjacenciesForVpnToBgp(primaryRd, dpId, vpnInterfaceOpIdentifier, vpnName, interfaceName);
// Perform similar operation as interface add event for extraroutes.
InstanceIdentifier<Adjacencies> path = identifier.augmentation(Adjacencies.class);
Optional<Adjacencies> optAdjacencies = VpnUtil.read(dataBroker, LogicalDatastoreType.CONFIGURATION, path);
if (!optAdjacencies.isPresent()) {
LOG.trace("No config adjacencies present for vpninterface {}", vpnInterface);
return;
}
List<Adjacency> adjacencies = optAdjacencies.get().getAdjacency();
for (Adjacency adjacency : adjacencies) {
if (adjacency.getAdjacencyType() == AdjacencyType.PrimaryAdjacency) {
continue;
}
// if BGPVPN Internet, filter only IPv6 Adjacencies
if (isBgpVpnInternetVpn && !VpnUtil.isAdjacencyEligibleToVpnInternet(dataBroker, adjacency)) {
continue;
}
addNewAdjToVpnInterface(vpnInterfaceOpIdentifier, primaryRd, adjacency, dpId, writeOperTxn, writeConfigTxn);
}
}
}
use of org.opendaylight.yang.gen.v1.urn.opendaylight.neutron.bgpvpns.rev150903.bgpvpns.attributes.bgpvpns.Bgpvpn in project netvirt by opendaylight.
the class NeutronvpnUtils method updateVpnInstanceWithFallback.
public void updateVpnInstanceWithFallback(String vpnName, boolean add) {
VpnInstanceOpDataEntry vpnInstanceOpDataEntry = getVpnInstanceOpDataEntryFromVpnId(vpnName);
if (vpnInstanceOpDataEntry == null) {
// BGPVPN context not found
return;
}
String routerIdUuid = getRouterIdfromVpnInstance(vpnInstanceOpDataEntry.getVrfId());
if (routerIdUuid != null) {
List<BigInteger> dpnIds = getDpnsForRouter(routerIdUuid);
if (!dpnIds.isEmpty()) {
Long vpnId = vpnInstanceOpDataEntry.getVpnId();
VpnInstanceOpDataEntry vpnOpDataEntry = getVpnInstanceOpDataEntryFromVpnId(routerIdUuid);
Long routerIdAsLong = vpnOpDataEntry.getVpnId();
if (routerIdAsLong == null) {
return;
}
for (BigInteger dpnId : dpnIds) {
if (add) {
ipV6InternetDefRt.installDefaultRoute(dpnId, vpnId, routerIdAsLong);
} else {
ipV6InternetDefRt.removeDefaultRoute(dpnId, vpnId, routerIdAsLong);
}
}
}
}
}
use of org.opendaylight.yang.gen.v1.urn.opendaylight.neutron.bgpvpns.rev150903.bgpvpns.attributes.bgpvpns.Bgpvpn in project netvirt by opendaylight.
the class VpnUtil method isAdjacencyEligibleToVpnInternet.
boolean isAdjacencyEligibleToVpnInternet(Adjacency adjacency) {
// returns true if BGPVPN Internet and adjacency is IPv6, false otherwise
boolean adjacencyEligible = false;
IpVersionChoice ipVerChoice = getIpVersionFromString(adjacency.getIpAddress());
if (ipVerChoice.isIpVersionChosen(IpVersionChoice.IPV6)) {
Subnetmap sn = getSubnetmapFromItsUuid(adjacency.getSubnetId());
if (sn != null && sn.getInternetVpnId() != null) {
adjacencyEligible = true;
}
}
return adjacencyEligible;
}
use of org.opendaylight.yang.gen.v1.urn.opendaylight.neutron.bgpvpns.rev150903.bgpvpns.attributes.bgpvpns.Bgpvpn in project netvirt by opendaylight.
the class VpnOpStatusListener method update.
@Override
@SuppressWarnings("checkstyle:IllegalCatch")
public void update(InstanceIdentifier<VpnInstanceOpDataEntry> identifier, VpnInstanceOpDataEntry original, VpnInstanceOpDataEntry update) {
LOG.info("update: Processing update for vpn {} with rd {}", update.getVpnInstanceName(), update.getVrfId());
if (update.getVpnState() == VpnInstanceOpDataEntry.VpnState.PendingDelete && vpnFootprintService.isVpnFootPrintCleared(update)) {
// Cleanup VPN data
final String vpnName = update.getVpnInstanceName();
final List<String> rds = update.getRd();
String primaryRd = update.getVrfId();
final Uint32 vpnId = vpnUtil.getVpnId(vpnName);
jobCoordinator.enqueueJob("VPN-" + update.getVpnInstanceName(), () -> {
// Two transactions are used, one for operational, one for config; we only submit the config
// transaction if the operational transaction succeeds
ListenableFuture<?> operationalFuture = txRunner.callWithNewWriteOnlyTransactionAndSubmit(Datastore.OPERATIONAL, operTx -> {
// Clean up VPNExtraRoutes Operational DS
if (rds != null && VpnUtil.isBgpVpn(vpnName, primaryRd)) {
if (update.getType() == VpnInstanceOpDataEntry.Type.L2) {
rds.parallelStream().forEach(rd -> bgpManager.deleteVrf(rd, false, AddressFamily.L2VPN));
}
if (update.getIpAddressFamilyConfigured() == VpnInstanceOpDataEntry.IpAddressFamilyConfigured.Ipv4) {
rds.parallelStream().forEach(rd -> bgpManager.deleteVrf(rd, false, AddressFamily.IPV4));
}
if (update.getIpAddressFamilyConfigured() == VpnInstanceOpDataEntry.IpAddressFamilyConfigured.Ipv6) {
rds.parallelStream().forEach(rd -> bgpManager.deleteVrf(rd, false, AddressFamily.IPV6));
}
if (update.getIpAddressFamilyConfigured() == VpnInstanceOpDataEntry.IpAddressFamilyConfigured.Ipv4AndIpv6) {
rds.parallelStream().forEach(rd -> bgpManager.deleteVrf(rd, false, AddressFamily.IPV4));
rds.parallelStream().forEach(rd -> bgpManager.deleteVrf(rd, false, AddressFamily.IPV6));
}
}
InstanceIdentifier<Vpn> vpnToExtraroute = VpnExtraRouteHelper.getVpnToExtrarouteVpnIdentifier(vpnName);
Optional<Vpn> optVpnToExtraroute = Optional.empty();
try {
optVpnToExtraroute = SingleTransactionDataBroker.syncReadOptional(dataBroker, LogicalDatastoreType.OPERATIONAL, vpnToExtraroute);
} catch (InterruptedException | ExecutionException e) {
LOG.error("update: Failed to read VpnToExtraRoute for vpn {}", vpnName);
}
if (optVpnToExtraroute.isPresent()) {
VpnUtil.removeVpnExtraRouteForVpn(vpnName, operTx);
}
if (VpnUtil.isL3VpnOverVxLan(update.getL3vni())) {
vpnUtil.removeExternalTunnelDemuxFlows(vpnName);
}
// Clean up PrefixToInterface Operational DS
Optional<VpnIds> optPrefixToIntf = Optional.empty();
try {
optPrefixToIntf = SingleTransactionDataBroker.syncReadOptional(dataBroker, LogicalDatastoreType.OPERATIONAL, VpnUtil.getPrefixToInterfaceIdentifier(vpnId));
} catch (InterruptedException | ExecutionException e) {
LOG.error("update: Failed to read PrefixToInterface for vpn {}", vpnName);
}
if (optPrefixToIntf.isPresent()) {
VpnUtil.removePrefixToInterfaceForVpnId(vpnId, operTx);
}
// Clean up L3NextHop Operational DS
InstanceIdentifier<VpnNexthops> vpnNextHops = InstanceIdentifier.builder(L3nexthop.class).child(VpnNexthops.class, new VpnNexthopsKey(vpnId)).build();
Optional<VpnNexthops> optL3nexthopForVpnId = Optional.empty();
try {
optL3nexthopForVpnId = SingleTransactionDataBroker.syncReadOptional(dataBroker, LogicalDatastoreType.OPERATIONAL, vpnNextHops);
} catch (InterruptedException | ExecutionException e) {
LOG.error("update: Failed to read VpnNextHops for vpn {}", vpnName);
}
if (optL3nexthopForVpnId.isPresent()) {
VpnUtil.removeL3nexthopForVpnId(vpnId, operTx);
}
// Clean up VPNInstanceOpDataEntry
VpnUtil.removeVpnOpInstance(primaryRd, operTx);
});
Futures.addCallback(operationalFuture, new FutureCallback<Object>() {
@Override
public void onSuccess(Object result) {
Futures.addCallback(txRunner.callWithNewWriteOnlyTransactionAndSubmit(Datastore.CONFIGURATION, confTx -> {
// Clean up VpnInstanceToVpnId from Config DS
VpnUtil.removeVpnIdToVpnInstance(vpnId, confTx);
VpnUtil.removeVpnInstanceToVpnId(vpnName, confTx);
LOG.trace("Removed vpnIdentifier for rd{} vpnname {}", primaryRd, vpnName);
// Clean up FIB Entries Config DS
// FIXME: separate out to somehow?
final ReentrantLock lock = JvmGlobalLocks.getLockForString(vpnName);
lock.lock();
try {
fibManager.removeVrfTable(primaryRd, confTx);
} finally {
lock.unlock();
}
}), new VpnOpStatusListener.PostDeleteVpnInstanceWorker(vpnName), MoreExecutors.directExecutor());
// Note: Release the of VpnId will happen in PostDeleteVpnInstancWorker only if
// operationalTxn/Config succeeds.
}
@Override
public void onFailure(Throwable throwable) {
LOG.error("Error deleting VPN {}", vpnName, throwable);
}
}, MoreExecutors.directExecutor());
LOG.info("Removed vpn data for vpnname {}", vpnName);
return Collections.singletonList(operationalFuture);
}, SystemPropertyReader.getDataStoreJobCoordinatorMaxRetries());
} else if (update.getVpnState() == VpnInstanceOpDataEntry.VpnState.Created) {
final String vpnName = update.getVpnInstanceName();
String primaryRd = update.getVrfId();
if (!VpnUtil.isBgpVpn(vpnName, primaryRd)) {
return;
}
if (original == null) {
LOG.error("VpnOpStatusListener.update: vpn {} with RD {}. add() handler already called", vpnName, primaryRd);
return;
}
if (update.getVpnTargets() == null) {
LOG.error("VpnOpStatusListener.update: vpn {} with RD {} vpnTargets not ready", vpnName, primaryRd);
return;
}
Map<VpnTargetKey, VpnTarget> vpnTargetMap = update.getVpnTargets().getVpnTarget();
List<String> ertList = new ArrayList<>();
List<String> irtList = new ArrayList<>();
if (vpnTargetMap != null) {
for (VpnTarget vpnTarget : vpnTargetMap.values()) {
if (vpnTarget.getVrfRTType() == VpnTarget.VrfRTType.ExportExtcommunity) {
ertList.add(vpnTarget.getVrfRTValue());
}
if (vpnTarget.getVrfRTType() == VpnTarget.VrfRTType.ImportExtcommunity) {
irtList.add(vpnTarget.getVrfRTValue());
}
if (vpnTarget.getVrfRTType() == VpnTarget.VrfRTType.Both) {
ertList.add(vpnTarget.getVrfRTValue());
irtList.add(vpnTarget.getVrfRTValue());
}
}
} else {
LOG.error("VpnOpStatusListener.update: vpn target list is empty, cannot add BGP" + " VPN {} RD {}", vpnName, primaryRd);
return;
}
jobCoordinator.enqueueJob("VPN-" + update.getVpnInstanceName(), () -> {
// RD update case get only updated RD list
List<String> rds = update.getRd() != null ? new ArrayList<>(update.getRd()) : new ArrayList<>();
if (original.getRd() != null && original.getRd().size() != rds.size()) {
rds.removeAll(original.getRd());
}
rds.parallelStream().forEach(rd -> {
try {
List<String> importRTList = rd.equals(primaryRd) ? irtList : emptyList();
LOG.info("VpnOpStatusListener.update: updating BGPVPN for vpn {} with RD {}" + " Type is {}, IPtype is {}, iRT {}", vpnName, primaryRd, update.getType(), update.getIpAddressFamilyConfigured(), importRTList);
int ipValue = VpnUtil.getIpFamilyValueToRemove(original, update);
switch(ipValue) {
case 4:
bgpManager.deleteVrf(rd, false, AddressFamily.IPV4);
break;
case 6:
bgpManager.deleteVrf(rd, false, AddressFamily.IPV6);
break;
case 10:
bgpManager.deleteVrf(rd, false, AddressFamily.IPV4);
bgpManager.deleteVrf(rd, false, AddressFamily.IPV6);
break;
default:
break;
}
/* Update vrf entry with newly added RD list. VPN does not support for
* deleting existing RDs
*/
if (original.getRd().size() != update.getRd().size()) {
ipValue = VpnUtil.getIpFamilyValueToAdd(original, update);
switch(ipValue) {
case 4:
bgpManager.addVrf(rd, importRTList, ertList, AddressFamily.IPV4);
break;
case 6:
bgpManager.addVrf(rd, importRTList, ertList, AddressFamily.IPV6);
break;
case 10:
bgpManager.addVrf(rd, importRTList, ertList, AddressFamily.IPV4);
bgpManager.addVrf(rd, importRTList, ertList, AddressFamily.IPV6);
break;
default:
break;
}
}
} catch (RuntimeException e) {
LOG.error("VpnOpStatusListener.update: Exception when updating VRF to BGP for vpn {} rd {}", vpnName, rd, e);
}
});
return emptyList();
});
}
}
use of org.opendaylight.yang.gen.v1.urn.opendaylight.neutron.bgpvpns.rev150903.bgpvpns.attributes.bgpvpns.Bgpvpn in project netvirt by opendaylight.
the class SubnetmapChangeListener method update.
@Override
// TODO Clean up the exception handling
@SuppressWarnings("checkstyle:IllegalCatch")
public void update(InstanceIdentifier<Subnetmap> identifier, Subnetmap subnetmapOriginal, Subnetmap subnetmapUpdate) {
LOG.debug("update: method - key {}, original {}, update {}", identifier, subnetmapOriginal, subnetmapUpdate);
Uuid subnetId = subnetmapUpdate.getId();
Network network = vpnUtil.getNeutronNetwork(subnetmapUpdate.getNetworkId());
if (network == null) {
LOG.error("update: network was not found for subnetId {}", subnetId.getValue());
return;
}
jobCoordinator.enqueueJob("SUBNETROUTE-" + subnetId, () -> {
List<ListenableFuture<Void>> futures = Collections.emptyList();
String elanInstanceName = subnetmapUpdate.getNetworkId().getValue();
long elanTag = getElanTag(elanInstanceName);
if (elanTag == 0L) {
LOG.error("update: unable to fetch elantag from ElanInstance {} for subnetId {}", elanInstanceName, subnetId);
return futures;
}
updateVlanDataEntry(subnetmapOriginal.getVpnId(), subnetmapUpdate.getVpnId(), subnetmapUpdate, subnetmapOriginal, elanInstanceName);
if (VpnUtil.getIsExternal(network)) {
LOG.debug("update: provider subnetwork {} is handling in " + "ExternalSubnetVpnInstanceListener", subnetId.getValue());
return futures;
}
// update on BGPVPN or InternalVPN change
Uuid vpnIdOld = subnetmapOriginal.getVpnId();
Uuid vpnIdNew = subnetmapUpdate.getVpnId();
if (!Objects.equals(vpnIdOld, vpnIdNew)) {
LOG.info("update: update subnetOpDataEntry for subnet {} imported in VPN", subnetmapUpdate.getId().getValue());
updateSubnetmapOpDataEntry(subnetmapOriginal.getVpnId(), subnetmapUpdate.getVpnId(), subnetmapUpdate, subnetmapOriginal, elanTag);
}
// update on Internet VPN Id change
Uuid inetVpnIdOld = subnetmapOriginal.getInternetVpnId();
Uuid inetVpnIdNew = subnetmapUpdate.getInternetVpnId();
if (!Objects.equals(inetVpnIdOld, inetVpnIdNew)) {
LOG.info("update: update subnetOpDataEntry for subnet {} imported in InternetVPN", subnetmapUpdate.getId().getValue());
updateSubnetmapOpDataEntry(inetVpnIdOld, inetVpnIdNew, subnetmapUpdate, subnetmapOriginal, elanTag);
}
// update on PortList change
List<Uuid> oldPortList;
List<Uuid> newPortList;
newPortList = subnetmapUpdate.getPortList() != null ? subnetmapUpdate.getPortList() : new ArrayList<>();
oldPortList = subnetmapOriginal.getPortList() != null ? subnetmapOriginal.getPortList() : new ArrayList<>();
if (newPortList.size() == oldPortList.size()) {
return futures;
}
LOG.info("update: update port list for subnet {}", subnetmapUpdate.getId().getValue());
if (newPortList.size() > oldPortList.size()) {
for (Uuid portId : newPortList) {
if (!oldPortList.contains(portId)) {
vpnSubnetRouteHandler.onPortAddedToSubnet(subnetmapUpdate, portId);
}
}
} else {
for (Uuid portId : oldPortList) {
if (!newPortList.contains(portId)) {
vpnSubnetRouteHandler.onPortRemovedFromSubnet(subnetmapUpdate, portId);
}
}
}
return futures;
});
}
Aggregations