From: Devin Avery Date: Thu, 8 May 2014 13:00:29 +0000 (-0400) Subject: Bug 953 - Enable mounted RPC calls via RestConf X-Git-Tag: autorelease-tag-v20140601202136_82eb3f9~64^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=aceaaf6a5ba5acada57ad0fa2c60be6567e55167 Bug 953 - Enable mounted RPC calls via RestConf Modify RestconfImpl to allow RPC calls via mount points. === MountPointImpl - wired pass through call for invokeRpc that was missing. RestconfImpl - abstract the execution of RPC out to an "RpcExecutor" class. Remaining classes - added tests to cover new code, including the toaster.yang file (modified from original) to test various RPC calls. - Patch Set 8 - fixed unit tests which were failing (one failure due to missing future enhancement, another due to missing test rpc in yang file). - Patch Set 10 - fixed run failure by addressing package import. - Patch Set 13 - Cleaned up exception throws / used Preconditions checking. - Patch Set 14 - Rebased - Patch Set 15 - Added back change that was accidently removed during merge / rebase. Change-Id: Ib214b4c82ffc487ee2f7b912e65d5c189b82154a Signed-off-by: Devin Avery --- diff --git a/opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/sal/dom/broker/MountPointImpl.java b/opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/sal/dom/broker/MountPointImpl.java index 263f0500fd..623bbdb195 100644 --- a/opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/sal/dom/broker/MountPointImpl.java +++ b/opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/sal/dom/broker/MountPointImpl.java @@ -135,7 +135,7 @@ public class MountPointImpl implements MountProvisionInstance, SchemaContextProv @Override public ListenableFuture> rpc(QName type, CompositeNode input) { - return null; + return rpcs.invokeRpc( type, input ); } @Override diff --git a/opendaylight/md-sal/sal-rest-connector/pom.xml b/opendaylight/md-sal/sal-rest-connector/pom.xml index fe00ab1836..c17a4b70cc 100644 --- a/opendaylight/md-sal/sal-rest-connector/pom.xml +++ b/opendaylight/md-sal/sal-rest-connector/pom.xml @@ -27,6 +27,10 @@ io.netty netty-codec-http + + org.apache.commons + commons-lang3 + org.opendaylight.controller sal-remote @@ -88,6 +92,7 @@ MD SAL Restconf Connector org.opendaylight.controller.sal.rest.*, + org.opendaylight.controller.sal.restconf.rpc.*, org.opendaylight.controller.sal.restconf.impl, *, com.sun.jersey.spi.container.servlet diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java index 0b7b693b0c..e9d489dd35 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java @@ -1,5 +1,6 @@ /** * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * Copyright (c) 2014 Brocade Communication Systems, Inc. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, @@ -12,6 +13,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -23,9 +25,13 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang3.StringUtils; import org.opendaylight.controller.md.sal.common.api.TransactionStatus; import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.rest.api.RestconfService; +import org.opendaylight.controller.sal.restconf.rpc.impl.BrokerRpcExecutor; +import org.opendaylight.controller.sal.restconf.rpc.impl.MountPointRpcExecutor; +import org.opendaylight.controller.sal.restconf.rpc.impl.RpcExecutor; import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter; import org.opendaylight.controller.sal.streams.listeners.Notificator; import org.opendaylight.controller.sal.streams.websockets.WebSocketServer; @@ -69,6 +75,8 @@ import com.google.common.collect.Lists; public class RestconfImpl implements RestconfService { private final static RestconfImpl INSTANCE = new RestconfImpl(); + private static final int CHAR_NOT_FOUND = -1; + private final static String MOUNT_POINT_MODULE_NAME = "ietf-netconf"; private final static SimpleDateFormat REVISION_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); @@ -452,109 +460,145 @@ public class RestconfImpl implements RestconfService { @Override public StructuredData invokeRpc(final String identifier, final CompositeNode payload) { - final RpcDefinition rpc = this.resolveIdentifierInInvokeRpc(identifier); - if (Objects.equal(rpc.getQName().getNamespace().toString(), SAL_REMOTE_NAMESPACE) && - Objects.equal(rpc.getQName().getLocalName(), SAL_REMOTE_RPC_SUBSRCIBE)) { + final RpcExecutor rpc = this.resolveIdentifierInInvokeRpc(identifier); + QName rpcName = rpc.getRpcDefinition().getQName(); + URI rpcNamespace = rpcName.getNamespace(); + if (Objects.equal(rpcNamespace.toString(), SAL_REMOTE_NAMESPACE) && + Objects.equal(rpcName.getLocalName(), SAL_REMOTE_RPC_SUBSRCIBE)) { - final CompositeNode value = this.normalizeNode(payload, rpc.getInput(), null); - final SimpleNode pathNode = value == null ? null : - value.getFirstSimpleByName( QName.create(rpc.getQName(), "path") ); - final Object pathValue = pathNode == null ? null : pathNode.getValue(); + return invokeSalRemoteRpcSubscribeRPC(payload, rpc.getRpcDefinition()); + } - if (!(pathValue instanceof InstanceIdentifier)) { - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, - "Instance identifier was not normalized correctly."); - } + return callRpc(rpc, payload); + } - final InstanceIdentifier pathIdentifier = ((InstanceIdentifier) pathValue); - String streamName = null; - if (!Iterables.isEmpty(pathIdentifier.getPath())) { - String fullRestconfIdentifier = this.controllerContext.toFullRestconfIdentifier(pathIdentifier); - streamName = Notificator.createStreamNameFromUri(fullRestconfIdentifier); - } + private StructuredData invokeSalRemoteRpcSubscribeRPC(final CompositeNode payload, + final RpcDefinition rpc) { + final CompositeNode value = this.normalizeNode(payload, rpc.getInput(), null); + final SimpleNode pathNode = value == null ? null : + value.getFirstSimpleByName( QName.create(rpc.getQName(), "path") ); + final Object pathValue = pathNode == null ? null : pathNode.getValue(); - if (Strings.isNullOrEmpty(streamName)) { - throw new ResponseException(Status.BAD_REQUEST, - "Path is empty or contains data node which is not Container or List build-in type."); - } + if (!(pathValue instanceof InstanceIdentifier)) { + throw new ResponseException(Status.INTERNAL_SERVER_ERROR, + "Instance identifier was not normalized correctly."); + } - final SimpleNode streamNameNode = NodeFactory.createImmutableSimpleNode( - QName.create(rpc.getOutput().getQName(), "stream-name"), null, streamName); - final List> output = new ArrayList>(); - output.add(streamNameNode); + final InstanceIdentifier pathIdentifier = ((InstanceIdentifier) pathValue); + String streamName = null; + if (!Iterables.isEmpty(pathIdentifier.getPath())) { + String fullRestconfIdentifier = this.controllerContext.toFullRestconfIdentifier(pathIdentifier); + streamName = Notificator.createStreamNameFromUri(fullRestconfIdentifier); + } - final MutableCompositeNode responseData = NodeFactory.createMutableCompositeNode( - rpc.getOutput().getQName(), null, output, null, null); + if (Strings.isNullOrEmpty(streamName)) { + throw new ResponseException(Status.BAD_REQUEST, + "Path is empty or contains data node which is not Container or List build-in type."); + } - if (!Notificator.existListenerFor(pathIdentifier)) { - Notificator.createListener(pathIdentifier, streamName); - } + final SimpleNode streamNameNode = NodeFactory.createImmutableSimpleNode( + QName.create(rpc.getOutput().getQName(), "stream-name"), null, streamName); + final List> output = new ArrayList>(); + output.add(streamNameNode); - return new StructuredData(responseData, rpc.getOutput(), null); + final MutableCompositeNode responseData = NodeFactory.createMutableCompositeNode( + rpc.getOutput().getQName(), null, output, null, null); + + if (!Notificator.existListenerFor(pathIdentifier)) { + Notificator.createListener(pathIdentifier, streamName); } - RpcDefinition rpcDefinition = this.controllerContext.getRpcDefinition(identifier); - return this.callRpc(rpcDefinition, payload); + return new StructuredData(responseData, rpc.getOutput(), null); } @Override public StructuredData invokeRpc(final String identifier, final String noPayload) { - if (!Strings.isNullOrEmpty(noPayload)) { - throw new ResponseException(Status.UNSUPPORTED_MEDIA_TYPE, - "Content-Type contains unsupported Media Type."); + if (StringUtils.isNotBlank(noPayload)) { + throw new ResponseException( + Status.UNSUPPORTED_MEDIA_TYPE, "Content-Type contains unsupported Media Type."); } - - final RpcDefinition rpc = this.resolveIdentifierInInvokeRpc(identifier); - return this.callRpc(rpc, null); + final RpcExecutor rpc = resolveIdentifierInInvokeRpc(identifier); + return callRpc(rpc, null); } - private RpcDefinition resolveIdentifierInInvokeRpc(final String identifier) { - if (identifier.indexOf("/") < 0) { - final String identifierDecoded = this.controllerContext.urlPathArgDecode(identifier); - final RpcDefinition rpc = this.controllerContext.getRpcDefinition(identifierDecoded); - if (rpc != null) { - return rpc; - } + private RpcExecutor resolveIdentifierInInvokeRpc(final String identifier) { + String identifierEncoded = null; + MountInstance mountPoint = null; + if (identifier.contains(ControllerContext.MOUNT)) { + // mounted RPC call - look up mount instance. + InstanceIdWithSchemaNode mountPointId = controllerContext + .toMountPointIdentifier(identifier); + mountPoint = mountPointId.getMountPoint(); + int startOfRemoteRpcName = identifier.lastIndexOf(ControllerContext.MOUNT) + + ControllerContext.MOUNT.length() + 1; + String remoteRpcName = identifier.substring(startOfRemoteRpcName); + identifierEncoded = remoteRpcName; + + } else if (identifier.indexOf("/") != CHAR_NOT_FOUND) { + final String slashErrorMsg = String + .format("Identifier %n%s%ncan\'t contain slash " + + "character (/).%nIf slash is part of identifier name then use %%2F placeholder.", + identifier); + throw new ResponseException(Status.NOT_FOUND, slashErrorMsg); + } else { + identifierEncoded = identifier; + } + + final String identifierDecoded = controllerContext.urlPathArgDecode(identifierEncoded); + RpcDefinition rpc = controllerContext.getRpcDefinition(identifierDecoded); + + if (rpc == null) { throw new ResponseException(Status.NOT_FOUND, "RPC does not exist."); } - final String slashErrorMsg = String.format( - "Identifier %n%s%ncan\'t contain slash character (/).%nIf slash is part of identifier name then use %%2F placeholder.", - identifier); + if (mountPoint == null) { + return new BrokerRpcExecutor(rpc, broker); + } else { + return new MountPointRpcExecutor(rpc, mountPoint); + } - throw new ResponseException(Status.NOT_FOUND, slashErrorMsg); } - private StructuredData callRpc(final RpcDefinition rpc, final CompositeNode payload) { - if (rpc == null) { + private StructuredData callRpc(final RpcExecutor rpcExecutor, final CompositeNode payload) { + if (rpcExecutor == null) { throw new ResponseException(Status.NOT_FOUND, "RPC does not exist."); } CompositeNode rpcRequest = null; + RpcDefinition rpc = rpcExecutor.getRpcDefinition(); + QName rpcName = rpc.getQName(); + if (payload == null) { - rpcRequest = NodeFactory.createMutableCompositeNode(rpc.getQName(), null, null, null, null); - } - else { + rpcRequest = NodeFactory.createMutableCompositeNode(rpcName, null, null, null, null); + } else { final CompositeNode value = this.normalizeNode(payload, rpc.getInput(), null); - final List> input = new ArrayList>(); - input.add(value); - - rpcRequest = NodeFactory.createMutableCompositeNode(rpc.getQName(), null, input, null, null); + List> input = Collections.> singletonList(value); + rpcRequest = NodeFactory.createMutableCompositeNode(rpcName, null, input, null, null); } - final RpcResult rpcResult = broker.invokeRpc(rpc.getQName(), rpcRequest); + RpcResult rpcResult = rpcExecutor.invokeRpc(rpcRequest); - if (!rpcResult.isSuccessful()) { - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, "Operation failed"); - } + checkRpcSuccessAndThrowException(rpcResult); - CompositeNode result = rpcResult.getResult(); - if (result == null) { + if (rpcResult.getResult() == null) { return null; } - return new StructuredData(result, rpc.getOutput(), null); + if( rpc.getOutput() == null ) + { + return null; //no output, nothing to send back. + } + + return new StructuredData(rpcResult.getResult(), rpc.getOutput(), null); + } + + private void checkRpcSuccessAndThrowException(RpcResult rpcResult) { + if (rpcResult.isSuccessful() == false) { + //TODO: Get smart about what error code we are return (Future Bug coming) + throw new ResponseException(Status.INTERNAL_SERVER_ERROR, + "The operation was not successful and there were no RPC errors returned"); + } } @Override @@ -670,7 +714,7 @@ public class RestconfImpl implements RestconfService { status = future == null ? null : future.get(); } } - catch( ResponseException e) { throw e; } + catch( ResponseException e ){ throw e; } catch( Exception e ) { throw new ResponseException( e, "Error creating data" ); } @@ -720,7 +764,7 @@ public class RestconfImpl implements RestconfService { status = future == null ? null : future.get(); } } - catch( ResponseException e) { throw e; } + catch( ResponseException e ){ throw e; } catch( Exception e ) { throw new ResponseException( e, "Error creating data" ); } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java new file mode 100644 index 0000000000..446d07d426 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2014 Brocade Communications 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.restconf.rpc.impl; + +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; + +public abstract class AbstractRpcExecutor implements RpcExecutor { + private final RpcDefinition rpcDef; + + public AbstractRpcExecutor( RpcDefinition rpcDef ){ + this.rpcDef = rpcDef; + } + + @Override + public RpcDefinition getRpcDefinition() { + return rpcDef; + } +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java new file mode 100644 index 0000000000..0748832247 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2014 Brocade Communications 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.restconf.rpc.impl; + +import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.CompositeNode; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; + +public class BrokerRpcExecutor extends AbstractRpcExecutor { + private final BrokerFacade broker; + + public BrokerRpcExecutor( RpcDefinition rpcDef, BrokerFacade broker ) + { + super( rpcDef ); + this.broker = broker; + } + + @Override + public RpcResult invokeRpc(CompositeNode rpcRequest) { + return broker.invokeRpc( getRpcDefinition().getQName(), rpcRequest ); + } +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java new file mode 100644 index 0000000000..b56db21951 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2014 Brocade Communications 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.restconf.rpc.impl; + +import java.util.concurrent.ExecutionException; + +import javax.ws.rs.core.Response.Status; + +import org.opendaylight.controller.sal.core.api.mount.MountInstance; +import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.CompositeNode; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; + +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Provides an implementation which invokes rpc methods via a mounted yang data model. + * @author Devin Avery + * + */ +public class MountPointRpcExecutor extends AbstractRpcExecutor { + private final MountInstance mountPoint; + + public MountPointRpcExecutor(RpcDefinition rpcDef, MountInstance mountPoint) { + super( rpcDef ); + this.mountPoint = mountPoint; + Preconditions.checkNotNull( mountPoint, "MountInstance can not be null." ); + } + + @Override + public RpcResult invokeRpc( CompositeNode rpcRequest ) throws ResponseException { + ListenableFuture> rpcFuture = + mountPoint.rpc( getRpcDefinition().getQName(), rpcRequest); + try { + return rpcFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ResponseException(Status.INTERNAL_SERVER_ERROR, + e.getCause().getMessage() ); + } + } +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/RpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/RpcExecutor.java new file mode 100644 index 0000000000..f628a63393 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/RpcExecutor.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2014 Brocade Communications 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.restconf.rpc.impl; + +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.CompositeNode; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; + +public interface RpcExecutor { + RpcResult invokeRpc( CompositeNode rpcRequest ); + + RpcDefinition getRpcDefinition(); +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java index 5689a82875..b42178a9c6 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * Copyright (c) 2014 Brocade Communications Systems, Inc. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, @@ -9,36 +10,56 @@ package org.opendaylight.controller.sal.restconf.impl.test; import static org.junit.Assert.assertEquals; 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; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import java.io.FileNotFoundException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; +import javax.ws.rs.core.Response.Status; + +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; +import org.opendaylight.controller.sal.restconf.impl.ResponseException; import org.opendaylight.controller.sal.restconf.impl.RestconfImpl; import org.opendaylight.controller.sal.restconf.impl.StructuredData; import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.RpcError; import org.opendaylight.yangtools.yang.common.RpcResult; import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.data.api.ModifyAction; import org.opendaylight.yangtools.yang.data.api.MutableCompositeNode; import org.opendaylight.yangtools.yang.data.api.MutableSimpleNode; -import org.opendaylight.yangtools.yang.data.api.Node; import org.opendaylight.yangtools.yang.data.impl.NodeFactory; import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +import com.google.common.util.concurrent.ListenableFuture; public class InvokeRpcMethodTest { - private static Set modules; + private RestconfImpl restconfImpl = null; + private static ControllerContext controllerContext = null; private class AnswerImpl implements Answer> { @Override @@ -49,11 +70,24 @@ public class InvokeRpcMethodTest { } @BeforeClass - public static void initialization() { - modules = TestUtils.loadModulesFrom("/invoke-rpc"); - assertEquals(1, modules.size()); - Module module = TestUtils.resolveModule("invoke-rpc-module", modules); + public static void init() throws FileNotFoundException { + Set allModules = new HashSet( TestUtils + .loadModulesFrom("/full-versions/yangs") ); + allModules.addAll( TestUtils.loadModulesFrom("/invoke-rpc") ); + assertNotNull(allModules); + Module module = TestUtils.resolveModule("invoke-rpc-module", allModules); assertNotNull(module); + SchemaContext schemaContext = TestUtils.loadSchemaContext(allModules); + controllerContext = spy( ControllerContext.getInstance() ); + controllerContext.setSchemas(schemaContext); + + } + + @Before + public void initMethod() + { + restconfImpl = RestconfImpl.getInstance(); + restconfImpl.setControllerContext( controllerContext ); } /** @@ -63,9 +97,8 @@ public class InvokeRpcMethodTest { * from string - first argument). */ @Test - public void invokeRpcMethodTest() { - ControllerContext contContext = ControllerContext.getInstance(); - contContext.onGlobalContextUpdated(TestUtils.loadSchemaContext(modules)); + public void invokeRpcMtethodTest() { + ControllerContext contContext = controllerContext; try { contContext.findModuleNameByNamespace(new URI("invoke:rpc:module")); } catch (URISyntaxException e) { @@ -81,21 +114,7 @@ public class InvokeRpcMethodTest { when(mockedBrokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))).thenAnswer(new AnswerImpl()); StructuredData structData = restconf.invokeRpc("invoke-rpc-module:rpc-test", preparePayload()); - - CompositeNode rpcCompNode = structData.getData(); - CompositeNode cont = null; - assertEquals("invoke:rpc:module", rpcCompNode.getNodeType().getNamespace().toString()); - assertEquals("rpc-test", rpcCompNode.getNodeType().getLocalName()); - - for (Node node : rpcCompNode.getChildren()) { - if (node.getNodeType().getLocalName().equals("cont") - && node.getNodeType().getNamespace().toString().equals("nmspc")) { - if (node instanceof CompositeNode) { - cont = (CompositeNode) node; - } - } - } - assertNotNull(cont); + assertTrue(structData == null); } @@ -110,4 +129,198 @@ public class InvokeRpcMethodTest { return cont; } + @Test + public void testInvokeRpcWithNoPayloadRpc_FailNoErrors() { + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(false); + + ArgumentCaptor payload = ArgumentCaptor + .forClass(CompositeNode.class); + BrokerFacade brokerFacade = mock(BrokerFacade.class); + when( + brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + payload.capture())).thenReturn(rpcResult); + + restconfImpl.setBroker(brokerFacade); + + try { + restconfImpl.invokeRpc("toaster:cancel-toast", ""); + fail("Expected an exception to be thrown."); + } catch (ResponseException e) { + assertEquals(e.getMessage(), + Status.INTERNAL_SERVER_ERROR.getStatusCode(), e + .getResponse().getStatus()); + } + } + + @Test + public void testInvokeRpcWithNoPayloadRpc_FailWithRpcError() { + List rpcErrors = new LinkedList(); + + RpcError unknownError = mock(RpcError.class); + when( unknownError.getTag() ).thenReturn( "bogusTag" ); + rpcErrors.add( unknownError ); + + RpcError knownError = mock( RpcError.class ); + when( knownError.getTag() ).thenReturn( "in-use" ); + rpcErrors.add( knownError ); + + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(false); + when(rpcResult.getErrors()).thenReturn( rpcErrors ); + + ArgumentCaptor payload = ArgumentCaptor + .forClass(CompositeNode.class); + BrokerFacade brokerFacade = mock(BrokerFacade.class); + when( + brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + payload.capture())).thenReturn(rpcResult); + + restconfImpl.setBroker(brokerFacade); + + try { + restconfImpl.invokeRpc("toaster:cancel-toast", ""); + fail("Expected an exception to be thrown."); + } catch (ResponseException e) { + //TODO: Change to a 409 in the future - waiting on additional BUG to enhance this. + assertEquals(e.getMessage(), 500, e.getResponse().getStatus()); + } + } + + @Test + public void testInvokeRpcWithNoPayload_Success() { + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(true); + + BrokerFacade brokerFacade = mock(BrokerFacade.class); + when( + brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + any( CompositeNode.class ))).thenReturn(rpcResult); + + restconfImpl.setBroker(brokerFacade); + + StructuredData output = restconfImpl.invokeRpc("toaster:cancel-toast", + ""); + assertEquals(null, output); + //additional validation in the fact that the restconfImpl does not throw an exception. + } + + @Test + public void testInvokeRpcMethodExpectingNoPayloadButProvidePayload() { + try { + restconfImpl.invokeRpc("toaster:cancel-toast", " a payload "); + fail("Expected an exception"); + } catch (ResponseException e) { + assertEquals(e.getMessage(), + Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(), e + .getResponse().getStatus()); + } + } + + @Test + public void testInvokeRpcMethodWithBadMethodName() { + try { + restconfImpl.invokeRpc("toaster:bad-method", ""); + fail("Expected an exception"); + } catch (ResponseException e) { + assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e + .getResponse().getStatus()); + } + } + + @Test + public void testInvokeRpcMethodWithInput() { + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(true); + + CompositeNode payload = mock(CompositeNode.class); + + BrokerFacade brokerFacade = mock(BrokerFacade.class); + when( + brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)make-toast")), + any(CompositeNode.class))).thenReturn(rpcResult); + + restconfImpl.setBroker(brokerFacade); + + StructuredData output = restconfImpl.invokeRpc("toaster:make-toast", + payload); + assertEquals(null, output); + //additional validation in the fact that the restconfImpl does not throw an exception. + } + + @Test + public void testThrowExceptionWhenSlashInModuleName() { + try { + restconfImpl.invokeRpc("toaster/slash", ""); + fail("Expected an exception."); + } catch (ResponseException e) { + assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e + .getResponse().getStatus()); + } + } + + @Test + public void testInvokeRpcWithNoPayloadWithOutput_Success() { + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(true); + + CompositeNode compositeNode = mock( CompositeNode.class ); + when( rpcResult.getResult() ).thenReturn( compositeNode ); + + BrokerFacade brokerFacade = mock(BrokerFacade.class); + when( brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)testOutput")), + any( CompositeNode.class ))).thenReturn(rpcResult); + + restconfImpl.setBroker(brokerFacade); + + StructuredData output = restconfImpl.invokeRpc("toaster:testOutput", + ""); + assertNotNull( output ); + assertSame( compositeNode, output.getData() ); + assertNotNull( output.getSchema() ); + } + + @Test + public void testMountedRpcCallNoPayload_Success() throws Exception + { + RpcResult rpcResult = mock(RpcResult.class); + when(rpcResult.isSuccessful()).thenReturn(true); + + ListenableFuture> mockListener = mock( ListenableFuture.class ); + when( mockListener.get() ).thenReturn( rpcResult ); + + QName cancelToastQName = QName.create( "cancelToast" ); + + RpcDefinition mockRpc = mock( RpcDefinition.class ); + when( mockRpc.getQName() ).thenReturn( cancelToastQName ); + + MountInstance mockMountPoint = mock( MountInstance.class ); + when( mockMountPoint.rpc( eq( cancelToastQName ), any( CompositeNode.class ) ) ) + .thenReturn( mockListener ); + + InstanceIdWithSchemaNode mockedInstanceId = mock( InstanceIdWithSchemaNode.class ); + when( mockedInstanceId.getMountPoint() ).thenReturn( mockMountPoint ); + + ControllerContext mockedContext = mock( ControllerContext.class ); + String cancelToastStr = "toaster:cancel-toast"; + when( mockedContext.urlPathArgDecode( cancelToastStr ) ).thenReturn( cancelToastStr ); + when( mockedContext.getRpcDefinition( cancelToastStr ) ).thenReturn( mockRpc ); + when( mockedContext.toMountPointIdentifier( "opendaylight-inventory:nodes/node/" + + "REMOTE_HOST/yang-ext:mount/toaster:cancel-toast" ) ).thenReturn( mockedInstanceId ); + + restconfImpl.setControllerContext( mockedContext ); + StructuredData output = restconfImpl.invokeRpc( + "opendaylight-inventory:nodes/node/REMOTE_HOST/yang-ext:mount/toaster:cancel-toast", + ""); + assertEquals(null, output); + + //additional validation in the fact that the restconfImpl does not throw an exception. + } + + } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfImplTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfImplTest.java index b681653d6b..e2559f4b70 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfImplTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfImplTest.java @@ -7,8 +7,17 @@ */ package org.opendaylight.controller.sal.restconf.impl.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + import java.io.FileNotFoundException; import java.util.Set; + +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; @@ -19,32 +28,45 @@ import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +/** + * @See {@link InvokeRpcMethodTest} + * + */ public class RestconfImplTest { - private static final RestconfImpl restconfImpl = RestconfImpl.getInstance(); + private RestconfImpl restconfImpl = null; + private static ControllerContext controllerContext = null; @BeforeClass public static void init() throws FileNotFoundException { - Set allModules = TestUtils.loadModulesFrom("/full-versions/yangs"); + Set allModules = TestUtils + .loadModulesFrom("/full-versions/yangs"); assertNotNull(allModules); SchemaContext schemaContext = TestUtils.loadSchemaContext(allModules); - ControllerContext controllerContext = ControllerContext.getInstance(); + controllerContext = spy( ControllerContext.getInstance() ); controllerContext.setSchemas(schemaContext); - restconfImpl.setControllerContext(controllerContext); + + } + + @Before + public void initMethod() + { + restconfImpl = RestconfImpl.getInstance(); + restconfImpl.setControllerContext( controllerContext ); } @Test public void testExample() throws FileNotFoundException { - CompositeNode loadedCompositeNode = TestUtils.readInputToCnSn("/parts/ietf-interfaces_interfaces.xml", XmlToCompositeNodeProvider.INSTANCE); + CompositeNode loadedCompositeNode = TestUtils.readInputToCnSn( + "/parts/ietf-interfaces_interfaces.xml", + XmlToCompositeNodeProvider.INSTANCE); BrokerFacade brokerFacade = mock(BrokerFacade.class); - when(brokerFacade.readOperationalData(any(InstanceIdentifier.class))).thenReturn(loadedCompositeNode); - assertEquals(loadedCompositeNode, brokerFacade.readOperationalData(null)); + when(brokerFacade.readOperationalData(any(InstanceIdentifier.class))) + .thenReturn(loadedCompositeNode); + assertEquals(loadedCompositeNode, + brokerFacade.readOperationalData(null)); } + } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/yangs/toaster.yang b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/yangs/toaster.yang new file mode 100644 index 0000000000..571ed0c86e --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/yangs/toaster.yang @@ -0,0 +1,197 @@ + module toaster { + + yang-version 1; + + namespace + "http://netconfcentral.org/ns/toaster"; + + prefix toast; + + organization "Netconf Central"; + + contact + "Andy Bierman "; + + description + "YANG version of the TOASTER-MIB."; + + revision "2009-11-20" { + description + "Toaster module in progress."; + } + + + identity toast-type { + description + "Base for all bread types supported by the toaster. + New bread types not listed here nay be added in the + future."; + } + + identity white-bread { + base toast:toast-type; + description "White bread."; + } + + identity wheat-bread { + base toast-type; + description "Wheat bread."; + } + + identity wonder-bread { + base toast-type; + description "Wonder bread."; + } + + identity frozen-waffle { + base toast-type; + description "Frozen waffle."; + } + + identity frozen-bagel { + base toast-type; + description "Frozen bagel."; + } + + identity hash-brown { + base toast-type; + description "Hash browned potatos."; + } + + typedef DisplayString { + type string { + length "0 .. 255"; + } + description + "YANG version of the SMIv2 DisplayString TEXTUAL-CONVENTION."; + reference + "RFC 2579, section 2."; + + } + + container toaster { + presence + "Indicates the toaster service is available"; + description + "Top-level container for all toaster database objects."; + leaf toasterManufacturer { + type DisplayString; + config false; + mandatory true; + description + "The name of the toaster's manufacturer. For instance, + Microsoft Toaster."; + } + + leaf toasterModelNumber { + type DisplayString; + config false; + mandatory true; + description + "The name of the toaster's model. For instance, + Radiant Automatic."; + } + + leaf toasterStatus { + type enumeration { + enum "up" { + value 1; + description + "The toaster knob position is up. + No toast is being made now."; + } + enum "down" { + value 2; + description + "The toaster knob position is down. + Toast is being made now."; + } + } + config false; + mandatory true; + description + "This variable indicates the current state of + the toaster."; + } + } // container toaster + + rpc make-toast { + description + "Make some toast. + The toastDone notification will be sent when + the toast is finished. + An 'in-use' error will be returned if toast + is already being made. + A 'resource-denied' error will be returned + if the toaster service is disabled."; + input { + leaf toasterDoneness { + type uint32 { + range "1 .. 10"; + } + default '5'; + description + "This variable controls how well-done is the + ensuing toast. It should be on a scale of 1 to 10. + Toast made at 10 generally is considered unfit + for human consumption; toast made at 1 is warmed + lightly."; + } + + leaf toasterToastType { + type identityref { + base toast:toast-type; + } + default 'wheat-bread'; + description + "This variable informs the toaster of the type of + material that is being toasted. The toaster + uses this information, combined with + toasterDoneness, to compute for how + long the material must be toasted to achieve + the required doneness."; + } + } + } // rpc make-toast + + rpc testOutput { + output { + leaf textOut { + type string; + } + } + } + + rpc cancel-toast { + description + "Stop making toast, if any is being made. + A 'resource-denied' error will be returned + if the toaster service is disabled."; + } // rpc cancel-toast + + notification toastDone { + description + "Indicates that the toast in progress has completed."; + leaf toastStatus { + type enumeration { + enum "done" { + value 0; + description "The toast is done."; + } + enum "cancelled" { + value 1; + description + "The toast was cancelled."; + } + enum "error" { + value 2; + description + "The toaster service was disabled or + the toaster is broken."; + } + } + description + "Indicates the final toast status"; + } + } // notification toastDone + } // module toaster