blob: 66d0b5313657b7159967e4670ff6bdc5e9ea129b [file] [log] [blame]
Thomas Vachuska58de4162015-09-10 16:15:33 -07001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2016-present Open Networking Laboratory
Thomas Vachuska58de4162015-09-10 16:15:33 -07003 *
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 */
Saurav Dasdecd7a62015-05-16 22:39:47 -070016package org.onosproject.driver.pipeline;
17
Saurav Dasdecd7a62015-05-16 22:39:47 -070018import org.onlab.osgi.ServiceDirectory;
Jonathan Hart63eeac32016-06-20 15:55:16 -070019import org.onlab.packet.EthType;
Saurav Dasdecd7a62015-05-16 22:39:47 -070020import org.onlab.packet.Ethernet;
Jonathan Hart29be97d2016-02-02 14:23:48 -080021import org.onlab.packet.IpPrefix;
Saurav Dasdecd7a62015-05-16 22:39:47 -070022import org.onlab.packet.VlanId;
23import org.onlab.util.KryoNamespace;
24import org.onosproject.core.ApplicationId;
25import org.onosproject.core.CoreService;
26import org.onosproject.net.DeviceId;
Jonathan Hart63eeac32016-06-20 15:55:16 -070027import org.onosproject.net.PortNumber;
Saurav Dasdecd7a62015-05-16 22:39:47 -070028import org.onosproject.net.behaviour.NextGroup;
29import org.onosproject.net.behaviour.Pipeliner;
30import org.onosproject.net.behaviour.PipelinerContext;
31import org.onosproject.net.driver.AbstractHandlerBehaviour;
32import org.onosproject.net.flow.DefaultFlowRule;
33import org.onosproject.net.flow.DefaultTrafficSelector;
34import org.onosproject.net.flow.DefaultTrafficTreatment;
35import org.onosproject.net.flow.FlowRule;
36import org.onosproject.net.flow.FlowRuleOperations;
37import org.onosproject.net.flow.FlowRuleOperationsContext;
38import org.onosproject.net.flow.FlowRuleService;
39import org.onosproject.net.flow.TrafficSelector;
40import org.onosproject.net.flow.TrafficTreatment;
41import org.onosproject.net.flow.criteria.Criteria;
42import org.onosproject.net.flow.criteria.Criterion;
43import org.onosproject.net.flow.criteria.EthCriterion;
44import org.onosproject.net.flow.criteria.EthTypeCriterion;
45import org.onosproject.net.flow.criteria.IPCriterion;
46import org.onosproject.net.flow.criteria.PortCriterion;
47import org.onosproject.net.flow.criteria.VlanIdCriterion;
Saurav Das49cb5a12016-01-16 22:54:07 -080048import org.onosproject.net.flow.instructions.Instruction;
49import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
Saurav Dasdecd7a62015-05-16 22:39:47 -070050import org.onosproject.net.flowobjective.FilteringObjective;
51import org.onosproject.net.flowobjective.FlowObjectiveStore;
52import org.onosproject.net.flowobjective.ForwardingObjective;
53import org.onosproject.net.flowobjective.NextObjective;
54import org.onosproject.net.flowobjective.Objective;
55import org.onosproject.net.flowobjective.ObjectiveError;
Saurav Dasdecd7a62015-05-16 22:39:47 -070056import org.onosproject.store.serializers.KryoNamespaces;
Jonathan Harte54bdbf2015-11-17 11:29:37 -080057import org.slf4j.Logger;
58
59import java.util.ArrayList;
60import java.util.Collection;
61import java.util.Collections;
Saurav Das24431192016-03-07 19:13:00 -080062import java.util.List;
Sho SHIMIZU45906042016-01-13 23:05:54 -080063import java.util.Objects;
Jonathan Harte54bdbf2015-11-17 11:29:37 -080064
Jonathan Hart29be97d2016-02-02 14:23:48 -080065import static org.onlab.util.Tools.delay;
Jonathan Harte54bdbf2015-11-17 11:29:37 -080066import static org.slf4j.LoggerFactory.getLogger;
Saurav Dasdecd7a62015-05-16 22:39:47 -070067
68/**
69 * Simple 2-Table Pipeline for Software/NPU based routers. This pipeline
70 * does not forward IP traffic to next-hop groups. Instead it forwards traffic
71 * using OF FlowMod actions.
72 */
73public class SoftRouterPipeline extends AbstractHandlerBehaviour implements Pipeliner {
74
75 protected static final int FILTER_TABLE = 0;
76 protected static final int FIB_TABLE = 1;
77
78 private static final int DROP_PRIORITY = 0;
79 private static final int DEFAULT_PRIORITY = 0x8000;
80 private static final int HIGHEST_PRIORITY = 0xffff;
81
82 private ServiceDirectory serviceDirectory;
83 protected FlowRuleService flowRuleService;
84 private CoreService coreService;
85 private FlowObjectiveStore flowObjectiveStore;
86 protected DeviceId deviceId;
87 protected ApplicationId appId;
88 private ApplicationId driverId;
Saurav Dasdecd7a62015-05-16 22:39:47 -070089
90 private KryoNamespace appKryo = new KryoNamespace.Builder()
Jonathan Hart888eeda2016-05-20 13:42:26 -070091 .register(KryoNamespaces.API)
92 .register(DummyGroup.class)
93 .build();
Saurav Dasdecd7a62015-05-16 22:39:47 -070094
95 private final Logger log = getLogger(getClass());
96
97 @Override
98 public void init(DeviceId deviceId, PipelinerContext context) {
99 this.serviceDirectory = context.directory();
100 this.deviceId = deviceId;
101 coreService = serviceDirectory.get(CoreService.class);
102 flowRuleService = serviceDirectory.get(FlowRuleService.class);
103 flowObjectiveStore = context.store();
104 driverId = coreService.registerApplication(
Jonathan Harte54bdbf2015-11-17 11:29:37 -0800105 "org.onosproject.driver.SoftRouterPipeline");
Jonathan Hart6344f572015-12-15 08:26:25 -0800106
Saurav Dasdecd7a62015-05-16 22:39:47 -0700107 initializePipeline();
108 }
109
110 @Override
111 public void filter(FilteringObjective filteringObjective) {
112 if (filteringObjective.type() == FilteringObjective.Type.PERMIT) {
113 processFilter(filteringObjective,
114 filteringObjective.op() == Objective.Operation.ADD,
115 filteringObjective.appId());
116 } else {
117 fail(filteringObjective, ObjectiveError.UNSUPPORTED);
118 }
119 }
120
121 @Override
122 public void forward(ForwardingObjective fwd) {
123 Collection<FlowRule> rules;
124 FlowRuleOperations.Builder flowOpsBuilder = FlowRuleOperations.builder();
125
126 rules = processForward(fwd);
127 switch (fwd.op()) {
128 case ADD:
129 rules.stream()
Sho SHIMIZU45906042016-01-13 23:05:54 -0800130 .filter(Objects::nonNull)
Saurav Dasdecd7a62015-05-16 22:39:47 -0700131 .forEach(flowOpsBuilder::add);
132 break;
133 case REMOVE:
134 rules.stream()
Sho SHIMIZU45906042016-01-13 23:05:54 -0800135 .filter(Objects::nonNull)
Saurav Dasdecd7a62015-05-16 22:39:47 -0700136 .forEach(flowOpsBuilder::remove);
137 break;
138 default:
139 fail(fwd, ObjectiveError.UNKNOWN);
140 log.warn("Unknown forwarding type {}", fwd.op());
141 }
142
143
144 flowRuleService.apply(flowOpsBuilder.build(new FlowRuleOperationsContext() {
145 @Override
146 public void onSuccess(FlowRuleOperations ops) {
147 pass(fwd);
148 }
149
150 @Override
151 public void onError(FlowRuleOperations ops) {
152 fail(fwd, ObjectiveError.FLOWINSTALLATIONFAILED);
153 }
154 }));
155
156 }
157
158 @Override
159 public void next(NextObjective nextObjective) {
160 switch (nextObjective.type()) {
161 case SIMPLE:
162 Collection<TrafficTreatment> treatments = nextObjective.next();
163 if (treatments.size() != 1) {
164 log.error("Next Objectives of type Simple should only have a "
165 + "single Traffic Treatment. Next Objective Id:{}", nextObjective.id());
166 fail(nextObjective, ObjectiveError.BADPARAMS);
167 return;
168 }
169 processSimpleNextObjective(nextObjective);
170 break;
171 case HASHED:
172 case BROADCAST:
173 case FAILOVER:
174 fail(nextObjective, ObjectiveError.UNSUPPORTED);
175 log.warn("Unsupported next objective type {}", nextObjective.type());
176 break;
177 default:
178 fail(nextObjective, ObjectiveError.UNKNOWN);
179 log.warn("Unknown next objective type {}", nextObjective.type());
180 }
181 }
182
183 private void pass(Objective obj) {
Sho SHIMIZUef7e2902016-02-12 18:38:29 -0800184 obj.context().ifPresent(context -> context.onSuccess(obj));
Saurav Dasdecd7a62015-05-16 22:39:47 -0700185 }
186
187 private void fail(Objective obj, ObjectiveError error) {
Sho SHIMIZUef7e2902016-02-12 18:38:29 -0800188 obj.context().ifPresent(context -> context.onError(obj, error));
Saurav Dasdecd7a62015-05-16 22:39:47 -0700189 }
190
Saurav Dasdecd7a62015-05-16 22:39:47 -0700191 private void initializePipeline() {
192 //Drop rules for both tables
193 TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
194 TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
195 FlowRuleOperations.Builder ops = FlowRuleOperations.builder();
196
197 treatment.drop();
198
199 FlowRule rule = DefaultFlowRule.builder()
200 .forDevice(deviceId)
201 .withSelector(selector.build())
202 .withTreatment(treatment.build())
203 .withPriority(DROP_PRIORITY)
204 .fromApp(driverId)
205 .makePermanent()
206 .forTable(FILTER_TABLE)
207 .build();
208 ops = ops.add(rule);
209
210 rule = DefaultFlowRule.builder().forDevice(deviceId)
211 .withSelector(selector.build())
212 .withTreatment(treatment.build())
213 .withPriority(DROP_PRIORITY)
214 .fromApp(driverId)
215 .makePermanent()
216 .forTable(FIB_TABLE)
217 .build();
218 ops = ops.add(rule);
219
220 flowRuleService.apply(ops.build(new FlowRuleOperationsContext() {
221 @Override
222 public void onSuccess(FlowRuleOperations ops) {
223 log.info("Provisioned drop rules in both tables");
224 }
225
226 @Override
227 public void onError(FlowRuleOperations ops) {
228 log.info("Failed to provision drop rules");
229 }
230 }));
231 }
232
233 private void processFilter(FilteringObjective filt, boolean install,
234 ApplicationId applicationId) {
235 // This driver only processes filtering criteria defined with switch
236 // ports as the key
Jonathan Hart6344f572015-12-15 08:26:25 -0800237 PortCriterion p;
238 EthCriterion e = null;
239 VlanIdCriterion v = null;
240
Saurav Dasdecd7a62015-05-16 22:39:47 -0700241 if (!filt.key().equals(Criteria.dummy()) &&
242 filt.key().type() == Criterion.Type.IN_PORT) {
243 p = (PortCriterion) filt.key();
244 } else {
245 log.warn("No key defined in filtering objective from app: {}. Not"
246 + "processing filtering objective", applicationId);
247 fail(filt, ObjectiveError.UNKNOWN);
248 return;
249 }
250
251 // convert filtering conditions for switch-intfs into flowrules
252 FlowRuleOperations.Builder ops = FlowRuleOperations.builder();
253 for (Criterion c : filt.conditions()) {
gauravadc90042016-05-09 23:17:05 +0530254 if (c.type() == Criterion.Type.ETH_DST ||
255 c.type() == Criterion.Type.ETH_DST_MASKED) {
Saurav Dasdecd7a62015-05-16 22:39:47 -0700256 e = (EthCriterion) c;
257 } else if (c.type() == Criterion.Type.VLAN_VID) {
258 v = (VlanIdCriterion) c;
Saurav Dasdecd7a62015-05-16 22:39:47 -0700259 } else {
260 log.error("Unsupported filter {}", c);
261 fail(filt, ObjectiveError.UNSUPPORTED);
262 return;
263 }
264 }
265
Jonathan Hartf8035d32016-06-16 16:23:26 -0700266 log.debug("Modifying Port/VLAN/MAC filtering rules in filter table: {}/{}/{}",
Saurav Dasdecd7a62015-05-16 22:39:47 -0700267 p.port(), v.vlanId(), e.mac());
268 TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
269 TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
270 selector.matchInPort(p.port());
Jonathan Hart6344f572015-12-15 08:26:25 -0800271
gauravadc90042016-05-09 23:17:05 +0530272 //Multicast MAC
273 if (e.mask() != null) {
274 selector.matchEthDstMasked(e.mac(), e.mask());
275 } else {
276 selector.matchEthDst(e.mac());
277 }
Jonathan Hart6344f572015-12-15 08:26:25 -0800278 selector.matchVlanId(v.vlanId());
Saurav Dasdecd7a62015-05-16 22:39:47 -0700279 selector.matchEthType(Ethernet.TYPE_IPV4);
Jonathan Hartca47cd72015-12-13 12:31:09 -0800280 if (!v.vlanId().equals(VlanId.NONE)) {
281 treatment.popVlan();
282 }
Jonathan Hart6344f572015-12-15 08:26:25 -0800283 treatment.transition(FIB_TABLE);
Saurav Dasdecd7a62015-05-16 22:39:47 -0700284 FlowRule rule = DefaultFlowRule.builder()
285 .forDevice(deviceId)
286 .withSelector(selector.build())
287 .withTreatment(treatment.build())
288 .withPriority(DEFAULT_PRIORITY)
289 .fromApp(applicationId)
290 .makePermanent()
291 .forTable(FILTER_TABLE).build();
Saurav Dasdecd7a62015-05-16 22:39:47 -0700292
Saurav Dasdecd7a62015-05-16 22:39:47 -0700293 ops = install ? ops.add(rule) : ops.remove(rule);
294 // apply filtering flow rules
295 flowRuleService.apply(ops.build(new FlowRuleOperationsContext() {
296 @Override
297 public void onSuccess(FlowRuleOperations ops) {
298 log.info("Applied filtering rules");
299 pass(filt);
300 }
301
302 @Override
303 public void onError(FlowRuleOperations ops) {
304 log.info("Failed to apply filtering rules");
305 fail(filt, ObjectiveError.FLOWINSTALLATIONFAILED);
306 }
307 }));
308 }
309
310 private Collection<FlowRule> processForward(ForwardingObjective fwd) {
311 switch (fwd.flag()) {
312 case SPECIFIC:
313 return processSpecific(fwd);
314 case VERSATILE:
315 return processVersatile(fwd);
316 default:
317 fail(fwd, ObjectiveError.UNKNOWN);
318 log.warn("Unknown forwarding flag {}", fwd.flag());
319 }
320 return Collections.emptySet();
321 }
322
323 /**
Saurav Das49cb5a12016-01-16 22:54:07 -0800324 * SoftRouter has a single versatile table - the filter table.
325 * This table can be used to filter entries that reach the next table (FIB table).
326 * It can also be used to punt packets to the controller and/or bypass
327 * the FIB table to forward out of a port.
Saurav Dasdecd7a62015-05-16 22:39:47 -0700328 *
329 * @param fwd The forwarding objective of type versatile
330 * @return A collection of flow rules meant to be delivered to the flowrule
331 * subsystem. May return empty collection in case of failures.
332 */
333 private Collection<FlowRule> processVersatile(ForwardingObjective fwd) {
Saurav Das49cb5a12016-01-16 22:54:07 -0800334 log.debug("Received versatile fwd: to next:{}", fwd.nextId());
Jonathan Hart6344f572015-12-15 08:26:25 -0800335 Collection<FlowRule> flowrules = new ArrayList<>();
Saurav Das49cb5a12016-01-16 22:54:07 -0800336 if (fwd.nextId() == null && fwd.treatment() == null) {
337 log.error("Forwarding objective {} from {} must contain "
338 + "nextId or Treatment", fwd.selector(), fwd.appId());
339 return Collections.emptySet();
340 }
Jonathan Hart63eeac32016-06-20 15:55:16 -0700341
342 int tableId = FILTER_TABLE;
343
344 // A punt rule for IP traffic should be directed to the FIB table
345 // so that it only takes effect if the packet misses the FIB rules
346 if (fwd.treatment() != null && containsPunt(fwd.treatment()) &&
347 fwd.selector() != null && matchesIp(fwd.selector())) {
348 tableId = FIB_TABLE;
349 }
350
Saurav Das49cb5a12016-01-16 22:54:07 -0800351 TrafficTreatment.Builder ttBuilder = DefaultTrafficTreatment.builder();
352 if (fwd.treatment() != null) {
353 fwd.treatment().immediate().forEach(ins -> ttBuilder.add(ins));
354 }
355 //convert nextId to flow actions
356 if (fwd.nextId() != null) {
357 // only acceptable value is output to port
358 NextGroup next = flowObjectiveStore.getNextGroup(fwd.nextId());
359 if (next == null) {
360 log.error("next-id {} does not exist in store", fwd.nextId());
361 return Collections.emptySet();
362 }
363 TrafficTreatment nt = appKryo.deserialize(next.data());
364 if (nt == null) {
365 log.error("Error in deserializing next-id {}", fwd.nextId());
366 return Collections.emptySet();
367 }
368 for (Instruction ins : nt.allInstructions()) {
369 if (ins instanceof OutputInstruction) {
370 ttBuilder.add(ins);
371 }
372 }
373 }
Jonathan Hart6344f572015-12-15 08:26:25 -0800374
375 FlowRule rule = DefaultFlowRule.builder()
376 .withSelector(fwd.selector())
Saurav Das49cb5a12016-01-16 22:54:07 -0800377 .withTreatment(ttBuilder.build())
Jonathan Hart63eeac32016-06-20 15:55:16 -0700378 .forTable(tableId)
Jonathan Hart6344f572015-12-15 08:26:25 -0800379 .makePermanent()
380 .forDevice(deviceId)
381 .fromApp(fwd.appId())
382 .withPriority(fwd.priority())
383 .build();
384
385 flowrules.add(rule);
386
Saurav Dasdecd7a62015-05-16 22:39:47 -0700387 return flowrules;
388 }
389
Jonathan Hart63eeac32016-06-20 15:55:16 -0700390 private boolean containsPunt(TrafficTreatment treatment) {
391 return treatment.immediate().stream()
392 .anyMatch(i -> i.type().equals(Instruction.Type.OUTPUT)
393 && ((OutputInstruction) i).port().equals(PortNumber.CONTROLLER));
394 }
395
396 private boolean matchesIp(TrafficSelector selector) {
397 EthTypeCriterion c = (EthTypeCriterion) selector.getCriterion(Criterion.Type.ETH_TYPE);
398 return c != null && c.ethType().equals(EthType.EtherType.IPV4.ethType());
399 }
400
Saurav Dasdecd7a62015-05-16 22:39:47 -0700401 /**
402 * SoftRouter has a single specific table - the FIB Table. It emulates
403 * LPM matching of dstIP by using higher priority flows for longer prefixes.
404 * Flows are forwarded using flow-actions
405 *
406 * @param fwd The forwarding objective of type simple
407 * @return A collection of flow rules meant to be delivered to the flowrule
408 * subsystem. Typically the returned collection has a single flowrule.
409 * May return empty collection in case of failures.
410 *
411 */
412 private Collection<FlowRule> processSpecific(ForwardingObjective fwd) {
Saurav Das49cb5a12016-01-16 22:54:07 -0800413 log.debug("Processing specific forwarding objective to next:{}", fwd.nextId());
Saurav Dasdecd7a62015-05-16 22:39:47 -0700414 TrafficSelector selector = fwd.selector();
415 EthTypeCriterion ethType =
416 (EthTypeCriterion) selector.getCriterion(Criterion.Type.ETH_TYPE);
417 // XXX currently supporting only the L3 unicast table
alshabibcaf1ca22015-06-25 15:18:16 -0700418 if (ethType == null || ethType.ethType().toShort() != Ethernet.TYPE_IPV4) {
Saurav Dasdecd7a62015-05-16 22:39:47 -0700419 fail(fwd, ObjectiveError.UNSUPPORTED);
420 return Collections.emptySet();
421 }
422
Jonathan Hart29be97d2016-02-02 14:23:48 -0800423 IpPrefix ipPrefix = ((IPCriterion)
424 selector.getCriterion(Criterion.Type.IPV4_DST)).ip();
425
426 TrafficSelector.Builder filteredSelector = DefaultTrafficSelector.builder()
427 .matchEthType(Ethernet.TYPE_IPV4);
428
429 if (ipPrefix.prefixLength() != 0) {
430 filteredSelector.matchIPDst(ipPrefix);
431 }
Saurav Dasdecd7a62015-05-16 22:39:47 -0700432
433 TrafficTreatment tt = null;
434 if (fwd.nextId() != null) {
435 NextGroup next = flowObjectiveStore.getNextGroup(fwd.nextId());
436 if (next == null) {
437 log.error("next-id {} does not exist in store", fwd.nextId());
438 return Collections.emptySet();
439 }
440 tt = appKryo.deserialize(next.data());
441 if (tt == null) {
442 log.error("Error in deserializing next-id {}", fwd.nextId());
443 return Collections.emptySet();
444 }
445 }
446
447 FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
448 .fromApp(fwd.appId())
449 .withPriority(fwd.priority())
450 .forDevice(deviceId)
Jonathan Hart29be97d2016-02-02 14:23:48 -0800451 .withSelector(filteredSelector.build());
Jonathan Hart3930f632015-10-19 12:12:51 -0700452
453 if (tt != null) {
454 ruleBuilder.withTreatment(tt);
455 }
Saurav Dasdecd7a62015-05-16 22:39:47 -0700456
457 if (fwd.permanent()) {
458 ruleBuilder.makePermanent();
459 } else {
460 ruleBuilder.makeTemporary(fwd.timeout());
461 }
462
463 ruleBuilder.forTable(FIB_TABLE);
464 return Collections.singletonList(ruleBuilder.build());
465 }
466
467 /**
468 * Next Objectives are stored as dummy groups for retrieval later
469 * when Forwarding Objectives reference the next objective id. At that point
470 * the dummy group is fetched from the distributed store and the enclosed
471 * treatment is applied as a flow rule action.
472 *
alshabibcaf1ca22015-06-25 15:18:16 -0700473 * @param nextObj the next objective of type simple
Saurav Dasdecd7a62015-05-16 22:39:47 -0700474 */
475 private void processSimpleNextObjective(NextObjective nextObj) {
476 // Simple next objective has a single treatment (not a collection)
Saurav Das49cb5a12016-01-16 22:54:07 -0800477 log.debug("Received nextObj {}", nextObj.id());
478 // delay processing to emulate group creation
479 delay(50);
Saurav Das6c44a632015-05-30 22:05:22 -0700480 TrafficTreatment treatment = nextObj.next().iterator().next();
Saurav Dasdecd7a62015-05-16 22:39:47 -0700481 flowObjectiveStore.putNextGroup(nextObj.id(),
Saurav Das6c44a632015-05-30 22:05:22 -0700482 new DummyGroup(treatment));
Saurav Dasdecd7a62015-05-16 22:39:47 -0700483 }
484
Saurav Dasdecd7a62015-05-16 22:39:47 -0700485 private class DummyGroup implements NextGroup {
486 TrafficTreatment nextActions;
487
488 public DummyGroup(TrafficTreatment next) {
489 this.nextActions = next;
490 }
491
492 @Override
493 public byte[] data() {
494 return appKryo.serialize(nextActions);
495 }
496
497 }
498
Saurav Das24431192016-03-07 19:13:00 -0800499 @Override
500 public List<String> getNextMappings(NextGroup nextGroup) {
501 // nextObjectives converted to flow-actions not groups
502 return Collections.emptyList();
503 }
504
Saurav Dasdecd7a62015-05-16 22:39:47 -0700505}