blob: 8d8d3c128ada21e46258cfbedcf0819292ad8ec8 [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.
84 // FIXME: get interpreter via driver once implemented.
85 // final PiPipelineInterpreter interpreter = device.is(PiPipelineInterpreter.class)
86 // ? device.as(PiPipelineInterpreter.class) : null;
87
88 final PiPipelineInterpreter interpreter;
89 try {
90 interpreter = (PiPipelineInterpreter) pipeconf.implementation(PiPipelineInterpreter.class)
91 .orElse(null)
92 .newInstance();
93 } catch (InstantiationException | IllegalAccessException e) {
94 throw new PiFlowRuleTranslationException(format(
95 "Unable to instantiate interpreter of pipeconf %s", pipeconf.id()));
96 }
97
98 PiTableId piTableId;
99 switch (rule.table().type()) {
100 case PIPELINE_INDEPENDENT:
101 piTableId = (PiTableId) rule.table();
102 break;
103 case INDEX:
104 IndexTableId indexId = (IndexTableId) rule.table();
105 if (interpreter == null) {
106 throw new PiFlowRuleTranslationException(format(
107 "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
108 } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
109 throw new PiFlowRuleTranslationException(format(
110 "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
111 } else {
112 piTableId = interpreter.mapFlowRuleTableId(indexId.id()).get();
113 }
114 break;
115 default:
116 throw new PiFlowRuleTranslationException(format(
117 "Unrecognized table ID type %s", rule.table().type().name()));
118 }
119
120 PiTableModel table = pipelineModel.table(piTableId.toString())
121 .orElseThrow(() -> new PiFlowRuleTranslationException(format(
122 "Not such a table in pipeline model: %s", piTableId)));
123
124 /* Translate selector */
125 Collection<PiFieldMatch> fieldMatches = buildFieldMatches(interpreter, rule.selector(), table);
126
127 /* Translate treatment */
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400128 PiAction piAction = buildAction(rule.treatment(), interpreter, piTableId);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900129 piAction = typeCheckAction(piAction, table);
130
131 PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
132
Carmelo Casconea3d811c2017-08-22 01:03:45 +0200133 // In the P4 world 0 is the highest priority, in ONOS the lowest one.
134 // FIXME: move priority conversion to the driver, where different constraints might apply
135 // e.g. less bits for encoding priority in TCAM-based implementations.
136 int newPriority;
137 if (rule.priority() > MAX_PI_PRIORITY) {
138 log.warn("Flow rule priority too big, setting translated priority to max value {}: {}",
139 MAX_PI_PRIORITY, rule);
140 newPriority = 0;
141 } else {
142 newPriority = MAX_PI_PRIORITY - rule.priority();
143 }
Carmelo Cascone00a59962017-06-16 17:51:49 +0900144
145 tableEntryBuilder
146 .forTable(piTableId)
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200147 .withPriority(newPriority)
Carmelo Cascone0e896a02017-07-31 07:22:27 +0200148 .withMatchKey(PiMatchKey.builder()
149 .addFieldMatches(fieldMatches)
150 .build())
Carmelo Cascone00a59962017-06-16 17:51:49 +0900151 .withAction(piAction);
152
153 if (!rule.isPermanent()) {
154 if (table.supportsAging()) {
155 tableEntryBuilder.withTimeout((double) rule.timeout());
156 } else {
157 log.warn("Flow rule is temporary, but table '{}' doesn't support " +
158 "aging, translating to permanent.", table.name());
159 }
160
161 }
162
163 return tableEntryBuilder.build();
164 }
165
166 /**
167 * Builds a PI action out of the given treatment, optionally using the given interpreter.
168 */
169 private static PiAction buildAction(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400170 PiTableId tableId)
Carmelo Cascone00a59962017-06-16 17:51:49 +0900171 throws PiFlowRuleTranslationException {
172
173 PiTableAction piTableAction = null;
174
175 // If treatment has only one instruction of type PiInstruction, use that.
176 for (Instruction inst : treatment.allInstructions()) {
177 if (inst.type() == Instruction.Type.PROTOCOL_INDEPENDENT) {
178 if (treatment.allInstructions().size() == 1) {
179 piTableAction = ((PiInstruction) inst).action();
180 } else {
181 throw new PiFlowRuleTranslationException(format(
182 "Unable to translate treatment, found multiple instructions " +
183 "of which one is protocol-independent: %s", treatment));
184 }
185 }
186 }
187
188 if (piTableAction == null && interpreter != null) {
189 // No PiInstruction, use interpreter to build action.
190 try {
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400191 piTableAction = interpreter.mapTreatment(treatment, tableId);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900192 } catch (PiPipelineInterpreter.PiInterpreterException e) {
193 throw new PiFlowRuleTranslationException(
194 "Interpreter was unable to translate treatment. " + e.getMessage());
195 }
196 }
197
198 if (piTableAction == null) {
199 // No PiInstruction, no interpreter. It's time to give up.
200 throw new PiFlowRuleTranslationException(
201 "Unable to translate treatment, neither an interpreter or a "
202 + "protocol-independent instruction were provided.");
203 }
204
205 if (piTableAction.type() != PiTableAction.Type.ACTION) {
206 // TODO: implement handling of other table action types, e.g. action profiles.
207 throw new PiFlowRuleTranslationException(format(
208 "PiTableAction type %s is not supported yet.", piTableAction.type()));
209 }
210
211 return (PiAction) piTableAction;
212 }
213
214 /**
215 * Checks that the given PI action is suitable for the given table model and returns a new action instance with
216 * parameters well-sized, according to the table model. If not suitable, throws an exception explaining why.
217 */
218 private static PiAction typeCheckAction(PiAction piAction, PiTableModel table)
219 throws PiFlowRuleTranslationException {
220
221 // Table supports this action?
222 PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
223 () -> new PiFlowRuleTranslationException(format("Not such action '%s' for table '%s'",
224 piAction.id(), table.name())));
225
226 // Is the number of runtime parameters correct?
227 if (actionModel.params().size() != piAction.parameters().size()) {
228 throw new PiFlowRuleTranslationException(format(
229 "Wrong number of runtime parameters for action '%s', expected %d but found %d",
230 actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
231 }
232
233 // Forge a new action instance with well-sized parameters.
234 // The same comment as in typeCheckFieldMatch() about duplicating field match instances applies here.
235 PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
236 for (PiActionParam param : piAction.parameters()) {
237 PiActionParamModel paramModel = actionModel.param(param.id().name())
238 .orElseThrow(() -> new PiFlowRuleTranslationException(format(
239 "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
240 try {
241 newActionBuilder.withParameter(new PiActionParam(param.id(),
242 fit(param.value(), paramModel.bitWidth())));
243 } catch (ByteSequenceTrimException e) {
244 throw new PiFlowRuleTranslationException(format(
245 "Size mismatch for parameter '%s' of action '%s': %s",
246 param.id(), piAction.id(), e.getMessage()));
247 }
248 }
249
250 return newActionBuilder.build();
251 }
252
253 /**
254 * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
255 * field matches returned are guaranteed to be compatible for the given table model.
256 */
257 private static Collection<PiFieldMatch> buildFieldMatches(PiPipelineInterpreter interpreter,
258 TrafficSelector selector, PiTableModel tableModel)
259 throws PiFlowRuleTranslationException {
260
261 Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
262
263 // If present, find a PiCriterion and get its field matches as a map. Otherwise, use an empty map.
264 Map<PiHeaderFieldId, PiFieldMatch> piCriterionFields = selector.criteria().stream()
265 .filter(c -> c.type().equals(PROTOCOL_INDEPENDENT))
266 .map(c -> (PiCriterion) c)
267 .findFirst()
268 .map(PiCriterion::fieldMatches)
269 .map(c -> {
270 Map<PiHeaderFieldId, PiFieldMatch> fieldMap = Maps.newHashMap();
271 c.forEach(fieldMatch -> fieldMap.put(fieldMatch.fieldId(), fieldMatch));
272 return fieldMap;
273 })
274 .orElse(Maps.newHashMap());
275
276 Set<Criterion> translatedCriteria = Sets.newHashSet();
277 Set<Criterion> ignoredCriteria = Sets.newHashSet();
278 Set<PiHeaderFieldId> usedPiCriterionFields = Sets.newHashSet();
279 Set<PiHeaderFieldId> ignoredPiCriterionFields = Sets.newHashSet();
280
281 for (PiTableMatchFieldModel fieldModel : tableModel.matchFields()) {
282
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200283 PiHeaderFieldId fieldId = PiHeaderFieldId.of(fieldModel.field().header().name(),
Carmelo Cascone00a59962017-06-16 17:51:49 +0900284 fieldModel.field().type().name(),
285 fieldModel.field().header().index());
286
287 int bitWidth = fieldModel.field().type().bitWidth();
288 int fieldByteWidth = (int) Math.ceil((double) bitWidth / 8);
289
290 Optional<Criterion.Type> criterionType =
291 interpreter == null
292 ? Optional.empty()
293 : interpreter.mapPiHeaderFieldId(fieldId);
294
295 Criterion criterion = criterionType.map(selector::getCriterion).orElse(null);
296
297 if (!piCriterionFields.containsKey(fieldId) && criterion == null) {
298 // Neither a field in PiCriterion is available nor a Criterion mapping is possible.
299 // Can ignore if the match is ternary or LPM.
300 switch (fieldModel.matchType()) {
301 case TERNARY:
302 // Wildcard the whole field.
303 fieldMatches.put(fieldId, new PiTernaryFieldMatch(
304 fieldId,
305 ImmutableByteSequence.ofZeros(fieldByteWidth),
306 ImmutableByteSequence.ofZeros(fieldByteWidth)));
307 break;
308 case LPM:
309 // LPM with prefix 0
310 fieldMatches.put(fieldId, new PiLpmFieldMatch(fieldId,
311 ImmutableByteSequence.ofZeros(fieldByteWidth),
312 0));
313 break;
314 // FIXME: Can we handle the case of RANGE or VALID match?
315 default:
316 throw new PiFlowRuleTranslationException(format(
317 "No value found for required match field '%s'", fieldId));
318 }
319 // Next field.
320 continue;
321 }
322
323 PiFieldMatch fieldMatch = null;
324
325 if (criterion != null) {
326 // Criterion mapping is possible for this field id.
327 try {
328 fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
329 translatedCriteria.add(criterion);
330 } catch (PiFlowRuleTranslationException ex) {
331 // Ignore exception if the same field was found in PiCriterion.
332 if (piCriterionFields.containsKey(fieldId)) {
333 ignoredCriteria.add(criterion);
334 } else {
335 throw ex;
336 }
337 }
338 }
339
340 if (piCriterionFields.containsKey(fieldId)) {
341 // Field was found in PiCriterion.
342 if (fieldMatch != null) {
343 // Field was already translated from other criterion.
344 // Throw exception only if we are trying to match on different values of the same field...
345 if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
346 throw new PiFlowRuleTranslationException(format(
347 "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
348 "what found in PiCriterion.", fieldId, criterion.type()));
349 }
350 ignoredPiCriterionFields.add(fieldId);
351 } else {
352 fieldMatch = piCriterionFields.get(fieldId);
353 fieldMatch = typeCheckFieldMatch(fieldMatch, fieldModel);
354 usedPiCriterionFields.add(fieldId);
355 }
356 }
357
358 fieldMatches.put(fieldId, fieldMatch);
359 }
360
361 // Check if all criteria have been translated.
362 StringJoiner skippedCriteriaJoiner = new StringJoiner(", ");
363 selector.criteria().stream()
364 .filter(c -> !c.type().equals(PROTOCOL_INDEPENDENT))
365 .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
366 .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
367 if (skippedCriteriaJoiner.length() > 0) {
368 throw new PiFlowRuleTranslationException(format(
369 "The following criteria cannot be translated for table '%s': %s",
370 tableModel.name(), skippedCriteriaJoiner.toString()));
371 }
372
373 // Check if all fields found in PiCriterion have been used.
374 StringJoiner skippedPiFieldsJoiner = new StringJoiner(", ");
375 piCriterionFields.keySet().stream()
376 .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
377 .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
378 if (skippedPiFieldsJoiner.length() > 0) {
379 throw new PiFlowRuleTranslationException(format(
380 "The following PiCriterion field matches are not supported in table '%s': %s",
381 tableModel.name(), skippedPiFieldsJoiner.toString()));
382 }
383
384 return fieldMatches.values();
385 }
386
387 private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
388 throws PiFlowRuleTranslationException {
389
390 // Check parameter type and size
391 if (!fieldModel.matchType().equals(fieldMatch.type())) {
392 throw new PiFlowRuleTranslationException(format(
393 "Wrong match type for field '%s', expected %s, but found %s",
394 fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
395 }
396
397 int modelBitWidth = fieldModel.field().type().bitWidth();
398
399 /*
400 Here we try to be robust against wrong size fields with the goal of having PiCriterion independent of the
401 pipeline model. We duplicate the field match, fitting the byte sequences to the bit-width specified in the
402 model. This operation is expensive when performed for each field match of each flow rule, but should be
403 mitigated by the translation cache provided by PiFlowRuleTranslationServiceImpl.
404 */
405
406 try {
407 switch (fieldModel.matchType()) {
408 case EXACT:
409 return new PiExactFieldMatch(fieldMatch.fieldId(),
410 fit(((PiExactFieldMatch) fieldMatch).value(), modelBitWidth));
411 case TERNARY:
412 return new PiTernaryFieldMatch(fieldMatch.fieldId(),
413 fit(((PiTernaryFieldMatch) fieldMatch).value(), modelBitWidth),
414 fit(((PiTernaryFieldMatch) fieldMatch).mask(), modelBitWidth));
415 case LPM:
416 PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
417 if (lpmfield.prefixLength() > modelBitWidth) {
418 throw new PiFlowRuleTranslationException(format(
419 "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
420 fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
421 }
422 return new PiLpmFieldMatch(fieldMatch.fieldId(),
423 fit(lpmfield.value(), modelBitWidth),
424 lpmfield.prefixLength());
425 case RANGE:
426 return new PiRangeFieldMatch(fieldMatch.fieldId(),
427 fit(((PiRangeFieldMatch) fieldMatch).lowValue(), modelBitWidth),
428 fit(((PiRangeFieldMatch) fieldMatch).highValue(), modelBitWidth));
429 case VALID:
430 return fieldMatch;
431 default:
432 // Should never be here.
433 throw new RuntimeException(
434 "Unrecognized match type " + fieldModel.matchType().name());
435 }
436 } catch (ByteSequenceTrimException e) {
437 throw new PiFlowRuleTranslationException(format(
438 "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
439 }
440 }
441}