NETCONF-557: Add support for URL capability 17/75317/1
authorMarek Gradzki <mgradzki@cisco.com>
Thu, 9 Aug 2018 11:45:54 +0000 (13:45 +0200)
committerMarek Gradzki <mgradzki@cisco.com>
Mon, 20 Aug 2018 07:27:02 +0000 (09:27 +0200)
This patch brings support of URL capability
in <edit-config> and <copy-config> 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 <mgradzki@cisco.com>
25 files changed:
netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractConfigOperation.java
netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractEdit.java
netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfig.java
netconf/mdsal-netconf-connector/src/main/java/org/opendaylight/netconf/mdsal/connector/ops/EditConfig.java
netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/AbstractNetconfOperationTest.java
netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/CopyConfigTest.java
netconf/mdsal-netconf-connector/src/test/java/org/opendaylight/netconf/mdsal/connector/ops/NetconfMDSalMappingTest.java
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_invalid.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/config_file_valid.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_from_file_control.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_invalid_url.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_control.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_file_from_running_control.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_to_unsupported_url_protocol.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/copyConfigs/copyConfig_url_remote_to_remote.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/config_file.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_from_file_control.xml [new file with mode: 0644]
netconf/mdsal-netconf-connector/src/test/resources/messages/mapping/editConfigs/editConfig_no_config.xml [new file with mode: 0644]
netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlNetconfConstants.java
netconf/netconf-impl/src/main/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringService.java
netconf/netconf-impl/src/test/java/org/opendaylight/netconf/impl/osgi/NetconfCapabilityMonitoringServiceTest.java

index cfeddcdb3aa85675128bd5f0f975d64a2ea83222..9a57a2ecad0d49d2570925f4d48e359d60b154e1 100644 (file)
@@ -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<XmlElement> configElement = parent.getOnlyChildElementOptionally(CONFIG_KEY);
+        if (configElement.isPresent()) {
+            return configElement.get();
+        } else {
+            final Optional<XmlElement> urlElement = parent.getOnlyChildElementOptionally(URL_KEY);
+            if (!urlElement.isPresent()) {
+                throw new DocumentedException("Invalid RPC, neither <config> not <url> 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.
+     *
+     * <p>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();
+    }
 }
index b68e17aacfcd052ee3e66d8e4e4a7db962754d85..209e4ec01aee567558a51d0a63560d804d052741 100644 (file)
@@ -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<XmlElement> childNode = parent.getOnlyChildElementOptionally(elementName);
-        if (!childNode.isPresent()) {
-            throw new DocumentedException(elementName + " element is missing",
-                ErrorType.PROTOCOL,
-                ErrorTag.MISSING_ELEMENT,
-                ErrorSeverity.ERROR);
-        }
-
-        return childNode.get();
-    }
 }
index df29fb30e152447a7d985d8f7583bb8663425887..c3e3cb423d84812517fc17767da4ee6da418daa1 100644 (file)
@@ -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<XmlElement> configElements = getConfigElement(source).getChildElements();
 
         // <copy-config>, unlike <edit-config>, 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 <config> 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<XmlElement> sourceElement = parent.getOnlyChildElementOptionally(SOURCE_KEY);
+        if (!sourceElement.isPresent()) {
+            throw new DocumentedException("<source> 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 <url> 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<NormalizedNode<?, ?>> 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 <url> 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<? extends YangInstanceIdentifier.PathArgument, ?> child : data.getValue()) {
+                nnWriter.write(child);
+            }
+            nnWriter.flush();
+            xmlWriter.flush();
+        } catch (XMLStreamException | IOException e) {
+            throw new RuntimeException(e);
+        }
+        return result.getNode();
     }
 
     @Override
index 6776b6be261ba9432db0d4445a1df6e2ad83982f..190c8bf1e94cfd639ac7177322f6f278099bf15b 100644 (file)
@@ -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();
index 019a37499478eed6d79f4b58305fe38a88aa3ad5..84629de6b25296d564199e213eb310e5f8bef126 100644 (file)
@@ -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;
     }
index 14c177b80616848cf8946c6703249f67eecf8b36..3a34ccd24c36927b87b6dff236fdab68473713ed 100644 (file)
@@ -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 - <config> element is missing");
+            fail("Should have failed - neither <config> nor <url> 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 <url> in <copy-config> 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 <url> 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 <url> in <copy-config> 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);
+    }
 }
index 6b97290130841386358798e1f33a607486b1ff3b..43592f4b72e035bb7f923d1055356be3afcc0773 100644 (file)
@@ -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 <config> nor <url> 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 <url> in <edit-config> 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 (file)
index 0000000..5cc446f
--- /dev/null
@@ -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 (file)
index 0000000..2575355
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <top xmlns="urn:opendaylight:mdsal:mapping:test">
+        <modules>
+            <module>
+                <id>module1</id>
+            </module>
+            <module>
+                <id>module2</id>
+            </module>
+        </modules>
+    </top>
+</config>
\ 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 (file)
index 0000000..a944693
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <target>
+            <candidate/>
+        </target>
+        <source>
+            <url>URL</url>
+        </source>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..3ab7eeb
--- /dev/null
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc-reply a="64" id="a" message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlnx="a:b:c:d">
+    <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <top xmlns="urn:opendaylight:mdsal:mapping:test">
+            <modules>
+                <module>
+                    <id>module1</id>
+                </module>
+                <module>
+                    <id>module2</id>
+                </module>
+            </modules>
+        </top>
+    </data>
+</rpc-reply>
\ 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 (file)
index 0000000..1435db1
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <target>
+            <candidate/>
+        </target>
+        <source>
+            <url>this is not a valid url</url>
+        </source>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..dd98bec
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <source>
+            <candidate/>
+        </source>
+        <target>
+            <url>URL</url>
+        </target>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..2575355
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <top xmlns="urn:opendaylight:mdsal:mapping:test">
+        <modules>
+            <module>
+                <id>module1</id>
+            </module>
+            <module>
+                <id>module2</id>
+            </module>
+        </modules>
+    </top>
+</config>
\ 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 (file)
index 0000000..87d1993
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <source>
+            <running/>
+        </source>
+        <target>
+            <url>URL</url>
+        </target>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..a902b62
--- /dev/null
@@ -0,0 +1,10 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+</config>
\ 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 (file)
index 0000000..d6aa3f7
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <source>
+            <candidate/>
+        </source>
+        <target>
+            <url>http://foo.bar</url>
+        </target>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..8586ed0
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <copy-config>
+        <target>
+            <url>file:foo</url>
+        </target>
+        <source>
+            <url>file:bar</url>
+        </source>
+    </copy-config>
+</rpc>
\ 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 (file)
index 0000000..2575355
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <top xmlns="urn:opendaylight:mdsal:mapping:test">
+        <modules>
+            <module>
+                <id>module1</id>
+            </module>
+            <module>
+                <id>module2</id>
+            </module>
+        </modules>
+    </top>
+</config>
\ 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 (file)
index 0000000..63818a9
--- /dev/null
@@ -0,0 +1,16 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <edit-config>
+        <target>
+            <candidate/>
+        </target>
+        <url>URL</url>
+    </edit-config>
+</rpc>
\ 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 (file)
index 0000000..3ab7eeb
--- /dev/null
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc-reply a="64" id="a" message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlnx="a:b:c:d">
+    <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <top xmlns="urn:opendaylight:mdsal:mapping:test">
+            <modules>
+                <module>
+                    <id>module1</id>
+                </module>
+                <module>
+                    <id>module2</id>
+                </module>
+            </modules>
+        </top>
+    </data>
+</rpc-reply>
\ 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 (file)
index 0000000..079fbe7
--- /dev/null
@@ -0,0 +1,15 @@
+<!--
+  ~ Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <edit-config>
+        <target>
+            <candidate/>
+        </target>
+    </edit-config>
+</rpc>
\ No newline at end of file
index d34838559b68bf0be2655dcee1259a94f2e6eecb..fa9ceececd4cd00251fd240d1dc4851b8dd46ca9 100644 (file)
@@ -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";
 }
index f627f15a7e81902889567ab61fc09a7ae7800dd7..0cc50d884799f17a721fc726d1c8a88124c8542e 100644 (file)
@@ -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<Schema.Location> 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, Uri> CAPABILITY_TO_URI = input -> new Uri(input.getCapabilityUri());
 
     private final NetconfOperationServiceFactory netconfOperationProvider;
@@ -195,6 +200,7 @@ class NetconfCapabilityMonitoringService implements CapabilityListener, AutoClos
     private static Set<Capability> setupCapabilities(final Set<Capability> caps) {
         Set<Capability> 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"));
index 73ef36ef15128e0c8a4fc7c2162442c171004430..d7186139c42d739c15f17908bd66cb795b06d732 100644 (file)
@@ -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<Uri> 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()));
     }