From 4ba390a033e1ac39c5b75cd24c6f9d415ea7fda3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jaroslav=20T=C3=B3th?= Date: Mon, 19 Oct 2020 09:28:13 +0200 Subject: [PATCH] Implement subtree filtering using fields MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - Fields are used for selection of specific subtrees under parent path - this way it is possible to limit requested data to specific entities without the necessity to send multiple get/get-config RPCs. - Look at added unit tests to understand the functionality. JIRA: NETCONF-735 Change-Id: I17cd364e11f8c8e61e5537fcff71a7ecedefdba3 Signed-off-by: Jaroslav Tóth Signed-off-by: Robert Varga --- .../netconf/util/NetconfUtil.java | 75 +++++++ .../opendaylight/netconf/util/PathNode.java | 116 +++++++++++ .../netconf/util/StreamingContext.java | 58 +++++- .../get-config-fields-request.xml | 13 ++ .../get-config-with-multiple-subtrees.xml | 18 ++ .../netconfMessages/get-fields-request.xml | 10 + .../get-with-multiple-root-subtrees.xml | 8 + .../get-with-multiple-subtrees.xml | 15 ++ .../connect/netconf/util/FieldsFilter.java | 64 ++++++ .../connect/netconf/util/NetconfBaseOps.java | 127 +++++++++++- .../util/NetconfMessageTransformUtil.java | 62 ++++-- .../util/NetconfRpcStructureTransformer.java | 7 + .../netconf/util/RpcStructureTransformer.java | 15 +- .../SchemalessRpcStructureTransformer.java | 7 + .../sal/NetconfDeviceDataBrokerTest.java | 44 +++- .../sal/tx/FieldsAwareReadOnlyTxTest.java | 60 ++++++ .../sal/tx/FieldsAwareReadWriteTxTest.java | 37 ++++ .../netconf/sal/tx/ReadWriteTxTest.java | 6 +- .../NetconfMessageTransformerTest.java | 193 +++++++++++++++++- .../netconf/util/NetconfBaseOpsTest.java | 124 +++++++++-- .../test/resources/schemas/test-module.yang | 15 ++ 21 files changed, 1021 insertions(+), 53 deletions(-) create mode 100644 netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml create mode 100644 netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java create mode 100644 netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java create mode 100644 netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java index 5184d675e2..eb7fb247e3 100644 --- a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java +++ b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java @@ -13,6 +13,8 @@ import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.net.URISyntaxException; import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; @@ -186,6 +188,16 @@ public final class NetconfUtil { } } + /** + * Writing subtree filter specified by {@link YangInstanceIdentifier} into {@link DOMResult}. + * + * @param query path to the root node + * @param result DOM result holder + * @param schemaPath schema path of the parent node + * @param context mountpoint schema context + * @throws IOException failed to write filter into {@link NormalizedNodeStreamWriter} + * @throws XMLStreamException failed to serialize filter into XML document + */ public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result, final SchemaPath schemaPath, final EffectiveModelContext context) throws IOException, XMLStreamException { if (query.isEmpty()) { @@ -207,6 +219,69 @@ public final class NetconfUtil { } } + /** + * Writing subtree filter specified by parent {@link YangInstanceIdentifier} and specific fields + * into {@link DOMResult}. Field paths are relative to parent query path. + * + * @param query path to the root node + * @param result DOM result holder + * @param schemaPath schema path of the parent node + * @param context mountpoint schema context + * @param fields list of specific fields for which the filter should be created + * @throws IOException failed to write filter into {@link NormalizedNodeStreamWriter} + * @throws XMLStreamException failed to serialize filter into XML document + * @throws NullPointerException if any argument is null + */ + public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result, + final SchemaPath schemaPath, final EffectiveModelContext context, + final List fields) throws IOException, XMLStreamException { + if (query.isEmpty() || fields.isEmpty()) { + // No query at all + return; + } + final List aggregatedFields = aggregateFields(fields); + final PathNode rootNode = constructPathArgumentTree(query, aggregatedFields); + + final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result); + try { + try (NormalizedNodeStreamWriter writer = XMLStreamNormalizedNodeStreamWriter.create( + xmlWriter, context, schemaPath)) { + final PathArgument first = rootNode.element(); + StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()) + .streamToWriter(writer, first, rootNode); + } + } finally { + xmlWriter.close(); + } + } + + private static List aggregateFields(final List fields) { + return fields.stream() + .filter(field -> fields.stream() + .filter(fieldYiid -> !field.equals(fieldYiid)) + .noneMatch(fieldYiid -> fieldYiid.contains(field))) + .collect(Collectors.toList()); + } + + private static PathNode constructPathArgumentTree(final YangInstanceIdentifier query, + final List fields) { + final Iterator queryIterator = query.getPathArguments().iterator(); + final PathNode rootTreeNode = new PathNode(queryIterator.next()); + + PathNode queryTreeNode = rootTreeNode; + while (queryIterator.hasNext()) { + queryTreeNode = queryTreeNode.ensureChild(queryIterator.next()); + } + + for (final YangInstanceIdentifier field : fields) { + PathNode actualFieldTreeNode = queryTreeNode; + for (final PathArgument fieldPathArg : field.getPathArguments()) { + actualFieldTreeNode = actualFieldTreeNode.ensureChild(fieldPathArg); + } + } + return rootTreeNode; + } + public static NormalizedNodeResult transformDOMSourceToNormalizedNode(final MountPointContext mountContext, final DOMSource value) throws XMLStreamException, URISyntaxException, IOException, SAXException { final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java new file mode 100644 index 0000000000..d794bf3088 --- /dev/null +++ b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java @@ -0,0 +1,116 @@ +/* + * Copyright © 2020 FRINX s.r.o. All rights reserved. + * Copyright © 2021 PANTHEON.tech, s.r.o. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.util; + +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.Maps; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.opendaylight.yangtools.concepts.Mutable; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; + +/** + * Representation of the tree node with possible multiple child nodes. Child nodes are identified uniquely by path + * argument. + */ +@NonNullByDefault +final class PathNode implements Mutable { + private final PathArgument argument; + + private Map children; + + private PathNode(final PathArgument argument, final LinkedHashMap children) { + this.argument = requireNonNull(argument); + this.children = requireNonNull(children); + } + + /** + * Creation of tree node using a path argument. + * + * @param argument Path argument + */ + PathNode(final PathArgument argument) { + this.argument = requireNonNull(argument); + this.children = Map.of(); + } + + /** + * Get path argument. + * + * @return path argument + */ + PathArgument element() { + return argument; + } + + /** + * Return current child nodes. + * + * @return Current child nodes + */ + Collection children() { + return children.values(); + } + + /** + * Return {@code true} if this node has no child nodes. + * + * @return {@code true} if this node has no child nodes + */ + boolean isEmpty() { + return children.isEmpty(); + } + + /** + * Create a copy of this node with specified immediate child nodes appended. + * + * @param childArguments Child arguments + * @return A copy of this {@link PathNode} + * @throws NullPointerException if {@code childArguments} is, or contains, {@code null} + */ + PathNode copyWith(final Collection childArguments) { + final LinkedHashMap copy = children instanceof LinkedHashMap + ? new LinkedHashMap<>(children) : Maps.newLinkedHashMapWithExpectedSize(childArguments.size()); + for (PathArgument childArgument : childArguments) { + ensureChild(copy, childArgument); + } + return new PathNode(argument, copy); + } + + /** + * Ensure a node for specified argument exists. + * + * @param childArgument Child argument + * @return A child {@link PathNode} + * @throws NullPointerException if {@code childArgument} is null + */ + PathNode ensureChild(final PathArgument childArgument) { + return ensureChild(mutableChildren(), childArgument); + } + + private static PathNode ensureChild(final LinkedHashMap children, + final PathArgument childArgument) { + return children.computeIfAbsent(requireNonNull(childArgument), PathNode::new); + } + + private LinkedHashMap mutableChildren() { + final Map local = children; + if (local instanceof LinkedHashMap) { + return (LinkedHashMap) local; + } + + // TODO: LinkedHashMap is rather heavy, do we need to retain insertion order? + final LinkedHashMap ret = new LinkedHashMap<>(4); + children = ret; + return ret; + } +} diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java index 52ce54fd6e..5cf6a7e732 100644 --- a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java +++ b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java @@ -13,6 +13,7 @@ import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedN import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -27,6 +28,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdent import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +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.util.DataSchemaContextNode; import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode; @@ -91,9 +93,29 @@ abstract class StreamingContext implements Identifiable< abstract StreamingContext getChild(PathArgument child); + /** + * Writing node structure that is described by series of {@link PathArgument} + * into {@link NormalizedNodeStreamWriter}. + * + * @param writer output {@link NormalizedNode} writer + * @param first the first {@link PathArgument} + * @param others iterator that points to next path arguments + * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter} + */ abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, Iterator others) throws IOException; + /** + * Writing node structure that is described by provided {@link PathNode} into {@link NormalizedNodeStreamWriter}. + * + * @param writer output {@link NormalizedNode} writer + * @param first the first {@link PathArgument} + * @param tree subtree of path arguments that starts with the first path argument + * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter} + */ + abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, PathNode tree) + throws IOException; + abstract boolean isMixin(); private static Optional findChildSchemaNode(final DataNodeContainer parent, final QName child) { @@ -135,13 +157,7 @@ abstract class StreamingContext implements Identifiable< @Override final void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first, final Iterator others) throws IOException { - if (!isMixin()) { - final QName type = getIdentifier().getNodeType(); - if (type != null) { - final QName firstType = first.getNodeType(); - checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType); - } - } + verifyActualPathArgument(first); emitElementStart(writer, first); if (others.hasNext()) { @@ -152,6 +168,28 @@ abstract class StreamingContext implements Identifiable< writer.endNode(); } + @Override + void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first, + final PathNode subtree) throws IOException { + verifyActualPathArgument(first); + + emitElementStart(writer, first); + for (final PathNode node : subtree.children()) { + final PathArgument childPath = node.element(); + final StreamingContext childOp = getChildOperation(childPath); + childOp.streamToWriter(writer, childPath, node); + } + writer.endNode(); + } + + private void verifyActualPathArgument(final PathArgument first) { + if (!isMixin()) { + final QName type = getIdentifier().getNodeType(); + final QName firstType = first.getNodeType(); + checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType); + } + } + abstract void emitElementStart(NormalizedNodeStreamWriter writer, PathArgument arg) throws IOException; @SuppressWarnings("checkstyle:illegalCatch") @@ -231,6 +269,12 @@ abstract class StreamingContext implements Identifiable< final boolean isMixin() { return false; } + + @Override + void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first, + final PathNode tree) throws IOException { + streamToWriter(writer, first, Collections.emptyIterator()); + } } private static final class AnyXml extends AbstractSimple { diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml new file mode 100644 index 0000000000..6ad30b273b --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml new file mode 100644 index 0000000000..fee24ee773 --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml new file mode 100644 index 0000000000..7d42854bdd --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml new file mode 100644 index 0000000000..92fe72863d --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml new file mode 100644 index 0000000000..334095abcf --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java new file mode 100644 index 0000000000..cdbf857b3c --- /dev/null +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2020 FRINX s.r.o. All rights reserved. + * Copyright © 2021 PANTHEON.tech, s.r.o. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.connect.netconf.util; + +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.yangtools.concepts.Immutable; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; + +/** + * Definition of the subtree filter with single parent path and possibly multiple field sub-paths that are used + * for reading/selection of specific entities. + */ +@Beta +public final class FieldsFilter implements Immutable { + private final @NonNull YangInstanceIdentifier path; + private final @NonNull List fields; + + private FieldsFilter(final YangInstanceIdentifier path, final List fields) { + this.path = requireNonNull(path); + this.fields = ImmutableList.copyOf(fields); + } + + /** + * Create a {@link FieldsFilter} using parent path and fields. Field paths are relative to parent path. + * + * @param path parent query path + * @param fields list of specific selection fields + * @return instance of {@link FieldsFilter} + * @throws NullPointerException if any argument is null, or if {@code fields} contains a null element + */ + public static @NonNull FieldsFilter of(final YangInstanceIdentifier path, + final List fields) { + return new FieldsFilter(path, fields); + } + + /** + * Get parent path. + * + * @return instance of {@link YangInstanceIdentifier} + */ + public @NonNull YangInstanceIdentifier path() { + return path; + } + + /** + * Get list of paths that narrows the filter for specific fields. Field paths are relative to parent path. + * + * @return {@link List} of field paths. + */ + public @NonNull List fields() { + return fields; + } +} \ No newline at end of file diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOps.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOps.java index d3bbb95797..8df170e8fc 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOps.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOps.java @@ -11,6 +11,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.COMMIT_RPC_CONTENT; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.EDIT_CONTENT_NODEID; +import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.GET_RPC_CONTENT; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_CANDIDATE_QNAME; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_COMMIT_QNAME; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_COPY_CONFIG_NODEID; @@ -37,13 +38,18 @@ import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTr import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toFilterStructure; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toId; +import com.google.common.collect.Iterables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.mdsal.dom.api.DOMRpcResult; import org.opendaylight.mdsal.dom.api.DOMRpcService; @@ -208,27 +214,93 @@ public final class NetconfBaseOps { return future; } + private ListenableFuture getConfig(final FutureCallback callback, + final QName datastore, final Optional filterPath, + final List fields) { + requireNonNull(callback); + requireNonNull(datastore); + + final NormalizedNode rpcInput; + if (isFilterPresent(filterPath)) { + final DataContainerChild node = transformer.toFilterStructure( + Collections.singletonList(FieldsFilter.of(filterPath.get(), fields))); + rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID, getSourceNode(datastore), node); + } else if (containsEmptyPath(fields)) { + rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID, getSourceNode(datastore)); + } else { + final DataContainerChild subtreeFilter = getSubtreeFilterFromRootFields(fields); + rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID, + getSourceNode(datastore), subtreeFilter); + } + final ListenableFuture response = rpc.invokeRpc(NETCONF_GET_CONFIG_QNAME, rpcInput); + Futures.addCallback(response, callback, MoreExecutors.directExecutor()); + return response; + } + + /** + * Calling GET-CONFIG RPC with subtree filter that is specified by {@link YangInstanceIdentifier}. + * + * @param callback RPC response callback + * @param filterPath path to requested data + * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance + */ public ListenableFuture>> getConfigRunningData( final FutureCallback callback, final Optional filterPath) { return extractData(filterPath, getConfigRunning(callback, filterPath)); } + /** + * Calling GET-CONFIG RPC with subtree filter tha tis specified by parent {@link YangInstanceIdentifier} and list + * of specific fields that caller would like to read. Field paths are relative to parent path. + * + * @param callback RPC response callback + * @param filterPath parent path to requested data + * @param fields paths to specific fields that are selected under parent path + * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance + */ public ListenableFuture>> getConfigRunningData( final FutureCallback callback, final Optional filterPath, final List fields) { - // FIXME: implement this method - throw new UnsupportedOperationException(); + if (fields.isEmpty()) { + // RFC doesn't allow to build subtree filter that would expect just empty element in response + return Futures.immediateFailedFuture(new IllegalArgumentException( + "Failed to build NETCONF GET-CONFIG RPC: provided list of fields is empty; filter path: " + + filterPath)); + } + final ListenableFuture response = getConfigRunning(callback, filterPath, fields); + return extractData(filterPath, response); } + /** + * Calling GET RPC with subtree filter that is specified by {@link YangInstanceIdentifier}. + * + * @param callback RPC response callback + * @param filterPath path to requested data + * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance + */ public ListenableFuture>> getData(final FutureCallback callback, final Optional filterPath) { return extractData(filterPath, get(callback, filterPath)); } + /** + * Calling GET RPC with subtree filter tha tis specified by parent {@link YangInstanceIdentifier} and list + * of specific fields that caller would like to read. Field paths are relative to parent path. + * + * @param callback RPC response callback + * @param filterPath parent path to requested data + * @param fields paths to specific fields that are selected under parent path + * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance + */ public ListenableFuture>> getData(final FutureCallback callback, - final Optional path, final List fields) { - // FIXME: implement this method - throw new UnsupportedOperationException(); + final Optional filterPath, final List fields) { + if (fields.isEmpty()) { + // RFC doesn't allow to build subtree filter that would expect just empty element in response + return Futures.immediateFailedFuture(new IllegalArgumentException( + "Failed to build NETCONF GET RPC: provided list of fields is empty; filter path: " + filterPath)); + } + final ListenableFuture response = get(callback, filterPath, fields); + return extractData(filterPath, response); } private ListenableFuture>> extractData( @@ -247,6 +319,11 @@ public final class NetconfBaseOps { return getConfig(callback, NETCONF_RUNNING_QNAME, filterPath); } + private ListenableFuture getConfigRunning(final FutureCallback callback, + final Optional filterPath, final List fields) { + return getConfig(callback, NETCONF_RUNNING_QNAME, filterPath, fields); + } + public ListenableFuture getConfigCandidate(final FutureCallback callback, final Optional filterPath) { return getConfig(callback, NETCONF_CANDIDATE_QNAME, filterPath); @@ -265,6 +342,46 @@ public final class NetconfBaseOps { return future; } + private ListenableFuture get(final FutureCallback callback, + final Optional filterPath, final List fields) { + requireNonNull(callback); + + final NormalizedNode rpcInput; + if (isFilterPresent(filterPath)) { + rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_NODEID, transformer.toFilterStructure( + Collections.singletonList(FieldsFilter.of(filterPath.get(), fields)))); + } else if (containsEmptyPath(fields)) { + rpcInput = GET_RPC_CONTENT; + } else { + final DataContainerChild subtreeFilter = getSubtreeFilterFromRootFields(fields); + rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_NODEID, subtreeFilter); + } + final ListenableFuture response = rpc.invokeRpc(NETCONF_GET_QNAME, rpcInput); + Futures.addCallback(response, callback, MoreExecutors.directExecutor()); + return response; + } + + private static boolean containsEmptyPath(final List fields) { + return fields.stream().anyMatch(YangInstanceIdentifier::isEmpty); + } + + private DataContainerChild getSubtreeFilterFromRootFields(final List fields) { + final Map> getConfigEntries = fields.stream() + .map(fieldPath -> { + final YangInstanceIdentifier rootPath = YangInstanceIdentifier.create( + Iterables.limit(fieldPath.getPathArguments(), 1)); + final YangInstanceIdentifier updatedFieldPath = YangInstanceIdentifier.create( + Iterables.skip(fieldPath.getPathArguments(), 1)); + return new SimpleEntry<>(rootPath, updatedFieldPath); + }) + .collect(Collectors.groupingBy(SimpleEntry::getKey, + Collectors.mapping(SimpleEntry::getValue, Collectors.toList()))); + final List fieldsFilters = getConfigEntries.keySet().stream() + .map(rootPath -> FieldsFilter.of(rootPath, getConfigEntries.get(rootPath))) + .collect(Collectors.toList()); + return transformer.toFilterStructure(fieldsFilters); + } + private static boolean isFilterPresent(final Optional filterPath) { return filterPath.isPresent() && !filterPath.get().isEmpty(); } diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java index 2f48d5403d..7dcf80e4ad 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java @@ -226,29 +226,65 @@ public final class NetconfMessageTransformUtil { public static final @NonNull DataContainerChild EMPTY_FILTER; static { - final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(), - Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString())); - element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), - NETCONF_TYPE_QNAME.getLocalName(), SUBTREE); - - EMPTY_FILTER = Builders.anyXmlBuilder().withNodeIdentifier(NETCONF_FILTER_NODEID) - .withValue(new DOMSource(element)).build(); + final Element element = getNetconfFilterElement(); + EMPTY_FILTER = buildFilterStructure(element); } + /** + * Creation of the subtree filter structure using {@link YangInstanceIdentifier} path. + * + * @param identifier parent path / query + * @param ctx mountpoint schema context + * @return created DOM structure with subtree filter + */ public static DataContainerChild toFilterStructure(final YangInstanceIdentifier identifier, final EffectiveModelContext ctx) { - final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(), - Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString())); - element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(), - SUBTREE); - + final Element element = getNetconfFilterElement(); try { NetconfUtil.writeFilter(identifier, new DOMResult(element), SchemaPath.ROOT, ctx); } catch (IOException | XMLStreamException e) { throw new IllegalStateException("Unable to serialize filter element for path " + identifier, e); } + return buildFilterStructure(element); + } + + /** + * Creation of the subtree filter structure using list of parent {@link YangInstanceIdentifier} + * and specific selection fields. Field paths are relative to parent query path. + * + * @param fieldsFilters list of: parent path and selection fields + * @param ctx mountpoint schema context + * @return created DOM structure with subtree filter + */ + public static DataContainerChild toFilterStructure(final List fieldsFilters, + final EffectiveModelContext ctx) { + Preconditions.checkState(!fieldsFilters.isEmpty(), "An empty list of subtree filters is not allowed"); + final Element element = getNetconfFilterElement(); + + for (final FieldsFilter filter : fieldsFilters) { + try { + NetconfUtil.writeFilter(filter.path(), new DOMResult(element), SchemaPath.ROOT, ctx, filter.fields()); + } catch (IOException | XMLStreamException e) { + throw new IllegalStateException(String.format( + "Unable to serialize filter element for path %s with fields: %s", + filter.path(), filter.fields()), e); + } + } + return buildFilterStructure(element); + } + + private static Element getNetconfFilterElement() { + final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(), + Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString())); + element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(), + SUBTREE); + return element; + } - return Builders.anyXmlBuilder().withNodeIdentifier(NETCONF_FILTER_NODEID).withValue(new DOMSource(element)) + private static DataContainerChild buildFilterStructure(final Element element) { + return Builders.anyXmlBuilder() + .withNodeIdentifier(NETCONF_FILTER_NODEID) + .withValue(new DOMSource(element)) .build(); } diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfRpcStructureTransformer.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfRpcStructureTransformer.java index 57e0a54714..d7d39039fa 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfRpcStructureTransformer.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfRpcStructureTransformer.java @@ -9,6 +9,7 @@ package org.opendaylight.netconf.sal.connect.netconf.util; import java.io.IOException; import java.net.URISyntaxException; +import java.util.List; import java.util.Optional; import javax.xml.stream.XMLStreamException; import org.opendaylight.netconf.api.ModifyAction; @@ -69,4 +70,10 @@ class NetconfRpcStructureTransformer implements RpcStructureTransformer { // FIXME: propagate MountPointContext return NetconfMessageTransformUtil.toFilterStructure(path, mountContext.getEffectiveModelContext()); } + + @Override + public DataContainerChild toFilterStructure(final List fieldsFilters) { + // FIXME: propagate MountPointContext + return NetconfMessageTransformUtil.toFilterStructure(fieldsFilters, mountContext.getEffectiveModelContext()); + } } diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/RpcStructureTransformer.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/RpcStructureTransformer.java index 17959667b3..26e6159faa 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/RpcStructureTransformer.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/RpcStructureTransformer.java @@ -7,6 +7,7 @@ */ package org.opendaylight.netconf.sal.connect.netconf.util; +import java.util.List; import java.util.Optional; import org.opendaylight.netconf.api.ModifyAction; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; @@ -22,6 +23,7 @@ interface RpcStructureTransformer { /** * Transforms data and path to the config element structure. It means creating of parent xml structure * specified by path and appending data to the structure. Operation is set as attribute on data element. + * * @param data data * @param dataPath path, where data will be written * @param operation operation @@ -32,14 +34,25 @@ interface RpcStructureTransformer { /** * Transforms path to filter structure. + * * @param path path * @return filter structure */ DataContainerChild toFilterStructure(YangInstanceIdentifier path); + /** + * Transforms list of fields filters to filter structure. + * Field paths are relative to parent query path. + * + * @param fieldsFilters list of: parent path and selection fields + * @return filter structure + */ + DataContainerChild toFilterStructure(List fieldsFilters); + /** * Selects data specified by path from data node. Data must be product of get-config rpc with filter created by - * {@link #toFilterStructure(YangInstanceIdentifier)} with same path. + * {@link #toFilterStructure(YangInstanceIdentifier)} or {@link #toFilterStructure(List)} )} with same path. + * * @param data data * @param path path to select * @return selected data diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/SchemalessRpcStructureTransformer.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/SchemalessRpcStructureTransformer.java index 936fdf2f86..372b356bfb 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/SchemalessRpcStructureTransformer.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/SchemalessRpcStructureTransformer.java @@ -137,6 +137,13 @@ class SchemalessRpcStructureTransformer implements RpcStructureTransformer { .build(); } + @Override + public DataContainerChild toFilterStructure(final List fieldsFilters) { + // todo: implementation of this feature + throw new UnsupportedOperationException( + "Creation of filter structure using fields for schemaless mountpoint is not supported"); + } + private static void checkDataValidForPath(final YangInstanceIdentifier dataPath, final Element dataNode) { //if datapath is empty, consider dataNode to be a root node if (dataPath.isEmpty()) { diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfDeviceDataBrokerTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfDeviceDataBrokerTest.java index 3e21a60279..b4480433e5 100644 --- a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfDeviceDataBrokerTest.java +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfDeviceDataBrokerTest.java @@ -7,28 +7,37 @@ */ package org.opendaylight.netconf.sal.connect.netconf.sal; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; +import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME; import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_QNAME; +import com.google.common.collect.ClassToInstanceMap; import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Collections; import org.junit.AfterClass; -import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.opendaylight.mdsal.binding.runtime.spi.BindingRuntimeHelpers; import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.dom.api.DOMDataBrokerExtension; import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction; import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction; import org.opendaylight.mdsal.dom.api.DOMRpcService; import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult; +import org.opendaylight.netconf.dom.api.tx.NetconfDOMDataBrokerFieldsExtension; +import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadTransaction; +import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadWriteTransaction; import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences; import org.opendaylight.netconf.sal.connect.netconf.sal.tx.AbstractWriteTx; import org.opendaylight.netconf.sal.connect.netconf.sal.tx.WriteCandidateRunningTx; @@ -41,6 +50,7 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.mon import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext; import org.opendaylight.yangtools.util.concurrent.FluentFutures; import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; @@ -70,40 +80,60 @@ public class NetconfDeviceDataBrokerTest { } @Test - public void testNewReadOnlyTransaction() throws Exception { + public void testNewReadOnlyTransaction() { final DOMDataTreeReadTransaction tx = dataBroker.newReadOnlyTransaction(); tx.read(LogicalDatastoreType.OPERATIONAL, null); verify(rpcService).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class)); } @Test - public void testNewReadWriteTransaction() throws Exception { + public void testNewReadWriteTransaction() { final DOMDataTreeReadWriteTransaction tx = dataBroker.newReadWriteTransaction(); tx.read(LogicalDatastoreType.OPERATIONAL, null); verify(rpcService).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class)); } @Test - public void testWritableRunningCandidateWriteTransaction() throws Exception { + public void testWritableRunningCandidateWriteTransaction() { testWriteTransaction( WriteCandidateRunningTx.class, NetconfMessageTransformUtil.NETCONF_RUNNING_WRITABLE_URI.toString(), NetconfMessageTransformUtil.NETCONF_CANDIDATE_URI.toString()); } @Test - public void testCandidateWriteTransaction() throws Exception { + public void testCandidateWriteTransaction() { testWriteTransaction(WriteCandidateTx.class, NetconfMessageTransformUtil.NETCONF_CANDIDATE_URI.toString()); } @Test - public void testRunningWriteTransaction() throws Exception { + public void testRunningWriteTransaction() { testWriteTransaction(WriteRunningTx.class, NetconfMessageTransformUtil.NETCONF_RUNNING_WRITABLE_URI.toString()); } + @Test + public void testDOMFieldsExtensions() { + final ClassToInstanceMap extensions = dataBroker.getExtensions(); + final NetconfDOMDataBrokerFieldsExtension fieldsExtension = extensions.getInstance( + NetconfDOMDataBrokerFieldsExtension.class); + assertNotNull(fieldsExtension); + + // read-only transaction + final NetconfDOMFieldsReadTransaction roTx = fieldsExtension.newReadOnlyTransaction(); + roTx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), + Collections.singletonList(YangInstanceIdentifier.empty())); + verify(rpcService).invokeRpc(Mockito.eq(NETCONF_GET_CONFIG_QNAME), any(ContainerNode.class)); + + // read-write transaction + final NetconfDOMFieldsReadWriteTransaction rwTx = fieldsExtension.newReadWriteTransaction(); + rwTx.read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.empty(), + Collections.singletonList(YangInstanceIdentifier.empty())); + verify(rpcService).invokeRpc(Mockito.eq(NETCONF_GET_QNAME), any(ContainerNode.class)); + } + private void testWriteTransaction(final Class transaction, final String... capabilities) { NetconfDeviceDataBroker db = getDataBroker(capabilities); - Assert.assertEquals(transaction, db.newWriteOnlyTransaction().getClass()); + assertEquals(transaction, db.newWriteOnlyTransaction().getClass()); } private NetconfDeviceDataBroker getDataBroker(final String... caps) { diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java new file mode 100644 index 0000000000..dc07c9483e --- /dev/null +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2020 FRINX s.r.o. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.connect.netconf.sal.tx; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME; +import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_QNAME; + +import java.net.InetSocketAddress; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.dom.api.DOMRpcService; +import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult; +import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps; +import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId; +import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext; +import org.opendaylight.yangtools.util.concurrent.FluentFutures; +import org.opendaylight.yangtools.yang.common.QName; +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.NormalizedNode; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class FieldsAwareReadOnlyTxTest { + @Mock + private DOMRpcService rpc; + @Mock + private NormalizedNode mockedNode; + + @Test + public void testReadWithFields() { + doReturn(FluentFutures.immediateFluentFuture(new DefaultDOMRpcResult(mockedNode))).when(rpc) + .invokeRpc(any(QName.class), any(ContainerNode.class)); + + final NetconfBaseOps netconfOps = new NetconfBaseOps(rpc, mock(MountPointContext.class)); + try (FieldsAwareReadOnlyTx readOnlyTx = new FieldsAwareReadOnlyTx(netconfOps, + new RemoteDeviceId("a", new InetSocketAddress("localhost", 196)))) { + + readOnlyTx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), + List.of(YangInstanceIdentifier.empty())); + verify(rpc).invokeRpc(eq(NETCONF_GET_CONFIG_QNAME), any(ContainerNode.class)); + + readOnlyTx.read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.empty()); + verify(rpc).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class)); + } + } +} \ No newline at end of file diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java new file mode 100644 index 0000000000..b7fedd99e8 --- /dev/null +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2020 FRINX s.r.o. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.connect.netconf.sal.tx; + +import static org.mockito.Mockito.verify; + +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction; +import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadTransaction; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class FieldsAwareReadWriteTxTest { + @Mock + private NetconfDOMFieldsReadTransaction delegateReadTx; + @Mock + private DOMDataTreeWriteTransaction delegateWriteTx; + + @Test + public void testReadWithFields() { + final FieldsAwareReadWriteTx tx = new FieldsAwareReadWriteTx(delegateReadTx, delegateWriteTx); + tx.read(LogicalDatastoreType.CONFIGURATION, TxTestUtils.getContainerId(), + List.of(YangInstanceIdentifier.empty())); + verify(delegateReadTx).read(LogicalDatastoreType.CONFIGURATION, TxTestUtils.getContainerId(), + List.of(YangInstanceIdentifier.empty())); + } +} \ No newline at end of file diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/ReadWriteTxTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/ReadWriteTxTest.java index 8976d82002..e5e1039360 100644 --- a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/ReadWriteTxTest.java +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/ReadWriteTxTest.java @@ -32,11 +32,11 @@ public class ReadWriteTxTest { private DOMDataTreeReadTransaction delegateReadTx; @Mock private DOMDataTreeWriteTransaction delegateWriteTx; - private ReadWriteTx tx; + private ReadWriteTx tx; @Before public void setUp() throws Exception { - tx = new ReadWriteTx(delegateReadTx, delegateWriteTx); + tx = new ReadWriteTx<>(delegateReadTx, delegateWriteTx); } @Test @@ -78,7 +78,7 @@ public class ReadWriteTxTest { @Test public void getIdentifier() throws Exception { - final ReadWriteTx tx2 = new ReadWriteTx(null, null); + final ReadWriteTx tx2 = new ReadWriteTx<>(null, null); assertNotEquals(tx.getIdentifier(), tx2.getIdentifier()); } } diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java index 1a57aeeffd..2bfea94396 100644 --- a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java @@ -56,14 +56,21 @@ import org.opendaylight.netconf.api.NetconfMessage; import org.opendaylight.netconf.api.xml.XmlUtil; import org.opendaylight.netconf.sal.connect.netconf.AbstractBaseSchemasTest; import org.opendaylight.netconf.sal.connect.netconf.schema.NetconfRemoteSchemaYangSourceProvider; +import org.opendaylight.netconf.sal.connect.netconf.util.FieldsFilter; import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps; import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil; import org.opendaylight.netconf.util.NetconfUtil; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.IetfNetconfService; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.NetconfState; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Capabilities; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Datastores; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Schemas; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Sessions; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Statistics; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.datastores.Datastore; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.datastores.datastore.Locks; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.sessions.Session; import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; @@ -812,6 +819,190 @@ public class NetconfMessageTransformerTest extends AbstractBaseSchemasTest { assertTrue(actionResult.getOutput().isEmpty()); } + @Test + public void getTwoNonOverlappingFieldsTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME)); + final YangInstanceIdentifier netconfStartTimeField = YangInstanceIdentifier.create(toId(Statistics.QNAME), + toId(QName.create(Statistics.QNAME, "netconf-start-time"))); + final YangInstanceIdentifier datastoresField = YangInstanceIdentifier.create(toId(Datastores.QNAME)); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(netconfStartTimeField, datastoresField))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + + @Test + public void getOverlappingFieldsTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME)); + final YangInstanceIdentifier capabilitiesField = YangInstanceIdentifier.create(toId(Capabilities.QNAME)); + final YangInstanceIdentifier capabilityField = YangInstanceIdentifier.create(toId(Capabilities.QNAME), + toId(QName.create(Capabilities.QNAME, "capability").intern())); + final YangInstanceIdentifier datastoreField = YangInstanceIdentifier.create(toId(Datastores.QNAME)); + final YangInstanceIdentifier locksFields = YangInstanceIdentifier.create(toId(Datastores.QNAME), + toId(Datastore.QNAME), NodeIdentifierWithPredicates.of(Datastore.QNAME), toId(Locks.QNAME)); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(capabilitiesField, capabilityField, datastoreField, locksFields))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + + @Test + public void getOverlappingFieldsWithEmptyFieldTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME)); + final YangInstanceIdentifier capabilitiesField = YangInstanceIdentifier.create(toId(Capabilities.QNAME)); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(capabilitiesField, YangInstanceIdentifier.empty()))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + + @Test + public void getSpecificFieldsUnderListTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME), + toId(Schemas.QNAME), toId(Schema.QNAME), NodeIdentifierWithPredicates.of(Schema.QNAME)); + final YangInstanceIdentifier versionField = YangInstanceIdentifier.create( + toId(QName.create(Schema.QNAME, "version").intern())); + final YangInstanceIdentifier identifierField = YangInstanceIdentifier.create( + toId(QName.create(Schema.QNAME, "identifier").intern())); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(versionField, identifierField))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + + @Test + public void getWholeListsUsingFieldsTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME)); + final YangInstanceIdentifier datastoreListField = YangInstanceIdentifier.create(toId(Datastores.QNAME), + toId(Datastore.QNAME), NodeIdentifierWithPredicates.of(Datastore.QNAME)); + final YangInstanceIdentifier sessionListField = YangInstanceIdentifier.create(toId(Sessions.QNAME), + toId(Session.QNAME), NodeIdentifierWithPredicates.of(Session.QNAME)); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(datastoreListField, sessionListField))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + + @Test + public void getSpecificListEntriesWithSpecificFieldsTest() throws IOException, SAXException { + // preparation of the fields + final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME), + toId(Sessions.QNAME)); + final QName sessionId = QName.create(Session.QNAME, "session-id").intern(); + final YangInstanceIdentifier session1Field = YangInstanceIdentifier.create(toId(Session.QNAME), + NodeIdentifierWithPredicates.of(Session.QNAME, sessionId, 1)); + final YangInstanceIdentifier session2TransportField = YangInstanceIdentifier.create(toId(Session.QNAME), + NodeIdentifierWithPredicates.of(Session.QNAME, sessionId, 2), + toId(QName.create(Session.QNAME, "transport").intern())); + + // building filter structure and NETCONF message + final DataContainerChild filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of( + parentYiid, List.of(session1Field, session2TransportField))), SCHEMA); + final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME, + NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure)); + + // testing + assertSimilarXml(netconfMessage, "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "2\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""); + } + private static void checkAction(final QName actionQname, final Node action , final String inputLocalName, final String inputNodeName, final String inputValue) { checkNode(action, actionQname.getLocalName(), actionQname.getLocalName(), @@ -833,8 +1024,8 @@ public class NetconfMessageTransformerTest extends AbstractBaseSchemasTest { Node messageId = baseRpc.getAttributes().getNamedItem("message-id"); assertNotNull(messageId); assertTrue(messageId.getNodeValue().contains("m-")); - Node childAction = baseRpc.getFirstChild(); + checkNode(childAction, "action", "action", NetconfMessageTransformUtil.NETCONF_ACTION_NAMESPACE.toString()); return childAction; } diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java index 1b015aaa61..f027faa72c 100644 --- a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java @@ -7,6 +7,9 @@ */ package org.opendaylight.netconf.sal.connect.netconf.util; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -16,12 +19,14 @@ import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; +import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.custommonkey.xmlunit.Diff; import org.custommonkey.xmlunit.XMLUnit; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,8 +46,11 @@ import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId; import org.opendaylight.netconf.util.NetconfUtil; import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext; import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.QNameModule; +import org.opendaylight.yangtools.yang.common.Revision; import org.opendaylight.yangtools.yang.common.RpcResultBuilder; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; @@ -54,7 +62,24 @@ import org.xml.sax.SAXException; @RunWith(MockitoJUnitRunner.StrictStubs.class) public class NetconfBaseOpsTest extends AbstractTestModelTest { - private static final QName CONTAINER_Q_NAME = QName.create("test:namespace", "2013-07-22", "c"); + private static final QNameModule TEST_MODULE = QNameModule.create( + URI.create("test:namespace"), Revision.of("2013-07-22")); + + private static final QName CONTAINER_C_QNAME = QName.create(TEST_MODULE, "c"); + private static final NodeIdentifier CONTAINER_C_NID = NodeIdentifier.create(CONTAINER_C_QNAME); + private static final QName LEAF_A_QNAME = QName.create(TEST_MODULE, "a"); + private static final NodeIdentifier LEAF_A_NID = NodeIdentifier.create(LEAF_A_QNAME); + private static final QName LEAF_B_QNAME = QName.create(TEST_MODULE, "b"); + private static final NodeIdentifier LEAF_B_NID = NodeIdentifier.create(LEAF_B_QNAME); + private static final QName CONTAINER_D_QNAME = QName.create(TEST_MODULE, "d"); + private static final NodeIdentifier CONTAINER_D_NID = NodeIdentifier.create(CONTAINER_D_QNAME); + private static final QName LEAF_X_QNAME = QName.create(TEST_MODULE, "x"); + private static final NodeIdentifier LEAF_X_NID = NodeIdentifier.create(LEAF_X_QNAME); + + private static final QName CONTAINER_E_QNAME = QName.create(TEST_MODULE, "e"); + private static final NodeIdentifier CONTAINER_E_NID = NodeIdentifier.create(CONTAINER_E_QNAME); + private static final QName LEAF_Z_QNAME = QName.create(TEST_MODULE, "z"); + private static final NodeIdentifier LEAF_Z_NID = NodeIdentifier.create(LEAF_Z_QNAME); static { XMLUnit.setIgnoreWhitespace(true); @@ -178,16 +203,16 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest { public void testGetConfigRunningData() throws Exception { final Optional> dataOpt = baseOps.getConfigRunningData(callback, Optional.of(YangInstanceIdentifier.empty())).get(); - Assert.assertTrue(dataOpt.isPresent()); - Assert.assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType()); + assertTrue(dataOpt.isPresent()); + assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType()); } @Test public void testGetData() throws Exception { final Optional> dataOpt = baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty())).get(); - Assert.assertTrue(dataOpt.isPresent()); - Assert.assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType()); + assertTrue(dataOpt.isPresent()); + assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType()); } @Test @@ -205,7 +230,7 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest { @Test public void testGetConfigCandidateWithFilter() throws Exception { final YangInstanceIdentifier id = YangInstanceIdentifier.builder() - .node(CONTAINER_Q_NAME) + .node(CONTAINER_C_QNAME) .build(); baseOps.getConfigCandidate(callback, Optional.of(id)); verifyMessageSent("getConfig_candidate-filter", NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME); @@ -219,14 +244,13 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest { @Test public void testEditConfigCandidate() throws Exception { - final QName leafQName = QName.create(CONTAINER_Q_NAME, "a"); final LeafNode leaf = Builders.leafBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(leafQName)) + .withNodeIdentifier(LEAF_A_NID) .withValue("leaf-value") .build(); final YangInstanceIdentifier leafId = YangInstanceIdentifier.builder() - .node(CONTAINER_Q_NAME) - .node(leafQName) + .node(CONTAINER_C_QNAME) + .node(LEAF_A_NID) .build(); final DataContainerChild structure = baseOps.createEditConfigStrcture(Optional.of(leaf), Optional.of(ModifyAction.REPLACE), leafId); @@ -236,15 +260,13 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest { @Test public void testEditConfigRunning() throws Exception { - final QName containerQName = QName.create("test:namespace", "2013-07-22", "c"); - final QName leafQName = QName.create(containerQName, "a"); final LeafNode leaf = Builders.leafBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(leafQName)) + .withNodeIdentifier(LEAF_A_NID) .withValue("leaf-value") .build(); final YangInstanceIdentifier leafId = YangInstanceIdentifier.builder() - .node(containerQName) - .node(leafQName) + .node(CONTAINER_C_NID) + .node(LEAF_A_NID) .build(); final DataContainerChild structure = baseOps.createEditConfigStrcture(Optional.of(leaf), Optional.of(ModifyAction.REPLACE), leafId); @@ -252,6 +274,76 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest { verifyMessageSent("edit-config-test-module-running", NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME); } + @Test + public void testGetWithFields() throws ExecutionException, InterruptedException { + final YangInstanceIdentifier path = YangInstanceIdentifier.create(CONTAINER_C_NID); + final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(LEAF_A_NID); + final YangInstanceIdentifier leafBField = YangInstanceIdentifier.create(LEAF_B_NID); + + baseOps.getData(callback, Optional.of(path), List.of(leafAField, leafBField)).get(); + verify(listener).sendRequest(msg("/netconfMessages/get-fields-request.xml"), + eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME)); + } + + @Test + public void testGetConfigWithFields() throws ExecutionException, InterruptedException { + final YangInstanceIdentifier path = YangInstanceIdentifier.create(CONTAINER_C_NID); + final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(LEAF_A_NID); + final YangInstanceIdentifier leafBField = YangInstanceIdentifier.create(LEAF_B_NID); + + baseOps.getConfigRunningData(callback, Optional.of(path), List.of(leafAField, leafBField)).get(); + verify(listener).sendRequest(msg("/netconfMessages/get-config-fields-request.xml"), + eq(NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME)); + } + + @Test + public void testGetDataWithoutFields() { + assertThrows(ExecutionException.class, () -> baseOps.getData(callback, + Optional.of(YangInstanceIdentifier.empty()), Collections.emptyList()).get()); + } + + @Test + public void getConfigRunningDataWithoutFields() { + assertThrows(ExecutionException.class, () -> baseOps.getConfigRunningData(callback, + Optional.of(YangInstanceIdentifier.empty()), Collections.emptyList()).get()); + } + + @Test + public void testGetWithFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException { + final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(CONTAINER_C_NID, LEAF_A_NID); + final YangInstanceIdentifier leafXField = YangInstanceIdentifier.create( + CONTAINER_C_NID, CONTAINER_D_NID, LEAF_X_NID); + final YangInstanceIdentifier leafZField = YangInstanceIdentifier.create(CONTAINER_E_NID, LEAF_Z_NID); + + baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty()), + List.of(leafAField, leafXField, leafZField)).get(); + verify(listener).sendRequest(msg("/netconfMessages/get-with-multiple-subtrees.xml"), + eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME)); + } + + @Test + public void testGetConfigWithFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException { + final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(CONTAINER_C_NID, LEAF_A_NID); + final YangInstanceIdentifier leafXField = YangInstanceIdentifier.create( + CONTAINER_C_NID, CONTAINER_D_NID, LEAF_X_NID); + final YangInstanceIdentifier leafZField = YangInstanceIdentifier.create(CONTAINER_E_NID, LEAF_Z_NID); + + baseOps.getConfigRunningData(callback, Optional.of(YangInstanceIdentifier.empty()), + List.of(leafAField, leafXField, leafZField)).get(); + verify(listener).sendRequest(msg("/netconfMessages/get-config-with-multiple-subtrees.xml"), + eq(NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME)); + } + + @Test + public void testGetWithRootFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException { + final YangInstanceIdentifier contCField = YangInstanceIdentifier.create(CONTAINER_C_NID); + final YangInstanceIdentifier contDField = YangInstanceIdentifier.create(CONTAINER_E_NID); + + baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty()), List.of(contCField, contDField)).get(); + verify(listener).sendRequest(msg("/netconfMessages/get-with-multiple-root-subtrees.xml"), + eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME)); + } + private void verifyMessageSent(final String fileName, final QName name) { final String path = "/netconfMessages/" + fileName + ".xml"; verify(listener).sendRequest(msg(path), eq(name)); diff --git a/netconf/sal-netconf-connector/src/test/resources/schemas/test-module.yang b/netconf/sal-netconf-connector/src/test/resources/schemas/test-module.yang index cd732fca70..7430f3308b 100644 --- a/netconf/sal-netconf-connector/src/test/resources/schemas/test-module.yang +++ b/netconf/sal-netconf-connector/src/test/resources/schemas/test-module.yang @@ -13,6 +13,21 @@ module test-module { leaf a { type string; } + + leaf b { + type string; + } + + container d { + leaf x { + type boolean; + } + } } + container e { + leaf z { + type uint8; + } + } } -- 2.36.6