41b2beefb15e90cf20e8e27744ec19958447078e
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / NetconfStateSchemas.java
1 /*
2  * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.client.mdsal;
9
10 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
11 import static org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.YangModuleInfoImpl.qnameOf;
12
13 import com.google.common.annotations.VisibleForTesting;
14 import com.google.common.base.VerifyException;
15 import com.google.common.collect.ImmutableSet;
16 import com.google.common.util.concurrent.FutureCallback;
17 import com.google.common.util.concurrent.Futures;
18 import com.google.common.util.concurrent.ListenableFuture;
19 import com.google.common.util.concurrent.MoreExecutors;
20 import com.google.common.util.concurrent.SettableFuture;
21 import java.io.IOException;
22 import java.net.URISyntaxException;
23 import java.time.format.DateTimeParseException;
24 import java.util.Set;
25 import javax.xml.stream.XMLStreamException;
26 import javax.xml.transform.dom.DOMSource;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
30 import org.opendaylight.netconf.api.NamespaceURN;
31 import org.opendaylight.netconf.api.xml.XmlUtil;
32 import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemas;
33 import org.opendaylight.netconf.client.mdsal.api.NetconfRpcService;
34 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
35 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
36 import org.opendaylight.netconf.common.mdsal.NormalizedDataUtil;
37 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.Get;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.GetInput;
39 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.get.input.Filter;
40 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.NetconfState;
41 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.Yang;
42 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Schemas;
43 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema;
44 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema.Location;
45 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
46 import org.opendaylight.yangtools.yang.common.OperationFailedException;
47 import org.opendaylight.yangtools.yang.common.QName;
48 import org.opendaylight.yangtools.yang.common.Revision;
49 import org.opendaylight.yangtools.yang.common.XMLNamespace;
50 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
51 import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
52 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
53 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
54 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
55 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
56 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
57 import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode;
58 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
59 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
60 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63 import org.w3c.dom.Document;
64 import org.w3c.dom.Node;
65 import org.w3c.dom.traversal.DocumentTraversal;
66 import org.w3c.dom.traversal.NodeFilter;
67 import org.w3c.dom.traversal.TreeWalker;
68 import org.xml.sax.SAXException;
69
70 /**
71  * Holds QNames for all YANG modules reported by ietf-netconf-monitoring/state/schemas.
72  */
73 public final class NetconfStateSchemas implements NetconfDeviceSchemas {
74     public static final NetconfStateSchemas EMPTY = new NetconfStateSchemas(ImmutableSet.of());
75
76     private static final Logger LOG = LoggerFactory.getLogger(NetconfStateSchemas.class);
77     private static final String MONITORING_NAMESPACE = NetconfState.QNAME.getNamespace().toString();
78     private static final @NonNull NodeIdentifier SCHEMA_FORMAT_NODEID = NodeIdentifier.create(qnameOf("format"));
79     private static final @NonNull NodeIdentifier SCHEMA_LOCATION_NODEID = NodeIdentifier.create(qnameOf("location"));
80     private static final @NonNull NodeIdentifier SCHEMA_NAMESPACE_NODEID = NodeIdentifier.create(qnameOf("namespace"));
81     private static final @NonNull NodeIdentifier SCHEMA_IDENTIFIER_NODEID =
82         NodeIdentifier.create(qnameOf("identifier"));
83     private static final @NonNull NodeIdentifier SCHEMA_VERSION_NODEID = NodeIdentifier.create(qnameOf("version"));
84     private static final @NonNull String NETCONF_LOCATION = Location.Enumeration.NETCONF.getName();
85     private static final @NonNull ContainerNode GET_SCHEMAS_RPC;
86
87     static {
88         final var document = XmlUtil.newDocument();
89         final var filterElem = document.createElementNS(NamespaceURN.BASE, "filter");
90         filterElem.setAttribute("type", "subtree");
91
92         final var stateElem = document.createElementNS(NetconfState.QNAME.getNamespace().toString(),
93             NetconfState.QNAME.getLocalName());
94         stateElem.appendChild(document.createElementNS(Schemas.QNAME.getNamespace().toString(),
95             Schemas.QNAME.getLocalName()));
96         filterElem.appendChild(stateElem);
97
98         GET_SCHEMAS_RPC = ImmutableNodes.newContainerBuilder()
99             .withNodeIdentifier(new NodeIdentifier(GetInput.QNAME))
100             .withChild(ImmutableNodes.newAnyxmlBuilder(DOMSource.class)
101                 .withNodeIdentifier(new NodeIdentifier(Filter.QNAME))
102                 .withValue(new DOMSource(filterElem))
103                 .build())
104             .build();
105     }
106
107     private final ImmutableSet<QName> availableYangSchemasQNames;
108
109     public NetconfStateSchemas(final Set<QName> availableYangSchemasQNames) {
110         this.availableYangSchemasQNames = ImmutableSet.copyOf(availableYangSchemasQNames);
111     }
112
113     @Override
114     public Set<QName> getAvailableYangSchemasQNames() {
115         return availableYangSchemasQNames;
116     }
117
118     /**
119      * Issue get request to remote device and parse response to find all schemas under netconf-state/schemas.
120      */
121     static ListenableFuture<NetconfStateSchemas> forDevice(final NetconfRpcService deviceRpc,
122             final NetconfSessionPreferences remoteSessionCapabilities, final RemoteDeviceId id,
123             final EffectiveModelContext modelContext) {
124         if (!remoteSessionCapabilities.isMonitoringSupported()) {
125             // TODO - need to search for get-schema support, not just ietf-netconf-monitoring support
126             // issue might be a deviation to ietf-netconf-monitoring where get-schema is unsupported...
127             LOG.warn("{}: Netconf monitoring not supported on device, cannot detect provided schemas", id);
128             return Futures.immediateFuture(EMPTY);
129         }
130
131         final var future = SettableFuture.<NetconfStateSchemas>create();
132         Futures.addCallback(deviceRpc.invokeNetconf(Get.QNAME, GET_SCHEMAS_RPC),
133             new FutureCallback<DOMRpcResult>() {
134                 @Override
135                 public void onSuccess(final DOMRpcResult result) {
136                     onGetSchemasResult(future, id, modelContext, result);
137                 }
138
139                 @Override
140                 public void onFailure(final Throwable cause) {
141                     // debug, because we expect this error to be reported by caller
142                     LOG.debug("{}: Unable to detect available schemas", id, cause);
143                     future.setException(cause);
144                 }
145             }, MoreExecutors.directExecutor());
146         return future;
147     }
148
149     private static void onGetSchemasResult(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
150             final EffectiveModelContext modelContext, final DOMRpcResult result) {
151         // Two-pass error reporting: first check if there is a hard error, then log any remaining warnings
152         final var errors = result.errors();
153         if (errors.stream().anyMatch(error -> error.getSeverity() == ErrorSeverity.ERROR)) {
154             // FIXME: a good exception, which can report the contents of errors?
155             future.setException(new OperationFailedException("Failed to get netconf-state", errors));
156             return;
157         }
158         for (var error : errors) {
159             LOG.info("{}: schema retrieval warning: {}", id, error);
160         }
161
162         final var value = result.value();
163         if (value == null) {
164             LOG.warn("{}: missing RPC output", id);
165             future.set(EMPTY);
166             return;
167         }
168         final var data = value.childByArg(NETCONF_DATA_NODEID);
169         if (data == null) {
170             LOG.warn("{}: missing RPC data", id);
171             future.set(EMPTY);
172             return;
173         }
174         if (!(data instanceof AnyxmlNode<?> anyxmlData)) {
175             future.setException(new VerifyException("Unexpected data " + data.prettyTree()));
176             return;
177         }
178         final var dataBody = anyxmlData.body();
179         if (!(dataBody instanceof DOMSource domDataBody)) {
180             future.setException(new VerifyException("Unexpected body " + dataBody));
181             return;
182         }
183
184         // Server may include additional data which we do not understand. Make sure we trim the input before we try
185         // to interpret it.
186         // FIXME: we should not be going to NormalizedNode at all. We are interpreting a very limited set of data
187         //        in the context of setting up the normalization schema. Everything we are dealing with are plain
188         //        strings for which yang-common provides everything we need -- with the notable exception of identityref
189         //        values. Those boil down into plain QNames -- so we can talk to XmlCodecs.identityRefCodec(). That
190         //        operation needs to also handle IAE and ignore unknown values.
191         final var filteredBody = ietfMonitoringCopy(domDataBody);
192
193         // Now normalize the anyxml content to the selected model context
194         final NormalizedNode normalizedData;
195         try {
196             normalizedData = NormalizedDataUtil.transformDOMSourceToNormalizedNode(modelContext, filteredBody)
197                 .getResult().data();
198         } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
199             LOG.debug("{}: failed to transform {}", id, filteredBody, e);
200             future.setException(e);
201             return;
202         }
203
204         // The result should be the root of datastore, hence a DataContainerNode
205         if (!(normalizedData instanceof DataContainerNode root)) {
206             future.setException(new VerifyException("Unexpected normalized data " + normalizedData.prettyTree()));
207             return;
208         }
209
210         // container netconf-state
211         final var netconfState = root.childByArg(new NodeIdentifier(NetconfState.QNAME));
212         if (netconfState == null) {
213             LOG.warn("{}: missing netconf-state", id);
214             future.set(EMPTY);
215             return;
216         }
217         if (!(netconfState instanceof ContainerNode netconfStateCont)) {
218             future.setException(new VerifyException("Unexpected netconf-state " + netconfState.prettyTree()));
219             return;
220         }
221
222         // container schemas
223         final var schemas = netconfStateCont.childByArg(new NodeIdentifier(Schemas.QNAME));
224         if (schemas == null) {
225             LOG.warn("{}: missing schemas", id);
226             future.set(EMPTY);
227             return;
228         }
229         if (!(schemas instanceof ContainerNode schemasNode)) {
230             future.setException(new VerifyException("Unexpected schemas " + schemas.prettyTree()));
231             return;
232         }
233
234         create(future, id, schemasNode);
235     }
236
237     /**
238      * Parse response of get(netconf-state/schemas) to find all schemas under netconf-state/schemas.
239      */
240     @VisibleForTesting
241     static void create(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
242             final ContainerNode schemasNode) {
243         final var child = schemasNode.childByArg(new NodeIdentifier(Schema.QNAME));
244         if (child == null) {
245             LOG.warn("{}: missing schema", id);
246             future.set(EMPTY);
247             return;
248         }
249         if (!(child instanceof SystemMapNode schemaMap)) {
250             future.setException(new VerifyException("Unexpected schemas " + child.prettyTree()));
251             return;
252         }
253
254         // FIXME: we are producing the wrong thing here and simply not handling all the use cases
255         //        - instead of QName we want to say 'SourceIdentifier and XMLNamespace', because these are source files
256         //          and there is some namespace guidance -- which we do not really need (because localName+revision is
257         //          guaranteed to be unique and hence there cannot be a conflict on submodule names
258         //        - we handle on "NETCONF" and completely ignore the URI case -- which is something useful for
259         //          offloading model discovery
260         //
261         //        At the end of the day, all this information is going into yang-parser-impl, i.e. it will need to go
262         //        through SchemaSource and all the yang-repo-{api,spi} stuff. That implies policy and further control
263         //        point which needs to be customizable as we want to plug in various providers and differing policies.
264         //
265         //        A few examples:
266         //        - all URIs need to be resolved, which needs pluggable resolvers (https:// is obvious, but xri:// needs
267         //          to hand this off to a dedicated resolver
268         //        - we do not want to use URI.toURL().openConnection(), but leave it up to policy -- for example one
269         //          would want to use java.net.http.HttpClient, which means authentication and content negotiation.
270         //          Content negotiation is needed to establish byte stream encoding, plus
271         //        - all sources of schema are subject to caching, perhaps even in IRSource form
272         //
273         //        At the end of the day, we should just produce an instance of Schema.class and let others deal with
274         //        translating it to the real world -- for example turning a String into a XMLNamespace or a local name.
275         final var builder = ImmutableSet.<QName>builderWithExpectedSize(schemaMap.size());
276         for (var schemaNode : schemaMap.body()) {
277             final var qname = createFromNormalizedNode(id, schemaNode);
278             if (qname != null) {
279                 builder.add(qname);
280             }
281         }
282         future.set(new NetconfStateSchemas(builder.build()));
283     }
284
285     private static @Nullable QName createFromNormalizedNode(final RemoteDeviceId id, final MapEntryNode schemaEntry) {
286         // These three are mandatory due to 'key "identifier version format"'
287         final var format = schemaEntry.getChildByArg(SCHEMA_FORMAT_NODEID).body();
288         // FIXME: we should support Yin as well
289         if (!Yang.QNAME.equals(format)) {
290             LOG.debug("{}: Ignoring schema due to unsupported format: {}", id, format);
291             return null;
292         }
293         // Note: module name or submodule name
294         final var identifier = (String) schemaEntry.getChildByArg(SCHEMA_IDENTIFIER_NODEID).body();
295         // Note: revision
296         final var version = (String) schemaEntry.getChildByArg(SCHEMA_VERSION_NODEID).body();
297
298         // FIXME: we should be able to promote to 'getChildByArg()', IFF the normalizer is enforcing mandatory nodes
299         @SuppressWarnings("unchecked")
300         final var namespaceLeaf = (LeafNode<String>) schemaEntry.childByArg(SCHEMA_NAMESPACE_NODEID);
301         if (namespaceLeaf == null) {
302             LOG.warn("{}: Ignoring schema due to missing namespace", id);
303             return null;
304         }
305
306         @SuppressWarnings("unchecked")
307         final var location = (SystemLeafSetNode<String>) schemaEntry.childByArg(SCHEMA_LOCATION_NODEID);
308         if (location == null) {
309             LOG.debug("{}: Ignoring schema due to missing location", id);
310             return null;
311         }
312
313         boolean foundNetconf = false;
314         for (var locEntry : location.body()) {
315             final var loc = locEntry.body();
316             if (NETCONF_LOCATION.equals(loc)) {
317                 foundNetconf = true;
318                 break;
319             }
320
321             // FIXME: the other string is an Uri, we should be exposing that as well
322             LOG.debug("{}: Ignoring schema due to unsupported location: {}", id, loc);
323         }
324
325         if (!foundNetconf) {
326             LOG.debug("{}: Ignoring schema due to no NETCONF location", id);
327             return null;
328         }
329
330         try {
331             final var namespace = XMLNamespace.of(namespaceLeaf.body());
332             final var revision = version.isEmpty() ? null : Revision.of(version);
333             return QName.create(namespace, revision, identifier);
334         } catch (DateTimeParseException | IllegalArgumentException e) {
335             LOG.warn("{}: Ignoring malformed schema {}", id, schemaEntry.prettyTree(), e);
336             return null;
337         }
338     }
339
340     @VisibleForTesting
341     static DOMSource ietfMonitoringCopy(final DOMSource domSource) {
342         final var sourceDoc = XmlUtil.newDocument();
343         sourceDoc.appendChild(sourceDoc.importNode(domSource.getNode(), true));
344
345         final var treeWalker = ((DocumentTraversal) sourceDoc).createTreeWalker(sourceDoc.getDocumentElement(),
346             NodeFilter.SHOW_ALL, node -> {
347                 final var namespace = node.getNamespaceURI();
348                 return namespace == null || MONITORING_NAMESPACE.equals(namespace)
349                     ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
350             }, false);
351
352         final var filteredDoc = XmlUtil.newDocument();
353         filteredDoc.appendChild(filteredDoc.importNode(treeWalker.getRoot(), false));
354         final var filteredElement = filteredDoc.getDocumentElement();
355         copyChildren(treeWalker, filteredDoc, filteredElement);
356
357         return new DOMSource(filteredElement);
358     }
359
360     private static void copyChildren(final TreeWalker walker, final Document targetDoc, final Node targetNode) {
361         if (walker.firstChild() != null) {
362             for (var node = walker.getCurrentNode(); node != null; node = walker.nextSibling()) {
363                 final var importedNode = targetDoc.importNode(node, false);
364                 targetNode.appendChild(importedNode);
365                 copyChildren(walker, targetDoc, importedNode);
366                 walker.setCurrentNode(node);
367             }
368         }
369     }
370 }