From: Ivan Hrasko Date: Fri, 2 Sep 2016 14:01:32 +0000 (+0200) Subject: Bug 6272 - support RESTCONF PATCH for mounted NETCONF nodes X-Git-Tag: release/beryllium-sr4~1^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F33%2F46533%2F5;p=netconf.git Bug 6272 - support RESTCONF PATCH for mounted NETCONF nodes - adaptation for beryllium - distinguish between operation on server or on mount point - when operates on server then use global schema context and data broker - when operates on mount point then use schema context and broker from mount point - if broker on mount point is missing then PATCH operation fails with global error - contains unit tests Change-Id: I97135a467209fcb4a5782d1c2677f482b842ff8a Signed-off-by: Ivan Hrasko --- diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java index 1ce14aee89..6161464a5f 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java @@ -136,84 +136,135 @@ public class BrokerFacade { throw new RestconfDocumentedException(errMsg); } - public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext context, - final SchemaContext globalSchema) + public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext patchContext) throws TransactionCommitFailedException { - final DOMDataReadWriteTransaction patchTransaction = this.domDataBroker.newReadWriteTransaction(); + final DOMMountPoint mountPoint = patchContext.getInstanceIdentifierContext().getMountPoint(); + + // get new transaction and schema context on server or on mounted device + final SchemaContext schemaContext; + final DOMDataReadWriteTransaction patchTransaction; + if (mountPoint == null) { + schemaContext = patchContext.getInstanceIdentifierContext().getSchemaContext(); + patchTransaction = this.domDataBroker.newReadWriteTransaction(); + } else { + schemaContext = mountPoint.getSchemaContext(); + + final Optional optional = mountPoint.getService(DOMDataBroker.class); + + if (optional.isPresent()) { + patchTransaction = optional.get().newReadWriteTransaction(); + } else { + // if mount point does not have broker it is not possible to continue and global error is reported + LOG.error("Http PATCH {} has failed - device {} does not support broker service", + patchContext.getPatchId(), mountPoint.getIdentifier()); + return new PATCHStatusContext( + patchContext.getPatchId(), + null, + false, + ImmutableList.of(new RestconfError( + ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, + "DOM data broker service isn't available for mount point " + + mountPoint.getIdentifier())) + ); + } + } + final List editCollection = new ArrayList<>(); List editErrors; - final List globalErrors = null; - int errorCounter = 0; + boolean withoutError = true; - for (final PATCHEntity patchEntity : context.getData()) { + for (final PATCHEntity patchEntity : patchContext.getData()) { final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase()); switch (operation) { case CREATE: - if (errorCounter == 0) { + if (withoutError) { try { postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(), - patchEntity.getNode(), globalSchema); + patchEntity.getNode(), schemaContext); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); } catch (final RestconfDocumentedException e) { + LOG.error("Error call http PATCH operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + editErrors = new ArrayList<>(); editErrors.addAll(e.getErrors()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); - errorCounter++; + withoutError = false; } } break; case REPLACE: - if (errorCounter == 0) { + if (withoutError) { try { putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity - .getTargetNode(), patchEntity.getNode(), globalSchema); + .getTargetNode(), patchEntity.getNode(), schemaContext); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); } catch (final RestconfDocumentedException e) { + LOG.error("Error call http PATCH operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + editErrors = new ArrayList<>(); editErrors.addAll(e.getErrors()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); - errorCounter++; + withoutError = false; } } break; case DELETE: - if (errorCounter == 0) { + if (withoutError) { try { deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity .getTargetNode()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); } catch (final RestconfDocumentedException e) { + LOG.error("Error call http PATCH operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + editErrors = new ArrayList<>(); editErrors.addAll(e.getErrors()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); - errorCounter++; + withoutError = false; } } break; case REMOVE: - if (errorCounter == 0) { + if (withoutError) { try { deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity .getTargetNode()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); } catch (final RestconfDocumentedException e) { - LOG.error("Error removing {} by {} operation", patchEntity.getTargetNode().toString(), - patchEntity.getEditId(), e); + LOG.error("Error call http PATCH operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); + withoutError = false; } } break; case MERGE: - if (errorCounter == 0) { + if (withoutError) { try { mergeDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(), - patchEntity.getNode(), globalSchema); + patchEntity.getNode(), schemaContext); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); } catch (final RestconfDocumentedException e) { + LOG.error("Error call http PATCH operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + editErrors = new ArrayList<>(); editErrors.addAll(e.getErrors()); editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); - errorCounter++; + withoutError = false; } } break; @@ -221,15 +272,12 @@ public class BrokerFacade { } //TODO: make sure possible global errors are filled up correctly and decide transaction submission based on that - //globalErrors = new ArrayList<>(); - if (errorCounter == 0) { + if (withoutError) { patchTransaction.submit().checkedGet(); - return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true, - globalErrors); + return new PATCHStatusContext(patchContext.getPatchId(), ImmutableList.copyOf(editCollection), true, null); } else { patchTransaction.cancel(); - return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false, - globalErrors); + return new PATCHStatusContext(patchContext.getPatchId(), ImmutableList.copyOf(editCollection), false, null); } } @@ -356,23 +404,24 @@ public class BrokerFacade { return transaction.submit(); } + // FIXME: This is doing correct post for container and list children + // not sure if this will work for choice case 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 {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); + LOG.trace("POST {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); + + if (payload instanceof MapNode) { final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); - for(final MapEntryNode child : ((MapNode) payload).getValue()) { + for (final MapEntryNode child : ((MapNode) payload).getValue()) { final YangInstanceIdentifier childPath = path.node(child.getIdentifier()); checkItemDoesNotExists(rWTransaction, datastore, childPath); rWTransaction.put(datastore, childPath, child); } } else { - checkItemDoesNotExists(rWTransaction,datastore, path); + checkItemDoesNotExists(rWTransaction, datastore, path); ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); rWTransaction.put(datastore, path, payload); } @@ -424,12 +473,27 @@ public class BrokerFacade { return transaction.submit(); } + // FIXME: This is doing correct post for container and list children + // not sure if this will work for choice case private void putDataWithinTransaction( - final DOMDataReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore, + final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, final SchemaContext schemaContext) { - LOG.trace("Put {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); - ensureParentsByMerge(datastore, path, writeTransaction, schemaContext); - writeTransaction.put(datastore, path, payload); + LOG.trace("PUT {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); + + if (payload instanceof MapNode) { + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); + rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); + for (final MapEntryNode child : ((MapNode) payload).getValue()) { + final YangInstanceIdentifier childPath = path.node(child.getIdentifier()); + checkItemDoesNotExists(rWTransaction, datastore, childPath); + rWTransaction.put(datastore, childPath, child); + } + } else { + checkItemDoesNotExists(rWTransaction, datastore, path); + ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); + rWTransaction.put(datastore, path, payload); + } } private CheckedFuture deleteDataViaTransaction( diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java index 36efcfbe16..f4bad219d4 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java @@ -1056,8 +1056,8 @@ public class RestconfImpl implements RestconfService { throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); } try { - return broker.patchConfigurationDataWithinTransaction(context, controllerContext.getGlobalSchema()); - } catch (TransactionCommitFailedException e) { + return broker.patchConfigurationDataWithinTransaction(context); + } catch (Exception e) { LOG.debug("Patch transaction failed", e); throw new RestconfDocumentedException(e.getMessage()); } @@ -1069,8 +1069,8 @@ public class RestconfImpl implements RestconfService { throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); } try { - return broker.patchConfigurationDataWithinTransaction(context, controllerContext.getGlobalSchema()); - } catch (TransactionCommitFailedException e) { + return broker.patchConfigurationDataWithinTransaction(context); + } catch (Exception e) { LOG.debug("Patch transaction failed", e); throw new RestconfDocumentedException(e.getMessage()); } diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java index bc37738464..53b9f4ccb1 100644 --- a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java @@ -112,4 +112,10 @@ public abstract class AbstractBodyReaderTest { assertNotNull(patchContext.getInstanceIdentifierContext().getSchemaContext()); assertNotNull(patchContext.getInstanceIdentifierContext().getSchemaNode()); } + + protected static void checkPATCHContextMountPoint(final PATCHContext patchContext) { + checkPATCHContext(patchContext); + assertNotNull(patchContext.getInstanceIdentifierContext().getMountPoint()); + assertNotNull(patchContext.getInstanceIdentifierContext().getMountPoint().getSchemaContext()); + } } diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReaderMountPoint.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReaderMountPoint.java new file mode 100644 index 0000000000..74058ca93b --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReaderMountPoint.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.rest.impl.test.providers; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Optional; +import java.io.InputStream; +import javax.ws.rs.core.MediaType; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.netconf.sal.rest.impl.JsonToPATCHBodyReader; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +public class TestJsonPATCHBodyReaderMountPoint extends AbstractBodyReaderTest { + + private final JsonToPATCHBodyReader jsonPATCHBodyReader; + private static SchemaContext schemaContext; + private static final String MOUNT_POINT = "instance-identifier-module:cont/yang-ext:mount"; + + public TestJsonPATCHBodyReaderMountPoint() throws NoSuchFieldException, SecurityException { + super(); + jsonPATCHBodyReader = new JsonToPATCHBodyReader(); + } + + @Override + protected MediaType getMediaType() { + return new MediaType(APPLICATION_JSON, null); + } + + @BeforeClass + public static void initialization() { + schemaContext = schemaContextLoader("/instanceidentifier/yang", schemaContext); + + final DOMMountPoint mockMountPoint = mock(DOMMountPoint.class); + when(mockMountPoint.getSchemaContext()).thenReturn(schemaContext); + final DOMMountPointService mockMountPointService = mock(DOMMountPointService.class); + when(mockMountPointService.getMountPoint(any(YangInstanceIdentifier.class))) + .thenReturn(Optional.of(mockMountPoint)); + + controllerContext.setMountService(mockMountPointService); + controllerContext.setSchemas(schemaContext); + } + + @Test + public void modulePATCHDataTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdata.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test of successful PATCH consisting of create and delete PATCH operations. + */ + @Test + public void modulePATCHCreateAndDeleteTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataCreateAndDelete.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test trying to use PATCH create operation which requires value without value. Test should fail with + * {@link RestconfDocumentedException} with error code 400. + */ + @Test + public void modulePATCHValueMissingNegativeTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataValueMissing.json"); + + try { + jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream); + fail("Test should return error 400 due to missing value node when attempt to invoke create operation"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Test trying to use value with PATCH delete operation which does not support value. Test should fail with + * {@link RestconfDocumentedException} with error code 400. + */ + @Test + public void modulePATCHValueNotSupportedNegativeTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataValueNotSupported.json"); + + try { + jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream); + fail("Test should return error 400 due to present value node when attempt to invoke delete operation"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign. + */ + @Test + public void modulePATCHCompleteTargetInURITest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test of Yang PATCH merge operation on list. Test consists of two edit operations - replace and merge. + */ + @Test + public void modulePATCHMergeOperationOnListTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHMergeOperationOnList.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test of Yang PATCH merge operation on container. Test consists of two edit operations - create and merge. + */ + @Test + public void modulePATCHMergeOperationOnContainerTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHMergeOperationOnContainer.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java index 4768a0c9e8..a6d9352f4a 100644 --- a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java @@ -87,7 +87,6 @@ public class TestXmlPATCHBodyReader extends AbstractBodyReaderTest { } } - /** * Test of Yang PATCH with absolute target path. */ diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReaderMountPoint.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReaderMountPoint.java new file mode 100644 index 0000000000..3cf253a11d --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReaderMountPoint.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.rest.impl.test.providers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Optional; +import java.io.InputStream; +import javax.ws.rs.core.MediaType; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.netconf.sal.rest.impl.XmlToPATCHBodyReader; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +public class TestXmlPATCHBodyReaderMountPoint extends AbstractBodyReaderTest { + + private final XmlToPATCHBodyReader xmlPATCHBodyReader; + private static SchemaContext schemaContext; + private static final String MOUNT_POINT = "instance-identifier-module:cont/yang-ext:mount"; + + public TestXmlPATCHBodyReaderMountPoint() throws NoSuchFieldException, SecurityException { + super(); + xmlPATCHBodyReader = new XmlToPATCHBodyReader(); + } + + @Override + protected MediaType getMediaType() { + return new MediaType(MediaType.APPLICATION_XML, null); + } + + @BeforeClass + public static void initialization() throws NoSuchFieldException, SecurityException { + schemaContext = schemaContextLoader("/instanceidentifier/yang", schemaContext); + + final DOMMountPoint mockMountPoint = mock(DOMMountPoint.class); + when(mockMountPoint.getSchemaContext()).thenReturn(schemaContext); + final DOMMountPointService mockMountPointService = mock(DOMMountPointService.class); + when(mockMountPointService.getMountPoint(any(YangInstanceIdentifier.class))) + .thenReturn(Optional.of(mockMountPoint)); + + controllerContext.setMountService(mockMountPointService); + controllerContext.setSchemas(schemaContext); + } + + @Test + public void moduleDataTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdata.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test trying to use PATCH create operation which requires value without value. Error code 400 should be returned. + */ + @Test + public void moduleDataValueMissingNegativeTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataValueMissing.xml"); + try { + xmlPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream); + fail("Test should return error 400 due to missing value node when attempt to invoke create operation"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Test trying to use value with PATCH delete operation which does not support value. Error code 400 should be + * returned. + */ + @Test + public void moduleDataNotValueNotSupportedNegativeTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataValueNotSupported.xml"); + try { + xmlPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream); + fail("Test should return error 400 due to present value node when attempt to invoke delete operation"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Test of Yang PATCH with absolute target path. + */ + @Test + public void moduleDataAbsoluteTargetPathTest() throws Exception { + final String uri = MOUNT_POINT; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataAbsoluteTargetPath.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign. + */ + @Test + public void modulePATCHCompleteTargetInURITest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test of Yang PATCH merge operation on list. Test consists of two edit operations - replace and merge. + */ + @Test + public void moduleDataMergeOperationOnListTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataMergeOperationOnList.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } + + /** + * Test of Yang PATCH merge operation on container. Test consists of two edit operations - create and merge. + */ + @Test + public void moduleDataMergeOperationOnContainerTest() throws Exception { + final String uri = MOUNT_POINT + "/instance-identifier-patch-module:patch-cont"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataMergeOperationOnContainer.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContextMountPoint(returnValue); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java index fadcb6bc26..9918d486ba 100644 --- a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java @@ -9,8 +9,10 @@ package org.opendaylight.controller.sal.restconf.impl.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -49,6 +51,10 @@ import org.opendaylight.controller.md.sal.dom.api.DOMRpcService; import org.opendaylight.controller.sal.core.api.Broker.ConsumerSession; import org.opendaylight.netconf.sal.restconf.impl.BrokerFacade; import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.netconf.sal.restconf.impl.RestconfError; import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag; @@ -71,11 +77,23 @@ import org.opendaylight.yangtools.yang.model.api.SchemaPath; * @author Thomas Pantelis */ public class BrokerFacadeTest { - @Mock private DOMDataBroker domDataBroker; - @Mock private DOMNotificationService domNotification; - @Mock private ConsumerSession context; - @Mock private DOMRpcService mockRpcService; - @Mock private DOMMountPoint mockMountInstance; + + @Mock + private DOMDataBroker domDataBroker; + @Mock + private DOMNotificationService domNotification; + @Mock + private ConsumerSession context; + @Mock + private DOMRpcService mockRpcService; + @Mock + private DOMMountPoint mockMountInstance; + @Mock + private DOMDataReadOnlyTransaction rTransaction; + @Mock + private DOMDataWriteTransaction wTransaction; + @Mock + private DOMDataReadWriteTransaction rwTransaction; private final BrokerFacade brokerFacade = BrokerFacade.getInstance(); private final NormalizedNode dummyNode = createDummyNode("test:module", "2014-01-09", "interfaces"); @@ -85,10 +103,6 @@ public class BrokerFacadeTest { private final SchemaPath type = SchemaPath.create(true, qname); private final YangInstanceIdentifier instanceID = YangInstanceIdentifier.builder().node(qname).build(); - @Mock private DOMDataReadOnlyTransaction rTransaction; - @Mock private DOMDataWriteTransaction wTransaction; - @Mock private DOMDataReadWriteTransaction rwTransaction; - @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -330,4 +344,91 @@ public class BrokerFacadeTest { listener.close(); Notificator.removeNotificationListenerIfNoSubscriberExists(listener); } + + /** + * Test PATCH method on the server with no data + */ + @Test + @SuppressWarnings("unchecked") + public void testPatchConfigurationDataWithinTransactionServer() throws Exception { + final PATCHContext patchContext = mock(PATCHContext.class); + final InstanceIdentifierContext identifierContext = mock(InstanceIdentifierContext.class); + final CheckedFuture expFuture = Futures.immediateCheckedFuture(null); + + when(patchContext.getData()).thenReturn(Lists.newArrayList()); + when(patchContext.getInstanceIdentifierContext()).thenReturn(identifierContext); + + // no mount point + when(identifierContext.getMountPoint()).thenReturn(null); + + when(this.rwTransaction.submit()).thenReturn(expFuture); + + final PATCHStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext); + + // assert success + assertTrue("PATCH operation should be successful on server", status.isOk()); + } + + /** + * Test PATCH method on mounted device with no data + */ + @Test + @SuppressWarnings("unchecked") + public void testPatchConfigurationDataWithinTransactionMount() throws Exception { + final PATCHContext patchContext = mock(PATCHContext.class); + final InstanceIdentifierContext identifierContext = mock(InstanceIdentifierContext.class); + final DOMMountPoint mountPoint = mock(DOMMountPoint.class); + final DOMDataBroker mountDataBroker = mock(DOMDataBroker.class); + final DOMDataReadWriteTransaction transaction = mock(DOMDataReadWriteTransaction.class); + final CheckedFuture expFuture = Futures.immediateCheckedFuture(null); + + when(patchContext.getData()).thenReturn(Lists.newArrayList()); + when(patchContext.getInstanceIdentifierContext()).thenReturn(identifierContext); + + // return mount point with broker + when(identifierContext.getMountPoint()).thenReturn(mountPoint); + when(mountPoint.getService(DOMDataBroker.class)).thenReturn(Optional.of(mountDataBroker)); + when(mountDataBroker.newReadWriteTransaction()).thenReturn(transaction); + when(transaction.submit()).thenReturn(expFuture); + + final PATCHStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext); + + // assert success + assertTrue("PATCH operation should be successful on mounted device", status.isOk()); + } + + /** + * Negative test for PATCH operation when mounted device does not support {@link DOMDataBroker service. + * PATCH operation should fail with global error. + */ + @Test + @SuppressWarnings("unchecked") + public void testPatchConfigurationDataWithinTransactionMountFail() throws Exception { + final PATCHContext patchContext = mock(PATCHContext.class); + final InstanceIdentifierContext identifierContext = mock(InstanceIdentifierContext.class); + final DOMMountPoint mountPoint = mock(DOMMountPoint.class); + final DOMDataBroker mountDataBroker = mock(DOMDataBroker.class); + final DOMDataReadWriteTransaction transaction = mock(DOMDataReadWriteTransaction.class); + final CheckedFuture expFuture = Futures.immediateCheckedFuture(null); + + when(patchContext.getData()).thenReturn(Lists.newArrayList()); + when(patchContext.getInstanceIdentifierContext()).thenReturn(identifierContext); + when(identifierContext.getMountPoint()).thenReturn(mountPoint); + + // missing broker on mounted device + when(mountPoint.getService(DOMDataBroker.class)).thenReturn(Optional.absent()); + + when(mountDataBroker.newReadWriteTransaction()).thenReturn(transaction); + when(transaction.submit()).thenReturn(expFuture); + + final PATCHStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext); + + // assert not successful operation with error + assertNotNull(status.getGlobalErrors()); + assertEquals(1, status.getGlobalErrors().size()); + assertEquals(ErrorType.APPLICATION, status.getGlobalErrors().get(0).getErrorType()); + assertEquals(ErrorTag.OPERATION_FAILED, status.getGlobalErrors().get(0).getErrorTag()); + + assertFalse("PATCH operation should fail on mounted device without Broker", status.isOk()); + } }