blob: 2126dfafdfd7cd1dd7450d41acfc0c3eae5a0c91 [file] [log] [blame]
Frank Wang0e805082017-07-21 14:37:35 +08001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2017-present Open Networking Foundation
Frank Wang0e805082017-07-21 14:37:35 +08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andrea Campanella0288c872017-08-07 18:32:51 +020017package org.onosproject.drivers.p4runtime;
Frank Wang0e805082017-07-21 14:37:35 +080018
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +053019import com.google.common.cache.LoadingCache;
20import com.google.common.cache.CacheBuilder;
21import com.google.common.cache.CacheLoader;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020022import com.google.common.collect.ImmutableList;
23import com.google.common.collect.Lists;
Carmelo Cascone7f75be42017-09-07 14:37:02 +020024import io.grpc.StatusRuntimeException;
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080025import org.onlab.util.SharedExecutors;
26import org.onosproject.drivers.p4runtime.mirror.P4RuntimeTableMirror;
27import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
Frank Wang0e805082017-07-21 14:37:35 +080028import org.onosproject.net.flow.DefaultFlowEntry;
29import org.onosproject.net.flow.FlowEntry;
30import org.onosproject.net.flow.FlowRule;
31import org.onosproject.net.flow.FlowRuleProgrammable;
Carmelo Cascone87892e22017-11-13 16:01:29 -080032import org.onosproject.net.pi.model.PiCounterId;
Frank Wang0e805082017-07-21 14:37:35 +080033import org.onosproject.net.pi.model.PiPipelineInterpreter;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020034import org.onosproject.net.pi.model.PiPipelineModel;
Carmelo Cascone87892e22017-11-13 16:01:29 -080035import org.onosproject.net.pi.model.PiTableId;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020036import org.onosproject.net.pi.model.PiTableModel;
Carmelo Cascone7f75be42017-09-07 14:37:02 +020037import org.onosproject.net.pi.runtime.PiCounterCellData;
38import org.onosproject.net.pi.runtime.PiCounterCellId;
Frank Wang0e805082017-07-21 14:37:35 +080039import org.onosproject.net.pi.runtime.PiTableEntry;
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080040import org.onosproject.net.pi.runtime.PiTableEntryHandle;
41import org.onosproject.net.pi.service.PiFlowRuleTranslator;
42import org.onosproject.net.pi.service.PiTranslatedEntity;
Carmelo Cascone326ad2d2017-11-28 18:09:13 -080043import org.onosproject.net.pi.service.PiTranslationException;
Frank Wang0e805082017-07-21 14:37:35 +080044import org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType;
Frank Wang0e805082017-07-21 14:37:35 +080045
46import java.util.Collection;
47import java.util.Collections;
Carmelo Cascone7f75be42017-09-07 14:37:02 +020048import java.util.Map;
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +053049import java.util.List;
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080050import java.util.Optional;
Carmelo Cascone7f75be42017-09-07 14:37:02 +020051import java.util.Set;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020052import java.util.concurrent.ExecutionException;
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +053053import java.util.concurrent.TimeUnit;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020054import java.util.concurrent.locks.Lock;
55import java.util.concurrent.locks.ReentrantLock;
Carmelo Casconefe99be92017-09-11 21:55:54 +020056import java.util.stream.Collectors;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020057
58import static com.google.common.collect.Lists.newArrayList;
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080059import static java.util.Collections.singleton;
Andrea Campanella0288c872017-08-07 18:32:51 +020060import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.APPLY;
61import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.REMOVE;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020062import static org.onosproject.net.flow.FlowEntry.FlowEntryState.ADDED;
Carmelo Cascone87892e22017-11-13 16:01:29 -080063import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
64import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
65import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY;
Frank Wang0e805082017-07-21 14:37:35 +080066
67/**
Carmelo Casconee3a7c742017-09-01 01:25:52 +020068 * Implementation of the flow rule programmable behaviour for P4Runtime.
Frank Wang0e805082017-07-21 14:37:35 +080069 */
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080070public class P4RuntimeFlowRuleProgrammable
71 extends AbstractP4RuntimeHandlerBehaviour
72 implements FlowRuleProgrammable {
Frank Wang0e805082017-07-21 14:37:35 +080073
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080074 // When updating an existing rule, if true, we issue a DELETE operation
75 // before inserting the new one, otherwise we issue a MODIFY operation. This
76 // is useful fore devices that do not support MODIFY operations for table
77 // entries.
Carmelo Casconefe99be92017-09-11 21:55:54 +020078 // TODO: make this attribute configurable by child drivers (e.g. BMv2 or Tofino)
Carmelo Cascone2308e522017-08-25 02:35:12 +020079 private boolean deleteEntryBeforeUpdate = true;
80
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080081 // If true, we ignore re-installing rules that are already exists the
82 // device, i.e. same match key and action.
83 // FIXME: can remove this check as soon as the multi-apply-per-same-flow rule bug is fixed.
84 private boolean checkStoreBeforeUpdate = true;
Carmelo Cascone2308e522017-08-25 02:35:12 +020085
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080086 // If true, we avoid querying the device and return what's already known by
87 // the ONOS store.
Yi Tseng63a82df2017-11-17 16:41:40 -080088 private boolean ignoreDeviceWhenGet = true;
Carmelo Casconefe99be92017-09-11 21:55:54 +020089
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -080090 /* If true, we read all direct counters of a table with one request.
91 Otherwise, we send as many requests as the number of table entries. */
92 // FIXME: set to true as soon as the feature is implemented in P4Runtime.
Carmelo Cascone7f75be42017-09-07 14:37:02 +020093 private boolean readAllDirectCounters = false;
94
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +053095 private static final int TABLE_ENTRY_LOCK_EXPIRE_TIME_IN_MIN = 10;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +020096
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +053097 // Needed to synchronize operations over the same table entry.
98 private static final LoadingCache<PiTableEntryHandle, Lock>
99 ENTRY_LOCKS = CacheBuilder.newBuilder()
100 .expireAfterAccess(TABLE_ENTRY_LOCK_EXPIRE_TIME_IN_MIN, TimeUnit.MINUTES)
101 .build(new CacheLoader<PiTableEntryHandle, Lock>() {
102 @Override
103 public Lock load(PiTableEntryHandle handle) {
104 return new ReentrantLock();
105 }
106 });
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200107 private PiPipelineModel pipelineModel;
108 private PiPipelineInterpreter interpreter;
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800109 private P4RuntimeTableMirror tableMirror;
110 private PiFlowRuleTranslator translator;
Frank Wang0e805082017-07-21 14:37:35 +0800111
Carmelo Casconee3a7c742017-09-01 01:25:52 +0200112 @Override
113 protected boolean setupBehaviour() {
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200114
Carmelo Casconee3a7c742017-09-01 01:25:52 +0200115 if (!super.setupBehaviour()) {
Frank Wang0e805082017-07-21 14:37:35 +0800116 return false;
117 }
118
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200119 if (!device.is(PiPipelineInterpreter.class)) {
120 log.warn("Unable to get interpreter of {}", deviceId);
Frank Wang0e805082017-07-21 14:37:35 +0800121 return false;
122 }
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200123 interpreter = device.as(PiPipelineInterpreter.class);
Carmelo Casconee3a7c742017-09-01 01:25:52 +0200124 pipelineModel = pipeconf.pipelineModel();
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800125 tableMirror = handler().get(P4RuntimeTableMirror.class);
126 translator = piTranslationService.flowRuleTranslator();
Frank Wang0e805082017-07-21 14:37:35 +0800127 return true;
128 }
129
130 @Override
131 public Collection<FlowEntry> getFlowEntries() {
132
Carmelo Casconee3a7c742017-09-01 01:25:52 +0200133 if (!setupBehaviour()) {
Frank Wang0e805082017-07-21 14:37:35 +0800134 return Collections.emptyList();
135 }
136
Carmelo Casconefe99be92017-09-11 21:55:54 +0200137 if (ignoreDeviceWhenGet) {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800138 return getFlowEntriesFromMirror();
Carmelo Casconefe99be92017-09-11 21:55:54 +0200139 }
140
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800141 final ImmutableList.Builder<FlowEntry> result = ImmutableList.builder();
142 final List<PiTableEntry> inconsistentEntries = Lists.newArrayList();
Frank Wang0e805082017-07-21 14:37:35 +0800143
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200144 for (PiTableModel tableModel : pipelineModel.tables()) {
145
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800146 final PiTableId piTableId = tableModel.id();
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200147
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800148 // Read table entries.
149 final Collection<PiTableEntry> installedEntries;
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200150 try {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800151 // TODO: optimize by dumping entries and counters in parallel
152 // From ALL tables with the same request.
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200153 installedEntries = client.dumpTable(piTableId, pipeconf).get();
154 } catch (InterruptedException | ExecutionException e) {
Carmelo Cascone7f75be42017-09-07 14:37:02 +0200155 if (!(e.getCause() instanceof StatusRuntimeException)) {
156 // gRPC errors are logged in the client.
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800157 log.error("Exception while dumping table {} of {}",
158 piTableId, deviceId, e);
Carmelo Cascone7f75be42017-09-07 14:37:02 +0200159 }
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800160 continue; // next table
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200161 }
162
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800163 if (installedEntries.size() == 0) {
164 continue; // next table
165 }
166
167 // Read table direct counters (if any).
168 final Map<PiTableEntry, PiCounterCellData> counterCellMap;
169 if (interpreter.mapTableCounter(piTableId).isPresent()) {
170 PiCounterId piCounterId = interpreter.mapTableCounter(piTableId).get();
171 counterCellMap = readEntryCounters(piCounterId, installedEntries);
172 } else {
Carmelo Cascone7f75be42017-09-07 14:37:02 +0200173 counterCellMap = Collections.emptyMap();
174 }
175
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800176 // Forge flow entries with counter values.
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200177 for (PiTableEntry installedEntry : installedEntries) {
178
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800179 final FlowEntry flowEntry = forgeFlowEntry(
180 installedEntry, counterCellMap.get(installedEntry));
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200181
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800182 if (flowEntry == null) {
183 // Entry is on device but unknown to translation service or
184 // device mirror. Inconsistent. Mark for removal.
185 // TODO: make this behaviour configurable
186 // In some cases it's fine for the device to have rules
187 // that were not installed by us.
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200188 inconsistentEntries.add(installedEntry);
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800189 } else {
190 result.add(flowEntry);
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200191 }
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200192 }
193 }
194
195 if (inconsistentEntries.size() > 0) {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800196 // Async clean up inconsistent entries.
197 SharedExecutors.getSingleThreadExecutor().execute(
198 () -> cleanUpInconsistentEntries(inconsistentEntries));
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200199 }
200
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800201 return result.build();
Frank Wang0e805082017-07-21 14:37:35 +0800202 }
203
204 @Override
205 public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200206 return processFlowRules(rules, APPLY);
Frank Wang0e805082017-07-21 14:37:35 +0800207 }
208
209 @Override
210 public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200211 return processFlowRules(rules, REMOVE);
Frank Wang0e805082017-07-21 14:37:35 +0800212 }
213
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800214 private FlowEntry forgeFlowEntry(PiTableEntry entry,
215 PiCounterCellData cellData) {
216 final PiTableEntryHandle handle = PiTableEntryHandle
217 .of(deviceId, entry);
218 final Optional<PiTranslatedEntity<FlowRule, PiTableEntry>>
219 translatedEntity = translator.lookup(handle);
220 final TimedEntry<PiTableEntry> timedEntry = tableMirror.get(handle);
221
222 if (!translatedEntity.isPresent()) {
223 log.debug("Handle not found in store: {}", handle);
224 return null;
225 }
226
227 if (timedEntry == null) {
228 log.debug("Handle not found in device mirror: {}", handle);
229 return null;
230 }
231
232 if (cellData != null) {
233 return new DefaultFlowEntry(translatedEntity.get().original(),
234 ADDED, timedEntry.lifeSec(), cellData.bytes(),
235 cellData.bytes());
236 } else {
237 return new DefaultFlowEntry(translatedEntity.get().original(),
238 ADDED, timedEntry.lifeSec(), 0, 0);
239 }
240 }
241
242 private Collection<FlowEntry> getFlowEntriesFromMirror() {
243 return tableMirror.getAll(deviceId).stream()
244 .map(timedEntry -> forgeFlowEntry(
245 timedEntry.entry(), null))
246 .collect(Collectors.toList());
247 }
248
249 private void cleanUpInconsistentEntries(Collection<PiTableEntry> piEntries) {
250 log.warn("Found {} entries from {} not on translation store, removing them...",
251 piEntries.size(), deviceId);
252 piEntries.forEach(entry -> {
253 log.debug(entry.toString());
254 applyEntry(PiTableEntryHandle.of(deviceId, entry),
255 entry, null, REMOVE);
256 });
257 }
258
259 private Collection<FlowRule> processFlowRules(Collection<FlowRule> rules,
260 Operation driverOperation) {
Frank Wang0e805082017-07-21 14:37:35 +0800261
Carmelo Casconee3a7c742017-09-01 01:25:52 +0200262 if (!setupBehaviour()) {
Frank Wang0e805082017-07-21 14:37:35 +0800263 return Collections.emptyList();
264 }
265
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800266 final ImmutableList.Builder<FlowRule> result = ImmutableList.builder();
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200267
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800268 // TODO: send writes in bulk (e.g. all entries to insert, modify or delete).
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200269 // Instead of calling the client for each one of them.
270
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800271 for (FlowRule ruleToApply : rules) {
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200272
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800273 final PiTableEntry piEntryToApply;
Frank Wang0e805082017-07-21 14:37:35 +0800274 try {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800275 piEntryToApply = translator.translate(ruleToApply, pipeconf);
Carmelo Cascone326ad2d2017-11-28 18:09:13 -0800276 } catch (PiTranslationException e) {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800277 log.warn("Unable to translate flow rule for pipeconf '{}': {} - {}",
278 pipeconf.id(), e.getMessage(), ruleToApply);
279 // Next rule.
280 continue;
Frank Wang0e805082017-07-21 14:37:35 +0800281 }
Frank Wang0e805082017-07-21 14:37:35 +0800282
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800283 final PiTableEntryHandle handle = PiTableEntryHandle
284 .of(deviceId, piEntryToApply);
Frank Wang0e805082017-07-21 14:37:35 +0800285
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800286 // Serialize operations over the same match key/table/device ID.
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +0530287 ENTRY_LOCKS.getUnchecked(handle).lock();
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200288 try {
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800289 if (applyEntry(handle, piEntryToApply,
290 ruleToApply, driverOperation)) {
291 result.add(ruleToApply);
Frank Wang0e805082017-07-21 14:37:35 +0800292 }
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200293 } finally {
Manjunath Vanaraj59ad6572017-12-26 11:10:57 +0530294 ENTRY_LOCKS.getUnchecked(handle).unlock();
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200295 }
296 }
297
Carmelo Cascone6a0b5a32017-11-20 23:08:32 -0800298 return result.build();
299 }
300
301 /**
302 * Applies the given entry to the device, and returns true if the operation
303 * was successful, false otherwise.
304 */
305 private boolean applyEntry(PiTableEntryHandle handle,
306 PiTableEntry piEntryToApply,
307 FlowRule ruleToApply,
308 Operation driverOperation) {
309 // Depending on the driver operation, and if a matching rule exists on
310 // the device, decide which P4 Runtime write operation to perform for
311 // this entry.
312 final TimedEntry<PiTableEntry> piEntryOnDevice = tableMirror.get(handle);
313 final WriteOperationType p4Operation;
314 if (driverOperation == APPLY) {
315 if (piEntryOnDevice == null) {
316 // Entry is first-timer.
317 p4Operation = INSERT;
318 } else {
319 if (checkStoreBeforeUpdate
320 && piEntryToApply.action().equals(piEntryOnDevice.entry().action())) {
321 log.debug("Ignoring re-apply of existing entry: {}", piEntryToApply);
322 p4Operation = null;
323 } else if (deleteEntryBeforeUpdate) {
324 // Some devices return error when updating existing
325 // entries. If requested, remove entry before
326 // re-inserting the modified one.
327 applyEntry(handle, piEntryOnDevice.entry(), null, REMOVE);
328 p4Operation = INSERT;
329 } else {
330 p4Operation = MODIFY;
331 }
332 }
333 } else {
334 p4Operation = DELETE;
335 }
336
337 if (p4Operation != null) {
338 if (writeEntry(piEntryToApply, p4Operation)) {
339 updateStores(handle, piEntryToApply, ruleToApply, p4Operation);
340 return true;
341 } else {
342 return false;
343 }
344 } else {
345 // If no operation, let's pretend we applied the rule to the device.
346 return true;
347 }
348 }
349
350 /**
351 * Performs a write operation on the device.
352 */
353 private boolean writeEntry(PiTableEntry entry,
354 WriteOperationType p4Operation) {
355 try {
356 if (client.writeTableEntries(
357 newArrayList(entry), p4Operation, pipeconf).get()) {
358 return true;
359 } else {
360 log.warn("Unable to {} table entry in {}: {}",
361 p4Operation.name(), deviceId, entry);
362 }
363 } catch (InterruptedException | ExecutionException e) {
364 log.warn("Exception while performing {} table entry operation:",
365 p4Operation, e);
366 }
367 return false;
368 }
369
370 private void updateStores(PiTableEntryHandle handle,
371 PiTableEntry entry,
372 FlowRule rule,
373 WriteOperationType p4Operation) {
374 switch (p4Operation) {
375 case INSERT:
376 case MODIFY:
377 tableMirror.put(handle, entry);
378 translator.learn(handle, new PiTranslatedEntity<>(rule, entry, handle));
379 break;
380 case DELETE:
381 tableMirror.remove(handle);
382 translator.forget(handle);
383 break;
384 default:
385 throw new IllegalArgumentException(
386 "Unknown operation " + p4Operation.name());
387 }
388 }
389
390 private Map<PiTableEntry, PiCounterCellData> readEntryCounters(
391 PiCounterId counterId, Collection<PiTableEntry> tableEntries) {
392 Collection<PiCounterCellData> cellDatas;
393 try {
394 if (readAllDirectCounters) {
395 cellDatas = client.readAllCounterCells(
396 singleton(counterId), pipeconf).get();
397 } else {
398 Set<PiCounterCellId> cellIds = tableEntries.stream()
399 .map(entry -> PiCounterCellId.ofDirect(counterId, entry))
400 .collect(Collectors.toSet());
401 cellDatas = client.readCounterCells(cellIds, pipeconf).get();
402 }
403 return cellDatas.stream()
404 .collect(Collectors.toMap(c -> c.cellId().tableEntry(), c -> c));
405 } catch (InterruptedException | ExecutionException e) {
406 if (!(e.getCause() instanceof StatusRuntimeException)) {
407 // gRPC errors are logged in the client.
408 log.error("Exception while reading counter '{}' from {}: {}",
409 counterId, deviceId, e);
410 }
411 return Collections.emptyMap();
412 }
Carmelo Cascone0b22d8f2017-07-31 07:22:27 +0200413 }
414
415 enum Operation {
416 APPLY, REMOVE
Frank Wang0e805082017-07-21 14:37:35 +0800417 }
Carmelo Cascone87892e22017-11-13 16:01:29 -0800418}