Add support for nested notifications to NetconfMessageTransformer 92/90692/5
authorAnna Bencurova <Anna.Bencurova@pantheon.tech>
Thu, 25 Jun 2020 12:16:02 +0000 (14:16 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 20 Jul 2020 20:59:06 +0000 (22:59 +0200)
RFC7950 nested notifications are becoming the norm, yet we cannot
seem to grasp them. This patch adds preliminary support for decoding
them and shuffling them towards whoever may be consuming them.

JIRA: NETCONF-704
Change-Id: I63bebf87de93611f7c887f8b077e59000876aa84
Signed-off-by: Anna Bencurova <Anna.Bencurova@pantheon.tech>
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformer.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/NetconfNestedNotificationTest.java [new file with mode: 0644]
netconf/sal-netconf-connector/src/test/resources/nested-notification-payload.xml [new file with mode: 0644]
netconf/sal-netconf-connector/src/test/resources/schemas/nested-notification.yang [new file with mode: 0644]

index 5974875ef7551708cc9a13f7ebad038e0f813b11..ef26ea8222277e2e4b39d0857d966f4231d181b2 100644 (file)
@@ -13,26 +13,37 @@ import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTr
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_URI;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toPath;
 
+import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
+import com.google.common.collect.Streams;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.Instant;
+import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.dom.DOMSource;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMActionResult;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
 import org.opendaylight.mdsal.dom.api.DOMEvent;
@@ -50,6 +61,7 @@ import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.YangConstants;
+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;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
@@ -65,6 +77,8 @@ import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
@@ -75,10 +89,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
 public class NetconfMessageTransformer implements MessageTransformer<NetconfMessage> {
-
     private static final Logger LOG = LoggerFactory.getLogger(NetconfMessageTransformer.class);
 
     private static final ImmutableSet<URI> BASE_OR_NOTIFICATION_NS = ImmutableSet.of(
@@ -109,6 +124,8 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
 
         this.mappedRpcs = Maps.uniqueIndex(schemaContext.getOperations(), SchemaNode::getQName);
         this.actions = Maps.uniqueIndex(getActions(schemaContext), ActionDefinition::getPath);
+
+        // RFC6020 normal notifications
         this.mappedNotifications = Multimaps.index(schemaContext.getNotifications(),
             node -> node.getQName().withoutRevision());
         this.baseSchema = baseSchema;
@@ -141,7 +158,7 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
 
     @Override
     public synchronized DOMNotification toNotification(final NetconfMessage message) {
-        final Map.Entry<Instant, XmlElement> stripped = NetconfMessageTransformUtil.stripNotification(message);
+        final Entry<Instant, XmlElement> stripped = NetconfMessageTransformUtil.stripNotification(message);
         final QName notificationNoRev;
         try {
             notificationNoRev = QName.create(
@@ -150,7 +167,20 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
             throw new IllegalArgumentException(
                     "Unable to parse notification " + message + ", cannot find namespace", e);
         }
-        final Collection<NotificationDefinition> notificationDefinitions = mappedNotifications.get(notificationNoRev);
+
+        Collection<NotificationDefinition> notificationDefinitions = mappedNotifications.get(notificationNoRev);
+        Element element = stripped.getValue().getDomElement();
+
+        NestedNotificationInfo nestedNotificationInfo = null;
+        if (notificationDefinitions.isEmpty()) {
+            // check if notification is nested notification
+            Optional<NestedNotificationInfo> nestedNotificationOptional = findNestedNotification(message, element);
+            if (nestedNotificationOptional.isPresent()) {
+                nestedNotificationInfo = nestedNotificationOptional.get();
+                notificationDefinitions = Collections.singletonList(nestedNotificationInfo.notificationDefinition);
+                element = (Element) nestedNotificationInfo.notificationNode;
+            }
+        }
         Preconditions.checkArgument(notificationDefinitions.size() > 0,
                 "Unable to parse notification %s, unknown notification. Available notifications: %s",
                 notificationDefinitions, mappedNotifications.keySet());
@@ -160,7 +190,6 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
         final ContainerSchemaNode notificationAsContainerSchemaNode =
                 NetconfMessageTransformUtil.createSchemaForNotification(mostRecentNotification);
 
-        final Element element = stripped.getValue().getDomElement();
         final ContainerNode content;
         try {
             final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
@@ -173,9 +202,120 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
                 | UnsupportedOperationException e) {
             throw new IllegalArgumentException(String.format("Failed to parse notification %s", element), e);
         }
+
+        if (nestedNotificationInfo != null) {
+            return new NetconfDeviceTreeNotification(content, mostRecentNotification.getPath(), stripped.getKey(),
+                nestedNotificationInfo.domDataTreeIdentifier);
+        }
+
         return new NetconfDeviceNotification(content, stripped.getKey());
     }
 
+    private Optional<NestedNotificationInfo> findNestedNotification(final NetconfMessage message,
+            final Element element) {
+        final Iterator<? extends Module> modules = mountContext.getSchemaContext()
+                .findModules(URI.create(element.getNamespaceURI())).iterator();
+        if (!modules.hasNext()) {
+            throw new IllegalArgumentException(
+                    "Unable to parse notification " + message + ", cannot find top level module");
+        }
+        final Module module = modules.next();
+        final QName topLevelNodeQName = QName.create(element.getNamespaceURI(), element.getLocalName());
+        for (DataSchemaNode childNode : module.getChildNodes()) {
+            if (topLevelNodeQName.isEqualWithoutRevision(childNode.getQName())) {
+                return Optional.of(traverseXmlNodeContainingNotification(element, childNode,
+                    YangInstanceIdentifier.builder()));
+            }
+        }
+        return Optional.empty();
+    }
+
+    private NestedNotificationInfo traverseXmlNodeContainingNotification(final Node xmlNode,
+            final SchemaNode schemaNode, final YangInstanceIdentifier.InstanceIdentifierBuilder builder) {
+        if (schemaNode instanceof ContainerSchemaNode) {
+            ContainerSchemaNode dataContainerNode = (ContainerSchemaNode) schemaNode;
+            builder.node(QName.create(xmlNode.getNamespaceURI(), xmlNode.getLocalName()));
+
+            Entry<Node, SchemaNode> xmlContainerChildPair = findXmlContainerChildPair(xmlNode, dataContainerNode);
+            return traverseXmlNodeContainingNotification(xmlContainerChildPair.getKey(),
+                    xmlContainerChildPair.getValue(), builder);
+        } else if (schemaNode instanceof ListSchemaNode) {
+            ListSchemaNode listSchemaNode = (ListSchemaNode) schemaNode;
+            builder.node(QName.create(xmlNode.getNamespaceURI(), xmlNode.getLocalName()));
+
+            Map<QName, Object> listKeys = findXmlListKeys(xmlNode, listSchemaNode);
+            builder.nodeWithKey(QName.create(xmlNode.getNamespaceURI(), xmlNode.getLocalName()), listKeys);
+
+            Entry<Node, SchemaNode> xmlListChildPair = findXmlListChildPair(xmlNode, listSchemaNode);
+            return traverseXmlNodeContainingNotification(xmlListChildPair.getKey(),
+                    xmlListChildPair.getValue(), builder);
+        } else if (schemaNode instanceof NotificationDefinition) {
+            builder.node(QName.create(xmlNode.getNamespaceURI(), xmlNode.getLocalName()));
+
+            NotificationDefinition notificationDefinition = (NotificationDefinition) schemaNode;
+            return new NestedNotificationInfo(notificationDefinition,
+                    new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, builder.build()), xmlNode);
+        }
+        throw new IllegalStateException("No notification found");
+    }
+
+    private static Entry<Node, SchemaNode> findXmlContainerChildPair(final Node xmlNode,
+            final ContainerSchemaNode container) {
+        final NodeList nodeList = xmlNode.getChildNodes();
+        final Map<QName, SchemaNode> childrenWithoutRevision =
+                Streams.concat(container.getChildNodes().stream(), container.getNotifications().stream())
+                    .collect(Collectors.toMap(child -> child.getQName().withoutRevision(), Function.identity()));
+
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node currentNode = nodeList.item(i);
+            if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
+                QName currentNodeQName = QName.create(currentNode.getNamespaceURI(), currentNode.getLocalName());
+                SchemaNode schemaChildNode = childrenWithoutRevision.get(currentNodeQName);
+                if (schemaChildNode != null) {
+                    return new AbstractMap.SimpleEntry<>(currentNode, schemaChildNode);
+                }
+            }
+        }
+        throw new IllegalStateException("No container child found.");
+    }
+
+    private static Map<QName, Object> findXmlListKeys(final Node xmlNode, final ListSchemaNode listSchemaNode) {
+        Map<QName, Object> listKeys = new HashMap<>();
+        NodeList nodeList = xmlNode.getChildNodes();
+        Set<QName> keyDefinitionsWithoutRevision = listSchemaNode.getKeyDefinition().stream()
+                .map(QName::withoutRevision).collect(Collectors.toSet());
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node currentNode = nodeList.item(i);
+            if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
+                QName currentNodeQName = QName.create(currentNode.getNamespaceURI(), currentNode.getLocalName());
+                if (keyDefinitionsWithoutRevision.contains(currentNodeQName)) {
+                    listKeys.put(currentNodeQName, currentNode.getFirstChild().getNodeValue());
+                }
+            }
+        }
+        if (listKeys.isEmpty()) {
+            throw new IllegalStateException("Notification cannot be contained in list without key statement.");
+        }
+        return listKeys;
+    }
+
+    private static Entry<Node, SchemaNode> findXmlListChildPair(final Node xmlNode, final ListSchemaNode list) {
+        final NodeList nodeList = xmlNode.getChildNodes();
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node currentNode = nodeList.item(i);
+            if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
+                QName currentNodeQName = QName.create(currentNode.getNamespaceURI(), currentNode.getLocalName());
+                for (SchemaNode childNode : Iterables.concat(list.getChildNodes(), list.getNotifications())) {
+                    if (!list.getKeyDefinition().contains(childNode.getQName())
+                            && currentNodeQName.isEqualWithoutRevision(childNode.getQName())) {
+                        return new AbstractMap.SimpleEntry<>(currentNode, childNode);
+                    }
+                }
+            }
+        }
+        throw new IllegalStateException("No list child found.");
+    }
+
     private static NotificationDefinition getMostRecentNotification(
             final Collection<NotificationDefinition> notificationDefinitions) {
         return Collections.max(notificationDefinitions, (o1, o2) ->
@@ -338,7 +478,8 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
         }
     }
 
-    static class NetconfDeviceNotification implements DOMNotification, DOMEvent {
+    @Beta
+    public static class NetconfDeviceNotification implements DOMNotification, DOMEvent {
         private final ContainerNode content;
         private final SchemaPath schemaPath;
         private final Instant eventTime;
@@ -349,6 +490,12 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
             this.schemaPath = toPath(content.getNodeType());
         }
 
+        NetconfDeviceNotification(final ContainerNode content, final SchemaPath schemaPath, final Instant eventTime) {
+            this.content = content;
+            this.eventTime = eventTime;
+            this.schemaPath = schemaPath;
+        }
+
         @Override
         public SchemaPath getType() {
             return schemaPath;
@@ -364,4 +511,32 @@ public class NetconfMessageTransformer implements MessageTransformer<NetconfMess
             return eventTime;
         }
     }
+
+    @Beta
+    public static class NetconfDeviceTreeNotification extends NetconfDeviceNotification {
+        private final DOMDataTreeIdentifier domDataTreeIdentifier;
+
+        NetconfDeviceTreeNotification(final ContainerNode content, final SchemaPath schemaPath, final Instant eventTime,
+                final DOMDataTreeIdentifier domDataTreeIdentifier) {
+            super(content, schemaPath, eventTime);
+            this.domDataTreeIdentifier = domDataTreeIdentifier;
+        }
+
+        public DOMDataTreeIdentifier getDomDataTreeIdentifier() {
+            return domDataTreeIdentifier;
+        }
+    }
+
+    private static final class NestedNotificationInfo {
+        private final NotificationDefinition notificationDefinition;
+        private final DOMDataTreeIdentifier domDataTreeIdentifier;
+        private final Node notificationNode;
+
+        NestedNotificationInfo(final NotificationDefinition notificationDefinition,
+                final DOMDataTreeIdentifier domDataTreeIdentifier, final Node notificationNode) {
+            this.notificationDefinition = notificationDefinition;
+            this.domDataTreeIdentifier = domDataTreeIdentifier;
+            this.notificationNode = notificationNode;
+        }
+    }
 }
diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/NetconfNestedNotificationTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/NetconfNestedNotificationTest.java
new file mode 100644 (file)
index 0000000..3b83f36
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.sal.connect.netconf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.opendaylight.mdsal.dom.api.DOMEvent;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.netconf.api.NetconfMessage;
+import org.opendaylight.netconf.api.xml.XmlUtil;
+import org.opendaylight.netconf.notifications.NetconfNotification;
+import org.opendaylight.netconf.sal.connect.netconf.schema.mapping.NetconfMessageTransformer;
+import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+public class NetconfNestedNotificationTest {
+
+    private static final QName INTERFACES_QNAME = QName
+            .create("org:opendaylight:notification:test:ns:yang:nested-notification", "2014-07-08", "interfaces");
+    private static final QName INTERFACE_QNAME = QName.create(INTERFACES_QNAME, "interface");
+    private static final QName INTERFACE_ENABLED_NOTIFICATION_QNAME = QName
+            .create(INTERFACE_QNAME, "interface-enabled");
+
+    @Test
+    public void testNestedNotificationToNotificationFunction() throws Exception {
+        final SchemaContext schemaContext =
+                getNotificationSchemaContext(Collections.singleton("/schemas/nested-notification.yang"));
+        final NetconfMessage notificationMessage = prepareNotification("/nested-notification-payload.xml");
+        NetconfMessageTransformer messageTransformer =
+                new NetconfMessageTransformer(new EmptyMountPointContext(schemaContext), true);
+        final DOMNotification domNotification = messageTransformer.toNotification(notificationMessage);
+        final ContainerNode root = domNotification.getBody();
+        assertNotNull(root);
+        assertEquals(1, Iterables.size(root.getValue()));
+        assertEquals("interface-enabled", root.getNodeType().getLocalName());
+        assertEquals(NetconfNotification.RFC3339_DATE_PARSER.apply("2008-07-08T00:01:00Z").toInstant(),
+                ((DOMEvent) domNotification).getEventInstant());
+        assertEquals(domNotification.getType(), SchemaPath.create(true, INTERFACES_QNAME, INTERFACE_QNAME,
+                INTERFACE_ENABLED_NOTIFICATION_QNAME));
+    }
+
+    private SchemaContext getNotificationSchemaContext(Collection<String> yangResources) {
+        final SchemaContext context = YangParserTestUtils.parseYangResources(getClass(), yangResources);
+        final Set<Module> modules = context.getModules();
+        assertTrue(!modules.isEmpty());
+        assertNotNull(context);
+        return context;
+    }
+
+    private NetconfMessage prepareNotification(String notificationPayloadPath) throws IOException, SAXException {
+        InputStream notifyPayloadStream = getClass().getResourceAsStream(notificationPayloadPath);
+        assertNotNull(notifyPayloadStream);
+
+        final Document doc = XmlUtil.readXmlToDocument(notifyPayloadStream);
+        assertNotNull(doc);
+        return new NetconfMessage(doc);
+    }
+}
diff --git a/netconf/sal-netconf-connector/src/test/resources/nested-notification-payload.xml b/netconf/sal-netconf-connector/src/test/resources/nested-notification-payload.xml
new file mode 100644 (file)
index 0000000..c900380
--- /dev/null
@@ -0,0 +1,11 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2008-07-08T00:01:00Z</eventTime>
+    <interfaces xmlns="org:opendaylight:notification:test:ns:yang:nested-notification">
+        <interface>
+            <name>eth1</name>
+            <interface-enabled>
+                <by-user>fred</by-user>
+            </interface-enabled>
+        </interface>
+    </interfaces>
+</notification>
\ No newline at end of file
diff --git a/netconf/sal-netconf-connector/src/test/resources/schemas/nested-notification.yang b/netconf/sal-netconf-connector/src/test/resources/schemas/nested-notification.yang
new file mode 100644 (file)
index 0000000..5dd1895
--- /dev/null
@@ -0,0 +1,25 @@
+module nested-notification {
+    yang-version 1.1;
+    namespace "org:opendaylight:notification:test:ns:yang:nested-notification";
+    prefix "nested";
+
+    description "Test model for testing nested notifications";
+
+    revision "2014-07-08" {
+        description "Initial revision";
+    }
+
+    container interfaces {
+        list interface {
+            key "name";
+            leaf name {
+                type string;
+            }
+            notification interface-enabled {
+                leaf by-user {
+                    type string;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file