Bug 3866: Support for Restconf HTTP Patch
[netconf.git] / opendaylight / restconf / sal-rest-connector / src / main / java / org / opendaylight / netconf / sal / restconf / impl / BrokerFacade.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. 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.restconf.impl;
9
10 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.CONFIGURATION;
11 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL;
12 import com.google.common.base.Optional;
13 import com.google.common.base.Preconditions;
14 import com.google.common.collect.ImmutableList;
15 import com.google.common.util.concurrent.CheckedFuture;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import java.util.ArrayList;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.concurrent.ExecutionException;
21 import javax.ws.rs.core.Response.Status;
22 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope;
23 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
24 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
25 import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
26 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
27 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadTransaction;
28 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
29 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
30 import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint;
31 import org.opendaylight.controller.md.sal.dom.api.DOMRpcException;
32 import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult;
33 import org.opendaylight.controller.md.sal.dom.api.DOMRpcService;
34 import org.opendaylight.controller.sal.core.api.Broker.ConsumerSession;
35 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
36 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
37 import org.opendaylight.netconf.sal.streams.listeners.ListenerAdapter;
38 import org.opendaylight.yangtools.concepts.ListenerRegistration;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
41 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
42 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
43 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
44 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
45 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
46 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 public class BrokerFacade {
51     private final static Logger LOG = LoggerFactory.getLogger(BrokerFacade.class);
52
53     private final static BrokerFacade INSTANCE = new BrokerFacade();
54     private volatile DOMRpcService rpcService;
55     private volatile ConsumerSession context;
56     private DOMDataBroker domDataBroker;
57
58     private BrokerFacade() {
59     }
60
61     public void setRpcService(final DOMRpcService router) {
62         rpcService = router;
63     }
64
65     public void setContext(final ConsumerSession context) {
66         this.context = context;
67     }
68
69     public static BrokerFacade getInstance() {
70         return BrokerFacade.INSTANCE;
71     }
72
73     private void checkPreconditions() {
74         if (context == null || domDataBroker == null) {
75             throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE);
76         }
77     }
78
79     // READ configuration
80     public NormalizedNode<?, ?> readConfigurationData(final YangInstanceIdentifier path) {
81         checkPreconditions();
82         return readDataViaTransaction(domDataBroker.newReadOnlyTransaction(), CONFIGURATION, path);
83     }
84
85     public NormalizedNode<?, ?> readConfigurationData(final DOMMountPoint mountPoint, final YangInstanceIdentifier path) {
86         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
87         if (domDataBrokerService.isPresent()) {
88             return readDataViaTransaction(domDataBrokerService.get().newReadOnlyTransaction(), CONFIGURATION, path);
89         }
90         final String errMsg = "DOM data broker service isn't available for mount point " + path;
91         LOG.warn(errMsg);
92         throw new RestconfDocumentedException(errMsg);
93     }
94
95     // READ operational
96     public NormalizedNode<?, ?> readOperationalData(final YangInstanceIdentifier path) {
97         checkPreconditions();
98         return readDataViaTransaction(domDataBroker.newReadOnlyTransaction(), OPERATIONAL, path);
99     }
100
101     public NormalizedNode<?, ?> readOperationalData(final DOMMountPoint mountPoint, final YangInstanceIdentifier path) {
102         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
103         if (domDataBrokerService.isPresent()) {
104             return readDataViaTransaction(domDataBrokerService.get().newReadOnlyTransaction(), OPERATIONAL, path);
105         }
106         final String errMsg = "DOM data broker service isn't available for mount point " + path;
107         LOG.warn(errMsg);
108         throw new RestconfDocumentedException(errMsg);
109     }
110
111     // PUT configuration
112     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPut(
113             final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
114         checkPreconditions();
115         return putDataViaTransaction(domDataBroker, CONFIGURATION, path, payload, globalSchema);
116     }
117
118     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPut(
119             final DOMMountPoint mountPoint, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
120         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
121         if (domDataBrokerService.isPresent()) {
122             return putDataViaTransaction(domDataBrokerService.get(), CONFIGURATION, path,
123                     payload, mountPoint.getSchemaContext());
124         }
125         final String errMsg = "DOM data broker service isn't available for mount point " + path;
126         LOG.warn(errMsg);
127         throw new RestconfDocumentedException(errMsg);
128     }
129
130     public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext context,
131                                                                       final SchemaContext globalSchema) {
132         final DOMDataReadWriteTransaction patchTransaction = domDataBroker.newReadWriteTransaction();
133         List<PATCHStatusEntity> editCollection = new ArrayList<>();
134         List<RestconfError> editErrors;
135         List<RestconfError> globalErrors = null;
136         int errorCounter = 0;
137
138         for (PATCHEntity patchEntity : context.getData()) {
139             final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase());
140
141             switch (operation) {
142                 case CREATE:
143                     if (errorCounter == 0) {
144                         try {
145                             postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(),
146                                     patchEntity.getNode(), globalSchema);
147                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
148                         } catch (RestconfDocumentedException e) {
149                             editErrors = new ArrayList<>();
150                             editErrors.addAll(e.getErrors());
151                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
152                             errorCounter++;
153                         }
154                     }
155                     break;
156                 case REPLACE:
157                     if (errorCounter == 0) {
158                         try {
159                             putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
160                                     .getTargetNode(), patchEntity.getNode(), globalSchema);
161                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
162                         } catch (RestconfDocumentedException e) {
163                             editErrors = new ArrayList<>();
164                             editErrors.addAll(e.getErrors());
165                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
166                             errorCounter++;
167                         }
168                     }
169                     break;
170                 case DELETE:
171                     if (errorCounter == 0) {
172                         try {
173                             deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
174                                     .getTargetNode());
175                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
176                         } catch (RestconfDocumentedException e) {
177                             editErrors = new ArrayList<>();
178                             editErrors.addAll(e.getErrors());
179                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
180                             errorCounter++;
181                         }
182                     }
183                     break;
184                 case REMOVE:
185                     if (errorCounter == 0) {
186                         try {
187                             deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
188                                     .getTargetNode());
189                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
190                         } catch (RestconfDocumentedException e) {
191                             LOG.error("Error removing {} by {} operation", patchEntity.getTargetNode().toString(),
192                                     patchEntity.getEditId(), e);
193                         }
194                     }
195                     break;
196             }
197         }
198
199         //TODO: make sure possible global errors are filled up correctly and decide transaction submission based on that
200         //globalErrors = new ArrayList<>();
201         if (errorCounter == 0) {
202             final CheckedFuture<Void, TransactionCommitFailedException> submit = patchTransaction.submit();
203             return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true,
204                     globalErrors);
205         } else {
206             patchTransaction.cancel();
207             return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false,
208                     globalErrors);
209         }
210     }
211
212     // POST configuration
213     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPost(
214             final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
215         checkPreconditions();
216         return postDataViaTransaction(domDataBroker, CONFIGURATION, path, payload, globalSchema);
217     }
218
219     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPost(
220             final DOMMountPoint mountPoint, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
221         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
222         if (domDataBrokerService.isPresent()) {
223             return postDataViaTransaction(domDataBrokerService.get(), CONFIGURATION, path,
224                     payload, mountPoint.getSchemaContext());
225         }
226         final String errMsg = "DOM data broker service isn't available for mount point " + path;
227         LOG.warn(errMsg);
228         throw new RestconfDocumentedException(errMsg);
229     }
230
231     // DELETE configuration
232     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataDelete(
233             final YangInstanceIdentifier path) {
234         checkPreconditions();
235         return deleteDataViaTransaction(domDataBroker.newWriteOnlyTransaction(), CONFIGURATION, path);
236     }
237
238     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataDelete(
239             final DOMMountPoint mountPoint, final YangInstanceIdentifier path) {
240         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
241         if (domDataBrokerService.isPresent()) {
242             return deleteDataViaTransaction(domDataBrokerService.get().newWriteOnlyTransaction(), CONFIGURATION, path);
243         }
244         final String errMsg = "DOM data broker service isn't available for mount point " + path;
245         LOG.warn(errMsg);
246         throw new RestconfDocumentedException(errMsg);
247     }
248
249     // RPC
250     public CheckedFuture<DOMRpcResult, DOMRpcException> invokeRpc(final SchemaPath type, final NormalizedNode<?, ?> input) {
251         checkPreconditions();
252         if (rpcService == null) {
253             throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE);
254         }
255         LOG.trace("Invoke RPC {} with input: {}", type, input);
256         return rpcService.invokeRpc(type, input);
257     }
258
259     public void registerToListenDataChanges(final LogicalDatastoreType datastore, final DataChangeScope scope,
260             final ListenerAdapter listener) {
261         checkPreconditions();
262
263         if (listener.isListening()) {
264             return;
265         }
266
267         final YangInstanceIdentifier path = listener.getPath();
268         final ListenerRegistration<DOMDataChangeListener> registration = domDataBroker.registerDataChangeListener(
269                 datastore, path, listener, scope);
270
271         listener.setRegistration(registration);
272     }
273
274     private NormalizedNode<?, ?> readDataViaTransaction(final DOMDataReadTransaction transaction,
275             final LogicalDatastoreType datastore, final YangInstanceIdentifier path) {
276         LOG.trace("Read {} via Restconf: {}", datastore.name(), path);
277         final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = transaction.read(datastore, path);
278         if (listenableFuture != null) {
279             Optional<NormalizedNode<?, ?>> optional;
280             try {
281                 LOG.debug("Reading result data from transaction.");
282                 optional = listenableFuture.get();
283             } catch (InterruptedException | ExecutionException e) {
284                 LOG.warn("Exception by reading {} via Restconf: {}", datastore.name(), path, e);
285                 throw new RestconfDocumentedException("Problem to get data from transaction.", e.getCause());
286
287             }
288             if (optional != null) {
289                 if (optional.isPresent()) {
290                     return optional.get();
291                 }
292             }
293         }
294         return null;
295     }
296
297     private CheckedFuture<Void, TransactionCommitFailedException> postDataViaTransaction(
298             final DOMDataBroker domDataBroker, final LogicalDatastoreType datastore,
299             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
300         // FIXME: This is doing correct post for container and list children
301         //        not sure if this will work for choice case
302         DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
303         if(payload instanceof MapNode) {
304             LOG.trace("POST {} via Restconf: {} with payload {}", datastore.name(), path, payload);
305             final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
306             try {
307                 transaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
308             } catch (RuntimeException e) {
309                 // FIXME: Figure out and catch specific RunTimeExceptions thrown by NETCONF instead of generic one.
310                 //        to make this cleaner and easier to maintain.
311                 transaction.cancel();
312                 transaction = domDataBroker.newReadWriteTransaction();
313                 LOG.debug("Empty subtree merge failed", e);
314             }
315             if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
316                 transaction.cancel();
317                 transaction = domDataBroker.newReadWriteTransaction();
318             }
319             for(final MapEntryNode child : ((MapNode) payload).getValue()) {
320                 final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
321                 checkItemDoesNotExists(transaction, datastore, childPath);
322                 transaction.put(datastore, childPath, child);
323             }
324         } else {
325             checkItemDoesNotExists(transaction,datastore, path);
326             if(!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
327                 transaction.cancel();
328                 transaction = domDataBroker.newReadWriteTransaction();
329             }
330             transaction.put(datastore, path, payload);
331         }
332         return transaction.submit();
333     }
334
335     private void postDataWithinTransaction(
336             final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore,
337             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
338         // FIXME: This is doing correct post for container and list children
339         //        not sure if this will work for choice case
340         if(payload instanceof MapNode) {
341             LOG.trace("POST {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
342             final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
343             rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
344             ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
345             for(final MapEntryNode child : ((MapNode) payload).getValue()) {
346                 final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
347                 checkItemDoesNotExists(rWTransaction, datastore, childPath);
348                 rWTransaction.put(datastore, childPath, child);
349             }
350         } else {
351             checkItemDoesNotExists(rWTransaction,datastore, path);
352             ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
353             rWTransaction.put(datastore, path, payload);
354         }
355     }
356
357     private void checkItemDoesNotExists(final DOMDataReadWriteTransaction rWTransaction,final LogicalDatastoreType store, final YangInstanceIdentifier path) {
358         final ListenableFuture<Boolean> futureDatastoreData = rWTransaction.exists(store, path);
359         try {
360             if (futureDatastoreData.get()) {
361                 final String errMsg = "Post Configuration via Restconf was not executed because data already exists";
362                 LOG.trace("{}:{}", errMsg, path);
363                 rWTransaction.cancel();
364                 throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL,
365                         ErrorTag.DATA_EXISTS);
366             }
367         } catch (InterruptedException | ExecutionException e) {
368             LOG.warn("It wasn't possible to get data loaded from datastore at path {}", path, e);
369         }
370
371     }
372
373     private CheckedFuture<Void, TransactionCommitFailedException> putDataViaTransaction(
374             final DOMDataBroker domDataBroker, final LogicalDatastoreType datastore,
375             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext)
376     {
377         DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
378         LOG.trace("Put {} via Restconf: {} with payload {}", datastore.name(), path, payload);
379         if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
380             transaction.cancel();
381             transaction = domDataBroker.newReadWriteTransaction();
382         }
383         transaction.put(datastore, path, payload);
384         return transaction.submit();
385     }
386
387     private void putDataWithinTransaction(
388             final DOMDataReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
389             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
390         LOG.trace("Put {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
391         ensureParentsByMerge(datastore, path, writeTransaction, schemaContext);
392         writeTransaction.put(datastore, path, payload);
393     }
394
395     private CheckedFuture<Void, TransactionCommitFailedException> deleteDataViaTransaction(
396             final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
397             final YangInstanceIdentifier path) {
398         LOG.trace("Delete {} via Restconf: {}", datastore.name(), path);
399         writeTransaction.delete(datastore, path);
400         return writeTransaction.submit();
401     }
402
403     private void deleteDataWithinTransaction(
404             final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
405             final YangInstanceIdentifier path) {
406         LOG.trace("Delete {} within Restconf PATCH: {}", datastore.name(), path);
407         writeTransaction.delete(datastore, path);
408     }
409
410     public void setDomDataBroker(final DOMDataBroker domDataBroker) {
411         this.domDataBroker = domDataBroker;
412     }
413
414     private boolean ensureParentsByMerge(final LogicalDatastoreType store,
415                                       final YangInstanceIdentifier normalizedPath, final DOMDataReadWriteTransaction rwTx, final SchemaContext schemaContext) {
416
417         boolean mergeResult = true;
418         final List<PathArgument> normalizedPathWithoutChildArgs = new ArrayList<>();
419         YangInstanceIdentifier rootNormalizedPath = null;
420
421         final Iterator<PathArgument> it = normalizedPath.getPathArguments().iterator();
422
423         while(it.hasNext()) {
424             final PathArgument pathArgument = it.next();
425             if(rootNormalizedPath == null) {
426                 rootNormalizedPath = YangInstanceIdentifier.create(pathArgument);
427             }
428
429             // Skip last element, its not a parent
430             if(it.hasNext()) {
431                 normalizedPathWithoutChildArgs.add(pathArgument);
432             }
433         }
434
435         // No parent structure involved, no need to ensure parents
436         if(normalizedPathWithoutChildArgs.isEmpty()) {
437             return mergeResult;
438         }
439
440         Preconditions.checkArgument(rootNormalizedPath != null, "Empty path received");
441
442         final NormalizedNode<?, ?> parentStructure =
443                 ImmutableNodes.fromInstanceId(schemaContext, YangInstanceIdentifier.create(normalizedPathWithoutChildArgs));
444         try {
445             rwTx.merge(store, rootNormalizedPath, parentStructure);
446         } catch (RuntimeException e) {
447             /*
448              * Catching the exception here, logging it and proceeding further
449              * for the following reasons.
450              *
451              * 1. For MD-SAL store if it fails we'll go with the next call
452              * anyway and let the failure happen there. 2. For NETCONF devices
453              * that can not handle these calls such as creation of empty lists
454              * etc, instead of failing we'll go with the actual call. Devices
455              * should be able to handle the actual calls made without the need
456              * to create parents. So instead of failing we will give a device a
457              * chance to configure the management entity in question. 3. If this
458              * merge call is handled properly by MD-SAL data store or a Netconf
459              * device this is a no-op.
460              */
461              // FIXME: Figure out and catch specific RunTimeExceptions thrown by NETCONF instead of generic one.
462              //        to make this cleaner and easier to maintain.
463             mergeResult = false;
464             LOG.debug("Exception while creating the parent in ensureParentsByMerge. Proceeding with the actual request", e);
465         }
466         return mergeResult;
467     }
468 }