blob: dc6d1c5c2bcd9c0ee698a5e75c9604326bbd59aa [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 */
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400134 PiAction piAction = buildAction(rule.treatment(), interpreter, piTableId);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900135 piAction = typeCheckAction(piAction, table);
136
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())
Carmelo Cascone00a59962017-06-16 17:51:49 +0900157 .withAction(piAction);
158
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 */
175 private static PiAction 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
211 if (piTableAction.type() != PiTableAction.Type.ACTION) {
212 // TODO: implement handling of other table action types, e.g. action profiles.
213 throw new PiFlowRuleTranslationException(format(
214 "PiTableAction type %s is not supported yet.", piTableAction.type()));
215 }
216
217 return (PiAction) piTableAction;
218 }
219
220 /**
221 * Checks that the given PI action is suitable for the given table model and returns a new action instance with
222 * parameters well-sized, according to the table model. If not suitable, throws an exception explaining why.
223 */
224 private static PiAction typeCheckAction(PiAction piAction, PiTableModel table)
225 throws PiFlowRuleTranslationException {
226
227 // Table supports this action?
228 PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
229 () -> new PiFlowRuleTranslationException(format("Not such action '%s' for table '%s'",
230 piAction.id(), table.name())));
231
232 // Is the number of runtime parameters correct?
233 if (actionModel.params().size() != piAction.parameters().size()) {
234 throw new PiFlowRuleTranslationException(format(
235 "Wrong number of runtime parameters for action '%s', expected %d but found %d",
236 actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
237 }
238
239 // Forge a new action instance with well-sized parameters.
240 // The same comment as in typeCheckFieldMatch() about duplicating field match instances applies here.
241 PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
242 for (PiActionParam param : piAction.parameters()) {
243 PiActionParamModel paramModel = actionModel.param(param.id().name())
244 .orElseThrow(() -> new PiFlowRuleTranslationException(format(
245 "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
246 try {
247 newActionBuilder.withParameter(new PiActionParam(param.id(),
248 fit(param.value(), paramModel.bitWidth())));
249 } catch (ByteSequenceTrimException e) {
250 throw new PiFlowRuleTranslationException(format(
251 "Size mismatch for parameter '%s' of action '%s': %s",
252 param.id(), piAction.id(), e.getMessage()));
253 }
254 }
255
256 return newActionBuilder.build();
257 }
258
259 /**
260 * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
261 * field matches returned are guaranteed to be compatible for the given table model.
262 */
263 private static Collection<PiFieldMatch> buildFieldMatches(PiPipelineInterpreter interpreter,
264 TrafficSelector selector, PiTableModel tableModel)
265 throws PiFlowRuleTranslationException {
266
267 Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
268
269 // If present, find a PiCriterion and get its field matches as a map. Otherwise, use an empty map.
270 Map<PiHeaderFieldId, PiFieldMatch> piCriterionFields = selector.criteria().stream()
271 .filter(c -> c.type().equals(PROTOCOL_INDEPENDENT))
272 .map(c -> (PiCriterion) c)
273 .findFirst()
274 .map(PiCriterion::fieldMatches)
275 .map(c -> {
276 Map<PiHeaderFieldId, PiFieldMatch> fieldMap = Maps.newHashMap();
277 c.forEach(fieldMatch -> fieldMap.put(fieldMatch.fieldId(), fieldMatch));
278 return fieldMap;
279 })
280 .orElse(Maps.newHashMap());
281
282 Set<Criterion> translatedCriteria = Sets.newHashSet();
283 Set<Criterion> ignoredCriteria = Sets.newHashSet();
284 Set<PiHeaderFieldId> usedPiCriterionFields = Sets.newHashSet();
285 Set<PiHeaderFieldId> ignoredPiCriterionFields = Sets.newHashSet();
286
287 for (PiTableMatchFieldModel fieldModel : tableModel.matchFields()) {
288
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200289 PiHeaderFieldId fieldId = PiHeaderFieldId.of(fieldModel.field().header().name(),
Carmelo Cascone00a59962017-06-16 17:51:49 +0900290 fieldModel.field().type().name(),
291 fieldModel.field().header().index());
292
293 int bitWidth = fieldModel.field().type().bitWidth();
294 int fieldByteWidth = (int) Math.ceil((double) bitWidth / 8);
295
296 Optional<Criterion.Type> criterionType =
297 interpreter == null
298 ? Optional.empty()
299 : interpreter.mapPiHeaderFieldId(fieldId);
300
301 Criterion criterion = criterionType.map(selector::getCriterion).orElse(null);
302
303 if (!piCriterionFields.containsKey(fieldId) && criterion == null) {
304 // Neither a field in PiCriterion is available nor a Criterion mapping is possible.
305 // Can ignore if the match is ternary or LPM.
306 switch (fieldModel.matchType()) {
307 case TERNARY:
308 // Wildcard the whole field.
309 fieldMatches.put(fieldId, new PiTernaryFieldMatch(
310 fieldId,
311 ImmutableByteSequence.ofZeros(fieldByteWidth),
312 ImmutableByteSequence.ofZeros(fieldByteWidth)));
313 break;
314 case LPM:
315 // LPM with prefix 0
316 fieldMatches.put(fieldId, new PiLpmFieldMatch(fieldId,
317 ImmutableByteSequence.ofZeros(fieldByteWidth),
318 0));
319 break;
320 // FIXME: Can we handle the case of RANGE or VALID match?
321 default:
322 throw new PiFlowRuleTranslationException(format(
323 "No value found for required match field '%s'", fieldId));
324 }
325 // Next field.
326 continue;
327 }
328
329 PiFieldMatch fieldMatch = null;
330
331 if (criterion != null) {
332 // Criterion mapping is possible for this field id.
333 try {
334 fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
335 translatedCriteria.add(criterion);
336 } catch (PiFlowRuleTranslationException ex) {
337 // Ignore exception if the same field was found in PiCriterion.
338 if (piCriterionFields.containsKey(fieldId)) {
339 ignoredCriteria.add(criterion);
340 } else {
341 throw ex;
342 }
343 }
344 }
345
346 if (piCriterionFields.containsKey(fieldId)) {
347 // Field was found in PiCriterion.
348 if (fieldMatch != null) {
349 // Field was already translated from other criterion.
350 // Throw exception only if we are trying to match on different values of the same field...
351 if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
352 throw new PiFlowRuleTranslationException(format(
353 "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
354 "what found in PiCriterion.", fieldId, criterion.type()));
355 }
356 ignoredPiCriterionFields.add(fieldId);
357 } else {
358 fieldMatch = piCriterionFields.get(fieldId);
359 fieldMatch = typeCheckFieldMatch(fieldMatch, fieldModel);
360 usedPiCriterionFields.add(fieldId);
361 }
362 }
363
364 fieldMatches.put(fieldId, fieldMatch);
365 }
366
367 // Check if all criteria have been translated.
368 StringJoiner skippedCriteriaJoiner = new StringJoiner(", ");
369 selector.criteria().stream()
370 .filter(c -> !c.type().equals(PROTOCOL_INDEPENDENT))
371 .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
372 .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
373 if (skippedCriteriaJoiner.length() > 0) {
374 throw new PiFlowRuleTranslationException(format(
375 "The following criteria cannot be translated for table '%s': %s",
376 tableModel.name(), skippedCriteriaJoiner.toString()));
377 }
378
379 // Check if all fields found in PiCriterion have been used.
380 StringJoiner skippedPiFieldsJoiner = new StringJoiner(", ");
381 piCriterionFields.keySet().stream()
382 .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
383 .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
384 if (skippedPiFieldsJoiner.length() > 0) {
385 throw new PiFlowRuleTranslationException(format(
386 "The following PiCriterion field matches are not supported in table '%s': %s",
387 tableModel.name(), skippedPiFieldsJoiner.toString()));
388 }
389
390 return fieldMatches.values();
391 }
392
393 private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
394 throws PiFlowRuleTranslationException {
395
396 // Check parameter type and size
397 if (!fieldModel.matchType().equals(fieldMatch.type())) {
398 throw new PiFlowRuleTranslationException(format(
399 "Wrong match type for field '%s', expected %s, but found %s",
400 fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
401 }
402
403 int modelBitWidth = fieldModel.field().type().bitWidth();
404
405 /*
406 Here we try to be robust against wrong size fields with the goal of having PiCriterion independent of the
407 pipeline model. We duplicate the field match, fitting the byte sequences to the bit-width specified in the
408 model. This operation is expensive when performed for each field match of each flow rule, but should be
409 mitigated by the translation cache provided by PiFlowRuleTranslationServiceImpl.
410 */
411
412 try {
413 switch (fieldModel.matchType()) {
414 case EXACT:
415 return new PiExactFieldMatch(fieldMatch.fieldId(),
416 fit(((PiExactFieldMatch) fieldMatch).value(), modelBitWidth));
417 case TERNARY:
418 return new PiTernaryFieldMatch(fieldMatch.fieldId(),
419 fit(((PiTernaryFieldMatch) fieldMatch).value(), modelBitWidth),
420 fit(((PiTernaryFieldMatch) fieldMatch).mask(), modelBitWidth));
421 case LPM:
422 PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
423 if (lpmfield.prefixLength() > modelBitWidth) {
424 throw new PiFlowRuleTranslationException(format(
425 "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
426 fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
427 }
428 return new PiLpmFieldMatch(fieldMatch.fieldId(),
429 fit(lpmfield.value(), modelBitWidth),
430 lpmfield.prefixLength());
431 case RANGE:
432 return new PiRangeFieldMatch(fieldMatch.fieldId(),
433 fit(((PiRangeFieldMatch) fieldMatch).lowValue(), modelBitWidth),
434 fit(((PiRangeFieldMatch) fieldMatch).highValue(), modelBitWidth));
435 case VALID:
436 return fieldMatch;
437 default:
438 // Should never be here.
439 throw new RuntimeException(
440 "Unrecognized match type " + fieldModel.matchType().name());
441 }
442 } catch (ByteSequenceTrimException e) {
443 throw new PiFlowRuleTranslationException(format(
444 "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
445 }
446 }
447}