blob: 97a721b687ce163971db3d7e521c45609246d904 [file] [log] [blame]
Carmelo Casconee44592f2018-09-12 02:24:47 -07001/*
2 * Copyright 2017-present Open Networking Foundation
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.drivers.p4runtime;
18
Carmelo Casconee44592f2018-09-12 02:24:47 -070019import com.google.common.collect.Lists;
Carmelo Casconee44592f2018-09-12 02:24:47 -070020import com.google.common.collect.Sets;
Carmelo Cascone61469462019-03-05 23:59:11 -080021import com.google.common.util.concurrent.Striped;
Carmelo Casconecb4327a2018-09-11 15:17:23 -070022import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileGroupMirror;
Carmelo Casconee44592f2018-09-12 02:24:47 -070023import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileMemberMirror;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080024import org.onosproject.drivers.p4runtime.mirror.P4RuntimeMirror;
Carmelo Casconee44592f2018-09-12 02:24:47 -070025import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
26import org.onosproject.net.DeviceId;
27import org.onosproject.net.group.DefaultGroup;
28import org.onosproject.net.group.DefaultGroupDescription;
29import org.onosproject.net.group.Group;
30import org.onosproject.net.group.GroupDescription;
31import org.onosproject.net.group.GroupOperation;
32import org.onosproject.net.group.GroupOperations;
33import org.onosproject.net.group.GroupProgrammable;
34import org.onosproject.net.group.GroupStore;
Carmelo Casconee44592f2018-09-12 02:24:47 -070035import org.onosproject.net.pi.model.PiActionProfileModel;
Carmelo Casconecb4327a2018-09-11 15:17:23 -070036import org.onosproject.net.pi.runtime.PiActionProfileGroup;
37import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
38import org.onosproject.net.pi.runtime.PiActionProfileMember;
39import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080040import org.onosproject.net.pi.runtime.PiEntity;
41import org.onosproject.net.pi.runtime.PiHandle;
Carmelo Casconee44592f2018-09-12 02:24:47 -070042import org.onosproject.net.pi.service.PiGroupTranslator;
43import org.onosproject.net.pi.service.PiTranslatedEntity;
44import org.onosproject.net.pi.service.PiTranslationException;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080045import org.onosproject.p4runtime.api.P4RuntimeReadClient;
Carmelo Cascone61469462019-03-05 23:59:11 -080046import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteRequest;
Carmelo Casconee44592f2018-09-12 02:24:47 -070047
48import java.util.Collection;
49import java.util.Collections;
50import java.util.List;
51import java.util.Map;
52import java.util.Objects;
53import java.util.Optional;
54import java.util.Set;
Carmelo Cascone61469462019-03-05 23:59:11 -080055import java.util.concurrent.locks.Lock;
Carmelo Casconee44592f2018-09-12 02:24:47 -070056import java.util.stream.Collectors;
Carmelo Casconee44592f2018-09-12 02:24:47 -070057
Carmelo Cascone99c59db2019-01-17 15:39:35 -080058import static java.util.stream.Collectors.toMap;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080059import static java.util.stream.Collectors.toSet;
Carmelo Casconee44592f2018-09-12 02:24:47 -070060
61/**
62 * Implementation of GroupProgrammable to handle action profile groups in
63 * P4Runtime.
64 */
65public class P4RuntimeActionGroupProgrammable
66 extends AbstractP4RuntimeHandlerBehaviour
67 implements GroupProgrammable {
68
69 // If true, we avoid querying the device and return what's already known by
70 // the ONOS store.
71 private static final String READ_ACTION_GROUPS_FROM_MIRROR = "actionGroupReadFromMirror";
72 private static final boolean DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR = false;
73
Carmelo Cascone61469462019-03-05 23:59:11 -080074 // Used to make sure concurrent calls to write groups are serialized so
75 // that each request gets consistent access to mirror state.
76 private static final Striped<Lock> WRITE_LOCKS = Striped.lock(30);
77
Carmelo Casconee44592f2018-09-12 02:24:47 -070078 protected GroupStore groupStore;
Carmelo Casconecb4327a2018-09-11 15:17:23 -070079 private P4RuntimeActionProfileGroupMirror groupMirror;
Carmelo Casconee44592f2018-09-12 02:24:47 -070080 private P4RuntimeActionProfileMemberMirror memberMirror;
81 private PiGroupTranslator groupTranslator;
82
Carmelo Casconee44592f2018-09-12 02:24:47 -070083 @Override
Carmelo Casconec32976e2019-04-08 14:50:52 -070084 protected boolean setupBehaviour(String opName) {
85 if (!super.setupBehaviour(opName)) {
Carmelo Casconee44592f2018-09-12 02:24:47 -070086 return false;
87 }
Carmelo Casconecb4327a2018-09-11 15:17:23 -070088 groupMirror = this.handler().get(P4RuntimeActionProfileGroupMirror.class);
Carmelo Casconee44592f2018-09-12 02:24:47 -070089 memberMirror = this.handler().get(P4RuntimeActionProfileMemberMirror.class);
90 groupStore = handler().get(GroupStore.class);
Yi Tsengd7716482018-10-31 15:34:30 -070091 groupTranslator = translationService.groupTranslator();
Carmelo Casconee44592f2018-09-12 02:24:47 -070092 return true;
93 }
94
95 @Override
96 public void performGroupOperation(DeviceId deviceId,
97 GroupOperations groupOps) {
Carmelo Casconec32976e2019-04-08 14:50:52 -070098 if (!setupBehaviour("performGroupOperation()")) {
Carmelo Casconee44592f2018-09-12 02:24:47 -070099 return;
100 }
101
102 groupOps.operations().stream()
103 .filter(op -> !op.groupType().equals(GroupDescription.Type.ALL))
104 .forEach(op -> {
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800105 // ONOS-7785 We need the group app cookie (which includes
106 // the action profile ID) but this is not part of the
107 // GroupDescription.
Carmelo Casconee44592f2018-09-12 02:24:47 -0700108 Group groupOnStore = groupStore.getGroup(deviceId, op.groupId());
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800109 if (groupOnStore == null) {
Carmelo Casconec32976e2019-04-08 14:50:52 -0700110 log.warn("Unable to find group {} in store, aborting {} operation [{}]",
111 op.groupId(), op.opType(), op);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800112 return;
113 }
Carmelo Casconee44592f2018-09-12 02:24:47 -0700114 GroupDescription groupDesc = new DefaultGroupDescription(
Yi Tseng556c8d42019-04-16 18:07:00 -0700115 deviceId, groupOnStore.type(), groupOnStore.buckets(), groupOnStore.appCookie(),
116 groupOnStore.id().id(), groupOnStore.appId());
Carmelo Casconee44592f2018-09-12 02:24:47 -0700117 DefaultGroup groupToApply = new DefaultGroup(op.groupId(), groupDesc);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800118 processPdGroup(groupToApply, op.opType());
Carmelo Casconee44592f2018-09-12 02:24:47 -0700119 });
120 }
121
122 @Override
123 public Collection<Group> getGroups() {
Carmelo Casconec32976e2019-04-08 14:50:52 -0700124 if (!setupBehaviour("getGroups()")) {
Carmelo Casconee44592f2018-09-12 02:24:47 -0700125 return Collections.emptyList();
126 }
Carmelo Casconee44592f2018-09-12 02:24:47 -0700127
128 if (driverBoolProperty(READ_ACTION_GROUPS_FROM_MIRROR,
129 DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR)) {
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800130 return getGroupsFromMirror();
Carmelo Casconee44592f2018-09-12 02:24:47 -0700131 }
132
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800133 // Dump groups and members from device for all action profiles.
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700134 final P4RuntimeReadClient.ReadRequest request = client.read(
135 p4DeviceId, pipeconf);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800136 pipeconf.pipelineModel().actionProfiles()
137 .stream().map(PiActionProfileModel::id)
138 .forEach(id -> request.actionProfileGroups(id)
139 .actionProfileMembers(id));
140 final P4RuntimeReadClient.ReadResponse response = request.submitSync();
141
142 if (!response.isSuccess()) {
143 // Error at client level.
144 return Collections.emptyList();
145 }
146
147 final Collection<PiActionProfileGroup> groupsOnDevice = response.all(
148 PiActionProfileGroup.class);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800149 final Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice =
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800150 response.all(PiActionProfileMember.class).stream()
151 .collect(toMap(m -> m.handle(deviceId), m -> m));
Carmelo Casconee44592f2018-09-12 02:24:47 -0700152
153 // Sync mirrors.
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800154 groupMirror.sync(deviceId, groupsOnDevice);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800155 memberMirror.sync(deviceId, membersOnDevice.values());
Carmelo Casconee44592f2018-09-12 02:24:47 -0700156
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800157 // Retrieve the original PD group before translation.
Carmelo Casconee44592f2018-09-12 02:24:47 -0700158 final List<Group> result = Lists.newArrayList();
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800159 final List<PiActionProfileGroup> groupsToRemove = Lists.newArrayList();
160 final Set<PiActionProfileMemberHandle> memberHandlesToKeep = Sets.newHashSet();
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800161 for (PiActionProfileGroup piGroup : groupsOnDevice) {
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800162 final Group pdGroup = checkAndForgeGroupEntry(piGroup, membersOnDevice);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700163 if (pdGroup == null) {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700164 // Entry is on device but is inconsistent with controller state.
165 // Mark for removal.
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800166 groupsToRemove.add(piGroup);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700167 } else {
Carmelo Casconee44592f2018-09-12 02:24:47 -0700168 result.add(pdGroup);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800169 // Keep track of member handles used in groups.
170 piGroup.members().stream()
171 .map(m -> PiActionProfileMemberHandle.of(
172 deviceId, piGroup.actionProfile(), m.id()))
173 .forEach(memberHandlesToKeep::add);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700174 }
175 }
176
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700177 // Trigger clean up of inconsistent groups and members (if any). Also
178 // take care of removing any orphan member, e.g. from a
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800179 // partial/unsuccessful group insertion.
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800180 final Set<PiActionProfileMemberHandle> memberHandlesToRemove = Sets.difference(
181 membersOnDevice.keySet(), memberHandlesToKeep);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800182 final Set<PiActionProfileGroupHandle> groupHandlesToRemove = groupsToRemove
183 .stream().map(g -> g.handle(deviceId)).collect(toSet());
184 if (groupHandlesToRemove.size() + memberHandlesToRemove.size() > 0) {
185 log.warn("Cleaning up {} action profile groups and " +
186 "{} members on {}...",
187 groupHandlesToRemove.size(), memberHandlesToRemove.size(), deviceId);
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700188 client.write(p4DeviceId, pipeconf)
Carmelo Cascone61469462019-03-05 23:59:11 -0800189 .delete(groupHandlesToRemove)
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700190 .delete(memberHandlesToRemove)
191 .submit().whenComplete((r, ex) -> {
192 if (ex != null) {
193 log.error("Exception removing inconsistent group/members", ex);
194 } else {
195 log.debug("Completed removal of inconsistent " +
196 "groups/members ({} of {} updates succeeded)",
197 r.success().size(), r.all().size());
198 groupMirror.applyWriteResponse(r);
199 memberMirror.applyWriteResponse(r);
200 }
201 });
202
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800203 }
Carmelo Casconee44592f2018-09-12 02:24:47 -0700204
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800205 // Done.
Carmelo Casconee44592f2018-09-12 02:24:47 -0700206 return result;
207 }
208
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800209 private Collection<Group> getGroupsFromMirror() {
210 final Map<PiActionProfileMemberHandle, PiActionProfileMember> members =
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800211 memberMirror.getAll(deviceId).stream()
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700212 .map(TimedEntry::entry)
213 .collect(toMap(e -> e.handle(deviceId), e -> e));
Carmelo Casconee44592f2018-09-12 02:24:47 -0700214 return groupMirror.getAll(deviceId).stream()
215 .map(TimedEntry::entry)
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800216 .map(g -> checkAndForgeGroupEntry(
217 g, members))
Carmelo Casconee44592f2018-09-12 02:24:47 -0700218 .filter(Objects::nonNull)
219 .collect(Collectors.toList());
220 }
221
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800222 private Group checkAndForgeGroupEntry(
223 PiActionProfileGroup piGroupOnDevice,
224 Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
225 final PiActionProfileGroupHandle handle = PiActionProfileGroupHandle.of(
226 deviceId, piGroupOnDevice);
Carmelo Casconecb4327a2018-09-11 15:17:23 -0700227 final Optional<PiTranslatedEntity<Group, PiActionProfileGroup>>
Carmelo Casconee44592f2018-09-12 02:24:47 -0700228 translatedEntity = groupTranslator.lookup(handle);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800229 final TimedEntry<PiActionProfileGroup> mirrorEntry = groupMirror.get(handle);
230 // Check that entry obtained from device is consistent with what is known
231 // by the translation store.
Carmelo Casconee44592f2018-09-12 02:24:47 -0700232 if (!translatedEntity.isPresent()) {
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800233 log.warn("Group not found in translation store: {}", handle);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700234 return null;
235 }
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800236 final PiActionProfileGroup piGroupFromStore = translatedEntity.get().translated();
237 if (!piGroupFromStore.equals(piGroupOnDevice)) {
238 log.warn("Group on device {} is different from the one in " +
239 "translation store: {} [device={}, store={}]",
240 deviceId, handle, piGroupOnDevice, piGroupFromStore);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700241 return null;
242 }
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800243 // Groups in P4Runtime contains only a reference to members. Check that
244 // the actual member instances in the translation store are the same
245 // found on the device.
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800246 if (!validateGroupMembers(piGroupFromStore, membersOnDevice)) {
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800247 log.warn("Group on device {} refers to members that are different " +
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700248 "than those found in translation store: {}",
249 deviceId, handle);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800250 return null;
251 }
252 if (mirrorEntry == null) {
Carmelo Casconee44592f2018-09-12 02:24:47 -0700253 log.warn("Group handle not found in device mirror: {}", handle);
254 return null;
255 }
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800256 // Check that members from device are the same as in the translated group.
257 return addedGroup(translatedEntity.get().original(), mirrorEntry.lifeSec());
258 }
259
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800260 private boolean validateGroupMembers(
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800261 PiActionProfileGroup piGroupFromStore,
262 Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
263 final Collection<PiActionProfileMember> groupMembers =
264 extractAllMemberInstancesOrNull(piGroupFromStore);
265 if (groupMembers == null) {
266 return false;
267 }
268 return groupMembers.stream().allMatch(
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800269 memberFromStore -> memberFromStore.equals(membersOnDevice.get(
270 memberFromStore.handle(deviceId))));
Carmelo Casconee44592f2018-09-12 02:24:47 -0700271 }
272
273 private Group addedGroup(Group original, long life) {
274 final DefaultGroup forgedGroup = new DefaultGroup(original.id(), original);
275 forgedGroup.setState(Group.GroupState.ADDED);
276 forgedGroup.setLife(life);
277 return forgedGroup;
278 }
279
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800280 private void processPdGroup(Group pdGroup, GroupOperation.Type opType) {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700281 // Translate.
Carmelo Casconecb4327a2018-09-11 15:17:23 -0700282 final PiActionProfileGroup piGroup;
Carmelo Casconee44592f2018-09-12 02:24:47 -0700283 try {
284 piGroup = groupTranslator.translate(pdGroup, pipeconf);
285 } catch (PiTranslationException e) {
286 log.warn("Unable to translate group, aborting {} operation: {} [{}]",
287 opType, e.getMessage(), pdGroup);
288 return;
289 }
290 final Operation operation = opType.equals(GroupOperation.Type.DELETE)
291 ? Operation.REMOVE : Operation.APPLY;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800292 final PiActionProfileGroupHandle handle = piGroup.handle(deviceId);
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700293 // Update translation store.
294 if (operation.equals(Operation.APPLY)) {
295 groupTranslator.learn(handle, new PiTranslatedEntity<>(
296 pdGroup, piGroup, handle));
297 } else {
298 groupTranslator.forget(handle);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700299 }
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700300 // Submit write and forget about it.
301 asyncWritePiGroup(piGroup, handle, operation);
Carmelo Casconee44592f2018-09-12 02:24:47 -0700302 }
303
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700304 private void asyncWritePiGroup(
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800305 PiActionProfileGroup group,
306 PiActionProfileGroupHandle groupHandle,
307 Operation operation) {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700308 // Generate and submit write request to write both members and groups.
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800309 final Collection<PiActionProfileMember> members = extractAllMemberInstancesOrNull(group);
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800310 if (members == null) {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700311 return;
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800312 }
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700313 final WriteRequest request = client.write(p4DeviceId, pipeconf);
Carmelo Cascone61469462019-03-05 23:59:11 -0800314 WRITE_LOCKS.get(deviceId).lock();
315 try {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700316 if (operation == Operation.APPLY) {
317 // First insert/update members, then group.
318 members.forEach(m -> appendEntityToWriteRequestOrSkip(
319 request, m.handle(deviceId), m, memberMirror, operation));
320 appendEntityToWriteRequestOrSkip(
321 request, groupHandle, group, groupMirror, operation);
Carmelo Cascone61469462019-03-05 23:59:11 -0800322 } else {
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700323 // First remove group, then members.
324 appendEntityToWriteRequestOrSkip(
325 request, groupHandle, group, groupMirror, operation);
326 members.forEach(m -> appendEntityToWriteRequestOrSkip(
327 request, m.handle(deviceId), m, memberMirror, operation));
Carmelo Cascone61469462019-03-05 23:59:11 -0800328 }
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700329 if (request.pendingUpdates().isEmpty()) {
330 // Nothing to do.
331 return;
332 }
333 // Optimistically update mirror before response arrives to make
334 // sure any write after this sees the expected mirror state. If
335 // anything goes wrong, mirror will be re-synced during
336 // reconciliation.
337 groupMirror.applyWriteRequest(request);
338 memberMirror.applyWriteRequest(request);
339 request.submit().whenComplete((r, ex) -> {
340 if (ex != null) {
341 log.error("Exception writing PI group to " + deviceId, ex);
342 } else {
343 log.debug("Completed write of PI group to {} " +
344 "({} of {} updates succeeded)",
345 deviceId, r.success().size(), r.all().size());
346 }
347 });
Carmelo Cascone61469462019-03-05 23:59:11 -0800348 } finally {
349 WRITE_LOCKS.get(deviceId).unlock();
Carmelo Casconee44592f2018-09-12 02:24:47 -0700350 }
351 }
352
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700353 private <H extends PiHandle, E extends PiEntity> void appendEntityToWriteRequestOrSkip(
Carmelo Cascone61469462019-03-05 23:59:11 -0800354 WriteRequest writeRequest, H handle, E entityToApply,
355 P4RuntimeMirror<H, E> mirror, Operation operation) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800356 final TimedEntry<E> entityOnDevice = mirror.get(handle);
357 switch (operation) {
358 case APPLY:
359 if (entityOnDevice == null) {
360 writeRequest.insert(entityToApply);
361 } else if (entityToApply.equals(entityOnDevice.entry())) {
362 // Skip writing if group is unchanged.
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700363 return;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800364 } else {
365 writeRequest.modify(entityToApply);
366 }
367 break;
368 case REMOVE:
369 if (entityOnDevice == null) {
370 // Skip deleting if group does not exist on device.
Carmelo Casconeb4863b32019-03-13 18:54:34 -0700371 return;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800372 } else {
373 writeRequest.delete(handle);
374 }
375 break;
376 default:
377 log.error("Unrecognized operation {}", operation);
378 break;
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800379 }
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800380 }
381
382 private Collection<PiActionProfileMember> extractAllMemberInstancesOrNull(
383 PiActionProfileGroup group) {
384 final Collection<PiActionProfileMember> instances = group.members().stream()
385 .map(PiActionProfileGroup.WeightedMember::instance)
386 .filter(Objects::nonNull)
387 .collect(Collectors.toList());
388 if (instances.size() != group.members().size()) {
389 log.error("PiActionProfileGroup has {} member references, " +
390 "but only {} instances were found",
391 group.members().size(), instances.size());
392 return null;
393 }
394 return instances;
Carmelo Casconee44592f2018-09-12 02:24:47 -0700395 }
396
Carmelo Casconee44592f2018-09-12 02:24:47 -0700397 enum Operation {
398 APPLY, REMOVE
399 }
400}