Refactor NetconfDataTreeServiceImpl
[netconf.git] / netconf / sal-netconf-connector / src / main / java / org / opendaylight / netconf / sal / connect / netconf / sal / AbstractNetconfDataTreeService.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.sal.connect.netconf.sal;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.util.concurrent.FutureCallback;
14 import com.google.common.util.concurrent.Futures;
15 import com.google.common.util.concurrent.ListenableFuture;
16 import com.google.common.util.concurrent.MoreExecutors;
17 import com.google.common.util.concurrent.SettableFuture;
18 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.StringJoiner;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.opendaylight.mdsal.common.api.CommitInfo;
26 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
27 import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
28 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
29 import org.opendaylight.mdsal.dom.api.DOMRpcService;
30 import org.opendaylight.netconf.api.DocumentedException;
31 import org.opendaylight.netconf.api.ModifyAction;
32 import org.opendaylight.netconf.api.NetconfDocumentedException;
33 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
34 import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
35 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps;
36 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfRpcFutureCallback;
37 import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
38 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
39 import org.opendaylight.yangtools.yang.common.RpcError;
40 import org.opendaylight.yangtools.yang.common.RpcResult;
41 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
43 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
44 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 public abstract class AbstractNetconfDataTreeService implements NetconfDataTreeService {
49     private static final class Candidate extends AbstractNetconfDataTreeService {
50         Candidate(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport) {
51             super(id, netconfOps, rollbackSupport);
52         }
53
54         /**
55          * This has to be non blocking since it is called from a callback on commit and it is netty threadpool that is
56          * really sensitive to blocking calls.
57          */
58         @Override
59         public void discardChanges() {
60             netconfOps.discardChanges(new NetconfRpcFutureCallback("Discarding candidate", id));
61         }
62
63         @Override
64         ListenableFuture<? extends DOMRpcResult> lockSingle() {
65             return netconfOps.lockCandidate(new NetconfRpcFutureCallback("Lock candidate", id) {
66                 @Override
67                 public void onFailure(final Throwable throwable) {
68                     super.onFailure(throwable);
69                     discardChanges();
70                 }
71             });
72         }
73
74         @Override
75         void unlockImpl() {
76             netconfOps.unlockCandidate(new NetconfRpcFutureCallback("Unlock candidate", id));
77         }
78
79         @Override
80         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild<?, ?> editStructure,
81                 final ModifyAction defaultOperation) {
82             final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit candidate", id);
83             return defaultOperation == null ? netconfOps.editConfigCandidate(callback, editStructure, rollbackSupport)
84                 : netconfOps.editConfigCandidate(callback, editStructure, defaultOperation, rollbackSupport);
85         }
86
87         @Override
88         ListenableFuture<RpcResult<Void>> commitImpl(final List<ListenableFuture<? extends DOMRpcResult>> results) {
89             results.add(netconfOps.commit(new NetconfRpcFutureCallback("Commit", id)));
90             final ListenableFuture<RpcResult<Void>> result = resultsToStatus(id, results);
91             Futures.addCallback(result, new FutureCallback<>() {
92                 @Override
93                 public void onSuccess(final RpcResult<Void> result) {
94                     unlock();
95                 }
96
97                 @Override
98                 public void onFailure(final Throwable throwable) {
99                     discardChanges();
100                     unlock();
101                 }
102             }, MoreExecutors.directExecutor());
103             return result;
104         }
105     }
106
107     private static final class Running extends AbstractNetconfDataTreeService {
108         Running(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport) {
109             super(id, netconfOps, rollbackSupport);
110         }
111
112         @Override
113         public void discardChanges() {
114             // Changes cannot be discarded from running
115         }
116
117         @Override
118         ListenableFuture<? extends DOMRpcResult> lockSingle() {
119             return netconfOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id));
120         }
121
122         @Override
123         void unlockImpl() {
124             netconfOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
125         }
126
127         @Override
128         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild<?, ?> editStructure,
129                 final ModifyAction defaultOperation) {
130             final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit running", id);
131             return defaultOperation == null ? netconfOps.editConfigRunning(callback, editStructure, rollbackSupport)
132                 : netconfOps.editConfigRunning(callback, editStructure, defaultOperation, rollbackSupport);
133         }
134
135         @Override
136         ListenableFuture<RpcResult<Void>> commitImpl(final List<ListenableFuture<? extends DOMRpcResult>> results) {
137             unlock();
138             return resultsToStatus(id, results);
139         }
140     }
141
142     private static final class CandidateWithRunning extends AbstractNetconfDataTreeService {
143         private final Candidate candidate;
144         private final Running running;
145
146         CandidateWithRunning(final RemoteDeviceId id, final NetconfBaseOps netconfOps,
147                 final boolean rollbackSupport) {
148             super(id, netconfOps, rollbackSupport);
149             candidate = new Candidate(id, netconfOps, rollbackSupport);
150             running = new Running(id, netconfOps, rollbackSupport);
151         }
152
153         @Override
154         public void discardChanges() {
155             candidate.discardChanges();
156         }
157
158         @Override
159         ListenableFuture<? extends DOMRpcResult> lockSingle() {
160             throw new UnsupportedOperationException();
161         }
162
163         @Override
164         List<ListenableFuture<? extends DOMRpcResult>> lockImpl() {
165             return List.of(candidate.lockSingle(), running.lockSingle());
166         }
167
168         @Override
169         void unlockImpl() {
170             running.unlock();
171             candidate.unlock();
172         }
173
174         @Override
175         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild<?, ?> editStructure,
176                 final ModifyAction defaultOperation) {
177             return candidate.editConfig(editStructure, defaultOperation);
178         }
179
180         @Override
181         ListenableFuture<RpcResult<Void>> commitImpl(final List<ListenableFuture<? extends DOMRpcResult>> results) {
182             return candidate.commitImpl(results);
183         }
184     }
185
186     private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfDataTreeService.class);
187
188     final @NonNull RemoteDeviceId id;
189     final NetconfBaseOps netconfOps;
190     final boolean rollbackSupport;
191
192     // FIXME: what do we do with locks acquired before this got flipped?
193     private volatile boolean isLockAllowed = true;
194
195     AbstractNetconfDataTreeService(final RemoteDeviceId id, final NetconfBaseOps netconfOps,
196             final boolean rollbackSupport) {
197         this.id = requireNonNull(id);
198         this.netconfOps = requireNonNull(netconfOps);
199         this.rollbackSupport = rollbackSupport;
200     }
201
202     public static @NonNull AbstractNetconfDataTreeService of(final RemoteDeviceId id,
203             final MountPointContext mountContext, final DOMRpcService rpc,
204             final NetconfSessionPreferences netconfSessionPreferences) {
205         final NetconfBaseOps netconfOps = new NetconfBaseOps(rpc, mountContext);
206         final boolean rollbackSupport = netconfSessionPreferences.isRollbackSupported();
207
208         // Examine preferences and decide which implementation to use
209         if (netconfSessionPreferences.isCandidateSupported()) {
210             return netconfSessionPreferences.isRunningWritable()
211                 ? new CandidateWithRunning(id, netconfOps, rollbackSupport)
212                     : new Candidate(id, netconfOps, rollbackSupport);
213         } else if (netconfSessionPreferences.isRunningWritable()) {
214             return new Running(id, netconfOps, rollbackSupport);
215         } else {
216             throw new IllegalArgumentException("Device " + id.getName() + " has advertised neither :writable-running "
217                 + "nor :candidate capability. Failed to establish session, as at least one of these must be "
218                 + "advertised.");
219         }
220     }
221
222     @Override
223     public synchronized List<ListenableFuture<? extends DOMRpcResult>> lock() {
224         if (isLockAllowed) {
225             return lockImpl();
226         }
227         LOG.trace("Lock is not allowed: {}", id);
228         return List.of();
229     }
230
231     List<ListenableFuture<? extends DOMRpcResult>> lockImpl() {
232         return List.of(lockSingle());
233     }
234
235     abstract ListenableFuture<? extends DOMRpcResult> lockSingle();
236
237     @Override
238     // FIXME: this should be asynchronous as well
239     public synchronized void unlock() {
240         // FIXME: deal with lock with lifecycle?
241         if (isLockAllowed) {
242             unlockImpl();
243         } else {
244             LOG.trace("Unlock is not allowed: {}", id);
245         }
246     }
247
248     abstract void unlockImpl();
249
250     @Override
251     public ListenableFuture<Optional<NormalizedNode<?, ?>>> get(final YangInstanceIdentifier path) {
252         return netconfOps.getData(new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path));
253     }
254
255     @Override
256     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getConfig(final YangInstanceIdentifier path) {
257         return netconfOps.getConfigRunningData(
258                 new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path));
259     }
260
261     @Override
262     public synchronized ListenableFuture<? extends DOMRpcResult> merge(final LogicalDatastoreType store,
263             final YangInstanceIdentifier path, final NormalizedNode<?, ?> data,
264             final Optional<ModifyAction> defaultOperation) {
265         checkEditable(store);
266         return editConfig(
267             netconfOps.createEditConfigStrcture(Optional.ofNullable(data), Optional.of(ModifyAction.MERGE), path),
268             defaultOperation.orElse(null));
269     }
270
271     @Override
272     public synchronized ListenableFuture<? extends DOMRpcResult> replace(final LogicalDatastoreType store,
273             final YangInstanceIdentifier path, final NormalizedNode<?, ?> data,
274             final Optional<ModifyAction> defaultOperation) {
275         checkEditable(store);
276         return editConfig(
277             netconfOps.createEditConfigStrcture(Optional.ofNullable(data), Optional.of(ModifyAction.REPLACE), path),
278             defaultOperation.orElse(null));
279     }
280
281     @Override
282     public synchronized ListenableFuture<? extends DOMRpcResult> create(final LogicalDatastoreType store,
283             final YangInstanceIdentifier path, final NormalizedNode<?, ?> data,
284             final Optional<ModifyAction> defaultOperation) {
285         checkEditable(store);
286         return editConfig(
287             netconfOps.createEditConfigStrcture(Optional.ofNullable(data), Optional.of(ModifyAction.CREATE), path),
288             defaultOperation.orElse(null));
289     }
290
291     @Override
292     public synchronized ListenableFuture<? extends DOMRpcResult> delete(final LogicalDatastoreType store,
293             final YangInstanceIdentifier path) {
294         return editConfig(netconfOps.createEditConfigStrcture(Optional.empty(), Optional.of(ModifyAction.DELETE), path),
295             null);
296     }
297
298     @Override
299     public synchronized ListenableFuture<? extends DOMRpcResult> remove(final LogicalDatastoreType store,
300             final YangInstanceIdentifier path) {
301         return editConfig(netconfOps.createEditConfigStrcture(Optional.empty(), Optional.of(ModifyAction.REMOVE), path),
302             null);
303     }
304
305     @Override
306     public synchronized ListenableFuture<? extends CommitInfo> commit(
307             final List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
308         final SettableFuture<CommitInfo> resultFuture = SettableFuture.create();
309         Futures.addCallback(commitImpl(resultsFutures), new FutureCallback<>() {
310             @Override
311             public void onSuccess(final RpcResult<Void> result) {
312                 if (!result.isSuccessful()) {
313                     final Collection<RpcError> errors = result.getErrors();
314                     resultFuture.setException(new TransactionCommitFailedException(
315                             String.format("Commit of transaction %s failed", this),
316                             errors.toArray(new RpcError[errors.size()])));
317                     return;
318                 }
319                 resultFuture.set(CommitInfo.empty());
320             }
321
322             @Override
323             public void onFailure(final Throwable failure) {
324                 resultFuture.setException(new TransactionCommitFailedException(
325                         String.format("Commit of transaction %s failed", this), failure));
326             }
327         }, MoreExecutors.directExecutor());
328         return resultFuture;
329     }
330
331     abstract ListenableFuture<RpcResult<Void>> commitImpl(List<ListenableFuture<? extends DOMRpcResult>> results);
332
333     @Override
334     public final Object getDeviceId() {
335         return id;
336     }
337
338     final void setLockAllowed(final boolean isLockAllowedOrig) {
339         this.isLockAllowed = isLockAllowedOrig;
340     }
341
342     abstract ListenableFuture<? extends DOMRpcResult> editConfig(DataContainerChild<?, ?> editStructure,
343         @Nullable ModifyAction defaultOperation);
344
345     private static void checkEditable(final LogicalDatastoreType store) {
346         checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can only edit configuration data, not %s", store);
347     }
348
349     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
350         justification = "https://github.com/spotbugs/spotbugs/issues/811")
351     private static ListenableFuture<RpcResult<Void>> resultsToStatus(
352             final RemoteDeviceId id, final List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
353         final SettableFuture<RpcResult<Void>> transformed = SettableFuture.create();
354
355         Futures.addCallback(Futures.allAsList(resultsFutures), new FutureCallback<>() {
356             @Override
357             public void onSuccess(final List<DOMRpcResult> domRpcResults) {
358                 if (!transformed.isDone()) {
359                     extractResult(domRpcResults, transformed, id);
360                 }
361             }
362
363             @Override
364             public void onFailure(final Throwable throwable) {
365                 final NetconfDocumentedException exception =
366                         new NetconfDocumentedException(
367                                 id + ":RPC during tx returned an exception" + throwable.getMessage(),
368                                 new Exception(throwable),
369                                 DocumentedException.ErrorType.APPLICATION,
370                                 DocumentedException.ErrorTag.OPERATION_FAILED,
371                                 DocumentedException.ErrorSeverity.ERROR);
372                 transformed.setException(exception);
373             }
374         }, MoreExecutors.directExecutor());
375
376         return transformed;
377     }
378
379     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
380             justification = "https://github.com/spotbugs/spotbugs/issues/811")
381     private static void extractResult(final List<DOMRpcResult> domRpcResults,
382                                       final SettableFuture<RpcResult<Void>> transformed,
383                                       final RemoteDeviceId id) {
384         DocumentedException.ErrorType errType = DocumentedException.ErrorType.APPLICATION;
385         DocumentedException.ErrorSeverity errSeverity = DocumentedException.ErrorSeverity.ERROR;
386         StringJoiner msgBuilder = new StringJoiner(" ");
387         boolean errorsEncouneterd = false;
388         String errorTag = "operation-failed";
389
390         for (final DOMRpcResult domRpcResult : domRpcResults) {
391             if (!domRpcResult.getErrors().isEmpty()) {
392                 errorsEncouneterd = true;
393                 final RpcError error = domRpcResult.getErrors().iterator().next();
394                 final RpcError.ErrorType errorType = error.getErrorType();
395                 switch (errorType) {
396                     case RPC:
397                         errType = DocumentedException.ErrorType.RPC;
398                         break;
399                     case PROTOCOL:
400                         errType = DocumentedException.ErrorType.PROTOCOL;
401                         break;
402                     case TRANSPORT:
403                         errType = DocumentedException.ErrorType.TRANSPORT;
404                         break;
405                     case APPLICATION:
406                     default:
407                         errType = DocumentedException.ErrorType.APPLICATION;
408                         break;
409                 }
410                 final RpcError.ErrorSeverity severity = error.getSeverity();
411                 switch (severity) {
412                     case WARNING:
413                         errSeverity = DocumentedException.ErrorSeverity.WARNING;
414                         break;
415                     case ERROR:
416                     default:
417                         errSeverity = DocumentedException.ErrorSeverity.ERROR;
418                         break;
419                 }
420                 msgBuilder.add(error.getMessage());
421                 msgBuilder.add(error.getInfo());
422                 errorTag = error.getTag();
423             }
424         }
425         if (errorsEncouneterd) {
426             final NetconfDocumentedException exception = new NetconfDocumentedException(id
427                     + ":RPC during tx failed. " + msgBuilder.toString(),
428                     errType,
429                     DocumentedException.ErrorTag.from(errorTag),
430                     errSeverity);
431             transformed.setException(exception);
432             return;
433         }
434         transformed.set(RpcResultBuilder.<Void>success().build());
435     }
436 }