From: Marek Gradzki Date: Thu, 9 Aug 2018 11:45:54 +0000 (+0200) Subject: NETCONF-557: Add support for URL capability X-Git-Tag: release/neon~144^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=3379e3cc37adc81a91e1be0ea89c7b0464dc7b6a;p=netconf.git NETCONF-557: Add support for URL capability This patch brings support of URL capability in and RPCs to mdsal-netconf-connector. Remote config upload is not supported. Remote to remote operations are not supported. The capability is advertised as: urn:ietf:params:netconf:capability:url:1.0?scheme=file but config download is also supported via http and https. Change-Id: Idb5bae4e24ff2a098bc60bf24c36fb40113fd8f5 Signed-off-by: Marek Gradzki --- diff --git a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractConfigOperation.java b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractConfigOperation.java index cfeddcdb3a..9a57a2ecad 100644 --- a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractConfigOperation.java +++ b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractConfigOperation.java @@ -8,14 +8,31 @@ package org.opendaylight.netconf.mdsal.connector.ops; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Optional; import com.google.common.base.Strings; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Base64; import org.opendaylight.netconf.api.DocumentedException; import org.opendaylight.netconf.api.xml.XmlElement; +import org.opendaylight.netconf.api.xml.XmlNetconfConstants; +import org.opendaylight.netconf.api.xml.XmlUtil; import org.opendaylight.netconf.util.mapping.AbstractSingletonNetconfOperation; +import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; abstract class AbstractConfigOperation extends AbstractSingletonNetconfOperation { + static final String URL_KEY = "url"; + static final String CONFIG_KEY = "config"; + private static final int TIMEOUT_MS = 5000; protected AbstractConfigOperation(final String netconfSessionIdForReporting) { super(netconfSessionIdForReporting); @@ -34,4 +51,66 @@ abstract class AbstractConfigOperation extends AbstractSingletonNetconfOperation return elementsByTagName; } + + static XmlElement getConfigElement(final XmlElement parent) throws DocumentedException { + final Optional configElement = parent.getOnlyChildElementOptionally(CONFIG_KEY); + if (configElement.isPresent()) { + return configElement.get(); + } else { + final Optional urlElement = parent.getOnlyChildElementOptionally(URL_KEY); + if (!urlElement.isPresent()) { + throw new DocumentedException("Invalid RPC, neither not element is present", + DocumentedException.ErrorType.PROTOCOL, + DocumentedException.ErrorTag.MISSING_ELEMENT, + DocumentedException.ErrorSeverity.ERROR); + } + + final Document document = getDocumentFromUrl(urlElement.get().getTextContent()); + return XmlElement.fromDomElementWithExpected(document.getDocumentElement(), CONFIG_KEY, + XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0); + } + } + + /** + * Parses XML Document available at given URL. + * + *

JDK8 supports URL schemes that include http, https, file, and jar, but {@link URLStreamHandler}s for other + * protocols (e.g. ftp) may be available. + * + * @param url URL as defined in RFC 2396 + * @see URL#URL(String, String, int, String) + */ + private static Document getDocumentFromUrl(final String url) throws DocumentedException { + try (InputStream input = openConnection(new URL(url))) { + return XmlUtil.readXmlToDocument(input); + } catch (MalformedURLException e) { + throw new DocumentedException(url + " URL is invalid or unsupported", e, + DocumentedException.ErrorType.APPLICATION, + DocumentedException.ErrorTag.INVALID_VALUE, + DocumentedException.ErrorSeverity.ERROR); + } catch (IOException e) { + throw new DocumentedException("Could not open URL:" + url, e, + DocumentedException.ErrorType.APPLICATION, + DocumentedException.ErrorTag.OPERATION_FAILED, + DocumentedException.ErrorSeverity.ERROR); + } catch (SAXException e) { + throw new DocumentedException("Could not parse XML at" + url, e, + DocumentedException.ErrorType.APPLICATION, + DocumentedException.ErrorTag.OPERATION_FAILED, + DocumentedException.ErrorSeverity.ERROR); + } + } + + private static InputStream openConnection(final URL url) throws IOException { + final URLConnection connection = url.openConnection(); + connection.setConnectTimeout(TIMEOUT_MS); + connection.setReadTimeout(TIMEOUT_MS); + + // Support Basic Authentication scheme, e.g. http://admin:admin@localhost:8000/config.conf + if (url.getUserInfo() != null) { + String basicAuth = "Basic " + Base64.getUrlEncoder().encodeToString(url.getUserInfo().getBytes(UTF_8)); + connection.setRequestProperty("Authorization", basicAuth); + } + return connection.getInputStream(); + } } diff --git a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractEdit.java b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractEdit.java index b68e17aacf..209e4ec01a 100644 --- a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractEdit.java +++ b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractEdit.java @@ -8,7 +8,6 @@ package org.opendaylight.netconf.mdsal.connector.ops; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import java.net.URI; import java.net.URISyntaxException; @@ -96,7 +95,7 @@ abstract class AbstractEdit extends AbstractConfigOperation { return schemaNode.get(); } - protected static Datastore extractTargetParameter(final XmlElement operationElement, final String operationName) + protected static XmlElement extractTargetElement(final XmlElement operationElement, final String operationName) throws DocumentedException { final NodeList elementsByTagName = getElementsByTagName(operationElement, TARGET_KEY); // Direct lookup instead of using XmlElement class due to performance @@ -109,22 +108,7 @@ abstract class AbstractEdit extends AbstractConfigOperation { throw new DocumentedException("Multiple target elements", ErrorType.RPC, ErrorTag.UNKNOWN_ATTRIBUTE, ErrorSeverity.ERROR); } else { - final XmlElement targetChildNode = - XmlElement.fromDomElement((Element) elementsByTagName.item(0)).getOnlyChildElement(); - return Datastore.valueOf(targetChildNode.getName()); + return XmlElement.fromDomElement((Element) elementsByTagName.item(0)).getOnlyChildElement(); } } - - protected static XmlElement getElement(final XmlElement parent, final String elementName) - throws DocumentedException { - final Optional childNode = parent.getOnlyChildElementOptionally(elementName); - if (!childNode.isPresent()) { - throw new DocumentedException(elementName + " element is missing", - ErrorType.PROTOCOL, - ErrorTag.MISSING_ELEMENT, - ErrorSeverity.ERROR); - } - - return childNode.get(); - } } diff --git a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfig.java b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfig.java index df29fb30e1..c3e3cb423d 100644 --- a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfig.java +++ b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfig.java @@ -8,8 +8,23 @@ package org.opendaylight.netconf.mdsal.connector.ops; +import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0; + import com.google.common.base.Optional; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.dom.DOMResult; import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction; import org.opendaylight.netconf.api.DocumentedException; import org.opendaylight.netconf.api.DocumentedException.ErrorSeverity; @@ -22,19 +37,30 @@ import org.opendaylight.netconf.mdsal.connector.CurrentSchemaContext; import org.opendaylight.netconf.mdsal.connector.TransactionProvider; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; 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.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.impl.schema.Builders; import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; public final class CopyConfig extends AbstractEdit { private static final String OPERATION_NAME = "copy-config"; - private static final String CONFIG_KEY = "config"; private static final String SOURCE_KEY = "source"; + private static final XMLOutputFactory XML_OUTPUT_FACTORY; + + static { + XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory(); + XML_OUTPUT_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } // Top-level "data" node without child nodes private static final ContainerNode EMPTY_ROOT_NODE = Builders.containerBuilder() @@ -50,15 +76,31 @@ public final class CopyConfig extends AbstractEdit { @Override protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement) - throws DocumentedException { - final Datastore targetDatastore = extractTargetParameter(operationElement, OPERATION_NAME); - if (targetDatastore == Datastore.running) { + throws DocumentedException { + final XmlElement targetElement = extractTargetElement(operationElement, OPERATION_NAME); + final String target = targetElement.getName(); + if (Datastore.running.toString().equals(target)) { throw new DocumentedException("edit-config on running datastore is not supported", - ErrorType.PROTOCOL, - ErrorTag.OPERATION_NOT_SUPPORTED, - ErrorSeverity.ERROR); + ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, + ErrorSeverity.ERROR); + } else if (Datastore.candidate.toString().equals(target)) { + copyToCandidate(operationElement); + } else if (URL_KEY.equals(target)) { + copyToUrl(targetElement, operationElement); + } else { + throw new DocumentedException("Unsupported target: " + target, + ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT, + ErrorSeverity.ERROR); } - final XmlElement configElement = extractConfigParameter(operationElement); + return XmlUtil.createElement(document, XmlNetconfConstants.OK, Optional.absent()); + } + + private void copyToCandidate(final XmlElement operationElement) + throws DocumentedException { + final XmlElement source = getSourceElement(operationElement); + final List configElements = getConfigElement(source).getChildElements(); // , unlike , always replaces entire configuration, // so remove old configuration first: @@ -66,7 +108,7 @@ public final class CopyConfig extends AbstractEdit { rwTx.put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.EMPTY, EMPTY_ROOT_NODE); // Then create nodes present in the element: - for (final XmlElement element : configElement.getChildElements()) { + for (final XmlElement element : configElements) { final String ns = element.getNamespace(); final DataSchemaNode schemaNode = getSchemaNodeFromNamespace(ns, element); final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); @@ -76,12 +118,110 @@ public final class CopyConfig extends AbstractEdit { // Doing merge instead of put to support top-level list: rwTx.merge(LogicalDatastoreType.CONFIGURATION, path, data); } - return XmlUtil.createElement(document, XmlNetconfConstants.OK, Optional.absent()); } - private static XmlElement extractConfigParameter(final XmlElement operationElement) throws DocumentedException { - final XmlElement source = getElement(operationElement, SOURCE_KEY); - return getElement(source, CONFIG_KEY); + private static XmlElement getSourceElement(final XmlElement parent) throws DocumentedException { + final Optional sourceElement = parent.getOnlyChildElementOptionally(SOURCE_KEY); + if (!sourceElement.isPresent()) { + throw new DocumentedException(" element is missing", + DocumentedException.ErrorType.PROTOCOL, + DocumentedException.ErrorTag.MISSING_ELEMENT, + DocumentedException.ErrorSeverity.ERROR); + } + + return sourceElement.get(); + } + + private void copyToUrl(final XmlElement urlElement, final XmlElement operationElement) throws DocumentedException { + final String url = urlElement.getTextContent(); + if (!url.startsWith("file:")) { + throw new DocumentedException("Unsupported protocol: " + url, + ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, + ErrorSeverity.ERROR); + } + + // Read data from datastore: + final XmlElement source = getSourceElement(operationElement).getOnlyChildElement(); + final ContainerNode data = readData(source); + + // Transform NN to XML: + final Document document = operationElement.getDomElement().getOwnerDocument(); + final Node node = transformNormalizedNode(document, data); + + // Save XML to file: + final String xml = XmlUtil.toString((Element) node); + try { + final Path file = Paths.get(new URI(url)); + Files.write(file, xml.getBytes(StandardCharsets.UTF_8)); + } catch (URISyntaxException | IllegalArgumentException e) { + throw new DocumentedException("Invalid URI: " + url, e, + ErrorType.RPC, + ErrorTag.INVALID_VALUE, + ErrorSeverity.ERROR); + } catch (IOException e) { + throw new DocumentedException("Failed to write : " + url, e, + ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, + ErrorSeverity.ERROR); + } + } + + private ContainerNode readData(final XmlElement source) throws DocumentedException { + final Datastore sourceDatastore = getDatastore(source); + final DOMDataReadWriteTransaction rwTx = getTransaction(sourceDatastore); + final YangInstanceIdentifier dataRoot = YangInstanceIdentifier.EMPTY; + try { + final Optional> normalizedNodeOptional = rwTx.read( + LogicalDatastoreType.CONFIGURATION, dataRoot).checkedGet(); + if (sourceDatastore == Datastore.running) { + transactionProvider.abortRunningTransaction(rwTx); + } + return (ContainerNode) normalizedNodeOptional.get(); + } catch (ReadFailedException e) { + throw new IllegalStateException("Unable to read data " + dataRoot, e); + } + } + + private static Datastore getDatastore(final XmlElement source) throws DocumentedException { + try { + return Datastore.valueOf(source.getName()); + } catch (IllegalArgumentException e) { + throw new DocumentedException("Unsupported source for target", e, + ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, + ErrorSeverity.ERROR); + } + } + + private DOMDataReadWriteTransaction getTransaction(final Datastore datastore) throws DocumentedException { + if (datastore == Datastore.candidate) { + return transactionProvider.getOrCreateTransaction(); + } else if (datastore == Datastore.running) { + return transactionProvider.createRunningTransaction(); + } + throw new DocumentedException("Incorrect Datastore: ", ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT, + ErrorSeverity.ERROR); + } + + private Node transformNormalizedNode(final Document document, final ContainerNode data) { + final Element configElement = document.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, CONFIG_KEY); + final DOMResult result = new DOMResult(configElement); + try { + final XMLStreamWriter xmlWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(result); + final NormalizedNodeStreamWriter nnStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, + schemaContext.getCurrentContext(), SchemaPath.ROOT); + + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(nnStreamWriter, true); + for (DataContainerChild child : data.getValue()) { + nnWriter.write(child); + } + nnWriter.flush(); + xmlWriter.flush(); + } catch (XMLStreamException | IOException e) { + throw new RuntimeException(e); + } + return result.getNode(); } @Override diff --git a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/EditConfig.java b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/EditConfig.java index 6776b6be26..190c8bf1e9 100644 --- a/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/EditConfig.java +++ b/netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/EditConfig.java @@ -49,7 +49,6 @@ public final class EditConfig extends AbstractEdit { private static final Logger LOG = LoggerFactory.getLogger(EditConfig.class); private static final String OPERATION_NAME = "edit-config"; - private static final String CONFIG_KEY = "config"; private static final String DEFAULT_OPERATION_KEY = "default-operation"; private final TransactionProvider transactionProvider; @@ -62,7 +61,8 @@ public final class EditConfig extends AbstractEdit { @Override protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement) throws DocumentedException { - final Datastore targetDatastore = extractTargetParameter(operationElement, OPERATION_NAME); + final XmlElement targetElement = extractTargetElement(operationElement, OPERATION_NAME); + final Datastore targetDatastore = Datastore.valueOf(targetElement.getName()); if (targetDatastore == Datastore.running) { throw new DocumentedException("edit-config on running datastore is not supported", ErrorType.PROTOCOL, @@ -72,7 +72,7 @@ public final class EditConfig extends AbstractEdit { final ModifyAction defaultAction = getDefaultOperation(operationElement); - final XmlElement configElement = getElement(operationElement, CONFIG_KEY); + final XmlElement configElement = getConfigElement(operationElement); for (final XmlElement element : configElement.getChildElements()) { final String ns = element.getNamespace(); diff --git a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractNetconfOperationTest.java b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractNetconfOperationTest.java index 019a374994..84629de6b2 100644 --- a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractNetconfOperationTest.java +++ b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractNetconfOperationTest.java @@ -130,6 +130,12 @@ public abstract class AbstractNetconfOperationTest { return executeOperation(editConfig, resource); } + protected Document edit(final Document request) throws Exception { + final EditConfig editConfig = new EditConfig(SESSION_ID_FOR_REPORTING, currentSchemaContext, + transactionProvider); + return executeOperation(editConfig, request); + } + protected Document get() throws Exception { final Get get = new Get(SESSION_ID_FOR_REPORTING, currentSchemaContext, transactionProvider); return executeOperation(get, "messages/mapping/get.xml"); @@ -187,8 +193,11 @@ public abstract class AbstractNetconfOperationTest { protected static Document executeOperation(final NetconfOperation op, final String filename) throws Exception { final Document request = XmlFileLoader.xmlFileToDocument(filename); - final Document response = op.handle(request, NetconfOperationChainedExecution.EXECUTION_TERMINATION_POINT); + return executeOperation(op, request); + } + protected static Document executeOperation(final NetconfOperation op, final Document request) throws Exception { + final Document response = op.handle(request, NetconfOperationChainedExecution.EXECUTION_TERMINATION_POINT); LOG.debug("Got response {}", response); return response; } diff --git a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfigTest.java b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfigTest.java index 14c177b806..3a34ccd24c 100644 --- a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfigTest.java +++ b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfigTest.java @@ -12,16 +12,26 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.opendaylight.yangtools.yang.test.util.YangParserTestUtils.parseYangResources; +import java.io.File; +import java.io.FileInputStream; +import java.net.MalformedURLException; +import java.net.URI; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.opendaylight.netconf.api.DocumentedException; import org.opendaylight.netconf.api.DocumentedException.ErrorSeverity; import org.opendaylight.netconf.api.DocumentedException.ErrorTag; import org.opendaylight.netconf.api.DocumentedException.ErrorType; +import org.opendaylight.netconf.api.xml.XmlUtil; import org.opendaylight.netconf.util.test.XmlFileLoader; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.w3c.dom.Document; +import org.xml.sax.SAXException; public class CopyConfigTest extends AbstractNetconfOperationTest { + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); @Override protected SchemaContext getSchemaContext() { @@ -58,7 +68,7 @@ public class CopyConfigTest extends AbstractNetconfOperationTest { public void testConfigMissing() throws Exception { try { copyConfig("messages/mapping/copyConfigs/copyConfig_no_config.xml"); - fail("Should have failed - element is missing"); + fail("Should have failed - neither nor element is present"); } catch (final DocumentedException e) { assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); assertTrue(e.getErrorTag() == ErrorTag.MISSING_ELEMENT); @@ -201,9 +211,125 @@ public class CopyConfigTest extends AbstractNetconfOperationTest { "messages/mapping/copyConfigs/copyConfig_choices_control.xml")); } + @Test + public void testConfigFromFile() throws Exception { + // Ask class loader for URI of config file and use it as in RPC: + final String template = XmlFileLoader.fileToString("messages/mapping/copyConfigs/copyConfig_from_file.xml"); + final URI uri = getClass().getClassLoader() + .getResource("messages/mapping/copyConfigs/config_file_valid.xml").toURI(); + final String copyConfig = template.replaceFirst("URL", uri.toString()); + final Document request = XmlUtil.readXmlToDocument(copyConfig); + + verifyResponse(copyConfig(request), RPC_REPLY_OK); + verifyResponse(getConfigCandidate(), XmlFileLoader.xmlFileToDocument( + "messages/mapping/copyConfigs/copyConfig_from_file_control.xml")); + } + + @Test + public void testConfigFromInvalidUrl() throws Exception { + try { + copyConfig("messages/mapping/copyConfigs/copyConfig_invalid_url.xml"); + fail("Should have failed - provided is not valid"); + } catch (final DocumentedException e) { + assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); + assertTrue(e.getErrorTag() == ErrorTag.INVALID_VALUE); + assertTrue(e.getErrorType() == ErrorType.APPLICATION); + assertTrue(e.getCause() instanceof MalformedURLException); + } + } + + @Test + public void testExternalConfigInvalid() throws Exception { + try { + // Ask class loader for URI of config file and use it as in RPC: + final String template = XmlFileLoader.fileToString("messages/mapping/copyConfigs/copyConfig_from_file.xml"); + final URI uri = getClass().getClassLoader() + .getResource("messages/mapping/copyConfigs/config_file_invalid.xml").toURI(); + final String copyConfig = template.replaceFirst("URL", uri.toString()); + final Document request = XmlUtil.readXmlToDocument(copyConfig); + copyConfig(request); + fail("Should have failed - provided config is not valid XML"); + } catch (final DocumentedException e) { + assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); + assertTrue(e.getErrorTag() == ErrorTag.OPERATION_FAILED); + assertTrue(e.getErrorType() == ErrorType.APPLICATION); + assertTrue(e.getCause() instanceof SAXException); + } + } + + @Test + public void testCopyToFile() throws Exception { + // Initialize config: + verifyResponse(copyConfig("messages/mapping/copyConfigs/copyConfig_top_modules.xml"), RPC_REPLY_OK); + verifyResponse(getConfigCandidate(), XmlFileLoader.xmlFileToDocument( + "messages/mapping/copyConfigs/copyConfig_top_modules_control.xml")); + + // Load copy-config template and replace URL with the URI of target file: + final String template = XmlFileLoader.fileToString("messages/mapping/copyConfigs/copyConfig_to_file.xml"); + final File outFile = new File(tmpDir.getRoot(),"test-copy-to-file.xml"); + final String copyConfig = template.replaceFirst("URL", outFile.toURI().toString()); + final Document request = XmlUtil.readXmlToDocument(copyConfig); + + // Invoke copy-config RPC: + verifyResponse(copyConfig(request), RPC_REPLY_OK); + + // Check if outFile was created with expected content: + verifyResponse(XmlUtil.readXmlToDocument(new FileInputStream(outFile)), + XmlFileLoader.xmlFileToDocument("messages/mapping/copyConfigs/copyConfig_to_file_control.xml")); + } + + @Test + public void testUnsupportedTargetUrlProtocol() throws Exception { + try { + copyConfig("messages/mapping/copyConfigs/copyConfig_to_unsupported_url_protocol.xml"); + fail("Should have failed - exporting config to http server is not supported"); + } catch (final DocumentedException e) { + assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); + assertTrue(e.getErrorTag() == ErrorTag.OPERATION_NOT_SUPPORTED); + assertTrue(e.getErrorType() == ErrorType.PROTOCOL); + } + } + + @Test + public void testCopyToFileFromRunning() throws Exception { + // Load copy-config template and replace URL with the URI of target file: + final String template = + XmlFileLoader.fileToString("messages/mapping/copyConfigs/copyConfig_to_file_from_running.xml"); + final File outFile = new File(tmpDir.getRoot(),"test-copy-to-file-from-running.xml"); + final String copyConfig = template.replaceFirst("URL", outFile.toURI().toString()); + final Document request = XmlUtil.readXmlToDocument(copyConfig); + + // Invoke copy-config RPC: + verifyResponse(copyConfig(request), RPC_REPLY_OK); + + // Check if outFile was created with expected content: + verifyResponse(XmlUtil.readXmlToDocument(new FileInputStream(outFile)), + XmlFileLoader.xmlFileToDocument( + "messages/mapping/copyConfigs/copyConfig_to_file_from_running_control.xml")); + + } + + @Test + public void testRemoteToRemoteOperationIsNotSupported() throws Exception { + try { + copyConfig("messages/mapping/copyConfigs/copyConfig_url_remote_to_remote.xml"); + fail("Should have failed - remote to remote operations are not supported"); + } catch (final DocumentedException e) { + assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); + assertTrue(e.getErrorTag() == ErrorTag.OPERATION_NOT_SUPPORTED); + assertTrue(e.getErrorType() == ErrorType.PROTOCOL); + } + } + private Document copyConfig(final String resource) throws Exception { final CopyConfig copyConfig = new CopyConfig(SESSION_ID_FOR_REPORTING, getCurrentSchemaContext(), getTransactionProvider()); return executeOperation(copyConfig, resource); } + + private Document copyConfig(final Document request) throws Exception { + final CopyConfig copyConfig = new CopyConfig(SESSION_ID_FOR_REPORTING, getCurrentSchemaContext(), + getTransactionProvider()); + return executeOperation(copyConfig, request); + } } diff --git a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/NetconfMDSalMappingTest.java b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/NetconfMDSalMappingTest.java index 6b97290130..43592f4b72 100644 --- a/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/NetconfMDSalMappingTest.java +++ b/netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/NetconfMDSalMappingTest.java @@ -13,6 +13,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.StringWriter; +import java.net.URI; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; @@ -105,6 +106,18 @@ public class NetconfMDSalMappingTest extends AbstractNetconfOperationTest { } } + @Test + public void testConfigMissing() throws Exception { + try { + edit("messages/mapping/editConfigs/editConfig_no_config.xml"); + fail("Should have failed - neither nor element is present"); + } catch (final DocumentedException e) { + assertTrue(e.getErrorSeverity() == ErrorSeverity.ERROR); + assertTrue(e.getErrorTag() == ErrorTag.MISSING_ELEMENT); + assertTrue(e.getErrorType() == ErrorType.PROTOCOL); + } + } + @Test public void testEditRunning() throws Exception { try { @@ -634,4 +647,17 @@ public class NetconfMDSalMappingTest extends AbstractNetconfOperationTest { verifyResponse(commit(), RPC_REPLY_OK); assertEmptyDatastore(getConfigRunning()); } + + @Test + public void testEditUsingConfigFromFile() throws Exception { + // Ask class loader for URI of config file and use it as in RPC: + final String template = XmlFileLoader.fileToString("messages/mapping/editConfigs/editConfig_from_file.xml"); + final URI uri = getClass().getClassLoader().getResource("messages/mapping/editConfigs/config_file.xml").toURI(); + final String copyConfig = template.replaceFirst("URL", uri.toString()); + final Document request = XmlUtil.readXmlToDocument(copyConfig); + + verifyResponse(edit(request), RPC_REPLY_OK); + verifyResponse(getConfigCandidate(), XmlFileLoader.xmlFileToDocument( + "messages/mapping/editConfigs/editConfig_from_file_control.xml")); + } } diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_invalid.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_invalid.xml new file mode 100644 index 0000000000..5cc446fb8c --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_invalid.xml @@ -0,0 +1 @@ +This is not a valid XML \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_valid.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_valid.xml new file mode 100644 index 0000000000..2575355971 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_valid.xml @@ -0,0 +1,20 @@ + + + + + + + module1 + + + module2 + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file.xml new file mode 100644 index 0000000000..a9446936c4 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file.xml @@ -0,0 +1,18 @@ + + + + + + + + + URL + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file_control.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file_control.xml new file mode 100644 index 0000000000..3ab7eebc29 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file_control.xml @@ -0,0 +1,22 @@ + + + + + + + + module1 + + + module2 + + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_invalid_url.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_invalid_url.xml new file mode 100644 index 0000000000..1435db1583 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_invalid_url.xml @@ -0,0 +1,18 @@ + + + + + + + + + this is not a valid url + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file.xml new file mode 100644 index 0000000000..dd98bec3ca --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file.xml @@ -0,0 +1,18 @@ + + + + + + + + + URL + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_control.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_control.xml new file mode 100644 index 0000000000..2575355971 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_control.xml @@ -0,0 +1,20 @@ + + + + + + + module1 + + + module2 + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running.xml new file mode 100644 index 0000000000..87d1993462 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running.xml @@ -0,0 +1,18 @@ + + + + + + + + + URL + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running_control.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running_control.xml new file mode 100644 index 0000000000..a902b6278f --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running_control.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_unsupported_url_protocol.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_unsupported_url_protocol.xml new file mode 100644 index 0000000000..d6aa3f7393 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_unsupported_url_protocol.xml @@ -0,0 +1,18 @@ + + + + + + + + + http://foo.bar + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_url_remote_to_remote.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_url_remote_to_remote.xml new file mode 100644 index 0000000000..8586ed0e5e --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_url_remote_to_remote.xml @@ -0,0 +1,18 @@ + + + + + + file:foo + + + file:bar + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/config_file.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/config_file.xml new file mode 100644 index 0000000000..2575355971 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/config_file.xml @@ -0,0 +1,20 @@ + + + + + + + module1 + + + module2 + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file.xml new file mode 100644 index 0000000000..63818a9af1 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file.xml @@ -0,0 +1,16 @@ + + + + + + + + URL + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file_control.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file_control.xml new file mode 100644 index 0000000000..3ab7eebc29 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file_control.xml @@ -0,0 +1,22 @@ + + + + + + + + module1 + + + module2 + + + + + \ No newline at end of file diff --git a/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_no_config.xml b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_no_config.xml new file mode 100644 index 0000000000..079fbe71e8 --- /dev/null +++ b/netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_no_config.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlNetconfConstants.java b/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlNetconfConstants.java index d34838559b..fa9ceececd 100644 --- a/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlNetconfConstants.java +++ b/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlNetconfConstants.java @@ -42,6 +42,10 @@ public final class XmlNetconfConstants { public static final String URN_IETF_PARAMS_NETCONF_CAPABILITY_EXI_1_0 = "urn:ietf:params:netconf:capability:exi:1.0"; + public static final String URN_IETF_PARAMS_NETCONF_CAPABILITY_CANDIDATE_1_0 = + "urn:ietf:params:netconf:capability:candidate:1.0"; + public static final String URN_IETF_PARAMS_NETCONF_CAPABILITY_URL_1_0 = + "urn:ietf:params:netconf:capability:url:1.0?scheme=file"; public static final String URN_IETF_PARAMS_XML_NS_YANG_IETF_NETCONF_MONITORING = "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring"; } diff --git a/netconf/netconf-impl/src/main/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringService.java b/netconf/netconf-impl/src/main/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringService.java index f627f15a7e..0cc50d8847 100644 --- a/netconf/netconf-impl/src/main/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringService.java +++ b/netconf/netconf-impl/src/main/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringService.java @@ -7,6 +7,9 @@ */ package org.opendaylight.netconf.impl.osgi; +import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_CANDIDATE_1_0; +import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_URL_1_0; + import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @@ -48,7 +51,9 @@ class NetconfCapabilityMonitoringService implements CapabilityListener, AutoClos private static final Schema.Location NETCONF_LOCATION = new Schema.Location(Schema.Location.Enumeration.NETCONF); private static final List NETCONF_LOCATIONS = ImmutableList.of(NETCONF_LOCATION); private static final BasicCapability CANDIDATE_CAPABILITY = - new BasicCapability("urn:ietf:params:netconf:capability:candidate:1.0"); + new BasicCapability(URN_IETF_PARAMS_NETCONF_CAPABILITY_CANDIDATE_1_0); + private static final BasicCapability URL_CAPABILITY = + new BasicCapability(URN_IETF_PARAMS_NETCONF_CAPABILITY_URL_1_0); private static final Function CAPABILITY_TO_URI = input -> new Uri(input.getCapabilityUri()); private final NetconfOperationServiceFactory netconfOperationProvider; @@ -195,6 +200,7 @@ class NetconfCapabilityMonitoringService implements CapabilityListener, AutoClos private static Set setupCapabilities(final Set caps) { Set capabilities = new HashSet<>(caps); capabilities.add(CANDIDATE_CAPABILITY); + capabilities.add(URL_CAPABILITY); // TODO rollback on error not supported EditConfigXmlParser:100 // [RFC6241] 8.5. Rollback-on-Error Capability // capabilities.add(new BasicCapability("urn:ietf:params:netconf:capability:rollback-on-error:1.0")); diff --git a/netconf/netconf-impl/src/test/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringServiceTest.java b/netconf/netconf-impl/src/test/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringServiceTest.java index 73ef36ef15..d7186139c4 100644 --- a/netconf/netconf-impl/src/test/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringServiceTest.java +++ b/netconf/netconf-impl/src/test/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringServiceTest.java @@ -12,6 +12,8 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_CANDIDATE_1_0; +import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_URL_1_0; import com.google.common.base.Optional; import java.net.URI; @@ -160,14 +162,15 @@ public class NetconfCapabilityMonitoringServiceTest { @Test public void testGetCapabilities() throws Exception { - Capabilities actual = monitoringService.getCapabilities(); List exp = new ArrayList<>(); for (Capability capability : capabilities) { exp.add(new Uri(capability.getCapabilityUri())); } - //candidate is added by monitoring service automatically - exp.add(0, new Uri("urn:ietf:params:netconf:capability:candidate:1.0")); + //candidate and url capabilities are added by monitoring service automatically + exp.add(new Uri(URN_IETF_PARAMS_NETCONF_CAPABILITY_CANDIDATE_1_0)); + exp.add(new Uri(URN_IETF_PARAMS_NETCONF_CAPABILITY_URL_1_0)); Capabilities expected = new CapabilitiesBuilder().setCapability(exp).build(); + Capabilities actual = monitoringService.getCapabilities(); Assert.assertEquals(new HashSet<>(expected.getCapability()), new HashSet<>(actual.getCapability())); }