blob: fbfd9b73123da15500839c6c7bc8f5af0fea9bd0 [file] [log] [blame]
Carmelo Cascone00a59962017-06-16 17:51:49 +09001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2017-present Open Networking Foundation
Carmelo Cascone00a59962017-06-16 17:51:49 +09003 *
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
17package org.onosproject.net.pi.impl;
18
19import com.google.common.collect.Maps;
20import com.google.common.collect.Sets;
21import org.onlab.util.ImmutableByteSequence;
22import org.onosproject.net.Device;
23import org.onosproject.net.flow.FlowRule;
24import org.onosproject.net.flow.IndexTableId;
25import org.onosproject.net.flow.TrafficSelector;
26import org.onosproject.net.flow.TrafficTreatment;
27import org.onosproject.net.flow.criteria.Criterion;
28import org.onosproject.net.flow.criteria.PiCriterion;
29import org.onosproject.net.flow.instructions.Instruction;
30import org.onosproject.net.flow.instructions.PiInstruction;
31import org.onosproject.net.pi.model.PiActionModel;
32import org.onosproject.net.pi.model.PiActionParamModel;
33import org.onosproject.net.pi.model.PiPipeconf;
34import org.onosproject.net.pi.model.PiPipelineInterpreter;
35import org.onosproject.net.pi.model.PiPipelineModel;
36import org.onosproject.net.pi.model.PiTableMatchFieldModel;
37import org.onosproject.net.pi.model.PiTableModel;
38import org.onosproject.net.pi.runtime.PiAction;
39import org.onosproject.net.pi.runtime.PiActionParam;
40import org.onosproject.net.pi.runtime.PiExactFieldMatch;
41import org.onosproject.net.pi.runtime.PiFieldMatch;
42import org.onosproject.net.pi.runtime.PiHeaderFieldId;
43import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
Carmelo Cascone0e896a02017-07-31 07:22:27 +020044import org.onosproject.net.pi.runtime.PiMatchKey;
Carmelo Cascone00a59962017-06-16 17:51:49 +090045import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
46import org.onosproject.net.pi.runtime.PiTableAction;
47import org.onosproject.net.pi.runtime.PiTableEntry;
48import org.onosproject.net.pi.runtime.PiTableId;
49import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
50import org.slf4j.Logger;
51import org.slf4j.LoggerFactory;
52
53import java.util.Collection;
54import java.util.Map;
55import java.util.Optional;
56import java.util.Set;
57import java.util.StringJoiner;
58
59import static java.lang.String.format;
60import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
61import static org.onlab.util.ImmutableByteSequence.fit;
62import static org.onosproject.net.flow.criteria.Criterion.Type.PROTOCOL_INDEPENDENT;
63import static org.onosproject.net.pi.impl.CriterionTranslatorHelper.translateCriterion;
64import static org.onosproject.net.pi.runtime.PiFlowRuleTranslationService.PiFlowRuleTranslationException;
65
66/**
67 * Implementation of flow rule translation logic.
68 */
69final class PiFlowRuleTranslator {
70
Carmelo Casconea3d811c2017-08-22 01:03:45 +020071 public static final int MAX_PI_PRIORITY = (int) Math.pow(2, 24);
Carmelo Cascone00a59962017-06-16 17:51:49 +090072 private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslationServiceImpl.class);
73
74 private PiFlowRuleTranslator() {
75 // Hide constructor.
76 }
77
78 static PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf, Device device)
79 throws PiFlowRuleTranslationException {
80
81 PiPipelineModel pipelineModel = pipeconf.pipelineModel();
82
83 // Retrieve interpreter, if any.
Carmelo Cascone00a59962017-06-16 17:51:49 +090084 final PiPipelineInterpreter interpreter;
Carmelo Casconea62ac3d2017-08-30 03:19:00 +020085
86 if (device != null) {
87 interpreter = device.is(PiPipelineInterpreter.class) ? device.as(PiPipelineInterpreter.class) : null;
88 } else {
89 // The case of device == null should be admitted only during unit testing.
90 // In any other case, the interpreter should be constructed using the device.as() method to make sure that
91 // behaviour's handler/data attributes are correctly populated.
92 // FIXME: modify test class PiFlowRuleTranslatorTest to avoid passing null device
93 // I.e. we need to create a device object that supports is/as method for obtaining the interpreter.
94 log.warn("translateFlowRule() called with device == null, is this a unit test?");
95 try {
96 interpreter = (PiPipelineInterpreter) pipeconf.implementation(PiPipelineInterpreter.class)
97 .orElse(null)
98 .newInstance();
99 } catch (InstantiationException | IllegalAccessException e) {
100 throw new RuntimeException(format("Unable to instantiate interpreter of pipeconf %s", pipeconf.id()));
101 }
Carmelo Cascone00a59962017-06-16 17:51:49 +0900102 }
103
104 PiTableId piTableId;
105 switch (rule.table().type()) {
106 case PIPELINE_INDEPENDENT:
107 piTableId = (PiTableId) rule.table();
108 break;
109 case INDEX:
110 IndexTableId indexId = (IndexTableId) rule.table();
111 if (interpreter == null) {
112 throw new PiFlowRuleTranslationException(format(
113 "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
114 } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
115 throw new PiFlowRuleTranslationException(format(
116 "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
117 } else {
118 piTableId = interpreter.mapFlowRuleTableId(indexId.id()).get();
119 }
120 break;
121 default:
122 throw new PiFlowRuleTranslationException(format(
123 "Unrecognized table ID type %s", rule.table().type().name()));
124 }
125
126 PiTableModel table = pipelineModel.table(piTableId.toString())
127 .orElseThrow(() -> new PiFlowRuleTranslationException(format(
128 "Not such a table in pipeline model: %s", piTableId)));
129
130 /* Translate selector */
131 Collection<PiFieldMatch> fieldMatches = buildFieldMatches(interpreter, rule.selector(), table);
132
133 /* Translate treatment */
Yi Tseng82512da2017-08-16 19:46:36 -0700134 PiTableAction piTableAction = buildAction(rule.treatment(), interpreter, piTableId);
135 piTableAction = typeCheckAction(piTableAction, table);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900136
137 PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
138
Carmelo Casconea3d811c2017-08-22 01:03:45 +0200139 // In the P4 world 0 is the highest priority, in ONOS the lowest one.
140 // FIXME: move priority conversion to the driver, where different constraints might apply
141 // e.g. less bits for encoding priority in TCAM-based implementations.
142 int newPriority;
143 if (rule.priority() > MAX_PI_PRIORITY) {
144 log.warn("Flow rule priority too big, setting translated priority to max value {}: {}",
145 MAX_PI_PRIORITY, rule);
146 newPriority = 0;
147 } else {
148 newPriority = MAX_PI_PRIORITY - rule.priority();
149 }
Carmelo Cascone00a59962017-06-16 17:51:49 +0900150
151 tableEntryBuilder
152 .forTable(piTableId)
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200153 .withPriority(newPriority)
Carmelo Cascone0e896a02017-07-31 07:22:27 +0200154 .withMatchKey(PiMatchKey.builder()
155 .addFieldMatches(fieldMatches)
156 .build())
Yi Tseng82512da2017-08-16 19:46:36 -0700157 .withAction(piTableAction);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900158
159 if (!rule.isPermanent()) {
160 if (table.supportsAging()) {
161 tableEntryBuilder.withTimeout((double) rule.timeout());
162 } else {
163 log.warn("Flow rule is temporary, but table '{}' doesn't support " +
164 "aging, translating to permanent.", table.name());
165 }
166
167 }
168
169 return tableEntryBuilder.build();
170 }
171
172 /**
173 * Builds a PI action out of the given treatment, optionally using the given interpreter.
174 */
Yi Tseng82512da2017-08-16 19:46:36 -0700175 private static PiTableAction buildAction(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400176 PiTableId tableId)
Carmelo Cascone00a59962017-06-16 17:51:49 +0900177 throws PiFlowRuleTranslationException {
178
179 PiTableAction piTableAction = null;
180
181 // If treatment has only one instruction of type PiInstruction, use that.
182 for (Instruction inst : treatment.allInstructions()) {
183 if (inst.type() == Instruction.Type.PROTOCOL_INDEPENDENT) {
184 if (treatment.allInstructions().size() == 1) {
185 piTableAction = ((PiInstruction) inst).action();
186 } else {
187 throw new PiFlowRuleTranslationException(format(
188 "Unable to translate treatment, found multiple instructions " +
189 "of which one is protocol-independent: %s", treatment));
190 }
191 }
192 }
193
194 if (piTableAction == null && interpreter != null) {
195 // No PiInstruction, use interpreter to build action.
196 try {
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400197 piTableAction = interpreter.mapTreatment(treatment, tableId);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900198 } catch (PiPipelineInterpreter.PiInterpreterException e) {
199 throw new PiFlowRuleTranslationException(
200 "Interpreter was unable to translate treatment. " + e.getMessage());
201 }
202 }
203
204 if (piTableAction == null) {
205 // No PiInstruction, no interpreter. It's time to give up.
206 throw new PiFlowRuleTranslationException(
207 "Unable to translate treatment, neither an interpreter or a "
208 + "protocol-independent instruction were provided.");
209 }
210
Yi Tseng82512da2017-08-16 19:46:36 -0700211 return piTableAction;
Carmelo Cascone00a59962017-06-16 17:51:49 +0900212 }
213
214 /**
Yi Tseng82512da2017-08-16 19:46:36 -0700215 * Checks that the given PI table action is suitable for the given table
216 * model and returns a new action instance with parameters well-sized,
217 * according to the table model. If not suitable, throws an exception explaining why.
Carmelo Cascone00a59962017-06-16 17:51:49 +0900218 */
Yi Tseng82512da2017-08-16 19:46:36 -0700219 private static PiTableAction typeCheckAction(PiTableAction piTableAction, PiTableModel table)
Carmelo Cascone00a59962017-06-16 17:51:49 +0900220 throws PiFlowRuleTranslationException {
Yi Tseng82512da2017-08-16 19:46:36 -0700221 switch (piTableAction.type()) {
222 case ACTION:
223 return checkPiAction((PiAction) piTableAction, table);
224 default:
225 // FIXME: should we check? how?
226 return piTableAction;
Carmelo Cascone00a59962017-06-16 17:51:49 +0900227
Yi Tseng82512da2017-08-16 19:46:36 -0700228 }
229 }
230
231 private static PiTableAction checkPiAction(PiAction piAction, PiTableModel table)
232 throws PiFlowRuleTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900233 // Table supports this action?
234 PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
235 () -> new PiFlowRuleTranslationException(format("Not such action '%s' for table '%s'",
236 piAction.id(), table.name())));
237
238 // Is the number of runtime parameters correct?
239 if (actionModel.params().size() != piAction.parameters().size()) {
240 throw new PiFlowRuleTranslationException(format(
241 "Wrong number of runtime parameters for action '%s', expected %d but found %d",
242 actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
243 }
244
245 // Forge a new action instance with well-sized parameters.
246 // The same comment as in typeCheckFieldMatch() about duplicating field match instances applies here.
247 PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
248 for (PiActionParam param : piAction.parameters()) {
249 PiActionParamModel paramModel = actionModel.param(param.id().name())
250 .orElseThrow(() -> new PiFlowRuleTranslationException(format(
251 "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
252 try {
253 newActionBuilder.withParameter(new PiActionParam(param.id(),
254 fit(param.value(), paramModel.bitWidth())));
255 } catch (ByteSequenceTrimException e) {
256 throw new PiFlowRuleTranslationException(format(
257 "Size mismatch for parameter '%s' of action '%s': %s",
258 param.id(), piAction.id(), e.getMessage()));
259 }
260 }
261
262 return newActionBuilder.build();
263 }
264
265 /**
266 * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
267 * field matches returned are guaranteed to be compatible for the given table model.
268 */
269 private static Collection<PiFieldMatch> buildFieldMatches(PiPipelineInterpreter interpreter,
270 TrafficSelector selector, PiTableModel tableModel)
271 throws PiFlowRuleTranslationException {
272
273 Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
274
275 // If present, find a PiCriterion and get its field matches as a map. Otherwise, use an empty map.
276 Map<PiHeaderFieldId, PiFieldMatch> piCriterionFields = selector.criteria().stream()
277 .filter(c -> c.type().equals(PROTOCOL_INDEPENDENT))
278 .map(c -> (PiCriterion) c)
279 .findFirst()
280 .map(PiCriterion::fieldMatches)
281 .map(c -> {
282 Map<PiHeaderFieldId, PiFieldMatch> fieldMap = Maps.newHashMap();
283 c.forEach(fieldMatch -> fieldMap.put(fieldMatch.fieldId(), fieldMatch));
284 return fieldMap;
285 })
286 .orElse(Maps.newHashMap());
287
288 Set<Criterion> translatedCriteria = Sets.newHashSet();
289 Set<Criterion> ignoredCriteria = Sets.newHashSet();
290 Set<PiHeaderFieldId> usedPiCriterionFields = Sets.newHashSet();
291 Set<PiHeaderFieldId> ignoredPiCriterionFields = Sets.newHashSet();
292
293 for (PiTableMatchFieldModel fieldModel : tableModel.matchFields()) {
294
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200295 PiHeaderFieldId fieldId = PiHeaderFieldId.of(fieldModel.field().header().name(),
Carmelo Cascone00a59962017-06-16 17:51:49 +0900296 fieldModel.field().type().name(),
297 fieldModel.field().header().index());
298
299 int bitWidth = fieldModel.field().type().bitWidth();
300 int fieldByteWidth = (int) Math.ceil((double) bitWidth / 8);
301
302 Optional<Criterion.Type> criterionType =
303 interpreter == null
304 ? Optional.empty()
305 : interpreter.mapPiHeaderFieldId(fieldId);
306
307 Criterion criterion = criterionType.map(selector::getCriterion).orElse(null);
308
309 if (!piCriterionFields.containsKey(fieldId) && criterion == null) {
310 // Neither a field in PiCriterion is available nor a Criterion mapping is possible.
311 // Can ignore if the match is ternary or LPM.
312 switch (fieldModel.matchType()) {
313 case TERNARY:
314 // Wildcard the whole field.
315 fieldMatches.put(fieldId, new PiTernaryFieldMatch(
316 fieldId,
317 ImmutableByteSequence.ofZeros(fieldByteWidth),
318 ImmutableByteSequence.ofZeros(fieldByteWidth)));
319 break;
320 case LPM:
321 // LPM with prefix 0
322 fieldMatches.put(fieldId, new PiLpmFieldMatch(fieldId,
323 ImmutableByteSequence.ofZeros(fieldByteWidth),
324 0));
325 break;
326 // FIXME: Can we handle the case of RANGE or VALID match?
327 default:
328 throw new PiFlowRuleTranslationException(format(
329 "No value found for required match field '%s'", fieldId));
330 }
331 // Next field.
332 continue;
333 }
334
335 PiFieldMatch fieldMatch = null;
336
337 if (criterion != null) {
338 // Criterion mapping is possible for this field id.
339 try {
340 fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
341 translatedCriteria.add(criterion);
342 } catch (PiFlowRuleTranslationException ex) {
343 // Ignore exception if the same field was found in PiCriterion.
344 if (piCriterionFields.containsKey(fieldId)) {
345 ignoredCriteria.add(criterion);
346 } else {
347 throw ex;
348 }
349 }
350 }
351
352 if (piCriterionFields.containsKey(fieldId)) {
353 // Field was found in PiCriterion.
354 if (fieldMatch != null) {
355 // Field was already translated from other criterion.
356 // Throw exception only if we are trying to match on different values of the same field...
357 if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
358 throw new PiFlowRuleTranslationException(format(
359 "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
360 "what found in PiCriterion.", fieldId, criterion.type()));
361 }
362 ignoredPiCriterionFields.add(fieldId);
363 } else {
364 fieldMatch = piCriterionFields.get(fieldId);
365 fieldMatch = typeCheckFieldMatch(fieldMatch, fieldModel);
366 usedPiCriterionFields.add(fieldId);
367 }
368 }
369
370 fieldMatches.put(fieldId, fieldMatch);
371 }
372
373 // Check if all criteria have been translated.
374 StringJoiner skippedCriteriaJoiner = new StringJoiner(", ");
375 selector.criteria().stream()
376 .filter(c -> !c.type().equals(PROTOCOL_INDEPENDENT))
377 .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
378 .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
379 if (skippedCriteriaJoiner.length() > 0) {
380 throw new PiFlowRuleTranslationException(format(
381 "The following criteria cannot be translated for table '%s': %s",
382 tableModel.name(), skippedCriteriaJoiner.toString()));
383 }
384
385 // Check if all fields found in PiCriterion have been used.
386 StringJoiner skippedPiFieldsJoiner = new StringJoiner(", ");
387 piCriterionFields.keySet().stream()
388 .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
389 .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
390 if (skippedPiFieldsJoiner.length() > 0) {
391 throw new PiFlowRuleTranslationException(format(
392 "The following PiCriterion field matches are not supported in table '%s': %s",
393 tableModel.name(), skippedPiFieldsJoiner.toString()));
394 }
395
396 return fieldMatches.values();
397 }
398
399 private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
400 throws PiFlowRuleTranslationException {
401
402 // Check parameter type and size
403 if (!fieldModel.matchType().equals(fieldMatch.type())) {
404 throw new PiFlowRuleTranslationException(format(
405 "Wrong match type for field '%s', expected %s, but found %s",
406 fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
407 }
408
409 int modelBitWidth = fieldModel.field().type().bitWidth();
410
411 /*
412 Here we try to be robust against wrong size fields with the goal of having PiCriterion independent of the
413 pipeline model. We duplicate the field match, fitting the byte sequences to the bit-width specified in the
414 model. This operation is expensive when performed for each field match of each flow rule, but should be
415 mitigated by the translation cache provided by PiFlowRuleTranslationServiceImpl.
416 */
417
418 try {
419 switch (fieldModel.matchType()) {
420 case EXACT:
421 return new PiExactFieldMatch(fieldMatch.fieldId(),
422 fit(((PiExactFieldMatch) fieldMatch).value(), modelBitWidth));
423 case TERNARY:
424 return new PiTernaryFieldMatch(fieldMatch.fieldId(),
425 fit(((PiTernaryFieldMatch) fieldMatch).value(), modelBitWidth),
426 fit(((PiTernaryFieldMatch) fieldMatch).mask(), modelBitWidth));
427 case LPM:
428 PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
429 if (lpmfield.prefixLength() > modelBitWidth) {
430 throw new PiFlowRuleTranslationException(format(
431 "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
432 fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
433 }
434 return new PiLpmFieldMatch(fieldMatch.fieldId(),
435 fit(lpmfield.value(), modelBitWidth),
436 lpmfield.prefixLength());
437 case RANGE:
438 return new PiRangeFieldMatch(fieldMatch.fieldId(),
439 fit(((PiRangeFieldMatch) fieldMatch).lowValue(), modelBitWidth),
440 fit(((PiRangeFieldMatch) fieldMatch).highValue(), modelBitWidth));
441 case VALID:
442 return fieldMatch;
443 default:
444 // Should never be here.
445 throw new RuntimeException(
446 "Unrecognized match type " + fieldModel.matchType().name());
447 }
448 } catch (ByteSequenceTrimException e) {
449 throw new PiFlowRuleTranslationException(format(
450 "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
451 }
452 }
453}