Move netconf-console to apps/
[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.collect.ImmutableList;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import com.google.common.util.concurrent.MoreExecutors;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Optional;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
24 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
25 import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult;
26 import org.opendaylight.netconf.api.EffectiveOperation;
27 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
28 import org.opendaylight.netconf.sal.connect.api.RemoteDeviceId;
29 import org.opendaylight.netconf.sal.connect.api.RemoteDeviceServices.Rpcs;
30 import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
31 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps;
32 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfRpcFutureCallback;
33 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
34 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
35 import org.opendaylight.yangtools.yang.common.RpcError;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
38 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 public abstract sealed class AbstractNetconfDataTreeService implements NetconfDataTreeService {
43     private static final class Candidate extends AbstractNetconfDataTreeService {
44         Candidate(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport,
45                 final boolean lockDatastore) {
46             super(id, netconfOps, rollbackSupport, lockDatastore);
47         }
48
49         /**
50          * This has to be non blocking since it is called from a callback on commit and it is netty threadpool that is
51          * really sensitive to blocking calls.
52          */
53         @Override
54         public ListenableFuture<? extends DOMRpcResult> discardChanges() {
55             return netconfOps.discardChanges(new NetconfRpcFutureCallback("Discard candidate", id));
56         }
57
58         @Override
59         ListenableFuture<? extends DOMRpcResult> lockSingle() {
60             return netconfOps.lockCandidate(new NetconfRpcFutureCallback("Lock candidate", id));
61         }
62
63         @Override
64         List<ListenableFuture<? extends DOMRpcResult>> unlockImpl() {
65             return List.of(netconfOps.unlockCandidate(new NetconfRpcFutureCallback("Unlock candidate", id)));
66         }
67
68         @Override
69         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild editStructure,
70                 final EffectiveOperation defaultOperation) {
71             final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit candidate", id);
72             return defaultOperation == null ? netconfOps.editConfigCandidate(callback, editStructure, rollbackSupport)
73                 : netconfOps.editConfigCandidate(callback, editStructure, defaultOperation, rollbackSupport);
74         }
75     }
76
77     private static final class Running extends AbstractNetconfDataTreeService {
78         Running(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport,
79                 final boolean lockDatastore) {
80             super(id, netconfOps, rollbackSupport, lockDatastore);
81         }
82
83         @Override
84         public ListenableFuture<DOMRpcResult> discardChanges() {
85             // Changes cannot be discarded from running
86             return RPC_SUCCESS;
87         }
88
89         @Override
90         public ListenableFuture<DOMRpcResult> commit() {
91             // No candidate, hence we commit immediately
92             return RPC_SUCCESS;
93         }
94
95         @Override
96         ListenableFuture<? extends DOMRpcResult> lockSingle() {
97             return netconfOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id));
98         }
99
100         @Override
101         List<ListenableFuture<? extends DOMRpcResult>> unlockImpl() {
102             return List.of(netconfOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id)));
103         }
104
105         @Override
106         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild editStructure,
107                 final EffectiveOperation defaultOperation) {
108             final NetconfRpcFutureCallback callback = new NetconfRpcFutureCallback("Edit running", id);
109             return defaultOperation == null ? netconfOps.editConfigRunning(callback, editStructure, rollbackSupport)
110                 : netconfOps.editConfigRunning(callback, editStructure, defaultOperation, rollbackSupport);
111         }
112     }
113
114     private static final class CandidateWithRunning extends AbstractNetconfDataTreeService {
115         private final Candidate candidate;
116         private final Running running;
117
118         CandidateWithRunning(final RemoteDeviceId id, final NetconfBaseOps netconfOps,
119                 final boolean rollbackSupport, final boolean lockDatastore) {
120             super(id, netconfOps, rollbackSupport, lockDatastore);
121             candidate = new Candidate(id, netconfOps, rollbackSupport, lockDatastore);
122             running = new Running(id, netconfOps, rollbackSupport, lockDatastore);
123         }
124
125         @Override
126         public ListenableFuture<? extends DOMRpcResult> discardChanges() {
127             return candidate.discardChanges();
128         }
129
130         @Override
131         ListenableFuture<DOMRpcResult> lockSingle() {
132             throw new UnsupportedOperationException();
133         }
134
135         @Override
136         List<ListenableFuture<? extends DOMRpcResult>> lockImpl() {
137             return List.of(candidate.lockSingle(), running.lockSingle());
138         }
139
140         @Override
141         List<ListenableFuture<? extends DOMRpcResult>> unlockImpl() {
142             return List.of(running.unlock(), candidate.unlock());
143         }
144
145         @Override
146         ListenableFuture<? extends DOMRpcResult> editConfig(final DataContainerChild editStructure,
147                 final EffectiveOperation defaultOperation) {
148             return candidate.editConfig(editStructure, defaultOperation);
149         }
150     }
151
152     private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfDataTreeService.class);
153     private static final ListenableFuture<DOMRpcResult> RPC_SUCCESS =
154         Futures.immediateFuture(new DefaultDOMRpcResult());
155
156     final @NonNull RemoteDeviceId id;
157     final NetconfBaseOps netconfOps;
158     final boolean rollbackSupport;
159
160     private final boolean lockDatastore;
161
162     AbstractNetconfDataTreeService(final RemoteDeviceId id, final NetconfBaseOps netconfOps,
163             final boolean rollbackSupport, final boolean lockDatastore) {
164         this.id = requireNonNull(id);
165         this.netconfOps = requireNonNull(netconfOps);
166         this.rollbackSupport = rollbackSupport;
167         this.lockDatastore = lockDatastore;
168     }
169
170     public static @NonNull AbstractNetconfDataTreeService of(final RemoteDeviceId id,
171             final MountPointContext mountContext, final Rpcs rpcs,
172             final NetconfSessionPreferences sessionPreferences, final boolean lockDatastore) {
173         final var netconfOps = new NetconfBaseOps(rpcs, mountContext);
174         final boolean rollbackSupport = sessionPreferences.isRollbackSupported();
175
176         // Examine preferences and decide which implementation to use
177         if (sessionPreferences.isCandidateSupported()) {
178             return sessionPreferences.isRunningWritable()
179                 ? new CandidateWithRunning(id, netconfOps, rollbackSupport, lockDatastore)
180                     : new Candidate(id, netconfOps, rollbackSupport, lockDatastore);
181         } else if (sessionPreferences.isRunningWritable()) {
182             return new Running(id, netconfOps, rollbackSupport, lockDatastore);
183         } else {
184             throw new IllegalArgumentException("Device " + id.name() + " has advertised neither :writable-running nor "
185                 + ":candidate capability. Failed to establish session, as at least one of these must be advertised.");
186         }
187     }
188
189     @Override
190     public synchronized ListenableFuture<DOMRpcResult> lock() {
191         if (!lockDatastore) {
192             LOG.trace("Lock is not allowed by device configuration, ignoring lock results: {}", id);
193             return RPC_SUCCESS;
194         }
195
196         final ListenableFuture<DOMRpcResult> result = mergeFutures(lockImpl());
197         Futures.addCallback(result, new FutureCallback<>() {
198             @Override
199             public void onSuccess(final DOMRpcResult result) {
200                 final var errors = result.errors();
201                 if (errors.isEmpty()) {
202                     LOG.debug("{}: Lock successful.", id);
203                     return;
204                 }
205                 if (allWarnings(errors)) {
206                     LOG.info("{}: Lock successful with warnings {}", errors, id);
207                     return;
208                 }
209
210                 LOG.warn("{}: Lock failed with errors {}", id, errors);
211             }
212
213             @Override
214             public void onFailure(final Throwable throwable) {
215                 LOG.warn("{}: Lock failed.", id, throwable);
216             }
217         }, MoreExecutors.directExecutor());
218
219         return result;
220     }
221
222     List<ListenableFuture<? extends DOMRpcResult>> lockImpl() {
223         return List.of(lockSingle());
224     }
225
226     abstract ListenableFuture<? extends DOMRpcResult> lockSingle();
227
228     @Override
229     public synchronized ListenableFuture<DOMRpcResult> unlock() {
230         if (!lockDatastore) {
231             LOG.trace("Unlock is not allowed: {}", id);
232             return RPC_SUCCESS;
233         }
234
235         final ListenableFuture<DOMRpcResult> result = mergeFutures(unlockImpl());
236         Futures.addCallback(result, new FutureCallback<>() {
237             @Override
238             public void onSuccess(final DOMRpcResult result) {
239                 final var errors = result.errors();
240                 if (errors.isEmpty()) {
241                     LOG.debug("{}: Unlock successful.", id);
242                     return;
243                 }
244                 if (allWarnings(errors)) {
245                     LOG.info("{}: Unlock successful with warnings {}", errors, id);
246                     return;
247                 }
248
249                 LOG.error("{}: Unlock failed with errors {}", id, errors);
250             }
251
252             @Override
253             public void onFailure(final Throwable throwable) {
254                 LOG.error("{}: Unlock failed.", id, throwable);
255             }
256         }, MoreExecutors.directExecutor());
257         return result;
258     }
259
260     abstract List<ListenableFuture<? extends DOMRpcResult>> unlockImpl();
261
262     @Override
263     public ListenableFuture<Optional<NormalizedNode>> get(final YangInstanceIdentifier path) {
264         return netconfOps.getData(new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path));
265     }
266
267     @Override
268     public ListenableFuture<Optional<NormalizedNode>> get(final YangInstanceIdentifier path,
269             final List<YangInstanceIdentifier> fields) {
270         return netconfOps.getData(new NetconfRpcFutureCallback("Data read", id), Optional.ofNullable(path), fields);
271     }
272
273     @Override
274     public ListenableFuture<Optional<NormalizedNode>> getConfig(final YangInstanceIdentifier path) {
275         return netconfOps.getConfigRunningData(new NetconfRpcFutureCallback("Data read", id),
276             Optional.ofNullable(path));
277     }
278
279     @Override
280     public ListenableFuture<Optional<NormalizedNode>> getConfig(final YangInstanceIdentifier path,
281             final List<YangInstanceIdentifier> fields) {
282         return netconfOps.getConfigRunningData(new NetconfRpcFutureCallback("Data read", id),
283             Optional.ofNullable(path), fields);
284     }
285
286     @Override
287     public synchronized ListenableFuture<? extends DOMRpcResult> merge(final LogicalDatastoreType store,
288             final YangInstanceIdentifier path, final NormalizedNode data,
289             final Optional<EffectiveOperation> defaultOperation) {
290         checkEditable(store);
291         return editConfig(
292             netconfOps.createEditConfigStructure(Optional.ofNullable(data), Optional.of(EffectiveOperation.MERGE),
293                 path),
294             defaultOperation.orElse(null));
295     }
296
297     @Override
298     public synchronized ListenableFuture<? extends DOMRpcResult> replace(final LogicalDatastoreType store,
299             final YangInstanceIdentifier path, final NormalizedNode data,
300             final Optional<EffectiveOperation> defaultOperation) {
301         checkEditable(store);
302         return editConfig(
303             netconfOps.createEditConfigStructure(Optional.ofNullable(data), Optional.of(EffectiveOperation.REPLACE),
304                 path),
305             defaultOperation.orElse(null));
306     }
307
308     @Override
309     public synchronized ListenableFuture<? extends DOMRpcResult> create(final LogicalDatastoreType store,
310             final YangInstanceIdentifier path, final NormalizedNode data,
311             final Optional<EffectiveOperation> defaultOperation) {
312         checkEditable(store);
313         return editConfig(
314             netconfOps.createEditConfigStructure(Optional.ofNullable(data), Optional.of(EffectiveOperation.CREATE),
315                 path),
316             defaultOperation.orElse(null));
317     }
318
319     @Override
320     public synchronized ListenableFuture<? extends DOMRpcResult> delete(final LogicalDatastoreType store,
321             final YangInstanceIdentifier path) {
322         return editConfig(netconfOps.createEditConfigStructure(Optional.empty(),
323                 Optional.of(EffectiveOperation.DELETE), path), null);
324     }
325
326     @Override
327     public synchronized ListenableFuture<? extends DOMRpcResult> remove(final LogicalDatastoreType store,
328             final YangInstanceIdentifier path) {
329         return editConfig(netconfOps.createEditConfigStructure(Optional.empty(),
330                 Optional.of(EffectiveOperation.REMOVE), path), null);
331     }
332
333     @Override
334     public synchronized ListenableFuture<? extends DOMRpcResult> commit() {
335         return netconfOps.commit(new NetconfRpcFutureCallback("Commit", id));
336     }
337
338     @Override
339     public final Object getDeviceId() {
340         return id;
341     }
342
343     abstract ListenableFuture<? extends DOMRpcResult> editConfig(DataContainerChild editStructure,
344         @Nullable EffectiveOperation defaultOperation);
345
346     private static void checkEditable(final LogicalDatastoreType store) {
347         checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can only edit configuration data, not %s", store);
348     }
349
350     // Transform list of futures related to RPC operation into a single Future
351     private static ListenableFuture<DOMRpcResult> mergeFutures(
352             final List<ListenableFuture<? extends DOMRpcResult>> futures) {
353         return Futures.whenAllComplete(futures).call(() -> {
354             if (futures.size() == 1) {
355                 // Fast path
356                 return Futures.getDone(futures.get(0));
357             }
358
359             final var builder = ImmutableList.<RpcError>builder();
360             for (ListenableFuture<? extends DOMRpcResult> future : futures) {
361                 builder.addAll(Futures.getDone(future).errors());
362             }
363             return new DefaultDOMRpcResult(null, builder.build());
364         }, MoreExecutors.directExecutor());
365     }
366
367     private static boolean allWarnings(final Collection<? extends @NonNull RpcError> errors) {
368         return errors.stream().allMatch(error -> error.getSeverity() == ErrorSeverity.WARNING);
369     }
370 }