Refactor NetconfDeviceSchemas
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / impl / NetconfStateSchemasResolverImpl.java
@@ -1,27 +1,29 @@
 /*
- * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
-package org.opendaylight.netconf.client.mdsal;
+package org.opendaylight.netconf.client.mdsal.impl;
 
 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
 import static org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.YangModuleInfoImpl.qnameOf;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.common.util.concurrent.SettableFuture;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.time.format.DateTimeParseException;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.transform.dom.DOMSource;
 import org.eclipse.jdt.annotation.NonNull;
@@ -30,8 +32,10 @@ import org.opendaylight.mdsal.dom.api.DOMRpcResult;
 import org.opendaylight.netconf.api.NamespaceURN;
 import org.opendaylight.netconf.api.xml.XmlUtil;
 import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemas;
+import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemasResolver;
 import org.opendaylight.netconf.client.mdsal.api.NetconfRpcService;
 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
+import org.opendaylight.netconf.client.mdsal.api.ProvidedSources;
 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
 import org.opendaylight.netconf.common.mdsal.NormalizedDataUtil;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.Get;
@@ -43,7 +47,6 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.mon
 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.schemas.Schema.Location;
 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
-import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
@@ -58,6 +61,8 @@ import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode;
 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
+import org.opendaylight.yangtools.yang.model.api.stmt.FeatureSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
@@ -68,12 +73,10 @@ import org.w3c.dom.traversal.TreeWalker;
 import org.xml.sax.SAXException;
 
 /**
- * Holds QNames for all YANG modules reported by ietf-netconf-monitoring/state/schemas.
+ * Default implementation resolving schemas QNames from netconf-state or from modules-state.
  */
-public final class NetconfStateSchemas implements NetconfDeviceSchemas {
-    public static final NetconfStateSchemas EMPTY = new NetconfStateSchemas(ImmutableSet.of());
-
-    private static final Logger LOG = LoggerFactory.getLogger(NetconfStateSchemas.class);
+public final class NetconfStateSchemasResolverImpl implements NetconfDeviceSchemasResolver {
+    private static final Logger LOG = LoggerFactory.getLogger(NetconfStateSchemasResolverImpl.class);
     private static final String MONITORING_NAMESPACE = NetconfState.QNAME.getNamespace().toString();
     private static final @NonNull NodeIdentifier SCHEMA_FORMAT_NODEID = NodeIdentifier.create(qnameOf("format"));
     private static final @NonNull NodeIdentifier SCHEMA_LOCATION_NODEID = NodeIdentifier.create(qnameOf("location"));
@@ -104,81 +107,86 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
             .build();
     }
 
-    private final ImmutableSet<QName> availableYangSchemasQNames;
-
-    public NetconfStateSchemas(final Set<QName> availableYangSchemasQNames) {
-        this.availableYangSchemasQNames = ImmutableSet.copyOf(availableYangSchemasQNames);
-    }
-
     @Override
-    public Set<QName> getAvailableYangSchemasQNames() {
-        return availableYangSchemasQNames;
-    }
+    public ListenableFuture<NetconfDeviceSchemas> resolve(final RemoteDeviceId deviceId,
+            final NetconfSessionPreferences sessionPreferences, final NetconfRpcService deviceRpc,
+            final EffectiveModelContext baseModelContext) {
+        // Find all schema sources provided via ietf-netconf-monitoring, if supported
+        final var monitoringFuture = sessionPreferences.isMonitoringSupported()
+            ? resolveMonitoringSources(deviceId, deviceRpc, baseModelContext)
+                : Futures.immediateFuture(List.<ProvidedSources<?>>of());
+
+        final AsyncFunction<List<ProvidedSources<?>>, NetconfDeviceSchemas> function;
+        LOG.debug("{}: resolving YANG 1.0 conformance", deviceId);
+        function = sources -> resolveYang10(deviceId, sessionPreferences, deviceRpc, baseModelContext, sources);
+
+        // FIXME: check for
+        //            urn:ietf:params:netconf:capability:yang-library:1.0?revision=<date>&module-set-id=<id>
+        //
+        //        and then dispatch to resolveYang11(), which should resolve schemas based on RFC7950's conformance
+        //        announcement via I <hello/> message, as defined in
+        //        https://www.rfc-editor.org/rfc/rfc6020#section-5.6.4
+        //
+        //        if (sessionPreferences.containsNonModuleCapability(CapabilityURN.YANG_LIBRARY)) {
+        //            LOG.debug("{}: resolving YANG 1.1 conformance", deviceId);
+        //            function = sources -> resolveYang11(deviceId, sessionPreferences, deviceRpc, baseModelContext,
+        //                sources);
+        //        }
+
+        // FIXME: check for
+        //            urn:ietf:params:netconf:capability:yang-library:1.1?revision=<date>&content-id=<content-id-value>
+        //        where date is at least 2019-01-04
+        //
+        //        and then dispatch to resolveNmda():
+        //
+        //        if (sessionPreferences.containsNonModuleCapability(CapabilityURN.YANG_LIBRARY)) {
+        //            LOG.debug("{}: resolving YANG 1.1 NMDA conformance", deviceId);
+        //            function = sources -> resolveNmda(deviceId, sessionPreferences, deviceRpc, baseModelContext,
+        //                sources);
+        //        }
 
-    /**
-     * Issue get request to remote device and parse response to find all schemas under netconf-state/schemas.
-     */
-    static ListenableFuture<NetconfStateSchemas> forDevice(final NetconfRpcService deviceRpc,
-            final NetconfSessionPreferences remoteSessionCapabilities, final RemoteDeviceId id,
-            final EffectiveModelContext modelContext) {
-        if (!remoteSessionCapabilities.isMonitoringSupported()) {
-            // TODO - need to search for get-schema support, not just ietf-netconf-monitoring support
-            // issue might be a deviation to ietf-netconf-monitoring where get-schema is unsupported...
-            LOG.warn("{}: Netconf monitoring not supported on device, cannot detect provided schemas", id);
-            return Futures.immediateFuture(EMPTY);
-        }
+        return Futures.transformAsync(monitoringFuture, function, MoreExecutors.directExecutor());
+    }
 
-        final var future = SettableFuture.<NetconfStateSchemas>create();
-        Futures.addCallback(deviceRpc.invokeNetconf(Get.QNAME, GET_SCHEMAS_RPC),
-            new FutureCallback<DOMRpcResult>() {
-                @Override
-                public void onSuccess(final DOMRpcResult result) {
-                    onGetSchemasResult(future, id, modelContext, result);
-                }
-
-                @Override
-                public void onFailure(final Throwable cause) {
-                    // debug, because we expect this error to be reported by caller
-                    LOG.debug("{}: Unable to detect available schemas", id, cause);
-                    future.setException(cause);
-                }
-            }, MoreExecutors.directExecutor());
-        return future;
+    private static ListenableFuture<List<ProvidedSources<?>>> resolveMonitoringSources(
+            final RemoteDeviceId deviceId, final NetconfRpcService deviceRpc,
+            final EffectiveModelContext baseModelContext) {
+        return Futures.transform(deviceRpc.invokeNetconf(Get.QNAME, GET_SCHEMAS_RPC),
+            result -> resolveMonitoringSources(deviceId, deviceRpc, result, baseModelContext),
+            MoreExecutors.directExecutor());
     }
 
-    private static void onGetSchemasResult(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
-            final EffectiveModelContext modelContext, final DOMRpcResult result) {
+    private static List<ProvidedSources<?>> resolveMonitoringSources(final RemoteDeviceId deviceId,
+            final NetconfRpcService deviceRpc, final DOMRpcResult rpcResult,
+            final EffectiveModelContext baseModelContext) {
         // Two-pass error reporting: first check if there is a hard error, then log any remaining warnings
-        final var errors = result.errors();
+        final var errors = rpcResult.errors();
         if (errors.stream().anyMatch(error -> error.getSeverity() == ErrorSeverity.ERROR)) {
-            // FIXME: a good exception, which can report the contents of errors?
-            future.setException(new OperationFailedException("Failed to get netconf-state", errors));
-            return;
+            LOG.warn("{}: failed to get netconf-state", errors);
+            return List.of();
         }
         for (var error : errors) {
-            LOG.info("{}: schema retrieval warning: {}", id, error);
+            LOG.info("{}: schema retrieval warning: {}", deviceId, error);
         }
 
-        final var value = result.value();
-        if (value == null) {
-            LOG.warn("{}: missing RPC output", id);
-            future.set(EMPTY);
-            return;
+        final var rpcOutput = rpcResult.value();
+        if (rpcOutput == null) {
+            LOG.warn("{}: missing RPC output", deviceId);
+            return List.of();
         }
-        final var data = value.childByArg(NETCONF_DATA_NODEID);
+        final var data = rpcOutput.childByArg(NETCONF_DATA_NODEID);
         if (data == null) {
-            LOG.warn("{}: missing RPC data", id);
-            future.set(EMPTY);
-            return;
+            LOG.warn("{}: missing RPC data", deviceId);
+            return List.of();
         }
         if (!(data instanceof AnyxmlNode<?> anyxmlData)) {
-            future.setException(new VerifyException("Unexpected data " + data.prettyTree()));
-            return;
+            LOG.warn("{}: unexpected data {}", deviceId, data.prettyTree());
+            return List.of();
         }
         final var dataBody = anyxmlData.body();
         if (!(dataBody instanceof DOMSource domDataBody)) {
-            future.setException(new VerifyException("Unexpected body " + dataBody));
-            return;
+            LOG.warn("{}: unexpected body {}", deviceId, dataBody);
+            return List.of();
         }
 
         // Server may include additional data which we do not understand. Make sure we trim the input before we try
@@ -193,62 +201,58 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
         // Now normalize the anyxml content to the selected model context
         final NormalizedNode normalizedData;
         try {
-            normalizedData = NormalizedDataUtil.transformDOMSourceToNormalizedNode(modelContext, filteredBody)
+            normalizedData = NormalizedDataUtil.transformDOMSourceToNormalizedNode(baseModelContext, filteredBody)
                 .getResult().data();
         } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
-            LOG.debug("{}: failed to transform {}", id, filteredBody, e);
-            future.setException(e);
-            return;
+            LOG.warn("{}: failed to transform {}", deviceId, filteredBody, e);
+            return List.of();
         }
 
         // The result should be the root of datastore, hence a DataContainerNode
         if (!(normalizedData instanceof DataContainerNode root)) {
-            future.setException(new VerifyException("Unexpected normalized data " + normalizedData.prettyTree()));
-            return;
+            LOG.warn("{}: unexpected normalized data {}", deviceId, normalizedData.prettyTree());
+            return List.of();
         }
 
         // container netconf-state
         final var netconfState = root.childByArg(new NodeIdentifier(NetconfState.QNAME));
         if (netconfState == null) {
-            LOG.warn("{}: missing netconf-state", id);
-            future.set(EMPTY);
-            return;
+            LOG.warn("{}: missing netconf-state", deviceId);
+            return List.of();
         }
         if (!(netconfState instanceof ContainerNode netconfStateCont)) {
-            future.setException(new VerifyException("Unexpected netconf-state " + netconfState.prettyTree()));
-            return;
+            LOG.warn("{}: unexpected netconf-state {}", deviceId, netconfState.prettyTree());
+            return List.of();
         }
 
         // container schemas
         final var schemas = netconfStateCont.childByArg(new NodeIdentifier(Schemas.QNAME));
         if (schemas == null) {
-            LOG.warn("{}: missing schemas", id);
-            future.set(EMPTY);
-            return;
+            LOG.warn("{}: missing schemas", deviceId);
+            return List.of();
         }
         if (!(schemas instanceof ContainerNode schemasNode)) {
-            future.setException(new VerifyException("Unexpected schemas " + schemas.prettyTree()));
-            return;
+            LOG.warn("{}: unexpected schemas {}", deviceId, schemas.prettyTree());
+            return List.of();
         }
 
-        create(future, id, schemasNode);
+        return resolveMonitoringSources(deviceId, deviceRpc, schemasNode);
     }
 
     /**
      * Parse response of get(netconf-state/schemas) to find all schemas under netconf-state/schemas.
      */
     @VisibleForTesting
-    static void create(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
-            final ContainerNode schemasNode) {
+    static List<ProvidedSources<?>> resolveMonitoringSources(final RemoteDeviceId deviceId,
+            final NetconfRpcService deviceRpc, final ContainerNode schemasNode) {
         final var child = schemasNode.childByArg(new NodeIdentifier(Schema.QNAME));
         if (child == null) {
-            LOG.warn("{}: missing schema", id);
-            future.set(EMPTY);
-            return;
+            LOG.warn("{}: missing schema", deviceId);
+            return List.of();
         }
         if (!(child instanceof SystemMapNode schemaMap)) {
-            future.setException(new VerifyException("Unexpected schemas " + child.prettyTree()));
-            return;
+            LOG.warn("{}: unexpected schema {}", deviceId, child.prettyTree());
+            return List.of();
         }
 
         // FIXME: we are producing the wrong thing here and simply not handling all the use cases
@@ -274,15 +278,19 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
         //        translating it to the real world -- for example turning a String into a XMLNamespace or a local name.
         final var builder = ImmutableSet.<QName>builderWithExpectedSize(schemaMap.size());
         for (var schemaNode : schemaMap.body()) {
-            final var qname = createFromNormalizedNode(id, schemaNode);
+            final var qname = createFromNormalizedNode(deviceId, schemaNode);
             if (qname != null) {
                 builder.add(qname);
             }
         }
-        future.set(new NetconfStateSchemas(builder.build()));
+
+        final var sources = builder.build();
+        return sources.isEmpty() ? List.of() : List.of(new ProvidedSources<>(YangTextSource.class,
+            new MonitoringSchemaSourceProvider(deviceId, deviceRpc), sources));
     }
 
-    private static @Nullable QName createFromNormalizedNode(final RemoteDeviceId id, final MapEntryNode schemaEntry) {
+    private static @Nullable QName createFromNormalizedNode(final RemoteDeviceId id,
+            final MapEntryNode schemaEntry) {
         // These three are mandatory due to 'key "identifier version format"'
         final var format = schemaEntry.getChildByArg(SCHEMA_FORMAT_NODEID).body();
         // FIXME: we should support Yin as well
@@ -367,4 +375,45 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
             }
         }
     }
+
+    // Resolve schemas based on RFC6020's conformance announcement in <hello/> message, as defined in
+    // https://www.rfc-editor.org/rfc/rfc6020#section-5.6.4
+    private static ListenableFuture<NetconfDeviceSchemas> resolveYang10(final RemoteDeviceId deviceId,
+            final NetconfSessionPreferences sessionPreferences, final NetconfRpcService deviceRpc,
+            final EffectiveModelContext baseModelContext, final List<ProvidedSources<?>> monitoringSources) {
+        final var providedSources = monitoringSources.stream()
+            .flatMap(sources -> sources.sources().stream())
+            .collect(Collectors.toSet());
+        LOG.debug("{}: Schemas exposed by ietf-netconf-monitoring: {}", deviceId, providedSources);
+
+        final var requiredSources = new HashSet<>(sessionPreferences.moduleBasedCaps().keySet());
+        final var requiredSourcesNotProvided = Sets.difference(requiredSources, providedSources);
+        if (!requiredSourcesNotProvided.isEmpty()) {
+            LOG.warn("{}: Netconf device does not provide all yang models reported in hello message capabilities,"
+                    + " required but not provided: {}", deviceId, requiredSourcesNotProvided);
+            LOG.warn("{}: Attempting to build schema context from required sources", deviceId);
+        }
+
+        // Here all the sources reported in netconf monitoring are merged with those reported in hello.
+        // It is necessary to perform this since submodules are not mentioned in hello but still required.
+        // This clashes with the option of a user to specify supported yang models manually in configuration
+        // for netconf-connector and as a result one is not able to fully override yang models of a device.
+        // It is only possible to add additional models.
+        final var providedSourcesNotRequired = Sets.difference(providedSources, requiredSources);
+        if (!providedSourcesNotRequired.isEmpty()) {
+            LOG.warn("{}: Netconf device provides additional yang models not reported in "
+                    + "hello message capabilities: {}", deviceId, providedSourcesNotRequired);
+            LOG.warn("{}: Adding provided but not required sources as required to prevent failures", deviceId);
+            LOG.debug("{}: Netconf device reported in hello: {}", deviceId, requiredSources);
+            requiredSources.addAll(providedSourcesNotRequired);
+        }
+
+
+        return Futures.immediateFuture(new NetconfDeviceSchemas(requiredSources,
+            // FIXME: determine features
+            FeatureSet.builder().build(),
+            // FIXME: use this instead of adjusted required sources
+            Set.of(),
+            monitoringSources));
+    }
 }