/* * Copyright 2016-present Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onosproject.incubator.store.virtual.impl; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.FluentIterable; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SettableFuture; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.Service; import org.onlab.util.Tools; import org.onosproject.incubator.net.virtual.NetworkId; import org.onosproject.incubator.net.virtual.VirtualNetworkFlowRuleStore; import org.onosproject.net.DeviceId; import org.onosproject.net.flow.CompletedBatchOperation; import org.onosproject.net.flow.DefaultFlowEntry; import org.onosproject.net.flow.FlowEntry; import org.onosproject.net.flow.FlowId; import org.onosproject.net.flow.FlowRule; import org.onosproject.net.flow.FlowRuleBatchEntry; import org.onosproject.net.flow.FlowRuleBatchEvent; import org.onosproject.net.flow.FlowRuleBatchOperation; import org.onosproject.net.flow.FlowRuleBatchRequest; import org.onosproject.net.flow.FlowRuleEvent; import org.onosproject.net.flow.FlowRuleStoreDelegate; import org.onosproject.net.flow.StoredFlowEntry; import org.onosproject.net.flow.TableStatisticsEntry; import org.onosproject.store.service.StorageService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_REMOVED; import static org.slf4j.LoggerFactory.getLogger; /** * Implementation of the virtual network flow rule store to manage inventory of * virtual flow rules using trivial in-memory implementation. */ //TODO: support distributed flowrule store for virtual networks @Component(immediate = true) @Service public class SimpleVirtualFlowRuleStore extends AbstractVirtualStore<FlowRuleBatchEvent, FlowRuleStoreDelegate> implements VirtualNetworkFlowRuleStore { private final Logger log = getLogger(getClass()); @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected StorageService storageService; private final ConcurrentMap<NetworkId, ConcurrentMap<DeviceId, ConcurrentMap<FlowId, List<StoredFlowEntry>>>> flowEntries = new ConcurrentHashMap<>(); private final AtomicInteger localBatchIdGen = new AtomicInteger(); private static final int DEFAULT_PENDING_FUTURE_TIMEOUT_MINUTES = 5; @Property(name = "pendingFutureTimeoutMinutes", intValue = DEFAULT_PENDING_FUTURE_TIMEOUT_MINUTES, label = "Expiration time after an entry is created that it should be automatically removed") private int pendingFutureTimeoutMinutes = DEFAULT_PENDING_FUTURE_TIMEOUT_MINUTES; private Cache<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures = CacheBuilder.newBuilder() .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES) .removalListener(new TimeoutFuture()) .build(); @Activate public void activate() { log.info("Started"); } @Deactivate public void deactivate() { flowEntries.clear(); log.info("Stopped"); } @Modified public void modified(ComponentContext context) { readComponentConfiguration(context); // Reset Cache and copy all. Cache<Integer, SettableFuture<CompletedBatchOperation>> prevFutures = pendingFutures; pendingFutures = CacheBuilder.newBuilder() .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES) .removalListener(new TimeoutFuture()) .build(); pendingFutures.putAll(prevFutures.asMap()); } /** * Extracts properties from the component configuration context. * * @param context the component context */ private void readComponentConfiguration(ComponentContext context) { Dictionary<?, ?> properties = context.getProperties(); Integer newPendingFutureTimeoutMinutes = Tools.getIntegerProperty(properties, "pendingFutureTimeoutMinutes"); if (newPendingFutureTimeoutMinutes == null) { pendingFutureTimeoutMinutes = DEFAULT_PENDING_FUTURE_TIMEOUT_MINUTES; log.info("Pending future timeout is not configured, " + "using current value of {}", pendingFutureTimeoutMinutes); } else { pendingFutureTimeoutMinutes = newPendingFutureTimeoutMinutes; log.info("Configured. Pending future timeout is configured to {}", pendingFutureTimeoutMinutes); } } @Override public int getFlowRuleCount(NetworkId networkId) { int sum = 0; if (flowEntries.get(networkId) == null) { return 0; } for (ConcurrentMap<FlowId, List<StoredFlowEntry>> ft : flowEntries.get(networkId).values()) { for (List<StoredFlowEntry> fes : ft.values()) { sum += fes.size(); } } return sum; } @Override public FlowEntry getFlowEntry(NetworkId networkId, FlowRule rule) { return getFlowEntryInternal(networkId, rule.deviceId(), rule); } @Override public Iterable<FlowEntry> getFlowEntries(NetworkId networkId, DeviceId deviceId) { return FluentIterable.from(getFlowTable(networkId, deviceId).values()) .transformAndConcat(Collections::unmodifiableList); } @Override public void storeFlowRule(NetworkId networkId, FlowRule rule) { storeFlowRuleInternal(networkId, rule); } @Override public void storeBatch(NetworkId networkId, FlowRuleBatchOperation batchOperation) { List<FlowRuleBatchEntry> toAdd = new ArrayList<>(); List<FlowRuleBatchEntry> toRemove = new ArrayList<>(); for (FlowRuleBatchEntry entry : batchOperation.getOperations()) { final FlowRule flowRule = entry.target(); if (entry.operator().equals(FlowRuleBatchEntry.FlowRuleOperation.ADD)) { if (!getFlowEntries(networkId, flowRule.deviceId(), flowRule.id()).contains(flowRule)) { storeFlowRule(networkId, flowRule); toAdd.add(entry); } } else if (entry.operator().equals(FlowRuleBatchEntry.FlowRuleOperation.REMOVE)) { if (getFlowEntries(networkId, flowRule.deviceId(), flowRule.id()).contains(flowRule)) { deleteFlowRule(networkId, flowRule); toRemove.add(entry); } } else { throw new UnsupportedOperationException("Unsupported operation type"); } } if (toAdd.isEmpty() && toRemove.isEmpty()) { notifyDelegate(networkId, FlowRuleBatchEvent.completed( new FlowRuleBatchRequest(batchOperation.id(), Collections.emptySet()), new CompletedBatchOperation(true, Collections.emptySet(), batchOperation.deviceId()))); return; } SettableFuture<CompletedBatchOperation> r = SettableFuture.create(); final int futureId = localBatchIdGen.incrementAndGet(); pendingFutures.put(futureId, r); toAdd.addAll(toRemove); notifyDelegate(networkId, FlowRuleBatchEvent.requested( new FlowRuleBatchRequest(batchOperation.id(), Sets.newHashSet(toAdd)), batchOperation.deviceId())); } @Override public void batchOperationComplete(NetworkId networkId, FlowRuleBatchEvent event) { final Long batchId = event.subject().batchId(); SettableFuture<CompletedBatchOperation> future = pendingFutures.getIfPresent(batchId); if (future != null) { future.set(event.result()); pendingFutures.invalidate(batchId); } notifyDelegate(networkId, event); } @Override public void deleteFlowRule(NetworkId networkId, FlowRule rule) { List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id()); synchronized (entries) { for (StoredFlowEntry entry : entries) { if (entry.equals(rule)) { synchronized (entry) { entry.setState(FlowEntry.FlowEntryState.PENDING_REMOVE); } } } } } @Override public FlowRuleEvent addOrUpdateFlowRule(NetworkId networkId, FlowEntry rule) { // check if this new rule is an update to an existing entry List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id()); synchronized (entries) { for (StoredFlowEntry stored : entries) { if (stored.equals(rule)) { synchronized (stored) { //FIXME modification of "stored" flow entry outside of flow table stored.setBytes(rule.bytes()); stored.setLife(rule.life()); stored.setPackets(rule.packets()); if (stored.state() == FlowEntry.FlowEntryState.PENDING_ADD) { stored.setState(FlowEntry.FlowEntryState.ADDED); // TODO: Do we need to change `rule` state? return new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED, rule); } return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule); } } } } // should not reach here // storeFlowRule was expected to be called log.error("FlowRule was not found in store {} to update", rule); return null; } @Override public FlowRuleEvent removeFlowRule(NetworkId networkId, FlowEntry rule) { // This is where one could mark a rule as removed and still keep it in the store. final DeviceId did = rule.deviceId(); List<StoredFlowEntry> entries = getFlowEntries(networkId, did, rule.id()); synchronized (entries) { if (entries.remove(rule)) { return new FlowRuleEvent(RULE_REMOVED, rule); } } return null; } @Override public FlowRuleEvent pendingFlowRule(NetworkId networkId, FlowEntry rule) { List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id()); synchronized (entries) { for (StoredFlowEntry entry : entries) { if (entry.equals(rule) && entry.state() != FlowEntry.FlowEntryState.PENDING_ADD) { synchronized (entry) { entry.setState(FlowEntry.FlowEntryState.PENDING_ADD); return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule); } } } } return null; } @Override public void purgeFlowRule(NetworkId networkId, DeviceId deviceId) { flowEntries.get(networkId).remove(deviceId); } @Override public void purgeFlowRules(NetworkId networkId) { flowEntries.get(networkId).clear(); } @Override public FlowRuleEvent updateTableStatistics(NetworkId networkId, DeviceId deviceId, List<TableStatisticsEntry> tableStats) { //TODO: Table operations are not supported yet return null; } @Override public Iterable<TableStatisticsEntry> getTableStatistics(NetworkId networkId, DeviceId deviceId) { //TODO: Table operations are not supported yet return null; } /** * Returns the flow table for specified device. * * @param networkId identifier of the virtual network * @param deviceId identifier of the virtual device * @return Map representing Flow Table of given device. */ private ConcurrentMap<FlowId, List<StoredFlowEntry>> getFlowTable(NetworkId networkId, DeviceId deviceId) { return flowEntries .computeIfAbsent(networkId, n -> new ConcurrentHashMap<>()) .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); } private List<StoredFlowEntry> getFlowEntries(NetworkId networkId, DeviceId deviceId, FlowId flowId) { final ConcurrentMap<FlowId, List<StoredFlowEntry>> flowTable = getFlowTable(networkId, deviceId); List<StoredFlowEntry> r = flowTable.get(flowId); if (r == null) { final List<StoredFlowEntry> concurrentlyAdded; r = new CopyOnWriteArrayList<>(); concurrentlyAdded = flowTable.putIfAbsent(flowId, r); if (concurrentlyAdded != null) { return concurrentlyAdded; } } return r; } private FlowEntry getFlowEntryInternal(NetworkId networkId, DeviceId deviceId, FlowRule rule) { List<StoredFlowEntry> fes = getFlowEntries(networkId, deviceId, rule.id()); for (StoredFlowEntry fe : fes) { if (fe.equals(rule)) { return fe; } } return null; } private void storeFlowRuleInternal(NetworkId networkId, FlowRule rule) { StoredFlowEntry f = new DefaultFlowEntry(rule); final DeviceId did = f.deviceId(); final FlowId fid = f.id(); List<StoredFlowEntry> existing = getFlowEntries(networkId, did, fid); synchronized (existing) { for (StoredFlowEntry fe : existing) { if (fe.equals(rule)) { // was already there? ignore return; } } // new flow rule added existing.add(f); } } private static final class TimeoutFuture implements RemovalListener<Integer, SettableFuture<CompletedBatchOperation>> { @Override public void onRemoval(RemovalNotification<Integer, SettableFuture<CompletedBatchOperation>> notification) { // wrapping in ExecutionException to support Future.get if (notification.wasEvicted()) { notification.getValue() .setException(new ExecutionException("Timed out", new TimeoutException())); } } } }