Create NetconfDataTreeService with base and additional operations for netconf
[netconf.git] / netconf / sal-netconf-connector / src / main / java / org / opendaylight / netconf / sal / connect / netconf / sal / NetconfDataTreeServiceImpl.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
12 import com.google.common.base.Preconditions;
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.ArrayList;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Optional;
23 import org.opendaylight.mdsal.common.api.CommitInfo;
24 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
25 import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
26 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
27 import org.opendaylight.mdsal.dom.api.DOMRpcService;
28 import org.opendaylight.netconf.api.DocumentedException;
29 import org.opendaylight.netconf.api.ModifyAction;
30 import org.opendaylight.netconf.api.NetconfDocumentedException;
31 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
32 import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
33 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps;
34 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfRpcFutureCallback;
35 import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
36 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
37 import org.opendaylight.yangtools.yang.common.RpcError;
38 import org.opendaylight.yangtools.yang.common.RpcResult;
39 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
42 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 public class NetconfDataTreeServiceImpl implements NetconfDataTreeService {
47     private static final Logger LOG = LoggerFactory.getLogger(NetconfDataTreeServiceImpl.class);
48
49     private final RemoteDeviceId id;
50     private final NetconfBaseOps netconfOps;
51     private final boolean rollbackSupport;
52     private final boolean candidateSupported;
53     private final boolean runningWritable;
54
55     private boolean isLockAllowed = true;
56
57     public NetconfDataTreeServiceImpl(final RemoteDeviceId id, final MountPointContext mountContext,
58                                       final DOMRpcService rpc,
59                                       final NetconfSessionPreferences netconfSessionPreferences) {
60         this.id = id;
61         this.netconfOps = new NetconfBaseOps(rpc, mountContext);
62         // get specific attributes from netconf preferences and get rid of it
63         // no need to keep the entire preferences object, its quite big with all the capability QNames
64         candidateSupported = netconfSessionPreferences.isCandidateSupported();
65         runningWritable = netconfSessionPreferences.isRunningWritable();
66         rollbackSupport = netconfSessionPreferences.isRollbackSupported();
67         Preconditions.checkArgument(candidateSupported || runningWritable,
68                 "Device %s has advertised neither :writable-running nor :candidate capability."
69                         + "At least one of these should be advertised. Failed to establish a session.", id.getName());
70     }
71
72     @Override
73     public synchronized List<ListenableFuture<? extends DOMRpcResult>> lock() {
74         final List<ListenableFuture<? extends DOMRpcResult>> resultsFutures = new ArrayList<>();
75         if (candidateSupported) {
76             lockCandidate(resultsFutures);
77             if (runningWritable) {
78                 lockRunning(resultsFutures);
79             }
80         } else {
81             lockRunning(resultsFutures);
82         }
83         return resultsFutures;
84     }
85
86     @Override
87     public synchronized void unlock() {
88         if (candidateSupported) {
89             unlockCandidate();
90             if (runningWritable) {
91                 unlockRunning();
92             }
93         } else {
94             unlockRunning();
95         }
96     }
97
98     /**
99      * This has to be non blocking since it is called from a callback on commit
100      * and its netty threadpool that is really sensitive to blocking calls.
101      */
102     @Override
103     public void discardChanges() {
104         if (candidateSupported) {
105             netconfOps.discardChanges(new NetconfRpcFutureCallback("Discarding candidate", id));
106         }
107     }
108
109     @Override
110     public ListenableFuture<Optional<NormalizedNode<?, ?>>> get(YangInstanceIdentifier path) {
111         return netconfOps.getData(new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path));
112     }
113
114     @Override
115     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getConfig(final YangInstanceIdentifier path) {
116         return netconfOps.getConfigRunningData(
117                 new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path));
118     }
119
120     @Override
121     public synchronized ListenableFuture<? extends DOMRpcResult> merge(final LogicalDatastoreType store,
122                                                                        final YangInstanceIdentifier path,
123                                                                        final NormalizedNode<?, ?> data,
124                                                                        final Optional<ModifyAction> defaultOperation) {
125         checkEditable(store);
126         final DataContainerChild<?, ?> editStructure = netconfOps.createEditConfigStrcture(Optional.ofNullable(data),
127                 Optional.of(ModifyAction.MERGE), path);
128
129         return editConfig(defaultOperation, editStructure);
130     }
131
132     @Override
133     public synchronized ListenableFuture<? extends DOMRpcResult> replace(
134             final LogicalDatastoreType store,
135             final YangInstanceIdentifier path,
136             final NormalizedNode<?, ?> data,
137             final Optional<ModifyAction> defaultOperation) {
138         checkEditable(store);
139         final DataContainerChild<?, ?> editStructure = netconfOps.createEditConfigStrcture(Optional.ofNullable(data),
140                 Optional.of(ModifyAction.REPLACE), path);
141
142         return editConfig(defaultOperation, editStructure);
143     }
144
145     @Override
146     public synchronized ListenableFuture<? extends DOMRpcResult> create(final LogicalDatastoreType store,
147                                                                         final YangInstanceIdentifier path,
148                                                                         final NormalizedNode<?, ?> data,
149                                                                         final Optional<ModifyAction> defaultOperation) {
150         checkEditable(store);
151         final DataContainerChild<?, ?> editStructure = netconfOps.createEditConfigStrcture(Optional.ofNullable(data),
152                 Optional.of(ModifyAction.CREATE), path);
153
154         return editConfig(defaultOperation, editStructure);
155     }
156
157     @Override
158     public synchronized ListenableFuture<? extends DOMRpcResult> delete(final LogicalDatastoreType store,
159                                                                         final YangInstanceIdentifier path) {
160         final DataContainerChild<?, ?> editStructure = netconfOps.createEditConfigStrcture(Optional.empty(),
161                 Optional.of(ModifyAction.DELETE), path);
162
163         return editConfig(Optional.empty(), editStructure);
164     }
165
166     @Override
167     public synchronized ListenableFuture<? extends DOMRpcResult> remove(final LogicalDatastoreType store,
168                                                                         final YangInstanceIdentifier path) {
169         final DataContainerChild<?, ?> editStructure = netconfOps.createEditConfigStrcture(Optional.empty(),
170                 Optional.of(ModifyAction.REMOVE), path);
171
172         return editConfig(Optional.empty(), editStructure);
173     }
174
175     @Override
176     public ListenableFuture<? extends CommitInfo> commit(
177             List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
178         final SettableFuture<CommitInfo> resultFuture = SettableFuture.create();
179         Futures.addCallback(performCommit(resultsFutures), new FutureCallback<>() {
180             @Override
181             public void onSuccess(final RpcResult<Void> result) {
182                 if (!result.isSuccessful()) {
183                     final Collection<RpcError> errors = result.getErrors();
184                     resultFuture.setException(new TransactionCommitFailedException(
185                             String.format("Commit of transaction %s failed", this),
186                             errors.toArray(new RpcError[errors.size()])));
187                     return;
188                 }
189                 resultFuture.set(CommitInfo.empty());
190             }
191
192             @Override
193             public void onFailure(final Throwable failure) {
194                 resultFuture.setException(new TransactionCommitFailedException(
195                         String.format("Commit of transaction %s failed", this), failure));
196             }
197         }, MoreExecutors.directExecutor());
198         return resultFuture;
199     }
200
201     @Override
202     public Object getDeviceId() {
203         return id;
204     }
205
206     void setLockAllowed(final boolean isLockAllowedOrig) {
207         this.isLockAllowed = isLockAllowedOrig;
208     }
209
210     private ListenableFuture<? extends DOMRpcResult> editConfig(final Optional<ModifyAction> defaultOperation,
211                                                                 final DataContainerChild<?, ?> editStructure) {
212         if (candidateSupported) {
213             return editConfigCandidate(defaultOperation, editStructure);
214         } else {
215             return editConfigRunning(defaultOperation, editStructure);
216         }
217     }
218
219     private ListenableFuture<? extends DOMRpcResult> editConfigRunning(final Optional<ModifyAction> defaultOperation,
220                                                                        final DataContainerChild<?, ?> editStructure) {
221         final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit running", id);
222         if (defaultOperation.isPresent()) {
223             return netconfOps.editConfigRunning(callback, editStructure, defaultOperation.get(), rollbackSupport);
224         } else {
225             return netconfOps.editConfigRunning(callback, editStructure, rollbackSupport);
226         }
227     }
228
229     private ListenableFuture<? extends DOMRpcResult> editConfigCandidate(final Optional<ModifyAction> defaultOperation,
230                                                                          final DataContainerChild<?, ?> editStructure) {
231         final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit candidate", id);
232         if (defaultOperation.isPresent()) {
233             return netconfOps.editConfigCandidate(callback, editStructure, defaultOperation.get(), rollbackSupport);
234         } else {
235             return netconfOps.editConfigCandidate(callback, editStructure, rollbackSupport);
236         }
237     }
238
239     private void lockRunning(List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
240         if (isLockAllowed) {
241             resultsFutures.add(netconfOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id)));
242         } else {
243             LOG.trace("Lock is not allowed: {}", id);
244         }
245     }
246
247     private void unlockRunning() {
248         if (isLockAllowed) {
249             netconfOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
250         } else {
251             LOG.trace("Unlock is not allowed: {}", id);
252         }
253     }
254
255     private void lockCandidate(List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
256         if (isLockAllowed) {
257             resultsFutures.add(netconfOps.lockCandidate(new NetconfRpcFutureCallback("Lock candidate", id) {
258                 @Override
259                 public void onFailure(Throwable throwable) {
260                     super.onFailure(throwable);
261                     discardChanges();
262                 }
263             }));
264         } else {
265             LOG.trace("Lock is not allowed: {}", id);
266         }
267     }
268
269     private void unlockCandidate() {
270         if (isLockAllowed) {
271             netconfOps.unlockCandidate(new NetconfRpcFutureCallback("Unlock candidate", id));
272         } else {
273             LOG.trace("Unlock is not allowed: {}", id);
274         }
275     }
276
277     private void checkEditable(final LogicalDatastoreType store) {
278         checkArgument(store == LogicalDatastoreType.CONFIGURATION,
279                 "Can edit only configuration data, not %s", store);
280     }
281
282     private synchronized ListenableFuture<RpcResult<Void>> performCommit(
283             final List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
284         resultsFutures.add(netconfOps.commit(new NetconfRpcFutureCallback("Commit", id)));
285
286         final ListenableFuture<RpcResult<Void>> txResult = resultsToTxStatus(id, resultsFutures);
287         Futures.addCallback(txResult, new FutureCallback<>() {
288             @Override
289             public void onSuccess(final RpcResult<Void> result) {
290                 unlock();
291             }
292
293             @Override
294             public void onFailure(final Throwable throwable) {
295                 discardChanges();
296                 unlock();
297             }
298         }, MoreExecutors.directExecutor());
299
300         return txResult;
301     }
302
303     private static ListenableFuture<RpcResult<Void>> resultsToTxStatus(
304             final RemoteDeviceId id, List<ListenableFuture<? extends DOMRpcResult>> resultsFutures) {
305         final SettableFuture<RpcResult<Void>> transformed = SettableFuture.create();
306
307         Futures.addCallback(Futures.allAsList(resultsFutures), new FutureCallback<>() {
308             @Override
309             public void onSuccess(final List<DOMRpcResult> domRpcResults) {
310                 if (!transformed.isDone()) {
311                     extractResult(domRpcResults, transformed, id);
312                 }
313             }
314
315             @Override
316             public void onFailure(final Throwable throwable) {
317                 final NetconfDocumentedException exception =
318                         new NetconfDocumentedException(
319                                 id + ":RPC during tx returned an exception" + throwable.getMessage(),
320                                 new Exception(throwable),
321                                 DocumentedException.ErrorType.APPLICATION,
322                                 DocumentedException.ErrorTag.OPERATION_FAILED,
323                                 DocumentedException.ErrorSeverity.ERROR);
324                 transformed.setException(exception);
325             }
326         }, MoreExecutors.directExecutor());
327
328         return transformed;
329     }
330
331     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
332             justification = "https://github.com/spotbugs/spotbugs/issues/811")
333     private static void extractResult(final List<DOMRpcResult> domRpcResults,
334                                       final SettableFuture<RpcResult<Void>> transformed,
335                                       final RemoteDeviceId id) {
336         DocumentedException.ErrorType errType = DocumentedException.ErrorType.APPLICATION;
337         DocumentedException.ErrorSeverity errSeverity = DocumentedException.ErrorSeverity.ERROR;
338         StringBuilder msgBuilder = new StringBuilder();
339         boolean errorsEncouneterd = false;
340         String errorTag = "operation-failed";
341
342         for (final DOMRpcResult domRpcResult : domRpcResults) {
343             if (!domRpcResult.getErrors().isEmpty()) {
344                 errorsEncouneterd = true;
345                 final RpcError error = domRpcResult.getErrors().iterator().next();
346                 final RpcError.ErrorType errorType = error.getErrorType();
347                 switch (errorType) {
348                     case RPC:
349                         errType = DocumentedException.ErrorType.RPC;
350                         break;
351                     case PROTOCOL:
352                         errType = DocumentedException.ErrorType.PROTOCOL;
353                         break;
354                     case TRANSPORT:
355                         errType = DocumentedException.ErrorType.TRANSPORT;
356                         break;
357                     case APPLICATION:
358                     default:
359                         errType = DocumentedException.ErrorType.APPLICATION;
360                         break;
361                 }
362                 final RpcError.ErrorSeverity severity = error.getSeverity();
363                 switch (severity) {
364                     case WARNING:
365                         errSeverity = DocumentedException.ErrorSeverity.WARNING;
366                         break;
367                     case ERROR:
368                     default:
369                         errSeverity = DocumentedException.ErrorSeverity.ERROR;
370                         break;
371                 }
372                 msgBuilder.append(error.getMessage());
373                 msgBuilder.append(error.getInfo());
374                 errorTag = error.getTag();
375             }
376         }
377         if (errorsEncouneterd) {
378             final NetconfDocumentedException exception = new NetconfDocumentedException(id
379                     + ":RPC during tx failed. " + msgBuilder.toString(),
380                     errType,
381                     DocumentedException.ErrorTag.from(errorTag),
382                     errSeverity);
383             transformed.setException(exception);
384             return;
385         }
386         transformed.set(RpcResultBuilder.<Void>success().build());
387     }
388 }