From: Tom Pantelis Date: Tue, 7 Jul 2015 15:07:31 +0000 (-0400) Subject: Bug 3999: Create internal service to access restconf X-Git-Tag: release/lithium-sr1~18 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=5504f854f82ef759f32df4b726ad31fb4f78227f;p=controller.git Bug 3999: Create internal service to access restconf There are use cases for invoking restconf from internal code. However issuing an HTTP request is problematic as one would need to know the credentials and scheme (http or https). So I added a JSONRestconfService interface and implementation with CRUD methods that call the JSON readers/writers and RestconfImpl internally. The implementation is advertised as an OSGi service via the config system for consumption by clients. I only added a service for JSON - an XML service could be added as well later. Change-Id: I5d1304c568c9be9c204afea68aadc0306bac50b3 Signed-off-by: Tom Pantelis --- diff --git a/features/restconf/pom.xml b/features/restconf/pom.xml index 197e2f70a8..8d8f38432f 100644 --- a/features/restconf/pom.xml +++ b/features/restconf/pom.xml @@ -176,6 +176,13 @@ xml config + + org.opendaylight.controller + sal-rest-connector-config + ${mdsal.version} + xml + configrestconfservice + com.fasterxml.jackson.core diff --git a/features/restconf/src/main/resources/features.xml b/features/restconf/src/main/resources/features.xml index dc2cce2ac2..f1a10914a8 100644 --- a/features/restconf/src/main/resources/features.xml +++ b/features/restconf/src/main/resources/features.xml @@ -108,6 +108,7 @@ mvn:io.netty/netty-handler/${netty.version} mvn:io.netty/netty-transport/${netty.version} mvn:org.opendaylight.controller/sal-rest-connector-config/${mdsal.version}/xml/config + mvn:org.opendaylight.controller/sal-rest-connector-config/${mdsal.version}/xml/configrestconfservice odl-restconf diff --git a/opendaylight/commons/opendaylight/pom.xml b/opendaylight/commons/opendaylight/pom.xml index 8d7e63edd4..f04bb20912 100644 --- a/opendaylight/commons/opendaylight/pom.xml +++ b/opendaylight/commons/opendaylight/pom.xml @@ -63,6 +63,7 @@ 03-toaster-sample.xml 08-mdsal-netconf.xml 10-rest-connector.xml + 10-restconf-service.xml 99-netconf-connector.xml 0.5.1-SNAPSHOT 0.5.1-SNAPSHOT diff --git a/opendaylight/md-sal/sal-rest-connector-config/pom.xml b/opendaylight/md-sal/sal-rest-connector-config/pom.xml index 3932d3c220..ad8334f027 100644 --- a/opendaylight/md-sal/sal-rest-connector-config/pom.xml +++ b/opendaylight/md-sal/sal-rest-connector-config/pom.xml @@ -36,6 +36,11 @@ xml config + + ${project.build.directory}/classes/initial/10-restconf-service.xml + xml + configrestconfservice + diff --git a/opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml b/opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml new file mode 100644 index 0000000000..6a0362e2e5 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml @@ -0,0 +1,35 @@ + + + + + + + + rest:json-restconf-service-impl + json-restconf-service-impl + + + + + + rest:json-restconf-service + + json-restconf-service + + /modules/module[type='json-restconf-service-impl'][name='json-restconf-service-impl'] + + + + + + + + urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service?module=sal-restconf-service&revision=2015-07-08 + + diff --git a/opendaylight/md-sal/sal-rest-connector/pom.xml b/opendaylight/md-sal/sal-rest-connector/pom.xml index 9349e2e7ba..c991b419f7 100644 --- a/opendaylight/md-sal/sal-rest-connector/pom.xml +++ b/opendaylight/md-sal/sal-rest-connector/pom.xml @@ -115,6 +115,11 @@ + + commons-io + commons-io + test + junit junit diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java new file mode 100644 index 0000000000..42dd8ddee1 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java @@ -0,0 +1,23 @@ +package org.opendaylight.controller.config.yang.sal.restconf.service; + +import org.opendaylight.controller.sal.restconf.impl.JSONRestconfServiceImpl; + +public class JSONRestconfServiceModule extends org.opendaylight.controller.config.yang.sal.restconf.service.AbstractJSONRestconfServiceModule { + public JSONRestconfServiceModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) { + super(identifier, dependencyResolver); + } + + public JSONRestconfServiceModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, org.opendaylight.controller.config.yang.sal.restconf.service.JSONRestconfServiceModule oldModule, java.lang.AutoCloseable oldInstance) { + super(identifier, dependencyResolver, oldModule, oldInstance); + } + + @Override + public void customValidation() { + // add custom validation form module attributes here. + } + + @Override + public java.lang.AutoCloseable createInstance() { + return new JSONRestconfServiceImpl(); + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java new file mode 100644 index 0000000000..4165882e12 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java @@ -0,0 +1,13 @@ +/* +* Generated file +* +* Generated from: yang module name: sal-restconf-service yang module local name: json-restconf-service-impl +* Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator +* Generated at: Tue Jul 07 18:18:52 EDT 2015 +* +* Do not modify this file unless it is present under src/main directory +*/ +package org.opendaylight.controller.config.yang.sal.restconf.service; +public class JSONRestconfServiceModuleFactory extends org.opendaylight.controller.config.yang.sal.restconf.service.AbstractJSONRestconfServiceModuleFactory { + +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java new file mode 100644 index 0000000000..8216258c23 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015 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.restconf.api; + +import com.google.common.base.Optional; +import javax.annotation.Nonnull; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.yangtools.yang.common.OperationFailedException; + +/** + * @author Thomas Pantelis + */ +public interface JSONRestconfService { + /** + * The data tree root path. + */ + String ROOT_PATH = null; + + /** + * Issues a restconf PUT request to the configuration data store. + * + * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id". + * To specify the root, use {@link ROOT_PATH}. + * @param payload the payload data in JSON format. + * @throws OperationFailedException if the request fails. + */ + void put(String uriPath, @Nonnull String payload) throws OperationFailedException; + + /** + * Issues a restconf POST request to the configuration data store. + * + * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id". + * To specify the root, use {@link ROOT_PATH}. + * @param payload the payload data in JSON format. + * @throws OperationFailedException if the request fails. + */ + void post(String uriPath, @Nonnull String payload) throws OperationFailedException; + + /** + * Issues a restconf DELETE request to the configuration data store. + * + * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id". + * To specify the root, use {@link ROOT_PATH}. + * @throws OperationFailedException if the request fails. + */ + void delete(String uriPath) throws OperationFailedException; + + /** + * Issues a restconf GET request to the given data store. + * + * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id". + * To specify the root, use {@link ROOT_PATH}. + * @param datastoreType the data store type to read from. + * @return an Optional containing the data in JSON format if present. + * @throws OperationFailedException if the request fails. + */ + Optional get(String uriPath, LogicalDatastoreType datastoreType) throws OperationFailedException; + + /** + * Invokes a yang-defined RPC. + * + * @param uriPath the path representing the RPC to invoke, eg "toaster:make-toast". + * @param input the input in JSON format if the RPC takes input. + * @return an Optional containing the output in JSON format if the RPC returns output. + * @throws OperationFailedException if the request fails. + */ + Optional invokeRpc(@Nonnull String uriPath, Optional input) throws OperationFailedException; +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonNormalizedNodeBodyReader.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonNormalizedNodeBodyReader.java index 42024cab08..0fbe94d5ab 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonNormalizedNodeBodyReader.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonNormalizedNodeBodyReader.java @@ -24,6 +24,7 @@ import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.Provider; import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.api.RestconfService; +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; import org.opendaylight.controller.sal.restconf.impl.InstanceIdentifierContext; import org.opendaylight.controller.sal.restconf.impl.NormalizedNodeContext; import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; @@ -64,79 +65,103 @@ public class JsonNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsPr @Override public NormalizedNodeContext readFrom(final Class type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, - final MultivaluedMap httpHeaders, final InputStream entityStream) throws IOException, - WebApplicationException { + final MultivaluedMap httpHeaders, final InputStream entityStream) + throws WebApplicationException, IOException { try { - final InstanceIdentifierContext path = getInstanceIdentifierContext(); - if (entityStream.available() < 1) { - return new NormalizedNodeContext(path, null); - } - final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); - final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); - - final SchemaNode parentSchema; - if(isPost()) { - // FIXME: We need dispatch for RPC. - parentSchema = path.getSchemaNode(); - } else if(path.getSchemaNode() instanceof SchemaContext) { + return readFrom(getInstanceIdentifierContext(), entityStream, isPost()); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private static void propagateExceptionAs(Exception e) throws RestconfDocumentedException { + if(e instanceof RestconfDocumentedException) { + throw (RestconfDocumentedException)e; + } + + if(e instanceof ResultAlreadySetException) { + LOG.debug("Error parsing json input:", e); + + throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. " + + "Are you creating multiple resources/subresources in POST request?"); + } + + LOG.debug("Error parsing json input", e); + + throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, e); + } + + public static NormalizedNodeContext readFrom(final String uriPath, final InputStream entityStream, + final boolean isPost) throws RestconfDocumentedException { + + try { + return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream, isPost); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private static NormalizedNodeContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream, + final boolean isPost) throws IOException { + if (entityStream.available() < 1) { + return new NormalizedNodeContext(path, null); + } + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + + final SchemaNode parentSchema; + if(isPost) { + // FIXME: We need dispatch for RPC. + parentSchema = path.getSchemaNode(); + } else if(path.getSchemaNode() instanceof SchemaContext) { + parentSchema = path.getSchemaContext(); + } else { + if (SchemaPath.ROOT.equals(path.getSchemaNode().getPath().getParent())) { parentSchema = path.getSchemaContext(); } else { - if (SchemaPath.ROOT.equals(path.getSchemaNode().getPath().getParent())) { - parentSchema = path.getSchemaContext(); - } else { - parentSchema = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(), path.getSchemaNode().getPath().getParent()); - } + parentSchema = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(), path.getSchemaNode().getPath().getParent()); } + } - final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), parentSchema); - final JsonReader reader = new JsonReader(new InputStreamReader(entityStream)); - jsonParser.parse(reader); + final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), parentSchema); + final JsonReader reader = new JsonReader(new InputStreamReader(entityStream)); + jsonParser.parse(reader); - NormalizedNode result = resultHolder.getResult(); - final List iiToDataList = new ArrayList<>(); - InstanceIdentifierContext newIIContext; + NormalizedNode result = resultHolder.getResult(); + final List iiToDataList = new ArrayList<>(); + InstanceIdentifierContext newIIContext; - while (result instanceof AugmentationNode || result instanceof ChoiceNode) { - final Object childNode = ((DataContainerNode) result).getValue().iterator().next(); - if (isPost()) { - iiToDataList.add(result.getIdentifier()); - } - result = (NormalizedNode) childNode; + while (result instanceof AugmentationNode || result instanceof ChoiceNode) { + final Object childNode = ((DataContainerNode) result).getValue().iterator().next(); + if (isPost) { + iiToDataList.add(result.getIdentifier()); } + result = (NormalizedNode) childNode; + } - if (isPost()) { - if (result instanceof MapEntryNode) { - iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getNodeType())); - iiToDataList.add(result.getIdentifier()); - } else { - iiToDataList.add(result.getIdentifier()); - } + if (isPost) { + if (result instanceof MapEntryNode) { + iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getNodeType())); + iiToDataList.add(result.getIdentifier()); } else { - if (result instanceof MapNode) { - result = Iterables.getOnlyElement(((MapNode) result).getValue()); - } + iiToDataList.add(result.getIdentifier()); } + } else { + if (result instanceof MapNode) { + result = Iterables.getOnlyElement(((MapNode) result).getValue()); + } + } - final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat( - path.getInstanceIdentifier().getPathArguments(), iiToDataList)); - - newIIContext = new InstanceIdentifierContext<>(fullIIToData, path.getSchemaNode(), path.getMountPoint(), - path.getSchemaContext()); - - return new NormalizedNodeContext(newIIContext, result); - } catch (final RestconfDocumentedException e) { - throw e; - } catch (final ResultAlreadySetException e) { - LOG.debug("Error parsing json input:", e); + final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat( + path.getInstanceIdentifier().getPathArguments(), iiToDataList)); - throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. " + - "Are you creating multiple resources/subresources in POST request?"); - } catch (final Exception e) { - LOG.debug("Error parsing json input", e); + newIIContext = new InstanceIdentifierContext<>(fullIIToData, path.getSchemaNode(), path.getMountPoint(), + path.getSchemaContext()); - throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL, - ErrorTag.MALFORMED_MESSAGE); - } + return new NormalizedNodeContext(newIIContext, result); } } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java new file mode 100644 index 0000000000..8f1a9f7d7d --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2015 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.impl; + +import com.google.common.base.Charsets; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.util.List; +import javax.ws.rs.core.MediaType; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.restconf.api.JSONRestconfService; +import org.opendaylight.controller.sal.rest.impl.JsonNormalizedNodeBodyReader; +import org.opendaylight.controller.sal.rest.impl.NormalizedNodeJsonBodyWriter; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.yangtools.yang.common.OperationFailedException; +import org.opendaylight.yangtools.yang.common.RpcError; +import org.opendaylight.yangtools.yang.common.RpcError.ErrorType; +import org.opendaylight.yangtools.yang.common.RpcResultBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the JSONRestconfService interface. + * + * @author Thomas Pantelis + */ +public class JSONRestconfServiceImpl implements JSONRestconfService, AutoCloseable { + private final static Logger LOG = LoggerFactory.getLogger(JSONRestconfServiceImpl.class); + + private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; + + @Override + public void put(String uriPath, String payload) throws OperationFailedException { + Preconditions.checkNotNull(payload, "payload can't be null"); + + LOG.debug("put: uriPath: {}, payload: {}", uriPath, payload); + + InputStream entityStream = new ByteArrayInputStream(payload.getBytes(Charsets.UTF_8)); + NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, false); + + LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", context.getData()); + + try { + RestconfImpl.getInstance().updateConfigurationData(uriPath, context); + } catch (Exception e) { + propagateExceptionAs(uriPath, e, "PUT"); + } + } + + @Override + public void post(String uriPath, String payload) throws OperationFailedException { + Preconditions.checkNotNull(payload, "payload can't be null"); + + LOG.debug("post: uriPath: {}, payload: {}", uriPath, payload); + + InputStream entityStream = new ByteArrayInputStream(payload.getBytes(Charsets.UTF_8)); + NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true); + + LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", context.getData()); + + try { + RestconfImpl.getInstance().createConfigurationData(uriPath, context, null); + } catch (Exception e) { + propagateExceptionAs(uriPath, e, "POST"); + } + } + + @Override + public void delete(String uriPath) throws OperationFailedException { + LOG.debug("delete: uriPath: {}", uriPath); + + try { + RestconfImpl.getInstance().deleteConfigurationData(uriPath); + } catch (Exception e) { + propagateExceptionAs(uriPath, e, "DELETE"); + } + } + + @Override + public Optional get(String uriPath, LogicalDatastoreType datastoreType) throws OperationFailedException { + LOG.debug("get: uriPath: {}", uriPath); + + try { + NormalizedNodeContext readData; + if(datastoreType == LogicalDatastoreType.CONFIGURATION) { + readData = RestconfImpl.getInstance().readConfigurationData(uriPath, null); + } else { + readData = RestconfImpl.getInstance().readOperationalData(uriPath, null); + } + + Optional result = Optional.of(toJson(readData)); + + LOG.debug("get returning: {}", result.get()); + + return result; + } catch (Exception e) { + if(!isDataMissing(e)) { + propagateExceptionAs(uriPath, e, "GET"); + } + + LOG.debug("Data missing - returning absent"); + return Optional.absent(); + } + } + + @Override + public Optional invokeRpc(String uriPath, Optional input) throws OperationFailedException { + Preconditions.checkNotNull(uriPath, "uriPath can't be null"); + + String actualInput = input.isPresent() ? input.get() : null; + + LOG.debug("invokeRpc: uriPath: {}, input: {}", uriPath, actualInput); + + String output = null; + try { + NormalizedNodeContext outputContext; + if(actualInput != null) { + InputStream entityStream = new ByteArrayInputStream(actualInput.getBytes(Charsets.UTF_8)); + NormalizedNodeContext inputContext = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true); + + LOG.debug("Parsed YangInstanceIdentifier: {}", inputContext.getInstanceIdentifierContext() + .getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", inputContext.getData()); + + outputContext = RestconfImpl.getInstance().invokeRpc(uriPath, inputContext, null); + } else { + outputContext = RestconfImpl.getInstance().invokeRpc(uriPath, "", null); + } + + if(outputContext.getData() != null) { + output = toJson(outputContext); + } + } catch (Exception e) { + propagateExceptionAs(uriPath, e, "RPC"); + } + + return Optional.fromNullable(output); + } + + @Override + public void close() { + } + + private String toJson(NormalizedNodeContext readData) throws IOException { + NormalizedNodeJsonBodyWriter writer = new NormalizedNodeJsonBodyWriter(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writer.writeTo(readData, NormalizedNodeContext.class, null, EMPTY_ANNOTATIONS, + MediaType.APPLICATION_JSON_TYPE, null, outputStream ); + return outputStream.toString(Charsets.UTF_8.name()); + } + + private boolean isDataMissing(Exception e) { + boolean dataMissing = false; + if(e instanceof RestconfDocumentedException) { + RestconfDocumentedException rde = (RestconfDocumentedException)e; + if(!rde.getErrors().isEmpty()) { + if(rde.getErrors().get(0).getErrorTag() == ErrorTag.DATA_MISSING) { + dataMissing = true; + } + } + } + + return dataMissing; + } + + private static void propagateExceptionAs(String uriPath, Exception e, String operation) throws OperationFailedException { + LOG.debug("Error for uriPath: {}", uriPath, e); + + if(e instanceof RestconfDocumentedException) { + throw new OperationFailedException(String.format("%s failed for URI %s", operation, uriPath), e.getCause(), + toRpcErrors(((RestconfDocumentedException)e).getErrors())); + } + + throw new OperationFailedException(String.format("%s failed for URI %s", operation, uriPath), e); + } + + private static RpcError[] toRpcErrors(List from) { + RpcError[] to = new RpcError[from.size()]; + int i = 0; + for(RestconfError e: from) { + to[i++] = RpcResultBuilder.newError(toRpcErrorType(e.getErrorType()), e.getErrorTag().getTagValue(), + e.getErrorMessage()); + } + + return to; + } + + private static ErrorType toRpcErrorType( + org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType errorType) { + switch(errorType) { + case TRANSPORT: { + return ErrorType.TRANSPORT; + } + case RPC: { + return ErrorType.RPC; + } + case PROTOCOL: { + return ErrorType.PROTOCOL; + } + default: { + return ErrorType.APPLICATION; + } + } + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/QueryParametersParser.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/QueryParametersParser.java index 4fc716e78a..2fba309986 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/QueryParametersParser.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/QueryParametersParser.java @@ -16,7 +16,7 @@ public class QueryParametersParser { PRETTY_PRINT("prettyPrint"), DEPTH("depth"); - private String uriParameterName; + private final String uriParameterName; UriParameters(final String uriParameterName) { this.uriParameterName = uriParameterName; @@ -30,6 +30,10 @@ public class QueryParametersParser { public static WriterParameters parseWriterParameters(final UriInfo info) { WriterParameters.WriterParametersBuilder wpBuilder = new WriterParameters.WriterParametersBuilder(); + if(info == null) { + return wpBuilder.build(); + } + String param = info.getQueryParameters(false).getFirst(UriParameters.DEPTH.toString()); if (!Strings.isNullOrEmpty(param) && !"unbounded".equals(param)) { try { diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java index bfa987ab8d..b013fe1d82 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java @@ -11,13 +11,10 @@ package org.opendaylight.controller.sal.restconf.impl; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; - import java.util.Collection; import java.util.List; - import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; - import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; import org.opendaylight.yangtools.yang.common.RpcError; @@ -62,6 +59,22 @@ public class RestconfDocumentedException extends WebApplicationException { this(null, new RestconfError(errorType, errorTag, message)); } + /** + * Constructs an instance with an error message, error type, error tag and exception cause. + * + * @param message + * A string which provides a plain text string describing the error. + * @param errorType + * The enumerated type indicating the layer where the error occurred. + * @param errorTag + * The enumerated tag representing a more specific error cause. + * @param cause + * The underlying exception cause. + */ + public RestconfDocumentedException(String message, ErrorType errorType, ErrorTag errorTag, Throwable cause) { + this(cause, new RestconfError(errorType, errorTag, message, null, RestconfError.toErrorInfo(cause))); + } + /** * Constructs an instance with an error message and exception cause. * The stack trace of the exception is included in the error info. 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 1a47c12842..87838872fb 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 @@ -867,7 +867,7 @@ public class RestconfImpl implements RestconfService { throw e; } catch (final Exception e) { final String errMsg = "Error creating data "; - LOG.info(errMsg + uriInfo.getPath(), e); + LOG.info(errMsg + (uriInfo != null ? uriInfo.getPath() : ""), e); throw new RestconfDocumentedException(errMsg, e); } @@ -881,6 +881,11 @@ public class RestconfImpl implements RestconfService { } private URI resolveLocation(final UriInfo uriInfo, final String uriBehindBase, final DOMMountPoint mountPoint, final YangInstanceIdentifier normalizedII) { + if(uriInfo == null) { + // This is null if invoked internally + return null; + } + final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("config"); try { diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang b/opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang new file mode 100644 index 0000000000..1f939c8dff --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang @@ -0,0 +1,30 @@ +module sal-restconf-service { + yang-version 1; + namespace "urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service"; + prefix "sal-restconf-service"; + + import config { prefix config; revision-date 2013-04-05; } + + description "Definition for the internal restconf service"; + + revision "2015-07-08" { + description "Initial revision"; + } + + identity json-restconf-service { + base "config:service-type"; + config:java-class "org.opendaylight.controller.restconf.api.JSONRestconfService"; + } + + identity json-restconf-service-impl { + base config:module-type; + config:provided-service json-restconf-service; + config:java-name-prefix JSONRestconfService; + } + + augment "/config:modules/config:module/config:configuration" { + case json-restconf-service-impl { + when "/config:modules/config:module/config:type = 'json-restconf-service-impl'"; + } + } +} \ 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/JSONRestconfServiceImplTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java new file mode 100644 index 0000000000..a9955cd0d4 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2015 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.impl.test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Matchers.notNull; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.Futures; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.controller.md.sal.dom.api.DOMRpcException; +import org.opendaylight.controller.md.sal.dom.api.DOMRpcImplementationNotAvailableException; +import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult; +import org.opendaylight.controller.md.sal.dom.spi.DefaultDOMRpcResult; +import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.JSONRestconfServiceImpl; +import org.opendaylight.controller.sal.restconf.impl.RestconfImpl; +import org.opendaylight.yangtools.yang.common.OperationFailedException; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableContainerNodeBuilder; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; + +/** + * Unit tests for JSONRestconfServiceImpl. + * + * @author Thomas Pantelis + */ +public class JSONRestconfServiceImplTest { + static final String IETF_INTERFACES_NS = "urn:ietf:params:xml:ns:yang:ietf-interfaces"; + static final String IETF_INTERFACES_VERSION = "2013-07-04"; + static final QName INTERFACES_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "interfaces"); + static final QName INTERFACE_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "interface"); + static final QName NAME_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "name"); + static final QName TYPE_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "type"); + static final QName ENABLED_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "enabled"); + static final QName DESC_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "description"); + + static final String TEST_MODULE_NS = "test:module"; + static final String TEST_MODULE_VERSION = "2014-01-09"; + static final QName TEST_CONT_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "cont"); + static final QName TEST_CONT1_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "cont1"); + static final QName TEST_LF11_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "lf11"); + static final QName TEST_LF12_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "lf12"); + + static final String TOASTER_MODULE_NS = "http://netconfcentral.org/ns/toaster"; + static final String TOASTER_MODULE_VERSION = "2009-11-20"; + static final QName TOASTER_DONENESS_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "toasterDoneness"); + static final QName TOASTER_TYPE_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "toasterToastType"); + static final QName WHEAT_BREAD_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "wheat-bread"); + static final QName MAKE_TOAST_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "make-toast"); + static final QName CANCEL_TOAST_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "cancel-toast"); + static final QName TEST_OUTPUT_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "testOutput"); + static final QName TEXT_OUT_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "textOut"); + + private static BrokerFacade brokerFacade; + + private final JSONRestconfServiceImpl service = new JSONRestconfServiceImpl(); + + @BeforeClass + public static void init() throws IOException { + ControllerContext.getInstance().setSchemas(TestUtils.loadSchemaContext("/full-versions/yangs")); + brokerFacade = mock(BrokerFacade.class); + RestconfImpl.getInstance().setBroker(brokerFacade); + RestconfImpl.getInstance().setControllerContext(ControllerContext.getInstance()); + } + + @Before + public void setup() { + reset(brokerFacade); + } + + private String loadData(String path) throws IOException { + InputStream stream = JSONRestconfServiceImplTest.class.getResourceAsStream(path); + return IOUtils.toString(stream, "UTF-8"); + } + + @SuppressWarnings("rawtypes") + @Test + public void testPut() throws Exception { + doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPut( + notNull(SchemaContext.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class)); + + String uriPath = "ietf-interfaces:interfaces/interface/eth0"; + String payload = loadData("/parts/ietf-interfaces_interfaces.json"); + + service.put(uriPath, payload); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + ArgumentCaptor capturedNode = ArgumentCaptor.forClass(NormalizedNode.class); + verify(brokerFacade).commitConfigurationDataPut(notNull(SchemaContext.class), capturedPath.capture(), + capturedNode.capture()); + + verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME, + new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"}); + + assertTrue("Expected MapEntryNode. Actual " + capturedNode.getValue().getClass(), + capturedNode.getValue() instanceof MapEntryNode); + MapEntryNode actualNode = (MapEntryNode) capturedNode.getValue(); + assertEquals("MapEntryNode node type", INTERFACE_QNAME, actualNode.getNodeType()); + verifyLeafNode(actualNode, NAME_QNAME, "eth0"); + verifyLeafNode(actualNode, TYPE_QNAME, "ethernetCsmacd"); + verifyLeafNode(actualNode, ENABLED_QNAME, Boolean.FALSE); + verifyLeafNode(actualNode, DESC_QNAME, "some interface"); + } + + @SuppressWarnings("rawtypes") + @Test + public void testPutBehindMountPoint() throws Exception { + DOMMountPoint mockMountPoint = setupTestMountPoint(); + + doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPut( + notNull(DOMMountPoint.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class)); + + String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont/cont1"; + String payload = loadData("/full-versions/testCont1Data.json"); + + service.put(uriPath, payload); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + ArgumentCaptor capturedNode = ArgumentCaptor.forClass(NormalizedNode.class); + verify(brokerFacade).commitConfigurationDataPut(same(mockMountPoint), capturedPath.capture(), + capturedNode.capture()); + + verifyPath(capturedPath.getValue(), TEST_CONT_QNAME, TEST_CONT1_QNAME); + + assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode); + ContainerNode actualNode = (ContainerNode) capturedNode.getValue(); + assertEquals("ContainerNode node type", TEST_CONT1_QNAME, actualNode.getNodeType()); + verifyLeafNode(actualNode, TEST_LF11_QNAME, "lf11 data"); + verifyLeafNode(actualNode, TEST_LF12_QNAME, "lf12 data"); + } + + @Test(expected=TransactionCommitFailedException.class) + public void testPutFailure() throws Throwable { + doReturn(Futures.immediateFailedCheckedFuture(new TransactionCommitFailedException("mock"))) + .when(brokerFacade).commitConfigurationDataPut(notNull(SchemaContext.class), + notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class)); + + String uriPath = "ietf-interfaces:interfaces/interface/eth0"; + String payload = loadData("/parts/ietf-interfaces_interfaces.json"); + + try { + service.put(uriPath, payload); + } catch (OperationFailedException e) { + assertNotNull(e.getCause()); + throw e.getCause(); + } + } + + @SuppressWarnings("rawtypes") + @Test + public void testPost() throws Exception { + doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPost( + any(SchemaContext.class), any(YangInstanceIdentifier.class), any(NormalizedNode.class)); + + String uriPath = null; + String payload = loadData("/parts/ietf-interfaces_interfaces_absolute_path.json"); + + service.post(uriPath, payload); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + ArgumentCaptor capturedNode = ArgumentCaptor.forClass(NormalizedNode.class); + verify(brokerFacade).commitConfigurationDataPost(notNull(SchemaContext.class), capturedPath.capture(), + capturedNode.capture()); + + verifyPath(capturedPath.getValue(), INTERFACES_QNAME); + + assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode); + ContainerNode actualNode = (ContainerNode) capturedNode.getValue(); + assertEquals("ContainerNode node type", INTERFACES_QNAME, actualNode.getNodeType()); + + Optional> mapChild = actualNode.getChild(new NodeIdentifier(INTERFACE_QNAME)); + assertEquals(INTERFACE_QNAME.toString() + " present", true, mapChild.isPresent()); + assertTrue("Expected MapNode. Actual " + mapChild.get().getClass(), mapChild.get() instanceof MapNode); + MapNode mapNode = (MapNode)mapChild.get(); + + NodeIdentifierWithPredicates entryNodeID = new NodeIdentifierWithPredicates( + INTERFACE_QNAME, NAME_QNAME, "eth0"); + Optional entryChild = mapNode.getChild(entryNodeID); + assertEquals(entryNodeID.toString() + " present", true, entryChild.isPresent()); + MapEntryNode entryNode = entryChild.get(); + verifyLeafNode(entryNode, NAME_QNAME, "eth0"); + verifyLeafNode(entryNode, TYPE_QNAME, "ethernetCsmacd"); + verifyLeafNode(entryNode, ENABLED_QNAME, Boolean.FALSE); + verifyLeafNode(entryNode, DESC_QNAME, "some interface"); + } + + @SuppressWarnings("rawtypes") + @Test + public void testPostBehindMountPoint() throws Exception { + DOMMountPoint mockMountPoint = setupTestMountPoint(); + + doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPost( + notNull(DOMMountPoint.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class)); + + String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont"; + String payload = loadData("/full-versions/testCont1Data.json"); + + service.post(uriPath, payload); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + ArgumentCaptor capturedNode = ArgumentCaptor.forClass(NormalizedNode.class); + verify(brokerFacade).commitConfigurationDataPost(same(mockMountPoint), capturedPath.capture(), + capturedNode.capture()); + + verifyPath(capturedPath.getValue(), TEST_CONT_QNAME, TEST_CONT1_QNAME); + + assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode); + ContainerNode actualNode = (ContainerNode) capturedNode.getValue(); + assertEquals("ContainerNode node type", TEST_CONT1_QNAME, actualNode.getNodeType()); + verifyLeafNode(actualNode, TEST_LF11_QNAME, "lf11 data"); + verifyLeafNode(actualNode, TEST_LF12_QNAME, "lf12 data"); + } + + @Test(expected=TransactionCommitFailedException.class) + public void testPostFailure() throws Throwable { + doReturn(Futures.immediateFailedCheckedFuture(new TransactionCommitFailedException("mock"))) + .when(brokerFacade).commitConfigurationDataPost(any(SchemaContext.class), + any(YangInstanceIdentifier.class), any(NormalizedNode.class)); + + String uriPath = null; + String payload = loadData("/parts/ietf-interfaces_interfaces_absolute_path.json"); + + try { + service.post(uriPath, payload); + } catch (OperationFailedException e) { + assertNotNull(e.getCause()); + throw e.getCause(); + } + } + + @Test + public void testDelete() throws Exception { + doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataDelete( + notNull(YangInstanceIdentifier.class)); + + String uriPath = "ietf-interfaces:interfaces/interface/eth0"; + + service.delete(uriPath); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + verify(brokerFacade).commitConfigurationDataDelete(capturedPath.capture()); + + verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME, + new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"}); + } + + @Test(expected=OperationFailedException.class) + public void testDeleteFailure() throws Exception { + String invalidUriPath = "ietf-interfaces:interfaces/invalid"; + + service.delete(invalidUriPath); + } + + @Test + public void testGetConfig() throws Exception { + testGet(LogicalDatastoreType.CONFIGURATION); + } + + @Test + public void testGetOperational() throws Exception { + testGet(LogicalDatastoreType.OPERATIONAL); + } + + @Test + public void testGetWithNoData() throws Exception { + doReturn(null).when(brokerFacade).readConfigurationData(notNull(YangInstanceIdentifier.class)); + + String uriPath = "ietf-interfaces:interfaces"; + + Optional optionalResp = service.get(uriPath, LogicalDatastoreType.CONFIGURATION); + + assertEquals("Response present", false, optionalResp.isPresent()); + } + + @Test(expected=OperationFailedException.class) + public void testGetFailure() throws Exception { + String invalidUriPath = "/ietf-interfaces:interfaces/invalid"; + + service.get(invalidUriPath, LogicalDatastoreType.CONFIGURATION); + } + + @SuppressWarnings("rawtypes") + @Test + public void testInvokeRpcWithInput() throws Exception { + SchemaPath path = SchemaPath.create(true, MAKE_TOAST_QNAME); + + DOMRpcResult expResult = new DefaultDOMRpcResult((NormalizedNode)null); + doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(eq(path), + any(NormalizedNode.class)); + + String uriPath = "toaster:make-toast"; + String input = loadData("/full-versions/make-toast-rpc-input.json"); + + Optional output = service.invokeRpc(uriPath, Optional.of(input)); + + assertEquals("Output present", false, output.isPresent()); + + ArgumentCaptor capturedNode = ArgumentCaptor.forClass(NormalizedNode.class); + verify(brokerFacade).invokeRpc(eq(path), capturedNode.capture()); + + assertTrue("Expected ContainerNode. Actual " + capturedNode.getValue().getClass(), + capturedNode.getValue() instanceof ContainerNode); + ContainerNode actualNode = (ContainerNode) capturedNode.getValue(); + verifyLeafNode(actualNode, TOASTER_DONENESS_QNAME, Long.valueOf(10)); + verifyLeafNode(actualNode, TOASTER_TYPE_QNAME, WHEAT_BREAD_QNAME); + } + + @Test + public void testInvokeRpcWithNoInput() throws Exception { + SchemaPath path = SchemaPath.create(true, CANCEL_TOAST_QNAME); + + DOMRpcResult expResult = new DefaultDOMRpcResult((NormalizedNode)null); + doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(any(SchemaPath.class), + any(NormalizedNode.class)); + + String uriPath = "toaster:cancel-toast"; + + Optional output = service.invokeRpc(uriPath, Optional.absent()); + + assertEquals("Output present", false, output.isPresent()); + + verify(brokerFacade).invokeRpc(eq(path), isNull(NormalizedNode.class)); + } + + @Test + public void testInvokeRpcWithOutput() throws Exception { + SchemaPath path = SchemaPath.create(true, TEST_OUTPUT_QNAME); + + NormalizedNode outputNode = ImmutableContainerNodeBuilder.create() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TEST_OUTPUT_QNAME)) + .withChild(ImmutableNodes.leafNode(TEXT_OUT_QNAME, "foo")).build(); + DOMRpcResult expResult = new DefaultDOMRpcResult(outputNode); + doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(any(SchemaPath.class), + any(NormalizedNode.class)); + + String uriPath = "toaster:testOutput"; + + Optional output = service.invokeRpc(uriPath, Optional.absent()); + + assertEquals("Output present", true, output.isPresent()); + assertNotNull("Returned null response", output.get()); + assertThat("Missing \"textOut\"", output.get(), containsString("\"textOut\":\"foo\"")); + + verify(brokerFacade).invokeRpc(eq(path), isNull(NormalizedNode.class)); + } + + @Test(expected=OperationFailedException.class) + public void testInvokeRpcFailure() throws Exception { + DOMRpcException exception = new DOMRpcImplementationNotAvailableException("testExeption"); + doReturn(Futures.immediateFailedCheckedFuture(exception)).when(brokerFacade).invokeRpc(any(SchemaPath.class), + any(NormalizedNode.class)); + + String uriPath = "toaster:cancel-toast"; + + service.invokeRpc(uriPath, Optional.absent()); + } + + void testGet(LogicalDatastoreType datastoreType) throws OperationFailedException { + MapEntryNode entryNode = ImmutableNodes.mapEntryBuilder(INTERFACE_QNAME, NAME_QNAME, "eth0") + .withChild(ImmutableNodes.leafNode(NAME_QNAME, "eth0")) + .withChild(ImmutableNodes.leafNode(TYPE_QNAME, "ethernetCsmacd")) + .withChild(ImmutableNodes.leafNode(ENABLED_QNAME, Boolean.TRUE)) + .withChild(ImmutableNodes.leafNode(DESC_QNAME, "eth interface")) + .build(); + + if(datastoreType == LogicalDatastoreType.CONFIGURATION) { + doReturn(entryNode).when(brokerFacade).readConfigurationData(notNull(YangInstanceIdentifier.class)); + } else { + doReturn(entryNode).when(brokerFacade).readOperationalData(notNull(YangInstanceIdentifier.class)); + } + + String uriPath = "/ietf-interfaces:interfaces/interface/eth0"; + + Optional optionalResp = service.get(uriPath, datastoreType); + assertEquals("Response present", true, optionalResp.isPresent()); + String jsonResp = optionalResp.get(); + + assertNotNull("Returned null response", jsonResp); + assertThat("Missing \"name\"", jsonResp, containsString("\"name\":\"eth0\"")); + assertThat("Missing \"type\"", jsonResp, containsString("\"type\":\"ethernetCsmacd\"")); + assertThat("Missing \"enabled\"", jsonResp, containsString("\"enabled\":true")); + assertThat("Missing \"description\"", jsonResp, containsString("\"description\":\"eth interface\"")); + + ArgumentCaptor capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class); + if (datastoreType == LogicalDatastoreType.CONFIGURATION) { + verify(brokerFacade).readConfigurationData(capturedPath.capture()); + } else { + verify(brokerFacade).readOperationalData(capturedPath.capture()); + } + + verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME, + new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"}); + } + + DOMMountPoint setupTestMountPoint() throws FileNotFoundException { + SchemaContext schemaContextTestModule = TestUtils.loadSchemaContext("/full-versions/test-module"); + DOMMountPoint mockMountPoint = mock(DOMMountPoint.class); + doReturn(schemaContextTestModule).when(mockMountPoint).getSchemaContext(); + + DOMMountPointService mockMountService = mock(DOMMountPointService.class); + doReturn(Optional.of(mockMountPoint)).when(mockMountService).getMountPoint(notNull(YangInstanceIdentifier.class)); + + ControllerContext.getInstance().setMountService(mockMountService); + return mockMountPoint; + } + + void verifyLeafNode(DataContainerNode parent, QName leafType, Object leafValue) { + Optional> leafChild = parent.getChild(new NodeIdentifier(leafType)); + assertEquals(leafType.toString() + " present", true, leafChild.isPresent()); + assertEquals(leafType.toString() + " value", leafValue, leafChild.get().getValue()); + } + + void verifyPath(YangInstanceIdentifier path, Object... expArgs) { + List pathArgs = path.getPathArguments(); + assertEquals("Arg count for actual path " + path, expArgs.length, pathArgs.size()); + int i = 0; + for(PathArgument actual: pathArgs) { + QName expNodeType; + if(expArgs[i] instanceof Object[]) { + Object[] listEntry = (Object[]) expArgs[i]; + expNodeType = (QName) listEntry[0]; + + assertTrue(actual instanceof NodeIdentifierWithPredicates); + Map keyValues = ((NodeIdentifierWithPredicates)actual).getKeyValues(); + assertEquals(String.format("Path arg %d keyValues size", i + 1), 1, keyValues.size()); + QName expKey = (QName) listEntry[1]; + assertEquals(String.format("Path arg %d keyValue for %s", i + 1, expKey), listEntry[2], + keyValues.get(expKey)); + } else { + expNodeType = (QName) expArgs[i]; + } + + assertEquals(String.format("Path arg %d node type", i + 1), expNodeType, actual.getNodeType()); + i++; + } + + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json new file mode 100644 index 0000000000..1bba146474 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json @@ -0,0 +1,7 @@ +{ + "input" : + { + "toaster:toasterDoneness" : "10", + "toaster:toasterToastType": "wheat-bread" + } +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json new file mode 100644 index 0000000000..c7554f76d7 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json @@ -0,0 +1,6 @@ +{ + "cont1": { + "lf11": "lf11 data", + "lf12": "lf12 data" + } +} \ No newline at end of file