| /* |
| * Copyright 2017-present Open Networking Foundation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.onosproject.drivers.p4runtime; |
| |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.google.common.util.concurrent.Striped; |
| import org.onosproject.drivers.p4runtime.mirror.P4RuntimeMeterMirror; |
| import org.onosproject.drivers.p4runtime.mirror.TimedEntry; |
| import org.onosproject.net.DeviceId; |
| import org.onosproject.net.meter.Band; |
| import org.onosproject.net.meter.DefaultMeter; |
| import org.onosproject.net.meter.DefaultMeterFeatures; |
| import org.onosproject.net.meter.Meter; |
| import org.onosproject.net.meter.MeterFeatures; |
| import org.onosproject.net.meter.MeterOperation; |
| import org.onosproject.net.meter.MeterProgrammable; |
| import org.onosproject.net.meter.MeterScope; |
| import org.onosproject.net.meter.MeterState; |
| import org.onosproject.net.pi.model.PiMeterId; |
| import org.onosproject.net.pi.model.PiMeterModel; |
| import org.onosproject.net.pi.model.PiPipelineModel; |
| import org.onosproject.net.pi.runtime.PiMeterCellConfig; |
| import org.onosproject.net.pi.runtime.PiMeterCellHandle; |
| import org.onosproject.net.pi.runtime.PiMeterCellId; |
| import org.onosproject.net.pi.service.PiMeterTranslator; |
| import org.onosproject.net.pi.service.PiTranslatedEntity; |
| import org.onosproject.net.pi.service.PiTranslationException; |
| import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteRequest; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.locks.Lock; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType; |
| |
| /** |
| * Implementation of MeterProgrammable behaviour for P4Runtime. |
| */ |
| public class P4RuntimeMeterProgrammable extends AbstractP4RuntimeHandlerBehaviour implements MeterProgrammable { |
| |
| private static final Striped<Lock> WRITE_LOCKS = Striped.lock(30); |
| |
| private PiMeterTranslator translator; |
| private P4RuntimeMeterMirror meterMirror; |
| private PiPipelineModel pipelineModel; |
| |
| @Override |
| protected boolean setupBehaviour(String opName) { |
| if (!super.setupBehaviour(opName)) { |
| return false; |
| } |
| |
| translator = translationService.meterTranslator(); |
| meterMirror = handler().get(P4RuntimeMeterMirror.class); |
| pipelineModel = pipeconf.pipelineModel(); |
| return true; |
| } |
| |
| @Override |
| public CompletableFuture<Boolean> performMeterOperation(MeterOperation meterOp) { |
| |
| if (!setupBehaviour("performMeterOperation()")) { |
| return CompletableFuture.completedFuture(false); |
| } |
| |
| return CompletableFuture.completedFuture(processMeterOp(meterOp)); |
| } |
| |
| private boolean processMeterOp(MeterOperation meterOp) { |
| PiMeterCellConfig piMeterCellConfig; |
| final PiMeterCellHandle handle = PiMeterCellHandle.of(deviceId, |
| (PiMeterCellId) meterOp.meter().meterCellId()); |
| boolean result = true; |
| WRITE_LOCKS.get(deviceId).lock(); |
| try { |
| switch (meterOp.type()) { |
| case ADD: |
| case MODIFY: |
| // Create a config for modify operation |
| try { |
| piMeterCellConfig = translator.translate(meterOp.meter(), pipeconf); |
| } catch (PiTranslationException e) { |
| log.warn("Unable translate meter, aborting meter operation {}: {}", |
| meterOp.type(), e.getMessage()); |
| log.debug("exception", e); |
| return false; |
| } |
| translator.learn(handle, new PiTranslatedEntity<>(meterOp.meter(), piMeterCellConfig, handle)); |
| break; |
| case REMOVE: |
| // Create a empty config for reset operation |
| PiMeterCellId piMeterCellId = (PiMeterCellId) meterOp.meter().meterCellId(); |
| piMeterCellConfig = PiMeterCellConfig.reset(piMeterCellId); |
| translator.forget(handle); |
| break; |
| default: |
| log.warn("Meter Operation type {} not supported", meterOp.type()); |
| return false; |
| } |
| |
| WriteRequest request = client.write(p4DeviceId, pipeconf); |
| appendEntryToWriteRequestOrSkip(request, handle, piMeterCellConfig); |
| if (!request.pendingUpdates().isEmpty()) { |
| result = request.submitSync().isSuccess(); |
| if (result) { |
| meterMirror.applyWriteRequest(request); |
| } |
| } |
| } finally { |
| WRITE_LOCKS.get(deviceId).unlock(); |
| } |
| return result; |
| } |
| |
| @Override |
| public CompletableFuture<Collection<Meter>> getMeters() { |
| |
| if (!setupBehaviour("getMeters()")) { |
| return CompletableFuture.completedFuture(Collections.emptyList()); |
| } |
| |
| Collection<PiMeterCellConfig> piMeterCellConfigs; |
| |
| Set<PiMeterId> meterIds = new HashSet<>(); |
| for (PiMeterModel mode : pipelineModel.meters()) { |
| meterIds.add(mode.id()); |
| } |
| |
| piMeterCellConfigs = client.read(p4DeviceId, pipeconf) |
| .meterCells(meterIds).submitSync().all(PiMeterCellConfig.class); |
| |
| meterMirror.sync(deviceId, piMeterCellConfigs); |
| |
| if (piMeterCellConfigs.isEmpty()) { |
| return CompletableFuture.completedFuture(Collections.emptyList()); |
| } |
| |
| List<PiMeterCellId> inconsistentOrDefaultCells = Lists.newArrayList(); |
| List<Meter> meters = Lists.newArrayList(); |
| |
| // Check the consistency of meter config |
| for (PiMeterCellConfig config : piMeterCellConfigs) { |
| PiMeterCellHandle handle = PiMeterCellHandle.of(deviceId, config); |
| DefaultMeter meter = (DefaultMeter) forgeMeter(config, handle); |
| if (meter == null) { |
| // A default config cannot be used to forge meter |
| // because meter has at least 1 band while default config has no band |
| inconsistentOrDefaultCells.add(config.cellId()); |
| } else { |
| meters.add(meter); |
| } |
| } |
| |
| // Reset all inconsistent meter cells to the default config |
| if (!inconsistentOrDefaultCells.isEmpty()) { |
| WriteRequest request = client.write(p4DeviceId, pipeconf); |
| for (PiMeterCellId cellId : inconsistentOrDefaultCells) { |
| PiMeterCellHandle handle = PiMeterCellHandle.of(deviceId, cellId); |
| appendEntryToWriteRequestOrSkip(request, handle, PiMeterCellConfig.reset(cellId)); |
| } |
| |
| request.submit().whenComplete((response, ex) -> { |
| if (ex != null) { |
| log.error("Exception resetting inconsistent meter entries", ex); |
| } else { |
| log.debug("Successfully removed {} out of {} inconsistent meter entries", |
| response.success().size(), response.all().size()); |
| } |
| response.success().forEach(entity -> meterMirror.remove((PiMeterCellHandle) entity.handle())); |
| }); |
| } |
| |
| return CompletableFuture.completedFuture(meters); |
| } |
| |
| @Override |
| public CompletableFuture<Collection<MeterFeatures>> getMeterFeatures() { |
| |
| if (!setupBehaviour("getMeterFeatures()")) { |
| return CompletableFuture.completedFuture(Collections.emptyList()); |
| } |
| |
| Collection<MeterFeatures> meterFeatures = new HashSet<>(); |
| pipeconf.pipelineModel().meters().forEach( |
| m -> meterFeatures.add(new P4RuntimeMeterFeaturesBuilder(m, deviceId).build())); |
| |
| return CompletableFuture.completedFuture(meterFeatures); |
| } |
| |
| private Meter forgeMeter(PiMeterCellConfig config, PiMeterCellHandle handle) { |
| final Optional<PiTranslatedEntity<Meter, PiMeterCellConfig>> |
| translatedEntity = translator.lookup(handle); |
| final TimedEntry<PiMeterCellConfig> timedEntry = meterMirror.get(handle); |
| |
| // A meter cell config might not be present in the translation store if it |
| // is default configuration. |
| if (translatedEntity.isEmpty()) { |
| if (!config.isDefaultConfig()) { |
| log.warn("Meter Cell Config obtained from device {} is different from " + |
| "one in in translation store: device={}, store=Default", deviceId, config); |
| } else { |
| log.debug("Configs obtained from device: {} and present in the store are default, " + |
| "skipping the forge section", deviceId); |
| } |
| return null; |
| } |
| |
| // The config is not consistent |
| if (!isSimilar(translatedEntity.get().translated(), config)) { |
| log.warn("Meter Cell Config obtained from device {} is different from " + |
| "one in in translation store: device={}, store={}", |
| deviceId, config, translatedEntity.get().translated()); |
| return null; |
| } |
| |
| // Big problems in the mirror ? This could happen in case of issues with |
| // the eventual consistent maps used in the AbstractDistributedP4RuntimeMirror |
| if (timedEntry == null) { |
| log.warn("Meter entry handle not found in device mirror: {}", handle); |
| return null; |
| } |
| |
| Meter original = translatedEntity.get().original(); |
| // Forge a meter with MeterCellId, Bands and DeviceId using the original value. |
| // Other values are not required because we cannot retrieve them from the south |
| DefaultMeter meter = (DefaultMeter) DefaultMeter.builder() |
| .withBands(original.bands()) |
| .withCellId(original.meterCellId()) |
| .forDevice(deviceId) |
| .build(); |
| meter.setState(MeterState.ADDED); |
| return meter; |
| } |
| |
| private boolean appendEntryToWriteRequestOrSkip( |
| final WriteRequest writeRequest, |
| final PiMeterCellHandle handle, |
| PiMeterCellConfig configToModify) { |
| |
| final TimedEntry<PiMeterCellConfig> configOnDevice = meterMirror.get(handle); |
| |
| if (configOnDevice != null && configOnDevice.entry().equals(configToModify)) { |
| log.debug("Ignoring re-apply of existing entry: {}", configToModify); |
| return true; |
| } |
| |
| writeRequest.entity(configToModify, UpdateType.MODIFY); |
| |
| return false; |
| } |
| |
| /** |
| * Returns true if the given PiMeterCellConfigs are similar enough to be deemed equal |
| * for reconciliation purposes. This is required to handle read/write asymmetry in devices |
| * that allow variations in the meter rate/burst. E.g., devices that implement metering |
| * with a rate or burst size that is slightly higher/lower than the configured ones, |
| * so the values written by ONOS will be different than those read from the device. |
| * |
| * @param onosMeter the ONOS meter |
| * @param deviceMeter the meter in the device |
| * @return true if the meters are similar, false otherwise |
| */ |
| protected boolean isSimilar(PiMeterCellConfig onosMeter, PiMeterCellConfig deviceMeter) { |
| return onosMeter.equals(deviceMeter); |
| } |
| |
| /** |
| * P4 meter features builder. |
| */ |
| public static class P4RuntimeMeterFeaturesBuilder { |
| private final PiMeterModel piMeterModel; |
| private DeviceId deviceId; |
| |
| private static final long PI_METER_START_INDEX = 0L; |
| private static final short PI_METER_MAX_BAND = 2; |
| private static final short PI_METER_MAX_COLOR = 3; |
| |
| public P4RuntimeMeterFeaturesBuilder(PiMeterModel piMeterModel, DeviceId deviceId) { |
| this.piMeterModel = checkNotNull(piMeterModel); |
| this.deviceId = deviceId; |
| } |
| |
| /** |
| * To build a MeterFeatures using the PiMeterModel object |
| * retrieved from pipeconf. |
| * |
| * @return the meter features object |
| */ |
| public MeterFeatures build() { |
| // We set the basic values before to extract the other information. |
| MeterFeatures.Builder builder = DefaultMeterFeatures.builder() |
| .forDevice(deviceId) |
| // The scope value will be PiMeterId |
| .withScope(MeterScope.of(piMeterModel.id().id())) |
| .withMaxBands(PI_METER_MAX_BAND) |
| .withMaxColors(PI_METER_MAX_COLOR); |
| |
| // We extract the number of supported meters. |
| if (piMeterModel.size() > 0) { |
| builder.withStartIndex(PI_METER_START_INDEX) |
| .withEndIndex(piMeterModel.size() - 1); |
| } |
| |
| // p4rt meters support MARK_YELLOW (committed config) and |
| // MARK_RED (peak config) band types. |
| Set<Band.Type> bands = Sets.newHashSet(); |
| bands.add(Band.Type.MARK_YELLOW); |
| bands.add(Band.Type.MARK_RED); |
| builder.withBandTypes(bands); |
| |
| // We extract the supported units; |
| Set<Meter.Unit> units = Sets.newHashSet(); |
| if (piMeterModel.unit() == PiMeterModel.Unit.BYTES) { |
| units.add(Meter.Unit.BYTES_PER_SEC); |
| } else if (piMeterModel.unit() == PiMeterModel.Unit.PACKETS) { |
| units.add(Meter.Unit.PKTS_PER_SEC); |
| } |
| |
| return builder.withUnits(units) |
| .hasBurst(true) |
| .hasStats(false) |
| .build(); |
| } |
| |
| /** |
| * To build an empty meter features. |
| * |
| * @param deviceId the device id |
| * @return the meter features |
| */ |
| public MeterFeatures noMeterFeatures(DeviceId deviceId) { |
| return DefaultMeterFeatures.noMeterFeatures(deviceId); |
| } |
| } |
| } |