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
index 8e1a674c93ebb2864e76aa64bf9130c9635e3a9f..b4be7c683ccc119d6b75e302f09d2f7122abdcdb 100644 (file)
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -11,6 +11,7 @@ import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastor
 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.ArrayList;
@@ -111,14 +112,14 @@ public class BrokerFacade {
     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPut(
             final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
         checkPreconditions();
-        return putDataViaTransaction(domDataBroker.newReadWriteTransaction(), CONFIGURATION, path, payload, globalSchema);
+        return putDataViaTransaction(domDataBroker, CONFIGURATION, path, payload, globalSchema);
     }
 
     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPut(
             final DOMMountPoint mountPoint, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
         if (domDataBrokerService.isPresent()) {
-            return putDataViaTransaction(domDataBrokerService.get().newReadWriteTransaction(), CONFIGURATION, path,
+            return putDataViaTransaction(domDataBrokerService.get(), CONFIGURATION, path,
                     payload, mountPoint.getSchemaContext());
         }
         final String errMsg = "DOM data broker service isn't available for mount point " + path;
@@ -126,18 +127,100 @@ public class BrokerFacade {
         throw new RestconfDocumentedException(errMsg);
     }
 
+    public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext context,
+                                                                      final SchemaContext globalSchema) {
+        final DOMDataReadWriteTransaction patchTransaction = domDataBroker.newReadWriteTransaction();
+        List<PATCHStatusEntity> editCollection = new ArrayList<>();
+        List<RestconfError> editErrors;
+        List<RestconfError> globalErrors = null;
+        int errorCounter = 0;
+
+        for (PATCHEntity patchEntity : context.getData()) {
+            final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase());
+
+            switch (operation) {
+                case CREATE:
+                    if (errorCounter == 0) {
+                        try {
+                            postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(),
+                                    patchEntity.getNode(), globalSchema);
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case REPLACE:
+                    if (errorCounter == 0) {
+                        try {
+                            putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode(), patchEntity.getNode(), globalSchema);
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case DELETE:
+                    if (errorCounter == 0) {
+                        try {
+                            deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case REMOVE:
+                    if (errorCounter == 0) {
+                        try {
+                            deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            LOG.error("Error removing {} by {} operation", patchEntity.getTargetNode().toString(),
+                                    patchEntity.getEditId(), e);
+                        }
+                    }
+                    break;
+            }
+        }
+
+        //TODO: make sure possible global errors are filled up correctly and decide transaction submission based on that
+        //globalErrors = new ArrayList<>();
+        if (errorCounter == 0) {
+            final CheckedFuture<Void, TransactionCommitFailedException> submit = patchTransaction.submit();
+            return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true,
+                    globalErrors);
+        } else {
+            patchTransaction.cancel();
+            return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false,
+                    globalErrors);
+        }
+    }
+
     // POST configuration
     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPost(
             final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
         checkPreconditions();
-        return postDataViaTransaction(domDataBroker.newReadWriteTransaction(), CONFIGURATION, path, payload, globalSchema);
+        return postDataViaTransaction(domDataBroker, CONFIGURATION, path, payload, globalSchema);
     }
 
     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPost(
             final DOMMountPoint mountPoint, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
         final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class);
         if (domDataBrokerService.isPresent()) {
-            return postDataViaTransaction(domDataBrokerService.get().newReadWriteTransaction(), CONFIGURATION, path,
+            return postDataViaTransaction(domDataBrokerService.get(), CONFIGURATION, path,
                     payload, mountPoint.getSchemaContext());
         }
         final String errMsg = "DOM data broker service isn't available for mount point " + path;
@@ -169,6 +252,7 @@ public class BrokerFacade {
         if (rpcService == null) {
             throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE);
         }
+        LOG.trace("Invoke RPC {} with input: {}", type, input);
         return rpcService.invokeRpc(type, input);
     }
 
@@ -189,7 +273,7 @@ public class BrokerFacade {
 
     private NormalizedNode<?, ?> readDataViaTransaction(final DOMDataReadTransaction transaction,
             final LogicalDatastoreType datastore, final YangInstanceIdentifier path) {
-        LOG.trace("Read " + datastore.name() + " via Restconf: {}", path);
+        LOG.trace("Read {} via Restconf: {}", datastore.name(), path);
         final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = transaction.read(datastore, path);
         if (listenableFuture != null) {
             Optional<NormalizedNode<?, ?>> optional;
@@ -197,7 +281,7 @@ public class BrokerFacade {
                 LOG.debug("Reading result data from transaction.");
                 optional = listenableFuture.get();
             } catch (InterruptedException | ExecutionException e) {
-                LOG.warn("Exception by reading " + datastore.name() + " via Restconf: {}", path, e);
+                LOG.warn("Exception by reading {} via Restconf: {}", datastore.name(), path, e);
                 throw new RestconfDocumentedException("Problem to get data from transaction.", e.getCause());
 
             }
@@ -211,12 +295,50 @@ public class BrokerFacade {
     }
 
     private CheckedFuture<Void, TransactionCommitFailedException> postDataViaTransaction(
+            final DOMDataBroker domDataBroker, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
+        // FIXME: This is doing correct post for container and list children
+        //        not sure if this will work for choice case
+        DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
+        if(payload instanceof MapNode) {
+            LOG.trace("POST {} via Restconf: {} with payload {}", datastore.name(), path, payload);
+            final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
+            try {
+                transaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
+            } catch (RuntimeException e) {
+                // FIXME: Figure out and catch specific RunTimeExceptions thrown by NETCONF instead of generic one.
+                //        to make this cleaner and easier to maintain.
+                transaction.cancel();
+                transaction = domDataBroker.newReadWriteTransaction();
+                LOG.debug("Empty subtree merge failed", e);
+            }
+            if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
+                transaction.cancel();
+                transaction = domDataBroker.newReadWriteTransaction();
+            }
+            for(final MapEntryNode child : ((MapNode) payload).getValue()) {
+                final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
+                checkItemDoesNotExists(transaction, datastore, childPath);
+                transaction.put(datastore, childPath, child);
+            }
+        } else {
+            checkItemDoesNotExists(transaction,datastore, path);
+            if(!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
+                transaction.cancel();
+                transaction = domDataBroker.newReadWriteTransaction();
+            }
+            transaction.put(datastore, path, payload);
+        }
+        return transaction.submit();
+    }
+
+    private void postDataWithinTransaction(
             final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore,
             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
         // FIXME: This is doing correct post for container and list children
         //        not sure if this will work for choice case
         if(payload instanceof MapNode) {
-            LOG.trace("POST " + datastore.name() + " via Restconf: {} with payload {}", path, payload);
+            LOG.trace("POST {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
             final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
             rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
             ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
@@ -230,7 +352,6 @@ public class BrokerFacade {
             ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
             rWTransaction.put(datastore, path, payload);
         }
-        return rWTransaction.submit();
     }
 
     private void checkItemDoesNotExists(final DOMDataReadWriteTransaction rWTransaction,final LogicalDatastoreType store, final YangInstanceIdentifier path) {
@@ -238,40 +359,62 @@ public class BrokerFacade {
         try {
             if (futureDatastoreData.get()) {
                 final String errMsg = "Post Configuration via Restconf was not executed because data already exists";
-                LOG.trace(errMsg + ":{}", path);
+                LOG.trace("{}:{}", errMsg, path);
                 rWTransaction.cancel();
                 throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL,
                         ErrorTag.DATA_EXISTS);
             }
         } catch (InterruptedException | ExecutionException e) {
-            LOG.warn("It wasn't possible to get data loaded from datastore at path " + path, e);
+            LOG.warn("It wasn't possible to get data loaded from datastore at path {}", path, e);
         }
 
     }
 
     private CheckedFuture<Void, TransactionCommitFailedException> putDataViaTransaction(
+            final DOMDataBroker domDataBroker, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext)
+    {
+        DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
+        LOG.trace("Put {} via Restconf: {} with payload {}", datastore.name(), path, payload);
+        if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
+            transaction.cancel();
+            transaction = domDataBroker.newReadWriteTransaction();
+        }
+        transaction.put(datastore, path, payload);
+        return transaction.submit();
+    }
+
+    private void putDataWithinTransaction(
             final DOMDataReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
-        LOG.trace("Put " + datastore.name() + " via Restconf: {} with payload {}", path, payload);
+        LOG.trace("Put {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
         ensureParentsByMerge(datastore, path, writeTransaction, schemaContext);
         writeTransaction.put(datastore, path, payload);
-        return writeTransaction.submit();
     }
 
     private CheckedFuture<Void, TransactionCommitFailedException> deleteDataViaTransaction(
             final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
             final YangInstanceIdentifier path) {
-        LOG.trace("Delete " + datastore.name() + " via Restconf: {}", path);
+        LOG.trace("Delete {} via Restconf: {}", datastore.name(), path);
         writeTransaction.delete(datastore, path);
         return writeTransaction.submit();
     }
 
+    private void deleteDataWithinTransaction(
+            final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path) {
+        LOG.trace("Delete {} within Restconf PATCH: {}", datastore.name(), path);
+        writeTransaction.delete(datastore, path);
+    }
+
     public void setDomDataBroker(final DOMDataBroker domDataBroker) {
         this.domDataBroker = domDataBroker;
     }
 
-    private void ensureParentsByMerge(final LogicalDatastoreType store,
+    private boolean ensureParentsByMerge(final LogicalDatastoreType store,
                                       final YangInstanceIdentifier normalizedPath, final DOMDataReadWriteTransaction rwTx, final SchemaContext schemaContext) {
+
+        boolean mergeResult = true;
         final List<PathArgument> normalizedPathWithoutChildArgs = new ArrayList<>();
         YangInstanceIdentifier rootNormalizedPath = null;
 
@@ -291,13 +434,35 @@ public class BrokerFacade {
 
         // No parent structure involved, no need to ensure parents
         if(normalizedPathWithoutChildArgs.isEmpty()) {
-            return;
+            return mergeResult;
         }
 
         Preconditions.checkArgument(rootNormalizedPath != null, "Empty path received");
 
         final NormalizedNode<?, ?> parentStructure =
                 ImmutableNodes.fromInstanceId(schemaContext, YangInstanceIdentifier.create(normalizedPathWithoutChildArgs));
-        rwTx.merge(store, rootNormalizedPath, parentStructure);
+        try {
+            rwTx.merge(store, rootNormalizedPath, parentStructure);
+        } catch (RuntimeException e) {
+            /*
+             * Catching the exception here, logging it and proceeding further
+             * for the following reasons.
+             *
+             * 1. For MD-SAL store if it fails we'll go with the next call
+             * anyway and let the failure happen there. 2. For NETCONF devices
+             * that can not handle these calls such as creation of empty lists
+             * etc, instead of failing we'll go with the actual call. Devices
+             * should be able to handle the actual calls made without the need
+             * to create parents. So instead of failing we will give a device a
+             * chance to configure the management entity in question. 3. If this
+             * merge call is handled properly by MD-SAL data store or a Netconf
+             * device this is a no-op.
+             */
+             // FIXME: Figure out and catch specific RunTimeExceptions thrown by NETCONF instead of generic one.
+             //        to make this cleaner and easier to maintain.
+            mergeResult = false;
+            LOG.debug("Exception while creating the parent in ensureParentsByMerge. Proceeding with the actual request", e);
+        }
+        return mergeResult;
     }
 }