| /* |
| * 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.ArrayListMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.util.concurrent.Striped; |
| import org.onlab.util.SharedExecutors; |
| import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileMemberMirror; |
| import org.onosproject.drivers.p4runtime.mirror.P4RuntimeGroupMirror; |
| import org.onosproject.drivers.p4runtime.mirror.TimedEntry; |
| import org.onosproject.net.DefaultAnnotations; |
| import org.onosproject.net.DeviceId; |
| import org.onosproject.net.group.DefaultGroup; |
| import org.onosproject.net.group.DefaultGroupDescription; |
| import org.onosproject.net.group.Group; |
| import org.onosproject.net.group.GroupDescription; |
| import org.onosproject.net.group.GroupOperation; |
| import org.onosproject.net.group.GroupOperations; |
| import org.onosproject.net.group.GroupProgrammable; |
| import org.onosproject.net.group.GroupStore; |
| import org.onosproject.net.pi.model.PiActionId; |
| import org.onosproject.net.pi.model.PiActionProfileId; |
| import org.onosproject.net.pi.model.PiActionProfileModel; |
| import org.onosproject.net.pi.runtime.PiAction; |
| import org.onosproject.net.pi.runtime.PiActionGroup; |
| import org.onosproject.net.pi.runtime.PiActionGroupHandle; |
| import org.onosproject.net.pi.runtime.PiActionGroupMember; |
| import org.onosproject.net.pi.runtime.PiActionGroupMemberHandle; |
| import org.onosproject.net.pi.runtime.PiActionGroupMemberId; |
| import org.onosproject.net.pi.service.PiGroupTranslator; |
| import org.onosproject.net.pi.service.PiTranslatedEntity; |
| import org.onosproject.net.pi.service.PiTranslationException; |
| import org.onosproject.p4runtime.api.P4RuntimeClient; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.locks.Lock; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE; |
| import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT; |
| import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY; |
| |
| /** |
| * Implementation of GroupProgrammable to handle action profile groups in |
| * P4Runtime. |
| */ |
| public class P4RuntimeActionGroupProgrammable |
| extends AbstractP4RuntimeHandlerBehaviour |
| implements GroupProgrammable { |
| |
| // If true, we avoid querying the device and return what's already known by |
| // the ONOS store. |
| private static final String READ_ACTION_GROUPS_FROM_MIRROR = "actionGroupReadFromMirror"; |
| private static final boolean DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR = false; |
| private static final String MAX_MEM_SIZE = "maxMemSize"; |
| |
| protected GroupStore groupStore; |
| private P4RuntimeGroupMirror groupMirror; |
| private P4RuntimeActionProfileMemberMirror memberMirror; |
| private PiGroupTranslator groupTranslator; |
| |
| // Needed to synchronize operations over the same group. |
| private static final Striped<Lock> STRIPED_LOCKS = Striped.lock(30); |
| private static final int GROUP_MEMBERS_BUFFER_SIZE = 3; |
| |
| @Override |
| protected boolean setupBehaviour() { |
| if (!super.setupBehaviour()) { |
| return false; |
| } |
| groupMirror = this.handler().get(P4RuntimeGroupMirror.class); |
| memberMirror = this.handler().get(P4RuntimeActionProfileMemberMirror.class); |
| groupStore = handler().get(GroupStore.class); |
| groupTranslator = translationService.groupTranslator(); |
| return true; |
| } |
| |
| @Override |
| public void performGroupOperation(DeviceId deviceId, |
| GroupOperations groupOps) { |
| if (!setupBehaviour()) { |
| return; |
| } |
| |
| groupOps.operations().stream() |
| .filter(op -> !op.groupType().equals(GroupDescription.Type.ALL)) |
| .forEach(op -> { |
| // ONOS-7785 We need app cookie (action profile id) from the group |
| Group groupOnStore = groupStore.getGroup(deviceId, op.groupId()); |
| GroupDescription groupDesc = new DefaultGroupDescription( |
| deviceId, op.groupType(), op.buckets(), groupOnStore.appCookie(), |
| op.groupId().id(), groupOnStore.appId()); |
| DefaultGroup groupToApply = new DefaultGroup(op.groupId(), groupDesc); |
| processGroupOperation(groupToApply, op.opType()); |
| }); |
| } |
| |
| @Override |
| public Collection<Group> getGroups() { |
| if (!setupBehaviour()) { |
| return Collections.emptyList(); |
| } |
| return getActionGroups(); |
| } |
| |
| private Collection<Group> getActionGroups() { |
| |
| if (driverBoolProperty(READ_ACTION_GROUPS_FROM_MIRROR, |
| DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR)) { |
| return getActionGroupsFromMirror(); |
| } |
| |
| final Collection<PiActionProfileId> actionProfileIds = pipeconf.pipelineModel() |
| .actionProfiles() |
| .stream() |
| .map(PiActionProfileModel::id) |
| .collect(Collectors.toList()); |
| final List<PiActionGroup> groupsOnDevice = actionProfileIds.stream() |
| .flatMap(this::streamGroupsFromDevice) |
| .collect(Collectors.toList()); |
| final Set<PiActionGroupMemberHandle> membersOnDevice = actionProfileIds |
| .stream() |
| .flatMap(actProfId -> getMembersFromDevice(actProfId) |
| .stream() |
| .map(memberId -> PiActionGroupMemberHandle.of( |
| deviceId, actProfId, memberId))) |
| .collect(Collectors.toSet()); |
| |
| if (groupsOnDevice.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| |
| // Sync mirrors. |
| syncGroupMirror(groupsOnDevice); |
| syncMemberMirror(membersOnDevice); |
| |
| final List<Group> result = Lists.newArrayList(); |
| final List<PiActionGroup> inconsistentGroups = Lists.newArrayList(); |
| final List<PiActionGroup> validGroups = Lists.newArrayList(); |
| |
| for (PiActionGroup piGroup : groupsOnDevice) { |
| final Group pdGroup = forgeGroupEntry(piGroup); |
| if (pdGroup == null) { |
| // Entry is on device but unknown to translation service or |
| // device mirror. Inconsistent. Mark for removal. |
| inconsistentGroups.add(piGroup); |
| } else { |
| validGroups.add(piGroup); |
| result.add(pdGroup); |
| } |
| } |
| |
| // Trigger clean up of inconsistent groups and members. This will also |
| // remove all members that are not used by any group, and update the |
| // mirror accordingly. |
| final Set<PiActionGroupMemberHandle> membersToKeep = validGroups.stream() |
| .flatMap(g -> g.members().stream()) |
| .map(m -> PiActionGroupMemberHandle.of(deviceId, m)) |
| .collect(Collectors.toSet()); |
| final Set<PiActionGroupMemberHandle> inconsistentMembers = Sets.difference( |
| membersOnDevice, membersToKeep); |
| SharedExecutors.getSingleThreadExecutor().execute( |
| () -> cleanUpInconsistentGroupsAndMembers( |
| inconsistentGroups, inconsistentMembers)); |
| |
| return result; |
| } |
| |
| private void syncGroupMirror(Collection<PiActionGroup> groups) { |
| Map<PiActionGroupHandle, PiActionGroup> handleMap = Maps.newHashMap(); |
| groups.forEach(g -> handleMap.put(PiActionGroupHandle.of(deviceId, g), g)); |
| groupMirror.sync(deviceId, handleMap); |
| } |
| |
| private void syncMemberMirror(Collection<PiActionGroupMemberHandle> memberHandles) { |
| Map<PiActionGroupMemberHandle, PiActionGroupMember> handleMap = Maps.newHashMap(); |
| memberHandles.forEach(handle -> handleMap.put( |
| handle, dummyMember(handle.actionProfileId(), handle.memberId()))); |
| memberMirror.sync(deviceId, handleMap); |
| } |
| |
| private Collection<Group> getActionGroupsFromMirror() { |
| return groupMirror.getAll(deviceId).stream() |
| .map(TimedEntry::entry) |
| .map(this::forgeGroupEntry) |
| .filter(Objects::nonNull) |
| .collect(Collectors.toList()); |
| } |
| |
| private void cleanUpInconsistentGroupsAndMembers(Collection<PiActionGroup> groupsToRemove, |
| Collection<PiActionGroupMemberHandle> membersToRemove) { |
| if (!groupsToRemove.isEmpty()) { |
| log.warn("Found {} inconsistent action profile groups on {}, removing them...", |
| groupsToRemove.size(), deviceId); |
| groupsToRemove.forEach(piGroup -> { |
| log.debug(piGroup.toString()); |
| processGroup(piGroup, null, Operation.REMOVE); |
| }); |
| } |
| if (!membersToRemove.isEmpty()) { |
| log.warn("Found {} inconsistent action profile members on {}, removing them...", |
| membersToRemove.size(), deviceId); |
| // FIXME: implement client call to remove members from multiple |
| // action profiles in one shot. |
| final ListMultimap<PiActionProfileId, PiActionGroupMemberId> |
| membersByActProfId = ArrayListMultimap.create(); |
| membersToRemove.forEach(m -> membersByActProfId.put( |
| m.actionProfileId(), m.memberId())); |
| membersByActProfId.keySet().forEach(actProfId -> { |
| List<PiActionGroupMemberId> removedMembers = getFutureWithDeadline( |
| client.removeActionProfileMembers( |
| actProfId, membersByActProfId.get(actProfId), pipeconf), |
| "cleaning up action profile members", Collections.emptyList()); |
| // Update member mirror. |
| removedMembers.stream() |
| .map(id -> PiActionGroupMemberHandle.of(deviceId, actProfId, id)) |
| .forEach(memberMirror::remove); |
| }); |
| } |
| } |
| |
| private Stream<PiActionGroup> streamGroupsFromDevice(PiActionProfileId actProfId) { |
| // TODO: implement P4Runtime client call to read all groups with one call |
| // Good if pipeline has multiple action profiles. |
| final Collection<PiActionGroup> groups = getFutureWithDeadline( |
| client.dumpGroups(actProfId, pipeconf), |
| "dumping groups", Collections.emptyList()); |
| return groups.stream(); |
| } |
| |
| private List<PiActionGroupMemberId> getMembersFromDevice(PiActionProfileId actProfId) { |
| // TODO: implement P4Runtime client call to read all members with one call |
| // Good if pipeline has multiple action profiles. |
| return getFutureWithDeadline( |
| client.dumpActionProfileMemberIds(actProfId, pipeconf), |
| "dumping action profile ids", Collections.emptyList()); |
| } |
| |
| private Group forgeGroupEntry(PiActionGroup piGroup) { |
| final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, piGroup); |
| final Optional<PiTranslatedEntity<Group, PiActionGroup>> |
| translatedEntity = groupTranslator.lookup(handle); |
| final TimedEntry<PiActionGroup> timedEntry = groupMirror.get(handle); |
| // Is entry consistent with our state? |
| if (!translatedEntity.isPresent()) { |
| log.warn("Group handle not found in translation store: {}", handle); |
| return null; |
| } |
| if (!translatedEntity.get().translated().equals(piGroup)) { |
| log.warn("Group obtained from device {} is different from the one in " + |
| "translation store: device={}, store={}", |
| deviceId, piGroup, translatedEntity.get().translated()); |
| return null; |
| } |
| if (timedEntry == null) { |
| log.warn("Group handle not found in device mirror: {}", handle); |
| return null; |
| } |
| return addedGroup(translatedEntity.get().original(), timedEntry.lifeSec()); |
| } |
| |
| private Group addedGroup(Group original, long life) { |
| final DefaultGroup forgedGroup = new DefaultGroup(original.id(), original); |
| forgedGroup.setState(Group.GroupState.ADDED); |
| forgedGroup.setLife(life); |
| return forgedGroup; |
| } |
| |
| private void processGroupOperation(Group pdGroup, GroupOperation.Type opType) { |
| final PiActionGroup piGroup; |
| try { |
| piGroup = groupTranslator.translate(pdGroup, pipeconf); |
| } catch (PiTranslationException e) { |
| log.warn("Unable to translate group, aborting {} operation: {} [{}]", |
| opType, e.getMessage(), pdGroup); |
| return; |
| } |
| final Operation operation = opType.equals(GroupOperation.Type.DELETE) |
| ? Operation.REMOVE : Operation.APPLY; |
| processGroup(piGroup, pdGroup, operation); |
| } |
| |
| private void processGroup(PiActionGroup groupToApply, |
| Group pdGroup, |
| Operation operation) { |
| final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, groupToApply); |
| STRIPED_LOCKS.get(handle).lock(); |
| try { |
| switch (operation) { |
| case APPLY: |
| if (applyGroupWithMembersOrNothing(groupToApply, handle)) { |
| groupTranslator.learn(handle, new PiTranslatedEntity<>( |
| pdGroup, groupToApply, handle)); |
| } |
| return; |
| case REMOVE: |
| if (deleteGroup(groupToApply, handle)) { |
| groupTranslator.forget(handle); |
| } |
| return; |
| default: |
| log.error("Unknwon group operation type {}, cannot process group", operation); |
| break; |
| } |
| } finally { |
| STRIPED_LOCKS.get(handle).unlock(); |
| } |
| } |
| |
| private boolean applyGroupWithMembersOrNothing(PiActionGroup group, PiActionGroupHandle handle) { |
| // First apply members, then group, if fails, delete members. |
| if (!applyAllMembersOrNothing(group.members())) { |
| return false; |
| } |
| if (!applyGroup(group, handle)) { |
| deleteMembers(group.members()); |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean applyGroup(PiActionGroup group, PiActionGroupHandle handle) { |
| final int currentMemberSize = group.members().size(); |
| if (groupMirror.get(handle) != null) { |
| String maxMemSize = ""; |
| if (groupMirror.annotations(handle) != null && |
| groupMirror.annotations(handle).value(MAX_MEM_SIZE) != null) { |
| maxMemSize = groupMirror.annotations(handle).value(MAX_MEM_SIZE); |
| } |
| if (!maxMemSize.equals("") || currentMemberSize > Integer.parseInt(maxMemSize)) { |
| deleteGroup(group, handle); |
| } |
| } |
| |
| P4RuntimeClient.WriteOperationType opType = |
| groupMirror.get(handle) == null ? INSERT : MODIFY; |
| int currentMaxMemberSize = opType == INSERT ? (currentMemberSize + GROUP_MEMBERS_BUFFER_SIZE) : 0; |
| |
| final boolean success = getFutureWithDeadline( |
| client.writeActionGroup(group, opType, pipeconf, currentMaxMemberSize), |
| "performing action profile group " + opType, false); |
| if (success) { |
| groupMirror.put(handle, group); |
| if (opType == INSERT) { |
| groupMirror.putAnnotations(handle, DefaultAnnotations |
| .builder() |
| .set(MAX_MEM_SIZE, Integer.toString(currentMaxMemberSize)) |
| .build()); |
| } |
| } |
| return success; |
| } |
| |
| private boolean deleteGroup(PiActionGroup group, PiActionGroupHandle handle) { |
| final boolean success = getFutureWithDeadline( |
| client.writeActionGroup(group, DELETE, pipeconf, 0), |
| "performing action profile group " + DELETE, false); |
| if (success) { |
| groupMirror.remove(handle); |
| } |
| return success; |
| } |
| |
| private boolean applyAllMembersOrNothing(Collection<PiActionGroupMember> members) { |
| Collection<PiActionGroupMember> appliedMembers = applyMembers(members); |
| if (appliedMembers.size() == members.size()) { |
| return true; |
| } else { |
| deleteMembers(appliedMembers); |
| return false; |
| } |
| } |
| |
| private Collection<PiActionGroupMember> applyMembers( |
| Collection<PiActionGroupMember> members) { |
| return members.stream() |
| .filter(this::applyMember) |
| .collect(Collectors.toList()); |
| } |
| |
| private boolean applyMember(PiActionGroupMember member) { |
| // If exists, modify, otherwise insert |
| final PiActionGroupMemberHandle handle = PiActionGroupMemberHandle.of( |
| deviceId, member); |
| final P4RuntimeClient.WriteOperationType opType = |
| memberMirror.get(handle) == null ? INSERT : MODIFY; |
| final boolean success = getFutureWithDeadline( |
| client.writeActionGroupMembers(Collections.singletonList(member), |
| opType, pipeconf), |
| "performing action profile member " + opType, false); |
| if (success) { |
| memberMirror.put(handle, dummyMember(member.actionProfile(), member.id())); |
| } |
| return success; |
| } |
| |
| private void deleteMembers(Collection<PiActionGroupMember> members) { |
| members.forEach(this::deleteMember); |
| } |
| |
| private void deleteMember(PiActionGroupMember member) { |
| final PiActionGroupMemberHandle handle = PiActionGroupMemberHandle.of( |
| deviceId, member); |
| final boolean success = getFutureWithDeadline( |
| client.writeActionGroupMembers(Collections.singletonList(member), |
| DELETE, pipeconf), |
| "performing action profile member " + DELETE, false); |
| if (success) { |
| memberMirror.remove(handle); |
| } |
| } |
| |
| // FIXME: this is nasty, we have to rely on a dummy member of the mirror |
| // because the PiActionGroupMember abstraction is broken, since it includes |
| // attributes that are not part of a P4Runtime member, e.g. weight. |
| // We should remove weight from the class, and have client methods that |
| // return the full PiActionGroupMember, not just the IDs. Also the naming |
| // "ActionGroupMember" is wrong since it makes believe that members can |
| // exists only inside a group, which is not true. |
| private PiActionGroupMember dummyMember( |
| PiActionProfileId actionProfileId, PiActionGroupMemberId memberId) { |
| return PiActionGroupMember.builder() |
| .forActionProfile(actionProfileId) |
| .withId(memberId) |
| .withAction(PiAction.builder() |
| .withId(PiActionId.of("dummy")) |
| .build()) |
| .build(); |
| } |
| |
| enum Operation { |
| APPLY, REMOVE |
| } |
| } |