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